跳至内容

分类: 应用软件

手把手教程,为WordPress实现智能化的网站内容版权检测与保护工具

手把手教程:为WordPress实现智能化的网站内容版权检测与保护工具 引言:数字时代的内容版权挑战 在当今数字化时代,内容创作已成为网站运营的核心。无论是个人博客、企业官网还是电子商务平台,原创内容都是吸引流量、建立品牌权威的关键要素。然而,随着互联网信息的快速传播,内容盗用、抄袭和未经授权的转载问题日益严重。根据最新统计,超过60%的网站管理员表示曾遭遇过内容被盗用的情况,这直接影响了原创者的权益和网站的SEO表现。 WordPress作为全球最流行的内容管理系统,占据了互联网近43%的网站份额。虽然WordPress拥有丰富的插件生态系统,但在内容版权保护方面,大多数现有解决方案要么功能有限,要么需要高昂的订阅费用。本文将手把手指导您通过WordPress代码二次开发,实现一个智能化的网站内容版权检测与保护工具,让您的原创内容得到更好的保护。 第一章:理解WordPress内容保护的基本原理 1.1 WordPress内容盗用的常见形式 在开始开发之前,我们需要了解内容盗用的主要形式: 直接复制粘贴:用户通过浏览器右键复制或查看源代码获取内容 RSS订阅盗用:通过网站的RSS源批量抓取内容 爬虫程序抓取:使用自动化脚本抓取网站内容 截图盗用:对内容进行截图后重新发布 1.2 现有版权保护方法的局限性 当前常见的WordPress版权保护方法包括: 禁用右键功能:简单但用户体验差,且技术用户可轻松绕过 添加水印:适用于图片,但对文本内容无效 使用JavaScript干扰:可以防止简单复制,但无法阻止源代码查看 法律声明:仅有威慑作用,缺乏实际保护能力 1.3 智能化版权保护的核心思路 我们将要开发的工具基于以下核心思路: 内容指纹技术:为每篇文章生成唯一数字指纹 智能监控系统:定期搜索互联网上的相似内容 动态防护机制:根据访问者行为动态调整保护策略 版权声明自动化:自动在内容中嵌入版权信息 第二章:开发环境准备与基础架构设计 2.1 开发环境配置 首先,确保您具备以下开发环境: WordPress安装(建议5.6以上版本) PHP开发环境(7.4以上版本) 代码编辑器(VS Code、PHPStorm等) 本地或测试服务器环境 2.2 创建插件基础结构 在WordPress的wp-content/plugins/目录下创建新文件夹smart-content-protector,并建立以下基础文件结构: smart-content-protector/ ├── smart-content-protector.php # 主插件文件 ├── includes/ │ ├── class-content-fingerprint.php # 内容指纹生成类 │ ├── class-content-monitor.php # 内容监控类 │ ├── class-protection-engine.php # 保护引擎类 │ └── class-settings-manager.php # 设置管理类 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 后台样式 │ ├── js/ │ │ └── admin-script.js # 后台脚本 │ └── class-admin-interface.php # 后台界面类 ├── public/ │ ├── css/ │ │ └── public-style.css # 前端样式 │ ├── js/ │ │ └── public-script.js # 前端脚本 │ └── class-public-handler.php # 前端处理类 ├── assets/ # 静态资源 └── uninstall.php # 插件卸载处理 2.3 主插件文件配置 编辑smart-content-protector.php文件,添加插件基本信息: <?php /** * Plugin Name: Smart Content Protector * Plugin URI: https://yourwebsite.com/smart-content-protector * Description: 智能化的WordPress网站内容版权检测与保护工具 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: smart-content-protector */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('SCP_VERSION', '1.0.0'); define('SCP_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SCP_PLUGIN_URL', plugin_dir_url(__FILE__)); define('SCP_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { $prefix = 'SCP_'; $base_dir = SCP_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class_name, $len) !== 0) { return; } $relative_class = substr($class_name, $len); $file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function scp_init_plugin() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.6', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('Smart Content Protector需要WordPress 5.6或更高版本。', 'smart-content-protector'); echo '</p></div>'; }); return; } // 初始化核心类 $content_fingerprint = new SCP_Content_Fingerprint(); $content_monitor = new SCP_Content_Monitor(); $protection_engine = new SCP_Protection_Engine(); $settings_manager = new SCP_Settings_Manager(); // 根据环境加载前端或后台 if (is_admin()) { require_once SCP_PLUGIN_DIR . 'admin/class-admin-interface.php'; new SCP_Admin_Interface($settings_manager); } else { require_once SCP_PLUGIN_DIR . 'public/class-public-handler.php'; new SCP_Public_Handler($protection_engine); } } add_action('plugins_loaded', 'scp_init_plugin'); // 激活插件时的操作 register_activation_hook(__FILE__, 'scp_activate_plugin'); function scp_activate_plugin() { // 创建必要的数据库表 scp_create_database_tables(); // 设置默认选项 $default_options = array( 'protection_level' => 'medium', 'auto_monitor' => true, 'monitor_frequency' => 'weekly', 'watermark_text' => '本文来自{site_name},原文链接:{post_url}', 'disable_right_click' => false, 'enable_fingerprint' => true, 'search_engines' => array('google', 'bing'), ); add_option('scp_settings', $default_options); // 安排定期监控任务 if (!wp_next_scheduled('scp_daily_monitor')) { wp_schedule_event(time(), 'daily', 'scp_daily_monitor'); } } // 停用插件时的操作 register_deactivation_hook(__FILE__, 'scp_deactivate_plugin'); function scp_deactivate_plugin() { // 清除定时任务 wp_clear_scheduled_hook('scp_daily_monitor'); } // 创建数据库表 function scp_create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'scp_content_fingerprints'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, post_id bigint(20) NOT NULL, fingerprint varchar(64) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY post_id (post_id), KEY fingerprint (fingerprint) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建侵权记录表 $infringement_table = $wpdb->prefix . 'scp_infringements'; $sql = "CREATE TABLE IF NOT EXISTS $infringement_table ( id bigint(20) NOT NULL AUTO_INCREMENT, post_id bigint(20) NOT NULL, infringing_url varchar(500) NOT NULL, similarity_score float NOT NULL, detected_at datetime DEFAULT CURRENT_TIMESTAMP, status varchar(20) DEFAULT 'pending', action_taken varchar(50) DEFAULT NULL, PRIMARY KEY (id), KEY post_id (post_id), KEY status (status) ) $charset_collate;"; dbDelta($sql); } 第三章:实现内容指纹生成系统 3.1 内容指纹算法设计 编辑includes/class-content-fingerprint.php文件: <?php class SCP_Content_Fingerprint { private $hash_algo = 'sha256'; public function __construct() { add_action('save_post', array($this, 'generate_post_fingerprint'), 10, 3); add_action('scp_daily_monitor', array($this, 'update_all_fingerprints')); } /** * 为文章生成内容指纹 */ public function generate_post_fingerprint($post_id, $post, $update) { // 跳过自动保存和修订 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (wp_is_post_revision($post_id)) { return; } // 只处理已发布的文章 if ($post->post_status !== 'publish') { return; } // 获取文章内容 $content = $this->extract_content_for_fingerprint($post); // 生成指纹 $fingerprint = $this->calculate_fingerprint($content); // 保存到数据库 $this->save_fingerprint($post_id, $fingerprint); return $fingerprint; } /** * 提取用于生成指纹的内容 */ private function extract_content_for_fingerprint($post) { $content = ''; // 添加标题 $content .= strip_tags($post->post_title) . "n"; // 添加正文内容(去除HTML标签) $post_content = strip_tags($post->post_content); $post_content = preg_replace('/s+/', ' ', $post_content); $content .= $post_content . "n"; // 添加前300个字符作为特征(即使内容被部分修改也能检测) $content .= substr($post_content, 0, 300); // 添加文章摘要(如果有) if (!empty($post->post_excerpt)) { $content .= strip_tags($post->post_excerpt) . "n"; } return $content; } /** * 计算内容指纹 */ private function calculate_fingerprint($content) { // 标准化内容:转换为小写,移除多余空格和标点 $normalized = $this->normalize_content($content); // 使用simhash算法生成指纹(更适合文本相似度检测) $fingerprint = $this->simhash($normalized); return $fingerprint; } /** * 内容标准化处理 */ private function normalize_content($content) { // 转换为小写 $content = mb_strtolower($content, 'UTF-8'); // 移除所有标点符号和特殊字符 $content = preg_replace('/[^p{L}p{N}s]/u', ' ', $content); // 将多个空格合并为一个 $content = preg_replace('/s+/', ' ', $content); // 移除停用词(常见但无实际意义的词) $stop_words = $this->get_stop_words(); $words = explode(' ', $content); $words = array_diff($words, $stop_words); return implode(' ', $words); } /** * 获取停用词列表 */ private function get_stop_words() { // 中文常见停用词 $chinese_stop_words = array( '的', '了', '在', '是', '我', '有', '和', '就', '不', '人', '都', '一', '一个', '上', '也', '很', '到', '说', '要', '去', '你', '会', '着', '没有', '看', '好', '自己', '这' ); // 英文常见停用词 $english_stop_words = array( 'a', 'an', 'the', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for', 'of', 'with', 'by', 'is', 'are', 'was', 'were', 'be', 'been', 'being' ); return array_merge($chinese_stop_words, $english_stop_words); } /** * SimHash算法实现 */ private function simhash($content) { $features = array(); $words = explode(' ', $content); // 统计词频 $word_counts = array_count_values($words); // 初始化特征向量(64位) $vector = array_fill(0, 64, 0); foreach ($word_counts as $word => $count) { // 为每个词生成hash $hash = hexdec(substr(md5($word), 0, 16)); for ($i = 0; $i < 64; $i++) { // 检查hash的每一位 $bit = ($hash >> $i) & 1; if ($bit == 1) { $vector[$i] += $count; } else { $vector[$i] -= $count; } } } // 生成最终的fingerprint $fingerprint = 0; for ($i = 0; $i < 64; $i++) { if ($vector[$i] > 0) { $fingerprint |= (1 << $i); } } return dechex($fingerprint); } /** * 保存指纹到数据库 */ private function save_fingerprint($post_id, $fingerprint) { global $wpdb; $table_name = $wpdb->prefix . 'scp_content_fingerprints'; // 检查是否已存在记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE post_id = %d", $post_id )); if ($existing) { // 更新现有记录 $wpdb->update( $table_name, array('fingerprint' => $fingerprint), array('post_id' => $post_id), array('%s'), array('%d') ); } else { // 插入新记录 $wpdb->insert( $table_name, array( 'post_id' => $post_id, 'fingerprint' => $fingerprint ), array('%d', '%s') ); } } /** * 更新所有文章的指纹 */ public function update_all_fingerprints() { $args = array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => -1, ); $posts = get_posts($args); foreach ($posts as $post) { $this->generate_post_fingerprint($post->ID, $post, true); } } /** * 计算两个指纹的相似度 */ public function calculate_similarity($fingerprint1, $fingerprint2) { // 将十六进制转换为二进制 $bin1 = hex2bin(str_pad($fingerprint1, 16, '0', STR_PAD_LEFT)); $bin2 = hex2bin(str_pad($fingerprint2, 16, '0', STR_PAD_LEFT)); // 计算海明距离 $hamming_distance = 0; for ($i = 0; $i < strlen($bin1); $i++) { $xor = ord($bin1[$i]) ^ ord($bin2[$i]); while ($xor) { $hamming_distance += $xor & 1; $xor >>= 1; } } // 转换为相似度百分比 $max_distance = strlen($bin1) * 8; $similarity = 100 * (1 - $hamming_distance / $max_distance); return round($similarity, 2); } } 第四章:构建智能内容监控系统 4.1 监控系统设计 编辑includes/class-content-monitor.php文件: <?php class SCP_Content_Monitor { private $search_engines = array(); private $api_keys = array(); public function __construct() { $settings = get_option('scp_settings', array()); $this->search_engines = isset($settings['search_engines']) ? $settings['search_engines'] : array('google'); // 设置API密钥(实际使用时需要从设置中获取) $this->api_keys = array( 'google' => defined('SCP_GOOGLE_API_KEY') ? SCP_GOOGLE_API_KEY : '', _API_KEY') ? SCP_BING_API_KEY : '' ); add_action('scp_daily_monitor', array($this, 'run_daily_monitoring')); add_action('scp_manual_monitor', array($this, 'monitor_specific_post')); } /** * 执行每日监控任务 */ public function run_daily_monitoring() { // 获取最近30天内发布的文章 $args = array( 'post_type' => 'post', 'post_status' => 'publish', 'date_query' => array( array( 'after' => '30 days ago', ) ), 'posts_per_page' => 20, // 每天监控20篇文章,避免API限制 ); $posts = get_posts($args); foreach ($posts as $post) { $this->monitor_post($post); // 避免请求过于频繁 sleep(2); } // 记录监控日志 $this->log_monitoring_result(count($posts)); } /** * 监控特定文章 */ public function monitor_specific_post($post_id) { $post = get_post($post_id); if ($post && $post->post_status === 'publish') { return $this->monitor_post($post); } return false; } /** * 监控单篇文章 */ private function monitor_post($post) { $results = array(); // 生成搜索查询 $search_queries = $this->generate_search_queries($post); foreach ($this->search_engines as $engine) { if (method_exists($this, "search_with_{$engine}")) { foreach ($search_queries as $query) { $engine_results = call_user_func( array($this, "search_with_{$engine}"), $query, $post->ID ); if (!empty($engine_results)) { $results = array_merge($results, $engine_results); } } } } // 分析结果并保存侵权记录 if (!empty($results)) { $this->analyze_and_save_results($results, $post->ID); } return $results; } /** * 生成搜索查询 */ private function generate_search_queries($post) { $queries = array(); // 提取文章标题中的关键词 $title = strip_tags($post->post_title); $title_words = explode(' ', preg_replace('/[^p{L}p{N}s]/u', ' ', $title)); $title_words = array_filter($title_words, function($word) { return mb_strlen($word, 'UTF-8') > 1; }); // 使用标题中的前5个词作为查询 if (count($title_words) > 0) { $title_query = implode(' ', array_slice($title_words, 0, 5)); $queries[] = $title_query; } // 提取文章中的独特短语(连续3-5个词) $content = strip_tags($post->post_content); $content = preg_replace('/s+/', ' ', $content); $words = explode(' ', $content); // 生成独特短语 $phrases = array(); for ($i = 0; $i < count($words) - 3; $i++) { $phrase = implode(' ', array_slice($words, $i, 4)); if (strlen($phrase) > 15 && strlen($phrase) < 50) { $phrases[] = $phrase; } } // 选择最独特的3个短语 $unique_phrases = $this->select_unique_phrases($phrases); $queries = array_merge($queries, array_slice($unique_phrases, 0, 3)); // 添加包含网站名称的查询 $site_name = get_bloginfo('name'); if (!empty($site_name) && !empty($title_query)) { $queries[] = ""{$title_query}" "{$site_name}""; } return array_unique($queries); } /** * 选择独特短语 */ private function select_unique_phrases($phrases) { // 简单的独特性评分:基于词频和长度 $scored_phrases = array(); foreach ($phrases as $phrase) { $score = 0; // 长度适中得分高 $length = strlen($phrase); if ($length > 20 && $length < 40) { $score += 3; } elseif ($length >= 40 && $length < 60) { $score += 2; } // 包含较少常见词得分高 $common_words = array('的', '了', '在', '是', '和', '就', '不', '人', '都'); $phrase_words = explode(' ', $phrase); $common_count = count(array_intersect($phrase_words, $common_words)); $score += max(0, 5 - $common_count); $scored_phrases[$phrase] = $score; } arsort($scored_phrases); return array_keys($scored_phrases); } /** * 使用Google搜索 */ private function search_with_google($query, $post_id) { $api_key = $this->api_keys['google']; $search_engine_id = defined('SCP_GOOGLE_CSE_ID') ? SCP_GOOGLE_CSE_ID : ''; if (empty($api_key) || empty($search_engine_id)) { // 如果没有API密钥,使用简单的HTTP请求模拟搜索 return $this->search_with_google_public($query, $post_id); } $url = 'https://www.googleapis.com/customsearch/v1'; $params = array( 'key' => $api_key, 'cx' => $search_engine_id, 'q' => $query, 'num' => 10, 'fields' => 'items(link,title,snippet)' ); $response = wp_remote_get(add_query_arg($params, $url)); if (is_wp_error($response)) { return array(); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); $results = array(); if (isset($data['items'])) { foreach ($data['items'] as $item) { // 排除自己的网站 if (strpos($item['link'], home_url()) !== false) { continue; } $results[] = array( 'url' => $item['link'], 'title' => $item['title'], 'snippet' => $item['snippet'], 'engine' => 'google', 'query' => $query ); } } return $results; } /** * 使用公开的Google搜索(无API) */ private function search_with_google_public($query, $post_id) { // 注意:这种方法可能违反Google的服务条款,仅作为示例 // 实际使用时建议使用官方API $url = 'https://www.google.com/search'; $params = array( 'q' => $query, 'num' => 10 ); $response = wp_remote_get(add_query_arg($params, $url), array( 'headers' => array( 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ) )); if (is_wp_error($response)) { return array(); } $body = wp_remote_retrieve_body($response); // 简单的HTML解析提取结果 $results = array(); if (preg_match_all('/<div class="g">(.*?)</div></div></div>/s', $body, $matches)) { foreach ($matches[1] as $match) { if (preg_match('/<a[^>]+href="([^"]+)"[^>]*>(.*?)</a>/', $match, $link_match)) { $url = $link_match[1]; $title = strip_tags($link_match[2]); // 排除自己的网站 if (strpos($url, home_url()) !== false) { continue; } // 提取摘要 $snippet = ''; if (preg_match('/<div[^>]*class="[^"]*VwiC3b[^"]*"[^>]*>(.*?)</div>/', $match, $snippet_match)) { $snippet = strip_tags($snippet_match[1]); } $results[] = array( 'url' => $url, 'title' => $title, 'snippet' => $snippet, 'engine' => 'google_public', 'query' => $query ); } } } return $results; } /** * 使用Bing搜索 */ private function search_with_bing($query, $post_id) { $api_key = $this->api_keys['bing']; if (empty($api_key)) { return array(); } $url = 'https://api.bing.microsoft.com/v7.0/search'; $params = array( 'q' => $query, 'count' => 10, 'responseFilter' => 'Webpages' ); $response = wp_remote_get(add_query_arg($params, $url), array( 'headers' => array( 'Ocp-Apim-Subscription-Key' => $api_key ) )); if (is_wp_error($response)) { return array(); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); $results = array(); if (isset($data['webPages']['value'])) { foreach ($data['webPages']['value'] as $item) { // 排除自己的网站 if (strpos($item['url'], home_url()) !== false) { continue; } $results[] = array( 'url' => $item['url'], 'title' => $item['name'], 'snippet' => $item['snippet'], 'engine' => 'bing', 'query' => $query ); } } return $results; } /** * 分析并保存结果 */ private function analyze_and_save_results($results, $post_id) { global $wpdb; $table_name = $wpdb->prefix . 'scp_infringements'; $post_content = get_post_field('post_content', $post_id); $post_content_plain = strip_tags($post_content); foreach ($results as $result) { // 获取疑似侵权页面的内容 $infringing_content = $this->fetch_page_content($result['url']); if (empty($infringing_content)) { continue; } // 计算相似度 $similarity = $this->calculate_content_similarity( $post_content_plain, $infringing_content ); // 如果相似度超过阈值,保存记录 $threshold = 70; // 70%相似度阈值 if ($similarity >= $threshold) { // 检查是否已存在记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE post_id = %d AND infringing_url = %s", $post_id, $result['url'] )); if (!$existing) { $wpdb->insert( $table_name, array( 'post_id' => $post_id, 'infringing_url' => $result['url'], 'similarity_score' => $similarity, 'detected_at' => current_time('mysql'), 'status' => 'pending' ), array('%d', '%s', '%f', '%s', '%s') ); // 发送通知 $this->send_infringement_notification($post_id, $result['url'], $similarity); } } } } /** * 获取页面内容 */ private function fetch_page_content($url) { $response = wp_remote_get($url, array( 'timeout' => 10, 'headers' => array( 'User-Agent' => 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36' ) )); if (is_wp_error($response)) { return ''; } $body = wp_remote_retrieve_body($response); // 提取正文内容 $content = ''; // 尝试多种方法提取正文 if (preg_match('/<body[^>]*>(.*?)</body>/s', $body, $matches)) { $body_content = $matches[1]; // 移除脚本和样式 $body_content = preg_replace('/<script[^>]*>.*?</script>/s', '', $body_content); $body_content = preg_replace('/<style[^>]*>.*?</style>/s', '', $body_content); // 提取文本 $content = strip_tags($body_content); $content = preg_replace('/s+/', ' ', $content); $content = trim($content); } return $content; } /** * 计算内容相似度 */ private function calculate_content_similarity($content1, $content2) { // 使用文本相似度算法 $similarity = 0; // 方法1:基于共享词的比例 $words1 = $this->extract_significant_words($content1); $words2 = $this->extract_significant_words($content2); $common_words = array_intersect($words1, $words2); $total_unique_words = count(array_unique(array_merge($words1, $words2))); if ($total_unique_words > 0) { $similarity = (count($common_words) / $total_unique_words) * 100; } // 方法2:检查长字符串匹配 $long_strings1 = $this->extract_long_strings($content1); $long_strings2 = $this->extract_long_strings($content2); $string_similarity = 0; foreach ($long_strings1 as $str1) { foreach ($long_strings2 as $str2) { similar_text($str1, $str2, $percent); if ($percent > 80) { // 80%相似的长字符串 $string_similarity = max($string_similarity, $percent); } } } // 取两种方法的较高值 $similarity = max($similarity, $string_similarity); return min(100, round($similarity, 2)); } /** * 提取重要词汇 */ private function extract_significant_words($content, $min_length = 2) { $content = mb_strtolower($content, 'UTF-8'); $content = preg_replace('/[^p{L}p{N}s]/u', ' ', $content); $words = explode(' ', $content); // 过滤短词和停用词 $stop_words = $this->get_stop_words(); $words = array_filter($words, function($word) use ($min_length, $stop_words) { return mb_strlen($word, 'UTF-8') >= $min_length && !in_array($word, $stop_words); }); return array_values($words); } /** * 提取长字符串 */ private function extract_long_strings($content, $min_length = 20) { $sentences = preg_split('/[。.!??]/u', $content); $long_strings = array(); foreach ($sentences as $sentence) { $sentence = trim($sentence); if (mb_strlen($sentence, 'UTF-8') >= $min_length) { $long_strings[] = $sentence; } } return $long_strings; } /** * 发送侵权通知 */ private function send_infringement_notification($post_id, $infringing_url, $similarity) { $admin_email = get_option('admin_email'); $post_title = get_the_title($post_id); $post_url = get_permalink($post_id); $subject = sprintf( __('[内容侵权警报] 文章 "%s" 可能被抄袭', 'smart-content-protector'), $post_title ); $message = sprintf( __('检测到可能的内容侵权: 您的文章:%s文章链接:%s 疑似侵权页面:%s相似度:%.2f%% 请登录WordPress后台查看详细信息并采取相应措施。 Smart Content Protector%s', 'smart-content-protector'), $post_title, $post_url, $infringing_url, $similarity, home_url() ); wp_mail($admin_email, $subject, $message); } /** * 记录监控日志 */ private function log_monitoring_result($posts_monitored) { $log_entry = sprintf( '[%s] 监控完成,检查了 %d 篇文章', current_time('mysql'), $posts_monitored ); // 保存到选项

发表评论

开发指南,打造网站内嵌的在线抽奖与幸运大转盘互动营销组件

