手把手教学:为WordPress集成多因素身份验证与登录保护 引言:为什么WordPress需要多因素身份验证? 在当今数字化时代,网络安全已成为网站运营者不可忽视的重要议题。WordPress作为全球最受欢迎的内容管理系统,占据了互联网近43%的网站份额,也因此成为黑客攻击的主要目标。根据Wordfence安全报告,仅2023年就有超过13亿次针对WordPress网站的攻击尝试。 传统的用户名和密码认证方式已不足以应对日益复杂的网络威胁。密码泄露、暴力破解和社会工程学攻击使得单因素认证变得脆弱。多因素身份验证(MFA)通过要求用户提供两种或更多验证因素,极大地增强了账户安全性。本文将手把手教您如何通过WordPress代码二次开发,集成多因素身份验证与登录保护功能,同时实现一些常用互联网小工具功能。 第一部分:多因素身份验证基础与原理 1.1 多因素身份验证的类型 多因素身份验证通常基于以下三种类型的因素组合: 知识因素:用户知道的信息,如密码、PIN码或安全问题答案 持有因素:用户拥有的物理设备,如手机、安全密钥或智能卡 固有因素:用户本身的生物特征,如指纹、面部识别或虹膜扫描 1.2 常见的MFA实现方式 基于时间的一次性密码(TOTP):使用手机应用(如Google Authenticator)生成随时间变化的6位数字代码 短信/邮件验证码:通过短信或电子邮件发送一次性验证码 推送通知:向已认证设备发送登录请求确认 生物识别:使用指纹、面部识别等生物特征 硬件安全密钥:如YubiKey等物理设备 1.3 WordPress安全现状分析 默认情况下,WordPress仅提供基本的用户名和密码认证。虽然有许多插件可以提供MFA功能,但通过代码二次开发实现有以下优势: 减少插件依赖:降低插件冲突风险 性能优化:定制化代码通常比通用插件更高效 完全控制:可以根据具体需求定制功能 学习价值:深入理解WordPress认证机制 第二部分:开发环境准备与基础设置 2.1 开发环境配置 在开始编码之前,请确保您已准备好以下环境: 本地开发环境:推荐使用Local by Flywheel、XAMPP或MAMP 代码编辑器:VS Code、PHPStorm或Sublime Text 测试WordPress安装:建议使用最新版本的WordPress 备份机制:确保可以随时恢复原始状态 2.2 创建自定义插件框架 首先,我们创建一个专门用于MFA功能的插件: <?php /** * Plugin Name: 高级登录保护与MFA * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress添加多因素身份验证和增强登录保护功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('MFA_PLUGIN_VERSION', '1.0.0'); define('MFA_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('MFA_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 add_action('plugins_loaded', 'mfa_plugin_init'); function mfa_plugin_init() { // 检查必要扩展 if (!extension_loaded('gd')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>高级登录保护插件需要GD扩展支持,请启用PHP的GD扩展。</p></div>'; }); return; } // 加载核心类 require_once MFA_PLUGIN_PATH . 'includes/class-mfa-core.php'; require_once MFA_PLUGIN_PATH . 'includes/class-totp-authenticator.php'; require_once MFA_PLUGIN_PATH . 'includes/class-login-protection.php'; // 初始化核心功能 $mfa_core = MFA_Core::get_instance(); } 第三部分:实现TOTP多因素身份验证 3.1 TOTP原理与实现 基于时间的一次性密码算法是当前最流行的MFA实现方式之一。以下是实现TOTP的核心类: <?php // includes/class-totp-authenticator.php class TOTP_Authenticator { private $secret_length = 16; private $time_step = 30; // 时间步长(秒) private $code_length = 6; // 验证码长度 /** * 生成随机密钥 */ public function generate_secret() { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // Base32字符集 $secret = ''; for ($i = 0; $i < $this->secret_length; $i++) { $secret .= $chars[random_int(0, 31)]; } return $secret; } /** * 生成TOTP验证码 */ public function generate_code($secret) { $time = floor(time() / $this->time_step); $time = pack('J', $time); // 64位大端字节序 $secret = $this->base32_decode($secret); $hash = hash_hmac('sha1', $time, $secret, true); // 动态截取 $offset = ord($hash[19]) & 0xF; $code = ( ((ord($hash[$offset]) & 0x7F) << 24) | ((ord($hash[$offset + 1]) & 0xFF) << 16) | ((ord($hash[$offset + 2]) & 0xFF) << 8) | (ord($hash[$offset + 3]) & 0xFF) ) % pow(10, $this->code_length); return str_pad($code, $this->code_length, '0', STR_PAD_LEFT); } /** * 验证TOTP代码 */ public function verify_code($secret, $code, $discrepancy = 1) { $current_time = floor(time() / $this->time_step); // 允许时间偏差 for ($i = -$discrepancy; $i <= $discrepancy; $i++) { $time = pack('J', $current_time + $i); $secret_decoded = $this->base32_decode($secret); $hash = hash_hmac('sha1', $time, $secret_decoded, true); $offset = ord($hash[19]) & 0xF; $calculated_code = ( ((ord($hash[$offset]) & 0x7F) << 24) | ((ord($hash[$offset + 1]) & 0xFF) << 16) | ((ord($hash[$offset + 2]) & 0xFF) << 8) | (ord($hash[$offset + 3]) & 0xFF) ) % pow(10, $this->code_length); $calculated_code = str_pad($calculated_code, $this->code_length, '0', STR_PAD_LEFT); if (hash_equals($calculated_code, $code)) { return true; } } return false; } /** * Base32解码 */ private function base32_decode($secret) { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; $buffer = 0; $bitsLeft = 0; $output = ''; for ($i = 0; $i < strlen($secret); $i++) { $char = $secret[$i]; $value = strpos($chars, $char); if ($value === false) { continue; } $buffer <<= 5; $buffer |= $value; $bitsLeft += 5; if ($bitsLeft >= 8) { $bitsLeft -= 8; $output .= chr(($buffer >> $bitsLeft) & 0xFF); } } return $output; } /** * 生成QR码URL(用于Google Authenticator等应用) */ public function generate_qr_url($secret, $username, $issuer = 'WordPress') { $label = rawurlencode($issuer . ':' . $username); $parameters = array( 'secret' => $secret, 'issuer' => $issuer, 'digits' => $this->code_length, 'period' => $this->time_step, ); $url = 'otpauth://totp/' . $label . '?' . http_build_query($parameters); return $url; } } 3.2 用户MFA设置界面 接下来,我们需要为用户创建MFA设置界面: <?php // includes/class-mfa-user-settings.php class MFA_User_Settings { public function __construct() { // 在用户个人资料页面添加MFA设置 add_action('show_user_profile', array($this, 'add_mfa_settings_fields')); add_action('edit_user_profile', array($this, 'add_mfa_settings_fields')); // 保存MFA设置 add_action('personal_options_update', array($this, 'save_mfa_settings')); add_action('edit_user_profile_update', array($this, 'save_mfa_settings')); // 添加AJAX处理 add_action('wp_ajax_generate_mfa_secret', array($this, 'ajax_generate_secret')); add_action('wp_ajax_verify_mfa_code', array($this, 'ajax_verify_code')); } /** * 在用户资料页面添加MFA设置字段 */ public function add_mfa_settings_fields($user) { // 只有管理员或用户自己可以查看 if (!current_user_can('edit_user', $user->ID)) { return; } $mfa_enabled = get_user_meta($user->ID, 'mfa_enabled', true); $mfa_secret = get_user_meta($user->ID, 'mfa_secret', true); $backup_codes = get_user_meta($user->ID, 'mfa_backup_codes', true); // 如果还没有密钥,生成一个 if (empty($mfa_secret)) { $totp = new TOTP_Authenticator(); $mfa_secret = $totp->generate_secret(); update_user_meta($user->ID, 'mfa_secret', $mfa_secret); } ?> <h3>多因素身份验证设置</h3> <table class="form-table"> <tr> <th><label for="mfa_enabled">启用MFA</label></th> <td> <input type="checkbox" name="mfa_enabled" id="mfa_enabled" value="1" <?php checked($mfa_enabled, '1'); ?> /> <span class="description">启用多因素身份验证以增强账户安全</span> </td> </tr> <?php if ($mfa_enabled): ?> <tr> <th>MFA状态</th> <td> <span style="color: green; font-weight: bold;">✓ 已启用</span> <p class="description">您的账户已受到多因素身份验证保护</p> </td> </tr> <?php endif; ?> <tr> <th>设置MFA</th> <td> <div id="mfa-setup-container"> <ol> <li>在手机上安装Google Authenticator或类似应用</li> <li>点击下方按钮生成二维码</li> <li>使用应用扫描二维码</li> <li>输入应用生成的6位验证码进行验证</li> </ol> <div id="mfa-qr-container" style="display: none;"> <div id="mfa-qr-code"></div> <p><strong>密钥:</strong> <code id="mfa-secret-text"><?php echo esc_html($mfa_secret); ?></code></p> <p class="description">如果无法扫描二维码,可以手动输入上方密钥</p> </div> <button type="button" id="show-mfa-qr" class="button">显示二维码</button> <div style="margin-top: 15px;"> <label for="mfa-verify-code">验证码:</label> <input type="text" id="mfa-verify-code" maxlength="6" style="width: 100px;" /> <button type="button" id="verify-mfa-code" class="button">验证并启用MFA</button> <span id="mfa-verify-result" style="margin-left: 10px;"></span> </div> </div> </td> </tr> <?php if (!empty($backup_codes)): ?> <tr> <th>备用验证码</th> <td> <p>以下是一次性备用验证码(每个仅能使用一次):</p> <div style="background: #f5f5f5; padding: 10px; font-family: monospace;"> <?php foreach ($backup_codes as $code) { if ($code['used'] == 0) { echo '<div>' . esc_html($code['code']) . '</div>'; } } ?> </div> <button type="button" id="generate-new-backup-codes" class="button">生成新的备用验证码</button> </td> </tr> <?php endif; ?> </table> <script type="text/javascript"> jQuery(document).ready(function($) { // 显示二维码 $('#show-mfa-qr').on('click', function() { $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'generate_mfa_secret', user_id: <?php echo $user->ID; ?>, nonce: '<?php echo wp_create_nonce('mfa_nonce'); ?>' }, success: function(response) { if (response.success) { $('#mfa-qr-container').show(); $('#mfa-qr-code').html('<img src="' + response.data.qr_url + '" />'); $('#mfa-secret-text').text(response.data.secret); } } }); }); // 验证MFA代码 $('#verify-mfa-code').on('click', function() { var code = $('#mfa-verify-code').val(); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'verify_mfa_code', user_id: <?php echo $user->ID; ?>, code: code, nonce: '<?php echo wp_create_nonce('mfa_nonce'); ?>' }, success: function(response) { if (response.success) { $('#mfa-verify-result').html('<span style="color: green;">✓ 验证成功!MFA已启用。</span>'); $('#mfa_enabled').prop('checked', true); } else { $('#mfa-verify-result').html('<span style="color: red;">✗ 验证失败,请重试。</span>'); } } }); }); }); </script> <?php } /** * 保存MFA设置 */ public function save_mfa_settings($user_id) { if (!current_user_can('edit_user', $user_id)) { return false; } // 验证nonce if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'update-user_' . $user_id)) { return false; } // 保存MFA启用状态 if (isset($_POST['mfa_enabled'])) { update_user_meta($user_id, 'mfa_enabled', '1'); } else { delete_user_meta($user_id, 'mfa_enabled'); } return true; } /** * AJAX生成MFA密钥和二维码 */ public function ajax_generate_secret() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'mfa_nonce')) { wp_die('安全验证失败'); } $user_id = intval($_POST['user_id']); $current_user_id = get_current_user_id(); // 验证权限 if ($user_id != $current_user_id && !current_user_can('edit_user', $user_id)) { wp_die('权限不足'); } $totp = new TOTP_Authenticator(); $secret = get_user_meta($user_id, 'mfa_secret', true); if (empty($secret)) { $secret = $totp->generate_secret(); update_user_meta($user_id, 'mfa_secret', $secret); } $user_info = get_userdata($user_id); $qr_url = $totp->generate_qr_url($secret, $user_info->user_login, get_bloginfo('name')); // 生成QR码图片 require_once MFA_PLUGIN_PATH . 'includes/class-qr-generator.php'; $qr_generator = new QR_Generator(); $qr_image_url = $qr_generator->generate($qr_url); wp_send_json_success(array( 'secret' => $secret, 'qr_url' => $qr_image_url )); } /** * AJAX验证MFA代码 */ public function ajax_verify_code() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'mfa_nonce')) { wp_die('安全验证失败'); } $user_id = intval($_POST['user_id']); $code = sanitize_text_field($_POST['code']); $current_user_id = get_current_user_id(); // 验证权限 if ($user_id != $current_user_id && !current_user_can('edit_user', $user_id)) { wp_die('权限不足'); } $secret = get_user_meta($user_id, 'mfa_secret', true); if (empty($secret)) { wp_send_json_error('未找到MFA密钥'); } $totp = new TOTP_Authenticator(); $is_valid = $totp->verify_code($secret, $code); if ($is_valid) { // 启用MFA update_user_meta($user_id, 'mfa_enabled', '1'); // 生成备用验证码 $this->generate_backup_codes($user_id); wp_send_json_success('验证成功'); } else { wp_send_json_error('验证码无效'); } } /** * 生成备用验证码 */ private function generate_backup_codes($user_id) { $backup_codes = array(); for ($i = 0; $i < 10; $i++) { $code = ''; for ($j = 0; $j < 8; $j++) { $code .= random_int(0, 9); } $backup_codes[] = array( 'code' => $code, 'used' => 0 ); } update_user_meta($user_id, 'mfa_backup_codes', $backup_codes); return $backup_codes; } } ### 3.3 集成QR码生成功能 <?php// includes/class-qr-generator.php class QR_Generator { public function generate($data, $size = 200) { // 使用PHP GD库生成QR码 if (!function_exists('imagecreate')) { return $this->generate_via_api($data, $size); } // 简单QR码实现(实际项目中建议使用专业库如phpqrcode) $qr_size = $size; $margin = 10; $module_size = ($qr_size - 2 * $margin) / 21; // 简单QR码版本 $image = imagecreate($qr_size, $qr_size); // 颜色 $white = imagecolorallocate($image, 255, 255, 255); $black = imagecolorallocate($image, 0, 0, 0); // 填充白色背景 imagefilledrectangle($image, 0, 0, $qr_size, $qr_size, $white); // 生成简单QR码图案(实际应使用专业算法) $this->draw_simple_qr($image, $data, $margin, $module_size, $black); // 保存临时文件 $upload_dir = wp_upload_dir(); $temp_filename = 'qr_' . md5($data . time()) . '.png'; $temp_path = $upload_dir['path'] . '/' . $temp_filename; $temp_url = $upload_dir['url'] . '/' . $temp_filename; imagepng($image, $temp_path); imagedestroy($image); // 清理旧文件 $this->cleanup_old_qr_files(); return $temp_url; } private function draw_simple_qr($image, $data, $margin, $module_size, $color) { // 简化的QR码绘制逻辑 // 实际项目中应使用专业QR码生成库 $hash = md5($data); $hash_binary = ''; // 将哈希转换为二进制字符串 for ($i = 0; $i < strlen($hash); $i++) { $char = $hash[$i]; $binary = str_pad(base_convert($char, 16, 2), 4, '0', STR_PAD_LEFT); $hash_binary .= $binary; } // 绘制简单的QR码图案 $bits = str_split($hash_binary); $bit_index = 0; for ($y = 0; $y < 21; $y++) { for ($x = 0; $x < 21; $x++) { if ($bit_index >= count($bits)) { $bit_index = 0; } $bit = $bits[$bit_index]; $bit_index++; if ($bit == '1') { $x_pos = $margin + $x * $module_size; $y_pos = $margin + $y * $module_size; imagefilledrectangle( $image, $x_pos, $y_pos, $x_pos + $module_size, $y_pos + $module_size, $color ); } } } // 添加定位标记(简化版) $this->draw_finder_pattern($image, $margin, $margin, $module_size, $color); $this->draw_finder_pattern($image, $margin + 14 * $module_size, $margin, $module_size, $color); $this->draw_finder_pattern($image, $margin, $margin + 14 * $module_size, $module_size, $color); } private function draw_finder_pattern($image, $x, $y, $module_size, $color) { // 绘制7x7的定位标记 for ($i = 0; $i < 7; $i++) { for ($j = 0; $j < 7; $j++) { if ($i == 0 || $i == 6 || $j == 0 || $j == 6 || ($i >= 2 && $i <= 4 && $j >= 2 && $j <= 4)) { $x_pos = $x + $i * $module_size; $y_pos = $y + $j * $module_size; imagefilledrectangle( $image, $x_pos, $y_pos, $x_pos + $module_size, $y_pos + $module_size, $color ); } } } } private function generate_via_api($data, $size) { // 使用外部API生成QR码(备用方案) $encoded_data = urlencode($data); $api_url = "https://api.qrserver.com/v1/create-qr-code/?size={$size}x{$size}&data={$encoded_data}"; return $api_url; } private function cleanup_old_qr_files() { $upload_dir = wp_upload_dir(); $files = glob($upload_dir['path'] . '/qr_*.png'); // 删除超过1小时的旧文件 $now = time(); foreach ($files as $file) { if ($now - filemtime($file) > 3600) { @unlink($file); } } } } ## 第四部分:增强登录保护机制 ### 4.1 登录尝试限制与IP封锁 <?php// includes/class-login-protection.php class Login_Protection { private $max_attempts = 5; private $lockout_time = 900; // 15分钟 private $max_lockouts = 3; private $extended_lockout_time = 86400; // 24小时 public function __construct() { // 登录验证钩子 add_filter('authenticate', array($this, 'check_login_attempts'), 30, 3); // 登录成功/失败钩子 add_action('wp_login_failed', array($this, 'record_failed_attempt')); add_action('wp_login', array($this, 'clear_login_attempts')); // 添加登录页面保护 add_action('login_form', array($this, 'add_login_protection')); // 添加AJAX处理 add_action('wp_ajax_nopriv_check_login_lock', array($this, 'ajax_check_login_lock')); } /** * 检查登录尝试次数 */ public function check_login_attempts($user, $username, $password) { // 如果已经通过其他方式验证,直接返回 if (is_wp_error($user) && $user->get_error_code() !== 'empty_username' && $user->get_error_code() !== 'empty_password') { return $user; } $ip = $this->get_client_ip(); $attempts = $this->get_login_attempts($ip); // 检查是否被封锁 if ($this->is_ip_locked($ip)) { $lockout_time = $this->get_lockout_time($ip); $remaining = $lockout_time - time(); return new WP_Error( 'ip_locked', sprintf( '登录尝试次数过多。请等待 %d 分 %d 秒后再试。', floor($remaining / 60), $remaining % 60 ) ); } // 检查尝试次数 if ($attempts >= $this->max_attempts) { $this->lock_ip($ip); return new WP_Error( 'too_many_attempts', '登录尝试次数过多。您的IP已被暂时封锁。' ); } return $user; } /** * 记录失败尝试 */ public function record_failed_attempt($username) { $ip = $this->get_client_ip(); $attempts = $this->get_login_attempts($ip); $attempts++; $this->set_login_attempts($ip, $attempts); // 记录详细信息 $this->log_failed_attempt($ip, $username); } /** * 清除登录尝试记录 */ public function clear_login_attempts($username) { $ip = $this->get_client_ip(); $this->set_login_attempts($ip, 0); $this->clear_ip_lock($ip); } /** * 获取客户端IP */ private function get_client_ip() { $ip_keys = array( '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) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); // 验证IP地址 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return $_SERVER['REMOTE_ADDR']; } /** * 获取登录尝试次数 */ private function get_login_attempts($ip) { $transient_key = 'login_attempts_' . md5($ip); $attempts = get_transient($transient_key); return $attempts ? intval($attempts) : 0; } /** * 设置登录尝试次数 */ private function set_login_attempts($ip, $attempts) { $transient_key = 'login_attempts_' . md5($ip); $expiration = 3600; // 1小时 set_transient($transient_key, $attempts, $expiration); } /** * 封锁IP */ private function lock_ip($ip) { $lockout_key = 'ip_lockout_' . md5($ip); $lockout_count_key = 'ip_lockout_count_' . md5($ip); // 获取当前封锁次数 $lockout_count = get_transient($lockout_count_key); $lockout_count = $lockout_count ? intval($lockout_count) + 1 : 1; // 设置封锁时间 $lockout_time = $this->lockout_time; if ($lockout_count >= $this->max_lockouts) { $lockout_time = $this->extended_lockout_time; } set_transient($lockout_key, time() + $lockout_time, $lockout_time); set_transient($lockout_count_key, $lockout_count, 86400); // 24小时 // 记录封锁日志 $this->log_ip_lock($ip, $lockout_time, $lockout_count); } /** * 检查IP是否被封锁 */ private function is_ip_locked($ip) { $lockout_key = 'ip_lockout_' . md5($ip); $lockout_time = get_transient($lockout_key); if ($lockout_time && $lockout_time > time()) { return true; } return false; } /** * 获取封锁剩余时间 */ private function get_lockout_time($ip) { $lockout_key = 'ip_lockout_' . md5($ip); return get_transient($lockout_key); } /** * 清除IP封锁 */ private function clear_ip_lock($ip) { $lockout_key = 'ip_lockout_' . md5($ip); $lockout_count_key = 'ip_lockout_count_' . md5($ip); delete_transient($lockout_key); delete_transient($lockout_count_key); } /** * 记录失败尝试日志 */ private function log_failed_attempt($ip, $username) { $log_entry = array( 'time' => current_time('mysql'), 'ip' => $ip, 'username' => $username, 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'referer' => $_SERVER['HTTP_REFERER'] ?? '' ); $log = get_option('mfa_failed_login_log', array()); $log[] = $log_entry; // 只保留最近100条记录 if (count($log) > 100) { $log = array_slice($log, -100); } update_option('mfa_failed_login_log', $log); } /** * 记录IP封锁日志 */ private function log_ip_lock($ip, $duration, $count) { $log_entry = array( 'time' => current_time('mysql'), 'ip' => $ip, 'duration' => $duration, 'count' => $count, 'reason' => 'too_many_failed_attempts' ); $log = get_option('mfa_ip_lock_log', array()); $log[] = $log_entry; // 只保留最近50条记录 if (count($log) > 50) { $log = array_slice($log, -50); } update_option('mfa_ip_lock_log', $log); } /** * 添加登录页面保护 */ public function add_login_protection() { ?> <script type="text/javascript"> jQuery(document).ready(function($) { // 检查登录锁定状态 function checkLoginLock() { $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'check_login_lock' }, success: function(response) { if (response.locked) { // 显示锁定消息 $('#login').prepend( '<div class="message error" style="border-left-color: #dc3232;">' + '<p>登录尝试次数过多。请等待 ' + Math.floor(response.remaining / 60) + ' 分 ' + (response.remaining % 60) + ' 秒后再试。</p>' + '</div>' ); // 禁用登录表单 $('#loginform input').prop('disabled', true); $('#wp-submit').prop('disabled', true); // 更新倒计时 setTimeout(updateCountdown, 1000); } } }); } // 更新倒计时显示 function updateCountdown() { var countdownEl = $('.countdown-timer'); if (countdownEl.length) { var remaining = parseInt(countdownEl.data('remaining')) - 1; if (remaining <= 0) { location.reload(); } else { countdownEl.data('remaining', remaining); countdownEl.text( Math.floor(remaining / 60) + ' 分 ' + (remaining % 60) + ' 秒' ); setTimeout(updateCountdown, 1000); } } } // 页面加载时检查 checkLoginLock(); }); </script> <?php } /** * AJAX检查登录锁定状态 */ public function ajax_check_login_lock() { $ip = $this->get_client_ip(); if ($this->is_ip_locked($ip)) { $lockout_time = $this->get_lockout_time($ip); $remaining = $lockout_time - time(); wp_send_json_success(array( 'locked' => true, 'remaining' => $remaining )); } else {
发表评论分类: 网站建设
详细指南:打造支持多用户的在线文件协作与审阅功能 引言:WordPress作为协作平台的潜力 在当今数字化工作环境中,在线文件协作与审阅已成为团队生产力的核心要素。虽然市面上已有许多成熟的协作工具,但将这一功能集成到现有WordPress网站中,能够为用户提供无缝的工作体验,同时保持品牌一致性。WordPress作为全球最受欢迎的内容管理系统,其强大的扩展性和开源特性使其成为实现定制化协作功能的理想平台。 本指南将详细介绍如何通过WordPress代码二次开发,构建一个支持多用户的在线文件协作与审阅系统。我们将从基础架构设计开始,逐步深入到具体实现,最终打造一个功能完善、用户体验优秀的协作工具。 第一部分:系统架构设计与技术选型 1.1 需求分析与功能规划 在开始开发之前,我们需要明确系统的基本需求: 多用户支持:不同角色(管理员、编辑者、审阅者、查看者)的权限管理 文件上传与管理:支持常见文档格式(DOC、DOCX、PDF、PPT等) 实时协作:多人同时编辑文档的能力 审阅与批注:添加评论、建议和修改跟踪 版本控制:文档修改历史记录与恢复 通知系统:活动提醒和更新通知 安全与隐私:数据加密和访问控制 1.2 技术栈选择 基于WordPress的生态系统,我们选择以下技术方案: 核心框架:WordPress 5.0+,使用REST API进行前后端通信 前端技术:React.js或Vue.js用于构建交互式界面(可选) 实时协作:WebSocket(通过Socket.io或Pusher服务) 文档处理:后端转换服务或前端库(如Mammoth.js、PDF.js) 数据库:MySQL(WordPress默认),考虑自定义表结构 文件存储:WordPress媒体库扩展或云存储集成(AWS S3、Google Cloud) 1.3 数据库设计 我们需要扩展WordPress的默认数据库结构,添加以下自定义表: -- 协作项目表 CREATE TABLE wp_collab_projects ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, owner_id BIGINT(20) UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (owner_id) REFERENCES wp_users(ID) ); -- 项目成员表 CREATE TABLE wp_collab_members ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, user_id BIGINT(20) UNSIGNED NOT NULL, role ENUM('owner', 'editor', 'reviewer', 'viewer') DEFAULT 'viewer', joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES wp_collab_projects(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ); -- 文档表 CREATE TABLE wp_collab_documents ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, title VARCHAR(255) NOT NULL, file_path VARCHAR(500), file_type VARCHAR(50), file_size INT, created_by BIGINT(20) UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, version INT DEFAULT 1, FOREIGN KEY (project_id) REFERENCES wp_collab_projects(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES wp_users(ID) ); -- 文档版本表 CREATE TABLE wp_collab_document_versions ( id INT AUTO_INCREMENT PRIMARY KEY, document_id INT NOT NULL, version_number INT NOT NULL, file_path VARCHAR(500), changes_summary TEXT, created_by BIGINT(20) UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (document_id) REFERENCES wp_collab_documents(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES wp_users(ID) ); -- 批注与评论表 CREATE TABLE wp_collab_comments ( id INT AUTO_INCREMENT PRIMARY KEY, document_id INT NOT NULL, version_id INT, user_id BIGINT(20) UNSIGNED NOT NULL, content TEXT NOT NULL, position_data JSON, -- 存储批注在文档中的位置信息 resolved BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (document_id) REFERENCES wp_collab_documents(id) ON DELETE CASCADE, FOREIGN KEY (version_id) REFERENCES wp_collab_document_versions(id), FOREIGN KEY (user_id) REFERENCES wp_users(ID) ); 第二部分:核心功能实现 2.1 用户权限与角色管理 WordPress本身具有强大的用户角色系统,但我们需要扩展它以支持协作场景: <?php // 在插件激活时添加自定义角色 function collab_add_custom_roles() { add_role('collab_editor', '协作文档编辑', array( 'read' => true, 'edit_posts' => true, 'upload_files' => true, 'collab_edit_document' => true, 'collab_comment' => true, )); add_role('collab_reviewer', '协作文档审阅', array( 'read' => true, 'collab_comment' => true, 'collab_review' => true, )); add_role('collab_viewer', '协作文档查看', array( 'read' => true, 'collab_view' => true, )); } register_activation_hook(__FILE__, 'collab_add_custom_roles'); // 添加自定义权限到现有角色 function collab_add_capabilities() { $roles = array('administrator', 'editor'); foreach ($roles as $role_name) { $role = get_role($role_name); if ($role) { $role->add_cap('collab_manage_projects'); $role->add_cap('collab_edit_document'); $role->add_cap('collab_comment'); $role->add_cap('collab_review'); $role->add_cap('collab_view'); } } } add_action('admin_init', 'collab_add_capabilities'); // 项目级别的权限检查 function collab_check_project_permission($project_id, $user_id, $capability) { global $wpdb; // 检查用户是否为项目成员 $member = $wpdb->get_row($wpdb->prepare( "SELECT role FROM {$wpdb->prefix}collab_members WHERE project_id = %d AND user_id = %d", $project_id, $user_id )); if (!$member) { return false; } // 根据角色检查权限 $role_permissions = array( 'owner' => array('collab_manage_projects', 'collab_edit_document', 'collab_comment', 'collab_review', 'collab_view'), 'editor' => array('collab_edit_document', 'collab_comment', 'collab_review', 'collab_view'), 'reviewer' => array('collab_comment', 'collab_review', 'collab_view'), 'viewer' => array('collab_view') ); return in_array($capability, $role_permissions[$member->role]); } ?> 2.2 文件上传与管理系统 扩展WordPress媒体处理功能以支持协作文档: <?php // 扩展允许上传的文件类型 function collab_allowed_mime_types($mimes) { $mimes['doc'] = 'application/msword'; $mimes['docx'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; $mimes['ppt'] = 'application/vnd.ms-powerpoint'; $mimes['pptx'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; $mimes['xls'] = 'application/vnd.ms-excel'; $mimes['xlsx'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; $mimes['pdf'] = 'application/pdf'; return $mimes; } add_filter('upload_mimes', 'collab_allowed_mime_types'); // 处理文档上传 function collab_handle_document_upload($project_id) { if (!isset($_FILES['collab_document']) || $_FILES['collab_document']['error'] !== UPLOAD_ERR_OK) { return new WP_Error('upload_failed', '文件上传失败'); } // 检查用户权限 if (!collab_check_project_permission($project_id, get_current_user_id(), 'collab_edit_document')) { return new WP_Error('permission_denied', '您没有权限上传文档'); } // 使用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'); $file = $_FILES['collab_document']; // 验证文件类型和大小 $max_size = 50 * 1024 * 1024; // 50MB if ($file['size'] > $max_size) { return new WP_Error('file_too_large', '文件大小不能超过50MB'); } // 上传文件 $upload_overrides = array('test_form' => false); $movefile = wp_handle_upload($file, $upload_overrides); if ($movefile && !isset($movefile['error'])) { // 创建文档记录 global $wpdb; $document_data = array( 'project_id' => $project_id, 'title' => sanitize_text_field($_POST['document_title']), 'file_path' => $movefile['file'], 'file_url' => $movefile['url'], 'file_type' => $file['type'], 'file_size' => $file['size'], 'created_by' => get_current_user_id(), 'version' => 1 ); $wpdb->insert("{$wpdb->prefix}collab_documents", $document_data); $document_id = $wpdb->insert_id; // 创建初始版本记录 $version_data = array( 'document_id' => $document_id, 'version_number' => 1, 'file_path' => $movefile['file'], 'created_by' => get_current_user_id() ); $wpdb->insert("{$wpdb->prefix}collab_document_versions", $version_data); // 记录活动日志 collab_log_activity($project_id, 'document_uploaded', array( 'document_id' => $document_id, 'document_title' => $document_data['title'], 'user_id' => get_current_user_id() )); return $document_id; } else { return new WP_Error('upload_failed', $movefile['error']); } } // 文档列表获取 function collab_get_project_documents($project_id, $user_id) { global $wpdb; // 检查查看权限 if (!collab_check_project_permission($project_id, $user_id, 'collab_view')) { return new WP_Error('permission_denied', '您没有权限查看文档'); } $documents = $wpdb->get_results($wpdb->prepare( "SELECT d.*, u.display_name as creator_name, (SELECT COUNT(*) FROM {$wpdb->prefix}collab_comments c WHERE c.document_id = d.id AND c.resolved = 0) as unresolved_comments FROM {$wpdb->prefix}collab_documents d LEFT JOIN {$wpdb->users} u ON d.created_by = u.ID WHERE d.project_id = %d ORDER BY d.updated_at DESC", $project_id )); return $documents; } ?> 2.3 实时协作功能实现 使用WebSocket实现实时协作功能: <?php // WebSocket服务器集成(使用PHP-WebSocket或外部服务) // 这里我们以Pusher服务为例,但也可以使用Socket.io class CollabRealtime { private $pusher; public function __construct() { // 初始化Pusher $this->pusher = new PusherPusher( get_option('collab_pusher_app_key'), get_option('collab_pusher_app_secret'), get_option('collab_pusher_app_id'), array('cluster' => get_option('collab_pusher_cluster')) ); } // 用户加入文档协作 public function user_joined_document($document_id, $user_id) { $user = get_userdata($user_id); $this->pusher->trigger("document-{$document_id}", 'user-joined', array( 'user_id' => $user_id, 'user_name' => $user->display_name, 'timestamp' => current_time('timestamp') )); // 记录用户活动 $this->update_document_presence($document_id, $user_id, 'join'); } // 用户离开文档协作 public function user_left_document($document_id, $user_id) { $this->update_document_presence($document_id, $user_id, 'leave'); $this->pusher->trigger("document-{$document_id}", 'user-left', array( 'user_id' => $user_id, 'timestamp' => current_time('timestamp') )); } // 文档内容更新 public function document_updated($document_id, $changes, $user_id) { $this->pusher->trigger("document-{$document_id}", 'content-updated', array( 'changes' => $changes, 'user_id' => $user_id, 'timestamp' => current_time('timestamp') )); } // 批注添加 public function comment_added($document_id, $comment_id, $user_id) { $this->pusher->trigger("document-{$document_id}", 'comment-added', array( 'comment_id' => $comment_id, 'user_id' => $user_id, 'timestamp' => current_time('timestamp') )); } // 更新在线用户状态 private function update_document_presence($document_id, $user_id, $action) { global $wpdb; $table_name = "{$wpdb->prefix}collab_document_presence"; if ($action === 'join') { $wpdb->replace($table_name, array( 'document_id' => $document_id, 'user_id' => $user_id, 'last_seen' => current_time('mysql') )); } else { $wpdb->delete($table_name, array( 'document_id' => $document_id, 'user_id' => $user_id )); } // 获取当前在线用户 $online_users = $wpdb->get_results($wpdb->prepare( "SELECT p.user_id, u.display_name, p.last_seen FROM {$table_name} p LEFT JOIN {$wpdb->users} u ON p.user_id = u.ID WHERE p.document_id = %d AND p.last_seen > %s", $document_id, date('Y-m-d H:i:s', strtotime('-5 minutes')) )); // 广播在线用户列表 $this->pusher->trigger("document-{$document_id}", 'presence-updated', array( 'online_users' => $online_users )); } } // 前端JavaScript处理实时更新 function collab_enqueue_realtime_scripts() { if (is_page('collab-document')) { // 引入Pusher库 wp_enqueue_script('pusher', 'https://js.pusher.com/7.0/pusher.min.js', array(), '7.0', true); // 自定义实时协作脚本 wp_enqueue_script('collab-realtime', plugin_dir_url(__FILE__) . 'js/collab-realtime.js', array('jquery', 'pusher'), '1.0', true); // 传递配置到前端 wp_localize_script('collab-realtime', 'collab_realtime_config', array( 'pusher_key' => get_option('collab_pusher_app_key'), 'pusher_cluster' => get_option('collab_pusher_cluster'), 'current_user_id' => get_current_user_id(), 'current_user_name' => wp_get_current_user()->display_name, 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('collab_realtime_nonce') )); } } add_action('wp_enqueue_scripts', 'collab_enqueue_realtime_scripts'); ?> 前端实时协作JavaScript示例: // collab-realtime.js class CollabRealtimeClient { constructor(documentId) { this.documentId = documentId; this.pusher = null; this.channel = null; this.initializePusher(); } initializePusher() { // 初始化Pusher连接 this.pusher = new Pusher(collab_realtime_config.pusher_key, { cluster: collab_realtime_config.pusher_cluster, authEndpoint: collab_realtime_config.ajax_url, auth: { params: { this.documentId, user_id: collab_realtime_config.current_user_id, nonce: collab_realtime_config.nonce } } }); // 订阅文档频道 this.channel = this.pusher.subscribe(`private-document-${this.documentId}`); // 绑定事件处理 this.bindEvents(); // 通知服务器用户已加入 this.notifyJoined(); } bindEvents() { // 用户加入事件 this.channel.bind('user-joined', (data) => { this.handleUserJoined(data); }); // 用户离开事件 this.channel.bind('user-left', (data) => { this.handleUserLeft(data); }); // 内容更新事件 this.channel.bind('content-updated', (data) => { this.handleContentUpdated(data); }); // 批注添加事件 this.channel.bind('comment-added', (data) => { this.handleCommentAdded(data); }); // 在线用户更新 this.channel.bind('presence-updated', (data) => { this.updateOnlineUsers(data.online_users); }); } notifyJoined() { jQuery.ajax({ url: collab_realtime_config.ajax_url, method: 'POST', data: { action: 'collab_user_joined', document_id: this.documentId, nonce: collab_realtime_config.nonce } }); } handleUserJoined(data) { // 显示用户加入通知 this.showNotification(`${data.user_name} 加入了文档`); // 更新在线用户界面 this.addOnlineUser(data.user_id, data.user_name); } handleContentUpdated(data) { // 如果不是当前用户的操作,应用更改 if (data.user_id !== collab_realtime_config.current_user_id) { this.applyRemoteChanges(data.changes); } } sendContentUpdate(changes) { // 发送内容更新到服务器 jQuery.ajax({ url: collab_realtime_config.ajax_url, method: 'POST', data: { action: 'collab_update_content', document_id: this.documentId, changes: JSON.stringify(changes), nonce: collab_realtime_config.nonce }, success: (response) => { if (response.success) { // 通过Pusher广播更新 this.channel.trigger('client-content-updated', { changes: changes, user_id: collab_realtime_config.current_user_id }); } } }); } disconnect() { if (this.pusher) { this.pusher.disconnect(); } } } ### 2.4 文档审阅与批注系统 实现完整的文档审阅功能: <?php// 批注系统处理class CollabCommentSystem { // 添加批注 public static function add_comment($document_id, $data) { global $wpdb; // 验证权限 if (!collab_check_project_permission( self::get_project_id_from_document($document_id), get_current_user_id(), 'collab_comment' )) { return new WP_Error('permission_denied', '您没有权限添加批注'); } $comment_data = array( 'document_id' => $document_id, 'user_id' => get_current_user_id(), 'content' => sanitize_textarea_field($data['content']), 'position_data' => json_encode($data['position']), 'resolved' => 0 ); // 如果有版本号,关联到特定版本 if (!empty($data['version_id'])) { $comment_data['version_id'] = intval($data['version_id']); } $wpdb->insert("{$wpdb->prefix}collab_comments", $comment_data); $comment_id = $wpdb->insert_id; // 触发实时通知 $realtime = new CollabRealtime(); $realtime->comment_added($document_id, $comment_id, get_current_user_id()); // 发送通知邮件 self::notify_comment_added($document_id, $comment_id); return $comment_id; } // 获取文档批注 public static function get_document_comments($document_id, $version_id = null) { global $wpdb; $query = $wpdb->prepare( "SELECT c.*, u.display_name as user_name, u.user_email, (SELECT display_name FROM {$wpdb->users} WHERE ID = c.resolved_by) as resolved_by_name FROM {$wpdb->prefix}collab_comments c LEFT JOIN {$wpdb->users} u ON c.user_id = u.ID WHERE c.document_id = %d", $document_id ); if ($version_id) { $query .= $wpdb->prepare(" AND (c.version_id = %d OR c.version_id IS NULL)", $version_id); } $query .= " ORDER BY c.created_at ASC"; return $wpdb->get_results($query); } // 解决批注 public static function resolve_comment($comment_id, $user_id) { global $wpdb; $comment = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_comments WHERE id = %d", $comment_id )); if (!$comment) { return new WP_Error('not_found', '批注不存在'); } // 检查权限 $project_id = self::get_project_id_from_document($comment->document_id); if (!collab_check_project_permission($project_id, $user_id, 'collab_comment')) { return new WP_Error('permission_denied', '您没有权限修改批注'); } $wpdb->update( "{$wpdb->prefix}collab_comments", array( 'resolved' => 1, 'resolved_by' => $user_id, 'resolved_at' => current_time('mysql') ), array('id' => $comment_id) ); return true; } // 批注通知 private static function notify_comment_added($document_id, $comment_id) { global $wpdb; // 获取文档信息 $document = $wpdb->get_row($wpdb->prepare( "SELECT d.title, p.id as project_id, p.title as project_title FROM {$wpdb->prefix}collab_documents d LEFT JOIN {$wpdb->prefix}collab_projects p ON d.project_id = p.id WHERE d.id = %d", $document_id )); // 获取项目成员(除了批注作者) $members = $wpdb->get_results($wpdb->prepare( "SELECT m.user_id, u.user_email, u.display_name FROM {$wpdb->prefix}collab_members m LEFT JOIN {$wpdb->users} u ON m.user_id = u.ID WHERE m.project_id = %d AND m.user_id != %d", $document->project_id, get_current_user_id() )); // 获取批注内容 $comment = $wpdb->get_row($wpdb->prepare( "SELECT content FROM {$wpdb->prefix}collab_comments WHERE id = %d", $comment_id )); // 发送邮件通知 foreach ($members as $member) { $subject = sprintf('新批注: %s - %s', $document->project_title, $document->title); $message = sprintf( "您好 %s,nn在项目 '%s' 的文档 '%s' 中有一条新批注:nn%snn请登录系统查看:%snn", $member->display_name, $document->project_title, $document->title, $comment->content, get_permalink(get_page_by_path('collab-document')) . '?id=' . $document_id ); wp_mail($member->user_email, $subject, $message); } } private static function get_project_id_from_document($document_id) { global $wpdb; $result = $wpdb->get_var($wpdb->prepare( "SELECT project_id FROM {$wpdb->prefix}collab_documents WHERE id = %d", $document_id )); return $result; } } // AJAX处理批注add_action('wp_ajax_collab_add_comment', 'collab_ajax_add_comment');function collab_ajax_add_comment() { check_ajax_referer('collab_nonce', 'nonce'); $document_id = intval($_POST['document_id']); $content = sanitize_textarea_field($_POST['content']); $position = json_decode(stripslashes($_POST['position']), true); $comment_id = CollabCommentSystem::add_comment($document_id, array( 'content' => $content, 'position' => $position )); if (is_wp_error($comment_id)) { wp_send_json_error($comment_id->get_error_message()); } else { wp_send_json_success(array('comment_id' => $comment_id)); } }?> ## 第三部分:版本控制与文档历史 ### 3.1 版本控制系统实现 <?phpclass CollabVersionControl { // 创建新版本 public static function create_new_version($document_id, $user_id, $changes_summary = '') { global $wpdb; // 获取当前文档信息 $document = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_documents WHERE id = %d", $document_id )); if (!$document) { return new WP_Error('not_found', '文档不存在'); } // 检查权限 if (!collab_check_project_permission($document->project_id, $user_id, 'collab_edit_document')) { return new WP_Error('permission_denied', '您没有权限创建新版本'); } // 获取下一个版本号 $next_version = $document->version + 1; // 复制当前文件为新版本 $original_path = $document->file_path; $new_filename = self::generate_version_filename($document->title, $next_version, pathinfo($original_path, PATHINFO_EXTENSION)); $new_path = self::get_versions_directory($document_id) . '/' . $new_filename; // 复制文件 if (!copy($original_path, $new_path)) { return new WP_Error('file_copy_failed', '无法创建版本副本'); } // 创建版本记录 $version_data = array( 'document_id' => $document_id, 'version_number' => $next_version, 'file_path' => $new_path, 'changes_summary' => sanitize_text_field($changes_summary), 'created_by' => $user_id ); $wpdb->insert("{$wpdb->prefix}collab_document_versions", $version_data); $version_id = $wpdb->insert_id; // 更新文档版本号 $wpdb->update( "{$wpdb->prefix}collab_documents", array('version' => $next_version, 'updated_at' => current_time('mysql')), array('id' => $document_id) ); // 记录活动 collab_log_activity($document->project_id, 'version_created', array( 'document_id' => $document_id, 'version' => $next_version, 'user_id' => $user_id )); return array( 'version_id' => $version_id, 'version_number' => $next_version, 'file_path' => $new_path ); } // 获取版本列表 public static function get_document_versions($document_id) { global $wpdb; return $wpdb->get_results($wpdb->prepare( "SELECT v.*, u.display_name as creator_name FROM {$wpdb->prefix}collab_document_versions v LEFT JOIN {$wpdb->users} u ON v.created_by = u.ID WHERE v.document_id = %d ORDER BY v.version_number DESC", $document_id )); } // 恢复到指定版本 public static function restore_to_version($document_id, $version_id, $user_id) { global $wpdb; // 获取文档和版本信息 $document = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_documents WHERE id = %d", $document_id )); $version = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_document_versions WHERE id = %d AND document_id = %d", $version_id, $document_id )); if (!$document || !$version) { return new WP_Error('not_found', '文档或版本不存在'); } // 检查权限 if (!collab_check_project_permission($document->project_id, $user_id, 'collab_edit_document')) { return new WP_Error('permission_denied', '您没有权限恢复版本'); } // 创建当前状态的备份 self::create_new_version($document_id, $user_id, '恢复版本前的备份'); // 复制版本文件到当前文档 if (!copy($version->file_path, $document->file_path)) { return new WP_Error('restore_failed', '恢复文件失败'); } // 更新文档信息 $wpdb->update( "{$wpdb->prefix}collab_documents", array( 'version' => $version->version_number, 'updated_at' => current_time('mysql') ), array('id' => $document_id) ); // 记录活动 collab_log_activity($document->project_id, 'version_restored', array( 'document_id' => $document_id, 'from_version' => $document->version, 'to_version' => $version->version_number, 'user_id' => $user_id )); return true; } // 比较两个版本 public static function compare_versions($document_id, $version1_id, $version2_id) { global $wpdb; $version1 = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_document_versions WHERE id = %d", $version1_id )); $version2 = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_document_versions WHERE id = %d", $version2_id )); if (!$version1 || !$version2) { return new WP_Error('not_found', '版本不存在'); } // 根据文件类型使用不同的比较方法 $extension = strtolower(pathinfo($version1->file_path, PATHINFO_EXTENSION)); switch ($extension) { case 'txt': case 'md': return self::compare_text_files($version1->file_path, $version2->file_path); case 'docx': return self::compare_docx_files($version1->file_path, $version2->file_path); case 'pdf': return self::compare_pdf_files($version1->file_path, $version2->file_path); default: return array( 'type' => 'binary', 'message' => '二进制文件,无法显示文本差异' ); } } private static function compare_text_files($file1, $file2) { $content1 = file_get_contents($file1); $content2 = file_get_contents($file2); // 使用文本差异算法 require_once(ABSPATH . 'wp-includes/Text/Diff.php'); require_once(ABSPATH . 'wp-includes/Text/Diff/Renderer.php'); $diff = new Text_Diff('auto', array(explode("n", $content1), explode("n", $content2))); $renderer = new Text_Diff_Renderer_inline(); return array( 'type' => 'text', 'diff' => $renderer->render($diff) ); } private static function generate_version_filename($title, $version, $extension) { $safe_title = sanitize_title($title); return sprintf('%s-v%d-%s.%s', $safe_title, $version, date('Ymd-His'), $extension ); } private static function get_versions_directory($document_id) { $upload_dir = wp_upload_dir(); $versions_dir = $upload_dir['basedir'] . '/collab-versions/' . $document_id; if (!file_exists($versions_dir)) { wp_mkdir_p($versions_dir); } return $versions_dir; } }?> ## 第四部分:用户界面与体验优化 ### 4.1 前端界面构建 使用React构建现代协作界面(简化示例): // DocumentEditor.jsx - 主文档编辑器组件import React, { useState, useEffect, useRef } from 'react';import { CommentList, VersionHistory, OnlineUsers, Toolbar } from './components'; const DocumentEditor = ({ documentId, userId }) => { const [document, setDocument] = useState(null); const
发表评论WordPress开发教程:集成网站安全扫描与漏洞检测工具,通过代码二次开发实现常用互联网小工具功能 引言:WordPress安全与功能扩展的双重挑战 在当今数字化时代,WordPress作为全球最受欢迎的内容管理系统,驱动着超过40%的网站。然而,随着其普及度的提升,WordPress网站也成为了黑客攻击的主要目标。据统计,每天有超过9万个WordPress网站遭受各种形式的网络攻击。与此同时,用户对网站功能的需求也日益多样化,不再满足于基本的内容发布,而是期望网站能够集成各种实用工具,提升用户体验。 本教程将深入探讨如何通过WordPress代码二次开发,实现两个关键目标:一是集成专业级网站安全扫描与漏洞检测工具,构建主动防御体系;二是开发常用互联网小工具,增强网站功能性。我们将从理论基础到实践操作,从安全原理到代码实现,全面解析这一综合解决方案。 第一部分:WordPress安全现状分析与安全工具集成必要性 1.1 WordPress安全威胁全景图 WordPress面临的安全威胁多种多样,主要包括: SQL注入攻击:通过恶意SQL代码操纵数据库 跨站脚本攻击(XSS):在网页中注入恶意脚本 跨站请求伪造(CSRF):诱使用户执行非本意的操作 文件包含漏洞:利用文件包含功能执行恶意代码 暴力破解攻击:尝试大量密码组合获取访问权限 主题和插件漏洞:第三方代码中的安全缺陷 根据Wordfence安全报告,2023年针对WordPress的攻击尝试比前一年增加了150%,其中插件和主题漏洞占比高达56%。这些数据凸显了加强WordPress安全防护的紧迫性。 1.2 传统安全方案的局限性 传统的WordPress安全方案通常包括: 基础安全插件安装 定期手动更新 简单的防火墙配置 然而,这些方法存在明显不足: 被动防御:多数方案只在攻击发生后响应 检测能力有限:难以发现复杂或新型攻击 误报率高:可能将正常流量误判为攻击 性能影响:某些安全插件显著降低网站速度 1.3 集成专业安全工具的优势 集成专业安全扫描与漏洞检测工具能够: 实现主动安全监测:定期自动扫描,提前发现潜在风险 深度漏洞检测:使用专业算法识别复杂安全漏洞 实时威胁情报:基于全球攻击数据提供预警 最小性能影响:优化代码减少对网站速度的影响 定制化报告:根据网站特点生成针对性安全建议 第二部分:WordPress安全扫描与漏洞检测工具集成方案 2.1 安全工具架构设计 我们将设计一个模块化的安全系统,包含以下核心组件: // 安全系统主类结构 class WP_Security_Scanner { private $scanner_modules = array(); private $vulnerability_db; private $reporting_system; public function __construct() { $this->init_modules(); $this->load_vulnerability_database(); $this->setup_reporting(); } private function init_modules() { // 初始化各扫描模块 $this->scanner_modules = array( 'file_integrity' => new File_Integrity_Scanner(), 'malware_detection' => new Malware_Scanner(), 'vulnerability_scan' => new Vulnerability_Scanner(), 'brute_force_protection' => new Brute_Force_Protector() ); } } 2.2 文件完整性监控模块实现 文件完整性监控是检测未经授权文件更改的关键技术: class File_Integrity_Scanner { private $baseline_hashes = array(); public function create_baseline() { $wp_files = $this->get_wordpress_files(); foreach ($wp_files as $file) { if ($this->is_scannable_file($file)) { $this->baseline_hashes[$file] = array( 'hash' => md5_file($file), 'size' => filesize($file), 'modified' => filemtime($file) ); } } $this->save_baseline(); } public function run_integrity_check() { $current_hashes = array(); $alerts = array(); foreach ($this->baseline_hashes as $file => $baseline_data) { if (!file_exists($file)) { $alerts[] = "文件删除警告: {$file}"; continue; } $current_hash = md5_file($file); if ($current_hash !== $baseline_data['hash']) { $alerts[] = "文件篡改检测: {$file}"; $this->analyze_file_changes($file, $baseline_data['hash'], $current_hash); } } return $alerts; } } 2.3 漏洞扫描引擎开发 漏洞扫描引擎需要结合本地检测和外部漏洞数据库: class Vulnerability_Scanner { private $vulnerability_sources = array( 'wpvulndb' => 'https://wpvulndb.com/api/v3/', 'nvd' => 'https://services.nvd.nist.gov/rest/json/cves/1.0' ); public function scan_plugins_themes() { $vulnerabilities = array(); // 扫描已安装插件 $plugins = get_plugins(); foreach ($plugins as $plugin_path => $plugin_data) { $plugin_slug = dirname($plugin_path); $plugin_version = $plugin_data['Version']; $plugin_vulns = $this->check_plugin_vulnerabilities($plugin_slug, $plugin_version); if (!empty($plugin_vulns)) { $vulnerabilities['plugins'][$plugin_slug] = $plugin_vulns; } } // 扫描当前主题 $theme = wp_get_theme(); $theme_vulns = $this->check_theme_vulnerabilities($theme->get('TextDomain'), $theme->get('Version')); if (!empty($theme_vulns)) { $vulnerabilities['theme'] = $theme_vulns; } return $vulnerabilities; } private function check_plugin_vulnerabilities($slug, $version) { // 查询漏洞数据库 $api_url = $this->vulnerability_sources['wpvulndb'] . "plugins/{$slug}"; $response = wp_remote_get($api_url); if (is_wp_error($response)) { return false; } $data = json_decode(wp_remote_retrieve_body($response), true); $relevant_vulns = array(); if (isset($data[$slug]['vulnerabilities'])) { foreach ($data[$slug]['vulnerabilities'] as $vuln) { if ($this->is_version_affected($version, $vuln['fixed_in'])) { $relevant_vulns[] = array( 'id' => $vuln['id'], 'title' => $vuln['title'], 'cvss_score' => $vuln['cvss']['score'], 'fixed_in' => $vuln['fixed_in'] ); } } } return $relevant_vulns; } } 2.4 恶意代码检测模块 恶意代码检测需要结合特征码检测和行为分析: class Malware_Scanner { private $malware_signatures = array( 'base64_decode' => '/base64_decodes*([^)]*)/', 'eval' => '/evals*([^)]*)/', 'shell_exec' => '/shell_execs*([^)]*)/', 'suspicious_url' => '/(https?://[^s<>"']*.(php|exe|bat|sh))/i' ); private $suspicious_patterns = array( 'obfuscated_code' => '/$(?:w+)s*=s*$(?:w+)s*.s*$(?:w+)/', 'long_string' => '/"[^"]{200,}"/' ); public function scan_directory($directory) { $malware_findings = array(); $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($directory) ); foreach ($files as $file) { if ($file->isDir()) continue; if ($this->is_php_file($file)) { $content = file_get_contents($file->getPathname()); $file_findings = $this->analyze_file_content($content, $file->getPathname()); if (!empty($file_findings)) { $malware_findings[$file->getPathname()] = $file_findings; } } } return $malware_findings; } private function analyze_file_content($content, $filename) { $findings = array(); // 检查已知恶意代码特征 foreach ($this->malware_signatures as $type => $pattern) { if (preg_match_all($pattern, $content, $matches)) { $findings[$type] = $matches[0]; } } // 检查可疑代码模式 foreach ($this->suspicious_patterns as $pattern_name => $pattern) { if (preg_match_all($pattern, $content, $matches)) { $findings[$pattern_name] = count($matches[0]); } } // 检查文件权限 $perms = substr(sprintf('%o', fileperms($filename)), -4); if ($perms == '0777') { $findings['insecure_permissions'] = $perms; } return $findings; } } 2.5 安全报告与警报系统 class Security_Reporter { private $report_types = array('daily', 'weekly', 'immediate'); private $notification_methods = array('email', 'dashboard', 'webhook'); public function generate_report($scan_results, $report_type = 'daily') { $report = array( 'timestamp' => current_time('timestamp'), 'scan_summary' => array( 'total_checks' => 0, 'issues_found' => 0, 'critical_issues' => 0 ), 'detailed_findings' => array(), 'recommendations' => array() ); // 汇总扫描结果 foreach ($scan_results as $module => $results) { $report['scan_summary']['total_checks']++; if (!empty($results)) { $report['scan_summary']['issues_found']++; $report['detailed_findings'][$module] = $results; // 生成建议 $report['recommendations'] = array_merge( $report['recommendations'], $this->generate_recommendations($module, $results) ); } } // 确定报告严重级别 $report['severity'] = $this->calculate_severity($report); return $report; } public function send_alerts($report) { if ($report['severity'] >= 7) { // 高严重级别 $this->send_immediate_alert($report); } // 发送定期报告 if ($this->is_time_for_report('daily')) { $this->send_email_report($report, 'daily'); } // 更新仪表板小工具 $this->update_dashboard_widget($report); } } 第三部分:常用互联网小工具的开发与集成 3.1 小工具系统架构设计 我们将创建一个可扩展的小工具框架: class WP_Toolkit_Framework { private $tools = array(); private $tool_categories = array( 'utility' => '实用工具', 'seo' => 'SEO工具', 'security' => '安全工具', 'development' => '开发工具' ); public function register_tool($tool_slug, $tool_config) { $defaults = array( 'name' => '', 'description' => '', 'category' => 'utility', 'callback' => null, 'settings' => array(), 'shortcode' => '' ); $config = wp_parse_args($tool_config, $defaults); $this->tools[$tool_slug] = $config; // 注册短代码 if (!empty($config['shortcode'])) { add_shortcode($config['shortcode'], array($this, 'render_tool')); } } public function render_tool($atts, $content = null, $tag = '') { $atts = shortcode_atts(array('tool' => ''), $atts, $tag); if (empty($atts['tool']) || !isset($this->tools[$atts['tool']])) { return '<p>工具未找到</p>'; } $tool = $this->tools[$atts['tool']]; ob_start(); ?> <div class="wp-toolkit-tool" id="tool-<?php echo esc_attr($atts['tool']); ?>"> <div class="tool-header"> <h3><?php echo esc_html($tool['name']); ?></h3> <p class="tool-description"><?php echo esc_html($tool['description']); ?></p> </div> <div class="tool-content"> <?php call_user_func($tool['callback'], $atts); ?> </div> </div> <?php return ob_get_clean(); } } 3.2 密码强度检测工具 class Password_Strength_Tool { public function init() { add_shortcode('password_strength_checker', array($this, 'render_checker')); add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); } public function render_checker() { ob_start(); ?> <div class="password-strength-checker"> <h3>密码强度检测</h3> <div class="input-group"> <input type="password" id="password-input" placeholder="输入密码进行强度检测" class="form-control"> <button id="toggle-visibility" class="btn btn-secondary"> 显示/隐藏 </button> </div> <div class="strength-meter"> <div class="strength-bar"></div> <div class="strength-labels"> <span class="strength-label" data-level="0">非常弱</span> <span class="strength-label" data-level="1">弱</span> <span class="strength-label" data-level="2">中等</span> <span class="strength-label" data-level="3">强</span> <span class="strength-label" data-level="4">非常强</span> </div> </div> <div class="password-feedback"> <h4>改进建议:</h4> <ul id="password-suggestions"></ul> </div> <div class="password-statistics"> <h4>密码统计:</h4> <p>长度: <span id="password-length">0</span> 字符</p> <p>熵值: <span id="password-entropy">0</span> bits</p> <p>破解时间: <span id="crack-time">立即</span></p> </div> </div> <?php return ob_get_clean(); } public function enqueue_scripts() { wp_enqueue_script('password-strength-js', plugin_dir_url(__FILE__) . 'js/password-strength.js', array('jquery'), '1.0', true); wp_enqueue_style('password-strength-css', plugin_dir_url(__FILE__) . 'css/password-strength.css'); } } 3.3 网站性能检测工具 class Website_Performance_Tool { public function performance_test($url = '') { if (empty($url)) { $url = home_url(); } $results = array( 'load_time' => 0, 'page_size' => 0, 'requests' => 0, 'performance_score' => 0, 'recommendations' => array() ); // 使用WordPress HTTP API进行测试 $start_time = microtime(true); $response = wp_remote_get($url, array( 'timeout' => 30, 'sslverify' => false )); $end_time = microtime(true); if (!is_wp_error($response)) { $results['load_time'] = round(($end_time - $start_time) * 1000, 2); $results['page_size'] = strlen($response['body']) / 1024; // 分析HTML内容 $results = $this->analyze_html_content($response['body'], $results); // 计算性能分数 $results['performance_score'] = $this->calculate_score($results); // 生成建议 $results['recommendations'] = $this->generate_recommendations($results); } return $results; } private function analyze_html_content($html, $results) { // 解析DOM $dom = new DOMDocument(); @$dom->loadHTML($html); // 统计资源请求 $scripts = $dom->getElementsByTagName('script'); $stylesheets = $dom->getElementsByTagName('link'); $images = $dom->getElementsByTagName('img'); $results['requests'] = $scripts->length + $stylesheets->length + $images->length; 3.4 SEO分析工具 class SEO_Analyzer_Tool { public function analyze_page($url = '') { if (empty($url)) { $url = get_permalink(); } $analysis = array( 'basic' => array(), 'on_page' => array(), 'technical' => array(), 'score' => 0 ); $response = wp_remote_get($url); if (!is_wp_error($response)) { $html = wp_remote_retrieve_body($response); $headers = wp_remote_retrieve_headers($response); // 基础分析 $analysis['basic'] = $this->basic_analysis($html, $headers); // 页面SEO分析 $analysis['on_page'] = $this->on_page_analysis($html); // 技术SEO分析 $analysis['technical'] = $this->technical_analysis($html, $headers); // 计算总分 $analysis['score'] = $this->calculate_seo_score($analysis); } return $analysis; } private function on_page_analysis($html) { $dom = new DOMDocument(); @$dom->loadHTML($html); $analysis = array( 'title' => array( 'value' => '', 'length' => 0, 'score' => 0 ), 'meta_description' => array( 'value' => '', 'length' => 0, 'score' => 0 ), 'headings' => array(), 'images' => array( 'total' => 0, 'with_alt' => 0 ), 'keywords' => array() ); // 分析标题标签 $title_tags = $dom->getElementsByTagName('title'); if ($title_tags->length > 0) { $title = $title_tags->item(0)->nodeValue; $analysis['title']['value'] = $title; $analysis['title']['length'] = mb_strlen($title); $analysis['title']['score'] = $this->evaluate_title($title); } // 分析meta描述 $meta_tags = $dom->getElementsByTagName('meta'); foreach ($meta_tags as $meta) { if ($meta->getAttribute('name') == 'description') { $description = $meta->getAttribute('content'); $analysis['meta_description']['value'] = $description; $analysis['meta_description']['length'] = mb_strlen($description); $analysis['meta_description']['score'] = $this->evaluate_description($description); } } // 分析标题结构 for ($i = 1; $i <= 6; $i++) { $h_tags = $dom->getElementsByTagName('h' . $i); $analysis['headings']['h' . $i] = array( 'count' => $h_tags->length, 'titles' => array() ); foreach ($h_tags as $h_tag) { $analysis['headings']['h' . $i]['titles'][] = $h_tag->nodeValue; } } return $analysis; } public function render_seo_tool() { ob_start(); ?> <div class="seo-analyzer-tool"> <div class="seo-input-section"> <input type="url" id="seo-analysis-url" placeholder="输入要分析的URL" value="<?php echo esc_url(home_url()); ?>"> <button id="run-seo-analysis" class="btn btn-primary"> 分析SEO </button> </div> <div class="seo-results-container"> <div class="seo-score-card"> <div class="score-circle" id="seo-score-circle"> <span class="score-value">0</span> </div> <h4>SEO总分</h4> </div> <div class="seo-details"> <div class="seo-section" id="basic-seo"> <h4>基础SEO</h4> <div class="seo-metrics"></div> </div> <div class="seo-section" id="on-page-seo"> <h4>页面SEO</h4> <div class="seo-metrics"></div> </div> <div class="seo-section" id="technical-seo"> <h4>技术SEO</h4> <div class="seo-metrics"></div> </div> </div> <div class="seo-recommendations"> <h4>改进建议</h4> <ul id="seo-suggestions"></ul> </div> </div> </div> <?php return ob_get_clean(); } } 3.5 二维码生成工具 class QR_Code_Generator { private $qr_library_path; public function __construct() { // 引入QR码生成库 require_once plugin_dir_path(__FILE__) . 'libs/phpqrcode/qrlib.php'; } public function generate_qr_code($data, $options = array()) { $defaults = array( 'size' => 10, 'margin' => 4, 'level' => 'L', // L, M, Q, H 'foreground' => array(0, 0, 0), 'background' => array(255, 255, 255), 'logo' => false, 'format' => 'png' ); $options = wp_parse_args($options, $defaults); // 创建临时文件 $temp_dir = wp_upload_dir()['basedir'] . '/qrcodes/'; if (!file_exists($temp_dir)) { wp_mkdir_p($temp_dir); } $filename = 'qr_' . md5(serialize($data) . serialize($options)) . '.png'; $filepath = $temp_dir . $filename; // 生成QR码 QRcode::png($data, $filepath, $options['level'], $options['size'], $options['margin']); // 添加Logo(如果指定) if ($options['logo'] && file_exists($options['logo'])) { $this->add_logo_to_qr($filepath, $options['logo']); } // 颜色调整 if ($options['foreground'] != array(0, 0, 0) || $options['background'] != array(255, 255, 255)) { $this->recolor_qr($filepath, $options['foreground'], $options['background']); } return array( 'url' => wp_upload_dir()['baseurl'] . '/qrcodes/' . $filename, 'path' => $filepath, 'filename' => $filename ); } public function render_generator_ui() { ob_start(); ?> <div class="qr-code-generator"> <div class="generator-form"> <div class="form-group"> <label for="qr-data">内容/URL:</label> <textarea id="qr-data" rows="3" placeholder="输入要编码的内容或URL"></textarea> </div> <div class="form-group"> <label for="qr-size">尺寸:</label> <select id="qr-size"> <option value="5">小 (200x200)</option> <option value="10" selected>中 (400x400)</option> <option value="15">大 (600x600)</option> <option value="20">超大 (800x800)</option> </select> </div> <div class="form-group"> <label for="qr-error-correction">容错级别:</label> <select id="qr-error-correction"> <option value="L">低 (7%)</option> <option value="M" selected>中 (15%)</option> <option value="Q">高 (25%)</option> <option value="Q">极高 (30%)</option> </select> </div> <div class="form-group"> <label>前景色:</label> <input type="color" id="qr-foreground-color" value="#000000"> </div> <div class="form-group"> <label>背景色:</label> <input type="color" id="qr-background-color" value="#ffffff"> </div> <div class="form-group"> <label for="qr-logo">添加Logo:</label> <input type="file" id="qr-logo" accept="image/*"> </div> <button id="generate-qr" class="btn btn-primary"> 生成QR码 </button> </div> <div class="qr-preview-container"> <div class="qr-preview" id="qr-preview"> <p>QR码预览将显示在这里</p> </div> <div class="qr-actions"> <button id="download-qr" class="btn btn-secondary" disabled> 下载PNG </button> <button id="copy-qr-link" class="btn btn-secondary" disabled> 复制链接 </button> <button id="share-qr" class="btn btn-secondary" disabled> 分享 </button> </div> <div class="qr-info"> <h4>QR码信息:</h4> <p>版本: <span id="qr-version">-</span></p> <p>数据容量: <span id="qr-capacity">-</span></p> <p>纠错级别: <span id="qr-ecc-level">-</span></p> </div> </div> </div> <?php return ob_get_clean(); } } 第四部分:系统集成与优化 4.1 统一管理界面开发 class Toolkit_Admin_Interface { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function add_admin_menu() { add_menu_page( '网站工具包', '网站工具包', 'manage_options', 'wp-toolkit', array($this, 'render_admin_page'), 'dashicons-admin-tools', 30 ); // 添加子菜单 add_submenu_page( 'wp-toolkit', '安全扫描', '安全扫描', 'manage_options', 'wp-toolkit-security', array($this, 'render_security_page') ); add_submenu_page( 'wp-toolkit', '工具集', '工具集', 'manage_options', 'wp-toolkit-tools', array($this, 'render_tools_page') ); add_submenu_page( 'wp-toolkit', '设置', '设置', 'manage_options', 'wp-toolkit-settings', array($this, 'render_settings_page') ); } public function render_admin_page() { ?> <div class="wrap wp-toolkit-dashboard"> <h1>网站工具包仪表板</h1> <div class="dashboard-widgets"> <div class="widget security-status"> <h3>安全状态</h3> <div class="widget-content"> <?php $this->display_security_status(); ?> </div> </div> <div class="widget quick-tools"> <h3>快速工具</h3> <div class="widget-content"> <?php $this->display_quick_tools(); ?> </div> </div> <div class="widget recent-scans"> <h3>最近扫描</h3> <div class="widget-content"> <?php $this->display_recent_scans(); ?> </div> </div> <div class="widget system-info"> <h3>系统信息</h3> <div class="widget-content"> <?php $this->display_system_info(); ?> </div> </div> </div> <div class="dashboard-main"> <div class="activity-log"> <h3>活动日志</h3> <div class="log-entries"> <?php $this->display_activity_log(); ?> </div> </div> </div> </div> <?php } public function render_security_page() { $scanner = new WP_Security_Scanner(); $scan_results = $scanner->run_full_scan(); ?> <div class="wrap wp-toolkit-security"> <h1>安全扫描中心</h1> <div class="security-controls"> <button class="button button-primary" id="run-full-scan"> 运行完整扫描 </button> <button class="button button-secondary" id="run-quick-scan"> 快速扫描 </button> <button class="button" id="schedule-scan"> 计划扫描 </button> </div> <div class="scan-results"> <div class="results-summary"> <h3>扫描摘要</h3> <div class="summary-cards"> <?php $this->display_scan_summary($scan_results); ?> </div> </div> <div class="detailed-results"> <h3>详细结果</h3> <div class="results-tabs"> <ul class="tab-nav"> <li class="active" data-tab="vulnerabilities">漏洞</li> <li data-tab="malware">恶意软件</li> <li data-tab="file-changes">文件变更</li> <li data-tab="security-headers">安全头</li> </ul> <div class="tab-content"> <?php $this->display_detailed_results($scan_results); ?> </div> </div> </div> </div> </div> <?php } } 4.2 性能优化与缓存机制 class Toolkit_Performance_Optimizer { private $cache_enabled = true; private $cache_expiry = 3600; // 1小时 public function __construct() { add_action('init', array($this, 'init_cache_system')); add_action('save_post', array($this, 'clear_post_cache')); add_action('switch_theme', array($this, 'clear_theme_cache')); } public function init_cache_system() { // 创建缓存目录 $cache_dir = WP_CONTENT_DIR . '/cache/wp-toolkit/'; if (!file_exists($cache_dir)) { wp_mkdir_p($cache_dir); } // 添加缓存清理计划任务 if (!wp_next_scheduled('wp_toolkit_clear_expired_cache')) { wp_schedule_event(time(), 'hourly', 'wp_toolkit_clear_expired_cache'); } add_action('wp_toolkit_clear_expired_cache', array($this, 'clear_expired_cache')); } public function get_cached_data($key, $callback, $expiry = null) { if (!$this->cache_enabled) { return call_user_func($callback); } $cache_key = 'wp_toolkit_' . md5($key); $cached = get_transient($cache_key); if ($cached !== false) { return $cached; } $data = call_user_func($callback); if ($expiry === null) { $expiry = $this->cache_expiry; } set_transient($cache_key, $data, $expiry); // 同时保存到文件缓存作为备份 $this->save_to_file_cache($key, $data); return $data; } private function save_to_file_cache($key, $data) { $cache_file = WP_CONTENT_DIR . '/cache/wp-toolkit/' . md5($key) . '.cache'; $cache_data = array( 'timestamp' => time(), 'data' => $data, 'key' => $key ); file_put_contents($cache_file, serialize($cache_data)); } public function optimize_database() { global $wpdb; $optimizations = array(); // 清理修订版本 $revisions = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = 'revision'" ); if ($revisions > 50) { $wpdb->query( "DELETE FROM $wpdb->posts WHERE post_type = 'revision' AND post_modified < DATE_SUB(NOW(), INTERVAL 30 DAY)" ); $optimizations[] = "清理了旧的文章修订版本"; } // 清理自动草稿 $wpdb->query( "DELETE FROM $wpdb->posts WHERE post_status = 'auto-draft' AND post_date < DATE_SUB(NOW(), INTERVAL 7 DAY)" ); // 优化数据库表 $tables = $wpdb->get_col("SHOW TABLES"); foreach ($tables as $table) { $wpdb->query("OPTIMIZE TABLE $table"); } $optimizations[] = "优化了所有数据库表"; return $optimizations; } } 4.3 API接口与扩展性设计 class Toolkit_API { private $api_version = 'v1';
发表评论一步步实现:在WordPress中添加实时数据大屏与可视化图表 引言:为什么WordPress需要实时数据可视化功能 在当今数据驱动的互联网时代,实时数据可视化已成为网站运营和内容展示的重要组成部分。WordPress作为全球最流行的内容管理系统,虽然拥有强大的内容管理能力,但在原生功能上却缺乏专业的数据可视化工具。许多网站运营者、电商店主和内容创作者都需要在自己的WordPress站点上展示实时数据,如网站流量统计、销售数据、用户行为分析等。 传统上,用户可能需要依赖第三方服务或插件来实现这些功能,但这往往带来数据安全、加载速度和定制灵活性等问题。通过WordPress代码二次开发,我们可以创建完全可控、高度定制化的实时数据大屏和可视化图表,不仅能提升网站的专业形象,还能为访客提供更有价值的数据洞察。 本文将详细介绍如何通过代码开发,在WordPress中实现实时数据大屏与可视化图表功能,涵盖从环境准备到最终部署的完整流程。 第一章:开发环境准备与项目规划 1.1 开发环境搭建 在开始开发之前,我们需要准备合适的开发环境。建议使用本地开发环境,如Local by Flywheel、XAMPP或MAMP,这样可以避免对生产站点造成影响。 首先,确保你的开发环境满足以下要求: PHP 7.4或更高版本 MySQL 5.6或更高版本 WordPress 5.8或更高版本 支持HTTPS(用于某些API调用) 接下来,创建一个专门用于开发的WordPress主题或插件目录。考虑到功能的独立性,我们建议将其开发为一个独立插件,这样可以在不同主题间保持功能一致性。 <?php /** * Plugin Name: 实时数据大屏与可视化工具 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress添加实时数据大屏和可视化图表功能 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later */ 1.2 项目架构设计 良好的项目架构是成功开发的关键。我们建议采用模块化设计,将功能分解为以下几个核心模块: 数据获取模块:负责从各种数据源收集数据 数据处理模块:对原始数据进行清洗、转换和聚合 图表渲染模块:使用JavaScript库生成可视化图表 前端展示模块:创建数据大屏的界面布局 管理后台模块:提供配置选项和管理界面 目录结构建议如下: real-time-dashboard/ ├── includes/ │ ├── class-data-fetcher.php │ ├── class-data-processor.php │ ├── class-chart-renderer.php │ └── class-admin-settings.php ├── assets/ │ ├── css/ │ ├── js/ │ └── images/ ├── templates/ │ ├── dashboard-template.php │ └── widget-template.php └── real-time-dashboard.php 1.3 技术选型与工具准备 对于数据可视化,我们需要选择合适的JavaScript图表库。以下是几个优秀的选择: Chart.js:轻量级、易于使用,适合基础图表 ECharts:功能强大、图表类型丰富,由百度开发 D3.js:最灵活、功能最强大,但学习曲线较陡 ApexCharts:现代、交互式图表库 考虑到易用性和功能平衡,本文选择ECharts作为主要图表库。同时,我们还需要以下工具: 用于实时数据更新的WebSocket或Server-Sent Events 用于数据缓存的Transients API或Redis 用于REST API开发WordPress的REST API功能 第二章:数据获取与处理模块开发 2.1 创建数据获取类 数据获取是实时数据大屏的基础。我们需要创建一个专门的数据获取类,负责从不同数据源收集信息。 <?php class RealTime_Data_Fetcher { private $data_sources; public function __construct() { $this->data_sources = array( 'website_traffic' => array( 'name' => '网站流量', 'type' => 'internal', 'callback' => array($this, 'fetch_website_traffic') ), 'woocommerce_sales' => array( 'name' => '销售数据', 'type' => 'woocommerce', 'callback' => array($this, 'fetch_woocommerce_sales') ), 'external_api' => array( 'name' => '外部API数据', 'type' => 'external', 'callback' => array($this, 'fetch_external_data') ) ); } /** * 获取网站流量数据 */ public function fetch_website_traffic($time_range = 'today') { global $wpdb; $data = array(); $current_time = current_time('timestamp'); switch($time_range) { case 'today': $start_time = strtotime('today midnight'); break; case 'week': $start_time = strtotime('-7 days'); break; case 'month': $start_time = strtotime('-30 days'); break; default: $start_time = strtotime('-24 hours'); } // 查询页面访问数据 $query = $wpdb->prepare( "SELECT COUNT(*) as count, DATE_FORMAT(visit_time, '%%H:00') as hour FROM {$wpdb->prefix}page_visits WHERE visit_time >= %s GROUP BY HOUR(visit_time) ORDER BY visit_time", date('Y-m-d H:i:s', $start_time) ); $results = $wpdb->get_results($query); foreach($results as $row) { $data[] = array( 'time' => $row->hour, 'visits' => (int)$row->count ); } return $data; } /** * 获取WooCommerce销售数据 */ public function fetch_woocommerce_sales($time_range = 'today') { if (!class_exists('WooCommerce')) { return array(); } $data = array(); // 根据时间范围获取订单数据 $args = array( 'status' => array('completed', 'processing'), 'date_created' => '>' . (time() - 24*60*60), // 最近24小时 'limit' => -1 ); $orders = wc_get_orders($args); $hourly_sales = array(); foreach($orders as $order) { $order_time = $order->get_date_created()->getTimestamp(); $hour = date('H:00', $order_time); $total = $order->get_total(); if (!isset($hourly_sales[$hour])) { $hourly_sales[$hour] = 0; } $hourly_sales[$hour] += $total; } ksort($hourly_sales); foreach($hourly_sales as $hour => $sales) { $data[] = array( 'time' => $hour, 'sales' => $sales ); } return $data; } /** * 获取所有数据源的数据 */ public function fetch_all_data() { $all_data = array(); foreach($this->data_sources as $key => $source) { if (is_callable($source['callback'])) { $all_data[$key] = call_user_func($source['callback']); } } return $all_data; } } 2.2 实现数据缓存机制 为了避免频繁查询数据库,我们需要实现数据缓存机制。WordPress提供了Transients API,可以方便地实现临时数据存储。 class RealTime_Data_Processor { private $cache_expiration = 300; // 5分钟缓存 /** * 处理并缓存数据 */ public function process_and_cache_data($data_key, $raw_data) { // 数据处理逻辑 $processed_data = $this->process_data($raw_data); // 设置缓存 set_transient( 'rtd_' . $data_key, $processed_data, $this->cache_expiration ); return $processed_data; } /** * 数据处理逻辑 */ private function process_data($raw_data) { $processed = array(); // 数据清洗和转换 foreach($raw_data as $item) { // 移除空值 $cleaned_item = array_filter($item, function($value) { return $value !== null && $value !== ''; }); // 数据格式化 if (!empty($cleaned_item)) { $processed[] = $this->format_data($cleaned_item); } } // 数据聚合(如果需要) if (count($processed) > 100) { $processed = $this->aggregate_data($processed); } return $processed; } /** * 数据格式化 */ private function format_data($data) { // 根据数据类型进行格式化 foreach($data as $key => $value) { if (is_numeric($value)) { $data[$key] = floatval($value); } elseif (is_string($value)) { $data[$key] = sanitize_text_field($value); } } return $data; } } 2.3 创建REST API端点 为了前端能够获取数据,我们需要创建REST API端点。 class RealTime_Dashboard_API { public function __construct() { add_action('rest_api_init', array($this, 'register_routes')); } public function register_routes() { // 获取所有数据 register_rest_route('real-time-dashboard/v1', '/data', array( 'methods' => 'GET', 'callback' => array($this, 'get_all_data'), 'permission_callback' => array($this, 'check_permissions') )); // 获取特定数据源的数据 register_rest_route('real-time-dashboard/v1', '/data/(?P<source>[a-zA-Z0-9_-]+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_source_data'), 'permission_callback' => array($this, 'check_permissions') )); } public function get_all_data($request) { $fetcher = new RealTime_Data_Fetcher(); $processor = new RealTime_Data_Processor(); $all_data = array(); $sources = array('website_traffic', 'woocommerce_sales'); foreach($sources as $source) { $raw_data = $fetcher->fetch_data_by_source($source); $processed_data = $processor->process_and_cache_data($source, $raw_data); $all_data[$source] = $processed_data; } return rest_ensure_response($all_data); } public function check_permissions($request) { // 检查用户权限,可以根据需要调整 return current_user_can('edit_posts'); } } 第三章:前端可视化界面开发 3.1 创建数据大屏基础布局 首先,我们需要创建一个响应式的大屏布局,确保在不同设备上都能良好显示。 <!-- templates/dashboard-template.php --> <div class="real-time-dashboard-container"> <header class="dashboard-header"> <h1>实时数据大屏</h1> <div class="dashboard-controls"> <select id="timeRangeSelect" class="dashboard-select"> <option value="realtime">实时</option> <option value="today">今日</option> <option value="week">本周</option> <option value="month">本月</option> </select> <button id="refreshBtn" class="dashboard-button"> <span class="refresh-icon">↻</span> 刷新 </button> <button id="fullscreenBtn" class="dashboard-button"> <span class="fullscreen-icon">⛶</span> 全屏 </button> </div> </header> <div class="dashboard-grid"> <!-- 图表容器将通过JavaScript动态生成 --> <div class="grid-item" data-chart-type="line" data-source="website_traffic"> <div class="chart-header"> <h3>网站实时访问量</h3> <div class="chart-stats"> <span class="current-value">0</span> <span class="change-indicator"></span> </div> </div> <div class="chart-container" id="trafficChart"></div> </div> <div class="grid-item" data-chart-type="bar" data-source="woocommerce_sales"> <div class="chart-header"> <h3>实时销售数据</h3> <div class="chart-stats"> <span class="current-value">¥0</span> <span class="change-indicator"></span> </div> </div> <div class="chart-container" id="salesChart"></div> </div> <!-- 更多图表容器... --> </div> <div class="dashboard-footer"> <div class="last-updated"> 最后更新: <span id="lastUpdatedTime">--:--:--</span> </div> <div class="data-source-info"> 数据每5分钟自动更新 </div> </div> </div> 3.2 引入ECharts并初始化图表 接下来,我们需要引入ECharts库并创建图表初始化函数。 // assets/js/dashboard-main.js (function($) { 'use strict'; class RealTimeDashboard { constructor() { this.charts = {}; this.dataCache = {}; this.updateInterval = 300000; // 5分钟 this.isFullscreen = false; this.timeRange = 'realtime'; this.init(); } init() { this.loadECharts(); this.initCharts(); this.bindEvents(); this.startAutoUpdate(); this.fetchInitialData(); } loadECharts() { // 动态加载ECharts库 if (typeof echarts === 'undefined') { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js'; script.onload = () => this.onEChartsLoaded(); document.head.appendChild(script); } else { this.onEChartsLoaded(); } } onEChartsLoaded() { console.log('ECharts loaded successfully'); this.echarts = echarts; this.initCharts(); } initCharts() { if (!this.echarts) return; // 初始化所有图表容器 $('.chart-container').each((index, container) => { const chartId = $(container).attr('id'); const chartType = $(container).closest('.grid-item').data('chart-type'); const dataSource = $(container).closest('.grid-item').data('source'); this.charts[chartId] = this.echarts.init(container); this.setChartOption(chartId, chartType, dataSource); }); // 监听窗口大小变化,重新调整图表大小 $(window).on('resize', () => { this.resizeCharts(); }); } setChartOption(chartId, chartType, dataSource) { const baseOption = { tooltip: { trigger: 'axis', formatter: function(params) { let result = `${params[0].axisValue}<br/>`; params.forEach(param => { result += `${param.marker} ${param.seriesName}: ${param.value}<br/>`; }); return result; } }, grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: [] }, yAxis: { type: 'value' }, series: [] }; // 根据图表类型设置不同选项 switch(chartType) { case 'line': baseOption.series.push({ name: '数据', type: 'line', smooth: true, data: [], itemStyle: { color: '#5470c6' }, areaStyle: { color: new this.echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(84, 112, 198, 0.5)' }, { offset: 1, color: 'rgba(84, 112, 198, 0.1)' } ]) } }); break; case 'bar': baseOption.series.push({ name: '数据', type: 'bar', data: [], itemStyle: { color: new this.echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#83bff6' }, { offset: 0.5, color: '#188df0' }, { offset: 1, color: '#188df0' } ]) } }); break; // 可以添加更多图表类型 } this.charts[chartId].setOption(baseOption); } fetchInitialData() { this.fetchData(); } fetchData() { const self = this; $.ajax({ url: '/wp-json/real-time-dashboard/v1/data', method: 'GET', dataType: 'json', beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', realTimeDashboard.nonce); }, success: function(response) { self.processData(response); self.updateLastUpdatedTime(); }, error: function(xhr, status, error) { console.error('数据获取失败:', error); self.showErrorMessage('数据加载失败,请稍后重试'); } }); } processData(data) { // 处理网站流量数据 if (data.website_traffic && this.charts.trafficChart) { this.updateChartData('trafficChart', data.website_traffic, '访问量'); this.updateStats('trafficChart', data.website_traffic); } // 处理销售数据 if (data.woocommerce_sales && this.charts.salesChart) { this.updateChartData('salesChart', data.woocommerce_sales, '销售额'); this.updateStats('salesChart', data.woocommerce_sales); } // 可以添加更多数据处理逻辑 } updateChartData(chartId, data, seriesName) { const chart = this.charts[chartId]; if (!chart) return; const xData = []; const yData = []; data.forEach(item => { xData.push(item.time || item.date); yData.push(item.visits || item.sales || item.value); }); const option = chart.getOption(); option.xAxis[0].data = xData; option.series[0].data = yData; option.series[0].name = seriesName; chart.setOption(option); } updateStats(chartId, data) { if (!data || data.length === 0) return; const currentValue = data[data.length - 1].visits || data[data.length - 1].sales || data[data.length - 1].value; const previousValue = data.length > 1 ? data[data.length - 2].visits || data[data.length - 2].sales || data[data.length - 2].value : 0; const change = previousValue > 0 ? ((currentValue - previousValue) / previousValue * 100).toFixed(1) : 0; const $gridItem = $(`#${chartId}`).closest('.grid-item'); const $currentValue = $gridItem.find('.current-value'); const $changeIndicator = $gridItem.find('.change-indicator'); // 更新当前值 $currentValue.text(this.formatValue(currentValue, chartId)); // 更新变化指示器 if (change > 0) { $changeIndicator.html(`<span class="increase">↑ ${change}%</span>`); $changeIndicator.removeClass('decrease').addClass('increase'); } else if (change < 0) { $changeIndicator.html(`<span class="decrease">↓ ${Math.abs(change)}%</span>`); $changeIndicator.removeClass('increase').addClass('decrease'); } else { $changeIndicator.html('<span class="neutral">→ 0%</span>'); $changeIndicator.removeClass('increase decrease'); } } formatValue(value, chartId) { if (chartId === 'salesChart') { return '¥' + Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } return Number(value).toLocaleString('zh-CN'); } bindEvents() { const self = this; // 刷新按钮 $('#refreshBtn').on('click', function() { self.fetchData(); $(this).addClass('refreshing'); setTimeout(() => { $(this).removeClass('refreshing'); }, 1000); }); // 时间范围选择 $('#timeRangeSelect').on('change', function() { self.timeRange = $(this).val(); self.fetchData(); }); // 全屏按钮 $('#fullscreenBtn').on('click', function() { self.toggleFullscreen(); }); // 键盘快捷键 $(document).on('keydown', function(e) { if (e.key === 'F11' || (e.key === 'f' && e.ctrlKey)) { e.preventDefault(); self.toggleFullscreen(); } else if (e.key === 'r' && e.ctrlKey) { e.preventDefault(); self.fetchData(); } }); } toggleFullscreen() { const container = $('.real-time-dashboard-container')[0]; if (!this.isFullscreen) { if (container.requestFullscreen) { container.requestFullscreen(); } else if (container.webkitRequestFullscreen) { container.webkitRequestFullscreen(); } else if (container.msRequestFullscreen) { container.msRequestFullscreen(); } this.isFullscreen = true; $('#fullscreenBtn').html('<span class="exit-fullscreen-icon">⛶</span> 退出全屏'); } else { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } this.isFullscreen = false; $('#fullscreenBtn').html('<span class="fullscreen-icon">⛶</span> 全屏'); } } resizeCharts() { Object.values(this.charts).forEach(chart => { chart.resize(); }); } startAutoUpdate() { setInterval(() => { this.fetchData(); }, this.updateInterval); } updateLastUpdatedTime() { const now = new Date(); const timeString = now.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); $('#lastUpdatedTime').text(timeString); } showErrorMessage(message) { // 显示错误提示 const $errorDiv = $('<div class="dashboard-error">') .text(message) .hide() .appendTo('.dashboard-header'); $errorDiv.fadeIn(300); setTimeout(() => { $errorDiv.fadeOut(300, function() { $(this).remove(); }); }, 5000); } } // 初始化仪表板 $(document).ready(function() { if ($('.real-time-dashboard-container').length) { window.realTimeDashboardApp = new RealTimeDashboard(); } }); })(jQuery); 3.3 添加CSS样式 /* assets/css/dashboard-style.css */ .real-time-dashboard-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #ffffff; min-height: 100vh; padding: 20px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .dashboard-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .dashboard-header h1 { margin: 0; font-size: 28px; font-weight: 600; background: linear-gradient(90deg, #4cc9f0, #4361ee); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .dashboard-controls { display: flex; gap: 12px; align-items: center; } .dashboard-select { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); color: white; padding: 8px 16px; border-radius: 6px; font-size: 14px; cursor: pointer; transition: all 0.3s ease; } .dashboard-select:hover { background: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.3); } .dashboard-select:focus { outline: none; box-shadow: 0 0 0 2px rgba(76, 201, 240, 0.3); } .dashboard-button { background: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%); border: none; color: white; padding: 8px 16px; border-radius: 6px; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 6px; transition: all 0.3s ease; } .dashboard-button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(67, 97, 238, 0.4); } .dashboard-button:active { transform: translateY(0); } .dashboard-button.refreshing .refresh-icon { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 20px; margin-bottom: 30px; } .grid-item { background: rgba(255, 255, 255, 0.05); border-radius: 10px; padding: 20px; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; } .grid-item:hover { transform: translateY(-5px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); border-color: rgba(76, 201, 240, 0.3); } .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .chart-header h3 { margin: 0; font-size: 18px; font-weight: 500; color: #e0e0e0; } .chart-stats { display: flex; align-items: center; gap: 10px; } .current-value { font-size: 24px; font-weight: 600; color: #4cc9f0; } .change-indicator { font-size: 14px; padding: 4px 8px; border-radius: 4px; } .change-indicator.increase { background: rgba(76, 175, 80, 0.2); color: #4caf50; } .change-indicator.decrease { background: rgba(244, 67, 54, 0.2); color: #f44336; } .change-indicator.neutral { background: rgba(158, 158, 158, 0.2); color: #9e9e9e; } .chart-container { width: 100%; height: 300px; } .dashboard-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 20px; border-top: 1px solid rgba(255, 255, 255, 0.1); font-size: 14px; color: #b0b0b0; } .last-updated { display: flex; align-items: center; gap: 8px; } #lastUpdatedTime { color: #4cc9f0; font-weight: 500; } .data-source-info { display: flex; align-items: center; gap: 8px; } .data-source-info::before { content: "⏱️"; font-size: 12px; } /* 全屏模式样式 */ :fullscreen .real-time-dashboard-container { padding: 40px; border-radius: 0; } :fullscreen .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); gap: 30px; } :fullscreen .chart-container { height: 400px; } /* 响应式设计 */ @media (max-width: 1200px) { .dashboard-grid { grid-template-columns: 1fr; } } @media (max-width: 768px) { .dashboard-header { flex-direction: column; gap: 15px; align-items: flex-start; } .dashboard-controls { width: 100%; justify-content: space-between; } .dashboard-grid { grid-template-columns: 1fr; } .grid-item { min-width: 100%; } } /* 加载动画 */ .chart-loading { position: relative; } .chart-loading::after { content: ''; position: absolute; top: 50%; left: 50%; width: 40px; height: 40px; margin: -20px 0 0 -20px; border: 3px solid rgba(76, 201, 240, 0.3); border-top-color: #4cc9f0; border-radius: 50%; animation: spin 1s linear infinite; } /* 错误提示 */ .dashboard-error { position: fixed; top: 20px; right: 20px; background: #f44336; color: white; padding: 12px 20px; border-radius: 6px; box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3); z-index: 1000; animation: slideIn 0.3s ease; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } 第四章:管理后台与配置界面 4.1 创建管理菜单和设置页面 <?php class RealTime_Dashboard_Admin { private $settings_page; public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_init', array($this, 'register_settings')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } public function add_admin_menu() { $this->settings_page = add_menu_page( '实时数据大屏设置', '数据大屏', 'manage_options', 'real-time-dashboard', array($this, 'render_settings_page'), 'dashicons-chart-area', 30 ); // 添加子菜单 add_submenu_page( 'real-time-dashboard', '数据源配置', '数据源配置', 'manage_options', 'real-time-dashboard-sources', array($this, 'render_sources_page') ); add_submenu_page( 'real-time-dashboard', '图表设置', '图表设置', 'manage_options', 'real-time-dashboard-charts', array($this, 'render_charts_page') ); } public function render_settings_page() { ?> <div class="wrap real-time-dashboard-admin"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <div class="dashboard-admin-header"> <div class="admin-stats"> <div class="stat-card"> <h3>数据源状态</h3> <div class="stat-value" id="dataSourcesStatus">检查中...</div> </div> <div class="stat-card"> <h3>最后更新</h3> <div class="stat-value" id="lastDataUpdate">--:--:--</div> </div> <div class="stat-card"> <h3>缓存状态</h3> <div class="stat-value" id="cacheStatus">正常</div> </div> </div> </div> <form method="post" action="options.php"> <?php settings_fields('real_time_dashboard_settings'); do_settings_sections('real_time_dashboard_settings'); submit_button('保存设置'); ?> </form> <div class="admin-actions"> <h2>快速操作</h2> <div class="action-buttons"> <button type="button" class="button button-primary" id="clearCacheBtn"> 清除缓存 </button> <button type="button" class="button button-secondary" id="testDataBtn"> 测试数据源 </button> <button type="button" class="button button-secondary" id="exportConfigBtn"> 导出配置 </button> <a href="<?php echo home_url('/real-time-dashboard/'); ?>" class="button button-secondary" target="_blank"> 查看大屏 </a> </div> </div> </div> <?php } public function register_settings() { // 注册基本设置 register_setting( 'real_time_dashboard_settings', 'rtd_general_settings',
发表评论实战教学:为网站集成在线预约排队与叫号系统——通过WordPress代码二次开发实现常用互联网小工具功能 引言:数字化时代下的服务升级需求 在当今快节奏的数字化社会中,无论是医疗机构、教育机构、政府服务大厅,还是餐饮、美容、维修等商业服务场所,排队等待已成为客户体验中最令人不满的环节之一。传统的排队方式不仅消耗客户宝贵时间,也增加了服务场所的管理难度和人力成本。随着互联网技术的普及,在线预约排队与叫号系统应运而生,成为提升服务效率、优化客户体验的重要工具。 对于大多数中小型企业和机构而言,定制开发一套完整的预约排队系统成本高昂,维护复杂。而WordPress作为全球最流行的内容管理系统,以其强大的扩展性和灵活性,为我们提供了一个经济高效的解决方案。本文将深入探讨如何通过WordPress程序的代码二次开发,为网站集成一套功能完善的在线预约排队与叫号系统,实现这一常用互联网小工具功能。 第一章:系统需求分析与设计规划 1.1 业务场景分析 在开始技术实现之前,我们首先需要明确系统的使用场景和功能需求。一个典型的在线预约排队与叫号系统应包含以下核心功能: 客户端功能: 服务项目浏览与选择 预约时间选择与提交 排队号码获取与状态查询 实时排队位置提醒 预约取消与改期 管理端功能: 预约申请审核与管理 排队队列实时监控 叫号操作与控制 服务窗口/人员管理 数据统计与分析报表 现场显示功能: 当前叫号信息展示 等待队列信息展示 服务窗口状态显示 1.2 技术架构设计 基于WordPress的二次开发,我们采用以下技术架构: 前端展示层:使用WordPress主题模板系统,结合HTML5、CSS3和JavaScript(特别是AJAX技术)实现用户界面 业务逻辑层:通过自定义Post Type、Taxonomy和Meta Box构建数据模型,利用Action和Filter钩子扩展核心功能 数据持久层:利用WordPress数据库结构,创建自定义数据表存储预约和排队信息 实时通信层:采用WebSocket或轮询技术实现排队状态的实时更新 外部接口层:集成短信/邮件通知API,提供状态提醒服务 第二章:WordPress开发环境搭建与基础配置 2.1 开发环境准备 在开始开发之前,我们需要搭建一个适合WordPress二次开发的环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:下载最新版WordPress,完成基础安装 开发工具配置: 代码编辑器:VS Code或PHPStorm 版本控制:Git初始化仓库 调试工具:安装Query Monitor、Debug Bar等调试插件 代码标准:配置PHP_CodeSniffer遵循WordPress编码标准 2.2 创建自定义插件 为了避免主题更新导致功能丢失,我们将所有功能集成到一个独立插件中: <?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('APPOINTMENT_SYSTEM_VERSION', '1.0.0'); define('APPOINTMENT_SYSTEM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('APPOINTMENT_SYSTEM_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once APPOINTMENT_SYSTEM_PLUGIN_DIR . 'includes/class-appointment-system.php'; function run_appointment_system() { $plugin = new Appointment_System(); $plugin->run(); } run_appointment_system(); 第三章:数据模型设计与数据库结构 3.1 自定义Post Type设计 我们将使用WordPress的自定义文章类型来管理预约和服务项目: // 注册服务项目自定义文章类型 function register_service_post_type() { $labels = array( 'name' => '服务项目', 'singular_name' => '服务项目', 'menu_name' => '服务项目管理', 'add_new' => '添加服务项目', 'add_new_item' => '添加新服务项目', 'edit_item' => '编辑服务项目', 'new_item' => '新服务项目', 'view_item' => '查看服务项目', 'search_items' => '搜索服务项目', 'not_found' => '未找到服务项目', 'not_found_in_trash' => '回收站中无服务项目' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'service'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-calendar-alt', 'supports' => array('title', 'editor', 'thumbnail', 'excerpt'), 'show_in_rest' => true ); register_post_type('service', $args); } add_action('init', 'register_service_post_type'); 3.2 自定义数据库表设计 对于排队和预约数据,我们需要创建独立的数据库表以保证查询效率: // 创建自定义数据库表 function create_appointment_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name_appointments = $wpdb->prefix . 'appointments'; $table_name_queues = $wpdb->prefix . 'queues'; // 预约表 $sql_appointments = "CREATE TABLE IF NOT EXISTS $table_name_appointments ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) DEFAULT 0, service_id bigint(20) NOT NULL, appointment_date date NOT NULL, time_slot varchar(50) NOT NULL, customer_name varchar(100) NOT NULL, customer_phone varchar(20) NOT NULL, customer_email varchar(100), status varchar(20) DEFAULT 'pending', queue_number varchar(50), created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY service_id (service_id), KEY appointment_date (appointment_date), KEY status (status) ) $charset_collate;"; // 排队表 $sql_queues = "CREATE TABLE IF NOT EXISTS $table_name_queues ( id bigint(20) NOT NULL AUTO_INCREMENT, appointment_id bigint(20) NOT NULL, service_point_id bigint(20) NOT NULL, queue_number varchar(50) NOT NULL, position int(11) NOT NULL, estimated_wait_time int(11) DEFAULT 0, called_at datetime, served_at datetime, status varchar(20) DEFAULT 'waiting', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY appointment_id (appointment_id), KEY service_point_id (service_point_id), KEY status (status) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_appointments); dbDelta($sql_queues); } register_activation_hook(__FILE__, 'create_appointment_tables'); 第四章:前端预约界面开发 4.1 预约表单设计与实现 创建一个用户友好的预约表单是系统的关键: // 预约表单短代码 function appointment_form_shortcode($atts) { ob_start(); // 获取服务项目 $services = get_posts(array( 'post_type' => 'service', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC' )); // 获取可预约日期(未来30天) $available_dates = array(); $today = new DateTime(); for ($i = 1; $i <= 30; $i++) { $date = clone $today; $date->add(new DateInterval("P{$i}D")); // 排除周末 if ($date->format('N') < 6) { $available_dates[] = $date->format('Y-m-d'); } } // 定义时间槽 $time_slots = array( '09:00-10:00', '10:00-11:00', '11:00-12:00', '13:00-14:00', '14:00-15:00', '15:00-16:00', '16:00-17:00' ); ?> <div class="appointment-form-container"> <h2>在线预约</h2> <form id="appointment-form" method="post"> <?php wp_nonce_field('submit_appointment', 'appointment_nonce'); ?> <div class="form-group"> <label for="service_id">选择服务项目 *</label> <select id="service_id" name="service_id" required> <option value="">请选择服务项目</option> <?php foreach ($services as $service): ?> <option value="<?php echo $service->ID; ?>"> <?php echo esc_html($service->post_title); ?> </option> <?php endforeach; ?> </select> </div> <div class="form-group"> <label for="appointment_date">选择预约日期 *</label> <select id="appointment_date" name="appointment_date" required> <option value="">请选择日期</option> <?php foreach ($available_dates as $date): ?> <option value="<?php echo $date; ?>"> <?php echo date('Y年m月d日', strtotime($date)); ?> </option> <?php endforeach; ?> </select> </div> <div class="form-group"> <label for="time_slot">选择时间段 *</label> <select id="time_slot" name="time_slot" required> <option value="">请选择时间段</option> <?php foreach ($time_slots as $slot): ?> <option value="<?php echo $slot; ?>"><?php echo $slot; ?></option> <?php endforeach; ?> </select> </div> <div class="form-group"> <label for="customer_name">姓名 *</label> <input type="text" id="customer_name" name="customer_name" required> </div> <div class="form-group"> <label for="customer_phone">手机号 *</label> <input type="tel" id="customer_phone" name="customer_phone" required> </div> <div class="form-group"> <label for="customer_email">电子邮箱</label> <input type="email" id="customer_email" name="customer_email"> </div> <div class="form-group"> <button type="submit" id="submit-appointment">提交预约</button> </div> <div id="appointment-response"></div> </form> </div> <script> jQuery(document).ready(function($) { $('#appointment-form').on('submit', function(e) { e.preventDefault(); var formData = $(this).serialize(); $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'submit_appointment', data: formData }, beforeSend: function() { $('#submit-appointment').prop('disabled', true).text('提交中...'); }, success: function(response) { if (response.success) { $('#appointment-response').html( '<div class="success-message">' + '预约成功!您的排队号码是:<strong>' + response.data.queue_number + '</strong>' + '</div>' ); $('#appointment-form')[0].reset(); } else { $('#appointment-response').html( '<div class="error-message">' + response.data + '</div>' ); } $('#submit-appointment').prop('disabled', false).text('提交预约'); } }); }); }); </script> <?php return ob_get_clean(); } add_shortcode('appointment_form', 'appointment_form_shortcode'); 4.2 实时排队状态展示 // 排队状态展示短代码 function queue_status_shortcode() { ob_start(); ?> <div class="queue-status-container"> <h2>实时排队状态</h2> <div class="current-calls"> <h3>当前叫号</h3> <div id="current-calls-display"> <!-- 通过AJAX动态加载 --> </div> </div> <div class="waiting-queue"> <h3>等待队列</h3> <div id="waiting-queue-display"> <!-- 通过AJAX动态加载 --> </div> </div> <div class="queue-search"> <h3>查询我的排队状态</h3> <input type="text" id="queue-number-input" placeholder="请输入您的排队号码"> <button id="check-queue-status">查询</button> <div id="personal-queue-status"></div> </div> </div> <script> jQuery(document).ready(function($) { // 更新排队状态 function updateQueueStatus() { $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'GET', data: { action: 'get_queue_status' }, success: function(response) { if (response.success) { $('#current-calls-display').html(response.data.current_calls); $('#waiting-queue-display').html(response.data.waiting_queue); } } }); } // 初始加载 updateQueueStatus(); // 每30秒更新一次 setInterval(updateQueueStatus, 30000); // 查询个人排队状态 $('#check-queue-status').on('click', function() { var queueNumber = $('#queue-number-input').val(); if (!queueNumber) { alert('请输入排队号码'); return; } $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'get_personal_queue_status', queue_number: queueNumber }, success: function(response) { if (response.success) { $('#personal-queue-status').html(response.data); } else { $('#personal-queue-status').html( '<div class="error">' + response.data + '</div>' ); } } }); }); }); </script> <?php return ob_get_clean(); } add_shortcode('queue_status', 'queue_status_shortcode'); 第五章:后台管理系统开发 5.1 预约管理界面 // 添加预约管理菜单 function add_appointment_admin_menu() { add_menu_page( '预约管理', '预约管理', 'manage_options', 'appointment-management', 'display_appointment_management_page', 'dashicons-calendar', 6 ); add_submenu_page( 'appointment-management', '排队叫号', '排队叫号', 'manage_options', 'queue-management', 'display_queue_management_page' ); add_submenu_page( 'appointment-management', '服务窗口设置', '服务窗口设置', 'manage_options', 'service-points', 'display_service_points_page' ); } add_action('admin_menu', 'add_appointment_admin_menu'); // 预约管理页面 function display_appointment_management_page() { global $wpdb; $table_name = $wpdb->prefix . 'appointments'; // 处理批量操作 if (isset($_POST['bulk_action']) && isset($_POST['appointment_ids'])) { $action = $_POST['bulk_action']; $ids = implode(',', array_map('intval', $_POST['appointment_ids'])); switch ($action) { case 'confirm': $wpdb->query("UPDATE $table_name SET status = 'confirmed' WHERE id IN ($ids)"); break; case 'cancel': $wpdb->query("UPDATE $table_name SET status = 'cancelled' WHERE id IN ($ids)"); break; case 'delete': $wpdb->query("DELETE FROM $table_name WHERE id IN ($ids)"); break; } } // 获取预约数据 $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $offset = ($current_page - 1) * $per_page; $where_conditions = array('1=1'); $where_values = array(); // 筛选条件 if (!empty($_GET['status'])) { $where_conditions[] = 'status = %s'; 5.2 排队叫号管理界面 // 排队叫号管理页面 function display_queue_management_page() { global $wpdb; $appointments_table = $wpdb->prefix . 'appointments'; $queues_table = $wpdb->prefix . 'queues'; $service_points_table = $wpdb->prefix . 'service_points'; // 获取所有服务窗口 $service_points = $wpdb->get_results("SELECT * FROM $service_points_table WHERE is_active = 1"); // 处理叫号操作 if (isset($_POST['call_next']) && isset($_POST['service_point_id'])) { $service_point_id = intval($_POST['service_point_id']); // 获取当前窗口的下一个排队号 $next_queue = $wpdb->get_row($wpdb->prepare( "SELECT q.*, a.customer_name, a.service_id FROM $queues_table q LEFT JOIN $appointments_table a ON q.appointment_id = a.id WHERE q.service_point_id = %d AND q.status = 'waiting' ORDER BY q.position ASC LIMIT 1", $service_point_id )); if ($next_queue) { // 更新为已叫号状态 $wpdb->update( $queues_table, array( 'status' => 'called', 'called_at' => current_time('mysql') ), array('id' => $next_queue->id) ); echo '<div class="notice notice-success"><p>已叫号:' . esc_html($next_queue->queue_number) . '</p></div>'; // 记录叫号历史 $wpdb->insert( $wpdb->prefix . 'call_history', array( 'queue_id' => $next_queue->id, 'service_point_id' => $service_point_id, 'called_at' => current_time('mysql'), 'operator_id' => get_current_user_id() ) ); } } // 处理完成服务操作 if (isset($_POST['complete_service']) && isset($_POST['queue_id'])) { $queue_id = intval($_POST['queue_id']); $wpdb->update( $queues_table, array( 'status' => 'completed', 'served_at' => current_time('mysql') ), array('id' => $queue_id) ); echo '<div class="notice notice-success"><p>服务已完成</p></div>'; } ?> <div class="wrap"> <h1>排队叫号管理</h1> <div class="queue-management-dashboard"> <!-- 服务窗口状态 --> <div class="service-points-status"> <h2>服务窗口状态</h2> <div class="service-points-grid"> <?php foreach ($service_points as $point): // 获取当前窗口状态 $current_queue = $wpdb->get_row($wpdb->prepare( "SELECT q.*, a.customer_name FROM $queues_table q LEFT JOIN $appointments_table a ON q.appointment_id = a.id WHERE q.service_point_id = %d AND q.status = 'called' ORDER BY q.called_at DESC LIMIT 1", $point->id )); $waiting_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $queues_table WHERE service_point_id = %d AND status = 'waiting'", $point->id )); ?> <div class="service-point-card"> <h3><?php echo esc_html($point->name); ?> (<?php echo esc_html($point->code); ?>)</h3> <div class="current-serving"> <?php if ($current_queue): ?> <p>当前服务:<?php echo esc_html($current_queue->queue_number); ?></p> <p>客户:<?php echo esc_html($current_queue->customer_name); ?></p> <p>等待时间:<?php echo human_time_diff(strtotime($current_queue->called_at), current_time('timestamp')); ?></p> <form method="post" style="display: inline;"> <input type="hidden" name="queue_id" value="<?php echo $current_queue->id; ?>"> <button type="submit" name="complete_service" class="button button-primary"> 完成服务 </button> </form> <?php else: ?> <p>窗口空闲</p> <?php endif; ?> </div> <div class="waiting-info"> <p>等待人数:<?php echo $waiting_count; ?></p> <form method="post"> <input type="hidden" name="service_point_id" value="<?php echo $point->id; ?>"> <button type="submit" name="call_next" class="button"> 叫下一个号 </button> </form> </div> </div> <?php endforeach; ?> </div> </div> <!-- 实时排队队列 --> <div class="real-time-queues"> <h2>实时排队队列</h2> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>排队号码</th> <th>客户姓名</th> <th>服务项目</th> <th>服务窗口</th> <th>排队位置</th> <th>预计等待</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <?php $queues = $wpdb->get_results(" SELECT q.*, a.customer_name, s.post_title as service_name, sp.name as point_name FROM $queues_table q LEFT JOIN $appointments_table a ON q.appointment_id = a.id LEFT JOIN {$wpdb->posts} s ON a.service_id = s.ID LEFT JOIN $service_points_table sp ON q.service_point_id = sp.id WHERE q.status IN ('waiting', 'called') ORDER BY CASE q.status WHEN 'called' THEN 1 WHEN 'waiting' THEN 2 ELSE 3 END, q.position ASC LIMIT 50 "); foreach ($queues as $queue): $wait_time = $queue->estimated_wait_time > 0 ? round($queue->estimated_wait_time / 60) . '分钟' : '计算中'; ?> <tr> <td><?php echo esc_html($queue->queue_number); ?></td> <td><?php echo esc_html($queue->customer_name); ?></td> <td><?php echo esc_html($queue->service_name); ?></td> <td><?php echo esc_html($queue->point_name); ?></td> <td><?php echo $queue->position; ?></td> <td><?php echo $wait_time; ?></td> <td> <span class="status-badge status-<?php echo $queue->status; ?>"> <?php $status_labels = array( 'waiting' => '等待中', 'called' => '已叫号', 'completed' => '已完成' ); echo $status_labels[$queue->status] ?? $queue->status; ?> </span> </td> <td> <?php if ($queue->status == 'waiting'): ?> <form method="post" style="display: inline;"> <input type="hidden" name="queue_id" value="<?php echo $queue->id; ?>"> <input type="hidden" name="service_point_id" value="<?php echo $queue->service_point_id; ?>"> <button type="submit" name="call_now" class="button button-small"> 立即叫号 </button> </form> <?php endif; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <!-- 叫号历史 --> <div class="call-history"> <h2>今日叫号历史</h2> <?php $today = current_time('Y-m-d'); $history = $wpdb->get_results($wpdb->prepare( "SELECT ch.*, q.queue_number, a.customer_name, sp.name as point_name FROM {$wpdb->prefix}call_history ch LEFT JOIN $queues_table q ON ch.queue_id = q.id LEFT JOIN $appointments_table a ON q.appointment_id = a.id LEFT JOIN $service_points_table sp ON ch.service_point_id = sp.id WHERE DATE(ch.called_at) = %s ORDER BY ch.called_at DESC LIMIT 20", $today )); ?> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>时间</th> <th>排队号码</th> <th>客户姓名</th> <th>服务窗口</th> <th>操作员</th> </tr> </thead> <tbody> <?php foreach ($history as $record): $operator = get_userdata($record->operator_id); ?> <tr> <td><?php echo date('H:i:s', strtotime($record->called_at)); ?></td> <td><?php echo esc_html($record->queue_number); ?></td> <td><?php echo esc_html($record->customer_name); ?></td> <td><?php echo esc_html($record->point_name); ?></td> <td><?php echo $operator ? esc_html($operator->display_name) : '系统'; ?></td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> </div> <style> .service-points-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; } .service-point-card { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #fff; } .service-point-card h3 { margin-top: 0; border-bottom: 2px solid #0073aa; padding-bottom: 10px; } .current-serving { background: #f0f8ff; padding: 10px; border-radius: 4px; margin-bottom: 15px; } .status-badge { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; } .status-waiting { background: #ffeb3b; color: #333; } .status-called { background: #4caf50; color: white; } .status-completed { background: #9e9e9e; color: white; } </style> <?php } 第六章:实时通信与状态更新 6.1 WebSocket服务器实现 // WebSocket服务器类 class QueueWebSocketServer { private $server; private $clients = []; private $rooms = []; public function __construct($host = '0.0.0.0', $port = 8080) { $this->server = new SwooleWebSocketServer($host, $port); $this->server->on('open', [$this, 'onOpen']); $this->server->on('message', [$this, 'onMessage']); $this->server->on('close', [$this, 'onClose']); // 定时推送队列更新 $this->server->tick(5000, [$this, 'broadcastQueueUpdates']); } public function onOpen($server, $request) { $client_id = $request->fd; $this->clients[$client_id] = [ 'id' => $client_id, 'room' => null, 'type' => 'client' // client, display, admin ]; echo "客户端 {$client_id} 已连接n"; } public function onMessage($server, $frame) { $data = json_decode($frame->data, true); $client_id = $frame->fd; if (!$data || !isset($data['action'])) { return; } switch ($data['action']) { case 'join_room': $this->joinRoom($client_id, $data['room']); break; case 'subscribe_queue': $this->subscribeQueue($client_id, $data['queue_number']); break; case 'admin_command': $this->handleAdminCommand($client_id, $data); break; } } public function onClose($server, $fd) { if (isset($this->clients[$fd])) { $client = $this->clients[$fd]; // 从房间中移除 if ($client['room'] && isset($this->rooms[$client['room']])) { $index = array_search($fd, $this->rooms[$client['room']]); if ($index !== false) { array_splice($this->rooms[$client['room']], $index, 1); } } unset($this->clients[$fd]); echo "客户端 {$fd} 已断开连接n"; } } private function joinRoom($client_id, $room) { $this->clients[$client_id]['room'] = $room; if (!isset($this->rooms[$room])) { $this->rooms[$room] = []; } if (!in_array($client_id, $this->rooms[$room])) { $this->rooms[$room][] = $client_id; } // 发送欢迎消息 $welcome = [ 'type' => 'system', 'message' => "已加入房间: {$room}", 'timestamp' => time() ]; $this->server->push($client_id, json_encode($welcome)); } private function subscribeQueue($client_id, $queue_number) { $this->clients[$client_id]['queue_number'] = $queue_number; // 发送当前队列状态 $queue_status = $this->getQueueStatus($queue_number); $this->server->push($client_id, json_encode([ 'type' => 'queue_status', 'data' => $queue_status ])); } private function handleAdminCommand($client_id, $data) { if ($this->clients[$client_id]['type'] !== 'admin') { return; } switch ($data['command']) { case 'call_number': $this->broadcastCallNumber($data['queue_number'], $data['service_point']); break; case 'update_queue': $this->broadcastQueueUpdate(); break; } } public function broadcastQueueUpdates() { global $wpdb; $queues_table = $wpdb->prefix . 'queues'; // 获取需要更新的队列信息 $active_queues = $wpdb->get_results(" SELECT queue_number, position, estimated_wait_time, status FROM $queues_table WHERE status IN ('waiting', 'called') AND updated_at >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) "); if (empty($active_queues)) { return; } $update_data = [ 'type' => 'queue_update', 'data' => $active_queues, 'timestamp' => time() ]; // 广播给所有客户端 foreach ($this->clients as $client_id => $client) { if ($client['type'] === 'client') { $this->server->push($client_id, json_encode($update_data)); } } // 广播给显示屏幕 if (isset($this->rooms['display'])) { $display_data = $this->getDisplayData(); foreach ($this->rooms['display'] as $display_client) { $this->server->push($display_client, json_encode([ 'type' => 'display_update', 'data' => $display_data ])); } } } private function broadcastCallNumber($queue_number, $service_point) { $call_data = [ 'type' => 'call_number', 'queue_number' => $queue_number, 'service_point' => $service_point, 'timestamp' => time() ]; // 通知特定客户 foreach ($this->clients as $client_id => $client) { if (isset($client['queue_number']) && $client['queue_number'] === $queue_number) { $this->server->push($client_id, json_encode($call_data)); } } // 广播给显示屏幕 if (isset($this->rooms['display'])) { foreach ($this->rooms['display'] as $display_client) { $this->server->push($display_client, json_encode($call_data)); } } // 记录到数据库 global $wpdb; $wpdb->insert($wpdb->prefix . 'call_notifications', [ 'queue_number' => $queue_number, 'service_point' => $service_point, 'called_at' => current_time('mysql') ]); } private function getQueueStatus($queue_number) { global $wpdb; $queues_table = $wpdb->prefix . 'queues'; $appointments_table = $wpdb->prefix . 'appointments';
发表评论WordPress高级教程:开发内部知识库与文档协作中心 引言:WordPress的无限可能性 WordPress作为全球最流行的内容管理系统,早已超越了简单的博客平台定位。根据W3Techs的数据,截至2023年,WordPress占据了全球网站市场份额的43%,其中超过三分之一的顶级网站使用WordPress。这一成功不仅源于其用户友好的界面和丰富的插件生态,更得益于其高度可扩展的架构设计。 本教程将深入探讨如何通过WordPress的二次开发,构建一个功能完善的内部知识库与文档协作中心,并集成常用互联网小工具功能。我们将从基础架构设计开始,逐步深入到高级功能实现,最终打造一个集知识管理、团队协作和工具集成为一体的企业级平台。 第一部分:知识库系统架构设计 1.1 知识库内容模型设计 一个高效的知识库系统需要精心设计的内容架构。在WordPress中,我们可以通过自定义文章类型(Custom Post Types)和分类法(Taxonomies)来构建专业的知识库结构。 // 注册知识库自定义文章类型 function register_knowledgebase_post_type() { $labels = array( 'name' => '知识库文章', 'singular_name' => '知识库文章', 'menu_name' => '知识库', 'add_new' => '添加新文章', 'add_new_item' => '添加新知识库文章', 'edit_item' => '编辑知识库文章', 'new_item' => '新知识库文章', 'view_item' => '查看知识库文章', 'search_items' => '搜索知识库文章', 'not_found' => '未找到知识库文章', 'not_found_in_trash' => '回收站中未找到知识库文章' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'knowledgebase'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-book', 'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments', 'revisions', 'custom-fields'), 'show_in_rest' => true, // 启用Gutenberg编辑器支持 ); register_post_type('knowledgebase', $args); } add_action('init', 'register_knowledgebase_post_type'); // 注册知识库分类法 function register_knowledgebase_taxonomies() { // 主分类 register_taxonomy( 'kb_category', 'knowledgebase', array( 'label' => '知识分类', 'rewrite' => array('slug' => 'kb-category'), 'hierarchical' => true, 'show_admin_column' => true, 'show_in_rest' => true, ) ); // 标签 register_taxonomy( 'kb_tag', 'knowledgebase', array( 'label' => '知识标签', 'rewrite' => array('slug' => 'kb-tag'), 'hierarchical' => false, 'show_admin_column' => true, 'show_in_rest' => true, ) ); } add_action('init', 'register_knowledgebase_taxonomies'); 1.2 高级权限管理系统 内部知识库需要精细的权限控制。我们可以扩展WordPress的角色和权限系统,实现基于团队、部门和用户级别的访问控制。 // 添加自定义用户角色和权限 function add_knowledgebase_roles() { // 知识库编辑者角色 add_role('kb_editor', '知识库编辑者', array( 'read' => true, 'edit_knowledgebase' => true, 'edit_published_knowledgebase' => true, 'publish_knowledgebase' => true, 'delete_knowledgebase' => true, 'delete_published_knowledgebase' => true, 'upload_files' => true, 'read_knowledgebase' => true, )); // 知识库审核者角色 add_role('kb_reviewer', '知识库审核者', array( 'read' => true, 'edit_knowledgebase' => true, 'edit_others_knowledgebase' => true, 'edit_published_knowledgebase' => true, 'publish_knowledgebase' => true, 'read_private_knowledgebase' => true, 'delete_knowledgebase' => true, 'delete_published_knowledgebase' => true, 'delete_private_knowledgebase' => true, 'delete_others_knowledgebase' => true, 'manage_knowledgebase_categories' => true, )); } add_action('init', 'add_knowledgebase_roles'); // 扩展用户权限 function add_knowledgebase_capabilities() { $roles = array('administrator', 'editor', 'kb_editor', 'kb_reviewer'); foreach ($roles as $role_name) { $role = get_role($role_name); if ($role) { $role->add_cap('read_knowledgebase'); $role->add_cap('edit_knowledgebase'); $role->add_cap('edit_knowledgebases'); $role->add_cap('edit_others_knowledgebases'); $role->add_cap('publish_knowledgebases'); $role->add_cap('read_private_knowledgebases'); } } } add_action('admin_init', 'add_knowledgebase_capabilities'); 第二部分:文档协作功能实现 2.1 实时协作编辑器集成 现代文档协作中心需要支持多人实时编辑。我们可以集成开源协作编辑器,如CKEditor 5或Quill,实现类似Google Docs的协作体验。 // 集成协作编辑器 function enqueue_collaborative_editor() { global $post; // 仅在知识库文章编辑页面加载 if (is_admin() && isset($post) && $post->post_type === 'knowledgebase') { // 加载CKEditor 5 wp_enqueue_script('ckeditor5', 'https://cdn.ckeditor.com/ckeditor5/36.0.1/super-build/ckeditor.js', array(), '36.0.1', true); // 加载协作插件 wp_enqueue_script('collab-editor', get_template_directory_uri() . '/js/collab-editor.js', array('ckeditor5', 'jquery'), '1.0.0', true); // 传递必要参数 wp_localize_script('collab-editor', 'collabConfig', array( 'postId' => $post->ID, 'userId' => get_current_user_id(), 'userName' => wp_get_current_user()->display_name, 'nonce' => wp_create_nonce('collab_editor_nonce'), 'apiUrl' => rest_url('collab/v1/'), )); } } add_action('admin_enqueue_scripts', 'enqueue_collaborative_editor'); // 创建REST API端点处理实时协作 function register_collaboration_api() { register_rest_route('collab/v1', '/update/(?P<id>d+)', array( 'methods' => 'POST', 'callback' => 'handle_collaborative_update', 'permission_callback' => function() { return current_user_can('edit_knowledgebase'); }, 'args' => array( 'id' => array( 'validate_callback' => function($param, $request, $key) { return is_numeric($param); } ), ), )); register_rest_route('collab/v1', '/presence/(?P<id>d+)', array( 'methods' => 'GET', 'callback' => 'get_active_users', 'permission_callback' => function() { return current_user_can('read_knowledgebase'); }, )); } add_action('rest_api_init', 'register_collaboration_api'); function handle_collaborative_update(WP_REST_Request $request) { $post_id = $request->get_param('id'); $content = $request->get_param('content'); $user_id = get_current_user_id(); // 验证权限 if (!current_user_can('edit_post', $post_id)) { return new WP_Error('rest_forbidden', '您没有编辑此文章的权限', array('status' => 403)); } // 更新文章内容 wp_update_post(array( 'ID' => $post_id, 'post_content' => wp_kses_post($content) )); // 记录协作历史 add_post_meta($post_id, '_collab_history', array( 'user_id' => $user_id, 'timestamp' => current_time('mysql'), 'action' => 'edit' )); return new WP_REST_Response(array( 'success' => true, 'message' => '内容已更新' ), 200); } 2.2 版本控制与历史记录 完善的文档协作需要完整的版本控制功能。我们可以扩展WordPress的修订版本系统,提供更强大的版本管理。 // 增强版本控制功能 class Enhanced_Revision_Manager { private $max_revisions = 50; public function __construct() { // 设置最大修订版本数 add_filter('wp_revisions_to_keep', array($this, 'set_max_revisions'), 10, 2); // 添加版本比较功能 add_action('add_meta_boxes', array($this, 'add_revision_comparison_meta_box')); // 添加版本标记功能 add_action('post_submitbox_misc_actions', array($this, 'add_version_note_field')); add_action('save_post', array($this, 'save_version_note')); } public function set_max_revisions($num, $post) { if ($post->post_type === 'knowledgebase') { return $this->max_revisions; } return $num; } public function add_revision_comparison_meta_box() { add_meta_box( 'revision-comparison', '版本比较', array($this, 'render_revision_comparison'), 'knowledgebase', 'side', 'default' ); } public function render_revision_comparison($post) { $revisions = wp_get_post_revisions($post->ID); if (count($revisions) > 1) { echo '<div class="revision-comparison">'; echo '<select id="revision-from">'; echo '<option value="">选择旧版本</option>'; foreach ($revisions as $revision) { $author = get_userdata($revision->post_author); $date = date_i18n('Y-m-d H:i', strtotime($revision->post_modified)); echo sprintf( '<option value="%d">%s - %s</option>', $revision->ID, $date, $author->display_name ); } echo '</select>'; echo '<select id="revision-to">'; echo '<option value="">选择新版本</option>'; foreach ($revisions as $revision) { $author = get_userdata($revision->post_author); $date = date_i18n('Y-m-d H:i', strtotime($revision->post_modified)); echo sprintf( '<option value="%d">%s - %s</option>', $revision->ID, $date, $author->display_name ); } echo '</select>'; echo '<button id="compare-revisions" class="button">比较版本</button>'; echo '<div id="revision-diff-result" style="margin-top:10px;"></div>'; echo '</div>'; // 添加比较脚本 wp_enqueue_script('revision-diff', get_template_directory_uri() . '/js/revision-diff.js', array('jquery'), '1.0.0', true); } else { echo '<p>暂无历史版本</p>'; } } public function add_version_note_field($post) { if ($post->post_type !== 'knowledgebase') return; echo '<div class="misc-pub-section">'; echo '<label for="version-note">版本说明:</label>'; echo '<input type="text" id="version-note" name="version_note" value="" placeholder="简要说明本次修改内容" style="width:100%;margin-top:5px;">'; echo '</div>'; } public function save_version_note($post_id) { if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (!current_user_can('edit_post', $post_id)) return; if (!isset($_POST['version_note'])) return; $version_note = sanitize_text_field($_POST['version_note']); if (!empty($version_note)) { // 获取最新修订版本 $revisions = wp_get_post_revisions($post_id); $latest_revision = reset($revisions); if ($latest_revision) { update_metadata('post', $latest_revision->ID, '_version_note', $version_note); } } } } new Enhanced_Revision_Manager(); 第三部分:常用互联网小工具集成 3.1 代码片段管理器 对于技术团队,代码片段管理是知识库的重要功能。我们可以创建一个专门的代码片段管理器。 // 代码片段管理器 class Code_Snippet_Manager { public function __construct() { // 注册代码片段文章类型 add_action('init', array($this, 'register_code_snippet_post_type')); // 添加上传代码文件支持 add_filter('upload_mimes', array($this, 'add_code_mime_types')); // 添加代码高亮 add_action('wp_enqueue_scripts', array($this, 'enqueue_code_highlight')); add_action('admin_enqueue_scripts', array($this, 'enqueue_code_highlight')); // 添加快捷码 add_shortcode('code_snippet', array($this, 'code_snippet_shortcode')); } public function register_code_snippet_post_type() { $args = array( 'public' => true, 'label' => '代码片段', 'menu_icon' => 'dashicons-editor-code', 'supports' => array('title', 'editor', 'author', 'custom-fields'), 'show_in_rest' => true, 'taxonomies' => array('post_tag', 'category'), 'has_archive' => true, ); register_post_type('code_snippet', $args); } public function add_code_mime_types($mimes) { $mimes['js'] = 'text/javascript'; $mimes['php'] = 'text/php'; $mimes['py'] = 'text/x-python'; $mimes['java'] = 'text/x-java'; $mimes['cpp'] = 'text/x-c++'; $mimes['c'] = 'text/x-c'; $mimes['sql'] = 'text/x-sql'; $mimes['json'] = 'application/json'; $mimes['xml'] = 'application/xml'; return $mimes; } public function enqueue_code_highlight() { // 加载Prism.js代码高亮库 wp_enqueue_style('prism-css', 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css'); wp_enqueue_script('prism-js', 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js', array(), '1.29.0', true); // 加载语言支持 $languages = array('php', 'javascript', 'python', 'java', 'cpp', 'sql', 'json', 'bash'); foreach ($languages as $lang) { wp_enqueue_script("prism-$lang", "https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-$lang.min.js", array('prism-js'), '1.29.0', true); } } public function code_snippet_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'language' => 'php', 'title' => '', 'linenumbers' => 'true', 'download' => 'false' ), $atts); $snippet_id = intval($atts['id']); if ($snippet_id <= 0) { return '<p>错误:未指定代码片段ID</p>'; } $snippet = get_post($snippet_id); if (!$snippet || $snippet->post_type !== 'code_snippet') { return '<p>错误:代码片段不存在</p>'; } $language = sanitize_text_field($atts['language']); $title = sanitize_text_field($atts['title']); $show_line_numbers = $atts['linenumbers'] === 'true'; $show_download = $atts['download'] === 'true'; $output = '<div class="code-snippet-container">'; if (!empty($title)) { $output .= '<div class="code-snippet-header">'; $output .= '<h4>' . esc_html($title) . '</h4>'; if ($show_download) { $output .= '<a href="' . get_permalink($snippet_id) . '?download=1" class="download-snippet" download>下载代码</a>'; } $output .= '</div>'; } $output .= '<pre class="' . ($show_line_numbers ? 'line-numbers' : '') . '">'; $output .= '<code class="language-' . esc_attr($language) . '">'; $output .= htmlspecialchars($snippet->post_content); $output .= '</code></pre>'; // 添加代码信息 $output .= '<div class="code-snippet-meta">'; $output .= '<span>语言: ' . esc_html($language) . '</span>'; $output .= '<span>作者: ' . get_the_author_meta('display_name', $snippet->post_author) . '</span>'; $output .= '<span>更新: ' . get_the_modified_date('', $snippet_id) . '</span>'; $output .= '</div>'; $output .= '</div>'; return $output; } } new Code_Snippet_Manager(); // 代码片段下载处理function handle_code_snippet_download() { if (isset($_GET['download']) && is_singular('code_snippet')) { $post_id = get_the_ID(); $post = get_post($post_id); if ($post && $post->post_type === 'code_snippet') { $filename = sanitize_title($post->post_title) . '.txt'; $content = $post->post_content; header('Content-Type: text/plain'); header('Content-Disposition: attachment; filename="' . $filename . '"'); header('Content-Length: ' . strlen($content)); echo $content; exit; } } }add_action('template_redirect', 'handle_code_snippet_download'); ### 3.2 API测试工具集成 为开发团队集成API测试工具,方便在知识库中直接测试和调试API接口。 // API测试工具class API_Test_Tool { public function __construct() { // 注册API测试文章类型 add_action('init', array($this, 'register_api_test_post_type')); // 添加快捷码 add_shortcode('api_tester', array($this, 'api_tester_shortcode')); // 添加REST API端点处理代理请求 add_action('rest_api_init', array($this, 'register_api_proxy_endpoint')); // 加载前端资源 add_action('wp_enqueue_scripts', array($this, 'enqueue_api_tester_assets')); } public function register_api_test_post_type() { $args = array( 'public' => true, 'label' => 'API文档', 'menu_icon' => 'dashicons-rest-api', 'supports' => array('title', 'editor', 'custom-fields'), 'show_in_rest' => true, 'has_archive' => true, ); register_post_type('api_doc', $args); } public function api_tester_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'title' => 'API测试工具', 'default_url' => '', 'default_method' => 'GET', 'default_headers' => '{"Content-Type": "application/json"}', 'default_body' => '{}' ), $atts); $api_doc_id = intval($atts['id']); $api_data = array(); if ($api_doc_id > 0) { $api_doc = get_post($api_doc_id); if ($api_doc && $api_doc->post_type === 'api_doc') { $api_data = array( 'endpoint' => get_post_meta($api_doc_id, '_api_endpoint', true), 'method' => get_post_meta($api_doc_id, '_api_method', true) ?: 'GET', 'headers' => get_post_meta($api_doc_id, '_api_headers', true) ?: '{"Content-Type": "application/json"}', 'body' => get_post_meta($api_doc_id, '_api_body', true) ?: '{}', 'description' => $api_doc->post_content ); } } ob_start(); ?> <div class="api-tester-container" data-doc-id="<?php echo esc_attr($api_doc_id); ?>"> <div class="api-tester-header"> <h3><?php echo esc_html($atts['title']); ?></h3> <div class="api-tester-actions"> <button class="btn-run-api">发送请求</button> <button class="btn-save-api">保存配置</button> <button class="btn-clear-api">清空</button> </div> </div> <div class="api-tester-form"> <div class="form-group"> <label>请求方法:</label> <select class="api-method"> <option value="GET" <?php selected($api_data['method'] ?? $atts['default_method'], 'GET'); ?>>GET</option> <option value="POST" <?php selected($api_data['method'] ?? $atts['default_method'], 'POST'); ?>>POST</option> <option value="PUT" <?php selected($api_data['method'] ?? $atts['default_method'], 'PUT'); ?>>PUT</option> <option value="DELETE" <?php selected($api_data['method'] ?? $atts['default_method'], 'DELETE'); ?>>DELETE</option> <option value="PATCH" <?php selected($api_data['method'] ?? $atts['default_method'], 'PATCH'); ?>>PATCH</option> </select> <label class="url-label">请求URL:</label> <input type="text" class="api-url" value="<?php echo esc_attr($api_data['endpoint'] ?? $atts['default_url']); ?>" placeholder="https://api.example.com/endpoint"> </div> <div class="form-group"> <label>请求头:</label> <textarea class="api-headers" rows="4"><?php echo esc_textarea($api_data['headers'] ?? $atts['default_headers']); ?></textarea> </div> <div class="form-group"> <label>请求体:</label> <textarea class="api-body" rows="8"><?php echo esc_textarea($api_data['body'] ?? $atts['default_body']); ?></textarea> </div> <?php if (!empty($api_data['description'])): ?> <div class="api-description"> <h4>API说明:</h4> <div><?php echo wp_kses_post($api_data['description']); ?></div> </div> <?php endif; ?> </div> <div class="api-tester-response"> <div class="response-header"> <h4>响应结果</h4> <div class="response-status"> <span class="status-label">状态:</span> <span class="status-code">-</span> <span class="status-time">时间: <span class="time-value">-</span>ms</span> </div> </div> <div class="response-body"> <pre><code class="language-json">等待请求...</code></pre> </div> </div> </div> <?php return ob_get_clean(); } public function register_api_proxy_endpoint() { register_rest_route('api-tester/v1', '/proxy', array( 'methods' => 'POST', 'callback' => array($this, 'handle_api_proxy_request'), 'permission_callback' => function() { // 仅限登录用户使用 return is_user_logged_in(); }, )); } public function handle_api_proxy_request(WP_REST_Request $request) { $url = sanitize_url($request->get_param('url')); $method = sanitize_text_field($request->get_param('method')); $headers = $request->get_param('headers'); $body = $request->get_param('body'); // 验证URL if (empty($url) || !filter_var($url, FILTER_VALIDATE_URL)) { return new WP_Error('invalid_url', '无效的URL地址', array('status' => 400)); } // 解析headers $headers_array = array(); if (!empty($headers)) { $headers_data = json_decode($headers, true); if (is_array($headers_data)) { foreach ($headers_data as $key => $value) { $headers_array[sanitize_text_field($key)] = sanitize_text_field($value); } } } // 发送请求 $start_time = microtime(true); $args = array( 'method' => $method, 'headers' => $headers_array, 'timeout' => 30, 'sslverify' => false, ); if (in_array($method, array('POST', 'PUT', 'PATCH')) && !empty($body)) { $args['body'] = $body; } $response = wp_remote_request($url, $args); $end_time = microtime(true); $response_time = round(($end_time - $start_time) * 1000, 2); if (is_wp_error($response)) { return array( 'success' => false, 'error' => $response->get_error_message(), 'response_time' => $response_time ); } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); $response_headers = wp_remote_retrieve_headers($response); // 尝试解析JSON响应 $json_response = json_decode($response_body); if (json_last_error() === JSON_ERROR_NONE) { $formatted_body = json_encode($json_response, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } else { $formatted_body = $response_body; } return array( 'success' => true, 'status_code' => $response_code, 'headers' => $response_headers, 'body' => $formatted_body, 'response_time' => $response_time, 'size' => strlen($response_body) ); } public function enqueue_api_tester_assets() { wp_enqueue_style('api-tester-css', get_template_directory_uri() . '/css/api-tester.css'); wp_enqueue_script('api-tester-js', get_template_directory_uri() . '/js/api-tester.js', array('jquery'), '1.0.0', true); wp_localize_script('api-tester-js', 'apiTesterConfig', array( 'ajax_url' => admin_url('admin-ajax.php'), 'rest_url' => rest_url('api-tester/v1/'), 'nonce' => wp_create_nonce('api_tester_nonce') )); } } new API_Test_Tool(); ### 3.3 数据可视化工具 集成数据可视化工具,支持在知识库中直接创建和展示图表。 // 数据可视化工具class Data_Visualization_Tool { public function __construct() { // 注册图表文章类型 add_action('init', array($this, 'register_chart_post_type')); // 添加快捷码 add_shortcode('chart', array($this, 'chart_shortcode')); // 添加图表编辑器元框 add_action('add_meta_boxes', array($this, 'add_chart_editor_meta_box')); add_action('save_post_chart', array($this, 'save_chart_data')); // 加载图表库 add_action('wp_enqueue_scripts', array($this, 'enqueue_chart_library')); } public function register_chart_post_type() { $args = array( 'public' => true, 'label' => '数据图表', 'menu_icon' => 'dashicons-chart-line', 'supports' => array('title', 'author'), 'show_in_rest' => true, 'has_archive' => true, ); register_post_type('chart', $args); } public function add_chart_editor_meta_box() { add_meta_box( 'chart-editor', '图表编辑器', array($this, 'render_chart_editor'), 'chart', 'normal', 'high' ); } public function render_chart_editor($post) { wp_nonce_field('save_chart_data', 'chart_data_nonce'); $chart_type = get_post_meta($post->ID, '_chart_type', true) ?: 'line'; $chart_data = get_post_meta($post->ID, '_chart_data', true) ?: '{}'; $chart_options = get_post_meta($post->ID, '_chart_options', true) ?: '{}'; ?> <div class="chart-editor-container"> <div class="chart-settings"> <div class="setting-group"> <label for="chart-type">图表类型:</label> <select id="chart-type" name="chart_type"> <option value="line" <?php selected($chart_type, 'line'); ?>>折线图</option> <option value="bar" <?php selected($chart_type, 'bar'); ?>>柱状图</option> <option value="pie" <?php selected($chart_type, 'pie'); ?>>饼图</option> <option value="doughnut" <?php selected($chart_type, 'doughnut'); ?>>环形图</option> <option value="radar" <?php selected($chart_type, 'radar'); ?>>雷达图</option> <option value="scatter" <?php selected($chart_type, 'scatter'); ?>>散点图</option> </select> </div> <div class="setting-group"> <label for="chart-data">图表数据 (JSON格式):</label> <textarea id="chart-data" name="chart_data" rows="10"><?php echo esc_textarea($chart_data); ?></textarea> <p class="description">示例: {"labels": ["一月", "二月", "三月"], "datasets": [{"label": "销售额", "data": [65, 59, 80]}]}</p> </div> <div class="setting-group"> <label for="chart-options">图表选项 (JSON格式):</label> <textarea id="chart-options" name="chart_options" rows="10"><?php echo esc_textarea($chart_options); ?></textarea> <p class="description">配置图表显示选项,如颜色、标题等</p> </div> </div> <div class="chart-preview"> <h4>预览:</h4> <canvas id="chart-preview-canvas" width="400" height="300"></canvas> </div> </div> <style> .chart-editor-container { display: flex; gap: 20px; } .chart-settings { flex: 1; } .chart-preview { flex: 1; } .setting-group { margin-bottom: 15px; } .setting-group label { display: block; margin-bottom: 5px; font-weight: bold; } .setting-group textarea, .setting-group select { width: 100%; padding: 8px; } </style> <script> jQuery(document).ready(function($) { function updateChartPreview() { var type = $('#chart-type').val(); var data = $('#chart-data').val(); var options = $('#chart-options').val(); try { var chartData = JSON.parse(data); var chartOptions = JSON.parse(options); var ctx = document.getElementById('chart-preview-canvas').getContext('2d'); // 销毁现有图表 if (window.previewChart) { window.previewChart.destroy(); } // 创建新图表 window.previewChart = new Chart(ctx, { type: type, data: chartData, options: chartOptions }); } catch (e) { console.error('图表数据解析错误:', e); } } // 监听输入变化 $('#chart-type, #chart-data, #chart-options').on('change keyup', function() { updateChartPreview(); }); // 初始预览 updateChartPreview(); }); </script> <?php } public function save_chart_data($post_id) { if (!isset($_POST['chart_data_nonce']) || !wp_verify_nonce($_POST['chart_data_nonce'], 'save_chart_data')) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (!current_user_can('edit_post', $post_id)) return; if (isset($_POST['chart_type'])) { update_post_meta($post_id, '_chart_type', sanitize_text_field($_POST['chart_type'])); } if (isset($_POST['chart_data'])) { // 验证JSON格式 $chart_data = stripslashes($_POST['chart_data']); json_decode($chart_data); if (json_last_error() === JSON_ERROR_NONE) { update_post_meta($
发表评论手把手教程:实现网站内容的多平台一键分发与推广,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:内容分发的挑战与机遇 在当今数字时代,内容创作者和网站管理员面临着一个共同的挑战:如何高效地将优质内容分发到多个平台,同时保持品牌一致性和用户参与度。每天,数以百万计的文章、视频和图片在互联网上发布,但只有少数能够真正触达目标受众。传统的内容分发方式需要手动将内容复制粘贴到各个平台,这不仅耗时耗力,还容易导致格式错乱和品牌信息不一致。 与此同时,WordPress作为全球最流行的内容管理系统,占据了互联网上超过40%的网站份额。它强大的可扩展性和开源特性使其成为实现自动化内容分发的理想平台。通过代码二次开发,我们可以在WordPress中集成各种互联网小工具,实现内容的一键多平台分发,大大提高工作效率。 本教程将带领您一步步实现这一目标,从基础概念到实际代码实现,最终打造一个功能完善的自动化内容分发系统。 第一部分:理解多平台内容分发的基本原理 1.1 内容分发的技术架构 多平台内容分发的核心在于API(应用程序编程接口)的使用。大多数主流社交平台和内容聚合网站都提供了API,允许开发者以编程方式发布内容、管理账户和获取分析数据。 典型的内容分发流程包括: 内容创建与格式化 目标平台身份验证 内容转换与适配 API调用与发布 发布结果跟踪与记录 1.2 WordPress作为分发中心的优势 WordPress本身已经是一个强大的内容管理系统,通过二次开发可以将其转变为内容分发中心: 内置的内容创建和编辑工具 丰富的插件生态系统 灵活的用户权限管理 强大的数据库支持 活跃的开发者社区 1.3 常用互联网小工具功能概览 在内容分发过程中,以下小工具功能将大大提高效率: 社交媒体分享按钮 内容自动摘要生成 多平台发布计划 发布效果分析面板 内容格式自动转换器 第二部分:搭建WordPress开发环境 2.1 本地开发环境配置 在开始代码开发之前,我们需要搭建一个合适的WordPress开发环境: // 示例:本地WordPress环境配置要点 // 1. 安装本地服务器环境(如XAMPP、MAMP或Local by Flywheel) // 2. 下载最新版WordPress并解压到服务器目录 // 3. 创建数据库并配置wp-config.php文件 // 4. 安装WordPress并设置管理员账户 // wp-config.php数据库配置示例 define('DB_NAME', 'your_database_name'); define('DB_USER', 'your_database_user'); define('DB_PASSWORD', 'your_database_password'); define('DB_HOST', 'localhost'); define('DB_CHARSET', 'utf8mb4'); 2.2 开发工具与资源准备 代码编辑器:VS Code、PHPStorm或Sublime Text 版本控制:Git和GitHub/GitLab账户 调试工具:Query Monitor、Debug Bar插件 API测试工具:Postman或Insomnia 2.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('MPCD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('MPCD_PLUGIN_URL', plugin_dir_url(__FILE__)); define('MPCD_VERSION', '1.0.0'); // 初始化插件 class MultiPlatformContentDistributor { 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($this, 'activate')); // 插件停用时执行 register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化 add_action('init', array($this, 'init')); // 管理界面 add_action('admin_menu', array($this, 'add_admin_menu')); // 文章发布钩子 add_action('publish_post', array($this, 'on_post_publish'), 10, 2); } public function activate() { // 创建必要的数据库表 $this->create_database_tables(); // 设置默认选项 $this->set_default_options(); // 刷新重写规则 flush_rewrite_rules(); } public function deactivate() { // 清理临时数据 // 注意:不删除用户设置和发布记录 flush_rewrite_rules(); } public function init() { // 加载文本域 load_plugin_textdomain('mpcd', false, dirname(plugin_basename(__FILE__)) . '/languages'); } // 其他方法将在后续章节实现 } // 启动插件 MultiPlatformContentDistributor::get_instance(); ?> 第三部分:实现多平台API连接 3.1 平台API配置管理 我们需要创建一个管理界面来配置各个平台的API密钥和设置: // 在MultiPlatformContentDistributor类中添加以下方法 public function add_admin_menu() { add_menu_page( '多平台分发设置', '内容分发', 'manage_options', 'mpcd-settings', array($this, 'render_settings_page'), 'dashicons-share', 30 ); add_submenu_page( 'mpcd-settings', '平台配置', '平台配置', 'manage_options', 'mpcd-platforms', array($this, 'render_platforms_page') ); add_submenu_page( 'mpcd-settings', '发布记录', '发布记录', 'manage_options', 'mpcd-logs', array($this, 'render_logs_page') ); } public function render_settings_page() { ?> <div class="wrap"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <div class="mpcd-dashboard"> <div class="mpcd-stats"> <h2>分发统计</h2> <div class="stats-grid"> <div class="stat-card"> <h3>今日发布</h3> <p class="stat-number"><?php echo $this->get_today_posts_count(); ?></p> </div> <div class="stat-card"> <h3>总发布数</h3> <p class="stat-number"><?php echo $this->get_total_posts_count(); ?></p> </div> <div class="stat-card"> <h3>平台覆盖</h3> <p class="stat-number"><?php echo $this->get_platforms_count(); ?></p> </div> </div> </div> <div class="mpcd-quick-actions"> <h2>快速操作</h2> <button class="button button-primary" id="mpcd-test-all">测试所有平台连接</button> <button class="button" id="mpcd-clear-logs">清理旧日志</button> </div> </div> </div> <style> .mpcd-stats .stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; } .stat-card { background: #fff; padding: 20px; border-radius: 5px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } .stat-number { font-size: 2em; font-weight: bold; color: #2271b1; margin: 10px 0 0; } </style> <script> jQuery(document).ready(function($) { $('#mpcd-test-all').on('click', function() { // AJAX测试所有平台连接 }); }); </script> <?php } 3.2 主流平台API集成示例 以下是几个主流平台的API集成示例: // 平台API基类 abstract class MPCD_Platform { protected $platform_name; protected $api_key; protected $api_secret; protected $access_token; abstract public function authenticate(); abstract public function publish($content); abstract public function test_connection(); public function __construct($config) { $this->api_key = $config['api_key'] ?? ''; $this->api_secret = $config['api_secret'] ?? ''; $this->access_token = $config['access_token'] ?? ''; } } // 微信公众号平台实现 class MPCD_WeChat extends MPCD_Platform { protected $platform_name = 'wechat'; public function authenticate() { // 微信公众号OAuth认证流程 $auth_url = 'https://api.weixin.qq.com/cgi-bin/token'; $params = array( 'grant_type' => 'client_credential', 'appid' => $this->api_key, 'secret' => $this->api_secret ); $response = wp_remote_get(add_query_arg($params, $auth_url)); if (is_wp_error($response)) { return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['access_token'])) { $this->access_token = $body['access_token']; update_option('mpcd_wechat_token', array( 'token' => $this->access_token, 'expires' => time() + 7000 // 微信token有效期为7200秒 )); return true; } return false; } public function publish($content) { if (!$this->access_token) { if (!$this->authenticate()) { return array('success' => false, 'message' => '认证失败'); } } $api_url = 'https://api.weixin.qq.com/cgi-bin/material/add_news?access_token=' . $this->access_token; $wechat_content = array( 'articles' => array( array( 'title' => $content['title'], 'thumb_media_id' => $this->upload_image($content['featured_image']), 'author' => $content['author'], 'digest' => wp_trim_words(strip_tags($content['content']), 100), 'show_cover_pic' => 1, 'content' => $this->format_content_for_wechat($content['content']), 'content_source_url' => $content['url'] ) ) ); $response = wp_remote_post($api_url, array( 'body' => json_encode($wechat_content, JSON_UNESCAPED_UNICODE), 'headers' => array('Content-Type' => 'application/json') )); // 处理响应 return $this->handle_response($response); } private function format_content_for_wechat($content) { // 转换WordPress内容为微信公众号格式 $content = preg_replace('/<img[^>]+>/i', '', $content); // 移除img标签 $content = strip_tags($content, '<p><br><strong><em><h1><h2><h3><h4><h5><h6>'); return $content; } } // 微博平台实现 class MPCD_Weibo extends MPCD_Platform { protected $platform_name = 'weibo'; public function publish($content) { $api_url = 'https://api.weibo.com/2/statuses/share.json'; // 微博有字数限制,需要处理 $weibo_content = $content['title'] . ' ' . $content['url']; if (mb_strlen($weibo_content) > 140) { $weibo_content = mb_substr($weibo_content, 0, 137) . '...'; } $params = array( 'access_token' => $this->access_token, 'status' => $weibo_content ); // 如果有特色图片,上传图片 if (!empty($content['featured_image'])) { $pic_id = $this->upload_pic($content['featured_image']); if ($pic_id) { $params['pic_id'] = $pic_id; } } $response = wp_remote_post($api_url, array( 'body' => $params )); return $this->handle_response($response); } } 第四部分:内容一键分发功能实现 4.1 文章发布钩子与自动分发 // 在MultiPlatformContentDistributor类中添加文章发布处理 public function on_post_publish($post_id, $post) { // 检查是否自动分发 $auto_distribute = get_option('mpcd_auto_distribute', true); if (!$auto_distribute || wp_is_post_revision($post_id)) { return; } // 检查文章类型 $allowed_types = apply_filters('mpcd_allowed_post_types', array('post')); if (!in_array($post->post_type, $allowed_types)) { return; } // 准备分发内容 $content = $this->prepare_content_for_distribution($post); // 获取启用的平台 $enabled_platforms = $this->get_enabled_platforms(); // 分发到各个平台 $results = array(); foreach ($enabled_platforms as $platform) { $platform_instance = $this->get_platform_instance($platform); if ($platform_instance) { $result = $platform_instance->publish($content); $results[$platform] = $result; // 记录日志 $this->log_distribution($post_id, $platform, $result); } } // 保存分发结果到文章meta update_post_meta($post_id, '_mpcd_distribution_results', $results); update_post_meta($post_id, '_mpcd_distributed_at', current_time('mysql')); } private function prepare_content_for_distribution($post) { $content = array( 'title' => get_the_title($post), 'content' => apply_filters('the_content', $post->post_content), 'excerpt' => $post->post_excerpt ?: wp_trim_words(strip_tags($post->post_content), 55), 'url' => get_permalink($post), 'author' => get_the_author_meta('display_name', $post->post_author), 'categories' => wp_get_post_categories($post->ID, array('fields' => 'names')), 'tags' => wp_get_post_tags($post->ID, array('fields' => 'names')), 'featured_image' => get_the_post_thumbnail_url($post, 'full') ); // 应用过滤器允许修改内容 return apply_filters('mpcd_prepared_content', $content, $post); } private function get_enabled_platforms() { $platforms = get_option('mpcd_enabled_platforms', array()); return is_array($platforms) ? $platforms : array(); } 4.2 手动分发与批量处理 // 添加文章编辑页面的分发按钮 public function add_post_distribution_meta_box() { add_meta_box( 'mpcd-distribution-box', '多平台分发', array($this, 'render_distribution_meta_box'), 'post', 'side', 'high' ); } public function render_distribution_meta_box($post) { $distributed = get_post_meta($post->ID, '_mpcd_distributed_at', true); $results = get_post_meta($post->ID, '_mpcd_distribution_results', true); ?> <div class="mpcd-distribution-controls"> <?php if ($distributed): ?> <p>已分发于: <?php echo date('Y-m-d H:i', strtotime($distributed)); ?></p> <button type="button" class="button" id="mpcd-redistribute">重新分发</button> <button type="button" class="button" id="mpcd-view-results">查看结果</button> <?php else: ?> <button type="button" class="button button-primary" id="mpcd-distribute-now">立即分发</button> <?php endif; ?> <div class="platform-selection" style="margin-top: 15px;"> <p><strong>选择平台:</strong></p> <?php $platforms = $this->get_available_platforms(); foreach ($platforms as $platform_id => $platform_name): $enabled = in_array($platform_id, $this->get_enabled_platforms()); ?> <label style="display: block; margin-bottom: 5px;"> <input type="checkbox" name="mpcd_platforms[]" value="<?php echo esc_attr($platform_id); ?>" <?php checked($enabled); ?>> <?php echo esc_html($platform_name); ?> </label> <?php endforeach; ?> </div> </div> <script> ready(function($) { $('#mpcd-distribute-now').on('click', function() { var platforms = []; $('input[name="mpcd_platforms[]"]:checked').each(function() { platforms.push($(this).val()); }); if (platforms.length === 0) { alert('请至少选择一个平台'); return; } $(this).prop('disabled', true).text('分发中...'); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'mpcd_distribute_post', post_id: <?php echo $post->ID; ?>, platforms: platforms, nonce: '<?php echo wp_create_nonce("mpcd_distribute_{$post->ID}"); ?>' }, success: function(response) { if (response.success) { alert('分发成功!'); location.reload(); } else { alert('分发失败: ' + response.data); } }, error: function() { alert('请求失败,请检查网络连接'); } }); }); }); </script> <?php } // AJAX处理分发请求public function handle_ajax_distribution() { check_ajax_referer('mpcd_distribute_' . $_POST['post_id'], 'nonce'); $post_id = intval($_POST['post_id']); $platforms = isset($_POST['platforms']) ? (array)$_POST['platforms'] : array(); if (!current_user_can('edit_post', $post_id)) { wp_die('权限不足'); } $post = get_post($post_id); if (!$post) { wp_die('文章不存在'); } $content = $this->prepare_content_for_distribution($post); $results = array(); foreach ($platforms as $platform_id) { $platform_instance = $this->get_platform_instance($platform_id); if ($platform_instance) { $result = $platform_instance->publish($content); $results[$platform_id] = $result; $this->log_distribution($post_id, $platform_id, $result); } } update_post_meta($post_id, '_mpcd_distribution_results', $results); update_post_meta($post_id, '_mpcd_distributed_at', current_time('mysql')); wp_send_json_success('分发完成'); } ## 第五部分:集成常用互联网小工具 ### 5.1 智能内容摘要生成器 class MPCD_Content_Summarizer { public static function generate_summary($content, $max_length = 200) { // 移除HTML标签 $plain_text = wp_strip_all_tags($content); // 如果内容已经足够短,直接返回 if (mb_strlen($plain_text) <= $max_length) { return $plain_text; } // 尝试查找文章摘要段落 $summary = self::extract_summary_paragraph($plain_text, $max_length); // 如果没有找到合适的段落,使用智能截取 if (empty($summary)) { $summary = self::smart_truncate($plain_text, $max_length); } return apply_filters('mpcd_generated_summary', $summary, $content, $max_length); } private static function extract_summary_paragraph($text, $max_length) { // 分割成段落 $paragraphs = preg_split('/ns*n/', $text); foreach ($paragraphs as $paragraph) { $paragraph = trim($paragraph); $length = mb_strlen($paragraph); // 寻找长度合适的段落(在最大长度的60%-100%之间) if ($length >= $max_length * 0.6 && $length <= $max_length) { return $paragraph; } } // 如果没有合适长度的段落,找第一个较长的段落并截取 foreach ($paragraphs as $paragraph) { $paragraph = trim($paragraph); if (mb_strlen($paragraph) > 50) { return self::smart_truncate($paragraph, $max_length); } } return ''; } private static function smart_truncate($text, $max_length) { // 在句子边界处截断 $sentences = preg_split('/(?<=[。!?.!?])s+/u', $text); $result = ''; foreach ($sentences as $sentence) { if (mb_strlen($result . $sentence) > $max_length) { break; } $result .= $sentence; } // 如果没有完整的句子,在词边界处截断 if (empty($result)) { $result = mb_substr($text, 0, $max_length - 3); // 避免截断在单词中间 $last_space = mb_strrpos($result, ' '); if ($last_space > $max_length * 0.7) { $result = mb_substr($result, 0, $last_space); } $result .= '...'; } return $result; } } // 集成到分发系统中add_filter('mpcd_prepared_content', function($content, $post) { if (empty($content['excerpt']) || strlen($content['excerpt']) < 50) { $content['excerpt'] = MPCD_Content_Summarizer::generate_summary( $content['content'], 150 ); } return $content; }, 10, 2); ### 5.2 多平台发布计划器 class MPCD_Scheduler { 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('mpcd_scheduled_distribution', array($this, 'process_scheduled_distribution'), 10, 2); add_action('init', array($this, 'schedule_cron_jobs')); } public function schedule_cron_jobs() { if (!wp_next_scheduled('mpcd_hourly_distribution_check')) { wp_schedule_event(time(), 'hourly', 'mpcd_hourly_distribution_check'); } add_action('mpcd_hourly_distribution_check', array($this, 'check_scheduled_distributions')); } public function check_scheduled_distributions() { global $wpdb; $table_name = $wpdb->prefix . 'mpcd_schedules'; $scheduled = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table_name} WHERE scheduled_time <= %s AND status = 'pending' LIMIT 10", current_time('mysql') ) ); foreach ($scheduled as $schedule) { $this->process_schedule($schedule); } } public function process_schedule($schedule) { global $wpdb; $table_name = $wpdb->prefix . 'mpcd_schedules'; // 更新状态为处理中 $wpdb->update( $table_name, array('status' => 'processing'), array('id' => $schedule->id) ); $post = get_post($schedule->post_id); if (!$post) { $wpdb->update( $table_name, array( 'status' => 'failed', 'error_message' => '文章不存在' ), array('id' => $schedule->id) ); return; } // 准备内容 $distributor = MultiPlatformContentDistributor::get_instance(); $content = $distributor->prepare_content_for_distribution($post); // 分发到指定平台 $platforms = maybe_unserialize($schedule->platforms); $results = array(); foreach ($platforms as $platform_id) { $platform_instance = $distributor->get_platform_instance($platform_id); if ($platform_instance) { $result = $platform_instance->publish($content); $results[$platform_id] = $result; $distributor->log_distribution($schedule->post_id, $platform_id, $result); } } // 更新状态 $wpdb->update( $table_name, array( 'status' => 'completed', 'completed_at' => current_time('mysql'), 'results' => maybe_serialize($results) ), array('id' => $schedule->id) ); // 更新文章meta update_post_meta($schedule->post_id, '_mpcd_scheduled_distribution', $results); } public function create_schedule($post_id, $platforms, $scheduled_time) { global $wpdb; $table_name = $wpdb->prefix . 'mpcd_schedules'; return $wpdb->insert( $table_name, array( 'post_id' => $post_id, 'platforms' => maybe_serialize($platforms), 'scheduled_time' => $scheduled_time, 'status' => 'pending', 'created_at' => current_time('mysql') ) ); } public function get_schedules($args = array()) { global $wpdb; $defaults = array( 'post_id' => null, 'status' => null, 'limit' => 50, 'offset' => 0 ); $args = wp_parse_args($args, $defaults); $table_name = $wpdb->prefix . 'mpcd_schedules'; $where = array('1=1'); $params = array(); if ($args['post_id']) { $where[] = 'post_id = %d'; $params[] = $args['post_id']; } if ($args['status']) { $where[] = 'status = %s'; $params[] = $args['status']; } $where_clause = implode(' AND ', $where); $query = "SELECT * FROM {$table_name} WHERE {$where_clause} ORDER BY scheduled_time DESC LIMIT %d OFFSET %d"; $params[] = $args['limit']; $params[] = $args['offset']; if (!empty($params)) { $query = $wpdb->prepare($query, $params); } return $wpdb->get_results($query); } } ### 5.3 发布效果分析面板 class MPCD_Analytics { public static function get_distribution_stats($period = '7days') { global $wpdb; $logs_table = $wpdb->prefix . 'mpcd_distribution_logs'; // 根据时间段设置日期范围 $date_range = self::get_date_range($period); $stats = array( 'total_posts' => 0, 'successful' => 0, 'failed' => 0, 'by_platform' => array(), 'by_day' => array() ); // 获取总体统计 $overall_stats = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(*) as total, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful, SUM(CASE WHEN success = 0 THEN 1 ELSE 0 END) as failed FROM {$logs_table} WHERE distributed_at >= %s AND distributed_at <= %s", $date_range['start'], $date_range['end'] ) ); if ($overall_stats) { $stats['total_posts'] = intval($overall_stats->total); $stats['successful'] = intval($overall_stats->successful); $stats['failed'] = intval($overall_stats->failed); } // 按平台统计 $platform_stats = $wpdb->get_results( $wpdb->prepare( "SELECT platform, COUNT(*) as total, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful FROM {$logs_table} WHERE distributed_at >= %s AND distributed_at <= %s GROUP BY platform ORDER BY total DESC", $date_range['start'], $date_range['end'] ) ); foreach ($platform_stats as $platform_stat) { $stats['by_platform'][$platform_stat->platform] = array( 'total' => intval($platform_stat->total), 'successful' => intval($platform_stat->successful), 'success_rate' => $platform_stat->total > 0 ? round($platform_stat->successful / $platform_stat->total * 100, 1) : 0 ); } // 按日期统计 $daily_stats = $wpdb->get_results( $wpdb->prepare( "SELECT DATE(distributed_at) as date, COUNT(*) as total, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful FROM {$logs_table} WHERE distributed_at >= %s AND distributed_at <= %s GROUP BY DATE(distributed_at) ORDER BY date", $date_range['start'], $date_range['end'] ) ); foreach ($daily_stats as $daily_stat) { $stats['by_day'][$daily_stat->date] = array( 'total' => intval($daily_stat->total), 'successful' => intval($daily_stat->successful) ); } return $stats; } public static function get_top_performing_posts($limit = 10) { global $wpdb; $logs_table = $wpdb->prefix . 'mpcd_distribution_logs'; $query = $wpdb->prepare( "SELECT post_id, COUNT(*) as total_distributions, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) as successful_distributions FROM {$logs_table} WHERE post_id IS NOT NULL GROUP BY post_id ORDER BY total_distributions DESC LIMIT %d", $limit ); $results = $wpdb->get_results($query); $posts = array(); foreach ($results as $result) { $post = get_post($result->post_id); if ($post) { $posts[] = array( 'post' => $post, 'stats' => array( 'total' => intval($result->total_distributions), 'successful' => intval($result->successful_distributions), 'success_rate' => $result->total_distributions > 0 ? round($result->successful_distributions / $result->total_distributions * 100, 1) : 0 ) ); } } return $posts; } private static function get_date_range($period) { $end_date = current_time('mysql'); switch ($period) { case 'today': $start_date = date('Y-m-d 00:00:00', strtotime($end_date)); break; case 'yesterday': $start_date = date('Y-m-d 00:00:00', strtotime('-1 day', strtotime($end_date))); $end_date = date('Y-m-d 23:59:59', strtotime('-1 day', strtotime($end_date))); break; case '7days': $start_date = date('Y-m-d 00:00:00', strtotime('-7 days', strtotime($end_date))); break; case '30days': $start_date = date('Y-m-d 00:00:00', strtotime('-30 days', strtotime($end_date))); break; default: $start_date = date('Y-m-d 00:00:00', strtotime('-7 days', strtotime($end_date))); } return array( 'start' => $start_date, 'end' => $end_date ); } } ## 第六部分:高级功能与优化 ### 6.1 内容格式自动转换器 class MPCD_Content_Transformer { public static function transform_for_platform($content, $platform) { $transformers = array( 'wechat' => array('self', 'transform_for_wechat'), 'weibo' => array('self', 'transform_for_weibo'), 'zhihu' => array('self', 'transform_for_zhihu'), 'toutiao' => array('self', 'transform_for_toutiao'), 'default' => array('self', 'transform_default') ); $transformer = isset($transformers[$platform]) ? $transformers[$platform] : $transformers['default']; return call_user_func($transformer, $content); } private static function transform_for_wechat($content) { // 微信公众号特殊处理 $transformed = $content; // 移除所有外部链接(微信不允许) $transformed['content'] = preg_replace( '/<as[^>]*href=["'](?!https?://mp.weixin.qq.com)[^"']*["'][^>]*>(.*?)</a>/i', '$1', $transformed['content'] ); // 转换图片为微信格式 $transformed['content'] = preg_replace_callback( '/<imgs[^>]*src=["']([^"']+)["'][^>]*>/i', array('self', 'convert_image_for_wechat'), $transformed['content'] ); //
发表评论详细教程:为WordPress网站集成电子签名与合同管理工具 引言:为什么WordPress网站需要电子签名功能 在数字化时代,电子签名已成为商业活动中不可或缺的一环。无论是自由职业者与客户签订服务协议,还是企业与员工签署劳动合同,电子签名都能显著提高效率、降低成本并确保法律合规性。对于WordPress网站所有者而言,集成电子签名与合同管理功能可以: 提升专业形象:为客户提供完整的在线签约体验 简化工作流程:自动化合同创建、发送、签署和存档过程 增强安全性:确保合同文件的完整性和不可否认性 节省成本:减少纸质合同打印、邮寄和存储费用 提高转化率:减少客户签约过程中的摩擦点 本教程将指导您通过WordPress代码二次开发,为您的网站集成完整的电子签名与合同管理系统,而无需依赖昂贵的第三方插件。 第一部分:准备工作与环境配置 1.1 系统要求与工具准备 在开始开发之前,请确保您的环境满足以下要求: WordPress 5.0或更高版本 PHP 7.4或更高版本(推荐8.0+) MySQL 5.6或更高版本 文本编辑器(如VS Code、Sublime Text等) FTP客户端或服务器文件管理器访问权限 基本的PHP、JavaScript和WordPress开发知识 1.2 创建开发子主题 为了避免直接修改主题文件导致更新时丢失更改,我们首先创建一个子主题: 在wp-content/themes/目录下创建新文件夹my-esignature-theme 创建style.css文件,添加以下内容: /* Theme Name: My eSignature Theme Template: your-parent-theme-folder-name Version: 1.0.0 Description: 子主题,用于集成电子签名功能 */ 创建functions.php文件,添加以下代码: <?php // 子主题functions.php add_action('wp_enqueue_scripts', 'my_esignature_theme_enqueue_styles'); function my_esignature_theme_enqueue_styles() { wp_enqueue_style('parent-style', get_template_directory_uri() . '/style.css'); } ?> 在WordPress后台激活此子主题 1.3 创建插件目录结构 我们将创建一个独立的插件来管理电子签名功能: wp-content/plugins/my-esignature-system/ ├── my-esignature-system.php ├── includes/ │ ├── class-database.php │ ├── class-contract-manager.php │ ├── class-signature-handler.php │ └── class-pdf-generator.php ├── assets/ │ ├── css/ │ ├── js/ │ └── images/ ├── templates/ │ ├── contract-form.php │ ├── signature-pad.php │ └── contract-view.php └── vendor/ (第三方库) 第二部分:数据库设计与合同存储 2.1 创建数据库表 在includes/class-database.php中,我们将创建必要的数据库表: <?php class ESIGN_Database { 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('init', array($this, 'create_tables')); } public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'esign_'; // 合同表 $contracts_table = $table_prefix . 'contracts'; $sql1 = "CREATE TABLE IF NOT EXISTS $contracts_table ( id INT(11) NOT NULL AUTO_INCREMENT, contract_id VARCHAR(50) NOT NULL UNIQUE, title VARCHAR(255) NOT NULL, content LONGTEXT NOT NULL, status ENUM('draft', 'sent', 'signed', 'expired', 'cancelled') DEFAULT 'draft', created_by INT(11) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, expires_at DATETIME, PRIMARY KEY (id), INDEX (contract_id), INDEX (status), INDEX (created_by) ) $charset_collate;"; // 签署方表 $parties_table = $table_prefix . 'parties'; $sql2 = "CREATE TABLE IF NOT EXISTS $parties_table ( id INT(11) NOT NULL AUTO_INCREMENT, contract_id VARCHAR(50) NOT NULL, user_id INT(11), email VARCHAR(255) NOT NULL, name VARCHAR(255) NOT NULL, role ENUM('sender', 'signer', 'witness', 'approver') DEFAULT 'signer', signing_order INT(3) DEFAULT 1, signature_data TEXT, signed_at DATETIME, ip_address VARCHAR(45), user_agent TEXT, PRIMARY KEY (id), INDEX (contract_id), INDEX (email), FOREIGN KEY (contract_id) REFERENCES $contracts_table(contract_id) ON DELETE CASCADE ) $charset_collate;"; // 审计日志表 $audit_table = $table_prefix . 'audit_log'; $sql3 = "CREATE TABLE IF NOT EXISTS $audit_log_table ( id INT(11) NOT NULL AUTO_INCREMENT, contract_id VARCHAR(50) NOT NULL, action VARCHAR(100) NOT NULL, details TEXT, performed_by INT(11), performed_at DATETIME DEFAULT CURRENT_TIMESTAMP, ip_address VARCHAR(45), PRIMARY KEY (id), INDEX (contract_id), INDEX (performed_at) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); } // 生成唯一合同ID public function generate_contract_id() { return 'CONTRACT-' . date('Ymd') . '-' . strtoupper(wp_generate_password(8, false)); } } ?> 2.2 合同数据模型 在includes/class-contract-manager.php中,创建合同管理类: <?php class ESIGN_Contract_Manager { private $db; public function __construct() { $this->db = ESIGN_Database::get_instance(); } // 创建新合同 public function create_contract($data) { global $wpdb; $contract_id = $this->db->generate_contract_id(); $current_user_id = get_current_user_id(); $contract_data = array( 'contract_id' => $contract_id, 'title' => sanitize_text_field($data['title']), 'content' => wp_kses_post($data['content']), 'created_by' => $current_user_id, 'status' => 'draft', 'expires_at' => !empty($data['expires_at']) ? $data['expires_at'] : null ); $table_name = $wpdb->prefix . 'esign_contracts'; $result = $wpdb->insert($table_name, $contract_data); if ($result) { // 添加签署方 if (!empty($data['parties'])) { $this->add_parties($contract_id, $data['parties']); } // 记录审计日志 $this->log_audit($contract_id, 'contract_created', '合同创建'); return $contract_id; } return false; } // 添加签署方 private function add_parties($contract_id, $parties) { global $wpdb; $table_name = $wpdb->prefix . 'esign_parties'; foreach ($parties as $index => $party) { $party_data = array( 'contract_id' => $contract_id, 'email' => sanitize_email($party['email']), 'name' => sanitize_text_field($party['name']), 'role' => sanitize_text_field($party['role']), 'signing_order' => $index + 1 ); $wpdb->insert($table_name, $party_data); } } // 获取合同详情 public function get_contract($contract_id) { global $wpdb; $contracts_table = $wpdb->prefix . 'esign_contracts'; $parties_table = $wpdb->prefix . 'esign_parties'; $contract = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $contracts_table WHERE contract_id = %s", $contract_id )); if ($contract) { $contract->parties = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $parties_table WHERE contract_id = %s ORDER BY signing_order ASC", $contract_id )); } return $contract; } // 记录审计日志 private function log_audit($contract_id, $action, $details = '') { global $wpdb; $table_name = $wpdb->prefix . 'esign_audit_log'; $log_data = array( 'contract_id' => $contract_id, 'action' => $action, 'details' => $details, 'performed_by' => get_current_user_id(), 'ip_address' => $this->get_client_ip() ); $wpdb->insert($table_name, $log_data); } // 获取客户端IP private function get_client_ip() { $ipaddress = ''; if (isset($_SERVER['HTTP_CLIENT_IP'])) $ipaddress = $_SERVER['HTTP_CLIENT_IP']; else if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) $ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR']; else if(isset($_SERVER['HTTP_X_FORWARDED'])) $ipaddress = $_SERVER['HTTP_X_FORWARDED']; else if(isset($_SERVER['HTTP_FORWARDED_FOR'])) $ipaddress = $_SERVER['HTTP_FORWARDED_FOR']; else if(isset($_SERVER['HTTP_FORWARDED'])) $ipaddress = $_SERVER['HTTP_FORWARDED']; else if(isset($_SERVER['REMOTE_ADDR'])) $ipaddress = $_SERVER['REMOTE_ADDR']; else $ipaddress = 'UNKNOWN'; return $ipaddress; } } ?> 第三部分:电子签名功能实现 3.1 签名画板实现 创建templates/signature-pad.php模板文件: <div class="esignature-container"> <div class="signature-pad-wrapper"> <h3>请在下方签署您的名字</h3> <div class="signature-pad-container"> <canvas id="signature-pad" width="600" height="200"></canvas> </div> <div class="signature-actions"> <button type="button" id="clear-signature" class="button button-secondary"> <span class="dashicons dashicons-trash"></span> 清除 </button> <button type="button" id="undo-signature" class="button button-secondary"> <span class="dashicons dashicons-undo"></span> 撤销 </button> <button type="button" id="save-signature" class="button button-primary"> <span class="dashicons dashicons-yes-alt"></span> 确认签名 </button> </div> <div class="signature-preview" style="display:none;"> <h4>签名预览:</h4> <img id="signature-preview" src="" alt="签名预览" /> </div> <div class="signature-consent"> <label> <input type="checkbox" id="signature-consent" required> 我确认此电子签名具有法律约束力,并同意电子签名条款 </label> </div> </div> </div> 3.2 签名画板JavaScript实现 创建assets/js/signature-pad.js: (function($) { 'use strict'; class SignaturePad { constructor(canvasId) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.signatureData = null; this.isDrawing = false; this.lastX = 0; this.lastY = 0; this.init(); } init() { // 设置画布样式 this.ctx.strokeStyle = '#000'; this.ctx.lineWidth = 2; this.ctx.lineCap = 'round'; this.ctx.lineJoin = 'round'; // 绑定事件 this.bindEvents(); // 清除画布 this.clear(); } bindEvents() { const canvas = this.canvas; // 鼠标事件 canvas.addEventListener('mousedown', (e) => this.startDrawing(e)); canvas.addEventListener('mousemove', (e) => this.draw(e)); canvas.addEventListener('mouseup', () => this.stopDrawing()); canvas.addEventListener('mouseout', () => this.stopDrawing()); // 触摸事件(移动设备支持) canvas.addEventListener('touchstart', (e) => { e.preventDefault(); const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousedown', { clientX: touch.clientX, clientY: touch.clientY }); canvas.dispatchEvent(mouseEvent); }); canvas.addEventListener('touchmove', (e) => { e.preventDefault(); const touch = e.touches[0]; const mouseEvent = new MouseEvent('mousemove', { clientX: touch.clientX, clientY: touch.clientY }); canvas.dispatchEvent(mouseEvent); }); canvas.addEventListener('touchend', (e) => { e.preventDefault(); const mouseEvent = new MouseEvent('mouseup', {}); canvas.dispatchEvent(mouseEvent); }); } startDrawing(e) { this.isDrawing = true; const rect = this.canvas.getBoundingClientRect(); this.lastX = e.clientX - rect.left; this.lastY = e.clientY - rect.top; } draw(e) { if (!this.isDrawing) return; const rect = this.canvas.getBoundingClientRect(); const currentX = e.clientX - rect.left; const currentY = e.clientY - rect.top; this.ctx.beginPath(); this.ctx.moveTo(this.lastX, this.lastY); this.ctx.lineTo(currentX, currentY); this.ctx.stroke(); this.lastX = currentX; this.lastY = currentY; } stopDrawing() { this.isDrawing = false; this.saveSignature(); } saveSignature() { this.signatureData = this.canvas.toDataURL('image/png'); } clear() { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.fillStyle = '#f8f9fa'; this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); this.signatureData = null; } undo() { // 简单实现:清除整个画布 this.clear(); } getSignatureData() { return this.signatureData; } setSignatureData(data) { const img = new Image(); img.onload = () => { this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); this.ctx.drawImage(img, 0, 0); this.signatureData = data; }; img.src = data; } } // 初始化签名板 $(document).ready(function() { if ($('#signature-pad').length) { window.signaturePad = new SignaturePad('signature-pad'); // 绑定按钮事件 $('#clear-signature').on('click', function() { signaturePad.clear(); $('#signature-preview').hide(); }); $('#undo-signature').on('click', function() { signaturePad.undo(); }); $('#save-signature').on('click', function() { const signatureData = signaturePad.getSignatureData(); if (signatureData) { $('#signature-preview').attr('src', signatureData).show(); $('#signature-data').val(signatureData); // 显示成功消息 $('.signature-preview').show(); alert('签名已保存!'); } else { alert('请先绘制您的签名'); } }); // 表单提交验证 $('form.esignature-form').on('submit', function(e) { if (!$('#signature-consent').is(':checked')) { e.preventDefault(); alert('请同意电子签名条款'); return false; } if (!signaturePad.getSignatureData()) { e.preventDefault(); alert('请提供您的签名'); return false; } }); } }); })(jQuery); 3.3 签名处理类 创建includes/class-signature-handler.php: <?php class ESIGN_Signature_Handler { public function __construct() { add_action('wp_ajax_submit_signature', array($this, 'handle_signature_submission')); ature_submission')); } // 处理签名提交 public function handle_signature_submission() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'esignature_nonce')) { wp_die('安全验证失败'); } $contract_id = sanitize_text_field($_POST['contract_id']); $signature_data = $_POST['signature_data']; $party_email = sanitize_email($_POST['party_email']); // 验证签名数据 if (!$this->validate_signature_data($signature_data)) { wp_send_json_error('无效的签名数据'); } // 保存签名 $result = $this->save_signature($contract_id, $party_email, $signature_data); if ($result) { // 更新合同状态 $this->update_contract_status($contract_id); // 发送通知邮件 $this->send_notification_email($contract_id, $party_email); wp_send_json_success('签名提交成功'); } else { wp_send_json_error('签名保存失败'); } } // 验证签名数据 private function validate_signature_data($signature_data) { // 检查是否为有效的base64图像数据 if (preg_match('/^data:image/(png|jpeg);base64,/', $signature_data)) { return true; } return false; } // 保存签名到数据库 private function save_signature($contract_id, $party_email, $signature_data) { global $wpdb; $table_name = $wpdb->prefix . 'esign_parties'; $result = $wpdb->update( $table_name, array( 'signature_data' => $signature_data, 'signed_at' => current_time('mysql'), 'ip_address' => $this->get_client_ip(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ), array( 'contract_id' => $contract_id, 'email' => $party_email ) ); return $result !== false; } // 更新合同状态 private function update_contract_status($contract_id) { global $wpdb; $contracts_table = $wpdb->prefix . 'esign_contracts'; $parties_table = $wpdb->prefix . 'esign_parties'; // 检查是否所有签署方都已签名 $unsigned_parties = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $parties_table WHERE contract_id = %s AND signature_data IS NULL", $contract_id )); $new_status = ($unsigned_parties == 0) ? 'signed' : 'sent'; $wpdb->update( $contracts_table, array('status' => $new_status), array('contract_id' => $contract_id) ); } // 发送通知邮件 private function send_notification_email($contract_id, $party_email) { $contract = ESIGN_Contract_Manager::get_instance()->get_contract($contract_id); $to = $party_email; $subject = '合同签署通知 - ' . $contract->title; $message = " <html> <body> <h2>合同签署通知</h2> <p>您好,</p> <p>您已成功签署合同:<strong>{$contract->title}</strong></p> <p>合同ID:{$contract_id}</p> <p>签署时间:" . date('Y-m-d H:i:s') . "</p> <p>您可以通过以下链接查看合同详情:</p> <p><a href='" . home_url("/contract-view/{$contract_id}") . "'>查看合同</a></p> <hr> <p>此邮件为系统自动发送,请勿回复。</p> </body> </html> "; $headers = array('Content-Type: text/html; charset=UTF-8'); wp_mail($to, $subject, $message, $headers); } }?> ## 第四部分:PDF生成与合同管理 ### 4.1 集成TCPDF库生成PDF 创建`includes/class-pdf-generator.php`: <?phprequire_once(plugin_dir_path(__FILE__) . '../vendor/tcpdf/tcpdf.php'); class ESIGN_PDF_Generator extends TCPDF { private $contract_data; public function __construct($contract_data) { parent::__construct(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); $this->contract_data = $contract_data; $this->init_pdf(); } private function init_pdf() { // 设置文档信息 $this->SetCreator('WordPress eSignature System'); $this->SetAuthor(get_bloginfo('name')); $this->SetTitle($this->contract_data->title); $this->SetSubject('电子合同'); // 设置默认字体 $this->SetFont('stsongstdlight', '', 12); // 设置边距 $this->SetMargins(15, 15, 15); $this->SetHeaderMargin(5); $this->SetFooterMargin(10); // 自动分页 $this->SetAutoPageBreak(TRUE, 15); // 设置图像比例因子 $this->setImageScale(PDF_IMAGE_SCALE_RATIO); } // 生成合同PDF public function generate_contract_pdf() { $this->AddPage(); // 添加合同标题 $this->SetFont('stsongstdlight', 'B', 16); $this->Cell(0, 10, $this->contract_data->title, 0, 1, 'C'); $this->Ln(10); // 添加合同信息 $this->SetFont('stsongstdlight', '', 10); $this->Cell(0, 6, '合同编号:' . $this->contract_data->contract_id, 0, 1); $this->Cell(0, 6, '创建日期:' . $this->contract_data->created_at, 0, 1); $this->Cell(0, 6, '状态:' . $this->get_status_text($this->contract_data->status), 0, 1); $this->Ln(10); // 添加合同内容 $this->SetFont('stsongstdlight', '', 12); $this->writeHTML($this->contract_data->content, true, false, true, false, ''); $this->Ln(20); // 添加签署方信息 $this->add_signature_section(); // 添加审计信息 $this->add_audit_info(); return $this; } // 添加签署方部分 private function add_signature_section() { $this->SetFont('stsongstdlight', 'B', 14); $this->Cell(0, 10, '签署方信息', 0, 1); $this->Ln(5); $this->SetFont('stsongstdlight', '', 11); foreach ($this->contract_data->parties as $index => $party) { $this->Cell(0, 6, '签署方 ' . ($index + 1) . ':', 0, 1); $this->Cell(40, 6, '姓名:', 0, 0); $this->Cell(0, 6, $party->name, 0, 1); $this->Cell(40, 6, '邮箱:', 0, 0); $this->Cell(0, 6, $party->email, 0, 1); $this->Cell(40, 6, '角色:', 0, 0); $this->Cell(0, 6, $this->get_role_text($party->role), 0, 1); if ($party->signature_data) { $this->Cell(40, 6, '签署时间:', 0, 0); $this->Cell(0, 6, $party->signed_at, 0, 1); // 添加签名图像 $signature_data = $party->signature_data; $temp_file = $this->save_signature_image($signature_data); if ($temp_file) { $this->Image($temp_file, 50, $this->GetY(), 40, 20); $this->Ln(25); unlink($temp_file); // 删除临时文件 } } $this->Ln(10); } } // 保存签名图像到临时文件 private function save_signature_image($signature_data) { $upload_dir = wp_upload_dir(); $temp_dir = $upload_dir['basedir'] . '/esignature_temp/'; if (!file_exists($temp_dir)) { wp_mkdir_p($temp_dir); } $temp_file = $temp_dir . uniqid('sig_') . '.png'; // 移除base64前缀 $signature_data = preg_replace('/^data:image/w+;base64,/', '', $signature_data); $signature_data = base64_decode($signature_data); if (file_put_contents($temp_file, $signature_data)) { return $temp_file; } return false; } // 添加审计信息 private function add_audit_info() { global $wpdb; $audit_table = $wpdb->prefix . 'esign_audit_log'; $audit_logs = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $audit_table WHERE contract_id = %s ORDER BY performed_at DESC LIMIT 10", $this->contract_data->contract_id )); if ($audit_logs) { $this->SetFont('stsongstdlight', 'B', 12); $this->Cell(0, 10, '操作记录', 0, 1); $this->Ln(5); $this->SetFont('stsongstdlight', '', 9); foreach ($audit_logs as $log) { $user = get_userdata($log->performed_by); $username = $user ? $user->display_name : '系统'; $this->Cell(0, 5, date('Y-m-d H:i:s', strtotime($log->performed_at)) . ' - ' . $username . ' - ' . $log->action . ' - ' . $log->details, 0, 1); } } } // 获取状态文本 private function get_status_text($status) { $status_map = array( 'draft' => '草稿', 'sent' => '已发送', 'signed' => '已签署', 'expired' => '已过期', 'cancelled' => '已取消' ); return isset($status_map[$status]) ? $status_map[$status] : $status; } // 获取角色文本 private function get_role_text($role) { $role_map = array( 'sender' => '发起方', 'signer' => '签署方', 'witness' => '见证方', 'approver' => '审批方' ); return isset($role_map[$role]) ? $role_map[$role] : $role; } // 输出PDF到浏览器 public function output_pdf($filename = 'contract.pdf') { $this->Output($filename, 'I'); } // 保存PDF到服务器 public function save_pdf($filename) { $this->Output($filename, 'F'); return file_exists($filename); } }?> ### 4.2 合同管理界面 创建`templates/contract-manager.php`: <?php/** 合同管理界面模板 */ if (!defined('ABSPATH')) { exit; } global $wpdb;$current_user_id = get_current_user_id(); // 获取用户相关的合同$contracts_table = $wpdb->prefix . 'esign_contracts';$parties_table = $wpdb->prefix . 'esign_parties'; $contracts = $wpdb->get_results($wpdb->prepare( "SELECT c.* FROM $contracts_table c LEFT JOIN $parties_table p ON c.contract_id = p.contract_id WHERE c.created_by = %d OR p.email = %s GROUP BY c.id ORDER BY c.created_at DESC", $current_user_id, wp_get_current_user()->user_email ));?> <div class="esignature-manager-container"> <div class="esignature-header"> <h1>合同管理</h1> <button id="create-new-contract" class="button button-primary"> <span class="dashicons dashicons-plus"></span> 创建新合同 </button> </div> <!-- 合同筛选 --> <div class="contract-filters"> <select id="status-filter"> <option value="">所有状态</option> <option value="draft">草稿</option> <option value="sent">已发送</option> <option value="signed">已签署</option> <option value="expired">已过期</option> </select> <input type="text" id="search-contracts" placeholder="搜索合同标题或ID..."> <button id="apply-filters" class="button">筛选</button> <button id="reset-filters" class="button button-secondary">重置</button> </div> <!-- 合同列表 --> <div class="contracts-list"> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>合同标题</th> <th>合同ID</th> <th>状态</th> <th>创建时间</th> <th>过期时间</th> <th>操作</th> </tr> </thead> <tbody> <?php if ($contracts): ?> <?php foreach ($contracts as $contract): ?> <?php $parties = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $parties_table WHERE contract_id = %s", $contract->contract_id )); ?> <tr data-contract-id="<?php echo esc_attr($contract->contract_id); ?>"> <td> <strong><?php echo esc_html($contract->title); ?></strong> <div class="row-actions"> <span class="view"> <a href="#" class="view-contract" data-id="<?php echo esc_attr($contract->contract_id); ?>">查看</a> | </span> <span class="edit"> <a href="#" class="edit-contract" data-id="<?php echo esc_attr($contract->contract_id); ?>">编辑</a> | </span> <span class="delete"> <a href="#" class="delete-contract" data-id="<?php echo esc_attr($contract->contract_id); ?>">删除</a> </span> </div> </td> <td><?php echo esc_html($contract->contract_id); ?></td> <td> <span class="contract-status status-<?php echo esc_attr($contract->status); ?>"> <?php echo $this->get_status_badge($contract->status); ?> </span> </td> <td><?php echo date('Y-m-d H:i', strtotime($contract->created_at)); ?></td> <td> <?php echo $contract->expires_at ? date('Y-m-d H:i', strtotime($contract->expires_at)) : '无'; ?> </td> <td> <div class="contract-actions"> <?php if ($contract->status == 'draft'): ?> <button class="button button-small send-contract" data-id="<?php echo esc_attr($contract->contract_id); ?>"> 发送签署 </button> <?php endif; ?> <?php if ($contract->status == 'sent'): ?> <?php $user_can_sign = false; foreach ($parties as $party) { if ($party->email == wp_get_current_user()->user_email && !$party->signature_data) { $user_can_sign = true; break; } } ?> <?php if ($user_can_sign): ?> <a href="<?php echo home_url("/sign-contract/{$contract->contract_id}"); ?>" class="button button-small button-primary"> 签署合同 </a> <?php endif; ?> <?php endif; ?> <button class="button button-small download-pdf" data-id="<?php echo esc_attr($contract->contract_id); ?>"> 下载PDF </button> <button class="button button-small view-audit" data-id="<?php echo esc_attr($contract->contract_id); ?>"> 查看记录 </button> </div> </td> </tr> <?php endforeach; ?> <?php else: ?> <tr> <td colspan="6" class="no-contracts"> 暂无合同记录。点击"创建新合同"按钮开始创建。 </td> </tr> <?php endif; ?> </tbody> </table> </div> <!-- 分页 --> <div class="tablenav bottom"> <
发表评论开发指南:打造网站内嵌的在线代码编辑与运行环境 摘要 在当今数字化时代,网站功能多样化已成为吸引用户的关键因素之一。本文将详细介绍如何通过WordPress程序的二次开发,打造一个内嵌的在线代码编辑与运行环境,并实现常用互联网小工具功能。我们将从技术选型、环境搭建、核心功能实现到安全优化等方面进行全面解析,帮助开发者构建功能强大且用户友好的代码编辑平台。 一、引言:在线代码编辑环境的价值与意义 1.1 在线代码编辑器的兴起 随着云计算和Web技术的发展,在线代码编辑器逐渐成为开发者工具箱中的重要组成部分。从简单的代码片段分享到完整的集成开发环境(IDE),在线编辑器为用户提供了无需本地安装即可编写、运行和调试代码的便利。对于技术博客、教育平台或开发者社区而言,内嵌代码编辑器能极大提升用户体验和参与度。 1.2 WordPress作为开发平台的优势 WordPress作为全球最流行的内容管理系统,拥有庞大的插件生态系统和灵活的扩展能力。通过二次开发,我们可以在WordPress平台上构建专业级的代码编辑环境,同时利用其用户管理、内容发布和社区功能,打造一体化的开发者服务平台。 1.3 本文目标与结构 本文将指导读者完成以下目标: 在WordPress中集成在线代码编辑器 实现代码运行环境(支持多种编程语言) 开发常用互联网小工具(如JSON格式化、加密解密等) 确保系统的安全性与性能优化 二、技术架构与工具选型 2.1 核心编辑器选择 2.1.1 Monaco Editor Monaco Editor是Visual Studio Code的底层编辑器,功能强大且高度可定制。它支持语法高亮、代码补全、错误检查等高级功能,是构建专业代码编辑器的理想选择。 2.1.2 CodeMirror CodeMirror是一个轻量级的代码编辑器,易于集成且资源消耗较小。对于不需要完整IDE功能的场景,CodeMirror是一个优秀的选择。 2.1.3 Ace Editor Ace Editor是另一个流行的在线代码编辑器,性能优异且支持多种语言模式。 推荐方案:对于需要接近桌面IDE体验的场景,我们选择Monaco Editor;对于轻量级需求,可选择CodeMirror。 2.2 代码执行环境 2.2.1 服务器端执行方案 Docker容器:为每种语言创建独立的Docker容器,确保环境隔离和安全 语言特定沙箱:如PySandbox for Python, JS-Sandbox for JavaScript 第三方API:利用如JDoodle、Runnable等在线编译API 2.2.2 客户端执行方案 WebAssembly:将语言运行时编译为WebAssembly,在浏览器中直接执行 JavaScript解释器:对于JavaScript代码,可直接在浏览器中运行 推荐方案:结合使用客户端执行(适合简单JavaScript代码)和服务器端Docker容器(支持多种语言,安全性高)。 2.3 WordPress开发框架 2.3.1 插件开发基础 使用标准的WordPress插件结构 遵循WordPress编码标准 利用WordPress的REST API进行前后端通信 2.3.2 前端技术栈 React或Vue.js用于构建交互式UI组件 Webpack或Vite进行模块打包 Sass/Less用于样式管理 三、环境搭建与基础配置 3.1 开发环境准备 // WordPress插件基础结构 /* Plugin Name: 在线代码编辑器与工具集 Plugin URI: https://yourwebsite.com/ Description: 在WordPress中嵌入在线代码编辑器和常用开发工具 Version: 1.0.0 Author: Your Name */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('CODE_EDITOR_VERSION', '1.0.0'); define('CODE_EDITOR_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('CODE_EDITOR_PLUGIN_URL', plugin_dir_url(__FILE__)); 3.2 Monaco Editor集成 // 前端初始化Monaco Editor import * as monaco from 'monaco-editor'; class CodeEditor { constructor(containerId, language = 'javascript') { this.container = document.getElementById(containerId); this.language = language; this.editor = null; this.init(); } init() { this.editor = monaco.editor.create(this.container, { value: this.getDefaultCode(), language: this.language, theme: 'vs-dark', fontSize: 14, minimap: { enabled: true }, automaticLayout: true, scrollBeyondLastLine: false, wordWrap: 'on' }); } getDefaultCode() { const defaults = { 'javascript': 'console.log("Hello, World!");', 'python': 'print("Hello, World!")', 'html': '<!DOCTYPE html>n<html>n<head>n<title>示例</title>n</head>n<body>n<h1>Hello World</h1>n</body>n</html>', 'css': 'body {n font-family: Arial, sans-serif;n}', 'php': '<?phpnecho "Hello, World!";n?>' }; return defaults[this.language] || '// 输入你的代码'; } getCode() { return this.editor.getValue(); } setCode(code) { this.editor.setValue(code); } changeLanguage(language) { const model = this.editor.getModel(); monaco.editor.setModelLanguage(model, language); this.language = language; } } 3.3 Docker执行环境配置 # docker-compose.yml 配置示例 version: '3.8' services: code-executor: build: ./executor container_name: code_executor restart: unless-stopped networks: - code_network volumes: - ./code-temp:/tmp/code environment: - MAX_EXECUTION_TIME=10 - MAX_MEMORY=256m # 不同语言的执行器 python-executor: image: python:3.9-slim container_name: python_executor command: tail -f /dev/null networks: - code_network node-executor: image: node:16-alpine container_name: node_executor command: tail -f /dev/null networks: - code_network networks: code_network: driver: bridge 四、核心功能实现 4.1 多语言代码编辑器 // WordPress短代码实现编辑器嵌入 add_shortcode('code_editor', 'code_editor_shortcode'); function code_editor_shortcode($atts) { $atts = shortcode_atts(array( 'language' => 'javascript', 'height' => '400px', 'theme' => 'vs-dark', 'readonly' => false ), $atts); // 生成唯一ID $editor_id = 'code-editor-' . uniqid(); // 加载必要资源 wp_enqueue_script('monaco-editor', 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs/loader.min.js'); wp_enqueue_style('code-editor-style', CODE_EDITOR_PLUGIN_URL . 'assets/css/editor.css'); ob_start(); ?> <div class="code-editor-container"> <div class="editor-toolbar"> <select class="language-selector"> <option value="javascript" <?php selected($atts['language'], 'javascript'); ?>>JavaScript</option> <option value="python" <?php selected($atts['language'], 'python'); ?>>Python</option> <option value="html" <?php selected($atts['language'], 'html'); ?>>HTML</option> <option value="css" <?php selected($atts['language'], 'css'); ?>>CSS</option> <option value="php" <?php selected($atts['language'], 'php'); ?>>PHP</option> <option value="java" <?php selected($atts['language'], 'java'); ?>>Java</option> </select> <button class="run-code-btn">运行代码</button> <button class="save-code-btn">保存代码</button> </div> <div id="<?php echo esc_attr($editor_id); ?>" class="code-editor" data-language="<?php echo esc_attr($atts['language']); ?>" data-theme="<?php echo esc_attr($atts['theme']); ?>" style="height: <?php echo esc_attr($atts['height']); ?>;"> </div> <div class="execution-result"> <h4>运行结果:</h4> <div class="result-output"></div> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // 初始化编辑器 const editorId = '<?php echo $editor_id; ?>'; const initLanguage = '<?php echo $atts["language"]; ?>'; // 配置Monaco Editor require.config({ paths: { vs: 'https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.34.0/min/vs' } }); require(['vs/editor/editor.main'], function() { const editorContainer = document.getElementById(editorId); const language = editorContainer.dataset.language; const theme = editorContainer.dataset.theme; // 创建编辑器实例 const editor = monaco.editor.create(editorContainer, { value: getDefaultCode(language), language: language, theme: theme, fontSize: 14, minimap: { enabled: true }, automaticLayout: true }); // 存储编辑器实例供后续使用 window.codeEditors = window.codeEditors || {}; window.codeEditors[editorId] = editor; // 语言切换功能 const languageSelector = editorContainer.parentElement.querySelector('.language-selector'); languageSelector.addEventListener('change', function(e) { const newLanguage = e.target.value; const model = editor.getModel(); monaco.editor.setModelLanguage(model, newLanguage); editorContainer.dataset.language = newLanguage; }); // 运行代码功能 const runButton = editorContainer.parentElement.querySelector('.run-code-btn'); runButton.addEventListener('click', function() { const code = editor.getValue(); const language = editorContainer.dataset.language; runCode(code, language, editorContainer); }); }); function getDefaultCode(language) { const defaults = { 'javascript': 'console.log("Hello, World!");n// 尝试修改并运行这段代码', 'python': 'print("Hello, World!")n# 尝试修改并运行这段代码', 'html': '<!DOCTYPE html>n<html>n<head>nt<title>示例页面</title>n</head>n<body>nt<h1>Hello World!</h1>nt<p>尝试修改并运行这段HTML代码</p>n</body>n</html>', 'css': 'body {ntfont-family: Arial, sans-serif;ntbackground-color: #f0f0f0;n}nnh1 {ntcolor: #333;n}', 'php': '<?phpntecho "Hello, World!\n";nt// 尝试修改并运行这段PHP代码n?>', 'java': 'public class Main {ntpublic static void main(String[] args) {nttSystem.out.println("Hello, World!");nt}n}' }; return defaults[language] || '// 输入你的代码'; } function runCode(code, language, container) { const outputArea = container.parentElement.querySelector('.result-output'); outputArea.innerHTML = '<div class="loading">执行中...</div>'; // 发送AJAX请求到WordPress后端 fetch('<?php echo admin_url("admin-ajax.php"); ?>', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ 'action': 'execute_code', 'code': code, 'language': language, 'nonce': '<?php echo wp_create_nonce("code_execution_nonce"); ?>' }) }) .then(response => response.json()) .then(data => { if (data.success) { outputArea.innerHTML = `<pre class="success-output">${data.data.output}</pre>`; } else { outputArea.innerHTML = `<pre class="error-output">错误: ${data.data.error}</pre>`; } }) .catch(error => { outputArea.innerHTML = `<pre class="error-output">请求失败: ${error.message}</pre>`; }); } }); </script> <?php return ob_get_clean(); } 4.2 代码执行后端处理 // WordPress AJAX处理代码执行 add_action('wp_ajax_execute_code', 'handle_code_execution'); add_action('wp_ajax_nopriv_execute_code', 'handle_code_execution'); function handle_code_execution() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'code_execution_nonce')) { wp_send_json_error(array('error' => '安全验证失败')); return; } // 获取参数 $code = isset($_POST['code']) ? stripslashes($_POST['code']) : ''; $language = isset($_POST['language']) ? sanitize_text_field($_POST['language']) : 'javascript'; if (empty($code)) { wp_send_json_error(array('error' => '代码不能为空')); return; } // 根据语言选择执行方式 $result = execute_code_safely($code, $language); if ($result['success']) { wp_send_json_success(array('output' => $result['output'])); } else { wp_send_json_error(array('error' => $result['error'])); } } function execute_code_safely($code, $language) { // 安全检查:防止危险代码 $blacklisted_patterns = array( '/systems*(/i', '/execs*(/i', '/shell_execs*(/i', '/passthrus*(/i', '/proc_opens*(/i', '/popens*(/i', '/`.*`/', '/evals*(/i', '/includes*(/i', '/requires*(/i' ); foreach ($blacklisted_patterns as $pattern) { if (preg_match($pattern, $code)) { return array( 'success' => false, 'error' => '代码包含不允许的操作' ); } } // 根据语言执行代码 switch ($language) { case 'javascript': return execute_javascript($code); case 'python': return execute_python($code); case 'php': return execute_php($code); case 'html': return execute_html($code); default: return array( 'success' => false, 'error' => '暂不支持该语言' ); } } function execute_javascript($code) { // 对于简单JavaScript,可以在服务端使用V8JS(如果安装) // 或者返回给客户端执行 // 这里我们使用一个简化的方法:返回给客户端执行的指令 return array( 'success' => true, 'output' => 'JavaScript代码将在客户端执行。注意:出于安全考虑,某些API可能受限。' ); } function execute_python($code) { // 使用Docker执行Python代码 $temp_file = tempnam(sys_get_temp_dir(), 'py_'); file_put_contents($temp_file, $code); // 使用Docker运行代码(需要Docker环境) $command = "docker run --rm -v " . dirname($temp_file) . ":/code python:3.9-slim python /code/" . basename($temp_file) . " 2>&1"; $output = shell_exec("timeout 10 " . $command); // 清理临时文件 unlink($temp_file); if ($output === null) { return array( 'success' => false, 'error' => '执行超时或出错' ); } return array( 'success' => true, 'output' => htmlspecialchars($output) ); } function execute_html($code) { // 对于HTML,我们可以返回一个可预览的iframe $html_content = $code; // 如果代码中没有完整的HTML结构,添加基本结构 if (!preg_match('/<!DOCTYPEs+html>/i', $html_content) && !preg_match('/<html>/i', $html_content)) { $html_content = '<!DOCTYPE html><html><head><meta charset="UTF-8"><title>运行结果</title></head><body>' . $html_content . '</body></html>'; } // 编码HTML内容以便安全传输 $encoded_html = base64_encode($html_content); return array( 'success' => true, 'output' => '<iframe srcdoc="' . htmlspecialchars($html_content) . '" style="width:100%; height:300px; border:1px solid #ddd;"></iframe>' ); } 4.3 常用互联网小工具集成 // 小工具集成的短代码 add_shortcode('web_tools', 'web_tools_shortcode'); function web_tools_shortcode() { ob_start(); ?><div class="web-tools-container"> <div class="tools-tabs"> <button class="tool-tab active" data-tool="json">JSON格式化</button> <button class="tool-tab" data-tool="encrypt">加密/解密</button> <button class="tool-tab" data-tool="timestamp">时间戳转换</button> <button class="tool-tab" data-tool="color">颜色转换</button> <button class="tool-tab" data-tool="regex">正则测试</button> <button class="tool-tab" data-tool="qrcode">二维码生成</button> </div> <div class="tool-content"> <!-- JSON格式化工具 --> <div class="tool-pane active" id="json-tool"> <div class="tool-description"> <h3>JSON格式化工具</h3> <p>格式化、验证和压缩JSON数据,支持JSON与XML互转</p> </div> <div class="tool-interface"> <div class="input-area"> <textarea id="json-input" placeholder="输入JSON字符串...">{"name": "示例", "value": 123, "items": [1, 2, 3]}</textarea> <div class="button-group"> <button id="json-format">格式化</button> <button id="json-minify">压缩</button> <button id="json-validate">验证</button> <button id="json-clear">清空</button> </div> </div> <div class="output-area"> <textarea id="json-output" readonly placeholder="格式化结果..."></textarea> <div class="conversion-options"> <button id="json-to-xml">JSON转XML</button> <button id="xml-to-json">XML转JSON</button> </div> </div> </div> </div> <!-- 加密解密工具 --> <div class="tool-pane" id="encrypt-tool"> <div class="tool-description"> <h3>加密/解密工具</h3> <p>支持Base64、MD5、SHA、AES、DES等多种加密算法</p> </div> <div class="tool-interface"> <div class="algorithm-selector"> <select id="encrypt-algorithm"> <option value="base64">Base64编码/解码</option> <option value="md5">MD5哈希</option> <option value="sha1">SHA-1哈希</option> <option value="sha256">SHA-256哈希</option> <option value="aes">AES加密/解密</option> <option value="des">DES加密/解密</option> </select> <input type="text" id="encrypt-key" placeholder="加密密钥(如适用)"> </div> <div class="input-area"> <textarea id="encrypt-input" placeholder="输入要加密/解密的文本...">Hello, World!</textarea> <div class="button-group"> <button id="encrypt-action">加密</button> <button id="decrypt-action">解密</button> </div> </div> <div class="output-area"> <textarea id="encrypt-output" readonly placeholder="加密/解密结果..."></textarea> </div> </div> </div> <!-- 时间戳转换工具 --> <div class="tool-pane" id="timestamp-tool"> <div class="tool-description"> <h3>时间戳转换工具</h3> <p>Unix时间戳与可读日期时间相互转换,支持多种格式</p> </div> <div class="tool-interface"> <div class="timestamp-input"> <div class="input-group"> <label>Unix时间戳:</label> <input type="text" id="timestamp-input" value="<?php echo time(); ?>"> <button id="timestamp-to-date">转换为日期</button> </div> <div class="input-group"> <label>日期时间:</label> <input type="datetime-local" id="date-input" value="<?php echo date('Y-m-dTH:i'); ?>"> <button id="date-to-timestamp">转换为时间戳</button> </div> </div> <div class="output-area"> <div id="timestamp-result"> <p><strong>当前时间戳:</strong> <span id="current-timestamp"><?php echo time(); ?></span></p> <p><strong>北京时间:</strong> <span id="beijing-time"><?php echo date('Y-m-d H:i:s'); ?></span></p> <p><strong>UTC时间:</strong> <span id="utc-time"><?php echo gmdate('Y-m-d H:i:s'); ?></span></p> </div> </div> </div> </div> <!-- 其他工具界面类似,此处省略详细HTML --> </div> </div> <script>document.addEventListener('DOMContentLoaded', function() { // 工具标签切换 const toolTabs = document.querySelectorAll('.tool-tab'); const toolPanes = document.querySelectorAll('.tool-pane'); toolTabs.forEach(tab => { tab.addEventListener('click', function() { const toolName = this.dataset.tool; // 更新标签状态 toolTabs.forEach(t => t.classList.remove('active')); this.classList.add('active'); // 显示对应工具 toolPanes.forEach(pane => { pane.classList.remove('active'); if (pane.id === toolName + '-tool') { pane.classList.add('active'); } }); }); }); // JSON格式化功能 const jsonInput = document.getElementById('json-input'); const jsonOutput = document.getElementById('json-output'); document.getElementById('json-format').addEventListener('click', function() { try { const jsonObj = JSON.parse(jsonInput.value); jsonOutput.value = JSON.stringify(jsonObj, null, 2); } catch (e) { jsonOutput.value = 'JSON格式错误: ' + e.message; } }); document.getElementById('json-minify').addEventListener('click', function() { try { const jsonObj = JSON.parse(jsonInput.value); jsonOutput.value = JSON.stringify(jsonObj); } catch (e) { jsonOutput.value = 'JSON格式错误: ' + e.message; } }); document.getElementById('json-validate').addEventListener('click', function() { try { JSON.parse(jsonInput.value); jsonOutput.value = '✓ JSON格式正确'; } catch (e) { jsonOutput.value = '✗ JSON格式错误: ' + e.message; } }); // 加密解密功能 const encryptInput = document.getElementById('encrypt-input'); const encryptOutput = document.getElementById('encrypt-output'); const encryptAlgorithm = document.getElementById('encrypt-algorithm'); document.getElementById('encrypt-action').addEventListener('click', function() { const text = encryptInput.value; const algorithm = encryptAlgorithm.value; const key = document.getElementById('encrypt-key').value; // 调用WordPress后端处理加密 fetch('<?php echo admin_url("admin-ajax.php"); ?>', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ 'action': 'process_encryption', 'text': text, 'algorithm': algorithm, 'key': key, 'mode': 'encrypt', 'nonce': '<?php echo wp_create_nonce("encryption_nonce"); ?>' }) }) .then(response => response.json()) .then(data => { if (data.success) { encryptOutput.value = data.data.result; } else { encryptOutput.value = '错误: ' + data.data.error; } }) .catch(error => { encryptOutput.value = '请求失败: ' + error.message; }); }); // 时间戳转换功能 document.getElementById('timestamp-to-date').addEventListener('click', function() { const timestamp = parseInt(document.getElementById('timestamp-input').value); if (!isNaN(timestamp)) { const date = new Date(timestamp * 1000); document.getElementById('date-input').value = date.getFullYear() + '-' + String(date.getMonth() + 1).padStart(2, '0') + '-' + String(date.getDate()).padStart(2, '0') + 'T' + String(date.getHours()).padStart(2, '0') + ':' + String(date.getMinutes()).padStart(2, '0'); } }); // 更新时间显示 function updateTimeDisplay() { const now = new Date(); document.getElementById('current-timestamp').textContent = Math.floor(now.getTime() / 1000); document.getElementById('beijing-time').textContent = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0') + '-' + String(now.getDate()).padStart(2, '0') + ' ' + String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0') + ':' + String(now.getSeconds()).padStart(2, '0'); const utcNow = new Date(now.toUTCString()); document.getElementById('utc-time').textContent = utcNow.getFullYear() + '-' + String(utcNow.getMonth() + 1).padStart(2, '0') + '-' + String(utcNow.getDate()).padStart(2, '0') + ' ' + String(utcNow.getHours()).padStart(2, '0') + ':' + String(utcNow.getMinutes()).padStart(2, '0') + ':' + String(utcNow.getSeconds()).padStart(2, '0'); } setInterval(updateTimeDisplay, 1000); });</script> <style>.web-tools-container { background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); margin: 20px 0; overflow: hidden; } .tools-tabs { display: flex; background: #f5f5f5; border-bottom: 1px solid #ddd; flex-wrap: wrap; } .tool-tab { padding: 12px 20px; background: none; border: none; border-right: 1px solid #ddd; cursor: pointer; font-size: 14px; transition: all 0.3s; } .tool-tab:hover { background: #e9e9e9; } .tool-tab.active { background: #0073aa; color: white; } .tool-pane { display: none; padding: 20px; } .tool-pane.active { display: block; } .tool-description { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; } .tool-description h3 { margin: 0 0 5px 0; color: #333; } .tool-description p { margin: 0; color: #666; font-size: 14px; } .tool-interface { display: flex; flex-direction: column; gap: 20px; } .input-area, .output-area { display: flex; flex-direction: column; gap: 10px; } textarea { width: 100%; min-height: 150px; padding: 12px; border: 1px solid #ddd; border-radius: 4px; font-family: 'Consolas', 'Monaco', monospace; font-size: 14px; resize: vertical; } .button-group { display: flex; gap: 10px; flex-wrap: wrap; } button { padding: 8px 16px; background: #0073aa; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s; } button:hover { background: #005a87; } .algorithm-selector { display: flex; gap: 10px; margin-bottom: 15px; } select, input[type="text"] { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .timestamp-input { display: flex; flex-direction: column; gap: 15px; } .input-group { display: flex; align-items: center; gap: 10px; } .input-group label { min-width: 120px; font-weight: bold; } timestamp-result { background: #f9f9f9; padding: 15px; border-radius: 4px; border: 1px solid #eee; } timestamp-result p { margin: 8px 0; }</style><?php return ob_get_clean(); } // 加密解密处理函数add_action('wp_ajax_process_encryption', 'handle_encryption_request');add_action('wp_ajax_nopriv_process_encryption', 'handle_encryption_request'); function handle_encryption_request() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'encryption_nonce')) { wp_send_json_error(array('error' => '安全验证失败')); return; } $text = isset($_POST['text']) ? $_POST['text'] : ''; $algorithm = isset($_POST['algorithm']) ? sanitize_text_field($_POST['algorithm']) : 'base64'; $key = isset($_POST['key']) ? $_POST['key'] : ''; $mode = isset($_POST['mode']) ? sanitize_text_field($_POST['mode']) : 'encrypt'; if (empty($text)) { wp_send_json_error(array('error' => '输入文本不能为空')); return; } $result = process_encryption($text, $algorithm, $key, $mode); if ($result['success']) { wp_send_json_success(array('result' => $result['output'])); } else { wp_send_json_error(array('error' => $result['error'])); } } function process_encryption($text, $algorithm, $key, $mode) { try { switch ($algorithm) { case 'base64': if ($mode === 'encrypt') { $output = base64_encode($text); } else { $output = base64_decode($text); if ($output === false) { throw new Exception('Base64解码失败,请检查输入'); } } break; case 'md5': $output = md5($text); break; case 'sha1': $output = sha1($text); break; case 'sha256': $output = hash('sha256', $text); break; case 'aes': // 简化的AES示例(实际应用中需要更安全的实现) if (empty($key)) { throw new Exception('AES加密需要提供密钥'); } if ($mode === 'encrypt') { $output = openssl_encrypt($text, 'AES-128-ECB', $key, 0); } else { $output = openssl_decrypt($text, 'AES-128-ECB', $key, 0); } if ($output === false) { throw new Exception('AES处理失败'); } break; default: throw new Exception('不支持的算法'); } return array( 'success' => true, 'output' => $output ); } catch (Exception $e) { return array( 'success' => false, 'error' => $e->getMessage() ); } } --- ## 五、安全性与性能优化 ### 5.1 安全防护措施 // 安全增强功能class CodeExecutionSecurity { // 代码执行时间限制 const MAX_EXECUTION_TIME = 10; // 秒 // 内存限制 const MAX_MEMORY_LIMIT = '256M'; // 危险函数黑名单 private static $dangerous_functions = [ 'system', 'exec', 'shell_exec', 'passthru', 'proc_open', 'popen', 'eval', 'create_function', 'include', 'require', 'include_once', 'require_once', 'fopen', 'file_get_contents', 'file_put_contents', 'unlink', 'rmdir', 'mkdir' ]; // 危险模式黑名单 private static $dangerous_patterns = [ '/`.*`/', // 反引号执行 '/$_(GET|POST|REQUEST|COOKIE|SERVER)/', // 超全局变量 '/phar:///', // PHAR反序列化 '/expect:///', // Expect包装器 '/php://(filter|input)/', // PHP包装器 ]; /** * 检查代码安全性 */ public static function check_code_safety($code, $language) { // 检查代码长度 if (strlen($code) > 10000) { return array( 'safe' => false, 'reason' => '代码过长(超过10000字符)' ); } // 语言特定的安全检查 switch ($language) { case 'php': return self::check_php_code($code);
发表评论WordPress集成教程:连接项目管理软件并展示项目状态 引言:WordPress的无限可能性 在当今数字化时代,企业网站已不再仅仅是展示公司信息的静态页面,而是逐渐演变为功能丰富的业务平台。WordPress作为全球最受欢迎的内容管理系统,其真正的力量不仅在于创建博客或简单网站,更在于通过代码二次开发实现各种复杂功能。本教程将深入探讨如何将WordPress与项目管理软件集成,实时展示项目状态,并实现一系列常用互联网小工具功能。 传统的企业网站往往与内部业务系统脱节,导致信息更新滞后、数据不一致等问题。通过将WordPress与项目管理工具(如Jira、Asana、Trello、Monday.com等)集成,我们可以创建一个动态的、实时更新的项目状态展示平台,让客户、团队成员和利益相关者随时了解项目进展。 第一部分:准备工作与环境搭建 1.1 选择合适的WordPress环境 在开始集成之前,确保你的WordPress环境满足以下要求: WordPress 5.0或更高版本 PHP 7.4或更高版本(推荐PHP 8.0+) 支持HTTPS的域名 适当的服务器资源(至少1GB RAM,建议2GB以上) 1.2 安装必要插件 虽然本教程主要关注代码开发,但一些基础插件能极大提高开发效率: Advanced Custom Fields (ACF) - 用于创建自定义字段和管理数据 Custom Post Type UI - 简化自定义文章类型的创建 Query Monitor - 调试工具,监控数据库查询和性能 WP REST API Controller - 管理REST API端点 1.3 设置子主题 为避免主题更新覆盖自定义代码,强烈建议创建子主题: 在wp-content/themes/目录下创建新文件夹,如my-custom-theme 创建style.css文件,添加主题信息: /* Theme Name: My Custom Theme Template: parent-theme-folder-name Version: 1.0 */ 创建functions.php文件,用于添加自定义功能 第二部分:连接项目管理软件API 2.1 选择项目管理软件并获取API凭证 不同的项目管理软件提供不同的API接口。以Jira为例: 登录Jira管理员账户 进入"设置" > "系统" > "API令牌" 创建新令牌并妥善保存 获取你的Jira实例URL(如https://yourcompany.atlassian.net) 2.2 创建API连接类 在WordPress中创建专门的类来处理API通信: <?php /** * 项目管理软件API连接类 */ class ProjectManagementAPI { private $api_url; private $api_token; private $username; public function __construct($url, $username, $token) { $this->api_url = $url; $this->username = $username; $this->api_token = $token; } /** * 发送API请求 */ private function make_request($endpoint, $method = 'GET', $data = []) { $url = $this->api_url . $endpoint; $args = [ 'method' => $method, 'headers' => [ 'Authorization' => 'Basic ' . base64_encode($this->username . ':' . $this->api_token), 'Content-Type' => 'application/json', 'Accept' => 'application/json' ], 'timeout' => 30 ]; if (!empty($data)) { $args['body'] = json_encode($data); } $response = wp_remote_request($url, $args); if (is_wp_error($response)) { return [ 'success' => false, 'error' => $response->get_error_message() ]; } $body = wp_remote_retrieve_body($response); $status_code = wp_remote_retrieve_response_code($response); return [ 'success' => $status_code >= 200 && $status_code < 300, 'status' => $status_code, 'data' => json_decode($body, true), 'raw_body' => $body ]; } /** * 获取项目列表 */ public function get_projects() { $endpoint = '/rest/api/3/project'; return $this->make_request($endpoint); } /** * 获取特定项目的问题/任务 */ public function get_project_issues($project_key, $max_results = 50) { $endpoint = '/rest/api/3/search'; $jql = "project = " . $project_key; $data = [ 'jql' => $jql, 'maxResults' => $max_results, 'fields' => ['summary', 'status', 'assignee', 'created', 'updated'] ]; return $this->make_request($endpoint, 'POST', $data); } /** * 获取项目状态概览 */ public function get_project_status($project_key) { $issues_response = $this->get_project_issues($project_key, 100); if (!$issues_response['success']) { return $issues_response; } $issues = $issues_response['data']['issues'] ?? []; $status_summary = [ 'total' => 0, 'by_status' => [], 'by_assignee' => [] ]; foreach ($issues as $issue) { $status_summary['total']++; // 按状态统计 $status_name = $issue['fields']['status']['name'] ?? '未知'; if (!isset($status_summary['by_status'][$status_name])) { $status_summary['by_status'][$status_name] = 0; } $status_summary['by_status'][$status_name]++; // 按负责人统计 $assignee_name = $issue['fields']['assignee']['displayName'] ?? '未分配'; if (!isset($status_summary['by_assignee'][$assignee_name])) { $status_summary['by_assignee'][$assignee_name] = 0; } $status_summary['by_assignee'][$assignee_name]++; } return [ 'success' => true, 'data' => $status_summary ]; } } ?> 2.3 安全存储API凭证 永远不要在代码中硬编码API凭证。使用WordPress选项API安全存储: <?php /** * 保存API设置 */ function save_pm_api_settings() { if (isset($_POST['pm_api_nonce']) && wp_verify_nonce($_POST['pm_api_nonce'], 'save_pm_api_settings')) { $api_settings = [ 'api_url' => sanitize_text_field($_POST['api_url']), 'username' => sanitize_text_field($_POST['username']), 'api_token' => $_POST['api_token'] // 注意:令牌需要特殊处理 ]; // 加密存储令牌 if (!empty($api_settings['api_token'])) { require_once(ABSPATH . 'wp-includes/class-phpass.php'); $hasher = new PasswordHash(8, true); $api_settings['api_token'] = $hasher->HashPassword($api_settings['api_token']); } else { // 如果令牌为空,保留现有令牌 $existing_settings = get_option('pm_api_settings', []); if (isset($existing_settings['api_token'])) { $api_settings['api_token'] = $existing_settings['api_token']; } } update_option('pm_api_settings', $api_settings); wp_redirect(add_query_arg('settings-updated', 'true', wp_get_referer())); exit; } } add_action('admin_post_save_pm_api_settings', 'save_pm_api_settings'); /** * 获取API设置 */ function get_pm_api_settings() { $settings = get_option('pm_api_settings', []); // 解密令牌(在实际使用时) if (isset($settings['api_token']) && !empty($settings['api_token'])) { // 注意:实际解密逻辑需要根据加密方式实现 // 这里只是示例结构 } return $settings; } ?> 第三部分:在WordPress中展示项目状态 3.1 创建自定义文章类型和分类 为了更好地组织项目数据,我们创建自定义文章类型: <?php /** * 注册项目自定义文章类型 */ function register_project_post_type() { $labels = [ 'name' => '项目', 'singular_name' => '项目', 'menu_name' => '项目管理', 'add_new' => '添加新项目', 'add_new_item' => '添加新项目', 'edit_item' => '编辑项目', 'new_item' => '新项目', 'view_item' => '查看项目', 'search_items' => '搜索项目', 'not_found' => '未找到项目', 'not_found_in_trash' => '回收站中无项目' ]; $args = [ 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => ['slug' => 'project'], 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 20, 'menu_icon' => 'dashicons-portfolio', 'supports' => ['title', 'editor', 'thumbnail', 'excerpt'], 'show_in_rest' => true, // 启用Gutenberg编辑器和REST API ]; register_post_type('project', $args); // 注册项目分类 register_taxonomy( 'project_category', 'project', [ 'label' => '项目分类', 'rewrite' => ['slug' => 'project-category'], 'hierarchical' => true, 'show_in_rest' => true ] ); } add_action('init', 'register_project_post_type'); ?> 3.2 使用Advanced Custom Fields添加项目元数据 通过ACF添加项目与外部项目管理软件的关联字段: <?php /** * 添加项目元字段 */ function add_project_meta_fields() { if (function_exists('acf_add_local_field_group')) { acf_add_local_field_group([ 'key' => 'group_project_meta', 'title' => '项目信息', 'fields' => [ [ 'key' => 'field_project_external_id', 'label' => '外部项目ID', 'name' => 'project_external_id', 'type' => 'text', 'instructions' => '在项目管理软件中的项目标识符', 'required' => 0, ], [ 'key' => 'field_project_key', 'label' => '项目键', 'name' => 'project_key', 'type' => 'text', 'instructions' => '项目键(如Jira中的项目键)', 'required' => 0, ], [ 'key' => 'field_project_status', 'label' => '项目状态', 'name' => 'project_status', 'type' => 'select', 'choices' => [ 'planning' => '规划中', 'active' => '进行中', 'on_hold' => '暂停', 'completed' => '已完成', 'cancelled' => '已取消' ], 'default_value' => 'planning', ], [ 'key' => 'field_project_start_date', 'label' => '开始日期', 'name' => 'project_start_date', 'type' => 'date_picker', ], [ 'key' => 'field_project_end_date', 'label' => '结束日期', 'name' => 'project_end_date', 'type' => 'date_picker', ], [ 'key' => 'field_project_progress', 'label' => '进度', 'name' => 'project_progress', 'type' => 'range', 'instructions' => '项目完成百分比', 'min' => 0, 'max' => 100, 'step' => 5, 'default_value' => 0, ] ], 'location' => [ [ [ 'param' => 'post_type', 'operator' => '==', 'value' => 'project', ], ], ], ]); } } add_action('acf/init', 'add_project_meta_fields'); ?> 3.3 创建项目状态展示短代码 创建短代码以便在文章或页面中插入项目状态: <?php /** * 项目状态展示短代码 */ function project_status_shortcode($atts) { // 解析短代码属性 $atts = shortcode_atts([ 'project_id' => '', // WordPress项目ID 'project_key' => '', // 外部项目键 'show_tasks' => 'true', // 是否显示任务 'max_tasks' => 10, // 最大任务显示数量 'refresh' => 30, // 自动刷新时间(秒),0表示不自动刷新 ], $atts); // 获取项目信息 $project_data = []; if (!empty($atts['project_id'])) { $project_post = get_post($atts['project_id']); if ($project_post && $project_post->post_type === 'project') { $project_data['title'] = $project_post->post_title; $project_data['description'] = $project_post->post_excerpt; $project_data['progress'] = get_field('project_progress', $atts['project_id']); $project_data['status'] = get_field('project_status', $atts['project_id']); $project_key = get_field('project_key', $atts['project_id']); } } // 如果直接提供了project_key,使用它 if (!empty($atts['project_key'])) { $project_key = $atts['project_key']; } // 如果没有项目键,返回错误 if (empty($project_key)) { return '<div class="project-status-error">未指定项目</div>'; } // 获取API设置 $api_settings = get_pm_api_settings(); // 初始化API连接 $api = new ProjectManagementAPI( $api_settings['api_url'] ?? '', $api_settings['username'] ?? '', $api_settings['api_token'] ?? '' ); // 获取项目状态 $status_response = $api->get_project_status($project_key); // 准备输出 ob_start(); // 添加自动刷新脚本 if ($atts['refresh'] > 0) { ?> <script> document.addEventListener('DOMContentLoaded', function() { setTimeout(function() { location.reload(); }, <?php echo $atts['refresh'] * 1000; ?>); }); </script> <?php } // 输出项目状态 ?> <div class="project-status-container" data-project-key="<?php echo esc_attr($project_key); ?>"> <div class="project-header"> <h3 class="project-title"><?php echo esc_html($project_data['title'] ?? '项目状态'); ?></h3> <?php if (!empty($project_data['description'])): ?> <p class="project-description"><?php echo esc_html($project_data['description']); ?></p> <?php endif; ?> </div> <?php if ($status_response['success']): $status_data = $status_response['data']; ?> <div class="project-stats"> <div class="stat-card total-tasks"> <div class="stat-value"><?php echo $status_data['total']; ?></div> <div class="stat-label">总任务数</div> </div> <?php foreach ($status_data['by_status'] as $status_name => $count): ?> <div class="stat-card status-<?php echo sanitize_title($status_name); ?>"> <div class="stat-value"><?php echo $count; ?></div> <div class="stat-label"><?php echo esc_html($status_name); ?></div> </div> <?php endforeach; ?> </div> <?php if ($atts['show_tasks'] === 'true'): $tasks_response = $api->get_project_issues($project_key, $atts['max_tasks']); if ($tasks_response['success'] && !empty($tasks_response['data']['issues'])): ?> <div class="project-tasks"> <h4>最近任务</h4> <table class="tasks-table"> <thead> <tr> <th>任务</th> <th>状态</th> <th>负责人</th> <th>更新时间</th> </tr> </thead> <tbody> <?php foreach ($tasks_response['data']['issues'] as $issue): $fields = $issue['fields'] ?? []; ?> <tr> <td> <a href="#" class="task-link" data-task-key="<?php echo esc_attr($issue['key']); ?>"> <?php echo esc_html($fields['summary'] ?? '无标题'); ?> </a> </td> <td> <span class="task-status status-<?php echo sanitize_title($fields['status']['name'] ?? '未知'); ?>"> <?php echo esc_html($fields['status']['name'] ?? '未知'); ?> </span> </td> displayName'] ?? '未分配'); ?></td> <td><?php $updated = $fields['updated'] ?? ''; if (!empty($updated)) { echo date('Y-m-d H:i', strtotime($updated)); } else { echo '未知'; } ?></td> </tr> <?php endforeach; ?> </tbody> </table> </div> <?php endif; endif; ?> <?php if (!empty($status_data['by_assignee'])): ?> <div class="assignee-distribution"> <h4>任务分配情况</h4> <div class="assignee-chart"> <?php foreach ($status_data['by_assignee'] as $assignee => $count): $percentage = $status_data['total'] > 0 ? ($count / $status_data['total']) * 100 : 0; ?> <div class="assignee-item"> <div class="assignee-name"><?php echo esc_html($assignee); ?></div> <div class="assignee-bar"> <div class="assignee-bar-fill" style="width: <?php echo $percentage; ?>%"></div> </div> <div class="assignee-count"><?php echo $count; ?> 任务</div> </div> <?php endforeach; ?> </div> </div> <?php endif; ?> <?php else: ?> <div class="project-status-error"> <p>无法获取项目状态数据</p> <?php if (current_user_can('manage_options')): ?> <p class="error-detail">错误: <?php echo esc_html($status_response['error'] ?? '未知错误'); ?></p> <?php endif; ?> </div> <?php endif; ?> <div class="project-status-footer"> <p class="update-time">最后更新: <?php echo current_time('Y-m-d H:i:s'); ?></p> <?php if ($atts['refresh'] > 0): ?> <p class="auto-refresh">自动刷新: 每 <?php echo $atts['refresh']; ?> 秒</p> <?php endif; ?> </div> </div> <style> .project-status-container { border: 1px solid #e0e0e0; border-radius: 8px; padding: 20px; margin: 20px 0; background: #fff; box-shadow: 0 2px 10px rgba(0,0,0,0.05); } .project-header { margin-bottom: 25px; padding-bottom: 15px; border-bottom: 2px solid #f0f0f0; } .project-title { margin: 0 0 10px 0; color: #333; } .project-description { margin: 0; color: #666; font-size: 14px; } .project-stats { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; margin-bottom: 25px; } .stat-card { background: #f8f9fa; border-radius: 6px; padding: 15px; text-align: center; border-left: 4px solid #0073aa; } .stat-card.total-tasks { border-left-color: #0073aa; } .stat-card.status-已完成 { border-left-color: #46b450; } .stat-card.status-进行中 { border-left-color: #00a0d2; } .stat-card.status-待处理 { border-left-color: #ffb900; } .stat-value { font-size: 24px; font-weight: bold; color: #333; margin-bottom: 5px; } .stat-label { font-size: 12px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; } .project-tasks { margin-bottom: 25px; } .project-tasks h4 { margin-bottom: 15px; color: #444; } .tasks-table { width: 100%; border-collapse: collapse; font-size: 14px; } .tasks-table th { background: #f8f9fa; padding: 12px 15px; text-align: left; font-weight: 600; color: #555; border-bottom: 2px solid #e0e0e0; } .tasks-table td { padding: 12px 15px; border-bottom: 1px solid #eee; } .tasks-table tr:hover { background: #f9f9f9; } .task-link { color: #0073aa; text-decoration: none; } .task-link:hover { text-decoration: underline; } .task-status { display: inline-block; padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: 500; } .status-已完成 { background: #d1f0d9; color: #1e7c1e; } .status-进行中 { background: #d1e8f0; color: #0a6a8c; } .status-待处理 { background: #fff0d1; color: #b36b00; } .assignee-distribution { margin-bottom: 20px; } .assignee-distribution h4 { margin-bottom: 15px; color: #444; } .assignee-item { display: flex; align-items: center; margin-bottom: 10px; } .assignee-name { width: 150px; font-size: 14px; color: #555; } .assignee-bar { flex-grow: 1; height: 20px; background: #f0f0f0; border-radius: 10px; overflow: hidden; margin: 0 15px; } .assignee-bar-fill { height: 100%; background: linear-gradient(90deg, #0073aa, #00a0d2); border-radius: 10px; transition: width 0.5s ease; } .assignee-count { width: 80px; text-align: right; font-size: 14px; color: #666; } .project-status-footer { margin-top: 20px; padding-top: 15px; border-top: 1px solid #eee; font-size: 12px; color: #888; display: flex; justify-content: space-between; } .project-status-error { padding: 20px; background: #f8d7da; border: 1px solid #f5c6cb; border-radius: 4px; color: #721c24; } .error-detail { font-size: 12px; margin-top: 10px; color: #856404; } @media (max-width: 768px) { .project-stats { grid-template-columns: repeat(2, 1fr); } .assignee-item { flex-direction: column; align-items: flex-start; } .assignee-name { width: 100%; margin-bottom: 5px; } .assignee-bar { width: 100%; margin: 5px 0; } .assignee-count { width: 100%; text-align: left; } .tasks-table { display: block; overflow-x: auto; } } </style> <?php return ob_get_clean(); }add_shortcode('project_status', 'project_status_shortcode');?> ## 第四部分:实现常用互联网小工具功能 ### 4.1 实时数据仪表板小工具 创建一个WordPress小工具,显示多个项目的实时状态: <?php/** 项目状态仪表板小工具 */ class ProjectDashboardWidget extends WP_Widget { public function __construct() { parent::__construct( 'project_dashboard_widget', '项目状态仪表板', ['description' => '显示多个项目的实时状态'] ); } public function widget($args, $instance) { echo $args['before_widget']; $title = apply_filters('widget_title', $instance['title']); if (!empty($title)) { echo $args['before_title'] . $title . $args['after_title']; } // 获取配置的项目键 $project_keys = !empty($instance['project_keys']) ? explode(',', $instance['project_keys']) : []; if (empty($project_keys)) { echo '<p>请配置要显示的项目</p>'; echo $args['after_widget']; return; } // 获取API设置 $api_settings = get_pm_api_settings(); $api = new ProjectManagementAPI( $api_settings['api_url'] ?? '', $api_settings['username'] ?? '', $api_settings['api_token'] ?? '' ); echo '<div class="project-dashboard-widget">'; foreach ($project_keys as $project_key) { $project_key = trim($project_key); if (empty($project_key)) continue; $status_response = $api->get_project_status($project_key); echo '<div class="dashboard-project-item">'; echo '<h4>' . esc_html($project_key) . '</h4>'; if ($status_response['success']) { $status_data = $status_response['data']; // 计算完成率 $completed = $status_data['by_status']['已完成'] ?? 0; $completion_rate = $status_data['total'] > 0 ? round(($completed / $status_data['total']) * 100) : 0; echo '<div class="completion-bar">'; echo '<div class="completion-fill" style="width: ' . $completion_rate . '%"></div>'; echo '</div>'; echo '<div class="project-metrics">'; echo '<span class="metric">总任务: ' . $status_data['total'] . '</span>'; echo '<span class="metric">完成率: ' . $completion_rate . '%</span>'; echo '</div>'; } else { echo '<p class="error">无法获取数据</p>'; } echo '</div>'; } echo '</div>'; // 添加样式 ?> <style> .project-dashboard-widget { font-size: 14px; } .dashboard-project-item { margin-bottom: 15px; padding: 10px; background: #f8f9fa; border-radius: 4px; border-left: 3px solid #0073aa; } .dashboard-project-item h4 { margin: 0 0 8px 0; font-size: 14px; color: #333; } .completion-bar { height: 6px; background: #e0e0e0; border-radius: 3px; overflow: hidden; margin-bottom: 8px; } .completion-fill { height: 100%; background: linear-gradient(90deg, #0073aa, #00a0d2); border-radius: 3px; transition: width 0.5s ease; } .project-metrics { display: flex; justify-content: space-between; font-size: 12px; color: #666; } .metric { display: inline-block; } .error { color: #dc3232; font-size: 12px; margin: 0; } </style> <?php echo $args['after_widget']; } public function form($instance) { $title = $instance['title'] ?? '项目状态'; $project_keys = $instance['project_keys'] ?? ''; ?> <p> <label for="<?php echo $this->get_field_id('title'); ?>">标题:</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 esc_attr($title); ?>"> </p> <p> <label for="<?php echo $this->get_field_id('project_keys'); ?>">项目键(用逗号分隔):</label> <input class="widefat" id="<?php echo $this->get_field_id('project_keys'); ?>" name="<?php echo $this->get_field_name('project_keys'); ?>" type="text" value="<?php echo esc_attr($project_keys); ?>"> <small>例如: PROJ1, PROJ2, PROJ3</small> </p> <?php } public function update($new_instance, $old_instance) { $instance = []; $instance['title'] = !empty($new_instance['title']) ? sanitize_text_field($new_instance['title']) : ''; $instance['project_keys'] = !empty($new_instance['project_keys']) ? sanitize_text_field($new_instance['project_keys']) : ''; return $instance; } } // 注册小工具function register_project_dashboard_widget() { register_widget('ProjectDashboardWidget'); }add_action('widgets_init', 'register_project_dashboard_widget');?> ### 4.2 项目时间线小工具 创建一个可视化项目时间线的小工具: <?php/** 项目时间线小工具 */ function project_timeline_shortcode($atts) { $atts = shortcode_atts([ 'project_id' => '', 'height' => '400px', 'show_milestones' => 'true', ], $atts); if (empty($atts['project_id'])) { return '<p>请指定项目ID</p>'; } // 获取项目信息 $project_post = get_post($atts['project_id']); if (!$project_post || $project_post->post_type !== 'project') { return '<p>项目不存在</p>'; } // 获取项目时间线数据 $timeline_data = get_project_timeline_data($atts['project_id']); ob_start(); ?> <div class="project-timeline-container" style="height: <?php echo esc_attr($atts['height']); ?>"> <div class="timeline-header"> <h3><?php echo esc_html($project_post->post_title); ?> 时间线</h3> </div> <div class="timeline-wrapper"> <div class="timeline"> <?php foreach ($timeline_data as $item): ?> <div class="timeline-item <?php echo esc_attr($item['type']); ?>" data-date="<?php echo esc_attr($item['date']); ?>"> <div class="timeline-marker"></div> <div class="timeline-content"> <div class="timeline-date"><?php echo esc_html($item['display_date']); ?></div> <h4 class="timeline-title"><?php echo esc_html($item['title']); ?></h4> <?php if (!empty($item['description'])): ?> <p class="timeline-description"><?php echo esc_html($item['description']); ?></p> <?php endif; ?> <?php if (!empty($item['status'])): ?> <span class="timeline-status status-<?php echo esc_attr($item['status']); ?>"> <?php echo esc_html($item['status']); ?> </span> <?php endif; ?> </div> </div> <?php endforeach; ?> </div> </div> </div> <style> .project-timeline-container { border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; background: #fff; } .timeline-header { padding: 15px 20px; background: #f8f9fa; border-bottom: 1px solid #e0e0e0; } .timeline-header h3 { margin: 0; font-size: 18px; color: #333; } .timeline-wrapper { padding: 20px; height: calc(100% - 60px); overflow-y: auto; } .timeline { position: relative; padding-left: 30px; } .timeline::before { content: ''; position: absolute; left: 10px; top: 0; bottom: 0; width: 2px; background: #0073aa; } .timeline-item { position: relative; margin-bottom: 25px; } .timeline-marker { position: absolute; left: -25px; top: 5px; width: 12px; height: 12px; border-radius: 50%; background: #fff; border: 3px solid #0073aa; z-index: 1; } .timeline-item.milestone .timeline-marker { border-color:
发表评论