一步步教你,为WordPress网站添加访客行为热力图与录屏分析,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:为什么你的WordPress网站需要行为分析工具? 在当今竞争激烈的数字环境中,仅仅拥有一个美观的WordPress网站已经远远不够。了解访客如何与你的网站互动,哪些内容吸引他们,哪些元素导致他们离开,这些洞察对于优化用户体验、提高转化率至关重要。传统的数据分析工具如Google Analytics提供了页面浏览量和用户数量的宏观数据,但却无法回答一些关键问题:用户究竟在哪里点击?他们是否看到了重要的行动号召按钮?他们在离开前在页面上做了什么? 这就是热力图和录屏分析的价值所在。热力图通过色彩可视化展示用户在页面上的点击、滚动和注意力分布,而录屏分析则像“数字监控摄像头”一样,记录真实用户在网站上的完整操作过程。本文将详细指导你如何通过WordPress代码二次开发,为你的网站集成这两项强大的分析工具,无需依赖昂贵的第三方服务。 第一部分:理解热力图与录屏分析的技术原理 1.1 热力图的三种类型及其实现原理 热力图主要分为三种类型:点击热力图、滚动热力图和注意力热力图。点击热力图记录用户在页面上的所有点击位置,包括链接、按钮甚至非链接区域。实现原理是通过JavaScript事件监听器捕获点击事件,记录点击元素的坐标、尺寸和页面URL,然后将这些数据发送到服务器进行处理和可视化。 滚动热力图显示用户在不同页面深度的停留比例,揭示有多少用户滚动到页面特定位置。这通过监听滚动事件,记录视窗位置和页面高度比例来实现。注意力热力图则基于视线追踪研究,假设用户的注意力与鼠标移动轨迹相关,通过跟踪鼠标移动和停留时间来模拟实现。 1.2 录屏分析的技术实现方式 录屏分析(也称为会话回放)的实现比热力图更为复杂。现代实现通常采用以下技术组合: DOM序列化:记录页面初始的DOM状态,然后监听所有可能改变DOM的事件(如点击、输入、滚动等) 突变观察器(MutationObserver):监控DOM元素的变化 事件监听:捕获用户交互事件 时间戳同步:确保所有事件按正确顺序和时序记录 数据压缩:将记录的数据高效压缩后发送到服务器 录屏数据通常以增量方式记录,而不是完整视频,这大大减少了数据量。回放时,通过重建DOM状态和应用记录的事件来“重现”用户会话。 1.3 数据收集与隐私保护的平衡 实现这些工具时,必须考虑隐私保护。敏感数据(如密码字段、个人身份信息)应在客户端就被过滤或遮蔽。GDPR、CCPA等法规要求对用户数据进行适当处理,因此你的实现应包含: 明确的隐私政策和使用告知 敏感数据自动遮蔽功能 用户选择退出机制 数据匿名化处理 第二部分:准备工作与开发环境搭建 2.1 开发环境配置 在开始编码前,确保你已准备好以下环境: 本地WordPress开发环境:可以使用Local by Flywheel、XAMPP或Docker配置 代码编辑器:VS Code、Sublime Text或PHPStorm 浏览器开发者工具:熟悉Chrome DevTools或Firefox开发者工具 版本控制系统:Git初始化你的插件目录 测试网站:不要在正式网站直接开发,使用测试网站或本地环境 2.2 创建自定义WordPress插件框架 我们将创建一个独立的WordPress插件来实现功能,而不是直接修改主题文件。这样做的好处是: 功能与主题分离,更换主题不影响分析工具 便于更新和维护 可以轻松在其他网站上复用 创建插件基本结构: wp-content/plugins/visitor-analytics-tool/ ├── visitor-analytics-tool.php ├── includes/ │ ├── class-data-collector.php │ ├── class-heatmap-generator.php │ ├── class-session-recorder.php │ └── class-data-viewer.php ├── assets/ │ ├── js/ │ │ ├── frontend.js │ │ └── admin.js │ └── css/ │ ├── frontend.css │ └── admin.css ├── admin/ │ └── admin-pages.php └── uninstall.php 2.3 插件主文件基本结构 在visitor-analytics-tool.php中,设置插件基本信息: <?php /** * Plugin Name: Visitor Analytics Tool * Plugin URI: https://yourwebsite.com/visitor-analytics * Description: 添加访客行为热力图与录屏分析功能 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: visitor-analytics */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('VAT_VERSION', '1.0.0'); define('VAT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('VAT_PLUGIN_URL', plugin_dir_url(__FILE__)); // 包含必要文件 require_once VAT_PLUGIN_DIR . 'includes/class-data-collector.php'; require_once VAT_PLUGIN_DIR . 'includes/class-heatmap-generator.php'; require_once VAT_PLUGIN_DIR . 'includes/class-session-recorder.php'; require_once VAT_PLUGIN_DIR . 'includes/class-data-viewer.php'; // 初始化插件 function vat_initialize_plugin() { // 检查用户权限 if (current_user_can('manage_options')) { // 初始化各个组件 $data_collector = new VAT_Data_Collector(); $heatmap_generator = new VAT_Heatmap_Generator(); $session_recorder = new VAT_Session_Recorder(); $data_viewer = new VAT_Data_Viewer(); // 注册激活/停用钩子 register_activation_hook(__FILE__, array($data_collector, 'activate')); register_deactivation_hook(__FILE__, array($data_collector, 'deactivate')); } } add_action('plugins_loaded', 'vat_initialize_plugin'); 第三部分:实现前端数据收集系统 3.1 设计高效的数据收集JavaScript 创建assets/js/frontend.js文件,负责收集用户行为数据: (function() { 'use strict'; // 配置参数 const config = { sampleRate: 0.3, // 30%的用户被记录 heatmap: { enabled: true, click: true, scroll: true, move: true }, sessionRecord: { enabled: false, // 默认关闭,需要时开启 maxDuration: 300000, // 5分钟 events: ['click', 'input', 'scroll', 'resize'] }, privacy: { maskText: true, excludeFields: ['password', 'credit-card', 'ssn'] } }; // 检查是否应该记录当前会话 function shouldRecordSession() { if (!Math.floor(Math.random() * 100) < (config.sampleRate * 100)) { return false; } // 检查排除条件(如管理员、特定页面等) const excludedPaths = ['/wp-admin', '/wp-login']; const currentPath = window.location.pathname; return !excludedPaths.some(path => currentPath.startsWith(path)); } // 数据收集器类 class DataCollector { constructor() { this.sessionId = this.generateSessionId(); this.pageId = window.location.pathname + window.location.search; this.startTime = Date.now(); this.events = []; this.isRecording = false; // 初始化 this.init(); } generateSessionId() { return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9); } init() { if (!shouldRecordSession()) return; // 设置数据收集 if (config.heatmap.enabled) { this.setupHeatmapCollection(); } if (config.sessionRecord.enabled) { this.setupSessionRecording(); } // 发送初始数据 this.sendInitialData(); this.isRecording = true; } setupHeatmapCollection() { // 点击事件收集 if (config.heatmap.click) { document.addEventListener('click', (e) => { this.recordClick(e); }, { capture: true }); } // 滚动事件收集 if (config.heatmap.scroll) { let scrollTimeout; window.addEventListener('scroll', () => { clearTimeout(scrollTimeout); scrollTimeout = setTimeout(() => { this.recordScroll(); }, 100); }); } // 鼠标移动收集(用于注意力热力图) if (config.heatmap.move) { let moveTimeout; document.addEventListener('mousemove', (e) => { clearTimeout(moveTimeout); moveTimeout = setTimeout(() => { this.recordMousePosition(e); }, 50); }); } } recordClick(event) { const target = event.target; const rect = target.getBoundingClientRect(); const clickData = { type: 'click', x: event.clientX, y: event.clientY, element: { tag: target.tagName, id: target.id, classes: target.className, text: this.maskText(target.textContent.trim().substring(0, 100)) }, pageX: event.pageX, pageY: event.pageY, viewport: { width: window.innerWidth, height: window.innerHeight }, timestamp: Date.now() - this.startTime, url: window.location.href }; this.events.push(clickData); this.sendDataIfNeeded(); } recordScroll() { const scrollData = { type: 'scroll', position: { x: window.scrollX, y: window.scrollY }, maxScroll: { x: document.documentElement.scrollWidth - window.innerWidth, y: document.documentElement.scrollHeight - window.innerHeight }, percentage: (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100, timestamp: Date.now() - this.startTime, url: window.location.href }; this.events.push(scrollData); this.sendDataIfNeeded(); } recordMousePosition(event) { const moveData = { type: 'move', x: event.clientX, y: event.clientY, timestamp: Date.now() - this.startTime, url: window.location.href }; this.events.push(moveData); // 鼠标移动数据较多,单独处理发送逻辑 this.sendMoveData(); } maskText(text) { if (!config.privacy.maskText) return text; // 简单文本遮蔽,实际应用中需要更复杂的逻辑 if (text.length > 5) { return text.substring(0, 2) + '***' + text.substring(text.length - 2); } return '***'; } sendDataIfNeeded() { // 每10个事件或每5秒发送一次数据 if (this.events.length >= 10) { this.sendBatchData(); } } sendMoveData() { // 鼠标移动数据单独处理,避免数据量过大 if (this.moveEvents && this.moveEvents.length >= 20) { const moveBatch = { sessionId: this.sessionId, type: 'move_batch', events: this.moveEvents, url: window.location.href }; this.sendToServer(moveBatch); this.moveEvents = []; } } sendBatchData() { if (this.events.length === 0) return; const batch = { sessionId: this.sessionId, events: [...this.events], url: window.location.href, timestamp: Date.now() }; this.sendToServer(batch); this.events = []; } sendToServer(data) { // 使用navigator.sendBeacon确保页面卸载时也能发送数据 const blob = new Blob([JSON.stringify(data)], { type: 'application/json' }); if (navigator.sendBeacon) { navigator.sendBeacon(vat_ajax.ajax_url + '?action=vat_track', blob); } else { // 回退方案 const xhr = new XMLHttpRequest(); xhr.open('POST', vat_ajax.ajax_url, false); xhr.setRequestHeader('Content-Type', 'application/json'); xhr.send(JSON.stringify(data)); } } sendInitialData() { const initialData = { sessionId: this.sessionId, type: 'session_start', url: window.location.href, referrer: document.referrer, viewport: { width: window.innerWidth, height: window.innerHeight }, device: { userAgent: navigator.userAgent, platform: navigator.platform }, timestamp: Date.now() }; this.sendToServer(initialData); } setupSessionRecording() { // 录屏功能需要更复杂的实现 console.log('Session recording setup would go here'); // 实际实现需要DOM序列化、突变观察器等 } } // 初始化数据收集器 document.addEventListener('DOMContentLoaded', () => { window.vatCollector = new DataCollector(); }); // 页面卸载前发送剩余数据 window.addEventListener('beforeunload', () => { if (window.vatCollector && window.vatCollector.isRecording) { window.vatCollector.sendBatchData(); } }); })(); 3.2 创建PHP后端数据处理类 在includes/class-data-collector.php中创建数据处理后端: <?php class VAT_Data_Collector { private $table_events; private $table_sessions; public function __construct() { global $wpdb; $this->table_events = $wpdb->prefix . 'vat_events'; $this->table_sessions = $wpdb->prefix . 'vat_sessions'; // 注册AJAX处理 add_action('wp_ajax_vat_track', array($this, 'handle_tracking_data')); add_action('wp_ajax_nopriv_vat_track', array($this, 'handle_tracking_data')); // 注册前端脚本 add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_scripts')); } public function activate() { $this->create_tables(); $this->set_default_options(); } private function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 创建事件表 $sql_events = "CREATE TABLE IF NOT EXISTS {$this->table_events} ( id bigint(20) NOT NULL AUTO_INCREMENT, session_id varchar(100) NOT NULL, event_type varchar(50) NOT NULL, event_data longtext NOT NULL, page_url varchar(500) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, PRIMARY KEY (id), KEY session_id (session_id), KEY event_type (event_type), KEY page_url (page_url(200)), KEY created_at (created_at) ) $charset_collate;"; // 创建会话表 $sql_sessions = "CREATE TABLE IF NOT EXISTS {$this->table_sessions} ( session_id varchar(100) NOT NULL, start_time datetime NOT NULL, end_time datetime, page_views int(11) DEFAULT 1, duration int(11), referrer varchar(500), device_info text, user_id bigint(20), PRIMARY KEY (session_id), KEY start_time (start_time), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_events); dbDelta($sql_sessions); } private function set_default_options() { if (!get_option('vat_settings')) { $default_settings = array( 'sample_rate' => 0.3, 'heatmap_enabled' => true, 'session_record_enabled' => false, 'data_retention_days' => 30, 'exclude_roles' => array('administrator'), 'privacy_mask' => true ); update_option('vat_settings', $default_settings); } } public function enqueue_frontend_scripts() { // 只有在前端页面加载 if (is_admin()) return; // 获取设置 $settings = get_option('vat_settings', array()); // 检查是否应该加载跟踪脚本 if (!$this->should_track_current_user()) return; // 计算采样率 $sample_rate = isset($settings['sample_rate']) ? floatval($settings['sample_rate']) : 0.3; // 只有部分用户加载跟踪脚本 if (mt_rand(1, 100) > ($sample_rate * 100)) return; wp_enqueue_script( 'vat-frontend', VAT_PLUGIN_URL . 'assets/js/frontend.js', array(), VAT_VERSION, true ); // 传递AJAX URL给前端脚本 wp_localize_script('vat-frontend', 'vat_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'sample_rate' => $sample_rate )); } 第三部分:实现前端数据收集系统(续) 3.2 创建PHP后端数据处理类(续) private function should_track_current_user() { $settings = get_option('vat_settings', array()); $exclude_roles = isset($settings['exclude_roles']) ? $settings['exclude_roles'] : array(); // 如果用户已登录,检查其角色 if (is_user_logged_in()) { $user = wp_get_current_user(); $user_roles = $user->roles; // 检查用户角色是否在排除列表中 foreach ($user_roles as $role) { if (in_array($role, $exclude_roles)) { return false; } } } // 排除特定页面 $excluded_pages = array('/wp-admin', '/wp-login.php', '/cart', '/checkout'); $current_path = $_SERVER['REQUEST_URI']; foreach ($excluded_pages as $page) { if (strpos($current_path, $page) === 0) { return false; } } return true; } public function handle_tracking_data() { // 验证请求 if ($_SERVER['REQUEST_METHOD'] !== 'POST') { wp_die('Invalid request method', 400); } // 获取原始POST数据 $raw_data = file_get_contents('php://input'); $data = json_decode($raw_data, true); if (!$data || !isset($data['sessionId'])) { wp_die('Invalid data format', 400); } // 处理不同类型的数据 if (isset($data['type']) && $data['type'] === 'session_start') { $this->record_session_start($data); } elseif (isset($data['type']) && $data['type'] === 'move_batch') { $this->record_move_batch($data); } elseif (isset($data['events'])) { $this->record_events($data); } // 返回成功响应 wp_send_json_success(array('message' => 'Data recorded')); } private function record_session_start($data) { global $wpdb; $session_data = array( 'session_id' => sanitize_text_field($data['sessionId']), 'start_time' => current_time('mysql'), 'referrer' => isset($data['referrer']) ? sanitize_text_field($data['referrer']) : '', 'device_info' => isset($data['device']) ? json_encode($data['device']) : '', 'user_id' => is_user_logged_in() ? get_current_user_id() : 0 ); $wpdb->insert($this->table_sessions, $session_data); } private function record_events($data) { global $wpdb; $session_id = sanitize_text_field($data['sessionId']); $events = $data['events']; foreach ($events as $event) { $event_type = isset($event['type']) ? sanitize_text_field($event['type']) : 'unknown'; $event_data = array( 'session_id' => $session_id, 'event_type' => $event_type, 'event_data' => json_encode($event), 'page_url' => isset($event['url']) ? sanitize_text_field($event['url']) : '' ); $wpdb->insert($this->table_events, $event_data); } // 更新会话的最后活动时间 $this->update_session_activity($session_id); } private function record_move_batch($data) { global $wpdb; $session_id = sanitize_text_field($data['sessionId']); $events = $data['events']; // 鼠标移动数据较多,进行聚合处理 $aggregated_data = array( 'session_id' => $session_id, 'event_type' => 'move_aggregated', 'event_data' => json_encode(array( 'count' => count($events), 'positions' => array_map(function($e) { return array($e['x'], $e['y']); }, $events) )), 'page_url' => isset($data['url']) ? sanitize_text_field($data['url']) : '' ); $wpdb->insert($this->table_events, $aggregated_data); } private function update_session_activity($session_id) { global $wpdb; $wpdb->update( $this->table_sessions, array('end_time' => current_time('mysql')), array('session_id' => $session_id) ); } public function cleanup_old_data() { global $wpdb; $settings = get_option('vat_settings', array()); $retention_days = isset($settings['data_retention_days']) ? intval($settings['data_retention_days']) : 30; $cutoff_date = date('Y-m-d H:i:s', strtotime("-$retention_days days")); // 删除旧事件数据 $wpdb->query($wpdb->prepare( "DELETE FROM {$this->table_events} WHERE created_at < %s", $cutoff_date )); // 删除旧会话数据 $wpdb->query($wpdb->prepare( "DELETE FROM {$this->table_sessions} WHERE start_time < %s", $cutoff_date )); } } 第四部分:构建热力图生成与可视化系统 4.1 创建热力图数据处理类 在includes/class-heatmap-generator.php中: <?php class VAT_Heatmap_Generator { private $data_collector; public function __construct() { global $wpdb; $this->table_events = $wpdb->prefix . 'vat_events'; // 注册管理页面 add_action('admin_menu', array($this, 'add_admin_menu')); // 注册AJAX端点 add_action('wp_ajax_vat_get_heatmap_data', array($this, 'get_heatmap_data')); } public function add_admin_menu() { add_menu_page( '访客行为分析', '行为分析', 'manage_options', 'vat-analytics', array($this, 'render_admin_page'), 'dashicons-chart-area', 30 ); add_submenu_page( 'vat-analytics', '热力图分析', '热力图', 'manage_options', 'vat-heatmaps', array($this, 'render_heatmap_page') ); } public function render_admin_page() { ?> <div class="wrap"> <h1>访客行为分析仪表板</h1> <div class="vat-dashboard"> <div class="vat-stats-grid"> <div class="vat-stat-card"> <h3>今日会话</h3> <p class="vat-stat-number"><?php echo $this->get_today_sessions(); ?></p> </div> <div class="vat-stat-card"> <h3>平均点击次数</h3> <p class="vat-stat-number"><?php echo $this->get_average_clicks(); ?></p> </div> <div class="vat-stat-card"> <h3>热门页面</h3> <p class="vat-stat-number"><?php echo $this->get_top_page(); ?></p> </div> </div> </div> </div> <?php } public function render_heatmap_page() { $pages = $this->get_pages_with_data(); ?> <div class="wrap"> <h1>热力图分析</h1> <div class="vat-heatmap-controls"> <div class="vat-control-group"> <label for="vat-page-select">选择页面:</label> <select id="vat-page-select" class="vat-select"> <option value="">选择页面...</option> <?php foreach ($pages as $page): ?> <option value="<?php echo esc_attr($page->page_url); ?>"> <?php echo esc_html($this->truncate_url($page->page_url)); ?> (<?php echo $page->event_count; ?> 事件) </option> <?php endforeach; ?> </select> </div> <div class="vat-control-group"> <label for="vat-date-range">日期范围:</label> <select id="vat-date-range" class="vat-select"> <option value="today">今天</option> <option value="yesterday">昨天</option> <option value="7days" selected>最近7天</option> <option value="30days">最近30天</option> <option value="custom">自定义</option> </select> <div id="vat-custom-dates" style="display: none; margin-top: 10px;"> <input type="date" id="vat-date-from" class="vat-date-input"> <span>至</span> <input type="date" id="vat-date-to" class="vat-date-input"> </div> </div> <div class="vat-control-group"> <label for="vat-heatmap-type">热力图类型:</label> <select id="vat-heatmap-type" class="vat-select"> <option value="click">点击热力图</option> <option value="scroll">滚动热力图</option> <option value="attention">注意力热力图</option> </select> </div> <button id="vat-generate-heatmap" class="button button-primary">生成热力图</button> </div> <div class="vat-heatmap-container"> <div id="vat-page-preview" style="border: 1px solid #ddd; margin: 20px 0; position: relative; overflow: hidden;"> <!-- 页面预览将在这里显示 --> </div> <div id="vat-heatmap-canvas" style="position: absolute; top: 0; left: 0; pointer-events: none;"> <!-- 热力图将在这里绘制 --> </div> <div class="vat-heatmap-legend"> <div class="vat-legend-title">热力强度</div> <div class="vat-legend-gradient"> <span>低</span> <div class="vat-gradient-bar"></div> <span>高</span> </div> </div> </div> <div class="vat-heatmap-stats"> <h3>页面分析数据</h3> <div id="vat-page-stats"> <!-- 统计数据将动态加载 --> </div> </div> </div> <script> jQuery(document).ready(function($) { // 初始化热力图页面 vatHeatmap.init(); }); </script> <?php } private function get_pages_with_data() { global $wpdb; return $wpdb->get_results($wpdb->prepare( "SELECT page_url, COUNT(*) as event_count FROM {$this->table_events} WHERE event_type IN ('click', 'scroll', 'move_aggregated') AND created_at >= %s GROUP BY page_url ORDER BY event_count DESC LIMIT 50", date('Y-m-d H:i:s', strtotime('-30 days')) )); } public function get_heatmap_data() { // 验证权限 if (!current_user_can('manage_options')) { wp_die('权限不足', 403); } $page_url = isset($_GET['page_url']) ? urldecode($_GET['page_url']) : ''; $date_range = isset($_GET['date_range']) ? $_GET['date_range'] : '7days'; $heatmap_type = isset($_GET['heatmap_type']) ? $_GET['heatmap_type'] : 'click'; if (empty($page_url)) { wp_send_json_error('请选择页面'); } // 计算日期范围 $date_conditions = $this->get_date_condition($date_range); // 根据热力图类型获取数据 switch ($heatmap_type) { case 'click': $data = $this->get_click_data($page_url, $date_conditions); break; case 'scroll': $data = $this->get_scroll_data($page_url, $date_conditions); break; case 'attention': $data = $this->get_attention_data($page_url, $date_conditions); break; default: $data = array(); } // 获取页面统计数据 $stats = $this->get_page_stats($page_url, $date_conditions); wp_send_json_success(array( 'data' => $data, 'stats' => $stats, 'page_url' => $page_url )); } private function get_click_data($page_url, $date_conditions) { global $wpdb; $events = $wpdb->get_results($wpdb->prepare( "SELECT event_data FROM {$this->table_events} WHERE page_url = %s AND event_type = 'click' AND created_at >= %s ORDER BY created_at DESC LIMIT 1000", $page_url, $date_conditions['start_date'] )); $clicks = array(); foreach ($events as $event) { $data = json_decode($event->event_data, true); if (isset($data['x']) && isset($data['y'])) { $clicks[] = array( 'x' => intval($data['x']), 'y' => intval($data['y']), 'viewport_width' => isset($data['viewport']['width']) ? intval($data['viewport']['width']) : 1920, 'viewport_height' => isset($data['viewport']['height']) ? intval($data['viewport']['height']) : 1080 ); } } return $clicks; } private function get_scroll_data($page_url, $date_conditions) { global $wpdb; $events = $wpdb->get_results($wpdb->prepare( "SELECT event_data FROM {$this->table_events} WHERE page_url = %s AND event_type = 'scroll' AND created_at >= %s ORDER BY created_at DESC LIMIT 5000", $page_url, $date_conditions['start_date'] )); $scroll_depths = array(); foreach ($events as $event) { $data = json_decode($event->event_data, true); if (isset($data['percentage'])) { $scroll_depths[] = floatval($data['percentage']); } } // 计算滚动深度分布 $distribution = array_fill(0, 10, 0); // 10个区间:0-10%, 10-20%, ..., 90-100% foreach ($scroll_depths as $depth) { $index = min(floor($depth / 10), 9); $distribution[$index]++; } return $distribution; } private function get_attention_data($page_url, $date_conditions) { global $wpdb; $events = $wpdb->get_results($wpdb->prepare( "SELECT event_data FROM {$this->table_events} WHERE page_url = %s AND event_type = 'move_aggregated' AND created_at >= %s ORDER BY created_at DESC LIMIT 100", $page_url, $date_conditions['start_date'] )); $positions = array(); foreach ($events as $event) { $data = json_decode($event->event_data, true); if (isset($data['positions'])) { foreach ($data['positions'] as $pos) { $positions[] = array( 'x' => $pos[0], 'y' => $pos[1] ); } } } return $positions; } private function get_page_stats($page_url, $date_conditions) { global $wpdb; $stats = array(); // 获取总点击次数 $stats['total_clicks'] = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$this->table_events} WHERE page_url = %s AND event_type = 'click' AND created_at >= %s", $page_url, $date_conditions['start_date'] )); // 获取独立访客数 $stats['unique_visitors'] = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(DISTINCT session_id) FROM {$this->table_events} WHERE page_url = %s AND created_at >= %s", $page_url, $date_conditions['start_date'] )); // 获取平均滚动深度 $scroll_data = $wpdb->get_results($wpdb->prepare( "SELECT event_data FROM {$this->table_events} WHERE page_url = %s AND event_type = 'scroll' AND created_at >= %s", $page_url, $date_conditions['start_date'] )); $total_depth = 0; $count = 0; foreach ($scroll_data as $event) { $data = json_decode($event->event_data, true); if (isset($data['percentage'])) { $total_depth += floatval($data['percentage']); $count++; } }
发表评论分类: 网站建设
实战教程:集成网站实时协同编辑文档与电子表格应用,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:为什么需要实时协同编辑功能? 在当今数字化工作环境中,实时协同编辑已成为提高团队效率的关键工具。无论是远程团队协作、在线教育还是客户服务,能够多人同时编辑文档和电子表格的功能都大大提升了工作效率和沟通效果。然而,对于许多中小型企业和个人网站所有者来说,集成专业的协同编辑工具往往面临高昂的成本和技术门槛。 WordPress作为全球最流行的内容管理系统,拥有超过40%的网站市场份额,其强大的可扩展性为我们提供了一个理想的平台。通过代码二次开发,我们可以在WordPress网站上集成实时协同编辑功能,同时实现其他常用互联网小工具,从而打造一个功能全面、成本可控的协作平台。 本教程将详细指导您如何通过WordPress代码二次开发,实现文档和电子表格的实时协同编辑功能,并集成其他实用工具,最终打造一个功能丰富的在线协作环境。 第一部分:准备工作与环境搭建 1.1 开发环境要求 在开始开发之前,我们需要确保具备以下环境: WordPress 5.8或更高版本 PHP 7.4或更高版本(建议8.0+) MySQL 5.6或更高版本 支持WebSocket的服务器环境(Nginx或Apache) SSL证书(HTTPS协议对于实时通信至关重要) 代码编辑器(如VS Code、Sublime Text等) 1.2 创建自定义插件框架 首先,我们需要创建一个自定义插件来容纳所有功能代码: 在WordPress的wp-content/plugins/目录下创建新文件夹real-time-collab-tools 在该文件夹中创建主插件文件real-time-collab-tools.php 添加插件基本信息: <?php /** * Plugin Name: 实时协同编辑工具套件 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站添加实时协同编辑文档和电子表格功能,集成常用互联网小工具 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: rt-collab-tools */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('RT_COLLAB_VERSION', '1.0.0'); define('RT_COLLAB_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('RT_COLLAB_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once RT_COLLAB_PLUGIN_DIR . 'includes/class-init.php'; 创建初始化类文件includes/class-init.php: <?php class RT_Collab_Init { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); $this->load_dependencies(); } private function init_hooks() { // 注册激活和停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化插件 add_action('plugins_loaded', array($this, 'init_plugin')); } private function load_dependencies() { // 加载依赖文件 require_once RT_COLLAB_PLUGIN_DIR . 'includes/class-websocket-server.php'; require_once RT_COLLAB_PLUGIN_DIR . 'includes/class-document-editor.php'; require_once RT_COLLAB_PLUGIN_DIR . 'includes/class-spreadsheet-editor.php'; require_once RT_COLLAB_PLUGIN_DIR . 'includes/class-tools-manager.php'; require_once RT_COLLAB_PLUGIN_DIR . 'includes/class-database.php'; } public function activate() { // 创建必要的数据库表 RT_Collab_Database::create_tables(); // 设置默认选项 update_option('rt_collab_version', RT_COLLAB_VERSION); } public function deactivate() { // 清理临时数据 // 注意:不删除用户数据 } public function init_plugin() { // 初始化各个组件 RT_WebSocket_Server::get_instance(); RT_Document_Editor::get_instance(); RT_Spreadsheet_Editor::get_instance(); RT_Tools_Manager::get_instance(); // 加载文本域 load_plugin_textdomain('rt-collab-tools', false, dirname(plugin_basename(__FILE__)) . '/languages'); } } // 初始化插件 RT_Collab_Init::get_instance(); 第二部分:实现WebSocket实时通信服务器 2.1 WebSocket服务器基础架构 实时协同编辑的核心是WebSocket通信。我们将使用PHP的Ratchet库来创建WebSocket服务器: 首先通过Composer安装Ratchet库: cd /path/to/your/plugin composer require cboden/ratchet 创建WebSocket服务器类includes/class-websocket-server.php: <?php require_once RT_COLLAB_PLUGIN_DIR . 'vendor/autoload.php'; use RatchetMessageComponentInterface; use RatchetConnectionInterface; use RatchetServerIoServer; use RatchetHttpHttpServer; use RatchetWebSocketWsServer; class RT_WebSocket_Server implements MessageComponentInterface { protected $clients; protected $documentSessions; protected $spreadsheetSessions; private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } public function __construct() { $this->clients = new SplObjectStorage; $this->documentSessions = []; $this->spreadsheetSessions = []; // 启动WebSocket服务器 $this->start_server(); } private function start_server() { // 仅在特定条件下启动服务器(如通过WP-CLI) if (defined('WP_CLI') && WP_CLI) { $port = get_option('rt_collab_websocket_port', 8080); $server = IoServer::factory( new HttpServer( new WsServer($this) ), $port ); // 在后台运行服务器 add_action('init', function() use ($server) { if (current_user_can('manage_options')) { $server->run(); } }); } } public function onOpen(ConnectionInterface $conn) { // 存储新连接 $this->clients->attach($conn); echo "新连接: {$conn->resourceId}n"; } public function onMessage(ConnectionInterface $from, $msg) { // 解析消息 $data = json_decode($msg, true); if (!$data || !isset($data['type'])) { return; } // 根据消息类型处理 switch ($data['type']) { case 'join_document': $this->handleJoinDocument($from, $data); break; case 'document_edit': $this->handleDocumentEdit($from, $data); break; case 'join_spreadsheet': $this->handleJoinSpreadsheet($from, $data); break; case 'spreadsheet_edit': $this->handleSpreadsheetEdit($from, $data); break; case 'cursor_move': $this->handleCursorMove($from, $data); break; } } private function handleJoinDocument($conn, $data) { $docId = $data['document_id']; $userId = $data['user_id']; if (!isset($this->documentSessions[$docId])) { $this->documentSessions[$docId] = [ 'clients' => [], 'content' => '', 'revision' => 0 ]; } // 添加客户端到会话 $this->documentSessions[$docId]['clients'][$conn->resourceId] = [ 'connection' => $conn, 'user_id' => $userId, 'cursor_position' => 0 ]; // 发送当前文档内容给新用户 $conn->send(json_encode([ 'type' => 'document_content', 'content' => $this->documentSessions[$docId]['content'], 'revision' => $this->documentSessions[$docId]['revision'] ])); // 通知其他用户有新成员加入 $this->broadcastToDocument($docId, $conn->resourceId, [ 'type' => 'user_joined', 'user_id' => $userId, 'total_users' => count($this->documentSessions[$docId]['clients']) ]); } private function handleDocumentEdit($conn, $data) { $docId = $data['document_id']; if (!isset($this->documentSessions[$docId])) { return; } // 应用操作转换(OT)算法处理并发编辑 $transformedOperation = $this->transformOperation( $data['operation'], $this->documentSessions[$docId]['revision'] ); // 更新文档内容 $this->documentSessions[$docId]['content'] = $this->applyOperation( $this->documentSessions[$docId]['content'], $transformedOperation ); $this->documentSessions[$docId]['revision']++; // 广播编辑操作给其他用户 $this->broadcastToDocument($docId, $conn->resourceId, [ 'type' => 'document_update', 'operation' => $transformedOperation, 'revision' => $this->documentSessions[$docId]['revision'], 'user_id' => $data['user_id'] ]); } private function broadcastToDocument($docId, $excludeClientId, $message) { if (!isset($this->documentSessions[$docId])) { return; } foreach ($this->documentSessions[$docId]['clients'] as $clientId => $client) { if ($clientId != $excludeClientId) { $client['connection']->send(json_encode($message)); } } } public function onClose(ConnectionInterface $conn) { // 从所有会话中移除客户端 foreach ($this->documentSessions as $docId => $session) { if (isset($session['clients'][$conn->resourceId])) { unset($this->documentSessions[$docId]['clients'][$conn->resourceId]); // 通知其他用户 $this->broadcastToDocument($docId, $conn->resourceId, [ 'type' => 'user_left', 'user_id' => $session['clients'][$conn->resourceId]['user_id'], 'total_users' => count($this->documentSessions[$docId]['clients']) ]); } } $this->clients->detach($conn); echo "连接关闭: {$conn->resourceId}n"; } public function onError(ConnectionInterface $conn, Exception $e) { echo "错误: {$e->getMessage()}n"; $conn->close(); } // 简化版的OT操作转换(实际项目应使用完整OT算法) private function transformOperation($operation, $revision) { // 这里实现操作转换逻辑 // 由于篇幅限制,这里返回原始操作 return $operation; } private function applyOperation($content, $operation) { // 应用操作到内容 // 简化实现 if ($operation['type'] == 'insert') { return substr($content, 0, $operation['position']) . $operation['text'] . substr($content, $operation['position']); } return $content; } } 2.2 数据库设计与数据持久化 创建数据库表来存储文档和电子表格数据: <?php class RT_Collab_Database { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 文档表 $documents_table = $wpdb->prefix . 'rt_collab_documents'; $sql_documents = "CREATE TABLE IF NOT EXISTS $documents_table ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, content longtext NOT NULL, owner_id bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, revision int(11) DEFAULT 0, permissions varchar(20) DEFAULT 'private', PRIMARY KEY (id), KEY owner_id (owner_id) ) $charset_collate;"; // 电子表格表 $spreadsheets_table = $wpdb->prefix . 'rt_collab_spreadsheets'; $sql_spreadsheets = "CREATE TABLE IF NOT EXISTS $spreadsheets_table ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, data longtext NOT NULL, owner_id bigint(20) NOT NULL, rows int(11) DEFAULT 100, columns int(11) DEFAULT 26, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, permissions varchar(20) DEFAULT 'private', PRIMARY KEY (id), KEY owner_id (owner_id) ) $charset_collate;"; // 文档访问记录表 $document_access_table = $wpdb->prefix . 'rt_collab_document_access'; $sql_document_access = "CREATE TABLE IF NOT EXISTS $document_access_table ( id bigint(20) NOT NULL AUTO_INCREMENT, document_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL, last_access datetime DEFAULT CURRENT_TIMESTAMP, permission_level varchar(20) DEFAULT 'view', PRIMARY KEY (id), UNIQUE KEY document_user (document_id, user_id), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_documents); dbDelta($sql_spreadsheets); dbDelta($sql_document_access); } } 第三部分:实现文档协同编辑器 3.1 前端编辑器界面 创建文档编辑器前端界面: <?php class RT_Document_Editor { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); add_shortcode('collab_document', array($this, 'document_shortcode')); add_action('wp_ajax_save_document', array($this, 'save_document')); add_action('wp_ajax_nopriv_save_document', array($this, 'save_document')); } public function enqueue_scripts() { // 仅在有需要的页面加载 if ($this->is_editor_page()) { // 加载CodeMirror编辑器 wp_enqueue_style('codemirror-css', 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.0/codemirror.min.css'); wp_enqueue_script('codemirror-js', 'https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.0/codemirror.min.js', array(), null, true); // 加载协同编辑前端脚本 wp_enqueue_script('rt-collab-document', RT_COLLAB_PLUGIN_URL . 'assets/js/document-editor.js', array('jquery', 'codemirror-js'), RT_COLLAB_VERSION, true); // 本地化脚本 wp_localize_script('rt-collab-document', 'rtCollabConfig', array( 'ajax_url' => admin_url('admin-ajax.php'), 'websocket_url' => $this->get_websocket_url(), 'current_user' => get_current_user_id(), 'nonce' => wp_create_nonce('rt_collab_nonce') )); } } public function document_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'title' => '协同文档', 'height' => '500px' ), $atts, 'collab_document'); if (!$atts['id']) { return '<p>请指定文档ID</p>'; } // 检查用户权限 if (!$this->check_document_access($atts['id'])) { return '<p>您没有权限访问此文档</p>'; } ob_start(); ?> <div class="rt-collab-document-editor" data-document-id="<?php echo esc_attr($atts['id']); ?>"> <div class="document-header"> <h3><?php echo esc_html($atts['title']); ?></h3> <div class="document-toolbar"> <button class="btn-save">保存</button> <button class="btn-share">分享</button> <span class="user-count">在线用户: <span class="count">1</span></span> </div> </div> attr($atts['height']); ?>"> <textarea id="document-editor-<?php echo esc_attr($atts['id']); ?>" class="document-editor"></textarea> </div> <div class="document-users"> <h4>当前在线用户</h4> <ul class="user-list"> <!-- 用户列表将通过JavaScript动态生成 --> </ul> </div> <div class="document-revision"> 版本: <span class="revision-number">0</span> </div> </div> <?php return ob_get_clean(); } private function check_document_access($document_id) { // 简化权限检查,实际应更复杂 if (is_user_logged_in()) { return true; } // 检查是否为公开文档 global $wpdb; $table_name = $wpdb->prefix . 'rt_collab_documents'; $document = $wpdb->get_row($wpdb->prepare( "SELECT permissions FROM $table_name WHERE id = %d", $document_id )); return $document && $document->permissions === 'public'; } private function get_websocket_url() { $protocol = is_ssl() ? 'wss://' : 'ws://'; $host = $_SERVER['HTTP_HOST']; $port = get_option('rt_collab_websocket_port', 8080); return $protocol . $host . ':' . $port; } private function is_editor_page() { global $post; if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'collab_document')) { return true; } return false; } public function save_document() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'rt_collab_nonce')) { wp_die('安全验证失败'); } $document_id = intval($_POST['document_id']); $content = wp_kses_post($_POST['content']); $user_id = get_current_user_id(); // 保存到数据库 global $wpdb; $table_name = $wpdb->prefix . 'rt_collab_documents'; $result = $wpdb->update( $table_name, array( 'content' => $content, 'updated_at' => current_time('mysql') ), array('id' => $document_id), array('%s', '%s'), array('%d') ); if ($result !== false) { wp_send_json_success(array('message' => '文档保存成功')); } else { wp_send_json_error(array('message' => '保存失败')); } } } ### 3.2 前端JavaScript协同逻辑 创建`assets/js/document-editor.js`: (function($) { 'use strict'; class DocumentEditor { constructor(element) { this.element = element; this.documentId = element.data('document-id'); this.editor = null; this.websocket = null; this.userId = rtCollabConfig.current_user; this.revision = 0; this.pendingOperations = []; this.isApplyingRemote = false; this.init(); } init() { this.initEditor(); this.initWebSocket(); this.bindEvents(); this.loadDocument(); } initEditor() { // 初始化CodeMirror编辑器 this.editor = CodeMirror.fromTextArea( this.element.find('.document-editor')[0], { lineNumbers: true, mode: 'text/html', theme: 'default', lineWrapping: true, autofocus: true } ); // 监听本地编辑 this.editor.on('change', (instance, changeObj) => { this.handleLocalEdit(changeObj); }); // 监听光标移动 this.editor.on('cursorActivity', (instance) => { this.sendCursorPosition(); }); } initWebSocket() { this.websocket = new WebSocket(rtCollabConfig.websocket_url); this.websocket.onopen = () => { console.log('WebSocket连接已建立'); this.joinDocument(); }; this.websocket.onmessage = (event) => { this.handleWebSocketMessage(JSON.parse(event.data)); }; this.websocket.onerror = (error) => { console.error('WebSocket错误:', error); }; this.websocket.onclose = () => { console.log('WebSocket连接已关闭'); // 尝试重新连接 setTimeout(() => this.initWebSocket(), 3000); }; } joinDocument() { this.websocket.send(JSON.stringify({ type: 'join_document', document_id: this.documentId, user_id: this.userId })); } handleLocalEdit(changeObj) { if (this.isApplyingRemote) { return; } const operation = { type: 'edit', revision: this.revision, changes: [{ from: changeObj.from, to: changeObj.to, text: changeObj.text, removed: changeObj.removed }] }; // 发送到服务器 this.websocket.send(JSON.stringify({ type: 'document_edit', document_id: this.documentId, user_id: this.userId, operation: operation })); // 添加到待处理队列 this.pendingOperations.push(operation); } handleWebSocketMessage(message) { switch (message.type) { case 'document_content': this.loadContent(message.content); this.revision = message.revision; break; case 'document_update': this.applyRemoteEdit(message.operation, message.user_id); this.revision = message.revision; break; case 'user_joined': this.updateUserList(message); break; case 'user_left': this.updateUserList(message); break; case 'cursor_position': this.showRemoteCursor(message); break; } } loadContent(content) { this.isApplyingRemote = true; this.editor.setValue(content); this.isApplyingRemote = false; } applyRemoteEdit(operation, userId) { if (userId === this.userId) { // 自己的操作,从待处理队列中移除 this.pendingOperations = this.pendingOperations.filter(op => op.revision !== operation.revision ); return; } this.isApplyingRemote = true; // 应用远程编辑 operation.changes.forEach(change => { this.editor.replaceRange( change.text, change.from, change.to ); }); this.isApplyingRemote = false; // 更新待处理操作 this.pendingOperations = this.pendingOperations.map(op => this.transformOperation(op, operation) ); } transformOperation(localOp, remoteOp) { // 简化版操作转换 // 实际应实现完整的OT算法 return localOp; } sendCursorPosition() { const cursor = this.editor.getCursor(); this.websocket.send(JSON.stringify({ type: 'cursor_move', document_id: this.documentId, user_id: this.userId, position: cursor })); } showRemoteCursor(message) { // 显示其他用户的光标位置 // 实现光标可视化 } updateUserList(message) { const userList = this.element.find('.user-list'); const countElement = this.element.find('.user-count .count'); countElement.text(message.total_users); // 更新用户列表显示 // 实际实现中应从服务器获取完整用户列表 } loadDocument() { // 从服务器加载文档 $.ajax({ url: rtCollabConfig.ajax_url, method: 'POST', data: { action: 'get_document', document_id: this.documentId, nonce: rtCollabConfig.nonce }, success: (response) => { if (response.success) { this.loadContent(response.data.content); } } }); } bindEvents() { // 保存按钮 this.element.find('.btn-save').on('click', () => { this.saveDocument(); }); // 分享按钮 this.element.find('.btn-share').on('click', () => { this.shareDocument(); }); } saveDocument() { const content = this.editor.getValue(); $.ajax({ url: rtCollabConfig.ajax_url, method: 'POST', data: { action: 'save_document', document_id: this.documentId, content: content, nonce: rtCollabConfig.nonce }, success: (response) => { if (response.success) { alert('文档保存成功'); } else { alert('保存失败: ' + response.data.message); } } }); } shareDocument() { // 生成分享链接 const shareUrl = window.location.origin + '/?document=' + this.documentId; // 复制到剪贴板 navigator.clipboard.writeText(shareUrl) .then(() => alert('分享链接已复制到剪贴板')) .catch(() => prompt('请手动复制链接:', shareUrl)); } } // 初始化所有文档编辑器 $(document).ready(function() { $('.rt-collab-document-editor').each(function() { new DocumentEditor($(this)); }); }); })(jQuery); ## 第四部分:实现电子表格协同编辑器 ### 4.1 电子表格后端处理 创建`includes/class-spreadsheet-editor.php`: <?phpclass RT_Spreadsheet_Editor { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); add_shortcode('collab_spreadsheet', array($this, 'spreadsheet_shortcode')); add_action('wp_ajax_save_spreadsheet', array($this, 'save_spreadsheet')); add_action('wp_ajax_get_spreadsheet', array($this, 'get_spreadsheet')); } public function enqueue_scripts() { if ($this->is_spreadsheet_page()) { // 加载Handsontable电子表格库 wp_enqueue_style('handsontable-css', 'https://cdn.jsdelivr.net/npm/handsontable@latest/dist/handsontable.full.min.css'); wp_enqueue_script('handsontable-js', 'https://cdn.jsdelivr.net/npm/handsontable@latest/dist/handsontable.full.min.js', array(), null, true); // 加载协同电子表格脚本 wp_enqueue_script('rt-collab-spreadsheet', RT_COLLAB_PLUGIN_URL . 'assets/js/spreadsheet-editor.js', array('jquery', 'handsontable-js'), RT_COLLAB_VERSION, true); wp_localize_script('rt-collab-spreadsheet', 'rtSpreadsheetConfig', array( 'ajax_url' => admin_url('admin-ajax.php'), 'websocket_url' => $this->get_websocket_url(), 'current_user' => get_current_user_id(), 'nonce' => wp_create_nonce('rt_spreadsheet_nonce') )); } } public function spreadsheet_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'title' => '协同电子表格', 'rows' => 100, 'cols' => 26, 'height' => '500px' ), $atts, 'collab_spreadsheet'); if (!$atts['id']) { // 创建新电子表格 $atts['id'] = $this->create_new_spreadsheet($atts); } if (!$this->check_spreadsheet_access($atts['id'])) { return '<p>您没有权限访问此电子表格</p>'; } ob_start(); ?> <div class="rt-collab-spreadsheet-editor" data-spreadsheet-id="<?php echo esc_attr($atts['id']); ?>" data-rows="<?php echo esc_attr($atts['rows']); ?>" data-cols="<?php echo esc_attr($atts['cols']); ?>"> <div class="spreadsheet-header"> <h3><?php echo esc_html($atts['title']); ?></h3> <div class="spreadsheet-toolbar"> <button class="btn-save-sheet">保存</button> <button class="btn-export">导出Excel</button> <button class="btn-add-sheet">添加工作表</button> <span class="user-count">在线用户: <span class="count">1</span></span> </div> </div> <div class="spreadsheet-container" style="height: <?php echo esc_attr($atts['height']); ?>"> <div id="spreadsheet-<?php echo esc_attr($atts['id']); ?>" class="hot"></div> </div> <div class="spreadsheet-info"> <div class="sheet-tabs"> <!-- 工作表标签将通过JavaScript生成 --> </div> <div class="cell-info"> 选中: <span class="selected-range">A1</span> </div> </div> </div> <?php return ob_get_clean(); } private function create_new_spreadsheet($atts) { global $wpdb; $table_name = $wpdb->prefix . 'rt_collab_spreadsheets'; $default_data = array( 'sheets' => array( 'Sheet1' => array( 'data' => array(), 'rows' => intval($atts['rows']), 'cols' => intval($atts['cols']) ) ), 'activeSheet' => 'Sheet1' ); $wpdb->insert( $table_name, array( 'title' => $atts['title'], 'data' => json_encode($default_data), 'owner_id' => get_current_user_id(), 'rows' => $atts['rows'], 'columns' => $atts['cols'] ), array('%s', '%s', '%d', '%d', '%d') ); return $wpdb->insert_id; } private function check_spreadsheet_access($spreadsheet_id) { // 权限检查逻辑 return true; } public function save_spreadsheet() { if (!wp_verify_nonce($_POST['nonce'], 'rt_spreadsheet_nonce')) { wp_die('安全验证失败'); } $spreadsheet_id = intval($_POST['spreadsheet_id']); $data = json_decode(stripslashes($_POST['data']), true); global $wpdb; $table_name = $wpdb->prefix . 'rt_collab_spreadsheets'; $result = $wpdb->update( $table_name, array( 'data' => json_encode($data), 'updated_at' => current_time('mysql') ), array('id' => $spreadsheet_id), array('%s', '%s'), array('%d') ); if ($result !== false) { wp_send_json_success(array('message' => '保存成功')); } else { wp_send_json_error(array('message' => '保存失败')); } } public function get_spreadsheet() { $spreadsheet_id = intval($_GET['spreadsheet_id']); global $wpdb; $table_name = $wpdb->prefix . 'rt_collab_spreadsheets'; $spreadsheet = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $spreadsheet_id )); if ($spreadsheet) { $data = json_decode($spreadsheet->data, true); wp_send_json_success(array( 'data' => $data, 'title' => $spreadsheet->title, 'rows' => $spreadsheet->rows, 'columns' => $spreadsheet->columns )); } else { wp_send_json_error(array('message' => '电子表格不存在')); } } private function is_spreadsheet_page() { global $post; return is_a($post, 'WP_Post') && (has_shortcode($post->post_content, 'collab_spreadsheet') || has_shortcode($post->post_content, 'collab_document')); } } ### 4.2 电子表格前端协同逻辑 创建`assets/js/spreadsheet-editor.js`: (function($) { 'use strict'; class SpreadsheetEditor { constructor(element) { this.element = element; this.spreadsheetId = element.data('spreadsheet-id'); this.hot = null; this.websocket = null; this.userId = rtSpreadsheetConfig.current_user; this.data = null; this.pendingChanges = []; this.init(); } init() { this.loadSpreadsheet().then(() => { this.initHandsontable(); this.initWebSocket(); this.bindEvents(); }); } async loadSpreadsheet() { try { const response = await $.ajax({ url: rtSpreadsheet
发表评论详细指南:在WordPress中开发内嵌式项目管理与甘特图工具 摘要 本文提供了一份完整的指南,介绍如何在WordPress平台中通过代码二次开发,实现内嵌式项目管理与甘特图工具。我们将从需求分析开始,逐步讲解数据库设计、功能模块开发、甘特图集成、用户界面设计以及性能优化等关键环节,帮助开发者将常用互联网小工具功能无缝集成到WordPress系统中。 目录 项目概述与需求分析 WordPress开发环境搭建 数据库设计与数据模型 项目管理核心功能开发 甘特图集成与可视化 用户权限与团队协作 前端界面与用户体验优化 数据安全与性能优化 测试与部署指南 扩展与维护建议 1. 项目概述与需求分析 1.1 项目背景 随着远程工作和团队协作的普及,项目管理工具成为企业日常运营的重要组成部分。许多中小型企业使用WordPress作为其官方网站或内容管理系统,但缺乏集成的项目管理功能。通过开发内嵌式项目管理与甘特图工具,用户可以在熟悉的WordPress环境中管理项目,无需切换多个平台,提高工作效率。 1.2 功能需求 项目管理:创建、编辑、删除项目,设置项目基本信息 任务管理:任务创建、分配、优先级设置、状态跟踪 甘特图可视化:直观展示项目时间线、任务依赖关系 团队协作:用户角色分配、任务评论、文件附件 进度跟踪:完成百分比、里程碑标记、时间跟踪 报告与分析:项目进度报告、团队绩效统计 1.3 技术选型 核心框架:WordPress插件架构 前端技术:React/Vue.js(可选)、jQuery、HTML5、CSS3 图表库:DHTMLX Gantt、Frappe Gantt或自定义SVG实现 数据库:WordPress默认MySQL数据库 通信方式:REST API + AJAX 2. WordPress开发环境搭建 2.1 本地开发环境配置 # 使用Local by Flywheel或Docker配置WordPress环境 # 安装必要工具 npm install -g @wordpress/env wp-env start # 或使用传统方法 # 1. 安装XAMPP/MAMP/WAMP # 2. 下载最新WordPress # 3. 配置数据库 2.2 插件基础结构 创建插件主文件 project-management-gantt.php: <?php /** * Plugin Name: 项目管理与甘特图工具 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress中集成项目管理与甘特图功能 * Version: 1.0.0 * Author: 开发者名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('PMG_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('PMG_PLUGIN_URL', plugin_dir_url(__FILE__)); define('PMG_VERSION', '1.0.0'); // 初始化插件 require_once PMG_PLUGIN_DIR . 'includes/class-pmg-init.php'; PMG_Init::register(); 2.3 插件目录结构 project-management-gantt/ ├── project-management-gantt.php # 主插件文件 ├── includes/ # 核心类文件 │ ├── class-pmg-init.php # 初始化类 │ ├── class-pmg-database.php # 数据库处理 │ ├── class-pmg-projects.php # 项目管理类 │ ├── class-pmg-tasks.php # 任务管理类 │ └── class-pmg-gantt.php # 甘特图处理类 ├── admin/ # 后台管理文件 │ ├── css/ # 管理端CSS │ ├── js/ # 管理端JavaScript │ └── views/ # 管理端视图 ├── public/ # 前端文件 │ ├── css/ # 前端CSS │ ├── js/ # 前端JavaScript │ └── views/ # 前端视图 ├── assets/ # 静态资源 │ ├── lib/ # 第三方库 │ └── images/ # 图片资源 └── templates/ # 模板文件 3. 数据库设计与数据模型 3.1 自定义数据表设计 为了避免与WordPress核心表冲突,我们创建独立的数据表: // includes/class-pmg-database.php class PMG_Database { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 项目表 $projects_table = $wpdb->prefix . 'pmg_projects'; $projects_sql = "CREATE TABLE IF NOT EXISTS $projects_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, name varchar(255) NOT NULL, description text, status varchar(50) DEFAULT 'active', start_date date, end_date date, progress tinyint(3) DEFAULT 0, created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 任务表 $tasks_table = $wpdb->prefix . 'pmg_tasks'; $tasks_sql = "CREATE TABLE IF NOT EXISTS $tasks_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, project_id mediumint(9) NOT NULL, parent_id mediumint(9) DEFAULT 0, title varchar(255) NOT NULL, description text, start_date date, end_date date, duration int(11) DEFAULT 1, progress tinyint(3) DEFAULT 0, priority varchar(20) DEFAULT 'medium', status varchar(50) DEFAULT 'pending', assigned_to bigint(20), sort_order int(11) DEFAULT 0, dependencies text, created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY project_id (project_id) ) $charset_collate;"; // 项目成员表 $members_table = $wpdb->prefix . 'pmg_project_members'; $members_sql = "CREATE TABLE IF NOT EXISTS $members_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, project_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, role varchar(50) DEFAULT 'member', joined_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY project_user (project_id, user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($projects_sql); dbDelta($tasks_sql); dbDelta($members_sql); } } 3.2 数据关系模型 一个项目包含多个任务 一个任务可以有子任务(通过parent_id实现层级结构) 一个项目可以有多个成员 一个用户可以参与多个项目 3.3 数据表优化考虑 添加适当的索引以提高查询性能 考虑大数据量下的分表策略 定期清理历史数据 4. 项目管理核心功能开发 4.1 项目CRUD操作 // includes/class-pmg-projects.php class PMG_Projects { // 创建新项目 public static function create_project($data) { global $wpdb; $table = $wpdb->prefix . 'pmg_projects'; $defaults = array( 'name' => '', 'description' => '', 'status' => 'active', 'start_date' => current_time('mysql'), 'end_date' => null, 'progress' => 0, 'created_by' => get_current_user_id() ); $data = wp_parse_args($data, $defaults); $wpdb->insert($table, $data); return $wpdb->insert_id; } // 获取项目列表 public static function get_projects($args = array()) { global $wpdb; $table = $wpdb->prefix . 'pmg_projects'; $defaults = array( 'status' => 'active', 'user_id' => 0, 'per_page' => 10, 'page' => 1 ); $args = wp_parse_args($args, $defaults); $where = "WHERE status = '" . esc_sql($args['status']) . "'"; if ($args['user_id'] > 0) { $members_table = $wpdb->prefix . 'pmg_project_members'; $where .= " AND id IN (SELECT project_id FROM $members_table WHERE user_id = " . intval($args['user_id']) . ")"; } $offset = ($args['page'] - 1) * $args['per_page']; $query = "SELECT * FROM $table $where ORDER BY created_at DESC LIMIT %d OFFSET %d"; return $wpdb->get_results($wpdb->prepare($query, $args['per_page'], $offset)); } // 更新项目 public static function update_project($project_id, $data) { global $wpdb; $table = $wpdb->prefix . 'pmg_projects'; return $wpdb->update($table, $data, array('id' => $project_id)); } // 删除项目 public static function delete_project($project_id) { global $wpdb; $table = $wpdb->prefix . 'pmg_projects'; // 同时删除相关任务和成员 $tasks_table = $wpdb->prefix . 'pmg_tasks'; $members_table = $wpdb->prefix . 'pmg_project_members'; $wpdb->delete($tasks_table, array('project_id' => $project_id)); $wpdb->delete($members_table, array('project_id' => $project_id)); return $wpdb->delete($table, array('id' => $project_id)); } } 4.2 任务管理功能 // includes/class-pmg-tasks.php class PMG_Tasks { // 创建任务 public static function create_task($data) { global $wpdb; $table = $wpdb->prefix . 'pmg_tasks'; $defaults = array( 'project_id' => 0, 'parent_id' => 0, 'title' => '', 'description' => '', 'start_date' => current_time('mysql'), 'end_date' => null, 'duration' => 1, 'progress' => 0, 'priority' => 'medium', 'status' => 'pending', 'assigned_to' => null, 'sort_order' => 0, 'dependencies' => '', 'created_by' => get_current_user_id() ); $data = wp_parse_args($data, $defaults); // 自动计算结束日期 if (empty($data['end_date']) && !empty($data['start_date']) && $data['duration'] > 0) { $start_date = new DateTime($data['start_date']); $start_date->modify('+' . ($data['duration'] - 1) . ' days'); $data['end_date'] = $start_date->format('Y-m-d'); } $wpdb->insert($table, $data); return $wpdb->insert_id; } // 获取项目任务树 public static function get_project_tasks($project_id, $flat = false) { global $wpdb; $table = $wpdb->prefix . 'pmg_tasks'; $tasks = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table WHERE project_id = %d ORDER BY parent_id, sort_order ASC", $project_id )); if ($flat) { return $tasks; } // 构建层级结构 return self::build_task_tree($tasks); } // 构建任务树 private static function build_task_tree($tasks, $parent_id = 0) { $tree = array(); foreach ($tasks as $task) { if ($task->parent_id == $parent_id) { $children = self::build_task_tree($tasks, $task->id); if ($children) { $task->children = $children; } $tree[] = $task; } } return $tree; } // 更新任务进度 public static function update_task_progress($task_id, $progress) { global $wpdb; $table = $wpdb->prefix . 'pmg_tasks'; $result = $wpdb->update($table, array('progress' => $progress, 'updated_at' => current_time('mysql')), array('id' => $task_id) ); // 更新父任务和项目进度 if ($result) { self::update_parent_progress($task_id); } return $result; } // 递归更新父任务进度 private static function update_parent_progress($task_id) { global $wpdb; $table = $wpdb->prefix . 'pmg_tasks'; $task = $wpdb->get_row($wpdb->prepare("SELECT * FROM $table WHERE id = %d", $task_id)); if ($task && $task->parent_id > 0) { // 计算父任务下所有子任务的平均进度 $children = $wpdb->get_results($wpdb->prepare( "SELECT progress FROM $table WHERE parent_id = %d", $task->parent_id )); if ($children) { $total_progress = 0; foreach ($children as $child) { $total_progress += $child->progress; } $avg_progress = round($total_progress / count($children)); $wpdb->update($table, array('progress' => $avg_progress), array('id' => $task->parent_id) ); // 递归更新 self::update_parent_progress($task->parent_id); } } // 更新项目进度 if ($task) { self::update_project_progress($task->project_id); } } // 更新项目进度 private static function update_project_progress($project_id) { global $wpdb; $tasks_table = $wpdb->prefix . 'pmg_tasks'; $projects_table = $wpdb->prefix . 'pmg_projects'; // 计算项目下所有根任务的平均进度 $root_tasks = $wpdb->get_results($wpdb->prepare( "SELECT progress FROM $tasks_table WHERE project_id = %d AND parent_id = 0", $project_id )); if ($root_tasks) { $total_progress = 0; foreach ($root_tasks as $task) { $total_progress += $task->progress; } $avg_progress = round($total_progress / count($root_tasks)); $wpdb->update($projects_table, array('progress' => $avg_progress), array('id' => $project_id) ); } } } 4.3 REST API端点 // 注册REST API路由 add_action('rest_api_init', function() { // 项目相关端点 register_rest_route('pmg/v1', '/projects', array( array( 'methods' => 'GET', 'callback' => 'pmg_rest_get_projects', 'permission_callback' => function() { return current_user_can('read'); } ), array( 'methods' => 'POST', 'callback' => 'pmg_rest_create_project', 'permission_callback' => function() { return current_user_can('edit_posts'); } ) )); // 任务相关端点 register_rest_route('pmg/v1', '/projects/(?P<project_id>d+)/tasks', array( 'methods' => 'GET', 'callback' => 'pmg_rest_get_tasks', 'permission_callback' => function($request) { return pmg_check_project_access($request['project_id']); } )); register_rest_route('pmg/v1', '/tasks/(?P<task_id>d+)', array( array( 'methods' => 'PUT', 'callback' => 'pmg_rest_update_task', 'permission_callback' => function($request) { return pmg_check_task_access($request['task_id']); } ), array( 'methods' => 'DELETE', 'callback' => 'pmg_rest_delete_task', 'permission_callback' => function($request) { return current_user_can('delete_posts'); } ) )); }); // REST API回调函数示例 function pmg_rest_get_projects(WP_REST_Request $request) { $args = array( 'status' => $request->get_param('status') ?: 'active', 'user_id' => get_current_user_id(), 'page' => $request->get_param('page') ?: 1 ); $projects = PMG_Projects::get_projects($args); return new WP_REST_Response($projects, 200); } function pmg_rest_create_project(WP_REST_Request $request) { $data = $request->get_json_params(); $project_id = PMG_Projects::create_project($data); if ($project_id) { // 自动将创建者添加为项目管理员 PMG_Projects::add_project_member($project_id, get_current_user_id(), 'admin'); return new WP_REST_Response(array( 'id' => $project_id, 'message' => '项目创建成功' ), 201); } return new WP_REST_Response(array( 'error' => '项目创建失败' ), 500); } 5. 甘特图集成与可视化 5.1 甘特图库选择与集成 5.1.1 选择适合的甘特图库 DHTMLX Gantt:功能强大,商业使用需授权 Frappe Gantt:开源免费,轻量级 Gantt-elastic:基于SVG,响应式设计 自定义实现:完全控制,但开发成本高 5.2 使用Frappe Gantt实现 <!-- public/views/gantt-view.php --> <div class="pmg-gantt-container"> <div class="pmg-gantt-toolbar"> <button class="pmg-btn pmg-btn-zoom-in">放大</button> <button class="pmg-btn pmg-btn-zoom-out">缩小</button> <button class="pmg-btn pmg-btn-today">今天</button> <select class="pmg-select-view"> <option value="Day">日视图</option> <option value="Week">周视图</option> <option value="Month">月视图</option> </select> </div> <div id="pmg-gantt-chart"></div> </div> // public/js/gantt-chart.js (function($) { 'use strict'; class PMG_GanttChart { constructor(containerId, projectId) { this.container = document.getElementById(containerId); this.projectId = projectId; this.gantt = null; this.init(); } init() { // 加载Frappe Gantt库 this.loadGanttLibrary().then(() => { this.setupGantt(); this.loadProjectData(); this.bindEvents(); }); } loadGanttLibrary() { return new Promise((resolve) => { if (typeof Gantt !== 'undefined') { resolve(); return; } // 动态加载CSS和JS const cssLink = document.createElement('link'); cssLink.rel = 'stylesheet'; cssLink.href = PMG_Gantt.pluginUrl + 'assets/lib/frappe-gantt/frappe-gantt.css'; document.head.appendChild(cssLink); const script = document.createElement('script'); script.src = PMG_Gantt.pluginUrl + 'assets/lib/frappe-gantt/frappe-gantt.min.js'; script.onload = resolve; document.head.appendChild(script); }); } setupGantt() { this.gantt = new Gantt(this.container, [], { header_height: 50, column_width: 30, step: 24, view_modes: ['Day', 'Week', 'Month'], bar_height: 20, bar_corner_radius: 3, arrow_curve: 5, padding: 18, view_mode: 'Week', date_format: 'YYYY-MM-DD', custom_popup_html: null, on_click: (task) => this.onTaskClick(task), on_date_change: (task, start, end) => this.onDateChange(task, start, end), on_progress_change: (task, progress) => this.onProgressChange(task, progress), on_view_change: (mode) => this.onViewChange(mode) }); } loadProjectData() { $.ajax({ url: PMG_Gantt.restUrl + 'pmg/v1/projects/' + this.projectId + '/gantt-data', method: 'GET', beforeSend: (xhr) => { xhr.setRequestHeader('X-WP-Nonce', PMG_Gantt.nonce); }, success: (response) => { this.transformAndLoadData(response); }, error: (error) => { console.error('加载甘特图数据失败:', error); } }); } transformAndLoadData(data) { // 转换数据为甘特图所需格式 const ganttTasks = data.tasks.map(task => ({ id: task.id.toString(), name: task.title, start: task.start_date, end: task.end_date, progress: task.progress, dependencies: task.dependencies ? task.dependencies.split(',') : [], custom_class: task.priority + '-priority' })); this.gantt.refresh(ganttTasks); } onTaskClick(task) { // 打开任务详情模态框 this.openTaskModal(task.id); } onDateChange(task, start, end) { // 更新任务日期 $.ajax({ url: PMG_Gantt.restUrl + 'pmg/v1/tasks/' + task.id, method: 'PUT', beforeSend: (xhr) => { xhr.setRequestHeader('X-WP-Nonce', PMG_Gantt.nonce); }, data: JSON.stringify({ start_date: start, end_date: end }), contentType: 'application/json', success: () => { console.log('任务日期更新成功'); }, error: (error) => { console.error('更新失败:', error); // 恢复原始日期 this.loadProjectData(); } }); } onProgressChange(task, progress) { // 更新任务进度 $.ajax({ url: PMG_Gantt.restUrl + 'pmg/v1/tasks/' + task.id + '/progress', method: 'PUT', beforeSend: (xhr) => { xhr.setRequestHeader('X-WP-Nonce', PMG_Gantt.nonce); }, data: JSON.stringify({ progress: progress }), contentType: 'application/json', success: () => { console.log('任务进度更新成功'); } }); } bindEvents() { // 工具栏事件绑定 $('.pmg-btn-zoom-in').on('click', () => this.zoomIn()); $('.pmg-btn-zoom-out').on('click', () => this.zoomOut()); $('.pmg-btn-today').on('click', () => this.scrollToToday()); $('.pmg-select-view').on('change', (e) => this.changeView(e.target.value)); } zoomIn() { const currentWidth = this.gantt.options.column_width; this.gantt.change_view_mode({ ...this.gantt.options, column_width: Math.min(currentWidth + 10, 100) }); } zoomOut() { const currentWidth = this.gantt.options.column_width; this.gantt.change_view_mode({ ...this.gantt.options, column_width: Math.max(currentWidth - 10, 10) }); } scrollToToday() { const today = new Date(); this.gantt.scroll_to(today); } changeView(mode) { this.gantt.change_view_mode(mode); } openTaskModal(taskId) { // 实现任务详情模态框 console.log('打开任务详情:', taskId); } } // 初始化甘特图 $(document).ready(function() { if ($('#pmg-gantt-chart').length) { const projectId = $('#pmg-gantt-chart').data('project-id'); window.pmgGantt = new PMG_GanttChart('pmg-gantt-chart', projectId); } }); })(jQuery); 5.3 甘特图数据API端点 // 添加甘特图数据端点 add_action('rest_api_init', function() { register_rest_route('pmg/v1', '/projects/(?P<project_id>d+)/gantt-data', array( 'methods' => 'GET', 'callback' => 'pmg_rest_get_gantt_data', 'permission_callback' => function($request) { return pmg_check_project_access($request['project_id']); } )); }); function pmg_rest_get_gantt_data(WP_REST_Request $request) { $project_id = $request->get_param('project_id'); // 获取项目任务 $tasks = PMG_Tasks::get_project_tasks($project_id, true); // 格式化任务依赖关系 $formatted_tasks = array(); foreach ($tasks as $task) { $formatted_task = array( 'id' => $task->id, 'title' => $task->title, 'start_date' => $task->start_date, 'end_date' => $task->end_date, 'progress' => $task->progress, 'priority' => $task->priority, 'dependencies' => $task->dependencies ); $formatted_tasks[] = $formatted_task; } // 获取项目信息 $project = PMG_Projects::get_project($project_id); return new WP_REST_Response(array( 'project' => $project, 'tasks' => $formatted_tasks ), 200); } 5.4 甘特图样式定制 /* public/css/gantt-styles.css */ .pmg-gantt-container { background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; margin: 20px 0; } .pmg-gantt-toolbar { padding: 15px; background: #f8f9fa; border-bottom: 1px solid #e9ecef; display: flex; gap: 10px; align-items: center; } .pmg-btn { padding: 8px 16px; background: #007cba; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s; } .pmg-btn:hover { background: #005a87; } .pmg-select-view { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; background: white; } /* 甘特图任务样式 */ .gantt .bar { rx: 4; ry: 4; } .gantt .bar-wrapper { cursor: pointer; } .gantt .bar-progress { fill: #4CAF50; } /* 优先级颜色 */ .high-priority .bar { fill: #ff6b6b; } .medium-priority .bar { fill: #4d96ff; } .low-priority .bar { fill: #6bcf7f; } /* 里程碑样式 */ .milestone .bar { fill: #ffd166; width: 10px; rx: 10; ry: 10; } /* 依赖线样式 */ .gantt .arrow { stroke: #666; stroke-width: 2; fill: none; } 6. 用户权限与团队协作 6.1 用户角色系统 // includes/class-pmg-permissions.php class PMG_Permissions { // 定义项目角色 const ROLES = array( 'admin' => array( 'name' => '管理员', 'capabilities' => array( 'edit_project', 'delete_project', 'manage_members', 'create_tasks', 'edit_all_tasks', 'delete_tasks', 'assign_tasks' ) ), 'manager' => array( 'name' => '经理', 'capabilities' => array( 'edit_project', 'create_tasks', 'edit_all_tasks', 'assign_tasks' ) ), 'member' => array( 'name' => '成员', 'capabilities' => array( 'view_project', 'create_tasks', 'edit_own_tasks' ) ), 'viewer' => array( 'name' => '观察者', 'capabilities' => array( 'view_project' ) ) ); // 检查用户权限 public static function user_can($user_id, $project_id, $capability) { $user_role = self::get_user_role($user_id, $project_id); if (!$user_role) { return false; } // 管理员拥有所有权限 if ($user_role === 'admin') { return true; } // 检查角色权限 if (isset(self::ROLES[$user_role])) { return in_array($capability, self::ROLES[$user_role]['capabilities']); } return false; } // 获取用户在项目中的角色 public static function get_user_role($user_id, $project_id) { global $wpdb; $table = $wpdb->prefix . 'pmg_project_members'; $role = $wpdb->get_var($wpdb->prepare( "SELECT role FROM $table WHERE user_id = %d AND project_id = %d", $user_id, $project_id )); return $role ?: false; } // 添加项目成员 public static function add_project_member($project_id, $user_id, $role = 'member') { global $wpdb; $table = $wpdb->prefix . 'pmg_project_members'; // 检查是否已是成员 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table WHERE project_id = %d AND user_id = %d", $project_id, $user_id )); if ($existing) { return $wpdb->update($table, array('role' => $role), array('id' => $existing) ); } return $wpdb->insert($table, array( 'project_id' => $project_id, 'user_id' => $user_id, 'role' => $role )); } // 获取项目成员列表 public static function get_project_members($project_id) { global $wpdb; $members_table = $wpdb->prefix . 'pmg_project_members'; $users_table = $wpdb->users; return $wpdb->get_results($wpdb->prepare( "SELECT m.*, u.display_name, u.user_email FROM $members_table m LEFT JOIN $users_table u ON m.user_id = u.ID WHERE m.project_id = %d ORDER BY m.joined_at ASC", $project_id )); } } 6.2 团队协作功能 // includes/class-pmg-collaboration.php class PMG_Collaboration { // 添加任务评论 public static function add_task_comment($task_id, $user_id, $content) { global $wpdb; $table = $wpdb->prefix . 'pmg_task_comments'; $comment_id = $wpdb->insert($table, array( 'task_id' => $task_id, 'user_id' => $user_id, 'content' => wp_kses_post($content), 'created_at' => current_time('mysql') )); if ($comment_id) { // 发送通知 self::notify_task_comment($task_id, $user_id, $content); } return $comment_id; } // 获取任务评论 public static function get_task_comments($task_id) { global $wpdb; $comments_table = $wpdb->prefix . 'pmg_task_comments'; $users_table = $wpdb->users; return $wpdb->get_results($wpdb->prepare( "SELECT c.*, u.display_name, u.user_email FROM $comments_table c LEFT JOIN $users_table u ON c.user_id = u.ID WHERE c.task_id = %d ORDER BY c.created_at ASC", $task_id )); } // 添加文件附件 public static function add_task_attachment($task_id, $user_id, $file_data) { // 使用WordPress媒体库上传文件 require_once(ABSPATH . 'wp-admin/includes/file.php'); require_once(ABSPATH . 'wp-admin/includes/media.php'); require_once(ABSPATH . 'wp-admin/includes/image.php'); $upload = wp_handle_upload($file_data, array('test_form' => false)); if (isset($upload['error'])) { return new WP_Error('upload_error', $upload['error']); } // 创建附件记录 global $wpdb; $table = $wpdb->prefix . 'pmg_task_attachments'; $attachment_id = $wpdb->insert($table, array( 'task_id' => $task_id,
发表评论手把手教学:为你的网站集成智能合同模板与在线签署流程 引言:数字时代的企业合同管理变革 在当今数字化商业环境中,纸质合同的局限性日益凸显。传统合同签署流程不仅耗时耗力,还存在存储不便、安全性低、管理困难等问题。随着远程办公和电子商务的蓬勃发展,企业对高效、安全、便捷的在线合同签署解决方案的需求日益增长。 WordPress作为全球最流行的内容管理系统,为中小企业和自由职业者提供了强大的网站建设平台。通过代码二次开发,我们可以为WordPress网站集成智能合同模板与在线签署流程,将原本复杂的法律文件处理转化为简单、自动化的在线操作。 本教程将带领您一步步实现这一功能,无需昂贵的第三方服务,通过自主开发打造完全符合您业务需求的智能合同系统。 第一部分:准备工作与环境搭建 1.1 系统需求分析 在开始开发之前,我们需要明确系统的基本功能需求: 合同模板管理:创建、编辑、存储可重复使用的合同模板 变量替换系统:在模板中插入动态字段(如客户姓名、日期、金额等) 在线签署功能:支持多方电子签名,具备法律效力 合同状态跟踪:实时监控合同创建、发送、签署、完成等状态 安全存储与备份:加密存储已签署合同,防止篡改 通知系统:自动邮件通知合同相关方 1.2 开发环境配置 确保您的WordPress环境满足以下条件: WordPress 5.0或更高版本 PHP 7.4或更高版本(推荐8.0+) MySQL 5.6或更高版本 启用HTTPS(电子签名必需) 安装并激活代码编辑器插件(如Code Snippets) 1.3 创建自定义插件 为了避免主题更新导致代码丢失,我们将创建一个独立的插件: <?php /** * Plugin Name: 智能合同管理系统 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站添加智能合同模板与在线签署功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('SCM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SCM_PLUGIN_URL', plugin_dir_url(__FILE__)); define('SCM_VERSION', '1.0.0'); // 初始化插件 require_once SCM_PLUGIN_DIR . 'includes/class-contract-manager.php'; function scm_init() { $contract_manager = new Contract_Manager(); $contract_manager->init(); } add_action('plugins_loaded', 'scm_init'); 第二部分:数据库设计与合同模板系统 2.1 创建数据库表 我们需要创建几个核心数据库表来存储合同相关数据: // 在Contract_Manager类中添加数据库创建方法 public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 合同模板表 $templates_table = $wpdb->prefix . 'scm_templates'; $sql1 = "CREATE TABLE IF NOT EXISTS $templates_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL, content longtext NOT NULL, variables text, created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, status varchar(20) DEFAULT 'active', PRIMARY KEY (id) ) $charset_collate;"; // 合同实例表 $contracts_table = $wpdb->prefix . 'scm_contracts'; $sql2 = "CREATE TABLE IF NOT EXISTS $contracts_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, template_id mediumint(9) NOT NULL, contract_number varchar(100) NOT NULL, title varchar(200) NOT NULL, content longtext NOT NULL, parties text NOT NULL, variables_values text, status varchar(50) DEFAULT 'draft', created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, expires_at datetime, completed_at datetime, PRIMARY KEY (id), UNIQUE KEY contract_number (contract_number) ) $charset_collate;"; // 签署记录表 $signatures_table = $wpdb->prefix . 'scm_signatures'; $sql3 = "CREATE TABLE IF NOT EXISTS $signatures_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, contract_id mediumint(9) NOT NULL, party_email varchar(200) NOT NULL, party_name varchar(200) NOT NULL, signature_data text, signed_at datetime, ip_address varchar(45), user_agent text, verification_hash varchar(255), PRIMARY KEY (id), KEY contract_id (contract_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); } 2.2 合同模板编辑器 创建合同模板管理界面,允许用户通过可视化编辑器创建模板: // 添加模板编辑器短代码 public function add_template_editor_shortcode() { add_shortcode('contract_template_editor', array($this, 'render_template_editor')); } public function render_template_editor() { // 检查用户权限 if (!current_user_can('manage_options')) { return '<p>您没有权限访问此功能。</p>'; } ob_start(); ?> <div class="scm-template-editor"> <h2>智能合同模板编辑器</h2> <div class="scm-editor-container"> <div class="scm-sidebar"> <h3>可用变量</h3> <div class="variable-list"> <div class="variable-item" data-variable="{client_name}"> <span class="variable-label">客户姓名</span> <code>{client_name}</code> </div> <div class="variable-item" data-variable="{client_email}"> <span class="variable-label">客户邮箱</span> <code>{client_email}</code> </div> <div class="variable-item" data-variable="{contract_date}"> <span class="variable-label">合同日期</span> <code>{contract_date}</code> </div> <div class="variable-item" data-variable="{amount}"> <span class="variable-label">金额</span> <code>{amount}</code> </div> <div class="variable-item" data-variable="{service_description}"> <span class="variable-label">服务描述</span> <code>{service_description}</code> </div> <div class="variable-item" data-variable="{terms}"> <span class="variable-label">条款</span> <code>{terms}</code> </div> </div> <button type="button" class="button button-primary" id="add-custom-variable"> 添加自定义变量 </button> </div> <div class="scm-editor-main"> <div class="form-group"> <label for="template-title">模板标题</label> <input type="text" id="template-title" class="widefat" placeholder="输入模板标题"> </div> <div class="form-group"> <label for="template-content">模板内容</label> <?php // 使用WordPress编辑器 wp_editor('', 'template-content', array( 'textarea_name' => 'template_content', 'editor_height' => 400, 'media_buttons' => false, 'tinymce' => array( 'toolbar1' => 'formatselect,bold,italic,bullist,numlist,blockquote,alignleft,aligncenter,alignright,link,unlink,undo,redo' ) )); ?> </div> <div class="form-actions"> <button type="button" class="button button-primary" id="save-template"> 保存模板 </button> <button type="button" class="button" id="preview-template"> 预览合同 </button> </div> </div> </div> <div id="template-preview-modal" class="scm-modal" style="display:none;"> <div class="scm-modal-content"> <span class="scm-close-modal">×</span> <h3>合同预览</h3> <div id="preview-content"></div> </div> </div> </div> <style> .scm-template-editor { max-width: 1200px; margin: 20px auto; background: #fff; padding: 20px; border-radius: 5px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .scm-editor-container { display: flex; gap: 20px; margin-top: 20px; } .scm-sidebar { width: 300px; background: #f5f5f5; padding: 15px; border-radius: 5px; } .scm-editor-main { flex: 1; } .variable-list { margin-bottom: 20px; } .variable-item { background: #fff; padding: 10px; margin-bottom: 8px; border-radius: 4px; border-left: 4px solid #0073aa; cursor: pointer; transition: all 0.3s; } .variable-item:hover { background: #e3f2fd; transform: translateX(5px); } .variable-item code { display: block; margin-top: 5px; padding: 3px 6px; background: #f1f1f1; border-radius: 3px; font-size: 12px; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 5px; font-weight: bold; } .scm-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); z-index: 9999; display: flex; align-items: center; justify-content: center; } .scm-modal-content { background: #fff; width: 80%; max-width: 900px; max-height: 80vh; overflow-y: auto; padding: 20px; border-radius: 5px; position: relative; } .scm-close-modal { position: absolute; top: 15px; right: 20px; font-size: 24px; cursor: pointer; } </style> <script> jQuery(document).ready(function($) { // 插入变量到编辑器 $('.variable-item').click(function() { var variable = $(this).data('variable'); var editor = tinyMCE.get('template-content'); if (editor) { editor.insertContent(variable); } else { var textarea = $('#template-content'); var currentVal = textarea.val(); var cursorPos = textarea.prop('selectionStart'); var newVal = currentVal.substring(0, cursorPos) + variable + currentVal.substring(cursorPos); textarea.val(newVal); } }); // 保存模板 $('#save-template').click(function() { var title = $('#template-title').val(); var content = ''; if (tinyMCE.get('template-content')) { content = tinyMCE.get('template-content').getContent(); } else { content = $('#template-content').val(); } if (!title || !content) { alert('请填写模板标题和内容'); return; } // 提取变量 var variables = []; var regex = /{([^}]+)}/g; var match; while ((match = regex.exec(content)) !== null) { if (variables.indexOf(match[1]) === -1) { variables.push(match[1]); } } $.ajax({ url: '<?php echo admin_url("admin-ajax.php"); ?>', type: 'POST', data: { action: 'scm_save_template', title: title, content: content, variables: JSON.stringify(variables), nonce: '<?php echo wp_create_nonce("scm_save_template_nonce"); ?>' }, success: function(response) { if (response.success) { alert('模板保存成功!'); window.location.reload(); } else { alert('保存失败:' + response.data); } } }); }); // 预览合同 $('#preview-template').click(function() { var content = ''; if (tinyMCE.get('template-content')) { content = tinyMCE.get('template-content').getContent(); } else { content = $('#template-content').val(); } if (!content) { alert('请先输入模板内容'); return; } // 替换变量为示例值 var previewContent = content .replace(/{client_name}/g, '<span class="variable-example">张三</span>') .replace(/{client_email}/g, '<span class="variable-example">zhangsan@example.com</span>') .replace(/{contract_date}/g, '<span class="variable-example">' + new Date().toLocaleDateString() + '</span>') .replace(/{amount}/g, '<span class="variable-example">¥5,000.00</span>') .replace(/{service_description}/g, '<span class="variable-example">网站设计与开发服务</span>') .replace(/{terms}/g, '<span class="variable-example">30天内完成所有工作</span>'); $('#preview-content').html(previewContent); $('#template-preview-modal').show(); }); // 关闭模态框 $('.scm-close-modal').click(function() { $('#template-preview-modal').hide(); }); // 点击模态框外部关闭 $(window).click(function(event) { if ($(event.target).hasClass('scm-modal')) { $('.scm-modal').hide(); } }); }); </script> <?php return ob_get_clean(); } 第三部分:合同创建与变量替换系统 3.1 合同创建界面 创建用户友好的合同创建界面,允许用户选择模板并填写变量值: // 添加合同创建短代码 public function add_contract_creator_shortcode() { add_shortcode('create_contract', array($this, 'render_contract_creator')); } public function render_contract_creator() { global $wpdb; // 获取可用模板 $templates_table = $wpdb->prefix . 'scm_templates'; $templates = $wpdb->get_results( "SELECT id, title FROM $templates_table WHERE status = 'active' ORDER BY title" ); ob_start(); ?> <div class="scm-contract-creator"> <h2>创建新合同</h2> <div class="scm-form-container"> <div class="form-step" id="step1"> <h3>步骤1:选择合同模板</h3> <div class="template-grid"> <?php if ($templates): ?> <?php foreach ($templates as $template): ?> <div class="template-card" data-template-id="<?php echo $template->id; ?>"> <h4><?php echo esc_html($template->title); ?></h4> <button type="button" class="button select-template" data-template-id="<?php echo $template->id; ?>"> 选择此模板 </button> </div> <?php endforeach; ?> <?php else: ?> <p>暂无可用模板。请先创建合同模板。</p> <?php endif; ?> </div> </div> <div class="form-step" id="step2" style="display:none;"> <h3>步骤2:填写合同信息</h3> <form id="contract-details-form"> <div id="dynamic-fields-container"> <!-- 动态字段将在这里生成 --> </div> <div class="form-section"> <h4>签署方信息</h4> <div class="party-section"> <h5>甲方(我方)</h5> <div class="form-row"> <div class="form-group"> <label>公司/个人名称</label> <input type="text" name="party_a_name" required value="<?php echo get_bloginfo('name'); ?>"> </div> <div class="form-group"> <label>电子邮箱</label> <input type="email" name="party_a_email" required value="<?php echo get_option('admin_email'); ?>"> </div> </div> </div> <div class="party-section"> <h5>乙方(客户)</h5> <div class="form-row"> <div class="form-group"> <label>客户姓名</label> <input type="text" name="party_b_name" required> </div> <div class="form-group"> </div> <div class="form-group"> <label>客户邮箱</label> <input type="email" name="party_b_email" required> </div> </div> <button type="button" class="button button-secondary" id="add-party"> 添加更多签署方 </button> </div> </div> <div class="form-actions"> <button type="button" class="button" id="back-to-step1"> 返回上一步 </button> <button type="submit" class="button button-primary"> 生成合同并发送 </button> </div> </form> </div> <div class="form-step" id="step3" style="display:none;"> <h3>合同创建成功!</h3> <div class="success-message"> <p>合同已成功创建并发送给相关签署方。</p> <p>合同编号:<strong id="contract-number"></strong></p> <p>您可以在 <a href="#" id="contract-management-link">合同管理页面</a> 跟踪签署状态。</p> </div> </div> </div> </div> <style> .scm-contract-creator { max-width: 1000px; margin: 20px auto; background: #fff; padding: 30px; border-radius: 8px; box-shadow: 0 4px 15px rgba(0,0,0,0.1); } .template-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); gap: 20px; margin-top: 20px; } .template-card { background: #f8f9fa; border: 2px solid #e9ecef; border-radius: 8px; padding: 20px; text-align: center; transition: all 0.3s ease; cursor: pointer; } .template-card:hover { border-color: #0073aa; transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,115,170,0.1); } .template-card h4 { margin-top: 0; color: #333; min-height: 60px; display: flex; align-items: center; justify-content: center; } .form-step { animation: fadeIn 0.5s ease; } @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .form-section { background: #f8f9fa; padding: 20px; border-radius: 6px; margin: 25px 0; } .form-section h4 { margin-top: 0; color: #0073aa; border-bottom: 2px solid #dee2e6; padding-bottom: 10px; } .party-section { margin-bottom: 25px; } .party-section h5 { color: #495057; margin-bottom: 15px; font-size: 16px; } .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } .form-group { margin-bottom: 15px; } .form-group label { display: block; margin-bottom: 5px; font-weight: 600; color: #495057; } .form-group input, .form-group textarea, .form-group select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; font-size: 14px; transition: border-color 0.3s; } .form-group input:focus, .form-group textarea:focus, .form-group select:focus { border-color: #0073aa; outline: none; box-shadow: 0 0 0 3px rgba(0,115,170,0.1); } .form-actions { display: flex; justify-content: space-between; margin-top: 30px; padding-top: 20px; border-top: 1px solid #dee2e6; } .success-message { background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 20px; border-radius: 6px; text-align: center; } .success-message p { margin: 10px 0; font-size: 16px; } .dynamic-field-group { background: #fff; border: 1px solid #dee2e6; border-radius: 6px; padding: 15px; margin-bottom: 15px; } .dynamic-field-group label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .dynamic-field-group .field-description { font-size: 12px; color: #6c757d; margin-top: 5px; font-style: italic; } </style> <script> jQuery(document).ready(function($) { var selectedTemplateId = null; var templateVariables = []; // 选择模板 $('.select-template').click(function() { selectedTemplateId = $(this).data('template-id'); // 获取模板变量 $.ajax({ url: '<?php echo admin_url("admin-ajax.php"); ?>', type: 'POST', data: { action: 'scm_get_template_variables', template_id: selectedTemplateId, nonce: '<?php echo wp_create_nonce("scm_get_variables_nonce"); ?>' }, success: function(response) { if (response.success) { templateVariables = response.data.variables; renderDynamicFields(templateVariables); $('#step1').hide(); $('#step2').show(); } else { alert('获取模板信息失败'); } } }); }); // 渲染动态字段 function renderDynamicFields(variables) { var container = $('#dynamic-fields-container'); container.empty(); if (variables.length === 0) { container.html('<p>此模板没有需要填写的变量。</p>'); return; } container.html('<h4>合同变量填写</h4>'); variables.forEach(function(variable) { var fieldHtml = ` <div class="dynamic-field-group"> <label for="var_${variable}">${formatVariableName(variable)}</label> `; // 根据变量名猜测字段类型 if (variable.includes('date') || variable.includes('Date')) { fieldHtml += ` <input type="date" id="var_${variable}" name="variables[${variable}]" required> <div class="field-description">请选择日期</div> `; } else if (variable.includes('amount') || variable.includes('price') || variable.includes('金额') || variable.includes('价格')) { fieldHtml += ` <input type="number" id="var_${variable}" name="variables[${variable}]" step="0.01" min="0" required> <div class="field-description">请输入金额(单位:元)</div> `; } else if (variable.includes('description') || variable.includes('content') || variable.includes('描述') || variable.includes('内容')) { fieldHtml += ` <textarea id="var_${variable}" name="variables[${variable}]" rows="3" required></textarea> <div class="field-description">请详细描述</div> `; } else if (variable.includes('email') || variable.includes('邮箱')) { fieldHtml += ` <input type="email" id="var_${variable}" name="variables[${variable}]" required> <div class="field-description">请输入有效的邮箱地址</div> `; } else { fieldHtml += ` <input type="text" id="var_${variable}" name="variables[${variable}]" required> <div class="field-description">请填写${formatVariableName(variable)}</div> `; } fieldHtml += '</div>'; container.append(fieldHtml); }); } // 格式化变量名显示 function formatVariableName(variable) { // 移除花括号 var name = variable.replace(/[{}]/g, ''); // 将下划线或连字符转换为空格 name = name.replace(/[_-]/g, ' '); // 首字母大写 return name.replace(/bw/g, function(l) { return l.toUpperCase(); }); } // 返回上一步 $('#back-to-step1').click(function() { $('#step2').hide(); $('#step1').show(); }); // 添加更多签署方 $('#add-party').click(function() { var partyCount = $('.party-section').length - 2; // 减去甲方和乙方 if (partyCount >= 3) { alert('最多支持5个签署方'); return; } var partyLetter = String.fromCharCode(67 + partyCount); // C, D, E... var partyHtml = ` <div class="party-section additional-party"> <h5>${partyLetter}方</h5> <div class="form-row"> <div class="form-group"> <label>签署方名称</label> <input type="text" name="party_${partyLetter.toLowerCase()}_name" required> </div> <div class="form-group"> <label>签署方邮箱</label> <input type="email" name="party_${partyLetter.toLowerCase()}_email" required> </div> </div> <button type="button" class="button button-small remove-party"> 移除此签署方 </button> </div> `; $(this).before(partyHtml); }); // 移除签署方 $(document).on('click', '.remove-party', function() { $(this).closest('.party-section').remove(); }); // 提交合同表单 $('#contract-details-form').submit(function(e) { e.preventDefault(); // 收集表单数据 var formData = $(this).serializeArray(); var variables = {}; // 提取变量数据 formData.forEach(function(item) { if (item.name.startsWith('variables[')) { var varName = item.name.match(/[(.*?)]/)[1]; variables[varName] = item.value; } }); // 收集签署方信息 var parties = []; $('input[name^="party_"]').each(function() { var name = $(this).attr('name'); var value = $(this).val(); if (name.endsWith('_name')) { var partyKey = name.replace('_name', ''); var partyIndex = parties.findIndex(p => p.key === partyKey); if (partyIndex === -1) { parties.push({ key: partyKey, name: value, email: '' }); } else { parties[partyIndex].name = value; } } else if (name.endsWith('_email')) { var partyKey = name.replace('_email', ''); var partyIndex = parties.findIndex(p => p.key === partyKey); if (partyIndex === -1) { parties.push({ key: partyKey, name: '', email: value }); } else { parties[partyIndex].email = value; } } }); // 发送AJAX请求创建合同 $.ajax({ url: '<?php echo admin_url("admin-ajax.php"); ?>', type: 'POST', data: { action: 'scm_create_contract', template_id: selectedTemplateId, variables: JSON.stringify(variables), parties: JSON.stringify(parties), nonce: '<?php echo wp_create_nonce("scm_create_contract_nonce"); ?>' }, beforeSend: function() { $('.form-actions button').prop('disabled', true).text('处理中...'); }, success: function(response) { if (response.success) { $('#contract-number').text(response.data.contract_number); $('#contract-management-link').attr('href', response.data.management_url); $('#step2').hide(); $('#step3').show(); } else { alert('创建合同失败:' + response.data); $('.form-actions button').prop('disabled', false).text('生成合同并发送'); } }, error: function() { alert('网络错误,请重试'); $('.form-actions button').prop('disabled', false).text('生成合同并发送'); } }); }); }); </script> <?php return ob_get_clean(); } ### 3.2 变量替换与合同生成逻辑 实现合同内容的变量替换和最终合同生成: // 处理AJAX请求:创建合同public function ajax_create_contract() { check_ajax_referer('scm_create_contract_nonce', 'nonce'); if (!current_user_can('edit_posts')) { wp_die('权限不足'); } global $wpdb; $template_id = intval($_POST['template_id']); $variables = json_decode(stripslashes($_POST['variables']), true); $parties = json_decode(stripslashes($_POST['parties']), true); // 获取模板内容 $templates_table = $wpdb->prefix . 'scm_templates'; $template = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $templates_table WHERE id = %d", $template_id )); if (!$template) { wp_send_json_error('模板不存在'); } // 生成合同编号 $contract_number = 'CONTRACT-' . date('Ymd') . '-' . strtoupper(wp_generate_password(6, false)); // 替换变量 $contract_content = $template->content; foreach ($variables as $key => $value) { $placeholder = '{' . $key . '}'; $contract_content = str_replace($placeholder, $value, $contract_content); } // 添加标准条款 $standard_terms = $this->get_standard_terms(); $contract_content .= $standard_terms; // 保存合同到数据库 $contracts_table = $wpdb->prefix . 'scm_contracts'; $wpdb->insert( $contracts_table, array( 'template_id' => $template_id, 'contract_number' => $contract_number, 'title' => $template->title, 'content' => $contract_content, 'parties' => json_encode($parties, JSON_UNESCAPED_UNICODE), 'variables_values' => json_encode($variables, JSON_UNESCAPED_UNICODE), 'status' => 'pending', 'created_by' => get_current_user_id(), 'expires_at' => date('Y-m-d H:i:s', strtotime('+30 days')) ), array('%d', '%s', '%s', '%s', '%s', '%s', '%s', '%d', '%s') ); $contract_id = $wpdb->insert_id; // 创建签署记录 $signatures_table = $wpdb->prefix . 'scm_signatures'; foreach ($parties as $party) { $wpdb->insert( $signatures_table, array( 'contract_id' => $contract_id, 'party_email' => $party['email'], 'party_name' => $party['name'], 'verification_hash' => wp_hash($contract_id . $party['email'] . time()) ), array('%d', '%s', '%s', '%s') ); } // 发送签署邀请邮件 $this->send_signature_invitations($contract_id); // 返回成功响应 wp_send_json_success(array( 'contract_number' => $contract_number, 'contract_id' => $contract_id, 'management_url' => home_url('/contract-management/') )); } // 获取标准条款private function get_standard_terms() { return ' <div class="standard-terms"> <h3>标准条款与条件</h3> <ol> <li><strong>电子签名有效性</strong>:本文件使用电子签名,根据《中华人民共和国电子签名法》规定,可靠的电子签名与手写签名或者盖章具有同等的法律效力。</li> <li><strong>合同生效</strong>:本合同自所有签署方完成电子签名之日起生效。</li> <li><strong>合同修改</strong>:任何对本合同的修改必须以书面形式(包括电子形式)进行,并经所有签署方同意。</li> <li><strong>争议解决</strong>:因本合同引起的或与本合同有关的任何争议,双方应友好协商解决;协商不成的,任何一方均有权向合同签订地有管辖权的人民法院提起诉讼。</li> <li><strong>完整性</strong>:本合同构成双方就本合同标的达成的完整协议,取代所有先前口头或书面的沟通、陈述或协议。</li> </ol> <div class="signature-disclaimer"> <p><
发表评论WordPress插件开发教程:实现网站自动化营销漏斗与客户旅程追踪 引言:WordPress插件开发的无限可能 在当今数字化营销时代,网站不仅仅是企业的线上名片,更是潜在客户转化的重要渠道。WordPress作为全球最受欢迎的内容管理系统,其强大的可扩展性为开发者提供了无限可能。通过自定义插件开发,我们可以将WordPress从一个简单的博客平台转变为功能强大的营销自动化工具。 本教程将深入探讨如何通过WordPress插件开发,实现网站自动化营销漏斗与客户旅程追踪功能。我们将从基础概念讲起,逐步构建一个完整的营销自动化插件,帮助您更好地理解客户行为,优化转化路径,并最终提升业务成果。 第一部分:理解营销漏斗与客户旅程 1.1 营销漏斗的基本概念 营销漏斗是描述潜在客户从认知到购买决策过程的模型。传统营销漏斗通常包括以下阶段: 认知阶段:用户首次接触品牌 兴趣阶段:用户对产品或服务产生兴趣 考虑阶段:用户评估不同选项 意向阶段:用户表现出购买意愿 购买阶段:用户完成交易 忠诚阶段:用户成为忠实客户并推荐他人 1.2 客户旅程追踪的重要性 客户旅程追踪是指记录和分析用户与品牌互动的全过程。通过追踪客户旅程,企业可以: 识别转化路径中的瓶颈 个性化用户体验 优化营销策略 提高客户留存率 增加客户生命周期价值 1.3 WordPress在营销自动化中的优势 WordPress作为开发平台具有以下优势: 庞大的用户基础和成熟的生态系统 灵活的插件架构和丰富的API 开源特性,允许深度定制 与各种第三方服务的良好兼容性 强大的社区支持和丰富的学习资源 第二部分:WordPress插件开发基础 2.1 插件开发环境搭建 在开始开发之前,我们需要准备以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel 代码编辑器:VS Code、PHPStorm或Sublime Text WordPress安装:最新版本的WordPress 调试工具:安装Query Monitor、Debug Bar等调试插件 2.2 创建第一个WordPress插件 每个WordPress插件至少需要一个主文件,通常以插件名称命名。以下是创建基础插件结构的步骤: 在wp-content/plugins/目录下创建新文件夹,命名为marketing-automation-funnel 在该文件夹中创建主文件marketing-automation-funnel.php 添加插件头部信息: <?php /** * Plugin Name: 营销自动化漏斗与客户旅程追踪 * Plugin URI: https://yourwebsite.com/marketing-automation-funnel * Description: 实现网站自动化营销漏斗与客户旅程追踪功能 * Version: 1.0.0 * Author: 您的姓名 * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: marketing-automation-funnel */ 2.3 WordPress插件架构基础 了解WordPress插件的基本架构是开发的关键: 钩子系统:动作钩子(Actions)和过滤器钩子(Filters) 短代码系统:允许在文章和页面中嵌入动态内容 自定义数据库表:存储插件特定数据 管理界面:为插件创建设置页面 AJAX处理:实现前端与后端的异步通信 第三部分:构建客户旅程追踪系统 3.1 设计数据存储结构 客户旅程数据需要专门的数据库表来存储。我们将创建两个主要表: 客户表:存储客户基本信息 互动事件表:记录客户的每一次互动 以下是创建数据库表的代码示例: // 在插件激活时创建数据库表 register_activation_hook(__FILE__, 'maf_create_tables'); function maf_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 客户表 $table_name_customers = $wpdb->prefix . 'maf_customers'; $sql_customers = "CREATE TABLE IF NOT EXISTS $table_name_customers ( id INT(11) NOT NULL AUTO_INCREMENT, email VARCHAR(100) NOT NULL, first_name VARCHAR(50), last_name VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_seen DATETIME, funnel_stage VARCHAR(50) DEFAULT 'awareness', PRIMARY KEY (id), UNIQUE KEY email (email) ) $charset_collate;"; // 互动事件表 $table_name_events = $wpdb->prefix . 'maf_events'; $sql_events = "CREATE TABLE IF NOT EXISTS $table_name_events ( id INT(11) NOT NULL AUTO_INCREMENT, customer_id INT(11) NOT NULL, event_type VARCHAR(50) NOT NULL, event_data TEXT, page_url VARCHAR(500), referrer VARCHAR(500), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY customer_id (customer_id), KEY event_type (event_type) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_customers); dbDelta($sql_events); } 3.2 实现客户识别与追踪 客户识别是旅程追踪的基础。我们将通过以下方式识别客户: Cookie追踪:为匿名用户设置唯一标识符 用户登录识别:对已登录用户直接识别 电子邮件识别:通过表单提交获取电子邮件 // 设置客户Cookie function maf_set_customer_cookie() { if (!isset($_COOKIE['maf_customer_id'])) { $customer_id = uniqid('maf_', true); setcookie('maf_customer_id', $customer_id, time() + (365 * 24 * 60 * 60), '/'); } } add_action('init', 'maf_set_customer_cookie'); // 识别当前客户 function maf_identify_customer() { global $wpdb; $customer_id = null; // 检查是否已登录 if (is_user_logged_in()) { $user_id = get_current_user_id(); $user_info = get_userdata($user_id); // 检查用户是否已存在于客户表中 $table_name = $wpdb->prefix . 'maf_customers'; $existing_customer = $wpdb->get_row( $wpdb->prepare( "SELECT id FROM $table_name WHERE email = %s", $user_info->user_email ) ); if ($existing_customer) { $customer_id = $existing_customer->id; } else { // 创建新客户记录 $wpdb->insert( $table_name, array( 'email' => $user_info->user_email, 'first_name' => $user_info->first_name, 'last_name' => $user_info->last_name, 'created_at' => current_time('mysql') ) ); $customer_id = $wpdb->insert_id; } } // 检查Cookie elseif (isset($_COOKIE['maf_customer_id'])) { $cookie_id = sanitize_text_field($_COOKIE['maf_customer_id']); // 查找基于Cookie的客户记录 $table_name = $wpdb->prefix . 'maf_customers'; $existing_customer = $wpdb->get_row( $wpdb->prepare( "SELECT id FROM $table_name WHERE email = %s", $cookie_id . '@anonymous.com' ) ); if ($existing_customer) { $customer_id = $existing_customer->id; } } return $customer_id; } 3.3 记录客户互动事件 客户与网站的每一次互动都应该被记录: // 记录客户事件 function maf_track_event($event_type, $event_data = array()) { global $wpdb; $customer_id = maf_identify_customer(); if (!$customer_id) { // 为匿名用户创建临时记录 $table_name = $wpdb->prefix . 'maf_customers'; $cookie_id = isset($_COOKIE['maf_customer_id']) ? sanitize_text_field($_COOKIE['maf_customer_id']) : uniqid('maf_', true); $wpdb->insert( $table_name, array( 'email' => $cookie_id . '@anonymous.com', 'created_at' => current_time('mysql'), 'last_seen' => current_time('mysql') ) ); $customer_id = $wpdb->insert_id; } // 更新客户最后访问时间 $wpdb->update( $wpdb->prefix . 'maf_customers', array('last_seen' => current_time('mysql')), array('id' => $customer_id) ); // 记录事件 $table_name = $wpdb->prefix . 'maf_events'; $wpdb->insert( $table_name, array( 'customer_id' => $customer_id, 'event_type' => sanitize_text_field($event_type), 'event_data' => maybe_serialize($event_data), 'page_url' => isset($_SERVER['REQUEST_URI']) ? esc_url_raw($_SERVER['REQUEST_URI']) : '', 'referrer' => isset($_SERVER['HTTP_REFERER']) ? esc_url_raw($_SERVER['HTTP_REFERER']) : '', 'created_at' => current_time('mysql') ) ); return $wpdb->insert_id; } // 自动追踪页面访问 function maf_track_page_view() { if (is_admin()) { return; } $post_id = get_the_ID(); $post_type = get_post_type(); $event_data = array( 'post_id' => $post_id, 'post_title' => get_the_title(), 'post_type' => $post_type ); maf_track_event('page_view', $event_data); } add_action('wp', 'maf_track_page_view'); 第四部分:构建自动化营销漏斗 4.1 定义漏斗阶段与规则 营销漏斗需要明确的阶段定义和转换规则: // 定义漏斗阶段 function maf_get_funnel_stages() { return array( 'awareness' => array( 'name' => '认知阶段', 'description' => '用户首次接触品牌', 'next_stages' => array('interest'), 'triggers' => array('page_view', 'social_click') ), 'interest' => array( 'name' => '兴趣阶段', 'description' => '用户对产品或服务产生兴趣', 'next_stages' => array('consideration'), 'triggers' => array('form_submission', 'content_download') ), 'consideration' => array( 'name' => '考虑阶段', 'description' => '用户评估不同选项', 'next_stages' => array('intent'), 'triggers' => array('product_view', 'pricing_view') ), 'intent' => array( 'name' => '意向阶段', 'description' => '用户表现出购买意愿', 'next_stages' => array('purchase'), 'triggers' => array('cart_add', 'demo_request') ), 'purchase' => array( 'name' => '购买阶段', 'description' => '用户完成交易', 'next_stages' => array('loyalty'), 'triggers' => array('order_complete') ), 'loyalty' => array( 'name' => '忠诚阶段', 'description' => '用户成为忠实客户并推荐他人', 'next_stages' => array(), 'triggers' => array('repeat_purchase', 'referral') ) ); } // 检查并更新客户漏斗阶段 function maf_update_customer_funnel_stage($customer_id, $event_type) { global $wpdb; $funnel_stages = maf_get_funnel_stages(); $table_name = $wpdb->prefix . 'maf_customers'; // 获取客户当前阶段 $current_stage = $wpdb->get_var( $wpdb->prepare( "SELECT funnel_stage FROM $table_name WHERE id = %d", $customer_id ) ); // 检查事件是否触发阶段转换 foreach ($funnel_stages as $stage_id => $stage) { if ($stage_id === $current_stage && in_array($event_type, $stage['triggers'])) { // 移动到下一个阶段 if (!empty($stage['next_stages'])) { $next_stage = $stage['next_stages'][0]; // 简单实现:取第一个下一个阶段 $wpdb->update( $table_name, array('funnel_stage' => $next_stage), array('id' => $customer_id) ); // 记录阶段转换事件 maf_track_event('funnel_stage_change', array( 'from_stage' => $current_stage, 'to_stage' => $next_stage, 'trigger_event' => $event_type )); // 触发阶段转换动作 do_action('maf_funnel_stage_changed', $customer_id, $current_stage, $next_stage); } break; } } } 4.2 实现自动化营销动作 根据客户所在的漏斗阶段,触发相应的营销动作: // 根据漏斗阶段执行自动化动作 function maf_execute_funnel_actions($customer_id, $from_stage, $to_stage) { $actions = array(); switch ($to_stage) { case 'interest': $actions[] = array( 'type' => 'email', 'action' => 'send_welcome_series', 'delay' => 0 ); $actions[] = array( 'type' => 'internal', 'action' => 'tag_customer', 'tag' => 'interested' ); break; case 'consideration': $actions[] = array( 'type' => 'email', 'action' => 'send_case_studies', 'delay' => 1 // 1天后发送 ); $actions[] = array( 'type' => 'internal', 'action' => 'tag_customer', 'tag' => 'considering' ); break; case 'intent': $actions[] = array( 'type' => 'email', 'action' => 'send_demo_offer', 'delay' => 0 ); $actions[] = array( 'type' => 'task', 'action' => 'notify_sales_team', 'delay' => 0 ); break; case 'purchase': $actions[] = array( 'type' => 'email', 'action' => 'send_thank_you', 'delay' => 0 ); $actions[] = array( 'type' => 'email', 'action' => 'send_upsell_offer', 'delay' => 7 // 7天后发送 ); break; } // 执行动作 foreach ($actions as $action) { if ($action['delay'] > 0) { // 计划延迟执行 wp_schedule_single_event( time() + ($action['delay'] * DAY_IN_SECONDS), 'maf_scheduled_action', array($customer_id, $action) ); } else { // 立即执行 maf_execute_single_action($customer_id, $action); } } } add_action('maf_funnel_stage_changed', 'maf_execute_funnel_actions', 10, 3); // 执行单个动作 function maf_execute_single_action($customer_id, $action) { global $wpdb; $table_name = $wpdb->prefix . 'maf_customers'; $customer = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $customer_id ) ); if (!$customer) { return; } switch ($action['type']) { case 'email': maf_send_automated_email($customer, $action['action']); break; case 'internal': if ($action['action'] === 'tag_customer') { // 为客户添加标签 $tags = get_user_meta($customer_id, 'maf_tags', true); if (!is_array($tags)) { $tags = array(); } if (!in_array($action['tag'], $tags)) { $tags[] = $action['tag']; update_user_meta($customer_id, 'maf_tags', $tags); } } break; case 'task': if ($action['action'] === 'notify_sales_team') { // 通知销售团队 $admin_email = get_option('admin_email'); $subject = '新销售线索:客户进入意向阶段'; $message = "客户 {$customer->email} 已进入意向阶段,请及时跟进。"; wp_mail($admin_email, $subject, $message); } break; } // 记录动作执行 maf_track_event('automation_action', array( 'action_type' => $action['type'], 'action_name' => $action['action'], 'customer_id' => $customer_id )); } 第五部分:创建管理界面与 第五部分:创建管理界面与数据分析面板 5.1 构建插件管理菜单 一个直观的管理界面对于营销自动化插件至关重要。我们将创建多级管理菜单,让用户可以轻松访问所有功能: // 添加管理菜单 function maf_add_admin_menu() { // 主菜单 add_menu_page( '营销自动化漏斗', '营销自动化', 'manage_options', 'marketing-automation-funnel', 'maf_dashboard_page', 'dashicons-chart-line', 30 ); // 子菜单项 add_submenu_page( 'marketing-automation-funnel', '仪表板', '仪表板', 'manage_options', 'marketing-automation-funnel', 'maf_dashboard_page' ); add_submenu_page( 'marketing-automation-funnel', '客户旅程', '客户旅程', 'manage_options', 'maf-customer-journeys', 'maf_customer_journeys_page' ); add_submenu_page( 'marketing-automation-funnel', '营销漏斗', '营销漏斗', 'manage_options', 'maf-marketing-funnel', 'maf_marketing_funnel_page' ); add_submenu_page( 'marketing-automation-funnel', '自动化规则', '自动化规则', 'manage_options', 'maf-automation-rules', 'maf_automation_rules_page' ); add_submenu_page( 'marketing-automation-funnel', '设置', '设置', 'manage_options', 'maf-settings', 'maf_settings_page' ); } add_action('admin_menu', 'maf_add_admin_menu'); // 仪表板页面 function maf_dashboard_page() { if (!current_user_can('manage_options')) { wp_die('您没有权限访问此页面'); } ?> <div class="wrap maf-dashboard"> <h1 class="wp-heading-inline">营销自动化仪表板</h1> <div class="maf-stats-container"> <div class="maf-stat-card"> <h3>总客户数</h3> <div class="maf-stat-number"><?php echo maf_get_total_customers(); ?></div> <div class="maf-stat-trend">+12% 较上月</div> </div> <div class="maf-stat-card"> <h3>今日互动</h3> <div class="maf-stat-number"><?php echo maf_get_today_interactions(); ?></div> <div class="maf-stat-trend">+5% 较昨日</div> </div> <div class="maf-stat-card"> <h3>转化率</h3> <div class="maf-stat-number"><?php echo maf_get_conversion_rate(); ?>%</div> <div class="maf-stat-trend">+2.3% 较上周</div> </div> <div class="maf-stat-card"> <h3>平均停留阶段</h3> <div class="maf-stat-number"><?php echo maf_get_avg_stage_duration(); ?>天</div> <div class="maf-stat-trend">-1.2天 较上月</div> </div> </div> <div class="maf-charts-container"> <div class="maf-chart-card"> <h3>漏斗阶段分布</h3> <div class="maf-chart" id="funnel-stage-chart"></div> </div> <div class="maf-chart-card"> <h3>客户增长趋势</h3> <div class="maf-chart" id="customer-growth-chart"></div> </div> </div> <div class="maf-recent-activity"> <h3>最近活动</h3> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>时间</th> <th>客户</th> <th>事件</th> <th>页面</th> <th>阶段变化</th> </tr> </thead> <tbody> <?php echo maf_get_recent_activities_html(); ?> </tbody> </table> </div> </div> <script> // 使用Chart.js渲染图表 jQuery(document).ready(function($) { // 漏斗阶段分布图 var funnelCtx = document.getElementById('funnel-stage-chart').getContext('2d'); var funnelChart = new Chart(funnelCtx, { type: 'doughnut', data: { labels: <?php echo json_encode(maf_get_funnel_stage_labels()); ?>, datasets: [{ data: <?php echo json_encode(maf_get_funnel_stage_counts()); ?>, backgroundColor: [ '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40' ] }] }, options: { responsive: true, maintainAspectRatio: false } }); // 客户增长趋势图 var growthCtx = document.getElementById('customer-growth-chart').getContext('2d'); var growthChart = new Chart(growthCtx, { type: 'line', data: { labels: <?php echo json_encode(maf_get_last_30_days_labels()); ?>, datasets: [{ label: '新客户', data: <?php echo json_encode(maf_get_new_customers_last_30_days()); ?>, borderColor: '#36A2EB', fill: false }, { label: '总客户', data: <?php echo json_encode(maf_get_total_customers_last_30_days()); ?>, borderColor: '#FF6384', fill: false }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } }); }); </script> <?php } 5.2 客户旅程可视化界面 创建详细的客户旅程查看界面,让营销人员能够深入了解每个客户的行为路径: // 客户旅程页面 function maf_customer_journeys_page() { if (!current_user_can('manage_options')) { wp_die('您没有权限访问此页面'); } global $wpdb; // 分页参数 $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $offset = ($current_page - 1) * $per_page; // 搜索功能 $search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : ''; $where_clause = ''; if (!empty($search)) { $where_clause = $wpdb->prepare( " WHERE email LIKE %s OR first_name LIKE %s OR last_name LIKE %s", '%' . $wpdb->esc_like($search) . '%', '%' . $wpdb->esc_like($search) . '%', '%' . $wpdb->esc_like($search) . '%' ); } // 获取客户总数 $table_name = $wpdb->prefix . 'maf_customers'; $total_customers = $wpdb->get_var("SELECT COUNT(*) FROM $table_name $where_clause"); $total_pages = ceil($total_customers / $per_page); // 获取客户列表 $customers = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name $where_clause ORDER BY last_seen DESC LIMIT %d OFFSET %d", $per_page, $offset ) ); ?> <div class="wrap"> <h1 class="wp-heading-inline">客户旅程</h1> <form method="get" class="search-form"> <input type="hidden" name="page" value="maf-customer-journeys"> <p class="search-box"> <label class="screen-reader-text" for="customer-search">搜索客户:</label> <input type="search" id="customer-search" name="s" value="<?php echo esc_attr($search); ?>"> <input type="submit" id="search-submit" class="button" value="搜索客户"> </p> </form> <div class="tablenav top"> <div class="tablenav-pages"> <span class="displaying-num"><?php echo $total_customers; ?> 个客户</span> <?php if ($total_pages > 1): ?> <span class="pagination-links"> <?php echo paginate_links(array( 'base' => add_query_arg('paged', '%#%'), 'format' => '', 'prev_text' => '«', 'next_text' => '»', 'total' => $total_pages, 'current' => $current_page )); ?> </span> <?php endif; ?> </div> </div> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>ID</th> <th>邮箱</th> <th>姓名</th> <th>当前阶段</th> <th>首次访问</th> <th>最后活动</th> <th>总互动数</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach ($customers as $customer): ?> <tr> <td><?php echo $customer->id; ?></td> <td> <strong><?php echo esc_html($customer->email); ?></strong> <?php if (strpos($customer->email, '@anonymous.com') !== false): ?> <span class="dashicons dashicons-admin-users" title="匿名用户"></span> <?php endif; ?> </td> <td><?php echo esc_html($customer->first_name . ' ' . $customer->last_name); ?></td> <td> <?php $stage_info = maf_get_funnel_stage_info($customer->funnel_stage); echo '<span class="maf-stage-badge maf-stage-' . $customer->funnel_stage . '">' . esc_html($stage_info['name']) . '</span>'; ?> </td> <td><?php echo date('Y-m-d H:i', strtotime($customer->created_at)); ?></td> <td><?php echo $customer->last_seen ? date('Y-m-d H:i', strtotime($customer->last_seen)) : '从未'; ?></td> <td><?php echo maf_get_customer_interaction_count($customer->id); ?></td> <td> <a href="<?php echo admin_url('admin.php?page=maf-customer-journeys&view=customer&id=' . $customer->id); ?>" class="button button-small">查看旅程</a> <button class="button button-small maf-send-message" data-customer-id="<?php echo $customer->id; ?>" data-customer-email="<?php echo esc_attr($customer->email); ?>"> 发送消息 </button> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <!-- 发送消息模态框 --> <div id="maf-message-modal" class="maf-modal" style="display:none;"> <div class="maf-modal-content"> <div class="maf-modal-header"> <h3>发送消息给客户</h3> <span class="maf-modal-close">×</span> </div> <div class="maf-modal-body"> <form id="maf-message-form"> <input type="hidden" id="maf-message-customer-id" name="customer_id"> <div class="maf-form-group"> <label for="maf-message-subject">主题:</label> <input type="text" id="maf-message-subject" name="subject" class="regular-text" required> </div> <div class="maf-form-group"> <label for="maf-message-content">内容:</label> <textarea id="maf-message-content" name="content" rows="6" class="large-text" required></textarea> </div> <div class="maf-form-group"> <label for="maf-message-type">消息类型:</label> <select id="maf-message-type" name="message_type"> <option value="email">电子邮件</option> <option value="notification">站内通知</option> <option value="sms">短信</option> </select> </div> <div class="maf-form-actions"> <button type="submit" class="button button-primary">发送</button> <button type="button" class="button maf-modal-cancel">取消</button> </div> </form> </div> </div> </div> <script> jQuery(document).ready(function($) { // 发送消息模态框 $('.maf-send-message').click(function() { var customerId = $(this).data('customer-id'); var customerEmail = $(this).data('customer-email'); $('#maf-message-customer-id').val(customerId); $('#maf-message-subject').val('来自 ' + '<?php echo get_bloginfo("name"); ?> 的消息'); $('#maf-message-modal').show(); }); $('.maf-modal-close, .maf-modal-cancel').click(function() { $('#maf-message-modal').hide(); }); // 发送消息表单提交 $('#maf-message-form').submit(function(e) { e.preventDefault(); var formData = $(this).serialize(); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'maf_send_customer_message', nonce: '<?php echo wp_create_nonce("maf_send_message"); ?>', form_data: formData }, beforeSend: function() { $('.maf-form-actions button').prop('disabled', true).text('发送中...'); }, success: function(response) { if (response.success) { alert('消息发送成功!'); $('#maf-message-modal').hide(); $('#maf-message-form')[0].reset(); } else { alert('发送失败: ' + response.data); } }, complete: function() { $('.maf-form-actions button').prop('disabled', false).text('发送'); } }); }); }); </script> <?php } 5.3 营销漏斗分析页面 创建可视化漏斗分析页面,帮助用户理解转化路径和瓶颈: // 营销漏斗分析页面 function maf_marketing_funnel_page() { if (!current_user_can('manage_options')) { wp_die('您没有权限访问此页面'); } global $wpdb; // 获取时间段参数 $time_range = isset($_GET['time_range']) ? sanitize_text_field($_GET['time_range']) : '30days'; $start_date = maf_calculate_start_date($time_range); // 获取漏斗数据 $funnel_data = maf_get_funnel_analysis_data($start_date); ?> <div class="wrap"> <h1 class="wp-heading-inline">营销漏斗分析</h1> <div class="maf-funnel-controls"> <form method="get" class="maf-time-range-form"> <input type="hidden" name="page" value="maf-marketing-funnel"> <label for="time-range">时间范围:</label> <select name="time_range" id="time-range" onchange="this.form.submit()"> <option value="7days" <?php selected($time_range, '7days'); ?>>最近7天</option> <option value="30days" <?php selected($time_range, '30days'); ?>>最近30天</option> <option value="90days" <?php selected($time_range, '90days'); ?>>最近90天</option> <option value="custom" <?php selected($time_range, 'custom'); ?>>自定义</option> </select> <?php if ($time_range === 'custom'): ?> <label for="start-date">开始日期:</label> <input type="date" name="start_date" id="start-date" value="<?php echo isset($_GET['start_date']) ? esc_attr($_GET['start_date']) : ''; ?>"> <label for="end-date">结束日期:</label> <input type="date" name="end_date" id="end-date" value="<?php echo isset($_GET['end_date']) ? esc_attr($_GET['end_date']) : ''; ?>"> <input type="submit" class="button" value="应用"> <?php endif; ?> </form> </div> <div class="maf-funnel-visualization"> <h3>漏斗可视化</h3> <div class="maf-funnel-chart-container"> <div class="maf-funnel-chart"> <?php foreach ($funnel_data['stages'] as $stage): ?>
发表评论WordPress集成教程:连接企业微信或钉钉实现消息互通,通过代码二次开发实现常用互联网小工具功能 引言:为什么需要将WordPress与企业通讯工具集成? 在当今数字化办公环境中,信息流通的效率和及时性直接影响企业运营效果。WordPress作为全球最流行的内容管理系统,承载着大量企业的官方网站、博客、产品展示甚至内部知识库。而企业微信和钉钉作为国内主流的企业通讯协作平台,已经成为日常工作中不可或缺的工具。 将WordPress与企业微信或钉钉集成,可以实现: 网站内容更新实时通知到工作群组 用户提交的表单数据即时推送到相关负责人 评论审核提醒、订单状态变更等业务通知 实现跨平台的信息同步与协作 本文将详细介绍如何通过WordPress代码二次开发,实现与企业微信或钉钉的消息互通,并扩展实现一些常用的互联网小工具功能。 第一部分:准备工作与环境配置 1.1 了解企业微信与钉钉开放平台 企业微信开放平台提供了丰富的API接口,包括: 消息推送接口:支持文本、图文、卡片等多种消息格式 部门与成员管理:获取组织架构信息 应用管理:创建自定义应用并获取访问凭证 钉钉开放平台同样提供了: 工作通知接口:向用户或部门发送消息 群机器人:通过Webhook方式发送消息到群聊 免登授权:实现用户身份验证 1.2 WordPress开发环境搭建 在进行二次开发前,需要准备: 本地开发环境:推荐使用Local by Flywheel或XAMPP 代码编辑器:VS Code或PHPStorm WordPress调试配置:在wp-config.php中启用调试模式 define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 1.3 创建WordPress插件框架 我们将创建一个独立的插件来实现所有功能: <?php /** * Plugin Name: 企业通讯集成工具 * Plugin URI: https://yourwebsite.com/ * Description: 连接企业微信或钉钉实现消息互通,集成常用小工具 * Version: 1.0.0 * Author: Your Name */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('ECIT_VERSION', '1.0.0'); define('ECIT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ECIT_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once ECIT_PLUGIN_DIR . 'includes/class-core.php'; ECIT_Core::init(); 第二部分:企业微信集成实现 2.1 企业微信应用配置与认证 首先需要在企业微信管理后台创建应用: 登录企业微信管理后台 进入"应用管理" → "自建应用" → "创建应用" 获取以下关键信息: CorpID:企业ID AgentId:应用ID Secret:应用密钥 2.2 获取访问令牌(Access Token) 企业微信API调用需要Access Token,以下是获取Token的类实现: class ECIT_WeChat_Work { private $corp_id; private $agent_id; private $secret; private $access_token; private $token_expire; public function __construct($corp_id, $agent_id, $secret) { $this->corp_id = $corp_id; $this->agent_id = $agent_id; $this->secret = $secret; $this->get_access_token(); } private function get_access_token() { $transient_key = 'ecit_wechat_token_' . $this->agent_id; $cached_token = get_transient($transient_key); if ($cached_token !== false) { $this->access_token = $cached_token; return; } $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"; $params = [ 'corpid' => $this->corp_id, 'corpsecret' => $this->secret ]; $response = wp_remote_get(add_query_arg($params, $url)); if (is_wp_error($response)) { error_log('企业微信获取Token失败: ' . $response->get_error_message()); return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['access_token'])) { $this->access_token = $body['access_token']; // Token有效期为7200秒,我们设置为7000秒过期 set_transient($transient_key, $this->access_token, 7000); return true; } error_log('企业微信Token获取失败: ' . json_encode($body)); return false; } public function get_token() { return $this->access_token; } } 2.3 实现消息发送功能 class ECIT_WeChat_Message { private $wechat_work; public function __construct($wechat_work) { $this->wechat_work = $wechat_work; } /** * 发送文本消息 */ public function send_text($content, $to_user = '@all', $to_party = '', $to_tag = '') { $token = $this->wechat_work->get_token(); if (!$token) { return false; } $url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={$token}"; $data = [ 'touser' => $to_user, 'toparty' => $to_party, 'totag' => $to_tag, 'msgtype' => 'text', 'agentid' => $this->wechat_work->agent_id, 'text' => [ 'content' => $content ], 'safe' => 0, 'enable_id_trans' => 0, 'enable_duplicate_check' => 0 ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE), 'timeout' => 10 ]); return $this->handle_response($response); } /** * 发送图文消息 */ public function send_news($articles, $to_user = '@all') { $token = $this->wechat_work->get_token(); if (!$token) { return false; } $url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={$token}"; $data = [ 'touser' => $to_user, 'msgtype' => 'news', 'agentid' => $this->wechat_work->agent_id, 'news' => [ 'articles' => $articles ] ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE) ]); return $this->handle_response($response); } private function handle_response($response) { if (is_wp_error($response)) { error_log('企业微信消息发送失败: ' . $response->get_error_message()); return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if ($body['errcode'] == 0) { return true; } else { error_log('企业微信消息发送失败: ' . json_encode($body)); return false; } } } 2.4 WordPress钩子集成示例 将WordPress事件与企业微信通知绑定: class ECIT_WordPress_Hooks { private $wechat_message; public function __construct($wechat_message) { $this->wechat_message = $wechat_message; $this->init_hooks(); } private function init_hooks() { // 新文章发布通知 add_action('publish_post', [$this, 'notify_new_post'], 10, 2); // 新评论通知 add_action('comment_post', [$this, 'notify_new_comment'], 10, 3); // 用户注册通知 add_action('user_register', [$this, 'notify_new_user']); // WooCommerce新订单通知 add_action('woocommerce_new_order', [$this, 'notify_new_order'], 10, 2); } public function notify_new_post($post_id, $post) { if ($post->post_status != 'publish' || $post->post_type != 'post') { return; } $author = get_user_by('ID', $post->post_author); $post_url = get_permalink($post_id); $message = "📢 新文章发布通知nn"; $message .= "标题:{$post->post_title}n"; $message .= "作者:{$author->display_name}n"; $message .= "摘要:" . wp_trim_words(strip_tags($post->post_content), 50) . "n"; $message .= "链接:{$post_url}"; $this->wechat_message->send_text($message); } public function notify_new_comment($comment_id, $approved, $commentdata) { if ($approved != 1) { return; } $post = get_post($commentdata['comment_post_ID']); $comment = get_comment($comment_id); $message = "💬 新评论通知nn"; $message .= "文章:{$post->post_title}n"; $message .= "评论人:{$comment->comment_author}n"; $message .= "内容:" . wp_trim_words($comment->comment_content, 30) . "n"; $message .= "管理链接:" . admin_url("comment.php?action=editcomment&c={$comment_id}"); $this->wechat_message->send_text($message); } public function notify_new_user($user_id) { $user = get_user_by('ID', $user_id); $message = "👤 新用户注册nn"; $message .= "用户名:{$user->user_login}n"; $message .= "邮箱:{$user->user_email}n"; $message .= "注册时间:" . date('Y-m-d H:i:s') . "n"; $message .= "管理链接:" . admin_url("user-edit.php?user_id={$user_id}"); $this->wechat_message->send_text($message); } } 第三部分:钉钉集成实现 3.1 钉钉机器人配置 钉钉群机器人提供了更简单的集成方式: class ECIT_DingTalk_Robot { private $webhook_url; private $secret; public function __construct($webhook_url, $secret = '') { $this->webhook_url = $webhook_url; $this->secret = $secret; } /** * 生成签名 */ private function generate_sign($timestamp) { if (empty($this->secret)) { return ''; } $string_to_sign = $timestamp . "n" . $this->secret; $sign = base64_encode(hash_hmac('sha256', $string_to_sign, $this->secret, true)); return urlencode($sign); } /** * 发送文本消息 */ public function send_text($content, $at_mobiles = [], $at_all = false) { $timestamp = time() * 1000; $sign = $this->generate_sign($timestamp); $url = $this->webhook_url; if ($sign) { $url .= "×tamp={$timestamp}&sign={$sign}"; } $data = [ 'msgtype' => 'text', 'text' => [ 'content' => $content ], 'at' => [ 'atMobiles' => $at_mobiles, 'isAtAll' => $at_all ] ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE), 'timeout' => 10 ]); return $this->handle_response($response); } /** * 发送Markdown消息 */ public function send_markdown($title, $text, $at_mobiles = [], $at_all = false) { $timestamp = time() * 1000; $sign = $this->generate_sign($timestamp); $url = $this->webhook_url; if ($sign) { $url .= "×tamp={$timestamp}&sign={$sign}"; } $data = [ 'msgtype' => 'markdown', 'markdown' => [ 'title' => $title, 'text' => $text ], 'at' => [ 'atMobiles' => $at_mobiles, 'isAtAll' => $at_all ] ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE) ]); return $this->handle_response($response); } /** * 发送链接消息 */ public function send_link($title, $text, $message_url, $pic_url = '') { $timestamp = time() * 1000; $sign = $this->generate_sign($timestamp); $url = $this->webhook_url; if ($sign) { $url .= "×tamp={$timestamp}&sign={$sign}"; } $data = [ 'msgtype' => 'link', 'link' => [ 'title' => $title, 'text' => $text, 'messageUrl' => $message_url, 'picUrl' => $pic_url ] ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE) ]); return $this->handle_response($response); } private function handle_response($response) { if (is_wp_error($response)) { error_log('钉钉消息发送失败: ' . $response->get_error_message()); return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if ($body['errcode'] == 0) { return true; } else { error_log('钉钉消息发送失败: ' . json_encode($body)); return false; } } } 3.2 钉钉工作通知API集成 对于更复杂的企业应用,可以使用钉钉工作通知API: class ECIT_DingTalk_Work_Notification { private $app_key; private $app_secret; private $access_token; public function __construct($app_key, $app_secret) { $this->app_key = $app_key; $this->app_secret = $app_secret; $this->get_access_token(); } private function get_access_token() { $transient_key = 'ecit_dingtalk_token'; $cached_token = get_transient($transient_key); if ($cached_token !== false) { $this->access_token = $cached_token; return; } $url = 'https://oapi.dingtalk.com/gettoken'; $params = [ 'appkey' => $this->app_key, 'appsecret' => $this->app_secret ]; $response = wp_remote_get(add_query_arg($params, $url)); if (is_wp_error($response)) { error_log('钉钉获取Token失败: ' . $response->get_error_message()); return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if ($body['errcode'] == 0) { $this->access_token = $body['access_token']; set_transient($transient_key, $this->access_token, 7000); return true; } error_log('钉钉Token获取失败: ' . json_encode($body)); return false; } /** * 发送工作通知 */ public function send_notification($userid_list, $dept_id_list, $msg) { $token = $this->access_token; if (!$token) { return false; } $url = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token={$token}"; $data = [ 'agent_id' => 'your_agent_id', // 需要在钉钉后台获取 'userid_list' => implode(',', $userid_list), 'dept_id_list' => implode(',', $dept_id_list), 'to_all_user' => false, 'msg' => $msg ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE) ]); return $this->handle_response($response); } } 第四部分:常用互联网小工具功能实现 4 4.1 短链接生成工具 短链接服务在内容分享和营销中非常实用,我们可以集成多种短链接API: class ECIT_Short_URL_Tool { /** * 使用百度短链接API */ public static function baidu_shorten($long_url) { $api_url = 'https://dwz.cn/admin/v2/create'; $token = get_option('ecit_baidu_dwz_token', ''); if (empty($token)) { return new WP_Error('no_token', '未配置百度短链接Token'); } $data = [ 'url' => $long_url, 'termOfValidity' => '1-year' // 有效期:1年 ]; $response = wp_remote_post($api_url, [ 'headers' => [ 'Content-Type' => 'application/json', 'Token' => $token ], 'body' => json_encode($data), 'timeout' => 10 ]); if (is_wp_error($response)) { return $response; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['Code']) && $body['Code'] == 0) { return $body['ShortUrl']; } return new WP_Error('api_error', $body['ErrMsg'] ?? '生成短链接失败'); } /** * 使用新浪短链接API */ public static function sina_shorten($long_url) { $api_url = 'http://api.t.sina.com.cn/short_url/shorten.json'; $source = get_option('ecit_sina_app_key', ''); $params = [ 'source' => $source, 'url_long' => $long_url ]; $response = wp_remote_get(add_query_arg($params, $api_url)); if (is_wp_error($response)) { return $response; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body[0]['url_short'])) { return $body[0]['url_short']; } return new WP_Error('api_error', '生成短链接失败'); } /** * 自建短链接服务 */ public static function custom_shorten($long_url, $custom_slug = '') { global $wpdb; $table_name = $wpdb->prefix . 'ecit_short_urls'; // 检查表是否存在,不存在则创建 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, long_url text NOT NULL, short_slug varchar(100) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, click_count int(11) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY short_slug (short_slug), KEY long_url (long_url(100)) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } // 如果已存在该长链接,返回已有的短链接 $existing = $wpdb->get_row($wpdb->prepare( "SELECT short_slug FROM $table_name WHERE long_url = %s", $long_url )); if ($existing) { return home_url('/s/' . $existing->short_slug); } // 生成短码 if (empty($custom_slug)) { $short_slug = self::generate_slug(); } else { $short_slug = sanitize_title($custom_slug); // 检查是否已存在 $exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE short_slug = %s", $short_slug )); if ($exists) { return new WP_Error('slug_exists', '该短码已存在'); } } // 插入数据库 $wpdb->insert($table_name, [ 'long_url' => $long_url, 'short_slug' => $short_slug ]); return home_url('/s/' . $short_slug); } /** * 生成随机短码 */ private static function generate_slug($length = 6) { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $characters_length = strlen($characters); $random_string = ''; for ($i = 0; $i < $length; $i++) { $random_string .= $characters[rand(0, $characters_length - 1)]; } return $random_string; } /** * 短链接重定向处理 */ public static function handle_redirect() { if (preg_match('#^/s/([a-zA-Z0-9]+)$#', $_SERVER['REQUEST_URI'], $matches)) { $slug = $matches[1]; global $wpdb; $table_name = $wpdb->prefix . 'ecit_short_urls'; $result = $wpdb->get_row($wpdb->prepare( "SELECT long_url FROM $table_name WHERE short_slug = %s", $slug )); if ($result) { // 更新点击次数 $wpdb->query($wpdb->prepare( "UPDATE $table_name SET click_count = click_count + 1 WHERE short_slug = %s", $slug )); // 重定向 wp_redirect($result->long_url, 301); exit; } } } } 4.2 二维码生成工具 class ECIT_QR_Code_Tool { /** * 使用Google Charts API生成二维码 */ public static function google_qrcode($content, $size = 200, $margin = 4) { $params = [ 'cht' => 'qr', 'chs' => $size . 'x' . $size, 'chl' => urlencode($content), 'choe' => 'UTF-8', 'chld' => 'L|' . $margin ]; $url = 'https://chart.googleapis.com/chart?' . http_build_query($params); // 下载图片到本地 $upload_dir = wp_upload_dir(); $filename = 'qrcode-' . md5($content . $size) . '.png'; $filepath = $upload_dir['path'] . '/' . $filename; if (!file_exists($filepath)) { $response = wp_remote_get($url); if (!is_wp_error($response)) { file_put_contents($filepath, wp_remote_retrieve_body($response)); } } return $upload_dir['url'] . '/' . $filename; } /** * 使用PHP QR Code库生成二维码 */ public static function php_qrcode($content, $size = 10, $margin = 1) { // 引入PHP QR Code库 if (!class_exists('QRcode')) { require_once ECIT_PLUGIN_DIR . 'lib/phpqrcode/qrlib.php'; } $upload_dir = wp_upload_dir(); $filename = 'qrcode-' . md5($content . $size) . '.png'; $filepath = $upload_dir['path'] . '/' . $filename; if (!file_exists($filepath)) { QRcode::png($content, $filepath, QR_ECLEVEL_L, $size, $margin); } return $upload_dir['url'] . '/' . $filename; } /** * 生成带Logo的二维码 */ public static function qrcode_with_logo($content, $logo_path, $size = 10) { // 先生成普通二维码 $qrcode_path = self::php_qrcode($content, $size, 0); $qrcode_path = str_replace(wp_upload_dir()['url'], wp_upload_dir()['path'], $qrcode_path); // 加载二维码图片 $qrcode = imagecreatefrompng($qrcode_path); $qrcode_width = imagesx($qrcode); $qrcode_height = imagesy($qrcode); // 加载Logo图片 $logo_info = getimagesize($logo_path); $logo_type = $logo_info[2]; switch ($logo_type) { case IMAGETYPE_JPEG: $logo = imagecreatefromjpeg($logo_path); break; case IMAGETYPE_PNG: $logo = imagecreatefrompng($logo_path); break; default: return $qrcode_path; } $logo_width = imagesx($logo); $logo_height = imagesy($logo); // 计算Logo大小(二维码的1/5) $new_logo_width = $qrcode_width / 5; $new_logo_height = $logo_height * ($new_logo_width / $logo_width); // 重新调整Logo大小 $resized_logo = imagecreatetruecolor($new_logo_width, $new_logo_height); imagecopyresampled($resized_logo, $logo, 0, 0, 0, 0, $new_logo_width, $new_logo_height, $logo_width, $logo_height); // 合并图片 $dst_x = ($qrcode_width - $new_logo_width) / 2; $dst_y = ($qrcode_height - $new_logo_height) / 2; imagecopymerge($qrcode, $resized_logo, $dst_x, $dst_y, 0, 0, $new_logo_width, $new_logo_height, 100); // 保存新图片 $filename = 'qrcode-logo-' . md5($content) . '.png'; $filepath = wp_upload_dir()['path'] . '/' . $filename; imagepng($qrcode, $filepath); // 释放内存 imagedestroy($qrcode); imagedestroy($logo); imagedestroy($resized_logo); return wp_upload_dir()['url'] . '/' . $filename; } /** * 生成文章二维码短代码 */ public static function qrcode_shortcode($atts) { $atts = shortcode_atts([ 'size' => 200, 'logo' => '', 'post_id' => get_the_ID() ], $atts); $post_url = get_permalink($atts['post_id']); $post_title = get_the_title($atts['post_id']); if (!empty($atts['logo'])) { $logo_path = get_attached_file($atts['logo']); $qrcode_url = self::qrcode_with_logo($post_url, $logo_path); } else { $qrcode_url = self::google_qrcode($post_url, $atts['size']); } return sprintf( '<div class="post-qrcode"> <img src="%s" alt="%s" width="%d" height="%d"> <p>扫描二维码阅读文章</p> </div>', esc_url($qrcode_url), esc_attr($post_title), $atts['size'], $atts['size'] ); } } 4.3 数据统计与分析工具 class ECIT_Analytics_Tool { /** * 获取网站访问统计 */ public static function get_visitor_stats($days = 30) { global $wpdb; $results = $wpdb->get_results($wpdb->prepare( "SELECT DATE(visit_time) as date, COUNT(*) as visits, COUNT(DISTINCT ip_address) as unique_visitors FROM {$wpdb->prefix}ecit_visits WHERE visit_time >= DATE_SUB(NOW(), INTERVAL %d DAY) GROUP BY DATE(visit_time) ORDER BY date DESC", $days )); return $results; } /** * 记录访问数据 */ public static function track_visit() { if (is_admin() || wp_doing_ajax() || wp_doing_cron()) { return; } global $wpdb; $table_name = $wpdb->prefix . 'ecit_visits'; // 检查表是否存在 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, ip_address varchar(45) NOT NULL, user_agent text, page_url varchar(500) NOT NULL, referrer varchar(500), visit_time datetime DEFAULT CURRENT_TIMESTAMP, user_id bigint(20) DEFAULT 0, PRIMARY KEY (id), KEY ip_address (ip_address), KEY visit_time (visit_time), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } $data = [ 'ip_address' => self::get_client_ip(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'page_url' => (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]", 'referrer' => $_SERVER['HTTP_REFERER'] ?? '', 'user_id' => get_current_user_id() ]; $wpdb->insert($table_name, $data); } /** * 获取客户端IP */ private static function get_client_ip() { $ip_keys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR']; foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER)) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } /** * 获取热门文章 */ public static function get_popular_posts($limit = 10, $days = 30) { global $wpdb; $results = $wpdb->get_results($wpdb->prepare( "SELECT p.ID, p.post_title, COUNT(v.id) as view_count, MAX(v.visit_time) as last_visit FROM {$wpdb->prefix}ecit_visits v INNER JOIN {$wpdb->posts} p ON v.page_url LIKE CONCAT('%%', p.guid, '%%') WHERE v.visit_time >= DATE_SUB(NOW(), INTERVAL %d DAY) AND p.post_status = 'publish' AND p.post_type = 'post' GROUP BY p.ID ORDER BY view_count DESC LIMIT %d", $days, $limit )); return $results; } /** * 生成统计报告 */ public static function generate_report($period = 'daily') { $stats = self::get_visitor_stats($period == 'daily' ? 1 : 30); $popular_posts = self::get_popular_posts(5, $period == 'daily' ? 1 : 30); $report = "📊 网站访问统计报告nn"; if ($period == 'daily') { $report .= "📅 日期: " . date('Y-m-d') . "n"; } else { $report .= "📅 周期: 最近30天n"; } $total_visits = 0; $total_visitors = 0; foreach ($stats as $stat) { $total_visits += $stat->visits; $total_visitors += $stat->unique_visitors; } $report .= "👥 总访问量: " . $total_visits . "n"; $report .= "👤 独立访客: " . $total_visitors . "nn"; $report .= "🔥 热门文章:n"; foreach ($popular_posts as $index => $post) { $report .= ($index + 1) . ". " . $post->post_title . " (" . $post->view_count . "次)n"; } return $report; } } 4.4 内容自动发布工具 class ECIT_Auto_Publish_Tool { /** * 从RSS源自动发布文章 */
发表评论详细教程:为网站开发活动报名与电子票务管理工具——通过WordPress代码二次开发实现常用互联网小工具功能 引言:为什么需要自定义活动报名与电子票务系统 在当今数字化时代,活动组织者面临着管理参与者、票务销售和活动执行的复杂挑战。虽然市场上有许多现成的活动管理解决方案,但它们往往缺乏灵活性、定制性,或者价格昂贵。对于许多中小型组织、社区团体或个人活动策划者来说,一个能够完全控制、成本可控且功能定制的解决方案显得尤为重要。 WordPress作为全球最流行的内容管理系统,拥有强大的扩展性和灵活性。通过代码二次开发,我们可以在WordPress平台上构建一个功能完善、符合特定需求的活动报名与电子票务管理工具。本教程将详细指导您如何从零开始,通过WordPress代码二次开发实现这一目标。 第一部分:准备工作与环境搭建 1.1 开发环境配置 在开始开发之前,我们需要搭建一个合适的开发环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel等工具搭建本地WordPress环境 代码编辑器:选择Visual Studio Code、PHPStorm或Sublime Text等专业编辑器 版本控制:初始化Git仓库,便于代码管理和版本控制 浏览器开发者工具:熟悉Chrome或Firefox的开发者工具,用于调试前端代码 1.2 WordPress安装与基础配置 下载最新版WordPress并安装到本地环境 配置数据库连接信息 设置管理员账户和网站基本信息 安装必要的开发插件: Query Monitor:用于调试数据库查询和性能 Show Current Template:显示当前使用的模板文件 Advanced Custom Fields:用于创建自定义字段(可选) 1.3 创建自定义插件结构 为了避免主题更新导致功能丢失,我们将创建一个独立插件来实现所有功能。在wp-content/plugins/目录下创建新文件夹event-ticket-manager,并创建以下基础文件结构: event-ticket-manager/ ├── event-ticket-manager.php # 主插件文件 ├── includes/ # 核心功能文件目录 │ ├── class-database.php # 数据库操作类 │ ├── class-event.php # 活动管理类 │ ├── class-ticket.php # 票务管理类 │ ├── class-payment.php # 支付处理类 │ └── class-email.php # 邮件通知类 ├── admin/ # 后台管理文件 │ ├── css/ # 后台样式 │ ├── js/ # 后台脚本 │ └── pages/ # 后台页面 ├── public/ # 前端文件 │ ├── css/ # 前端样式 │ ├── js/ # 前端脚本 │ └── templates/ # 前端模板 ├── assets/ # 静态资源 └── vendor/ # 第三方库(如果需要) 第二部分:数据库设计与数据模型 2.1 设计数据库表结构 活动报名与电子票务系统需要多个数据表来存储不同类型的信息。我们将在插件激活时创建这些表: // includes/class-database.php class ETM_Database { public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 活动表 $events_table = $wpdb->prefix . 'etm_events'; $events_sql = "CREATE TABLE IF NOT EXISTS $events_table ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, start_date datetime NOT NULL, end_date datetime NOT NULL, location varchar(500), max_participants int(11) DEFAULT 0, current_participants int(11) DEFAULT 0, status varchar(50) DEFAULT 'draft', created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 票务类型表 $ticket_types_table = $wpdb->prefix . 'etm_ticket_types'; $ticket_types_sql = "CREATE TABLE IF NOT EXISTS $ticket_types_table ( id int(11) NOT NULL AUTO_INCREMENT, event_id int(11) NOT NULL, name varchar(255) NOT NULL, description text, price decimal(10,2) DEFAULT 0.00, quantity int(11) NOT NULL, sold int(11) DEFAULT 0, sale_start datetime, sale_end datetime, status varchar(50) DEFAULT 'active', PRIMARY KEY (id), FOREIGN KEY (event_id) REFERENCES $events_table(id) ON DELETE CASCADE ) $charset_collate;"; // 订单表 $orders_table = $wpdb->prefix . 'etm_orders'; $orders_sql = "CREATE TABLE IF NOT EXISTS $orders_table ( id int(11) NOT NULL AUTO_INCREMENT, order_number varchar(100) NOT NULL UNIQUE, event_id int(11) NOT NULL, customer_name varchar(255) NOT NULL, customer_email varchar(255) NOT NULL, customer_phone varchar(50), total_amount decimal(10,2) NOT NULL, payment_status varchar(50) DEFAULT 'pending', payment_method varchar(100), payment_id varchar(255), order_status varchar(50) DEFAULT 'pending', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), FOREIGN KEY (event_id) REFERENCES $events_table(id) ) $charset_collate;"; // 订单详情表 $order_items_table = $wpdb->prefix . 'etm_order_items'; $order_items_sql = "CREATE TABLE IF NOT EXISTS $order_items_table ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL, ticket_type_id int(11) NOT NULL, quantity int(11) NOT NULL, unit_price decimal(10,2) NOT NULL, subtotal decimal(10,2) NOT NULL, ticket_code varchar(100) NOT NULL UNIQUE, checked_in tinyint(1) DEFAULT 0, checked_in_at datetime, PRIMARY KEY (id), FOREIGN KEY (order_id) REFERENCES $orders_table(id) ON DELETE CASCADE, FOREIGN KEY (ticket_type_id) REFERENCES $ticket_types_table(id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($events_sql); dbDelta($ticket_types_sql); dbDelta($orders_sql); dbDelta($order_items_sql); } } 2.2 实现数据模型类 为每个主要实体创建对应的PHP类,封装数据库操作: // includes/class-event.php class ETM_Event { private $db; private $table_name; public function __construct() { global $wpdb; $this->db = $wpdb; $this->table_name = $wpdb->prefix . 'etm_events'; } public function create($data) { $defaults = array( 'status' => 'draft', 'current_participants' => 0, 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ); $data = wp_parse_args($data, $defaults); $result = $this->db->insert( $this->table_name, $data ); if ($result) { return $this->db->insert_id; } return false; } public function get($id) { $query = $this->db->prepare( "SELECT * FROM $this->table_name WHERE id = %d", $id ); return $this->db->get_row($query); } public function update($id, $data) { $data['updated_at'] = current_time('mysql'); $where = array('id' => $id); return $this->db->update( $this->table_name, $data, $where ); } public function delete($id) { return $this->db->delete( $this->table_name, array('id' => $id) ); } public function get_all($args = array()) { $defaults = array( 'status' => 'publish', 'orderby' => 'start_date', 'order' => 'ASC', 'limit' => 10, 'offset' => 0 ); $args = wp_parse_args($args, $defaults); $where = array(); if (!empty($args['status'])) { $where[] = $this->db->prepare("status = %s", $args['status']); } if (!empty($args['search'])) { $where[] = $this->db->prepare("(title LIKE %s OR description LIKE %s)", '%' . $args['search'] . '%', '%' . $args['search'] . '%' ); } $where_clause = ''; if (!empty($where)) { $where_clause = 'WHERE ' . implode(' AND ', $where); } $orderby = esc_sql($args['orderby']); $order = esc_sql($args['order']); $query = "SELECT * FROM $this->table_name $where_clause ORDER BY $orderby $order LIMIT %d OFFSET %d"; $query = $this->db->prepare( $query, $args['limit'], $args['offset'] ); return $this->db->get_results($query); } public function get_upcoming($limit = 5) { $current_time = current_time('mysql'); $query = $this->db->prepare( "SELECT * FROM $this->table_name WHERE status = 'publish' AND start_date > %s ORDER BY start_date ASC LIMIT %d", $current_time, $limit ); return $this->db->get_results($query); } } 第三部分:后台管理界面开发 3.1 创建管理菜单和页面 在WordPress后台添加自定义管理菜单,用于管理活动、票务和订单: // admin/class-admin-menu.php class ETM_Admin_Menu { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menus')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } public function add_admin_menus() { // 主菜单 add_menu_page( '活动票务管理', '活动票务', 'manage_options', 'etm-events', array($this, 'render_events_page'), 'dashicons-tickets-alt', 30 ); // 子菜单 add_submenu_page( 'etm-events', '活动管理', '所有活动', 'manage_options', 'etm-events', array($this, 'render_events_page') ); add_submenu_page( 'etm-events', '添加新活动', '添加活动', 'manage_options', 'etm-add-event', array($this, 'render_add_event_page') ); add_submenu_page( 'etm-events', '票务类型', '票务类型', 'manage_options', 'etm-ticket-types', array($this, 'render_ticket_types_page') ); add_submenu_page( 'etm-events', '订单管理', '订单', 'manage_options', 'etm-orders', array($this, 'render_orders_page') ); add_submenu_page( 'etm-events', '签到管理', '签到', 'manage_options', 'etm-checkin', array($this, 'render_checkin_page') ); add_submenu_page( 'etm-events', '报表统计', '报表', 'manage_options', 'etm-reports', array($this, 'render_reports_page') ); add_submenu_page( 'etm-events', '系统设置', '设置', 'manage_options', 'etm-settings', array($this, 'render_settings_page') ); } public function enqueue_admin_scripts($hook) { // 只在插件页面加载脚本和样式 if (strpos($hook, 'etm-') === false) { return; } wp_enqueue_style( 'etm-admin-style', plugins_url('admin/css/admin-style.css', dirname(__FILE__)), array(), '1.0.0' ); wp_enqueue_script( 'etm-admin-script', plugins_url('admin/js/admin-script.js', dirname(__FILE__)), array('jquery', 'jquery-ui-datepicker'), '1.0.0', true ); // 本地化脚本,传递数据到JavaScript wp_localize_script('etm-admin-script', 'etm_admin', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('etm_admin_nonce'), 'confirm_delete' => '确定要删除这个项目吗?此操作不可撤销。' )); // 加载jQuery UI日期选择器样式 wp_enqueue_style('jquery-ui-style', 'https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css'); } public function render_events_page() { include plugin_dir_path(__FILE__) . 'pages/events.php'; } public function render_add_event_page() { include plugin_dir_path(__FILE__) . 'pages/add-event.php'; } // 其他页面渲染方法... } 3.2 实现活动管理界面 创建活动列表和编辑界面: // admin/pages/events.php <div class="wrap etm-admin-wrap"> <h1 class="wp-heading-inline">活动管理</h1> <a href="<?php echo admin_url('admin.php?page=etm-add-event'); ?>" class="page-title-action">添加新活动</a> <hr class="wp-header-end"> <div class="etm-admin-container"> <div class="etm-admin-header"> <div class="tablenav top"> <div class="alignleft actions"> <select name="filter_status" id="filter-status"> <option value="">所有状态</option> <option value="draft">草稿</option> <option value="publish">已发布</option> <option value="cancelled">已取消</option> <option value="completed">已结束</option> </select> <input type="text" name="search" id="event-search" placeholder="搜索活动..." value="<?php echo isset($_GET['s']) ? esc_attr($_GET['s']) : ''; ?>"> <button type="button" class="button" id="search-events">搜索</button> </div> <div class="tablenav-pages"> <span class="displaying-num"><?php echo $total_events; ?> 个活动</span> <span class="pagination-links"> <?php echo $pagination; ?> </span> </div> </div> </div> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th scope="col" class="column-primary">活动名称</th> <th scope="col">日期</th> <th scope="col">地点</th> <th scope="col">参与人数</th> <th scope="col">状态</th> <th scope="col">操作</th> </tr> </thead> <tbody> <?php if (!empty($events)) : ?> <?php foreach ($events as $event) : ?> <tr> <td class="column-primary"> <strong><?php echo esc_html($event->title); ?></strong> <div class="row-actions"> <span class="edit"> <a href="<?php echo admin_url('admin.php?page=etm-add-event&id=' . $event->id); ?>">编辑</a> | </span> <span class="view"> <a href="<?php echo get_permalink($event->post_id); ?>" target="_blank">查看</a> | </span> <span class="duplicate"> <a href="#" class="duplicate-event" data-id="<?php echo $event->id; ?>">复制</a> | </span> <span class="trash"> <a href="#" class="delete-event" data-id="<?php echo $event->id; ?>">删除</a> </span> </div> <button type="button" class="toggle-row"> <span class="screen-reader-text">显示详情</span> </button> </td> <td> <?php $start_date = date_i18n('Y年m月d日 H:i', strtotime($event->start_date)); $end_date = date_i18n('Y年m月d日 H:i', strtotime($event->end_date)); echo $start_date . '<br>至<br>' . $end_date; ?> </td> <td><?php echo esc_html($event->location); ?></td> <td> <?php echo $event->max_participants > 0 ? $event->max_participants : '不限'; ?> </td> <td> <span class="event-status status-<?php echo esc_attr($event->status); ?>"> <?php $status_labels = array( 'draft' => '草稿', 'publish' => '已发布', 'cancelled' => '已取消', 'completed' => '已结束' ); echo isset($status_labels[$event->status]) ? $status_labels[$event->status] : $event->status; ?> </span> </td> <td> <div class="action-buttons"> <a href="<?php echo admin_url('admin.php?page=etm-ticket-types&event_id=' . $event->id); ?>" class="button button-small">管理票务</a> <a href="<?php echo admin_url('admin.php?page=etm-orders&event_id=' . $event->id); ?>" class="button button-small">查看订单</a> <?php if ($event->status == 'publish') : ?> <a href="#" class="button button-small button-primary copy-registration-link" data-link="<?php echo home_url('/event-registration/?event_id=' . $event->id); ?>">复制报名链接</a> <?php endif; ?> </div> </td> </tr> <?php endforeach; ?> <?php else : ?> <tr> <td colspan="6" class="no-items">暂无活动,<a href="<?php echo admin_url('admin.php?page=etm-add-event'); ?>">创建第一个活动</a></td> </tr> <?php endif; ?> </tbody> </table> </div> </div> ## 第四部分:前端报名与票务购买功能 ### 4.1 创建前端报名页面模板 // public/templates/registration-form.php<div class="etm-registration-container"> <div class="etm-registration-header"> <h1><?php echo esc_html($event->title); ?></h1> <div class="event-meta"> <div class="meta-item"> <span class="dashicons dashicons-calendar"></span> <span class="meta-label">时间:</span> <span class="meta-value"><?php echo date_i18n('Y年m月d日 H:i', strtotime($event->start_date)); ?> - <?php echo date_i18n('Y年m月d日 H:i', strtotime($event->end_date)); ?></span> </div> <div class="meta-item"> <span class="dashicons dashicons-location"></span> <span class="meta-label">地点:</span> <span class="meta-value"><?php echo esc_html($event->location); ?></span> </div> <div class="meta-item"> <span class="dashicons dashicons-groups"></span> <span class="meta-label">已报名:</span> <span class="meta-value"><?php echo $event->current_participants; ?>/<?php echo $event->max_participants > 0 ? $event->max_participants : '不限'; ?></span> </div> </div> </div> <?php if ($event->status !== 'publish') : ?> <div class="etm-alert etm-alert-warning"> 此活动当前不可报名。 </div> <?php elseif ($event->max_participants > 0 && $event->current_participants >= $event->max_participants) : ?> <div class="etm-alert etm-alert-warning"> 抱歉,此活动报名人数已满。 </div> <?php else : ?> <div class="etm-registration-content"> <div class="etm-event-description"> <h2>活动介绍</h2> <div class="description-content"> <?php echo wpautop($event->description); ?> </div> </div> <div class="etm-registration-form-wrapper"> <h2>报名信息</h2> <form id="etm-registration-form" method="post" action="<?php echo admin_url('admin-ajax.php'); ?>"> <?php wp_nonce_field('etm_registration_nonce', 'registration_nonce'); ?> <input type="hidden" name="action" value="etm_process_registration"> <input type="hidden" name="event_id" value="<?php echo $event->id; ?>"> <div class="form-section"> <h3>选择票种</h3> <div class="ticket-selection"> <?php if (!empty($ticket_types)) : ?> <?php foreach ($ticket_types as $ticket) : ?> <?php if ($ticket->status == 'active' && $ticket->sold < $ticket->quantity) : ?> <div class="ticket-option"> <div class="ticket-header"> <input type="radio" name="ticket_type_id" id="ticket_<?php echo $ticket->id; ?>" value="<?php echo $ticket->id; ?>" data-price="<?php echo $ticket->price; ?>" required> <label for="ticket_<?php echo $ticket->id; ?>"> <span class="ticket-name"><?php echo esc_html($ticket->name); ?></span> <span class="ticket-price"> <?php if ($ticket->price > 0) : ?> ¥<?php echo number_format($ticket->price, 2); ?> <?php else : ?> 免费 <?php endif; ?> </span> </label> </div> <div class="ticket-details"> <p><?php echo esc_html($ticket->description); ?></p> <div class="ticket-meta"> <span class="available">剩余:<?php echo $ticket->quantity - $ticket->sold; ?>张</span> <?php if ($ticket->sale_start && $ticket->sale_end) : ?> <span class="sale-period"> 销售时间:<?php echo date_i18n('m月d日 H:i', strtotime($ticket->sale_start)); ?> - <?php echo date_i18n('m月d日 H:i', strtotime($ticket->sale_end)); ?> </span> <?php endif; ?> </div> </div> </div> <?php endif; ?> <?php endforeach; ?> <?php else : ?> <p class="no-tickets">暂无可用票种</p> <?php endif; ?> </div> </div> <div class="form-section"> <h3>购票数量</h3> <div class="quantity-selection"> <div class="quantity-control"> <button type="button" class="quantity-minus" disabled>-</button> <input type="number" name="ticket_quantity" id="ticket_quantity" value="1" min="1" max="10" class="quantity-input"> <button type="button" class="quantity-plus">+</button> </div> <div class="quantity-note"> <p>每人最多可购买10张票</p> </div> </div> </div> <div class="form-section"> <h3>填写个人信息</h3> <div class="personal-info"> <div class="form-row"> <div class="form-group"> <label for="customer_name">姓名 <span class="required">*</span></label> <input type="text" id="customer_name" name="customer_name" required placeholder="请输入真实姓名"> </div> <div class="form-group"> <label for="customer_email">邮箱 <span class="required">*</span></label> <input type="email" id="customer_email" name="customer_email" required placeholder="用于接收电子票和通知"> </div> </div> <div class="form-row"> <div class="form-group"> <label for="customer_phone">手机号 <span class="required">*</span></label> <input type="tel" id="customer_phone" name="customer_phone" required placeholder="用于活动通知"> </div> <div class="form-group"> <label for="customer_company">公司/机构</label> <input type="text" id="customer_company" name="customer_company" placeholder="选填"> </div> </div> <div class="form-row"> <div class="form-group full-width"> <label for="special_requirements">特殊需求</label> <textarea id="special_requirements" name="special_requirements" rows="3" placeholder="如有饮食禁忌、特殊需求等请在此说明"></textarea> </div> </div> </div> </div> <div class="form-section"> <h3>订单总览</h3> <div class="order-summary"> <div class="summary-row"> <span class="summary-label">票种:</span> <span class="summary-value" id="summary_ticket_name">请选择票种</span> </div> <div class="summary-row"> <span class="summary-label">单价:</span> <span class="summary-value" id="summary_ticket_price">¥0.00</span> </div> <div class="summary-row"> <span class="summary-label">数量:</span> <span class="summary-value" id="summary_quantity">1</span> </div> <div class="summary-row total"> <span class="summary-label">总计:</span> <span class="summary-value" id="summary_total">¥0.00</span> </div> </div> </div> <div class="form-section"> <div class="form-submit"> <button type="submit" class="etm-submit-button" id="submit-registration"> <span class="button-text">提交报名</span> <span class="loading-spinner" style="display:none;"></span> </button> <p class="form-notice">点击提交即表示您同意我们的<a href="<?php echo get_privacy_policy_url(); ?>" target="_blank">隐私政策</a>和<a href="<?php echo home_url('/terms'); ?>" target="_blank">服务条款</a></p> </div> </div> </form> </div> </div> <?php endif; ?> </div> <script>jQuery(document).ready(function($) { // 更新订单总览 function updateOrderSummary() { var selectedTicket = $('input[name="ticket_type_id"]:checked'); var quantity = parseInt($('#ticket_quantity').val()); if (selectedTicket.length > 0) { var ticketName = selectedTicket.siblings('label').find('.ticket-name').text(); var ticketPrice = parseFloat(selectedTicket.data('price')); var total = ticketPrice * quantity; $('#summary_ticket_name').text(ticketName); $('#summary_ticket_price').text('¥' + ticketPrice.toFixed(2)); $('#summary_quantity').text(quantity); $('#summary_total').text('¥' + total.toFixed(2)); } } // 票种选择事件 $('input[name="ticket_type_id"]').on('change', updateOrderSummary); // 数量控制 $('.quantity-minus').on('click', function() { var input = $('#ticket_quantity'); var currentVal = parseInt(input.val()); if (currentVal > 1) { input.val(currentVal - 1); updateOrderSummary(); } updateQuantityButtons(); }); $('.quantity-plus').on('click', function() { var input = $('#ticket_quantity'); var currentVal = parseInt(input.val()); var maxVal = parseInt(input.attr('max')); if (currentVal < maxVal) { input.val(currentVal + 1); updateOrderSummary(); } updateQuantityButtons(); }); $('#ticket_quantity').on('change', function() { var val = parseInt($(this).val()); var maxVal = parseInt($(this).attr('max')); var minVal = parseInt($(this).attr('min')); if (val > maxVal) val = maxVal; if (val < minVal) val = minVal; $(this).val(val); updateOrderSummary(); updateQuantityButtons(); }); function updateQuantityButtons() { var currentVal = parseInt($('#ticket_quantity').val()); var maxVal = parseInt($('#ticket_quantity').attr('max')); var minVal = parseInt($('#ticket_quantity').attr('min')); $('.quantity-minus').prop('disabled', currentVal <= minVal); $('.quantity-plus').prop('disabled', currentVal >= maxVal); } // 表单提交 $('#etm-registration-form').on('submit', function(e) { e.preventDefault(); var form = $(this); var submitButton = $('#submit-registration'); var buttonText = submitButton.find('.button-text'); var spinner = submitButton.find('.loading-spinner'); // 验证表单 if (!form[0].checkValidity()) { form[0].reportValidity(); return; } // 显示加载状态 buttonText.text('处理中...'); spinner.show(); submitButton.prop('disabled', true); // 发送AJAX请求 $.ajax({ url: form.attr('action'), type: 'POST', data: form.serialize(), dataType: 'json', success: function(response) { if (response.success) { // 跳转到支付页面或成功页面 if (response.data.payment_required) { window.location.href = response.data.payment_url; } else { window.location.href = response.data.success_url; } } else { // 显示错误信息 alert(response.data.message || '提交失败,请重试'); buttonText.text('提交报名'); spinner.hide(); submitButton.prop('disabled', false); } }, error: function() { alert('网络错误,请重试'); buttonText.text('提交报名'); spinner.hide(); submitButton.prop('disabled', false); } }); }); // 初始化 updateQuantityButtons(); $('input[name="ticket_type_id"]:first').prop('checked', true).trigger('change'); });</script> ### 4.2 实现AJAX表单处理 // includes/class-ajax-handler.phpclass ETM_Ajax_Handler { public function __construct() { // 前端AJAX处理 add_action('wp_ajax_etm_process_registration', array($this, 'process_registration')); add_action('wp_ajax_nopriv_etm_process_registration', array($this, 'process_registration')); // 后台AJAX处理 add_action('wp_ajax_etm_admin_actions', array($this, 'handle_admin_actions')); } public function process_registration() { // 验证nonce if (!isset($_POST['registration_nonce']) || !wp_verify_nonce($_POST['registration_nonce'], 'etm_registration_nonce')) { wp_send_json_error(array('message' => '安全验证失败')); } // 验证必填字段 $required_fields = array( 'event_id', 'ticket_type_id', 'ticket_quantity', 'customer_name', 'customer_email', 'customer_phone' ); foreach ($required_fields as $field) { if (empty($_POST[$field])) { wp_send_json_error(array('message' => '请填写所有必填字段')); } } $event_id = intval($_POST['event_id']); $ticket_type_id = intval($_POST['ticket_type_id']); $quantity = intval($_POST['ticket_quantity']); // 验证活动状态 $event_model = new ETM_Event(); $event = $event_model->get($event_id); if (!$event || $event->status !== 'publish') { wp_send_json_error(array('message' => '活动不可用')); } // 验证票种 $ticket_model = new ETM_Ticket(); $ticket_type = $ticket_model->get_type($ticket_type_id); if (!$ticket_type || $ticket_type->event_id != $event_id) { wp_send_json_error(array('message' => '票种无效')); } if ($ticket_type->status !== 'active') { wp_send_json_error(array('message' => '该票种已停售')); } // 检查库存 if ($ticket_type->sold + $quantity > $ticket_type->quantity) { wp_send_json_error(array('message' => '票数不足,仅剩' . ($ticket_type->quantity - $ticket_type->sold) . '张')); } // 检查活动人数限制 if ($event->max_participants > 0 && $event->current_participants + $quantity > $event->max_participants) { wp_send_json_error(array('
发表评论一步步教你,集成在线考试与题库管理系统到WordPress,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:为什么要在WordPress中集成在线考试系统? 在当今数字化教育与企业培训蓬勃发展的时代,在线考试与评估已成为教育机构、企业培训部门乃至知识付费创作者不可或缺的功能。WordPress作为全球最受欢迎的内容管理系统(CMS),其强大的可扩展性使其远不止是一个博客平台。通过代码二次开发,我们可以将专业的在线考试与题库管理系统无缝集成到WordPress中,从而打造一个功能全面、用户体验良好的学习与评估平台。 传统的独立考试系统往往存在与主网站风格不一、用户数据割裂、维护成本高等问题。而在WordPress中集成此类系统,则能实现品牌统一、数据集中管理、用户单点登录等诸多优势。更重要的是,通过深度集成,我们还能利用WordPress的生态,结合其他插件与功能,创造出更丰富的互联网小工具应用场景。 本文将详细解析如何通过代码二次开发,在WordPress中从零开始构建一个功能完善的在线考试与题库管理系统,并探讨如何扩展其作为常用互联网小工具的潜力。无论你是WordPress开发者、教育科技创业者,还是企业培训负责人,都能从中获得切实可行的技术方案与灵感。 第一章:项目规划与系统架构设计 1.1 需求分析与功能规划 在开始编码之前,明确的需求分析是成功的关键。一个完整的在线考试与题库管理系统应包含以下核心模块: 题库管理模块:支持单选题、多选题、判断题、填空题、问答题等多种题型;支持试题分类、标签、难度分级;支持试题导入导出(Word、Excel、JSON格式)。 试卷管理模块:支持手动组卷(按分类、难度随机抽题)和固定试卷;设置考试时间、及格分数、考试次数限制等参数。 考试执行模块:全屏考试防作弊、实时计时、自动保存答案、中途恢复功能。 成绩与统计分析模块:个人成绩报告、错题集、整体考试统计分析、成绩导出。 用户与权限管理:与WordPress用户系统集成,区分考生、教师、管理员角色。 1.2 技术选型与架构设计 考虑到系统的复杂性与WordPress的特性,我们采用分层架构: 数据层:在WordPress默认数据库基础上,创建自定义数据表来存储试题、试卷、考试记录等结构化数据。利用WordPress的$wpdb类进行安全的数据操作。 业务逻辑层:开发自定义插件,封装所有考试相关的业务逻辑。采用面向对象编程(OOP)设计,提高代码可维护性。 表现层:结合WordPress主题模板和短代码(Shortcode)或Gutenberg块,实现前端界面的灵活嵌入。使用AJAX提升考试过程的交互体验。 数据库设计示例: wp_exam_questions:试题表(ID、题型、题干、选项、答案、解析、分类、难度、创建时间) wp_exam_papers:试卷表(ID、试卷名称、组卷规则、设置参数) wp_exam_records:考试记录表(ID、用户ID、试卷ID、开始时间、结束时间、得分、答案详情) wp_exam_user_meta:用户扩展表(错题集、收藏试题等) 第二章:开发环境搭建与基础插件创建 2.1 本地开发环境配置 推荐使用Local by Flywheel或XAMPP搭建本地WordPress环境。确保环境支持PHP 7.4+、MySQL 5.6+。安装代码编辑器(如VS Code)并配置调试工具(如Xdebug)。 2.2 创建自定义插件骨架 在wp-content/plugins/目录下创建新文件夹exam-system,并创建主插件文件exam-system.php: <?php /** * Plugin Name: WordPress在线考试与题库管理系统 * Description: 在WordPress中集成完整的在线考试与题库管理功能 * Version: 1.0.0 * Author: 你的名字 */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('EXAM_SYSTEM_VERSION', '1.0.0'); define('EXAM_SYSTEM_PATH', plugin_dir_path(__FILE__)); define('EXAM_SYSTEM_URL', plugin_dir_url(__FILE__)); // 核心类加载 require_once EXAM_SYSTEM_PATH . 'includes/class-database.php'; require_once EXAM_SYSTEM_PATH . 'includes/class-question.php'; require_once EXAM_SYSTEM_PATH . 'includes/class-exam.php'; require_once EXAM_SYSTEM_PATH . 'includes/class-shortcodes.php'; require_once EXAM_SYSTEM_PATH . 'includes/class-admin.php'; // 初始化插件 class Exam_System_Plugin { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 激活/停用钩子 register_activation_hook(__FILE__, array('Exam_Database', 'create_tables')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化各模块 add_action('plugins_loaded', array($this, 'init_modules')); } public function init_modules() { Exam_Database::init(); Exam_Question::init(); Exam_Manager::init(); Exam_Shortcodes::init(); if (is_admin()) { Exam_Admin::init(); } } public function deactivate() { // 清理临时数据,但不删除考试记录 flush_rewrite_rules(); } } // 启动插件 Exam_System_Plugin::get_instance(); 2.3 数据库表创建 在includes/class-database.php中实现数据表的创建与升级: class Exam_Database { public static function init() { // 数据库版本控制 add_action('plugins_loaded', array(__CLASS__, 'check_db_version')); } public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'exam_'; // 试题表 $questions_table = $table_prefix . 'questions'; $sql1 = "CREATE TABLE IF NOT EXISTS $questions_table ( id bigint(20) NOT NULL AUTO_INCREMENT, question_type varchar(50) NOT NULL DEFAULT 'single_choice', title text NOT NULL, options longtext, correct_answer longtext NOT NULL, analysis text, category_id bigint(20) DEFAULT 0, difficulty tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY category_id (category_id), KEY difficulty (difficulty) ) $charset_collate;"; // 试卷表 $papers_table = $table_prefix . 'papers'; $sql2 = "CREATE TABLE IF NOT EXISTS $papers_table ( id bigint(20) NOT NULL AUTO_INCREMENT, paper_name varchar(255) NOT NULL, question_ids text, settings longtext, total_score int(11) DEFAULT 100, time_limit int(11) DEFAULT 60, status tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 考试记录表 $records_table = $table_prefix . 'records'; $sql3 = "CREATE TABLE IF NOT EXISTS $records_table ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, paper_id bigint(20) NOT NULL, start_time datetime, end_time datetime, score decimal(5,2) DEFAULT 0, answers longtext, is_passed tinyint(1) DEFAULT 0, PRIMARY KEY (id), KEY user_id (user_id), KEY paper_id (paper_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); // 存储数据库版本 add_option('exam_system_db_version', '1.0.0'); } public static function check_db_version() { if (get_option('exam_system_db_version') !== '1.0.0') { self::create_tables(); } } } 第三章:核心功能模块开发 3.1 题库管理模块实现 试题管理类 (includes/class-question.php): class Exam_Question { private static $question_types = array( 'single_choice' => '单选题', 'multi_choice' => '多选题', 'true_false' => '判断题', 'fill_blank' => '填空题', 'short_answer' => '简答题' ); public static function init() { add_action('admin_menu', array(__CLASS__, 'add_admin_menu')); add_action('wp_ajax_save_question', array(__CLASS__, 'ajax_save_question')); add_action('wp_ajax_delete_question', array(__CLASS__, 'ajax_delete_question')); } // 添加管理菜单 public static function add_admin_menu() { add_menu_page( '题库管理', '考试系统', 'manage_options', 'exam-system', array(__CLASS__, 'render_questions_page'), 'dashicons-welcome-learn-more', 30 ); add_submenu_page( 'exam-system', '试题管理', '试题管理', 'manage_options', 'exam-questions', array(__CLASS__, 'render_questions_page') ); } // 渲染试题管理页面 public static function render_questions_page() { ?> <div class="wrap"> <h1 class="wp-heading-inline">试题管理</h1> <button id="add-new-question" class="page-title-action">添加新试题</button> <hr class="wp-header-end"> <!-- 试题列表表格 --> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th width="5%">ID</th> <th width="40%">试题内容</th> <th width="10%">题型</th> <th width="10%">难度</th> <th width="15%">分类</th> <th width="20%">操作</th> </tr> </thead> <tbody id="questions-list"> <!-- 通过AJAX动态加载 --> </tbody> </table> <!-- 添加/编辑试题模态框 --> <div id="question-modal" class="modal" style="display:none;"> <div class="modal-content"> <span class="close">×</span> <h2>编辑试题</h2> <form id="question-form"> <input type="hidden" id="question_id" name="question_id" value="0"> <p> <label for="question_type">题型:</label> <select id="question_type" name="question_type"> <?php foreach(self::$question_types as $key => $label): ?> <option value="<?php echo esc_attr($key); ?>"><?php echo esc_html($label); ?></option> <?php endforeach; ?> </select> </p> <p> <label for="question_title">题干:</label><br> <textarea id="question_title" name="question_title" rows="3" cols="80" required></textarea> </p> <div id="options-container"> <!-- 选项内容动态生成 --> </div> <p> <label for="correct_answer">正确答案:</label><br> <textarea id="correct_answer" name="correct_answer" rows="2" cols="80" required></textarea> </p> <p> <label for="analysis">试题解析:</label><br> <textarea id="analysis" name="analysis" rows="3" cols="80"></textarea> </p> <p> <button type="submit" class="button button-primary">保存试题</button> </p> </form> </div> </div> </div> <style> .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.4); } .modal-content { background-color: #fefefe; margin: 5% auto; padding: 20px; border: 1px solid #888; width: 80%; max-width: 800px; } .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; } </style> <script> jQuery(document).ready(function($) { // 加载试题列表 function loadQuestions() { $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'get_questions', nonce: '<?php echo wp_create_nonce('exam_nonce'); ?>' }, success: function(response) { $('#questions-list').html(response.data); } }); } // 打开模态框 $('#add-new-question').click(function() { $('#question-modal').show(); $('#question-form')[0].reset(); $('#question_id').val('0'); }); // 关闭模态框 $('.close').click(function() { $('#question-modal').hide(); }); // 保存试题 $('#question-form').submit(function(e) { e.preventDefault(); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'save_question', nonce: '<?php echo wp_create_nonce('exam_nonce'); ?>', form_data: $(this).serialize() }, success: function(response) { if (response.success) { alert('保存成功!'); $('#question-modal').hide(); loadQuestions(); } else { alert('保存失败:' + response.data); } } }); }); // 初始加载 loadQuestions(); }); </script> <?php } // AJAX保存试题 public static function ajax_save_question() { check_ajax_referer('exam_nonce', 'nonce'); global $wpdb; $table_name = $wpdb->prefix . 'exam_questions'; parse_str($_POST['form_data'], $form_data); $data = array( 'question_type' => sanitize_text_field($form_data['question_type']), 'title' => wp_kses_post($form_data['question_title']), 'correct_answer' => sanitize_textarea_field($form_data['correct_answer']), 'analysis' => sanitize_textarea_field($form_data['analysis']), 'updated_at' => current_time('mysql') ); // 处理选项(针对选择题) if (in_array($form_data['question_type'], array('single_choice', 'multi_choice'))) { $options = array(); if (isset($form_data['option'])) { foreach ($form_data['option'] as $key => $value) { if (!empty(trim($value))) { $options[$key] = sanitize_text_field($value); } } } $data['options'] = maybe_serialize($options); } $question_id = intval($form_data['question_id']); if ($question_id > 0) { // 更新 $result = $wpdb->update($table_name, $data, array('id' => $question_id)); } else { // 新增 $data['created_at'] = current_time('mysql'); $result = $wpdb->insert($table_name, $data); $question_id = $wpdb->insert_id; } if ($result !== false) { wp_send_json_success('试题保存成功'); } else { wp_send_json_error('保存失败:' . $wpdb->last_error); } } } 3.2 试卷管理与组卷功能 试卷管理类 (includes/class-exam.php): class Exam_Manager { public static function init() { add_action('admin_menu', array(__CLASS__, 'add_exam_menu')); add_action('wp_ajax_generate_paper', array(__CLASS__, 'ajax_generate_paper')); add_action('wp_ajax_start_exam', array(__CLASS__, 'ajax_start_exam')); add_action('wp_ajax_submit_exam', array(__CLASS__, 'ajax_submit_exam')); // 注册短代码 add_shortcode('exam_list', array(__CLASS__, 'shortcode_exam_list')); add_shortcode('take_exam', array(__CLASS__, 'shortcode_take_exam')); add_shortcode('exam_results', array(__CLASS__, 'shortcode_exam_results')); } // 手动组卷功能 public static function render_paper_generator() { global $wpdb; $categories = $wpdb->get_results("SELECT DISTINCT category_id FROM {$wpdb->prefix}exam_questions"); ?> <div class="wrap"> <h1>试卷生成器</h1> <form id="paper-generator-form"> <table class="form-table"> <tr> <th><label for="paper_name">试卷名称</label></th> id="paper_name" name="paper_name" class="regular-text" required></td> </tr> <tr> <th><label>组卷方式</label></th> <td> <label><input type="radio" name="generate_type" value="manual" checked> 手动选题</label> <label><input type="radio" name="generate_type" value="auto"> 自动组卷</label> </td> </tr> <tbody id="manual-selection"> <tr> <th><label>选择试题</label></th> <td> <div style="max-height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;"> <?php $questions = $wpdb->get_results("SELECT id, title, question_type FROM {$wpdb->prefix}exam_questions ORDER BY id DESC"); foreach ($questions as $q) { $type_labels = Exam_Question::$question_types; $type = isset($type_labels[$q->question_type]) ? $type_labels[$q->question_type] : $q->question_type; echo '<label style="display: block; margin-bottom: 5px;">'; echo '<input type="checkbox" name="selected_questions[]" value="' . $q->id . '"> '; echo wp_trim_words(strip_tags($q->title), 10) . ' <small>(' . $type . ')</small>'; echo '</label>'; } ?> </div> </td> </tr> </tbody> <tbody id="auto-selection" style="display:none;"> <tr> <th><label>按分类组卷</label></th> <td> <div id="category-rules"> <div class="rule-row"> <select name="category_rules[0][category_id]" class="category-select"> <option value="0">所有分类</option> <?php foreach($categories as $cat): ?> <option value="<?php echo $cat->category_id; ?>">分类 <?php echo $cat->category_id; ?></option> <?php endforeach; ?> </select> <select name="category_rules[0][question_type]"> <option value="all">所有题型</option> <?php foreach(Exam_Question::$question_types as $key => $label): ?> <option value="<?php echo $key; ?>"><?php echo $label; ?></option> <?php endforeach; ?> </select> <input type="number" name="category_rules[0][count]" min="1" max="50" value="5" style="width: 60px;"> 题 <select name="category_rules[0][difficulty]"> <option value="0">所有难度</option> <option value="1">简单</option> <option value="2">中等</option> <option value="3">困难</option> </select> <button type="button" class="button remove-rule">删除</button> </div> </div> <button type="button" id="add-rule" class="button">添加规则</button> </td> </tr> </tbody> <tr> <th><label for="time_limit">考试时间(分钟)</label></th> <td><input type="number" id="time_limit" name="time_limit" min="1" max="300" value="60"></td> </tr> <tr> <th><label for="passing_score">及格分数</label></th> <td><input type="number" id="passing_score" name="passing_score" min="0" max="100" value="60"></td> </tr> <tr> <th><label for="attempts_allowed">考试次数限制</label></th> <td><input type="number" id="attempts_allowed" name="attempts_allowed" min="0" value="0"> (0表示无限制)</td> </tr> </table> <p class="submit"> <button type="submit" class="button button-primary">生成试卷</button> </p> </form> <div id="generated-paper" style="display:none; margin-top: 30px; padding: 20px; background: #f5f5f5; border: 1px solid #ddd;"> <h3>试卷生成成功!</h3> <div id="paper-preview"></div> <p> <button id="save-paper" class="button button-primary">保存试卷</button> <button id="regenerate-paper" class="button">重新生成</button> </p> </div> </div> <script> jQuery(document).ready(function($) { // 切换组卷方式 $('input[name="generate_type"]').change(function() { if ($(this).val() === 'manual') { $('#manual-selection').show(); $('#auto-selection').hide(); } else { $('#manual-selection').hide(); $('#auto-selection').show(); } }); // 添加规则行 var ruleIndex = 1; $('#add-rule').click(function() { var newRow = $('.rule-row:first').clone(); newRow.find('select, input').each(function() { var name = $(this).attr('name'); $(this).attr('name', name.replace('[0]', '[' + ruleIndex + ']')); }); newRow.appendTo('#category-rules'); ruleIndex++; }); // 删除规则行 $(document).on('click', '.remove-rule', function() { if ($('.rule-row').length > 1) { $(this).closest('.rule-row').remove(); } }); // 提交生成试卷 $('#paper-generator-form').submit(function(e) { e.preventDefault(); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'generate_paper', nonce: '<?php echo wp_create_nonce("exam_nonce"); ?>', form_data: $(this).serialize() }, success: function(response) { if (response.success) { $('#paper-preview').html(response.data.preview); $('#generated-paper').show().data('paper-data', response.data.paper_data); $('html, body').animate({ scrollTop: $('#generated-paper').offset().top }, 500); } else { alert('生成失败:' + response.data); } } }); }); // 保存试卷 $('#save-paper').click(function() { var paperData = $('#generated-paper').data('paper-data'); paperData.paper_name = $('#paper_name').val(); paperData.time_limit = $('#time_limit').val(); paperData.passing_score = $('#passing_score').val(); paperData.attempts_allowed = $('#attempts_allowed').val(); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'save_paper', nonce: '<?php echo wp_create_nonce("exam_nonce"); ?>', paper_data: paperData }, success: function(response) { if (response.success) { alert('试卷保存成功!'); window.location.href = '<?php echo admin_url("admin.php?page=exam-papers"); ?>'; } else { alert('保存失败:' + response.data); } } }); }); }); </script> <?php } // AJAX生成试卷 public static function ajax_generate_paper() { check_ajax_referer('exam_nonce', 'nonce'); global $wpdb; parse_str($_POST['form_data'], $form_data); $question_ids = array(); if ($form_data['generate_type'] === 'manual') { // 手动选题 if (!empty($form_data['selected_questions'])) { $question_ids = array_map('intval', $form_data['selected_questions']); } } else { // 自动组卷 foreach ($form_data['category_rules'] as $rule) { $where = array('1=1'); $params = array(); if (!empty($rule['category_id']) && $rule['category_id'] != '0') { $where[] = 'category_id = %d'; $params[] = intval($rule['category_id']); } if (!empty($rule['question_type']) && $rule['question_type'] != 'all') { $where[] = 'question_type = %s'; $params[] = sanitize_text_field($rule['question_type']); } if (!empty($rule['difficulty']) && $rule['difficulty'] != '0') { $where[] = 'difficulty = %d'; $params[] = intval($rule['difficulty']); } $sql = "SELECT id FROM {$wpdb->prefix}exam_questions WHERE " . implode(' AND ', $where) . " ORDER BY RAND() LIMIT %d"; $params[] = intval($rule['count']); if (!empty($params)) { $results = $wpdb->get_col($wpdb->prepare($sql, $params)); $question_ids = array_merge($question_ids, $results); } } } if (empty($question_ids)) { wp_send_json_error('没有选择任何试题'); } // 生成预览 $preview = '<h4>试卷包含 ' . count($question_ids) . ' 道试题:</h4><ol>'; foreach ($question_ids as $qid) { $question = $wpdb->get_row($wpdb->prepare("SELECT title, question_type FROM {$wpdb->prefix}exam_questions WHERE id = %d", $qid)); if ($question) { $type_labels = Exam_Question::$question_types; $type = isset($type_labels[$question->question_type]) ? $type_labels[$question->question_type] : $question->question_type; $preview .= '<li>' . wp_trim_words(strip_tags($question->title), 15) . ' <em>(' . $type . ')</em></li>'; } } $preview .= '</ol>'; wp_send_json_success(array( 'preview' => $preview, 'paper_data' => array( 'question_ids' => $question_ids, 'total_questions' => count($question_ids) ) )); } } 第四章:前端考试界面与交互实现 4.1 考试短代码与页面模板 短代码类 (includes/class-shortcodes.php): class Exam_Shortcodes { public static function init() { // 短代码已在Exam_Manager中注册 add_action('wp_enqueue_scripts', array(__CLASS__, 'enqueue_frontend_scripts')); } public static function enqueue_frontend_scripts() { wp_enqueue_style('exam-frontend', EXAM_SYSTEM_URL . 'assets/css/frontend.css', array(), EXAM_SYSTEM_VERSION); wp_enqueue_script('exam-frontend', EXAM_SYSTEM_URL . 'assets/js/frontend.js', array('jquery'), EXAM_SYSTEM_VERSION, true); wp_localize_script('exam-frontend', 'exam_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('exam_frontend_nonce') )); } // 考试列表短代码 public static function shortcode_exam_list($atts) { if (!is_user_logged_in()) { return '<div class="exam-notice">请先登录后参加考试</div>'; } global $wpdb; $papers = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}exam_papers WHERE status = 1 ORDER BY id DESC"); $output = '<div class="exam-list-container">'; $output .= '<h2>可用考试</h2>'; if (empty($papers)) { $output .= '<p>暂无可用考试</p>'; } else { $output .= '<div class="exam-grid">'; foreach ($papers as $paper) { $settings = maybe_unserialize($paper->settings); $attempts_allowed = isset($settings['attempts_allowed']) ? $settings['attempts_allowed'] : 0; // 检查已考次数 $attempts = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d", get_current_user_id(), $paper->id )); $can_take = ($attempts_allowed == 0 || $attempts < $attempts_allowed); $output .= '<div class="exam-card">'; $output .= '<h3>' . esc_html($paper->paper_name) . '</h3>'; $output .= '<ul class="exam-meta">'; $output .= '<li>题数: ' . $paper->total_questions . '</li>'; $output .= '<li>时间: ' . $paper->time_limit . '分钟</li>'; $output .= '<li>及格分: ' . $paper->passing_score . '</li>'; $output .= '<li>已考次数: ' . $attempts . ($attempts_allowed > 0 ? '/' . $attempts_allowed : '') . '</li>'; $output .= '</ul>'; if ($can_take) { $output .= '<a href="?exam_id=' . $paper->id . '" class="button start-exam-btn">开始考试</a>'; } else { $output .= '<button class="button disabled" disabled>考试次数已用完</button>'; } // 查看历史成绩 $best_score = $wpdb->get_var($wpdb->prepare( "SELECT MAX(score) FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d", get_current_user_id(), $paper->id )); if ($best_score !== null) { $output .= '<div class="best-score">最佳成绩: ' . floatval($best_score) . '分</div>'; } $output .= '</div>'; } $output .= '</div>'; } $output .= '</div>'; return $output; } // 参加考试短代码 public static function shortcode_take_exam($atts) { if (!is_user_logged_in()) { return '<div class="exam-notice">请先登录后参加考试</div>'; } if (!isset($_GET['exam_id'])) { return '<div class="exam-notice">请选择要参加的考试</div>'; } $paper_id = intval($_GET['exam_id']); $user_id = get_current_user_id(); global $wpdb; // 检查考试次数限制 $paper = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_papers WHERE id = %d AND status = 1", $paper_id )); if (!$paper) { return '<div class="exam-error">考试不存在或已关闭</div>'; } $settings = maybe_unserialize($paper->settings); $attempts_allowed = isset($settings['attempts_allowed']) ? $settings['attempts_allowed'] : 0; $attempts = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d", $user_id, $paper_id )); if ($attempts_allowed > 0 && $attempts >= $attempts_allowed) { return '<div class="exam-error">您已达到该考试的最大尝试次数</div>'; } // 检查是否有未完成的考试 $unfinished = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d AND end_time IS NULL", $user_id, $paper_id )); if ($unfinished) { // 恢复未完成的考试 $record_id = $unfinished->id; $remaining_time = $paper->time_limit * 60 - (time() - strtotime($unfinished->start_time)); $remaining_time = max(0, $remaining_time); } else { // 创建新的考试记录 $wpdb->insert( $wpdb->prefix . 'exam_records', array( 'user_id' => $user_id, 'paper_id' => $paper_id, 'start_time' => current_time('mysql'), 'answers' => maybe_serialize(array()) ) ); $record_id = $wpdb->insert_id; $remaining_time = $paper->time_limit * 60; } // 获取试题 $question_ids = maybe_unserialize($paper->question_ids); if (empty($question_ids)) { return '<div class="exam-error">试卷试题配置错误</div>'; } $questions = array(); foreach ($question_ids as $qid) { $question = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_questions WHERE id = %d", $qid )); if ($question) { $question->options = maybe_unserialize($question->options); $questions[] = $question; } } // 输出考试界面
发表评论WordPress插件开发教程:实现网站广告位智能管理与投放 引言:为什么需要广告位智能管理插件 在当今的数字营销时代,网站广告已成为许多网站盈利的重要方式。然而,对于大多数WordPress网站管理员来说,广告管理常常是一个令人头疼的问题。手动插入广告代码不仅效率低下,而且难以实现精准投放、效果追踪和优化调整。传统的广告管理方式存在诸多局限性:广告位置固定无法根据用户行为调整、缺乏数据统计功能、无法实现A/B测试、难以针对不同设备展示不同广告等。 正是这些痛点催生了智能广告管理系统的需求。通过开发一个专业的WordPress插件,我们可以实现广告位的自动化、智能化管理,让广告投放变得更加高效和精准。本教程将带领您从零开始,开发一个功能完整的WordPress广告位智能管理插件,同时展示如何通过WordPress代码二次开发实现常用互联网小工具功能。 第一章:WordPress插件开发基础与环境搭建 1.1 WordPress插件架构概述 WordPress插件本质上是一组PHP文件,遵循特定的结构和规范,用于扩展WordPress核心功能。一个标准的WordPress插件至少包含一个主PHP文件,该文件头部包含特定的插件信息注释。插件可以包含CSS、JavaScript、图片资源以及其他PHP文件。 插件开发的核心原则包括:遵循WordPress编码标准、确保插件安全性和稳定性、提供清晰的用户界面、保持与WordPress核心和其他插件的兼容性。我们的广告管理插件将采用模块化设计,将不同功能分离到不同的类和文件中,便于维护和扩展。 1.2 开发环境配置 在开始开发之前,我们需要搭建合适的开发环境: 本地服务器环境:推荐使用XAMPP、MAMP或Local by Flywheel,它们提供了集成的Apache、MySQL和PHP环境。 WordPress安装:下载最新版本的WordPress,安装在本地服务器上。建议创建一个专门用于开发的测试站点。 代码编辑器:选择功能强大的代码编辑器,如Visual Studio Code、PHPStorm或Sublime Text。这些编辑器提供了代码高亮、智能提示和调试功能。 调试工具:在wp-config.php中启用WordPress调试模式: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 版本控制:使用Git进行版本控制,便于团队协作和代码管理。 1.3 创建插件基本结构 首先,在WordPress的wp-content/plugins目录下创建我们的插件文件夹,命名为"smart-ad-manager"。在该文件夹中创建以下基本文件结构: smart-ad-manager/ ├── smart-ad-manager.php # 主插件文件 ├── includes/ # 包含核心类文件 │ ├── class-ad-manager.php │ ├── class-ad-renderer.php │ └── class-ad-analytics.php ├── admin/ # 后台管理相关文件 │ ├── css/ │ ├── js/ │ └── partials/ ├── public/ # 前端相关文件 │ ├── css/ │ ├── js/ │ └── templates/ ├── assets/ # 静态资源 └── uninstall.php # 插件卸载脚本 接下来,编辑主插件文件smart-ad-manager.php,添加插件头部信息: <?php /** * Plugin Name: Smart Ad Manager * Plugin URI: https://yourwebsite.com/smart-ad-manager * Description: 智能广告位管理与投放系统,支持精准投放、效果追踪和优化调整。 * Version: 1.0.0 * Author: Your Name * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: smart-ad-manager * Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('SAM_VERSION', '1.0.0'); define('SAM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SAM_PLUGIN_URL', plugin_dir_url(__FILE__)); define('SAM_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 初始化插件 require_once SAM_PLUGIN_DIR . 'includes/class-ad-manager.php'; function run_smart_ad_manager() { $plugin = new Ad_Manager(); $plugin->run(); } run_smart_ad_manager(); 第二章:广告位管理系统的核心功能实现 2.1 数据库设计与广告位存储 广告数据需要存储在数据库中,以便持久化和高效查询。我们将创建自定义数据库表来存储广告信息: // 在Ad_Manager类中添加数据库创建方法 class Ad_Manager { public function create_ad_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'sam_ads'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL, ad_type varchar(50) NOT NULL, ad_code text NOT NULL, ad_image varchar(500), ad_link varchar(500), start_date datetime, end_date datetime, max_impressions int DEFAULT 0, current_impressions int DEFAULT 0, max_clicks int DEFAULT 0, current_clicks int DEFAULT 0, status varchar(20) DEFAULT 'active', targeting_rules text, priority int DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建广告位表 $positions_table = $wpdb->prefix . 'sam_ad_positions'; $sql2 = "CREATE TABLE IF NOT EXISTS $positions_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, position_key varchar(100) NOT NULL, position_name varchar(200) NOT NULL, description text, default_ad_id mediumint(9), max_ads int DEFAULT 1, dimensions varchar(50), allowed_types varchar(200), is_active tinyint(1) DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY position_key (position_key) ) $charset_collate;"; dbDelta($sql2); } // 插件激活时创建表 public static function activate() { self::create_ad_tables(); flush_rewrite_rules(); } } 2.2 广告位管理后台界面开发 创建一个用户友好的后台界面是插件成功的关键。我们将使用WordPress的设置API和自定义管理页面来构建广告管理后台: class Ad_Manager_Admin { private $ad_manager; public function __construct($ad_manager) { $this->ad_manager = $ad_manager; add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); add_action('wp_ajax_sam_save_ad', array($this, 'ajax_save_ad')); add_action('wp_ajax_sam_get_ads', array($this, 'ajax_get_ads')); } public function add_admin_menu() { // 主菜单 add_menu_page( '智能广告管理', '广告管理', 'manage_options', 'smart-ad-manager', array($this, 'display_main_page'), 'dashicons-megaphone', 30 ); // 子菜单 add_submenu_page( 'smart-ad-manager', '广告列表', '广告列表', 'manage_options', 'sam-ads', array($this, 'display_ads_page') ); add_submenu_page( 'smart-ad-manager', '广告位设置', '广告位设置', 'manage_options', 'sam-positions', array($this, 'display_positions_page') ); add_submenu_page( 'smart-ad-manager', '数据统计', '数据统计', 'manage_options', 'sam-analytics', array($this, 'display_analytics_page') ); } public function display_main_page() { include SAM_PLUGIN_DIR . 'admin/partials/dashboard.php'; } public function enqueue_admin_scripts($hook) { if (strpos($hook, 'smart-ad-manager') !== false) { wp_enqueue_style('sam-admin-style', SAM_PLUGIN_URL . 'admin/css/admin.css', array(), SAM_VERSION); wp_enqueue_script('sam-admin-script', SAM_PLUGIN_URL . 'admin/js/admin.js', array('jquery', 'jquery-ui-sortable'), SAM_VERSION, true); wp_localize_script('sam-admin-script', 'sam_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('sam_ajax_nonce') )); } } } 2.3 广告投放逻辑与智能匹配算法 智能广告投放的核心是根据多种条件匹配最合适的广告。我们将实现一个智能匹配算法: class Ad_Renderer { public function get_ad_for_position($position_key, $context = array()) { global $wpdb; // 获取广告位信息 $positions_table = $wpdb->prefix . 'sam_ad_positions'; $position = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $positions_table WHERE position_key = %s AND is_active = 1", $position_key )); if (!$position) { return ''; } // 获取该广告位可用的广告 $ads_table = $wpdb->prefix . 'sam_ads'; $query = "SELECT * FROM $ads_table WHERE status = 'active' AND (start_date IS NULL OR start_date <= NOW()) AND (end_date IS NULL OR end_date >= NOW())"; // 添加智能匹配条件 $conditions = array(); $params = array(); // 1. 设备类型匹配 if (isset($context['device'])) { $conditions[] = "(targeting_rules LIKE %s OR targeting_rules IS NULL OR targeting_rules = '')"; $params[] = '%"device":"' . $context['device'] . '"%'; } // 2. 用户地理位置匹配 if (isset($context['location'])) { $conditions[] = "(targeting_rules LIKE %s OR targeting_rules IS NULL OR targeting_rules = '')"; $params[] = '%"location":"' . $context['location'] . '"%'; } // 3. 页面类型匹配 if (isset($context['page_type'])) { $conditions[] = "(targeting_rules LIKE %s OR targeting_rules IS NULL OR targeting_rules = '')"; $params[] = '%"page_type":"' . $context['page_type'] . '"%'; } if (!empty($conditions)) { $query .= " AND (" . implode(" OR ", $conditions) . ")"; } $query .= " ORDER BY priority DESC, RAND() LIMIT %d"; $params[] = $position->max_ads; if (!empty($params)) { $ads = $wpdb->get_results($wpdb->prepare($query, $params)); } else { $ads = $wpdb->get_results($wpdb->prepare($query . " LIMIT %d", $position->max_ads)); } if (empty($ads)) { // 返回默认广告或空 return $this->get_default_ad($position); } // 渲染广告 return $this->render_ads($ads, $position); } private function render_ads($ads, $position) { $output = '<div class="sam-ad-container" data-position="' . esc_attr($position->position_key) . '">'; foreach ($ads as $ad) { $output .= $this->render_single_ad($ad); // 更新展示次数 $this->record_impression($ad->id); } $output .= '</div>'; return $output; } } 第三章:通过短代码和Widget实现广告位插入 3.1 创建广告短代码 短代码是WordPress中插入动态内容的简便方式。我们将创建广告短代码,让用户可以在文章、页面或侧边栏中轻松插入广告: class Ad_Shortcodes { public function init() { add_shortcode('display_ad', array($this, 'display_ad_shortcode')); add_shortcode('ad_position', array($this, 'ad_position_shortcode')); } public function display_ad_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'position' => '', 'class' => '', 'style' => '' ), $atts, 'display_ad'); if (!empty($atts['id'])) { // 显示特定ID的广告 return $this->render_ad_by_id($atts['id'], $atts); } elseif (!empty($atts['position'])) { // 显示特定广告位的广告 $renderer = new Ad_Renderer(); $context = $this->get_current_context(); return $renderer->get_ad_for_position($atts['position'], $context); } return ''; } public function ad_position_shortcode($atts) { $atts = shortcode_atts(array( 'key' => '', 'title' => '', 'description' => '', 'width' => 'auto', 'height' => 'auto' ), $atts, 'ad_position'); if (empty($atts['key'])) { return '<p>请指定广告位key</p>'; } // 检查广告位是否存在,不存在则创建 $this->create_position_if_not_exists($atts); $renderer = new Ad_Renderer(); $context = $this->get_current_context(); return $renderer->get_ad_for_position($atts['key'], $context); } private function get_current_context() { $context = array(); // 检测设备类型 $context['device'] = wp_is_mobile() ? 'mobile' : 'desktop'; // 检测页面类型 if (is_front_page()) { $context['page_type'] = 'home'; } elseif (is_single()) { $context['page_type'] = 'single'; } elseif (is_page()) { $context['page_type'] = 'page'; } elseif (is_category()) { $context['page_type'] = 'category'; } return $context; } } 3.2 开发广告位Widget Widget是WordPress侧边栏和小工具区域的内容块。我们将创建一个广告位Widget,让用户可以通过拖拽方式在侧边栏添加广告: class Ad_Position_Widget extends WP_Widget { public function __construct() { parent::__construct( 'sam_ad_widget', __('智能广告位', 'smart-ad-manager'), array( 'description' => __('在侧边栏显示智能广告', 'smart-ad-manager'), 'classname' => 'sam-ad-widget' ) ); } public function widget($args, $instance) { $title = apply_filters('widget_title', empty($instance['title']) ? '' : $instance['title'], $instance, $this->id_base); $position_key = empty($instance['position_key']) ? '' : $instance['position_key']; if (empty($position_key)) { return; } echo $args['before_widget']; if ($title) { echo $args['before_title'] . $title . $args['after_title']; } $renderer = new Ad_Renderer(); $context = array( 'device' => wp_is_mobile() ? 'mobile' : 'desktop', 'location' => 'sidebar' ); echo $renderer->get_ad_for_position($position_key, $context); echo $args['after_widget']; } public function form($instance) { global $wpdb; $title = isset($instance['title']) ? esc_attr($instance['title']) : ''; $position_key = isset($instance['position_key']) ? esc_attr($instance['position_key']) : ''; // 获取所有广告位 $positions_table = $wpdb->prefix . 'sam_ad_positions'; $positions = $wpdb->get_results("SELECT * FROM $positions_table WHERE is_active = 1 ORDER BY position_name"); ?> <p> <label for="<?php echo $this->get_field_id('title'); ?>"> <?php _e('标题:', 'smart-ad-manager'); ?> </label> <input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo $title; ?>"> </p> <p> <label for="<?php echo $this->get_field_id('position_key'); ?>"> <?php _e('选择广告位:', 'smart-ad-manager'); ?> </label> <select class="widefat" id="<?php echo $this->get_field_id('position_key'); ?>" name="<?php echo $this->get_field_name('position_key'); ?>"> <option value=""><?php _e('-- 请选择广告位 --', 'smart-ad-manager'); ?></option> <?php foreach ($positions as $position): ?> <option value="<?php echo esc_attr($position->position_key); ?>" <?php selected($position_key, $position->position_key); ?>> 第四章:广告效果追踪与数据分析系统 4.1 数据采集与存储机制 要实现智能广告管理,必须建立完善的数据追踪系统。我们需要记录广告的展示、点击、转化等关键指标: class Ad_Analytics { private $db; public function __construct() { global $wpdb; $this->db = $wpdb; // 创建数据追踪表 $this->create_tracking_tables(); // 注册追踪钩子 add_action('wp_ajax_nopriv_sam_track_click', array($this, 'track_click')); add_action('wp_ajax_sam_track_click', array($this, 'track_click')); add_action('wp_ajax_nopriv_sam_track_impression', array($this, 'track_impression')); add_action('wp_ajax_sam_track_impression', array($this, 'track_impression')); } private function create_tracking_tables() { $charset_collate = $this->db->get_charset_collate(); // 点击记录表 $clicks_table = $this->db->prefix . 'sam_ad_clicks'; $sql = "CREATE TABLE IF NOT EXISTS $clicks_table ( id bigint(20) NOT NULL AUTO_INCREMENT, ad_id mediumint(9) NOT NULL, user_id bigint(20), session_id varchar(100), ip_address varchar(45), user_agent text, referrer varchar(500), page_url varchar(500), click_time datetime DEFAULT CURRENT_TIMESTAMP, device_type varchar(20), browser varchar(100), os varchar(50), country varchar(100), city varchar(100), conversion_value decimal(10,2) DEFAULT 0, conversion_time datetime, PRIMARY KEY (id), KEY ad_id (ad_id), KEY click_time (click_time) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 展示记录表 $impressions_table = $this->db->prefix . 'sam_ad_impressions'; $sql2 = "CREATE TABLE IF NOT EXISTS $impressions_table ( id bigint(20) NOT NULL AUTO_INCREMENT, ad_id mediumint(9) NOT NULL, position_key varchar(100), user_id bigint(20), session_id varchar(100), ip_address varchar(45), impression_time datetime DEFAULT CURRENT_TIMESTAMP, device_type varchar(20), page_type varchar(50), is_unique tinyint(1) DEFAULT 1, PRIMARY KEY (id), KEY ad_id (ad_id), KEY impression_time (impression_time) ) $charset_collate;"; dbDelta($sql2); } public function track_click() { // 验证nonce check_ajax_referer('sam_tracking_nonce', 'security'); $ad_id = intval($_POST['ad_id']); $session_id = sanitize_text_field($_POST['session_id']); // 收集用户信息 $user_info = $this->collect_user_info(); // 记录点击 $this->db->insert( $this->db->prefix . 'sam_ad_clicks', array( 'ad_id' => $ad_id, 'user_id' => get_current_user_id(), 'session_id' => $session_id, 'ip_address' => $user_info['ip'], 'user_agent' => $user_info['user_agent'], 'referrer' => $user_info['referrer'], 'page_url' => $user_info['page_url'], 'device_type' => $user_info['device_type'], 'browser' => $user_info['browser'], 'os' => $user_info['os'], 'country' => $user_info['country'], 'city' => $user_info['city'] ) ); // 更新广告点击计数 $this->update_ad_click_count($ad_id); wp_die(); } private function collect_user_info() { $info = array(); // IP地址 $info['ip'] = $this->get_user_ip(); // User Agent $info['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? ''; // Referrer $info['referrer'] = $_SERVER['HTTP_REFERER'] ?? ''; // 当前页面URL $info['page_url'] = home_url(add_query_arg(array())); // 设备类型 $info['device_type'] = wp_is_mobile() ? 'mobile' : 'desktop'; // 浏览器和操作系统信息 $browser_info = $this->parse_user_agent($info['user_agent']); $info['browser'] = $browser_info['browser']; $info['os'] = $browser_info['os']; // 地理位置(可以使用第三方API或本地数据库) $info['country'] = $this->get_country_from_ip($info['ip']); $info['city'] = $this->get_city_from_ip($info['ip']); return $info; } } 4.2 数据统计与可视化展示 创建直观的数据统计界面,帮助管理员了解广告效果: class Ad_Statistics { public function display_statistics_page() { ?> <div class="wrap sam-statistics"> <h1><?php _e('广告数据统计', 'smart-ad-manager'); ?></h1> <div class="sam-stat-filters"> <form method="get" action=""> <input type="hidden" name="page" value="sam-analytics"> <label for="date_range"><?php _e('时间范围:', 'smart-ad-manager'); ?></label> <select name="date_range" id="date_range"> <option value="today"><?php _e('今天', 'smart-ad-manager'); ?></option> <option value="yesterday"><?php _e('昨天', 'smart-ad-manager'); ?></option> <option value="last7days"><?php _e('最近7天', 'smart-ad-manager'); ?></option> <option value="last30days"><?php _e('最近30天', 'smart-ad-manager'); ?></option> <option value="custom"><?php _e('自定义', 'smart-ad-manager'); ?></option> </select> <div id="custom-date-range" style="display:none;"> <input type="date" name="start_date" value="<?php echo date('Y-m-d', strtotime('-7 days')); ?>"> <input type="date" name="end_date" value="<?php echo date('Y-m-d'); ?>"> </div> <label for="ad_id"><?php _e('广告:', 'smart-ad-manager'); ?></label> <select name="ad_id" id="ad_id"> <option value=""><?php _e('所有广告', 'smart-ad-manager'); ?></option> <?php $this->render_ad_options(); ?> </select> <button type="submit" class="button button-primary"><?php _e('筛选', 'smart-ad-manager'); ?></button> </form> </div> <div class="sam-stat-overview"> <?php $this->display_overview_stats(); ?> </div> <div class="sam-stat-charts"> <div class="chart-container"> <h3><?php _e('点击率趋势', 'smart-ad-manager'); ?></h3> <canvas id="ctr-chart" width="400" height="200"></canvas> </div> <div class="chart-container"> <h3><?php _e('设备分布', 'smart-ad-manager'); ?></h3> <canvas id="device-chart" width="400" height="200"></canvas> </div> </div> <div class="sam-stat-details"> <h3><?php _e('详细数据', 'smart-ad-manager'); ?></h3> <?php $this->display_detailed_stats_table(); ?> </div> </div> <script> jQuery(document).ready(function($) { // 初始化图表 var ctrChart = new Chart($('#ctr-chart'), { type: 'line', data: { labels: <?php echo json_encode($this->get_chart_labels()); ?>, datasets: [{ label: '点击率(%)', data: <?php echo json_encode($this->get_ctr_data()); ?>, borderColor: 'rgb(75, 192, 192)', tension: 0.1 }] } }); var deviceChart = new Chart($('#device-chart'), { type: 'doughnut', data: { labels: ['桌面端', '移动端', '平板'], datasets: [{ data: <?php echo json_encode($this->get_device_distribution()); ?>, backgroundColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)' ] }] } }); }); </script> <?php } private function get_overview_stats($start_date = null, $end_date = null) { global $wpdb; $clicks_table = $wpdb->prefix . 'sam_ad_clicks'; $impressions_table = $wpdb->prefix . 'sam_ad_impressions'; $where_clause = ''; if ($start_date && $end_date) { $where_clause = $wpdb->prepare( "WHERE click_time >= %s AND click_time <= %s", $start_date . ' 00:00:00', $end_date . ' 23:59:59' ); } // 总点击量 $total_clicks = $wpdb->get_var( "SELECT COUNT(*) FROM $clicks_table $where_clause" ); // 总展示量 $total_impressions = $wpdb->get_var( "SELECT COUNT(*) FROM $impressions_table " . str_replace('click_time', 'impression_time', $where_clause) ); // 点击率 $ctr = $total_impressions > 0 ? round(($total_clicks / $total_impressions) * 100, 2) : 0; // 独立访客数 $unique_visitors = $wpdb->get_var( "SELECT COUNT(DISTINCT session_id) FROM $impressions_table " . str_replace('click_time', 'impression_time', $where_clause) ); return array( 'total_clicks' => $total_clicks, 'total_impressions' => $total_impressions, 'ctr' => $ctr, 'unique_visitors' => $unique_visitors ); } } 第五章:高级功能实现与优化 5.1 A/B测试功能 A/B测试是优化广告效果的重要手段。我们将实现一个简单的A/B测试系统: class Ad_AB_Testing { public function init() { add_action('sam_ad_selection', array($this, 'apply_ab_testing'), 10, 3); add_action('wp_ajax_sam_create_ab_test', array($this, 'create_ab_test')); } public function apply_ab_testing($ads, $position_key, $context) { // 检查该广告位是否有正在进行的A/B测试 $active_tests = $this->get_active_tests($position_key); if (empty($active_tests)) { return $ads; } foreach ($active_tests as $test) { // 根据测试分组分配用户 $user_group = $this->assign_user_to_group($test); if ($user_group === 'control') { // 控制组:显示原始广告 continue; } // 实验组:显示测试广告 $variant_ads = $this->get_test_variants($test->id); if (!empty($variant_ads)) { // 替换或添加测试广告 $ads = $this->merge_test_ads($ads, $variant_ads, $test); } } return $ads; } private function assign_user_to_group($test) { $session_id = $this->get_session_id(); // 检查用户是否已经分配了组别 $existing_group = $this->get_user_test_group($test->id, $session_id); if ($existing_group) { return $existing_group; } // 根据测试配置分配组别 $groups = array('control', 'variant_a', 'variant_b'); $weights = array( 'control' => $test->control_percentage, 'variant_a' => $test->variant_a_percentage, 'variant_b' => $test->variant_b_percentage ); $group = $this->weighted_random_selection($weights); // 记录用户分组 $this->record_user_group($test->id, $session_id, $group); return $group; } public function create_ab_test() { check_ajax_referer('sam_admin_nonce', 'nonce'); $test_data = array( 'name' => sanitize_text_field($_POST['test_name']), 'position_key' => sanitize_text_field($_POST['position_key']), 'control_ad_id' => intval($_POST['control_ad_id']), 'variant_a_ad_id' => intval($_POST['variant_a_ad_id']), 'variant_b_ad_id' => intval($_POST['variant_b_ad_id']), 'control_percentage' => intval($_POST['control_percentage']), 'variant_a_percentage' => intval($_POST['variant_a_percentage']), 'variant_b_percentage' => intval($_POST['variant_b_percentage']), 'start_date' => sanitize_text_field($_POST['start_date']), 'end_date' => sanitize_text_field($_POST['end_date']), 'primary_metric' => sanitize_text_field($_POST['primary_metric']), 'confidence_level' => floatval($_POST['confidence_level']) ); global $wpdb; $tests_table = $wpdb->prefix . 'sam_ab_tests'; $wpdb->insert($tests_table, $test_data); wp_send_json_success(array( 'message' => __('A/B测试创建成功', 'smart-ad-manager'), 'test_id' => $wpdb->insert_id )); } public function evaluate_test_results($test_id) { global $wpdb; $test = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}sam_ab_tests WHERE id = %d", $test_id )); if (!$test) { return false; } // 收集各组数据 $groups = array('control', 'variant_a', 'variant_b'); $results = array(); foreach ($groups as $group) { $ad_id_field = $group . '_ad_id'; $ad_id = $test->$ad_id_field; if (!$ad_id) { continue; } $results[$group] = array( 'impressions' => $this->get_ad_impressions($ad_id, $test->start_date, $test->end_date), 'clicks' => $this->get_ad_clicks($ad_id, $test->start_date, $test->end_date), 'conversions' => $this->get_ad_conversions($ad_id, $test->start_date, $test->end_date) ); // 计算指标 if ($results[$group]['impressions'] > 0) { $results[$group]['ctr'] = ($results[$group]['clicks'] / $results[$group]['impressions']) * 100; } else { $results[$group]['ctr'] = 0; } } // 统计显著性检验 $significance_results = $this->calculate_statistical_significance($results); // 判断测试结果 $conclusion = $this->draw_test_conclusion($significance_results, $test->confidence_level); // 更新测试状态 $this->update_test_status($test_id, 'completed', $conclusion); return array( 'results' => $results, 'significance' => $significance_results, 'conclusion' => $conclusion ); } } 5.2 智能优化算法 基于收集的数据,实现广告投放的智能优化: class Ad_Optimizer { private $learning_rate = 0.1; private $exploration_rate = 0.2; public function optimize_ad_selection($ads, $context) { if (empty($ads)) { return $ads; } // 计算每个广告的预期价值 $ad_values = array(); foreach ($ads as $ad) { $ad_values[$ad->id] = $this->calculate_expected_value($ad, $context); } // 探索-利用平衡 if (mt_rand(0, 100) / 100 < $this->exploration_rate) { // 探索:随机选择一个广告 $selected_key = array_rand($ads); } else { // 利用:选择价值最高的广告 arsort($ad_values); $selected_key = array_key_first($ad_values); } // 重新排序广告,将最优广告放在前面 $optimized_ads = array(); $optimized_ads[] = $ads[$selected_key];
发表评论实战教程:在WordPress网站中添加用户生成内容管理与审核系统 引言:用户生成内容的价值与挑战 在当今互联网环境中,用户生成内容(User-Generated Content, UGC)已成为网站互动性和价值的重要组成部分。从电商网站的产品评价、社交媒体平台的用户分享,到知识社区的问答讨论,UGC不仅丰富了网站内容,还增强了用户参与感和社区凝聚力。然而,UGC管理也带来了诸多挑战:垃圾信息泛滥、不当内容传播、版权问题以及内容质量参差不齐等。 对于WordPress网站管理员而言,构建一个高效、安全的UGC管理与审核系统至关重要。本教程将深入探讨如何通过WordPress代码二次开发,实现一个完整的用户生成内容管理与审核系统,同时集成常用互联网小工具功能,提升网站互动性和管理效率。 第一部分:系统架构设计与规划 1.1 用户生成内容类型分析 在开始开发前,我们需要明确网站需要管理的UGC类型: 评论系统:文章评论、产品评价 用户提交内容:文章投稿、图片上传、视频分享 社区互动:论坛帖子、问答内容 用户资料:个人简介、头像、联系方式 1.2 系统功能需求规划 一个完整的UGC管理系统应包含以下核心功能: 内容提交接口:用户友好的内容提交表单 审核工作流:多级审核机制与状态管理 垃圾过滤:自动识别与过滤垃圾内容 用户信誉系统:基于用户行为的信任评分 通知系统:审核状态通知与用户提醒 管理后台:高效的内容审核与管理界面 数据分析:UGC数据统计与报告 1.3 技术架构设计 我们将采用以下技术架构: 前端:HTML5、CSS3、JavaScript(jQuery/AJAX) 后端:PHP(WordPress核心API) 数据库:MySQL(WordPress数据库结构) 安全机制:Nonce验证、权限检查、数据过滤 第二部分:基础环境搭建与配置 2.1 创建自定义插件 首先,我们需要创建一个独立的WordPress插件来管理所有UGC功能: <?php /** * Plugin Name: UGC管理与审核系统 * Plugin URI: https://yourwebsite.com/ * Description: 用户生成内容管理与审核系统 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('UGC_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('UGC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('UGC_VERSION', '1.0.0'); // 初始化插件 function ugc_system_init() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { wp_die('本插件需要WordPress 5.0或更高版本'); } // 加载核心功能 require_once UGC_PLUGIN_PATH . 'includes/core-functions.php'; require_once UGC_PLUGIN_PATH . 'includes/submission-handler.php'; require_once UGC_PLUGIN_PATH . 'includes/moderation-system.php'; require_once UGC_PLUGIN_PATH . 'includes/admin-interface.php'; // 国际化支持 load_plugin_textdomain('ugc-system', false, dirname(plugin_basename(__FILE__)) . '/languages'); } add_action('plugins_loaded', 'ugc_system_init'); 2.2 创建数据库表结构 我们需要扩展WordPress数据库来存储UGC相关数据: // 在activation hook中创建数据库表 function ugc_create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'ugc_submissions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, content_type varchar(50) NOT NULL, title varchar(255), content longtext NOT NULL, status varchar(20) DEFAULT 'pending', moderation_notes text, moderated_by bigint(20), moderated_at datetime, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, metadata text, PRIMARY KEY (id), KEY user_id (user_id), KEY status (status), KEY content_type (content_type) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建审核日志表 $log_table = $wpdb->prefix . 'ugc_moderation_logs'; $sql_log = "CREATE TABLE IF NOT EXISTS $log_table ( id bigint(20) NOT NULL AUTO_INCREMENT, submission_id bigint(20) NOT NULL, moderator_id bigint(20) NOT NULL, action varchar(50) NOT NULL, notes text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY submission_id (submission_id), KEY moderator_id (moderator_id) ) $charset_collate;"; dbDelta($sql_log); } register_activation_hook(__FILE__, 'ugc_create_database_tables'); 第三部分:用户内容提交系统开发 3.1 创建内容提交表单 设计一个用户友好的内容提交界面: // 短代码生成内容提交表单 function ugc_submission_form_shortcode($atts) { // 只有登录用户才能提交内容 if (!is_user_logged_in()) { return '<div class="ugc-login-required">请<a href="' . wp_login_url(get_permalink()) . '">登录</a>后提交内容</div>'; } $atts = shortcode_atts(array( 'type' => 'article', 'category' => '', 'max_length' => 5000 ), $atts); ob_start(); ?> <div class="ugc-submission-form-container"> <form id="ugc-submission-form" class="ugc-form" method="post" enctype="multipart/form-data"> <?php wp_nonce_field('ugc_submission_action', 'ugc_submission_nonce'); ?> <input type="hidden" name="ugc_content_type" value="<?php echo esc_attr($atts['type']); ?>"> <div class="form-group"> <label for="ugc-title">标题 *</label> <input type="text" id="ugc-title" name="ugc_title" required maxlength="200" placeholder="请输入内容标题"> </div> <div class="form-group"> <label for="ugc-content">内容 *</label> <textarea id="ugc-content" name="ugc_content" rows="10" required maxlength="<?php echo esc_attr($atts['max_length']); ?>" placeholder="请输入详细内容..."></textarea> <div class="char-count"> <span id="char-counter">0</span>/<?php echo esc_attr($atts['max_length']); ?> 字符 </div> </div> <div class="form-group"> <label for="ugc-category">分类</label> <select id="ugc-category" name="ugc_category"> <option value="">选择分类</option> <?php $categories = get_categories(array('hide_empty' => false)); foreach ($categories as $category) { echo '<option value="' . esc_attr($category->term_id) . '">' . esc_html($category->name) . '</option>'; } ?> </select> </div> <div class="form-group"> <label for="ugc-tags">标签</label> <input type="text" id="ugc-tags" name="ugc_tags" placeholder="用逗号分隔多个标签"> </div> <div class="form-group"> <label for="ugc-attachments">附件</label> <input type="file" id="ugc-attachments" name="ugc_attachments[]" multiple accept="image/*,.pdf,.doc,.docx"> <p class="help-text">支持图片、PDF、Word文档,单个文件不超过5MB</p> </div> <div class="form-group"> <label> <input type="checkbox" name="ugc_terms" required> 我同意<a href="<?php echo get_permalink(get_page_by_path('terms')); ?>" target="_blank">服务条款</a> </label> </div> <div class="form-submit"> <button type="submit" class="ugc-submit-btn">提交内容</button> <div class="form-feedback" id="form-feedback"></div> </div> </form> </div> <script> jQuery(document).ready(function($) { // 字符计数 $('#ugc-content').on('input', function() { var length = $(this).val().length; $('#char-counter').text(length); }); // AJAX表单提交 $('#ugc-submission-form').on('submit', function(e) { e.preventDefault(); var formData = new FormData(this); var submitBtn = $(this).find('.ugc-submit-btn'); submitBtn.prop('disabled', true).text('提交中...'); $('#form-feedback').removeClass('success error').html(''); $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: formData, processData: false, contentType: false, success: function(response) { if (response.success) { $('#form-feedback').addClass('success').html(response.data.message); $('#ugc-submission-form')[0].reset(); $('#char-counter').text('0'); } else { $('#form-feedback').addClass('error').html(response.data); } submitBtn.prop('disabled', false).text('提交内容'); }, error: function() { $('#form-feedback').addClass('error').html('提交失败,请稍后重试'); submitBtn.prop('disabled', false).text('提交内容'); } }); }); }); </script> <?php // 添加样式 wp_enqueue_style('ugc-frontend-style', UGC_PLUGIN_URL . 'assets/css/frontend.css'); wp_enqueue_script('ugc-frontend-script', UGC_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), UGC_VERSION, true); return ob_get_clean(); } add_shortcode('ugc_submission_form', 'ugc_submission_form_shortcode'); 3.2 内容提交处理逻辑 // 处理AJAX内容提交 function handle_ugc_submission() { // 验证nonce if (!isset($_POST['ugc_submission_nonce']) || !wp_verify_nonce($_POST['ugc_submission_nonce'], 'ugc_submission_action')) { wp_die('安全验证失败'); } // 验证用户权限 if (!is_user_logged_in()) { wp_die('请先登录'); } $user_id = get_current_user_id(); $content_type = sanitize_text_field($_POST['ugc_content_type']); $title = sanitize_text_field($_POST['ugc_title']); $content = wp_kses_post($_POST['ugc_content']); $category = intval($_POST['ugc_category']); $tags = sanitize_text_field($_POST['ugc_tags']); // 基础验证 if (empty($title) || empty($content)) { wp_send_json_error('标题和内容不能为空'); } // 检查用户提交频率限制 if (!check_submission_rate_limit($user_id)) { wp_send_json_error('提交过于频繁,请稍后再试'); } // 垃圾内容检测 if (detect_spam_content($title . ' ' . $content)) { wp_send_json_error('内容疑似垃圾信息,请修改后重新提交'); } global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; // 准备元数据 $metadata = array( 'category_id' => $category, 'tags' => $tags, 'attachments' => array() ); // 处理文件上传 if (!empty($_FILES['ugc_attachments'])) { $attachments = handle_file_uploads($_FILES['ugc_attachments'], $user_id); if (is_wp_error($attachments)) { wp_send_json_error($attachments->get_error_message()); } $metadata['attachments'] = $attachments; } // 插入提交记录 $result = $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'content_type' => $content_type, 'title' => $title, 'content' => $content, 'status' => 'pending', 'metadata' => serialize($metadata), 'created_at' => current_time('mysql') ), array('%d', '%s', '%s', '%s', '%s', '%s', '%s') ); if ($result) { $submission_id = $wpdb->insert_id; // 发送通知邮件给管理员 send_moderation_notification($submission_id); // 记录用户活动 update_user_submission_stats($user_id); wp_send_json_success(array( 'message' => '内容提交成功,等待审核', 'submission_id' => $submission_id )); } else { wp_send_json_error('提交失败,请稍后重试'); } } add_action('wp_ajax_ugc_submit_content', 'handle_ugc_submission'); add_action('wp_ajax_nopriv_ugc_submit_content', 'handle_ugc_submission'); // 文件上传处理函数 function handle_file_uploads($files, $user_id) { require_once(ABSPATH . 'wp-admin/includes/file.php'); require_once(ABSPATH . 'wp-admin/includes/image.php'); require_once(ABSPATH . 'wp-admin/includes/media.php'); $attachments = array(); $upload_dir = wp_upload_dir(); $ugc_dir = $upload_dir['basedir'] . '/ugc-uploads/' . $user_id . '/' . date('Y/m'); // 创建目录 if (!file_exists($ugc_dir)) { wp_mkdir_p($ugc_dir); } $file_count = count($files['name']); for ($i = 0; $i < $file_count; $i++) { // 检查文件大小(5MB限制) if ($files['size'][$i] > 5 * 1024 * 1024) { continue; // 跳过过大文件 } // 检查文件类型 $file_type = wp_check_filetype($files['name'][$i]); $allowed_types = array('jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'); if (!in_array($file_type['ext'], $allowed_types)) { continue; // 跳过不允许的文件类型 } // 生成唯一文件名 $filename = wp_unique_filename($ugc_dir, $files['name'][$i]); $filepath = $ugc_dir . '/' . $filename; // 移动文件 if (move_uploaded_file($files['tmp_name'][$i], $filepath)) { $attachments[] = array( 'name' => $files['name'][$i], 'path' => str_replace($upload_dir['basedir'], '', $filepath), 'type' => $file_type['type'], 'size' => $files['size'][$i] ); } } return $attachments; } 第四部分:智能审核系统开发 4.1 多级审核工作流设计 // 审核状态管理类 class UGC_Moderation_Workflow { private $statuses = array( 'pending' => array( 'name' => '待审核', 'actions' => array('approve', 'reject', 'request_revision') ), 'under_review' => array( 'name' => '审核中', 'actions' => array('approve', 'reject', 'request_revision') ), 'approved' => array( 'name' => '已通过', 'actions' => array('unpublish', 'delete') ), 'rejected' => array( 'name' => '已拒绝', 'actions' => array('restore', 'delete') ), 'needs_revision' => array( 'name' => '需要修改', 'actions' => array('approve', 'reject') ), 'published' => array( 'name' => '已发布', 'actions' => array('unpublish', 'delete') ) ); // 获取下一个可用状态 public function get_next_status($current_status, $action) { $transitions = array( 'pending' => array( 'approve' => 'approved', 'reject' => 'rejected', 'request_revision' => 'needs_revision' ), 'needs_revision' => array( 'approve' => 'approved', 'reject' => 'rejected' ), 'approved' => array( 'publish' => 'published', 'unpublish' => 'pending', 'delete' => 'deleted' ), 'rejected' => array( 'restore' => 'pending', 'delete' => 'deleted' ), 'published' => array( 'unpublish' => 'approved', 'delete' => 'deleted' ) ); return isset($transitions[$current_status][$action]) ? $transitions[$current_status][$action] : $current_status; } // 执行审核操作 public function moderate_submission($submission_id, $action, $moderator_id, $notes = '') { global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; $log_table = $wpdb->prefix . 'ugc_moderation_logs'; // 获取当前状态 $current_status = $wpdb->get_var($wpdb->prepare( "SELECT status FROM $table_name WHERE id = %d", $submission_id )); if (!$current_status) { return new WP_Error('not_found', '提交内容不存在'); } // 验证操作是否允许 if (!in_array($action, $this->statuses[$current_status]['actions'])) { return new WP_Error('invalid_action', '当前状态下不允许此操作'); } // 获取新状态 $new_status = $this->get_next_status($current_status, $action); // 开始事务 $wpdb->query('START TRANSACTION'); try { // 更新提交状态 $update_result = $wpdb->update( $table_name, array( 'status' => $new_status, 'moderated_by' => $moderator_id, 'moderated_at' => current_time('mysql'), 'moderation_notes' => $notes ), array('id' => $submission_id), array('%s', '%d', '%s', '%s'), array('%d') ); if (!$update_result) { throw new Exception('更新状态失败'); } // 记录审核日志 $log_result = $wpdb->insert( $log_table, array( 'submission_id' => $submission_id, 'moderator_id' => $moderator_id, 'action' => $action, 'notes' => $notes ), array('%d', '%d', '%s', '%s') ); if (!$log_result) { throw new Exception('记录日志失败'); } // 如果状态变为已发布,创建正式内容 if ($new_status === 'published') { $this->publish_submission($submission_id); } // 发送通知给用户 $this->send_status_notification($submission_id, $new_status, $notes); $wpdb->query('COMMIT'); return array( 'success' => true, 'new_status' => $new_status, 'message' => '审核操作成功' ); } catch (Exception $e) { $wpdb->query('ROLLBACK'); return new WP_Error('transaction_failed', $e->getMessage()); } } // 发布提交内容 private function publish_submission($submission_id) { global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; $submission = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $submission_id )); if (!$submission) { return false; } $metadata = unserialize($submission->metadata); // 创建WordPress文章 $post_data = array( 'post_title' => $submission->title, 'post_content' => $submission->content, 'post_status' => 'publish', 'post_author' => $submission->user_id, 'post_type' => 'post' ); if (!empty($metadata['category_id'])) { $post_data['post_category'] = array($metadata['category_id']); } $post_id = wp_insert_post($post_data); if ($post_id && !empty($metadata['tags'])) { wp_set_post_tags($post_id, $metadata['tags']); } // 更新提交记录中的文章ID $wpdb->update( $table_name, array('published_post_id' => $post_id), array('id' => $submission_id), array('%d'), array('%d') ); return $post_id; } // 发送状态通知 private function send_status_notification($submission_id, $status, $notes = '') { global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; $submission = $wpdb->get_row($wpdb->prepare( "SELECT user_id, title FROM $table_name WHERE id = %d", $submission_id )); if (!$submission) { return false; } $user = get_user_by('id', $submission->user_id); if (!$user) { return false; } $status_names = array( 'approved' => '已通过', 'rejected' => '已拒绝', 'published' => '已发布', 'needs_revision' => '需要修改' ); $subject = sprintf('您的投稿"%s"状态已更新', $submission->title); $message = sprintf( "您好 %s,nn您的投稿《%s》状态已更新为:%snn", $user->display_name, $submission->title, $status_names[$status] ?? $status ); if (!empty($notes)) { $message .= "审核备注:n" . $notes . "nn"; } $message .= "您可以登录网站查看详情。nn"; $message .= get_bloginfo('name'); wp_mail($user->user_email, $subject, $message); return true; } } 4.2 智能垃圾内容检测 // 垃圾内容检测系统 class UGC_Spam_Detection { private $spam_keywords = array(); private $spam_patterns = array(); public function __construct() { // 加载垃圾关键词库 $this->load_spam_keywords(); $this->load_spam_patterns(); } // 检测内容是否为垃圾 public function detect($content, $author_id = 0) { $score = 0; $reasons = array(); // 1. 关键词检测 $keyword_score = $this->check_keywords($content); if ($keyword_score > 0) { $score += $keyword_score; $reasons[] = '包含垃圾关键词'; } // 2. 链接检测 $link_score = $this->check_links($content); if ($link_score > 0) { $score += $link_score; $reasons[] = '包含可疑链接'; } // 3. 重复内容检测 $duplicate_score = $this->check_duplicate($content); if ($duplicate_score > 0) { $score += $duplicate_score; $reasons[] = '疑似重复内容'; } // 4. 用户行为分析 if ($author_id > 0) { $user_score = $this->check_user_behavior($author_id); if ($user_score > 0) { $score += $user_score; $reasons[] = '用户行为可疑'; } } // 5. 模式匹配 $pattern_score = $this->check_patterns($content); if ($pattern_score > 0) { $score += $pattern_score; $reasons[] = '匹配垃圾内容模式'; } return array( 'score' => $score, 'is_spam' => $score >= 5, // 阈值设为5分 'reasons' => $reasons, 'details' => array( 'keyword_score' => $keyword_score, 'link_score' => $link_score, 'duplicate_score' => $duplicate_score, 'user_score' => $user_score ?? 0, 'pattern_score' => $pattern_score ) ); } // 关键词检测 private function check_keywords($content) { $score = 0; $content_lower = strtolower($content); foreach ($this->spam_keywords as $keyword => $weight) { if (strpos($content_lower, $keyword) !== false) { $score += $weight; } } return min($score, 3); // 最高3分 } // 链接检测 private function check_links($content) { $score = 0; // 提取所有链接 preg_match_all('/https?://[^s]+/', $content, $matches); $links = $matches[0] ?? array(); if (count($links) > 3) { $score += 2; // 链接过多 } // 检查可疑域名 $suspicious_domains = array('.ru', '.cn', '.xyz', '.top', '.club'); foreach ($links as $link) { foreach ($suspicious_domains as $domain) { if (strpos($link, $domain) !== false) { $score += 1; break; } } } return min($score, 3); } // 重复内容检测 private function check_duplicate($content) { global $wpdb; // 计算内容哈希 $content_hash = md5(trim($content)); // 检查最近24小时内的重复内容 $table_name = $wpdb->prefix . 'ugc_submissions'; $one_day_ago = date('Y-m-d H:i:s', strtotime('-24 hours')); $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE MD5(content) = %s AND created_at > %s", $content_hash, $one_day_ago )); return $count > 0 ? 2 : 0; } // 用户行为分析 private function check_user_behavior($user_id) { global $wpdb; $score = 0; $table_name = $wpdb->prefix . 'ugc_submissions'; // 检查用户提交频率 $one_hour_ago = date('Y-m-d H:i:s', strtotime('-1 hour')); $recent_submissions = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND created_at > %s", $user_id, $one_hour_ago )); if ($recent_submissions > 5) { $score += 2; // 1小时内提交超过5次 } // 检查用户被拒绝率 $total_submissions = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d", $user_id )); if ($total_submissions > 0) { $rejected_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND status = 'rejected'", $user_id )); $rejection_rate = $rejected_count / $total_submissions; if ($rejection_rate > 0.5) { $score += 2; // 拒绝率超过50% } } return min($score, 3); } // 模式匹配检测 private function check_patterns($content) { $score = 0; foreach ($this->spam_patterns as $pattern => $weight) { if (preg_match($pattern, $content)) { $score += $weight; } } return min($score, 2); } // 加载垃圾关键词 private function load_spam_keywords() { // 可以从文件或数据库加载 $this->spam_keywords = array( 'viagra' => 2, 'casino' => 2, 'loan' => 1, 'mortgage' => 1, 'click here' => 1, 'buy now' => 1, 'discount' => 1, 'free' => 1, 'win' => 1, 'prize' => 1, // 添加更多关键词... ); } // 加载垃圾模式 private function load_spam_patterns() { $this->spam_patterns = array( '/[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,}/i' => 1, // 邮箱地址 '/bd{3}[-.]?d{3}[-.]?d{4}b/' => 1, // 电话号码 '/bd{4}[- ]?d{4}[- ]?d{4}[- ]?d{4}b/' => 2, // 信用卡号 '/http:///' => 1, // HTTP链接(非HTTPS) ); } } 第五部分:管理后台界面开发 5.1 审核管理页面 // 创建审核管理后台页面 class UGC_Admin_Interface { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); add_action('wp_ajax_ugc_bulk_action', array($this, 'handle_bulk_action')); } // 添加管理菜单 public function add_admin_menu() { add_menu_page( 'UGC管理', 'UGC管理', 'manage_options', 'ugc-management', array($this, 'render_main_page'), 'dashicons-admin-comments', 30 ); add_submenu_page( 'ugc-management', '内容审核', '内容审核', 'manage_options', 'ugc-moderation', array($this, 'render_moderation_page') ); add_submenu_page( 'ugc-management', '审核日志', '审核日志', 'manage_options', 'ugc-logs', array($this, 'render_logs_page') ); add_submenu_page( 'ugc-management', '垃圾检测', '垃圾检测', 'manage_options', 'ugc-spam', array($this, 'render_spam_page') ); add_submenu_page( 'ugc-management', '用户信誉', '用户信誉', 'manage_options', 'ugc-reputation', array($this, 'render_reputation_page') ); } // 渲染审核页面 public function render_moderation_page() { global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; // 获取筛选参数 $status = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : 'pending'; $type = isset($_GET['type']) ? sanitize_text_field($_GET['type']) : ''; $page = isset($_GET['paged']) ? intval($_GET['paged']) : 1; $per_page = 20; $offset = ($page - 1) * $per_page; // 构建查询 $where = array('1=1'); $params = array(); if ($status) { $where[] = 'status = %s'; $params[] = $status; } if ($type) { $where[] = 'content_type = %s'; $params[] = $type; } $where_clause = implode(' AND ', $where); // 获取总数 $count_query = "SELECT COUNT(*) FROM $table_name WHERE $where_clause"; if ($params) { $count_query = $wpdb->prepare($count_query, $params); } $total_items = $wpdb->get_var($count_query); // 获取数据 $data_query = "SELECT * FROM $table_name WHERE $where_clause ORDER BY created_at DESC LIMIT %d OFFSET %d"; $params[] = $per_page; $params[] = $offset; $submissions = $wpdb->get_results($wpdb->prepare($data_query, $params)); ?> <div class="wrap"> <h1 class="wp-heading-inline">内容审核</h1> <!-- 筛选器 --> <div class="ugc-filters"> <form method="get" action=""> <input type="hidden" name="page" value="ugc-moderation"> <select name="status" onchange="this.form.submit()"> <option value="">所有状态</option> <?php $statuses = array( 'pending' => '待审核', 'under_review' => '审核中',
发表评论