开发指南:打造网站内嵌的在线抽奖与幸运大转盘互动营销组件 摘要 在当今数字营销时代,互动式营销工具已成为提升用户参与度和转化率的关键手段。本文将详细介绍如何通过WordPress程序的代码二次开发,实现一个功能完整的在线抽奖与幸运大转盘互动营销组件。我们将从需求分析、技术选型、代码实现到部署测试,全面解析开发流程,帮助开发者快速构建这一常用互联网小工具功能。 目录 引言:互动营销组件的重要性 需求分析与功能规划 技术架构与开发环境搭建 数据库设计与数据模型 前端实现:转盘界面与交互设计 后端开发:抽奖逻辑与数据管理 WordPress集成与插件化开发 安全性与性能优化 测试与部署策略 维护与扩展建议 结语:未来发展方向 1. 引言:互动营销组件的重要性 在竞争激烈的互联网环境中,网站需要不断创新以吸引和留住用户。互动营销组件,如在线抽奖和幸运大转盘,已成为提升用户参与度、增加页面停留时间、促进转化的有效工具。这些组件不仅能够增强用户体验,还能为网站运营者提供宝贵的用户行为数据。 WordPress作为全球最流行的内容管理系统,拥有庞大的用户基础和丰富的扩展生态。通过二次开发,我们可以将定制化的互动营销组件无缝集成到WordPress网站中,满足特定业务需求,同时保持与现有主题和插件的兼容性。 2. 需求分析与功能规划 2.1 核心功能需求 转盘可视化界面:美观、可自定义的转盘界面,支持多种奖品设置 抽奖逻辑系统:可配置的中奖概率、奖品库存管理 用户参与管理:用户身份验证、参与次数限制、中奖记录 数据统计与分析:参与数据、中奖率、用户行为分析 后台管理界面:奖品管理、概率设置、数据监控 2.2 非功能性需求 响应式设计:适配各种设备屏幕尺寸 性能优化:快速加载和流畅的动画效果 安全性:防止作弊和恶意攻击 可扩展性:便于未来功能扩展和定制 2.3 用户角色与权限 网站管理员:完全控制抽奖设置和数据管理 编辑人员:管理奖品内容和基本设置 普通用户:参与抽奖活动,查看中奖记录 未注册访客:有限次数的参与机会 3. 技术架构与开发环境搭建 3.1 技术选型 前端技术:HTML5、CSS3、JavaScript (ES6+)、Canvas/SVG(转盘绘制) 动画库:GSAP或CSS3动画实现流畅转盘效果 后端技术:PHP 7.4+(WordPress核心语言) 数据库:MySQL(WordPress默认数据库) WordPress开发:遵循WordPress插件开发规范 3.2 开发环境配置 本地开发环境:安装XAMPP/MAMP或Local by Flywheel WordPress安装:最新稳定版WordPress 代码编辑器:VS Code或PHPStorm 版本控制:Git 调试工具:浏览器开发者工具、Xdebug 3.3 项目结构规划 wp-content/plugins/lucky-wheel-roulette/ ├── assets/ │ ├── css/ │ ├── js/ │ └── images/ ├── includes/ │ ├── class-database.php │ ├── class-wheel-ui.php │ ├── class-lottery-logic.php │ └── class-admin-panel.php ├── templates/ │ ├── frontend-wheel.php │ └── admin-settings.php ├── languages/ ├── lucky-wheel-roulette.php (主插件文件) └── uninstall.php 4. 数据库设计与数据模型 4.1 自定义数据表设计 除了使用WordPress默认的数据表,我们需要创建几个自定义表来存储抽奖相关数据: -- 奖品表 CREATE TABLE wp_lwr_prizes ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT, image_url VARCHAR(500), probability DECIMAL(5,4) DEFAULT 0.1, stock INT DEFAULT 0, daily_limit INT DEFAULT 0, type ENUM('physical', 'virtual', 'coupon') DEFAULT 'virtual', value VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); -- 抽奖记录表 CREATE TABLE wp_lwr_records ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) UNSIGNED, prize_id INT, ip_address VARCHAR(45), user_agent TEXT, result ENUM('win', 'lose') DEFAULT 'lose', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE SET NULL, FOREIGN KEY (prize_id) REFERENCES wp_lwr_prizes(id) ON DELETE SET NULL ); -- 用户抽奖次数表 CREATE TABLE wp_lwr_user_attempts ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) UNSIGNED, attempts_today INT DEFAULT 0, total_attempts INT DEFAULT 0, last_attempt_date DATE, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE ); 4.2 数据模型类实现 // includes/class-database.php class LWR_Database { private static $instance = null; private $wpdb; private $table_prizes; private $table_records; private $table_user_attempts; private function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->table_prizes = $wpdb->prefix . 'lwr_prizes'; $this->table_records = $wpdb->prefix . 'lwr_records'; $this->table_user_attempts = $wpdb->prefix . 'lwr_user_attempts'; } public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } // 创建数据表 public function create_tables() { require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); $charset_collate = $this->wpdb->get_charset_collate(); // 创建奖品表 $sql_prizes = "CREATE TABLE IF NOT EXISTS {$this->table_prizes} ( id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255) NOT NULL, description TEXT, image_url VARCHAR(500), probability DECIMAL(5,4) DEFAULT 0.1, stock INT DEFAULT 0, daily_limit INT DEFAULT 0, type ENUM('physical', 'virtual', 'coupon') DEFAULT 'virtual', value VARCHAR(255), created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) {$charset_collate};"; dbDelta($sql_prizes); // 创建其他表的SQL语句... // 插入默认奖品数据 $this->insert_default_prizes(); } // 插入默认奖品 private function insert_default_prizes() { $default_prizes = array( array('name' => '一等奖', 'probability' => 0.01, 'type' => 'virtual'), array('name' => '二等奖', 'probability' => 0.05, 'type' => 'virtual'), array('name' => '三等奖', 'probability' => 0.1, 'type' => 'virtual'), array('name' => '谢谢参与', 'probability' => 0.84, 'type' => 'virtual') ); foreach ($default_prizes as $prize) { $this->wpdb->insert( $this->table_prizes, $prize ); } } // 获取所有奖品 public function get_prizes() { return $this->wpdb->get_results( "SELECT * FROM {$this->table_prizes} ORDER BY probability ASC" ); } // 保存抽奖记录 public function save_record($user_id, $prize_id, $ip, $user_agent, $result) { return $this->wpdb->insert( $this->table_records, array( 'user_id' => $user_id, 'prize_id' => $prize_id, 'ip_address' => $ip, 'user_agent' => $user_agent, 'result' => $result ) ); } // 其他数据库操作方法... } 5. 前端实现:转盘界面与交互设计 5.1 HTML结构 <!-- templates/frontend-wheel.php --> <div id="lucky-wheel-container" class="lwr-container"> <div class="lwr-wheel-wrapper"> <canvas id="lucky-wheel-canvas" width="500" height="500"></canvas> <div class="lwr-wheel-pointer"> <div class="pointer-arrow"></div> </div> <button id="spin-button" class="lwr-spin-button">开始抽奖</button> </div> <div class="lwr-sidebar"> <div class="lwr-prize-list"> <h3>奖品列表</h3> <ul id="prize-list"> <!-- 通过JavaScript动态加载 --> </ul> </div> <div class="lwr-user-info"> <h3>我的信息</h3> <p>今日剩余次数: <span id="remaining-attempts">3</span></p> <p>总参与次数: <span id="total-attempts">0</span></p> </div> <div class="lwr-winning-history"> <h3>中奖记录</h3> <ul id="winning-history"> <!-- 通过JavaScript动态加载 --> </ul> </div> </div> <!-- 中奖结果弹窗 --> <div id="result-modal" class="lwr-modal"> <div class="lwr-modal-content"> <span class="lwr-close-modal">&times;</span> <h2 id="result-title">恭喜中奖!</h2> <div id="result-details"> <!-- 中奖详情 --> </div> <button id="claim-prize" class="lwr-claim-button">领取奖品</button> </div> </div> </div> 5.2 CSS样式设计 /* assets/css/lucky-wheel.css */ .lwr-container { display: flex; flex-wrap: wrap; max-width: 1200px; margin: 0 auto; padding: 20px; font-family: 'Arial', sans-serif; } .lwr-wheel-wrapper { position: relative; flex: 1; min-width: 300px; max-width: 500px; margin: 0 auto 30px; } #lucky-wheel-canvas { width: 100%; height: auto; border-radius: 50%; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2); transition: transform 0.1s; } .lwr-wheel-pointer { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 60px; height: 60px; z-index: 10; } .pointer-arrow { width: 0; height: 0; border-left: 30px solid transparent; border-right: 30px solid transparent; border-top: 50px solid #e74c3c; position: absolute; top: -40px; left: 0; } .lwr-spin-button { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 100px; height: 100px; border-radius: 50%; background: linear-gradient(145deg, #e74c3c, #c0392b); color: white; border: none; font-size: 18px; font-weight: bold; cursor: pointer; z-index: 5; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); transition: all 0.3s; } .lwr-spin-button:hover { transform: translate(-50%, -50%) scale(1.05); box-shadow: 0 8px 20px rgba(0, 0, 0, 0.4); } .lwr-spin-button:disabled { background: #95a5a6; cursor: not-allowed; transform: translate(-50%, -50%) scale(1); } .lwr-sidebar { flex: 1; min-width: 300px; padding: 20px; background: #f8f9fa; border-radius: 10px; margin-left: 20px; } .lwr-prize-list, .lwr-user-info, .lwr-winning-history { margin-bottom: 30px; padding: 15px; background: white; border-radius: 8px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.08); } /* 响应式设计 */ @media (max-width: 768px) { .lwr-container { flex-direction: column; } .lwr-sidebar { margin-left: 0; margin-top: 20px; } .lwr-wheel-wrapper { max-width: 100%; } } 5.3 JavaScript交互逻辑 // assets/js/lucky-wheel.js class LuckyWheel { constructor() { this.canvas = document.getElementById('lucky-wheel-canvas'); this.ctx = this.canvas.getContext('2d'); this.spinButton = document.getElementById('spin-button'); this.resultModal = document.getElementById('result-modal'); this.remainingAttempts = document.getElementById('remaining-attempts'); this.totalAttempts = document.getElementById('total-attempts'); this.prizes = []; this.isSpinning = false; this.currentRotation = 0; this.wheelRadius = Math.min(this.canvas.width, this.canvas.height) / 2; this.centerX = this.canvas.width / 2; this.centerY = this.canvas.height / 2; this.init(); } async init() { // 加载奖品数据 await this.loadPrizes(); // 绘制转盘 this.drawWheel(); // 绑定事件 this.bindEvents(); // 加载用户数据 this.loadUserData(); } async loadPrizes() { try { const response = await fetch('/wp-json/lwr/v1/prizes'); this.prizes = await response.json(); this.updatePrizeList(); } catch (error) { console.error('加载奖品数据失败:', error); } } drawWheel() { const ctx = this.ctx; const totalPrizes = this.prizes.length; const angleStep = (2 * Math.PI) / totalPrizes; // 清空画布 ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 绘制转盘扇形 for (let i = 0; i < totalPrizes; i++) { const startAngle = i * angleStep + this.currentRotation; const endAngle = (i + 1) * angleStep + this.currentRotation; // 交替颜色 const color = i % 2 === 0 ? '#3498db' : '#2ecc71'; // 绘制扇形 ctx.beginPath(); ctx.moveTo(this.centerX, this.centerY); ctx.arc(this.centerX, this.centerY, this.wheelRadius, startAngle, endAngle); ctx.closePath(); ctx.fillStyle = color; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 2; ctx.stroke(); // 绘制奖品文字 ctx.save(); ctx.translate(this.centerX, this.centerY); ctx.rotate(startAngle + angleStep / 2); ctx.textAlign = 'right'; ctx.fillStyle = '#fff'; ctx.font = 'bold 16px Arial'; ctx.fillText(this.prizes[i].name, this.wheelRadius - 20, 10); ctx.restore(); } // 绘制中心圆 ctx.beginPath(); ctx.arc(this.centerX, this.centerY, 30, 0, 2 * Math.PI); ctx.fillStyle = '#e74c3c'; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 4; ctx.stroke(); } bindEvents() { this.spinButton.addEventListener('click', () => this.spinWheel()); // 关闭弹窗 document.querySelector('.lwr-close-modal').addEventListener('click', () => { this.resultModal.style.display = 'none'; }); // 领取奖品按钮 document.getElementById('claim-prize').addEventListener('click', () => { }); } async spinWheel() { if (this.isSpinning) return; // 检查抽奖次数 const remaining = parseInt(this.remainingAttempts.textContent); if (remaining <= 0) { alert('今日抽奖次数已用完!'); return; } this.isSpinning = true; this.spinButton.disabled = true; // 发送抽奖请求 try { const response = await fetch('/wp-json/lwr/v1/spin', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': lwr_ajax.nonce }, body: JSON.stringify({}) }); const result = await response.json(); if (result.success) { // 执行转盘动画 await this.animateWheel(result.prize_index); // 显示中奖结果 this.showResult(result.prize); // 更新用户数据 this.updateUserData(result.user_data); } else { alert(result.message || '抽奖失败,请重试!'); this.isSpinning = false; this.spinButton.disabled = false; } } catch (error) { console.error('抽奖请求失败:', error); this.isSpinning = false; this.spinButton.disabled = false; } } animateWheel(prizeIndex) { return new Promise((resolve) => { const totalPrizes = this.prizes.length; const targetRotation = this.currentRotation + 5 * Math.PI + (prizeIndex * (2 * Math.PI / totalPrizes)); const duration = 5000; // 动画持续时间(毫秒) const startTime = Date.now(); const startRotation = this.currentRotation; const animate = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / duration, 1); // 缓动函数,使动画先快后慢 const easeOut = 1 - Math.pow(1 - progress, 3); this.currentRotation = startRotation + (targetRotation - startRotation) * easeOut; this.drawWheel(); if (progress < 1) { requestAnimationFrame(animate); } else { this.isSpinning = false; this.spinButton.disabled = false; resolve(); } }; animate(); }); } showResult(prize) { const resultTitle = document.getElementById('result-title'); const resultDetails = document.getElementById('result-details'); if (prize.name === '谢谢参与') { resultTitle.textContent = '很遗憾,未中奖'; resultDetails.innerHTML = ` <p>您抽中了: <strong>${prize.name}</strong></p> <p>不要灰心,明天再来试试!</p> `; } else { resultTitle.textContent = '恭喜您中奖了!'; resultDetails.innerHTML = ` <div class="prize-result"> <h3>${prize.name}</h3> <p>${prize.description || ''}</p> <p class="prize-value">奖品价值: ${prize.value || '无'}</p> </div> `; } this.resultModal.style.display = 'block'; } updateUserData(userData) { if (userData.remaining_attempts !== undefined) { this.remainingAttempts.textContent = userData.remaining_attempts; } if (userData.total_attempts !== undefined) { this.totalAttempts.textContent = userData.total_attempts; } } updatePrizeList() { const prizeList = document.getElementById('prize-list'); prizeList.innerHTML = ''; this.prizes.forEach(prize => { const li = document.createElement('li'); li.className = 'prize-item'; li.innerHTML = ` <span class="prize-name">${prize.name}</span> <span class="prize-probability">${(prize.probability * 100).toFixed(2)}%</span> ${prize.stock > 0 ? `<span class="prize-stock">剩余: ${prize.stock}</span>` : ''} `; prizeList.appendChild(li); }); } async loadUserData() { try { const response = await fetch('/wp-json/lwr/v1/user-data'); const data = await response.json(); if (data.success) { this.updateUserData(data.data); } } catch (error) { console.error('加载用户数据失败:', error); } } async claimPrize() { // 领取奖品的逻辑 alert('奖品领取功能开发中...'); this.resultModal.style.display = 'none'; } } // 页面加载完成后初始化document.addEventListener('DOMContentLoaded', () => { new LuckyWheel(); }); ## 6. 后端开发:抽奖逻辑与数据管理 ### 6.1 抽奖逻辑实现 // includes/class-lottery-logic.phpclass LWR_Lottery_Logic { private $db; public function __construct() { $this->db = LWR_Database::get_instance(); } /** * 执行抽奖逻辑 */ public function spin_wheel($user_id = 0) { // 检查用户抽奖资格 $can_spin = $this->check_user_eligibility($user_id); if (!$can_spin['success']) { return $can_spin; } // 获取可用奖品 $available_prizes = $this->get_available_prizes(); if (empty($available_prizes)) { return array( 'success' => false, 'message' => '暂无可用奖品' ); } // 根据概率计算中奖奖品 $winning_prize = $this->calculate_winning_prize($available_prizes); // 记录抽奖结果 $record_id = $this->record_spin_result($user_id, $winning_prize); if (!$record_id) { return array( 'success' => false, 'message' => '记录抽奖结果失败' ); } // 更新用户抽奖次数 $this->update_user_attempts($user_id); // 更新奖品库存 if ($winning_prize->id && $winning_prize->stock > 0) { $this->update_prize_stock($winning_prize->id); } // 准备返回数据 $prize_index = array_search($winning_prize, array_values($available_prizes)); return array( 'success' => true, 'prize' => $winning_prize, 'prize_index' => $prize_index, 'user_data' => $this->get_user_spin_data($user_id) ); } /** * 检查用户抽奖资格 */ private function check_user_eligibility($user_id) { global $wpdb; // 检查IP限制(防止刷奖) $ip_address = $this->get_client_ip(); $today = date('Y-m-d'); // 同一IP今日抽奖次数限制 $ip_attempts = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}lwr_records WHERE ip_address = %s AND DATE(created_at) = %s", $ip_address, $today )); if ($ip_attempts >= 10) { // IP限制10次/天 return array( 'success' => false, 'message' => '今日抽奖次数已达上限' ); } // 注册用户检查 if ($user_id > 0) { $user_attempts = $this->get_user_attempts($user_id); // 每日次数限制 if ($user_attempts['attempts_today'] >= 5) { return array( 'success' => false, 'message' => '您今日的抽奖次数已用完' ); } // 总次数限制 if ($user_attempts['total_attempts'] >= 100) { return array( 'success' => false, 'message' => '您的总抽奖次数已用完' ); } } else { // 未登录用户限制 $guest_attempts = $this->get_guest_attempts($ip_address); if ($guest_attempts >= 3) { return array( 'success' => false, 'message' => '请登录后继续抽奖' ); } } return array('success' => true); } /** * 获取可用奖品列表 */ private function get_available_prizes() { global $wpdb; $today = date('Y-m-d'); $prizes = $wpdb->get_results( "SELECT p.*, (SELECT COUNT(*) FROM {$wpdb->prefix}lwr_records r WHERE r.prize_id = p.id AND DATE(r.created_at) = '{$today}') as today_wins FROM {$wpdb->prefix}lwr_prizes p WHERE p.stock > 0 OR p.stock = -1 ORDER BY p.probability ASC" ); // 过滤掉达到每日限制的奖品 $available_prizes = array(); foreach ($prizes as $prize) { if ($prize->daily_limit == 0 || $prize->today_wins < $prize->daily_limit) { $available_prizes[] = $prize; } } // 确保至少有一个"谢谢参与"选项 $has_thank_you = false; foreach ($available_prizes as $prize) { if (strpos($prize->name, '谢谢参与') !== false) { $has_thank_you = true; break; } } if (!$has_thank_you) { $thank_you_prize = (object) array( 'id' => 0, 'name' => '谢谢参与', 'description' => '感谢参与,下次好运!', 'probability' => 0.5, 'stock' => -1, 'type' => 'virtual' ); array_push($available_prizes, $thank_you_prize); } return $available_prizes; } /** * 根据概率计算中奖奖品 */ private function calculate_winning_prize($prizes) { // 计算总概率 $total_probability = 0; foreach ($prizes as $prize) { $total_probability += floatval($prize->probability); } // 生成随机数 $random = mt_rand() / mt_getrandmax() * $total_probability; // 根据概率选择奖品 $current_probability = 0; foreach ($prizes as $prize) { $current_probability += floatval($prize->probability); if ($random <= $current_probability) { return $prize; } } // 默认返回最后一个奖品(谢谢参与) return end($prizes); } /** * 获取客户端IP */ private function get_client_ip() { $ip_keys = array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'); foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } // 其他辅助方法... } ### 6.2 REST API端点注册 // 在主插件文件中添加add_action('rest_api_init', function() { // 获取奖品列表 register_rest_route('lwr/v1', '/prizes', array( 'methods' => 'GET', 'callback' => 'lwr_api_get_prizes', 'permission_callback' => '__return_true' )); // 执行抽奖 register_rest_route('lwr/v1', '/spin', array( 'methods' => 'POST', 'callback' => 'lwr_api_spin_wheel', 'permission_callback' => function() { return is_user_logged_in() || $this->check_guest_permission(); } )); // 获取用户数据 register_rest_route('lwr/v1', '/user-data', array( 'methods' => 'GET', 'callback' => 'lwr_api_get_user_data', 'permission_callback' => '__return_true' )); }); function lwr_api_get_prizes() { $db = LWR_Database::get_instance(); $prizes = $db->get_prizes(); return new WP_REST_Response(array( 'success' => true, 'data' => $prizes ), 200); } function lwr_api_spin_wheel($request) { $user_id = get_current_user_id(); $logic = new LWR_Lottery_Logic(); $result = $logic->spin_wheel($user_id); return new WP_REST_Response($result, $result['success'] ? 200 : 400); } function lwr_api_get_user_data($request) { $user_id = get_current_user_id(); $logic = new LWR_Lottery_Logic(); $user_data = $logic->get_user_spin_data($user_id); return new WP_REST_Response(array( 'success' => true, 'data' => $user_data ), 200); } ## 7. WordPress集成与插件化开发 ### 7.1 主插件文件结构 <?php/** Plugin Name: 幸运大转盘抽奖系统 Plugin URI: https://yourwebsite.com/lucky-wheel Description: 为WordPress网站添加在线抽奖和幸运大转盘功能 Version: 1.0.0 Author: Your Name License: GPL v2 or later */ // 防止直接访问if (!defined('ABSPATH')) { exit; } // 定义插件常量define('LWR_VERSION', '1.0.0');define('LWR_PLUGIN_DIR', plugin_dir_path(__FILE__));define('LWR_PLUGIN_URL', plugin_dir_url(__FILE__)); // 自动加载类文件spl_autoload_register(function($class_name) { if (strpos($class_name, 'LWR_') === 0) { $file = LWR_PLUGIN_DIR . 'includes/' . 'class-' . strtolower(str_replace('_', '-', $class_name)) . '.php'; if (file_exists($file)) { require_once $file; } } }); // 初始化插件class Lucky_Wheel_Roulette { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 激活/停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化 add_action('plugins_loaded', array($this, 'init')); // 添加短代码 add_shortcode('lucky_wheel', array($this, 'shortcode_handler')); // 添加管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 加载脚本和样式 add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function activate() { // 创建数据库表 $db = LWR_Database::get_instance(); $db->create_tables(); // 设置默认选项 $default_options = array( 'daily_limit_logged_in' => 5, 'daily_limit_guest' => 3, 'enable_sound' => true, 'wheel_colors' => array('#3498db', '#2ecc71', '#e74c3c', '#f39c12'), 'require_email' => false ); add_option('lwr_settings', $default_options); // 创建必要的页面 $this->create_pages(); } public function deactivate() { // 清理临时数据 // 注意:不删除用户数据和设置 } public function init() { // 初始化组件 if (is_admin()) { new LWR_Admin_Panel(); } } public function shortcode_handler($atts) { $atts = shortcode_atts(array( 'width' => '100%', '

发表评论

WordPress集成教程,连接快递物流接口实现订单跟踪地图展示

