WordPress开发教程:集成网站用户积分兑换虚拟礼物与打赏系统 引言:为什么WordPress网站需要用户互动系统 在当今互联网环境中,用户参与度已成为衡量网站成功与否的关键指标。无论是内容型网站、社区论坛还是电商平台,如何增强用户粘性、促进用户互动都是运营者面临的重要课题。用户积分系统和虚拟礼物打赏功能正是解决这一问题的有效工具。 WordPress作为全球最流行的内容管理系统,其强大的扩展性和灵活性使其成为实现这些功能的理想平台。通过代码二次开发,我们可以在WordPress网站上集成完整的用户积分、虚拟礼物兑换和打赏系统,从而提升用户体验,增加网站活跃度,甚至创造新的盈利模式。 本教程将详细讲解如何通过WordPress代码二次开发,实现一个完整的用户积分兑换虚拟礼物与打赏系统,涵盖从数据库设计、功能实现到前端展示的全过程。 系统架构设计与技术选型 1.1 系统功能模块划分 在开始开发之前,我们需要明确系统的功能模块: 用户积分管理模块:包括积分获取、消费、查询和统计功能 虚拟礼物商城模块:礼物的展示、分类、购买和赠送功能 打赏系统模块:支持内容打赏、用户间打赏和打赏记录查询 后台管理模块:积分规则设置、礼物管理、数据统计等功能 前端交互模块:用户界面、AJAX交互和实时通知 1.2 技术栈选择 核心框架:WordPress 5.0+ 数据库:MySQL 5.6+ 前端技术:HTML5, CSS3, JavaScript (jQuery/AJAX) 安全机制:WordPress Nonce验证、数据过滤与转义 缓存优化:Transients API、对象缓存 1.3 数据库表设计 我们需要创建以下自定义数据库表来支持系统功能: -- 用户积分表 CREATE TABLE wp_user_points ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) NOT NULL, points INT DEFAULT 0, total_earned INT DEFAULT 0, total_spent INT DEFAULT 0, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE ); -- 积分记录表 CREATE TABLE wp_points_log ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) NOT NULL, points_change INT NOT NULL, action_type VARCHAR(50) NOT NULL, related_id BIGINT(20), description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE ); -- 虚拟礼物表 CREATE TABLE wp_virtual_gifts ( id INT AUTO_INCREMENT PRIMARY KEY, gift_name VARCHAR(100) NOT NULL, gift_description TEXT, gift_price INT NOT NULL, gift_image VARCHAR(255), gift_category VARCHAR(50), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 礼物交易记录表 CREATE TABLE wp_gift_transactions ( id INT AUTO_INCREMENT PRIMARY KEY, sender_id BIGINT(20) NOT NULL, receiver_id BIGINT(20) NOT NULL, gift_id INT NOT NULL, quantity INT DEFAULT 1, message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (sender_id) REFERENCES wp_users(ID) ON DELETE CASCADE, FOREIGN KEY (receiver_id) REFERENCES wp_users(ID) ON DELETE CASCADE, FOREIGN KEY (gift_id) REFERENCES wp_virtual_gifts(id) ON DELETE CASCADE ); -- 打赏记录表 CREATE TABLE wp_tip_records ( id INT AUTO_INCREMENT PRIMARY KEY, tipper_id BIGINT(20) NOT NULL, receiver_id BIGINT(20) NOT NULL, post_id BIGINT(20), points_amount INT NOT NULL, message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (tipper_id) REFERENCES wp_users(ID) ON DELETE CASCADE, FOREIGN KEY (receiver_id) REFERENCES wp_users(ID) ON DELETE CASCADE, FOREIGN KEY (post_id) REFERENCES wp_posts(ID) ON DELETE SET NULL ); 用户积分系统的实现 2.1 积分系统核心类设计 首先,我们创建一个积分系统的核心类,用于处理所有积分相关操作: <?php /** * WordPress用户积分系统核心类 */ class WP_Points_System { 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() { // 用户注册时初始化积分账户 add_action('user_register', array($this, 'init_user_points_account')); // 删除用户时清理积分数据 add_action('delete_user', array($this, 'delete_user_points_data')); // 添加积分获取规则 add_action('publish_post', array($this, 'award_points_for_publishing')); add_action('wp_insert_comment', array($this, 'award_points_for_comment')); add_action('daily_points_check', array($this, 'award_daily_login_points')); } /** * 初始化用户积分账户 */ public function init_user_points_account($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'user_points'; // 检查是否已存在记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE user_id = %d", $user_id )); if (!$existing) { // 新用户赠送初始积分 $initial_points = get_option('points_initial', 100); $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'points' => $initial_points, 'total_earned' => $initial_points ), array('%d', '%d', '%d') ); // 记录积分日志 $this->add_points_log( $user_id, $initial_points, 'registration', null, '新用户注册赠送积分' ); } } /** * 获取用户当前积分 */ public function get_user_points($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'user_points'; $points = $wpdb->get_var($wpdb->prepare( "SELECT points FROM $table_name WHERE user_id = %d", $user_id )); return $points ? intval($points) : 0; } /** * 增加用户积分 */ public function add_points($user_id, $points, $action_type, $related_id = null, $description = '') { global $wpdb; if ($points <= 0) { return false; } $points_table = $wpdb->prefix . 'user_points'; // 更新用户积分总额 $wpdb->query($wpdb->prepare( "UPDATE $points_table SET points = points + %d, total_earned = total_earned + %d WHERE user_id = %d", $points, $points, $user_id )); // 记录积分日志 $this->add_points_log($user_id, $points, $action_type, $related_id, $description); // 触发积分增加动作 do_action('points_added', $user_id, $points, $action_type); return true; } /** * 扣除用户积分 */ public function deduct_points($user_id, $points, $action_type, $related_id = null, $description = '') { global $wpdb; if ($points <= 0) { return false; } // 检查用户是否有足够积分 $current_points = $this->get_user_points($user_id); if ($current_points < $points) { return false; } $points_table = $wpdb->prefix . 'user_points'; // 更新用户积分总额 $wpdb->query($wpdb->prepare( "UPDATE $points_table SET points = points - %d, total_spent = total_spent + %d WHERE user_id = %d", $points, $points, $user_id )); // 记录积分日志 $this->add_points_log($user_id, -$points, $action_type, $related_id, $description); // 触发积分扣除动作 do_action('points_deducted', $user_id, $points, $action_type); return true; } /** * 添加积分记录 */ private function add_points_log($user_id, $points_change, $action_type, $related_id, $description) { global $wpdb; $log_table = $wpdb->prefix . 'points_log'; $wpdb->insert( $log_table, array( 'user_id' => $user_id, 'points_change' => $points_change, 'action_type' => $action_type, 'related_id' => $related_id, 'description' => $description ), array('%d', '%d', '%s', '%d', '%s') ); return $wpdb->insert_id; } /** * 获取用户积分记录 */ public function get_user_points_log($user_id, $limit = 20, $offset = 0) { global $wpdb; $log_table = $wpdb->prefix . 'points_log'; $logs = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $log_table WHERE user_id = %d ORDER BY created_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset )); return $logs; } /** * 发布文章奖励积分 */ public function award_points_for_publishing($post_id) { $post = get_post($post_id); $user_id = $post->post_author; // 避免重复奖励 if (get_post_meta($post_id, '_points_awarded', true)) { return; } $points_for_post = get_option('points_for_post', 50); if ($points_for_post > 0) { $this->add_points( $user_id, $points_for_post, 'publish_post', $post_id, sprintf('发布文章《%s》', $post->post_title) ); update_post_meta($post_id, '_points_awarded', true); } } /** * 评论奖励积分 */ public function award_points_for_comment($comment_id) { $comment = get_comment($comment_id); $user_id = $comment->user_id; // 只奖励已批准的评论 if ($comment->comment_approved != 1) { return; } // 避免重复奖励 if (get_comment_meta($comment_id, '_points_awarded', true)) { return; } $points_for_comment = get_option('points_for_comment', 10); if ($points_for_comment > 0 && $user_id > 0) { $this->add_points( $user_id, $points_for_comment, 'add_comment', $comment_id, '发表评论' ); update_comment_meta($comment_id, '_points_awarded', true); } } /** * 每日登录奖励积分 */ public function award_daily_login_points() { // 获取所有活跃用户 $users = get_users(array( 'meta_key' => 'last_login_date', 'meta_value' => date('Y-m-d', strtotime('-1 day')), 'meta_compare' => '>=' )); $points_for_daily_login = get_option('points_for_daily_login', 5); foreach ($users as $user) { $this->add_points( $user->ID, $points_for_daily_login, 'daily_login', null, '每日登录奖励' ); } } /** * 删除用户积分数据 */ public function delete_user_points_data($user_id) { global $wpdb; $points_table = $wpdb->prefix . 'user_points'; $log_table = $wpdb->prefix . 'points_log'; $wpdb->delete($points_table, array('user_id' => $user_id), array('%d')); $wpdb->delete($log_table, array('user_id' => $user_id), array('%d')); } } 2.2 积分系统短代码与模板标签 为了让前端可以方便地显示积分信息,我们需要创建一些短代码和模板函数: /** * 积分系统短代码和模板函数 */ class WP_Points_Template_Functions { /** * 显示当前用户积分的短代码 */ public static function points_balance_shortcode($atts) { if (!is_user_logged_in()) { return '<div class="points-balance">请登录查看积分</div>'; } $user_id = get_current_user_id(); $points_system = WP_Points_System::get_instance(); $points = $points_system->get_user_points($user_id); return sprintf( '<div class="points-balance">当前积分: <strong>%d</strong></div>', $points ); } /** * 显示用户积分排行榜的短代码 */ public static function points_leaderboard_shortcode($atts) { $atts = shortcode_atts(array( 'limit' => 10, 'show_avatar' => true, 'show_points' => true ), $atts); global $wpdb; $points_table = $wpdb->prefix . 'user_points'; $users_table = $wpdb->prefix . 'users'; $leaderboard = $wpdb->get_results($wpdb->prepare( "SELECT u.ID, u.user_login, u.display_name, p.points FROM $points_table p INNER JOIN $users_table u ON p.user_id = u.ID ORDER BY p.points DESC LIMIT %d", $atts['limit'] )); if (empty($leaderboard)) { return '<p>暂无积分数据</p>'; } $output = '<div class="points-leaderboard"><h3>积分排行榜</h3><ol>'; foreach ($leaderboard as $index => $user) { $output .= '<li>'; if ($atts['show_avatar']) { $output .= get_avatar($user->ID, 40) . ' '; } $output .= esc_html($user->display_name ?: $user->user_login); if ($atts['show_points']) { $output .= sprintf(' <span class="points-count">(%d 积分)</span>', $user->points); } $output .= '</li>'; } $output .= '</ol></div>'; return $output; } /** * 模板函数:获取用户积分 */ public static function get_user_points($user_id = null) { if (!$user_id) { $user_id = get_current_user_id(); } $points_system = WP_Points_System::get_instance(); return $points_system->get_user_points($user_id); } /** * 模板函数:显示积分日志 */ public static function display_points_log($user_id = null, $limit = 20) { if (!$user_id) { $user_id = get_current_user_id(); } $points_system = WP_Points_System::get_instance(); $logs = $points_system->get_user_points_log($user_id, $limit); if (empty($logs)) { return '<p>暂无积分记录</p>'; } $output = '<table class="points-log-table"><thead> <tr><th>时间</th><th>操作</th><th>积分变化</th><th>描述</th></tr> </thead><tbody>'; foreach ($logs as $log) { $points_change = $log->points_change > 0 ? sprintf('+%d', $log->points_change) : sprintf('%d', $log->points_change); $points_class = $log->points_change > 0 ? 'points-increase' : 'points-decrease'; $output .= sprintf( '<tr> <td>%s</td> <td>%s</td> <td class="%s">%s</td> <td>%s</td> </tr>', $log->created_at, esc_html($log->action_type), $points_class, $points_change, esc_html($log->description) ); } $output .= '</tbody></table>'; return $output; } } // 注册短代码 add_shortcode('points_balance', array('WP_Points_Template_Functions', 'points_balance_shortcode')); add_shortcode('points_leaderboard', array('WP_Points_Template_Functions', 'points_leaderboard_shortcode')); 虚拟礼物系统的实现 3.1 虚拟礼物管理类 <?php /** * WordPress虚拟礼物系统 */ class WP_Virtual_Gifts_System { private static $instance = null; get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 初始化礼物数据表 add_action('init', array($this, 'check_gifts_table')); // 添加AJAX处理 add_action('wp_ajax_purchase_gift', array($this, 'ajax_purchase_gift')); add_action('wp_ajax_nopriv_purchase_gift', array($this, 'ajax_no_permission')); add_action('wp_ajax_send_gift', array($this, 'ajax_send_gift')); add_action('wp_ajax_nopriv_send_gift', array($this, 'ajax_no_permission')); // 添加管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); } /** * 检查并创建礼物数据表 */ public function check_gifts_table() { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $transactions_table = $wpdb->prefix . 'gift_transactions'; // 检查表是否存在,如果不存在则创建 if ($wpdb->get_var("SHOW TABLES LIKE '$gifts_table'") != $gifts_table) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $gifts_table ( id INT AUTO_INCREMENT PRIMARY KEY, gift_name VARCHAR(100) NOT NULL, gift_description TEXT, gift_price INT NOT NULL, gift_image VARCHAR(255), gift_category VARCHAR(50), is_active BOOLEAN DEFAULT TRUE, sort_order INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 插入示例礼物数据 $this->insert_sample_gifts(); } if ($wpdb->get_var("SHOW TABLES LIKE '$transactions_table'") != $transactions_table) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $transactions_table ( id INT AUTO_INCREMENT PRIMARY KEY, sender_id BIGINT(20) NOT NULL, receiver_id BIGINT(20) NOT NULL, gift_id INT NOT NULL, quantity INT DEFAULT 1, message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (sender_id) REFERENCES {$wpdb->prefix}users(ID) ON DELETE CASCADE, FOREIGN KEY (receiver_id) REFERENCES {$wpdb->prefix}users(ID) ON DELETE CASCADE, FOREIGN KEY (gift_id) REFERENCES $gifts_table(id) ON DELETE CASCADE ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } } /** * 插入示例礼物数据 */ private function insert_sample_gifts() { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $sample_gifts = array( array( 'gift_name' => '玫瑰花', 'gift_description' => '表达喜爱之情', 'gift_price' => 10, 'gift_category' => '情感', 'sort_order' => 1 ), array( 'gift_name' => '咖啡', 'gift_description' => '请喝一杯咖啡', 'gift_price' => 20, 'gift_category' => '日常', 'sort_order' => 2 ), array( 'gift_name' => '奖杯', 'gift_description' => '表彰优秀贡献', 'gift_price' => 50, 'gift_category' => '荣誉', 'sort_order' => 3 ), array( 'gift_name' => '钻石', 'gift_description' => '珍贵的心意', 'gift_price' => 100, 'gift_category' => '豪华', 'sort_order' => 4 ), array( 'gift_name' => '蛋糕', 'gift_description' => '庆祝特别时刻', 'gift_price' => 30, 'gift_category' => '庆祝', 'sort_order' => 5 ) ); foreach ($sample_gifts as $gift) { $wpdb->insert($gifts_table, $gift); } } /** * 获取所有可用礼物 */ public function get_available_gifts($category = null) { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $where = "WHERE is_active = 1"; if ($category) { $where .= $wpdb->prepare(" AND gift_category = %s", $category); } $gifts = $wpdb->get_results( "SELECT * FROM $gifts_table $where ORDER BY sort_order ASC, gift_price ASC" ); return $gifts; } /** * 获取单个礼物信息 */ public function get_gift($gift_id) { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $gift = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $gifts_table WHERE id = %d", $gift_id )); return $gift; } /** * 购买礼物 */ public function purchase_gift($user_id, $gift_id, $quantity = 1) { $gift = $this->get_gift($gift_id); if (!$gift || !$gift->is_active) { return array('success' => false, 'message' => '礼物不存在或已下架'); } $total_cost = $gift->gift_price * $quantity; // 检查用户积分是否足够 $points_system = WP_Points_System::get_instance(); $user_points = $points_system->get_user_points($user_id); if ($user_points < $total_cost) { return array('success' => false, 'message' => '积分不足'); } // 扣除积分 $deduct_result = $points_system->deduct_points( $user_id, $total_cost, 'purchase_gift', $gift_id, sprintf('购买礼物「%s」x%d', $gift->gift_name, $quantity) ); if (!$deduct_result) { return array('success' => false, 'message' => '积分扣除失败'); } // 这里可以添加礼物库存逻辑,如果需要的话 return array( 'success' => true, 'message' => '购买成功', 'gift_id' => $gift_id, 'quantity' => $quantity, 'total_cost' => $total_cost ); } /** * 赠送礼物给其他用户 */ public function send_gift($sender_id, $receiver_id, $gift_id, $quantity = 1, $message = '') { global $wpdb; // 检查接收者是否存在 $receiver = get_user_by('ID', $receiver_id); if (!$receiver) { return array('success' => false, 'message' => '接收者不存在'); } // 不能送给自己 if ($sender_id == $receiver_id) { return array('success' => false, 'message' => '不能给自己赠送礼物'); } // 先购买礼物 $purchase_result = $this->purchase_gift($sender_id, $gift_id, $quantity); if (!$purchase_result['success']) { return $purchase_result; } // 记录礼物交易 $transactions_table = $wpdb->prefix . 'gift_transactions'; $wpdb->insert( $transactions_table, array( 'sender_id' => $sender_id, 'receiver_id' => $receiver_id, 'gift_id' => $gift_id, 'quantity' => $quantity, 'message' => $message ), array('%d', '%d', '%d', '%d', '%s') ); $transaction_id = $wpdb->insert_id; // 发送通知给接收者 $this->send_gift_notification($sender_id, $receiver_id, $gift_id, $quantity, $message); return array( 'success' => true, 'message' => '礼物赠送成功', 'transaction_id' => $transaction_id ); } /** * 发送礼物通知 */ private function send_gift_notification($sender_id, $receiver_id, $gift_id, $quantity, $message) { $sender = get_user_by('ID', $sender_id); $gift = $this->get_gift($gift_id); $notification = sprintf( '%s 向您赠送了 %d 个「%s」', $sender->display_name, $quantity, $gift->gift_name ); if (!empty($message)) { $notification .= sprintf(',并留言:%s', $message); } // 使用WordPress的通知系统或自定义通知 if (function_exists('bp_notifications_add_notification')) { // BuddyPress通知 bp_notifications_add_notification(array( 'user_id' => $receiver_id, 'item_id' => $gift_id, 'component_name' => 'gifts', 'component_action' => 'new_gift', 'date_notified' => bp_core_current_time(), 'is_new' => 1, )); } // 发送站内信 $this->send_private_message($sender_id, $receiver_id, $notification); // 发送邮件通知 $receiver_email = get_user_by('ID', $receiver_id)->user_email; $subject = '您收到了一份新礼物'; wp_mail($receiver_email, $subject, $notification); } /** * 发送站内私信 */ private function send_private_message($sender_id, $receiver_id, $message) { // 这里可以集成站内信系统 // 如果使用BuddyPress,可以使用bp_messages_new_message函数 // 或者使用自定义的站内信系统 } /** * 获取用户收到的礼物 */ public function get_received_gifts($user_id, $limit = 20, $offset = 0) { global $wpdb; $transactions_table = $wpdb->prefix . 'gift_transactions'; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $users_table = $wpdb->prefix . 'users'; $gifts = $wpdb->get_results($wpdb->prepare( "SELECT t.*, g.gift_name, g.gift_image, u.display_name as sender_name FROM $transactions_table t INNER JOIN $gifts_table g ON t.gift_id = g.id INNER JOIN $users_table u ON t.sender_id = u.ID WHERE t.receiver_id = %d ORDER BY t.created_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset )); return $gifts; } /** * 获取用户送出的礼物 */ public function get_sent_gifts($user_id, $limit = 20, $offset = 0) { global $wpdb; $transactions_table = $wpdb->prefix . 'gift_transactions'; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $users_table = $wpdb->prefix . 'users'; $gifts = $wpdb->get_results($wpdb->prepare( "SELECT t.*, g.gift_name, g.gift_image, u.display_name as receiver_name FROM $transactions_table t INNER JOIN $gifts_table g ON t.gift_id = g.id INNER JOIN $users_table u ON t.receiver_id = u.ID WHERE t.sender_id = %d ORDER BY t.created_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset )); return $gifts; } /** * AJAX处理:购买礼物 */ public function ajax_purchase_gift() { // 安全检查 check_ajax_referer('gift_nonce', 'nonce'); if (!is_user_logged_in()) { wp_die(json_encode(array( 'success' => false, 'message' => '请先登录' ))); } $user_id = get_current_user_id(); $gift_id = intval($_POST['gift_id']); $quantity = isset($_POST['quantity']) ? intval($_POST['quantity']) : 1; if ($quantity < 1) { $quantity = 1; } $result = $this->purchase_gift($user_id, $gift_id, $quantity); wp_die(json_encode($result)); } /** * AJAX处理:赠送礼物 */ public function ajax_send_gift() { // 安全检查 check_ajax_referer('gift_nonce', 'nonce'); if (!is_user_logged_in()) { wp_die(json_encode(array( 'success' => false, 'message' => '请先登录' ))); } $sender_id = get_current_user_id(); $receiver_id = intval($_POST['receiver_id']); $gift_id = intval($_POST['gift_id']); $quantity = isset($_POST['quantity']) ? intval($_POST['quantity']) : 1; $message = isset($_POST['message']) ? sanitize_text_field($_POST['message']) : ''; $result = $this->send_gift($sender_id, $receiver_id, $gift_id, $quantity, $message); wp_die(json_encode($result)); } /** * 无权限的AJAX请求处理 */ public function ajax_no_permission() { wp_die(json_encode(array( 'success' => false, 'message' => '无权限操作' ))); } /** * 添加管理菜单 */ public function add_admin_menu() { add_menu_page( '虚拟礼物管理', '虚拟礼物', 'manage_options', 'virtual-gifts', array($this, 'gifts_admin_page'), 'dashicons-games', 30 ); add_submenu_page( 'virtual-gifts', '礼物管理', '礼物列表', 'manage_options', 'virtual-gifts', array($this, 'gifts_admin_page') ); add_submenu_page( 'virtual-gifts', '交易记录', '交易记录', 'manage_options', 'gift-transactions', array($this, 'transactions_admin_page') ); } /** * 礼物管理页面 */ public function gifts_admin_page() { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; // 处理表单提交 if (isset($_POST['add_gift'])) { $this->handle_add_gift(); } elseif (isset($_POST['update_gift'])) { $this->handle_update_gift(); } elseif (isset($_GET['delete_gift'])) { $this->handle_delete_gift(); } // 获取所有礼物 $gifts = $wpdb->get_results("SELECT * FROM $gifts_table ORDER BY sort_order ASC"); ?> <div class="wrap"> <h1>虚拟礼物管理</h1> <h2>添加新礼物</h2> <form method="post" action=""> <?php wp_nonce_field('gift_management', 'gift_nonce'); ?> <table class="form-table"> <tr> <th><label for="gift_name">礼物名称</label></th> <td><input type="text" id="gift_name" name="gift_name" required class="regular-text"></td> </tr> <tr> <th><label for="gift_description">描述</label></th> <td><textarea id="gift_description" name="gift_description" rows="3" class="large-text"></textarea></td> </tr> <tr> <th><label for="gift_price">价格(积分)</label></th> <td><input type="number" id="gift_price" name="gift_price" required min="1" class="small-text"></td> </tr> <tr> <th><label for="gift_category">分类</label></th> <td><input type="text" id="gift_category" name="gift_category" class="regular-text"></td> </tr> <tr> <th><label for="sort_order">排序</label></th> <td><input type="number" id="sort_order" name="sort_order" value="0" class="small-text"></td> </tr> </table> <p class="submit"> <input type="submit" name="add_gift"
发表评论分类: 应用软件
实战教学:为你的网站添加在线团队任务投票与优先级排序工具 引言:为什么你的团队需要在线任务投票与优先级排序工具 在现代团队协作中,任务优先级管理是决定工作效率的关键因素。根据项目管理协会(PMI)的调查,超过37%的项目失败是由于需求优先级不明确造成的。对于使用WordPress搭建的企业网站、团队协作平台或项目管理门户而言,集成一个在线任务投票与优先级排序工具可以显著提升团队决策效率。 传统的任务优先级确定方式往往依赖于少数管理者的主观判断,而在线投票工具能够让每个团队成员参与决策过程,通过集体智慧确定任务的真实优先级。这不仅提高了决策的透明度,也增强了团队成员的责任感和参与感。 本文将带领你通过WordPress代码二次开发,实现一个功能完整的在线团队任务投票与优先级排序工具。我们将从需求分析开始,逐步完成数据库设计、前端界面开发、后端逻辑实现以及安全性优化,最终打造一个适合团队协作的实用工具。 第一部分:环境准备与需求分析 1.1 开发环境配置 在开始开发之前,我们需要确保WordPress环境已正确配置: 本地开发环境:建议使用Local by Flywheel、XAMPP或MAMP搭建本地WordPress环境 WordPress版本:确保使用最新版本的WordPress(5.8+) 代码编辑器:推荐使用VS Code、PHPStorm或Sublime Text 浏览器开发者工具:用于调试前端代码 1.2 功能需求分析 我们的在线团队任务投票与优先级排序工具需要包含以下核心功能: 任务管理功能: 创建、编辑、删除任务 任务描述、截止日期、负责人设置 任务分类与标签 投票系统功能: 团队成员对任务进行投票 支持多种投票方式(优先级评分、简单投票等) 实时显示投票结果 优先级计算功能: 根据投票结果自动计算任务优先级 支持多种优先级算法(加权平均、多数决等) 可视化优先级展示 用户权限管理: 不同用户角色权限控制 投票权限管理 任务管理权限控制 数据可视化功能: 任务优先级看板 投票结果图表 团队参与度统计 1.3 技术选型 为了实现上述功能,我们将采用以下技术方案: 后端:WordPress原生PHP函数 + 自定义数据库表 前端:HTML5、CSS3、JavaScript(Vanilla JS + 少量jQuery) 数据可视化:Chart.js库 AJAX通信:WordPress REST API + admin-ajax.php 安全性:WordPress非ces、能力检查、数据验证 第二部分:数据库设计与数据模型 2.1 自定义数据库表设计 我们需要创建三个自定义数据库表来存储任务、投票和用户数据: -- 任务表 CREATE TABLE wp_team_tasks ( task_id INT AUTO_INCREMENT PRIMARY KEY, task_title VARCHAR(255) NOT NULL, task_description TEXT, task_status ENUM('pending', 'active', 'completed', 'archived') DEFAULT 'pending', created_by INT NOT NULL, assigned_to INT, due_date DATE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES wp_users(ID), FOREIGN KEY (assigned_to) REFERENCES wp_users(ID) ); -- 投票表 CREATE TABLE wp_task_votes ( vote_id INT AUTO_INCREMENT PRIMARY KEY, task_id INT NOT NULL, user_id INT NOT NULL, vote_value INT NOT NULL CHECK (vote_value BETWEEN 1 AND 10), vote_comment TEXT, voted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_user_task (user_id, task_id), FOREIGN KEY (task_id) REFERENCES wp_team_tasks(task_id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ); -- 任务分类表 CREATE TABLE wp_task_categories ( category_id INT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(100) NOT NULL, category_color VARCHAR(7) DEFAULT '#3498db', created_by INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES wp_users(ID) ); 2.2 数据库表关系与索引优化 为了提高查询效率,我们需要为常用查询字段添加索引: -- 为任务表添加索引 ALTER TABLE wp_team_tasks ADD INDEX idx_task_status (task_status); ALTER TABLE wp_team_tasks ADD INDEX idx_due_date (due_date); ALTER TABLE wp_team_tasks ADD INDEX idx_assigned_to (assigned_to); -- 为投票表添加索引 ALTER TABLE wp_task_votes ADD INDEX idx_task_id (task_id); ALTER TABLE wp_task_votes ADD INDEX idx_user_id (user_id); -- 为分类表添加索引 ALTER TABLE wp_task_categories ADD INDEX idx_created_by (created_by); 2.3 创建数据库表的WordPress实现 在WordPress中,我们通常在插件激活时创建自定义数据库表: <?php /** * 创建自定义数据库表 */ function team_task_voting_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix; // 任务表 $tasks_table = $table_prefix . 'team_tasks'; $tasks_sql = "CREATE TABLE IF NOT EXISTS $tasks_table ( task_id INT AUTO_INCREMENT PRIMARY KEY, task_title VARCHAR(255) NOT NULL, task_description TEXT, task_status ENUM('pending', 'active', 'completed', 'archived') DEFAULT 'pending', created_by INT NOT NULL, assigned_to INT, due_date DATE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) $charset_collate;"; // 投票表 $votes_table = $table_prefix . 'task_votes'; $votes_sql = "CREATE TABLE IF NOT EXISTS $votes_table ( vote_id INT AUTO_INCREMENT PRIMARY KEY, task_id INT NOT NULL, user_id INT NOT NULL, vote_value INT NOT NULL, vote_comment TEXT, voted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_user_task (user_id, task_id) ) $charset_collate;"; // 分类表 $categories_table = $table_prefix . 'task_categories'; $categories_sql = "CREATE TABLE IF NOT EXISTS $categories_table ( category_id INT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(100) NOT NULL, category_color VARCHAR(7) DEFAULT '#3498db', created_by INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($tasks_sql); dbDelta($votes_sql); dbDelta($categories_sql); // 添加外键约束(如果支持) if (strpos($wpdb->dbname, 'mysql') !== false) { $wpdb->query("ALTER TABLE $tasks_table ADD FOREIGN KEY (created_by) REFERENCES {$table_prefix}users(ID) ON DELETE CASCADE"); $wpdb->query("ALTER TABLE $tasks_table ADD FOREIGN KEY (assigned_to) REFERENCES {$table_prefix}users(ID) ON DELETE SET NULL"); $wpdb->query("ALTER TABLE $votes_table ADD FOREIGN KEY (task_id) REFERENCES $tasks_table(task_id) ON DELETE CASCADE"); $wpdb->query("ALTER TABLE $votes_table ADD FOREIGN KEY (user_id) REFERENCES {$table_prefix}users(ID) ON DELETE CASCADE"); $wpdb->query("ALTER TABLE $categories_table ADD FOREIGN KEY (created_by) REFERENCES {$table_prefix}users(ID) ON DELETE CASCADE"); } } register_activation_hook(__FILE__, 'team_task_voting_create_tables'); ?> 第三部分:后端功能开发 3.1 创建自定义Post Type与Taxonomy 虽然我们使用了自定义数据库表,但为了利用WordPress的权限系统和部分功能,我们也可以创建自定义文章类型: <?php /** * 注册任务自定义文章类型 */ function register_task_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' => 'team-task'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-clipboard', 'supports' => array('title', 'editor', 'author', 'custom-fields'), 'show_in_rest' => true, // 启用Gutenberg编辑器支持 ); register_post_type('team_task', $args); // 注册任务分类 register_taxonomy( 'task_category', 'team_task', array( 'label' => '任务分类', 'rewrite' => array('slug' => 'task-category'), 'hierarchical' => true, 'show_in_rest' => true, ) ); } add_action('init', 'register_task_post_type'); ?> 3.2 创建任务管理类 我们将创建一个任务管理类来处理所有与任务相关的操作: <?php /** * 任务管理类 */ class Team_Task_Manager { private $db; private $tasks_table; private $votes_table; private $categories_table; public function __construct() { global $wpdb; $this->db = $wpdb; $this->tasks_table = $wpdb->prefix . 'team_tasks'; $this->votes_table = $wpdb->prefix . 'task_votes'; $this->categories_table = $wpdb->prefix . 'task_categories'; } /** * 创建新任务 */ public function create_task($data) { $defaults = array( 'task_title' => '', 'task_description' => '', 'task_status' => 'pending', 'created_by' => get_current_user_id(), 'assigned_to' => null, 'due_date' => null, 'category_id' => null ); $data = wp_parse_args($data, $defaults); // 验证数据 if (empty($data['task_title'])) { return new WP_Error('empty_title', '任务标题不能为空'); } // 插入数据 $result = $this->db->insert( $this->tasks_table, array( 'task_title' => sanitize_text_field($data['task_title']), 'task_description' => wp_kses_post($data['task_description']), 'task_status' => sanitize_text_field($data['task_status']), 'created_by' => intval($data['created_by']), 'assigned_to' => !empty($data['assigned_to']) ? intval($data['assigned_to']) : null, 'due_date' => !empty($data['due_date']) ? sanitize_text_field($data['due_date']) : null ), array('%s', '%s', '%s', '%d', '%d', '%s') ); if ($result === false) { return new WP_Error('db_error', '数据库插入失败'); } $task_id = $this->db->insert_id; // 如果有分类,添加到分类关系表 if (!empty($data['category_id'])) { $this->add_task_to_category($task_id, intval($data['category_id'])); } return $task_id; } /** * 获取任务列表 */ public function get_tasks($args = array()) { $defaults = array( 'status' => null, 'assigned_to' => null, 'created_by' => null, 'category_id' => null, 'page' => 1, 'per_page' => 10, 'orderby' => 'created_at', 'order' => 'DESC' ); $args = wp_parse_args($args, $defaults); $where = array('1=1'); $values = array(); if (!empty($args['status'])) { $where[] = 'task_status = %s'; $values[] = $args['status']; } if (!empty($args['assigned_to'])) { $where[] = 'assigned_to = %d'; $values[] = $args['assigned_to']; } if (!empty($args['created_by'])) { $where[] = 'created_by = %d'; $values[] = $args['created_by']; } $where_clause = implode(' AND ', $where); // 分页计算 $offset = ($args['page'] - 1) * $args['per_page']; // 如果有分类筛选,需要联表查询 if (!empty($args['category_id'])) { $query = $this->db->prepare( "SELECT t.* FROM {$this->tasks_table} t INNER JOIN {$this->db->prefix}task_category_relationships r ON t.task_id = r.task_id WHERE $where_clause AND r.category_id = %d ORDER BY {$args['orderby']} {$args['order']} LIMIT %d OFFSET %d", array_merge($values, array($args['category_id'], $args['per_page'], $offset)) ); } else { $query = $this->db->prepare( "SELECT * FROM {$this->tasks_table} WHERE $where_clause ORDER BY {$args['orderby']} {$args['order']} LIMIT %d OFFSET %d", array_merge($values, array($args['per_page'], $offset)) ); } $tasks = $this->db->get_results($query, ARRAY_A); // 获取总数量用于分页 if (!empty($args['category_id'])) { $count_query = $this->db->prepare( "SELECT COUNT(*) FROM {$this->tasks_table} t INNER JOIN {$this->db->prefix}task_category_relationships r ON t.task_id = r.task_id WHERE $where_clause AND r.category_id = %d", array_merge($values, array($args['category_id'])) ); } else { $count_query = $this->db->prepare( "SELECT COUNT(*) FROM {$this->tasks_table} WHERE $where_clause", $values ); } $total = $this->db->get_var($count_query); return array( 'tasks' => $tasks, 'total' => $total, 'total_pages' => ceil($total / $args['per_page']) ); } /** * 提交投票 */ public function submit_vote($task_id, $user_id, $vote_value, $comment = '') { // 验证投票值 $vote_value = intval($vote_value); if ($vote_value < 1 || $vote_value > 10) { return new WP_Error('invalid_vote', '投票值必须在1-10之间'); } // 检查用户是否已投票 $existing_vote = $this->db->get_var($this->db->prepare( "SELECT vote_id FROM {$this->votes_table} WHERE task_id = %d AND user_id = %d", $task_id, $user_id )); if ($existing_vote) { // 更新现有投票 $result = $this->db->update( $this->votes_table, array( 'vote_value' => $vote_value, 'vote_comment' => sanitize_textarea_field($comment), 'voted_at' => current_time('mysql') ), array('vote_id' => $existing_vote), array('%d', '%s', '%s'), array('%d') ); } else { // 插入新投票 $result = $this->db->insert( $this->votes_table, array( 'task_id' => $task_id, 'user_id' => $user_id, 'vote_value' => $vote_value, 'vote_comment' => sanitize_textarea_field($comment) ), array('%d', '%d', '%d', '%s') ); } if ($result === false) { return new WP_Error('db_error', '投票提交失败'); } return true; } /** * 计算任务优先级 */ public function calculate_task_priority($task_id) { // 获取所有投票 $votes = $this->db->get_results($this->db->prepare( "SELECT vote_value FROM {$this->votes_table} WHERE task_id = %d", $task_id ), ARRAY_A); if (empty($votes)) { return 0; // 没有投票,优先级为0 3.3 优先级计算算法实现 /** * 计算任务优先级 */ public function calculate_task_priority($task_id) { // 获取所有投票 $votes = $this->db->get_results($this->db->prepare( "SELECT vote_value FROM {$this->votes_table} WHERE task_id = %d", $task_id ), ARRAY_A); if (empty($votes)) { return 0; // 没有投票,优先级为0 } // 提取投票值 $vote_values = array_column($votes, 'vote_value'); // 方法1:简单平均值 $simple_average = array_sum($vote_values) / count($vote_values); // 方法2:加权平均值(考虑投票者权重) $weighted_average = $this->calculate_weighted_average($task_id, $vote_values); // 方法3:去除极端值后的平均值 $trimmed_average = $this->calculate_trimmed_average($vote_values); // 综合优先级计算(可根据需求调整权重) $final_priority = ( $simple_average * 0.4 + $weighted_average * 0.4 + $trimmed_average * 0.2 ); // 获取投票数量作为置信度因子 $vote_count = count($votes); $confidence_factor = min(1, $vote_count / 10); // 最多10票达到完全置信 // 最终优先级(0-10分制) $final_score = $final_priority * $confidence_factor; return round($final_score, 2); } /** * 计算加权平均值 */ private function calculate_weighted_average($task_id, $vote_values) { global $wpdb; // 获取投票者信息 $voters = $wpdb->get_results($wpdb->prepare( "SELECT v.user_id, v.vote_value, u.user_login FROM {$this->votes_table} v LEFT JOIN {$wpdb->users} u ON v.user_id = u.ID WHERE v.task_id = %d", $task_id ), ARRAY_A); if (empty($voters)) { return 0; } $total_weight = 0; $weighted_sum = 0; foreach ($voters as $voter) { // 计算用户权重(基于用户角色、历史投票准确度等) $user_weight = $this->calculate_user_weight($voter['user_id']); $weighted_sum += $voter['vote_value'] * $user_weight; $total_weight += $user_weight; } return $total_weight > 0 ? $weighted_sum / $total_weight : 0; } /** * 计算用户权重 */ private function calculate_user_weight($user_id) { // 基础权重 $weight = 1.0; // 基于用户角色调整权重 $user = get_userdata($user_id); if ($user) { if (in_array('administrator', $user->roles)) { $weight *= 1.5; // 管理员权重更高 } elseif (in_array('editor', $user->roles)) { $weight *= 1.3; } elseif (in_array('author', $user->roles)) { $weight *= 1.2; } } // 基于历史投票参与度调整权重 $participation_rate = $this->get_user_participation_rate($user_id); $weight *= (0.5 + $participation_rate * 0.5); // 参与度影响权重 return $weight; } /** * 计算去除极端值后的平均值 */ private function calculate_trimmed_average($values, $trim_percent = 20) { if (count($values) < 3) { return array_sum($values) / count($values); } sort($values); $trim_count = floor(count($values) * $trim_percent / 100); $trimmed_values = array_slice($values, $trim_count, count($values) - 2 * $trim_count); return array_sum($trimmed_values) / count($trimmed_values); } /** * 获取用户投票参与度 */ private function get_user_participation_rate($user_id) { $total_tasks = $this->db->get_var( "SELECT COUNT(*) FROM {$this->tasks_table} WHERE task_status IN ('pending', 'active')" ); $user_votes = $this->db->get_var($this->db->prepare( "SELECT COUNT(DISTINCT task_id) FROM {$this->votes_table} WHERE user_id = %d", $user_id )); if ($total_tasks == 0) { return 1.0; } return min(1.0, $user_votes / $total_tasks); } /** * 获取任务投票详情 */ public function get_task_voting_details($task_id) { $votes = $this->db->get_results($this->db->prepare( "SELECT v.*, u.display_name, u.user_email FROM {$this->votes_table} v LEFT JOIN {$this->db->users} u ON v.user_id = u.ID WHERE v.task_id = %d ORDER BY v.voted_at DESC", $task_id ), ARRAY_A); $priority_score = $this->calculate_task_priority($task_id); // 统计投票分布 $vote_distribution = array_fill(1, 10, 0); foreach ($votes as $vote) { if ($vote['vote_value'] >= 1 && $vote['vote_value'] <= 10) { $vote_distribution[$vote['vote_value']]++; } } return array( 'votes' => $votes, 'vote_count' => count($votes), 'priority_score' => $priority_score, 'vote_distribution' => $vote_distribution, 'average_vote' => count($votes) > 0 ? array_sum(array_column($votes, 'vote_value')) / count($votes) : 0 ); } } ?> 3.4 REST API端点创建 为了支持前后端分离和AJAX调用,我们需要创建REST API端点: <?php /** * 注册REST API端点 */ class Task_Voting_REST_Controller extends WP_REST_Controller { private $task_manager; public function __construct() { $this->namespace = 'team-tasks/v1'; $this->task_manager = new Team_Task_Manager(); add_action('rest_api_init', array($this, 'register_routes')); } public function register_routes() { // 获取任务列表 register_rest_route($this->namespace, '/tasks', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_tasks'), 'permission_callback' => array($this, 'get_tasks_permissions_check'), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array($this, 'create_task'), 'permission_callback' => array($this, 'create_task_permissions_check'), ), )); // 单个任务操作 register_rest_route($this->namespace, '/tasks/(?P<id>d+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_task'), 'permission_callback' => array($this, 'get_task_permissions_check'), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array($this, 'update_task'), 'permission_callback' => array($this, 'update_task_permissions_check'), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array($this, 'delete_task'), 'permission_callback' => array($this, 'delete_task_permissions_check'), ), )); // 投票相关端点 register_rest_route($this->namespace, '/tasks/(?P<task_id>d+)/vote', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array($this, 'submit_vote'), 'permission_callback' => array($this, 'vote_permissions_check'), ), )); // 获取投票结果 register_rest_route($this->namespace, '/tasks/(?P<task_id>d+)/voting-results', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_voting_results'), 'permission_callback' => array($this, 'get_voting_results_permissions_check'), ), )); // 优先级看板数据 register_rest_route($this->namespace, '/priority-board', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_priority_board'), 'permission_callback' => array($this, 'get_priority_board_permissions_check'), ), )); } /** * 获取任务列表 */ public function get_tasks($request) { $params = $request->get_params(); $args = array( 'status' => isset($params['status']) ? sanitize_text_field($params['status']) : null, 'assigned_to' => isset($params['assigned_to']) ? intval($params['assigned_to']) : null, 'category_id' => isset($params['category_id']) ? intval($params['category_id']) : null, 'page' => isset($params['page']) ? intval($params['page']) : 1, 'per_page' => isset($params['per_page']) ? intval($params['per_page']) : 10, 'orderby' => isset($params['orderby']) ? sanitize_text_field($params['orderby']) : 'created_at', 'order' => isset($params['order']) ? sanitize_text_field($params['order']) : 'DESC', ); $result = $this->task_manager->get_tasks($args); // 为每个任务添加优先级分数 foreach ($result['tasks'] as &$task) { $task['priority_score'] = $this->task_manager->calculate_task_priority($task['task_id']); $task['vote_count'] = $this->task_manager->get_vote_count($task['task_id']); } $response = new WP_REST_Response($result); return $response; } /** * 创建新任务 */ public function create_task($request) { $params = $request->get_params(); $task_data = array( 'task_title' => sanitize_text_field($params['title']), 'task_description' => wp_kses_post($params['description']), 'assigned_to' => isset($params['assigned_to']) ? intval($params['assigned_to']) : null, 'due_date' => isset($params['due_date']) ? sanitize_text_field($params['due_date']) : null, 'category_id' => isset($params['category_id']) ? intval($params['category_id']) : null, ); $task_id = $this->task_manager->create_task($task_data); if (is_wp_error($task_id)) { return $task_id; } $response = new WP_REST_Response(array( 'success' => true, 'task_id' => $task_id, 'message' => '任务创建成功' )); return $response; } /** * 提交投票 */ public function submit_vote($request) { $task_id = intval($request['task_id']); $user_id = get_current_user_id(); $params = $request->get_params(); $vote_value = isset($params['vote_value']) ? intval($params['vote_value']) : 0; $comment = isset($params['comment']) ? sanitize_textarea_field($params['comment']) : ''; if ($vote_value < 1 || $vote_value > 10) { return new WP_Error('invalid_vote', '投票值必须在1-10之间', array('status' => 400)); } $result = $this->task_manager->submit_vote($task_id, $user_id, $vote_value, $comment); if (is_wp_error($result)) { return $result; } // 重新计算优先级 $priority_score = $this->task_manager->calculate_task_priority($task_id); $response = new WP_REST_Response(array( 'success' => true, 'priority_score' => $priority_score, 'message' => '投票成功' )); return $response; } /** * 获取优先级看板数据 */ public function get_priority_board($request) { global $wpdb; // 获取所有活跃任务 $tasks = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}team_tasks WHERE task_status IN ('pending', 'active') ORDER BY created_at DESC", ARRAY_A ); $board_data = array(); foreach ($tasks as $task) { $voting_details = $this->task_manager->get_task_voting_details($task['task_id']); // 获取负责人信息 $assigned_to_name = ''; if ($task['assigned_to']) { $user = get_userdata($task['assigned_to']); $assigned_to_name = $user ? $user->display_name : '未知用户'; } // 获取创建者信息 $creator = get_userdata($task['created_by']); $created_by_name = $creator ? $creator->display_name : '未知用户'; $board_data[] = array( 'id' => $task['task_id'], 'title' => $task['task_title'], 'description' => wp_trim_words($task['task_description'], 20), 'status' => $task['task_status'], 'priority_score' => $voting_details['priority_score'], 'vote_count' => $voting_details['vote_count'], 'average_vote' => $voting_details['average_vote'], 'assigned_to' => $assigned_to_name, 'created_by' => $created_by_name, 'due_date' => $task['due_date'], 'created_at' => $task['created_at'], 'vote_distribution' => $voting_details['vote_distribution'] ); } // 按优先级排序 usort($board_data, function($a, $b) { return $b['priority_score'] <=> $a['priority_score']; }); // 计算统计信息 $stats = array( 'total_tasks' => count($board_data), 'total_votes' => array_sum(array_column($board_data, 'vote_count')), 'average_priority' => count($board_data) > 0 ? array_sum(array_column($board_data, 'priority_score')) / count($board_data) : 0, 'participation_rate' => $this->calculate_overall_participation_rate(), ); $response = new WP_REST_Response(array( 'tasks' => $board_data, 'stats' => $stats )); return $response; } /** * 计算整体参与率 */ private function calculate_overall_participation_rate() { global $wpdb; $total_users = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->users}"); $active_tasks = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}team_tasks WHERE task_status IN ('pending', 'active')" ); if ($active_tasks == 0 || $total_users == 0) { return 0; } $users_voted = $wpdb->get_var( "SELECT COUNT(DISTINCT user_id) FROM {$wpdb->prefix}task_votes" ); // 参与率 = (已投票用户数 / 总用户数) * (平均投票任务数 / 总活跃任务数) $user_participation = $users_voted / $total_users; $total_votes = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}task_votes"); $average_votes_per_user = $users_voted > 0 ? $total_votes / $users_voted : 0; $task_participation = min(1, $average_votes_per_user / $active_tasks); return round(($user_participation + $task_participation) / 2 * 100, 2); } // 权限检查方法 public function get_tasks_permissions_check($request) { return current_user_can('read'); } public function create_task_permissions_check($request) { return current_user_can('publish_posts'); } public function vote_permissions_check($request) { return is_user_logged_in(); } public function get_priority_board_permissions_check($request) { return current_user_can('read'); } } // 初始化REST控制器 new Task_Voting_REST_Controller(); ?> 第四部分:前端界面开发 4.1 任务列表与投票界面
发表评论手把手教程:在WordPress中集成网站第三方API状态监控与异常报警面板 引言:为什么WordPress需要API状态监控功能 在当今数字化时代,网站已经不再是简单的信息展示平台,而是集成了各种第三方服务和API的复杂生态系统。无论是支付网关、社交媒体集成、地图服务、数据分析工具还是云存储服务,现代网站通常依赖多个外部API来提供完整功能。然而,这些第三方服务偶尔会出现故障或响应缓慢,直接影响网站性能和用户体验。 传统的解决方案是使用独立的监控服务,但这些服务往往需要额外订阅费用,且与WordPress后台分离,增加了管理复杂度。本教程将向您展示如何通过WordPress代码二次开发,创建一个集成的API状态监控与异常报警面板,让您在一个熟悉的界面中监控所有关键服务的健康状况。 通过本教程,您将学习到如何利用WordPress的灵活性,扩展其功能,实现一个专业级的监控系统,而无需依赖外部高价服务。这个解决方案不仅成本效益高,而且完全可控,可以根据您的具体需求进行定制。 第一部分:准备工作与环境配置 1.1 确定监控目标与需求 在开始编码之前,我们需要明确监控系统的目标。您应该列出所有需要监控的第三方API,例如: 支付网关(PayPal、Stripe等) 社交媒体API(Facebook、Twitter、Instagram等) 地图服务(Google Maps、Mapbox等) 邮件服务(SMTP、SendGrid、MailChimp等) 云存储(AWS S3、Google Cloud Storage等) CDN服务(Cloudflare、Akamai等) 对于每个API,确定以下监控参数: 检查频率(每5分钟、每15分钟等) 超时阈值 响应状态码要求 响应内容验证规则 性能基准(响应时间) 1.2 开发环境设置 为了安全地进行开发,建议在本地或测试服务器上设置WordPress开发环境: 安装本地服务器环境:使用XAMPP、MAMP或Local by Flywheel 设置WordPress测试站点:安装最新版本的WordPress 启用调试模式:在wp-config.php中添加以下代码: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 安装代码编辑器:推荐VS Code、PHPStorm或Sublime Text 创建子主题或插件目录:我们将创建一个独立插件来实现功能 1.3 创建基础插件结构 在wp-content/plugins目录下创建新文件夹api-status-monitor,并创建以下文件结构: api-status-monitor/ ├── api-status-monitor.php # 主插件文件 ├── includes/ │ ├── class-api-monitor.php # 核心监控类 │ ├── class-alert-system.php # 报警系统类 │ └── class-dashboard-widget.php # 仪表板小工具 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 管理界面样式 │ ├── js/ │ │ └── admin-script.js # 管理界面脚本 │ └── partials/ │ └── settings-page.php # 设置页面模板 ├── assets/ │ ├── css/ │ │ └── frontend-style.css # 前端样式(可选) │ └── js/ │ └── frontend-script.js # 前端脚本(可选) └── uninstall.php # 插件卸载清理脚本 第二部分:构建核心监控系统 2.1 创建主插件文件 编辑api-status-monitor.php,添加插件基本信息: <?php /** * Plugin Name: API状态监控与异常报警面板 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress中集成第三方API状态监控与异常报警功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: api-status-monitor */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('ASM_VERSION', '1.0.0'); define('ASM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ASM_PLUGIN_URL', plugin_dir_url(__FILE__)); // 包含必要文件 require_once ASM_PLUGIN_DIR . 'includes/class-api-monitor.php'; require_once ASM_PLUGIN_DIR . 'includes/class-alert-system.php'; require_once ASM_PLUGIN_DIR . 'includes/class-dashboard-widget.php'; // 初始化插件 function asm_init() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('API状态监控插件需要WordPress 5.0或更高版本。', 'api-status-monitor'); echo '</p></div>'; }); return; } // 初始化核心类 $api_monitor = new API_Monitor(); $alert_system = new Alert_System(); $dashboard_widget = new Dashboard_Widget(); // 注册激活/停用钩子 register_activation_hook(__FILE__, array($api_monitor, 'activate')); register_deactivation_hook(__FILE__, array($api_monitor, 'deactivate')); // 初始化 add_action('init', array($api_monitor, 'init')); add_action('admin_menu', array($api_monitor, 'add_admin_menu')); } add_action('plugins_loaded', 'asm_init'); 2.2 实现API监控核心类 创建includes/class-api-monitor.php文件: <?php class API_Monitor { private $monitored_apis; private $check_interval; private $options_name = 'asm_settings'; public function __construct() { $this->check_interval = 300; // 默认5分钟 $this->monitored_apis = array(); } public function init() { // 加载文本域 load_plugin_textdomain('api-status-monitor', false, dirname(plugin_basename(__FILE__)) . '/languages'); // 注册设置 add_action('admin_init', array($this, 'register_settings')); // 安排定时任务 $this->schedule_cron_jobs(); // 加载监控的API列表 $this->load_monitored_apis(); // 添加AJAX处理 add_action('wp_ajax_asm_test_api', array($this, 'ajax_test_api')); add_action('wp_ajax_asm_get_status', array($this, 'ajax_get_status')); } public function activate() { // 创建数据库表 $this->create_tables(); // 设置默认选项 $default_options = array( 'check_interval' => 300, 'enable_email_alerts' => true, 'enable_sms_alerts' => false, 'alert_email' => get_option('admin_email'), 'history_days' => 30 ); if (!get_option($this->options_name)) { add_option($this->options_name, $default_options); } // 安排初始监控任务 wp_schedule_event(time(), 'asm_five_minutes', 'asm_check_all_apis'); } public function deactivate() { // 清除定时任务 wp_clear_scheduled_hook('asm_check_all_apis'); // 清理选项(可选) // delete_option($this->options_name); } private function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'asm_api_status'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, api_name varchar(100) NOT NULL, endpoint varchar(255) NOT NULL, status_code smallint(3) NOT NULL, response_time float NOT NULL, status varchar(20) NOT NULL, checked_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, response_message text, PRIMARY KEY (id), KEY api_name (api_name), KEY checked_at (checked_at) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建报警日志表 $alert_table = $wpdb->prefix . 'asm_alerts'; $alert_sql = "CREATE TABLE IF NOT EXISTS $alert_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, api_name varchar(100) NOT NULL, alert_type varchar(50) NOT NULL, alert_message text NOT NULL, alert_level varchar(20) NOT NULL, sent_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, resolved_at datetime, PRIMARY KEY (id), KEY api_name (api_name), KEY alert_level (alert_level), KEY sent_at (sent_at) ) $charset_collate;"; dbDelta($alert_sql); } private function schedule_cron_jobs() { // 添加自定义cron间隔 add_filter('cron_schedules', array($this, 'add_cron_intervals')); // 添加监控任务 add_action('asm_check_all_apis', array($this, 'check_all_apis')); } public function add_cron_intervals($schedules) { $schedules['asm_five_minutes'] = array( 'interval' => 300, 'display' => __('每5分钟', 'api-status-monitor') ); $schedules['asm_fifteen_minutes'] = array( 'interval' => 900, 'display' => __('每15分钟', 'api-status-monitor') ); $schedules['asm_thirty_minutes'] = array( 'interval' => 1800, 'display' => __('每30分钟', 'api-status-monitor') ); return $schedules; } private function load_monitored_apis() { // 从数据库或选项加载API列表 $default_apis = array( array( 'name' => 'Stripe支付网关', 'endpoint' => 'https://api.stripe.com/v1/', 'method' => 'GET', 'expected_status' => 200, 'check_interval' => 300, 'timeout' => 10, 'headers' => array(), 'body' => '', 'validation' => 'json', 'active' => true ), array( 'name' => 'Google Maps API', 'endpoint' => 'https://maps.googleapis.com/maps/api/geocode/json?address=1600+Amphitheatre+Parkway,+Mountain+View,+CA', 'method' => 'GET', 'expected_status' => 200, 'check_interval' => 900, 'timeout' => 15, 'headers' => array(), 'body' => '', 'validation' => 'json_contains', 'validation_value' => 'OK', 'active' => true ), // 添加更多API配置 ); $saved_apis = get_option('asm_monitored_apis', array()); if (empty($saved_apis)) { $this->monitored_apis = $default_apis; update_option('asm_monitored_apis', $default_apis); } else { $this->monitored_apis = $saved_apis; } } public function check_all_apis() { foreach ($this->monitored_apis as $api) { if ($api['active']) { $this->check_single_api($api); } } } private function check_single_api($api_config) { $start_time = microtime(true); $args = array( 'method' => $api_config['method'], 'timeout' => $api_config['timeout'], 'headers' => $api_config['headers'] ); if (!empty($api_config['body'])) { $args['body'] = $api_config['body']; } $response = wp_remote_request($api_config['endpoint'], $args); $response_time = round((microtime(true) - $start_time) * 1000, 2); // 毫秒 $status_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); // 验证响应 $status = 'unknown'; $message = ''; if (is_wp_error($response)) { $status = 'error'; $message = $response->get_error_message(); } elseif ($status_code == $api_config['expected_status']) { // 进一步验证响应内容 if ($this->validate_response($api_config, $response_body)) { $status = 'healthy'; $message = __('API响应正常', 'api-status-monitor'); } else { $status = 'warning'; $message = __('API响应异常', 'api-status-monitor'); } } else { $status = 'error'; $message = sprintf(__('HTTP状态码异常: %d', 'api-status-monitor'), $status_code); } // 保存结果到数据库 $this->save_api_status($api_config['name'], $api_config['endpoint'], $status_code, $response_time, $status, $message); // 触发报警检查 if ($status != 'healthy') { do_action('asm_api_status_changed', $api_config['name'], $status, $message); } return array( 'status' => $status, 'response_time' => $response_time, 'status_code' => $status_code, 'message' => $message ); } private function validate_response($api_config, $response_body) { if (empty($api_config['validation'])) { return true; } switch ($api_config['validation']) { case 'json': json_decode($response_body); return json_last_error() === JSON_ERROR_NONE; case 'json_contains': $data = json_decode($response_body, true); if (json_last_error() !== JSON_ERROR_NONE) { return false; } // 检查响应中是否包含特定值 $needle = $api_config['validation_value']; $haystack = json_encode($data); return strpos($haystack, $needle) !== false; case 'regex': return preg_match($api_config['validation_pattern'], $response_body) === 1; default: return true; } } private function save_api_status($api_name, $endpoint, $status_code, $response_time, $status, $message) { global $wpdb; $table_name = $wpdb->prefix . 'asm_api_status'; $wpdb->insert( $table_name, array( 'api_name' => $api_name, 'endpoint' => $endpoint, 'status_code' => $status_code, 'response_time' => $response_time, 'status' => $status, 'response_message' => $message ), array('%s', '%s', '%d', '%f', '%s', '%s') ); // 清理旧记录(保留最近30天) $history_days = get_option($this->options_name)['history_days'] ?? 30; $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$history_days} days")); $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE checked_at < %s", $cutoff_date ) ); } // 更多方法将在后续部分添加... } 第三部分:实现报警系统 3.1 创建报警系统类 创建includes/class-alert-system.php文件: <?php class Alert_System { private $alert_methods; public function __construct() { $this->alert_methods = array(); // 初始化报警方法 $this->init_alert_methods(); } public function init() { // 监听API状态变化事件 add_action('asm_api_status_changed', array($this, 'handle_status_change'), 10, 3); // 注册报警方法 add_action('asm_send_email_alert', array($this, 'send_email_alert'), 10, 3); add_action('asm_send_sms_alert', array($this, 'send_sms_alert'), 10, 3); add_action('asm_send_slack_alert', array($this, 'send_slack_alert'), 10, 3); // 添加报警设置页面 add_action('admin_menu', array($this, 'add_alert_settings_page')); } private function init_alert_methods() { $settings = get_option('asm_settings', array()); $this->alert_methods = array( 'email' => array( 'enabled' => $settings['enable_email_alerts'] ?? true, 'recipients' => isset($settings['alert_email']) ? explode(',', $settings['alert_email']) : array(get_option('admin_email')) ), 'sms' => array( 'enabled' => $settings['enable_sms_alerts'] ?? false, 'provider' => $settings['sms_provider'] ?? '', 'api_key' => $settings['sms_api_key'] ?? '', 'phone_numbers' => isset($settings['sms_numbers']) ? explode(',', $settings['sms_numbers']) : ), 'slack' => array( 'enabled' => $settings['enable_slack_alerts'] ?? false, 'webhook_url' => $settings['slack_webhook'] ?? '', 'channel' => $settings['slack_channel'] ?? '#alerts' ) ); } public function handle_status_change($api_name, $status, $message) { // 检查是否需要发送报警(避免重复报警) if ($this->should_send_alert($api_name, $status)) { $alert_data = array( 'api_name' => $api_name, 'status' => $status, 'message' => $message, 'timestamp' => current_time('mysql'), 'site_url' => get_site_url() ); // 根据状态确定报警级别 $alert_level = $this->determine_alert_level($status); // 记录报警 $this->log_alert($api_name, 'status_change', $message, $alert_level); // 发送报警 $this->dispatch_alerts($alert_data, $alert_level); } } private function should_send_alert($api_name, $status) { global $wpdb; $table_name = $wpdb->prefix . 'asm_alerts'; // 检查最近是否已发送相同报警 $recent_alerts = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE api_name = %s AND alert_type = 'status_change' AND alert_level = %s AND sent_at > DATE_SUB(NOW(), INTERVAL 1 HOUR) AND resolved_at IS NULL", $api_name, $this->determine_alert_level($status) )); return $recent_alerts == 0; } private function determine_alert_level($status) { switch ($status) { case 'error': return 'critical'; case 'warning': return 'warning'; case 'unknown': return 'info'; default: return 'info'; } } private function log_alert($api_name, $alert_type, $message, $alert_level) { global $wpdb; $table_name = $wpdb->prefix . 'asm_alerts'; $wpdb->insert( $table_name, array( 'api_name' => $api_name, 'alert_type' => $alert_type, 'alert_message' => $message, 'alert_level' => $alert_level ), array('%s', '%s', '%s', '%s') ); } private function dispatch_alerts($alert_data, $alert_level) { $settings = get_option('asm_settings', array()); // 发送邮件报警 if ($this->alert_methods['email']['enabled']) { do_action('asm_send_email_alert', $alert_data, $alert_level, $this->alert_methods['email']); } // 发送短信报警 if ($this->alert_methods['sms']['enabled'] && $alert_level == 'critical') { do_action('asm_send_sms_alert', $alert_data, $alert_level, $this->alert_methods['sms']); } // 发送Slack报警 if ($this->alert_methods['slack']['enabled']) { do_action('asm_send_slack_alert', $alert_data, $alert_level, $this->alert_methods['slack']); } } public function send_email_alert($alert_data, $alert_level, $email_config) { $subject = sprintf( '[%s] API监控报警: %s - %s', get_bloginfo('name'), $alert_data['api_name'], strtoupper($alert_level) ); $message = $this->build_alert_message($alert_data, $alert_level); $headers = array('Content-Type: text/html; charset=UTF-8'); foreach ($email_config['recipients'] as $recipient) { wp_mail(trim($recipient), $subject, $message, $headers); } } public function send_sms_alert($alert_data, $alert_level, $sms_config) { // 这里实现短信发送逻辑 // 可以使用Twilio、阿里云、腾讯云等短信服务 $message = sprintf( "API报警: %s状态异常(%s)。详情请查看%s", $alert_data['api_name'], $alert_level, get_site_url() ); // 示例:使用Twilio发送短信 if ($sms_config['provider'] === 'twilio') { $this->send_twilio_sms($sms_config, $message); } // 可以添加更多短信服务商 } public function send_slack_alert($alert_data, $alert_level, $slack_config) { $message = array( 'text' => sprintf("*API监控报警* :warning:n*服务*: %sn*状态*: %sn*详情*: %sn*时间*: %s", $alert_data['api_name'], $alert_level, $alert_data['message'], $alert_data['timestamp'] ), 'channel' => $slack_config['channel'], 'username' => 'API监控机器人', 'icon_emoji' => ':robot_face:' ); if ($alert_level === 'critical') { $message['attachments'] = array(array( 'color' => 'danger', 'fields' => array( array( 'title' => '紧急程度', 'value' => '需要立即处理', 'short' => true ) ) )); } wp_remote_post($slack_config['webhook_url'], array( 'body' => json_encode($message), 'headers' => array('Content-Type' => 'application/json') )); } private function build_alert_message($alert_data, $alert_level) { $color = $alert_level === 'critical' ? '#dc3545' : ($alert_level === 'warning' ? '#ffc107' : '#17a2b8'); ob_start(); ?> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> body { font-family: Arial, sans-serif; line-height: 1.6; } .container { max-width: 600px; margin: 0 auto; padding: 20px; } .header { background-color: <?php echo $color; ?>; color: white; padding: 15px; border-radius: 5px 5px 0 0; } .content { background-color: #f8f9fa; padding: 20px; border-radius: 0 0 5px 5px; } .alert-level { display: inline-block; padding: 3px 10px; border-radius: 3px; background-color: <?php echo $color; ?>; color: white; font-weight: bold; } .details { margin-top: 20px; } .button { display: inline-block; padding: 10px 20px; background-color: <?php echo $color; ?>; color: white; text-decoration: none; border-radius: 4px; } .footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #dee2e6; color: #6c757d; font-size: 12px; } </style> </head> <body> <div class="container"> <div class="header"> <h2>API监控系统报警通知</h2> </div> <div class="content"> <p><strong>报警级别:</strong> <span class="alert-level"><?php echo strtoupper($alert_level); ?></span></p> <p><strong>API服务:</strong> <?php echo esc_html($alert_data['api_name']); ?></p> <p><strong>状态:</strong> <?php echo esc_html($alert_data['status']); ?></p> <p><strong>详细信息:</strong> <?php echo esc_html($alert_data['message']); ?></p> <p><strong>发生时间:</strong> <?php echo $alert_data['timestamp']; ?></p> <div class="details"> <p>请及时处理此问题,避免影响网站正常功能。</p> <a href="<?php echo admin_url('admin.php?page=api-status-monitor'); ?>" class="button">查看详细报告</a> </div> <div class="footer"> <p>此邮件由 <?php echo get_bloginfo('name'); ?> API监控系统自动发送</p> <p>如需修改报警设置,请访问: <?php echo admin_url('admin.php?page=api-status-monitor-settings'); ?></p> </div> </div> </div> </body> </html> <?php return ob_get_clean(); } public function add_alert_settings_page() { add_submenu_page( 'api-status-monitor', '报警设置', '报警设置', 'manage_options', 'api-status-monitor-alerts', array($this, 'render_alert_settings_page') ); } public function render_alert_settings_page() { include ASM_PLUGIN_DIR . 'admin/partials/alert-settings-page.php'; } } 第四部分:创建仪表板小工具 4.1 创建仪表板小工具类 创建includes/class-dashboard-widget.php文件: <?php class Dashboard_Widget { public function __construct() { // 初始化方法 } public function init() { // 添加仪表板小工具 add_action('wp_dashboard_setup', array($this, 'add_dashboard_widget')); // 添加AJAX处理 add_action('wp_ajax_asm_refresh_status', array($this, 'ajax_refresh_status')); // 添加前端样式和脚本 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function add_dashboard_widget() { wp_add_dashboard_widget( 'asm_api_status_widget', 'API状态监控', array($this, 'render_dashboard_widget'), null, null, 'normal', 'high' ); } public function render_dashboard_widget() { global $wpdb; $table_name = $wpdb->prefix . 'asm_api_status'; // 获取最近一次检查结果 $recent_status = $wpdb->get_results( "SELECT t1.* FROM $table_name t1 INNER JOIN ( SELECT api_name, MAX(checked_at) as latest FROM $table_name GROUP BY api_name ) t2 ON t1.api_name = t2.api_name AND t1.checked_at = t2.latest ORDER BY CASE WHEN t1.status = 'error' THEN 1 WHEN t1.status = 'warning' THEN 2 WHEN t1.status = 'unknown' THEN 3 ELSE 4 END, t1.api_name", ARRAY_A ); // 获取24小时内的统计数据 $stats = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(DISTINCT api_name) as total_apis, SUM(CASE WHEN status = 'healthy' THEN 1 ELSE 0 END) as healthy_count, SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count, SUM(CASE WHEN status = 'warning' THEN 1 ELSE 0 END) as warning_count, AVG(response_time) as avg_response_time FROM $table_name WHERE checked_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)", array() ), ARRAY_A ); include ASM_PLUGIN_DIR . 'admin/partials/dashboard-widget.php'; } public function ajax_refresh_status() { // 验证nonce check_ajax_referer('asm_refresh_nonce', 'nonce'); // 验证权限 if (!current_user_can('manage_options')) { wp_die('权限不足'); } // 执行一次API检查 $api_monitor = new API_Monitor(); $api_monitor->check_all_apis(); // 返回更新后的状态 $this->render_dashboard_widget(); wp_die(); } public function enqueue_admin_assets($hook) { if ('index.php' !== $hook) { return; } wp_enqueue_style( 'asm-dashboard-style', ASM_PLUGIN_URL . 'admin/css/admin-style.css', array(), ASM_VERSION ); wp_enqueue_script( 'asm-dashboard-script', ASM_PLUGIN_URL . 'admin/js/admin-script.js', array('jquery'), ASM_VERSION, true ); wp_localize_script('asm-dashboard-script', 'asm_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('asm_refresh_nonce') )); } } 4.2 创建仪表板小工具模板 创建admin/partials/dashboard-widget.php: <div class="asm-dashboard-widget"> <div class="asm-stats-summary"> <div class="asm-stat-card"> <span class="asm-stat-label">监控API总数</span> <span class="asm-stat-value"><?php echo $stats['total_apis'] ?? 0; ?></span> </div> <div class="asm-stat-card asm-stat-healthy"> <span class="asm-stat-label">正常</span> <span class="asm-stat-value"><?php echo $stats['healthy_count'] ?? 0; ?></span> </div> <div class="asm-stat-card asm-stat-warning"> <span class="asm-stat-label">警告</span> <span class="asm-stat-value"><?php echo $stats['warning_count'] ?? 0; ?></span> </div> <div class="asm-stat-card asm-stat-error"> <span class="asm-stat-label">异常</span> <span class="asm-stat-value"><?php echo $stats['error_count'] ?? 0; ?></span> </div> <div class="asm-stat-card"> <span class="asm-stat-label">平均响应时间</span> <span class="asm-stat-value"><?php echo round($stats['avg_response_time'] ?? 0, 2); ?>ms</span> </div> </div> <div class="asm-controls"> <button id="asm-refresh-status" class="button button-primary"> <span class="dashicons dashicons-update"></span> 立即检查 </button> <a href="<?php echo admin_url('admin.php?page=api-status-monitor'); ?>" class="button"> <span class="dashicons dashicons-dashboard"></span> 详细报告 </a> <a href="<?php echo admin_url('admin.php?page=api-status-monitor-settings'); ?>" class="button"> <span class="dashicons dashicons-admin-settings"></span> 设置 </a> </div> <div class="asm-status-list"> <h3>API状态概览</h3> <table class="widefat fixed" cellspacing="0"> <thead> <tr> <th>API名称</th> <th>状态</th> <th>响应时间</th> <th>最后检查</th> <th>操作</th> </tr> </thead> <tbody> <?php if (empty($recent_status)): ?> <tr> <td colspan="5" style="text-align: center;">暂无监控数据</td> </tr> <?php else: ?> <?php foreach ($recent_status as $status): ?> <tr class="asm-status-row asm-status-<?php echo esc_attr($status['status']); ?>"> <td><?php echo esc_html($status['api_name']); ?></td> <td> <span class="asm-status-badge asm-badge-<?php echo esc_attr($status['status']); ?>"> <?php $status_labels = array( 'healthy' => '正常', 'warning' => '警告', 'error' => '异常', 'unknown' => '未知' ); echo $status_labels[$status['status']] ?? $status['status']; ?> </span> </td> <td><?php echo esc_html($status['response_time']); ?>ms</td> <td><?php echo human_time_diff(strtotime($status['checked_at']), current_time('timestamp')) . '前'; ?></td> <td> <button class="button button-small asm-test-api" data-api="<?php echo esc_attr($status['api_name']); ?>"> 测试 </button> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <div class="asm-recent-alerts"> <h3>最近报警</h3> <?php global $wpdb; $alert_table = $wpdb->prefix . 'asm_alerts'; $recent_alerts = $wpdb->get_results( "SELECT * FROM $alert_table WHERE resolved_at IS NULL ORDER BY sent_at DESC LIMIT 5", ARRAY_A );
发表评论详细教程:为WordPress网站打造内嵌在线简易动画制作与GIF生成工具 引言:为什么网站需要内置动画制作工具? 在当今数字时代,视觉内容已成为网站吸引用户、提升参与度的关键因素。动画和GIF图像因其生动、直观的表达方式,在社交媒体、产品演示、教程说明等场景中发挥着不可替代的作用。然而,对于大多数网站运营者来说,创建这些视觉元素通常需要依赖外部工具或专业设计人员,这不仅增加了成本,也降低了内容创作的灵活性。 通过为WordPress网站开发一个内置的简易动画制作与GIF生成工具,您可以: 大幅降低视觉内容创作门槛 提高内容生产效率 保持品牌视觉一致性 增强用户互动体验 减少对外部服务的依赖 本教程将详细指导您如何通过WordPress代码二次开发,实现这一实用功能,让您的网站具备专业级的简易动画制作能力。 第一部分:准备工作与环境配置 1.1 开发环境搭建 在开始开发之前,确保您已具备以下条件: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel搭建本地WordPress环境 代码编辑器:Visual Studio Code、Sublime Text或PHPStorm WordPress版本:5.0或更高版本 基础技能:HTML、CSS、JavaScript、PHP基础知识和WordPress主题开发经验 1.2 创建开发专用子主题 为避免影响主主题功能,建议创建专用子主题: /* Theme Name: Animation Tools Child Theme Template: your-parent-theme-folder-name Version: 1.0 */ // 引入父主题样式表 add_action('wp_enqueue_scripts', 'animation_tools_enqueue_styles'); function animation_tools_enqueue_styles() { wp_enqueue_style('parent-style', get_template_directory_uri() . '/style.css'); wp_enqueue_style('child-style', get_stylesheet_directory_uri() . '/style.css', array('parent-style')); } 1.3 创建必要的目录结构 在子主题目录中创建以下结构: your-child-theme/ ├── animation-tools/ │ ├── css/ │ │ └── animation-editor.css │ ├── js/ │ │ ├── animation-editor.js │ │ └── gif-generator.js │ └── lib/ │ └── gif.js ├── templates/ │ └── animation-tool-page.php └── functions.php 第二部分:构建动画编辑器界面 2.1 创建动画工具管理页面 首先,在WordPress后台添加一个管理页面: // 在functions.php中添加 add_action('admin_menu', 'animation_tools_admin_menu'); function animation_tools_admin_menu() { add_menu_page( '动画制作工具', '动画工具', 'manage_options', 'animation-tools', 'animation_tools_admin_page', 'dashicons-video-alt3', 30 ); } function animation_tools_admin_page() { ?> <div class="wrap"> <h1>网站动画制作工具</h1> <div id="animation-tools-admin"> <p>从这里可以访问网站内置的动画编辑器。</p> <a href="<?php echo home_url('/animation-editor/'); ?>" class="button button-primary" target="_blank">打开动画编辑器</a> </div> </div> <?php } 2.2 创建前端动画编辑器页面模板 创建自定义页面模板,用于前端动画编辑: <?php /* Template Name: 动画编辑器 */ get_header(); ?> <div class="animation-editor-container"> <div class="editor-header"> <h1>简易动画制作工具</h1> <div class="editor-tabs"> <button class="tab-btn active" data-tab="canvas-editor">画布编辑</button> <button class="tab-btn" data-tab="timeline">时间轴</button> <button class="tab-btn" data-tab="export">导出选项</button> </div> </div> <div class="editor-main"> <div class="tool-panel"> <div class="tool-section"> <h3>绘图工具</h3> <div class="tool-buttons"> <button class="tool-btn active" data-tool="select">选择</button> <button class="tool-btn" data-tool="brush">画笔</button> <button class="tool-btn" data-tool="shape">形状</button> <button class="tool-btn" data-tool="text">文字</button> <button class="tool-btn" data-tool="eraser">橡皮擦</button> </div> </div> <div class="tool-section"> <h3>属性设置</h3> <div class="property-controls"> <div class="control-group"> <label>画笔大小:</label> <input type="range" id="brush-size" min="1" max="50" value="5"> <span id="brush-size-value">5px</span> </div> <div class="control-group"> <label>颜色:</label> <input type="color" id="brush-color" value="#ff0000"> </div> <div class="control-group"> <label>不透明度:</label> <input type="range" id="brush-opacity" min="0" max="100" value="100"> <span id="opacity-value">100%</span> </div> </div> </div> </div> <div class="canvas-area"> <div class="canvas-container"> <canvas id="animation-canvas" width="800" height="600"></canvas> <div class="canvas-overlay"> <div class="canvas-grid"></div> </div> </div> <div class="canvas-controls"> <button id="clear-canvas">清空画布</button> <button id="undo-action">撤销</button> <button id="redo-action">重做</button> <button id="add-frame">添加帧</button> </div> </div> <div class="timeline-panel"> <div class="frames-container"> <div class="frames-list" id="frames-list"> <!-- 帧缩略图将通过JS动态生成 --> </div> <div class="timeline-controls"> <button id="play-animation">播放</button> <input type="range" id="frame-speed" min="1" max="30" value="12"> <span>帧速率: <span id="fps-value">12</span> fps</span> </div> </div> </div> </div> <div class="export-panel"> <h3>导出选项</h3> <div class="export-options"> <div class="export-format"> <label><input type="radio" name="export-format" value="gif" checked> GIF动画</label> <label><input type="radio" name="export-format" value="apng"> APNG</label> <label><input type="radio" name="export-format" value="spritesheet"> 精灵图</label> </div> <div class="export-settings"> <div class="setting-group"> <label>循环次数:</label> <select id="loop-count"> <option value="0">无限循环</option> <option value="1">1次</option> <option value="3">3次</option> <option value="5">5次</option> </select> </div> <div class="setting-group"> <label>质量:</label> <input type="range" id="export-quality" min="1" max="100" value="80"> <span id="quality-value">80%</span> </div> </div> <div class="export-actions"> <button id="preview-export">预览</button> <button id="generate-export" class="button-primary">生成并下载</button> <button id="save-to-media">保存到媒体库</button> </div> </div> </div> </div> <?php get_footer(); ?> 第三部分:实现动画编辑器核心功能 3.1 动画编辑器JavaScript核心 创建动画编辑器的主要JavaScript功能: // animation-editor.js document.addEventListener('DOMContentLoaded', function() { // 初始化变量 const canvas = document.getElementById('animation-canvas'); const ctx = canvas.getContext('2d'); const framesList = document.getElementById('frames-list'); let currentTool = 'brush'; let brushSize = 5; let brushColor = '#ff0000'; let brushOpacity = 1.0; let frames = []; let currentFrameIndex = 0; let isDrawing = false; let lastX = 0; let lastY = 0; let undoStack = []; let redoStack = []; // 初始化第一帧 function initializeFirstFrame() { const frameData = { id: Date.now(), imageData: null, thumbnail: null, delay: 100 // 默认每帧延迟100ms }; frames.push(frameData); updateFrameDisplay(); saveFrameState(); } // 保存当前帧状态 function saveFrameState() { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); frames[currentFrameIndex].imageData = imageData; // 创建缩略图 const thumbnailCanvas = document.createElement('canvas'); thumbnailCanvas.width = 80; thumbnailCanvas.height = 60; const thumbnailCtx = thumbnailCanvas.getContext('2d'); thumbnailCtx.drawImage(canvas, 0, 0, 80, 60); frames[currentFrameIndex].thumbnail = thumbnailCanvas.toDataURL(); updateFrameDisplay(); } // 更新帧显示 function updateFrameDisplay() { framesList.innerHTML = ''; frames.forEach((frame, index) => { const frameElement = document.createElement('div'); frameElement.className = `frame-thumb ${index === currentFrameIndex ? 'active' : ''}`; frameElement.innerHTML = ` <div class="frame-number">${index + 1}</div> <img src="${frame.thumbnail || ''}" alt="帧 ${index + 1}"> <div class="frame-actions"> <button class="frame-delete" data-index="${index}">删除</button> </div> `; frameElement.addEventListener('click', () => { switchToFrame(index); }); framesList.appendChild(frameElement); }); } // 切换到指定帧 function switchToFrame(index) { saveFrameState(); // 保存当前帧 currentFrameIndex = index; // 恢复帧内容 const frame = frames[index]; if (frame.imageData) { ctx.putImageData(frame.imageData, 0, 0); } else { ctx.clearRect(0, 0, canvas.width, canvas.height); } updateFrameDisplay(); } // 添加新帧 document.getElementById('add-frame').addEventListener('click', function() { saveFrameState(); const newFrame = { id: Date.now(), imageData: null, thumbnail: null, delay: 100 }; frames.splice(currentFrameIndex + 1, 0, newFrame); currentFrameIndex++; // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); updateFrameDisplay(); saveFrameState(); }); // 绘图功能 function startDrawing(e) { if (currentTool === 'select') return; isDrawing = true; [lastX, lastY] = getMousePos(canvas, e); // 开始新路径 if (currentTool === 'brush' || currentTool === 'eraser') { ctx.beginPath(); ctx.moveTo(lastX, lastY); } } function draw(e) { if (!isDrawing) return; const [x, y] = getMousePos(canvas, e); switch(currentTool) { case 'brush': ctx.lineTo(x, y); ctx.strokeStyle = brushColor; ctx.lineWidth = brushSize; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.globalAlpha = brushOpacity; ctx.stroke(); break; case 'eraser': ctx.lineTo(x, y); ctx.strokeStyle = '#ffffff'; ctx.lineWidth = brushSize; ctx.lineCap = 'round'; ctx.stroke(); break; case 'shape': // 绘制形状逻辑 break; case 'text': // 文本输入逻辑 break; } [lastX, lastY] = [x, y]; } function stopDrawing() { if (!isDrawing) return; isDrawing = false; ctx.closePath(); saveFrameState(); } // 获取鼠标位置 function getMousePos(canvas, evt) { const rect = canvas.getBoundingClientRect(); return [ evt.clientX - rect.left, evt.clientY - rect.top ]; } // 工具选择 document.querySelectorAll('.tool-btn').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); currentTool = this.dataset.tool; }); }); // 属性控制 document.getElementById('brush-size').addEventListener('input', function() { brushSize = this.value; document.getElementById('brush-size-value').textContent = brushSize + 'px'; }); document.getElementById('brush-color').addEventListener('input', function() { brushColor = this.value; }); document.getElementById('brush-opacity').addEventListener('input', function() { brushOpacity = this.value / 100; document.getElementById('opacity-value').textContent = this.value + '%'; }); // 画布事件监听 canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseout', stopDrawing); // 清空画布 document.getElementById('clear-canvas').addEventListener('click', function() { if (confirm('确定要清空当前帧吗?')) { ctx.clearRect(0, 0, canvas.width, canvas.height); saveFrameState(); } }); // 初始化 initializeFirstFrame(); }); 3.2 添加动画播放功能 // 在animation-editor.js中添加动画播放功能 let animationInterval = null; let isPlaying = false; function playAnimation() { if (frames.length < 2) { alert('至少需要两帧才能播放动画'); return; } if (isPlaying) { stopAnimation(); return; } isPlaying = true; document.getElementById('play-animation').textContent = '停止'; let frameIndex = 0; const fps = parseInt(document.getElementById('frame-speed').value); const delay = 1000 / fps; animationInterval = setInterval(() => { switchToFrame(frameIndex); frameIndex = (frameIndex + 1) % frames.length; }, delay); } function stopAnimation() { isPlaying = false; document.getElementById('play-animation').textContent = '播放'; clearInterval(animationInterval); } // 播放按钮事件 document.getElementById('play-animation').addEventListener('click', playAnimation); // 帧速率控制 document.getElementById('frame-speed').addEventListener('input', function() { document.getElementById('fps-value').textContent = this.value; // 如果正在播放,重新开始以应用新速度 if (isPlaying) { stopAnimation(); playAnimation(); } }); 第四部分:实现GIF生成与导出功能 4.1 集成GIF.js库 首先,下载并引入GIF.js库: // 在functions.php中注册GIF.js add_action('wp_enqueue_scripts', 'enqueue_animation_tools_scripts'); function enqueue_animation_tools_scripts() { if (is_page_template('animation-editor.php')) { wp_enqueue_script('gif-js', get_stylesheet_directory_uri() . '/animation-tools/lib/gif.js', array(), '1.0.0', true); wp_enqueue_script('animation-editor', get_stylesheet_directory_uri() . '/animation-tools/js/animation-editor.js', array('jquery', 'gif-js'), '1.0.0', true); wp_enqueue_script('gif-generator', get_stylesheet_directory_uri() . '/animation-tools/js/gif-generator.js', array('animation-editor'), '1.0.0', true); wp_enqueue_style('animation-editor-style', get_stylesheet_directory_uri() . '/animation-tools/css/animation-editor.css', array(), '1.0.0'); } } 4.2 创建GIF生成器 // gif-generator.js class GIFGenerator { constructor(options = {}) { this.options = { quality: options.quality || 10, width: options.width || 800, height: options.height || 600, workerScript: options.workerScript || '/wp-content/themes/your-child-theme/animation-tools/lib/gif.worker.js' }; this.gif = new GIF({ workers: 2, quality: this.options.quality, width: this.options.width, height: this.options.height, workerScript: this.options.workerScript }); this.frames = []; this.onProgress = null; this.onFinished = null; } addFrame(canvas, delay) { this.gif.addFrame(canvas, { delay: delay || 100, copy: true }); } setOptions(options) { if (options.quality) this.gif.setOption('quality', options.quality); if (options.repeat !== undefined) this.gif.setOption('repeat', options.repeat); } generate() { return new Promise((resolve, reject) => { this.gif.on('progress', (progress) => { if (this.onProgress) { this.onProgress(progress); } }); this.gif.on('finished', (blob) => { if (this.onFinished) { this.onFinished(blob); } resolve(blob); }); this.gif.render(); }); } abort() { this.gif.abort(); } } // 导出功能实现document.addEventListener('DOMContentLoaded', function() { const generateExportBtn = document.getElementById('generate-export'); const previewExportBtn = document.getElementById('preview-export'); const saveToMediaBtn = document.getElementById('save-to-media'); const exportQuality = document.getElementById('export-quality'); const loopCount = document.getElementById('loop-count'); let currentGIFBlob = null; // 更新质量显示 exportQuality.addEventListener('input', function() { document.getElementById('quality-value').textContent = this.value + '%'; }); // 生成GIF async function generateGIF() { if (frames.length === 0) { alert('请先创建至少一帧动画'); return; } generateExportBtn.disabled = true; generateExportBtn.textContent = '生成中...'; try { // 创建临时画布用于生成GIF const tempCanvas = document.createElement('canvas'); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; const tempCtx = tempCanvas.getContext('2d'); // 初始化GIF生成器 const gifGenerator = new GIFGenerator({ quality: parseInt(exportQuality.value), width: canvas.width, height: canvas.height }); // 设置循环次数 const repeat = parseInt(loopCount.value); gifGenerator.setOptions({ repeat: repeat === 0 ? 0 : repeat - 1 }); // 添加进度监听 gifGenerator.onProgress = (progress) => { console.log(`生成进度: ${Math.round(progress * 100)}%`); }; // 添加所有帧 for (let i = 0; i < frames.length; i++) { const frame = frames[i]; if (frame.imageData) { tempCtx.putImageData(frame.imageData, 0, 0); gifGenerator.addFrame(tempCanvas, frame.delay); } } // 生成GIF currentGIFBlob = await gifGenerator.generate(); // 创建下载链接 const url = URL.createObjectURL(currentGIFBlob); const a = document.createElement('a'); a.href = url; a.download = `animation-${Date.now()}.gif`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); alert('GIF生成完成并已开始下载!'); } catch (error) { console.error('GIF生成失败:', error); alert('GIF生成失败,请重试'); } finally { generateExportBtn.disabled = false; generateExportBtn.textContent = '生成并下载'; } } // 预览GIF async function previewGIF() { if (frames.length === 0) { alert('请先创建至少一帧动画'); return; } previewExportBtn.disabled = true; previewExportBtn.textContent = '预览生成中...'; try { // 创建临时画布 const tempCanvas = document.createElement('canvas'); tempCanvas.width = 400; // 预览尺寸较小 tempCanvas.height = 300; const tempCtx = tempCanvas.getContext('2d'); // 创建预览GIF生成器 const previewGenerator = new GIFGenerator({ quality: 20, // 预览质量较低 width: 400, height: 300 }); // 添加所有帧(缩放) for (let i = 0; i < frames.length; i++) { const frame = frames[i]; if (frame.imageData) { // 创建临时画布绘制并缩放 const frameCanvas = document.createElement('canvas'); frameCanvas.width = canvas.width; frameCanvas.height = canvas.height; const frameCtx = frameCanvas.getContext('2d'); frameCtx.putImageData(frame.imageData, 0, 0); // 缩放到预览尺寸 tempCtx.clearRect(0, 0, 400, 300); tempCtx.drawImage(frameCanvas, 0, 0, 400, 300); previewGenerator.addFrame(tempCanvas, frame.delay); } } // 生成预览GIF const previewBlob = await previewGenerator.generate(); const previewUrl = URL.createObjectURL(previewBlob); // 显示预览 showPreviewModal(previewUrl); } catch (error) { console.error('预览生成失败:', error); alert('预览生成失败'); } finally { previewExportBtn.disabled = false; previewExportBtn.textContent = '预览'; } } // 显示预览模态框 function showPreviewModal(gifUrl) { // 移除现有模态框 const existingModal = document.querySelector('.preview-modal'); if (existingModal) { existingModal.remove(); } // 创建模态框 const modal = document.createElement('div'); modal.className = 'preview-modal'; modal.innerHTML = ` <div class="preview-modal-content"> <div class="preview-modal-header"> <h3>GIF预览</h3> <button class="close-preview">×</button> </div> <div class="preview-modal-body"> <img src="${gifUrl}" alt="GIF预览" class="preview-gif"> <div class="preview-info"> <p>尺寸: 400x300 (预览)</p> <p>帧数: ${frames.length}</p> <p>文件大小: ${Math.round(currentGIFBlob?.size / 1024) || '未知'} KB</p> </div> </div> <div class="preview-modal-footer"> <button id="download-preview">下载预览</button> <button id="use-full-quality">使用高质量生成</button> </div> </div> `; document.body.appendChild(modal); // 关闭按钮事件 modal.querySelector('.close-preview').addEventListener('click', function() { modal.remove(); URL.revokeObjectURL(gifUrl); }); // 点击外部关闭 modal.addEventListener('click', function(e) { if (e.target === modal) { modal.remove(); URL.revokeObjectURL(gifUrl); } }); // 下载预览 modal.querySelector('#download-preview').addEventListener('click', function() { const a = document.createElement('a'); a.href = gifUrl; a.download = `preview-${Date.now()}.gif`; a.click(); }); // 使用高质量生成 modal.querySelector('#use-full-quality').addEventListener('click', function() { modal.remove(); URL.revokeObjectURL(gifUrl); generateGIF(); }); } // 保存到媒体库 async function saveToMediaLibrary() { if (!currentGIFBlob) { alert('请先生成GIF'); return; } saveToMediaBtn.disabled = true; saveToMediaBtn.textContent = '上传中...'; try { // 创建FormData const formData = new FormData(); formData.append('action', 'save_animation_to_media'); formData.append('security', animationToolsAjax.nonce); formData.append('gif_data', currentGIFBlob, `animation-${Date.now()}.gif`); formData.append('title', `动画作品 ${new Date().toLocaleDateString()}`); // 发送AJAX请求 const response = await fetch(animationToolsAjax.ajax_url, { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { alert(`动画已保存到媒体库!n文件ID: ${result.data.attachment_id}n查看: ${result.data.edit_link}`); } else { throw new Error(result.data || '上传失败'); } } catch (error) { console.error('保存到媒体库失败:', error); alert('保存失败: ' + error.message); } finally { saveToMediaBtn.disabled = false; saveToMediaBtn.textContent = '保存到媒体库'; } } // 绑定事件 generateExportBtn.addEventListener('click', generateGIF); previewExportBtn.addEventListener('click', previewGIF); saveToMediaBtn.addEventListener('click', saveToMediaLibrary); }); ## 第五部分:后端处理与媒体库集成 ### 5.1 创建AJAX处理函数 // 在functions.php中添加AJAX处理add_action('wp_ajax_save_animation_to_media', 'handle_save_animation_to_media');add_action('wp_ajax_nopriv_save_animation_to_media', 'handle_save_animation_to_media'); function handle_save_animation_to_media() { // 安全检查 check_ajax_referer('animation_tools_nonce', 'security'); // 验证用户权限 if (!current_user_can('upload_files')) { wp_die('权限不足'); } // 检查文件上传 if (!isset($_FILES['gif_data']) || $_FILES['gif_data']['error'] !== UPLOAD_ERR_OK) { wp_send_json_error('文件上传失败'); } $file = $_FILES['gif_data']; // 验证文件类型 $allowed_types = array('image/gif'); $filetype = wp_check_filetype(basename($file['name'])); if (!in_array($filetype['type'], $allowed_types)) { wp_send_json_error('仅支持GIF格式'); } // 准备上传文件 $upload_overrides = array( 'test_form' => false, 'mimes' => array('gif' => 'image/gif') ); // 上传文件 $upload = wp_handle_upload($file, $upload_overrides); if (isset($upload['error'])) { wp_send_json_error($upload['error']); } // 准备附件数据 $attachment = array( 'post_mime_type' => $filetype['type'], 'post_title' => sanitize_text_field($_POST['title'] ?? '动画作品'), 'post_content' => '', 'post_status' => 'inherit', 'post_author' => get_current_user_id() ); // 插入附件到媒体库 $attachment_id = wp_insert_attachment($attachment, $upload['file']); if (is_wp_error($attachment_id)) { wp_send_json_error('插入媒体库失败'); } // 生成附件元数据 require_once(ABSPATH . 'wp-admin/includes/image.php'); $attachment_data = wp_generate_attachment_metadata($attachment_id, $upload['file']); wp_update_attachment_metadata($attachment_id, $attachment_data); // 返回成功响应 wp_send_json_success(array( 'attachment_id' => $attachment_id, 'url' => wp_get_attachment_url($attachment_id), 'edit_link' => admin_url('post.php?post=' . $attachment_id . '&action=edit') )); } // 注册AJAX脚本add_action('wp_enqueue_scripts', 'register_animation_tools_ajax');function register_animation_tools_ajax() { if (is_page_template('animation-editor.php')) { wp_localize_script('gif-generator', 'animationToolsAjax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('animation_tools_nonce') )); } } ### 5.2 创建短代码功能 // 添加短代码,允许在文章/页面中插入动画工具add_shortcode('animation_tool', 'animation_tool_shortcode');function animation_tool_shortcode($atts) { $atts = shortcode_atts(array( 'width' => '100%', 'height' => '600px', 'mode' => 'editor' // editor, preview, simple ), $atts); ob_start(); if ($atts['mode'] === 'simple') { // 简化版动画工具 ?> <div class="simple-animation-tool" style="width: <?php echo esc_attr($atts['width']); ?>; height: <?php echo esc_attr($atts['height']); ?>;"> <div class="simple-tool-header"> <h4>简易动画制作</h4> </div> <div class="simple-canvas-container"> <canvas class="simple-animation-canvas"></canvas> </div> <div class="simple-controls"> <button class="simple-draw-btn">绘制</button> <button class="simple-clear-btn">清空</button> <button class="simple-save-btn">保存为GIF</button> </div> </div> <?php } else { // 完整版工具链接 $editor_url = home_url('/animation-editor/'); ?> <div class="animation-tool-link"> <h3>动画制作工具</h3> <p>使用我们的内置工具创建自定义动画和GIF</p> <a href="<?php echo esc_url($editor_url); ?>" class="button button-primary"> 打开动画编辑器 </a> </div> <?php } return ob_get_clean(); } ## 第六部分:样式设计与优化 ### 6.1 基础样式设计 / animation-editor.css /.animation-editor-container { max-width: 1400px; margin: 20px auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; } .editor-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; } .editor-header h1 { margin: 0 0 20px 0; font-size: 28px; } .editor-tabs { display: flex; gap: 10px; } .tab-btn { background: rgba(255,255,255,0.2); border: none; color: white; padding: 10px 20px; border-radius: 4px; cursor: pointer; transition: background 0.3s; } .tab-btn.active { background: rgba(255,255,255,0.4); } .tab-btn:hover { background: rgba(255,255,255,0.3); } .editor-main { display: grid; grid-template-columns: 250px 1fr 300px; min-height: 600px; } .tool-panel { background: #f8f9fa; border-right: 1px solid #dee2e6; padding: 20px; } .tool-section { margin-bottom: 30px; } .tool-section h3 { margin-top: 0; color: #495057; font-size: 16px; border-bottom: 2px solid #667eea; padding-bottom: 5px; } .tool-buttons { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } .tool-btn { background: white; border: 2px solid #dee2e6; padding: 10px; border-radius: 4px; cursor: pointer; transition: all 0.3s; } .tool-btn.active { border-color: #667eea; background: #667eea; color: white; } .tool-btn:hover { border-color: #495057; } .property-controls { display: flex; flex-direction: column; gap: 15px; } .control-group { display: flex; flex-direction: column; gap: 5px; } .control-group label { font-size: 14px; color: #6c757d; } .control-group input[type="range"] { width: 100%; } .canvas-area { padding: 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; } .canvas-container { position: relative; border: 2px dashed #dee2e6; border-radius: 4px; margin-bottom: 20px; } animation-canvas { display: block; background: white; cursor: crosshair; } .canvas-overlay { position: absolute; top: 0; left: 0; right:
发表评论WordPress高级教程:开发集成在线考试测评与自动组卷评分系统 引言:WordPress作为企业级应用开发平台的潜力 WordPress作为全球最流行的内容管理系统,早已超越了简单的博客平台范畴。凭借其强大的插件架构、丰富的API接口和庞大的开发者社区,WordPress已经演变为一个功能完善的企业级应用开发平台。在许多人的印象中,WordPress可能仍是一个"博客工具",但实际上,全球超过43%的网站都运行在WordPress上,其中不乏复杂的企业级应用。 本教程将深入探讨如何通过WordPress的代码二次开发,实现一个功能完整的在线考试测评与自动组卷评分系统。这个系统不仅能够满足教育机构、企业培训部门的在线测评需求,还能展示WordPress作为应用开发平台的强大扩展能力。我们将从系统设计、数据库结构、核心功能实现到前端展示,全方位解析开发过程。 第一章:系统架构设计与技术选型 1.1 需求分析与功能规划 在线考试测评系统需要满足以下核心功能: 试题库管理(支持多种题型:单选、多选、判断、填空、简答等) 智能组卷策略(随机组卷、按知识点组卷、难度系数组卷) 在线考试与计时功能 自动评分与人工阅卷结合 成绩统计与分析报表 用户权限与考试管理 1.2 WordPress作为开发平台的优势 选择WordPress作为开发基础有以下几个显著优势: 用户管理系统完善:WordPress自带的用户角色和权限管理系统可以快速扩展为考试系统的用户体系 主题模板机制:利用WordPress主题系统可以快速构建一致的前端界面 插件架构:通过自定义插件的方式实现功能模块,便于维护和升级 丰富的API:REST API、AJAX、短代码等机制便于前后端交互 安全机制成熟:WordPress经过多年发展,拥有完善的安全防护机制 SEO友好:天生具备良好的搜索引擎优化基础 1.3 技术栈选择 后端:PHP 7.4+,WordPress 5.6+,MySQL 5.6+ 前端:HTML5,CSS3,JavaScript (ES6+),jQuery(兼容性考虑) 关键WordPress特性:自定义文章类型(CPT)、自定义分类法、元数据、短代码、REST API 辅助工具:Chart.js(数据可视化),Select2(下拉框增强),Bootstrap 5(响应式布局) 第二章:数据库设计与数据模型 2.1 核心数据表设计 虽然WordPress本身使用wp_posts和wp_postmeta作为主要数据存储结构,但对于复杂的考试系统,我们需要创建专门的数据表来保证性能和数据一致性。 -- 试题表扩展(基于wp_posts的自定义文章类型) -- 使用post_type = 'exam_question'标识试题 -- 自定义试题元数据表 CREATE TABLE wp_exam_question_meta ( meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, question_id bigint(20) unsigned NOT NULL, meta_key varchar(255) DEFAULT NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY question_id (question_id), KEY meta_key (meta_key(191)) ); -- 试卷表 CREATE TABLE wp_exam_papers ( paper_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, paper_title varchar(255) NOT NULL, paper_description text, paper_type varchar(50) DEFAULT 'random', -- random/fixed/manual total_score int(11) DEFAULT 100, time_limit int(11) DEFAULT 60, -- 分钟 created_by bigint(20) unsigned NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (paper_id) ); -- 试卷-试题关联表 CREATE TABLE wp_exam_paper_questions ( relation_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, paper_id bigint(20) unsigned NOT NULL, question_id bigint(20) unsigned NOT NULL, question_order int(11) DEFAULT 0, question_score decimal(5,2) DEFAULT 0, PRIMARY KEY (relation_id), KEY paper_id (paper_id), KEY question_id (question_id) ); -- 考试记录表 CREATE TABLE wp_exam_records ( record_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NOT NULL, paper_id bigint(20) unsigned NOT NULL, start_time datetime DEFAULT NULL, end_time datetime DEFAULT NULL, submit_time datetime DEFAULT NULL, total_score decimal(5,2) DEFAULT 0, auto_score decimal(5,2) DEFAULT 0, manual_score decimal(5,2) DEFAULT 0, status varchar(20) DEFAULT 'in_progress', -- in_progress/completed/reviewing ip_address varchar(45) DEFAULT NULL, user_answers longtext, -- 存储用户答案的JSON PRIMARY KEY (record_id), KEY user_id (user_id), KEY paper_id (paper_id), KEY status (status) ); 2.2 利用WordPress自定义文章类型管理试题 试题作为系统的核心数据,我们可以使用WordPress的自定义文章类型(CPT)来管理: // 注册试题自定义文章类型 function register_exam_question_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' => false, 'publicly_queryable' => false, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => false, 'capability_type' => 'post', 'has_archive' => false, 'hierarchical' => false, 'menu_position' => 30, 'menu_icon' => 'dashicons-welcome-learn-more', 'supports' => array('title', 'editor', 'author'), 'show_in_rest' => true, // 启用REST API支持 ); register_post_type('exam_question', $args); } add_action('init', 'register_exam_question_post_type'); 2.3 试题分类与标签系统 利用WordPress的自定义分类法为试题添加分类和标签: // 注册试题分类法 function register_exam_taxonomies() { // 试题分类(如:数学、语文、英语) register_taxonomy( 'question_category', 'exam_question', array( 'labels' => array( 'name' => '试题分类', 'singular_name' => '试题分类', 'search_items' => '搜索分类', 'all_items' => '全部分类', 'edit_item' => '编辑分类', 'update_item' => '更新分类', 'add_new_item' => '添加新分类', 'new_item_name' => '新分类名称', 'menu_name' => '试题分类' ), 'hierarchical' => true, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => false, 'show_in_rest' => true, ) ); // 试题难度标签 register_taxonomy( 'question_difficulty', 'exam_question', array( 'labels' => array( 'name' => '难度级别', 'singular_name' => '难度级别', 'search_items' => '搜索难度', 'all_items' => '全部难度', 'edit_item' => '编辑难度', 'update_item' => '更新难度', 'add_new_item' => '添加新难度', 'new_item_name' => '新难度名称', 'menu_name' => '难度级别' ), 'hierarchical' => false, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => false, 'show_in_rest' => true, ) ); } add_action('init', 'register_exam_taxonomies'); 第三章:试题管理与组卷策略实现 3.1 试题录入与管理界面 在WordPress后台创建试题管理界面,支持多种题型: // 为试题添加元数据框 function add_exam_question_meta_boxes() { add_meta_box( 'question-type-meta', '试题类型与选项', 'render_question_type_meta_box', 'exam_question', 'normal', 'high' ); add_meta_box( 'question-answer-meta', '正确答案与解析', 'render_question_answer_meta_box', 'exam_question', 'normal', 'high' ); } add_action('add_meta_boxes', 'add_exam_question_meta_boxes'); // 渲染试题类型元数据框 function render_question_type_meta_box($post) { wp_nonce_field('save_question_meta', 'question_meta_nonce'); $question_type = get_post_meta($post->ID, '_question_type', true); $question_options = get_post_meta($post->ID, '_question_options', true); // 解析选项(如果是选择题) $options = !empty($question_options) ? json_decode($question_options, true) : array(); ?> <div class="question-meta-container"> <div class="form-group"> <label for="question_type">试题类型:</label> <select name="question_type" id="question_type" class="widefat"> <option value="single_choice" <?php selected($question_type, 'single_choice'); ?>>单选题</option> <option value="multiple_choice" <?php selected($question_type, 'multiple_choice'); ?>>多选题</option> <option value="true_false" <?php selected($question_type, 'true_false'); ?>>判断题</option> <option value="fill_blank" <?php selected($question_type, 'fill_blank'); ?>>填空题</option> <option value="short_answer" <?php selected($question_type, 'short_answer'); ?>>简答题</option> <option value="essay" <?php selected($question_type, 'essay'); ?>>论述题</option> </select> </div> <div id="choice-options-container" style="display: <?php echo in_array($question_type, array('single_choice', 'multiple_choice')) ? 'block' : 'none'; ?>;"> <h4>选项设置</h4> <div id="choice-options-list"> <?php if (!empty($options)): ?> <?php foreach ($options as $key => $option): ?> <div class="option-item"> <input type="text" name="question_options[]" value="<?php echo esc_attr($option); ?>" class="widefat" placeholder="选项内容"> <button type="button" class="button remove-option">删除</button> </div> <?php endforeach; ?> <?php else: ?> <div class="option-item"> <input type="text" name="question_options[]" value="" class="widefat" placeholder="选项内容"> <button type="button" class="button remove-option">删除</button> </div> <div class="option-item"> <input type="text" name="question_options[]" value="" class="widefat" placeholder="选项内容"> <button type="button" class="button remove-option">删除</button> </div> <?php endif; ?> </div> <button type="button" id="add-option" class="button">添加选项</button> </div> <div class="form-group"> <label for="question_score">默认分值:</label> <input type="number" name="question_score" id="question_score" value="<?php echo esc_attr(get_post_meta($post->ID, '_question_score', true) ?: 5); ?>" min="0" step="0.5" class="small-text"> 分 </div> </div> <script> jQuery(document).ready(function($) { // 切换试题类型时显示/隐藏选项设置 $('#question_type').change(function() { var type = $(this).val(); if (type === 'single_choice' || type === 'multiple_choice') { $('#choice-options-container').show(); } else { $('#choice-options-container').hide(); } }); // 添加选项 $('#add-option').click(function() { var newOption = '<div class="option-item">' + '<input type="text" name="question_options[]" value="" class="widefat" placeholder="选项内容">' + '<button type="button" class="button remove-option">删除</button>' + '</div>'; $('#choice-options-list').append(newOption); }); // 删除选项 $(document).on('click', '.remove-option', function() { if ($('#choice-options-list .option-item').length > 1) { $(this).closest('.option-item').remove(); } else { alert('至少需要保留一个选项'); } }); }); </script> <?php } 3.2 智能组卷算法实现 智能组卷是考试系统的核心功能,这里实现一个基于分类和难度的随机组卷算法: class ExamPaperGenerator { /** * 生成随机试卷 * * @param array $params 组卷参数 * @return int|false 试卷ID或false */ public static function generateRandomPaper($params) { global $wpdb; $defaults = array( 'title' => '随机试卷', 'description' => '', 'total_questions' => 50, 'categories' => array(), 'difficulties' => array('easy', 'medium', 'hard'), 'difficulty_distribution' => array('easy' => 0.3, 'medium' => 0.5, 'hard' => 0.2), 'question_types' => array('single_choice', 'multiple_choice', 'true_false'), 'time_limit' => 60, 'created_by' => get_current_user_id(), ); $params = wp_parse_args($params, $defaults); // 开始事务 $wpdb->query('START TRANSACTION'); try { // 创建试卷记录 $paper_data = array( 'paper_title' => sanitize_text_field($params['title']), 'paper_description' => sanitize_textarea_field($params['description']), 'paper_type' => 'random', 'total_score' => $params['total_questions'] * 2, // 假设每题2分 'time_limit' => intval($params['time_limit']), 'created_by' => intval($params['created_by']), 'created_at' => current_time('mysql'), ); $wpdb->insert($wpdb->prefix . 'exam_papers', $paper_data); $paper_id = $wpdb->insert_id; if (!$paper_id) { throw new Exception('创建试卷失败'); } // 获取符合条件的试题 $questions = self::getQuestionsByCriteria($params); if (count($questions) < $params['total_questions']) { throw new Exception('试题数量不足,无法生成试卷'); } // 随机选择试题 shuffle($questions); $selected_questions = array_slice($questions, 0, $params['total_questions']); // 关联试题到试卷 $order = 1; foreach ($selected_questions as $question) { $relation_data = array( 'paper_id' => $paper_id, 'question_id' => $question->ID, 'question_order' => $order, 'question_score' => get_post_meta($question->ID, '_question_score', true) ?: 2, ); $wpdb->insert($wpdb->prefix . 'exam_paper_questions', $relation_data); $order++; } // 提交事务 $wpdb->query('COMMIT'); return $paper_id; } catch (Exception $e) { // 回滚事务 $wpdb->query('ROLLBACK'); error_log('生成试卷失败: ' . $e->getMessage()); return false; } } /** * 根据条件获取试题 */ private static function getQuestionsByCriteria($params) { $args = array( 'post_type' => 'exam_question', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', ); // 添加分类筛选 if (!empty($params['categories'])) { $args['tax_query'][] = array( 'taxonomy' => 'question_category', 'field' => 'term_id', 'terms' => $params['categories'], 'operator' => 'IN', ); } // 添加难度筛选 if (!empty($params['difficulties'])) { $args['tax_query'][] = array( 'taxonomy' => 'question_difficulty', 'field' => 'slug', 'terms' => $params['difficulties'], 'operator' => 'IN', ); } // 添加题型筛选 if (!empty($params['question_types'])) { $args['meta_query'][] = array( 'key' => '_question_type', 'value' => $params['question_types'], 'compare' => 'IN', ); } // 确保tax_query关系正确 if (isset($args['tax_query']) && count($args['tax_query']) > 1) { $args['tax_query']['relation'] = 'AND'; } $query = new WP_Query($args); return $query->posts; } /** * 按知识点比例组卷 */ public static function generatePaperByKnowledgePoints($params) { // 实现按知识点比例分配试题的逻辑 // 这里可以扩展为更复杂的组卷策略 } } ### 3.3 试卷管理后台界面 创建试卷管理后台界面,支持手动组卷和自动组卷: // 添加试卷管理菜单function add_exam_paper_admin_menu() { add_menu_page( '试卷管理', '考试系统', 'manage_options', 'exam-system', 'render_exam_dashboard', 'dashicons-clipboard', 30 ); add_submenu_page( 'exam-system', '试卷管理', '试卷管理', 'manage_options', 'exam-papers', 'render_exam_papers_page' ); add_submenu_page( 'exam-system', '组卷工具', '智能组卷', 'manage_options', 'exam-generator', 'render_paper_generator_page' ); add_submenu_page( 'exam-system', '考试记录', '考试记录', 'manage_options', 'exam-records', 'render_exam_records_page' ); add_submenu_page( 'exam-system', '成绩统计', '成绩统计', 'manage_options', 'exam-statistics', 'render_exam_statistics_page' ); }add_action('admin_menu', 'add_exam_paper_admin_menu'); // 渲染智能组卷页面function render_paper_generator_page() { // 获取所有试题分类 $categories = get_terms(array( 'taxonomy' => 'question_category', 'hide_empty' => false, )); // 获取所有难度级别 $difficulties = get_terms(array( 'taxonomy' => 'question_difficulty', 'hide_empty' => false, )); ?> <div class="wrap"> <h1>智能组卷工具</h1> <div class="card"> <h2>随机组卷</h2> <form id="random-paper-form" method="post"> <?php wp_nonce_field('generate_random_paper', 'paper_nonce'); ?> <table class="form-table"> <tr> <th scope="row"><label for="paper_title">试卷标题</label></th> <td> <input type="text" name="paper_title" id="paper_title" class="regular-text" value="随机试卷 <?php echo date('Y-m-d'); ?>" required> </td> </tr> <tr> <th scope="row"><label for="paper_description">试卷描述</label></th> <td> <textarea name="paper_description" id="paper_description" class="large-text" rows="3"></textarea> </td> </tr> <tr> <th scope="row"><label for="total_questions">试题数量</label></th> <td> <input type="number" name="total_questions" id="total_questions" min="10" max="200" value="50" required> </td> </tr> <tr> <th scope="row"><label>试题分类</label></th> <td> <div style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;"> <?php foreach ($categories as $category): ?> <label style="display: block; margin-bottom: 5px;"> <input type="checkbox" name="categories[]" value="<?php echo $category->term_id; ?>"> <?php echo $category->name; ?> (<?php echo $category->count; ?>) </label> <?php endforeach; ?> </div> <p class="description">不选择任何分类表示从所有分类中选题</p> </td> </tr> <tr> <th scope="row"><label>难度分布</label></th> <td> <div id="difficulty-distribution"> <?php foreach ($difficulties as $difficulty): ?> <div style="margin-bottom: 10px;"> <label style="display: inline-block; width: 100px;"> <?php echo $difficulty->name; ?>: </label> <input type="number" name="difficulty_<?php echo $difficulty->slug; ?>" min="0" max="100" value="30" style="width: 60px;"> % </div> <?php endforeach; ?> </div> </td> </tr> <tr> <th scope="row"><label>试题类型</label></th> <td> <label style="margin-right: 15px;"> <input type="checkbox" name="question_types[]" value="single_choice" checked> 单选题 </label> <label style="margin-right: 15px;"> <input type="checkbox" name="question_types[]" value="multiple_choice" checked> 多选题 </label> <label style="margin-right: 15px;"> <input type="checkbox" name="question_types[]" value="true_false"> 判断题 </label> <label style="margin-right: 15px;"> <input type="checkbox" name="question_types[]" value="fill_blank"> 填空题 </label> </td> </tr> <tr> <th scope="row"><label for="time_limit">考试时间(分钟)</label></th> <td> <input type="number" name="time_limit" id="time_limit" min="10" max="300" value="60" required> </td> </tr> </table> <p class="submit"> <button type="submit" name="generate_paper" class="button button-primary">生成试卷</button> </p> </form> </div> <?php // 处理表单提交 if (isset($_POST['generate_paper']) && check_admin_referer('generate_random_paper', 'paper_nonce')) { $params = array( 'title' => sanitize_text_field($_POST['paper_title']), 'description' => sanitize_textarea_field($_POST['paper_description']), 'total_questions' => intval($_POST['total_questions']), 'categories' => isset($_POST['categories']) ? array_map('intval', $_POST['categories']) : array(), 'time_limit' => intval($_POST['time_limit']), 'question_types' => isset($_POST['question_types']) ? array_map('sanitize_text_field', $_POST['question_types']) : array(), ); $paper_id = ExamPaperGenerator::generateRandomPaper($params); if ($paper_id) { echo '<div class="notice notice-success"><p>试卷生成成功!<a href="' . admin_url('admin.php?page=exam-papers&action=edit&paper_id=' . $paper_id) . '">查看试卷</a></p></div>'; } else { echo '<div class="notice notice-error"><p>试卷生成失败,请检查试题库是否足够</p></div>'; } } ?> </div> <script> jQuery(document).ready(function($) { // 验证难度分布总和为100% $('#random-paper-form').submit(function(e) { var total = 0; $('#difficulty-distribution input[type="number"]').each(function() { total += parseInt($(this).val()) || 0; }); if (total !== 100) { alert('难度分布总和必须为100%,当前总和为:' + total + '%'); e.preventDefault(); return false; } }); }); </script> <?php } ## 第四章:在线考试功能实现 ### 4.1 考试前端界面与交互 创建考试前端界面,使用AJAX实现无刷新答题: // 创建考试短代码function exam_paper_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'preview' => false, ), $atts, 'exam_paper'); $paper_id = intval($atts['id']); if (!$paper_id) { return '<div class="exam-error">请指定试卷ID</div>'; } // 检查用户权限 if (!is_user_logged_in()) { return '<div class="exam-error">请先登录后再参加考试</div>'; } // 获取试卷信息 global $wpdb; $paper = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_papers WHERE paper_id = %d", $paper_id )); if (!$paper) { return '<div class="exam-error">试卷不存在</div>'; } // 检查是否已有进行中的考试 $user_id = get_current_user_id(); $existing_record = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d AND status = 'in_progress'", $user_id, $paper_id )); // 如果是预览模式或管理员 $is_preview = $atts['preview'] || current_user_can('manage_options'); if (!$existing_record && !$is_preview) { // 创建新的考试记录 $record_data = array( 'user_id' => $user_id, 'paper_id' => $paper_id, 'start_time' => current_time('mysql'), 'status' => 'in_progress', 'ip_address' => $_SERVER['REMOTE_ADDR'], ); $wpdb->insert($wpdb->prefix . 'exam_records', $record_data); $record_id = $wpdb->insert_id; } else { $record_id = $existing_record ? $existing_record->record_id : 0; } // 获取试卷试题 $questions = $wpdb->get_results($wpdb->prepare( "SELECT q.*, pq.question_order, pq.question_score FROM {$wpdb->prefix}exam_paper_questions pq JOIN {$wpdb->prefix}posts q ON pq.question_id = q.ID WHERE pq.paper_id = %d AND q.post_status = 'publish' ORDER BY pq.question_order ASC", $paper_id )); if (empty($questions)) { return '<div class="exam-error">试卷中没有试题</div>'; } // 获取用户已有的答案(如果是继续考试) $user_answers = array(); if ($record_id && $existing_record && !empty($existing_record->user_answers)) { $user_answers = json_decode($existing_record->user_answers, true); } // 生成考试界面HTML ob_start(); ?> <div class="exam-container" data-paper-id="<?php echo $paper_id; ?>" data-record-id="<?php echo $record_id; ?>" data-time-limit="<?php echo $paper->time_limit; ?>" data-is-preview="<?php echo $is_preview ? '1' : '0'; ?>"> <div class="exam-header"> <h1 class="exam-title"><?php echo esc_html($paper->paper_title); ?></h1> <div class="exam-info"> <div class="exam-timer"> <span class="timer-label">剩余时间:</span> <span class="timer-display"><?php echo $paper->time_limit; ?>:00</span> </div> <div class="exam-progress"> <span class="progress-label">答题进度:</span> <span class="progress-display">0/<?php echo count($questions); ?></span> </div> </div> </div> <div class="exam-navigation"> <div class="question-nav"> <?php foreach ($questions as $index => $question): ?> <button type="button" class="nav-btn" data-question-index="<?php echo $index; ?>"> <?php echo $index + 1; ?> </button> <?php endforeach; ?> </div> <div class="nav-controls"> <button type="button" id="prev-question" class="button">上一题</button> <button type="button" id="next-question" class="button">下一题</button> <button type="button" id="submit-exam" class="button button-primary">提交试卷</button> </div> </div> <div class="exam-questions"> <?php foreach ($questions as $index => $question): $question_type = get_post_meta($question->ID, '_question_type', true); $question_options = get_post_meta($question->ID, '_question_options', true); $options = !empty($question_options) ? json_decode($question_options, true) : array(); $user_answer = isset($user_answers[$question->ID]) ? $user_answers[$question->ID] : ''; ?> <div class="question-item" data-question-id="<?php echo $question->ID; ?>" data-question-index="<?php echo $index; ?>" style="<?php echo $index > 0 ? 'display:none;' : ''; ?>"> <div class="question-header"> <h3 class="question-title"> 第<?php echo $index + 1; ?>题 <span class="question-score">(<?php echo $question->question_score; ?>分)</span> </h3> <div class="question-type"><?php echo get_question_type_name($question_type); ?></div> </div> <div class="question-content"> <?php echo apply_filters('the_content', $question->post_content); ?> </div> <div class="question-answer"> <?php switch ($question_type): case 'single_choice': ?> <div class="choice-options"> <?php foreach ($options as $key => $option): ?> <label class="choice-option"> <input type="radio" name="answer_<?php echo $question->ID; ?>" value="<?php echo chr(65 + $key); ?>" <?php checked($user_answer, chr(65 + $key)); ?>> <span class="option-label"><?php echo chr(65 + $key); ?>.</span> <span class="option-text"><?php echo esc_html($option); ?></span> </label> <?php endforeach; ?> </div> <?php break; case 'multiple_choice': ?> <div class="choice-options"> <?php foreach ($options as $key => $option): $user_answers_array = !empty($user_answer) ? explode(',', $user_answer) : array(); ?> <label class="choice-option"> <input type="checkbox" name="answer_<?php echo $question->ID; ?>[]" value="<?php echo chr(65 + $key); ?>" <?php checked(in_array(chr(65 + $key), $user_answers_array)); ?>> <span class="option-label"><?php echo chr(65 + $key); ?>.</span> <span class="option-text"><?php echo esc_html($option); ?></span> </label> <?php endforeach; ?> </div> <?php break; case 'true_false': ?> <div class="true-false-options"> <label class="true-false-option"> <input type="radio" name="answer_<?php echo $question->ID; ?>" value="true" <?php checked($user_answer, 'true'); ?>> <span>正确</span> </label> <label class="true-false-option"> <input type="radio" name="answer_<?php echo $question->ID; ?>" value="false" <?php checked($user_answer, 'false'); ?>> <span>错误</span> </label> </div> <?php break; case 'fill_blank': ?> <div class="fill-blank-answer"> <input type="text" name="answer_<?php echo $question->ID; ?>" value="<?php echo esc_attr($user_answer); ?>" class="widefat" placeholder="请输入答案"> </div> <?php break; case 'short_answer': case 'essay': ?> <div class="essay-answer"> <textarea name="answer_<?php echo $question->ID; ?>" class="widefat" rows="6" placeholder="请输入您的答案"><?php echo esc_textarea($user_answer); ?></textarea> </div> <?php break; endswitch; ?> </div> <div class="question-actions"> <button type="button" class="button mark-question" data-marked="0"> <span class="mark-text">标记此题</
发表评论一步步教你,在WordPress中添加网站内容自动生成思维导图与知识图谱工具 引言:当WordPress遇见知识可视化 在信息爆炸的时代,网站内容的管理和呈现方式直接影响着用户体验和知识传递效率。传统的线性文章结构虽然清晰,却难以展现复杂概念间的关联性。想象一下,当读者浏览一篇关于“人工智能发展史”的长文时,如果能同时看到一个动态生成的思维导图,清晰展示从图灵测试到深度学习的关键节点及其关联,理解效率将大幅提升。 WordPress作为全球最流行的内容管理系统,其强大之处不仅在于开箱即用的功能,更在于无限的可扩展性。通过代码二次开发,我们可以将这种知识可视化能力直接集成到WordPress中,让每一篇文章、每一个知识点都能自动转化为结构化的思维导图和知识图谱。 本文将详细指导您如何通过WordPress代码二次开发,实现网站内容的自动可视化,不仅提升用户体验,也为您的网站增添独特的智能工具功能。 第一部分:理解基础概念与技术选型 1.1 思维导图与知识图谱的核心区别 在开始技术实现之前,我们需要明确两个核心概念: 思维导图(Mind Map)是一种放射状的树形结构,以一个中心主题出发,逐级展开相关子主题,强调发散性思维和记忆关联。它适合用于内容摘要、思路整理和快速浏览。 知识图谱(Knowledge Graph)则是更为复杂的网络结构,由实体、属性和关系构成,能够表达多对多的复杂关联。它更适合表现深层次的知识体系和概念间的多维联系。 在WordPress中实现这两种可视化工具,需要根据内容类型和用户需求进行选择。例如,教程类文章更适合思维导图,而学术类或产品比较类内容则更适合知识图谱。 1.2 技术栈选择与评估 实现这一功能,我们需要考虑以下技术组件: 前端可视化库: Mind Map库:Markmap、jsMind、MindElixir都是优秀的选择。Markmap基于Markdown,与WordPress编辑器兼容性好;jsMind功能丰富,支持多种布局;MindElixir界面现代,交互流畅。 知识图谱库:Vis.js、D3.js、Cytoscape.js。Vis.js易于上手,性能良好;D3.js功能强大但学习曲线陡峭;Cytoscape.js专为复杂网络设计,适合大型知识图谱。 后端处理方案: 纯前端方案:内容直接在前端解析生成,适合小型网站,减轻服务器压力。 前后端结合:后端预处理内容结构,前端负责渲染,适合内容复杂、需要优化的场景。 API服务集成:利用第三方NLP服务进行内容分析,适合需要深度语义理解的场景。 WordPress集成方式: 短代码(Shortcode)实现:最简单的方式,通过短代码嵌入可视化内容 Gutenberg块开发:现代WordPress编辑器的原生扩展方式 元数据字段扩展:为文章添加结构化数据字段 独立插件开发:最灵活、可复用性最高的方式 考虑到可维护性和扩展性,本文将采用Gutenberg块开发为主,短代码为辅的混合方案。 第二部分:开发环境搭建与基础配置 2.1 本地开发环境配置 在开始编码前,我们需要搭建合适的开发环境: 本地服务器环境:推荐使用Local by Flywheel或XAMPP,它们提供了完整的PHP+MySQL环境,并针对WordPress进行了优化。 代码编辑器:VS Code是当前最流行的选择,安装以下扩展: PHP Intelephense(PHP智能提示) WordPress Snippet(WordPress代码片段) ESLint(JavaScript代码检查) GitLens(版本管理) 浏览器开发工具:Chrome DevTools是前端调试的必备工具,特别是Elements和Console面板。 版本控制:初始化Git仓库,定期提交代码变更。 2.2 创建自定义插件框架 我们首先创建一个独立的插件,避免直接修改主题文件: <?php /** * Plugin Name: Content Visualizer - Mind Map & Knowledge Graph * Plugin URI: https://yourwebsite.com/ * Description: 自动将WordPress内容转换为思维导图和知识图谱 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('CV_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('CV_PLUGIN_URL', plugin_dir_url(__FILE__)); define('CV_VERSION', '1.0.0'); // 初始化插件 class Content_Visualizer { 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() { // 前后端钩子 add_action('init', array($this, 'register_blocks')); add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); add_action('enqueue_block_editor_assets', array($this, 'enqueue_editor_assets')); // 短代码注册 add_shortcode('mind_map', array($this, 'mind_map_shortcode')); add_shortcode('knowledge_graph', array($this, 'knowledge_graph_shortcode')); } // 后续方法将在这里添加 } // 启动插件 Content_Visualizer::get_instance(); 2.3 资源文件组织架构 创建清晰的目录结构: content-visualizer/ ├── content-visualizer.php # 主插件文件 ├── includes/ # PHP类文件 │ ├── class-content-parser.php │ ├── class-mindmap-generator.php │ └── class-knowledgegraph-generator.php ├── assets/ # 静态资源 │ ├── css/ │ ├── js/ │ └── lib/ # 第三方库 ├── blocks/ # Gutenberg块 │ ├── mind-map/ │ └── knowledge-graph/ ├── templates/ # 前端模板 └── languages/ # 国际化文件 第三部分:内容解析与结构化处理 3.1 自动提取文章结构 内容解析是自动生成可视化的核心。我们需要从文章中提取层次结构和关键概念: // includes/class-content-parser.php class Content_Parser { /** * 从文章内容中提取标题层次结构 */ public static function extract_headings($content) { $pattern = '/<h([1-6])[^>]*>(.*?)</h[1-6]>/i'; preg_match_all($pattern, $content, $matches, PREG_SET_ORDER); $structure = array(); foreach ($matches as $match) { $level = intval($match[1]); $text = strip_tags($match[2]); $id = sanitize_title($text); $structure[] = array( 'level' => $level, 'text' => $text, 'id' => $id, 'children' => array() ); } // 构建层次树 return self::build_heading_tree($structure); } /** * 将扁平标题列表转换为树形结构 */ private static function build_heading_tree($headings) { $tree = array(); $stack = array(); foreach ($headings as $heading) { $node = array( 'text' => $heading['text'], 'id' => $heading['id'], 'children' => array() ); // 确定父节点 while (!empty($stack)) { $last = end($stack); if ($last['level'] < $heading['level']) { // 当前节点是最后一个节点的子节点 $tree_ref = &$this->find_node_ref($tree, $last['id']); $tree_ref['children'][] = &$node; break; } else { array_pop($stack); } } // 如果没有父节点,添加到根 if (empty($stack)) { $tree[] = &$node; } $stack[] = array( 'level' => $heading['level'], 'id' => $heading['id'], 'node' => &$node ); unset($node); } return $tree; } /** * 提取文章中的关键实体(人名、地名、专业术语等) */ public static function extract_entities($content) { // 简化版实体提取,实际项目中可集成NLP服务 $text = strip_tags($content); // 提取可能的技术术语(大写字母开头的单词序列) preg_match_all('/b[A-Z][a-z]+(?:s+[A-Z][a-z]+)*b/', $text, $matches); $technical_terms = array_unique($matches[0]); // 提取引号内的概念 preg_match_all('/["']([^"']+)["']/', $text, $matches); $quoted_concepts = array_unique($matches[1]); // 合并并过滤 $entities = array_merge($technical_terms, $quoted_concepts); $entities = array_filter($entities, function($entity) { return strlen($entity) > 2 && strlen($entity) < 50; }); return array_values($entities); } /** * 分析实体间的关系(基于共现分析) */ public static function analyze_relationships($content, $entities) { $relationships = array(); $text = strip_tags($content); $sentences = preg_split('/[.!?]+/', $text); foreach ($sentences as $sentence) { $sentence_entities = array(); foreach ($entities as $entity) { if (stripos($sentence, $entity) !== false) { $sentence_entities[] = $entity; } } // 如果句子中包含多个实体,建立关系 if (count($sentence_entities) > 1) { for ($i = 0; $i < count($sentence_entities); $i++) { for ($j = $i + 1; $j < count($sentence_entities); $j++) { $key = $sentence_entities[$i] . '|' . $sentence_entities[$j]; if (!isset($relationships[$key])) { $relationships[$key] = 0; } $relationships[$key]++; } } } } // 转换为前端需要的格式 $result = array(); foreach ($relationships as $key => $strength) { list($source, $target) = explode('|', $key); if ($strength > 1) { // 只保留强度大于1的关系 $result[] = array( 'source' => $source, 'target' => $target, 'strength' => $strength, 'label' => '相关' // 可进一步分析关系类型 ); } } return $result; } } 3.2 缓存机制优化性能 内容解析可能消耗较多资源,我们需要实现缓存机制: class Content_Cache { private static $cache_group = 'content_visualizer'; /** * 获取缓存内容 */ public static function get($post_id, $type) { $key = "{$type}_{$post_id}"; $cached = wp_cache_get($key, self::$cache_group); if ($cached !== false) { return $cached; } // 从数据库获取 $transient_key = "cv_{$key}"; $cached = get_transient($transient_key); if ($cached !== false) { wp_cache_set($key, $cached, self::$cache_group, 3600); return $cached; } return false; } /** * 设置缓存 */ public static function set($post_id, $type, $data, $expiration = 3600) { $key = "{$type}_{$post_id}"; // 设置内存缓存 wp_cache_set($key, $data, self::$cache_group, $expiration); // 设置数据库缓存 $transient_key = "cv_{$key}"; set_transient($transient_key, $data, $expiration); // 建立缓存索引,便于文章更新时清理 $index = get_option('cv_cache_index', array()); if (!isset($index[$post_id])) { $index[$post_id] = array(); } $index[$post_id][] = $transient_key; update_option('cv_cache_index', $index, false); } /** * 清理文章缓存 */ public static function clear_post_cache($post_id) { $index = get_option('cv_cache_index', array()); if (isset($index[$post_id])) { foreach ($index[$post_id] as $transient_key) { delete_transient($transient_key); } unset($index[$post_id]); update_option('cv_cache_index', $index, false); } // 清理内存缓存 wp_cache_delete("mindmap_{$post_id}", self::$cache_group); wp_cache_delete("knowledgegraph_{$post_id}", self::$cache_group); } } // 文章更新时清理缓存 add_action('save_post', function($post_id) { if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; Content_Cache::clear_post_cache($post_id); }); 第四部分:思维导图功能实现 4.1 集成Markmap可视化库 Markmap是基于D3.js的思维导图库,支持Markdown直接转换: // assets/js/mindmap-renderer.js class MindmapRenderer { constructor(containerId, options = {}) { this.container = document.getElementById(containerId); this.options = Object.assign({ autoFit: true, duration: 500, maxWidth: 800, nodeMinHeight: 16, spacingVertical: 5, spacingHorizontal: 80, paddingX: 8 }, options); this.mindmap = null; this.initialized = false; } /** * 从文章结构数据生成思维导图 */ async renderFromStructure(structure) { if (!this.initialized) { await this.initialize(); } // 转换结构为Markdown格式 const markdown = this.structureToMarkdown(structure); // 创建Markmap const { Markmap } = window.markmap; const mm = Markmap.create(this.container, this.options, markdown); this.mindmap = mm; return mm; } /** * 将树形结构转换为Markdown */ structureToMarkdown(node, level = 0) { let markdown = ''; const indent = ' '.repeat(level); if (Array.isArray(node)) { // 根节点数组 node.forEach(child => { markdown += this.structureToMarkdown(child, level); }); } else { // 单个节点 markdown += `${indent}- ${node.text}n`; if (node.children && node.children.length > 0) { node.children.forEach(child => { markdown += this.structureToMarkdown(child, level + 1); }); } } return markdown; } /** * 初始化Markmap库 */ async initialize() { if (window.markmap) { this.initialized = true; return; } // 动态加载Markmap库 await this.loadScript(CV_PLUGIN_URL + 'assets/lib/markmap/markmap.min.js'); await this.loadStylesheet(CV_PLUGIN_URL + 'assets/lib/markmap/markmap.css'); this.initialized = true; } /** * 动态加载脚本 */ loadScript(src) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } /** * 动态加载样式 */ loadStylesheet(href) { return new Promise((resolve, reject) => { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = href; link.onload = resolve; link.onerror = reject; document.head.appendChild(link); }); } /** * 导出功能 */ exportAsImage(format = 'png') { if (!this.mindmap) return; const container = this.container; const svg = container.querySelector('svg'); if (format === 'svg') { // 导出SVG const serializer = new XMLSerializer(); const source = serializer.serializeToString(svg); const blob = new Blob([source], { type: 'image/svg+xml' }); this.downloadBlob(blob, 'mindmap.svg'); } else if (format === 'png') { // 导出PNG const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const data = new XMLSerializer().serializeToString(svg); const img = new Image(); img.onload = () => { canvas.width = img.width; canvas.height = img.height; ctx.drawImage(img, 0, 0); canvas.toBlob(blob => { this.downloadBlob(blob, 'mindmap.png'); }, 'image/png'); }; btoa(unescape(encodeURIComponent(data))); } } /** * 下载Blob文件 */ downloadBlob(blob, filename) { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } } // 全局可用window.MindmapRenderer = MindmapRenderer; #### 4.2 创建Gutenberg思维导图块 // blocks/mind-map/block.jsconst { registerBlockType } = wp.blocks;const { InspectorControls, useBlockProps, BlockControls } = wp.blockEditor;const { PanelBody, SelectControl, ToggleControl, RangeControl, ColorPicker } = wp.components;const { useState, useEffect } = wp.element; registerBlockType('content-visualizer/mind-map', { title: '思维导图', icon: 'chart-line', category: 'widgets', attributes: { postId: { type: 'number', default: 0 }, autoGenerate: { type: 'boolean', default: true }, layout: { type: 'string', default: 'tree' }, depth: { type: 'number', default: 3 }, primaryColor: { type: 'string', default: '#3366cc' }, showControls: { type: 'boolean', default: true } }, edit: function({ attributes, setAttributes, clientId }) { const blockProps = useBlockProps(); const [isLoading, setIsLoading] = useState(false); const [structure, setStructure] = useState(null); const [error, setError] = useState(null); // 获取当前文章ID useEffect(() => { if (attributes.postId === 0) { const currentPostId = wp.data.select('core/editor').getCurrentPostId(); setAttributes({ postId: currentPostId }); } }, []); // 加载文章结构 useEffect(() => { if (attributes.autoGenerate && attributes.postId > 0) { fetchStructure(); } }, [attributes.postId, attributes.autoGenerate]); const fetchStructure = async () => { setIsLoading(true); setError(null); try { const response = await wp.apiFetch({ path: `/content-visualizer/v1/mindmap/${attributes.postId}`, method: 'GET' }); if (response.success) { setStructure(response.data); } else { setError(response.message || '获取文章结构失败'); } } catch (err) { setError('网络请求失败: ' + err.message); } finally { setIsLoading(false); } }; const renderPreview = () => { if (isLoading) { return ( <div className="mindmap-loading"> <div className="spinner is-active"></div> <p>正在生成思维导图...</p> </div> ); } if (error) { return ( <div className="mindmap-error"> <p>❌ {error}</p> <button className="components-button is-secondary" onClick={fetchStructure} > 重试 </button> </div> ); } if (structure) { // 简化预览 return ( <div className="mindmap-preview"> <div className="mindmap-tree"> {renderTreePreview(structure)} </div> <p className="mindmap-hint"> 前端将显示交互式思维导图 </p> </div> ); } return ( <div className="mindmap-empty"> <p>请确保已选择文章并启用自动生成</p> </div> ); }; const renderTreePreview = (nodes, level = 0) => { if (!nodes || nodes.length === 0) return null; return ( <ul style={{ paddingLeft: level * 20 }}> {nodes.slice(0, 3).map((node, index) => ( <li key={index}> <span>{node.text}</span> {node.children && level < attributes.depth - 1 && renderTreePreview(node.children, level + 1)} </li> ))} {nodes.length > 3 && ( <li>... 还有 {nodes.length - 3} 个节点</li> )} </ul> ); }; return ( <div {...blockProps}> <BlockControls> <div className="components-toolbar"> <button className="components-button is-button" onClick={fetchStructure} disabled={isLoading} > {isLoading ? '刷新中...' : '刷新导图'} </button> </div> </BlockControls> <InspectorControls> <PanelBody title="基本设置" initialOpen={true}> <ToggleControl label="自动生成" checked={attributes.autoGenerate} onChange={(value) => setAttributes({ autoGenerate: value })} /> <SelectControl label="布局方式" value={attributes.layout} options={[ { label: '树状图', value: 'tree' }, { label: '放射状', value: 'radial' }, { label: '鱼骨图', value: 'fishbone' } ]} onChange={(value) => setAttributes({ layout: value })} /> <RangeControl label="显示深度" value={attributes.depth} onChange={(value) => setAttributes({ depth: value })} min={1} max={6} /> </PanelBody> <PanelBody title="样式设置" initialOpen={false}> <div className="color-picker-control"> <label>主色调</label> <ColorPicker color={attributes.primaryColor} onChangeComplete={(color) => setAttributes({ primaryColor: color.hex }) } disableAlpha /> </div> <ToggleControl label="显示控制按钮" checked={attributes.showControls} onChange={(value) => setAttributes({ showControls: value })} /> </PanelBody> </InspectorControls> <div className="mindmap-block"> <h3>思维导图预览</h3> {renderPreview()} </div> </div> ); }, save: function() { // 动态块,由前端渲染 return null; } }); #### 4.3 前端渲染与交互实现 // 在前端渲染思维导图add_action('wp_footer', function() { if (!has_block('content-visualizer/mind-map')) { return; } ?> <script type="text/javascript"> document.addEventListener('DOMContentLoaded', function() { // 查找所有思维导图容器 const mindmapContainers = document.querySelectorAll('.wp-block-content-visualizer-mind-map'); mindmapContainers.forEach(async (container, index) => { const blockId = container.getAttribute('data-block-id'); const postId = container.getAttribute('data-post-id'); const config = JSON.parse(container.getAttribute('data-config') || '{}'); // 创建唯一容器ID const renderContainerId = `mindmap-${postId}-${index}`; const renderDiv = document.createElement('div'); renderDiv.id = renderContainerId; renderDiv.className = 'mindmap-render-container'; container.appendChild(renderDiv); try { // 获取思维导图数据 const response = await fetch( `<?php echo rest_url('content-visualizer/v1/mindmap/'); ?>${postId}` ); if (!response.ok) { throw new Error('获取数据失败'); } const data = await response.json(); if (data.success) { // 初始化渲染器 const renderer = new MindmapRenderer(renderContainerId, { autoFit: true, maxWidth: config.maxWidth || 800, primaryColor: config.primaryColor || '#3366cc' }); // 渲染思维导图 await renderer.renderFromStructure(data.data); // 添加控制按钮 if (config.showControls !== false) { addControlButtons(renderDiv, renderer); } } else { renderDiv.innerHTML = `<p class="error">${data.message}</p>`; } } catch (error) { renderDiv.innerHTML = `<p class="error">加载失败: ${error.message}</p>`; } }); function addControlButtons(container, renderer) { const controls = document.createElement('div'); controls.className = 'mindmap-controls'; const zoomInBtn = createButton('放大', '+', () => { if (renderer.mindmap) { renderer.mindmap.fit(); renderer.mindmap.scale(renderer.mindmap.scale() * 1.2); } }); const zoomOutBtn = createButton('缩小', '-', () => { if (renderer.mindmap) { renderer.mindmap.scale(renderer.mindmap.scale() * 0.8); } }); const resetBtn = createButton('重置', '↺', () => { if (renderer.mindmap) { renderer.mindmap.fit(); } }); const exportPngBtn = createButton('导出PNG', '📷', () => { renderer.exportAsImage('png'); }); const exportSvgBtn = createButton('导出SVG', '🖼️', () => { renderer.exportAsImage('svg'); }); controls.appendChild(zoomInBtn); controls.appendChild(zoomOutBtn); controls.appendChild(resetBtn); controls.appendChild(exportPngBtn); controls.appendChild(exportSvgBtn); container.parentNode.insertBefore(controls, container.nextSibling); } function createButton(title, text, onClick) { const button = document.createElement('button'); button.className = 'mindmap-control-btn'; button.title = title; button.innerHTML = text; button.addEventListener('click', onClick); return button; } }); </script> <style> .mindmap-render-container { width: 100%; height: 500px; border: 1px solid #eee; border-radius: 8px; overflow: hidden; margin: 20px 0; } .mindmap-controls { display: flex; gap: 8px; margin-top: 10px; justify-content: center; } .mindmap-control-btn { padding: 6px 12px; background: #f5f5f5; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; font-size: 14px; transition: all 0.2s; } .mindmap-control-btn:hover { background: #e9e9e9; border-color: #ccc; } .mindmap-loading, .mindmap-error, .mindmap-empty { padding: 40px; text-align: center; background: #f9f9f9; border-radius: 8px; margin: 20px 0; } .mindmap-preview .mindmap-tree { max-height: 300px; overflow-y: auto; padding: 15px; background: white; border-radius: 6px; border: 1px solid #e0e0e0; } .mindmap-preview ul { list-style-type: none; margin: 0; padding: 0; } .mindmap-preview li { padding: 4px 0; color: #333; } .mindmap-preview li span { padding: 2px 6px; background: #f0f7ff; border-radius: 3px; border-left: 3px solid #3366cc; } .mindmap-hint { font-size: 12px; color: #666; text-align: center; margin-top: 10px; font-style: italic; } </style> <?php }); ### 第五部分:知识图谱功能实现 #### 5.1 集成Vis.js网络图库 // includes/class-knowledgegraph-generator.phpclass KnowledgeGraph_Generator { /** * 生成知识图谱数据 */ public static function generate_graph_data($post_id) { // 检查缓存 $cached = Content_Cache::get($post_id, 'knowledgegraph'); if ($cached !== false) { return $cached; } $post = get_post($post_id); if (!$post) { return new WP_Error('invalid_post', '文章不存在'); } // 提取实体和关系 $entities = Content_Parser::extract_entities($post->post_content); $relationships = Content_Parser::analyze_relationships($post->post_content, $entities); // 构建Vis.js格式的数据 $nodes = array(); $edges = array(); // 创建节点 foreach ($entities as $index => $entity) { $nodes[] = array( 'id' => $index, 'label' => $entity, 'value' => self::calculate_entity_importance($entity, $post->post_content), 'title' => self::generate_entity_tooltip($entity, $post->post_content), 'group' => self::categorize_entity($entity), 'font' => array( 'size' => 16, 'bold' => true ) ); } // 创建边(关系) $entity_index = array_flip($entities); foreach ($relationships as $rel) { if (isset($entity_index[$rel['source']], $entity_index[$rel['target']])) { $edges[] = array( 'from' => $entity_index[$rel['source']], 'to' => $entity_index[$rel['target']], 'label' => $rel['label'], 'value' => $rel['strength'], 'title' => "关联强度: {$rel['strength']}", 'arrows' => 'to', 'smooth' => array('type' => 'continuous') ); } } $graph_data = array( 'nodes' => $nodes, 'edges' => $edges, 'metadata' => array( 'entity_count' => count($entities), 'relationship_count' => count($edges), 'generated_at' => current_time('mysql') ) ); // 缓存结果 Content_Cache::set($post_id, 'knowledgegraph', $graph_data, 86400); // 缓存24小时 return $graph_data; } /** * 计算实体重要性 */ private static function calculate_entity_importance($entity, $content) { // 基于出现频率和位置计算重要性 $frequency = substr_count(strtolower($content), strtolower($entity)); // 检查是否出现在标题中 $title_importance = 0; $post_title = get_the_title(); if (stripos($post_title, $entity) !== false) { $title_importance = 5; } // 检查是否出现在开头段落 $first_paragraph = wp_trim_words($content, 50); $position_importance = stripos($first_paragraph, $entity) !== false ? 3 : 1; return ($frequency * 2) + $title_importance + $position_importance; } /** * 生成实体悬停提示 */ private static function generate_entity_tooltip($entity, $content) { $sentences = array(); $pattern = '/[^.!?]*b' . preg_quote($entity, '/') . 'b[^.!?]*[.!?]/i'; preg_match_all($pattern, $content, $matches); if (!empty($matches[0])) { $sentences = array_slice($matches[0], 0, 3); // 取前3个包含实体的句子 } $tooltip = "<strong>{$entity}</strong><br><br>"; if (!empty($sentences)) { $tooltip .= "相关描述:<br>"; foreach ($sentences as $sentence) { $tooltip .= "• " . trim($sentence) . "<br>"; } } else { $tooltip .= "在文章中多次提及"; } return $tooltip; } /** * 实体分类 */ private static function categorize_entity($entity) { // 简单的分类逻辑,可根据需要扩展 $tech_keywords = ['AI', '算法', '编程', '数据', '网络', '系统']; $business_keywords = ['市场', '营销', '管理', '战略', '投资', '财务']; $people_keywords = ['先生', '女士', '博士', '教授', '团队', '专家']; $entity_lower = strtolower($entity); foreach ($tech_keywords as $keyword) { if (stripos($entity, $keyword) !== false) { return 'technology'; } } foreach ($business_keywords as $keyword) { if (stripos($entity, $keyword) !== false) { return 'business'; } } foreach ($people_keywords as $keyword) { if (stripos($entity_lower, strtol
发表评论实战教程:为WordPress网站集成智能化的用户行为分析与个性化推荐引擎 引言:智能化网站运营的必要性 在当今互联网竞争日益激烈的环境下,网站运营已从简单的信息发布转变为以用户体验为核心的智能化运营。据统计,采用个性化推荐系统的网站平均能提升30%的用户参与度和20%的转化率。然而,许多中小型网站由于技术门槛和成本限制,难以享受到智能化运营带来的红利。 本教程将详细指导您如何通过WordPress代码二次开发,为您的网站集成用户行为分析与个性化推荐引擎,实现常用互联网小工具功能。无论您是WordPress开发者、网站管理员还是对网站智能化感兴趣的技术爱好者,都能通过本教程掌握实用技能,将您的网站提升到新的智能化水平。 第一部分:系统架构设计与技术选型 1.1 整体架构设计 为WordPress网站集成智能化功能,我们需要设计一个模块化、可扩展的系统架构: 数据采集层:负责收集用户行为数据 数据处理层:清洗、存储和分析用户数据 推荐算法层:基于用户行为生成个性化推荐 前端展示层:将推荐结果以友好方式展示给用户 1.2 技术选型与工具准备 我们将使用以下技术和工具: WordPress:作为基础CMS平台 PHP 7.4+:主要开发语言 MySQL 5.7+:数据存储 JavaScript/jQuery:前端交互与数据采集 Redis:缓存与实时数据处理(可选) Python:复杂算法处理(可选,通过API集成) 1.3 开发环境搭建 在开始开发前,请确保您的环境满足以下要求: 本地或服务器安装WordPress 5.0+ PHP环境支持mysqli扩展和JSON处理 MySQL数据库已创建 代码编辑器(如VS Code、PHPStorm等) 浏览器开发者工具用于调试 第二部分:用户行为数据采集系统开发 2.1 设计用户行为数据模型 首先,我们需要设计合理的数据结构来存储用户行为数据。在WordPress数据库中创建自定义表: CREATE TABLE wp_user_behavior ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT(20) UNSIGNED DEFAULT NULL, session_id VARCHAR(64) NOT NULL, post_id BIGINT(20) UNSIGNED DEFAULT NULL, behavior_type ENUM('view', 'click', 'like', 'share', 'comment', 'purchase') NOT NULL, behavior_data TEXT, referrer VARCHAR(500), user_agent TEXT, ip_address VARCHAR(45), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX idx_user_id (user_id), INDEX idx_post_id (post_id), INDEX idx_behavior_type (behavior_type), INDEX idx_created_at (created_at) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; 2.2 创建数据采集插件 接下来,创建一个WordPress插件来采集用户行为数据: <?php /** * Plugin Name: 智能用户行为分析系统 * Description: 采集和分析用户行为数据,为个性化推荐提供支持 * Version: 1.0.0 * Author: 您的名称 */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } class Intelligent_Behavior_Analysis { private static $instance = null; private $table_name; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { global $wpdb; $this->table_name = $wpdb->prefix . 'user_behavior'; // 初始化钩子 add_action('init', array($this, 'init_session')); add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); add_action('wp_footer', array($this, 'add_tracking_code')); add_action('wp_ajax_track_behavior', array($this, 'track_behavior_ajax')); add_action('wp_ajax_nopriv_track_behavior', array($this, 'track_behavior_ajax')); // 文章浏览追踪 add_action('wp', array($this, 'track_post_view')); } // 初始化用户会话 public function init_session() { if (!session_id() && !headers_sent()) { session_start(); } if (!isset($_SESSION['user_session_id'])) { $_SESSION['user_session_id'] = $this->generate_session_id(); } } // 生成会话ID private function generate_session_id() { return md5(uniqid(mt_rand(), true) . $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']); } // 加载前端脚本 public function enqueue_scripts() { wp_enqueue_script( 'behavior-tracker', plugin_dir_url(__FILE__) . 'js/behavior-tracker.js', array('jquery'), '1.0.0', true ); wp_localize_script('behavior-tracker', 'behaviorTracker', array( 'ajax_url' => admin_url('admin-ajax.php'), 'session_id' => isset($_SESSION['user_session_id']) ? $_SESSION['user_session_id'] : '', 'user_id' => is_user_logged_in() ? get_current_user_id() : 0 )); } // 追踪文章浏览 public function track_post_view() { if (is_single() || is_page()) { global $post; $user_id = is_user_logged_in() ? get_current_user_id() : 0; $session_id = isset($_SESSION['user_session_id']) ? $_SESSION['user_session_id'] : ''; $this->save_behavior(array( 'user_id' => $user_id, 'session_id' => $session_id, 'post_id' => $post->ID, 'behavior_type' => 'view', 'referrer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '', 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'ip_address' => $this->get_client_ip() )); } } // AJAX处理行为追踪 public function track_behavior_ajax() { check_ajax_referer('behavior_tracking_nonce', 'nonce'); $data = array( 'user_id' => intval($_POST['user_id']), 'session_id' => sanitize_text_field($_POST['session_id']), 'post_id' => isset($_POST['post_id']) ? intval($_POST['post_id']) : null, 'behavior_type' => sanitize_text_field($_POST['behavior_type']), 'behavior_data' => isset($_POST['behavior_data']) ? sanitize_textarea_field($_POST['behavior_data']) : '', 'referrer' => isset($_POST['referrer']) ? sanitize_text_field($_POST['referrer']) : '', 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '', 'ip_address' => $this->get_client_ip() ); $result = $this->save_behavior($data); if ($result) { wp_send_json_success(array('message' => '行为记录成功')); } else { wp_send_json_error(array('message' => '行为记录失败')); } } // 保存行为数据到数据库 private function save_behavior($data) { global $wpdb; // 防止过于频繁的记录(同一用户同一内容30秒内不重复记录) $cache_key = 'behavior_' . md5(serialize($data)); if (get_transient($cache_key)) { return false; } set_transient($cache_key, true, 30); return $wpdb->insert( $this->table_name, $data, array('%d', '%s', '%d', '%s', '%s', '%s', '%s', '%s') ); } // 获取客户端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); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; } // 添加追踪代码到页脚 public function add_tracking_code() { ?> <script type="text/javascript"> // 非敏感数据追踪代码 console.log('智能行为分析系统已加载'); </script> <?php } } // 初始化插件 Intelligent_Behavior_Analysis::get_instance(); 2.3 前端行为追踪JavaScript 创建js/behavior-tracker.js文件: (function($) { 'use strict'; // 全局配置 const BehaviorTracker = { config: { ajax_url: behaviorTracker.ajax_url, session_id: behaviorTracker.session_id, user_id: behaviorTracker.user_id, nonce: behaviorTracker.nonce || '' }, // 初始化 init: function() { this.bindEvents(); this.trackPagePerformance(); }, // 绑定事件 bindEvents: function() { // 链接点击追踪 $(document).on('click', 'a[href*=""]', function(e) { const $link = $(this); const href = $link.attr('href'); // 排除外部链接和特殊链接 if (href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:')) { return; } // 获取文章ID(如果链接指向站内文章) let postId = null; if (href.includes(window.location.origin)) { const match = href.match(//?p=(d+)/) || href.match(//(d+)//); if (match && match[1]) { postId = match[1]; } } BehaviorTracker.track('click', { element: $link.text().substring(0, 100), url: href, post_id: postId }); }); // 按钮点击追踪 $(document).on('click', 'button, .btn, input[type="submit"]', function(e) { const $btn = $(this); const text = $btn.text() || $btn.val() || $btn.attr('aria-label') || ''; BehaviorTracker.track('click', { element: 'button', text: text.substring(0, 100), class: $btn.attr('class') || '' }); }); // 表单提交追踪 $(document).on('submit', 'form', function(e) { const $form = $(this); const formId = $form.attr('id') || $form.attr('name') || ''; BehaviorTracker.track('form_submit', { form_id: formId, action: $form.attr('action') || '' }); }); // 滚动深度追踪 let scrollTracked = [25, 50, 75, 100]; $(window).on('scroll', $.throttle(250, function() { const scrollPercent = BehaviorTracker.getScrollPercentage(); scrollTracked = scrollTracked.filter(percent => { if (scrollPercent >= percent) { BehaviorTracker.track('scroll', { depth: percent, current_position: scrollPercent }); return false; } return true; }); })); // 视频播放追踪 $(document).on('play', 'video', function(e) { const $video = $(this); const src = $video.attr('src') || ''; BehaviorTracker.track('video_play', { video_src: src.substring(src.lastIndexOf('/') + 1), duration: $video.prop('duration') || 0 }); }); }, // 追踪行为 track: function(type, data = {}) { const postId = $('article').data('post-id') || $('.post').data('post-id') || $('#post-id').val() || null; $.ajax({ url: this.config.ajax_url, type: 'POST', data: { action: 'track_behavior', nonce: this.config.nonce, user_id: this.config.user_id, session_id: this.config.session_id, post_id: postId, behavior_type: type, behavior_data: JSON.stringify(data), referrer: document.referrer }, success: function(response) { if (!response.success) { console.warn('行为追踪失败:', response.data); } }, error: function(xhr, status, error) { console.error('行为追踪请求失败:', error); } }); }, // 获取滚动百分比 getScrollPercentage: function() { const windowHeight = $(window).height(); const documentHeight = $(document).height(); const scrollTop = $(window).scrollTop(); return Math.round((scrollTop / (documentHeight - windowHeight)) * 100); }, // 追踪页面性能 trackPagePerformance: function() { if (window.performance && window.performance.timing) { const perf = window.performance.timing; const pageLoadTime = perf.loadEventEnd - perf.navigationStart; const domReadyTime = perf.domContentLoadedEventEnd - perf.navigationStart; if (pageLoadTime > 0) { this.track('performance', { page_load_time: pageLoadTime, dom_ready_time: domReadyTime, redirect_time: perf.redirectEnd - perf.redirectStart, dns_time: perf.domainLookupEnd - perf.domainLookupStart, tcp_time: perf.connectEnd - perf.connectStart, request_time: perf.responseEnd - perf.requestStart, dom_parsing_time: perf.domComplete - perf.domInteractive }); } } } }; // jQuery节流函数 $.throttle = function(delay, callback) { let timeout = null; let lastExec = 0; return function() { const context = this; const args = arguments; const elapsed = Date.now() - lastExec; function exec() { lastExec = Date.now(); callback.apply(context, args); } if (timeout) { clearTimeout(timeout); } if (elapsed > delay) { exec(); } else { timeout = setTimeout(exec, delay - elapsed); } }; }; // 文档加载完成后初始化 $(document).ready(function() { BehaviorTracker.init(); }); })(jQuery); 第三部分:用户行为数据分析与处理 3.1 创建数据分析类 <?php /** * 用户行为数据分析类 */ class User_Behavior_Analyzer { private $wpdb; private $table_name; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->table_name = $wpdb->prefix . 'user_behavior'; } // 获取用户兴趣标签 public function get_user_interests($user_id, $limit = 10) { $query = $this->wpdb->prepare( "SELECT p.ID as post_id, p.post_title, COUNT(ub.id) as view_count, GROUP_CONCAT(DISTINCT t.name) as tags, GROUP_CONCAT(DISTINCT c.name) as categories FROM {$this->table_name} ub LEFT JOIN {$this->wpdb->posts} p ON ub.post_id = p.ID LEFT JOIN {$this->wpdb->term_relationships} tr ON p.ID = tr.object_id LEFT JOIN {$this->wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id LEFT JOIN {$this->wpdb->terms} t ON tt.term_id = t.term_id AND tt.taxonomy = 'post_tag' LEFT JOIN {$this->wpdb->terms} c ON tt.term_id = c.term_id AND tt.taxonomy = 'category' WHERE ub.user_id = %d AND ub.behavior_type IN ('view', 'click', 'like') AND ub.created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY) AND p.post_status = 'publish' GROUP BY p.ID ORDER BY view_count DESC LIMIT %d", $user_id, $limit ); $results = $this->wpdb->get_results($query); // 提取标签和分类作为兴趣点 $interests = array(); foreach ($results as $row) { if ($row->tags) { $tags = explode(',', $row->tags); foreach ($tags as $tag) { $tag = trim($tag); if ($tag && !in_array($tag, $interests)) { } } } if ($row->categories) { $categories = explode(',', $row->categories); foreach ($categories as $category) { $category = trim($category); if ($category && !in_array($category, $interests)) { $interests[] = $category; } } } } return array_slice($interests, 0, $limit); } // 计算用户相似度(基于协同过滤) public function find_similar_users($user_id, $limit = 5) { // 获取目标用户浏览过的文章 $target_user_posts = $this->get_user_viewed_posts($user_id); if (empty($target_user_posts)) { return array(); } // 查找浏览过相同文章的其他用户 $post_ids_str = implode(',', array_keys($target_user_posts)); $query = $this->wpdb->prepare( "SELECT ub.user_id, COUNT(DISTINCT ub.post_id) as common_views, GROUP_CONCAT(DISTINCT ub.post_id) as common_post_ids FROM {$this->table_name} ub WHERE ub.user_id != %d AND ub.user_id > 0 AND ub.post_id IN ({$post_ids_str}) AND ub.behavior_type = 'view' AND ub.created_at >= DATE_SUB(NOW(), INTERVAL 60 DAY) GROUP BY ub.user_id HAVING common_views >= 2 ORDER BY common_views DESC LIMIT %d", $user_id, $limit ); return $this->wpdb->get_results($query); } // 获取用户浏览过的文章 private function get_user_viewed_posts($user_id) { $query = $this->wpdb->prepare( "SELECT post_id, COUNT(*) as view_count, MAX(created_at) as last_viewed FROM {$this->table_name} WHERE user_id = %d AND behavior_type = 'view' AND created_at >= DATE_SUB(NOW(), INTERVAL 60 DAY) GROUP BY post_id ORDER BY view_count DESC", $user_id ); $results = $this->wpdb->get_results($query); $posts = array(); foreach ($results as $row) { $posts[$row->post_id] = array( 'view_count' => $row->view_count, 'last_viewed' => $row->last_viewed ); } return $posts; } // 获取热门内容(基于浏览量和时间衰减) public function get_popular_content($limit = 10, $days = 30) { $query = $this->wpdb->prepare( "SELECT ub.post_id, p.post_title, p.post_type, COUNT(ub.id) as total_views, COUNT(DISTINCT ub.user_id) as unique_users, MAX(ub.created_at) as last_viewed, -- 时间衰减权重:最近7天的浏览量权重为1,8-30天的权重按线性衰减 SUM( CASE WHEN ub.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) THEN 1.0 WHEN ub.created_at >= DATE_SUB(NOW(), INTERVAL %d DAY) THEN 1.0 - (DATEDIFF(NOW(), ub.created_at) - 7) / (%d - 7) * 0.7 ELSE 0.3 END ) as weighted_score FROM {$this->table_name} ub INNER JOIN {$this->wpdb->posts} p ON ub.post_id = p.ID WHERE ub.behavior_type = 'view' AND ub.created_at >= DATE_SUB(NOW(), INTERVAL %d DAY) AND p.post_status = 'publish' GROUP BY ub.post_id ORDER BY weighted_score DESC, total_views DESC LIMIT %d", $days, $days, $days, $limit ); return $this->wpdb->get_results($query); } // 获取用户行为统计报告 public function get_user_behavior_report($user_id = null, $days = 7) { $where_clause = ''; $params = array($days); if ($user_id) { $where_clause = 'AND ub.user_id = %d'; $params[] = $user_id; } $query = $this->wpdb->prepare( "SELECT DATE(ub.created_at) as date, COUNT(*) as total_actions, SUM(CASE WHEN ub.behavior_type = 'view' THEN 1 ELSE 0 END) as views, SUM(CASE WHEN ub.behavior_type = 'click' THEN 1 ELSE 0 END) as clicks, SUM(CASE WHEN ub.behavior_type = 'like' THEN 1 ELSE 0 END) as likes, SUM(CASE WHEN ub.behavior_type = 'comment' THEN 1 ELSE 0 END) as comments, COUNT(DISTINCT ub.post_id) as unique_posts, COUNT(DISTINCT ub.session_id) as sessions FROM {$this->table_name} ub WHERE ub.created_at >= DATE_SUB(NOW(), INTERVAL %d DAY) {$where_clause} GROUP BY DATE(ub.created_at) ORDER BY date DESC", ...$params ); return $this->wpdb->get_results($query); } // 获取内容关联性分析 public function get_content_relationships($post_id, $limit = 5) { // 查找同时浏览过这篇文章和其他文章的用户 $query = $this->wpdb->prepare( "SELECT ub2.post_id as related_post_id, p.post_title, COUNT(DISTINCT ub1.user_id) as common_users, COUNT(ub2.id) as total_views FROM {$this->table_name} ub1 INNER JOIN {$this->table_name} ub2 ON ub1.user_id = ub2.user_id AND ub1.session_id = ub2.session_id AND ub1.post_id != ub2.post_id INNER JOIN {$this->wpdb->posts} p ON ub2.post_id = p.ID WHERE ub1.post_id = %d AND ub1.behavior_type = 'view' AND ub2.behavior_type = 'view' AND ub1.created_at >= DATE_SUB(NOW(), INTERVAL 60 DAY) AND ub2.created_at >= DATE_SUB(NOW(), INTERVAL 60 DAY) AND p.post_status = 'publish' GROUP BY ub2.post_id ORDER BY common_users DESC, total_views DESC LIMIT %d", $post_id, $limit ); return $this->wpdb->get_results($query); } } ### 3.2 创建数据缓存与优化机制 为了提高系统性能,我们需要实现数据缓存机制: <?php/** 智能推荐缓存管理类 */ class Recommendation_Cache_Manager { private $cache_prefix = 'intelligent_rec_'; private $cache_expiration = 3600; // 1小时 // 获取缓存数据 public function get_cached_recommendations($key, $user_id = 0) { $cache_key = $this->get_cache_key($key, $user_id); $cached = get_transient($cache_key); if ($cached !== false) { return json_decode($cached, true); } return false; } // 设置缓存数据 public function set_cached_recommendations($key, $data, $user_id = 0) { $cache_key = $this->get_cache_key($key, $user_id); $cache_data = json_encode($data); // 根据数据类型设置不同的过期时间 $expiration = $this->cache_expiration; // 热门内容缓存时间较短(15分钟) if (strpos($key, 'popular') !== false) { $expiration = 900; } // 个性化推荐缓存时间较长(2小时) if (strpos($key, 'personal') !== false && $user_id > 0) { $expiration = 7200; } set_transient($cache_key, $cache_data, $expiration); // 记录缓存统计 $this->record_cache_stat($key, $user_id); return true; } // 删除缓存 public function delete_cached_recommendations($key, $user_id = 0) { $cache_key = $this->get_cache_key($key, $user_id); return delete_transient($cache_key); } // 生成缓存键 private function get_cache_key($key, $user_id) { return $this->cache_prefix . $key . '_' . $user_id; } // 记录缓存统计 private function record_cache_stat($key, $user_id) { $stats = get_option('rec_cache_stats', array()); $today = date('Y-m-d'); if (!isset($stats[$today])) { $stats[$today] = array( 'total' => 0, 'hits' => 0, 'misses' => 0, 'by_type' => array() ); } $stats[$today]['total']++; if (!isset($stats[$today]['by_type'][$key])) { $stats[$today]['by_type'][$key] = 0; } $stats[$today]['by_type'][$key]++; // 只保留最近30天的统计 if (count($stats) > 30) { ksort($stats); array_shift($stats); } update_option('rec_cache_stats', $stats, false); } // 获取缓存命中率 public function get_cache_hit_rate($days = 7) { $stats = get_option('rec_cache_stats', array()); $recent_stats = array_slice($stats, -$days, $days, true); $total_hits = 0; $total_misses = 0; foreach ($recent_stats as $day_stats) { $total_hits += $day_stats['hits']; $total_misses += $day_stats['misses']; } $total = $total_hits + $total_misses; if ($total > 0) { return array( 'hit_rate' => round($total_hits / $total * 100, 2), 'total_requests' => $total, 'hits' => $total_hits, 'misses' => $total_misses ); } return array('hit_rate' => 0, 'total_requests' => 0, 'hits' => 0, 'misses' => 0); } } ## 第四部分:个性化推荐引擎实现 ### 4.1 核心推荐算法类 <?php/** 个性化推荐引擎核心类 */ class Personalized_Recommendation_Engine { private $analyzer; private $cache_manager; public function __construct() { $this->analyzer = new User_Behavior_Analyzer(); $this->cache_manager = new Recommendation_Cache_Manager(); } // 获取个性化推荐(主入口方法) public function get_recommendations($user_id = null, $limit = 10, $strategy = 'hybrid') { $cache_key = 'rec_' . $strategy . '_' . $limit; // 尝试从缓存获取 if ($user_id) { $cached = $this->cache_manager->get_cached_recommendations($cache_key, $user_id); if ($cached !== false) { return $cached; } } // 根据策略选择推荐算法 $recommendations = array(); switch ($strategy) { case 'content_based': $recommendations = $this->get_content_based_recommendations($user_id, $limit); break; case 'collaborative': $recommendations = $this->get_collaborative_recommendations($user_id, $limit); break; case 'popular': $recommendations = $this->get_popular_recommendations($limit); break; case 'hybrid': default: $recommendations = $this->get_hybrid_recommendations($user_id, $limit); break; } // 过滤掉用户已经看过的内容 $recommendations = $this->filter_viewed_content($recommendations, $user_id); // 缓存结果 if ($user_id) { $this->cache_manager->set_cached_recommendations($cache_key, $recommendations, $user_id); } return $recommendations; } // 基于内容的推荐(根据用户兴趣标签) private function get_content_based_recommendations($user_id, $limit) { if (!$user_id) { return $this->get_popular_recommendations($limit); } // 获取用户兴趣标签 $user_interests = $this->analyzer->get_user_interests($user_id, 5); if (empty($user_interests)) { return $this->get_popular_recommendations($limit); } global $wpdb; // 构建标签查询条件 $tag_conditions = array(); $params = array(); foreach ($user_interests as $interest) { $tag_conditions[] = "(t.name LIKE %s OR c.name LIKE %s)"; $params[] = '%' . $wpdb->esc_like($interest) . '%'; $params[] = '%' . $wpdb->esc_like($interest) . '%'; } $tag_where = implode(' OR ', $tag_conditions); $query = $wpdb->prepare( "SELECT p.ID, p.post_title, p.post_excerpt, p.post_date, ( -- 标签匹配度评分 SUM( CASE WHEN t.name IN (" . implode(',', array_fill(0, count($user_interests), '%s')) . ") THEN 3 WHEN c.name IN (" . implode(',', array_fill(0, count($user_interests), '%s')) . ") THEN 2 ELSE 0 END ) + -- 时间衰减因子(最近发布的内容权重更高) (DATEDIFF(NOW(), p.post_date) <= 7) * 2 + (DATEDIFF(NOW(), p.post_date) <= 30) * 1 ) as relevance_score FROM {$wpdb->posts} p LEFT JOIN {$wpdb->term_relationships} tr ON p.ID = tr.object_id LEFT JOIN {$wpdb->term_taxonomy} tt ON tr.term_taxonomy_id = tt.term_taxonomy_id LEFT JOIN {$wpdb->terms} t ON tt.term_id = t.term_id AND tt.taxonomy = 'post_tag' LEFT JOIN {$wpdb->terms} c ON tt.term_id = c.term_id AND tt.taxonomy = 'category' WHERE p.post_status = 'publish' AND p.post_type = 'post' AND p.ID NOT IN ( SELECT post_id FROM {$wpdb->prefix}user_behavior WHERE user_id = %d AND behavior_type = 'view' AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY) ) AND ({$tag_where}) GROUP BY p.ID HAVING relevance_score > 0 ORDER BY relevance_score DESC, p.post_date DESC LIMIT %d", ...array_merge($user_interests, $user_interests, array($user_id, $limit * 2)) ); $results = $wpdb->get_results($query); // 格式化结果 $recommendations = array(); foreach ($results as $post) { $recommendations[] = array( 'id' => $post->ID, 'title' => $post->post_title, 'excerpt' => wp_trim_words($post->post_excerpt, 20), 'date' => $post->post_date, 'score' => $post->relevance_score, 'type' => 'content_based', 'reason' => '基于您的兴趣标签推荐' ); } return array_slice($recommendations, 0, $limit); } // 协同过滤推荐(基于相似用户) private function get_collaborative_recommendations($user_id, $limit) { if (!$user_id) { return array(); } // 查找相似用户 $similar_users = $this->analyzer->find_similar_users($user_id, 3); if (empty($similar_users)) { return array(); } global $wpdb; // 获取相似用户喜欢但目标用户没看过的内容 $similar_user_ids = array(); foreach ($similar_users as $user) { $similar_user_ids[] = $user->user_id; } $similar_user_ids_str = implode(',', $similar_user_ids); $query = $wpdb->prepare( "SELECT ub.post_id, p.post_title, p.post_excerpt, p.post_date, COUNT(DISTINCT ub.user_id) as liked_by_users, SUM( CASE WHEN ub.behavior_type = 'like' THEN 2 WHEN ub.behavior_type = 'view' THEN 1 ELSE 0 END ) as engagement_score FROM {$wpdb->prefix}user_behavior ub INNER JOIN {$wpdb->posts} p
发表评论详细指南:在WordPress中开发集成在线简易图片批量处理与格式转换工具 摘要 随着互联网内容的日益丰富,图片处理已成为网站运营中不可或缺的环节。对于WordPress网站管理员和内容创作者而言,拥有一个集成的图片批量处理与格式转换工具可以极大提高工作效率。本文将详细介绍如何在WordPress中通过代码二次开发,实现一个功能完善的在线简易图片批量处理与格式转换工具,涵盖从需求分析、技术选型到具体实现的完整流程。 目录 引言:为什么WordPress需要集成图片处理工具 需求分析与功能规划 技术选型与环境准备 创建WordPress插件基础结构 实现图片上传与批量选择功能 开发图片批量处理核心功能 集成图片格式转换模块 优化用户界面与用户体验 安全性与性能优化 测试与部署 扩展功能与未来展望 结论 1. 引言:为什么WordPress需要集成图片处理工具 WordPress作为全球最流行的内容管理系统,拥有超过40%的网站市场份额。尽管其内置了基础的图片上传和编辑功能,但在实际运营中,用户常常面临以下痛点: 需要批量调整图片尺寸以适应不同展示场景 需要将图片转换为不同格式(如PNG转JPG、WebP转换) 缺乏对图片进行批量压缩以优化网站加载速度的工具 依赖外部图片处理软件,工作流程不连贯 通过开发一个集成在WordPress后台的图片批量处理工具,管理员可以直接在熟悉的环境中完成这些操作,无需切换不同应用程序,大大提高了内容管理效率。本文将指导您通过WordPress代码二次开发,实现这样一个实用工具。 2. 需求分析与功能规划 2.1 核心功能需求 批量图片上传:支持多文件同时上传 图片预览:上传后显示缩略图预览 批量处理选项: 尺寸调整(按比例、固定尺寸) 质量压缩 格式转换(JPG、PNG、GIF、WebP之间转换) 处理进度显示:实时显示处理进度 结果下载:打包下载处理后的图片 2.2 技术需求 前端使用React或Vue.js实现交互界面 后端使用PHP配合WordPress钩子机制 图片处理使用GD库或ImageMagick 支持AJAX异步处理,避免超时问题 2.3 用户界面规划 WordPress后台独立菜单页 拖拽上传区域 图片预览网格 处理选项侧边栏 进度指示器 结果操作按钮 3. 技术选型与环境准备 3.1 开发环境要求 WordPress 5.0+ PHP 7.4+(支持GD库或ImageMagick扩展) MySQL 5.6+ 现代浏览器支持(Chrome、Firefox、Edge等) 3.2 关键技术选择 图片处理库选择: GD库:PHP内置,无需额外安装,基础功能齐全 ImageMagick:功能更强大,支持更多格式,需要服务器安装 考虑到通用性,本指南将使用GD库作为主要图片处理引擎,同时提供ImageMagick备选方案。 前端框架选择:考虑到与WordPress的兼容性和开发效率,我们选择使用原生JavaScript配合少量jQuery,避免框架冲突。 3.3 开发工具准备 本地WordPress开发环境(如Local by Flywheel、XAMPP) 代码编辑器(VS Code、PHPStorm等) 版本控制系统(Git) 浏览器开发者工具 4. 创建WordPress插件基础结构 4.1 插件目录结构 wp-content/plugins/bulk-image-processor/ ├── bulk-image-processor.php # 主插件文件 ├── includes/ │ ├── class-image-processor.php # 图片处理核心类 │ ├── class-ajax-handler.php # AJAX处理类 │ └── class-format-converter.php # 格式转换类 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 后台样式 │ ├── js/ │ │ └── admin-script.js # 后台脚本 │ └── partials/ │ └── admin-display.php # 后台界面 ├── assets/ │ ├── images/ # 插件图片资源 │ └── fonts/ # 字体文件 └── languages/ # 国际化文件 4.2 主插件文件配置 <?php /** * Plugin Name: 批量图片处理器 * Plugin URI: https://yourwebsite.com/bulk-image-processor * Description: WordPress在线简易图片批量处理与格式转换工具 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: bulk-image-processor */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('BIP_VERSION', '1.0.0'); define('BIP_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('BIP_PLUGIN_URL', plugin_dir_url(__FILE__)); define('BIP_UPLOAD_DIR', wp_upload_dir()['basedir'] . '/bip-temp/'); define('BIP_UPLOAD_URL', wp_upload_dir()['baseurl'] . '/bip-temp/'); // 创建临时目录 if (!file_exists(BIP_UPLOAD_DIR)) { wp_mkdir_p(BIP_UPLOAD_DIR); } // 包含必要文件 require_once BIP_PLUGIN_DIR . 'includes/class-image-processor.php'; require_once BIP_PLUGIN_DIR . 'includes/class-ajax-handler.php'; require_once BIP_PLUGIN_DIR . 'includes/class-format-converter.php'; // 初始化插件 class Bulk_Image_Processor { 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() { // 后台菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 加载脚本和样式 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); // 初始化AJAX处理 $ajax_handler = new BIP_Ajax_Handler(); $ajax_handler->init(); // 插件激活/停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); } public function add_admin_menu() { add_menu_page( '批量图片处理器', '图片批量处理', 'manage_options', 'bulk-image-processor', array($this, 'display_admin_page'), 'dashicons-images-alt2', 30 ); } public function display_admin_page() { include BIP_PLUGIN_DIR . 'admin/partials/admin-display.php'; } public function enqueue_admin_assets($hook) { if ('toplevel_page_bulk-image-processor' !== $hook) { return; } // 加载CSS wp_enqueue_style( 'bip-admin-style', BIP_PLUGIN_URL . 'admin/css/admin-style.css', array(), BIP_VERSION ); // 加载JavaScript wp_enqueue_script( 'bip-admin-script', BIP_PLUGIN_URL . 'admin/js/admin-script.js', array('jquery'), BIP_VERSION, true ); // 本地化脚本,传递数据到JS wp_localize_script('bip-admin-script', 'bip_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('bip_ajax_nonce'), 'max_file_size' => wp_max_upload_size(), 'supported_formats' => array('jpg', 'jpeg', 'png', 'gif', 'webp') )); } public function activate() { // 创建数据库表(如果需要) // 设置默认选项 } public function deactivate() { // 清理临时文件 $this->cleanup_temp_files(); } private function cleanup_temp_files() { // 删除临时目录中的所有文件 $files = glob(BIP_UPLOAD_DIR . '*'); foreach ($files as $file) { if (is_file($file)) { unlink($file); } } } } // 启动插件 Bulk_Image_Processor::get_instance(); 5. 实现图片上传与批量选择功能 5.1 创建后台界面 <!-- admin/partials/admin-display.php --> <div class="wrap bip-container"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <div class="bip-upload-area" id="bip-dropzone"> <div class="bip-upload-icon"> <span class="dashicons dashicons-upload"></span> </div> <h3>拖放图片到这里,或点击选择文件</h3> <p>支持格式:JPG、PNG、GIF、WebP,最大文件大小:<?php echo size_format(wp_max_upload_size()); ?></p> <input type="file" id="bip-file-input" multiple accept="image/*" style="display: none;"> <button type="button" id="bip-select-files" class="button button-primary">选择图片</button> </div> <div class="bip-preview-container" id="bip-preview-container" style="display: none;"> <h3>已选择图片 (<span id="bip-selected-count">0</span>)</h3> <div class="bip-preview-grid" id="bip-preview-grid"> <!-- 图片预览将动态插入这里 --> </div> <div class="bip-preview-actions"> <button type="button" id="bip-remove-all" class="button">清除全部</button> <button type="button" id="bip-select-all" class="button">全选</button> </div> </div> <div class="bip-processing-options" id="bip-options-panel" style="display: none;"> <h3>处理选项</h3> <div class="bip-option-section"> <h4>尺寸调整</h4> <div class="bip-option-row"> <label> <input type="checkbox" id="bip-resize-enable"> 启用尺寸调整 </label> </div> <div class="bip-option-row bip-resize-options" style="display: none;"> <div class="bip-option-group"> <label>宽度:</label> <input type="number" id="bip-resize-width" min="1" placeholder="像素"> <label>高度:</label> <input type="number" id="bip-resize-height" min="1" placeholder="像素"> </div> <div class="bip-option-group"> <label>保持比例:</label> <input type="checkbox" id="bip-keep-aspect" checked> </div> </div> </div> <div class="bip-option-section"> <h4>格式转换</h4> <div class="bip-option-row"> <label>输出格式:</label> <select id="bip-output-format"> <option value="original">保持原格式</option> <option value="jpg">JPG</option> <option value="png">PNG</option> <option value="webp">WebP</option> <option value="gif">GIF</option> </select> </div> <div class="bip-option-row bip-jpg-quality" style="display: none;"> <label>JPG质量 (1-100):</label> <input type="range" id="bip-jpg-quality" min="1" max="100" value="85"> <span id="bip-quality-value">85</span> </div> </div> <div class="bip-option-section"> <h4>压缩选项</h4> <div class="bip-option-row"> <label> <input type="checkbox" id="bip-compress-enable"> 启用压缩优化 </label> </div> </div> <div class="bip-process-actions"> <button type="button" id="bip-start-process" class="button button-primary button-large">开始处理</button> </div> </div> <div class="bip-progress-container" id="bip-progress-container" style="display: none;"> <h3>处理进度</h3> <div class="bip-progress-bar"> <div class="bip-progress-fill" id="bip-progress-fill"></div> </div> <div class="bip-progress-text"> 正在处理:<span id="bip-current-file">-</span> </div> <div class="bip-progress-stats"> 已完成:<span id="bip-processed-count">0</span> / 总计:<span id="bip-total-count">0</span> </div> </div> <div class="bip-results-container" id="bip-results-container" style="display: none;"> <h3>处理完成</h3> <div class="bip-results-info"> <p>成功处理 <span id="bip-success-count">0</span> 张图片,失败 <span id="bip-failed-count">0</span> 张。</p> </div> <div class="bip-results-actions"> <button type="button" id="bip-download-all" class="button button-primary">下载全部</button> <button type="button" id="bip-process-again" class="button">再次处理</button> </div> </div> </div> 5.2 实现前端JavaScript功能 // admin/js/admin-script.js jQuery(document).ready(function($) { let selectedFiles = []; let processedFiles = []; // 初始化拖放上传 initDropzone(); // 文件选择按钮点击事件 $('#bip-select-files').on('click', function() { $('#bip-file-input').click(); }); // 文件输入变化事件 $('#bip-file-input').on('change', function(e) { handleFiles(e.target.files); }); // 初始化拖放区域 function initDropzone() { const dropzone = $('#bip-dropzone')[0]; dropzone.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); $(this).addClass('dragover'); }); dropzone.addEventListener('dragleave', function(e) { e.preventDefault(); e.stopPropagation(); $(this).removeClass('dragover'); }); dropzone.addEventListener('drop', function(e) { e.preventDefault(); e.stopPropagation(); $(this).removeClass('dragover'); const files = e.dataTransfer.files; handleFiles(files); }); } // 处理选择的文件 function handleFiles(files) { const supportedFormats = bip_ajax.supported_formats; for (let i = 0; i < files.length; i++) { const file = files[i]; const fileExt = file.name.split('.').pop().toLowerCase(); // 检查文件格式 if (!supportedFormats.includes(fileExt)) { alert(`文件 ${file.name} 格式不支持,已跳过。`); continue; } // 检查文件大小 if (file.size > bip_ajax.max_file_size) { alert(`文件 ${file.name} 太大,已跳过。`); continue; } // 添加到选择列表 if (!selectedFiles.some(f => f.name === file.name && f.size === file.size)) { selectedFiles.push(file); } } updatePreview(); } // 更新预览区域 function updatePreview() { const previewGrid = $('#bip-preview-grid'); previewGrid.empty(); if (selectedFiles.length === 0) { $('#bip-preview-container').hide(); $('#bip-options-panel').hide(); return; } $('#bip-preview-container').show(); $('#bip-options-panel').show(); $('#bip-selected-count').text(selectedFiles.length); selectedFiles.forEach((file, index) => { const reader = new FileReader(); reader.onload = function(e) { const previewItem = $(` <div class="bip-preview-item" data-index="${index}"> <div class="bip-preview-checkbox"> <input type="checkbox" class="bip-file-checkbox" checked data-index="${index}"> </div> <div class="bip-preview-image"> <img src="${e.target.result}" alt="${file.name}"> </div> <div class="bip-preview-info"> file.name}</div> <div class="bip-filesize">${formatFileSize(file.size)}</div> </div> <button type="button" class="bip-remove-file" data-index="${index}"> <span class="dashicons dashicons-no"></span> </button> </div> `); previewGrid.append(previewItem); }; reader.readAsDataURL(file); }); // 绑定移除按钮事件 $('.bip-remove-file').on('click', function() { const index = parseInt($(this).data('index')); selectedFiles.splice(index, 1); updatePreview(); }); // 绑定复选框事件 $('.bip-file-checkbox').on('change', function() { const index = parseInt($(this).data('index')); $(this).closest('.bip-preview-item').toggleClass('unselected', !this.checked); }); } // 全选/取消全选 $('#bip-select-all').on('click', function() { const allChecked = $('.bip-file-checkbox:checked').length === selectedFiles.length; $('.bip-file-checkbox').prop('checked', !allChecked).trigger('change'); }); // 清除全部 $('#bip-remove-all').on('click', function() { if (confirm('确定要清除所有已选择的图片吗?')) { selectedFiles = []; updatePreview(); } }); // 格式转换选项变化 $('#bip-output-format').on('change', function() { if ($(this).val() === 'jpg') { $('.bip-jpg-quality').show(); } else { $('.bip-jpg-quality').hide(); } }); // JPG质量滑块 $('#bip-jpg-quality').on('input', function() { $('#bip-quality-value').text($(this).val()); }); // 尺寸调整选项切换 $('#bip-resize-enable').on('change', function() { $('.bip-resize-options').toggle(this.checked); }); // 开始处理按钮 $('#bip-start-process').on('click', function() { startProcessing(); }); // 开始处理函数 function startProcessing() { // 获取选中的文件 const filesToProcess = []; $('.bip-file-checkbox:checked').each(function() { const index = parseInt($(this).data('index')); filesToProcess.push(selectedFiles[index]); }); if (filesToProcess.length === 0) { alert('请至少选择一张图片进行处理。'); return; } // 收集处理选项 const options = { resize: { enabled: $('#bip-resize-enable').is(':checked'), width: parseInt($('#bip-resize-width').val()) || 0, height: parseInt($('#bip-resize-height').val()) || 0, keepAspect: $('#bip-keep-aspect').is(':checked') }, format: $('#bip-output-format').val(), quality: parseInt($('#bip-jpg-quality').val()) || 85, compress: $('#bip-compress-enable').is(':checked') }; // 验证尺寸参数 if (options.resize.enabled) { if (options.resize.width <= 0 && options.resize.height <= 0) { alert('请至少指定宽度或高度中的一个值。'); return; } } // 显示进度条 $('#bip-options-panel').hide(); $('#bip-progress-container').show(); $('#bip-progress-fill').css('width', '0%'); $('#bip-total-count').text(filesToProcess.length); $('#bip-processed-count').text('0'); // 开始处理 processFiles(filesToProcess, options); } // 处理文件队列 async function processFiles(files, options) { processedFiles = []; for (let i = 0; i < files.length; i++) { const file = files[i]; // 更新进度显示 $('#bip-current-file').text(file.name); $('#bip-processed-count').text(i); const progress = ((i + 1) / files.length) * 100; $('#bip-progress-fill').css('width', progress + '%'); // 上传并处理文件 try { const result = await uploadAndProcessFile(file, options); processedFiles.push(result); } catch (error) { console.error('处理失败:', error); processedFiles.push({ success: false, filename: file.name, error: error.message }); } } // 处理完成 showResults(); } // 上传并处理单个文件 function uploadAndProcessFile(file, options) { return new Promise((resolve, reject) => { const formData = new FormData(); formData.append('action', 'bip_process_image'); formData.append('nonce', bip_ajax.nonce); formData.append('file', file); formData.append('options', JSON.stringify(options)); $.ajax({ url: bip_ajax.ajax_url, type: 'POST', data: formData, processData: false, contentType: false, dataType: 'json', success: function(response) { if (response.success) { resolve(response.data); } else { reject(new Error(response.data || '处理失败')); } }, error: function(xhr, status, error) { reject(new Error('上传失败: ' + error)); } }); }); } // 显示处理结果 function showResults() { const successCount = processedFiles.filter(f => f.success).length; const failedCount = processedFiles.length - successCount; $('#bip-success-count').text(successCount); $('#bip-failed-count').text(failedCount); $('#bip-progress-container').hide(); $('#bip-results-container').show(); } // 下载全部结果 $('#bip-download-all').on('click', function() { if (processedFiles.length === 0) return; // 创建ZIP文件 const zip = new JSZip(); processedFiles.forEach(file => { if (file.success && file.data) { // 将base64数据转换为二进制 const binaryData = atob(file.data.split(',')[1]); const array = []; for (let i = 0; i < binaryData.length; i++) { array.push(binaryData.charCodeAt(i)); } const blob = new Uint8Array(array); zip.file(file.filename, blob); } }); // 生成并下载ZIP zip.generateAsync({type: "blob"}) .then(function(content) { const link = document.createElement('a'); link.href = URL.createObjectURL(content); link.download = 'processed-images.zip'; link.click(); }); }); // 再次处理 $('#bip-process-again').on('click', function() { $('#bip-results-container').hide(); $('#bip-options-panel').show(); }); // 辅助函数:格式化文件大小 function formatFileSize(bytes) { if (bytes === 0) return '0 Bytes'; const k = 1024; const sizes = ['Bytes', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; } }); ## 6. 开发图片批量处理核心功能 ### 6.1 创建图片处理类 <?php// includes/class-image-processor.php class BIP_Image_Processor { private $image_resource = null; private $image_info = null; private $error = null; /** * 加载图片 */ public function load($file_path) { if (!file_exists($file_path)) { $this->error = '文件不存在: ' . $file_path; return false; } $this->image_info = getimagesize($file_path); if (!$this->image_info) { $this->error = '无法读取图片信息'; return false; } $mime_type = $this->image_info['mime']; switch ($mime_type) { case 'image/jpeg': case 'image/jpg': $this->image_resource = imagecreatefromjpeg($file_path); break; case 'image/png': $this->image_resource = imagecreatefrompng($file_path); break; case 'image/gif': $this->image_resource = imagecreatefromgif($file_path); break; case 'image/webp': if (function_exists('imagecreatefromwebp')) { $this->image_resource = imagecreatefromwebp($file_path); } else { $this->error = '服务器不支持WebP格式'; return false; } break; default: $this->error = '不支持的图片格式: ' . $mime_type; return false; } if (!$this->image_resource) { $this->error = '无法创建图片资源'; return false; } return true; } /** * 调整图片尺寸 */ public function resize($width, $height, $keep_aspect = true) { if (!$this->image_resource) { $this->error = '未加载图片'; return false; } $original_width = $this->image_info[0]; $original_height = $this->image_info[1]; // 计算新尺寸 if ($keep_aspect) { if ($width > 0 && $height > 0) { // 按最大边缩放 $width_ratio = $width / $original_width; $height_ratio = $height / $original_height; $ratio = min($width_ratio, $height_ratio); $new_width = round($original_width * $ratio); $new_height = round($original_height * $ratio); } elseif ($width > 0) { // 按宽度缩放 $ratio = $width / $original_width; $new_width = $width; $new_height = round($original_height * $ratio); } elseif ($height > 0) { // 按高度缩放 $ratio = $height / $original_height; $new_width = round($original_width * $ratio); $new_height = $height; } else { // 保持原尺寸 $new_width = $original_width; $new_height = $original_height; } } else { $new_width = $width > 0 ? $width : $original_width; $new_height = $height > 0 ? $height : $original_height; } // 创建新图片资源 $new_image = imagecreatetruecolor($new_width, $new_height); // 处理PNG和WebP的透明度 if ($this->image_info['mime'] == 'image/png' || $this->image_info['mime'] == 'image/webp') { imagealphablending($new_image, false); imagesavealpha($new_image, true); $transparent = imagecolorallocatealpha($new_image, 255, 255, 255, 127); imagefilledrectangle($new_image, 0, 0, $new_width, $new_height, $transparent); } // 调整尺寸 $result = imagecopyresampled( $new_image, $this->image_resource, 0, 0, 0, 0, $new_width, $new_height, $original_width, $original_height ); if (!$result) { $this->error = '调整尺寸失败'; return false; } // 替换原图片资源 imagedestroy($this->image_resource); $this->image_resource = $new_image; $this->image_info[0] = $new_width; $this->image_info[1] = $new_height; return true; } /** * 压缩图片 */ public function compress($quality = 85) { if (!$this->image_resource) { $this->error = '未加载图片'; return false; } // 对于JPG,使用指定的质量参数 // 对于PNG,使用压缩级别(0-9) // 这里主要针对JPG优化 return true; } /** * 保存图片到指定格式 */ public function save($file_path, $format = 'jpg', $quality = 85) { if (!$this->image_resource) { $this->error = '未加载图片'; return false; } $result = false; switch (strtolower($format)) { case 'jpg': case 'jpeg': $result = imagejpeg($this->image_resource, $file_path, $quality); break; case 'png': // PNG质量参数是压缩级别(0-9) $png_quality = 9 - round(($quality / 100) * 9); $result = imagepng($this->image_resource, $file_path, $png_quality); break; case 'gif': $result = imagegif($this->image_resource, $file_path); break; case 'webp': if (function_exists('imagewebp')) { $result = imagewebp($this->image_resource, $file_path, $quality); } else { $this->error = '服务器不支持WebP格式'; return false; } break; default: $this->error = '不支持的输出格式: ' . $format; return false; } if (!$result) { $this->error = '保存图片失败'; return false; } return true; } /** * 获取图片的base64编码 */ public function get_base64($format = 'jpg', $quality = 85) { if (!$this->image_resource) { $this->error = '未加载图片'; return false; } // 创建临时文件 $temp_file = tempnam(sys_get_temp_dir(), 'bip_'); if (!$this->save($temp_file, $format, $quality)) { unlink($temp_file); return false; } // 读取文件内容并转换为base64 $file_content = file_get_contents($temp_file); $base64 = 'data:image/' . $format . ';base64,' . base64_encode($file_content); // 清理临时文件 unlink($temp_file); return $base64; } /** * 获取错误信息 */ public function get_error() { return $this->error; } /** * 清理资源 */ public function cleanup() { if ($this->image_resource) { imagedestroy($this->image_resource); $this->image_resource = null; } } /** * 获取支持的图片格式 */ public static function get_supported_formats() { $formats = array('jpg', 'jpeg', 'png', 'gif'); if (function_exists('imagewebp')) { $formats[] = 'webp'; } return $formats; } /** * 批量处理图片 */ public static function batch_process($files, $options) { $results = array(); foreach ($files as $index => $file) { $processor = new self(); // 加载图片 if (!$processor->load($file['tmp_name'])) { $results[] = array( 'success' => false, 'filename' => $file['name'], 'error' => $processor->get_error() ); continue; } // 调整尺寸 if ($options['resize']['enabled']) { $width = $options['resize']['width']; $height = $options['resize']['height']; $keep_aspect = $options['resize']['keepAspect']; if (!$processor->resize($width, $height, $keep_aspect)) { $results[] = array( 'success' => false, 'filename' => $file['name'], 'error' => $processor->get_error() ); $processor->cleanup(); continue; } } // 压缩 if ($options['compress']) { $processor->compress($options['quality']); } // 获取处理后的图片 $output_format = $options['format'] == 'original' ? pathinfo($file['name'], PATHINFO_EXTENSION) : $options['format']; $base64_data = $processor->get_base64($output_format, $options['quality']); if ($base64_data) { $new_filename = pathinfo($file['name'], PATHINFO_FILENAME) . '_processed.' . $output_format; $results[] = array( 'success' => true, 'filename' => $new_filename, 'data' => $base64_data, 'format' => $output_format, 'size' => strlen(base64_decode(explode(',', $base64_data)[1])) ); } else { $results[] = array( 'success' => false, 'filename' => $file['name'], 'error' => $processor->get_error() ); } $processor->cleanup(); } return $results; } } ## 7. 集成图片格式转换模块
发表评论手把手教学:为WordPress网站添加在线协同文档编辑与版本管理功能 引言:为什么网站需要协同编辑功能? 在当今数字化工作环境中,协同办公已成为企业和团队提高效率的关键。根据Statista的数据,2023年全球协同软件市场规模已达到138亿美元,预计到2027年将增长至287亿美元。对于拥有WordPress网站的企业、教育机构或内容创作团队而言,集成在线协同文档编辑功能可以显著提升团队协作效率,减少邮件往来,实现实时内容共创。 传统的WordPress内容编辑方式存在明显局限:单用户编辑锁定机制导致多人协作困难;版本管理功能有限,难以追踪每次修改;缺乏实时协同编辑体验,团队成员无法同时处理同一文档。本教程将指导您通过代码二次开发,为WordPress网站添加类似Google Docs的协同编辑与版本管理功能,而无需依赖昂贵的第三方服务。 第一部分:项目规划与技术选型 1.1 功能需求分析 在开始编码前,我们需要明确要实现的协同编辑系统应包含以下核心功能: 实时协同编辑:支持多用户同时编辑同一文档,实时显示他人光标位置和编辑内容 版本控制系统:完整记录文档修改历史,支持版本对比与回滚 用户权限管理:基于WordPress用户角色设置文档访问和编辑权限 冲突解决机制:智能处理编辑冲突,确保数据一致性 评论与批注系统:支持文档内评论和批注功能 导出与分享:支持多种格式导出和链接分享功能 1.2 技术架构设计 我们将采用以下技术栈实现协同编辑功能: 前端编辑器:使用开源协同编辑器框架如Y.js或ShareDB 实时通信:WebSocket协议实现实时数据同步 后端框架:基于WordPress REST API扩展 数据存储:MySQL数据库存储文档内容和版本历史 版本控制:自定义版本管理算法或集成Git原理 1.3 开发环境准备 确保您的开发环境满足以下要求: WordPress 5.8或更高版本 PHP 7.4+(建议使用PHP 8.0+以获得更好性能) MySQL 5.7+或MariaDB 10.3+ Node.js环境(用于构建前端资源) WebSocket服务器(如Socket.io服务器) 第二部分:构建协同编辑核心系统 2.1 创建自定义文章类型 首先,我们需要创建一个新的自定义文章类型来存储协同文档: // 在主题的functions.php或自定义插件中添加 function register_collaborative_doc_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' => 'collab-doc'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'supports' => array('title', 'author'), 'show_in_rest' => true, // 启用REST API支持 ); register_post_type('collab_doc', $args); } add_action('init', 'register_collaborative_doc_type'); 2.2 设置WebSocket服务器 为了实现实时协同编辑,我们需要设置WebSocket服务器处理实时通信: // websocket-server.js - Node.js WebSocket服务器 const WebSocket = require('ws'); const http = require('http'); const server = http.createServer(); const wss = new WebSocket.Server({ server }); // 存储文档状态和连接 const documents = new Map(); wss.on('connection', (ws, request) => { const params = new URLSearchParams(request.url.split('?')[1]); const docId = params.get('docId'); const userId = params.get('userId'); if (!docId || !userId) { ws.close(); return; } // 初始化文档状态 if (!documents.has(docId)) { documents.set(docId, { content: '', users: new Map(), version: 0 }); } const doc = documents.get(docId); // 存储用户连接 doc.users.set(userId, { ws, cursorPosition: 0, lastActive: Date.now() }); // 发送当前文档状态给新用户 ws.send(JSON.stringify({ type: 'init', content: doc.content, version: doc.version, activeUsers: Array.from(doc.users.keys()).filter(id => id !== userId) })); // 广播新用户加入 broadcastToOthers(docId, userId, { type: 'user_joined', userId }); // 处理客户端消息 ws.on('message', (message) => { try { const data = JSON.parse(message); handleClientMessage(docId, userId, data); } catch (error) { console.error('消息解析错误:', error); } }); // 处理连接关闭 ws.on('close', () => { if (doc.users.has(userId)) { doc.users.delete(userId); broadcastToOthers(docId, userId, { type: 'user_left', userId }); } }); }); function handleClientMessage(docId, userId, data) { const doc = documents.get(docId); if (!doc) return; switch (data.type) { case 'edit': // 应用编辑操作 applyEdit(doc, data.operation); doc.version++; // 广播编辑给其他用户 broadcastToOthers(docId, userId, { type: 'update', operation: data.operation, version: doc.version, userId }); break; case 'cursor_move': // 广播光标移动 broadcastToOthers(docId, userId, { type: 'cursor_move', userId, position: data.position }); break; case 'selection_change': // 广播选择变化 broadcastToOthers(docId, userId, { type: 'selection_change', userId, selection: data.selection }); break; } } function applyEdit(doc, operation) { // 根据操作类型更新文档内容 // 这里需要实现操作转换(OT)或冲突无关数据类型(CRDT)逻辑 // 简化示例:直接替换内容 if (operation.type === 'insert') { doc.content = doc.content.slice(0, operation.position) + operation.text + doc.content.slice(operation.position); } else if (operation.type === 'delete') { doc.content = doc.content.slice(0, operation.position) + doc.content.slice(operation.position + operation.length); } } function broadcastToOthers(docId, excludeUserId, message) { const doc = documents.get(docId); if (!doc) return; doc.users.forEach((user, userId) => { if (userId !== excludeUserId && user.ws.readyState === WebSocket.OPEN) { user.ws.send(JSON.stringify(message)); } }); } // 启动服务器 const PORT = process.env.PORT || 8080; server.listen(PORT, () => { console.log(`WebSocket服务器运行在端口 ${PORT}`); }); 2.3 集成前端协同编辑器 创建前端编辑器界面,使用Y.js库实现协同编辑: <!-- collaborative-editor.php - 编辑器模板文件 --> <div id="collaborative-editor-container"> <div class="editor-header"> <h1 id="doc-title" contenteditable="true"><?php echo get_the_title(); ?></h1> <div class="editor-toolbar"> <button class="format-btn" data-format="bold">粗体</button> <button class="format-btn" data-format="italic">斜体</button> <button class="format-btn" data-format="underline">下划线</button> <select id="font-size"> <option value="12px">12px</option> <option value="14px">14px</option> <option value="16px" selected>16px</option> <option value="18px">18px</option> <option value="24px">24px</option> </select> <button id="save-version">保存版本</button> <button id="export-pdf">导出PDF</button> </div> <div class="user-presence"> <span>在线用户: </span> <div id="active-users"></div> </div> </div> <div class="editor-content"> <div id="editor" contenteditable="true"></div> </div> <div class="editor-sidebar"> <div class="version-history"> <h3>版本历史</h3> <ul id="version-list"></ul> </div> <div class="comments-section"> <h3>评论</h3> <div id="comments-container"></div> <textarea id="new-comment" placeholder="添加评论..."></textarea> <button id="add-comment">提交评论</button> </div> </div> </div> <script> // 协同编辑器前端JavaScript document.addEventListener('DOMContentLoaded', function() { const docId = <?php echo get_the_ID(); ?>; const userId = <?php echo get_current_user_id(); ?>; // 初始化Y.js协同编辑 const ydoc = new Y.Doc(); const ytext = ydoc.getText('content'); const editor = document.getElementById('editor'); // 连接WebSocket服务器 const ws = new WebSocket(`ws://localhost:8080?docId=${docId}&userId=${userId}`); // 绑定Y.js到编辑器 const binding = new Y.QuillBinding(ytext, editor); // 监听WebSocket消息 ws.onmessage = function(event) { const data = JSON.parse(event.data); switch(data.type) { case 'init': // 初始化文档内容 ytext.delete(0, ytext.length); ytext.insert(0, data.content); updateActiveUsers(data.activeUsers); break; case 'update': // 应用远程更新 applyRemoteUpdate(data.operation); break; case 'user_joined': addActiveUser(data.userId); break; case 'user_left': removeActiveUser(data.userId); break; case 'cursor_move': showRemoteCursor(data.userId, data.position); break; } }; // 发送本地编辑到服务器 ydoc.on('update', function(update) { ws.send(JSON.stringify({ type: 'edit', operation: extractOperationFromUpdate(update) })); }); // 光标移动跟踪 editor.addEventListener('keyup', function() { const selection = window.getSelection(); const position = getCursorPosition(editor, selection); ws.send(JSON.stringify({ type: 'cursor_move', position: position })); }); // 初始化版本历史 loadVersionHistory(docId); // 保存版本功能 document.getElementById('save-version').addEventListener('click', function() { saveDocumentVersion(docId, ytext.toString()); }); }); // 辅助函数 function updateActiveUsers(userIds) { const container = document.getElementById('active-users'); container.innerHTML = ''; userIds.forEach(userId => { const userElement = document.createElement('span'); userElement.className = 'active-user'; userElement.textContent = `用户${userId}`; userElement.style.backgroundColor = getUserColor(userId); container.appendChild(userElement); }); } function getUserColor(userId) { // 根据用户ID生成一致的颜色 const colors = ['#FF6B6B', '#4ECDC4', '#FFD166', '#06D6A0', '#118AB2']; return colors[userId % colors.length]; } function loadVersionHistory(docId) { // 通过AJAX加载版本历史 fetch(`/wp-json/collab/v1/document/${docId}/versions`) .then(response => response.json()) .then(versions => { const list = document.getElementById('version-list'); list.innerHTML = ''; versions.forEach(version => { const li = document.createElement('li'); li.innerHTML = ` <span>${version.date}</span> <span>${version.author}</span> <button onclick="restoreVersion(${version.id})">恢复</button> <button onclick="compareVersion(${version.id})">对比</button> `; list.appendChild(li); }); }); } function saveDocumentVersion(docId, content) { fetch(`/wp-json/collab/v1/document/${docId}/version`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': collabVars.nonce }, body: JSON.stringify({ content: content, comment: document.getElementById('version-comment')?.value || '手动保存' }) }) .then(response => response.json()) .then(data => { if(data.success) { alert('版本保存成功'); loadVersionHistory(docId); } }); } </script> 第三部分:实现版本管理系统 3.1 设计版本数据库结构 我们需要创建自定义数据库表来存储文档版本历史: // 创建版本管理数据库表 function create_version_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'collab_doc_versions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, doc_id bigint(20) UNSIGNED NOT NULL, version_number int(11) NOT NULL, content longtext NOT NULL, author_id bigint(20) UNSIGNED NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, change_summary text, parent_version_id bigint(20) UNSIGNED DEFAULT NULL, PRIMARY KEY (id), KEY doc_id (doc_id), KEY author_id (author_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } register_activation_hook(__FILE__, 'create_version_tables'); 3.2 实现版本控制API 创建REST API端点处理版本管理操作: // 版本管理REST API add_action('rest_api_init', function() { // 获取文档版本列表 register_rest_route('collab/v1', '/document/(?P<id>d+)/versions', array( 'methods' => 'GET', 'callback' => 'get_document_versions', 'permission_callback' => 'check_document_permission' )); // 创建新版本 register_rest_route('collab/v1', '/document/(?P<id>d+)/version', array( 'methods' => 'POST', 'callback' => 'create_document_version', 'permission_callback' => 'check_document_permission' )); // 恢复特定版本 register_rest_route('collab/v1', '/version/(?P<id>d+)/restore', array( 'methods' => 'POST', 'callback' => 'restore_document_version', 'permission_callback' => 'check_document_permission' )); // 比较两个版本 register_rest_route('collab/v1', '/compare-versions', array( 'methods' => 'GET', 'callback' => 'compare_versions', 'permission_callback' => 'check_document_permission' )); }); function get_document_versions($request) { global $wpdb; $doc_id = $request['id']; $table_name = $wpdb->prefix . 'collab_doc_versions'; $versions = $wpdb->get_results($wpdb->prepare( "SELECT v.*, u.user_login as author_name FROM $table_name v LEFT JOIN {$wpdb->users} u ON v.author_id = u.ID WHERE v.doc_id = %d ORDER BY v.created_at DESC LIMIT 50", $doc_id )); // 格式化返回数据 $formatted_versions = array(); foreach ($versions as $version) { $formatted_versions[] = array( 'id' => $version->id, 'version_number' => $version->version_number, 'created_at' => $version->created_at, 'author' => $version->author_name, 'change_summary' => $version->change_summary, 'content_preview' => wp_trim_words($version->content, 20) ); } return rest_ensure_response($formatted_versions); } function create_document_version($request) { global $wpdb; $doc_id = $request['id']; current_user_id(); $content = sanitize_text_field($request['content']); $comment = sanitize_text_field($request['comment']); // 获取当前最高版本号 $table_name = $wpdb->prefix . 'collab_doc_versions'; $latest_version = $wpdb->get_var($wpdb->prepare( "SELECT MAX(version_number) FROM $table_name WHERE doc_id = %d", $doc_id )); $new_version = $latest_version ? $latest_version + 1 : 1; // 插入新版本 $result = $wpdb->insert( $table_name, array( 'doc_id' => $doc_id, 'version_number' => $new_version, 'content' => $content, 'author_id' => $user_id, 'change_summary' => $comment, 'parent_version_id' => $latest_version ? $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE doc_id = %d AND version_number = %d", $doc_id, $latest_version )) : null ), array('%d', '%d', '%s', '%d', '%s', '%d') ); if ($result) { return rest_ensure_response(array( 'success' => true, 'version_id' => $wpdb->insert_id, 'version_number' => $new_version )); } return new WP_Error('version_creation_failed', '版本创建失败', array('status' => 500)); } function restore_document_version($request) { global $wpdb; $version_id = $request['id']; // 获取版本内容 $table_name = $wpdb->prefix . 'collab_doc_versions'; $version = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $version_id )); if (!$version) { return new WP_Error('version_not_found', '版本不存在', array('status' => 404)); } // 更新文档当前内容 update_post_meta($version->doc_id, '_current_content', $version->content); // 创建恢复记录 $user_id = get_current_user_id(); $latest_version = $wpdb->get_var($wpdb->prepare( "SELECT MAX(version_number) FROM $table_name WHERE doc_id = %d", $version->doc_id )); $wpdb->insert( $table_name, array( 'doc_id' => $version->doc_id, 'version_number' => $latest_version + 1, 'content' => $version->content, 'author_id' => $user_id, 'change_summary' => sprintf('恢复到版本 %d', $version->version_number), 'parent_version_id' => $version->id ), array('%d', '%d', '%s', '%d', '%s', '%d') ); return rest_ensure_response(array( 'success' => true, 'message' => '版本恢复成功' )); } function compare_versions($request) { $version1_id = $request->get_param('v1'); $version2_id = $request->get_param('v2'); global $wpdb; $table_name = $wpdb->prefix . 'collab_doc_versions'; $version1 = $wpdb->get_row($wpdb->prepare( "SELECT content FROM $table_name WHERE id = %d", $version1_id )); $version2 = $wpdb->get_row($wpdb->prepare( "SELECT content FROM $table_name WHERE id = %d", $version2_id )); if (!$version1 || !$version2) { return new WP_Error('versions_not_found', '版本不存在', array('status' => 404)); } // 使用文本差异算法比较版本 $diff = compute_text_diff($version1->content, $version2->content); return rest_ensure_response(array( 'diff' => $diff, 'version1' => array('id' => $version1_id), 'version2' => array('id' => $version2_id) )); } function compute_text_diff($text1, $text2) { // 使用PHP内置的差异计算函数 require_once(ABSPATH . 'wp-admin/includes/diff.php'); $text1_lines = explode("n", $text1); $text2_lines = explode("n", $text2); $diff = new Text_Diff($text1_lines, $text2_lines); $renderer = new Text_Diff_Renderer_inline(); return $renderer->render($diff); } ### 3.3 自动版本保存机制 实现智能的自动版本保存功能: // 自动版本保存机制class AutoVersionSaver { private $save_threshold = 30; // 每30秒检查一次 private $change_threshold = 10; // 至少10个字符变化才保存 public function __construct() { add_action('wp_ajax_save_auto_version', array($this, 'handle_auto_save')); add_action('wp_ajax_nopriv_save_auto_version', array($this, 'handle_auto_save')); } public function handle_auto_save() { $doc_id = intval($_POST['doc_id']); $content = wp_unslash($_POST['content']); $last_saved_version = isset($_POST['last_saved']) ? $_POST['last_saved'] : null; // 验证权限 if (!current_user_can('edit_post', $doc_id)) { wp_die('权限不足'); } // 获取上次保存的内容 $last_content = $this->get_last_saved_content($doc_id, $last_saved_version); // 计算变化量 $changes = $this->calculate_changes($last_content, $content); // 如果变化足够大,则保存新版本 if ($changes['change_count'] >= $this->change_threshold) { $this->save_minor_version($doc_id, $content, $changes['summary']); wp_send_json_success(array( 'saved' => true, 'change_summary' => $changes['summary'], 'timestamp' => current_time('mysql') )); } else { wp_send_json_success(array( 'saved' => false, 'message' => '变化太小,未保存版本' )); } } private function get_last_saved_content($doc_id, $last_saved_version) { global $wpdb; $table_name = $wpdb->prefix . 'collab_doc_versions'; if ($last_saved_version) { $content = $wpdb->get_var($wpdb->prepare( "SELECT content FROM $table_name WHERE id = %d", $last_saved_version )); } else { $content = $wpdb->get_var($wpdb->prepare( "SELECT content FROM $table_name WHERE doc_id = %d ORDER BY created_at DESC LIMIT 1", $doc_id )); } return $content ?: ''; } private function calculate_changes($old_content, $new_content) { // 简单计算变化:字符差异 $old_length = strlen($old_content); $new_length = strlen($new_content); $length_change = $new_length - $old_length; // 使用更高级的差异检测(可选) similar_text($old_content, $new_content, $similarity); $change_percentage = 100 - $similarity; // 生成变化摘要 $summary = sprintf( '长度变化: %+d 字符, 相似度: %.1f%%', $length_change, $similarity ); return array( 'change_count' => abs($length_change), 'summary' => $summary, 'similarity' => $similarity ); } private function save_minor_version($doc_id, $content, $summary) { global $wpdb; $table_name = $wpdb->prefix . 'collab_doc_versions'; $user_id = get_current_user_id(); // 获取当前版本号 $latest_version = $wpdb->get_var($wpdb->prepare( "SELECT MAX(version_number) FROM $table_name WHERE doc_id = %d", $doc_id )); $new_version = $latest_version ? $latest_version + 1 : 1; $wpdb->insert( $table_name, array( 'doc_id' => $doc_id, 'version_number' => $new_version, 'content' => $content, 'author_id' => $user_id, 'change_summary' => '自动保存: ' . $summary, 'parent_version_id' => $latest_version ? $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE doc_id = %d AND version_number = %d", $doc_id, $latest_version )) : null ), array('%d', '%d', '%s', '%d', '%s', '%d') ); } } new AutoVersionSaver(); ## 第四部分:用户权限与访问控制 ### 4.1 扩展WordPress权限系统 // 协同文档权限管理class CollaborativeDocPermissions { public function __construct() { add_filter('user_has_cap', array($this, 'add_collab_capabilities'), 10, 4); add_action('admin_init', array($this, 'setup_roles_and_capabilities')); } public function setup_roles_and_capabilities() { $roles = array('administrator', 'editor', 'author', 'contributor', 'subscriber'); foreach ($roles as $role_name) { $role = get_role($role_name); if ($role) { // 基础权限 $role->add_cap('read_collab_doc'); // 根据角色分配不同权限 switch ($role_name) { case 'administrator': case 'editor': $role->add_cap('edit_collab_docs'); $role->add_cap('edit_others_collab_docs'); $role->add_cap('publish_collab_docs'); $role->add_cap('delete_collab_docs'); $role->add_cap('manage_collab_doc_settings'); break; case 'author': $role->add_cap('edit_collab_docs'); $role->add_cap('publish_collab_docs'); $role->add_cap('delete_collab_docs'); break; case 'contributor': $role->add_cap('edit_collab_docs'); break; } } } } public function add_collab_capabilities($allcaps, $caps, $args, $user) { $requested_capability = $args[0]; $post_id = isset($args[2]) ? $args[2] : 0; // 处理协同文档特定权限 if (strpos($requested_capability, 'collab_doc') !== false && $post_id) { $post_type = get_post_type($post_id); if ($post_type === 'collab_doc') { // 检查文档特定权限设置 $doc_permissions = get_post_meta($post_id, '_collab_permissions', true); if (!empty($doc_permissions)) { $user_id = $user->ID; // 文档所有者有完全权限 $post = get_post($post_id); if ($post && $post->post_author == $user_id) { $allcaps['edit_collab_docs'] = true; $allcaps['edit_others_collab_docs'] = true; $allcaps['delete_collab_docs'] = true; return $allcaps; } // 检查用户是否在允许列表中 if (isset($doc_permissions['allowed_users'])) { $allowed_users = $doc_permissions['allowed_users']; if (in_array($user_id, $allowed_users)) { $permission_level = $doc_permissions['user_levels'][$user_id] ?? 'viewer'; switch ($permission_level) { case 'editor': $allcaps['edit_collab_docs'] = true; break; case 'commenter': $allcaps['comment_collab_docs'] = true; break; case 'viewer': $allcaps['read_collab_doc'] = true; break; } } } // 检查用户组权限 if (isset($doc_permissions['allowed_roles'])) { $user_roles = $user->roles; foreach ($user_roles as $role) { if (in_array($role, $doc_permissions['allowed_roles'])) { $allcaps['read_collab_doc'] = true; if (in_array($role, $doc_permissions['editor_roles'])) { $allcaps['edit_collab_docs'] = true; } break; } } } } } } return $allcaps; } // 文档共享功能 public static function share_document($doc_id, $user_emails, $permission_level = 'viewer') { $user_ids = array(); foreach ($user_emails as $email) { $user = get_user_by('email', $email); if ($user) { $user_ids[] = $user->ID; // 发送通知邮件 self::send_sharing_notification($user, $doc_id, $permission_level); } } // 更新文档权限设置 $permissions = get_post_meta($doc_id, '_collab_permissions', true) ?: array(); if (!isset($permissions['allowed_users'])) { $permissions['allowed_users'] = array(); } foreach ($user_ids as $user_id) { if (!in_array($user_id, $permissions['allowed_users'])) { $permissions['allowed_users'][] = $user_id; } $permissions['user_levels'][$user_id] = $permission_level; } update_post_meta($doc_id, '_collab_permissions', $permissions); return count($user_ids); } private static function send_sharing_notification($user, $doc_id, $permission_level) { $doc_title = get_the_title($doc_id); $doc_link = get_permalink($doc_id); $admin_email = get_option('admin_email'); $subject = sprintf('您被邀请协作编辑文档: %s', $doc_title); $message = sprintf( "您好 %s,nn您被邀请%s文档《%s》。nn文档链接: %snn权限级别: %snn请点击链接开始协作。nn此邮件由系统自动发送,请勿回复。", $user->display_name, $permission_level === 'viewer' ? '查看' : ($permission_level === 'commenter' ? '评论' : '编辑'), $doc_title, $doc_link, $permission_level ); wp_mail($user->user_email, $subject, $message, array( 'From: ' . get_bloginfo('name') . ' <' . $admin_email . '>' )); } } new CollaborativeDocPermissions(); ### 4.2 实时用户状态显示 // 实时用户状态管理class UserPresenceManager { constructor(docId) { this.docId = docId; this.activeUsers = new Map(); this.userColors = new Map(); this.cursorPositions = new Map(); this.initWebSocket(); this.setupHeartbeat(); } initWebSocket() { this.ws = new WebSocket(`ws://localhost:8080/presence?docId=${this.docId}&userId=${this.userId}`); this.ws.onmessage = (event) => { const data = JSON.parse(event.data); this.handlePresenceUpdate(data); }; this.ws.onopen = () => { this.sendHeartbeat(); }; } handlePresenceUpdate(data) { switch(data.type) { case 'user_joined': this.addUser(data.userId, data.userInfo); break; case 'user_left': this.removeUser(data.userId); break; case 'user_activity': this.updateUserActivity(data.userId, data.activity); break; case 'cursor_update': this.updateCursor(data.userId, data.position, data.selection); break; } } addUser(userId, userInfo) { if (!this.activeUsers.has(userId)) { this.activeUsers.set(userId, { ...userInfo, lastActive: Date.now(), color: this.getUserColor(userId) }); this.updateUI(); // 显示通知 this.showNotification(`${userInfo.name} 加入了文档`); } } removeUser(userId) { if (this.activeUsers.has(userId)) { const user = this.activeUsers.get(userId); this.activeUsers.delete(userId); this.updateUI(); // 显示通知 this.showNotification(`${user.name} 离开了文档`); // 移除光标显示 this.removeCursor(userId); } } updateCursor(userId,
发表评论WordPress插件开发教程:集成实时翻译与跨语言即时通讯工具 引言:WordPress插件开发的无限可能 在当今全球化的互联网环境中,多语言支持和跨语言交流已成为网站建设的重要需求。WordPress作为全球最受欢迎的内容管理系统,其强大的插件架构为开发者提供了无限的可能性。本教程将深入探讨如何通过WordPress插件开发,集成网站实时翻译与跨语言即时通讯工具,将常用互联网小工具功能融入您的WordPress网站。 随着人工智能和自然语言处理技术的飞速发展,实时翻译和即时通讯已成为现代网站的标配功能。通过本教程,您将学习如何利用WordPress的插件架构,创建功能强大、用户友好的多语言交流工具,不仅提升用户体验,还能拓展网站的国际化潜力。 第一部分:WordPress插件开发基础 1.1 WordPress插件架构概述 WordPress插件系统基于事件驱动的钩子(Hooks)机制,主要包括动作(Actions)和过滤器(Filters)。理解这一架构是开发高级插件的基础。 创建插件基本结构: <?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('MLC_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('MLC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('MLC_VERSION', '1.0.0'); // 初始化插件 function mlc_init() { // 插件初始化代码 } add_action('plugins_loaded', 'mlc_init'); 1.2 插件开发最佳实践 在开始具体功能开发前,遵循以下最佳实践至关重要: 命名空间和前缀:为所有函数、类和变量添加唯一前缀,避免与其他插件冲突 国际化支持:使用WordPress的翻译函数,使插件支持多语言 安全性考虑:对所有用户输入进行验证和清理,使用nonce保护表单 性能优化:合理使用缓存,避免不必要的数据库查询 错误处理:实现完善的错误处理机制,提供有意义的错误信息 第二部分:集成实时翻译功能 2.1 选择翻译API服务 市场上有多种翻译API可供选择,包括Google Translate API、Microsoft Azure Translator、DeepL API等。本教程以Google Translate API为例,但原理适用于其他服务。 API选择考虑因素: 翻译质量和语言支持 价格和免费额度 API调用限制和响应时间 数据隐私和合规性 2.2 实现翻译功能核心类 创建翻译处理类,封装API调用和缓存逻辑: class MLC_Translator { private $api_key; private $cache_enabled; private $cache_expiry; public function __construct($api_key) { $this->api_key = $api_key; $this->cache_enabled = true; $this->cache_expiry = 24 * HOUR_IN_SECONDS; // 缓存24小时 // 初始化缓存表 $this->init_cache_table(); } /** * 翻译文本 * @param string $text 待翻译文本 * @param string $target_lang 目标语言代码 * @param string $source_lang 源语言代码(可选,自动检测) * @return string 翻译结果 */ public function translate($text, $target_lang, $source_lang = 'auto') { // 检查缓存 if ($this->cache_enabled) { $cached = $this->get_cached_translation($text, $source_lang, $target_lang); if ($cached !== false) { return $cached; } } // 调用翻译API $translated_text = $this->call_translation_api($text, $target_lang, $source_lang); // 缓存结果 if ($this->cache_enabled && $translated_text) { $this->cache_translation($text, $source_lang, $target_lang, $translated_text); } return $translated_text; } /** * 调用Google翻译API */ private function call_translation_api($text, $target_lang, $source_lang) { $url = 'https://translation.googleapis.com/language/translate/v2'; $args = array( 'method' => 'POST', 'timeout' => 15, 'headers' => array( 'Content-Type' => 'application/json', ), 'body' => json_encode(array( 'q' => $text, 'target' => $target_lang, 'source' => $source_lang, 'format' => 'text', 'key' => $this->api_key )) ); $response = wp_remote_post($url, $args); if (is_wp_error($response)) { // 错误处理 error_log('翻译API调用失败: ' . $response->get_error_message()); return $text; // 返回原文作为降级方案 } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['data']['translations'][0]['translatedText'])) { return $data['data']['translations'][0]['translatedText']; } return $text; } /** * 初始化翻译缓存表 */ private function init_cache_table() { global $wpdb; $table_name = $wpdb->prefix . 'mlc_translation_cache'; // 检查表是否存在 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, source_text text NOT NULL, source_lang varchar(10) NOT NULL, target_lang varchar(10) NOT NULL, translated_text text NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY source_lang (source_lang), KEY target_lang (target_lang), KEY source_text_hash (source_text(100), source_lang, target_lang) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } } // 其他缓存相关方法... } 2.3 前端翻译界面实现 创建用户友好的前端翻译界面,允许访客实时翻译页面内容: class MLC_Frontend_Translator { public function __construct() { // 添加前端资源 add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); // 添加翻译工具栏 add_action('wp_footer', array($this, 'add_translation_toolbar')); // AJAX翻译端点 add_action('wp_ajax_mlc_translate_text', array($this, 'ajax_translate_text')); add_action('wp_ajax_nopriv_mlc_translate_text', array($this, 'ajax_translate_text')); } public function enqueue_scripts() { // 加载CSS样式 wp_enqueue_style( 'mlc-frontend-style', MLC_PLUGIN_URL . 'assets/css/frontend.css', array(), MLC_VERSION ); // 加载JavaScript wp_enqueue_script( 'mlc-frontend-script', MLC_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), MLC_VERSION, true ); // 本地化脚本,传递参数给JavaScript wp_localize_script('mlc-frontend-script', 'mlc_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('mlc_translate_nonce'), 'default_language' => get_locale(), 'available_languages' => $this->get_available_languages() )); } public function add_translation_toolbar() { if (!is_user_logged_in() && !current_user_can('edit_posts')) { // 可根据需要调整显示条件 return; } $languages = $this->get_available_languages(); ?> <div id="mlc-translation-toolbar" class="mlc-hidden"> <div class="mlc-toolbar-header"> <span class="mlc-toolbar-title"><?php _e('实时翻译', 'mlc'); ?></span> <button class="mlc-close-toolbar">×</button> </div> <div class="mlc-toolbar-body"> <div class="mlc-translation-controls"> <div class="mlc-language-selectors"> <div class="mlc-language-from"> <label for="mlc-source-lang"><?php _e('从', 'mlc'); ?></label> <select id="mlc-source-lang"> <option value="auto"><?php _e('自动检测', 'mlc'); ?></option> <?php foreach ($languages as $code => $name): ?> <option value="<?php echo esc_attr($code); ?>"><?php echo esc_html($name); ?></option> <?php endforeach; ?> </select> </div> <div class="mlc-language-swap"> <button id="mlc-swap-languages">⇄</button> </div> <div class="mlc-language-to"> <label for="mlc-target-lang"><?php _e('到', 'mlc'); ?></label> <select id="mlc-target-lang"> <?php foreach ($languages as $code => $name): ?> <option value="<?php echo esc_attr($code); ?>" <?php selected($code, 'en_US'); ?>> <?php echo esc_html($name); ?> </option> <?php endforeach; ?> </select> </div> </div> <div class="mlc-text-areas"> <textarea id="mlc-source-text" placeholder="<?php esc_attr_e('输入要翻译的文本...', 'mlc'); ?>"></textarea> <textarea id="mlc-translated-text" readonly placeholder="<?php esc_attr_e('翻译结果将显示在这里...', 'mlc'); ?>"></textarea> </div> <div class="mlc-translation-actions"> <button id="mlc-translate-btn" class="mlc-primary-btn"> <?php _e('翻译', 'mlc'); ?> </button> <button id="mlc-clear-text" class="mlc-secondary-btn"> <?php _e('清空', 'mlc'); ?> </button> <button id="mlc-speak-source" class="mlc-icon-btn" title="<?php esc_attr_e('朗读原文', 'mlc'); ?>"> 🔊 </button> <button id="mlc-speak-translation" class="mlc-icon-btn" title="<?php esc_attr_e('朗读翻译', 'mlc'); ?>"> 🔊 </button> <button id="mlc-copy-translation" class="mlc-icon-btn" title="<?php esc_attr_e('复制翻译', 'mlc'); ?>"> 📋 </button> </div> </div> <div class="mlc-page-translation"> <h4><?php _e('翻译当前页面', 'mlc'); ?></h4> <select id="mlc-page-target-lang"> <?php foreach ($languages as $code => $name): ?> <option value="<?php echo esc_attr($code); ?>"><?php echo esc_html($name); ?></option> <?php endforeach; ?> </select> <button id="mlc-translate-page" class="mlc-primary-btn"> <?php _e('翻译页面', 'mlc'); ?> </button> <div class="mlc-page-translation-status"></div> </div> </div> </div> <button id="mlc-toggle-toolbar" class="mlc-floating-btn" title="<?php esc_attr_e('打开翻译工具', 'mlc'); ?>"> 🌐 </button> <?php } public function ajax_translate_text() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'mlc_translate_nonce')) { wp_die('安全验证失败'); } // 获取参数 $text = sanitize_text_field($_POST['text']); $target_lang = sanitize_text_field($_POST['target_lang']); $source_lang = sanitize_text_field($_POST['source_lang']); // 调用翻译器 $translator = new MLC_Translator(get_option('mlc_google_api_key')); $translated = $translator->translate($text, $target_lang, $source_lang); // 返回结果 wp_send_json_success(array( 'translated_text' => $translated, 'source_lang' => $source_lang === 'auto' ? $this->detect_language($text) : $source_lang )); } // 其他辅助方法... } 第三部分:实现跨语言即时通讯工具 3.1 设计通讯系统架构 跨语言即时通讯系统需要处理以下核心功能: 用户身份管理(注册/登录/匿名) 实时消息传递 消息自动翻译 对话管理 通知系统 系统架构设计: 前端:使用WebSocket或长轮询实现实时通信 后端:WordPress REST API处理消息逻辑 数据库:存储用户、对话和消息数据 翻译服务:集成到消息处理流程中 3.2 创建聊天数据库表 class MLC_Chat_Database { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 对话表 $conversations_table = $wpdb->prefix . 'mlc_conversations'; $sql1 = "CREATE TABLE IF NOT EXISTS $conversations_table ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(255) DEFAULT '', created_by bigint(20), created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, status varchar(20) DEFAULT 'active', PRIMARY KEY (id), KEY created_by (created_by), KEY status (status) ) $charset_collate;"; // 对话参与者表 $participants_table = $wpdb->prefix . 'mlc_conversation_participants'; $sql2 = "CREATE TABLE IF NOT EXISTS $participants_table ( id bigint(20) NOT NULL AUTO_INCREMENT, conversation_id bigint(20) NOT NULL, user_id bigint(20), user_type varchar(20) DEFAULT 'registered', -- registered, guest, support user_language varchar(10) DEFAULT 'en_US', joined_at datetime DEFAULT CURRENT_TIMESTAMP, last_seen datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY participant_conversation (conversation_id, user_id, user_type), KEY conversation_id (conversation_id), KEY user_id (user_id) ) $charset_collate;"; // 消息表 $messages_table = $wpdb->prefix . 'mlc_messages'; $sql3 = "CREATE TABLE IF NOT EXISTS $messages_table ( id bigint(20) NOT NULL AUTO_INCREMENT, conversation_id bigint(20) NOT NULL, sender_id bigint(20), sender_type varchar(20) DEFAULT 'registered', message_type varchar(20) DEFAULT 'text', -- text, image, file, system original_text text NOT NULL, original_language varchar(10) DEFAULT 'en_US', translated_text text, translated_language varchar(10), translation_status varchar(20) DEFAULT 'pending', -- pending, completed, failed attachments text, -- JSON格式存储附件信息 sent_at datetime DEFAULT CURRENT_TIMESTAMP, delivered_at datetime, read_at datetime, PRIMARY KEY (id), KEY conversation_id (conversation_id), KEY sender_info (sender_id, sender_type), KEY sent_at (sent_at) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); } } 3.3 实现实时通讯后端 class MLC_Chat_Server { private $translator; public function __construct() { $this->translator = new MLC_Translator(get_option('mlc_google_api_key')); // 注册REST API端点 add_action('rest_api_init', array($this, 'register_rest_routes')); // 初始化WebSocket服务器(如果使用WebSocket) $this->init_websocket_server(); } public function register_rest_routes() { // 创建新对话 register_rest_route('mlc/v1', '/conversations', array( 'methods' => 'POST', 'callback' => array($this, 'create_conversation'), 'permission_callback' => array($this, 'check_chat_permission') )); // 发送消息 register_rest_route('mlc/v1', '/conversations/(?P<id>d+)/messages', array( 'methods' => 'POST', 'callback' => array($this, 'send_message'), 'permission_callback' => array($this, 'check_chat_permission') )); // 获取消息历史 register_rest_route('mlc/v1', '/conversations/(?P<id>d+)/messages', array( 'methods' => 'GET', 'callback' => array($this, 'get_messages'), 'permission_callback' => array($this, 'check_chat_permission') )); // 获取用户对话列表 register_rest_route('mlc/v1', '/users/(?P<user_id>d+)/conversations', array( 'methods' => 'GET', 'callback' => array($this, 'get_user_conversations'), 'permission_callback' => array($this, 'check_chat_permission') )); } public function create_conversation($request) { global $wpdb; $participants = $request->get_param('participants'); $title = sanitize_text_field($request->get_param('title')); $current_user_id = get_current_user_id(); // 开始事务 $wpdb->query('START TRANSACTION'); try { // 创建对话 $conversations_table = $wpdb->prefix . 'mlc_conversations'; $wpdb->insert( $conversations_table, array( 'title' => $title, 'created_by' => $current_user_id, 'status' => 'active' ) ); $conversation_id = $wpdb->insert_id; if (!$conversation_id) { throw new Exception('创建对话失败'); } // 添加参与者 $participants_table = $wpdb->prefix . 'mlc_conversation_participants'; // 添加创建者 $wpdb->insert( $participants_table, array( 'conversation_id' => $conversation_id, 'user_id' => $current_user_id, 'user_type' => 'registered', 'user_language' => get_user_locale($current_user_id) ) ); // 添加其他参与者 foreach ($participants as $participant) { $wpdb->insert( $participants_table, array( 'conversation_id' => $conversation_id, 'user_id' => $participant['user_id'], 'user_type' => $participant['user_type'] ?? 'registered', 'user_language' => $participant['language'] ?? 'en_US' ) ); } $wpdb->query('COMMIT'); return rest_ensure_response(array( 'success' => true, 'conversation_id' => $conversation_id, 'message' => '对话创建成功' )); } catch (Exception $e) { $wpdb->query('ROLLBACK'); return new WP_Error('create_failed', $e->getMessage(), array('status' => 500)); } } public function send_message($request) { global $wpdb; $conversation_id = $request->get_param('id'); $message_text = sanitize_textarea_field($request->get_param('message')); $attachments = $request->get_param('attachments'); $current_user_id = get_current_user_id(); // 验证用户是否参与对话 if (!$this->is_user_in_conversation($current_user_id, $conversation_id)) { return new WP_Error('permission_denied', '您不在这个对话中', array('status' => 403)); } // 获取用户语言设置 $user_language = $this->get_user_language($current_user_id, $conversation_id); // 插入原始消息 $messages_table = $wpdb->prefix . 'mlc_messages'; $wpdb->insert( $messages_table, array( 'conversation_id' => $conversation_id, 'sender_id' => $current_user_id, 'sender_type' => 'registered', 'message_type' => 'text', 'original_text' => $message_text, 'original_language' => $user_language, 'translation_status' => 'pending', 'attachments' => $attachments ? json_encode($attachments) : null, 'sent_at' => current_time('mysql') ) ); $message_id = $wpdb->insert_id; if (!$message_id) { return new WP_Error('send_failed', '发送消息失败', array('status' => 500)); } // 异步处理消息翻译 $this->process_message_translation_async($message_id, $conversation_id); // 实时推送消息给其他参与者 $this->broadcast_message($message_id, $conversation_id); return rest_ensure_response(array( 'success' => true, 'message_id' => $message_id, 'sent_at' => current_time('mysql') )); } private function process_message_translation_async($message_id, $conversation_id) { // 使用WordPress的调度系统异步处理翻译 wp_schedule_single_event(time() + 1, 'mlc_process_message_translation', array( 'message_id' => $message_id, 'conversation_id' => $conversation_id )); } public function process_message_translation($message_id, $conversation_id) { global $wpdb; $messages_table = $wpdb->prefix . 'mlc_messages'; $participants_table = $wpdb->prefix . 'mlc_conversation_participants'; // 获取原始消息 $message = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $messages_table WHERE id = %d", $message_id )); if (!$message) { return; } // 获取对话中的所有参与者(除了发送者) $participants = $wpdb->get_results($wpdb->prepare( "SELECT user_id, user_type, user_language FROM $participants_table WHERE conversation_id = %d AND (user_id != %d OR user_type != 'registered')", $conversation_id, $message->sender_id )); // 为每个语言需求生成翻译 $translations = array(); foreach ($participants as $participant) { $target_language = $participant->user_language; // 如果目标语言与原始语言相同,不需要翻译 if ($target_language === $message->original_language) { $translations[$target_language] = $message->original_text; continue; } // 检查是否已有该语言的翻译 if (!isset($translations[$target_language])) { $translated_text = $this->translator->translate( $message->original_text, $target_language, $message->original_language ); $translations[$target_language] = $translated_text; } } // 更新消息记录,存储翻译结果 $wpdb->update( $messages_table, array( 'translation_status' => 'completed', 'translated_text' => json_encode($translations), 'translated_language' => json_encode(array_keys($translations)) ), array('id' => $message_id) ); // 通知客户端翻译完成 $this->notify_translation_complete($message_id, $conversation_id); } private function broadcast_message($message_id, $conversation_id) { // 这里实现WebSocket或Server-Sent Events推送 // 实际实现取决于您选择的实时通信技术 // 示例:使用Action钩子让其他插件处理推送 do_action('mlc_message_sent', $message_id, $conversation_id); } // 其他必要的方法... } 3.4 前端聊天界面实现 // assets/js/chat-frontend.js class MLChat { constructor(options) { this.options = Object.assign({ container: '#mlc-chat-container', apiUrl: mlc_ajax.ajax_url, websocketUrl: mlc_ajax.websocket_url, currentUserId: mlc_ajax.current_user_id, currentUserLanguage: mlc_ajax.current_language, nonce: mlc_ajax.nonce }, options); this.conversations = []; this.activeConversation = null; this.websocket = null; this.init(); } init() { this.renderContainer(); this.loadConversations(); this.initWebSocket(); this.bindEvents(); } renderContainer() { const container = document.querySelector(this.options.container); if (!container) return; container.innerHTML = ` <div class="mlc-chat-wrapper"> <div class="mlc-chat-sidebar"> <div class="mlc-chat-header"> <h3>${mlc_ajax.i18n.conversations}</h3> <button class="mlc-new-chat-btn">+ ${mlc_ajax.i18n.new_chat}</button> </div> <div class="mlc-conversations-list"> <!-- 对话列表将通过JavaScript动态加载 --> </div> <div class="mlc-user-profile"> <div class="mlc-user-avatar"> ${this.getUserAvatar()} </div> <div class="mlc-user-info"> <span class="mlc-user-name">${mlc_ajax.current_user_name}</span> <select class="mlc-user-language"> ${this.getLanguageOptions()} </select> </div> </div> </div> <div class="mlc-chat-main"> <div class="mlc-chat-empty-state"> <div class="mlc-empty-icon">💬</div> <h4>${mlc_ajax.i18n.select_conversation}</h4> <p>${mlc_ajax.i18n.select_conversation_desc}</p> <button class="mlc-start-new-chat">${mlc_ajax.i18n.start_chat}</button> </div> <div class="mlc-chat-active" style="display: none;"> <div class="mlc-chat-header"> <div class="mlc-chat-info"> <h4 class="mlc-chat-title"></h4> <span class="mlc-chat-participants"></span> </div> <div class="mlc-chat-actions"> <button class="mlc-translate-chat" title="${mlc_ajax.i18n.translate_chat}">🌐</button> <button class="mlc-chat-settings" title="${mlc_ajax.i18n.settings}">⚙️</button> </div> </div> <div class="mlc-messages-container"> <div class="mlc-messages-loading">${mlc_ajax.i18n.loading}</div> <div class="mlc-messages-list"></div> </div> <div class="mlc-message-input-area"> <div class="mlc-input-tools"> <button class="mlc-attach-file" title="${mlc_ajax.i18n.attach_file}">📎</button> <button class="mlc-emoji-picker" title="${mlc_ajax.i18n.emoji}">😊</button> <select class="mlc-translation-mode"> <option value="auto">${mlc_ajax.i18n.translate_auto}</option> <option value="manual">${mlc_ajax.i18n.translate_manual}</option> <option value="off">${mlc_ajax.i18n.translate_off}</option> </select> </div> <textarea class="mlc-message-input" placeholder="${mlc_ajax.i18n.type_message}"></textarea> <button class="mlc-send-message">${mlc_ajax.i18n.send}</button> </div> </div> </div> <div class="mlc-chat-modal" id="mlc-new-chat-modal" style="display: none;"> <div class="mlc-modal-content"> <h3>${mlc_ajax.i18n.new_conversation}</h3> <div class="mlc-modal-body"> <input type="text" class="mlc-chat-title-input" placeholder="${mlc_ajax.i18n.conversation_title}"> <div class="mlc-participants-selector"> <h4>${mlc_ajax.i18n.select_participants}</h4> <div class="mlc-users-list"></div> </div> </div> <div class="mlc-modal-footer"> <button class="mlc-modal-cancel">${mlc_ajax.i18n.cancel}</button> <button class="mlc-modal-create">${mlc_ajax.i18n.create}</button> </div> </div> </div> </div> `; } async loadConversations() { try { const response = await fetch( `${this.options.apiUrl}/wp-json/mlc/v1/users/${this.options.currentUserId}/conversations`, { headers: { 'X-WP-Nonce': this.options.nonce } } ); if (response.ok) { const data = await response.json(); this.conversations = data; this.renderConversationsList(); } } catch (error) { console.error('加载对话失败:', error); } } async sendMessage(conversationId, messageText, attachments = []) { try { const response = await fetch( `${this.options.apiUrl}/wp-json/mlc/v1/conversations/${conversationId}/messages`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': this.options.nonce }, body: JSON.stringify({ message: messageText, attachments: attachments }) } ); if (response.ok) { const data = await response.json(); return data; } } catch (error) { console.error('发送消息失败:', error); throw error; } } initWebSocket() { if (!this.options.websocketUrl) return; this.websocket = new WebSocket(this.options.websocketUrl); this.websocket.onopen = () => { console.log('WebSocket连接已建立'); // 发送身份验证 this.websocket.send(JSON.stringify({ type: 'auth', userId: this.options.currentUserId, nonce: this.options.nonce })); }; this.websocket.onmessage = (event) => { const data = JSON.parse(event.data); this.handleWebSocketMessage(data); }; this.websocket.onclose = () => { console.log('WebSocket连接已关闭'); // 尝试重新连接 setTimeout(() => this.initWebSocket(), 5000); }; } handleWebSocketMessage(data) { switch (data.type) { case 'new_message': this.handleNewMessage(data.message); break; case 'translation_complete': this.handleTranslationComplete(data.messageId, data.translations); break; case 'user_online': this.updateUserStatus(data.userId, true); break; case 'user_offline': this.updateUserStatus(data.userId, false); break; } } handleNewMessage(message) { // 如果消息属于当前活跃对话,则显示 if (this.activeConversation && this.activeConversation.id === message.conversation_id) { this.appendMessage(message); } // 更新对话列表中的最后消息预览 this.updateConversationPreview(message.conversation_id, message); // 显示通知 if (!document.hasFocus() || this.activeConversation?.id !== message.conversation_id) { this.showNotification(message); } } // 其他前端方法... } 第四部分:集成其他常用互联网小工具 4.1 创建小工具管理器 class MLC_Tools_Manager { private $available_tools = array(); public function __construct() { $this->init_tools(); add_action('widgets_init', array($this, 'register_widgets')); add_shortcode('mlc_tool', array($this, 'tool_shortcode')); } private function init_tools() { // 注册可用工具 $this->available_tools = array( 'currency_converter' => array( 'name' => __('货币转换器', 'mlc'), 'description' => __('实时货币汇率转换工具', 'mlc'), 'class' => 'MLC_Currency_Converter', 'icon' => '💰' ), 'timezone_converter' => array( 'name' => __('时区转换器', 'mlc'), 'description' => __('全球时区转换和会议时间安排', 'mlc'), 'class' => 'MLC_Timezone_Converter', 'icon' => '🕐' ), 'unit_converter' => array( 'name' => __('单位转换器', 'mlc'), 'description' => __('长度、重量、温度等单位转换', 'mlc'),
发表评论