跳至内容

分类: 应用软件

WordPress集成教程,连接企业微信或钉钉实现消息互通

WordPress集成教程:连接企业微信或钉钉实现消息互通,通过代码二次开发实现常用互联网小工具功能 引言:为什么需要将WordPress与企业通讯工具集成? 在当今数字化办公环境中,信息流通的效率和及时性直接影响企业运营效果。WordPress作为全球最流行的内容管理系统,承载着大量企业的官方网站、博客、产品展示甚至内部知识库。而企业微信和钉钉作为国内主流的企业通讯协作平台,已经成为日常工作中不可或缺的工具。 将WordPress与企业微信或钉钉集成,可以实现: 网站内容更新实时通知到工作群组 用户提交的表单数据即时推送到相关负责人 评论审核提醒、订单状态变更等业务通知 实现跨平台的信息同步与协作 本文将详细介绍如何通过WordPress代码二次开发,实现与企业微信或钉钉的消息互通,并扩展实现一些常用的互联网小工具功能。 第一部分:准备工作与环境配置 1.1 了解企业微信与钉钉开放平台 企业微信开放平台提供了丰富的API接口,包括: 消息推送接口:支持文本、图文、卡片等多种消息格式 部门与成员管理:获取组织架构信息 应用管理:创建自定义应用并获取访问凭证 钉钉开放平台同样提供了: 工作通知接口:向用户或部门发送消息 群机器人:通过Webhook方式发送消息到群聊 免登授权:实现用户身份验证 1.2 WordPress开发环境搭建 在进行二次开发前,需要准备: 本地开发环境:推荐使用Local by Flywheel或XAMPP 代码编辑器:VS Code或PHPStorm WordPress调试配置:在wp-config.php中启用调试模式 define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 1.3 创建WordPress插件框架 我们将创建一个独立的插件来实现所有功能: <?php /** * Plugin Name: 企业通讯集成工具 * Plugin URI: https://yourwebsite.com/ * Description: 连接企业微信或钉钉实现消息互通,集成常用小工具 * Version: 1.0.0 * Author: Your Name */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('ECIT_VERSION', '1.0.0'); define('ECIT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ECIT_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once ECIT_PLUGIN_DIR . 'includes/class-core.php'; ECIT_Core::init(); 第二部分:企业微信集成实现 2.1 企业微信应用配置与认证 首先需要在企业微信管理后台创建应用: 登录企业微信管理后台 进入"应用管理" → "自建应用" → "创建应用" 获取以下关键信息: CorpID:企业ID AgentId:应用ID Secret:应用密钥 2.2 获取访问令牌(Access Token) 企业微信API调用需要Access Token,以下是获取Token的类实现: class ECIT_WeChat_Work { private $corp_id; private $agent_id; private $secret; private $access_token; private $token_expire; public function __construct($corp_id, $agent_id, $secret) { $this->corp_id = $corp_id; $this->agent_id = $agent_id; $this->secret = $secret; $this->get_access_token(); } private function get_access_token() { $transient_key = 'ecit_wechat_token_' . $this->agent_id; $cached_token = get_transient($transient_key); if ($cached_token !== false) { $this->access_token = $cached_token; return; } $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken"; $params = [ 'corpid' => $this->corp_id, 'corpsecret' => $this->secret ]; $response = wp_remote_get(add_query_arg($params, $url)); if (is_wp_error($response)) { error_log('企业微信获取Token失败: ' . $response->get_error_message()); return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['access_token'])) { $this->access_token = $body['access_token']; // Token有效期为7200秒,我们设置为7000秒过期 set_transient($transient_key, $this->access_token, 7000); return true; } error_log('企业微信Token获取失败: ' . json_encode($body)); return false; } public function get_token() { return $this->access_token; } } 2.3 实现消息发送功能 class ECIT_WeChat_Message { private $wechat_work; public function __construct($wechat_work) { $this->wechat_work = $wechat_work; } /** * 发送文本消息 */ public function send_text($content, $to_user = '@all', $to_party = '', $to_tag = '') { $token = $this->wechat_work->get_token(); if (!$token) { return false; } $url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={$token}"; $data = [ 'touser' => $to_user, 'toparty' => $to_party, 'totag' => $to_tag, 'msgtype' => 'text', 'agentid' => $this->wechat_work->agent_id, 'text' => [ 'content' => $content ], 'safe' => 0, 'enable_id_trans' => 0, 'enable_duplicate_check' => 0 ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE), 'timeout' => 10 ]); return $this->handle_response($response); } /** * 发送图文消息 */ public function send_news($articles, $to_user = '@all') { $token = $this->wechat_work->get_token(); if (!$token) { return false; } $url = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={$token}"; $data = [ 'touser' => $to_user, 'msgtype' => 'news', 'agentid' => $this->wechat_work->agent_id, 'news' => [ 'articles' => $articles ] ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE) ]); return $this->handle_response($response); } private function handle_response($response) { if (is_wp_error($response)) { error_log('企业微信消息发送失败: ' . $response->get_error_message()); return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if ($body['errcode'] == 0) { return true; } else { error_log('企业微信消息发送失败: ' . json_encode($body)); return false; } } } 2.4 WordPress钩子集成示例 将WordPress事件与企业微信通知绑定: class ECIT_WordPress_Hooks { private $wechat_message; public function __construct($wechat_message) { $this->wechat_message = $wechat_message; $this->init_hooks(); } private function init_hooks() { // 新文章发布通知 add_action('publish_post', [$this, 'notify_new_post'], 10, 2); // 新评论通知 add_action('comment_post', [$this, 'notify_new_comment'], 10, 3); // 用户注册通知 add_action('user_register', [$this, 'notify_new_user']); // WooCommerce新订单通知 add_action('woocommerce_new_order', [$this, 'notify_new_order'], 10, 2); } public function notify_new_post($post_id, $post) { if ($post->post_status != 'publish' || $post->post_type != 'post') { return; } $author = get_user_by('ID', $post->post_author); $post_url = get_permalink($post_id); $message = "📢 新文章发布通知nn"; $message .= "标题:{$post->post_title}n"; $message .= "作者:{$author->display_name}n"; $message .= "摘要:" . wp_trim_words(strip_tags($post->post_content), 50) . "n"; $message .= "链接:{$post_url}"; $this->wechat_message->send_text($message); } public function notify_new_comment($comment_id, $approved, $commentdata) { if ($approved != 1) { return; } $post = get_post($commentdata['comment_post_ID']); $comment = get_comment($comment_id); $message = "💬 新评论通知nn"; $message .= "文章:{$post->post_title}n"; $message .= "评论人:{$comment->comment_author}n"; $message .= "内容:" . wp_trim_words($comment->comment_content, 30) . "n"; $message .= "管理链接:" . admin_url("comment.php?action=editcomment&c={$comment_id}"); $this->wechat_message->send_text($message); } public function notify_new_user($user_id) { $user = get_user_by('ID', $user_id); $message = "👤 新用户注册nn"; $message .= "用户名:{$user->user_login}n"; $message .= "邮箱:{$user->user_email}n"; $message .= "注册时间:" . date('Y-m-d H:i:s') . "n"; $message .= "管理链接:" . admin_url("user-edit.php?user_id={$user_id}"); $this->wechat_message->send_text($message); } } 第三部分:钉钉集成实现 3.1 钉钉机器人配置 钉钉群机器人提供了更简单的集成方式: class ECIT_DingTalk_Robot { private $webhook_url; private $secret; public function __construct($webhook_url, $secret = '') { $this->webhook_url = $webhook_url; $this->secret = $secret; } /** * 生成签名 */ private function generate_sign($timestamp) { if (empty($this->secret)) { return ''; } $string_to_sign = $timestamp . "n" . $this->secret; $sign = base64_encode(hash_hmac('sha256', $string_to_sign, $this->secret, true)); return urlencode($sign); } /** * 发送文本消息 */ public function send_text($content, $at_mobiles = [], $at_all = false) { $timestamp = time() * 1000; $sign = $this->generate_sign($timestamp); $url = $this->webhook_url; if ($sign) { $url .= "&timestamp={$timestamp}&sign={$sign}"; } $data = [ 'msgtype' => 'text', 'text' => [ 'content' => $content ], 'at' => [ 'atMobiles' => $at_mobiles, 'isAtAll' => $at_all ] ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE), 'timeout' => 10 ]); return $this->handle_response($response); } /** * 发送Markdown消息 */ public function send_markdown($title, $text, $at_mobiles = [], $at_all = false) { $timestamp = time() * 1000; $sign = $this->generate_sign($timestamp); $url = $this->webhook_url; if ($sign) { $url .= "&timestamp={$timestamp}&sign={$sign}"; } $data = [ 'msgtype' => 'markdown', 'markdown' => [ 'title' => $title, 'text' => $text ], 'at' => [ 'atMobiles' => $at_mobiles, 'isAtAll' => $at_all ] ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE) ]); return $this->handle_response($response); } /** * 发送链接消息 */ public function send_link($title, $text, $message_url, $pic_url = '') { $timestamp = time() * 1000; $sign = $this->generate_sign($timestamp); $url = $this->webhook_url; if ($sign) { $url .= "&timestamp={$timestamp}&sign={$sign}"; } $data = [ 'msgtype' => 'link', 'link' => [ 'title' => $title, 'text' => $text, 'messageUrl' => $message_url, 'picUrl' => $pic_url ] ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE) ]); return $this->handle_response($response); } private function handle_response($response) { if (is_wp_error($response)) { error_log('钉钉消息发送失败: ' . $response->get_error_message()); return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if ($body['errcode'] == 0) { return true; } else { error_log('钉钉消息发送失败: ' . json_encode($body)); return false; } } } 3.2 钉钉工作通知API集成 对于更复杂的企业应用,可以使用钉钉工作通知API: class ECIT_DingTalk_Work_Notification { private $app_key; private $app_secret; private $access_token; public function __construct($app_key, $app_secret) { $this->app_key = $app_key; $this->app_secret = $app_secret; $this->get_access_token(); } private function get_access_token() { $transient_key = 'ecit_dingtalk_token'; $cached_token = get_transient($transient_key); if ($cached_token !== false) { $this->access_token = $cached_token; return; } $url = 'https://oapi.dingtalk.com/gettoken'; $params = [ 'appkey' => $this->app_key, 'appsecret' => $this->app_secret ]; $response = wp_remote_get(add_query_arg($params, $url)); if (is_wp_error($response)) { error_log('钉钉获取Token失败: ' . $response->get_error_message()); return false; } $body = json_decode(wp_remote_retrieve_body($response), true); if ($body['errcode'] == 0) { $this->access_token = $body['access_token']; set_transient($transient_key, $this->access_token, 7000); return true; } error_log('钉钉Token获取失败: ' . json_encode($body)); return false; } /** * 发送工作通知 */ public function send_notification($userid_list, $dept_id_list, $msg) { $token = $this->access_token; if (!$token) { return false; } $url = "https://oapi.dingtalk.com/topapi/message/corpconversation/asyncsend_v2?access_token={$token}"; $data = [ 'agent_id' => 'your_agent_id', // 需要在钉钉后台获取 'userid_list' => implode(',', $userid_list), 'dept_id_list' => implode(',', $dept_id_list), 'to_all_user' => false, 'msg' => $msg ]; $response = wp_remote_post($url, [ 'headers' => ['Content-Type' => 'application/json'], 'body' => json_encode($data, JSON_UNESCAPED_UNICODE) ]); return $this->handle_response($response); } } 第四部分:常用互联网小工具功能实现 4 4.1 短链接生成工具 短链接服务在内容分享和营销中非常实用,我们可以集成多种短链接API: class ECIT_Short_URL_Tool { /** * 使用百度短链接API */ public static function baidu_shorten($long_url) { $api_url = 'https://dwz.cn/admin/v2/create'; $token = get_option('ecit_baidu_dwz_token', ''); if (empty($token)) { return new WP_Error('no_token', '未配置百度短链接Token'); } $data = [ 'url' => $long_url, 'termOfValidity' => '1-year' // 有效期:1年 ]; $response = wp_remote_post($api_url, [ 'headers' => [ 'Content-Type' => 'application/json', 'Token' => $token ], 'body' => json_encode($data), 'timeout' => 10 ]); if (is_wp_error($response)) { return $response; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['Code']) && $body['Code'] == 0) { return $body['ShortUrl']; } return new WP_Error('api_error', $body['ErrMsg'] ?? '生成短链接失败'); } /** * 使用新浪短链接API */ public static function sina_shorten($long_url) { $api_url = 'http://api.t.sina.com.cn/short_url/shorten.json'; $source = get_option('ecit_sina_app_key', ''); $params = [ 'source' => $source, 'url_long' => $long_url ]; $response = wp_remote_get(add_query_arg($params, $api_url)); if (is_wp_error($response)) { return $response; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body[0]['url_short'])) { return $body[0]['url_short']; } return new WP_Error('api_error', '生成短链接失败'); } /** * 自建短链接服务 */ public static function custom_shorten($long_url, $custom_slug = '') { global $wpdb; $table_name = $wpdb->prefix . 'ecit_short_urls'; // 检查表是否存在,不存在则创建 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, long_url text NOT NULL, short_slug varchar(100) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, click_count int(11) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY short_slug (short_slug), KEY long_url (long_url(100)) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } // 如果已存在该长链接,返回已有的短链接 $existing = $wpdb->get_row($wpdb->prepare( "SELECT short_slug FROM $table_name WHERE long_url = %s", $long_url )); if ($existing) { return home_url('/s/' . $existing->short_slug); } // 生成短码 if (empty($custom_slug)) { $short_slug = self::generate_slug(); } else { $short_slug = sanitize_title($custom_slug); // 检查是否已存在 $exists = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE short_slug = %s", $short_slug )); if ($exists) { return new WP_Error('slug_exists', '该短码已存在'); } } // 插入数据库 $wpdb->insert($table_name, [ 'long_url' => $long_url, 'short_slug' => $short_slug ]); return home_url('/s/' . $short_slug); } /** * 生成随机短码 */ private static function generate_slug($length = 6) { $characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; $characters_length = strlen($characters); $random_string = ''; for ($i = 0; $i < $length; $i++) { $random_string .= $characters[rand(0, $characters_length - 1)]; } return $random_string; } /** * 短链接重定向处理 */ public static function handle_redirect() { if (preg_match('#^/s/([a-zA-Z0-9]+)$#', $_SERVER['REQUEST_URI'], $matches)) { $slug = $matches[1]; global $wpdb; $table_name = $wpdb->prefix . 'ecit_short_urls'; $result = $wpdb->get_row($wpdb->prepare( "SELECT long_url FROM $table_name WHERE short_slug = %s", $slug )); if ($result) { // 更新点击次数 $wpdb->query($wpdb->prepare( "UPDATE $table_name SET click_count = click_count + 1 WHERE short_slug = %s", $slug )); // 重定向 wp_redirect($result->long_url, 301); exit; } } } } 4.2 二维码生成工具 class ECIT_QR_Code_Tool { /** * 使用Google Charts API生成二维码 */ public static function google_qrcode($content, $size = 200, $margin = 4) { $params = [ 'cht' => 'qr', 'chs' => $size . 'x' . $size, 'chl' => urlencode($content), 'choe' => 'UTF-8', 'chld' => 'L|' . $margin ]; $url = 'https://chart.googleapis.com/chart?' . http_build_query($params); // 下载图片到本地 $upload_dir = wp_upload_dir(); $filename = 'qrcode-' . md5($content . $size) . '.png'; $filepath = $upload_dir['path'] . '/' . $filename; if (!file_exists($filepath)) { $response = wp_remote_get($url); if (!is_wp_error($response)) { file_put_contents($filepath, wp_remote_retrieve_body($response)); } } return $upload_dir['url'] . '/' . $filename; } /** * 使用PHP QR Code库生成二维码 */ public static function php_qrcode($content, $size = 10, $margin = 1) { // 引入PHP QR Code库 if (!class_exists('QRcode')) { require_once ECIT_PLUGIN_DIR . 'lib/phpqrcode/qrlib.php'; } $upload_dir = wp_upload_dir(); $filename = 'qrcode-' . md5($content . $size) . '.png'; $filepath = $upload_dir['path'] . '/' . $filename; if (!file_exists($filepath)) { QRcode::png($content, $filepath, QR_ECLEVEL_L, $size, $margin); } return $upload_dir['url'] . '/' . $filename; } /** * 生成带Logo的二维码 */ public static function qrcode_with_logo($content, $logo_path, $size = 10) { // 先生成普通二维码 $qrcode_path = self::php_qrcode($content, $size, 0); $qrcode_path = str_replace(wp_upload_dir()['url'], wp_upload_dir()['path'], $qrcode_path); // 加载二维码图片 $qrcode = imagecreatefrompng($qrcode_path); $qrcode_width = imagesx($qrcode); $qrcode_height = imagesy($qrcode); // 加载Logo图片 $logo_info = getimagesize($logo_path); $logo_type = $logo_info[2]; switch ($logo_type) { case IMAGETYPE_JPEG: $logo = imagecreatefromjpeg($logo_path); break; case IMAGETYPE_PNG: $logo = imagecreatefrompng($logo_path); break; default: return $qrcode_path; } $logo_width = imagesx($logo); $logo_height = imagesy($logo); // 计算Logo大小(二维码的1/5) $new_logo_width = $qrcode_width / 5; $new_logo_height = $logo_height * ($new_logo_width / $logo_width); // 重新调整Logo大小 $resized_logo = imagecreatetruecolor($new_logo_width, $new_logo_height); imagecopyresampled($resized_logo, $logo, 0, 0, 0, 0, $new_logo_width, $new_logo_height, $logo_width, $logo_height); // 合并图片 $dst_x = ($qrcode_width - $new_logo_width) / 2; $dst_y = ($qrcode_height - $new_logo_height) / 2; imagecopymerge($qrcode, $resized_logo, $dst_x, $dst_y, 0, 0, $new_logo_width, $new_logo_height, 100); // 保存新图片 $filename = 'qrcode-logo-' . md5($content) . '.png'; $filepath = wp_upload_dir()['path'] . '/' . $filename; imagepng($qrcode, $filepath); // 释放内存 imagedestroy($qrcode); imagedestroy($logo); imagedestroy($resized_logo); return wp_upload_dir()['url'] . '/' . $filename; } /** * 生成文章二维码短代码 */ public static function qrcode_shortcode($atts) { $atts = shortcode_atts([ 'size' => 200, 'logo' => '', 'post_id' => get_the_ID() ], $atts); $post_url = get_permalink($atts['post_id']); $post_title = get_the_title($atts['post_id']); if (!empty($atts['logo'])) { $logo_path = get_attached_file($atts['logo']); $qrcode_url = self::qrcode_with_logo($post_url, $logo_path); } else { $qrcode_url = self::google_qrcode($post_url, $atts['size']); } return sprintf( '<div class="post-qrcode"> <img src="%s" alt="%s" width="%d" height="%d"> <p>扫描二维码阅读文章</p> </div>', esc_url($qrcode_url), esc_attr($post_title), $atts['size'], $atts['size'] ); } } 4.3 数据统计与分析工具 class ECIT_Analytics_Tool { /** * 获取网站访问统计 */ public static function get_visitor_stats($days = 30) { global $wpdb; $results = $wpdb->get_results($wpdb->prepare( "SELECT DATE(visit_time) as date, COUNT(*) as visits, COUNT(DISTINCT ip_address) as unique_visitors FROM {$wpdb->prefix}ecit_visits WHERE visit_time >= DATE_SUB(NOW(), INTERVAL %d DAY) GROUP BY DATE(visit_time) ORDER BY date DESC", $days )); return $results; } /** * 记录访问数据 */ public static function track_visit() { if (is_admin() || wp_doing_ajax() || wp_doing_cron()) { return; } global $wpdb; $table_name = $wpdb->prefix . 'ecit_visits'; // 检查表是否存在 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, ip_address varchar(45) NOT NULL, user_agent text, page_url varchar(500) NOT NULL, referrer varchar(500), visit_time datetime DEFAULT CURRENT_TIMESTAMP, user_id bigint(20) DEFAULT 0, PRIMARY KEY (id), KEY ip_address (ip_address), KEY visit_time (visit_time), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } $data = [ 'ip_address' => self::get_client_ip(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'page_url' => (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]", 'referrer' => $_SERVER['HTTP_REFERER'] ?? '', 'user_id' => get_current_user_id() ]; $wpdb->insert($table_name, $data); } /** * 获取客户端IP */ private static function get_client_ip() { $ip_keys = ['HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR']; foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER)) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE)) { return $ip; } } } } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } /** * 获取热门文章 */ public static function get_popular_posts($limit = 10, $days = 30) { global $wpdb; $results = $wpdb->get_results($wpdb->prepare( "SELECT p.ID, p.post_title, COUNT(v.id) as view_count, MAX(v.visit_time) as last_visit FROM {$wpdb->prefix}ecit_visits v INNER JOIN {$wpdb->posts} p ON v.page_url LIKE CONCAT('%%', p.guid, '%%') WHERE v.visit_time >= DATE_SUB(NOW(), INTERVAL %d DAY) AND p.post_status = 'publish' AND p.post_type = 'post' GROUP BY p.ID ORDER BY view_count DESC LIMIT %d", $days, $limit )); return $results; } /** * 生成统计报告 */ public static function generate_report($period = 'daily') { $stats = self::get_visitor_stats($period == 'daily' ? 1 : 30); $popular_posts = self::get_popular_posts(5, $period == 'daily' ? 1 : 30); $report = "📊 网站访问统计报告nn"; if ($period == 'daily') { $report .= "📅 日期: " . date('Y-m-d') . "n"; } else { $report .= "📅 周期: 最近30天n"; } $total_visits = 0; $total_visitors = 0; foreach ($stats as $stat) { $total_visits += $stat->visits; $total_visitors += $stat->unique_visitors; } $report .= "👥 总访问量: " . $total_visits . "n"; $report .= "👤 独立访客: " . $total_visitors . "nn"; $report .= "🔥 热门文章:n"; foreach ($popular_posts as $index => $post) { $report .= ($index + 1) . ". " . $post->post_title . " (" . $post->view_count . "次)n"; } return $report; } } 4.4 内容自动发布工具 class ECIT_Auto_Publish_Tool { /** * 从RSS源自动发布文章 */

发表评论

详细教程,为网站开发活动报名与电子票务管理工具

详细教程:为网站开发活动报名与电子票务管理工具——通过WordPress代码二次开发实现常用互联网小工具功能 引言:为什么需要自定义活动报名与电子票务系统 在当今数字化时代,活动组织者面临着管理参与者、票务销售和活动执行的复杂挑战。虽然市场上有许多现成的活动管理解决方案,但它们往往缺乏灵活性、定制性,或者价格昂贵。对于许多中小型组织、社区团体或个人活动策划者来说,一个能够完全控制、成本可控且功能定制的解决方案显得尤为重要。 WordPress作为全球最流行的内容管理系统,拥有强大的扩展性和灵活性。通过代码二次开发,我们可以在WordPress平台上构建一个功能完善、符合特定需求的活动报名与电子票务管理工具。本教程将详细指导您如何从零开始,通过WordPress代码二次开发实现这一目标。 第一部分:准备工作与环境搭建 1.1 开发环境配置 在开始开发之前,我们需要搭建一个合适的开发环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel等工具搭建本地WordPress环境 代码编辑器:选择Visual Studio Code、PHPStorm或Sublime Text等专业编辑器 版本控制:初始化Git仓库,便于代码管理和版本控制 浏览器开发者工具:熟悉Chrome或Firefox的开发者工具,用于调试前端代码 1.2 WordPress安装与基础配置 下载最新版WordPress并安装到本地环境 配置数据库连接信息 设置管理员账户和网站基本信息 安装必要的开发插件: Query Monitor:用于调试数据库查询和性能 Show Current Template:显示当前使用的模板文件 Advanced Custom Fields:用于创建自定义字段(可选) 1.3 创建自定义插件结构 为了避免主题更新导致功能丢失,我们将创建一个独立插件来实现所有功能。在wp-content/plugins/目录下创建新文件夹event-ticket-manager,并创建以下基础文件结构: event-ticket-manager/ ├── event-ticket-manager.php # 主插件文件 ├── includes/ # 核心功能文件目录 │ ├── class-database.php # 数据库操作类 │ ├── class-event.php # 活动管理类 │ ├── class-ticket.php # 票务管理类 │ ├── class-payment.php # 支付处理类 │ └── class-email.php # 邮件通知类 ├── admin/ # 后台管理文件 │ ├── css/ # 后台样式 │ ├── js/ # 后台脚本 │ └── pages/ # 后台页面 ├── public/ # 前端文件 │ ├── css/ # 前端样式 │ ├── js/ # 前端脚本 │ └── templates/ # 前端模板 ├── assets/ # 静态资源 └── vendor/ # 第三方库(如果需要) 第二部分:数据库设计与数据模型 2.1 设计数据库表结构 活动报名与电子票务系统需要多个数据表来存储不同类型的信息。我们将在插件激活时创建这些表: // includes/class-database.php class ETM_Database { public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 活动表 $events_table = $wpdb->prefix . 'etm_events'; $events_sql = "CREATE TABLE IF NOT EXISTS $events_table ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, start_date datetime NOT NULL, end_date datetime NOT NULL, location varchar(500), max_participants int(11) DEFAULT 0, current_participants int(11) DEFAULT 0, status varchar(50) DEFAULT 'draft', created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 票务类型表 $ticket_types_table = $wpdb->prefix . 'etm_ticket_types'; $ticket_types_sql = "CREATE TABLE IF NOT EXISTS $ticket_types_table ( id int(11) NOT NULL AUTO_INCREMENT, event_id int(11) NOT NULL, name varchar(255) NOT NULL, description text, price decimal(10,2) DEFAULT 0.00, quantity int(11) NOT NULL, sold int(11) DEFAULT 0, sale_start datetime, sale_end datetime, status varchar(50) DEFAULT 'active', PRIMARY KEY (id), FOREIGN KEY (event_id) REFERENCES $events_table(id) ON DELETE CASCADE ) $charset_collate;"; // 订单表 $orders_table = $wpdb->prefix . 'etm_orders'; $orders_sql = "CREATE TABLE IF NOT EXISTS $orders_table ( id int(11) NOT NULL AUTO_INCREMENT, order_number varchar(100) NOT NULL UNIQUE, event_id int(11) NOT NULL, customer_name varchar(255) NOT NULL, customer_email varchar(255) NOT NULL, customer_phone varchar(50), total_amount decimal(10,2) NOT NULL, payment_status varchar(50) DEFAULT 'pending', payment_method varchar(100), payment_id varchar(255), order_status varchar(50) DEFAULT 'pending', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), FOREIGN KEY (event_id) REFERENCES $events_table(id) ) $charset_collate;"; // 订单详情表 $order_items_table = $wpdb->prefix . 'etm_order_items'; $order_items_sql = "CREATE TABLE IF NOT EXISTS $order_items_table ( id int(11) NOT NULL AUTO_INCREMENT, order_id int(11) NOT NULL, ticket_type_id int(11) NOT NULL, quantity int(11) NOT NULL, unit_price decimal(10,2) NOT NULL, subtotal decimal(10,2) NOT NULL, ticket_code varchar(100) NOT NULL UNIQUE, checked_in tinyint(1) DEFAULT 0, checked_in_at datetime, PRIMARY KEY (id), FOREIGN KEY (order_id) REFERENCES $orders_table(id) ON DELETE CASCADE, FOREIGN KEY (ticket_type_id) REFERENCES $ticket_types_table(id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($events_sql); dbDelta($ticket_types_sql); dbDelta($orders_sql); dbDelta($order_items_sql); } } 2.2 实现数据模型类 为每个主要实体创建对应的PHP类,封装数据库操作: // includes/class-event.php class ETM_Event { private $db; private $table_name; public function __construct() { global $wpdb; $this->db = $wpdb; $this->table_name = $wpdb->prefix . 'etm_events'; } public function create($data) { $defaults = array( 'status' => 'draft', 'current_participants' => 0, 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ); $data = wp_parse_args($data, $defaults); $result = $this->db->insert( $this->table_name, $data ); if ($result) { return $this->db->insert_id; } return false; } public function get($id) { $query = $this->db->prepare( "SELECT * FROM $this->table_name WHERE id = %d", $id ); return $this->db->get_row($query); } public function update($id, $data) { $data['updated_at'] = current_time('mysql'); $where = array('id' => $id); return $this->db->update( $this->table_name, $data, $where ); } public function delete($id) { return $this->db->delete( $this->table_name, array('id' => $id) ); } public function get_all($args = array()) { $defaults = array( 'status' => 'publish', 'orderby' => 'start_date', 'order' => 'ASC', 'limit' => 10, 'offset' => 0 ); $args = wp_parse_args($args, $defaults); $where = array(); if (!empty($args['status'])) { $where[] = $this->db->prepare("status = %s", $args['status']); } if (!empty($args['search'])) { $where[] = $this->db->prepare("(title LIKE %s OR description LIKE %s)", '%' . $args['search'] . '%', '%' . $args['search'] . '%' ); } $where_clause = ''; if (!empty($where)) { $where_clause = 'WHERE ' . implode(' AND ', $where); } $orderby = esc_sql($args['orderby']); $order = esc_sql($args['order']); $query = "SELECT * FROM $this->table_name $where_clause ORDER BY $orderby $order LIMIT %d OFFSET %d"; $query = $this->db->prepare( $query, $args['limit'], $args['offset'] ); return $this->db->get_results($query); } public function get_upcoming($limit = 5) { $current_time = current_time('mysql'); $query = $this->db->prepare( "SELECT * FROM $this->table_name WHERE status = 'publish' AND start_date > %s ORDER BY start_date ASC LIMIT %d", $current_time, $limit ); return $this->db->get_results($query); } } 第三部分:后台管理界面开发 3.1 创建管理菜单和页面 在WordPress后台添加自定义管理菜单,用于管理活动、票务和订单: // admin/class-admin-menu.php class ETM_Admin_Menu { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menus')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } public function add_admin_menus() { // 主菜单 add_menu_page( '活动票务管理', '活动票务', 'manage_options', 'etm-events', array($this, 'render_events_page'), 'dashicons-tickets-alt', 30 ); // 子菜单 add_submenu_page( 'etm-events', '活动管理', '所有活动', 'manage_options', 'etm-events', array($this, 'render_events_page') ); add_submenu_page( 'etm-events', '添加新活动', '添加活动', 'manage_options', 'etm-add-event', array($this, 'render_add_event_page') ); add_submenu_page( 'etm-events', '票务类型', '票务类型', 'manage_options', 'etm-ticket-types', array($this, 'render_ticket_types_page') ); add_submenu_page( 'etm-events', '订单管理', '订单', 'manage_options', 'etm-orders', array($this, 'render_orders_page') ); add_submenu_page( 'etm-events', '签到管理', '签到', 'manage_options', 'etm-checkin', array($this, 'render_checkin_page') ); add_submenu_page( 'etm-events', '报表统计', '报表', 'manage_options', 'etm-reports', array($this, 'render_reports_page') ); add_submenu_page( 'etm-events', '系统设置', '设置', 'manage_options', 'etm-settings', array($this, 'render_settings_page') ); } public function enqueue_admin_scripts($hook) { // 只在插件页面加载脚本和样式 if (strpos($hook, 'etm-') === false) { return; } wp_enqueue_style( 'etm-admin-style', plugins_url('admin/css/admin-style.css', dirname(__FILE__)), array(), '1.0.0' ); wp_enqueue_script( 'etm-admin-script', plugins_url('admin/js/admin-script.js', dirname(__FILE__)), array('jquery', 'jquery-ui-datepicker'), '1.0.0', true ); // 本地化脚本,传递数据到JavaScript wp_localize_script('etm-admin-script', 'etm_admin', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('etm_admin_nonce'), 'confirm_delete' => '确定要删除这个项目吗?此操作不可撤销。' )); // 加载jQuery UI日期选择器样式 wp_enqueue_style('jquery-ui-style', 'https://code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css'); } public function render_events_page() { include plugin_dir_path(__FILE__) . 'pages/events.php'; } public function render_add_event_page() { include plugin_dir_path(__FILE__) . 'pages/add-event.php'; } // 其他页面渲染方法... } 3.2 实现活动管理界面 创建活动列表和编辑界面: // admin/pages/events.php <div class="wrap etm-admin-wrap"> <h1 class="wp-heading-inline">活动管理</h1> <a href="<?php echo admin_url('admin.php?page=etm-add-event'); ?>" class="page-title-action">添加新活动</a> <hr class="wp-header-end"> <div class="etm-admin-container"> <div class="etm-admin-header"> <div class="tablenav top"> <div class="alignleft actions"> <select name="filter_status" id="filter-status"> <option value="">所有状态</option> <option value="draft">草稿</option> <option value="publish">已发布</option> <option value="cancelled">已取消</option> <option value="completed">已结束</option> </select> <input type="text" name="search" id="event-search" placeholder="搜索活动..." value="<?php echo isset($_GET['s']) ? esc_attr($_GET['s']) : ''; ?>"> <button type="button" class="button" id="search-events">搜索</button> </div> <div class="tablenav-pages"> <span class="displaying-num"><?php echo $total_events; ?> 个活动</span> <span class="pagination-links"> <?php echo $pagination; ?> </span> </div> </div> </div> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th scope="col" class="column-primary">活动名称</th> <th scope="col">日期</th> <th scope="col">地点</th> <th scope="col">参与人数</th> <th scope="col">状态</th> <th scope="col">操作</th> </tr> </thead> <tbody> <?php if (!empty($events)) : ?> <?php foreach ($events as $event) : ?> <tr> <td class="column-primary"> <strong><?php echo esc_html($event->title); ?></strong> <div class="row-actions"> <span class="edit"> <a href="<?php echo admin_url('admin.php?page=etm-add-event&id=' . $event->id); ?>">编辑</a> | </span> <span class="view"> <a href="<?php echo get_permalink($event->post_id); ?>" target="_blank">查看</a> | </span> <span class="duplicate"> <a href="#" class="duplicate-event" data-id="<?php echo $event->id; ?>">复制</a> | </span> <span class="trash"> <a href="#" class="delete-event" data-id="<?php echo $event->id; ?>">删除</a> </span> </div> <button type="button" class="toggle-row"> <span class="screen-reader-text">显示详情</span> </button> </td> <td> <?php $start_date = date_i18n('Y年m月d日 H:i', strtotime($event->start_date)); $end_date = date_i18n('Y年m月d日 H:i', strtotime($event->end_date)); echo $start_date . '<br>至<br>' . $end_date; ?> </td> <td><?php echo esc_html($event->location); ?></td> <td> <?php echo $event->max_participants > 0 ? $event->max_participants : '不限'; ?> </td> <td> <span class="event-status status-<?php echo esc_attr($event->status); ?>"> <?php $status_labels = array( 'draft' => '草稿', 'publish' => '已发布', 'cancelled' => '已取消', 'completed' => '已结束' ); echo isset($status_labels[$event->status]) ? $status_labels[$event->status] : $event->status; ?> </span> </td> <td> <div class="action-buttons"> <a href="<?php echo admin_url('admin.php?page=etm-ticket-types&event_id=' . $event->id); ?>" class="button button-small">管理票务</a> <a href="<?php echo admin_url('admin.php?page=etm-orders&event_id=' . $event->id); ?>" class="button button-small">查看订单</a> <?php if ($event->status == 'publish') : ?> <a href="#" class="button button-small button-primary copy-registration-link" data-link="<?php echo home_url('/event-registration/?event_id=' . $event->id); ?>">复制报名链接</a> <?php endif; ?> </div> </td> </tr> <?php endforeach; ?> <?php else : ?> <tr> <td colspan="6" class="no-items">暂无活动,<a href="<?php echo admin_url('admin.php?page=etm-add-event'); ?>">创建第一个活动</a></td> </tr> <?php endif; ?> </tbody> </table> </div> </div> ## 第四部分:前端报名与票务购买功能 ### 4.1 创建前端报名页面模板 // public/templates/registration-form.php<div class="etm-registration-container"> <div class="etm-registration-header"> <h1><?php echo esc_html($event->title); ?></h1> <div class="event-meta"> <div class="meta-item"> <span class="dashicons dashicons-calendar"></span> <span class="meta-label">时间:</span> <span class="meta-value"><?php echo date_i18n('Y年m月d日 H:i', strtotime($event->start_date)); ?> - <?php echo date_i18n('Y年m月d日 H:i', strtotime($event->end_date)); ?></span> </div> <div class="meta-item"> <span class="dashicons dashicons-location"></span> <span class="meta-label">地点:</span> <span class="meta-value"><?php echo esc_html($event->location); ?></span> </div> <div class="meta-item"> <span class="dashicons dashicons-groups"></span> <span class="meta-label">已报名:</span> <span class="meta-value"><?php echo $event->current_participants; ?>/<?php echo $event->max_participants > 0 ? $event->max_participants : '不限'; ?></span> </div> </div> </div> <?php if ($event->status !== 'publish') : ?> <div class="etm-alert etm-alert-warning"> 此活动当前不可报名。 </div> <?php elseif ($event->max_participants > 0 && $event->current_participants >= $event->max_participants) : ?> <div class="etm-alert etm-alert-warning"> 抱歉,此活动报名人数已满。 </div> <?php else : ?> <div class="etm-registration-content"> <div class="etm-event-description"> <h2>活动介绍</h2> <div class="description-content"> <?php echo wpautop($event->description); ?> </div> </div> <div class="etm-registration-form-wrapper"> <h2>报名信息</h2> <form id="etm-registration-form" method="post" action="<?php echo admin_url('admin-ajax.php'); ?>"> <?php wp_nonce_field('etm_registration_nonce', 'registration_nonce'); ?> <input type="hidden" name="action" value="etm_process_registration"> <input type="hidden" name="event_id" value="<?php echo $event->id; ?>"> <div class="form-section"> <h3>选择票种</h3> <div class="ticket-selection"> <?php if (!empty($ticket_types)) : ?> <?php foreach ($ticket_types as $ticket) : ?> <?php if ($ticket->status == 'active' && $ticket->sold < $ticket->quantity) : ?> <div class="ticket-option"> <div class="ticket-header"> <input type="radio" name="ticket_type_id" id="ticket_<?php echo $ticket->id; ?>" value="<?php echo $ticket->id; ?>" data-price="<?php echo $ticket->price; ?>" required> <label for="ticket_<?php echo $ticket->id; ?>"> <span class="ticket-name"><?php echo esc_html($ticket->name); ?></span> <span class="ticket-price"> <?php if ($ticket->price > 0) : ?> ¥<?php echo number_format($ticket->price, 2); ?> <?php else : ?> 免费 <?php endif; ?> </span> </label> </div> <div class="ticket-details"> <p><?php echo esc_html($ticket->description); ?></p> <div class="ticket-meta"> <span class="available">剩余:<?php echo $ticket->quantity - $ticket->sold; ?>张</span> <?php if ($ticket->sale_start && $ticket->sale_end) : ?> <span class="sale-period"> 销售时间:<?php echo date_i18n('m月d日 H:i', strtotime($ticket->sale_start)); ?> - <?php echo date_i18n('m月d日 H:i', strtotime($ticket->sale_end)); ?> </span> <?php endif; ?> </div> </div> </div> <?php endif; ?> <?php endforeach; ?> <?php else : ?> <p class="no-tickets">暂无可用票种</p> <?php endif; ?> </div> </div> <div class="form-section"> <h3>购票数量</h3> <div class="quantity-selection"> <div class="quantity-control"> <button type="button" class="quantity-minus" disabled>-</button> <input type="number" name="ticket_quantity" id="ticket_quantity" value="1" min="1" max="10" class="quantity-input"> <button type="button" class="quantity-plus">+</button> </div> <div class="quantity-note"> <p>每人最多可购买10张票</p> </div> </div> </div> <div class="form-section"> <h3>填写个人信息</h3> <div class="personal-info"> <div class="form-row"> <div class="form-group"> <label for="customer_name">姓名 <span class="required">*</span></label> <input type="text" id="customer_name" name="customer_name" required placeholder="请输入真实姓名"> </div> <div class="form-group"> <label for="customer_email">邮箱 <span class="required">*</span></label> <input type="email" id="customer_email" name="customer_email" required placeholder="用于接收电子票和通知"> </div> </div> <div class="form-row"> <div class="form-group"> <label for="customer_phone">手机号 <span class="required">*</span></label> <input type="tel" id="customer_phone" name="customer_phone" required placeholder="用于活动通知"> </div> <div class="form-group"> <label for="customer_company">公司/机构</label> <input type="text" id="customer_company" name="customer_company" placeholder="选填"> </div> </div> <div class="form-row"> <div class="form-group full-width"> <label for="special_requirements">特殊需求</label> <textarea id="special_requirements" name="special_requirements" rows="3" placeholder="如有饮食禁忌、特殊需求等请在此说明"></textarea> </div> </div> </div> </div> <div class="form-section"> <h3>订单总览</h3> <div class="order-summary"> <div class="summary-row"> <span class="summary-label">票种:</span> <span class="summary-value" id="summary_ticket_name">请选择票种</span> </div> <div class="summary-row"> <span class="summary-label">单价:</span> <span class="summary-value" id="summary_ticket_price">¥0.00</span> </div> <div class="summary-row"> <span class="summary-label">数量:</span> <span class="summary-value" id="summary_quantity">1</span> </div> <div class="summary-row total"> <span class="summary-label">总计:</span> <span class="summary-value" id="summary_total">¥0.00</span> </div> </div> </div> <div class="form-section"> <div class="form-submit"> <button type="submit" class="etm-submit-button" id="submit-registration"> <span class="button-text">提交报名</span> <span class="loading-spinner" style="display:none;"></span> </button> <p class="form-notice">点击提交即表示您同意我们的<a href="<?php echo get_privacy_policy_url(); ?>" target="_blank">隐私政策</a>和<a href="<?php echo home_url('/terms'); ?>" target="_blank">服务条款</a></p> </div> </div> </form> </div> </div> <?php endif; ?> </div> <script>jQuery(document).ready(function($) { // 更新订单总览 function updateOrderSummary() { var selectedTicket = $('input[name="ticket_type_id"]:checked'); var quantity = parseInt($('#ticket_quantity').val()); if (selectedTicket.length > 0) { var ticketName = selectedTicket.siblings('label').find('.ticket-name').text(); var ticketPrice = parseFloat(selectedTicket.data('price')); var total = ticketPrice * quantity; $('#summary_ticket_name').text(ticketName); $('#summary_ticket_price').text('¥' + ticketPrice.toFixed(2)); $('#summary_quantity').text(quantity); $('#summary_total').text('¥' + total.toFixed(2)); } } // 票种选择事件 $('input[name="ticket_type_id"]').on('change', updateOrderSummary); // 数量控制 $('.quantity-minus').on('click', function() { var input = $('#ticket_quantity'); var currentVal = parseInt(input.val()); if (currentVal > 1) { input.val(currentVal - 1); updateOrderSummary(); } updateQuantityButtons(); }); $('.quantity-plus').on('click', function() { var input = $('#ticket_quantity'); var currentVal = parseInt(input.val()); var maxVal = parseInt(input.attr('max')); if (currentVal < maxVal) { input.val(currentVal + 1); updateOrderSummary(); } updateQuantityButtons(); }); $('#ticket_quantity').on('change', function() { var val = parseInt($(this).val()); var maxVal = parseInt($(this).attr('max')); var minVal = parseInt($(this).attr('min')); if (val > maxVal) val = maxVal; if (val < minVal) val = minVal; $(this).val(val); updateOrderSummary(); updateQuantityButtons(); }); function updateQuantityButtons() { var currentVal = parseInt($('#ticket_quantity').val()); var maxVal = parseInt($('#ticket_quantity').attr('max')); var minVal = parseInt($('#ticket_quantity').attr('min')); $('.quantity-minus').prop('disabled', currentVal <= minVal); $('.quantity-plus').prop('disabled', currentVal >= maxVal); } // 表单提交 $('#etm-registration-form').on('submit', function(e) { e.preventDefault(); var form = $(this); var submitButton = $('#submit-registration'); var buttonText = submitButton.find('.button-text'); var spinner = submitButton.find('.loading-spinner'); // 验证表单 if (!form[0].checkValidity()) { form[0].reportValidity(); return; } // 显示加载状态 buttonText.text('处理中...'); spinner.show(); submitButton.prop('disabled', true); // 发送AJAX请求 $.ajax({ url: form.attr('action'), type: 'POST', data: form.serialize(), dataType: 'json', success: function(response) { if (response.success) { // 跳转到支付页面或成功页面 if (response.data.payment_required) { window.location.href = response.data.payment_url; } else { window.location.href = response.data.success_url; } } else { // 显示错误信息 alert(response.data.message || '提交失败,请重试'); buttonText.text('提交报名'); spinner.hide(); submitButton.prop('disabled', false); } }, error: function() { alert('网络错误,请重试'); buttonText.text('提交报名'); spinner.hide(); submitButton.prop('disabled', false); } }); }); // 初始化 updateQuantityButtons(); $('input[name="ticket_type_id"]:first').prop('checked', true).trigger('change'); });</script> ### 4.2 实现AJAX表单处理 // includes/class-ajax-handler.phpclass ETM_Ajax_Handler { public function __construct() { // 前端AJAX处理 add_action('wp_ajax_etm_process_registration', array($this, 'process_registration')); add_action('wp_ajax_nopriv_etm_process_registration', array($this, 'process_registration')); // 后台AJAX处理 add_action('wp_ajax_etm_admin_actions', array($this, 'handle_admin_actions')); } public function process_registration() { // 验证nonce if (!isset($_POST['registration_nonce']) || !wp_verify_nonce($_POST['registration_nonce'], 'etm_registration_nonce')) { wp_send_json_error(array('message' => '安全验证失败')); } // 验证必填字段 $required_fields = array( 'event_id', 'ticket_type_id', 'ticket_quantity', 'customer_name', 'customer_email', 'customer_phone' ); foreach ($required_fields as $field) { if (empty($_POST[$field])) { wp_send_json_error(array('message' => '请填写所有必填字段')); } } $event_id = intval($_POST['event_id']); $ticket_type_id = intval($_POST['ticket_type_id']); $quantity = intval($_POST['ticket_quantity']); // 验证活动状态 $event_model = new ETM_Event(); $event = $event_model->get($event_id); if (!$event || $event->status !== 'publish') { wp_send_json_error(array('message' => '活动不可用')); } // 验证票种 $ticket_model = new ETM_Ticket(); $ticket_type = $ticket_model->get_type($ticket_type_id); if (!$ticket_type || $ticket_type->event_id != $event_id) { wp_send_json_error(array('message' => '票种无效')); } if ($ticket_type->status !== 'active') { wp_send_json_error(array('message' => '该票种已停售')); } // 检查库存 if ($ticket_type->sold + $quantity > $ticket_type->quantity) { wp_send_json_error(array('message' => '票数不足,仅剩' . ($ticket_type->quantity - $ticket_type->sold) . '张')); } // 检查活动人数限制 if ($event->max_participants > 0 && $event->current_participants + $quantity > $event->max_participants) { wp_send_json_error(array('

发表评论

一步步教你,集成在线考试与题库管理系统到WordPress

一步步教你,集成在线考试与题库管理系统到WordPress,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:为什么要在WordPress中集成在线考试系统? 在当今数字化教育与企业培训蓬勃发展的时代,在线考试与评估已成为教育机构、企业培训部门乃至知识付费创作者不可或缺的功能。WordPress作为全球最受欢迎的内容管理系统(CMS),其强大的可扩展性使其远不止是一个博客平台。通过代码二次开发,我们可以将专业的在线考试与题库管理系统无缝集成到WordPress中,从而打造一个功能全面、用户体验良好的学习与评估平台。 传统的独立考试系统往往存在与主网站风格不一、用户数据割裂、维护成本高等问题。而在WordPress中集成此类系统,则能实现品牌统一、数据集中管理、用户单点登录等诸多优势。更重要的是,通过深度集成,我们还能利用WordPress的生态,结合其他插件与功能,创造出更丰富的互联网小工具应用场景。 本文将详细解析如何通过代码二次开发,在WordPress中从零开始构建一个功能完善的在线考试与题库管理系统,并探讨如何扩展其作为常用互联网小工具的潜力。无论你是WordPress开发者、教育科技创业者,还是企业培训负责人,都能从中获得切实可行的技术方案与灵感。 第一章:项目规划与系统架构设计 1.1 需求分析与功能规划 在开始编码之前,明确的需求分析是成功的关键。一个完整的在线考试与题库管理系统应包含以下核心模块: 题库管理模块:支持单选题、多选题、判断题、填空题、问答题等多种题型;支持试题分类、标签、难度分级;支持试题导入导出(Word、Excel、JSON格式)。 试卷管理模块:支持手动组卷(按分类、难度随机抽题)和固定试卷;设置考试时间、及格分数、考试次数限制等参数。 考试执行模块:全屏考试防作弊、实时计时、自动保存答案、中途恢复功能。 成绩与统计分析模块:个人成绩报告、错题集、整体考试统计分析、成绩导出。 用户与权限管理:与WordPress用户系统集成,区分考生、教师、管理员角色。 1.2 技术选型与架构设计 考虑到系统的复杂性与WordPress的特性,我们采用分层架构: 数据层:在WordPress默认数据库基础上,创建自定义数据表来存储试题、试卷、考试记录等结构化数据。利用WordPress的$wpdb类进行安全的数据操作。 业务逻辑层:开发自定义插件,封装所有考试相关的业务逻辑。采用面向对象编程(OOP)设计,提高代码可维护性。 表现层:结合WordPress主题模板和短代码(Shortcode)或Gutenberg块,实现前端界面的灵活嵌入。使用AJAX提升考试过程的交互体验。 数据库设计示例: wp_exam_questions:试题表(ID、题型、题干、选项、答案、解析、分类、难度、创建时间) wp_exam_papers:试卷表(ID、试卷名称、组卷规则、设置参数) wp_exam_records:考试记录表(ID、用户ID、试卷ID、开始时间、结束时间、得分、答案详情) wp_exam_user_meta:用户扩展表(错题集、收藏试题等) 第二章:开发环境搭建与基础插件创建 2.1 本地开发环境配置 推荐使用Local by Flywheel或XAMPP搭建本地WordPress环境。确保环境支持PHP 7.4+、MySQL 5.6+。安装代码编辑器(如VS Code)并配置调试工具(如Xdebug)。 2.2 创建自定义插件骨架 在wp-content/plugins/目录下创建新文件夹exam-system,并创建主插件文件exam-system.php: <?php /** * Plugin Name: WordPress在线考试与题库管理系统 * Description: 在WordPress中集成完整的在线考试与题库管理功能 * Version: 1.0.0 * Author: 你的名字 */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('EXAM_SYSTEM_VERSION', '1.0.0'); define('EXAM_SYSTEM_PATH', plugin_dir_path(__FILE__)); define('EXAM_SYSTEM_URL', plugin_dir_url(__FILE__)); // 核心类加载 require_once EXAM_SYSTEM_PATH . 'includes/class-database.php'; require_once EXAM_SYSTEM_PATH . 'includes/class-question.php'; require_once EXAM_SYSTEM_PATH . 'includes/class-exam.php'; require_once EXAM_SYSTEM_PATH . 'includes/class-shortcodes.php'; require_once EXAM_SYSTEM_PATH . 'includes/class-admin.php'; // 初始化插件 class Exam_System_Plugin { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 激活/停用钩子 register_activation_hook(__FILE__, array('Exam_Database', 'create_tables')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化各模块 add_action('plugins_loaded', array($this, 'init_modules')); } public function init_modules() { Exam_Database::init(); Exam_Question::init(); Exam_Manager::init(); Exam_Shortcodes::init(); if (is_admin()) { Exam_Admin::init(); } } public function deactivate() { // 清理临时数据,但不删除考试记录 flush_rewrite_rules(); } } // 启动插件 Exam_System_Plugin::get_instance(); 2.3 数据库表创建 在includes/class-database.php中实现数据表的创建与升级: class Exam_Database { public static function init() { // 数据库版本控制 add_action('plugins_loaded', array(__CLASS__, 'check_db_version')); } public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'exam_'; // 试题表 $questions_table = $table_prefix . 'questions'; $sql1 = "CREATE TABLE IF NOT EXISTS $questions_table ( id bigint(20) NOT NULL AUTO_INCREMENT, question_type varchar(50) NOT NULL DEFAULT 'single_choice', title text NOT NULL, options longtext, correct_answer longtext NOT NULL, analysis text, category_id bigint(20) DEFAULT 0, difficulty tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY category_id (category_id), KEY difficulty (difficulty) ) $charset_collate;"; // 试卷表 $papers_table = $table_prefix . 'papers'; $sql2 = "CREATE TABLE IF NOT EXISTS $papers_table ( id bigint(20) NOT NULL AUTO_INCREMENT, paper_name varchar(255) NOT NULL, question_ids text, settings longtext, total_score int(11) DEFAULT 100, time_limit int(11) DEFAULT 60, status tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 考试记录表 $records_table = $table_prefix . 'records'; $sql3 = "CREATE TABLE IF NOT EXISTS $records_table ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, paper_id bigint(20) NOT NULL, start_time datetime, end_time datetime, score decimal(5,2) DEFAULT 0, answers longtext, is_passed tinyint(1) DEFAULT 0, PRIMARY KEY (id), KEY user_id (user_id), KEY paper_id (paper_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); // 存储数据库版本 add_option('exam_system_db_version', '1.0.0'); } public static function check_db_version() { if (get_option('exam_system_db_version') !== '1.0.0') { self::create_tables(); } } } 第三章:核心功能模块开发 3.1 题库管理模块实现 试题管理类 (includes/class-question.php): class Exam_Question { private static $question_types = array( 'single_choice' => '单选题', 'multi_choice' => '多选题', 'true_false' => '判断题', 'fill_blank' => '填空题', 'short_answer' => '简答题' ); public static function init() { add_action('admin_menu', array(__CLASS__, 'add_admin_menu')); add_action('wp_ajax_save_question', array(__CLASS__, 'ajax_save_question')); add_action('wp_ajax_delete_question', array(__CLASS__, 'ajax_delete_question')); } // 添加管理菜单 public static function add_admin_menu() { add_menu_page( '题库管理', '考试系统', 'manage_options', 'exam-system', array(__CLASS__, 'render_questions_page'), 'dashicons-welcome-learn-more', 30 ); add_submenu_page( 'exam-system', '试题管理', '试题管理', 'manage_options', 'exam-questions', array(__CLASS__, 'render_questions_page') ); } // 渲染试题管理页面 public static function render_questions_page() { ?> <div class="wrap"> <h1 class="wp-heading-inline">试题管理</h1> <button id="add-new-question" class="page-title-action">添加新试题</button> <hr class="wp-header-end"> <!-- 试题列表表格 --> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th width="5%">ID</th> <th width="40%">试题内容</th> <th width="10%">题型</th> <th width="10%">难度</th> <th width="15%">分类</th> <th width="20%">操作</th> </tr> </thead> <tbody id="questions-list"> <!-- 通过AJAX动态加载 --> </tbody> </table> <!-- 添加/编辑试题模态框 --> <div id="question-modal" class="modal" style="display:none;"> <div class="modal-content"> <span class="close">&times;</span> <h2>编辑试题</h2> <form id="question-form"> <input type="hidden" id="question_id" name="question_id" value="0"> <p> <label for="question_type">题型:</label> <select id="question_type" name="question_type"> <?php foreach(self::$question_types as $key => $label): ?> <option value="<?php echo esc_attr($key); ?>"><?php echo esc_html($label); ?></option> <?php endforeach; ?> </select> </p> <p> <label for="question_title">题干:</label><br> <textarea id="question_title" name="question_title" rows="3" cols="80" required></textarea> </p> <div id="options-container"> <!-- 选项内容动态生成 --> </div> <p> <label for="correct_answer">正确答案:</label><br> <textarea id="correct_answer" name="correct_answer" rows="2" cols="80" required></textarea> </p> <p> <label for="analysis">试题解析:</label><br> <textarea id="analysis" name="analysis" rows="3" cols="80"></textarea> </p> <p> <button type="submit" class="button button-primary">保存试题</button> </p> </form> </div> </div> </div> <style> .modal { display: none; position: fixed; z-index: 1000; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.4); } .modal-content { background-color: #fefefe; margin: 5% auto; padding: 20px; border: 1px solid #888; width: 80%; max-width: 800px; } .close { color: #aaa; float: right; font-size: 28px; font-weight: bold; cursor: pointer; } </style> <script> jQuery(document).ready(function($) { // 加载试题列表 function loadQuestions() { $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'get_questions', nonce: '<?php echo wp_create_nonce('exam_nonce'); ?>' }, success: function(response) { $('#questions-list').html(response.data); } }); } // 打开模态框 $('#add-new-question').click(function() { $('#question-modal').show(); $('#question-form')[0].reset(); $('#question_id').val('0'); }); // 关闭模态框 $('.close').click(function() { $('#question-modal').hide(); }); // 保存试题 $('#question-form').submit(function(e) { e.preventDefault(); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'save_question', nonce: '<?php echo wp_create_nonce('exam_nonce'); ?>', form_data: $(this).serialize() }, success: function(response) { if (response.success) { alert('保存成功!'); $('#question-modal').hide(); loadQuestions(); } else { alert('保存失败:' + response.data); } } }); }); // 初始加载 loadQuestions(); }); </script> <?php } // AJAX保存试题 public static function ajax_save_question() { check_ajax_referer('exam_nonce', 'nonce'); global $wpdb; $table_name = $wpdb->prefix . 'exam_questions'; parse_str($_POST['form_data'], $form_data); $data = array( 'question_type' => sanitize_text_field($form_data['question_type']), 'title' => wp_kses_post($form_data['question_title']), 'correct_answer' => sanitize_textarea_field($form_data['correct_answer']), 'analysis' => sanitize_textarea_field($form_data['analysis']), 'updated_at' => current_time('mysql') ); // 处理选项(针对选择题) if (in_array($form_data['question_type'], array('single_choice', 'multi_choice'))) { $options = array(); if (isset($form_data['option'])) { foreach ($form_data['option'] as $key => $value) { if (!empty(trim($value))) { $options[$key] = sanitize_text_field($value); } } } $data['options'] = maybe_serialize($options); } $question_id = intval($form_data['question_id']); if ($question_id > 0) { // 更新 $result = $wpdb->update($table_name, $data, array('id' => $question_id)); } else { // 新增 $data['created_at'] = current_time('mysql'); $result = $wpdb->insert($table_name, $data); $question_id = $wpdb->insert_id; } if ($result !== false) { wp_send_json_success('试题保存成功'); } else { wp_send_json_error('保存失败:' . $wpdb->last_error); } } } 3.2 试卷管理与组卷功能 试卷管理类 (includes/class-exam.php): class Exam_Manager { public static function init() { add_action('admin_menu', array(__CLASS__, 'add_exam_menu')); add_action('wp_ajax_generate_paper', array(__CLASS__, 'ajax_generate_paper')); add_action('wp_ajax_start_exam', array(__CLASS__, 'ajax_start_exam')); add_action('wp_ajax_submit_exam', array(__CLASS__, 'ajax_submit_exam')); // 注册短代码 add_shortcode('exam_list', array(__CLASS__, 'shortcode_exam_list')); add_shortcode('take_exam', array(__CLASS__, 'shortcode_take_exam')); add_shortcode('exam_results', array(__CLASS__, 'shortcode_exam_results')); } // 手动组卷功能 public static function render_paper_generator() { global $wpdb; $categories = $wpdb->get_results("SELECT DISTINCT category_id FROM {$wpdb->prefix}exam_questions"); ?> <div class="wrap"> <h1>试卷生成器</h1> <form id="paper-generator-form"> <table class="form-table"> <tr> <th><label for="paper_name">试卷名称</label></th> id="paper_name" name="paper_name" class="regular-text" required></td> </tr> <tr> <th><label>组卷方式</label></th> <td> <label><input type="radio" name="generate_type" value="manual" checked> 手动选题</label> <label><input type="radio" name="generate_type" value="auto"> 自动组卷</label> </td> </tr> <tbody id="manual-selection"> <tr> <th><label>选择试题</label></th> <td> <div style="max-height: 400px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;"> <?php $questions = $wpdb->get_results("SELECT id, title, question_type FROM {$wpdb->prefix}exam_questions ORDER BY id DESC"); foreach ($questions as $q) { $type_labels = Exam_Question::$question_types; $type = isset($type_labels[$q->question_type]) ? $type_labels[$q->question_type] : $q->question_type; echo '<label style="display: block; margin-bottom: 5px;">'; echo '<input type="checkbox" name="selected_questions[]" value="' . $q->id . '"> '; echo wp_trim_words(strip_tags($q->title), 10) . ' <small>(' . $type . ')</small>'; echo '</label>'; } ?> </div> </td> </tr> </tbody> <tbody id="auto-selection" style="display:none;"> <tr> <th><label>按分类组卷</label></th> <td> <div id="category-rules"> <div class="rule-row"> <select name="category_rules[0][category_id]" class="category-select"> <option value="0">所有分类</option> <?php foreach($categories as $cat): ?> <option value="<?php echo $cat->category_id; ?>">分类 <?php echo $cat->category_id; ?></option> <?php endforeach; ?> </select> <select name="category_rules[0][question_type]"> <option value="all">所有题型</option> <?php foreach(Exam_Question::$question_types as $key => $label): ?> <option value="<?php echo $key; ?>"><?php echo $label; ?></option> <?php endforeach; ?> </select> <input type="number" name="category_rules[0][count]" min="1" max="50" value="5" style="width: 60px;"> 题 <select name="category_rules[0][difficulty]"> <option value="0">所有难度</option> <option value="1">简单</option> <option value="2">中等</option> <option value="3">困难</option> </select> <button type="button" class="button remove-rule">删除</button> </div> </div> <button type="button" id="add-rule" class="button">添加规则</button> </td> </tr> </tbody> <tr> <th><label for="time_limit">考试时间(分钟)</label></th> <td><input type="number" id="time_limit" name="time_limit" min="1" max="300" value="60"></td> </tr> <tr> <th><label for="passing_score">及格分数</label></th> <td><input type="number" id="passing_score" name="passing_score" min="0" max="100" value="60"></td> </tr> <tr> <th><label for="attempts_allowed">考试次数限制</label></th> <td><input type="number" id="attempts_allowed" name="attempts_allowed" min="0" value="0"> (0表示无限制)</td> </tr> </table> <p class="submit"> <button type="submit" class="button button-primary">生成试卷</button> </p> </form> <div id="generated-paper" style="display:none; margin-top: 30px; padding: 20px; background: #f5f5f5; border: 1px solid #ddd;"> <h3>试卷生成成功!</h3> <div id="paper-preview"></div> <p> <button id="save-paper" class="button button-primary">保存试卷</button> <button id="regenerate-paper" class="button">重新生成</button> </p> </div> </div> <script> jQuery(document).ready(function($) { // 切换组卷方式 $('input[name="generate_type"]').change(function() { if ($(this).val() === 'manual') { $('#manual-selection').show(); $('#auto-selection').hide(); } else { $('#manual-selection').hide(); $('#auto-selection').show(); } }); // 添加规则行 var ruleIndex = 1; $('#add-rule').click(function() { var newRow = $('.rule-row:first').clone(); newRow.find('select, input').each(function() { var name = $(this).attr('name'); $(this).attr('name', name.replace('[0]', '[' + ruleIndex + ']')); }); newRow.appendTo('#category-rules'); ruleIndex++; }); // 删除规则行 $(document).on('click', '.remove-rule', function() { if ($('.rule-row').length > 1) { $(this).closest('.rule-row').remove(); } }); // 提交生成试卷 $('#paper-generator-form').submit(function(e) { e.preventDefault(); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'generate_paper', nonce: '<?php echo wp_create_nonce("exam_nonce"); ?>', form_data: $(this).serialize() }, success: function(response) { if (response.success) { $('#paper-preview').html(response.data.preview); $('#generated-paper').show().data('paper-data', response.data.paper_data); $('html, body').animate({ scrollTop: $('#generated-paper').offset().top }, 500); } else { alert('生成失败:' + response.data); } } }); }); // 保存试卷 $('#save-paper').click(function() { var paperData = $('#generated-paper').data('paper-data'); paperData.paper_name = $('#paper_name').val(); paperData.time_limit = $('#time_limit').val(); paperData.passing_score = $('#passing_score').val(); paperData.attempts_allowed = $('#attempts_allowed').val(); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'save_paper', nonce: '<?php echo wp_create_nonce("exam_nonce"); ?>', paper_data: paperData }, success: function(response) { if (response.success) { alert('试卷保存成功!'); window.location.href = '<?php echo admin_url("admin.php?page=exam-papers"); ?>'; } else { alert('保存失败:' + response.data); } } }); }); }); </script> <?php } // AJAX生成试卷 public static function ajax_generate_paper() { check_ajax_referer('exam_nonce', 'nonce'); global $wpdb; parse_str($_POST['form_data'], $form_data); $question_ids = array(); if ($form_data['generate_type'] === 'manual') { // 手动选题 if (!empty($form_data['selected_questions'])) { $question_ids = array_map('intval', $form_data['selected_questions']); } } else { // 自动组卷 foreach ($form_data['category_rules'] as $rule) { $where = array('1=1'); $params = array(); if (!empty($rule['category_id']) && $rule['category_id'] != '0') { $where[] = 'category_id = %d'; $params[] = intval($rule['category_id']); } if (!empty($rule['question_type']) && $rule['question_type'] != 'all') { $where[] = 'question_type = %s'; $params[] = sanitize_text_field($rule['question_type']); } if (!empty($rule['difficulty']) && $rule['difficulty'] != '0') { $where[] = 'difficulty = %d'; $params[] = intval($rule['difficulty']); } $sql = "SELECT id FROM {$wpdb->prefix}exam_questions WHERE " . implode(' AND ', $where) . " ORDER BY RAND() LIMIT %d"; $params[] = intval($rule['count']); if (!empty($params)) { $results = $wpdb->get_col($wpdb->prepare($sql, $params)); $question_ids = array_merge($question_ids, $results); } } } if (empty($question_ids)) { wp_send_json_error('没有选择任何试题'); } // 生成预览 $preview = '<h4>试卷包含 ' . count($question_ids) . ' 道试题:</h4><ol>'; foreach ($question_ids as $qid) { $question = $wpdb->get_row($wpdb->prepare("SELECT title, question_type FROM {$wpdb->prefix}exam_questions WHERE id = %d", $qid)); if ($question) { $type_labels = Exam_Question::$question_types; $type = isset($type_labels[$question->question_type]) ? $type_labels[$question->question_type] : $question->question_type; $preview .= '<li>' . wp_trim_words(strip_tags($question->title), 15) . ' <em>(' . $type . ')</em></li>'; } } $preview .= '</ol>'; wp_send_json_success(array( 'preview' => $preview, 'paper_data' => array( 'question_ids' => $question_ids, 'total_questions' => count($question_ids) ) )); } } 第四章:前端考试界面与交互实现 4.1 考试短代码与页面模板 短代码类 (includes/class-shortcodes.php): class Exam_Shortcodes { public static function init() { // 短代码已在Exam_Manager中注册 add_action('wp_enqueue_scripts', array(__CLASS__, 'enqueue_frontend_scripts')); } public static function enqueue_frontend_scripts() { wp_enqueue_style('exam-frontend', EXAM_SYSTEM_URL . 'assets/css/frontend.css', array(), EXAM_SYSTEM_VERSION); wp_enqueue_script('exam-frontend', EXAM_SYSTEM_URL . 'assets/js/frontend.js', array('jquery'), EXAM_SYSTEM_VERSION, true); wp_localize_script('exam-frontend', 'exam_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('exam_frontend_nonce') )); } // 考试列表短代码 public static function shortcode_exam_list($atts) { if (!is_user_logged_in()) { return '<div class="exam-notice">请先登录后参加考试</div>'; } global $wpdb; $papers = $wpdb->get_results("SELECT * FROM {$wpdb->prefix}exam_papers WHERE status = 1 ORDER BY id DESC"); $output = '<div class="exam-list-container">'; $output .= '<h2>可用考试</h2>'; if (empty($papers)) { $output .= '<p>暂无可用考试</p>'; } else { $output .= '<div class="exam-grid">'; foreach ($papers as $paper) { $settings = maybe_unserialize($paper->settings); $attempts_allowed = isset($settings['attempts_allowed']) ? $settings['attempts_allowed'] : 0; // 检查已考次数 $attempts = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d", get_current_user_id(), $paper->id )); $can_take = ($attempts_allowed == 0 || $attempts < $attempts_allowed); $output .= '<div class="exam-card">'; $output .= '<h3>' . esc_html($paper->paper_name) . '</h3>'; $output .= '<ul class="exam-meta">'; $output .= '<li>题数: ' . $paper->total_questions . '</li>'; $output .= '<li>时间: ' . $paper->time_limit . '分钟</li>'; $output .= '<li>及格分: ' . $paper->passing_score . '</li>'; $output .= '<li>已考次数: ' . $attempts . ($attempts_allowed > 0 ? '/' . $attempts_allowed : '') . '</li>'; $output .= '</ul>'; if ($can_take) { $output .= '<a href="?exam_id=' . $paper->id . '" class="button start-exam-btn">开始考试</a>'; } else { $output .= '<button class="button disabled" disabled>考试次数已用完</button>'; } // 查看历史成绩 $best_score = $wpdb->get_var($wpdb->prepare( "SELECT MAX(score) FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d", get_current_user_id(), $paper->id )); if ($best_score !== null) { $output .= '<div class="best-score">最佳成绩: ' . floatval($best_score) . '分</div>'; } $output .= '</div>'; } $output .= '</div>'; } $output .= '</div>'; return $output; } // 参加考试短代码 public static function shortcode_take_exam($atts) { if (!is_user_logged_in()) { return '<div class="exam-notice">请先登录后参加考试</div>'; } if (!isset($_GET['exam_id'])) { return '<div class="exam-notice">请选择要参加的考试</div>'; } $paper_id = intval($_GET['exam_id']); $user_id = get_current_user_id(); global $wpdb; // 检查考试次数限制 $paper = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_papers WHERE id = %d AND status = 1", $paper_id )); if (!$paper) { return '<div class="exam-error">考试不存在或已关闭</div>'; } $settings = maybe_unserialize($paper->settings); $attempts_allowed = isset($settings['attempts_allowed']) ? $settings['attempts_allowed'] : 0; $attempts = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d", $user_id, $paper_id )); if ($attempts_allowed > 0 && $attempts >= $attempts_allowed) { return '<div class="exam-error">您已达到该考试的最大尝试次数</div>'; } // 检查是否有未完成的考试 $unfinished = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d AND end_time IS NULL", $user_id, $paper_id )); if ($unfinished) { // 恢复未完成的考试 $record_id = $unfinished->id; $remaining_time = $paper->time_limit * 60 - (time() - strtotime($unfinished->start_time)); $remaining_time = max(0, $remaining_time); } else { // 创建新的考试记录 $wpdb->insert( $wpdb->prefix . 'exam_records', array( 'user_id' => $user_id, 'paper_id' => $paper_id, 'start_time' => current_time('mysql'), 'answers' => maybe_serialize(array()) ) ); $record_id = $wpdb->insert_id; $remaining_time = $paper->time_limit * 60; } // 获取试题 $question_ids = maybe_unserialize($paper->question_ids); if (empty($question_ids)) { return '<div class="exam-error">试卷试题配置错误</div>'; } $questions = array(); foreach ($question_ids as $qid) { $question = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_questions WHERE id = %d", $qid )); if ($question) { $question->options = maybe_unserialize($question->options); $questions[] = $question; } } // 输出考试界面

发表评论

WordPress插件开发教程,实现网站广告位智能管理与投放

WordPress插件开发教程:实现网站广告位智能管理与投放 引言:为什么需要广告位智能管理插件 在当今的数字营销时代,网站广告已成为许多网站盈利的重要方式。然而,对于大多数WordPress网站管理员来说,广告管理常常是一个令人头疼的问题。手动插入广告代码不仅效率低下,而且难以实现精准投放、效果追踪和优化调整。传统的广告管理方式存在诸多局限性:广告位置固定无法根据用户行为调整、缺乏数据统计功能、无法实现A/B测试、难以针对不同设备展示不同广告等。 正是这些痛点催生了智能广告管理系统的需求。通过开发一个专业的WordPress插件,我们可以实现广告位的自动化、智能化管理,让广告投放变得更加高效和精准。本教程将带领您从零开始,开发一个功能完整的WordPress广告位智能管理插件,同时展示如何通过WordPress代码二次开发实现常用互联网小工具功能。 第一章:WordPress插件开发基础与环境搭建 1.1 WordPress插件架构概述 WordPress插件本质上是一组PHP文件,遵循特定的结构和规范,用于扩展WordPress核心功能。一个标准的WordPress插件至少包含一个主PHP文件,该文件头部包含特定的插件信息注释。插件可以包含CSS、JavaScript、图片资源以及其他PHP文件。 插件开发的核心原则包括:遵循WordPress编码标准、确保插件安全性和稳定性、提供清晰的用户界面、保持与WordPress核心和其他插件的兼容性。我们的广告管理插件将采用模块化设计,将不同功能分离到不同的类和文件中,便于维护和扩展。 1.2 开发环境配置 在开始开发之前,我们需要搭建合适的开发环境: 本地服务器环境:推荐使用XAMPP、MAMP或Local by Flywheel,它们提供了集成的Apache、MySQL和PHP环境。 WordPress安装:下载最新版本的WordPress,安装在本地服务器上。建议创建一个专门用于开发的测试站点。 代码编辑器:选择功能强大的代码编辑器,如Visual Studio Code、PHPStorm或Sublime Text。这些编辑器提供了代码高亮、智能提示和调试功能。 调试工具:在wp-config.php中启用WordPress调试模式: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 版本控制:使用Git进行版本控制,便于团队协作和代码管理。 1.3 创建插件基本结构 首先,在WordPress的wp-content/plugins目录下创建我们的插件文件夹,命名为"smart-ad-manager"。在该文件夹中创建以下基本文件结构: smart-ad-manager/ ├── smart-ad-manager.php # 主插件文件 ├── includes/ # 包含核心类文件 │ ├── class-ad-manager.php │ ├── class-ad-renderer.php │ └── class-ad-analytics.php ├── admin/ # 后台管理相关文件 │ ├── css/ │ ├── js/ │ └── partials/ ├── public/ # 前端相关文件 │ ├── css/ │ ├── js/ │ └── templates/ ├── assets/ # 静态资源 └── uninstall.php # 插件卸载脚本 接下来,编辑主插件文件smart-ad-manager.php,添加插件头部信息: <?php /** * Plugin Name: Smart Ad Manager * Plugin URI: https://yourwebsite.com/smart-ad-manager * Description: 智能广告位管理与投放系统,支持精准投放、效果追踪和优化调整。 * Version: 1.0.0 * Author: Your Name * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: smart-ad-manager * Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('SAM_VERSION', '1.0.0'); define('SAM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SAM_PLUGIN_URL', plugin_dir_url(__FILE__)); define('SAM_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 初始化插件 require_once SAM_PLUGIN_DIR . 'includes/class-ad-manager.php'; function run_smart_ad_manager() { $plugin = new Ad_Manager(); $plugin->run(); } run_smart_ad_manager(); 第二章:广告位管理系统的核心功能实现 2.1 数据库设计与广告位存储 广告数据需要存储在数据库中,以便持久化和高效查询。我们将创建自定义数据库表来存储广告信息: // 在Ad_Manager类中添加数据库创建方法 class Ad_Manager { public function create_ad_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'sam_ads'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL, ad_type varchar(50) NOT NULL, ad_code text NOT NULL, ad_image varchar(500), ad_link varchar(500), start_date datetime, end_date datetime, max_impressions int DEFAULT 0, current_impressions int DEFAULT 0, max_clicks int DEFAULT 0, current_clicks int DEFAULT 0, status varchar(20) DEFAULT 'active', targeting_rules text, priority int DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建广告位表 $positions_table = $wpdb->prefix . 'sam_ad_positions'; $sql2 = "CREATE TABLE IF NOT EXISTS $positions_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, position_key varchar(100) NOT NULL, position_name varchar(200) NOT NULL, description text, default_ad_id mediumint(9), max_ads int DEFAULT 1, dimensions varchar(50), allowed_types varchar(200), is_active tinyint(1) DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY position_key (position_key) ) $charset_collate;"; dbDelta($sql2); } // 插件激活时创建表 public static function activate() { self::create_ad_tables(); flush_rewrite_rules(); } } 2.2 广告位管理后台界面开发 创建一个用户友好的后台界面是插件成功的关键。我们将使用WordPress的设置API和自定义管理页面来构建广告管理后台: class Ad_Manager_Admin { private $ad_manager; public function __construct($ad_manager) { $this->ad_manager = $ad_manager; add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); add_action('wp_ajax_sam_save_ad', array($this, 'ajax_save_ad')); add_action('wp_ajax_sam_get_ads', array($this, 'ajax_get_ads')); } public function add_admin_menu() { // 主菜单 add_menu_page( '智能广告管理', '广告管理', 'manage_options', 'smart-ad-manager', array($this, 'display_main_page'), 'dashicons-megaphone', 30 ); // 子菜单 add_submenu_page( 'smart-ad-manager', '广告列表', '广告列表', 'manage_options', 'sam-ads', array($this, 'display_ads_page') ); add_submenu_page( 'smart-ad-manager', '广告位设置', '广告位设置', 'manage_options', 'sam-positions', array($this, 'display_positions_page') ); add_submenu_page( 'smart-ad-manager', '数据统计', '数据统计', 'manage_options', 'sam-analytics', array($this, 'display_analytics_page') ); } public function display_main_page() { include SAM_PLUGIN_DIR . 'admin/partials/dashboard.php'; } public function enqueue_admin_scripts($hook) { if (strpos($hook, 'smart-ad-manager') !== false) { wp_enqueue_style('sam-admin-style', SAM_PLUGIN_URL . 'admin/css/admin.css', array(), SAM_VERSION); wp_enqueue_script('sam-admin-script', SAM_PLUGIN_URL . 'admin/js/admin.js', array('jquery', 'jquery-ui-sortable'), SAM_VERSION, true); wp_localize_script('sam-admin-script', 'sam_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('sam_ajax_nonce') )); } } } 2.3 广告投放逻辑与智能匹配算法 智能广告投放的核心是根据多种条件匹配最合适的广告。我们将实现一个智能匹配算法: class Ad_Renderer { public function get_ad_for_position($position_key, $context = array()) { global $wpdb; // 获取广告位信息 $positions_table = $wpdb->prefix . 'sam_ad_positions'; $position = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $positions_table WHERE position_key = %s AND is_active = 1", $position_key )); if (!$position) { return ''; } // 获取该广告位可用的广告 $ads_table = $wpdb->prefix . 'sam_ads'; $query = "SELECT * FROM $ads_table WHERE status = 'active' AND (start_date IS NULL OR start_date <= NOW()) AND (end_date IS NULL OR end_date >= NOW())"; // 添加智能匹配条件 $conditions = array(); $params = array(); // 1. 设备类型匹配 if (isset($context['device'])) { $conditions[] = "(targeting_rules LIKE %s OR targeting_rules IS NULL OR targeting_rules = '')"; $params[] = '%"device":"' . $context['device'] . '"%'; } // 2. 用户地理位置匹配 if (isset($context['location'])) { $conditions[] = "(targeting_rules LIKE %s OR targeting_rules IS NULL OR targeting_rules = '')"; $params[] = '%"location":"' . $context['location'] . '"%'; } // 3. 页面类型匹配 if (isset($context['page_type'])) { $conditions[] = "(targeting_rules LIKE %s OR targeting_rules IS NULL OR targeting_rules = '')"; $params[] = '%"page_type":"' . $context['page_type'] . '"%'; } if (!empty($conditions)) { $query .= " AND (" . implode(" OR ", $conditions) . ")"; } $query .= " ORDER BY priority DESC, RAND() LIMIT %d"; $params[] = $position->max_ads; if (!empty($params)) { $ads = $wpdb->get_results($wpdb->prepare($query, $params)); } else { $ads = $wpdb->get_results($wpdb->prepare($query . " LIMIT %d", $position->max_ads)); } if (empty($ads)) { // 返回默认广告或空 return $this->get_default_ad($position); } // 渲染广告 return $this->render_ads($ads, $position); } private function render_ads($ads, $position) { $output = '<div class="sam-ad-container" data-position="' . esc_attr($position->position_key) . '">'; foreach ($ads as $ad) { $output .= $this->render_single_ad($ad); // 更新展示次数 $this->record_impression($ad->id); } $output .= '</div>'; return $output; } } 第三章:通过短代码和Widget实现广告位插入 3.1 创建广告短代码 短代码是WordPress中插入动态内容的简便方式。我们将创建广告短代码,让用户可以在文章、页面或侧边栏中轻松插入广告: class Ad_Shortcodes { public function init() { add_shortcode('display_ad', array($this, 'display_ad_shortcode')); add_shortcode('ad_position', array($this, 'ad_position_shortcode')); } public function display_ad_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'position' => '', 'class' => '', 'style' => '' ), $atts, 'display_ad'); if (!empty($atts['id'])) { // 显示特定ID的广告 return $this->render_ad_by_id($atts['id'], $atts); } elseif (!empty($atts['position'])) { // 显示特定广告位的广告 $renderer = new Ad_Renderer(); $context = $this->get_current_context(); return $renderer->get_ad_for_position($atts['position'], $context); } return ''; } public function ad_position_shortcode($atts) { $atts = shortcode_atts(array( 'key' => '', 'title' => '', 'description' => '', 'width' => 'auto', 'height' => 'auto' ), $atts, 'ad_position'); if (empty($atts['key'])) { return '<p>请指定广告位key</p>'; } // 检查广告位是否存在,不存在则创建 $this->create_position_if_not_exists($atts); $renderer = new Ad_Renderer(); $context = $this->get_current_context(); return $renderer->get_ad_for_position($atts['key'], $context); } private function get_current_context() { $context = array(); // 检测设备类型 $context['device'] = wp_is_mobile() ? 'mobile' : 'desktop'; // 检测页面类型 if (is_front_page()) { $context['page_type'] = 'home'; } elseif (is_single()) { $context['page_type'] = 'single'; } elseif (is_page()) { $context['page_type'] = 'page'; } elseif (is_category()) { $context['page_type'] = 'category'; } return $context; } } 3.2 开发广告位Widget Widget是WordPress侧边栏和小工具区域的内容块。我们将创建一个广告位Widget,让用户可以通过拖拽方式在侧边栏添加广告: class Ad_Position_Widget extends WP_Widget { public function __construct() { parent::__construct( 'sam_ad_widget', __('智能广告位', 'smart-ad-manager'), array( 'description' => __('在侧边栏显示智能广告', 'smart-ad-manager'), 'classname' => 'sam-ad-widget' ) ); } public function widget($args, $instance) { $title = apply_filters('widget_title', empty($instance['title']) ? '' : $instance['title'], $instance, $this->id_base); $position_key = empty($instance['position_key']) ? '' : $instance['position_key']; if (empty($position_key)) { return; } echo $args['before_widget']; if ($title) { echo $args['before_title'] . $title . $args['after_title']; } $renderer = new Ad_Renderer(); $context = array( 'device' => wp_is_mobile() ? 'mobile' : 'desktop', 'location' => 'sidebar' ); echo $renderer->get_ad_for_position($position_key, $context); echo $args['after_widget']; } public function form($instance) { global $wpdb; $title = isset($instance['title']) ? esc_attr($instance['title']) : ''; $position_key = isset($instance['position_key']) ? esc_attr($instance['position_key']) : ''; // 获取所有广告位 $positions_table = $wpdb->prefix . 'sam_ad_positions'; $positions = $wpdb->get_results("SELECT * FROM $positions_table WHERE is_active = 1 ORDER BY position_name"); ?> <p> <label for="<?php echo $this->get_field_id('title'); ?>"> <?php _e('标题:', 'smart-ad-manager'); ?> </label> <input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo $title; ?>"> </p> <p> <label for="<?php echo $this->get_field_id('position_key'); ?>"> <?php _e('选择广告位:', 'smart-ad-manager'); ?> </label> <select class="widefat" id="<?php echo $this->get_field_id('position_key'); ?>" name="<?php echo $this->get_field_name('position_key'); ?>"> <option value=""><?php _e('-- 请选择广告位 --', 'smart-ad-manager'); ?></option> <?php foreach ($positions as $position): ?> <option value="<?php echo esc_attr($position->position_key); ?>" <?php selected($position_key, $position->position_key); ?>> 第四章:广告效果追踪与数据分析系统 4.1 数据采集与存储机制 要实现智能广告管理,必须建立完善的数据追踪系统。我们需要记录广告的展示、点击、转化等关键指标: class Ad_Analytics { private $db; public function __construct() { global $wpdb; $this->db = $wpdb; // 创建数据追踪表 $this->create_tracking_tables(); // 注册追踪钩子 add_action('wp_ajax_nopriv_sam_track_click', array($this, 'track_click')); add_action('wp_ajax_sam_track_click', array($this, 'track_click')); add_action('wp_ajax_nopriv_sam_track_impression', array($this, 'track_impression')); add_action('wp_ajax_sam_track_impression', array($this, 'track_impression')); } private function create_tracking_tables() { $charset_collate = $this->db->get_charset_collate(); // 点击记录表 $clicks_table = $this->db->prefix . 'sam_ad_clicks'; $sql = "CREATE TABLE IF NOT EXISTS $clicks_table ( id bigint(20) NOT NULL AUTO_INCREMENT, ad_id mediumint(9) NOT NULL, user_id bigint(20), session_id varchar(100), ip_address varchar(45), user_agent text, referrer varchar(500), page_url varchar(500), click_time datetime DEFAULT CURRENT_TIMESTAMP, device_type varchar(20), browser varchar(100), os varchar(50), country varchar(100), city varchar(100), conversion_value decimal(10,2) DEFAULT 0, conversion_time datetime, PRIMARY KEY (id), KEY ad_id (ad_id), KEY click_time (click_time) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 展示记录表 $impressions_table = $this->db->prefix . 'sam_ad_impressions'; $sql2 = "CREATE TABLE IF NOT EXISTS $impressions_table ( id bigint(20) NOT NULL AUTO_INCREMENT, ad_id mediumint(9) NOT NULL, position_key varchar(100), user_id bigint(20), session_id varchar(100), ip_address varchar(45), impression_time datetime DEFAULT CURRENT_TIMESTAMP, device_type varchar(20), page_type varchar(50), is_unique tinyint(1) DEFAULT 1, PRIMARY KEY (id), KEY ad_id (ad_id), KEY impression_time (impression_time) ) $charset_collate;"; dbDelta($sql2); } public function track_click() { // 验证nonce check_ajax_referer('sam_tracking_nonce', 'security'); $ad_id = intval($_POST['ad_id']); $session_id = sanitize_text_field($_POST['session_id']); // 收集用户信息 $user_info = $this->collect_user_info(); // 记录点击 $this->db->insert( $this->db->prefix . 'sam_ad_clicks', array( 'ad_id' => $ad_id, 'user_id' => get_current_user_id(), 'session_id' => $session_id, 'ip_address' => $user_info['ip'], 'user_agent' => $user_info['user_agent'], 'referrer' => $user_info['referrer'], 'page_url' => $user_info['page_url'], 'device_type' => $user_info['device_type'], 'browser' => $user_info['browser'], 'os' => $user_info['os'], 'country' => $user_info['country'], 'city' => $user_info['city'] ) ); // 更新广告点击计数 $this->update_ad_click_count($ad_id); wp_die(); } private function collect_user_info() { $info = array(); // IP地址 $info['ip'] = $this->get_user_ip(); // User Agent $info['user_agent'] = $_SERVER['HTTP_USER_AGENT'] ?? ''; // Referrer $info['referrer'] = $_SERVER['HTTP_REFERER'] ?? ''; // 当前页面URL $info['page_url'] = home_url(add_query_arg(array())); // 设备类型 $info['device_type'] = wp_is_mobile() ? 'mobile' : 'desktop'; // 浏览器和操作系统信息 $browser_info = $this->parse_user_agent($info['user_agent']); $info['browser'] = $browser_info['browser']; $info['os'] = $browser_info['os']; // 地理位置(可以使用第三方API或本地数据库) $info['country'] = $this->get_country_from_ip($info['ip']); $info['city'] = $this->get_city_from_ip($info['ip']); return $info; } } 4.2 数据统计与可视化展示 创建直观的数据统计界面,帮助管理员了解广告效果: class Ad_Statistics { public function display_statistics_page() { ?> <div class="wrap sam-statistics"> <h1><?php _e('广告数据统计', 'smart-ad-manager'); ?></h1> <div class="sam-stat-filters"> <form method="get" action=""> <input type="hidden" name="page" value="sam-analytics"> <label for="date_range"><?php _e('时间范围:', 'smart-ad-manager'); ?></label> <select name="date_range" id="date_range"> <option value="today"><?php _e('今天', 'smart-ad-manager'); ?></option> <option value="yesterday"><?php _e('昨天', 'smart-ad-manager'); ?></option> <option value="last7days"><?php _e('最近7天', 'smart-ad-manager'); ?></option> <option value="last30days"><?php _e('最近30天', 'smart-ad-manager'); ?></option> <option value="custom"><?php _e('自定义', 'smart-ad-manager'); ?></option> </select> <div id="custom-date-range" style="display:none;"> <input type="date" name="start_date" value="<?php echo date('Y-m-d', strtotime('-7 days')); ?>"> <input type="date" name="end_date" value="<?php echo date('Y-m-d'); ?>"> </div> <label for="ad_id"><?php _e('广告:', 'smart-ad-manager'); ?></label> <select name="ad_id" id="ad_id"> <option value=""><?php _e('所有广告', 'smart-ad-manager'); ?></option> <?php $this->render_ad_options(); ?> </select> <button type="submit" class="button button-primary"><?php _e('筛选', 'smart-ad-manager'); ?></button> </form> </div> <div class="sam-stat-overview"> <?php $this->display_overview_stats(); ?> </div> <div class="sam-stat-charts"> <div class="chart-container"> <h3><?php _e('点击率趋势', 'smart-ad-manager'); ?></h3> <canvas id="ctr-chart" width="400" height="200"></canvas> </div> <div class="chart-container"> <h3><?php _e('设备分布', 'smart-ad-manager'); ?></h3> <canvas id="device-chart" width="400" height="200"></canvas> </div> </div> <div class="sam-stat-details"> <h3><?php _e('详细数据', 'smart-ad-manager'); ?></h3> <?php $this->display_detailed_stats_table(); ?> </div> </div> <script> jQuery(document).ready(function($) { // 初始化图表 var ctrChart = new Chart($('#ctr-chart'), { type: 'line', data: { labels: <?php echo json_encode($this->get_chart_labels()); ?>, datasets: [{ label: '点击率(%)', data: <?php echo json_encode($this->get_ctr_data()); ?>, borderColor: 'rgb(75, 192, 192)', tension: 0.1 }] } }); var deviceChart = new Chart($('#device-chart'), { type: 'doughnut', data: { labels: ['桌面端', '移动端', '平板'], datasets: [{ data: <?php echo json_encode($this->get_device_distribution()); ?>, backgroundColor: [ 'rgb(255, 99, 132)', 'rgb(54, 162, 235)', 'rgb(255, 205, 86)' ] }] } }); }); </script> <?php } private function get_overview_stats($start_date = null, $end_date = null) { global $wpdb; $clicks_table = $wpdb->prefix . 'sam_ad_clicks'; $impressions_table = $wpdb->prefix . 'sam_ad_impressions'; $where_clause = ''; if ($start_date && $end_date) { $where_clause = $wpdb->prepare( "WHERE click_time >= %s AND click_time <= %s", $start_date . ' 00:00:00', $end_date . ' 23:59:59' ); } // 总点击量 $total_clicks = $wpdb->get_var( "SELECT COUNT(*) FROM $clicks_table $where_clause" ); // 总展示量 $total_impressions = $wpdb->get_var( "SELECT COUNT(*) FROM $impressions_table " . str_replace('click_time', 'impression_time', $where_clause) ); // 点击率 $ctr = $total_impressions > 0 ? round(($total_clicks / $total_impressions) * 100, 2) : 0; // 独立访客数 $unique_visitors = $wpdb->get_var( "SELECT COUNT(DISTINCT session_id) FROM $impressions_table " . str_replace('click_time', 'impression_time', $where_clause) ); return array( 'total_clicks' => $total_clicks, 'total_impressions' => $total_impressions, 'ctr' => $ctr, 'unique_visitors' => $unique_visitors ); } } 第五章:高级功能实现与优化 5.1 A/B测试功能 A/B测试是优化广告效果的重要手段。我们将实现一个简单的A/B测试系统: class Ad_AB_Testing { public function init() { add_action('sam_ad_selection', array($this, 'apply_ab_testing'), 10, 3); add_action('wp_ajax_sam_create_ab_test', array($this, 'create_ab_test')); } public function apply_ab_testing($ads, $position_key, $context) { // 检查该广告位是否有正在进行的A/B测试 $active_tests = $this->get_active_tests($position_key); if (empty($active_tests)) { return $ads; } foreach ($active_tests as $test) { // 根据测试分组分配用户 $user_group = $this->assign_user_to_group($test); if ($user_group === 'control') { // 控制组:显示原始广告 continue; } // 实验组:显示测试广告 $variant_ads = $this->get_test_variants($test->id); if (!empty($variant_ads)) { // 替换或添加测试广告 $ads = $this->merge_test_ads($ads, $variant_ads, $test); } } return $ads; } private function assign_user_to_group($test) { $session_id = $this->get_session_id(); // 检查用户是否已经分配了组别 $existing_group = $this->get_user_test_group($test->id, $session_id); if ($existing_group) { return $existing_group; } // 根据测试配置分配组别 $groups = array('control', 'variant_a', 'variant_b'); $weights = array( 'control' => $test->control_percentage, 'variant_a' => $test->variant_a_percentage, 'variant_b' => $test->variant_b_percentage ); $group = $this->weighted_random_selection($weights); // 记录用户分组 $this->record_user_group($test->id, $session_id, $group); return $group; } public function create_ab_test() { check_ajax_referer('sam_admin_nonce', 'nonce'); $test_data = array( 'name' => sanitize_text_field($_POST['test_name']), 'position_key' => sanitize_text_field($_POST['position_key']), 'control_ad_id' => intval($_POST['control_ad_id']), 'variant_a_ad_id' => intval($_POST['variant_a_ad_id']), 'variant_b_ad_id' => intval($_POST['variant_b_ad_id']), 'control_percentage' => intval($_POST['control_percentage']), 'variant_a_percentage' => intval($_POST['variant_a_percentage']), 'variant_b_percentage' => intval($_POST['variant_b_percentage']), 'start_date' => sanitize_text_field($_POST['start_date']), 'end_date' => sanitize_text_field($_POST['end_date']), 'primary_metric' => sanitize_text_field($_POST['primary_metric']), 'confidence_level' => floatval($_POST['confidence_level']) ); global $wpdb; $tests_table = $wpdb->prefix . 'sam_ab_tests'; $wpdb->insert($tests_table, $test_data); wp_send_json_success(array( 'message' => __('A/B测试创建成功', 'smart-ad-manager'), 'test_id' => $wpdb->insert_id )); } public function evaluate_test_results($test_id) { global $wpdb; $test = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}sam_ab_tests WHERE id = %d", $test_id )); if (!$test) { return false; } // 收集各组数据 $groups = array('control', 'variant_a', 'variant_b'); $results = array(); foreach ($groups as $group) { $ad_id_field = $group . '_ad_id'; $ad_id = $test->$ad_id_field; if (!$ad_id) { continue; } $results[$group] = array( 'impressions' => $this->get_ad_impressions($ad_id, $test->start_date, $test->end_date), 'clicks' => $this->get_ad_clicks($ad_id, $test->start_date, $test->end_date), 'conversions' => $this->get_ad_conversions($ad_id, $test->start_date, $test->end_date) ); // 计算指标 if ($results[$group]['impressions'] > 0) { $results[$group]['ctr'] = ($results[$group]['clicks'] / $results[$group]['impressions']) * 100; } else { $results[$group]['ctr'] = 0; } } // 统计显著性检验 $significance_results = $this->calculate_statistical_significance($results); // 判断测试结果 $conclusion = $this->draw_test_conclusion($significance_results, $test->confidence_level); // 更新测试状态 $this->update_test_status($test_id, 'completed', $conclusion); return array( 'results' => $results, 'significance' => $significance_results, 'conclusion' => $conclusion ); } } 5.2 智能优化算法 基于收集的数据,实现广告投放的智能优化: class Ad_Optimizer { private $learning_rate = 0.1; private $exploration_rate = 0.2; public function optimize_ad_selection($ads, $context) { if (empty($ads)) { return $ads; } // 计算每个广告的预期价值 $ad_values = array(); foreach ($ads as $ad) { $ad_values[$ad->id] = $this->calculate_expected_value($ad, $context); } // 探索-利用平衡 if (mt_rand(0, 100) / 100 < $this->exploration_rate) { // 探索:随机选择一个广告 $selected_key = array_rand($ads); } else { // 利用:选择价值最高的广告 arsort($ad_values); $selected_key = array_key_first($ad_values); } // 重新排序广告,将最优广告放在前面 $optimized_ads = array(); $optimized_ads[] = $ads[$selected_key];

发表评论

实战教程,在网站中添加用户生成内容管理与审核系统

实战教程:在WordPress网站中添加用户生成内容管理与审核系统 引言:用户生成内容的价值与挑战 在当今互联网环境中,用户生成内容(User-Generated Content, UGC)已成为网站互动性和价值的重要组成部分。从电商网站的产品评价、社交媒体平台的用户分享,到知识社区的问答讨论,UGC不仅丰富了网站内容,还增强了用户参与感和社区凝聚力。然而,UGC管理也带来了诸多挑战:垃圾信息泛滥、不当内容传播、版权问题以及内容质量参差不齐等。 对于WordPress网站管理员而言,构建一个高效、安全的UGC管理与审核系统至关重要。本教程将深入探讨如何通过WordPress代码二次开发,实现一个完整的用户生成内容管理与审核系统,同时集成常用互联网小工具功能,提升网站互动性和管理效率。 第一部分:系统架构设计与规划 1.1 用户生成内容类型分析 在开始开发前,我们需要明确网站需要管理的UGC类型: 评论系统:文章评论、产品评价 用户提交内容:文章投稿、图片上传、视频分享 社区互动:论坛帖子、问答内容 用户资料:个人简介、头像、联系方式 1.2 系统功能需求规划 一个完整的UGC管理系统应包含以下核心功能: 内容提交接口:用户友好的内容提交表单 审核工作流:多级审核机制与状态管理 垃圾过滤:自动识别与过滤垃圾内容 用户信誉系统:基于用户行为的信任评分 通知系统:审核状态通知与用户提醒 管理后台:高效的内容审核与管理界面 数据分析:UGC数据统计与报告 1.3 技术架构设计 我们将采用以下技术架构: 前端:HTML5、CSS3、JavaScript(jQuery/AJAX) 后端:PHP(WordPress核心API) 数据库:MySQL(WordPress数据库结构) 安全机制:Nonce验证、权限检查、数据过滤 第二部分:基础环境搭建与配置 2.1 创建自定义插件 首先,我们需要创建一个独立的WordPress插件来管理所有UGC功能: <?php /** * Plugin Name: UGC管理与审核系统 * Plugin URI: https://yourwebsite.com/ * Description: 用户生成内容管理与审核系统 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('UGC_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('UGC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('UGC_VERSION', '1.0.0'); // 初始化插件 function ugc_system_init() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { wp_die('本插件需要WordPress 5.0或更高版本'); } // 加载核心功能 require_once UGC_PLUGIN_PATH . 'includes/core-functions.php'; require_once UGC_PLUGIN_PATH . 'includes/submission-handler.php'; require_once UGC_PLUGIN_PATH . 'includes/moderation-system.php'; require_once UGC_PLUGIN_PATH . 'includes/admin-interface.php'; // 国际化支持 load_plugin_textdomain('ugc-system', false, dirname(plugin_basename(__FILE__)) . '/languages'); } add_action('plugins_loaded', 'ugc_system_init'); 2.2 创建数据库表结构 我们需要扩展WordPress数据库来存储UGC相关数据: // 在activation hook中创建数据库表 function ugc_create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'ugc_submissions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, content_type varchar(50) NOT NULL, title varchar(255), content longtext NOT NULL, status varchar(20) DEFAULT 'pending', moderation_notes text, moderated_by bigint(20), moderated_at datetime, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, metadata text, PRIMARY KEY (id), KEY user_id (user_id), KEY status (status), KEY content_type (content_type) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建审核日志表 $log_table = $wpdb->prefix . 'ugc_moderation_logs'; $sql_log = "CREATE TABLE IF NOT EXISTS $log_table ( id bigint(20) NOT NULL AUTO_INCREMENT, submission_id bigint(20) NOT NULL, moderator_id bigint(20) NOT NULL, action varchar(50) NOT NULL, notes text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY submission_id (submission_id), KEY moderator_id (moderator_id) ) $charset_collate;"; dbDelta($sql_log); } register_activation_hook(__FILE__, 'ugc_create_database_tables'); 第三部分:用户内容提交系统开发 3.1 创建内容提交表单 设计一个用户友好的内容提交界面: // 短代码生成内容提交表单 function ugc_submission_form_shortcode($atts) { // 只有登录用户才能提交内容 if (!is_user_logged_in()) { return '<div class="ugc-login-required">请<a href="' . wp_login_url(get_permalink()) . '">登录</a>后提交内容</div>'; } $atts = shortcode_atts(array( 'type' => 'article', 'category' => '', 'max_length' => 5000 ), $atts); ob_start(); ?> <div class="ugc-submission-form-container"> <form id="ugc-submission-form" class="ugc-form" method="post" enctype="multipart/form-data"> <?php wp_nonce_field('ugc_submission_action', 'ugc_submission_nonce'); ?> <input type="hidden" name="ugc_content_type" value="<?php echo esc_attr($atts['type']); ?>"> <div class="form-group"> <label for="ugc-title">标题 *</label> <input type="text" id="ugc-title" name="ugc_title" required maxlength="200" placeholder="请输入内容标题"> </div> <div class="form-group"> <label for="ugc-content">内容 *</label> <textarea id="ugc-content" name="ugc_content" rows="10" required maxlength="<?php echo esc_attr($atts['max_length']); ?>" placeholder="请输入详细内容..."></textarea> <div class="char-count"> <span id="char-counter">0</span>/<?php echo esc_attr($atts['max_length']); ?> 字符 </div> </div> <div class="form-group"> <label for="ugc-category">分类</label> <select id="ugc-category" name="ugc_category"> <option value="">选择分类</option> <?php $categories = get_categories(array('hide_empty' => false)); foreach ($categories as $category) { echo '<option value="' . esc_attr($category->term_id) . '">' . esc_html($category->name) . '</option>'; } ?> </select> </div> <div class="form-group"> <label for="ugc-tags">标签</label> <input type="text" id="ugc-tags" name="ugc_tags" placeholder="用逗号分隔多个标签"> </div> <div class="form-group"> <label for="ugc-attachments">附件</label> <input type="file" id="ugc-attachments" name="ugc_attachments[]" multiple accept="image/*,.pdf,.doc,.docx"> <p class="help-text">支持图片、PDF、Word文档,单个文件不超过5MB</p> </div> <div class="form-group"> <label> <input type="checkbox" name="ugc_terms" required> 我同意<a href="<?php echo get_permalink(get_page_by_path('terms')); ?>" target="_blank">服务条款</a> </label> </div> <div class="form-submit"> <button type="submit" class="ugc-submit-btn">提交内容</button> <div class="form-feedback" id="form-feedback"></div> </div> </form> </div> <script> jQuery(document).ready(function($) { // 字符计数 $('#ugc-content').on('input', function() { var length = $(this).val().length; $('#char-counter').text(length); }); // AJAX表单提交 $('#ugc-submission-form').on('submit', function(e) { e.preventDefault(); var formData = new FormData(this); var submitBtn = $(this).find('.ugc-submit-btn'); submitBtn.prop('disabled', true).text('提交中...'); $('#form-feedback').removeClass('success error').html(''); $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: formData, processData: false, contentType: false, success: function(response) { if (response.success) { $('#form-feedback').addClass('success').html(response.data.message); $('#ugc-submission-form')[0].reset(); $('#char-counter').text('0'); } else { $('#form-feedback').addClass('error').html(response.data); } submitBtn.prop('disabled', false).text('提交内容'); }, error: function() { $('#form-feedback').addClass('error').html('提交失败,请稍后重试'); submitBtn.prop('disabled', false).text('提交内容'); } }); }); }); </script> <?php // 添加样式 wp_enqueue_style('ugc-frontend-style', UGC_PLUGIN_URL . 'assets/css/frontend.css'); wp_enqueue_script('ugc-frontend-script', UGC_PLUGIN_URL . 'assets/js/frontend.js', array('jquery'), UGC_VERSION, true); return ob_get_clean(); } add_shortcode('ugc_submission_form', 'ugc_submission_form_shortcode'); 3.2 内容提交处理逻辑 // 处理AJAX内容提交 function handle_ugc_submission() { // 验证nonce if (!isset($_POST['ugc_submission_nonce']) || !wp_verify_nonce($_POST['ugc_submission_nonce'], 'ugc_submission_action')) { wp_die('安全验证失败'); } // 验证用户权限 if (!is_user_logged_in()) { wp_die('请先登录'); } $user_id = get_current_user_id(); $content_type = sanitize_text_field($_POST['ugc_content_type']); $title = sanitize_text_field($_POST['ugc_title']); $content = wp_kses_post($_POST['ugc_content']); $category = intval($_POST['ugc_category']); $tags = sanitize_text_field($_POST['ugc_tags']); // 基础验证 if (empty($title) || empty($content)) { wp_send_json_error('标题和内容不能为空'); } // 检查用户提交频率限制 if (!check_submission_rate_limit($user_id)) { wp_send_json_error('提交过于频繁,请稍后再试'); } // 垃圾内容检测 if (detect_spam_content($title . ' ' . $content)) { wp_send_json_error('内容疑似垃圾信息,请修改后重新提交'); } global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; // 准备元数据 $metadata = array( 'category_id' => $category, 'tags' => $tags, 'attachments' => array() ); // 处理文件上传 if (!empty($_FILES['ugc_attachments'])) { $attachments = handle_file_uploads($_FILES['ugc_attachments'], $user_id); if (is_wp_error($attachments)) { wp_send_json_error($attachments->get_error_message()); } $metadata['attachments'] = $attachments; } // 插入提交记录 $result = $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'content_type' => $content_type, 'title' => $title, 'content' => $content, 'status' => 'pending', 'metadata' => serialize($metadata), 'created_at' => current_time('mysql') ), array('%d', '%s', '%s', '%s', '%s', '%s', '%s') ); if ($result) { $submission_id = $wpdb->insert_id; // 发送通知邮件给管理员 send_moderation_notification($submission_id); // 记录用户活动 update_user_submission_stats($user_id); wp_send_json_success(array( 'message' => '内容提交成功,等待审核', 'submission_id' => $submission_id )); } else { wp_send_json_error('提交失败,请稍后重试'); } } add_action('wp_ajax_ugc_submit_content', 'handle_ugc_submission'); add_action('wp_ajax_nopriv_ugc_submit_content', 'handle_ugc_submission'); // 文件上传处理函数 function handle_file_uploads($files, $user_id) { require_once(ABSPATH . 'wp-admin/includes/file.php'); require_once(ABSPATH . 'wp-admin/includes/image.php'); require_once(ABSPATH . 'wp-admin/includes/media.php'); $attachments = array(); $upload_dir = wp_upload_dir(); $ugc_dir = $upload_dir['basedir'] . '/ugc-uploads/' . $user_id . '/' . date('Y/m'); // 创建目录 if (!file_exists($ugc_dir)) { wp_mkdir_p($ugc_dir); } $file_count = count($files['name']); for ($i = 0; $i < $file_count; $i++) { // 检查文件大小(5MB限制) if ($files['size'][$i] > 5 * 1024 * 1024) { continue; // 跳过过大文件 } // 检查文件类型 $file_type = wp_check_filetype($files['name'][$i]); $allowed_types = array('jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'); if (!in_array($file_type['ext'], $allowed_types)) { continue; // 跳过不允许的文件类型 } // 生成唯一文件名 $filename = wp_unique_filename($ugc_dir, $files['name'][$i]); $filepath = $ugc_dir . '/' . $filename; // 移动文件 if (move_uploaded_file($files['tmp_name'][$i], $filepath)) { $attachments[] = array( 'name' => $files['name'][$i], 'path' => str_replace($upload_dir['basedir'], '', $filepath), 'type' => $file_type['type'], 'size' => $files['size'][$i] ); } } return $attachments; } 第四部分:智能审核系统开发 4.1 多级审核工作流设计 // 审核状态管理类 class UGC_Moderation_Workflow { private $statuses = array( 'pending' => array( 'name' => '待审核', 'actions' => array('approve', 'reject', 'request_revision') ), 'under_review' => array( 'name' => '审核中', 'actions' => array('approve', 'reject', 'request_revision') ), 'approved' => array( 'name' => '已通过', 'actions' => array('unpublish', 'delete') ), 'rejected' => array( 'name' => '已拒绝', 'actions' => array('restore', 'delete') ), 'needs_revision' => array( 'name' => '需要修改', 'actions' => array('approve', 'reject') ), 'published' => array( 'name' => '已发布', 'actions' => array('unpublish', 'delete') ) ); // 获取下一个可用状态 public function get_next_status($current_status, $action) { $transitions = array( 'pending' => array( 'approve' => 'approved', 'reject' => 'rejected', 'request_revision' => 'needs_revision' ), 'needs_revision' => array( 'approve' => 'approved', 'reject' => 'rejected' ), 'approved' => array( 'publish' => 'published', 'unpublish' => 'pending', 'delete' => 'deleted' ), 'rejected' => array( 'restore' => 'pending', 'delete' => 'deleted' ), 'published' => array( 'unpublish' => 'approved', 'delete' => 'deleted' ) ); return isset($transitions[$current_status][$action]) ? $transitions[$current_status][$action] : $current_status; } // 执行审核操作 public function moderate_submission($submission_id, $action, $moderator_id, $notes = '') { global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; $log_table = $wpdb->prefix . 'ugc_moderation_logs'; // 获取当前状态 $current_status = $wpdb->get_var($wpdb->prepare( "SELECT status FROM $table_name WHERE id = %d", $submission_id )); if (!$current_status) { return new WP_Error('not_found', '提交内容不存在'); } // 验证操作是否允许 if (!in_array($action, $this->statuses[$current_status]['actions'])) { return new WP_Error('invalid_action', '当前状态下不允许此操作'); } // 获取新状态 $new_status = $this->get_next_status($current_status, $action); // 开始事务 $wpdb->query('START TRANSACTION'); try { // 更新提交状态 $update_result = $wpdb->update( $table_name, array( 'status' => $new_status, 'moderated_by' => $moderator_id, 'moderated_at' => current_time('mysql'), 'moderation_notes' => $notes ), array('id' => $submission_id), array('%s', '%d', '%s', '%s'), array('%d') ); if (!$update_result) { throw new Exception('更新状态失败'); } // 记录审核日志 $log_result = $wpdb->insert( $log_table, array( 'submission_id' => $submission_id, 'moderator_id' => $moderator_id, 'action' => $action, 'notes' => $notes ), array('%d', '%d', '%s', '%s') ); if (!$log_result) { throw new Exception('记录日志失败'); } // 如果状态变为已发布,创建正式内容 if ($new_status === 'published') { $this->publish_submission($submission_id); } // 发送通知给用户 $this->send_status_notification($submission_id, $new_status, $notes); $wpdb->query('COMMIT'); return array( 'success' => true, 'new_status' => $new_status, 'message' => '审核操作成功' ); } catch (Exception $e) { $wpdb->query('ROLLBACK'); return new WP_Error('transaction_failed', $e->getMessage()); } } // 发布提交内容 private function publish_submission($submission_id) { global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; $submission = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $submission_id )); if (!$submission) { return false; } $metadata = unserialize($submission->metadata); // 创建WordPress文章 $post_data = array( 'post_title' => $submission->title, 'post_content' => $submission->content, 'post_status' => 'publish', 'post_author' => $submission->user_id, 'post_type' => 'post' ); if (!empty($metadata['category_id'])) { $post_data['post_category'] = array($metadata['category_id']); } $post_id = wp_insert_post($post_data); if ($post_id && !empty($metadata['tags'])) { wp_set_post_tags($post_id, $metadata['tags']); } // 更新提交记录中的文章ID $wpdb->update( $table_name, array('published_post_id' => $post_id), array('id' => $submission_id), array('%d'), array('%d') ); return $post_id; } // 发送状态通知 private function send_status_notification($submission_id, $status, $notes = '') { global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; $submission = $wpdb->get_row($wpdb->prepare( "SELECT user_id, title FROM $table_name WHERE id = %d", $submission_id )); if (!$submission) { return false; } $user = get_user_by('id', $submission->user_id); if (!$user) { return false; } $status_names = array( 'approved' => '已通过', 'rejected' => '已拒绝', 'published' => '已发布', 'needs_revision' => '需要修改' ); $subject = sprintf('您的投稿"%s"状态已更新', $submission->title); $message = sprintf( "您好 %s,nn您的投稿《%s》状态已更新为:%snn", $user->display_name, $submission->title, $status_names[$status] ?? $status ); if (!empty($notes)) { $message .= "审核备注:n" . $notes . "nn"; } $message .= "您可以登录网站查看详情。nn"; $message .= get_bloginfo('name'); wp_mail($user->user_email, $subject, $message); return true; } } 4.2 智能垃圾内容检测 // 垃圾内容检测系统 class UGC_Spam_Detection { private $spam_keywords = array(); private $spam_patterns = array(); public function __construct() { // 加载垃圾关键词库 $this->load_spam_keywords(); $this->load_spam_patterns(); } // 检测内容是否为垃圾 public function detect($content, $author_id = 0) { $score = 0; $reasons = array(); // 1. 关键词检测 $keyword_score = $this->check_keywords($content); if ($keyword_score > 0) { $score += $keyword_score; $reasons[] = '包含垃圾关键词'; } // 2. 链接检测 $link_score = $this->check_links($content); if ($link_score > 0) { $score += $link_score; $reasons[] = '包含可疑链接'; } // 3. 重复内容检测 $duplicate_score = $this->check_duplicate($content); if ($duplicate_score > 0) { $score += $duplicate_score; $reasons[] = '疑似重复内容'; } // 4. 用户行为分析 if ($author_id > 0) { $user_score = $this->check_user_behavior($author_id); if ($user_score > 0) { $score += $user_score; $reasons[] = '用户行为可疑'; } } // 5. 模式匹配 $pattern_score = $this->check_patterns($content); if ($pattern_score > 0) { $score += $pattern_score; $reasons[] = '匹配垃圾内容模式'; } return array( 'score' => $score, 'is_spam' => $score >= 5, // 阈值设为5分 'reasons' => $reasons, 'details' => array( 'keyword_score' => $keyword_score, 'link_score' => $link_score, 'duplicate_score' => $duplicate_score, 'user_score' => $user_score ?? 0, 'pattern_score' => $pattern_score ) ); } // 关键词检测 private function check_keywords($content) { $score = 0; $content_lower = strtolower($content); foreach ($this->spam_keywords as $keyword => $weight) { if (strpos($content_lower, $keyword) !== false) { $score += $weight; } } return min($score, 3); // 最高3分 } // 链接检测 private function check_links($content) { $score = 0; // 提取所有链接 preg_match_all('/https?://[^s]+/', $content, $matches); $links = $matches[0] ?? array(); if (count($links) > 3) { $score += 2; // 链接过多 } // 检查可疑域名 $suspicious_domains = array('.ru', '.cn', '.xyz', '.top', '.club'); foreach ($links as $link) { foreach ($suspicious_domains as $domain) { if (strpos($link, $domain) !== false) { $score += 1; break; } } } return min($score, 3); } // 重复内容检测 private function check_duplicate($content) { global $wpdb; // 计算内容哈希 $content_hash = md5(trim($content)); // 检查最近24小时内的重复内容 $table_name = $wpdb->prefix . 'ugc_submissions'; $one_day_ago = date('Y-m-d H:i:s', strtotime('-24 hours')); $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE MD5(content) = %s AND created_at > %s", $content_hash, $one_day_ago )); return $count > 0 ? 2 : 0; } // 用户行为分析 private function check_user_behavior($user_id) { global $wpdb; $score = 0; $table_name = $wpdb->prefix . 'ugc_submissions'; // 检查用户提交频率 $one_hour_ago = date('Y-m-d H:i:s', strtotime('-1 hour')); $recent_submissions = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND created_at > %s", $user_id, $one_hour_ago )); if ($recent_submissions > 5) { $score += 2; // 1小时内提交超过5次 } // 检查用户被拒绝率 $total_submissions = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d", $user_id )); if ($total_submissions > 0) { $rejected_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND status = 'rejected'", $user_id )); $rejection_rate = $rejected_count / $total_submissions; if ($rejection_rate > 0.5) { $score += 2; // 拒绝率超过50% } } return min($score, 3); } // 模式匹配检测 private function check_patterns($content) { $score = 0; foreach ($this->spam_patterns as $pattern => $weight) { if (preg_match($pattern, $content)) { $score += $weight; } } return min($score, 2); } // 加载垃圾关键词 private function load_spam_keywords() { // 可以从文件或数据库加载 $this->spam_keywords = array( 'viagra' => 2, 'casino' => 2, 'loan' => 1, 'mortgage' => 1, 'click here' => 1, 'buy now' => 1, 'discount' => 1, 'free' => 1, 'win' => 1, 'prize' => 1, // 添加更多关键词... ); } // 加载垃圾模式 private function load_spam_patterns() { $this->spam_patterns = array( '/[A-Z0-9._%+-]+@[A-Z0-9.-]+.[A-Z]{2,}/i' => 1, // 邮箱地址 '/bd{3}[-.]?d{3}[-.]?d{4}b/' => 1, // 电话号码 '/bd{4}[- ]?d{4}[- ]?d{4}[- ]?d{4}b/' => 2, // 信用卡号 '/http:///' => 1, // HTTP链接(非HTTPS) ); } } 第五部分:管理后台界面开发 5.1 审核管理页面 // 创建审核管理后台页面 class UGC_Admin_Interface { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); add_action('wp_ajax_ugc_bulk_action', array($this, 'handle_bulk_action')); } // 添加管理菜单 public function add_admin_menu() { add_menu_page( 'UGC管理', 'UGC管理', 'manage_options', 'ugc-management', array($this, 'render_main_page'), 'dashicons-admin-comments', 30 ); add_submenu_page( 'ugc-management', '内容审核', '内容审核', 'manage_options', 'ugc-moderation', array($this, 'render_moderation_page') ); add_submenu_page( 'ugc-management', '审核日志', '审核日志', 'manage_options', 'ugc-logs', array($this, 'render_logs_page') ); add_submenu_page( 'ugc-management', '垃圾检测', '垃圾检测', 'manage_options', 'ugc-spam', array($this, 'render_spam_page') ); add_submenu_page( 'ugc-management', '用户信誉', '用户信誉', 'manage_options', 'ugc-reputation', array($this, 'render_reputation_page') ); } // 渲染审核页面 public function render_moderation_page() { global $wpdb; $table_name = $wpdb->prefix . 'ugc_submissions'; // 获取筛选参数 $status = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : 'pending'; $type = isset($_GET['type']) ? sanitize_text_field($_GET['type']) : ''; $page = isset($_GET['paged']) ? intval($_GET['paged']) : 1; $per_page = 20; $offset = ($page - 1) * $per_page; // 构建查询 $where = array('1=1'); $params = array(); if ($status) { $where[] = 'status = %s'; $params[] = $status; } if ($type) { $where[] = 'content_type = %s'; $params[] = $type; } $where_clause = implode(' AND ', $where); // 获取总数 $count_query = "SELECT COUNT(*) FROM $table_name WHERE $where_clause"; if ($params) { $count_query = $wpdb->prepare($count_query, $params); } $total_items = $wpdb->get_var($count_query); // 获取数据 $data_query = "SELECT * FROM $table_name WHERE $where_clause ORDER BY created_at DESC LIMIT %d OFFSET %d"; $params[] = $per_page; $params[] = $offset; $submissions = $wpdb->get_results($wpdb->prepare($data_query, $params)); ?> <div class="wrap"> <h1 class="wp-heading-inline">内容审核</h1> <!-- 筛选器 --> <div class="ugc-filters"> <form method="get" action=""> <input type="hidden" name="page" value="ugc-moderation"> <select name="status" onchange="this.form.submit()"> <option value="">所有状态</option> <?php $statuses = array( 'pending' => '待审核', 'under_review' => '审核中',

发表评论

手把手教学,为WordPress集成多因素身份验证与登录保护

手把手教学:为WordPress集成多因素身份验证与登录保护 引言:为什么WordPress需要多因素身份验证? 在当今数字化时代,网络安全已成为网站运营者不可忽视的重要议题。WordPress作为全球最受欢迎的内容管理系统,占据了互联网近43%的网站份额,也因此成为黑客攻击的主要目标。根据Wordfence安全报告,仅2023年就有超过13亿次针对WordPress网站的攻击尝试。 传统的用户名和密码认证方式已不足以应对日益复杂的网络威胁。密码泄露、暴力破解和社会工程学攻击使得单因素认证变得脆弱。多因素身份验证(MFA)通过要求用户提供两种或更多验证因素,极大地增强了账户安全性。本文将手把手教您如何通过WordPress代码二次开发,集成多因素身份验证与登录保护功能,同时实现一些常用互联网小工具功能。 第一部分:多因素身份验证基础与原理 1.1 多因素身份验证的类型 多因素身份验证通常基于以下三种类型的因素组合: 知识因素:用户知道的信息,如密码、PIN码或安全问题答案 持有因素:用户拥有的物理设备,如手机、安全密钥或智能卡 固有因素:用户本身的生物特征,如指纹、面部识别或虹膜扫描 1.2 常见的MFA实现方式 基于时间的一次性密码(TOTP):使用手机应用(如Google Authenticator)生成随时间变化的6位数字代码 短信/邮件验证码:通过短信或电子邮件发送一次性验证码 推送通知:向已认证设备发送登录请求确认 生物识别:使用指纹、面部识别等生物特征 硬件安全密钥:如YubiKey等物理设备 1.3 WordPress安全现状分析 默认情况下,WordPress仅提供基本的用户名和密码认证。虽然有许多插件可以提供MFA功能,但通过代码二次开发实现有以下优势: 减少插件依赖:降低插件冲突风险 性能优化:定制化代码通常比通用插件更高效 完全控制:可以根据具体需求定制功能 学习价值:深入理解WordPress认证机制 第二部分:开发环境准备与基础设置 2.1 开发环境配置 在开始编码之前,请确保您已准备好以下环境: 本地开发环境:推荐使用Local by Flywheel、XAMPP或MAMP 代码编辑器:VS Code、PHPStorm或Sublime Text 测试WordPress安装:建议使用最新版本的WordPress 备份机制:确保可以随时恢复原始状态 2.2 创建自定义插件框架 首先,我们创建一个专门用于MFA功能的插件: <?php /** * Plugin Name: 高级登录保护与MFA * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress添加多因素身份验证和增强登录保护功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('MFA_PLUGIN_VERSION', '1.0.0'); define('MFA_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('MFA_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 add_action('plugins_loaded', 'mfa_plugin_init'); function mfa_plugin_init() { // 检查必要扩展 if (!extension_loaded('gd')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>高级登录保护插件需要GD扩展支持,请启用PHP的GD扩展。</p></div>'; }); return; } // 加载核心类 require_once MFA_PLUGIN_PATH . 'includes/class-mfa-core.php'; require_once MFA_PLUGIN_PATH . 'includes/class-totp-authenticator.php'; require_once MFA_PLUGIN_PATH . 'includes/class-login-protection.php'; // 初始化核心功能 $mfa_core = MFA_Core::get_instance(); } 第三部分:实现TOTP多因素身份验证 3.1 TOTP原理与实现 基于时间的一次性密码算法是当前最流行的MFA实现方式之一。以下是实现TOTP的核心类: <?php // includes/class-totp-authenticator.php class TOTP_Authenticator { private $secret_length = 16; private $time_step = 30; // 时间步长(秒) private $code_length = 6; // 验证码长度 /** * 生成随机密钥 */ public function generate_secret() { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; // Base32字符集 $secret = ''; for ($i = 0; $i < $this->secret_length; $i++) { $secret .= $chars[random_int(0, 31)]; } return $secret; } /** * 生成TOTP验证码 */ public function generate_code($secret) { $time = floor(time() / $this->time_step); $time = pack('J', $time); // 64位大端字节序 $secret = $this->base32_decode($secret); $hash = hash_hmac('sha1', $time, $secret, true); // 动态截取 $offset = ord($hash[19]) & 0xF; $code = ( ((ord($hash[$offset]) & 0x7F) << 24) | ((ord($hash[$offset + 1]) & 0xFF) << 16) | ((ord($hash[$offset + 2]) & 0xFF) << 8) | (ord($hash[$offset + 3]) & 0xFF) ) % pow(10, $this->code_length); return str_pad($code, $this->code_length, '0', STR_PAD_LEFT); } /** * 验证TOTP代码 */ public function verify_code($secret, $code, $discrepancy = 1) { $current_time = floor(time() / $this->time_step); // 允许时间偏差 for ($i = -$discrepancy; $i <= $discrepancy; $i++) { $time = pack('J', $current_time + $i); $secret_decoded = $this->base32_decode($secret); $hash = hash_hmac('sha1', $time, $secret_decoded, true); $offset = ord($hash[19]) & 0xF; $calculated_code = ( ((ord($hash[$offset]) & 0x7F) << 24) | ((ord($hash[$offset + 1]) & 0xFF) << 16) | ((ord($hash[$offset + 2]) & 0xFF) << 8) | (ord($hash[$offset + 3]) & 0xFF) ) % pow(10, $this->code_length); $calculated_code = str_pad($calculated_code, $this->code_length, '0', STR_PAD_LEFT); if (hash_equals($calculated_code, $code)) { return true; } } return false; } /** * Base32解码 */ private function base32_decode($secret) { $chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567'; $buffer = 0; $bitsLeft = 0; $output = ''; for ($i = 0; $i < strlen($secret); $i++) { $char = $secret[$i]; $value = strpos($chars, $char); if ($value === false) { continue; } $buffer <<= 5; $buffer |= $value; $bitsLeft += 5; if ($bitsLeft >= 8) { $bitsLeft -= 8; $output .= chr(($buffer >> $bitsLeft) & 0xFF); } } return $output; } /** * 生成QR码URL(用于Google Authenticator等应用) */ public function generate_qr_url($secret, $username, $issuer = 'WordPress') { $label = rawurlencode($issuer . ':' . $username); $parameters = array( 'secret' => $secret, 'issuer' => $issuer, 'digits' => $this->code_length, 'period' => $this->time_step, ); $url = 'otpauth://totp/' . $label . '?' . http_build_query($parameters); return $url; } } 3.2 用户MFA设置界面 接下来,我们需要为用户创建MFA设置界面: <?php // includes/class-mfa-user-settings.php class MFA_User_Settings { public function __construct() { // 在用户个人资料页面添加MFA设置 add_action('show_user_profile', array($this, 'add_mfa_settings_fields')); add_action('edit_user_profile', array($this, 'add_mfa_settings_fields')); // 保存MFA设置 add_action('personal_options_update', array($this, 'save_mfa_settings')); add_action('edit_user_profile_update', array($this, 'save_mfa_settings')); // 添加AJAX处理 add_action('wp_ajax_generate_mfa_secret', array($this, 'ajax_generate_secret')); add_action('wp_ajax_verify_mfa_code', array($this, 'ajax_verify_code')); } /** * 在用户资料页面添加MFA设置字段 */ public function add_mfa_settings_fields($user) { // 只有管理员或用户自己可以查看 if (!current_user_can('edit_user', $user->ID)) { return; } $mfa_enabled = get_user_meta($user->ID, 'mfa_enabled', true); $mfa_secret = get_user_meta($user->ID, 'mfa_secret', true); $backup_codes = get_user_meta($user->ID, 'mfa_backup_codes', true); // 如果还没有密钥,生成一个 if (empty($mfa_secret)) { $totp = new TOTP_Authenticator(); $mfa_secret = $totp->generate_secret(); update_user_meta($user->ID, 'mfa_secret', $mfa_secret); } ?> <h3>多因素身份验证设置</h3> <table class="form-table"> <tr> <th><label for="mfa_enabled">启用MFA</label></th> <td> <input type="checkbox" name="mfa_enabled" id="mfa_enabled" value="1" <?php checked($mfa_enabled, '1'); ?> /> <span class="description">启用多因素身份验证以增强账户安全</span> </td> </tr> <?php if ($mfa_enabled): ?> <tr> <th>MFA状态</th> <td> <span style="color: green; font-weight: bold;">✓ 已启用</span> <p class="description">您的账户已受到多因素身份验证保护</p> </td> </tr> <?php endif; ?> <tr> <th>设置MFA</th> <td> <div id="mfa-setup-container"> <ol> <li>在手机上安装Google Authenticator或类似应用</li> <li>点击下方按钮生成二维码</li> <li>使用应用扫描二维码</li> <li>输入应用生成的6位验证码进行验证</li> </ol> <div id="mfa-qr-container" style="display: none;"> <div id="mfa-qr-code"></div> <p><strong>密钥:</strong> <code id="mfa-secret-text"><?php echo esc_html($mfa_secret); ?></code></p> <p class="description">如果无法扫描二维码,可以手动输入上方密钥</p> </div> <button type="button" id="show-mfa-qr" class="button">显示二维码</button> <div style="margin-top: 15px;"> <label for="mfa-verify-code">验证码:</label> <input type="text" id="mfa-verify-code" maxlength="6" style="width: 100px;" /> <button type="button" id="verify-mfa-code" class="button">验证并启用MFA</button> <span id="mfa-verify-result" style="margin-left: 10px;"></span> </div> </div> </td> </tr> <?php if (!empty($backup_codes)): ?> <tr> <th>备用验证码</th> <td> <p>以下是一次性备用验证码(每个仅能使用一次):</p> <div style="background: #f5f5f5; padding: 10px; font-family: monospace;"> <?php foreach ($backup_codes as $code) { if ($code['used'] == 0) { echo '<div>' . esc_html($code['code']) . '</div>'; } } ?> </div> <button type="button" id="generate-new-backup-codes" class="button">生成新的备用验证码</button> </td> </tr> <?php endif; ?> </table> <script type="text/javascript"> jQuery(document).ready(function($) { // 显示二维码 $('#show-mfa-qr').on('click', function() { $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'generate_mfa_secret', user_id: <?php echo $user->ID; ?>, nonce: '<?php echo wp_create_nonce('mfa_nonce'); ?>' }, success: function(response) { if (response.success) { $('#mfa-qr-container').show(); $('#mfa-qr-code').html('<img src="' + response.data.qr_url + '" />'); $('#mfa-secret-text').text(response.data.secret); } } }); }); // 验证MFA代码 $('#verify-mfa-code').on('click', function() { var code = $('#mfa-verify-code').val(); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'verify_mfa_code', user_id: <?php echo $user->ID; ?>, code: code, nonce: '<?php echo wp_create_nonce('mfa_nonce'); ?>' }, success: function(response) { if (response.success) { $('#mfa-verify-result').html('<span style="color: green;">✓ 验证成功!MFA已启用。</span>'); $('#mfa_enabled').prop('checked', true); } else { $('#mfa-verify-result').html('<span style="color: red;">✗ 验证失败,请重试。</span>'); } } }); }); }); </script> <?php } /** * 保存MFA设置 */ public function save_mfa_settings($user_id) { if (!current_user_can('edit_user', $user_id)) { return false; } // 验证nonce if (!isset($_POST['_wpnonce']) || !wp_verify_nonce($_POST['_wpnonce'], 'update-user_' . $user_id)) { return false; } // 保存MFA启用状态 if (isset($_POST['mfa_enabled'])) { update_user_meta($user_id, 'mfa_enabled', '1'); } else { delete_user_meta($user_id, 'mfa_enabled'); } return true; } /** * AJAX生成MFA密钥和二维码 */ public function ajax_generate_secret() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'mfa_nonce')) { wp_die('安全验证失败'); } $user_id = intval($_POST['user_id']); $current_user_id = get_current_user_id(); // 验证权限 if ($user_id != $current_user_id && !current_user_can('edit_user', $user_id)) { wp_die('权限不足'); } $totp = new TOTP_Authenticator(); $secret = get_user_meta($user_id, 'mfa_secret', true); if (empty($secret)) { $secret = $totp->generate_secret(); update_user_meta($user_id, 'mfa_secret', $secret); } $user_info = get_userdata($user_id); $qr_url = $totp->generate_qr_url($secret, $user_info->user_login, get_bloginfo('name')); // 生成QR码图片 require_once MFA_PLUGIN_PATH . 'includes/class-qr-generator.php'; $qr_generator = new QR_Generator(); $qr_image_url = $qr_generator->generate($qr_url); wp_send_json_success(array( 'secret' => $secret, 'qr_url' => $qr_image_url )); } /** * AJAX验证MFA代码 */ public function ajax_verify_code() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'mfa_nonce')) { wp_die('安全验证失败'); } $user_id = intval($_POST['user_id']); $code = sanitize_text_field($_POST['code']); $current_user_id = get_current_user_id(); // 验证权限 if ($user_id != $current_user_id && !current_user_can('edit_user', $user_id)) { wp_die('权限不足'); } $secret = get_user_meta($user_id, 'mfa_secret', true); if (empty($secret)) { wp_send_json_error('未找到MFA密钥'); } $totp = new TOTP_Authenticator(); $is_valid = $totp->verify_code($secret, $code); if ($is_valid) { // 启用MFA update_user_meta($user_id, 'mfa_enabled', '1'); // 生成备用验证码 $this->generate_backup_codes($user_id); wp_send_json_success('验证成功'); } else { wp_send_json_error('验证码无效'); } } /** * 生成备用验证码 */ private function generate_backup_codes($user_id) { $backup_codes = array(); for ($i = 0; $i < 10; $i++) { $code = ''; for ($j = 0; $j < 8; $j++) { $code .= random_int(0, 9); } $backup_codes[] = array( 'code' => $code, 'used' => 0 ); } update_user_meta($user_id, 'mfa_backup_codes', $backup_codes); return $backup_codes; } } ### 3.3 集成QR码生成功能 <?php// includes/class-qr-generator.php class QR_Generator { public function generate($data, $size = 200) { // 使用PHP GD库生成QR码 if (!function_exists('imagecreate')) { return $this->generate_via_api($data, $size); } // 简单QR码实现(实际项目中建议使用专业库如phpqrcode) $qr_size = $size; $margin = 10; $module_size = ($qr_size - 2 * $margin) / 21; // 简单QR码版本 $image = imagecreate($qr_size, $qr_size); // 颜色 $white = imagecolorallocate($image, 255, 255, 255); $black = imagecolorallocate($image, 0, 0, 0); // 填充白色背景 imagefilledrectangle($image, 0, 0, $qr_size, $qr_size, $white); // 生成简单QR码图案(实际应使用专业算法) $this->draw_simple_qr($image, $data, $margin, $module_size, $black); // 保存临时文件 $upload_dir = wp_upload_dir(); $temp_filename = 'qr_' . md5($data . time()) . '.png'; $temp_path = $upload_dir['path'] . '/' . $temp_filename; $temp_url = $upload_dir['url'] . '/' . $temp_filename; imagepng($image, $temp_path); imagedestroy($image); // 清理旧文件 $this->cleanup_old_qr_files(); return $temp_url; } private function draw_simple_qr($image, $data, $margin, $module_size, $color) { // 简化的QR码绘制逻辑 // 实际项目中应使用专业QR码生成库 $hash = md5($data); $hash_binary = ''; // 将哈希转换为二进制字符串 for ($i = 0; $i < strlen($hash); $i++) { $char = $hash[$i]; $binary = str_pad(base_convert($char, 16, 2), 4, '0', STR_PAD_LEFT); $hash_binary .= $binary; } // 绘制简单的QR码图案 $bits = str_split($hash_binary); $bit_index = 0; for ($y = 0; $y < 21; $y++) { for ($x = 0; $x < 21; $x++) { if ($bit_index >= count($bits)) { $bit_index = 0; } $bit = $bits[$bit_index]; $bit_index++; if ($bit == '1') { $x_pos = $margin + $x * $module_size; $y_pos = $margin + $y * $module_size; imagefilledrectangle( $image, $x_pos, $y_pos, $x_pos + $module_size, $y_pos + $module_size, $color ); } } } // 添加定位标记(简化版) $this->draw_finder_pattern($image, $margin, $margin, $module_size, $color); $this->draw_finder_pattern($image, $margin + 14 * $module_size, $margin, $module_size, $color); $this->draw_finder_pattern($image, $margin, $margin + 14 * $module_size, $module_size, $color); } private function draw_finder_pattern($image, $x, $y, $module_size, $color) { // 绘制7x7的定位标记 for ($i = 0; $i < 7; $i++) { for ($j = 0; $j < 7; $j++) { if ($i == 0 || $i == 6 || $j == 0 || $j == 6 || ($i >= 2 && $i <= 4 && $j >= 2 && $j <= 4)) { $x_pos = $x + $i * $module_size; $y_pos = $y + $j * $module_size; imagefilledrectangle( $image, $x_pos, $y_pos, $x_pos + $module_size, $y_pos + $module_size, $color ); } } } } private function generate_via_api($data, $size) { // 使用外部API生成QR码(备用方案) $encoded_data = urlencode($data); $api_url = "https://api.qrserver.com/v1/create-qr-code/?size={$size}x{$size}&data={$encoded_data}"; return $api_url; } private function cleanup_old_qr_files() { $upload_dir = wp_upload_dir(); $files = glob($upload_dir['path'] . '/qr_*.png'); // 删除超过1小时的旧文件 $now = time(); foreach ($files as $file) { if ($now - filemtime($file) > 3600) { @unlink($file); } } } } ## 第四部分:增强登录保护机制 ### 4.1 登录尝试限制与IP封锁 <?php// includes/class-login-protection.php class Login_Protection { private $max_attempts = 5; private $lockout_time = 900; // 15分钟 private $max_lockouts = 3; private $extended_lockout_time = 86400; // 24小时 public function __construct() { // 登录验证钩子 add_filter('authenticate', array($this, 'check_login_attempts'), 30, 3); // 登录成功/失败钩子 add_action('wp_login_failed', array($this, 'record_failed_attempt')); add_action('wp_login', array($this, 'clear_login_attempts')); // 添加登录页面保护 add_action('login_form', array($this, 'add_login_protection')); // 添加AJAX处理 add_action('wp_ajax_nopriv_check_login_lock', array($this, 'ajax_check_login_lock')); } /** * 检查登录尝试次数 */ public function check_login_attempts($user, $username, $password) { // 如果已经通过其他方式验证,直接返回 if (is_wp_error($user) && $user->get_error_code() !== 'empty_username' && $user->get_error_code() !== 'empty_password') { return $user; } $ip = $this->get_client_ip(); $attempts = $this->get_login_attempts($ip); // 检查是否被封锁 if ($this->is_ip_locked($ip)) { $lockout_time = $this->get_lockout_time($ip); $remaining = $lockout_time - time(); return new WP_Error( 'ip_locked', sprintf( '登录尝试次数过多。请等待 %d 分 %d 秒后再试。', floor($remaining / 60), $remaining % 60 ) ); } // 检查尝试次数 if ($attempts >= $this->max_attempts) { $this->lock_ip($ip); return new WP_Error( 'too_many_attempts', '登录尝试次数过多。您的IP已被暂时封锁。' ); } return $user; } /** * 记录失败尝试 */ public function record_failed_attempt($username) { $ip = $this->get_client_ip(); $attempts = $this->get_login_attempts($ip); $attempts++; $this->set_login_attempts($ip, $attempts); // 记录详细信息 $this->log_failed_attempt($ip, $username); } /** * 清除登录尝试记录 */ public function clear_login_attempts($username) { $ip = $this->get_client_ip(); $this->set_login_attempts($ip, 0); $this->clear_ip_lock($ip); } /** * 获取客户端IP */ private function get_client_ip() { $ip_keys = array( 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR' ); foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); // 验证IP地址 if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return $_SERVER['REMOTE_ADDR']; } /** * 获取登录尝试次数 */ private function get_login_attempts($ip) { $transient_key = 'login_attempts_' . md5($ip); $attempts = get_transient($transient_key); return $attempts ? intval($attempts) : 0; } /** * 设置登录尝试次数 */ private function set_login_attempts($ip, $attempts) { $transient_key = 'login_attempts_' . md5($ip); $expiration = 3600; // 1小时 set_transient($transient_key, $attempts, $expiration); } /** * 封锁IP */ private function lock_ip($ip) { $lockout_key = 'ip_lockout_' . md5($ip); $lockout_count_key = 'ip_lockout_count_' . md5($ip); // 获取当前封锁次数 $lockout_count = get_transient($lockout_count_key); $lockout_count = $lockout_count ? intval($lockout_count) + 1 : 1; // 设置封锁时间 $lockout_time = $this->lockout_time; if ($lockout_count >= $this->max_lockouts) { $lockout_time = $this->extended_lockout_time; } set_transient($lockout_key, time() + $lockout_time, $lockout_time); set_transient($lockout_count_key, $lockout_count, 86400); // 24小时 // 记录封锁日志 $this->log_ip_lock($ip, $lockout_time, $lockout_count); } /** * 检查IP是否被封锁 */ private function is_ip_locked($ip) { $lockout_key = 'ip_lockout_' . md5($ip); $lockout_time = get_transient($lockout_key); if ($lockout_time && $lockout_time > time()) { return true; } return false; } /** * 获取封锁剩余时间 */ private function get_lockout_time($ip) { $lockout_key = 'ip_lockout_' . md5($ip); return get_transient($lockout_key); } /** * 清除IP封锁 */ private function clear_ip_lock($ip) { $lockout_key = 'ip_lockout_' . md5($ip); $lockout_count_key = 'ip_lockout_count_' . md5($ip); delete_transient($lockout_key); delete_transient($lockout_count_key); } /** * 记录失败尝试日志 */ private function log_failed_attempt($ip, $username) { $log_entry = array( 'time' => current_time('mysql'), 'ip' => $ip, 'username' => $username, 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? '', 'referer' => $_SERVER['HTTP_REFERER'] ?? '' ); $log = get_option('mfa_failed_login_log', array()); $log[] = $log_entry; // 只保留最近100条记录 if (count($log) > 100) { $log = array_slice($log, -100); } update_option('mfa_failed_login_log', $log); } /** * 记录IP封锁日志 */ private function log_ip_lock($ip, $duration, $count) { $log_entry = array( 'time' => current_time('mysql'), 'ip' => $ip, 'duration' => $duration, 'count' => $count, 'reason' => 'too_many_failed_attempts' ); $log = get_option('mfa_ip_lock_log', array()); $log[] = $log_entry; // 只保留最近50条记录 if (count($log) > 50) { $log = array_slice($log, -50); } update_option('mfa_ip_lock_log', $log); } /** * 添加登录页面保护 */ public function add_login_protection() { ?> <script type="text/javascript"> jQuery(document).ready(function($) { // 检查登录锁定状态 function checkLoginLock() { $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'check_login_lock' }, success: function(response) { if (response.locked) { // 显示锁定消息 $('#login').prepend( '<div class="message error" style="border-left-color: #dc3232;">' + '<p>登录尝试次数过多。请等待 ' + Math.floor(response.remaining / 60) + ' 分 ' + (response.remaining % 60) + ' 秒后再试。</p>' + '</div>' ); // 禁用登录表单 $('#loginform input').prop('disabled', true); $('#wp-submit').prop('disabled', true); // 更新倒计时 setTimeout(updateCountdown, 1000); } } }); } // 更新倒计时显示 function updateCountdown() { var countdownEl = $('.countdown-timer'); if (countdownEl.length) { var remaining = parseInt(countdownEl.data('remaining')) - 1; if (remaining <= 0) { location.reload(); } else { countdownEl.data('remaining', remaining); countdownEl.text( Math.floor(remaining / 60) + ' 分 ' + (remaining % 60) + ' 秒' ); setTimeout(updateCountdown, 1000); } } } // 页面加载时检查 checkLoginLock(); }); </script> <?php } /** * AJAX检查登录锁定状态 */ public function ajax_check_login_lock() { $ip = $this->get_client_ip(); if ($this->is_ip_locked($ip)) { $lockout_time = $this->get_lockout_time($ip); $remaining = $lockout_time - time(); wp_send_json_success(array( 'locked' => true, 'remaining' => $remaining )); } else {

发表评论

详细指南,打造支持多用户的在线文件协作与审阅功能

详细指南:打造支持多用户的在线文件协作与审阅功能 引言:WordPress作为协作平台的潜力 在当今数字化工作环境中,在线文件协作与审阅已成为团队生产力的核心要素。虽然市面上已有许多成熟的协作工具,但将这一功能集成到现有WordPress网站中,能够为用户提供无缝的工作体验,同时保持品牌一致性。WordPress作为全球最受欢迎的内容管理系统,其强大的扩展性和开源特性使其成为实现定制化协作功能的理想平台。 本指南将详细介绍如何通过WordPress代码二次开发,构建一个支持多用户的在线文件协作与审阅系统。我们将从基础架构设计开始,逐步深入到具体实现,最终打造一个功能完善、用户体验优秀的协作工具。 第一部分:系统架构设计与技术选型 1.1 需求分析与功能规划 在开始开发之前,我们需要明确系统的基本需求: 多用户支持:不同角色(管理员、编辑者、审阅者、查看者)的权限管理 文件上传与管理:支持常见文档格式(DOC、DOCX、PDF、PPT等) 实时协作:多人同时编辑文档的能力 审阅与批注:添加评论、建议和修改跟踪 版本控制:文档修改历史记录与恢复 通知系统:活动提醒和更新通知 安全与隐私:数据加密和访问控制 1.2 技术栈选择 基于WordPress的生态系统,我们选择以下技术方案: 核心框架:WordPress 5.0+,使用REST API进行前后端通信 前端技术:React.js或Vue.js用于构建交互式界面(可选) 实时协作:WebSocket(通过Socket.io或Pusher服务) 文档处理:后端转换服务或前端库(如Mammoth.js、PDF.js) 数据库:MySQL(WordPress默认),考虑自定义表结构 文件存储:WordPress媒体库扩展或云存储集成(AWS S3、Google Cloud) 1.3 数据库设计 我们需要扩展WordPress的默认数据库结构,添加以下自定义表: -- 协作项目表 CREATE TABLE wp_collab_projects ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, owner_id BIGINT(20) UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (owner_id) REFERENCES wp_users(ID) ); -- 项目成员表 CREATE TABLE wp_collab_members ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, user_id BIGINT(20) UNSIGNED NOT NULL, role ENUM('owner', 'editor', 'reviewer', 'viewer') DEFAULT 'viewer', joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (project_id) REFERENCES wp_collab_projects(id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ); -- 文档表 CREATE TABLE wp_collab_documents ( id INT AUTO_INCREMENT PRIMARY KEY, project_id INT NOT NULL, title VARCHAR(255) NOT NULL, file_path VARCHAR(500), file_type VARCHAR(50), file_size INT, created_by BIGINT(20) UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, version INT DEFAULT 1, FOREIGN KEY (project_id) REFERENCES wp_collab_projects(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES wp_users(ID) ); -- 文档版本表 CREATE TABLE wp_collab_document_versions ( id INT AUTO_INCREMENT PRIMARY KEY, document_id INT NOT NULL, version_number INT NOT NULL, file_path VARCHAR(500), changes_summary TEXT, created_by BIGINT(20) UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (document_id) REFERENCES wp_collab_documents(id) ON DELETE CASCADE, FOREIGN KEY (created_by) REFERENCES wp_users(ID) ); -- 批注与评论表 CREATE TABLE wp_collab_comments ( id INT AUTO_INCREMENT PRIMARY KEY, document_id INT NOT NULL, version_id INT, user_id BIGINT(20) UNSIGNED NOT NULL, content TEXT NOT NULL, position_data JSON, -- 存储批注在文档中的位置信息 resolved BOOLEAN DEFAULT FALSE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (document_id) REFERENCES wp_collab_documents(id) ON DELETE CASCADE, FOREIGN KEY (version_id) REFERENCES wp_collab_document_versions(id), FOREIGN KEY (user_id) REFERENCES wp_users(ID) ); 第二部分:核心功能实现 2.1 用户权限与角色管理 WordPress本身具有强大的用户角色系统,但我们需要扩展它以支持协作场景: <?php // 在插件激活时添加自定义角色 function collab_add_custom_roles() { add_role('collab_editor', '协作文档编辑', array( 'read' => true, 'edit_posts' => true, 'upload_files' => true, 'collab_edit_document' => true, 'collab_comment' => true, )); add_role('collab_reviewer', '协作文档审阅', array( 'read' => true, 'collab_comment' => true, 'collab_review' => true, )); add_role('collab_viewer', '协作文档查看', array( 'read' => true, 'collab_view' => true, )); } register_activation_hook(__FILE__, 'collab_add_custom_roles'); // 添加自定义权限到现有角色 function collab_add_capabilities() { $roles = array('administrator', 'editor'); foreach ($roles as $role_name) { $role = get_role($role_name); if ($role) { $role->add_cap('collab_manage_projects'); $role->add_cap('collab_edit_document'); $role->add_cap('collab_comment'); $role->add_cap('collab_review'); $role->add_cap('collab_view'); } } } add_action('admin_init', 'collab_add_capabilities'); // 项目级别的权限检查 function collab_check_project_permission($project_id, $user_id, $capability) { global $wpdb; // 检查用户是否为项目成员 $member = $wpdb->get_row($wpdb->prepare( "SELECT role FROM {$wpdb->prefix}collab_members WHERE project_id = %d AND user_id = %d", $project_id, $user_id )); if (!$member) { return false; } // 根据角色检查权限 $role_permissions = array( 'owner' => array('collab_manage_projects', 'collab_edit_document', 'collab_comment', 'collab_review', 'collab_view'), 'editor' => array('collab_edit_document', 'collab_comment', 'collab_review', 'collab_view'), 'reviewer' => array('collab_comment', 'collab_review', 'collab_view'), 'viewer' => array('collab_view') ); return in_array($capability, $role_permissions[$member->role]); } ?> 2.2 文件上传与管理系统 扩展WordPress媒体处理功能以支持协作文档: <?php // 扩展允许上传的文件类型 function collab_allowed_mime_types($mimes) { $mimes['doc'] = 'application/msword'; $mimes['docx'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; $mimes['ppt'] = 'application/vnd.ms-powerpoint'; $mimes['pptx'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; $mimes['xls'] = 'application/vnd.ms-excel'; $mimes['xlsx'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; $mimes['pdf'] = 'application/pdf'; return $mimes; } add_filter('upload_mimes', 'collab_allowed_mime_types'); // 处理文档上传 function collab_handle_document_upload($project_id) { if (!isset($_FILES['collab_document']) || $_FILES['collab_document']['error'] !== UPLOAD_ERR_OK) { return new WP_Error('upload_failed', '文件上传失败'); } // 检查用户权限 if (!collab_check_project_permission($project_id, get_current_user_id(), 'collab_edit_document')) { return new WP_Error('permission_denied', '您没有权限上传文档'); } // 使用WordPress媒体处理功能 require_once(ABSPATH . 'wp-admin/includes/file.php'); require_once(ABSPATH . 'wp-admin/includes/media.php'); require_once(ABSPATH . 'wp-admin/includes/image.php'); $file = $_FILES['collab_document']; // 验证文件类型和大小 $max_size = 50 * 1024 * 1024; // 50MB if ($file['size'] > $max_size) { return new WP_Error('file_too_large', '文件大小不能超过50MB'); } // 上传文件 $upload_overrides = array('test_form' => false); $movefile = wp_handle_upload($file, $upload_overrides); if ($movefile && !isset($movefile['error'])) { // 创建文档记录 global $wpdb; $document_data = array( 'project_id' => $project_id, 'title' => sanitize_text_field($_POST['document_title']), 'file_path' => $movefile['file'], 'file_url' => $movefile['url'], 'file_type' => $file['type'], 'file_size' => $file['size'], 'created_by' => get_current_user_id(), 'version' => 1 ); $wpdb->insert("{$wpdb->prefix}collab_documents", $document_data); $document_id = $wpdb->insert_id; // 创建初始版本记录 $version_data = array( 'document_id' => $document_id, 'version_number' => 1, 'file_path' => $movefile['file'], 'created_by' => get_current_user_id() ); $wpdb->insert("{$wpdb->prefix}collab_document_versions", $version_data); // 记录活动日志 collab_log_activity($project_id, 'document_uploaded', array( 'document_id' => $document_id, 'document_title' => $document_data['title'], 'user_id' => get_current_user_id() )); return $document_id; } else { return new WP_Error('upload_failed', $movefile['error']); } } // 文档列表获取 function collab_get_project_documents($project_id, $user_id) { global $wpdb; // 检查查看权限 if (!collab_check_project_permission($project_id, $user_id, 'collab_view')) { return new WP_Error('permission_denied', '您没有权限查看文档'); } $documents = $wpdb->get_results($wpdb->prepare( "SELECT d.*, u.display_name as creator_name, (SELECT COUNT(*) FROM {$wpdb->prefix}collab_comments c WHERE c.document_id = d.id AND c.resolved = 0) as unresolved_comments FROM {$wpdb->prefix}collab_documents d LEFT JOIN {$wpdb->users} u ON d.created_by = u.ID WHERE d.project_id = %d ORDER BY d.updated_at DESC", $project_id )); return $documents; } ?> 2.3 实时协作功能实现 使用WebSocket实现实时协作功能: <?php // WebSocket服务器集成(使用PHP-WebSocket或外部服务) // 这里我们以Pusher服务为例,但也可以使用Socket.io class CollabRealtime { private $pusher; public function __construct() { // 初始化Pusher $this->pusher = new PusherPusher( get_option('collab_pusher_app_key'), get_option('collab_pusher_app_secret'), get_option('collab_pusher_app_id'), array('cluster' => get_option('collab_pusher_cluster')) ); } // 用户加入文档协作 public function user_joined_document($document_id, $user_id) { $user = get_userdata($user_id); $this->pusher->trigger("document-{$document_id}", 'user-joined', array( 'user_id' => $user_id, 'user_name' => $user->display_name, 'timestamp' => current_time('timestamp') )); // 记录用户活动 $this->update_document_presence($document_id, $user_id, 'join'); } // 用户离开文档协作 public function user_left_document($document_id, $user_id) { $this->update_document_presence($document_id, $user_id, 'leave'); $this->pusher->trigger("document-{$document_id}", 'user-left', array( 'user_id' => $user_id, 'timestamp' => current_time('timestamp') )); } // 文档内容更新 public function document_updated($document_id, $changes, $user_id) { $this->pusher->trigger("document-{$document_id}", 'content-updated', array( 'changes' => $changes, 'user_id' => $user_id, 'timestamp' => current_time('timestamp') )); } // 批注添加 public function comment_added($document_id, $comment_id, $user_id) { $this->pusher->trigger("document-{$document_id}", 'comment-added', array( 'comment_id' => $comment_id, 'user_id' => $user_id, 'timestamp' => current_time('timestamp') )); } // 更新在线用户状态 private function update_document_presence($document_id, $user_id, $action) { global $wpdb; $table_name = "{$wpdb->prefix}collab_document_presence"; if ($action === 'join') { $wpdb->replace($table_name, array( 'document_id' => $document_id, 'user_id' => $user_id, 'last_seen' => current_time('mysql') )); } else { $wpdb->delete($table_name, array( 'document_id' => $document_id, 'user_id' => $user_id )); } // 获取当前在线用户 $online_users = $wpdb->get_results($wpdb->prepare( "SELECT p.user_id, u.display_name, p.last_seen FROM {$table_name} p LEFT JOIN {$wpdb->users} u ON p.user_id = u.ID WHERE p.document_id = %d AND p.last_seen > %s", $document_id, date('Y-m-d H:i:s', strtotime('-5 minutes')) )); // 广播在线用户列表 $this->pusher->trigger("document-{$document_id}", 'presence-updated', array( 'online_users' => $online_users )); } } // 前端JavaScript处理实时更新 function collab_enqueue_realtime_scripts() { if (is_page('collab-document')) { // 引入Pusher库 wp_enqueue_script('pusher', 'https://js.pusher.com/7.0/pusher.min.js', array(), '7.0', true); // 自定义实时协作脚本 wp_enqueue_script('collab-realtime', plugin_dir_url(__FILE__) . 'js/collab-realtime.js', array('jquery', 'pusher'), '1.0', true); // 传递配置到前端 wp_localize_script('collab-realtime', 'collab_realtime_config', array( 'pusher_key' => get_option('collab_pusher_app_key'), 'pusher_cluster' => get_option('collab_pusher_cluster'), 'current_user_id' => get_current_user_id(), 'current_user_name' => wp_get_current_user()->display_name, 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('collab_realtime_nonce') )); } } add_action('wp_enqueue_scripts', 'collab_enqueue_realtime_scripts'); ?> 前端实时协作JavaScript示例: // collab-realtime.js class CollabRealtimeClient { constructor(documentId) { this.documentId = documentId; this.pusher = null; this.channel = null; this.initializePusher(); } initializePusher() { // 初始化Pusher连接 this.pusher = new Pusher(collab_realtime_config.pusher_key, { cluster: collab_realtime_config.pusher_cluster, authEndpoint: collab_realtime_config.ajax_url, auth: { params: { this.documentId, user_id: collab_realtime_config.current_user_id, nonce: collab_realtime_config.nonce } } }); // 订阅文档频道 this.channel = this.pusher.subscribe(`private-document-${this.documentId}`); // 绑定事件处理 this.bindEvents(); // 通知服务器用户已加入 this.notifyJoined(); } bindEvents() { // 用户加入事件 this.channel.bind('user-joined', (data) => { this.handleUserJoined(data); }); // 用户离开事件 this.channel.bind('user-left', (data) => { this.handleUserLeft(data); }); // 内容更新事件 this.channel.bind('content-updated', (data) => { this.handleContentUpdated(data); }); // 批注添加事件 this.channel.bind('comment-added', (data) => { this.handleCommentAdded(data); }); // 在线用户更新 this.channel.bind('presence-updated', (data) => { this.updateOnlineUsers(data.online_users); }); } notifyJoined() { jQuery.ajax({ url: collab_realtime_config.ajax_url, method: 'POST', data: { action: 'collab_user_joined', document_id: this.documentId, nonce: collab_realtime_config.nonce } }); } handleUserJoined(data) { // 显示用户加入通知 this.showNotification(`${data.user_name} 加入了文档`); // 更新在线用户界面 this.addOnlineUser(data.user_id, data.user_name); } handleContentUpdated(data) { // 如果不是当前用户的操作,应用更改 if (data.user_id !== collab_realtime_config.current_user_id) { this.applyRemoteChanges(data.changes); } } sendContentUpdate(changes) { // 发送内容更新到服务器 jQuery.ajax({ url: collab_realtime_config.ajax_url, method: 'POST', data: { action: 'collab_update_content', document_id: this.documentId, changes: JSON.stringify(changes), nonce: collab_realtime_config.nonce }, success: (response) => { if (response.success) { // 通过Pusher广播更新 this.channel.trigger('client-content-updated', { changes: changes, user_id: collab_realtime_config.current_user_id }); } } }); } disconnect() { if (this.pusher) { this.pusher.disconnect(); } } } ### 2.4 文档审阅与批注系统 实现完整的文档审阅功能: <?php// 批注系统处理class CollabCommentSystem { // 添加批注 public static function add_comment($document_id, $data) { global $wpdb; // 验证权限 if (!collab_check_project_permission( self::get_project_id_from_document($document_id), get_current_user_id(), 'collab_comment' )) { return new WP_Error('permission_denied', '您没有权限添加批注'); } $comment_data = array( 'document_id' => $document_id, 'user_id' => get_current_user_id(), 'content' => sanitize_textarea_field($data['content']), 'position_data' => json_encode($data['position']), 'resolved' => 0 ); // 如果有版本号,关联到特定版本 if (!empty($data['version_id'])) { $comment_data['version_id'] = intval($data['version_id']); } $wpdb->insert("{$wpdb->prefix}collab_comments", $comment_data); $comment_id = $wpdb->insert_id; // 触发实时通知 $realtime = new CollabRealtime(); $realtime->comment_added($document_id, $comment_id, get_current_user_id()); // 发送通知邮件 self::notify_comment_added($document_id, $comment_id); return $comment_id; } // 获取文档批注 public static function get_document_comments($document_id, $version_id = null) { global $wpdb; $query = $wpdb->prepare( "SELECT c.*, u.display_name as user_name, u.user_email, (SELECT display_name FROM {$wpdb->users} WHERE ID = c.resolved_by) as resolved_by_name FROM {$wpdb->prefix}collab_comments c LEFT JOIN {$wpdb->users} u ON c.user_id = u.ID WHERE c.document_id = %d", $document_id ); if ($version_id) { $query .= $wpdb->prepare(" AND (c.version_id = %d OR c.version_id IS NULL)", $version_id); } $query .= " ORDER BY c.created_at ASC"; return $wpdb->get_results($query); } // 解决批注 public static function resolve_comment($comment_id, $user_id) { global $wpdb; $comment = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_comments WHERE id = %d", $comment_id )); if (!$comment) { return new WP_Error('not_found', '批注不存在'); } // 检查权限 $project_id = self::get_project_id_from_document($comment->document_id); if (!collab_check_project_permission($project_id, $user_id, 'collab_comment')) { return new WP_Error('permission_denied', '您没有权限修改批注'); } $wpdb->update( "{$wpdb->prefix}collab_comments", array( 'resolved' => 1, 'resolved_by' => $user_id, 'resolved_at' => current_time('mysql') ), array('id' => $comment_id) ); return true; } // 批注通知 private static function notify_comment_added($document_id, $comment_id) { global $wpdb; // 获取文档信息 $document = $wpdb->get_row($wpdb->prepare( "SELECT d.title, p.id as project_id, p.title as project_title FROM {$wpdb->prefix}collab_documents d LEFT JOIN {$wpdb->prefix}collab_projects p ON d.project_id = p.id WHERE d.id = %d", $document_id )); // 获取项目成员(除了批注作者) $members = $wpdb->get_results($wpdb->prepare( "SELECT m.user_id, u.user_email, u.display_name FROM {$wpdb->prefix}collab_members m LEFT JOIN {$wpdb->users} u ON m.user_id = u.ID WHERE m.project_id = %d AND m.user_id != %d", $document->project_id, get_current_user_id() )); // 获取批注内容 $comment = $wpdb->get_row($wpdb->prepare( "SELECT content FROM {$wpdb->prefix}collab_comments WHERE id = %d", $comment_id )); // 发送邮件通知 foreach ($members as $member) { $subject = sprintf('新批注: %s - %s', $document->project_title, $document->title); $message = sprintf( "您好 %s,nn在项目 '%s' 的文档 '%s' 中有一条新批注:nn%snn请登录系统查看:%snn", $member->display_name, $document->project_title, $document->title, $comment->content, get_permalink(get_page_by_path('collab-document')) . '?id=' . $document_id ); wp_mail($member->user_email, $subject, $message); } } private static function get_project_id_from_document($document_id) { global $wpdb; $result = $wpdb->get_var($wpdb->prepare( "SELECT project_id FROM {$wpdb->prefix}collab_documents WHERE id = %d", $document_id )); return $result; } } // AJAX处理批注add_action('wp_ajax_collab_add_comment', 'collab_ajax_add_comment');function collab_ajax_add_comment() { check_ajax_referer('collab_nonce', 'nonce'); $document_id = intval($_POST['document_id']); $content = sanitize_textarea_field($_POST['content']); $position = json_decode(stripslashes($_POST['position']), true); $comment_id = CollabCommentSystem::add_comment($document_id, array( 'content' => $content, 'position' => $position )); if (is_wp_error($comment_id)) { wp_send_json_error($comment_id->get_error_message()); } else { wp_send_json_success(array('comment_id' => $comment_id)); } }?> ## 第三部分:版本控制与文档历史 ### 3.1 版本控制系统实现 <?phpclass CollabVersionControl { // 创建新版本 public static function create_new_version($document_id, $user_id, $changes_summary = '') { global $wpdb; // 获取当前文档信息 $document = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_documents WHERE id = %d", $document_id )); if (!$document) { return new WP_Error('not_found', '文档不存在'); } // 检查权限 if (!collab_check_project_permission($document->project_id, $user_id, 'collab_edit_document')) { return new WP_Error('permission_denied', '您没有权限创建新版本'); } // 获取下一个版本号 $next_version = $document->version + 1; // 复制当前文件为新版本 $original_path = $document->file_path; $new_filename = self::generate_version_filename($document->title, $next_version, pathinfo($original_path, PATHINFO_EXTENSION)); $new_path = self::get_versions_directory($document_id) . '/' . $new_filename; // 复制文件 if (!copy($original_path, $new_path)) { return new WP_Error('file_copy_failed', '无法创建版本副本'); } // 创建版本记录 $version_data = array( 'document_id' => $document_id, 'version_number' => $next_version, 'file_path' => $new_path, 'changes_summary' => sanitize_text_field($changes_summary), 'created_by' => $user_id ); $wpdb->insert("{$wpdb->prefix}collab_document_versions", $version_data); $version_id = $wpdb->insert_id; // 更新文档版本号 $wpdb->update( "{$wpdb->prefix}collab_documents", array('version' => $next_version, 'updated_at' => current_time('mysql')), array('id' => $document_id) ); // 记录活动 collab_log_activity($document->project_id, 'version_created', array( 'document_id' => $document_id, 'version' => $next_version, 'user_id' => $user_id )); return array( 'version_id' => $version_id, 'version_number' => $next_version, 'file_path' => $new_path ); } // 获取版本列表 public static function get_document_versions($document_id) { global $wpdb; return $wpdb->get_results($wpdb->prepare( "SELECT v.*, u.display_name as creator_name FROM {$wpdb->prefix}collab_document_versions v LEFT JOIN {$wpdb->users} u ON v.created_by = u.ID WHERE v.document_id = %d ORDER BY v.version_number DESC", $document_id )); } // 恢复到指定版本 public static function restore_to_version($document_id, $version_id, $user_id) { global $wpdb; // 获取文档和版本信息 $document = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_documents WHERE id = %d", $document_id )); $version = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_document_versions WHERE id = %d AND document_id = %d", $version_id, $document_id )); if (!$document || !$version) { return new WP_Error('not_found', '文档或版本不存在'); } // 检查权限 if (!collab_check_project_permission($document->project_id, $user_id, 'collab_edit_document')) { return new WP_Error('permission_denied', '您没有权限恢复版本'); } // 创建当前状态的备份 self::create_new_version($document_id, $user_id, '恢复版本前的备份'); // 复制版本文件到当前文档 if (!copy($version->file_path, $document->file_path)) { return new WP_Error('restore_failed', '恢复文件失败'); } // 更新文档信息 $wpdb->update( "{$wpdb->prefix}collab_documents", array( 'version' => $version->version_number, 'updated_at' => current_time('mysql') ), array('id' => $document_id) ); // 记录活动 collab_log_activity($document->project_id, 'version_restored', array( 'document_id' => $document_id, 'from_version' => $document->version, 'to_version' => $version->version_number, 'user_id' => $user_id )); return true; } // 比较两个版本 public static function compare_versions($document_id, $version1_id, $version2_id) { global $wpdb; $version1 = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_document_versions WHERE id = %d", $version1_id )); $version2 = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}collab_document_versions WHERE id = %d", $version2_id )); if (!$version1 || !$version2) { return new WP_Error('not_found', '版本不存在'); } // 根据文件类型使用不同的比较方法 $extension = strtolower(pathinfo($version1->file_path, PATHINFO_EXTENSION)); switch ($extension) { case 'txt': case 'md': return self::compare_text_files($version1->file_path, $version2->file_path); case 'docx': return self::compare_docx_files($version1->file_path, $version2->file_path); case 'pdf': return self::compare_pdf_files($version1->file_path, $version2->file_path); default: return array( 'type' => 'binary', 'message' => '二进制文件,无法显示文本差异' ); } } private static function compare_text_files($file1, $file2) { $content1 = file_get_contents($file1); $content2 = file_get_contents($file2); // 使用文本差异算法 require_once(ABSPATH . 'wp-includes/Text/Diff.php'); require_once(ABSPATH . 'wp-includes/Text/Diff/Renderer.php'); $diff = new Text_Diff('auto', array(explode("n", $content1), explode("n", $content2))); $renderer = new Text_Diff_Renderer_inline(); return array( 'type' => 'text', 'diff' => $renderer->render($diff) ); } private static function generate_version_filename($title, $version, $extension) { $safe_title = sanitize_title($title); return sprintf('%s-v%d-%s.%s', $safe_title, $version, date('Ymd-His'), $extension ); } private static function get_versions_directory($document_id) { $upload_dir = wp_upload_dir(); $versions_dir = $upload_dir['basedir'] . '/collab-versions/' . $document_id; if (!file_exists($versions_dir)) { wp_mkdir_p($versions_dir); } return $versions_dir; } }?> ## 第四部分:用户界面与体验优化 ### 4.1 前端界面构建 使用React构建现代协作界面(简化示例): // DocumentEditor.jsx - 主文档编辑器组件import React, { useState, useEffect, useRef } from 'react';import { CommentList, VersionHistory, OnlineUsers, Toolbar } from './components'; const DocumentEditor = ({ documentId, userId }) => { const [document, setDocument] = useState(null); const

发表评论

WordPress开发教程,集成网站安全扫描与漏洞检测工具

WordPress开发教程:集成网站安全扫描与漏洞检测工具,通过代码二次开发实现常用互联网小工具功能 引言:WordPress安全与功能扩展的双重挑战 在当今数字化时代,WordPress作为全球最受欢迎的内容管理系统,驱动着超过40%的网站。然而,随着其普及度的提升,WordPress网站也成为了黑客攻击的主要目标。据统计,每天有超过9万个WordPress网站遭受各种形式的网络攻击。与此同时,用户对网站功能的需求也日益多样化,不再满足于基本的内容发布,而是期望网站能够集成各种实用工具,提升用户体验。 本教程将深入探讨如何通过WordPress代码二次开发,实现两个关键目标:一是集成专业级网站安全扫描与漏洞检测工具,构建主动防御体系;二是开发常用互联网小工具,增强网站功能性。我们将从理论基础到实践操作,从安全原理到代码实现,全面解析这一综合解决方案。 第一部分:WordPress安全现状分析与安全工具集成必要性 1.1 WordPress安全威胁全景图 WordPress面临的安全威胁多种多样,主要包括: SQL注入攻击:通过恶意SQL代码操纵数据库 跨站脚本攻击(XSS):在网页中注入恶意脚本 跨站请求伪造(CSRF):诱使用户执行非本意的操作 文件包含漏洞:利用文件包含功能执行恶意代码 暴力破解攻击:尝试大量密码组合获取访问权限 主题和插件漏洞:第三方代码中的安全缺陷 根据Wordfence安全报告,2023年针对WordPress的攻击尝试比前一年增加了150%,其中插件和主题漏洞占比高达56%。这些数据凸显了加强WordPress安全防护的紧迫性。 1.2 传统安全方案的局限性 传统的WordPress安全方案通常包括: 基础安全插件安装 定期手动更新 简单的防火墙配置 然而,这些方法存在明显不足: 被动防御:多数方案只在攻击发生后响应 检测能力有限:难以发现复杂或新型攻击 误报率高:可能将正常流量误判为攻击 性能影响:某些安全插件显著降低网站速度 1.3 集成专业安全工具的优势 集成专业安全扫描与漏洞检测工具能够: 实现主动安全监测:定期自动扫描,提前发现潜在风险 深度漏洞检测:使用专业算法识别复杂安全漏洞 实时威胁情报:基于全球攻击数据提供预警 最小性能影响:优化代码减少对网站速度的影响 定制化报告:根据网站特点生成针对性安全建议 第二部分:WordPress安全扫描与漏洞检测工具集成方案 2.1 安全工具架构设计 我们将设计一个模块化的安全系统,包含以下核心组件: // 安全系统主类结构 class WP_Security_Scanner { private $scanner_modules = array(); private $vulnerability_db; private $reporting_system; public function __construct() { $this->init_modules(); $this->load_vulnerability_database(); $this->setup_reporting(); } private function init_modules() { // 初始化各扫描模块 $this->scanner_modules = array( 'file_integrity' => new File_Integrity_Scanner(), 'malware_detection' => new Malware_Scanner(), 'vulnerability_scan' => new Vulnerability_Scanner(), 'brute_force_protection' => new Brute_Force_Protector() ); } } 2.2 文件完整性监控模块实现 文件完整性监控是检测未经授权文件更改的关键技术: class File_Integrity_Scanner { private $baseline_hashes = array(); public function create_baseline() { $wp_files = $this->get_wordpress_files(); foreach ($wp_files as $file) { if ($this->is_scannable_file($file)) { $this->baseline_hashes[$file] = array( 'hash' => md5_file($file), 'size' => filesize($file), 'modified' => filemtime($file) ); } } $this->save_baseline(); } public function run_integrity_check() { $current_hashes = array(); $alerts = array(); foreach ($this->baseline_hashes as $file => $baseline_data) { if (!file_exists($file)) { $alerts[] = "文件删除警告: {$file}"; continue; } $current_hash = md5_file($file); if ($current_hash !== $baseline_data['hash']) { $alerts[] = "文件篡改检测: {$file}"; $this->analyze_file_changes($file, $baseline_data['hash'], $current_hash); } } return $alerts; } } 2.3 漏洞扫描引擎开发 漏洞扫描引擎需要结合本地检测和外部漏洞数据库: class Vulnerability_Scanner { private $vulnerability_sources = array( 'wpvulndb' => 'https://wpvulndb.com/api/v3/', 'nvd' => 'https://services.nvd.nist.gov/rest/json/cves/1.0' ); public function scan_plugins_themes() { $vulnerabilities = array(); // 扫描已安装插件 $plugins = get_plugins(); foreach ($plugins as $plugin_path => $plugin_data) { $plugin_slug = dirname($plugin_path); $plugin_version = $plugin_data['Version']; $plugin_vulns = $this->check_plugin_vulnerabilities($plugin_slug, $plugin_version); if (!empty($plugin_vulns)) { $vulnerabilities['plugins'][$plugin_slug] = $plugin_vulns; } } // 扫描当前主题 $theme = wp_get_theme(); $theme_vulns = $this->check_theme_vulnerabilities($theme->get('TextDomain'), $theme->get('Version')); if (!empty($theme_vulns)) { $vulnerabilities['theme'] = $theme_vulns; } return $vulnerabilities; } private function check_plugin_vulnerabilities($slug, $version) { // 查询漏洞数据库 $api_url = $this->vulnerability_sources['wpvulndb'] . "plugins/{$slug}"; $response = wp_remote_get($api_url); if (is_wp_error($response)) { return false; } $data = json_decode(wp_remote_retrieve_body($response), true); $relevant_vulns = array(); if (isset($data[$slug]['vulnerabilities'])) { foreach ($data[$slug]['vulnerabilities'] as $vuln) { if ($this->is_version_affected($version, $vuln['fixed_in'])) { $relevant_vulns[] = array( 'id' => $vuln['id'], 'title' => $vuln['title'], 'cvss_score' => $vuln['cvss']['score'], 'fixed_in' => $vuln['fixed_in'] ); } } } return $relevant_vulns; } } 2.4 恶意代码检测模块 恶意代码检测需要结合特征码检测和行为分析: class Malware_Scanner { private $malware_signatures = array( 'base64_decode' => '/base64_decodes*([^)]*)/', 'eval' => '/evals*([^)]*)/', 'shell_exec' => '/shell_execs*([^)]*)/', 'suspicious_url' => '/(https?://[^s<>"']*.(php|exe|bat|sh))/i' ); private $suspicious_patterns = array( 'obfuscated_code' => '/$(?:w+)s*=s*$(?:w+)s*.s*$(?:w+)/', 'long_string' => '/"[^"]{200,}"/' ); public function scan_directory($directory) { $malware_findings = array(); $files = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($directory) ); foreach ($files as $file) { if ($file->isDir()) continue; if ($this->is_php_file($file)) { $content = file_get_contents($file->getPathname()); $file_findings = $this->analyze_file_content($content, $file->getPathname()); if (!empty($file_findings)) { $malware_findings[$file->getPathname()] = $file_findings; } } } return $malware_findings; } private function analyze_file_content($content, $filename) { $findings = array(); // 检查已知恶意代码特征 foreach ($this->malware_signatures as $type => $pattern) { if (preg_match_all($pattern, $content, $matches)) { $findings[$type] = $matches[0]; } } // 检查可疑代码模式 foreach ($this->suspicious_patterns as $pattern_name => $pattern) { if (preg_match_all($pattern, $content, $matches)) { $findings[$pattern_name] = count($matches[0]); } } // 检查文件权限 $perms = substr(sprintf('%o', fileperms($filename)), -4); if ($perms == '0777') { $findings['insecure_permissions'] = $perms; } return $findings; } } 2.5 安全报告与警报系统 class Security_Reporter { private $report_types = array('daily', 'weekly', 'immediate'); private $notification_methods = array('email', 'dashboard', 'webhook'); public function generate_report($scan_results, $report_type = 'daily') { $report = array( 'timestamp' => current_time('timestamp'), 'scan_summary' => array( 'total_checks' => 0, 'issues_found' => 0, 'critical_issues' => 0 ), 'detailed_findings' => array(), 'recommendations' => array() ); // 汇总扫描结果 foreach ($scan_results as $module => $results) { $report['scan_summary']['total_checks']++; if (!empty($results)) { $report['scan_summary']['issues_found']++; $report['detailed_findings'][$module] = $results; // 生成建议 $report['recommendations'] = array_merge( $report['recommendations'], $this->generate_recommendations($module, $results) ); } } // 确定报告严重级别 $report['severity'] = $this->calculate_severity($report); return $report; } public function send_alerts($report) { if ($report['severity'] >= 7) { // 高严重级别 $this->send_immediate_alert($report); } // 发送定期报告 if ($this->is_time_for_report('daily')) { $this->send_email_report($report, 'daily'); } // 更新仪表板小工具 $this->update_dashboard_widget($report); } } 第三部分:常用互联网小工具的开发与集成 3.1 小工具系统架构设计 我们将创建一个可扩展的小工具框架: class WP_Toolkit_Framework { private $tools = array(); private $tool_categories = array( 'utility' => '实用工具', 'seo' => 'SEO工具', 'security' => '安全工具', 'development' => '开发工具' ); public function register_tool($tool_slug, $tool_config) { $defaults = array( 'name' => '', 'description' => '', 'category' => 'utility', 'callback' => null, 'settings' => array(), 'shortcode' => '' ); $config = wp_parse_args($tool_config, $defaults); $this->tools[$tool_slug] = $config; // 注册短代码 if (!empty($config['shortcode'])) { add_shortcode($config['shortcode'], array($this, 'render_tool')); } } public function render_tool($atts, $content = null, $tag = '') { $atts = shortcode_atts(array('tool' => ''), $atts, $tag); if (empty($atts['tool']) || !isset($this->tools[$atts['tool']])) { return '<p>工具未找到</p>'; } $tool = $this->tools[$atts['tool']]; ob_start(); ?> <div class="wp-toolkit-tool" id="tool-<?php echo esc_attr($atts['tool']); ?>"> <div class="tool-header"> <h3><?php echo esc_html($tool['name']); ?></h3> <p class="tool-description"><?php echo esc_html($tool['description']); ?></p> </div> <div class="tool-content"> <?php call_user_func($tool['callback'], $atts); ?> </div> </div> <?php return ob_get_clean(); } } 3.2 密码强度检测工具 class Password_Strength_Tool { public function init() { add_shortcode('password_strength_checker', array($this, 'render_checker')); add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); } public function render_checker() { ob_start(); ?> <div class="password-strength-checker"> <h3>密码强度检测</h3> <div class="input-group"> <input type="password" id="password-input" placeholder="输入密码进行强度检测" class="form-control"> <button id="toggle-visibility" class="btn btn-secondary"> 显示/隐藏 </button> </div> <div class="strength-meter"> <div class="strength-bar"></div> <div class="strength-labels"> <span class="strength-label" data-level="0">非常弱</span> <span class="strength-label" data-level="1">弱</span> <span class="strength-label" data-level="2">中等</span> <span class="strength-label" data-level="3">强</span> <span class="strength-label" data-level="4">非常强</span> </div> </div> <div class="password-feedback"> <h4>改进建议:</h4> <ul id="password-suggestions"></ul> </div> <div class="password-statistics"> <h4>密码统计:</h4> <p>长度: <span id="password-length">0</span> 字符</p> <p>熵值: <span id="password-entropy">0</span> bits</p> <p>破解时间: <span id="crack-time">立即</span></p> </div> </div> <?php return ob_get_clean(); } public function enqueue_scripts() { wp_enqueue_script('password-strength-js', plugin_dir_url(__FILE__) . 'js/password-strength.js', array('jquery'), '1.0', true); wp_enqueue_style('password-strength-css', plugin_dir_url(__FILE__) . 'css/password-strength.css'); } } 3.3 网站性能检测工具 class Website_Performance_Tool { public function performance_test($url = '') { if (empty($url)) { $url = home_url(); } $results = array( 'load_time' => 0, 'page_size' => 0, 'requests' => 0, 'performance_score' => 0, 'recommendations' => array() ); // 使用WordPress HTTP API进行测试 $start_time = microtime(true); $response = wp_remote_get($url, array( 'timeout' => 30, 'sslverify' => false )); $end_time = microtime(true); if (!is_wp_error($response)) { $results['load_time'] = round(($end_time - $start_time) * 1000, 2); $results['page_size'] = strlen($response['body']) / 1024; // 分析HTML内容 $results = $this->analyze_html_content($response['body'], $results); // 计算性能分数 $results['performance_score'] = $this->calculate_score($results); // 生成建议 $results['recommendations'] = $this->generate_recommendations($results); } return $results; } private function analyze_html_content($html, $results) { // 解析DOM $dom = new DOMDocument(); @$dom->loadHTML($html); // 统计资源请求 $scripts = $dom->getElementsByTagName('script'); $stylesheets = $dom->getElementsByTagName('link'); $images = $dom->getElementsByTagName('img'); $results['requests'] = $scripts->length + $stylesheets->length + $images->length; 3.4 SEO分析工具 class SEO_Analyzer_Tool { public function analyze_page($url = '') { if (empty($url)) { $url = get_permalink(); } $analysis = array( 'basic' => array(), 'on_page' => array(), 'technical' => array(), 'score' => 0 ); $response = wp_remote_get($url); if (!is_wp_error($response)) { $html = wp_remote_retrieve_body($response); $headers = wp_remote_retrieve_headers($response); // 基础分析 $analysis['basic'] = $this->basic_analysis($html, $headers); // 页面SEO分析 $analysis['on_page'] = $this->on_page_analysis($html); // 技术SEO分析 $analysis['technical'] = $this->technical_analysis($html, $headers); // 计算总分 $analysis['score'] = $this->calculate_seo_score($analysis); } return $analysis; } private function on_page_analysis($html) { $dom = new DOMDocument(); @$dom->loadHTML($html); $analysis = array( 'title' => array( 'value' => '', 'length' => 0, 'score' => 0 ), 'meta_description' => array( 'value' => '', 'length' => 0, 'score' => 0 ), 'headings' => array(), 'images' => array( 'total' => 0, 'with_alt' => 0 ), 'keywords' => array() ); // 分析标题标签 $title_tags = $dom->getElementsByTagName('title'); if ($title_tags->length > 0) { $title = $title_tags->item(0)->nodeValue; $analysis['title']['value'] = $title; $analysis['title']['length'] = mb_strlen($title); $analysis['title']['score'] = $this->evaluate_title($title); } // 分析meta描述 $meta_tags = $dom->getElementsByTagName('meta'); foreach ($meta_tags as $meta) { if ($meta->getAttribute('name') == 'description') { $description = $meta->getAttribute('content'); $analysis['meta_description']['value'] = $description; $analysis['meta_description']['length'] = mb_strlen($description); $analysis['meta_description']['score'] = $this->evaluate_description($description); } } // 分析标题结构 for ($i = 1; $i <= 6; $i++) { $h_tags = $dom->getElementsByTagName('h' . $i); $analysis['headings']['h' . $i] = array( 'count' => $h_tags->length, 'titles' => array() ); foreach ($h_tags as $h_tag) { $analysis['headings']['h' . $i]['titles'][] = $h_tag->nodeValue; } } return $analysis; } public function render_seo_tool() { ob_start(); ?> <div class="seo-analyzer-tool"> <div class="seo-input-section"> <input type="url" id="seo-analysis-url" placeholder="输入要分析的URL" value="<?php echo esc_url(home_url()); ?>"> <button id="run-seo-analysis" class="btn btn-primary"> 分析SEO </button> </div> <div class="seo-results-container"> <div class="seo-score-card"> <div class="score-circle" id="seo-score-circle"> <span class="score-value">0</span> </div> <h4>SEO总分</h4> </div> <div class="seo-details"> <div class="seo-section" id="basic-seo"> <h4>基础SEO</h4> <div class="seo-metrics"></div> </div> <div class="seo-section" id="on-page-seo"> <h4>页面SEO</h4> <div class="seo-metrics"></div> </div> <div class="seo-section" id="technical-seo"> <h4>技术SEO</h4> <div class="seo-metrics"></div> </div> </div> <div class="seo-recommendations"> <h4>改进建议</h4> <ul id="seo-suggestions"></ul> </div> </div> </div> <?php return ob_get_clean(); } } 3.5 二维码生成工具 class QR_Code_Generator { private $qr_library_path; public function __construct() { // 引入QR码生成库 require_once plugin_dir_path(__FILE__) . 'libs/phpqrcode/qrlib.php'; } public function generate_qr_code($data, $options = array()) { $defaults = array( 'size' => 10, 'margin' => 4, 'level' => 'L', // L, M, Q, H 'foreground' => array(0, 0, 0), 'background' => array(255, 255, 255), 'logo' => false, 'format' => 'png' ); $options = wp_parse_args($options, $defaults); // 创建临时文件 $temp_dir = wp_upload_dir()['basedir'] . '/qrcodes/'; if (!file_exists($temp_dir)) { wp_mkdir_p($temp_dir); } $filename = 'qr_' . md5(serialize($data) . serialize($options)) . '.png'; $filepath = $temp_dir . $filename; // 生成QR码 QRcode::png($data, $filepath, $options['level'], $options['size'], $options['margin']); // 添加Logo(如果指定) if ($options['logo'] && file_exists($options['logo'])) { $this->add_logo_to_qr($filepath, $options['logo']); } // 颜色调整 if ($options['foreground'] != array(0, 0, 0) || $options['background'] != array(255, 255, 255)) { $this->recolor_qr($filepath, $options['foreground'], $options['background']); } return array( 'url' => wp_upload_dir()['baseurl'] . '/qrcodes/' . $filename, 'path' => $filepath, 'filename' => $filename ); } public function render_generator_ui() { ob_start(); ?> <div class="qr-code-generator"> <div class="generator-form"> <div class="form-group"> <label for="qr-data">内容/URL:</label> <textarea id="qr-data" rows="3" placeholder="输入要编码的内容或URL"></textarea> </div> <div class="form-group"> <label for="qr-size">尺寸:</label> <select id="qr-size"> <option value="5">小 (200x200)</option> <option value="10" selected>中 (400x400)</option> <option value="15">大 (600x600)</option> <option value="20">超大 (800x800)</option> </select> </div> <div class="form-group"> <label for="qr-error-correction">容错级别:</label> <select id="qr-error-correction"> <option value="L">低 (7%)</option> <option value="M" selected>中 (15%)</option> <option value="Q">高 (25%)</option> <option value="Q">极高 (30%)</option> </select> </div> <div class="form-group"> <label>前景色:</label> <input type="color" id="qr-foreground-color" value="#000000"> </div> <div class="form-group"> <label>背景色:</label> <input type="color" id="qr-background-color" value="#ffffff"> </div> <div class="form-group"> <label for="qr-logo">添加Logo:</label> <input type="file" id="qr-logo" accept="image/*"> </div> <button id="generate-qr" class="btn btn-primary"> 生成QR码 </button> </div> <div class="qr-preview-container"> <div class="qr-preview" id="qr-preview"> <p>QR码预览将显示在这里</p> </div> <div class="qr-actions"> <button id="download-qr" class="btn btn-secondary" disabled> 下载PNG </button> <button id="copy-qr-link" class="btn btn-secondary" disabled> 复制链接 </button> <button id="share-qr" class="btn btn-secondary" disabled> 分享 </button> </div> <div class="qr-info"> <h4>QR码信息:</h4> <p>版本: <span id="qr-version">-</span></p> <p>数据容量: <span id="qr-capacity">-</span></p> <p>纠错级别: <span id="qr-ecc-level">-</span></p> </div> </div> </div> <?php return ob_get_clean(); } } 第四部分:系统集成与优化 4.1 统一管理界面开发 class Toolkit_Admin_Interface { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function add_admin_menu() { add_menu_page( '网站工具包', '网站工具包', 'manage_options', 'wp-toolkit', array($this, 'render_admin_page'), 'dashicons-admin-tools', 30 ); // 添加子菜单 add_submenu_page( 'wp-toolkit', '安全扫描', '安全扫描', 'manage_options', 'wp-toolkit-security', array($this, 'render_security_page') ); add_submenu_page( 'wp-toolkit', '工具集', '工具集', 'manage_options', 'wp-toolkit-tools', array($this, 'render_tools_page') ); add_submenu_page( 'wp-toolkit', '设置', '设置', 'manage_options', 'wp-toolkit-settings', array($this, 'render_settings_page') ); } public function render_admin_page() { ?> <div class="wrap wp-toolkit-dashboard"> <h1>网站工具包仪表板</h1> <div class="dashboard-widgets"> <div class="widget security-status"> <h3>安全状态</h3> <div class="widget-content"> <?php $this->display_security_status(); ?> </div> </div> <div class="widget quick-tools"> <h3>快速工具</h3> <div class="widget-content"> <?php $this->display_quick_tools(); ?> </div> </div> <div class="widget recent-scans"> <h3>最近扫描</h3> <div class="widget-content"> <?php $this->display_recent_scans(); ?> </div> </div> <div class="widget system-info"> <h3>系统信息</h3> <div class="widget-content"> <?php $this->display_system_info(); ?> </div> </div> </div> <div class="dashboard-main"> <div class="activity-log"> <h3>活动日志</h3> <div class="log-entries"> <?php $this->display_activity_log(); ?> </div> </div> </div> </div> <?php } public function render_security_page() { $scanner = new WP_Security_Scanner(); $scan_results = $scanner->run_full_scan(); ?> <div class="wrap wp-toolkit-security"> <h1>安全扫描中心</h1> <div class="security-controls"> <button class="button button-primary" id="run-full-scan"> 运行完整扫描 </button> <button class="button button-secondary" id="run-quick-scan"> 快速扫描 </button> <button class="button" id="schedule-scan"> 计划扫描 </button> </div> <div class="scan-results"> <div class="results-summary"> <h3>扫描摘要</h3> <div class="summary-cards"> <?php $this->display_scan_summary($scan_results); ?> </div> </div> <div class="detailed-results"> <h3>详细结果</h3> <div class="results-tabs"> <ul class="tab-nav"> <li class="active" data-tab="vulnerabilities">漏洞</li> <li data-tab="malware">恶意软件</li> <li data-tab="file-changes">文件变更</li> <li data-tab="security-headers">安全头</li> </ul> <div class="tab-content"> <?php $this->display_detailed_results($scan_results); ?> </div> </div> </div> </div> </div> <?php } } 4.2 性能优化与缓存机制 class Toolkit_Performance_Optimizer { private $cache_enabled = true; private $cache_expiry = 3600; // 1小时 public function __construct() { add_action('init', array($this, 'init_cache_system')); add_action('save_post', array($this, 'clear_post_cache')); add_action('switch_theme', array($this, 'clear_theme_cache')); } public function init_cache_system() { // 创建缓存目录 $cache_dir = WP_CONTENT_DIR . '/cache/wp-toolkit/'; if (!file_exists($cache_dir)) { wp_mkdir_p($cache_dir); } // 添加缓存清理计划任务 if (!wp_next_scheduled('wp_toolkit_clear_expired_cache')) { wp_schedule_event(time(), 'hourly', 'wp_toolkit_clear_expired_cache'); } add_action('wp_toolkit_clear_expired_cache', array($this, 'clear_expired_cache')); } public function get_cached_data($key, $callback, $expiry = null) { if (!$this->cache_enabled) { return call_user_func($callback); } $cache_key = 'wp_toolkit_' . md5($key); $cached = get_transient($cache_key); if ($cached !== false) { return $cached; } $data = call_user_func($callback); if ($expiry === null) { $expiry = $this->cache_expiry; } set_transient($cache_key, $data, $expiry); // 同时保存到文件缓存作为备份 $this->save_to_file_cache($key, $data); return $data; } private function save_to_file_cache($key, $data) { $cache_file = WP_CONTENT_DIR . '/cache/wp-toolkit/' . md5($key) . '.cache'; $cache_data = array( 'timestamp' => time(), 'data' => $data, 'key' => $key ); file_put_contents($cache_file, serialize($cache_data)); } public function optimize_database() { global $wpdb; $optimizations = array(); // 清理修订版本 $revisions = $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->posts WHERE post_type = 'revision'" ); if ($revisions > 50) { $wpdb->query( "DELETE FROM $wpdb->posts WHERE post_type = 'revision' AND post_modified < DATE_SUB(NOW(), INTERVAL 30 DAY)" ); $optimizations[] = "清理了旧的文章修订版本"; } // 清理自动草稿 $wpdb->query( "DELETE FROM $wpdb->posts WHERE post_status = 'auto-draft' AND post_date < DATE_SUB(NOW(), INTERVAL 7 DAY)" ); // 优化数据库表 $tables = $wpdb->get_col("SHOW TABLES"); foreach ($tables as $table) { $wpdb->query("OPTIMIZE TABLE $table"); } $optimizations[] = "优化了所有数据库表"; return $optimizations; } } 4.3 API接口与扩展性设计 class Toolkit_API { private $api_version = 'v1';

发表评论

一步步实现,在WordPress中添加实时数据大屏与可视化图表

一步步实现:在WordPress中添加实时数据大屏与可视化图表 引言:为什么WordPress需要实时数据可视化功能 在当今数据驱动的互联网时代,实时数据可视化已成为网站运营和内容展示的重要组成部分。WordPress作为全球最流行的内容管理系统,虽然拥有强大的内容管理能力,但在原生功能上却缺乏专业的数据可视化工具。许多网站运营者、电商店主和内容创作者都需要在自己的WordPress站点上展示实时数据,如网站流量统计、销售数据、用户行为分析等。 传统上,用户可能需要依赖第三方服务或插件来实现这些功能,但这往往带来数据安全、加载速度和定制灵活性等问题。通过WordPress代码二次开发,我们可以创建完全可控、高度定制化的实时数据大屏和可视化图表,不仅能提升网站的专业形象,还能为访客提供更有价值的数据洞察。 本文将详细介绍如何通过代码开发,在WordPress中实现实时数据大屏与可视化图表功能,涵盖从环境准备到最终部署的完整流程。 第一章:开发环境准备与项目规划 1.1 开发环境搭建 在开始开发之前,我们需要准备合适的开发环境。建议使用本地开发环境,如Local by Flywheel、XAMPP或MAMP,这样可以避免对生产站点造成影响。 首先,确保你的开发环境满足以下要求: PHP 7.4或更高版本 MySQL 5.6或更高版本 WordPress 5.8或更高版本 支持HTTPS(用于某些API调用) 接下来,创建一个专门用于开发的WordPress主题或插件目录。考虑到功能的独立性,我们建议将其开发为一个独立插件,这样可以在不同主题间保持功能一致性。 <?php /** * Plugin Name: 实时数据大屏与可视化工具 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress添加实时数据大屏和可视化图表功能 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later */ 1.2 项目架构设计 良好的项目架构是成功开发的关键。我们建议采用模块化设计,将功能分解为以下几个核心模块: 数据获取模块:负责从各种数据源收集数据 数据处理模块:对原始数据进行清洗、转换和聚合 图表渲染模块:使用JavaScript库生成可视化图表 前端展示模块:创建数据大屏的界面布局 管理后台模块:提供配置选项和管理界面 目录结构建议如下: real-time-dashboard/ ├── includes/ │ ├── class-data-fetcher.php │ ├── class-data-processor.php │ ├── class-chart-renderer.php │ └── class-admin-settings.php ├── assets/ │ ├── css/ │ ├── js/ │ └── images/ ├── templates/ │ ├── dashboard-template.php │ └── widget-template.php └── real-time-dashboard.php 1.3 技术选型与工具准备 对于数据可视化,我们需要选择合适的JavaScript图表库。以下是几个优秀的选择: Chart.js:轻量级、易于使用,适合基础图表 ECharts:功能强大、图表类型丰富,由百度开发 D3.js:最灵活、功能最强大,但学习曲线较陡 ApexCharts:现代、交互式图表库 考虑到易用性和功能平衡,本文选择ECharts作为主要图表库。同时,我们还需要以下工具: 用于实时数据更新的WebSocket或Server-Sent Events 用于数据缓存的Transients API或Redis 用于REST API开发WordPress的REST API功能 第二章:数据获取与处理模块开发 2.1 创建数据获取类 数据获取是实时数据大屏的基础。我们需要创建一个专门的数据获取类,负责从不同数据源收集信息。 <?php class RealTime_Data_Fetcher { private $data_sources; public function __construct() { $this->data_sources = array( 'website_traffic' => array( 'name' => '网站流量', 'type' => 'internal', 'callback' => array($this, 'fetch_website_traffic') ), 'woocommerce_sales' => array( 'name' => '销售数据', 'type' => 'woocommerce', 'callback' => array($this, 'fetch_woocommerce_sales') ), 'external_api' => array( 'name' => '外部API数据', 'type' => 'external', 'callback' => array($this, 'fetch_external_data') ) ); } /** * 获取网站流量数据 */ public function fetch_website_traffic($time_range = 'today') { global $wpdb; $data = array(); $current_time = current_time('timestamp'); switch($time_range) { case 'today': $start_time = strtotime('today midnight'); break; case 'week': $start_time = strtotime('-7 days'); break; case 'month': $start_time = strtotime('-30 days'); break; default: $start_time = strtotime('-24 hours'); } // 查询页面访问数据 $query = $wpdb->prepare( "SELECT COUNT(*) as count, DATE_FORMAT(visit_time, '%%H:00') as hour FROM {$wpdb->prefix}page_visits WHERE visit_time >= %s GROUP BY HOUR(visit_time) ORDER BY visit_time", date('Y-m-d H:i:s', $start_time) ); $results = $wpdb->get_results($query); foreach($results as $row) { $data[] = array( 'time' => $row->hour, 'visits' => (int)$row->count ); } return $data; } /** * 获取WooCommerce销售数据 */ public function fetch_woocommerce_sales($time_range = 'today') { if (!class_exists('WooCommerce')) { return array(); } $data = array(); // 根据时间范围获取订单数据 $args = array( 'status' => array('completed', 'processing'), 'date_created' => '>' . (time() - 24*60*60), // 最近24小时 'limit' => -1 ); $orders = wc_get_orders($args); $hourly_sales = array(); foreach($orders as $order) { $order_time = $order->get_date_created()->getTimestamp(); $hour = date('H:00', $order_time); $total = $order->get_total(); if (!isset($hourly_sales[$hour])) { $hourly_sales[$hour] = 0; } $hourly_sales[$hour] += $total; } ksort($hourly_sales); foreach($hourly_sales as $hour => $sales) { $data[] = array( 'time' => $hour, 'sales' => $sales ); } return $data; } /** * 获取所有数据源的数据 */ public function fetch_all_data() { $all_data = array(); foreach($this->data_sources as $key => $source) { if (is_callable($source['callback'])) { $all_data[$key] = call_user_func($source['callback']); } } return $all_data; } } 2.2 实现数据缓存机制 为了避免频繁查询数据库,我们需要实现数据缓存机制。WordPress提供了Transients API,可以方便地实现临时数据存储。 class RealTime_Data_Processor { private $cache_expiration = 300; // 5分钟缓存 /** * 处理并缓存数据 */ public function process_and_cache_data($data_key, $raw_data) { // 数据处理逻辑 $processed_data = $this->process_data($raw_data); // 设置缓存 set_transient( 'rtd_' . $data_key, $processed_data, $this->cache_expiration ); return $processed_data; } /** * 数据处理逻辑 */ private function process_data($raw_data) { $processed = array(); // 数据清洗和转换 foreach($raw_data as $item) { // 移除空值 $cleaned_item = array_filter($item, function($value) { return $value !== null && $value !== ''; }); // 数据格式化 if (!empty($cleaned_item)) { $processed[] = $this->format_data($cleaned_item); } } // 数据聚合(如果需要) if (count($processed) > 100) { $processed = $this->aggregate_data($processed); } return $processed; } /** * 数据格式化 */ private function format_data($data) { // 根据数据类型进行格式化 foreach($data as $key => $value) { if (is_numeric($value)) { $data[$key] = floatval($value); } elseif (is_string($value)) { $data[$key] = sanitize_text_field($value); } } return $data; } } 2.3 创建REST API端点 为了前端能够获取数据,我们需要创建REST API端点。 class RealTime_Dashboard_API { public function __construct() { add_action('rest_api_init', array($this, 'register_routes')); } public function register_routes() { // 获取所有数据 register_rest_route('real-time-dashboard/v1', '/data', array( 'methods' => 'GET', 'callback' => array($this, 'get_all_data'), 'permission_callback' => array($this, 'check_permissions') )); // 获取特定数据源的数据 register_rest_route('real-time-dashboard/v1', '/data/(?P<source>[a-zA-Z0-9_-]+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_source_data'), 'permission_callback' => array($this, 'check_permissions') )); } public function get_all_data($request) { $fetcher = new RealTime_Data_Fetcher(); $processor = new RealTime_Data_Processor(); $all_data = array(); $sources = array('website_traffic', 'woocommerce_sales'); foreach($sources as $source) { $raw_data = $fetcher->fetch_data_by_source($source); $processed_data = $processor->process_and_cache_data($source, $raw_data); $all_data[$source] = $processed_data; } return rest_ensure_response($all_data); } public function check_permissions($request) { // 检查用户权限,可以根据需要调整 return current_user_can('edit_posts'); } } 第三章:前端可视化界面开发 3.1 创建数据大屏基础布局 首先,我们需要创建一个响应式的大屏布局,确保在不同设备上都能良好显示。 <!-- templates/dashboard-template.php --> <div class="real-time-dashboard-container"> <header class="dashboard-header"> <h1>实时数据大屏</h1> <div class="dashboard-controls"> <select id="timeRangeSelect" class="dashboard-select"> <option value="realtime">实时</option> <option value="today">今日</option> <option value="week">本周</option> <option value="month">本月</option> </select> <button id="refreshBtn" class="dashboard-button"> <span class="refresh-icon">↻</span> 刷新 </button> <button id="fullscreenBtn" class="dashboard-button"> <span class="fullscreen-icon">⛶</span> 全屏 </button> </div> </header> <div class="dashboard-grid"> <!-- 图表容器将通过JavaScript动态生成 --> <div class="grid-item" data-chart-type="line" data-source="website_traffic"> <div class="chart-header"> <h3>网站实时访问量</h3> <div class="chart-stats"> <span class="current-value">0</span> <span class="change-indicator"></span> </div> </div> <div class="chart-container" id="trafficChart"></div> </div> <div class="grid-item" data-chart-type="bar" data-source="woocommerce_sales"> <div class="chart-header"> <h3>实时销售数据</h3> <div class="chart-stats"> <span class="current-value">¥0</span> <span class="change-indicator"></span> </div> </div> <div class="chart-container" id="salesChart"></div> </div> <!-- 更多图表容器... --> </div> <div class="dashboard-footer"> <div class="last-updated"> 最后更新: <span id="lastUpdatedTime">--:--:--</span> </div> <div class="data-source-info"> 数据每5分钟自动更新 </div> </div> </div> 3.2 引入ECharts并初始化图表 接下来,我们需要引入ECharts库并创建图表初始化函数。 // assets/js/dashboard-main.js (function($) { 'use strict'; class RealTimeDashboard { constructor() { this.charts = {}; this.dataCache = {}; this.updateInterval = 300000; // 5分钟 this.isFullscreen = false; this.timeRange = 'realtime'; this.init(); } init() { this.loadECharts(); this.initCharts(); this.bindEvents(); this.startAutoUpdate(); this.fetchInitialData(); } loadECharts() { // 动态加载ECharts库 if (typeof echarts === 'undefined') { const script = document.createElement('script'); script.src = 'https://cdn.jsdelivr.net/npm/echarts@5.4.0/dist/echarts.min.js'; script.onload = () => this.onEChartsLoaded(); document.head.appendChild(script); } else { this.onEChartsLoaded(); } } onEChartsLoaded() { console.log('ECharts loaded successfully'); this.echarts = echarts; this.initCharts(); } initCharts() { if (!this.echarts) return; // 初始化所有图表容器 $('.chart-container').each((index, container) => { const chartId = $(container).attr('id'); const chartType = $(container).closest('.grid-item').data('chart-type'); const dataSource = $(container).closest('.grid-item').data('source'); this.charts[chartId] = this.echarts.init(container); this.setChartOption(chartId, chartType, dataSource); }); // 监听窗口大小变化,重新调整图表大小 $(window).on('resize', () => { this.resizeCharts(); }); } setChartOption(chartId, chartType, dataSource) { const baseOption = { tooltip: { trigger: 'axis', formatter: function(params) { let result = `${params[0].axisValue}<br/>`; params.forEach(param => { result += `${param.marker} ${param.seriesName}: ${param.value}<br/>`; }); return result; } }, grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false, data: [] }, yAxis: { type: 'value' }, series: [] }; // 根据图表类型设置不同选项 switch(chartType) { case 'line': baseOption.series.push({ name: '数据', type: 'line', smooth: true, data: [], itemStyle: { color: '#5470c6' }, areaStyle: { color: new this.echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: 'rgba(84, 112, 198, 0.5)' }, { offset: 1, color: 'rgba(84, 112, 198, 0.1)' } ]) } }); break; case 'bar': baseOption.series.push({ name: '数据', type: 'bar', data: [], itemStyle: { color: new this.echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: '#83bff6' }, { offset: 0.5, color: '#188df0' }, { offset: 1, color: '#188df0' } ]) } }); break; // 可以添加更多图表类型 } this.charts[chartId].setOption(baseOption); } fetchInitialData() { this.fetchData(); } fetchData() { const self = this; $.ajax({ url: '/wp-json/real-time-dashboard/v1/data', method: 'GET', dataType: 'json', beforeSend: function(xhr) { xhr.setRequestHeader('X-WP-Nonce', realTimeDashboard.nonce); }, success: function(response) { self.processData(response); self.updateLastUpdatedTime(); }, error: function(xhr, status, error) { console.error('数据获取失败:', error); self.showErrorMessage('数据加载失败,请稍后重试'); } }); } processData(data) { // 处理网站流量数据 if (data.website_traffic && this.charts.trafficChart) { this.updateChartData('trafficChart', data.website_traffic, '访问量'); this.updateStats('trafficChart', data.website_traffic); } // 处理销售数据 if (data.woocommerce_sales && this.charts.salesChart) { this.updateChartData('salesChart', data.woocommerce_sales, '销售额'); this.updateStats('salesChart', data.woocommerce_sales); } // 可以添加更多数据处理逻辑 } updateChartData(chartId, data, seriesName) { const chart = this.charts[chartId]; if (!chart) return; const xData = []; const yData = []; data.forEach(item => { xData.push(item.time || item.date); yData.push(item.visits || item.sales || item.value); }); const option = chart.getOption(); option.xAxis[0].data = xData; option.series[0].data = yData; option.series[0].name = seriesName; chart.setOption(option); } updateStats(chartId, data) { if (!data || data.length === 0) return; const currentValue = data[data.length - 1].visits || data[data.length - 1].sales || data[data.length - 1].value; const previousValue = data.length > 1 ? data[data.length - 2].visits || data[data.length - 2].sales || data[data.length - 2].value : 0; const change = previousValue > 0 ? ((currentValue - previousValue) / previousValue * 100).toFixed(1) : 0; const $gridItem = $(`#${chartId}`).closest('.grid-item'); const $currentValue = $gridItem.find('.current-value'); const $changeIndicator = $gridItem.find('.change-indicator'); // 更新当前值 $currentValue.text(this.formatValue(currentValue, chartId)); // 更新变化指示器 if (change > 0) { $changeIndicator.html(`<span class="increase">↑ ${change}%</span>`); $changeIndicator.removeClass('decrease').addClass('increase'); } else if (change < 0) { $changeIndicator.html(`<span class="decrease">↓ ${Math.abs(change)}%</span>`); $changeIndicator.removeClass('increase').addClass('decrease'); } else { $changeIndicator.html('<span class="neutral">→ 0%</span>'); $changeIndicator.removeClass('increase decrease'); } } formatValue(value, chartId) { if (chartId === 'salesChart') { return '¥' + Number(value).toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } return Number(value).toLocaleString('zh-CN'); } bindEvents() { const self = this; // 刷新按钮 $('#refreshBtn').on('click', function() { self.fetchData(); $(this).addClass('refreshing'); setTimeout(() => { $(this).removeClass('refreshing'); }, 1000); }); // 时间范围选择 $('#timeRangeSelect').on('change', function() { self.timeRange = $(this).val(); self.fetchData(); }); // 全屏按钮 $('#fullscreenBtn').on('click', function() { self.toggleFullscreen(); }); // 键盘快捷键 $(document).on('keydown', function(e) { if (e.key === 'F11' || (e.key === 'f' && e.ctrlKey)) { e.preventDefault(); self.toggleFullscreen(); } else if (e.key === 'r' && e.ctrlKey) { e.preventDefault(); self.fetchData(); } }); } toggleFullscreen() { const container = $('.real-time-dashboard-container')[0]; if (!this.isFullscreen) { if (container.requestFullscreen) { container.requestFullscreen(); } else if (container.webkitRequestFullscreen) { container.webkitRequestFullscreen(); } else if (container.msRequestFullscreen) { container.msRequestFullscreen(); } this.isFullscreen = true; $('#fullscreenBtn').html('<span class="exit-fullscreen-icon">⛶</span> 退出全屏'); } else { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } this.isFullscreen = false; $('#fullscreenBtn').html('<span class="fullscreen-icon">⛶</span> 全屏'); } } resizeCharts() { Object.values(this.charts).forEach(chart => { chart.resize(); }); } startAutoUpdate() { setInterval(() => { this.fetchData(); }, this.updateInterval); } updateLastUpdatedTime() { const now = new Date(); const timeString = now.toLocaleTimeString('zh-CN', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' }); $('#lastUpdatedTime').text(timeString); } showErrorMessage(message) { // 显示错误提示 const $errorDiv = $('<div class="dashboard-error">') .text(message) .hide() .appendTo('.dashboard-header'); $errorDiv.fadeIn(300); setTimeout(() => { $errorDiv.fadeOut(300, function() { $(this).remove(); }); }, 5000); } } // 初始化仪表板 $(document).ready(function() { if ($('.real-time-dashboard-container').length) { window.realTimeDashboardApp = new RealTimeDashboard(); } }); })(jQuery); 3.3 添加CSS样式 /* assets/css/dashboard-style.css */ .real-time-dashboard-container { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%); color: #ffffff; min-height: 100vh; padding: 20px; border-radius: 12px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); } .dashboard-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid rgba(255, 255, 255, 0.1); } .dashboard-header h1 { margin: 0; font-size: 28px; font-weight: 600; background: linear-gradient(90deg, #4cc9f0, #4361ee); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; } .dashboard-controls { display: flex; gap: 12px; align-items: center; } .dashboard-select { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.2); color: white; padding: 8px 16px; border-radius: 6px; font-size: 14px; cursor: pointer; transition: all 0.3s ease; } .dashboard-select:hover { background: rgba(255, 255, 255, 0.15); border-color: rgba(255, 255, 255, 0.3); } .dashboard-select:focus { outline: none; box-shadow: 0 0 0 2px rgba(76, 201, 240, 0.3); } .dashboard-button { background: linear-gradient(135deg, #4361ee 0%, #3a0ca3 100%); border: none; color: white; padding: 8px 16px; border-radius: 6px; font-size: 14px; cursor: pointer; display: flex; align-items: center; gap: 6px; transition: all 0.3s ease; } .dashboard-button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(67, 97, 238, 0.4); } .dashboard-button:active { transform: translateY(0); } .dashboard-button.refreshing .refresh-icon { animation: spin 1s linear infinite; } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .dashboard-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(500px, 1fr)); gap: 20px; margin-bottom: 30px; } .grid-item { background: rgba(255, 255, 255, 0.05); border-radius: 10px; padding: 20px; backdrop-filter: blur(10px); border: 1px solid rgba(255, 255, 255, 0.1); transition: all 0.3s ease; } .grid-item:hover { transform: translateY(-5px); box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2); border-color: rgba(76, 201, 240, 0.3); } .chart-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; } .chart-header h3 { margin: 0; font-size: 18px; font-weight: 500; color: #e0e0e0; } .chart-stats { display: flex; align-items: center; gap: 10px; } .current-value { font-size: 24px; font-weight: 600; color: #4cc9f0; } .change-indicator { font-size: 14px; padding: 4px 8px; border-radius: 4px; } .change-indicator.increase { background: rgba(76, 175, 80, 0.2); color: #4caf50; } .change-indicator.decrease { background: rgba(244, 67, 54, 0.2); color: #f44336; } .change-indicator.neutral { background: rgba(158, 158, 158, 0.2); color: #9e9e9e; } .chart-container { width: 100%; height: 300px; } .dashboard-footer { display: flex; justify-content: space-between; align-items: center; padding-top: 20px; border-top: 1px solid rgba(255, 255, 255, 0.1); font-size: 14px; color: #b0b0b0; } .last-updated { display: flex; align-items: center; gap: 8px; } #lastUpdatedTime { color: #4cc9f0; font-weight: 500; } .data-source-info { display: flex; align-items: center; gap: 8px; } .data-source-info::before { content: "⏱️"; font-size: 12px; } /* 全屏模式样式 */ :fullscreen .real-time-dashboard-container { padding: 40px; border-radius: 0; } :fullscreen .dashboard-grid { grid-template-columns: repeat(auto-fit, minmax(600px, 1fr)); gap: 30px; } :fullscreen .chart-container { height: 400px; } /* 响应式设计 */ @media (max-width: 1200px) { .dashboard-grid { grid-template-columns: 1fr; } } @media (max-width: 768px) { .dashboard-header { flex-direction: column; gap: 15px; align-items: flex-start; } .dashboard-controls { width: 100%; justify-content: space-between; } .dashboard-grid { grid-template-columns: 1fr; } .grid-item { min-width: 100%; } } /* 加载动画 */ .chart-loading { position: relative; } .chart-loading::after { content: ''; position: absolute; top: 50%; left: 50%; width: 40px; height: 40px; margin: -20px 0 0 -20px; border: 3px solid rgba(76, 201, 240, 0.3); border-top-color: #4cc9f0; border-radius: 50%; animation: spin 1s linear infinite; } /* 错误提示 */ .dashboard-error { position: fixed; top: 20px; right: 20px; background: #f44336; color: white; padding: 12px 20px; border-radius: 6px; box-shadow: 0 4px 12px rgba(244, 67, 54, 0.3); z-index: 1000; animation: slideIn 0.3s ease; } @keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } } 第四章:管理后台与配置界面 4.1 创建管理菜单和设置页面 <?php class RealTime_Dashboard_Admin { private $settings_page; public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_init', array($this, 'register_settings')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } public function add_admin_menu() { $this->settings_page = add_menu_page( '实时数据大屏设置', '数据大屏', 'manage_options', 'real-time-dashboard', array($this, 'render_settings_page'), 'dashicons-chart-area', 30 ); // 添加子菜单 add_submenu_page( 'real-time-dashboard', '数据源配置', '数据源配置', 'manage_options', 'real-time-dashboard-sources', array($this, 'render_sources_page') ); add_submenu_page( 'real-time-dashboard', '图表设置', '图表设置', 'manage_options', 'real-time-dashboard-charts', array($this, 'render_charts_page') ); } public function render_settings_page() { ?> <div class="wrap real-time-dashboard-admin"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <div class="dashboard-admin-header"> <div class="admin-stats"> <div class="stat-card"> <h3>数据源状态</h3> <div class="stat-value" id="dataSourcesStatus">检查中...</div> </div> <div class="stat-card"> <h3>最后更新</h3> <div class="stat-value" id="lastDataUpdate">--:--:--</div> </div> <div class="stat-card"> <h3>缓存状态</h3> <div class="stat-value" id="cacheStatus">正常</div> </div> </div> </div> <form method="post" action="options.php"> <?php settings_fields('real_time_dashboard_settings'); do_settings_sections('real_time_dashboard_settings'); submit_button('保存设置'); ?> </form> <div class="admin-actions"> <h2>快速操作</h2> <div class="action-buttons"> <button type="button" class="button button-primary" id="clearCacheBtn"> 清除缓存 </button> <button type="button" class="button button-secondary" id="testDataBtn"> 测试数据源 </button> <button type="button" class="button button-secondary" id="exportConfigBtn"> 导出配置 </button> <a href="<?php echo home_url('/real-time-dashboard/'); ?>" class="button button-secondary" target="_blank"> 查看大屏 </a> </div> </div> </div> <?php } public function register_settings() { // 注册基本设置 register_setting( 'real_time_dashboard_settings', 'rtd_general_settings',

发表评论

实战教学,为网站集成在线预约排队与叫号系统

实战教学:为网站集成在线预约排队与叫号系统——通过WordPress代码二次开发实现常用互联网小工具功能 引言:数字化时代下的服务升级需求 在当今快节奏的数字化社会中,无论是医疗机构、教育机构、政府服务大厅,还是餐饮、美容、维修等商业服务场所,排队等待已成为客户体验中最令人不满的环节之一。传统的排队方式不仅消耗客户宝贵时间,也增加了服务场所的管理难度和人力成本。随着互联网技术的普及,在线预约排队与叫号系统应运而生,成为提升服务效率、优化客户体验的重要工具。 对于大多数中小型企业和机构而言,定制开发一套完整的预约排队系统成本高昂,维护复杂。而WordPress作为全球最流行的内容管理系统,以其强大的扩展性和灵活性,为我们提供了一个经济高效的解决方案。本文将深入探讨如何通过WordPress程序的代码二次开发,为网站集成一套功能完善的在线预约排队与叫号系统,实现这一常用互联网小工具功能。 第一章:系统需求分析与设计规划 1.1 业务场景分析 在开始技术实现之前,我们首先需要明确系统的使用场景和功能需求。一个典型的在线预约排队与叫号系统应包含以下核心功能: 客户端功能: 服务项目浏览与选择 预约时间选择与提交 排队号码获取与状态查询 实时排队位置提醒 预约取消与改期 管理端功能: 预约申请审核与管理 排队队列实时监控 叫号操作与控制 服务窗口/人员管理 数据统计与分析报表 现场显示功能: 当前叫号信息展示 等待队列信息展示 服务窗口状态显示 1.2 技术架构设计 基于WordPress的二次开发,我们采用以下技术架构: 前端展示层:使用WordPress主题模板系统,结合HTML5、CSS3和JavaScript(特别是AJAX技术)实现用户界面 业务逻辑层:通过自定义Post Type、Taxonomy和Meta Box构建数据模型,利用Action和Filter钩子扩展核心功能 数据持久层:利用WordPress数据库结构,创建自定义数据表存储预约和排队信息 实时通信层:采用WebSocket或轮询技术实现排队状态的实时更新 外部接口层:集成短信/邮件通知API,提供状态提醒服务 第二章:WordPress开发环境搭建与基础配置 2.1 开发环境准备 在开始开发之前,我们需要搭建一个适合WordPress二次开发的环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:下载最新版WordPress,完成基础安装 开发工具配置: 代码编辑器:VS Code或PHPStorm 版本控制:Git初始化仓库 调试工具:安装Query Monitor、Debug Bar等调试插件 代码标准:配置PHP_CodeSniffer遵循WordPress编码标准 2.2 创建自定义插件 为了避免主题更新导致功能丢失,我们将所有功能集成到一个独立插件中: <?php /** * Plugin Name: 智能预约排队系统 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站集成在线预约排队与叫号功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('APPOINTMENT_SYSTEM_VERSION', '1.0.0'); define('APPOINTMENT_SYSTEM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('APPOINTMENT_SYSTEM_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once APPOINTMENT_SYSTEM_PLUGIN_DIR . 'includes/class-appointment-system.php'; function run_appointment_system() { $plugin = new Appointment_System(); $plugin->run(); } run_appointment_system(); 第三章:数据模型设计与数据库结构 3.1 自定义Post Type设计 我们将使用WordPress的自定义文章类型来管理预约和服务项目: // 注册服务项目自定义文章类型 function register_service_post_type() { $labels = array( 'name' => '服务项目', 'singular_name' => '服务项目', 'menu_name' => '服务项目管理', 'add_new' => '添加服务项目', 'add_new_item' => '添加新服务项目', 'edit_item' => '编辑服务项目', 'new_item' => '新服务项目', 'view_item' => '查看服务项目', 'search_items' => '搜索服务项目', 'not_found' => '未找到服务项目', 'not_found_in_trash' => '回收站中无服务项目' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'service'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-calendar-alt', 'supports' => array('title', 'editor', 'thumbnail', 'excerpt'), 'show_in_rest' => true ); register_post_type('service', $args); } add_action('init', 'register_service_post_type'); 3.2 自定义数据库表设计 对于排队和预约数据,我们需要创建独立的数据库表以保证查询效率: // 创建自定义数据库表 function create_appointment_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name_appointments = $wpdb->prefix . 'appointments'; $table_name_queues = $wpdb->prefix . 'queues'; // 预约表 $sql_appointments = "CREATE TABLE IF NOT EXISTS $table_name_appointments ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) DEFAULT 0, service_id bigint(20) NOT NULL, appointment_date date NOT NULL, time_slot varchar(50) NOT NULL, customer_name varchar(100) NOT NULL, customer_phone varchar(20) NOT NULL, customer_email varchar(100), status varchar(20) DEFAULT 'pending', queue_number varchar(50), created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY service_id (service_id), KEY appointment_date (appointment_date), KEY status (status) ) $charset_collate;"; // 排队表 $sql_queues = "CREATE TABLE IF NOT EXISTS $table_name_queues ( id bigint(20) NOT NULL AUTO_INCREMENT, appointment_id bigint(20) NOT NULL, service_point_id bigint(20) NOT NULL, queue_number varchar(50) NOT NULL, position int(11) NOT NULL, estimated_wait_time int(11) DEFAULT 0, called_at datetime, served_at datetime, status varchar(20) DEFAULT 'waiting', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY appointment_id (appointment_id), KEY service_point_id (service_point_id), KEY status (status) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_appointments); dbDelta($sql_queues); } register_activation_hook(__FILE__, 'create_appointment_tables'); 第四章:前端预约界面开发 4.1 预约表单设计与实现 创建一个用户友好的预约表单是系统的关键: // 预约表单短代码 function appointment_form_shortcode($atts) { ob_start(); // 获取服务项目 $services = get_posts(array( 'post_type' => 'service', 'posts_per_page' => -1, 'orderby' => 'title', 'order' => 'ASC' )); // 获取可预约日期(未来30天) $available_dates = array(); $today = new DateTime(); for ($i = 1; $i <= 30; $i++) { $date = clone $today; $date->add(new DateInterval("P{$i}D")); // 排除周末 if ($date->format('N') < 6) { $available_dates[] = $date->format('Y-m-d'); } } // 定义时间槽 $time_slots = array( '09:00-10:00', '10:00-11:00', '11:00-12:00', '13:00-14:00', '14:00-15:00', '15:00-16:00', '16:00-17:00' ); ?> <div class="appointment-form-container"> <h2>在线预约</h2> <form id="appointment-form" method="post"> <?php wp_nonce_field('submit_appointment', 'appointment_nonce'); ?> <div class="form-group"> <label for="service_id">选择服务项目 *</label> <select id="service_id" name="service_id" required> <option value="">请选择服务项目</option> <?php foreach ($services as $service): ?> <option value="<?php echo $service->ID; ?>"> <?php echo esc_html($service->post_title); ?> </option> <?php endforeach; ?> </select> </div> <div class="form-group"> <label for="appointment_date">选择预约日期 *</label> <select id="appointment_date" name="appointment_date" required> <option value="">请选择日期</option> <?php foreach ($available_dates as $date): ?> <option value="<?php echo $date; ?>"> <?php echo date('Y年m月d日', strtotime($date)); ?> </option> <?php endforeach; ?> </select> </div> <div class="form-group"> <label for="time_slot">选择时间段 *</label> <select id="time_slot" name="time_slot" required> <option value="">请选择时间段</option> <?php foreach ($time_slots as $slot): ?> <option value="<?php echo $slot; ?>"><?php echo $slot; ?></option> <?php endforeach; ?> </select> </div> <div class="form-group"> <label for="customer_name">姓名 *</label> <input type="text" id="customer_name" name="customer_name" required> </div> <div class="form-group"> <label for="customer_phone">手机号 *</label> <input type="tel" id="customer_phone" name="customer_phone" required> </div> <div class="form-group"> <label for="customer_email">电子邮箱</label> <input type="email" id="customer_email" name="customer_email"> </div> <div class="form-group"> <button type="submit" id="submit-appointment">提交预约</button> </div> <div id="appointment-response"></div> </form> </div> <script> jQuery(document).ready(function($) { $('#appointment-form').on('submit', function(e) { e.preventDefault(); var formData = $(this).serialize(); $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'submit_appointment', data: formData }, beforeSend: function() { $('#submit-appointment').prop('disabled', true).text('提交中...'); }, success: function(response) { if (response.success) { $('#appointment-response').html( '<div class="success-message">' + '预约成功!您的排队号码是:<strong>' + response.data.queue_number + '</strong>' + '</div>' ); $('#appointment-form')[0].reset(); } else { $('#appointment-response').html( '<div class="error-message">' + response.data + '</div>' ); } $('#submit-appointment').prop('disabled', false).text('提交预约'); } }); }); }); </script> <?php return ob_get_clean(); } add_shortcode('appointment_form', 'appointment_form_shortcode'); 4.2 实时排队状态展示 // 排队状态展示短代码 function queue_status_shortcode() { ob_start(); ?> <div class="queue-status-container"> <h2>实时排队状态</h2> <div class="current-calls"> <h3>当前叫号</h3> <div id="current-calls-display"> <!-- 通过AJAX动态加载 --> </div> </div> <div class="waiting-queue"> <h3>等待队列</h3> <div id="waiting-queue-display"> <!-- 通过AJAX动态加载 --> </div> </div> <div class="queue-search"> <h3>查询我的排队状态</h3> <input type="text" id="queue-number-input" placeholder="请输入您的排队号码"> <button id="check-queue-status">查询</button> <div id="personal-queue-status"></div> </div> </div> <script> jQuery(document).ready(function($) { // 更新排队状态 function updateQueueStatus() { $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'GET', data: { action: 'get_queue_status' }, success: function(response) { if (response.success) { $('#current-calls-display').html(response.data.current_calls); $('#waiting-queue-display').html(response.data.waiting_queue); } } }); } // 初始加载 updateQueueStatus(); // 每30秒更新一次 setInterval(updateQueueStatus, 30000); // 查询个人排队状态 $('#check-queue-status').on('click', function() { var queueNumber = $('#queue-number-input').val(); if (!queueNumber) { alert('请输入排队号码'); return; } $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'get_personal_queue_status', queue_number: queueNumber }, success: function(response) { if (response.success) { $('#personal-queue-status').html(response.data); } else { $('#personal-queue-status').html( '<div class="error">' + response.data + '</div>' ); } } }); }); }); </script> <?php return ob_get_clean(); } add_shortcode('queue_status', 'queue_status_shortcode'); 第五章:后台管理系统开发 5.1 预约管理界面 // 添加预约管理菜单 function add_appointment_admin_menu() { add_menu_page( '预约管理', '预约管理', 'manage_options', 'appointment-management', 'display_appointment_management_page', 'dashicons-calendar', 6 ); add_submenu_page( 'appointment-management', '排队叫号', '排队叫号', 'manage_options', 'queue-management', 'display_queue_management_page' ); add_submenu_page( 'appointment-management', '服务窗口设置', '服务窗口设置', 'manage_options', 'service-points', 'display_service_points_page' ); } add_action('admin_menu', 'add_appointment_admin_menu'); // 预约管理页面 function display_appointment_management_page() { global $wpdb; $table_name = $wpdb->prefix . 'appointments'; // 处理批量操作 if (isset($_POST['bulk_action']) && isset($_POST['appointment_ids'])) { $action = $_POST['bulk_action']; $ids = implode(',', array_map('intval', $_POST['appointment_ids'])); switch ($action) { case 'confirm': $wpdb->query("UPDATE $table_name SET status = 'confirmed' WHERE id IN ($ids)"); break; case 'cancel': $wpdb->query("UPDATE $table_name SET status = 'cancelled' WHERE id IN ($ids)"); break; case 'delete': $wpdb->query("DELETE FROM $table_name WHERE id IN ($ids)"); break; } } // 获取预约数据 $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $offset = ($current_page - 1) * $per_page; $where_conditions = array('1=1'); $where_values = array(); // 筛选条件 if (!empty($_GET['status'])) { $where_conditions[] = 'status = %s'; 5.2 排队叫号管理界面 // 排队叫号管理页面 function display_queue_management_page() { global $wpdb; $appointments_table = $wpdb->prefix . 'appointments'; $queues_table = $wpdb->prefix . 'queues'; $service_points_table = $wpdb->prefix . 'service_points'; // 获取所有服务窗口 $service_points = $wpdb->get_results("SELECT * FROM $service_points_table WHERE is_active = 1"); // 处理叫号操作 if (isset($_POST['call_next']) && isset($_POST['service_point_id'])) { $service_point_id = intval($_POST['service_point_id']); // 获取当前窗口的下一个排队号 $next_queue = $wpdb->get_row($wpdb->prepare( "SELECT q.*, a.customer_name, a.service_id FROM $queues_table q LEFT JOIN $appointments_table a ON q.appointment_id = a.id WHERE q.service_point_id = %d AND q.status = 'waiting' ORDER BY q.position ASC LIMIT 1", $service_point_id )); if ($next_queue) { // 更新为已叫号状态 $wpdb->update( $queues_table, array( 'status' => 'called', 'called_at' => current_time('mysql') ), array('id' => $next_queue->id) ); echo '<div class="notice notice-success"><p>已叫号:' . esc_html($next_queue->queue_number) . '</p></div>'; // 记录叫号历史 $wpdb->insert( $wpdb->prefix . 'call_history', array( 'queue_id' => $next_queue->id, 'service_point_id' => $service_point_id, 'called_at' => current_time('mysql'), 'operator_id' => get_current_user_id() ) ); } } // 处理完成服务操作 if (isset($_POST['complete_service']) && isset($_POST['queue_id'])) { $queue_id = intval($_POST['queue_id']); $wpdb->update( $queues_table, array( 'status' => 'completed', 'served_at' => current_time('mysql') ), array('id' => $queue_id) ); echo '<div class="notice notice-success"><p>服务已完成</p></div>'; } ?> <div class="wrap"> <h1>排队叫号管理</h1> <div class="queue-management-dashboard"> <!-- 服务窗口状态 --> <div class="service-points-status"> <h2>服务窗口状态</h2> <div class="service-points-grid"> <?php foreach ($service_points as $point): // 获取当前窗口状态 $current_queue = $wpdb->get_row($wpdb->prepare( "SELECT q.*, a.customer_name FROM $queues_table q LEFT JOIN $appointments_table a ON q.appointment_id = a.id WHERE q.service_point_id = %d AND q.status = 'called' ORDER BY q.called_at DESC LIMIT 1", $point->id )); $waiting_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $queues_table WHERE service_point_id = %d AND status = 'waiting'", $point->id )); ?> <div class="service-point-card"> <h3><?php echo esc_html($point->name); ?> (<?php echo esc_html($point->code); ?>)</h3> <div class="current-serving"> <?php if ($current_queue): ?> <p>当前服务:<?php echo esc_html($current_queue->queue_number); ?></p> <p>客户:<?php echo esc_html($current_queue->customer_name); ?></p> <p>等待时间:<?php echo human_time_diff(strtotime($current_queue->called_at), current_time('timestamp')); ?></p> <form method="post" style="display: inline;"> <input type="hidden" name="queue_id" value="<?php echo $current_queue->id; ?>"> <button type="submit" name="complete_service" class="button button-primary"> 完成服务 </button> </form> <?php else: ?> <p>窗口空闲</p> <?php endif; ?> </div> <div class="waiting-info"> <p>等待人数:<?php echo $waiting_count; ?></p> <form method="post"> <input type="hidden" name="service_point_id" value="<?php echo $point->id; ?>"> <button type="submit" name="call_next" class="button"> 叫下一个号 </button> </form> </div> </div> <?php endforeach; ?> </div> </div> <!-- 实时排队队列 --> <div class="real-time-queues"> <h2>实时排队队列</h2> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>排队号码</th> <th>客户姓名</th> <th>服务项目</th> <th>服务窗口</th> <th>排队位置</th> <th>预计等待</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <?php $queues = $wpdb->get_results(" SELECT q.*, a.customer_name, s.post_title as service_name, sp.name as point_name FROM $queues_table q LEFT JOIN $appointments_table a ON q.appointment_id = a.id LEFT JOIN {$wpdb->posts} s ON a.service_id = s.ID LEFT JOIN $service_points_table sp ON q.service_point_id = sp.id WHERE q.status IN ('waiting', 'called') ORDER BY CASE q.status WHEN 'called' THEN 1 WHEN 'waiting' THEN 2 ELSE 3 END, q.position ASC LIMIT 50 "); foreach ($queues as $queue): $wait_time = $queue->estimated_wait_time > 0 ? round($queue->estimated_wait_time / 60) . '分钟' : '计算中'; ?> <tr> <td><?php echo esc_html($queue->queue_number); ?></td> <td><?php echo esc_html($queue->customer_name); ?></td> <td><?php echo esc_html($queue->service_name); ?></td> <td><?php echo esc_html($queue->point_name); ?></td> <td><?php echo $queue->position; ?></td> <td><?php echo $wait_time; ?></td> <td> <span class="status-badge status-<?php echo $queue->status; ?>"> <?php $status_labels = array( 'waiting' => '等待中', 'called' => '已叫号', 'completed' => '已完成' ); echo $status_labels[$queue->status] ?? $queue->status; ?> </span> </td> <td> <?php if ($queue->status == 'waiting'): ?> <form method="post" style="display: inline;"> <input type="hidden" name="queue_id" value="<?php echo $queue->id; ?>"> <input type="hidden" name="service_point_id" value="<?php echo $queue->service_point_id; ?>"> <button type="submit" name="call_now" class="button button-small"> 立即叫号 </button> </form> <?php endif; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <!-- 叫号历史 --> <div class="call-history"> <h2>今日叫号历史</h2> <?php $today = current_time('Y-m-d'); $history = $wpdb->get_results($wpdb->prepare( "SELECT ch.*, q.queue_number, a.customer_name, sp.name as point_name FROM {$wpdb->prefix}call_history ch LEFT JOIN $queues_table q ON ch.queue_id = q.id LEFT JOIN $appointments_table a ON q.appointment_id = a.id LEFT JOIN $service_points_table sp ON ch.service_point_id = sp.id WHERE DATE(ch.called_at) = %s ORDER BY ch.called_at DESC LIMIT 20", $today )); ?> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>时间</th> <th>排队号码</th> <th>客户姓名</th> <th>服务窗口</th> <th>操作员</th> </tr> </thead> <tbody> <?php foreach ($history as $record): $operator = get_userdata($record->operator_id); ?> <tr> <td><?php echo date('H:i:s', strtotime($record->called_at)); ?></td> <td><?php echo esc_html($record->queue_number); ?></td> <td><?php echo esc_html($record->customer_name); ?></td> <td><?php echo esc_html($record->point_name); ?></td> <td><?php echo $operator ? esc_html($operator->display_name) : '系统'; ?></td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> </div> <style> .service-points-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; } .service-point-card { border: 1px solid #ddd; border-radius: 8px; padding: 15px; background: #fff; } .service-point-card h3 { margin-top: 0; border-bottom: 2px solid #0073aa; padding-bottom: 10px; } .current-serving { background: #f0f8ff; padding: 10px; border-radius: 4px; margin-bottom: 15px; } .status-badge { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; } .status-waiting { background: #ffeb3b; color: #333; } .status-called { background: #4caf50; color: white; } .status-completed { background: #9e9e9e; color: white; } </style> <?php } 第六章:实时通信与状态更新 6.1 WebSocket服务器实现 // WebSocket服务器类 class QueueWebSocketServer { private $server; private $clients = []; private $rooms = []; public function __construct($host = '0.0.0.0', $port = 8080) { $this->server = new SwooleWebSocketServer($host, $port); $this->server->on('open', [$this, 'onOpen']); $this->server->on('message', [$this, 'onMessage']); $this->server->on('close', [$this, 'onClose']); // 定时推送队列更新 $this->server->tick(5000, [$this, 'broadcastQueueUpdates']); } public function onOpen($server, $request) { $client_id = $request->fd; $this->clients[$client_id] = [ 'id' => $client_id, 'room' => null, 'type' => 'client' // client, display, admin ]; echo "客户端 {$client_id} 已连接n"; } public function onMessage($server, $frame) { $data = json_decode($frame->data, true); $client_id = $frame->fd; if (!$data || !isset($data['action'])) { return; } switch ($data['action']) { case 'join_room': $this->joinRoom($client_id, $data['room']); break; case 'subscribe_queue': $this->subscribeQueue($client_id, $data['queue_number']); break; case 'admin_command': $this->handleAdminCommand($client_id, $data); break; } } public function onClose($server, $fd) { if (isset($this->clients[$fd])) { $client = $this->clients[$fd]; // 从房间中移除 if ($client['room'] && isset($this->rooms[$client['room']])) { $index = array_search($fd, $this->rooms[$client['room']]); if ($index !== false) { array_splice($this->rooms[$client['room']], $index, 1); } } unset($this->clients[$fd]); echo "客户端 {$fd} 已断开连接n"; } } private function joinRoom($client_id, $room) { $this->clients[$client_id]['room'] = $room; if (!isset($this->rooms[$room])) { $this->rooms[$room] = []; } if (!in_array($client_id, $this->rooms[$room])) { $this->rooms[$room][] = $client_id; } // 发送欢迎消息 $welcome = [ 'type' => 'system', 'message' => "已加入房间: {$room}", 'timestamp' => time() ]; $this->server->push($client_id, json_encode($welcome)); } private function subscribeQueue($client_id, $queue_number) { $this->clients[$client_id]['queue_number'] = $queue_number; // 发送当前队列状态 $queue_status = $this->getQueueStatus($queue_number); $this->server->push($client_id, json_encode([ 'type' => 'queue_status', 'data' => $queue_status ])); } private function handleAdminCommand($client_id, $data) { if ($this->clients[$client_id]['type'] !== 'admin') { return; } switch ($data['command']) { case 'call_number': $this->broadcastCallNumber($data['queue_number'], $data['service_point']); break; case 'update_queue': $this->broadcastQueueUpdate(); break; } } public function broadcastQueueUpdates() { global $wpdb; $queues_table = $wpdb->prefix . 'queues'; // 获取需要更新的队列信息 $active_queues = $wpdb->get_results(" SELECT queue_number, position, estimated_wait_time, status FROM $queues_table WHERE status IN ('waiting', 'called') AND updated_at >= DATE_SUB(NOW(), INTERVAL 5 MINUTE) "); if (empty($active_queues)) { return; } $update_data = [ 'type' => 'queue_update', 'data' => $active_queues, 'timestamp' => time() ]; // 广播给所有客户端 foreach ($this->clients as $client_id => $client) { if ($client['type'] === 'client') { $this->server->push($client_id, json_encode($update_data)); } } // 广播给显示屏幕 if (isset($this->rooms['display'])) { $display_data = $this->getDisplayData(); foreach ($this->rooms['display'] as $display_client) { $this->server->push($display_client, json_encode([ 'type' => 'display_update', 'data' => $display_data ])); } } } private function broadcastCallNumber($queue_number, $service_point) { $call_data = [ 'type' => 'call_number', 'queue_number' => $queue_number, 'service_point' => $service_point, 'timestamp' => time() ]; // 通知特定客户 foreach ($this->clients as $client_id => $client) { if (isset($client['queue_number']) && $client['queue_number'] === $queue_number) { $this->server->push($client_id, json_encode($call_data)); } } // 广播给显示屏幕 if (isset($this->rooms['display'])) { foreach ($this->rooms['display'] as $display_client) { $this->server->push($display_client, json_encode($call_data)); } } // 记录到数据库 global $wpdb; $wpdb->insert($wpdb->prefix . 'call_notifications', [ 'queue_number' => $queue_number, 'service_point' => $service_point, 'called_at' => current_time('mysql') ]); } private function getQueueStatus($queue_number) { global $wpdb; $queues_table = $wpdb->prefix . 'queues'; $appointments_table = $wpdb->prefix . 'appointments';

发表评论