WordPress集成教程:连接快递物流接口实现订单跟踪地图展示 引言:WordPress的无限扩展可能 在当今数字化商业环境中,一个功能完善的网站已不仅仅是信息展示的平台,更是企业与客户互动、提供服务的关键枢纽。WordPress作为全球最受欢迎的内容管理系统,其真正的强大之处在于可扩展性——通过代码二次开发,我们可以将各种互联网小工具集成到网站中,创造出独特而实用的功能。本教程将深入探讨如何在WordPress中集成快递物流接口,实现订单跟踪地图展示功能,同时展示WordPress代码二次开发实现常用互联网小工具的方法论。 对于电商网站而言,订单跟踪功能是提升客户体验的重要环节。传统的物流跟踪通常只提供文字状态的更新,而将物流信息与地图可视化结合,能够直观展示包裹的运输路径和当前位置,显著增强用户的信任感和参与度。这种功能的实现,正是WordPress扩展能力的绝佳例证。 第一章:准备工作与环境配置 1.1 开发环境搭建 在开始集成开发之前,我们需要确保拥有合适的开发环境。建议使用本地开发环境如XAMPP、MAMP或Local by Flywheel,这些工具能够快速搭建PHP、MySQL和Apache/Nginx环境。对于WordPress开发,确保你的环境满足以下要求:PHP 7.4或更高版本、MySQL 5.6或更高版本、以及适当的内存限制(建议256MB以上)。 除了基础环境,我们还需要一些开发工具:代码编辑器(如VS Code、PHPStorm)、Git版本控制系统、浏览器开发者工具,以及用于API测试的工具(如Postman或Insomnia)。这些工具将在开发过程中发挥重要作用。 1.2 WordPress开发基础 理解WordPress的开发模式是成功集成的关键。WordPress采用主题和插件两种扩展方式:主题控制网站的外观和展示,而插件则添加特定功能。对于物流跟踪功能,我们通常选择创建专用插件,这样可以保持功能的独立性,便于维护和迁移。 WordPress插件的基本结构包括主插件文件、资产文件(CSS、JavaScript)、模板文件以及可能的语言文件。所有插件文件应放置在wp-content/plugins目录下的独立文件夹中。插件的主文件必须包含标准的插件头信息,这样WordPress才能识别并激活它。 1.3 选择快递物流API 市场上有多种快递物流API可供选择,如快递鸟、快递100、阿里云物流跟踪等。选择API时需要考虑以下因素:支持的快递公司数量、查询稳定性、更新频率、费用结构以及文档完整性。本教程将以快递鸟API为例,但方法和原理适用于大多数物流API。 注册选定的API服务后,我们将获得关键的访问凭证:通常是API Key和Secret Key。这些凭证需要安全地存储在WordPress中,建议使用WordPress的选项API或自定义数据库表进行存储,避免硬编码在插件文件中。 第二章:创建物流跟踪插件框架 2.1 插件基础结构 首先创建插件目录和主文件。在wp-content/plugins目录下创建新文件夹“shipment-tracker”,然后在该文件夹中创建主文件“shipment-tracker.php”: <?php /** * Plugin Name: Shipment Tracker * Plugin URI: https://yourwebsite.com/shipment-tracker * Description: 集成快递物流接口,实现订单跟踪地图展示功能 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: shipment-tracker */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('ST_VERSION', '1.0.0'); define('ST_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ST_PLUGIN_URL', plugin_dir_url(__FILE__)); define('ST_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 初始化插件 require_once ST_PLUGIN_DIR . 'includes/class-shipment-tracker.php'; function run_shipment_tracker() { $plugin = new Shipment_Tracker(); $plugin->run(); } run_shipment_tracker(); 2.2 创建核心类 在includes目录下创建核心类文件class-shipment-tracker.php: <?php class Shipment_Tracker { private $loader; public function __construct() { $this->load_dependencies(); $this->define_admin_hooks(); $this->define_public_hooks(); } private function load_dependencies() { require_once ST_PLUGIN_DIR . 'includes/class-api-handler.php'; require_once ST_PLUGIN_DIR . 'includes/class-database-handler.php'; require_once ST_PLUGIN_DIR . 'includes/class-shortcode-handler.php'; require_once ST_PLUGIN_DIR . 'includes/class-admin-settings.php'; } private function define_admin_hooks() { // 管理员相关钩子 $admin_settings = new Admin_Settings(); add_action('admin_menu', array($admin_settings, 'add_admin_menu')); add_action('admin_init', array($admin_settings, 'register_settings')); } private function define_public_hooks() { // 前端相关钩子 $shortcode_handler = new Shortcode_Handler(); add_shortcode('track_shipment', array($shortcode_handler, 'render_tracking_form')); add_shortcode('shipment_map', array($shortcode_handler, 'render_tracking_map')); // 添加前端脚本和样式 add_action('wp_enqueue_scripts', array($this, 'enqueue_public_scripts')); } public function enqueue_public_scripts() { wp_enqueue_style('shipment-tracker-style', ST_PLUGIN_URL . 'assets/css/public.css', array(), ST_VERSION); wp_enqueue_script('shipment-tracker-script', ST_PLUGIN_URL . 'assets/js/public.js', array('jquery'), ST_VERSION, true); // 本地化脚本,传递数据到JavaScript wp_localize_script('shipment-tracker-script', 'st_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('st_nonce') )); } public function run() { // 插件运行入口 } } 2.3 数据库设计 物流跟踪数据需要适当的数据库结构来存储。我们将创建自定义数据库表来存储物流查询结果,减少对API的重复调用: // 在class-database-handler.php中 class Database_Handler { public function create_tables() { global $wpdb; $table_name = $wpdb->prefix . 'shipment_tracking'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, order_id varchar(50) NOT NULL, tracking_number varchar(100) NOT NULL, carrier_code varchar(50) NOT NULL, status varchar(50) DEFAULT '', last_update datetime DEFAULT CURRENT_TIMESTAMP, raw_data longtext, PRIMARY KEY (id), INDEX tracking_idx (tracking_number, carrier_code) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } public function get_tracking_data($tracking_number, $carrier_code) { global $wpdb; $table_name = $wpdb->prefix . 'shipment_tracking'; return $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE tracking_number = %s AND carrier_code = %s", $tracking_number, $carrier_code )); } public function update_tracking_data($data) { global $wpdb; $table_name = $wpdb->prefix . 'shipment_tracking'; // 检查记录是否存在 $existing = $this->get_tracking_data($data['tracking_number'], $data['carrier_code']); if ($existing) { // 更新现有记录 return $wpdb->update( $table_name, array( 'status' => $data['status'], 'last_update' => current_time('mysql'), 'raw_data' => maybe_serialize($data['raw_data']) ), array('id' => $existing->id) ); } else { // 插入新记录 return $wpdb->insert( $table_name, array( 'order_id' => $data['order_id'], 'tracking_number' => $data['tracking_number'], 'carrier_code' => $data['carrier_code'], 'status' => $data['status'], 'last_update' => current_time('mysql'), 'raw_data' => maybe_serialize($data['raw_data']) ) ); } } } 第三章:集成快递物流API 3.1 API处理类设计 API处理类负责与物流API进行通信,处理请求和响应。我们创建一个独立的类来处理这些逻辑: <?php class API_Handler { private $api_key; private $api_secret; private $api_url = 'https://api.kdniao.com/Ebusiness/EbusinessOrderHandle.aspx'; public function __construct() { $options = get_option('st_settings'); $this->api_key = isset($options['api_key']) ? $options['api_key'] : ''; $this->api_secret = isset($options['api_secret']) ? $options['api_secret'] : ''; } public function track_shipment($tracking_number, $carrier_code) { // 首先检查本地数据库是否有缓存数据 $db_handler = new Database_Handler(); $cached_data = $db_handler->get_tracking_data($tracking_number, $carrier_code); // 如果缓存数据在1小时内更新过,直接返回 if ($cached_data && strtotime($cached_data->last_update) > time() - 3600) { return maybe_unserialize($cached_data->raw_data); } // 否则调用API $request_data = array( 'OrderCode' => '', 'ShipperCode' => $carrier_code, 'LogisticCode' => $tracking_number ); $data_json = json_encode($request_data); $datasign = $this->encrypt($data_json, $this->api_secret); $post_data = array( 'RequestData' => urlencode($data_json), 'EBusinessID' => $this->api_key, 'RequestType' => '1002', 'DataSign' => urlencode($datasign), 'DataType' => '2' ); $response = $this->send_request($this->api_url, $post_data); if ($response && isset($response['Success']) && $response['Success']) { // 保存到数据库 $db_handler->update_tracking_data(array( 'tracking_number' => $tracking_number, 'carrier_code' => $carrier_code, 'status' => $response['State'] ?? '', 'raw_data' => $response )); return $response; } return false; } private function send_request($url, $data) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, 1); curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($data)); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $response = curl_exec($ch); curl_close($ch); return json_decode($response, true); } private function encrypt($data, $app_key) { return urlencode(base64_encode(md5($data . $app_key))); } public function get_carriers() { // 获取支持的快递公司列表 $carriers = array( 'SF' => '顺丰速运', 'HTKY' => '百世快递', 'ZTO' => '中通快递', 'STO' => '申通快递', 'YTO' => '圆通速递', 'YD' => '韵达速递', 'YZPY' => '邮政快递包裹', 'EMS' => 'EMS', 'HHTT' => '天天快递', 'JD' => '京东物流' ); return apply_filters('st_available_carriers', $carriers); } } 3.2 错误处理与日志记录 健壮的API集成需要完善的错误处理机制。我们添加错误处理和日志记录功能: class Logger { public static function log($message, $level = 'info') { if (!defined('WP_DEBUG') || !WP_DEBUG) { return; } $log_dir = ST_PLUGIN_DIR . 'logs/'; // 确保日志目录存在 if (!file_exists($log_dir)) { wp_mkdir_p($log_dir); } $log_file = $log_dir . 'shipment-tracker-' . date('Y-m-d') . '.log'; $timestamp = current_time('mysql'); $log_message = "[$timestamp] [$level] $message" . PHP_EOL; file_put_contents($log_file, $log_message, FILE_APPEND); } public static function log_api_error($tracking_number, $carrier_code, $error) { $message = sprintf( 'API查询失败 - 运单号: %s, 快递公司: %s, 错误: %s', $tracking_number, $carrier_code, $error ); self::log($message, 'error'); } } // 在API_Handler类中添加错误处理 public function track_shipment($tracking_number, $carrier_code) { try { // ... 原有代码 ... } catch (Exception $e) { Logger::log_api_error($tracking_number, $carrier_code, $e->getMessage()); return false; } } 第四章:前端展示与地图集成 4.1 创建跟踪表单短代码 短代码是WordPress中在前端嵌入功能的便捷方式。我们创建跟踪表单短代码: class Shortcode_Handler { public function render_tracking_form($atts) { $atts = shortcode_atts(array( 'title' => '物流跟踪查询', 'button_text' => '查询' ), $atts, 'track_shipment'); ob_start(); ?> <div class="shipment-tracker-form"> <h3><?php echo esc_html($atts['title']); ?></h3> <form id="st-tracking-form" method="post"> <?php wp_nonce_field('st_tracking_action', 'st_tracking_nonce'); ?> <div class="form-group"> <label for="tracking_number">运单号:</label> <input type="text" id="tracking_number" name="tracking_number" required> </div> <div class="form-group"> <label for="carrier_code">快递公司:</label> <select id="carrier_code" name="carrier_code" required> <option value="">请选择快递公司</option> <?php $api_handler = new API_Handler(); $carriers = $api_handler->get_carriers(); foreach ($carriers as $code => $name) { echo '<option value="' . esc_attr($code) . '">' . esc_html($name) . '</option>'; } ?> </select> </div> <div class="form-group"> <button type="submit"><?php echo esc_html($atts['button_text']); ?></button> </div> </form> <div id="st-tracking-result"></div> </div> <?php return ob_get_clean(); } } 4.2 集成地图展示功能 地图展示是物流跟踪的亮点功能。我们使用Leaflet.js,这是一个开源的移动友好交互地图库: public function render_tracking_map($atts) { $atts = shortcode_atts(array( 'height' => '400px', 'width' => '100%' ), $atts, 'shipment_map'); wp_enqueue_script('leaflet', 'https://unpkg.com/leaflet@1.7.1/dist/leaflet.js', array(), '1.7.1'); wp_enqueue_style('leaflet-css', 'https://unpkg.com/leaflet@1.7.1/dist/leaflet.css', array(), '1.7.1'); ob_start(); ?> <div id="shipment-map" style="height: <?php echo esc_attr($atts['height']); ?>; width: <?php echo esc_attr($atts['width']); ?>;"></div> <script> jQuery(document).ready(function($) { // 初始化地图 var map = L.map('shipment-map').setView([39.9042, 116.4074], 5); // 默认北京中心 // 添加地图图层 L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors' }).addTo(map); // 如果有跟踪数据,显示路径 <?php if (isset($_GET['tracking_data'])) : ?> var trackingData = <?php echo json_encode($_GET['tracking_data']); ?>; displayTrackingPath(map, trackingData); <?php endif; ?> function displayTrackingPath(map, data) { var path = []; var markers = []; // 处理轨迹点 if (data.Traces && data.Traces.length > 0) { data.Traces.forEach(function(trace, index) { // 这里需要根据实际API返回的地址信息转换为坐标 // 实际应用中可能需要地理编码服务 var coords = geocodeAddress(trace.AcceptStation); if (coords) { path.push(coords); // 添加标记点 var marker = L.marker(coords) .bindPopup('<b>' + trace.AcceptTime + '</b><br>' + trace.AcceptStation) .addTo(map); markers.push(marker); // 如果是第一个或最后一个点,特别标记 if (index === 0) { marker.setIcon(L.icon({ iconUrl: '<?php echo ST_PLUGIN_URL; ?>assets/images/start-marker.png', iconSize: [32, 32] })); } else if (index === data.Traces.length - 1) { marker.setIcon(L.icon({ iconUrl: '<?php echo ST_PLUGIN_URL; ?>assets/images/end-marker.png', iconSize: [32, 32] })); } } }); // 绘制路径线 if (path.length > 1) { var polyline = L.polyline(path, { color: '#3498db', weight: 3, opacity: 0.7 }).addTo(map); // 调整地图视野以显示完整路径 map.fitBounds(polyline.getBounds()); } } } // 简化的地理编码函数(实际应用中应使用专业地理编码服务) function geocodeAddress(address) { // 这里应调用地理编码API // 为示例目的,返回随机坐标 return [ 39.9042 + (Math.random() - 0.5) * 10, 116.4074 + (Math.random() - 0.5) * 10 ]; } }); </script> <?php return ob_get_clean(); } #### 4.3 AJAX交互实现 为了实现无需页面刷新的查询体验,我们添加AJAX处理功能: // 在Shortcode_Handler类中添加AJAX处理方法public function register_ajax_handlers() { add_action('wp_ajax_st_track_shipment', array($this, 'handle_tracking_request')); add_action('wp_ajax_nopriv_st_track_shipment', array($this, 'handle_tracking_request')); } public function handle_tracking_request() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'st_nonce')) { wp_die('安全验证失败'); } $tracking_number = sanitize_text_field($_POST['tracking_number']); $carrier_code = sanitize_text_field($_POST['carrier_code']); $api_handler = new API_Handler(); $result = $api_handler->track_shipment($tracking_number, $carrier_code); if ($result) { wp_send_json_success(array( 'data' => $result, 'html' => $this->generate_tracking_html($result) )); } else { wp_send_json_error('查询失败,请检查运单号和快递公司是否正确'); } } private function generate_tracking_html($data) { ob_start(); ?> <div class="tracking-result"> <div class="tracking-header"> <h4>物流跟踪信息</h4> <div class="tracking-status"> 状态:<span class="status-badge status-<?php echo esc_attr(strtolower($data['State'] ?? '')); ?>"> <?php echo $this->get_status_text($data['State'] ?? ''); ?> </span> </div> </div> <div class="tracking-timeline"> <?php if (!empty($data['Traces'])) : ?> <?php foreach (array_reverse($data['Traces']) as $trace) : ?> <div class="timeline-item"> <div class="timeline-dot"></div> <div class="timeline-content"> <div class="timeline-time"><?php echo esc_html($trace['AcceptTime']); ?></div> <div class="timeline-desc"><?php echo esc_html($trace['AcceptStation']); ?></div> </div> </div> <?php endforeach; ?> <?php else : ?> <p>暂无物流信息</p> <?php endif; ?> </div> <div class="tracking-actions"> <button class="view-map-btn" data-tracking='<?php echo json_encode($data); ?>'> 查看运输路径地图 </button> </div> </div> <?php return ob_get_clean(); } ### 第五章:后台管理与设置 #### 5.1 创建设置页面 为插件创建专业的设置页面,让管理员可以配置API密钥和其他选项: class Admin_Settings { public function add_admin_menu() { add_menu_page( '物流跟踪设置', '物流跟踪', 'manage_options', 'shipment-tracker', array($this, 'render_settings_page'), 'dashicons-location-alt', 30 ); add_submenu_page( 'shipment-tracker', 'API设置', 'API设置', 'manage_options', 'shipment-tracker-api', array($this, 'render_api_settings_page') ); add_submenu_page( 'shipment-tracker', '查询记录', '查询记录', 'manage_options', 'shipment-tracker-logs', array($this, 'render_logs_page') ); } public function register_settings() { register_setting('st_settings_group', 'st_settings'); add_settings_section( 'st_api_section', 'API配置', array($this, 'render_api_section'), 'shipment-tracker-api' ); add_settings_field( 'api_key', 'API Key', array($this, 'render_api_key_field'), 'shipment-tracker-api', 'st_api_section' ); add_settings_field( 'api_secret', 'API Secret', array($this, 'render_api_secret_field'), 'shipment-tracker-api', 'st_api_section' ); } public function render_settings_page() { ?> <div class="wrap"> <h1>物流跟踪设置</h1> <div class="st-admin-container"> <div class="st-admin-card"> <h2>插件使用说明</h2> <p>1. 在<a href="<?php echo admin_url('admin.php?page=shipment-tracker-api'); ?>">API设置</a>页面配置您的物流API密钥</p> <p>2. 使用短代码在页面中插入跟踪表单:<code>[track_shipment]</code></p> <p>3. 使用短代码插入地图展示:<code>[shipment_map]</code></p> <p>4. 可选参数:<code>[track_shipment title="我的标题" button_text="查询物流"]</code></p> </div> <div class="st-admin-card"> <h2>统计信息</h2> <?php global $wpdb; $table_name = $wpdb->prefix . 'shipment_tracking'; $total_queries = $wpdb->get_var("SELECT COUNT(*) FROM $table_name"); $today_queries = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE DATE(last_update) = %s", current_time('Y-m-d') )); ?> <p>总查询次数:<?php echo $total_queries; ?></p> <p>今日查询:<?php echo $today_queries; ?></p> </div> </div> </div> <?php } public function render_api_settings_page() { ?> <div class="wrap"> <h1>API设置</h1> <form method="post" action="options.php"> <?php settings_fields('st_settings_group'); do_settings_sections('shipment-tracker-api'); submit_button(); ?> </form> <div class="st-api-test"> <h3>API连接测试</h3> <input type="text" id="test-tracking-number" placeholder="测试运单号"> <select id="test-carrier-code"> <option value="">选择快递公司</option> <?php $api_handler = new API_Handler(); $carriers = $api_handler->get_carriers(); foreach ($carriers as $code => $name) { echo '<option value="' . esc_attr($code) . '">' . esc_html($name) . '</option>'; } ?> </select> <button id="test-api-btn" class="button">测试连接</button> <div id="test-result"></div> </div> </div> <?php } public function render_logs_page() { ?> <div class="wrap"> <h1>查询记录</h1> <?php $this->render_logs_table(); ?> </div> <?php } private function render_logs_table() { global $wpdb; $table_name = $wpdb->prefix . 'shipment_tracking'; // 分页逻辑 $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $offset = ($current_page - 1) * $per_page; $total_items = $wpdb->get_var("SELECT COUNT(*) FROM $table_name"); $logs = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name ORDER BY last_update DESC LIMIT %d OFFSET %d", $per_page, $offset ) ); ?> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>ID</th> <th>运单号</th> <th>快递公司</th> <th>状态</th> <th>最后更新</th> <th>操作</th> </tr> </thead> <tbody> <?php if (empty($logs)) : ?> <tr> <td colspan="6">暂无查询记录</td> </tr> <?php else : ?> <?php foreach ($logs as $log) : ?> <tr> <td><?php echo $log->id; ?></td> <td><?php echo esc_html($log->tracking_number); ?></td> <td><?php echo esc_html($log->carrier_code); ?></td> <td> <span class="status-badge status-<?php echo strtolower($log->status); ?>"> <?php echo $this->get_status_text($log->status); ?> </span> </td> <td><?php echo $log->last_update; ?></td> <td> <button class="button view-log-details" data-log-id="<?php echo $log->id; ?>"> 查看详情 </button> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> <div class="tablenav bottom"> <div class="tablenav-pages"> <?php $total_pages = ceil($total_items / $per_page); echo paginate_links(array( 'base' => add_query_arg('paged', '%#%'), 'format' => '', 'prev_text' => '&laquo;', 'next_text' => '&raquo;', 'total' => $total_pages, 'current' => $current_page )); ?> </div> </div> <?php } } #### 5.2 添加管理员通知和帮助标签 public function add_admin_notices() { $options = get_option('st_settings'); if (empty($options['api_key']) || empty($options['api_secret'])) { ?> <div class="notice notice-warning"> <p>物流跟踪插件需要配置API密钥才能正常工作。请前往<a href="<?php echo admin_url('admin.php?page=shipment-tracker-api'); ?>">设置页面</a>进行配置。</p> </div> <?php } } public function add_help_tab() { $screen = get_current_screen(); if ($screen->id === 'toplevel_page_shipment-tracker') { $screen->add_help_tab(array( 'id' => 'st_help_tab', 'title' => '使用帮助', 'content' => ' <h3>物流跟踪插件使用指南</h3> <p><strong>1. 配置API</strong><br> 首先需要在API设置页面填写从快递鸟或其他物流API服务商获取的API密钥。</p> <p><strong>2. 插入短代码</strong><br> 在文章或页面中使用以下短代码:<br> - 跟踪表单:<code>[track_shipment]</code><br> - 地图展示:<code>[shipment_map]</code></p> <p><strong>3. 自定义样式</strong><br> 可以通过CSS自定义插件外观,样式文件位于:<code>/wp-content/plugins/shipment-tracker/assets/css/</code></p> ' )); } } ### 第六章:样式优化与响应式设计 #### 6.1 创建CSS样式文件 在assets/css目录下创建public.css文件: / 物流跟踪插件样式 /.shipment-tracker-form { max-width: 600px; margin: 20px auto; padding: 30px; background: #f8f9fa; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .shipment-tracker-form h3 { margin-top: 0; color: #333; border-bottom: 2px solid #3498db; padding-bottom: 10px; } .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 5px; font-weight: 600; color: #555; } .form-group input[type="text"],.form-group select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; transition: border-color 0.3s; } .form-group input[type="text"]:focus,.form-group select:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 2px rgba(52,152,219,0.2); } .form-group button { background: #3498db; color: white; border: none; padding: 12px 30px; border-radius: 4px; cursor: pointer; font-size: 16px; transition: background 0.3s; } .form-group button:hover { background: #2980b9; } / 跟踪结果样式 /.tracking-result { margin-top: 30px; padding: 20px; background: white; border-radius: 6px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .tracking-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; } .status-badge { display: inline-block; padding: 4px 12px; border-radius: 20px; font-size: 12px; font-weight: 600; } .status-2 { background: #2ecc71; color: white; } / 已签收 /.status-3 { background: #e74c3c; color: white; } / 问题件 /.status-1 { background: #3498db; color: white; } / 运输中 /.status-0 { background: #95a5a6; color: white; } / 无信息 / / 时间线样式 /.tracking-timeline { position: relative; padding-left: 30px; } .tracking-timeline::before { content: ''; position: absolute; left: 10px; top: 0; bottom: 0; width: 2px; background: #3498db; } .timeline-item { position: relative; margin-bottom: 20px; } .timeline-dot { position: absolute; left: -25px; top: 5px; width: 12px; height: 12px; background: white; border: 2px solid #3498db; border-radius: 50%; } .timeline-content { padding: 10px; background: #f8f9fa; border-radius: 4px; } .timeline-time { font-weight: 600; color: #2c3e50; margin-bottom: 5px; } .timeline-desc { color: #555; line-height: 1.5; } / 地图容器 / shipment-map { border-radius: 8px; overflow: hidden; box-shadow: 0

发表评论

详细教程,为WordPress网站开发员工排班与考勤管理应用

WordPress网站员工排班与考勤管理应用开发详细教程 引言:为什么选择WordPress开发企业应用? 在当今数字化时代,企业管理系统正逐渐从传统的桌面软件转向基于Web的解决方案。WordPress作为全球最流行的内容管理系统,不仅适用于博客和内容网站,其强大的扩展性和灵活性也使其成为开发企业级应用的理想平台。通过WordPress开发员工排班与考勤管理系统,企业可以享受以下优势: 成本效益:相比定制开发或购买专业软件,基于WordPress的解决方案成本更低 易于维护:WordPress拥有庞大的开发者社区和丰富的文档资源 高度可定制:可以根据企业具体需求进行灵活调整 集成能力:可与现有WordPress网站无缝集成,无需额外登录系统 本教程将详细指导您如何通过WordPress代码二次开发,创建一个功能完整的员工排班与考勤管理应用。 第一部分:开发环境准备与项目规划 1.1 开发环境搭建 在开始开发之前,我们需要准备合适的开发环境: // 推荐开发环境配置 - WordPress版本:5.8或更高 - PHP版本:7.4或更高 - MySQL版本:5.6或更高 - 本地开发环境:XAMPP、MAMP或Local by Flywheel - 代码编辑器:VS Code、PHPStorm或Sublime Text 1.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('EMP_SCHEDULE_VERSION', '1.0.0'); define('EMP_SCHEDULE_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('EMP_SCHEDULE_PLUGIN_URL', plugin_dir_url(__FILE__)); 1.3 数据库表设计 我们需要创建几个数据库表来存储员工、排班和考勤数据: // 在插件激活时创建数据库表 register_activation_hook(__FILE__, 'emp_schedule_create_tables'); function emp_schedule_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 员工信息表 $employees_table = $wpdb->prefix . 'emp_employees'; $sql1 = "CREATE TABLE IF NOT EXISTS $employees_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, employee_id varchar(50) NOT NULL, full_name varchar(100) NOT NULL, department varchar(100), position varchar(100), hire_date date, status varchar(20) DEFAULT 'active', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY employee_id (employee_id) ) $charset_collate;"; // 排班表 $schedules_table = $wpdb->prefix . 'emp_schedules'; $sql2 = "CREATE TABLE IF NOT EXISTS $schedules_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, employee_id mediumint(9) NOT NULL, schedule_date date NOT NULL, shift_start time NOT NULL, shift_end time NOT NULL, shift_type varchar(50), notes text, created_by bigint(20), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY employee_date (employee_id, schedule_date) ) $charset_collate;"; // 考勤记录表 $attendance_table = $wpdb->prefix . 'emp_attendance'; $sql3 = "CREATE TABLE IF NOT EXISTS $attendance_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, employee_id mediumint(9) NOT NULL, attendance_date date NOT NULL, check_in datetime, check_out datetime, status varchar(50), late_minutes int(11) DEFAULT 0, overtime_minutes int(11) DEFAULT 0, notes text, verified_by bigint(20), verified_at datetime, PRIMARY KEY (id), UNIQUE KEY employee_date (employee_id, attendance_date) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); } 第二部分:员工管理模块开发 2.1 员工信息管理界面 创建员工管理后台页面,允许管理员添加、编辑和删除员工信息: // 添加管理菜单 add_action('admin_menu', 'emp_schedule_admin_menu'); function emp_schedule_admin_menu() { // 主菜单 add_menu_page( '员工排班与考勤系统', '员工考勤', 'manage_options', 'emp-schedule', 'emp_schedule_dashboard_page', 'dashicons-calendar-alt', 30 ); // 子菜单 add_submenu_page( 'emp-schedule', '员工管理', '员工管理', 'manage_options', 'emp-employees', 'emp_employees_page' ); add_submenu_page( 'emp-schedule', '排班管理', '排班管理', 'manage_options', 'emp-schedules', 'emp_schedules_page' ); add_submenu_page( 'emp-schedule', '考勤记录', '考勤记录', 'manage_options', 'emp-attendance', 'emp_attendance_page' ); add_submenu_page( 'emp-schedule', '报表统计', '报表统计', 'manage_options', 'emp-reports', 'emp_reports_page' ); } // 员工管理页面 function emp_employees_page() { global $wpdb; // 处理表单提交 if (isset($_POST['add_employee'])) { // 验证和清理数据 $employee_data = array( 'user_id' => intval($_POST['user_id']), 'employee_id' => sanitize_text_field($_POST['employee_id']), 'full_name' => sanitize_text_field($_POST['full_name']), 'department' => sanitize_text_field($_POST['department']), 'position' => sanitize_text_field($_POST['position']), 'hire_date' => sanitize_text_field($_POST['hire_date']), 'status' => sanitize_text_field($_POST['status']) ); $table_name = $wpdb->prefix . 'emp_employees'; $wpdb->insert($table_name, $employee_data); echo '<div class="notice notice-success"><p>员工添加成功!</p></div>'; } // 获取员工列表 $employees_table = $wpdb->prefix . 'emp_employees'; $employees = $wpdb->get_results("SELECT * FROM $employees_table ORDER BY id DESC"); ?> <div class="wrap"> <h1 class="wp-heading-inline">员工管理</h1> <a href="#add-employee-form" class="page-title-action">添加新员工</a> <hr class="wp-header-end"> <!-- 员工列表 --> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>员工ID</th> <th>姓名</th> <th>部门</th> <th>职位</th> <th>入职日期</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <?php if ($employees): ?> <?php foreach ($employees as $employee): ?> <tr> <td><?php echo esc_html($employee->employee_id); ?></td> <td><?php echo esc_html($employee->full_name); ?></td> <td><?php echo esc_html($employee->department); ?></td> <td><?php echo esc_html($employee->position); ?></td> <td><?php echo esc_html($employee->hire_date); ?></td> <td> <span class="status-badge status-<?php echo esc_attr($employee->status); ?>"> <?php $status_labels = array( 'active' => '在职', 'inactive' => '离职', 'on_leave' => '休假' ); echo isset($status_labels[$employee->status]) ? $status_labels[$employee->status] : $employee->status; ?> </span> </td> <td> <a href="?page=emp-employees&action=edit&id=<?php echo $employee->id; ?>" class="button button-small">编辑</a> <a href="?page=emp-employees&action=delete&id=<?php echo $employee->id; ?>" class="button button-small button-link-delete" onclick="return confirm('确定要删除此员工吗?')">删除</a> </td> </tr> <?php endforeach; ?> <?php else: ?> <tr> <td colspan="7" style="text-align:center;">暂无员工数据</td> </tr> <?php endif; ?> </tbody> </table> <!-- 添加员工表单 --> <h2 id="add-employee-form">添加新员工</h2> <form method="post" action=""> <table class="form-table"> <tr> <th scope="row"><label for="employee_id">员工编号</label></th> <td><input type="text" id="employee_id" name="employee_id" required class="regular-text"></td> </tr> <tr> <th scope="row"><label for="full_name">姓名</label></th> <td><input type="text" id="full_name" name="full_name" required class="regular-text"></td> </tr> <tr> <th scope="row"><label for="department">部门</label></th> <td> <select id="department" name="department" class="regular-text"> <option value="">选择部门</option> <option value="技术部">技术部</option> <option value="市场部">市场部</option> <option value="销售部">销售部</option> <option value="人事部">人事部</option> <option value="财务部">财务部</option> </select> </td> </tr> <tr> <th scope="row"><label for="position">职位</label></th> <td><input type="text" id="position" name="position" class="regular-text"></td> </tr> <tr> <th scope="row"><label for="hire_date">入职日期</label></th> <td><input type="date" id="hire_date" name="hire_date" class="regular-text"></td> </tr> <tr> <th scope="row"><label for="status">状态</label></th> <td> <select id="status" name="status" class="regular-text"> <option value="active">在职</option> <option value="inactive">离职</option> <option value="on_leave">休假</option> </select> </td> </tr> </table> <?php submit_button('添加员工', 'primary', 'add_employee'); ?> </form> </div> <style> .status-badge { display: inline-block; padding: 3px 8px; border-radius: 3px; font-size: 12px; font-weight: bold; } .status-active { background-color: #d4edda; color: #155724; } .status-inactive { background-color: #f8d7da; color: #721c24; } .status-on_leave { background-color: #fff3cd; color: #856404; } </style> <?php } 2.2 员工数据导入导出功能 为了方便批量管理员工信息,我们添加导入导出功能: // 添加上传处理功能 function emp_handle_employee_import() { if (isset($_POST['import_employees']) && isset($_FILES['import_file'])) { $file = $_FILES['import_file']; if ($file['error'] === UPLOAD_ERR_OK) { $file_type = wp_check_filetype($file['name']); if ($file_type['ext'] === 'csv') { $handle = fopen($file['tmp_name'], 'r'); $header = fgetcsv($handle); // 跳过标题行 global $wpdb; $table_name = $wpdb->prefix . 'emp_employees'; $imported = 0; $skipped = 0; while (($data = fgetcsv($handle)) !== FALSE) { if (count($data) >= 6) { $employee_data = array( 'employee_id' => sanitize_text_field($data[0]), 'full_name' => sanitize_text_field($data[1]), 'department' => sanitize_text_field($data[2]), 'position' => sanitize_text_field($data[3]), 'hire_date' => sanitize_text_field($data[4]), 'status' => sanitize_text_field($data[5]) ); // 检查员工ID是否已存在 $existing = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE employee_id = %s", $employee_data['employee_id'] )); if (!$existing) { $wpdb->insert($table_name, $employee_data); $imported++; } else { $skipped++; } } } fclose($handle); echo '<div class="notice notice-success"><p>导入完成!成功导入 ' . $imported . ' 条记录,跳过 ' . $skipped . ' 条重复记录。</p></div>'; } else { echo '<div class="notice notice-error"><p>请上传CSV格式的文件。</p></div>'; } } } } // 导出功能 function emp_export_employees_csv() { if (isset($_GET['export_employees']) && $_GET['export_employees'] === '1') { global $wpdb; $table_name = $wpdb->prefix . 'emp_employees'; $employees = $wpdb->get_results("SELECT * FROM $table_name", ARRAY_A); header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename=employees_' . date('Y-m-d') . '.csv'); $output = fopen('php://output', 'w'); // 添加BOM头,确保Excel正确识别UTF-8编码 fwrite($output, "xEFxBBxBF"); // 写入标题行 fputcsv($output, array('员工编号', '姓名', '部门', '职位', '入职日期', '状态')); // 写入数据 foreach ($employees as $employee) { fputcsv($output, array( $employee['employee_id'], $employee['full_name'], $employee['department'], $employee['position'], $employee['hire_date'], $employee['status'] )); } fclose($output); exit; } } add_action('admin_init', 'emp_export_employees_csv'); 第三部分:排班管理模块开发 3.1 排班日历视图 创建一个直观的日历界面来管理员工排班: // 排班管理页面 function emp_schedules_page() { global $wpdb; // 获取当前月份 $current_month = isset($_GET['month']) ? $_GET['month'] : date('Y-m'); ?> <div class="wrap"> <h1 class="wp-heading-inline">排班管理</h1> <a href="#add-schedule-form" class="page-title-action">添加排班</a> <hr class="wp-header-end"> <!-- 月份导航 --> <div class="month-navigation"> <?php $prev_month = date('Y-m', strtotime($current_month . ' -1 month')); $next_month = date('Y-m', strtotime($current_month . ' +1 month')); ?> <a href="?page=emp-schedules&month=<?php echo $prev_month; ?>" class="button">&larr; 上月</a> <h2 style="display:inline-block; margin:0 20px;"><?php echo date('Y年m月', strtotime($current_month)); ?></h2> <a href="?page=emp-schedules&month=<?php echo $next_month; ?>" class="button">下月 &rarr;</a> </div> <!-- 排班日历 --> <div class="schedule-calendar"> <?php // 生成日历 $year = date('Y', strtotime($current_month)); $month = date('m', strtotime($current_month)); $first_day = mktime(0, 0, 0, $month, 1, $year); $days_in_month = date('t', $first_day); $first_day_of_week = date('w', $first_day); // 获取员工列表 $employees_table = $wpdb->prefix . 'emp_employees'; // 获取排班数据 $schedules_table = $wpdb->prefix . 'emp_schedules'; $schedules = $wpdb->get_results($wpdb->prepare( "SELECT s.*, e.full_name FROM $schedules_table s LEFT JOIN $employees_table e ON s.employee_id = e.id WHERE YEAR(schedule_date) = %d AND MONTH(schedule_date) = %d ORDER BY schedule_date, shift_start", $year, $month )); // 按日期组织排班数据 $schedule_by_date = array(); foreach ($schedules as $schedule) { $date = $schedule->schedule_date; if (!isset($schedule_by_date[$date])) { $schedule_by_date[$date] = array(); } $schedule_by_date[$date][] = $schedule; } ?> <table class="widefat fixed schedule-calendar-table"> <thead> <tr> <th>周日</th> <th>周一</th> <th>周二</th> <th>周三</th> <th>周四</th> <th>周五</th> <th>周六</th> </tr> </thead> <tbody> <?php $day_count = 1; echo '<tr>'; // 填充第一个星期前的空白 for ($i = 0; $i < $first_day_of_week; $i++) { echo '<td class="empty-day"></td>'; $day_count++; } // 填充日期 for ($day = 1; $day <= $days_in_month; $day++) { $current_date = sprintf('%04d-%02d-%02d', $year, $month, $day); $is_today = ($current_date == date('Y-m-d')) ? 'today' : ''; echo '<td class="calendar-day ' . $is_today . '">'; echo '<div class="day-number">' . $day . '</div>'; // 显示当天的排班 if (isset($schedule_by_date[$current_date])) { echo '<div class="day-schedules">'; foreach ($schedule_by_date[$current_date] as $schedule) { echo '<div class="schedule-item" data-id="' . $schedule->id . '">'; echo '<span class="employee-name">' . esc_html($schedule->full_name) . '</span>'; echo '<span class="shift-time">' . date('H:i', strtotime($schedule->shift_start)) . '-' . date('H:i', strtotime($schedule->shift_end)) . '</span>'; echo '</div>'; } echo '</div>'; } echo '</td>'; // 换行 if ($day_count % 7 == 0 && $day != $days_in_month) { echo '</tr><tr>'; } $day_count++; } // 填充最后一个星期后的空白 while ($day_count % 7 != 1) { echo '<td class="empty-day"></td>'; $day_count++; } echo '</tr>'; ?> </tbody> </table> </div> <!-- 添加排班表单 --> <h2 id="add-schedule-form">添加排班</h2> <form method="post" action=""> <table class="form-table"> <tr> <th scope="row"><label for="employee_id">员工</label></th> <td> <select id="employee_id" name="employee_id" required class="regular-text"> <option value="">选择员工</option> <?php foreach ($employees as $employee): ?> <option value="<?php echo $employee->id; ?>"> <?php echo esc_html($employee->full_name . ' (' . $employee->employee_id . ')'); ?> </option> <?php endforeach; ?> </select> </td> </tr> <tr> <th scope="row"><label for="schedule_date">日期</label></th> <td><input type="date" id="schedule_date" name="schedule_date" required class="regular-text" value="<?php echo date('Y-m-d'); ?>"></td> </tr> <tr> <th scope="row"><label for="shift_start">上班时间</label></th> <td><input type="time" id="shift_start" name="shift_start" required class="regular-text" value="09:00"></td> </tr> <tr> <th scope="row"><label for="shift_end">下班时间</label></th> <td><input type="time" id="shift_end" name="shift_end" required class="regular-text" value="18:00"></td> </tr> <tr> <th scope="row"><label for="shift_type">班次类型</label></th> <td> <select id="shift_type" name="shift_type" class="regular-text"> <option value="normal">正常班</option> <option value="morning">早班</option> <option value="night">晚班</option> <option value="overtime">加班</option> <option value="weekend">周末班</option> </select> </td> </tr> <tr> <th scope="row"><label for="notes">备注</label></th> <td><textarea id="notes" name="notes" rows="3" class="regular-text"></textarea></td> </tr> </table> <?php submit_button('添加排班', 'primary', 'add_schedule'); ?> </form> </div> <style> .schedule-calendar-table { border-collapse: collapse; margin: 20px 0; } .schedule-calendar-table th { background-color: #f1f1f1; text-align: center; padding: 10px; border: 1px solid #ddd; } .calendar-day { height: 120px; vertical-align: top; border: 1px solid #ddd; padding: 5px; position: relative; } .calendar-day.today { background-color: #e6f7ff; } .day-number { font-weight: bold; margin-bottom: 5px; } .day-schedules { max-height: 90px; overflow-y: auto; } .schedule-item { background-color: #f0f8ff; border-left: 3px solid #1890ff; padding: 3px 5px; margin-bottom: 3px; font-size: 12px; border-radius: 2px; } .schedule-item .employee-name { display: block; font-weight: bold; } .schedule-item .shift-time { color: #666; font-size: 11px; } .empty-day { background-color: #f9f9f9; border: 1px solid #ddd; } .month-navigation { margin: 20px 0; text-align: center; } </style> <?php } 3.2 批量排班功能 为了方便批量设置排班,我们添加批量排班功能: // 批量排班功能 function emp_batch_schedule_form() { ?> <div class="batch-schedule-form" style="margin: 20px 0; padding: 20px; background: #f9f9f9; border: 1px solid #ddd;"> <h3>批量排班设置</h3> <form method="post" action=""> <table class="form-table"> <tr> <th scope="row"><label for="batch_employees">选择员工</label></th> <td> <select id="batch_employees" name="batch_employees[]" multiple class="regular-text" style="height: 150px;"> <?php global $wpdb; $employees_table = $wpdb->prefix . 'emp_employees'; $employees = $wpdb->get_results("SELECT id, full_name, employee_id FROM $employees_table WHERE status = 'active' ORDER BY full_name"); foreach ($employees as $employee) { echo '<option value="' . $employee->id . '">' . esc_html($employee->full_name . ' (' . $employee->employee_id . ')') . '</option>'; } ?> </select> <p class="description">按住Ctrl键可多选</p> </td> </tr> <tr> <th scope="row"><label for="batch_start_date">开始日期</label></th> <td><input type="date" id="batch_start_date" name="batch_start_date" required class="regular-text" value="<?php echo date('Y-m-d'); ?>"></td> </tr> <tr> <th scope="row"><label for="batch_end_date">结束日期</label></th> <td><input type="date" id="batch_end_date" name="batch_end_date" required class="regular-text" value="<?php echo date('Y-m-d', strtotime('+7 days')); ?>"></td> </tr> <tr> <th scope="row"><label for="batch_weekdays">工作日</label></th> <td> <label><input type="checkbox" name="batch_weekdays[]" value="1" checked> 周一</label> <label><input type="checkbox" name="batch_weekdays[]" value="2" checked> 周二</label> <label><input type="checkbox" name="batch_weekdays[]" value="3" checked> 周三</label> <label><input type="checkbox" name="batch_weekdays[]" value="4" checked> 周四</label> <label><input type="checkbox" name="batch_weekdays[]" value="5" checked> 周五</label> <label><input type="checkbox" name="batch_weekdays[]" value="6"> 周六</label> <label><input type="checkbox" name="batch_weekdays[]" value="0"> 周日</label> </td> </tr> <tr> <th scope="row"><label for="batch_shift_start">上班时间</label></th> <td><input type="time" id="batch_shift_start" name="batch_shift_start" required class="regular-text" value="09:00"></td> </tr> <tr> <th scope="row"><label for="batch_shift_end">下班时间</label></th> <td><input type="time" id="batch_shift_end" name="batch_shift_end" required class="regular-text" value="18:00"></td> </tr> <tr> <th scope="row"><label for="batch_shift_type">班次类型</label></th> <td> <select id="batch_shift_type" name="batch_shift_type" class="regular-text"> <option value="normal">正常班</option> <option value="morning">早班</option> <option value="night">晚班</option> </select> </td> </tr> </table> <?php submit_button('批量设置排班', 'primary', 'batch_schedule'); ?> </form> </div> <?php } // 处理批量排班 function emp_process_batch_schedule() { if (isset($_POST['batch_schedule'])) { global $wpdb; $employees = $_POST['batch_employees']; $start_date = $_POST['batch_start_date']; $end_date = $_POST['batch_end_date']; $weekdays = isset($_POST['batch_weekdays']) ? $_POST['batch_weekdays'] : array(); $shift_start = $_POST['batch_shift_start']; $shift_end = $_POST['batch_shift_end']; $shift_type = $_POST['batch_shift_type']; $schedules_table = $wpdb->prefix . 'emp_schedules'; $current_user_id = get_current_user_id(); $start = new DateTime($start_date); $end = new DateTime($end_date); $interval = new DateInterval('P1D'); $period = new DatePeriod($start, $interval, $end->modify('+1 day')); $added_count = 0; foreach ($period as $date) { $weekday = $date->format('w'); // 0=周日, 1=周一... if (in_array($weekday, $weekdays)) { $schedule_date = $date->format('Y-m-d'); foreach ($employees as $employee_id) { // 检查是否已有排班 $existing = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $schedules_table WHERE employee_id = %d AND schedule_date = %s", $employee_id, $schedule_date )); if (!$existing) { $wpdb->insert($schedules_table, array( 'employee_id' => $employee_id, 'schedule_date' => $schedule_date, 'shift_start' => $shift_start, 'shift_end' => $shift_end, 'shift_type' => $shift_type, 'created_by' => $current_user_id )); $added_count++; } } } } echo '<div class="notice notice-success"><p>批量排班完成!成功添加 ' . $added_count . ' 条排班记录。</p></div>'; } } 第四部分:考勤管理模块开发 4.1 考勤打卡功能 创建员工考勤打卡界面: // 添加快捷码支持 add_shortcode('emp_attendance_check', 'emp_attendance_check_shortcode'); function emp_attendance_check_shortcode() { if (!is_user_logged_in()) { return '<p>请先登录系统。</p>'; } $user_id = get_current_user_id(); $today = date('Y-m-d'); // 获取员工信息 global $wpdb; $employees_table = $wpdb->prefix . 'emp_employees'; $employee = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $employees_table WHERE user_id = %d", $user_id )); if (!$employee) { return '<p>您不是注册员工,无法使用考勤功能。</p>'; } // 获取今日排班 $schedules_table = $wpdb->prefix . 'emp_schedules'; $schedule = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $schedules_table WHERE employee_id = %d AND schedule_date = %s", $employee->id, $today )); // 获取今日考勤记录 $attendance_table = $wpdb->prefix . 'emp_attendance'; $attendance = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $attendance_table WHERE employee_id = %d AND attendance_date = %s", $employee->id, $today )); ob_start(); ?> <div class="emp-attendance-check"> <div class="attendance-header"> <h2>员工考勤打卡</h2> <div class="employee-info"> <p><strong>员工:</strong> <?php echo esc_html($employee->full_name); ?></p> <p><strong>日期:</strong> <?php echo date('Y年m月d日'); ?></p> <p><strong>时间:</strong> <span id="current-time"><?php echo date('H:i:s'); ?></span></p> </div> </div> <?php if ($schedule): ?> <div class="schedule-info"> <h3>今日排班信息</h3> <p><strong>上班时间:</strong> <?php echo date('H:i', strtotime($schedule->shift_start)); ?></p> <p><strong>下班时间:</strong> <?php echo date('H:i', strtotime($schedule->shift_end)); ?></p> <p><strong>班次类型:</strong> <?php echo esc_html($schedule->shift_type); ?></p> </div> <?php else: ?> <div class="notice notice-warning"> <p>今日无排班安排。</p> </div> <?php endif; ?> <div class="attendance-actions"> <?php if (!$attendance || !$attendance->check_in): ?> <form method="post" action="" class="checkin-form"> <input type="hidden" name="action" value="check_in"> <input type="hidden" name="employee_id" value="<?php echo $employee->id; ?>"> <button type="submit" class="button button-primary button-large" name="check_in"> <span class="dashicons dashicons-clock"></span> 上班打卡 </button> <p class="description">上班时间: <?php echo $schedule ? date('H:i', strtotime($schedule->shift_start)) : '无排班'; ?></p> </form> <?php elseif ($attendance && $attendance->check_in && !$attendance->check_out): ?> <div class="checked-in-info"> <p class="checked-in-time">上班打卡时间: <?php echo date('H:i:s', strtotime($attendance->check_in)); ?></p> <?php // 计算是否迟到 if ($schedule) { $check_in_time = strtotime($attendance->check_in);

发表评论

一步步教你,集成在线法律文书生成与合同智能审查工具到网站

一步步教你,集成在线法律文书生成与合同智能审查工具到网站,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:数字化时代下的法律工具集成需求 在当今数字化快速发展的时代,越来越多的企业和个人网站需要集成专业工具来提升用户体验和服务价值。特别是法律相关服务,如在线法律文书生成和合同智能审查,已成为许多商业网站、法律服务平台和企业门户的必备功能。通过将这些专业工具集成到网站中,不仅可以为用户提供即时、便捷的法律服务,还能显著提升网站的专业性和实用性。 WordPress作为全球最流行的内容管理系统,以其强大的扩展性和灵活性,成为实现这类功能集成的理想平台。本文将详细介绍如何通过WordPress代码二次开发,将在线法律文书生成与合同智能审查工具集成到您的网站中,并探讨如何实现其他常用互联网小工具功能,帮助您打造一个功能全面、用户体验卓越的专业网站。 第一部分:准备工作与环境搭建 1.1 确定需求与功能规划 在开始技术实现之前,首先需要明确您的具体需求: 法律文书生成功能:确定需要支持哪些类型的法律文书(如劳动合同、租赁合同、保密协议等) 合同智能审查功能:明确审查标准、风险点识别和修改建议的详细程度 用户权限管理:区分普通用户、会员用户和管理员的不同权限 数据安全与隐私保护:确保用户上传的合同文件和个人信息得到充分保护 界面与用户体验:设计直观易用的操作界面和流畅的用户流程 1.2 开发环境搭建 为了进行WordPress代码二次开发,您需要准备以下环境: 本地开发环境:安装XAMPP、MAMP或Local by Flywheel等本地服务器环境 WordPress安装:下载最新版WordPress并完成基本配置 代码编辑器:选择适合的代码编辑器,如VS Code、Sublime Text或PHPStorm 版本控制:设置Git仓库以管理代码版本 调试工具:安装Query Monitor、Debug Bar等WordPress调试插件 1.3 创建自定义插件框架 为了避免主题更新导致功能丢失,我们建议通过创建自定义插件的方式实现功能: <?php /** * Plugin Name: 法律工具集成套件 * Plugin URI: https://yourwebsite.com/ * Description: 集成在线法律文书生成与合同智能审查工具 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('LEGAL_TOOLS_VERSION', '1.0.0'); define('LEGAL_TOOLS_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('LEGAL_TOOLS_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once LEGAL_TOOLS_PLUGIN_DIR . 'includes/class-legal-tools-init.php'; 第二部分:在线法律文书生成功能实现 2.1 文书模板管理系统 首先,我们需要创建一个文书模板管理系统,用于存储和管理各种法律文书模板: // 创建自定义文章类型用于存储文书模板 function legal_tools_register_document_type() { $labels = array( 'name' => '法律文书模板', 'singular_name' => '文书模板', 'menu_name' => '文书模板', 'add_new' => '添加模板', 'add_new_item' => '添加新模板', 'edit_item' => '编辑模板', 'new_item' => '新模板', 'view_item' => '查看模板', 'search_items' => '搜索模板', 'not_found' => '未找到模板', 'not_found_in_trash' => '回收站中无模板' ); $args = array( 'labels' => $labels, 'public' => false, 'publicly_queryable' => false, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'document-template'), 'capability_type' => 'post', 'has_archive' => false, 'hierarchical' => false, 'menu_position' => 25, 'menu_icon' => 'dashicons-media-document', 'supports' => array('title', 'editor', 'custom-fields') ); register_post_type('document_template', $args); } add_action('init', 'legal_tools_register_document_type'); 2.2 动态表单生成系统 文书生成的核心是根据用户输入动态填充模板内容。我们需要创建一个表单生成系统: // 表单生成器类 class Legal_Document_Form_Builder { private $template_id; private $fields; public function __construct($template_id) { $this->template_id = $template_id; $this->fields = $this->get_template_fields($template_id); } // 获取模板字段定义 private function get_template_fields($template_id) { $fields_meta = get_post_meta($template_id, '_document_fields', true); if (empty($fields_meta)) { // 默认字段结构 $fields = array( 'parties' => array( 'label' => '合同双方信息', 'type' => 'section', 'fields' => array( 'party_a_name' => array( 'label' => '甲方名称', 'type' => 'text', 'required' => true, 'placeholder' => '请输入甲方全称' ), 'party_b_name' => array( 'label' => '乙方名称', 'type' => 'text', 'required' => true, 'placeholder' => '请输入乙方全称' ) ) ), 'contract_terms' => array( 'label' => '合同条款', 'type' => 'section', 'fields' => array( 'effective_date' => array( 'label' => '生效日期', 'type' => 'date', 'required' => true ), 'contract_period' => array( 'label' => '合同期限(月)', 'type' => 'number', 'required' => true, 'min' => 1, 'max' => 120 ) ) ) ); // 保存默认字段到模板 update_post_meta($template_id, '_document_fields', $fields); return $fields; } return $fields_meta; } // 渲染表单 public function render_form() { $output = '<form id="legal-document-form" class="legal-document-form" method="post">'; $output .= wp_nonce_field('generate_legal_document', 'legal_document_nonce', true, false); $output .= '<input type="hidden" name="template_id" value="' . esc_attr($this->template_id) . '">'; foreach ($this->fields as $section_key => $section) { if ($section['type'] === 'section') { $output .= '<div class="form-section">'; $output .= '<h3 class="section-title">' . esc_html($section['label']) . '</h3>'; if (isset($section['fields']) && is_array($section['fields'])) { foreach ($section['fields'] as $field_key => $field) { $output .= $this->render_field($field_key, $field); } } $output .= '</div>'; } else { $output .= $this->render_field($section_key, $section); } } $output .= '<div class="form-submit">'; $output .= '<button type="submit" class="btn btn-primary">生成文书</button>'; $output .= '</div>'; $output .= '</form>'; return $output; } // 渲染单个字段 private function render_field($field_key, $field) { $required = isset($field['required']) && $field['required'] ? ' required' : ''; $field_html = ''; switch ($field['type']) { case 'text': case 'email': case 'number': $field_html = sprintf( '<div class="form-group"> <label for="%s">%s</label> <input type="%s" id="%s" name="%s" class="form-control"%s placeholder="%s"> </div>', esc_attr($field_key), esc_html($field['label']), esc_attr($field['type']), esc_attr($field_key), esc_attr($field_key), $required, isset($field['placeholder']) ? esc_attr($field['placeholder']) : '' ); break; case 'textarea': $field_html = sprintf( '<div class="form-group"> <label for="%s">%s</label> <textarea id="%s" name="%s" class="form-control"%s rows="4" placeholder="%s"></textarea> </div>', esc_attr($field_key), esc_html($field['label']), esc_attr($field_key), esc_attr($field_key), $required, isset($field['placeholder']) ? esc_attr($field['placeholder']) : '' ); break; case 'select': if (isset($field['options']) && is_array($field['options'])) { $options_html = ''; foreach ($field['options'] as $option_value => $option_label) { $options_html .= sprintf( '<option value="%s">%s</option>', esc_attr($option_value), esc_html($option_label) ); } $field_html = sprintf( '<div class="form-group"> <label for="%s">%s</label> <select id="%s" name="%s" class="form-control"%s> %s </select> </div>', esc_attr($field_key), esc_html($field['label']), esc_attr($field_key), esc_attr($field_key), $required, $options_html ); } break; } return $field_html; } } 2.3 模板渲染与文档生成 当用户提交表单后,我们需要将用户数据填充到模板中,生成最终的法律文书: // 文档生成处理器 class Legal_Document_Generator { public function process_generation() { // 验证非ce和权限 if (!isset($_POST['legal_document_nonce']) || !wp_verify_nonce($_POST['legal_document_nonce'], 'generate_legal_document')) { return false; } // 获取模板ID和用户数据 $template_id = intval($_POST['template_id']); $user_data = $_POST; unset($user_data['legal_document_nonce']); unset($user_data['template_id']); // 获取模板内容 $template_post = get_post($template_id); if (!$template_post || $template_post->post_type !== 'document_template') { return false; } // 获取模板HTML $template_content = $template_post->post_content; // 替换模板变量 $generated_content = $this->replace_template_variables($template_content, $user_data); // 保存生成的文档 $document_id = $this->save_generated_document($generated_content, $template_id, $user_data); // 返回文档ID或内容 return array( 'document_id' => $document_id, 'content' => $generated_content ); } // 替换模板变量 private function replace_template_variables($template, $data) { foreach ($data as $key => $value) { $placeholder = '{{' . $key . '}}'; $template = str_replace($placeholder, esc_html($value), $template); } // 替换系统变量 $system_vars = array( '{{current_date}}' => date('Y年m月d日'), '{{generation_date}}' => date('Y年m月d日 H:i:s'), '{{site_name}}' => get_bloginfo('name') ); foreach ($system_vars as $placeholder => $value) { $template = str_replace($placeholder, $value, $template); } return $template; } // 保存生成的文档 private function save_generated_document($content, $template_id, $user_data) { $user_id = get_current_user_id(); $document_data = array( 'post_title' => '法律文书-' . date('YmdHis'), 'post_content' => $content, 'post_status' => 'private', 'post_type' => 'legal_document', 'post_author' => $user_id, 'meta_input' => array( '_generated_from_template' => $template_id, '_generation_data' => $user_data, '_generation_date' => current_time('mysql'), '_generated_by_user' => $user_id ) ); $document_id = wp_insert_post($document_data); return $document_id; } } 第三部分:合同智能审查功能实现 3.1 合同上传与解析系统 合同智能审查的第一步是允许用户上传合同文件并解析其内容: // 合同上传处理器 class Contract_Upload_Handler { public function handle_upload() { // 检查文件上传 if (!isset($_FILES['contract_file']) || $_FILES['contract_file']['error'] !== UPLOAD_ERR_OK) { return new WP_Error('upload_failed', '文件上传失败'); } // 验证文件类型 $allowed_types = array('pdf', 'doc', 'docx', 'txt'); $file_ext = pathinfo($_FILES['contract_file']['name'], PATHINFO_EXTENSION); if (!in_array(strtolower($file_ext), $allowed_types)) { return new WP_Error('invalid_type', '不支持的文件类型'); } // 验证文件大小(最大10MB) $max_size = 10 * 1024 * 1024; if ($_FILES['contract_file']['size'] > $max_size) { return new WP_Error('file_too_large', '文件大小超过限制'); } // 处理上传 $upload_result = $this->process_upload($_FILES['contract_file']); if (is_wp_error($upload_result)) { return $upload_result; } // 解析合同内容 $parsed_content = $this->parse_contract_content($upload_result['file_path'], $file_ext); return array( 'file_info' => $upload_result, 'parsed_content' => $parsed_content ); } // 处理文件上传 private function process_upload($file) { $upload_dir = wp_upload_dir(); $legal_dir = $upload_dir['basedir'] . '/legal-contracts/'; // 创建目录 if (!file_exists($legal_dir)) { wp_mkdir_p($legal_dir); } // 生成唯一文件名 $filename = uniqid('contract_') . '_' . sanitize_file_name($file['name']); $filepath = $legal_dir . $filename; // 移动文件 if (move_uploaded_file($file['tmp_name'], $filepath)) { return array( 'file_path' => $filepath, 'file_url' => $upload_dir['baseurl'] . '/legal-contracts/' . $filename, 'file_name' => $filename, 'original_name' => $file['name'] ); } return new WP_Error('move_failed', '文件保存失败'); } // 解析合同内容 private function parse_contract_content($file_path, $file_ext) { $content = ''; switch (strtolower($file_ext)) { case 'txt': $content = file_get_contents($file_path); break; case 'pdf': // 使用PDF解析库(需要安装适当的PHP扩展或库) $content = $this->parse_pdf_content($file_path); break; case 'doc': case 'docx': // 使用Word文档解析库 $content = $this->parse_word_content($file_path); break; } // 清理和标准化内容 $content = $this->clean_contract_content($content); return $content; } // 清理合同内容 private function clean_contract_content($content) { // 移除多余空格和换行 $content = preg_replace('/s+/', ' ', $content); // 提取关键部分(这里可以根据需要扩展) $sections = $this->extract_contract_sections($content); return array( 'full_text' => $content, 'sections' => $sections ); } // 提取合同章节 private function extract_contract_sections($content) { $sections = array(); // 常见合同章节模式 $section_patterns = array( 'parties' => '/(双方|甲方|乙方|当事人).*?(?=条款|约定|如下|:)/u', 'effective_date' => '/(生效|起始).*?(d{4}年d{1,2}月d{1,2}日|d{4}.d{1,2}.d{1,2})/u', +(年|月|日|天))/u', 'payment_terms' => '/(付款|支付|价款).*?(?=违约责任|争议解决|其他)/u', 'liability' => '/(违约|责任).*?(?=争议解决|其他|附则)/u', 'dispute_resolution' => '/(争议|纠纷|仲裁|诉讼).*?(?=其他|附则|签字)/u' ); foreach ($section_patterns as $section_key => $pattern) { if (preg_match($pattern, $content, $matches)) { $sections[$section_key] = $matches[0]; } } return $sections; } } ### 3.2 智能审查规则引擎 合同审查的核心是规则引擎,用于识别潜在风险和问题: // 合同审查规则引擎class Contract_Review_Engine { private $rules; private $risk_levels; public function __construct() { $this->risk_levels = array( 'high' => '高风险', 'medium' => '中风险', 'low' => '低风险', 'info' => '提示信息' ); $this->initialize_rules(); } // 初始化审查规则 private function initialize_rules() { $this->rules = array( // 缺失关键条款规则 'missing_essential_clauses' => array( 'name' => '缺失关键条款', 'description' => '检查合同是否缺少必要条款', 'risk_level' => 'high', 'check_method' => 'check_missing_clauses', 'keywords' => array('违约责任', '争议解决', '保密', '不可抗力', '终止条件') ), // 模糊表述规则 'ambiguous_language' => array( 'name' => '模糊表述', 'description' => '识别合同中可能引起歧义的模糊表述', 'risk_level' => 'medium', 'check_method' => 'check_ambiguous_language', 'patterns' => array( '/合理的|适当的|必要的/u', '/重大变化|特殊情况/u', '/及时通知|尽快办理/u' ) ), // 权利义务不对等规则 'unbalanced_rights' => array( 'name' => '权利义务不对等', 'description' => '检查合同中双方权利义务是否对等', 'risk_level' => 'high', 'check_method' => 'check_rights_balance', 'indicators' => array( 'exclusive_rights' => array('独家', '排他'), 'unilateral_termination' => array('单方解除', '任意解除'), 'excessive_liability' => array('承担一切损失', '无限责任') ) ), // 付款条款风险 'payment_risks' => array( 'name' => '付款条款风险', 'description' => '识别付款条款中的潜在风险', 'risk_level' => 'medium', 'check_method' => 'check_payment_terms', 'risk_patterns' => array( 'advance_full_payment' => '/预付全款|全额预付/u', 'vague_payment_time' => '/完成后付款|验收后付款/u', 'no_late_fee' => '/(?<!逾期)付款(?!.*违约金|滞纳金)/u' ) ), // 法律引用检查 'legal_references' => array( 'name' => '法律引用检查', 'description' => '检查引用的法律法规是否准确有效', 'risk_level' => 'low', 'check_method' => 'check_legal_references', 'valid_laws' => array( '《中华人民共和国民法典》', '《中华人民共和国合同法》', '《中华人民共和国劳动法》' ) ) ); } // 执行合同审查 public function review_contract($contract_content) { $results = array( 'overall_risk' => 'low', 'issues' => array(), 'statistics' => array( 'total_issues' => 0, 'high_risk' => 0, 'medium_risk' => 0, 'low_risk' => 0 ), 'recommendations' => array() ); // 对每个规则执行检查 foreach ($this->rules as $rule_id => $rule) { $method_name = $rule['check_method']; if (method_exists($this, $method_name)) { $rule_results = $this->$method_name($contract_content, $rule); if (!empty($rule_results)) { $results['issues'][$rule_id] = array( 'rule_name' => $rule['name'], 'risk_level' => $rule['risk_level'], 'description' => $rule['description'], 'findings' => $rule_results ); // 更新统计 $results['statistics']['total_issues']++; $results['statistics'][$rule['risk_level'] . '_risk']++; } } } // 确定总体风险等级 $results['overall_risk'] = $this->determine_overall_risk($results['statistics']); // 生成建议 $results['recommendations'] = $this->generate_recommendations($results['issues']); return $results; } // 检查缺失条款 private function check_missing_clauses($content, $rule) { $missing_clauses = array(); foreach ($rule['keywords'] as $keyword) { if (strpos($content, $keyword) === false) { $missing_clauses[] = $keyword; } } if (!empty($missing_clauses)) { return array( 'message' => '合同可能缺少以下关键条款:' . implode('、', $missing_clauses), 'suggestion' => '建议补充相关条款以明确双方权利义务' ); } return array(); } // 检查模糊表述 private function check_ambiguous_language($content, $rule) { $ambiguous_phrases = array(); foreach ($rule['patterns'] as $pattern) { if (preg_match_all($pattern, $content, $matches)) { $ambiguous_phrases = array_merge($ambiguous_phrases, $matches[0]); } } if (!empty($ambiguous_phrases)) { $unique_phrases = array_unique($ambiguous_phrases); return array( 'message' => '发现模糊表述:' . implode('、', array_slice($unique_phrases, 0, 5)), 'suggestion' => '建议将模糊表述具体化、量化,避免未来产生歧义' ); } return array(); } // 确定总体风险等级 private function determine_overall_risk($statistics) { if ($statistics['high_risk'] > 0) { return 'high'; } elseif ($statistics['medium_risk'] > 2) { return 'medium'; } elseif ($statistics['medium_risk'] > 0 || $statistics['low_risk'] > 3) { return 'low'; } else { return 'info'; } } // 生成建议 private function generate_recommendations($issues) { $recommendations = array(); // 高风险问题建议 if (isset($issues['missing_essential_clauses'])) { $recommendations[] = array( 'priority' => 'high', 'content' => '合同缺少关键条款,建议补充相关条款后再签署' ); } if (isset($issues['unbalanced_rights'])) { $recommendations[] = array( 'priority' => 'high', 'content' => '合同权利义务不对等,建议重新协商相关条款' ); } // 中风险问题建议 $medium_issues = array_filter($issues, function($issue) { return $issue['risk_level'] === 'medium'; }); if (!empty($medium_issues)) { $recommendations[] = array( 'priority' => 'medium', 'content' => '合同中存在多处需要明确的表述,建议进一步细化' ); } // 通用建议 $recommendations[] = array( 'priority' => 'info', 'content' => '建议咨询专业律师对合同进行最终审查' ); // 按优先级排序 usort($recommendations, function($a, $b) { $priority_order = array('high' => 3, 'medium' => 2, 'low' => 1, 'info' => 0); return $priority_order[$b['priority']] - $priority_order[$a['priority']]; }); return $recommendations; } } ### 3.3 审查结果可视化展示 将审查结果以直观的方式展示给用户: // 审查结果展示器class Contract_Review_Display { public function display_results($review_results) { $output = '<div class="contract-review-results">'; // 总体风险评估 $output .= $this->display_overall_risk($review_results['overall_risk']); // 问题列表 $output .= $this->display_issues_list($review_results['issues']); // 统计信息 $output .= $this->display_statistics($review_results['statistics']); // 建议 $output .= $this->display_recommendations($review_results['recommendations']); $output .= '</div>'; return $output; } // 显示总体风险评估 private function display_overall_risk($risk_level) { $risk_classes = array( 'high' => 'risk-high', 'medium' => 'risk-medium', 'low' => 'risk-low', 'info' => 'risk-info' ); $risk_labels = array( 'high' => '高风险', 'medium' => '中风险', 'low' => '低风险', 'info' => '信息提示' ); $class = isset($risk_classes[$risk_level]) ? $risk_classes[$risk_level] : 'risk-info'; $label = isset($risk_labels[$risk_level]) ? $risk_labels[$risk_level] : '待评估'; return sprintf( '<div class="overall-risk %s"> <h3>总体风险评估</h3> <div class="risk-indicator"> <span class="risk-level">%s</span> </div> </div>', esc_attr($class), esc_html($label) ); } // 显示问题列表 private function display_issues_list($issues) { if (empty($issues)) { return '<div class="no-issues"> <h3>审查结果</h3> <p>未发现明显风险问题,合同结构基本完整。</p> </div>'; } $output = '<div class="issues-list"> <h3>发现的问题</h3> <div class="issues-container">'; foreach ($issues as $issue_id => $issue) { $output .= $this->display_single_issue($issue_id, $issue); } $output .= '</div></div>'; return $output; } // 显示单个问题 private function display_single_issue($issue_id, $issue) { $risk_class = 'risk-' . $issue['risk_level']; $output = sprintf( '<div class="issue-item %s" id="issue-%s"> <div class="issue-header"> <h4>%s</h4> <span class="risk-badge">%s</span> </div> <div class="issue-description"> <p>%s</p>', esc_attr($risk_class), esc_attr($issue_id), esc_html($issue['rule_name']), esc_html($issue['risk_level'] === 'high' ? '高风险' : ($issue['risk_level'] === 'medium' ? '中风险' : '低风险')), esc_html($issue['description']) ); if (isset($issue['findings']['message'])) { $output .= sprintf( '<div class="issue-finding"> <strong>具体发现:</strong> <p>%s</p> </div>', esc_html($issue['findings']['message']) ); } if (isset($issue['findings']['suggestion'])) { $output .= sprintf( '<div class="issue-suggestion"> <strong>修改建议:</strong> <p>%s</p> </div>', esc_html($issue['findings']['suggestion']) ); } $output .= '</div></div>'; return $output; } // 显示统计信息 private function display_statistics($stats) { return sprintf( '<div class="review-statistics"> <h3>审查统计</h3> <div class="stats-grid"> <div class="stat-item"> <span class="stat-number">%d</span> <span class="stat-label">总问题数</span> </div> <div class="stat-item high-risk"> <span class="stat-number">%d</span> <span class="stat-label">高风险</span> </div> <div class="stat-item medium-risk"> <span class="stat-number">%d</span> <span class="stat-label">中风险</span> </div> <div class="stat-item low-risk"> <span class="stat-number">%d</span> <span class="stat-label">低风险</span> </div> </div> </div>', esc_html($stats['total_issues']), esc_html($stats['high_risk']), esc_html($stats['medium_risk']), esc_html($stats['low_risk']) ); } // 显示建议 private function display_recommendations($recommendations) { if (empty($recommendations)) { return ''; } $output = '<div class="recommendations"> <h3>修改建议</h3> <div class="recommendations-list">'; foreach ($recommendations as $index => $rec) { $output .= sprintf( '<div class="recommendation-item priority-%s"> <span class="recommendation-number">%d.</span> <div class="recommendation-content"> <p>%s</p> </div> </div>', esc_attr($rec['priority']), $index + 1, esc_html($rec['content']) ); } $output .= '</div></div>'; return $output; } } ## 第四部分:WordPress集成与用户界面 ### 4.1 创建短代码系统 为了方便在文章和页面中调用功能,我们创建短代码系统: // 短代码处理器class Legal_Tools_Shortcodes { public function __construct() { // 注册短代码 add_shortcode('legal_document_generator', array($this, 'document_generator_shortcode')); add_shortcode('contract_review_tool', array($this, 'contract_review_shortcode')); add_shortcode('legal_tools_dashboard', array($this, 'dashboard_shortcode')); } // 文书生成器短代码 public function document_generator_shortcode($atts) { $atts = shortcode_atts(array( 'template_id' => 0, 'category' => '', 'title' => '法律文书生成器' ), $atts, 'legal_document_generator'); ob_start(); // 检查用户权限 if (!is_user_logged_in()) { echo $this->login_prompt(); return ob_get_clean(); } // 获取模板 $template_id = intval($atts['template_id']); if ($template_id === 0 && !empty($atts['category'])) { $template_id = $this->get_template_by_category($atts['category']); } if ($template_id === 0) { // 显示模板选择界面 $this->display_template_selector(); } else { // 显示指定模板的表单 $this->display_document_form($template_id, $atts['title']); } return ob_get_clean(); } // 合同审查工具短代码 public function contract_review_shortcode($atts) { $atts = shortcode_atts(array( 'title' => '合同智能审查', 'max_size' => '10', 'allowed_types' => 'pdf,doc,docx,txt' ), $atts, 'contract_review_tool'); ob_start(); // 检查用户权限 if (!is_user_logged_in()) { echo $this->login_prompt(); return ob_get_clean(); } // 显示上传表单 $this->display_upload_form($atts); // 处理上传和审查 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_FILES['contract_file'])) { $this->process_contract_review(); } return ob_get_clean(); } // 仪表板短代码 public function dashboard_shortcode($atts) { $atts = shortcode_atts(array( 'show_documents' => 'true', 'show_reviews' => 'true', 'limit' => '10' ), $atts, 'legal_tools_dashboard'); ob_start(); // 检查用户权限 if (!is_user_logged_in()) { echo $this->login_prompt(); return ob_get_clean(); } $user_id = get_current

发表评论

WordPress插件开发教程,实现网站活动日历与票务验票核销系统

WordPress插件开发教程:实现网站活动日历与票务验票核销系统 引言:为什么选择WordPress进行功能扩展 在当今数字化时代,网站功能多样化已成为吸引用户、提升用户体验的关键因素。WordPress作为全球最受欢迎的内容管理系统,其强大的可扩展性为开发者提供了无限可能。通过插件开发,我们可以在不修改核心代码的前提下,为网站添加各种定制化功能。 本教程将详细讲解如何开发一个集活动日历与票务验票核销系统于一体的WordPress插件。这个插件不仅可以帮助网站管理者轻松发布和管理活动,还能实现电子票务的生成、验证和核销功能,适用于各类活动主办方、会议组织者和票务销售平台。 第一部分:开发环境准备与插件基础架构 1.1 开发环境配置 在开始开发之前,我们需要准备以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:最新版本的WordPress(建议5.6以上) 代码编辑器:VS Code、PHPStorm或Sublime Text 浏览器开发者工具:用于调试前端代码 1.2 创建插件基础文件 首先,在WordPress的wp-content/plugins目录下创建一个新文件夹,命名为event-ticket-system。在该文件夹中创建以下基础文件: event-ticket-system/ ├── event-ticket-system.php (主插件文件) ├── includes/ │ ├── class-database.php (数据库处理类) │ ├── class-events.php (活动管理类) │ ├── class-tickets.php (票务管理类) │ └── class-checkin.php (验票核销类) ├── admin/ │ ├── css/ (后台样式) │ ├── js/ (后台脚本) │ └── admin-pages.php (后台页面) ├── public/ │ ├── css/ (前端样式) │ ├── js/ (前端脚本) │ └── shortcodes.php (短代码处理) ├── templates/ (模板文件) ├── assets/ (静态资源) └── languages/ (国际化文件) 1.3 插件主文件结构 打开event-ticket-system.php文件,添加以下基础代码: <?php /** * Plugin Name: 活动日历与票务系统 * Plugin URI: https://yourwebsite.com/event-ticket-system * Description: 一个完整的活动日历与电子票务验票核销系统 * Version: 1.0.0 * Author: 你的名字 * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: event-ticket-system * Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('ETS_VERSION', '1.0.0'); define('ETS_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ETS_PLUGIN_URL', plugin_dir_url(__FILE__)); define('ETS_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { $prefix = 'ETS_'; $base_dir = ETS_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class_name, $len) !== 0) { return; } $relative_class = substr($class_name, $len); $file = $base_dir . 'class-' . strtolower(str_replace('_', '-', $relative_class)) . '.php'; if (file_exists($file)) { require_once $file; } }); // 初始化插件 function ets_init_plugin() { // 检查依赖 if (!function_exists('register_post_type')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>活动票务系统需要WordPress 5.0或更高版本。</p></div>'; }); return; } // 初始化数据库 ETS_Database::init(); // 初始化其他组件 if (is_admin()) { require_once ETS_PLUGIN_DIR . 'admin/admin-pages.php'; } // 加载前端功能 require_once ETS_PLUGIN_DIR . 'public/shortcodes.php'; // 加载文本域 load_plugin_textdomain('event-ticket-system', false, dirname(ETS_PLUGIN_BASENAME) . '/languages'); } add_action('plugins_loaded', 'ets_init_plugin'); // 激活插件时执行的操作 function ets_activate_plugin() { require_once ETS_PLUGIN_DIR . 'includes/class-database.php'; ETS_Database::create_tables(); // 设置默认选项 add_option('ets_version', ETS_VERSION); add_option('ets_currency', 'CNY'); add_option('ets_timezone', 'Asia/Shanghai'); // 刷新重写规则 flush_rewrite_rules(); } register_activation_hook(__FILE__, 'ets_activate_plugin'); // 停用插件时执行的操作 function ets_deactivate_plugin() { // 清理临时数据 // 注意:这里不删除数据表,以防数据丢失 flush_rewrite_rules(); } register_deactivation_hook(__FILE__, 'ets_deactivate_plugin'); 第二部分:数据库设计与活动管理 2.1 数据库表结构设计 在includes/class-database.php中,我们创建数据库表: <?php class ETS_Database { public static function init() { // 确保数据库表存在 self::create_tables(); } public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'ets_'; // 活动表 $events_table = $table_prefix . 'events'; $events_sql = "CREATE TABLE IF NOT EXISTS $events_table ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, post_id bigint(20) UNSIGNED NOT NULL, title varchar(255) NOT NULL, description text, start_date datetime NOT NULL, end_date datetime NOT NULL, venue varchar(255), address text, capacity int(11) DEFAULT 0, booked int(11) DEFAULT 0, price decimal(10,2) DEFAULT 0.00, currency varchar(10) DEFAULT 'CNY', status varchar(20) DEFAULT 'draft', created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY post_id (post_id), KEY start_date (start_date), KEY status (status) ) $charset_collate;"; // 票务表 $tickets_table = $table_prefix . 'tickets'; $tickets_sql = "CREATE TABLE IF NOT EXISTS $tickets_table ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, event_id bigint(20) UNSIGNED NOT NULL, ticket_number varchar(100) NOT NULL, attendee_name varchar(255) NOT NULL, attendee_email varchar(255) NOT NULL, attendee_phone varchar(50), quantity int(11) DEFAULT 1, total_price decimal(10,2) DEFAULT 0.00, payment_status varchar(20) DEFAULT 'pending', payment_method varchar(50), transaction_id varchar(100), checkin_status tinyint(1) DEFAULT 0, checkin_time datetime, checkin_by bigint(20) UNSIGNED, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY ticket_number (ticket_number), KEY event_id (event_id), KEY attendee_email (attendee_email), KEY checkin_status (checkin_status) ) $charset_collate;"; // 票务类型表 $ticket_types_table = $table_prefix . 'ticket_types'; $ticket_types_sql = "CREATE TABLE IF NOT EXISTS $ticket_types_table ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, event_id bigint(20) UNSIGNED NOT NULL, name varchar(255) NOT NULL, description text, price decimal(10,2) NOT NULL, quantity int(11) DEFAULT 0, sold int(11) DEFAULT 0, sale_start datetime, sale_end datetime, status varchar(20) DEFAULT 'active', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY event_id (event_id), KEY status (status) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($events_sql); dbDelta($tickets_sql); dbDelta($ticket_types_sql); } } 2.2 自定义文章类型:活动 在includes/class-events.php中创建活动管理类: <?php class ETS_Events { public function __construct() { add_action('init', array($this, 'register_event_post_type')); add_action('add_meta_boxes', array($this, 'add_event_meta_boxes')); add_action('save_post', array($this, 'save_event_meta_data')); add_filter('manage_event_posts_columns', array($this, 'add_event_columns')); add_action('manage_event_posts_custom_column', array($this, 'manage_event_columns'), 10, 2); } // 注册自定义文章类型:活动 public function register_event_post_type() { $labels = array( 'name' => __('活动', 'event-ticket-system'), 'singular_name' => __('活动', 'event-ticket-system'), 'menu_name' => __('活动管理', 'event-ticket-system'), 'add_new' => __('添加活动', 'event-ticket-system'), 'add_new_item' => __('添加新活动', 'event-ticket-system'), 'edit_item' => __('编辑活动', 'event-ticket-system'), 'new_item' => __('新活动', 'event-ticket-system'), 'view_item' => __('查看活动', 'event-ticket-system'), 'search_items' => __('搜索活动', 'event-ticket-system'), 'not_found' => __('未找到活动', 'event-ticket-system'), 'not_found_in_trash' => __('回收站中无活动', 'event-ticket-system'), ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'event'), '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('event', $args); // 注册活动分类 $category_labels = array( 'name' => __('活动分类', 'event-ticket-system'), 'singular_name' => __('活动分类', 'event-ticket-system'), 'search_items' => __('搜索分类', 'event-ticket-system'), 'all_items' => __('所有分类', 'event-ticket-system'), 'parent_item' => __('父分类', 'event-ticket-system'), 'parent_item_colon' => __('父分类:', 'event-ticket-system'), 'edit_item' => __('编辑分类', 'event-ticket-system'), 'update_item' => __('更新分类', 'event-ticket-system'), 'add_new_item' => __('添加新分类', 'event-ticket-system'), 'new_item_name' => __('新分类名称', 'event-ticket-system'), 'menu_name' => __('活动分类', 'event-ticket-system'), ); register_taxonomy('event_category', 'event', array( 'labels' => $category_labels, 'hierarchical' => true, 'public' => true, 'show_ui' => true, 'show_admin_column' => true, 'show_in_rest' => true, 'query_var' => true, 'rewrite' => array('slug' => 'event-category'), )); } // 添加活动元数据框 public function add_event_meta_boxes() { add_meta_box( 'event_details', __('活动详情', 'event-ticket-system'), array($this, 'render_event_details_meta_box'), 'event', 'normal', 'high' ); add_meta_box( 'ticket_settings', __('票务设置', 'event-ticket-system'), array($this, 'render_ticket_settings_meta_box'), 'event', 'normal', 'high' ); } // 渲染活动详情元数据框 public function render_event_details_meta_box($post) { wp_nonce_field('event_details_nonce', 'event_details_nonce_field'); $start_date = get_post_meta($post->ID, '_event_start_date', true); $end_date = get_post_meta($post->ID, '_event_end_date', true); $venue = get_post_meta($post->ID, '_event_venue', true); $address = get_post_meta($post->ID, '_event_address', true); $capacity = get_post_meta($post->ID, '_event_capacity', true); ?> <div class="event-details-container"> <table class="form-table"> <tr> <th><label for="event_start_date"><?php _e('开始时间', 'event-ticket-system'); ?></label></th> <td> <input type="datetime-local" id="event_start_date" name="event_start_date" value="<?php echo esc_attr($start_date); ?>" class="regular-text" required> </td> </tr> <tr> <th><label for="event_end_date"><?php _e('结束时间', 'event-ticket-system'); ?></label></th> <td> <input type="datetime-local" id="event_end_date" name="event_end_date" value="<?php echo esc_attr($end_date); ?>" class="regular-text" required> </td> </tr> <tr> <th><label for="event_venue"><?php _e('活动地点', 'event-ticket-system'); ?></label></th> <td> <input type="text" id="event_venue" name="event_venue" value="<?php echo esc_attr($venue); ?>" class="regular-text"> </td> </tr> <tr> <th><label for="event_address"><?php _e('详细地址', 'event-ticket-system'); ?></label></th> <td> <textarea id="event_address" name="event_address" rows="3" class="large-text"><?php echo esc_textarea($address); ?></textarea> </td> </tr> <tr> <th><label for="event_capacity"><?php _e('活动容量', 'event-ticket-system'); ?></label></th> <td> <input type="number" id="event_capacity" name="event_capacity" value="<?php echo esc_attr($capacity); ?>" min="0" class="small-text"> <p class="description"><?php _e('0表示无限制', 'event-ticket-system'); ?></p> </td> </tr> </table> </div> <?php } // 保存活动元数据 public function save_event_meta_data($post_id) { if (!isset($_POST['event_details_nonce_field']) || !wp_verify_nonce($_POST['event_details_nonce_field'], 'event_details_nonce')) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (!current_user_can('edit_post', $post_id)) { return; } $fields = array( 'event_start_date', 'event_end_date', 'event_venue', 'event_address', 'event_capacity' ); foreach ($fields as $field) { if (isset($_POST[$field])) { update_post_meta($post_id, '_' . $field, sanitize_text_field($_POST[$field])); } } // 同步到自定义数据库表 $this->sync_event_to_custom_table($post_id); } // 同步活动数据到自定义表 private function sync_event_to_custom_table($post_id) { global $wpdb; $post = get_post($post_id); if ($post->post_type !== 'event') { return; } $table_name = $wpdb->prefix . 'ets_events'; $event_data = array( 'post_id' => $post_id, 'title' => $post->post_title, 'description' => $post->post_content, 'start_date' => get_post_meta($post_id, '_event_start_date', true), 第三部分:票务系统与验票核销功能 3.1 票务管理类实现 在includes/class-tickets.php中创建票务管理类: <?php class ETS_Tickets { private $db; public function __construct() { global $wpdb; $this->db = $wpdb; add_action('wp_ajax_ets_purchase_ticket', array($this, 'handle_ticket_purchase')); add_action('wp_ajax_nopriv_ets_purchase_ticket', array($this, 'handle_ticket_purchase')); add_action('wp_ajax_ets_generate_ticket', array($this, 'generate_ticket')); add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_scripts')); } // 生成唯一票号 public function generate_ticket_number($event_id) { $prefix = 'TICKET'; $timestamp = time(); $random = mt_rand(1000, 9999); return $prefix . '-' . $event_id . '-' . $timestamp . '-' . $random; } // 处理购票请求 public function handle_ticket_purchase() { // 验证nonce if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'ets_ticket_nonce')) { wp_send_json_error(array('message' => __('安全验证失败', 'event-ticket-system'))); } // 验证必填字段 $required_fields = array('event_id', 'ticket_type_id', 'attendee_name', 'attendee_email', 'quantity'); foreach ($required_fields as $field) { if (empty($_POST[$field])) { wp_send_json_error(array('message' => sprintf(__('缺少必填字段: %s', 'event-ticket-system'), $field))); } } $event_id = intval($_POST['event_id']); $ticket_type_id = intval($_POST['ticket_type_id']); $attendee_name = sanitize_text_field($_POST['attendee_name']); $attendee_email = sanitize_email($_POST['attendee_email']); $attendee_phone = isset($_POST['attendee_phone']) ? sanitize_text_field($_POST['attendee_phone']) : ''; $quantity = intval($_POST['quantity']); $payment_method = isset($_POST['payment_method']) ? sanitize_text_field($_POST['payment_method']) : 'online'; // 检查活动是否存在 $event = $this->get_event($event_id); if (!$event) { wp_send_json_error(array('message' => __('活动不存在', 'event-ticket-system'))); } // 检查票务类型 $ticket_type = $this->get_ticket_type($ticket_type_id); if (!$ticket_type || $ticket_type->event_id != $event_id) { wp_send_json_error(array('message' => __('票务类型无效', 'event-ticket-system'))); } // 检查余票 if ($ticket_type->quantity > 0 && ($ticket_type->sold + $quantity) > $ticket_type->quantity) { wp_send_json_error(array('message' => __('余票不足', 'event-ticket-system'))); } // 计算总价 $total_price = $ticket_type->price * $quantity; // 开始事务 $this->db->query('START TRANSACTION'); try { // 更新已售数量 $update_result = $this->db->update( $this->db->prefix . 'ets_ticket_types', array('sold' => $ticket_type->sold + $quantity), array('id' => $ticket_type_id), array('%d'), array('%d') ); if (!$update_result) { throw new Exception(__('更新票务数据失败', 'event-ticket-system')); } // 创建票务记录 $ticket_data = array(); for ($i = 0; $i < $quantity; $i++) { $ticket_number = $this->generate_ticket_number($event_id); $ticket_data[] = array( 'event_id' => $event_id, 'ticket_type_id' => $ticket_type_id, 'ticket_number' => $ticket_number, 'attendee_name' => $attendee_name, 'attendee_email' => $attendee_email, 'attendee_phone' => $attendee_phone, 'quantity' => 1, 'total_price' => $ticket_type->price, 'payment_status' => 'pending', 'payment_method' => $payment_method, 'created_at' => current_time('mysql') ); } // 批量插入票务记录 foreach ($ticket_data as $data) { $insert_result = $this->db->insert( $this->db->prefix . 'ets_tickets', $data, array('%d', '%d', '%s', '%s', '%s', '%s', '%d', '%f', '%s', '%s', '%s') ); if (!$insert_result) { throw new Exception(__('创建票务记录失败', 'event-ticket-system')); } $ticket_id = $this->db->insert_id; // 发送确认邮件 $this->send_ticket_confirmation_email($ticket_id, $data['ticket_number']); } // 提交事务 $this->db->query('COMMIT'); // 返回成功响应 wp_send_json_success(array( 'message' => __('购票成功!请查收确认邮件。', 'event-ticket-system'), 'ticket_numbers' => array_column($ticket_data, 'ticket_number') )); } catch (Exception $e) { // 回滚事务 $this->db->query('ROLLBACK'); wp_send_json_error(array('message' => $e->getMessage())); } } // 发送票务确认邮件 private function send_ticket_confirmation_email($ticket_id, $ticket_number) { $ticket = $this->get_ticket_by_number($ticket_number); if (!$ticket) { return false; } $event = $this->get_event($ticket->event_id); $attendee_email = $ticket->attendee_email; $subject = sprintf(__('【%s】活动票务确认', 'event-ticket-system'), get_bloginfo('name')); $message = sprintf(__('尊敬的 %s:', 'event-ticket-system'), $ticket->attendee_name) . "nn"; $message .= __('感谢您购买活动票务,以下是您的票务信息:', 'event-ticket-system') . "nn"; $message .= __('活动名称:', 'event-ticket-system') . $event->title . "n"; $message .= __('活动时间:', 'event-ticket-system') . $event->start_date . "n"; $message .= __('活动地点:', 'event-ticket-system') . $event->venue . "n"; $message .= __('票务号码:', 'event-ticket-system') . $ticket->ticket_number . "n"; $message .= __('购票数量:', 'event-ticket-system') . $ticket->quantity . "n"; $message .= __('总金额:', 'event-ticket-system') . $ticket->total_price . ' ' . $event->currency . "nn"; $message .= __('验票二维码:', 'event-ticket-system') . "n"; $message .= site_url('/check-ticket?code=' . urlencode($ticket->ticket_number)) . "nn"; $message .= __('注意事项:', 'event-ticket-system') . "n"; $message .= __('1. 请妥善保管此邮件,活动当天凭票务二维码入场', 'event-ticket-system') . "n"; $message .= __('2. 如需退票,请在活动开始前24小时联系客服', 'event-ticket-system') . "n"; $message .= __('3. 如有疑问,请回复此邮件咨询', 'event-ticket-system') . "nn"; $message .= __('祝您活动愉快!', 'event-ticket-system') . "n"; $message .= get_bloginfo('name') . "n"; $message .= date('Y-m-d'); $headers = array('Content-Type: text/plain; charset=UTF-8'); return wp_mail($attendee_email, $subject, $message, $headers); } // 获取活动信息 private function get_event($event_id) { $table_name = $this->db->prefix . 'ets_events'; return $this->db->get_row($this->db->prepare( "SELECT * FROM $table_name WHERE id = %d", $event_id )); } // 获取票务类型 private function get_ticket_type($ticket_type_id) { $table_name = $this->db->prefix . 'ets_ticket_types'; return $this->db->get_row($this->db->prepare( "SELECT * FROM $table_name WHERE id = %d", $ticket_type_id )); } // 根据票号获取票务信息 private function get_ticket_by_number($ticket_number) { $table_name = $this->db->prefix . 'ets_tickets'; return $this->db->get_row($this->db->prepare( "SELECT * FROM $table_name WHERE ticket_number = %s", $ticket_number )); } // 加载前端脚本 public function enqueue_frontend_scripts() { if (is_singular('event')) { wp_enqueue_script( 'ets-ticket-script', ETS_PLUGIN_URL . 'public/js/ticket-purchase.js', array('jquery'), ETS_VERSION, true ); wp_localize_script('ets-ticket-script', 'ets_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('ets_ticket_nonce') )); wp_enqueue_style( 'ets-ticket-style', ETS_PLUGIN_URL . 'public/css/ticket-style.css', array(), ETS_VERSION ); } } } 3.2 验票核销系统 在includes/class-checkin.php中创建验票核销类: <?php class ETS_Checkin { private $db; public function __construct() { global $wpdb; $this->db = $wpdb; add_action('admin_menu', array($this, 'add_checkin_page')); add_action('wp_ajax_ets_check_ticket', array($this, 'check_ticket')); add_action('wp_ajax_ets_validate_ticket', array($this, 'validate_ticket')); add_action('init', array($this, 'register_checkin_shortcode')); } // 添加验票管理页面 public function add_checkin_page() { add_submenu_page( 'edit.php?post_type=event', __('验票核销', 'event-ticket-system'), __('验票核销', 'event-ticket-system'), 'manage_options', 'event-checkin', array($this, 'render_checkin_page') ); } // 渲染验票页面 public function render_checkin_page() { ?> <div class="wrap"> <h1><?php _e('活动票务验票核销系统', 'event-ticket-system'); ?></h1> <div class="checkin-container"> <div class="checkin-scanner"> <h2><?php _e('扫码验票', 'event-ticket-system'); ?></h2> <div id="qr-scanner" style="width: 400px; height: 300px; margin: 20px 0;"> <!-- QR扫码器将在这里渲染 --> </div> <div class="manual-checkin"> <h3><?php _e('手动输入票号', 'event-ticket-system'); ?></h3> <input type="text" id="manual-ticket-number" placeholder="<?php _e('输入票务号码', 'event-ticket-system'); ?>" style="width: 300px;"> <button id="manual-check-btn" class="button button-primary"><?php _e('验证', 'event-ticket-system'); ?></button> </div> </div> <div class="checkin-results"> <h2><?php _e('验票结果', 'event-ticket-system'); ?></h2> <div id="checkin-result" style="padding: 20px; border: 1px solid #ddd; min-height: 200px;"> <?php _e('等待验票...', 'event-ticket-system'); ?> </div> <div class="checkin-stats"> <h3><?php _e('今日统计', 'event-ticket-system'); ?></h3> <?php $this->display_daily_stats(); ?> </div> </div> </div> <div class="recent-checkins"> <h2><?php _e('最近验票记录', 'event-ticket-system'); ?></h2> <?php $this->display_recent_checkins(); ?> </div> </div> <script> jQuery(document).ready(function($) { // 初始化QR扫码器 initQRScanner(); // 手动验票 $('#manual-check-btn').click(function() { var ticketNumber = $('#manual-ticket-number').val(); if (ticketNumber) { validateTicket(ticketNumber); } }); // 回车键触发验票 $('#manual-ticket-number').keypress(function(e) { if (e.which == 13) { $('#manual-check-btn').click(); } }); }); function initQRScanner() { // 这里可以集成第三方QR扫码库,如Html5Qrcode console.log('QR扫码器初始化'); } function validateTicket(ticketNumber) { jQuery.ajax({ url: ajaxurl, type: 'POST', data: { action: 'ets_validate_ticket', ticket_number: ticketNumber, nonce: '<?php echo wp_create_nonce("ets_checkin_nonce"); ?>' }, success: function(response) { if (response.success) { $('#checkin-result').html(response.data.message); if (response.data.checked_in) { $('#checkin-result').addClass('success').removeClass('error'); } else { $('#checkin-result').addClass('error').removeClass('success'); } } else { $('#checkin-result').html(response.data).addClass('error'); } } }); } </script> <style> .checkin-container { display: flex; gap: 30px; margin: 20px 0; } .checkin-scanner { flex: 1; } .checkin-results { flex: 1; } .success { background-color: #d4edda; border-color: #c3e6cb; color: #155724; } .error { background-color: #f8d7da; border-color: #f5c6cb; color: #721c24; } </style> <?php } // 验证票务 public function validate_ticket() { // 验证nonce if (!isset($_POST['nonce']) || !wp_verify_nonce($_POST['nonce'], 'ets_checkin_nonce')) { wp_send_json_error(__('安全验证失败', 'event-ticket-system')); } $ticket_number = sanitize_text_field($_POST['ticket_number']); if (empty($ticket_number)) { wp_send_json_error(__('请输入票务号码', 'event-ticket-system')); } // 查询票务信息 $ticket = $this->get_ticket_details($ticket_number); if (!$ticket) { wp_send_json_error(__('票务号码无效', 'event-ticket-system')); } // 检查是否已验票 if ($ticket->checkin_status) { $message = sprintf( __('该票务已于 %s 验票通过<br>验票人员:%s', 'event-ticket-system'), $ticket->checkin_time, get_userdata($ticket->checkin_by)->display_name ); wp_send_json_success(array( 'message' => $message, 'checked_in' => true, 'ticket' => $ticket )); } // 检查活动是否已开始 $event = $this->get_event($ticket->event_id); $current_time = current_time('mysql'); if (strtotime($current_time) < strtotime($event->start_date)) { wp_send_json_error(__('活动尚未开始', 'event-ticket-system')); } // 执行验票 $result = $this->perform_checkin($ticket->id); if ($result) { $message = sprintf( __('验票成功!<br>票务号码:%s<br>参会人:%s<br>验票时间:%s', 'event-ticket-system'), $ticket->ticket_number, $ticket->attendee_name, current_time('mysql') ); wp_send_json_success(array( 'message' => $message, 'checked_in' => false,

发表评论

实战教程,在网站中添加在线食谱管理与食材采购清单工具

实战教程:在WordPress网站中添加在线食谱管理与食材采购清单工具 引言:为什么网站需要实用小工具? 在当今互联网时代,网站的功能性已成为吸引和留住用户的关键因素。对于美食、生活类网站而言,提供实用工具不仅能增加用户粘性,还能显著提升用户体验。想象一下,当用户在你的美食博客上找到心仪的食谱后,能够直接保存到个人收藏夹,并一键生成食材采购清单,这将大大简化他们的烹饪准备过程。 本教程将详细指导您如何通过WordPress代码二次开发,为您的网站添加在线食谱管理与食材采购清单工具。我们将从零开始,逐步构建这两个实用功能,无需依赖昂贵的插件,完全自主控制功能与样式。 第一部分:准备工作与环境搭建 1.1 开发环境要求 在开始之前,请确保您的开发环境满足以下要求: WordPress 5.0或更高版本 PHP 7.2或更高版本(建议7.4+) MySQL 5.6或更高版本 基本的HTML、CSS、JavaScript和PHP知识 代码编辑器(如VS Code、Sublime Text等) 本地开发环境(如XAMPP、MAMP或Local by Flywheel) 1.2 创建子主题 为了避免主题更新时丢失自定义代码,我们首先创建一个子主题: 在WordPress的wp-content/themes/目录下创建新文件夹,命名为my-cooking-tools 在该文件夹中创建style.css文件,添加以下内容: /* Theme Name: My Cooking Tools Theme URI: https://yourwebsite.com Description: 子主题用于添加食谱管理功能 Author: Your Name Author URI: https://yourwebsite.com Template: your-parent-theme // 替换为您的父主题名称 Version: 1.0.0 */ /* 导入父主题样式 */ @import url("../your-parent-theme/style.css"); 创建functions.php文件,添加以下基础代码: <?php // 子主题功能文件 // 添加父主题样式 add_action('wp_enqueue_scripts', 'my_cooking_tools_enqueue_styles'); function my_cooking_tools_enqueue_styles() { wp_enqueue_style('parent-style', get_template_directory_uri() . '/style.css'); wp_enqueue_style('child-style', get_stylesheet_directory_uri() . '/style.css', array('parent-style')); } // 在这里添加自定义功能代码 ?> 在WordPress后台启用这个子主题 第二部分:数据库设计与数据模型 2.1 创建自定义数据库表 我们需要创建两个自定义表来存储食谱和食材清单数据。在子主题的functions.php文件中添加以下代码: // 创建自定义数据库表 register_activation_hook(__FILE__, 'cooking_tools_create_tables'); function cooking_tools_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $recipes_table = $wpdb->prefix . 'user_recipes'; $ingredients_table = $wpdb->prefix . 'recipe_ingredients'; $shopping_lists_table = $wpdb->prefix . 'shopping_lists'; // 用户食谱表 $recipes_sql = "CREATE TABLE IF NOT EXISTS $recipes_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, recipe_title varchar(255) NOT NULL, recipe_content longtext NOT NULL, prep_time int(11), cook_time int(11), servings int(11), difficulty varchar(50), category varchar(100), tags text, featured_image varchar(255), created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id) ) $charset_collate;"; // 食谱食材表 $ingredients_sql = "CREATE TABLE IF NOT EXISTS $ingredients_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, recipe_id mediumint(9) NOT NULL, ingredient_name varchar(255) NOT NULL, quantity decimal(10,2), unit varchar(50), notes text, sort_order int(11) DEFAULT 0, PRIMARY KEY (id), KEY recipe_id (recipe_id) ) $charset_collate;"; // 采购清单表 $shopping_lists_sql = "CREATE TABLE IF NOT EXISTS $shopping_lists_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, list_name varchar(255) NOT NULL, ingredients text NOT NULL, status varchar(50) DEFAULT 'active', created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($recipes_sql); dbDelta($ingredients_sql); dbDelta($shopping_lists_sql); } 2.2 数据模型类 为了更好的代码组织,我们创建一个数据模型类来处理数据库操作: // 食谱管理数据模型类 class CookingTools_Model { private $wpdb; private $recipes_table; private $ingredients_table; private $shopping_lists_table; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->recipes_table = $wpdb->prefix . 'user_recipes'; $this->ingredients_table = $wpdb->prefix . 'recipe_ingredients'; $this->shopping_lists_table = $wpdb->prefix . 'shopping_lists'; } // 获取用户食谱 public function get_user_recipes($user_id, $limit = 20, $offset = 0) { $sql = $this->wpdb->prepare( "SELECT * FROM {$this->recipes_table} WHERE user_id = %d ORDER BY created_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset ); return $this->wpdb->get_results($sql); } // 获取单个食谱 public function get_recipe($recipe_id, $user_id = null) { if ($user_id) { $sql = $this->wpdb->prepare( "SELECT * FROM {$this->recipes_table} WHERE id = %d AND user_id = %d", $recipe_id, $user_id ); } else { $sql = $this->wpdb->prepare( "SELECT * FROM {$this->recipes_table} WHERE id = %d", $recipe_id ); } return $this->wpdb->get_row($sql); } // 保存食谱 public function save_recipe($data) { $defaults = array( 'user_id' => get_current_user_id(), 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ); $data = wp_parse_args($data, $defaults); if (isset($data['id']) && $data['id'] > 0) { // 更新现有食谱 $recipe_id = $data['id']; unset($data['id']); $this->wpdb->update($this->recipes_table, $data, array('id' => $recipe_id)); return $recipe_id; } else { // 插入新食谱 unset($data['id']); $this->wpdb->insert($this->recipes_table, $data); return $this->wpdb->insert_id; } } // 获取食谱食材 public function get_recipe_ingredients($recipe_id) { $sql = $this->wpdb->prepare( "SELECT * FROM {$this->ingredients_table} WHERE recipe_id = %d ORDER BY sort_order ASC", $recipe_id ); return $this->wpdb->get_results($sql); } // 保存食谱食材 public function save_recipe_ingredients($recipe_id, $ingredients) { // 先删除旧的食材 $this->wpdb->delete($this->ingredients_table, array('recipe_id' => $recipe_id)); // 插入新食材 foreach ($ingredients as $index => $ingredient) { $ingredient_data = array( 'recipe_id' => $recipe_id, 'ingredient_name' => sanitize_text_field($ingredient['name']), 'quantity' => floatval($ingredient['quantity']), 'unit' => sanitize_text_field($ingredient['unit']), 'notes' => sanitize_text_field($ingredient['notes']), 'sort_order' => $index ); $this->wpdb->insert($this->ingredients_table, $ingredient_data); } } // 获取用户采购清单 public function get_user_shopping_lists($user_id) { $sql = $this->wpdb->prepare( "SELECT * FROM {$this->shopping_lists_table} WHERE user_id = %d ORDER BY created_at DESC", $user_id ); return $this->wpdb->get_results($sql); } // 保存采购清单 public function save_shopping_list($data) { $defaults = array( 'user_id' => get_current_user_id(), 'status' => 'active', 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ); $data = wp_parse_args($data, $defaults); if (isset($data['id']) && $data['id'] > 0) { // 更新现有清单 $list_id = $data['id']; unset($data['id']); $this->wpdb->update($this->shopping_lists_table, $data, array('id' => $list_id)); return $list_id; } else { // 插入新清单 unset($data['id']); $this->wpdb->insert($this->shopping_lists_table, $data); return $this->wpdb->insert_id; } } // 删除食谱 public function delete_recipe($recipe_id, $user_id) { // 先删除相关食材 $this->wpdb->delete($this->ingredients_table, array('recipe_id' => $recipe_id)); // 再删除食谱 return $this->wpdb->delete($this->recipes_table, array('id' => $recipe_id, 'user_id' => $user_id) ); } } 第三部分:前端界面设计与开发 3.1 创建食谱管理页面模板 在子主题目录中创建recipe-manager.php文件: <?php /** * Template Name: 食谱管理器 */ get_header(); ?> <div class="cooking-tools-container"> <div class="recipe-manager-wrapper"> <header class="recipe-manager-header"> <h1><?php the_title(); ?></h1> <div class="user-actions"> <?php if (is_user_logged_in()): ?> <button id="add-new-recipe" class="btn btn-primary">添加新食谱</button> <a href="#shopping-lists" class="btn btn-secondary">我的采购清单</a> <?php else: ?> <p>请<a href="<?php echo wp_login_url(get_permalink()); ?>">登录</a>以使用食谱管理功能</p> <?php endif; ?> </div> </header> <?php if (is_user_logged_in()): ?> <div class="recipe-manager-content"> <!-- 食谱列表区域 --> <section id="recipes-list" class="recipes-section"> <h2>我的食谱</h2> <div class="recipes-filter"> <input type="text" id="recipe-search" placeholder="搜索食谱..." class="search-input"> <select id="category-filter" class="filter-select"> <option value="">所有分类</option> <option value="早餐">早餐</option> <option value="午餐">午餐</option> <option value="晚餐">晚餐</option> <option value="甜点">甜点</option> <option value="饮品">饮品</option> </select> </div> <div class="recipes-grid" id="recipes-container"> <!-- 食谱将通过AJAX加载 --> <div class="loading-spinner">加载中...</div> </div> <div class="pagination" id="recipes-pagination"></div> </section> <!-- 食谱编辑/添加区域 --> <section id="recipe-editor" class="editor-section" style="display:none;"> <h2 id="editor-title">添加新食谱</h2> <form id="recipe-form" class="recipe-form"> <input type="hidden" id="recipe-id" name="recipe_id" value="0"> <div class="form-group"> <label for="recipe-title">食谱名称 *</label> <input type="text" id="recipe-title" name="recipe_title" required class="form-control"> </div> <div class="form-row"> <div class="form-group"> <label for="prep-time">准备时间 (分钟)</label> <input type="number" id="prep-time" name="prep_time" min="0" class="form-control"> </div> <div class="form-group"> <label for="cook-time">烹饪时间 (分钟)</label> <input type="number" id="cook-time" name="cook_time" min="0" class="form-control"> </div> <div class="form-group"> <label for="servings">份量</label> <input type="number" id="servings" name="servings" min="1" class="form-control"> </div> </div> <div class="form-group"> <label for="difficulty">难度</label> <select id="difficulty" name="difficulty" class="form-control"> <option value="">选择难度</option> <option value="简单">简单</option> <option value="中等">中等</option> <option value="困难">困难</option> </select> </div> <div class="form-group"> <label for="category">分类</label> <select id="category" name="category" class="form-control"> <option value="">选择分类</option> <option value="早餐">早餐</option> <option value="午餐">午餐</option> <option value="晚餐">晚餐</option> <option value="甜点">甜点</option> <option value="饮品">饮品</option> <option value="其他">其他</option> </select> </div> <div class="form-group"> <label for="recipe-content">食谱步骤 *</label> <textarea id="recipe-content" name="recipe_content" rows="8" required class="form-control"></textarea> <small class="form-text">请详细描述烹饪步骤,每一步用换行分隔</small> </div> <!-- 食材管理部分 --> <div class="form-group"> <label>食材清单</label> <div class="ingredients-container" id="ingredients-container"> <div class="ingredient-row"> <input type="text" class="ingredient-name" placeholder="食材名称" name="ingredients[0][name]"> <input type="number" step="0.01" class="ingredient-quantity" placeholder="数量" name="ingredients[0][quantity]"> <input type="text" class="ingredient-unit" placeholder="单位 (克/个/汤匙等)" name="ingredients[0][unit]"> <input type="text" class="ingredient-notes" placeholder="备注" name="ingredients[0][notes]"> <button type="button" class="btn-remove-ingredient">×</button> </div> </div> <button type="button" id="add-ingredient" class="btn btn-secondary">添加食材</button> </div> <div class="form-group"> <label for="tags">标签</label> <input type="text" id="tags" name="tags" class="form-control" placeholder="用逗号分隔标签,如:中式,辣味,健康"> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary">保存食谱</button> <button type="button" id="cancel-edit" class="btn btn-secondary">取消</button> <button type="button" id="generate-shopping-list" class="btn btn-success" style="display:none;">生成采购清单</button> </div> </form> </section> <!-- 采购清单区域 --> <section id="shopping-lists" class="shopping-section" style="display:none;"> <h2>我的采购清单</h2> <div class="shopping-lists-container" id="shopping-lists-container"> <!-- 采购清单将通过AJAX加载 --> </div> <div class="shopping-list-editor" id="shopping-list-editor" style="display:none;"> <h3 id="shopping-list-title">新建采购清单</h3> <form id="shopping-list-form"> <input type="hidden" id="shopping-list-id" name="list_id" value="0"> <div class="form-group"> <label for="list-name">清单名称</label> <input type="text" id="list-name" name="list_name" required class="form-control"> </div> <div class="form-group"> <label>清单内容</label> <div class="shopping-items-container" id="shopping-items-container"> <div class="shopping-item-row"> <input type="text" class="shopping-item-name" placeholder="物品名称" name="items[0][name]"> <input type="number" step="0.01" class="shopping-item-quantity" placeholder="数量" name="items[0][quantity]"> <input type="text" class="shopping-item-unit" placeholder="单位" name="items[0][unit]"> <div class="item-checkbox"> <input type="checkbox" class="shopping-item-checked" name="items[0][checked]"> <label>已购</label> </div> <button type="button" class="btn-remove-item">×</button> </div> </div> <button type="button" id="add-shopping-item" class="btn btn-secondary">添加物品</button> </div> <div class="form-actions"> <button type="submit" class="btn btn-primary">保存清单</button> <button type="button" id="cancel-shopping-edit" class="btn btn-secondary">取消</button> </div> </form> </div> </section> </div> <?php endif; ?> </div> </div> <?php get_footer(); ?> ### 3.2 添加CSS样式 在子主题的`style.css`文件中添加以下样式: / 食谱管理工具样式 / / 容器样式 /.cooking-tools-container { max-width: 1200px; margin: 0 auto; padding: 20px; } .recipe-manager-wrapper { background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; } / 头部样式 /.recipe-manager-header { background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 30px; text-align: center; } .recipe-manager-header h1 { margin: 0 0 20px 0; font-size: 2.5em; } .user-actions { display: flex; justify-content: center; gap: 15px; flex-wrap: wrap; } / 按钮样式 /.btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; transition: all 0.3s ease; text-decoration: none; display: inline-block; } .btn-primary { background-color: #4CAF50; color: white; } .btn-primary:hover { background-color: #45a049; transform: translateY(-2px); } .btn-secondary { background-color: #f0f0f0; color: #333; } .btn-secondary:hover { background-color: #e0e0e0; } .btn-success { background-color: #2196F3; color: white; } .btn-success:hover { background-color: #0b7dda; } / 内容区域 /.recipe-manager-content { padding: 30px; } / 食谱列表样式 /.recipes-section, .editor-section, .shopping-section { margin-bottom: 40px; } .recipes-section h2, .editor-section h2, .shopping-section h2 { color: #333; border-bottom: 2px solid #f0f0f0; padding-bottom: 10px; margin-bottom: 20px; } / 筛选区域 /.recipes-filter { display: flex; gap: 15px; margin-bottom: 20px; flex-wrap: wrap; } .search-input, .filter-select, .form-control { padding: 10px 15px; border: 1px solid #ddd; border-radius: 4px; font-size: 16px; flex: 1; min-width: 200px; } / 食谱网格 /.recipes-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px; } .recipe-card { background: #fff; border-radius: 8px; overflow: hidden; box-shadow: 0 3px 10px rgba(0,0,0,0.1); transition: transform 0.3s ease, box-shadow 0.3s ease; border: 1px solid #eee; } .recipe-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.15); } .recipe-image { height: 180px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); display: flex; align-items: center; justify-content: center; color: #666; } .recipe-info { padding: 20px; } .recipe-title { font-size: 1.3em; margin: 0 0 10px 0; color: #333; } .recipe-meta { display: flex; gap: 15px; margin-bottom: 15px; color: #666; font-size: 0.9em; } .recipe-meta span { display: flex; align-items: center; gap: 5px; } .recipe-actions { display: flex; gap: 10px; margin-top: 15px; } / 表单样式 /.form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 8px; font-weight: 600; color: #333; } .form-row { display: flex; gap: 20px; flex-wrap: wrap; } .form-row .form-group { flex: 1; min-width: 200px; } / 食材管理 /.ingredients-container, .shopping-items-container { border: 1px solid #ddd; border-radius: 4px; padding: 15px; margin-bottom: 15px; max-height: 300px; overflow-y: auto; } .ingredient-row, .shopping-item-row { display: flex; gap: 10px; margin-bottom: 10px; align-items: center; flex-wrap: wrap; } .ingredient-row input, .shopping-item-row input { flex: 1; min-width: 150px; padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; } .ingredient-name, .shopping-item-name { min-width: 200px !important; } .btn-remove-ingredient, .btn-remove-item { background: #ff6b6b; color: white; border: none; border-radius: 50%; width: 30px; height: 30px; cursor: pointer; font-size: 18px; display: flex; align-items: center; justify-content: center; } .btn-remove-ingredient:hover, .btn-remove-item:hover { background: #ff5252; } / 采购清单样式 /.shopping-lists-container { display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px; } .shopping-list-card { background: #fff; border-radius: 8px; padding: 20px; box-shadow: 0 3px 10px rgba(0,0,0,0.1); border-left: 4px solid #4CAF50; } .shopping-list-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .shopping-list-title { font-size: 1.2em; margin: 0; color: #333; } .shopping-list-date { color: #666; font-size: 0.9em; } .shopping-items-list { margin: 15px 0; } .shopping-item { display: flex; align-items: center; padding: 8px 0; border-bottom: 1px solid #f0f0f0; } .shopping-item:last-child { border-bottom: none; } .shopping-item.checked { opacity: 0.6; text-decoration: line-through; } .item-checkbox { margin-left: auto; display: flex; align-items: center; gap: 8px; } / 响应式设计 /@media (max-width: 768px) { .recipes-grid { grid-template-columns: 1fr; } .shopping-lists-container { grid-template-columns: 1fr; } .form-row { flex-direction: column; } .ingredient-row, .shopping-item-row { flex-direction: column; align-items: stretch; } .ingredient-row input, .shopping-item-row input { min-width: 100% !important; } } / 加载动画 /.loading-spinner { text-align: center; padding: 40px; color: #666; font-size: 1.1em; } / 分页样式 /.pagination { display: flex; justify-content: center; gap: 10px; margin-top: 30px; } .page-number { padding: 8px 15px; border: 1px solid #ddd; border-radius: 4px; cursor: pointer; transition: all 0.3s ease; } .page-number.active { background-color: #4CAF50; color: white; border-color: #4CAF50; } .page-number:hover:not(.active) { background-color: #f0f0f0; } ## 第四部分:JavaScript功能实现 ### 4.1 创建JavaScript文件 在子主题目录中创建`js/cooking-tools.js`文件: // 食谱管理工具JavaScript功能 jQuery(document).ready(function($) { // 初始化变量 let currentPage = 1; let recipesPerPage = 9; let currentRecipeId = 0; let ingredientCounter = 0; let shoppingItemCounter = 0; // 初始化模型对象 const cookingToolsModel = { // 获取食谱列表 getRecipes: function(page = 1, search = '', category = '') { return $.ajax({ url: cookingToolsAjax.ajax_url, type: 'POST', data: { action: 'cooking_tools_get_recipes', page: page, search: search, category: category, per_page: recipesPerPage, nonce: cookingToolsAjax.nonce } }); }, // 获取单个食谱 getRecipe: function(recipeId) { return $.ajax({ url: cookingToolsAjax.ajax_url, type: 'POST', data: { action: 'cooking_tools_get_recipe', recipe_id: recipeId, nonce: cookingToolsAjax.nonce } }); }, // 保存食谱 saveRecipe: function(recipeData) { return $.ajax({ url: cookingToolsAjax.ajax_url, type: 'POST', data: { action: 'cooking_tools_save_recipe', recipe_data: recipeData, nonce: cookingToolsAjax.nonce } }); }, // 删除食谱 deleteRecipe: function(recipeId) { return $.ajax({ url: cookingToolsAjax.ajax_url, type: 'POST', data: { action: 'cooking_tools_delete_recipe', recipe_id: recipeId, nonce: cookingToolsAjax.nonce } }); }, // 获取采购清单 getShoppingLists: function() { return $.ajax({ url: cookingToolsAjax.ajax_url, type: 'POST', data: { action: 'cooking_tools_get_shopping_lists', nonce: cookingToolsAjax.nonce } }); }, // 保存采购清单 saveShoppingList: function(listData) { return $.ajax({ url: cookingToolsAjax.ajax_url, type: 'POST', data: { action: 'cooking_tools_save_shopping_list', list_data: listData, nonce: cookingToolsAjax.nonce } }); }, // 删除采购清单 deleteShoppingList: function(listId) { return $.ajax({ url: cookingToolsAjax.ajax_url, type: 'POST', data: { action: 'cooking_tools_delete_shopping_list', list_id: listId, nonce: cookingToolsAjax.nonce } }); } }; // 初始化UI组件 const cookingToolsUI = { // 显示食谱列表 showRecipesList: function() { $('#recipe-editor').hide(); $('#shopping-lists').hide(); $('#recipes-list').show(); $('#add-new-recipe').text('添加新食谱'); }, // 显示食谱编辑器 showRecipeEditor: function(recipeId = 0) { $('#recipes-list').hide(); $('#shopping-lists').hide(); $('#recipe-editor').show(); if (recipeId === 0) { $('#editor-title').text('添加新食谱'); $('#recipe-id').val(0); $('#recipe-form')[0].reset(); $('#ingredients-container').html(this.createIngredientRow(0)); ingredientCounter = 1; $('#generate-shopping-list').hide(); } else { $('#editor-title').text('编辑食谱'); $('#generate-shopping-list').show(); } $('#add-new-recipe').text('返回食谱列表'); }, // 显示采购清单 showShoppingLists: function() { $('#recipes-list').hide(); $('#recipe-editor').hide(); $('#shopping-lists').show(); $('#add-new-recipe').text('返回食谱列表'); }, // 创建食材行 createIngredientRow: function(index) { return ` <div class="ingredient-row"> <input type="text" class="ingredient-name" placeholder="食材名称" name="ingredients[${index}][name]"> <input type="number" step="0.01" class="ingredient-quantity" placeholder="数量" name="ingredients[${index}][quantity]"> <input type="text" class="ingredient-unit" placeholder="单位 (克/个/汤匙等)" name="ingredients[${index}][unit]"> <input type="text" class="ingredient-notes" placeholder="备注" name="ingredients[${index}][notes]"> <button type="button" class="btn-remove-ingredient">×</button> </div> `; }, // 创建采购物品行 createShoppingItemRow: function(index) { return ` <div class="shopping-item-row"> <input type="text" class="shopping-item-name" placeholder="物品名称" name="items[${index}][name]"> <input type="number" step="0.01" class="shopping-item-quantity" placeholder="数量" name="items[${index}][quantity]"> <input type="text" class="shopping-item-unit" placeholder="单位" name="items[${index}][unit]"> <div class="item-checkbox"> <input type="checkbox" class="shopping-item-checked" name="items[${index}][checked]"> <label>已购</label> </div> <button type="button" class="btn-remove-item">×</button> </div> `; }, // 渲染食谱卡片 renderRecipeCard: function(recipe) { const prepTime = recipe.prep_time ? `${recipe.prep_time}分钟` : '未设置'; const cookTime = recipe.cook_time ? `${recipe.cook_time}分钟` : '未设置'; const servings = recipe.servings ? `${recipe.servings}人份` : '未设置'; return ` <div class="recipe-card" data-recipe-id="${recipe.id}"> <div class="recipe-image"> ${recipe.featured_image ? `<img src="${recipe.featured_image}" alt="${recipe.recipe_title}" style="width:100%;height:100%;object-fit:cover;">` : '<span>暂无图片</span>' } </div> <div class="recipe-info"> <h3 class="recipe-title">${recipe.recipe_title}</h3> <div class="recipe-meta"> <span title="准备时间">⏱️ ${prepTime}</span> <span title="烹饪时间">🔥 ${cookTime}</span> <span title="份量">👥 ${servings}</span> </div> <div class="recipe-category"> <span class="category-badge">${recipe.category || '未分类'}</span> ${recipe.difficulty ? `<span class="difficulty-badge">${recipe.difficulty}</span>` : ''} </div> <div class="recipe-actions"> <button class="btn btn-secondary btn-view-recipe" data-id="${recipe.id}">查看</button> <button class="btn btn-primary btn-edit-recipe" data-id="${recipe.id}">编辑</button> <button class="btn btn-danger btn-delete-recipe" data-id="${recipe.id}">删除</button> </div> </div> </div>

发表评论

手把手教学,为WordPress集成网站Uptime监控与停机报警通知

手把手教学:为WordPress集成网站Uptime监控与停机报警通知 引言:为什么WordPress网站需要Uptime监控? 在当今数字化时代,网站的可用性直接关系到企业的声誉、客户信任和收入。根据行业研究,即使是短短几分钟的停机时间,也可能导致数千美元的损失,更不用说对搜索引擎排名和用户体验的长期负面影响。对于使用WordPress构建的网站而言,由于其动态特性和插件依赖,面临停机风险的可能性更高。 传统的监控解决方案往往需要第三方服务,这些服务可能价格昂贵,或者无法完全满足个性化需求。通过WordPress代码二次开发,我们可以创建一个轻量级、高度可定制的Uptime监控与报警系统,不仅能够实时监测网站状态,还能在发现问题时立即通知管理员,大大缩短故障响应时间。 本文将详细介绍如何通过WordPress代码二次开发,实现一个完整的网站Uptime监控与停机报警系统。我们将从基础概念讲起,逐步深入到具体实现,最终打造一个功能完善、稳定可靠的监控解决方案。 第一部分:Uptime监控系统设计原理 1.1 监控系统的基本架构 一个完整的Uptime监控系统通常包含以下核心组件: 监控代理:定期检查网站状态 状态存储:记录检查结果和历史数据 报警引擎:分析状态数据并触发报警 通知系统:向管理员发送报警信息 管理界面:配置和查看监控状态 对于WordPress集成方案,我们可以利用其现有的数据库结构、调度系统和插件架构,将这些组件无缝整合到WordPress生态中。 1.2 WordPress作为监控平台的优势 WordPress本身提供了许多可用于构建监控系统的功能: Cron调度系统:可以定期执行监控任务 数据库抽象层:方便存储监控数据 用户角色系统:控制不同用户对监控功能的访问权限 REST API:可以扩展为监控API端点 丰富的钩子系统:允许在适当位置插入监控逻辑 1.3 监控频率与性能平衡 在设计监控系统时,需要平衡监控频率与服务器负载。过于频繁的检查会增加服务器负担,而间隔太长则可能无法及时发现故障。对于大多数网站,5-10分钟的检查间隔是一个合理的起点。我们将设计一个可配置的监控频率系统,允许管理员根据实际需求进行调整。 第二部分:搭建基础监控框架 2.1 创建监控插件的基本结构 首先,我们需要创建一个独立的WordPress插件来承载监控功能。以下是插件的基本目录结构: wp-uptime-monitor/ ├── wp-uptime-monitor.php # 主插件文件 ├── includes/ │ ├── class-monitor-core.php # 监控核心类 │ ├── class-checker.php # 网站检查器 │ ├── class-notifier.php # 通知处理器 │ └── class-database.php # 数据库操作类 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 管理界面样式 │ ├── js/ │ │ └── admin-script.js # 管理界面脚本 │ └── class-admin-panel.php # 管理面板 ├── public/ │ └── class-public-display.php # 前端显示 └── templates/ ├── settings-page.php # 设置页面模板 └── status-page.php # 状态页面模板 2.2 主插件文件初始化 在wp-uptime-monitor.php中,我们需要设置插件的基本信息和初始化逻辑: <?php /** * Plugin Name: WordPress Uptime Monitor * Plugin URI: https://yourwebsite.com/uptime-monitor * Description: 一个完整的网站Uptime监控与停机报警系统 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: wp-uptime-monitor */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('WUM_VERSION', '1.0.0'); define('WUM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WUM_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WUM_CHECK_INTERVAL', 300); // 默认检查间隔:5分钟 // 自动加载类文件 spl_autoload_register(function ($class_name) { $prefix = 'WUM_'; $base_dir = WUM_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class_name, $len) !== 0) { return; } $relative_class = substr($class_name, $len); $file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function wum_init_plugin() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('WordPress Uptime Monitor需要WordPress 5.0或更高版本。', 'wp-uptime-monitor'); echo '</p></div>'; }); return; } // 初始化核心组件 $monitor_core = new WUM_Monitor_Core(); $monitor_core->init(); // 初始化管理界面 if (is_admin()) { $admin_panel = new WUM_Admin_Panel(); $admin_panel->init(); } } add_action('plugins_loaded', 'wum_init_plugin'); // 插件激活时执行的操作 register_activation_hook(__FILE__, function() { require_once WUM_PLUGIN_DIR . 'includes/class-database.php'; $database = new WUM_Database(); $database->create_tables(); // 设置默认选项 $default_options = array( 'check_interval' => WUM_CHECK_INTERVAL, 'notification_emails' => get_option('admin_email'), 'enable_sms' => false, 'sms_phone' => '', 'enable_discord' => false, 'discord_webhook' => '', 'uptime_goal' => 99.9, 'check_timeout' => 30, ); add_option('wum_settings', $default_options); // 调度监控任务 if (!wp_next_scheduled('wum_perform_check')) { wp_schedule_event(time(), 'wum_five_minutes', 'wum_perform_check'); } }); // 插件停用时执行的操作 register_deactivation_hook(__FILE__, function() { // 清除调度任务 $timestamp = wp_next_scheduled('wum_perform_check'); if ($timestamp) { wp_unschedule_event($timestamp, 'wum_perform_check'); } // 可选:保留数据以便重新激活时使用 // 或者添加设置选项让用户选择是否删除数据 }); // 添加自定义Cron调度间隔 add_filter('cron_schedules', function($schedules) { $schedules['wum_five_minutes'] = array( 'interval' => 300, 'display' => __('每5分钟', 'wp-uptime-monitor') ); $schedules['wum_ten_minutes'] = array( 'interval' => 600, 'display' => __('每10分钟', 'wp-uptime-monitor') ); $schedules['wum_thirty_minutes'] = array( 'interval' => 1800, 'display' => __('每30分钟', 'wp-uptime-monitor') ); return $schedules; }); 2.3 创建数据库表结构 监控系统需要存储检查结果和历史数据。在includes/class-database.php中创建必要的数据库表: <?php class WUM_Database { public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'wum_checks'; $history_table = $wpdb->prefix . 'wum_history'; $incidents_table = $wpdb->prefix . 'wum_incidents'; // 检查结果表 $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, check_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, response_code int(11) NOT NULL, response_time float NOT NULL, status varchar(20) NOT NULL, error_message text, PRIMARY KEY (id), KEY check_time (check_time), KEY status (status) ) $charset_collate;"; // 历史统计表(每日汇总) $sql .= "CREATE TABLE IF NOT EXISTS $history_table ( id bigint(20) NOT NULL AUTO_INCREMENT, date date NOT NULL, total_checks int(11) NOT NULL DEFAULT 0, successful_checks int(11) NOT NULL DEFAULT 0, total_response_time float NOT NULL DEFAULT 0, downtime_minutes int(11) NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY date (date) ) $charset_collate;"; // 故障事件表 $sql .= "CREATE TABLE IF NOT EXISTS $incidents_table ( id bigint(20) NOT NULL AUTO_INCREMENT, start_time datetime NOT NULL, end_time datetime, duration_minutes int(11), resolved tinyint(1) DEFAULT 0, notification_sent tinyint(1) DEFAULT 0, PRIMARY KEY (id), KEY start_time (start_time), KEY resolved (resolved) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } } 第三部分:实现核心监控功能 3.1 网站状态检查器 创建includes/class-checker.php,实现网站状态检查功能: <?php class WUM_Checker { private $timeout; private $check_url; public function __construct() { $settings = get_option('wum_settings', array()); $this->timeout = isset($settings['check_timeout']) ? $settings['check_timeout'] : 30; $this->check_url = home_url(); } /** * 执行网站状态检查 */ public function perform_check() { $start_time = microtime(true); // 使用WordPress HTTP API进行请求 $response = wp_remote_get($this->check_url, array( 'timeout' => $this->timeout, 'sslverify' => false, 'redirection' => 0 )); $response_time = microtime(true) - $start_time; // 分析响应 if (is_wp_error($response)) { return $this->handle_error($response, $response_time); } $response_code = wp_remote_retrieve_response_code($response); // 判断是否成功(2xx和3xx状态码视为成功) $is_success = ($response_code >= 200 && $response_code < 400); return array( 'success' => $is_success, 'response_code' => $response_code, 'response_time' => round($response_time, 3), 'error_message' => $is_success ? '' : 'HTTP错误: ' . $response_code, 'check_time' => current_time('mysql') ); } /** * 处理请求错误 */ private function handle_error($error, $response_time) { $error_message = $error->get_error_message(); // 根据错误类型分类 if (strpos($error_message, 'cURL error 28') !== false) { $error_message = '请求超时 (' . $this->timeout . '秒)'; } elseif (strpos($error_message, 'cURL error 6') !== false) { $error_message = '无法解析主机名'; } elseif (strpos($error_message, 'cURL error 7') !== false) { $error_message = '无法连接到主机'; } return array( 'success' => false, 'response_code' => 0, 'response_time' => round($response_time, 3), 'error_message' => $error_message, 'check_time' => current_time('mysql') ); } /** * 执行深度检查(检查关键页面和功能) */ public function perform_deep_check() { $checks = array(); // 检查首页 $checks['homepage'] = $this->check_specific_url(home_url()); // 检查WP REST API端点 $checks['rest_api'] = $this->check_specific_url(rest_url('wp/v2/posts')); // 检查登录页面 $checks['login_page'] = $this->check_specific_url(wp_login_url()); // 检查数据库连接 $checks['database'] = $this->check_database(); // 检查磁盘空间 $checks['disk_space'] = $this->check_disk_space(); return $checks; } /** * 检查特定URL */ private function check_specific_url($url) { $response = wp_remote_get($url, array( 'timeout' => 10, 'sslverify' => false )); return !is_wp_error($response) && wp_remote_retrieve_response_code($response) == 200; } /** * 检查数据库连接 */ private function check_database() { global $wpdb; $start_time = microtime(true); $result = $wpdb->get_var("SELECT 1"); $query_time = microtime(true) - $start_time; return array( 'success' => ($result === '1'), 'query_time' => round($query_time, 3) ); } /** * 检查磁盘空间 */ private function check_disk_space() { if (function_exists('disk_free_space')) { $free_space = disk_free_space(ABSPATH); $total_space = disk_total_space(ABSPATH); if ($total_space > 0) { $percentage_free = ($free_space / $total_space) * 100; return array( 'free_gb' => round($free_space / 1024 / 1024 / 1024, 2), 'total_gb' => round($total_space / 1024 / 1024 / 1024, 2), 'percentage_free' => round($percentage_free, 1), 'warning' => $percentage_free < 10 ); } } return array( 'free_gb' => 0, 'total_gb' => 0, 'percentage_free' => 0, 'warning' => false ); } } 3.2 监控核心逻辑 创建includes/class-monitor-core.php,实现监控系统的核心逻辑: <?php class WUM_Monitor_Core { private $checker; private $notifier; private $last_status; public function __construct() { $this->checker = new WUM_Checker(); $this->notifier = new WUM_Notifier(); $this->last_status = $this->get_last_status(); } public function init() { // 注册监控检查动作 add_action('wum_perform_check', array($this, 'perform_scheduled_check')); // 注册AJAX端点用于手动检查 add_action('wp_ajax_wum_manual_check', array($this, 'ajax_manual_check')); // 注册REST API端点 add_action('rest_api_init', array($this, 'register_rest_routes')); } /** * 执行计划检查 */ public function perform_scheduled_check() { global $wpdb; // 执行基本检查 $check_result = $this->checker->perform_check(); // 保存检查结果 $table_name = $wpdb->prefix . 'wum_checks'; $wpdb->insert($table_name, array( 'response_code' => $check_result['response_code'], 'response_time' => $check_result['response_time'], 'status' => $check_result['success'] ? 'up' : 'down', 'error_message' => $check_result['error_message'] )); // 更新历史统计 $this->update_daily_stats($check_result['success'], $check_result['response_time']); // 检查状态变化并处理 $this->handle_status_change($check_result['success'], $check_result['error_message']); // 如果状态为down,执行深度检查 if (!$check_result['success']) { $deep_checks = $this->checker->perform_deep_check(); $this->log_deep_checks($deep_checks); } // 清理旧数据(保留30天) $this->cleanup_old_data(); } /** * 处理状态变化 */ private function handle_status_change($current_status, $error_message = '') { // 如果状态没有变化,直接返回 if ($this->last_status === $current_status) { return; } // 状态从up变为down:记录故障开始 if ($this->last_status === true && $current_status === false) { $this->record_incident_start($error_message); } // 状态从down变为up:记录故障结束 if ($this->last_status === false && $current_status === true) { $this->record_incident_end(); } // 更新最后状态 $this->update_last_status($current_status); } /** * 记录故障开始 */ private function record_incident_start($error_message) { global $wpdb; $table_name = $wpdb->prefix . 'wum_incidents'; $wpdb->insert($table_name, array( 'start_time' => current_time('mysql'), 'resolved' => 0, 'notification_sent' => 0 )); $incident_id = $wpdb->insert_id; // 发送故障通知 $this->notifier->send_downtime_notification($incident_id, $error_message); // 标记通知已发送 $wpdb->update($table_name, array('notification_sent' => 1), array('id' => $incident_id) ); } /** * 记录故障结束 */ private function record_incident_end() { global $wpdb; $table_name = $wpdb->prefix . 'wum_incidents'; // 获取未解决的故障 $unresolved = $wpdb->get_row( "SELECT * FROM $table_name WHERE resolved = 0 ORDER BY start_time DESC LIMIT 1" ); if ($unresolved) { $end_time = current_time('mysql'); $start_time = strtotime($unresolved->start_time); $duration_minutes = round((strtotime($end_time) - $start_time) / 60); $wpdb->update($table_name, array( 'end_time' => $end_time, 'duration_minutes' => $duration_minutes, 'resolved' => 1 ), array('id' => $unresolved->id) ); // 发送恢复通知 $this->notifier->send_recovery_notification($unresolved->id, $duration_minutes); } } /** * 更新每日统计 */ private function update_daily_stats($success, $response_time) { global $wpdb; $today = current_time('Y-m-d'); $table_name = $wpdb->prefix . 'wum_history'; // 检查今天是否已有记录 $existing = $wpdb->get_row( $wpdb->prepare("SELECT * FROM $table_name WHERE date = %s", $today) ); if ($existing) { // 更新现有记录 $data = array( 'total_checks' => $existing->total_checks + 1, 'total_response_time' => $existing->total_response_time + $response_time ); if ($success) { $data['successful_checks'] = $existing->successful_checks + 1; } else { $data['downtime_minutes'] = $existing->downtime_minutes + 5; // 假设5分钟检查间隔 } $wpdb->update($table_name, $data, array('id' => $existing->id)); } else { // 创建新记录 $data = array( 'date' => $today, 'total_checks' => 1, 'successful_checks' => $success ? 1 : 0, 'total_response_time' => $response_time, 'downtime_minutes' => $success ? 0 : 5 ); $wpdb->insert($table_name, $data); } } /** * 记录深度检查结果 */ private function log_deep_checks($deep_checks) { // 这里可以将深度检查结果记录到单独的日志表或选项 update_option('wum_last_deep_check', array( 'time' => current_time('mysql'), 'results' => $deep_checks )); } /** * 清理旧数据 */ private function cleanup_old_data() { global $wpdb; $retention_days = 30; $delete_before = date('Y-m-d H:i:s', strtotime("-$retention_days days")); // 删除旧的检查记录 $checks_table = $wpdb->prefix . 'wum_checks'; $wpdb->query( $wpdb->prepare("DELETE FROM $checks_table WHERE check_time < %s", $delete_before) ); // 删除旧的故障记录(只保留已解决的) $incidents_table = $wpdb->prefix . 'wum_incidents'; $wpdb->query( $wpdb->prepare( "DELETE FROM $incidents_table WHERE resolved = 1 AND start_time < %s", $delete_before ) ); } /** * 获取最后状态 */ private function get_last_status() { global $wpdb; $table_name = $wpdb->prefix . 'wum_checks'; $last_check = $wpdb->get_row( "SELECT status FROM $table_name ORDER BY check_time DESC LIMIT 1" ); return $last_check ? ($last_check->status === 'up') : true; } /** * 更新最后状态 */ private function update_last_status($status) { $this->last_status = $status; } /** * AJAX手动检查 */ public function ajax_manual_check() { // 检查权限 if (!current_user_can('manage_options')) { wp_die('权限不足'); } // 执行检查 $check_result = $this->checker->perform_check(); $deep_checks = $this->checker->perform_deep_check(); // 保存结果 $this->perform_scheduled_check(); // 返回结果 wp_send_json_success(array( 'basic_check' => $check_result, 'deep_checks' => $deep_checks, 'timestamp' => current_time('mysql') )); } /** * 注册REST API路由 */ public function register_rest_routes() { register_rest_route('wum/v1', '/status', array( 'methods' => 'GET', 'callback' => array($this, 'rest_get_status'), 'permission_callback' => array($this, 'rest_permission_check') )); register_rest_route('wum/v1', '/stats', array( 'methods' => 'GET', 'callback' => array($this, 'rest_get_stats'), 'permission_callback' => array($this, 'rest_permission_check') )); } /** * REST API权限检查 */ public function rest_permission_check() { // 可以设置为需要API密钥或用户权限 return current_user_can('manage_options') || $this->validate_api_key($_GET['api_key'] ?? ''); } /** * 验证API密钥 */ private function validate_api_key($api_key) { $stored_key = get_option('wum_api_key', ''); return !empty($stored_key) && hash_equals($stored_key, $api_key); } /** * 获取状态REST端点 */ public function rest_get_status() { global $wpdb; $table_name = $wpdb->prefix . 'wum_checks'; $last_check = $wpdb->get_row( "SELECT * FROM $table_name ORDER BY check_time DESC LIMIT 1" ); $incidents_table = $wpdb->prefix . 'wum_incidents'; $active_incident = $wpdb->get_row( "SELECT * FROM $incidents_table WHERE resolved = 0 ORDER BY start_time DESC LIMIT 1" ); return rest_ensure_response(array( 'status' => $last_check ? $last_check->status : 'unknown', 'last_check' => $last_check ? $last_check->check_time : null, 'response_time' => $last_check ? $last_check->response_time : null, 'active_incident' => $active_incident ? true : false, 'incident_start' => $active_incident ? $active_incident->start_time : null )); } /** * 获取统计REST端点 */ public function rest_get_stats() { global $wpdb; $history_table = $wpdb->prefix . 'wum_history'; // 获取最近30天数据 $recent_stats = $wpdb->get_results( "SELECT * FROM $history_table ORDER BY date DESC LIMIT 30" ); // 计算总体统计 $total_checks = 0; $successful_checks = 0; $total_downtime = 0; foreach ($recent_stats as $stat) { $total_checks += $stat->total_checks; $successful_checks += $stat->successful_checks; $total_downtime += $stat->downtime_minutes; } $uptime_percentage = $total_checks > 0 ? ($successful_checks / $total_checks) * 100 : 100; return rest_ensure_response(array( 'uptime_percentage' => round($uptime_percentage, 2), 'total_checks' => $total_checks, 'successful_checks' => $successful_checks, 'total_downtime_minutes' => $total_downtime, 'recent_stats' => $recent_stats )); } } ## 第四部分:实现多通道报警通知系统 ### 4.1 通知处理器基础类 创建`includes/class-notifier.php`,实现多通道通知功能: <?phpclass WUM_Notifier { private $settings; public function __construct() { $this->settings = get_option('wum_settings', array()); } /** * 发送停机通知 */ public function send_downtime_notification($incident_id, $error_message) { $subject = '🚨 网站停机警报 - ' . get_bloginfo('name'); $message = $this->build_downtime_message($incident_id, $error_message); // 发送到所有启用的通道 $this->send_to_all_channels($subject, $message, 'downtime'); } /** * 发送恢复通知 */ public function send_recovery_notification($incident_id, $duration_minutes) { $subject = '✅ 网站恢复通知 - ' . get_bloginfo('name'); $message = $this->build_recovery_message($incident_id, $duration_minutes); $this->send_to_all_channels($subject, $message, 'recovery'); } /** * 构建停机消息 */ private function build_downtime_message($incident_id, $error_message) { $site_name = get_bloginfo('name'); $site_url = home_url(); $current_time = current_time('Y-m-d H:i:s'); $message = "🚨 网站停机警报nn"; $message .= "网站名称: $site_namen"; $message .= "网站地址: $site_urln"; $message .= "故障时间: $current_timen"; $message .= "故障ID: #$incident_idn"; $message .= "错误信息: $error_messagenn"; $message .= "请立即检查服务器状态。n"; $message .= "监控系统将持续检查,恢复后将发送通知。"; return $message; } /** * 构建恢复消息 */ private function build_recovery_message($incident_id, $duration_minutes) { $site_name = get_bloginfo('name'); $site_url = home_url(); $current_time = current_time('Y-m-d H:i:s'); $message = "✅ 网站恢复通知nn"; $message .= "网站名称: $site_namen"; $message .= "网站地址: $site_urln"; $message .= "恢复时间: $current_timen"; $message .= "故障ID: #$incident_idn"; $message .= "停机时长: $duration_minutes 分钟nn"; $message .= "网站现已恢复正常运行。n"; $message .= "建议检查日志以确定故障原因。"; return $message; } /** * 发送到所有通道 */ private function send_to_all_channels($subject, $message, $type) { // 电子邮件通知 if (!empty($this->settings['notification_emails'])) { $this->send_email_notification($subject, $message); } // SMS通知(需要集成第三方服务) if (!empty($this->settings['enable_sms']) && !empty($this->settings['sms_phone'])) { $this->send_sms_notification($message); } // Discord Webhook通知 if (!empty($this->settings['enable_discord']) && !empty($this->settings['discord_webhook'])) { $this->send_discord_notification($subject, $message, $type); } // Slack Webhook通知 if (!empty($this->settings['enable_slack']) && !empty($this->settings['slack_webhook'])) { $this->send_slack_notification($subject, $message, $type); } // 微信通知(需要企业微信或Server酱) if (!empty($this->settings['enable_wechat']) && !empty($this->settings['wechat_key'])) { $this->send_wechat_notification($subject, $message); } // 执行自定义动作 do_action('wum_notification_sent', $type, $subject, $message); } /** * 发送电子邮件通知 */ private function send_email_notification($subject, $message) { $emails = explode(',', $this->settings['notification_emails']); $emails = array_map('trim', $emails); $headers = array('Content-Type: text/plain; charset=UTF-8'); foreach ($emails as $email) { if (is_email($email)) { wp_mail($email, $subject, $message, $headers); } } } /** * 发送SMS通知(示例:使用Twilio) */ private function send_sms_notification($message) { // 这里需要集成SMS服务提供商,如Twilio、阿里云等 $phone = $this->settings['sms_phone']; $api_key = $this->settings['sms_api_key'] ?? ''; $api_secret = $this->settings['sms_api_secret'] ?? ''; // 示例:使用Twilio if (class_exists('TwilioRestClient') && $api_key && $api_secret) { try { $client = new TwilioRestClient($api_key, $api_secret); $client->messages->create( $phone, array( 'from' => $this->settings['sms_from_number'], 'body' => substr($message, 0, 160) // SMS长度限制 ) ); } catch (Exception $e) { error_log('WUM SMS发送失败: ' . $e->getMessage()); } } } /** * 发送Discord通知 */ private function send_discord_notification($subject, $message, $type) { $webhook_url = $this->settings['discord_webhook']; // 根据类型设置颜色和标题 $color = $type === 'downtime' ? 15158332 : 3066993; // 红色或绿色 $title = $type === 'downtime' ? '🚨 网站停机警报' : '✅ 网站恢复通知'; $embed = array( 'title' => $title, 'description' => $message, 'color' => $color, 'timestamp' => date('c'), 'footer' => array( 'text' => get_bloginfo('name') . ' 监控系统' ) ); $data = array('embeds' => array($embed)); $response = wp_remote_post($webhook_url, array( 'headers' => array('Content-Type' => 'application/json'), 'body' => json_encode($data), 'timeout' => 10 )); if (is_wp_error($response)) { error_log('WUM Discord通知发送失败: ' . $response->get_error_message()); } } /** * 发送Slack通知 */ private function send_slack_notification($subject, $message, $type) { $webhook_url = $this->settings['slack_webhook']; $icon = $type === 'downtime' ? ':warning:' : ':white_check_mark:'; $color = $type === 'downtime' ? 'danger' : 'good'; $attachments = array(array( 'fallback' => $subject, 'color' => $color, 'title' => $subject, 'text' => $message, 'footer' => get_bloginfo('name'), 'ts' => time() )); $data = array( 'attachments' => $attachments, 'icon_emoji' => $icon ); $response = wp_remote_post($webhook_url, array( 'headers' => array('Content-Type

发表评论

详细指南,开发网站内嵌的在线流程图与思维导图协作工具

详细指南:开发WordPress内嵌在线流程图与思维导图协作工具 引言:为什么在WordPress中集成协作工具? 在当今数字化工作环境中,可视化协作工具已成为团队沟通和项目管理的重要组成部分。流程图和思维导图能够帮助团队清晰地表达复杂概念、规划项目流程和激发创意。然而,许多团队面临工具碎片化的问题——使用外部工具导致数据分散、协作不便和额外成本。 将在线流程图与思维导图工具直接集成到WordPress网站中,可以解决这些问题。用户无需离开网站即可创建、编辑和协作,所有数据集中存储,与现有用户系统无缝集成。本指南将详细介绍如何通过WordPress代码二次开发,实现这一功能强大的协作工具。 第一部分:项目规划与技术选型 1.1 功能需求分析 在开始开发前,我们需要明确工具的核心功能: 流程图功能: 基本图形绘制(矩形、圆形、菱形等) 连接线与箭头 文本编辑与格式化 拖拽与缩放界面 图层管理与分组 思维导图功能: 中心主题与分支节点 节点折叠/展开 主题样式与颜色 关系连接线 图标与图片插入 协作功能: 实时协同编辑 用户权限管理 版本历史与恢复 评论与批注系统 导出与分享功能 WordPress集成: 用户系统对接 数据存储与检索 短代码嵌入 媒体库集成 响应式设计 1.2 技术架构设计 基于功能需求,我们选择以下技术栈: 前端框架:React.js + Redux(用于复杂状态管理) 绘图库:JointJS或GoJS(专业图表库) 实时协作:Socket.io或Pusher(实时通信) WordPress集成:自定义插件架构 数据存储:WordPress自定义数据库表 + 文件系统 样式框架:Tailwind CSS(快速UI开发) 1.3 开发环境搭建 安装本地WordPress开发环境(推荐使用Local by Flywheel或Docker) 设置代码编辑器(VS Code推荐) 配置Node.js环境用于前端开发 安装Git进行版本控制 准备测试数据库 第二部分:WordPress插件基础架构 2.1 创建插件基本结构 首先,在WordPress的wp-content/plugins目录下创建插件文件夹collab-diagram-tool,并建立以下结构: collab-diagram-tool/ ├── collab-diagram-tool.php # 主插件文件 ├── includes/ # PHP包含文件 │ ├── class-database.php # 数据库处理 │ ├── class-shortcodes.php # 短代码处理 │ ├── class-api.php # REST API端点 │ └── class-admin.php # 后台管理 ├── assets/ # 静态资源 │ ├── css/ │ ├── js/ │ └── images/ ├── src/ # React前端源码 │ ├── components/ │ ├── redux/ │ ├── utils/ │ └── App.js ├── templates/ # 前端模板 └── vendor/ # 第三方库 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('CDT_VERSION', '1.0.0'); define('CDT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('CDT_PLUGIN_URL', plugin_dir_url(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class) { $prefix = 'CDT_'; $base_dir = CDT_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { return; } $relative_class = substr($class, $len); $file = $base_dir . 'class-' . strtolower(str_replace('_', '-', $relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function cdt_init() { // 检查依赖 if (!function_exists('register_rest_route')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>协作流程图工具需要WordPress 4.7+版本支持REST API。</p></div>'; }); return; } // 初始化组件 CDT_Database::init(); CDT_Shortcodes::init(); CDT_API::init(); CDT_Admin::init(); // 加载文本域 load_plugin_textdomain('cdt', false, dirname(plugin_basename(__FILE__)) . '/languages/'); } add_action('plugins_loaded', 'cdt_init'); // 激活/停用钩子 register_activation_hook(__FILE__, ['CDT_Database', 'create_tables']); register_deactivation_hook(__FILE__, ['CDT_Database', 'cleanup']); 第三部分:数据库设计与实现 3.1 数据库表结构 我们需要创建多个表来存储图表数据、用户协作信息等: class CDT_Database { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 图表主表 $table_diagrams = $wpdb->prefix . 'cdt_diagrams'; $sql1 = "CREATE TABLE IF NOT EXISTS $table_diagrams ( id bigint(20) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, type enum('flowchart','mindmap') NOT NULL DEFAULT 'flowchart', content longtext, settings text, created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, status varchar(20) DEFAULT 'draft', PRIMARY KEY (id), KEY created_by (created_by), KEY status (status) ) $charset_collate;"; // 协作权限表 $table_collaborators = $wpdb->prefix . 'cdt_collaborators'; $sql2 = "CREATE TABLE IF NOT EXISTS $table_collaborators ( id bigint(20) NOT NULL AUTO_INCREMENT, diagram_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL, permission enum('view','edit','admin') NOT NULL DEFAULT 'view', invited_by bigint(20) NOT NULL, invited_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY diagram_user (diagram_id, user_id), KEY user_id (user_id) ) $charset_collate;"; // 版本历史表 $table_versions = $wpdb->prefix . 'cdt_versions'; $sql3 = "CREATE TABLE IF NOT EXISTS $table_versions ( id bigint(20) NOT NULL AUTO_INCREMENT, diagram_id bigint(20) NOT NULL, version int(11) NOT NULL, content longtext, created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, change_log text, PRIMARY KEY (id), KEY diagram_version (diagram_id, version) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); dbDelta($sql3); } } 3.2 数据模型类 创建数据操作类来处理CRUD操作: class CDT_Diagram_Model { private $wpdb; private $table; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->table = $wpdb->prefix . 'cdt_diagrams'; } public function create($data) { $defaults = [ 'title' => '未命名图表', 'type' => 'flowchart', 'content' => '{}', 'settings' => '{}', 'created_by' => get_current_user_id(), 'status' => 'draft' ]; $data = wp_parse_args($data, $defaults); $this->wpdb->insert($this->table, $data); if ($this->wpdb->insert_id) { $diagram_id = $this->wpdb->insert_id; // 自动添加创建者为管理员 $this->add_collaborator($diagram_id, $data['created_by'], 'admin'); return $diagram_id; } return false; } public function update($id, $data) { $data['updated_at'] = current_time('mysql'); return $this->wpdb->update($this->table, $data, ['id' => $id]); } public function get($id) { return $this->wpdb->get_row( $this->wpdb->prepare("SELECT * FROM $this->table WHERE id = %d", $id) ); } public function get_by_user($user_id, $limit = 20, $offset = 0) { $collaborator_table = $this->wpdb->prefix . 'cdt_collaborators'; $query = $this->wpdb->prepare( "SELECT d.*, c.permission FROM $this->table d INNER JOIN $collaborator_table c ON d.id = c.diagram_id WHERE c.user_id = %d ORDER BY d.updated_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset ); return $this->wpdb->get_results($query); } private function add_collaborator($diagram_id, $user_id, $permission) { $table = $this->wpdb->prefix . 'cdt_collaborators'; return $this->wpdb->insert($table, [ 'diagram_id' => $diagram_id, 'user_id' => $user_id, 'permission' => $permission, 'invited_by' => get_current_user_id() ]); } } 第四部分:前端编辑器开发 4.1 React应用架构 在src/目录下创建React应用: // src/App.js import React, { useState, useEffect } from 'react'; import { Provider } from 'react-redux'; import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './redux/reducers'; import DiagramEditor from './components/DiagramEditor'; import Toolbar from './components/Toolbar'; import Sidebar from './components/Sidebar'; import CollaborationPanel from './components/CollaborationPanel'; import './styles/main.css'; const store = createStore(rootReducer, applyMiddleware(thunk)); function App({ diagramId, userId, userPermission }) { const [isLoaded, setIsLoaded] = useState(false); const [diagramData, setDiagramData] = useState(null); useEffect(() => { // 加载图表数据 fetchDiagramData(diagramId).then(data => { setDiagramData(data); setIsLoaded(true); }); }, [diagramId]); if (!isLoaded) { return <div className="loading">加载中...</div>; } return ( <Provider store={store}> <div className="cdt-app"> <Toolbar diagramId={diagramId} permission={userPermission} /> <div className="cdt-main-area"> <Sidebar /> <DiagramEditor data={diagramData} diagramId={diagramId} userId={userId} /> <CollaborationPanel diagramId={diagramId} permission={userPermission} /> </div> </div> </Provider> ); } export default App; 4.2 流程图编辑器组件 // src/components/DiagramEditor/FlowchartEditor.js import React, { useRef, useEffect } from 'react'; import * as joint from 'jointjs'; import 'jointjs/dist/joint.css'; const FlowchartEditor = ({ data, onUpdate, readOnly }) => { const containerRef = useRef(null); const graphRef = useRef(null); const paperRef = useRef(null); useEffect(() => { if (!containerRef.current) return; // 初始化JointJS图形 const graph = new joint.dia.Graph(); graphRef.current = graph; // 创建画布 const paper = new joint.dia.Paper({ el: containerRef.current, model: graph, width: '100%', height: '100%', gridSize: 10, drawGrid: true, background: { color: '#f8f9fa' }, interactive: !readOnly }); paperRef.current = paper; // 加载现有数据 if (data && data.elements) { graph.fromJSON(data); } // 监听变化 graph.on('change', () => { if (onUpdate) { onUpdate(graph.toJSON()); } }); // 添加工具面板 if (!readOnly) { initTools(paper, graph); } return () => { paper.remove(); graph.clear(); }; }, [data, readOnly]); const initTools = (paper, graph) => { // 创建图形工具 const shapes = { rectangle: new joint.shapes.standard.Rectangle(), circle: new joint.shapes.standard.Circle(), ellipse: new joint.shapes.standard.Ellipse(), rhombus: new joint.shapes.standard.Rhombus() }; // 设置默认样式 Object.values(shapes).forEach(shape => { shape.attr({ body: { fill: '#ffffff', stroke: '#333333', strokeWidth: 2 }, label: { text: '文本', fill: '#333333', fontSize: 14, fontFamily: 'Arial' } }); }); // 连接线 const link = new joint.shapes.standard.Link(); link.attr({ line: { stroke: '#333333', strokeWidth: 2, targetMarker: { type: 'path', d: 'M 10 -5 0 0 10 5 z' } } }); // 将工具暴露给全局,供工具栏使用 window.cdtTools = { shapes, link, graph, paper }; }; return ( <div className="flowchart-editor"> <div ref={containerRef} className="joint-paper-container" /> </div> ); }; export default FlowchartEditor; 4.3 思维导图编辑器组件 // src/components/DiagramEditor/MindmapEditor.js import React, { useState, useRef, useEffect } from 'react'; import MindNode from './MindNode'; const MindmapEditor = ({ data, onUpdate, readOnly }) => { const [nodes, setNodes] = useState(data?.nodes || []); const [connections, setConnections] = useState(data?.connections || []); const containerRef = useRef(null); // 添加新节点 const addNode = (parentId = null) => { const newNode = { id: `node_${Date.now()}`, content: '新节点', x: parentId ? 200 : 400, y: parentId ? 150 : 300, width: 120, height: 40, color: '#ffffff', borderColor: '#4a90e2', parentId }; setNodes(prev => [...prev, newNode]); if (parentId) { setConnections(prev => [...prev, { id: `conn_${Date.now()}`, from: parentId, to: newNode.id, type: 'straight' }]); } if (onUpdate) { onUpdate({ nodes: [...nodes, newNode], connections }); } }; // 更新节点 const updateNode = (id, updates) => { const updatedNodes = nodes.map(node => node.id === id ? { ...node, ...updates } : node ); setNodes(updatedNodes); if (onUpdate) { onUpdate({ nodes: updatedNodes, connections }); } }; // 删除节点 const deleteNode = (id) => { // 递归删除子节点 const getChildIds = (parentId) => { const children = nodes.filter(n => n.parentId === parentId); let ids = children.map(c => c.id); children.forEach(child => { ids = [...ids, ...getChildIds(child.id)]; }); return ids; }; const idsToDelete = [id, ...getChildIds(id)]; const updatedNodes = nodes.filter(n => !idsToDelete.includes(n.id)); const updatedConnections = connections.filter( c => !idsToDelete.includes(c.from) && !idsToDelete.includes(c.to) ); setNodes(updatedNodes); setConnections(updatedConnections); if (onUpdate) { onUpdate({ nodes: updatedNodes, connections: updatedConnections }); } }; // 绘制连接线 useEffect(() => { if (!containerRef.current) return; const canvas = containerRef.current; const ctx = canvas.getContext('2d'); // 设置Canvas尺寸 const resizeCanvas = () => { const container = canvas.parentElement; canvas.width = container.clientWidth; canvas.height = container.clientHeight; }; resizeCanvas(); window.addEventListener('resize', resizeCanvas); // 绘制函数 const drawConnections = () => { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = '#999'; ctx.lineWidth = 2; connections.forEach(conn => { const fromNode = nodes.find(n => n.id === conn.from); const toNode = nodes.find(n => n.id === conn.to); if (!fromNode || !toNode) return; const startX = fromNode.x + fromNode.width; const startY = fromNode.y + fromNode.height / 2; const endX = toNode.x; const endY = toNode.y + toNode.height / 2; // 绘制贝塞尔曲线 ctx.beginPath(); ctx.moveTo(startX, startY); const cp1x = startX + (endX - startX) / 3; const cp2x = startX + 2 * (endX - startX) / 3; ctx.bezierCurveTo( cp1x, startY, cp2x, endY, endX, endY ); ctx.stroke(); // 绘制箭头 const angle = Math.atan2(endY - startY, endX - startX); const arrowLength = 10; ctx.beginPath(); ctx.moveTo(endX, endY); ctx.lineTo( endX - arrowLength * Math.cos(angle - Math.PI / 6), endY - arrowLength * Math.sin(angle - Math.PI / 6) ); ctx.moveTo(endX, endY); ctx.lineTo( endX - arrowLength * Math.cos(angle + Math.PI / 6), endY - arrowLength * Math.sin(angle + Math.PI / 6) ); ctx.stroke(); }); }; drawConnections(); return () => { window.removeEventListener('resize', resizeCanvas); }; }, [nodes, connections]); return ( <div className="mindmap-editor"> <canvas ref={containerRef} className="connections-canvas" /> <div className="nodes-container"> {nodes.map(node => ( <MindNode key={node.id} node={node} onUpdate={updateNode} onDelete={deleteNode} onAddChild={() => addNode(node.id)} readOnly={readOnly} /> ))} </div> {!readOnly && ( <button className="add-root-node" onClick={() => addNode()} > + 添加根节点 </button> )} </div> ); }; export default MindmapEditor; 第五部分:实时协作功能实现 5.1 WebSocket服务器集成 由于WordPress本身不是实时服务器,我们需要集成WebSocket服务: // includes/class-websocket.php class CDT_WebSocket { private static $instance = null; private $server; public static function init() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { add_action('init', [$this, 'start_websocket_server']); add_action('wp_enqueue_scripts', [$this, 'enqueue_websocket_client']); } public function start_websocket_server() { // 检查是否应该启动WebSocket服务器 if (defined('DOING_AJAX') && DOING_AJAX) { return; } // 使用外部WebSocket服务或启动内置服务器 if (apply_filters('cdt_use_external_websocket', false)) { $this->init_external_websocket(); } else { $this->init_builtin_websocket(); } } private function init_external_websocket() { // 集成Pusher或Socket.io服务 $service = get_option('cdt_websocket_service', 'pusher'); if ($service === 'pusher') { $this->init_pusher(); } elseif ($service === 'socketio') { $this->init_socketio(); } } private function init_pusher() { // Pusher.com集成 $app_id = get_option('cdt_pusher_app_id'); $key = get_option('cdt_pusher_key'); $secret = get_option('cdt_pusher_secret'); $cluster = get_option('cdt_pusher_cluster', 'mt1'); if ($app_id && $key && $secret) { // 注册Pusher PHP库 if (!class_exists('PusherPusher')) { require_once CDT_PLUGIN_DIR . 'vendor/autoload.php'; } $this->pusher = new PusherPusher( $key, $secret, $app_id, ['cluster' => $cluster] ); } } public function enqueue_websocket_client() { if (is_singular() && has_shortcode(get_post()->post_content, 'collab_diagram')) { $service = get_option('cdt_websocket_service', 'pusher'); if ($service === 'pusher') { wp_enqueue_script( 'pusher-js', 'https://js.pusher.com/7.0/pusher.min.js', [], '7.0', true ); wp_add_inline_script('pusher-js', ' document.addEventListener("DOMContentLoaded", function() { const pusher = new Pusher("' . get_option('cdt_pusher_key') . '", { cluster: "' . get_option('cdt_pusher_cluster', 'mt1') . '" }); window.cdtPusher = pusher; }); '); } } } // 发送实时更新 public function broadcast_update($diagram_id, $data, $exclude_user = null) { $channel = 'cdt-diagram-' . $diagram_id; $event = 'diagram-update'; if (isset($this->pusher)) { $this->pusher->trigger($channel, $event, [ 'data' => $data, 'timestamp' => time(), 'exclude' => $exclude_user ]); } } } 5.2 前端协作逻辑 // src/utils/collaboration.js class CollaborationManager { constructor(diagramId, userId) { this.diagramId = diagramId; this.userId = userId; this.pusher = null; this.channel = null; this.lastUpdateTime = 0; this.updateQueue = []; this.isProcessing = false; this.initWebSocket(); } initWebSocket() { if (window.cdtPusher) { this.pusher = window.cdtPusher; this.channel = this.pusher.subscribe(`cdt-diagram-${this.diagramId}`); this.channel.bind('diagram-update', (data) => { // 排除自己发送的更新 if (data.exclude === this.userId) return; this.handleRemoteUpdate(data.data); }); this.channel.bind('user-joined', (data) => { this.onUserJoined(data.user); }); this.channel.bind('user-left', (data) => { this.onUserLeft(data.user); }); } } // 发送本地更新 sendUpdate(updateData, isImmediate = false) { const now = Date.now(); // 防抖处理:避免发送过多更新 if (!isImmediate && now - this.lastUpdateTime < 100) { this.updateQueue.push(updateData); if (!this.isProcessing) { this.processQueue(); } return; } this.lastUpdateTime = now; // 发送到服务器 fetch(cdtApi.diagramUpdate(this.diagramId), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cdtApi.nonce }, body: JSON.stringify({ update: updateData, userId: this.userId, timestamp: now }) }).then(response => response.json()) .then(data => { if (data.success && this.pusher) { // 广播给其他用户,排除自己 this.pusher.trigger(`cdt-diagram-${this.diagramId}`, 'diagram-update', { data: updateData, exclude: this.userId }); } }); } processQueue() { if (this.updateQueue.length === 0) { this.isProcessing = false; return; } this.isProcessing = true; // 合并队列中的更新 const mergedUpdate = this.mergeUpdates(this.updateQueue); this.updateQueue = []; // 发送合并后的更新 this.sendUpdate(mergedUpdate, true); // 继续处理可能的新更新 setTimeout(() => this.processQueue(), 50); } mergeUpdates(updates) { // 根据具体数据结构实现合并逻辑 // 这里简化为返回最后一个更新 return updates[updates.length - 1]; } handleRemoteUpdate(remoteData) { // 处理远程更新,与本地状态合并 // 这里需要根据具体业务逻辑实现冲突解决 if (this.onRemoteUpdate) { this.onRemoteUpdate(remoteData); } } onUserJoined(user) { console.log(`用户 ${user.name} 加入了协作`); if (this.onUserJoinedCallback) { this.onUserJoinedCallback(user); } } onUserLeft(user) { console.log(`用户 ${user.name} 离开了协作`); if (this.onUserLeftCallback) { this.onUserLeftCallback(user); } } disconnect() { if (this.channel) { this.channel.unbind_all(); this.channel.unsubscribe(); } } } export default CollaborationManager; 5.3 协同光标与选择指示 // src/components/Collaboration/CursorIndicator.js import React, { useEffect, useRef } from 'react'; const CursorIndicator = ({ userId, userName, color, x, y, isActive }) => { const cursorRef = useRef(null); useEffect(() => { if (cursorRef.current && x !== undefined && y !== undefined) { cursorRef.current.style.transform = `translate(${x}px, ${y}px)`; } }, [x, y]); if (!isActive) return null; return ( <div ref={cursorRef} className="cursor-indicator" style={{ '--cursor-color': color, position: 'absolute', left: 0, top: 0, zIndex: 1000, pointerEvents: 'none', transition: 'transform 0.1s ease-out' }} > <svg width="24" height="24" viewBox="0 0 24 24"> <path d="M3 3L10 18L13 13L18 10L3 3Z" fill={color} stroke="#fff" strokeWidth="1" /> </svg> <div className="cursor-label" style={{ backgroundColor: color, color: '#fff', padding: '2px 6px', borderRadius: '3px', fontSize: '12px', whiteSpace: 'nowrap', transform: 'translate(5px, 5px)' }}> {userName} </div> </div> ); }; // 用户状态管理 const UserPresenceManager = ({ diagramId, currentUser }) => { const [users, setUsers] = useState([]); const collaborationRef = useRef(null); useEffect(() => { collaborationRef.current = new CollaborationManager(diagramId, currentUser.id); collaborationRef.current.onUserJoinedCallback = (user) => { setUsers(prev => { const exists = prev.find(u => u.id === user.id); if (exists) return prev; return [...prev, { ...user, active: true, lastSeen: Date.now() }]; }); }; collaborationRef.current.onUserLeftCallback = (user) => { setUsers(prev => prev.map(u => u.id === user.id ? { ...u, active: false, lastSeen: Date.now() } : u ) ); }; // 定期清理不活跃用户 const cleanupInterval = setInterval(() => { const now = Date.now(); setUsers(prev => prev.filter(u => u.active || now - u.lastSeen < 300000) // 5分钟 ); }, 60000); return () => { clearInterval(cleanupInterval); if (collaborationRef.current) { collaborationRef.current.disconnect(); } }; }, [diagramId, currentUser.id]); // 发送光标位置 const sendCursorPosition = useCallback((x, y) => { if (collaborationRef.current) { collaborationRef.current.sendUpdate({ type: 'cursor_move', position: { x, y }, userId: currentUser.id, timestamp: Date.now() }, true); // 立即发送光标更新 } }, [currentUser.id]); return ( <div className="user-presence"> <div className="active-users"> {users.filter(u => u.active).map(user => ( <div key={user.id} className="user-badge" style={{ backgroundColor: user.color, color: '#fff' }}> {user.name.charAt(0)} </div> ))} </div> </div> ); }; 第六部分:WordPress REST API集成 6.1 创建自定义API端点 // includes/class-api.php class CDT_API { public static function init() { add_action('rest_api_init', [self::class, 'register_routes']); } public static function register_routes() { // 图表CRUD端点 register_rest_route('cdt/v1', '/diagrams', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_diagrams'], 'permission_callback' => [self::class, 'check_permission'], 'args' => [ 'page' => [ 'required' => false, 'default' => 1, 'sanitize_callback' => 'absint' ], 'per_page' => [ 'required' => false, 'default' => 20, 'sanitize_callback' => 'absint' ] ] ], [ 'methods' => 'POST', 'callback' => [self::class, 'create_diagram'], 'permission_callback' => [self::class, 'check_permission'] ] ]); register_rest_route('cdt/v1', '/diagrams/(?P<id>d+)', [ [ 'methods' => 'GET', 'callback' => [self::class, 'get_diagram'], 'permission_callback' => [self::class, 'check_diagram_permission'] ], [ 'methods' => 'PUT', 'callback' => [self::class, 'update_diagram'], 'permission_callback' => [self::class, 'check_diagram_edit_permission'] ], [ 'methods' => 'DELETE', 'callback' => [self::class, 'delete_diagram'], 'permission_callback' => [self::class, 'check_diagram_admin_permission'] ] ]); // 实时更新端点 register_rest_route('cdt/v1', '/diagrams/(?P<id>d+)/update', [ 'methods' => 'POST', 'callback' => [self::class, 'update_diagram_content'], 'permission_callback' => [self::class, 'check_diagram_edit_permission'] ]); // 协作管理端点 register_rest_route('cdt/v1', '/diagrams/(?P<id>d+)/collaborators', [ 'methods' => 'GET', 'callback' => [self::class, 'get_collaborators'], 'permission_callback' => [self::class, 'check_diagram_permission'] ]); register_rest_route('cdt/v1', '/diagrams/(?P<id>d+)/collaborators/(?P<user_id>d+)', [ 'methods' => 'PUT', 'callback' => [self::class, 'update_collaborator'], 'permission_callback' => [self::class, 'check_diagram_admin_permission'] ]); } public static function check_permission($request) { return is_user_logged_in(); } public static function check_diagram_permission($request) { $diagram_id = $request->get_param('id');

发表评论

一步步实现,为WordPress打造智能化的内容过期检测与更新提醒工具

一步步实现:为WordPress打造智能化的内容过期检测与更新提醒工具 引言:为什么WordPress需要内容过期检测功能 在当今信息爆炸的时代,网站内容的时效性变得尤为重要。无论是新闻资讯、产品评测、技术教程还是行业分析,过时的内容不仅会降低用户体验,还可能影响网站的权威性和搜索引擎排名。对于拥有大量内容的WordPress网站来说,手动跟踪每篇文章的时效性几乎是不可能的任务。 据统计,超过60%的企业网站存在大量过时内容,这些内容可能导致用户流失率增加40%以上。同时,搜索引擎越来越重视内容的时效性,新鲜度已成为排名算法的重要因素之一。 本文将通过WordPress代码二次开发,打造一个智能化的内容过期检测与更新提醒工具,帮助网站管理员自动识别需要更新的内容,确保网站始终保持活力与相关性。 第一部分:需求分析与功能规划 1.1 核心需求分析 在开始开发之前,我们需要明确工具的核心需求: 自动检测内容时效性:根据预设规则判断内容是否过期 智能提醒机制:通过多种渠道通知相关人员 优先级分类系统:区分内容的紧急更新程度 数据统计与分析:提供内容时效性的整体报告 灵活的配置选项:允许不同网站根据需求调整参数 1.2 功能模块设计 基于以上需求,我们将工具分为以下几个模块: 过期检测引擎:核心检测逻辑 提醒通知系统:邮件、站内信、Slack等通知方式 管理界面:后台配置和内容管理 数据统计面板:可视化报告和数据分析 API接口:与其他系统集成的可能性 第二部分:开发环境准备与基础架构 2.1 开发环境配置 首先,我们需要准备一个安全的开发环境: // 创建插件基础结构 /* Plugin Name: 智能内容过期检测工具 Description: 自动检测WordPress内容时效性并发送更新提醒 Version: 1.0.0 Author: Your Name */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } 2.2 数据库表设计 为了存储检测结果和配置信息,我们需要创建必要的数据库表: class ContentExpiry_Setup { public static function activate() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'content_expiry_logs'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, post_id bigint(20) NOT NULL, post_type varchar(50) NOT NULL, expiry_status varchar(50) NOT NULL, expiry_date datetime DEFAULT NULL, last_checked datetime DEFAULT CURRENT_TIMESTAMP, notified tinyint(1) DEFAULT 0, notification_sent_date datetime DEFAULT NULL, priority_level int(11) DEFAULT 1, custom_notes text, PRIMARY KEY (id), KEY post_id (post_id), KEY expiry_status (expiry_status), KEY priority_level (priority_level) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建配置表 $config_table = $wpdb->prefix . 'content_expiry_config'; $sql_config = "CREATE TABLE IF NOT EXISTS $config_table ( config_id bigint(20) NOT NULL AUTO_INCREMENT, config_key varchar(100) NOT NULL, config_value text, config_group varchar(50) DEFAULT 'general', PRIMARY KEY (config_id), UNIQUE KEY config_key (config_key) ) $charset_collate;"; dbDelta($sql_config); } } 第三部分:核心检测引擎的实现 3.1 过期检测算法设计 内容过期检测需要考虑多个因素: class ContentExpiry_Detector { private $detection_rules = array(); public function __construct() { $this->load_detection_rules(); } private function load_detection_rules() { // 默认检测规则 $this->detection_rules = array( 'time_based' => array( 'enabled' => true, 'max_age_days' => 365, // 默认365天为过期 'warning_before_days' => 30 // 提前30天警告 ), 'reference_based' => array( 'enabled' => true, 'check_external_links' => true, 'broken_link_threshold' => 3 ), 'engagement_based' => array( 'enabled' => true, 'comment_threshold' => 50, 'view_threshold' => 1000 ), 'seasonal_content' => array( 'enabled' => true, 'seasonal_months' => array(12, 1, 2) // 季节性内容检测 ) ); } public function check_post_expiry($post_id) { $post = get_post($post_id); if (!$post) { return false; } $expiry_data = array( 'post_id' => $post_id, 'post_type' => $post->post_type, 'checks' => array(), 'score' => 0, 'status' => 'fresh' ); // 执行各项检测 $expiry_data['checks']['time_check'] = $this->check_by_time($post); $expiry_data['checks']['reference_check'] = $this->check_references($post); $expiry_data['checks']['engagement_check'] = $this->check_engagement($post); $expiry_data['checks']['seasonal_check'] = $this->check_seasonal($post); // 计算综合分数 $expiry_data['score'] = $this->calculate_expiry_score($expiry_data['checks']); $expiry_data['status'] = $this->determine_status($expiry_data['score']); return $expiry_data; } private function check_by_time($post) { $post_date = strtotime($post->post_date); $current_time = current_time('timestamp'); $days_old = floor(($current_time - $post_date) / (60 * 60 * 24)); $max_age = $this->detection_rules['time_based']['max_age_days']; $warning_days = $this->detection_rules['time_based']['warning_before_days']; $result = array( 'days_old' => $days_old, 'max_allowed' => $max_age, 'status' => 'fresh' ); if ($days_old > $max_age) { $result['status'] = 'expired'; } elseif ($days_old > ($max_age - $warning_days)) { $result['status'] = 'warning'; } return $result; } private function check_references($post) { // 检查外部链接是否有效 $content = $post->post_content; $broken_links = 0; $total_links = 0; // 使用正则表达式提取链接 preg_match_all('/href=["'](https?://[^"']+)["']/i', $content, $matches); if (!empty($matches[1])) { $total_links = count($matches[1]); // 抽样检查部分链接 $sample_links = array_slice($matches[1], 0, min(5, $total_links)); foreach ($sample_links as $link) { if (!$this->check_link_status($link)) { $broken_links++; } } } return array( 'total_links' => $total_links, 'broken_links' => $broken_links, 'status' => ($broken_links > 2) ? 'warning' : 'fresh' ); } private function check_link_status($url) { // 简化的链接检查 $headers = @get_headers($url); if (!$headers) { return false; } $status_code = substr($headers[0], 9, 3); return ($status_code == '200' || $status_code == '301' || $status_code == '302'); } } 3.2 智能检测算法优化 为了提高检测的准确性,我们可以引入机器学习概念: class ContentExpiry_AI_Detector extends ContentExpiry_Detector { private $learning_data = array(); public function __construct() { parent::__construct(); $this->load_learning_data(); } private function load_learning_data() { // 从数据库加载历史学习数据 global $wpdb; $table_name = $wpdb->prefix . 'content_expiry_learning'; $results = $wpdb->get_results("SELECT * FROM $table_name LIMIT 1000"); foreach ($results as $row) { $this->learning_data[] = array( 'features' => unserialize($row->feature_vector), 'label' => $row->expiry_label ); } } public function enhanced_check($post_id) { $basic_result = parent::check_post_expiry($post_id); // 添加AI增强检测 $ai_features = $this->extract_ai_features($post_id); $ai_prediction = $this->predict_expiry($ai_features); // 结合传统检测和AI预测 $final_score = ($basic_result['score'] * 0.7) + ($ai_prediction * 0.3); $basic_result['ai_prediction'] = $ai_prediction; $basic_result['enhanced_score'] = $final_score; $basic_result['status'] = $this->determine_status($final_score); return $basic_result; } private function extract_ai_features($post_id) { $features = array(); // 提取多种特征 $post = get_post($post_id); // 1. 内容特征 $content = strip_tags($post->post_content); $features['content_length'] = strlen($content); $features['word_count'] = str_word_count($content); // 2. 互动特征 $features['comment_count'] = get_comments_number($post_id); $features['view_count'] = $this->get_post_views($post_id); // 3. SEO特征 $features['seo_score'] = $this->calculate_seo_score($content); // 4. 更新历史 $features['update_frequency'] = $this->get_update_frequency($post_id); return $features; } private function predict_expiry($features) { // 简化的预测算法 $score = 0; // 基于规则的基础预测 if ($features['content_length'] < 500) { $score += 20; // 短内容更容易过期 } if ($features['comment_count'] > 50) { $score -= 15; // 高互动内容更持久 } if ($features['update_frequency'] < 0.5) { $score += 25; // 很少更新的内容 } return min(max($score, 0), 100); } } 第四部分:通知提醒系统的构建 4.1 多渠道通知系统 class ContentExpiry_Notifier { private $notification_methods = array(); public function __construct() { $this->init_notification_methods(); } private function init_notification_methods() { $this->notification_methods = array( 'email' => array( 'enabled' => true, 'priority' => 1, 'handler' => 'send_email_notification' ), 'slack' => array( 'enabled' => false, 'priority' => 2, 'handler' => 'send_slack_notification' ), 'webhook' => array( 'enabled' => false, 'priority' => 3, 'handler' => 'send_webhook_notification' ), 'dashboard' => array( 'enabled' => true, 'priority' => 0, 'handler' => 'add_dashboard_notice' ) ); } public function send_expiry_notification($post_id, $expiry_data) { $notifications_sent = array(); // 按优先级排序 uasort($this->notification_methods, function($a, $b) { return $a['priority'] <=> $b['priority']; }); foreach ($this->notification_methods as $method => $config) { if ($config['enabled']) { $handler = $config['handler']; if (method_exists($this, $handler)) { $result = $this->$handler($post_id, $expiry_data); if ($result) { $notifications_sent[] = $method; } } } } return $notifications_sent; } private function send_email_notification($post_id, $expiry_data) { $post = get_post($post_id); $admin_email = get_option('admin_email'); $subject = sprintf('[内容过期提醒] %s 需要更新', $post->post_title); $message = $this->build_email_template($post, $expiry_data); $headers = array( 'Content-Type: text/html; charset=UTF-8', 'From: WordPress内容管理系统 <noreply@' . $_SERVER['HTTP_HOST'] . '>' ); return wp_mail($admin_email, $subject, $message, $headers); } private function build_email_template($post, $expiry_data) { $template = ' <!DOCTYPE html> <html> <head> <style> body { font-family: Arial, sans-serif; line-height: 1.6; } .container { max-width: 600px; margin: 0 auto; padding: 20px; } .header { background: #f8f9fa; padding: 20px; border-radius: 5px; } .content { padding: 20px 0; } .status-badge { display: inline-block; padding: 5px 10px; border-radius: 3px; color: white; font-weight: bold; } .status-expired { background: #dc3545; } .status-warning { background: #ffc107; color: #000; } .status-fresh { background: #28a745; } .button { display: inline-block; padding: 10px 20px; background: #0073aa; color: white; text-decoration: none; border-radius: 3px; } </style> </head> <body> <div class="container"> <div class="header"> <h2>内容过期检测提醒</h2> </div> <div class="content"> <h3>' . esc_html($post->post_title) . '</h3> <p><strong>状态:</strong> <span class="status-badge status-' . $expiry_data['status'] . '"> ' . $this->get_status_text($expiry_data['status']) . ' </span> </p> <p><strong>过期评分:</strong> ' . $expiry_data['score'] . '/100</p> <h4>检测详情:</h4> <ul>'; foreach ($expiry_data['checks'] as $check_name => $check_result) { $template .= '<li>' . $this->format_check_result($check_name, $check_result) . '</li>'; } $template .= ' </ul> <p>建议您在方便的时候更新此内容,以保持网站的新鲜度和权威性。</p> <a href="' . get_edit_post_link($post->ID) . '" class="button"> 立即编辑内容 </a> <p style="margin-top: 30px; color: #666; font-size: 12px;"> 此邮件由智能内容过期检测系统自动发送。<br> 如需调整通知设置,请访问插件设置页面。 </p> </div> </div> </body> </html>'; return $template; } private function send_slack_notification($post_id, $expiry_data) { $slack_webhook = get_option('content_expiry_slack_webhook'); if (!$slack_webhook) { return false; } $post = get_post($post_id); $message = array( 'text' => sprintf('*内容过期提醒*: %s', $post->post_title), 'attachments' => array( array( 'color' => $this->get_slack_color($expiry_data['status']), 'fields' => array( array( 'title' => '状态', 'value' => $this->get_status_text($expiry_data['status']), 'short' => true ), array( 'title' => '评分', 'value' => $expiry_data['score'] . '/100', 'short' => true ), array( 'title' => '操作', 'value' => '<' . get_edit_post_link($post_id) . '|编辑内容>', 'short' => false ) ) ) ) ); $args = array( 'body' => json_encode($message), 'headers' => array( 第四部分:通知提醒系统的构建(续) 4.2 智能通知调度与频率控制 class ContentExpiry_Notification_Scheduler { private $notification_log = array(); public function __construct() { add_action('content_expiry_daily_check', array($this, 'daily_notification_batch')); } public function schedule_notification($post_id, $expiry_data) { $notification_needed = $this->should_notify($post_id, $expiry_data); if (!$notification_needed) { return false; } // 根据紧急程度安排通知时间 $schedule_time = $this->calculate_schedule_time($expiry_data['status']); // 将通知加入队列 $this->add_to_notification_queue($post_id, $expiry_data, $schedule_time); return true; } private function should_notify($post_id, $expiry_data) { // 检查是否已发送过通知 $last_notification = $this->get_last_notification_time($post_id); if ($last_notification) { $days_since_last = (time() - $last_notification) / (60 * 60 * 24); // 根据状态确定通知频率 $min_interval_days = $this->get_notification_interval($expiry_data['status']); if ($days_since_last < $min_interval_days) { return false; } } // 检查是否达到通知阈值 return $expiry_data['score'] >= $this->get_notification_threshold($expiry_data['status']); } private function get_notification_interval($status) { $intervals = array( 'expired' => 7, // 过期内容每周提醒一次 'warning' => 14, // 警告内容每两周提醒一次 'fresh' => 30 // 新鲜内容每月提醒一次 ); return isset($intervals[$status]) ? $intervals[$status] : 30; } public function daily_notification_batch() { global $wpdb; $table_name = $wpdb->prefix . 'content_expiry_logs'; // 获取需要今天发送通知的内容 $today = current_time('mysql'); $query = $wpdb->prepare( "SELECT * FROM $table_name WHERE expiry_status IN ('expired', 'warning') AND notified = 0 AND DATE(expiry_date) <= DATE(%s) ORDER BY priority_level DESC", $today ); $expired_items = $wpdb->get_results($query); if (empty($expired_items)) { return; } // 分组处理,避免一次性发送太多通知 $batches = array_chunk($expired_items, 10); // 每批10个 foreach ($batches as $batch) { $this->process_notification_batch($batch); } } private function process_notification_batch($items) { $notifier = new ContentExpiry_Notifier(); foreach ($items as $item) { $expiry_data = array( 'status' => $item->expiry_status, 'score' => $item->priority_level * 10, 'checks' => unserialize($item->custom_notes) ); $notifier->send_expiry_notification($item->post_id, $expiry_data); // 标记为已通知 $this->mark_as_notified($item->id); } } private function mark_as_notified($log_id) { global $wpdb; $table_name = $wpdb->prefix . 'content_expiry_logs'; $wpdb->update( $table_name, array( 'notified' => 1, 'notification_sent_date' => current_time('mysql') ), array('id' => $log_id) ); } } 4.3 邮件模板优化与个性化 class ContentExpiry_Email_Templates { private $templates = array(); public function __construct() { $this->load_templates(); } private function load_templates() { $this->templates = array( 'expired' => array( 'subject' => '紧急:内容已过期 - {post_title}', 'priority' => 'high', 'template' => 'expired-template.php' ), 'warning' => array( 'subject' => '提醒:内容即将过期 - {post_title}', 'priority' => 'normal', 'template' => 'warning-template.php' ), 'weekly_summary' => array( 'subject' => '内容过期情况周报 - {site_name}', 'priority' => 'low', 'template' => 'weekly-summary.php' ) ); } public function get_template($type, $data = array()) { if (!isset($this->templates[$type])) { $type = 'warning'; } $template_config = $this->templates[$type]; // 加载模板文件 $template_path = plugin_dir_path(__FILE__) . 'templates/email/' . $template_config['template']; if (file_exists($template_path)) { ob_start(); extract($data); include($template_path); return ob_get_clean(); } // 如果模板文件不存在,返回默认模板 return $this->get_default_template($type, $data); } private function get_default_template($type, $data) { $default_templates = array( 'expired' => ' <h2>紧急内容更新通知</h2> <p>您的内容 <strong>{post_title}</strong> 已被标记为过期。</p> <p>过期时间:{expiry_date}</p> <p>过期评分:{expiry_score}/100</p> <p>请尽快更新此内容以保持网站质量。</p> <a href="{edit_link}" style="background: #dc3545; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;"> 立即更新 </a> ', 'weekly_summary' => ' <h2>内容过期情况周报</h2> <p>本周内容过期统计:</p> <ul> <li>过期内容:{expired_count} 篇</li> <li>即将过期:{warning_count} 篇</li> <li>需要关注:{attention_count} 篇</li> </ul> <p>详细报告请登录后台查看。</p> ' ); $template = isset($default_templates[$type]) ? $default_templates[$type] : $default_templates['expired']; // 替换变量 foreach ($data as $key => $value) { $template = str_replace('{' . $key . '}', $value, $template); } return $template; } public function send_weekly_summary() { $stats = $this->get_weekly_stats(); $data = array( 'site_name' => get_bloginfo('name'), 'expired_count' => $stats['expired'], 'warning_count' => $stats['warning'], 'attention_count' => $stats['attention'], 'report_date' => date('Y年m月d日'), 'admin_url' => admin_url('admin.php?page=content-expiry-reports') ); $subject = $this->templates['weekly_summary']['subject']; $subject = str_replace('{site_name}', $data['site_name'], $subject); $message = $this->get_template('weekly_summary', $data); // 获取所有管理员邮箱 $admin_emails = $this->get_admin_emails(); foreach ($admin_emails as $email) { wp_mail($email, $subject, $message, array('Content-Type: text/html; charset=UTF-8')); } } private function get_weekly_stats() { global $wpdb; $table_name = $wpdb->prefix . 'content_expiry_logs'; $week_ago = date('Y-m-d', strtotime('-7 days')); $stats = $wpdb->get_row(" SELECT SUM(CASE WHEN expiry_status = 'expired' THEN 1 ELSE 0 END) as expired, SUM(CASE WHEN expiry_status = 'warning' THEN 1 ELSE 0 END) as warning, SUM(CASE WHEN expiry_status = 'attention' THEN 1 ELSE 0 END) as attention FROM $table_name WHERE last_checked >= '$week_ago' ", ARRAY_A); return $stats ?: array('expired' => 0, 'warning' => 0, 'attention' => 0); } } 第五部分:管理界面与用户交互 5.1 后台管理页面开发 class ContentExpiry_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_content_expiry_actions', array($this, 'handle_ajax_requests')); } public function add_admin_menu() { // 主菜单 add_menu_page( '内容过期检测', '内容过期', 'manage_options', 'content-expiry', array($this, 'render_main_page'), 'dashicons-calendar-alt', 30 ); // 子菜单 add_submenu_page( 'content-expiry', '过期内容列表', '过期内容', 'manage_options', 'content-expiry-list', array($this, 'render_expired_list') ); add_submenu_page( 'content-expiry', '检测设置', '设置', 'manage_options', 'content-expiry-settings', array($this, 'render_settings_page') ); add_submenu_page( 'content-expiry', '统计报告', '报告', 'manage_options', 'content-expiry-reports', array($this, 'render_reports_page') ); } public function render_main_page() { ?> <div class="wrap content-expiry-dashboard"> <h1 class="wp-heading-inline">内容过期检测仪表板</h1> <div class="dashboard-widgets"> <div class="widget-card"> <h3>内容健康度概览</h3> <div class="health-score"> <?php $this->render_health_score(); ?> </div> </div> <div class="widget-card"> <h3>紧急任务</h3> <div class="urgent-tasks"> <?php $this->render_urgent_tasks(); ?> </div> </div> <div class="widget-card full-width"> <h3>最近检测结果</h3> <div class="recent-checks"> <?php $this->render_recent_checks(); ?> </div> </div> </div> <div class="quick-actions"> <button class="button button-primary" onclick="runQuickScan()"> <span class="dashicons dashicons-update"></span> 快速扫描 </button> <button class="button button-secondary" onclick="viewFullReport()"> <span class="dashicons dashicons-chart-bar"></span> 查看完整报告 </button> <button class="button" onclick="manageNotifications()"> <span class="dashicons dashicons-email"></span> 通知设置 </button> </div> </div> <script> function runQuickScan() { jQuery.post(ajaxurl, { action: 'content_expiry_actions', task: 'quick_scan' }, function(response) { alert('扫描完成:' + response.message); location.reload(); }); } </script> <style> .content-expiry-dashboard { padding: 20px; } .dashboard-widgets { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 20px 0; } .widget-card { background: white; border: 1px solid #ccd0d4; border-radius: 4px; padding: 20px; box-shadow: 0 1px 1px rgba(0,0,0,.04); } .widget-card.full-width { grid-column: 1 / -1; } .quick-actions { margin-top: 30px; display: flex; gap: 10px; } </style> <?php } private function render_health_score() { global $wpdb; $table_name = $wpdb->prefix . 'content_expiry_logs'; $stats = $wpdb->get_row(" SELECT COUNT(*) as total, AVG(priority_level) as avg_score, SUM(CASE WHEN expiry_status = 'expired' THEN 1 ELSE 0 END) as expired_count FROM $table_name "); if ($stats) { $health_score = 100 - ($stats->expired_count / max(1, $stats->total) * 100); ?> <div class="score-circle" style=" width: 120px; height: 120px; border-radius: 50%; background: conic-gradient( #4CAF50 <?php echo $health_score * 3.6; ?>deg, #f0f0f0 0deg ); display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; "> <div style=" background: white; width: 80px; height: 80px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 24px; font-weight: bold; "> <?php echo round($health_score); ?>% </div> </div> <p>已检测内容:<?php echo $stats->total; ?> 篇</p> <p>过期内容:<?php echo $stats->expired_count; ?> 篇</p> <?php } } public function render_expired_list() { ?> <div class="wrap"> <h1 class="wp-heading-inline">过期内容管理</h1> <div class="tablenav top"> <div class="alignleft actions"> <select name="filter_status"> <option value="">所有状态</option> <option value="expired">已过期</option> <option value="warning">即将过期</option> <option value="fresh">正常</option> </select> <select name="filter_post_type"> <option value="">所有类型</option> <?php $post_types = get_post_types(array('public' => true), 'objects'); foreach ($post_types as $post_type) { echo '<option value="' . $post_type->name . '">' . $post_type->label . '</option>'; } ?> </select> <button class="button" onclick="filterContent()">筛选</button> </div> <div class="alignright"> <button class="button button-primary" onclick="exportToCSV()"> <span class="dashicons dashicons-download"></span> 导出CSV </button> </div> </div> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th width="50">ID</th> <th>标题</th> <th width="100">类型</th> <th width="100">状态</th> <th width="120">过期时间</th> <th width="100">优先级</th> <th width="150">操作</th> </tr> </thead> <tbody id="expired-content-list"> <?php $this->render_expired_content_rows(); ?> </tbody> </table> <div class="tablenav bottom"> <div class="tablenav-pages"> <?php $this->render_pagination(); ?> </div> </div> </div> <script> function filterContent() { var status = jQuery('select[name="filter_status"]').val(); var postType = jQuery('select[name="filter_post_type"]').val(); jQuery.post(ajaxurl, { action: 'content_expiry_actions', task: 'filter_content', status: status, post_type: postType }, function(response) { jQuery('#expired-content-list').html(response.data); }); } function exportToCSV() { window.location.href = ajaxurl + '?action=content_expiry_export&type=csv'; } </script> <?php } private function render_expired_content_rows() { global $wpdb; $table_name = $wpdb->prefix . 'content_expiry_logs'; $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $per_page = 20; $offset = ($page - 1) * $per_page; $items = $wpdb->get_results(" SELECT l.*, p.post_title, p.post_type FROM $table_name l

发表评论