跳至内容

分类: 网站建设

WordPress插件开发教程,实现网站内容自动同步至新闻资讯聚合平台

WordPress插件开发教程:实现网站内容自动同步至新闻资讯聚合平台 引言:WordPress插件开发的无限可能 在当今数字化时代,内容分发已成为网站运营的关键环节。WordPress作为全球最受欢迎的内容管理系统,其强大的可扩展性为开发者提供了无限可能。本教程将深入探讨如何通过WordPress插件开发,实现网站内容自动同步至新闻资讯聚合平台,并在此过程中展示如何通过代码二次开发实现常用互联网小工具功能。 随着信息爆炸式增长,单一平台的内容传播已无法满足现代网站的需求。新闻资讯聚合平台如Flipboard、今日头条、Google新闻等已成为用户获取信息的重要渠道。通过自动化同步机制,WordPress网站可以显著扩大内容覆盖面,提高流量和影响力。 第一部分:WordPress插件开发基础 1.1 WordPress插件架构概述 WordPress插件本质上是一组PHP文件,遵循特定的结构和命名规范。一个标准的WordPress插件至少包含: 主插件文件(必须包含插件头信息) 功能实现文件 可选的CSS、JavaScript和图像资源 插件头信息是插件的"身份证",必须放置在主插件文件的顶部: <?php /** * Plugin Name: 内容同步与工具增强插件 * Plugin URI: https://yourwebsite.com/sync-plugin * Description: 自动同步WordPress内容至新闻聚合平台,并提供实用小工具功能 * Version: 1.0.0 * Author: 你的名字 * Author URI: https://yourwebsite.com * License: GPL v2 or later * Text Domain: content-sync-tools */ 1.2 插件开发环境搭建 在开始开发前,需要准备以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel 代码编辑器:VS Code、PHPStorm或Sublime Text 测试WordPress安装:建议使用最新版本的WordPress 调试工具:启用WP_DEBUG,安装Query Monitor插件 创建插件目录结构: /wp-content/plugins/content-sync-tools/ ├── content-sync-tools.php # 主插件文件 ├── includes/ # 包含文件目录 │ ├── class-sync-manager.php # 同步管理器 │ ├── class-tools-manager.php # 工具管理器 │ └── class-api-handler.php # API处理器 ├── admin/ # 后台管理文件 │ ├── css/ │ ├── js/ │ └── admin-settings.php ├── public/ # 前端文件 │ ├── css/ │ └── js/ └── uninstall.php # 插件卸载处理 第二部分:内容自动同步功能实现 2.1 同步机制设计原理 内容自动同步的核心是在特定事件触发时,将文章数据发送到目标平台。WordPress提供了多种钩子(Hooks)来实现这一功能: 发布钩子:publish_post - 文章发布时触发 更新钩子:save_post - 文章保存时触发 定时发布钩子:future_to_publish - 定时文章发布时触发 同步流程设计: 用户发布或更新文章 插件捕获发布事件 提取文章数据并格式化 通过API发送到目标聚合平台 记录同步状态和结果 2.2 同步管理器类实现 创建同步管理器类,负责处理所有同步逻辑: <?php // includes/class-sync-manager.php class Content_Sync_Manager { private $target_platforms; private $sync_options; public function __construct() { $this->target_platforms = array( 'news_aggregator_1' => array( 'name' => '新闻聚合平台A', 'api_endpoint' => 'https://api.platform-a.com/v1/submit', 'api_key' => '', 'enabled' => false ), 'news_aggregator_2' => array( 'name' => '新闻聚合平台B', 'api_endpoint' => 'https://api.platform-b.com/publish', 'api_key' => '', 'enabled' => false ) ); $this->sync_options = get_option('content_sync_settings', array()); $this->init_hooks(); } private function init_hooks() { // 文章发布时触发同步 add_action('publish_post', array($this, 'sync_on_publish'), 10, 2); // 文章更新时触发同步 add_action('save_post', array($this, 'sync_on_update'), 10, 3); // 添加管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 注册设置 add_action('admin_init', array($this, 'register_settings')); } public function sync_on_publish($post_id, $post) { // 防止无限循环 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) return; if (wp_is_post_revision($post_id)) return; // 检查文章类型 if ($post->post_type !== 'post') return; // 检查是否已同步 $already_synced = get_post_meta($post_id, '_synced_to_platforms', true); if ($already_synced) return; // 执行同步 $this->sync_post($post_id); } private function sync_post($post_id) { $post = get_post($post_id); $author = get_userdata($post->post_author); $categories = wp_get_post_categories($post_id, array('fields' => 'names')); $tags = wp_get_post_tags($post_id, array('fields' => 'names')); // 准备同步数据 $sync_data = array( 'title' => $post->post_title, 'content' => $this->prepare_content($post->post_content), 'excerpt' => $post->post_excerpt ?: wp_trim_words(strip_tags($post->post_content), 55, '...'), 'author' => $author->display_name, 'categories' => $categories, 'tags' => $tags, 'publish_date' => $post->post_date, 'url' => get_permalink($post_id), 'featured_image' => get_the_post_thumbnail_url($post_id, 'full'), 'source' => get_bloginfo('name'), 'source_url' => get_site_url() ); $synced_platforms = array(); // 遍历所有平台进行同步 foreach ($this->target_platforms as $platform_id => $platform) { if (isset($this->sync_options[$platform_id . '_enabled']) && $this->sync_options[$platform_id . '_enabled'] === '1') { $api_key = isset($this->sync_options[$platform_id . '_api_key']) ? $this->sync_options[$platform_id . '_api_key'] : ''; $result = $this->send_to_platform($platform_id, $sync_data, $api_key); if ($result['success']) { $synced_platforms[] = $platform_id; // 记录日志 $this->log_sync($post_id, $platform_id, 'success', $result['message']); } else { $this->log_sync($post_id, $platform_id, 'error', $result['message']); } } } // 更新同步状态 if (!empty($synced_platforms)) { update_post_meta($post_id, '_synced_to_platforms', $synced_platforms); update_post_meta($post_id, '_last_sync_time', current_time('mysql')); } } private function prepare_content($content) { // 清理和格式化内容 $content = strip_shortcodes($content); $content = strip_tags($content, '<p><br><h2><h3><strong><em><ul><ol><li><a>'); // 移除多余空白 $content = preg_replace('/s+/', ' ', $content); // 限制长度(根据平台要求调整) $max_length = 5000; if (strlen($content) > $max_length) { $content = substr($content, 0, $max_length) . '...'; } return $content; } private function send_to_platform($platform_id, $data, $api_key) { $platform = $this->target_platforms[$platform_id]; // 根据平台要求格式化数据 $formatted_data = $this->format_for_platform($platform_id, $data); // 发送HTTP请求 $args = array( 'method' => 'POST', 'timeout' => 30, 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key, 'User-Agent' => 'WordPress Content Sync Plugin/1.0' ), 'body' => json_encode($formatted_data) ); $response = wp_remote_post($platform['api_endpoint'], $args); if (is_wp_error($response)) { return array( 'success' => false, 'message' => $response->get_error_message() ); } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); if ($response_code >= 200 && $response_code < 300) { return array( 'success' => true, 'message' => '同步成功: ' . $response_body ); } else { return array( 'success' => false, 'message' => '同步失败 (HTTP ' . $response_code . '): ' . $response_body ); } } private function format_for_platform($platform_id, $data) { // 根据不同平台的API要求格式化数据 switch ($platform_id) { case 'news_aggregator_1': return array( 'article' => array( 'title' => $data['title'], 'body' => $data['content'], 'summary' => $data['excerpt'], 'author' => $data['author'], 'categories' => $data['categories'], 'tags' => $data['tags'], 'published_at' => $data['publish_date'], 'original_url' => $data['url'], 'image_url' => $data['featured_image'] ) ); case 'news_aggregator_2': return array( 'title' => $data['title'], 'content' => $data['content'], 'description' => $data['excerpt'], 'author_name' => $data['author'], 'topics' => array_merge($data['categories'], $data['tags']), 'source' => array( 'name' => $data['source'], 'url' => $data['source_url'] ), 'url' => $data['url'], 'images' => $data['featured_image'] ? array($data['featured_image']) : array() ); default: return $data; } } private function log_sync($post_id, $platform_id, $status, $message) { $log_entry = array( 'time' => current_time('mysql'), 'post_id' => $post_id, 'platform' => $platform_id, 'status' => $status, 'message' => $message ); $logs = get_option('content_sync_logs', array()); $logs[] = $log_entry; // 只保留最近100条日志 if (count($logs) > 100) { $logs = array_slice($logs, -100); } update_option('content_sync_logs', $logs); } // 管理界面方法将在下一部分实现 public function add_admin_menu() { /* ... */ } public function register_settings() { /* ... */ } } 2.3 后台管理界面实现 创建用户友好的后台界面,让网站管理员可以配置同步设置: <?php // admin/admin-settings.php class Content_Sync_Admin { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_init', array($this, 'register_settings')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } public function add_admin_menu() { add_menu_page( '内容同步设置', '内容同步', 'manage_options', 'content-sync-settings', array($this, 'render_settings_page'), 'dashicons-share', 30 ); add_submenu_page( 'content-sync-settings', '同步日志', '同步日志', 'manage_options', 'content-sync-logs', array($this, 'render_logs_page') ); } public function register_settings() { register_setting('content_sync_settings_group', 'content_sync_settings'); // 平台A设置 add_settings_section( 'platform_a_section', '新闻聚合平台A设置', array($this, 'render_platform_a_section'), 'content-sync-settings' ); add_settings_field( 'platform_a_enabled', '启用平台A', array($this, 'render_checkbox_field'), 'content-sync-settings', 'platform_a_section', array( 'label_for' => 'platform_a_enabled', 'description' => '启用同步到新闻聚合平台A' ) ); add_settings_field( 'platform_a_api_key', 'API密钥', array($this, 'render_text_field'), 'content-sync-settings', 'platform_a_section', array( 'label_for' => 'platform_a_api_key', 'description' => '从平台A获取的API密钥' ) ); // 平台B设置 add_settings_section( 'platform_b_section', '新闻聚合平台B设置', array($this, 'render_platform_b_section'), 'content-sync-settings' ); // 类似地添加平台B的字段... } public function render_settings_page() { ?> <div class="wrap"> <h1>内容同步设置</h1> <form method="post" action="options.php"> <?php settings_fields('content_sync_settings_group'); do_settings_sections('content-sync-settings'); submit_button(); ?> </form> <div class="sync-test-section"> <h2>测试同步功能</h2> <p>选择一篇文章测试同步功能:</p> <select id="test_post_select"> <option value="">选择文章...</option> <?php $recent_posts = wp_get_recent_posts(array( 'numberposts' => 10, 'post_status' => 'publish' )); foreach ($recent_posts as $post) { echo '<option value="' . $post['ID'] . '">' . $post['post_title'] . '</option>'; } ?> </select> <button id="test_sync_btn" class="button button-secondary">测试同步</button> <div id="test_result" style="margin-top: 15px; display: none;"></div> </div> </div> <?php } public function render_logs_page() { $logs = get_option('content_sync_logs', array()); ?> <div class="wrap"> <h1>同步日志</h1> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>时间</th> <th>文章ID</th> <th>平台</th> <th>状态</th> <th>消息</th> </tr> </thead> <tbody> <?php if (empty($logs)): ?> <tr> <td colspan="5">暂无同步日志</td> </tr> <?php else: ?> <?php foreach (array_reverse($logs) as $log): ?> <tr> <td><?php echo $log['time']; ?></td> <td> <a href="<?php echo get_edit_post_link($log['post_id']); ?>"> <?php echo $log['post_id']; ?> </a> </td> <td><?php echo $log['platform']; ?></td> <td> <span class="sync-status <?php echo $log['status']; ?>"> <?php echo $log['status']; ?> </span> </td> <td><?php echo $log['message']; ?></td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <?php } public function enqueue_admin_scripts($hook) { if ($hook === 'toplevel_page_content-sync-settings' || $hook === 'content-sync_page_content-sync-logs') { wp_enqueue_style( 'content-sync-admin', plugin_dir_url(__FILE__) . 'css/admin-style.css', array(), '1.0.0' ); wp_enqueue_script( 'content-sync-admin', plugin_dir_url(__FILE__) . 'js/admin-script.js', array('jquery'), '1.0.0', true ); wp_localize_script('content-sync-admin', 'contentSyncAjax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('content_sync_test') )); } } public function render_checkbox_field($args) { $options = get_option('content_sync_settings'); $value = isset($options[$args['label_for']]) ? $options[$args['label_for']] : '0'; ?> <input type="checkbox" id="<?php echo esc_attr($args['label_for']); ?>" name="content_sync_settings[<?php echo esc_attr($args['label_for']); ?>]" value="1" <?php checked($value, '1'); ?>> <p class="description"><?php echo esc_html($args['description']); ?></p> <?php } public function render_text_field($args) { $options = get_option('content_sync_settings'); $value = isset($options[$args['label_for']]) ? $options[$args['label_for']] : ''; ?> <input type="text" id="<?php echo esc_attr($args['label_for']); ?>" name="content_sync_settings[<?php echo esc_attr($args['label_for']); ?>]" value="<?php echo esc_attr($value); ?>" class="regular-text"> <p class="description"><?php echo esc_html($args['description']); ?></p> <?php } } ## 第三部分:常用互联网小工具功能实现 ### 3.1 小工具管理器设计 除了内容同步功能,我们还可以为WordPress添加实用的互联网小工具。创建一个统一的小工具管理器: <?php// includes/class-tools-manager.php class Internet_Tools_Manager { private $available_tools; public function __construct() { $this->available_tools = array( 'url_shortener' => array( 'name' => 'URL短链接生成器', 'description' => '将长URL转换为短链接', 'class' => 'URL_Shortener_Tool' ), 'qr_code_generator' => array( 'name' => '二维码生成器', 'description' => '为URL或文本生成二维码', 'class' => 'QR_Code_Generator_Tool' ), 'social_share' => array( 'name' => '社交分享增强', 'description' => '增强文章社交分享功能', 'class' => 'Social_Share_Tool' ), 'related_content' => array( 'name' => '智能相关内容推荐', 'description' => '基于AI的内容推荐系统', 'class' => 'Related_Content_Tool' ) ); $this->init_hooks(); $this->load_tools(); } private function init_hooks() { add_action('widgets_init', array($this, 'register_widgets')); add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_scripts')); add_action('admin_menu', array($this, 'add_tools_admin_menu')); add_action('the_content', array($this, 'enhance_content'), 20); } private function load_tools() { foreach ($this->available_tools as $tool_id => $tool_info) { $tool_class = $tool_info['class']; $tool_file = plugin_dir_path(__FILE__) . 'tools/class-' . str_replace('_', '-', strtolower($tool_id)) . '.php'; if (file_exists($tool_file)) { require_once $tool_file; if (class_exists($tool_class)) { new $tool_class(); } } } } public function enhance_content($content) { if (!is_single() || !is_main_query()) { return $content; } global $post; // 在文章末尾添加工具 $enhanced_content = $content; // 添加社交分享按钮 if ($this->is_tool_enabled('social_share')) { $enhanced_content .= $this->get_social_share_buttons($post); } // 添加相关内容推荐 if ($this->is_tool_enabled('related_content')) { $enhanced_content .= $this->get_related_content($post); } // 添加二维码 if ($this->is_tool_enabled('qr_code_generator')) { $enhanced_content .= $this->get_qr_code_section($post); } return $enhanced_content; } private function is_tool_enabled($tool_id) { $tools_settings = get_option('internet_tools_settings', array()); return isset($tools_settings[$tool_id . '_enabled']) && $tools_settings[$tool_id . '_enabled'] === '1'; } private function get_social_share_buttons($post) { $post_url = urlencode(get_permalink($post->ID)); $post_title = urlencode(get_the_title($post->ID)); $post_excerpt = urlencode(wp_trim_words(get_the_excerpt($post), 20)); ob_start(); ?> <div class="social-share-tool"> <h3>分享这篇文章</h3> <div class="share-buttons"> <a href="https://www.facebook.com/sharer/sharer.php?u=<?php echo $post_url; ?>" target="_blank" class="share-btn facebook" title="分享到Facebook"> <span class="dashicons dashicons-facebook"></span> Facebook </a> <a href="https://twitter.com/intent/tweet?url=<?php echo $post_url; ?>&text=<?php echo $post_title; ?>" target="_blank" class="share-btn twitter" title="分享到Twitter"> <span class="dashicons dashicons-twitter"></span> Twitter </a> <a href="https://www.linkedin.com/shareArticle?mini=true&url=<?php echo $post_url; ?>&title=<?php echo $post_title; ?>&summary=<?php echo $post_excerpt; ?>" target="_blank" class="share-btn linkedin" title="分享到LinkedIn"> <span class="dashicons dashicons-linkedin"></span> LinkedIn </a> <a href="https://api.whatsapp.com/send?text=<?php echo $post_title . ' ' . $post_url; ?>" target="_blank" class="share-btn whatsapp" title="分享到WhatsApp"> <span class="dashicons dashicons-whatsapp"></span> WhatsApp </a> <button class="share-btn copy-link" title="复制链接" data-url="<?php echo get_permalink($post->ID); ?>"> <span class="dashicons dashicons-admin-links"></span> 复制链接 </button> </div> </div> <?php return ob_get_clean(); } private function get_related_content($post) { $related_posts = $this->find_related_posts($post); if (empty($related_posts)) { return ''; } ob_start(); ?> <div class="related-content-tool"> <h3>相关内容推荐</h3> <div class="related-posts-grid"> <?php foreach ($related_posts as $related_post): ?> <div class="related-post-item"> <a href="<?php echo get_permalink($related_post->ID); ?>"> <?php if (has_post_thumbnail($related_post->ID)): ?> <div class="related-post-thumbnail"> <?php echo get_the_post_thumbnail($related_post->ID, 'medium'); ?> </div> <?php endif; ?> <h4><?php echo get_the_title($related_post->ID); ?></h4> <p><?php echo wp_trim_words(get_the_excerpt($related_post->ID), 15); ?></p> </a> </div> <?php endforeach; ?> </div> </div> <?php return ob_get_clean(); } private function find_related_posts($post, $limit = 3) { $categories = wp_get_post_categories($post->ID, array('fields' => 'ids')); $tags = wp_get_post_tags($post->ID, array('fields' => 'ids')); $args = array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => $limit, 'post__not_in' => array($post->ID), 'orderby' => 'rand', 'tax_query' => array( 'relation' => 'OR', array( 'taxonomy' => 'category', 'field' => 'term_id', 'terms' => $categories ), array( 'taxonomy' => 'post_tag', 'field' => 'term_id', 'terms' => $tags ) ) ); return get_posts($args); } private function get_qr_code_section($post) { $post_url = get_permalink($post->ID); $qr_code_url = 'https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=' . urlencode($post_url); ob_start(); ?> <div class="qr-code-tool"> <h3>手机阅读二维码</h3> <div class="qr-code-container"> <img src="<?php echo esc_url($qr_code_url); ?>" alt="文章二维码" class="qr-code-image"> <div class="qr-code-info"> <p>扫描二维码,在手机上阅读此文章</p> <div class="url-shortener"> <input type="text" value="<?php echo esc_url($post_url); ?>" readonly class="url-display"> <button class="copy-url-btn" data-url="<?php echo esc_url($post_url); ?>"> 复制链接 </button> </div> </div> </div> </div> <?php return ob_get_clean(); } public function register_widgets() { register_widget('URL_Shortener_Widget'); register_widget('QR_Code_Widget'); } public function enqueue_frontend_scripts() { if (is_single()) { wp_enqueue_style( 'internet-tools-frontend', plugin_dir_url(__FILE__) . '../public/css/tools-style.css', array(), '1.0.0' ); wp_enqueue_script( 'internet-tools-frontend', plugin_dir_url(__FILE__) . '../public/js/tools-script.js', array('jquery'), '1.0.0', true ); wp_localize_script('internet-tools-frontend', 'toolsData', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('internet_tools_nonce') )); } } public function add_tools_admin_menu() { add_submenu_page( 'content-sync-settings', '小工具设置', '小工具设置', 'manage_options', 'internet-tools-settings', array($this, 'render_tools_settings_page') ); } public function render_tools_settings_page() { ?> <div class="wrap"> <h1>互联网小工具设置</h1> <form method="post" action="options.php"> <?php settings_fields('internet_tools_settings_group'); do_settings_sections('internet-tools-settings'); submit_button(); ?> </form> </div> <?php } } ### 3.2 URL短链接生成器实现 <?php// tools/class-url-shortener-tool.php class URL_Shortener_Tool { private $api_services; public function __construct() { $this->api_services = array( 'bitly' => array( 'name' => 'Bitly', 'endpoint' => 'https://api-ssl.bitly.com/v4/shorten', 'requires_key' => true ), 'tinyurl' => array( 'name' => 'TinyURL', 'endpoint' => 'https://tinyurl.com/api-create.php', 'requires_key' => false ), 'isgd' => array( 'name' => 'is.gd', 'endpoint' => 'https://is.gd/create.php', 'requires_key' => false ) ); $this->init_hooks(); } private function init_hooks() { add_action('admin_bar_menu', array($this, 'add_admin_bar_shortener'), 100); add_action('wp_ajax_generate_short_url', array($this, 'ajax_generate_short_url')); add_action('wp_ajax_nopriv_generate_short_url', array($this, 'ajax_generate_short_url')); } public function add_admin_bar_shortener($admin_bar) { if (!current_user_can('edit_posts') || !is_single()) { return; } global $post; $admin_bar->add_node(array( 'id' => 'url-shortener', 'title' => '生成短链接', 'href' => '#', 'meta' => array( 'class' => 'url-shortener-tool', 'onclick' => 'generateShortURL(' . $post->ID . '); return false;', 'title' => '为当前文章生成短链接' ) )); } public function ajax_generate_short_url() { check_ajax_referer('internet_tools_nonce', 'nonce'); $post_id = intval($_POST['post_id']); $service = sanitize_text_field($_POST['service']); if (!$post_id) { wp_die(json_encode(array( 'success' => false, 'message' => '无效的文章ID' ))); } $post_url = get_permalink($post_id); $short_url = $this->shorten_url($post_url, $service); if ($short_url) { // 保存到文章元数据 update_post_meta($post_id, '_short_url_' . $service, $short_url); wp_die(json_encode(array( 'success' => true, 'short_url' => $short_url, 'message' => '短链接生成成功' ))); } else { wp_die(json_encode(array( 'success' => false, 'message' => '短链接生成失败' ))); } } private function shorten_url($url, $service) { if (!isset($this->api_services[$service])) { return false; } $service_info = $this->api_services[$service]; switch ($service) { case 'bitly': return $this->shorten_with_bitly($url); case 'tinyurl': return $this->shorten_with_tinyurl($url); case 'isgd': return $this->shorten_with_isgd($url); default: return false; } } private function shorten_with_bitly($url) { $api_key = get_option('bitly_api_key', ''); if (empty($api_key)) { return false; } $args = array( 'method' => 'POST', 'headers' => array( 'Content-Type' => 'application/json', 'Authorization' => 'Bearer ' . $api_key ), 'body' => json_encode(array( 'long_url' => $url, 'domain' => 'bit.ly' )) ); $response = wp_remote_post($this->api_services['bitly']['endpoint'], $args); if (is_wp_error($response)) { return false; } $body = json_decode(wp_remote_retrieve_body($response), true); return isset($body['link']) ? $body['link'] : false; } private function shorten_with_tinyurl($url) { $api_url = $this->api_services['tinyurl']['endpoint'] . '?url=' . urlencode($url); $response = wp_remote_get($api_url); if (is_wp_error($response)) { return false; } return wp_remote_retrieve_body($response); } private function shorten_with_isgd($url) { $api_url = $this->api_services['isgd']['endpoint'] . '?format=simple&url=' . urlencode($url); $response = wp_remote_get($api_url); if (is_wp_error($response)) { return false; } return wp_remote_retrieve_body($response); } } // URL短链接小工具class URL_Shortener_Widget extends WP_Widget { public function __construct() { parent::__construct( 'url_shortener_widget', 'URL短链接生成器', array('description' => '生成当前页面的短链接') ); } public function widget($args, $instance) { if (!is_single() && !is_page()) { return; } global $post; echo $args['before_widget']; if (!empty($instance['title'])) { echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title']; } ?> <div class="url-shortener-widget"> <input type="text" id="current-url"

发表评论

实战教程,在网站中添加在线个人健康数据记录与趋势分析小程序

实战教程:在WordPress网站中添加在线个人健康数据记录与趋势分析小程序 摘要 本教程将详细介绍如何在WordPress网站中通过代码二次开发,实现一个功能完整的在线个人健康数据记录与趋势分析小程序。我们将从需求分析、数据库设计、前端界面开发、后端数据处理到数据可视化展示,一步步构建一个实用的健康管理工具。本教程适合有一定WordPress开发基础的开发者,通过约5000字的详细讲解,帮助您掌握在WordPress中集成自定义功能的完整流程。 目录 项目概述与需求分析 环境准备与WordPress开发基础 数据库设计与数据表创建 用户界面设计与前端开发 后端数据处理与API开发 数据可视化与趋势分析实现 安全性与数据隐私保护 测试、优化与部署 功能扩展与未来改进方向 1. 项目概述与需求分析 1.1 项目背景 随着健康意识的提升,越来越多的人希望系统性地记录和分析自己的健康数据。然而,市面上的健康管理应用往往存在数据隐私担忧、功能过于复杂或缺乏定制化等问题。通过在个人或企业WordPress网站中集成健康数据记录与分析功能,用户可以完全控制自己的数据,同时获得个性化的健康管理体验。 1.2 核心功能需求 用户认证与权限管理:确保用户只能访问自己的健康数据 健康数据录入:支持多种数据类型(体重、血压、血糖、运动量等) 数据存储与管理:安全可靠的数据库存储与检索 数据可视化:图表展示健康数据变化趋势 趋势分析与报告:基于历史数据的智能分析 数据导出:支持CSV、PDF等格式导出 移动端适配:确保在手机和平板上良好显示 1.3 技术选型 核心平台:WordPress 5.8+ 前端技术:HTML5、CSS3、JavaScript (jQuery/Vue.js可选) 图表库:Chart.js 或 Google Charts 数据库:MySQL (WordPress默认) 开发方式:WordPress插件开发模式 2. 环境准备与WordPress开发基础 2.1 开发环境搭建 安装本地开发环境(推荐XAMPP、MAMP或Local by Flywheel) 下载最新版WordPress并完成安装 配置调试模式,在wp-config.php中添加: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 2.2 WordPress插件开发基础 我们将以插件形式开发健康管理功能,确保与主题的兼容性和可移植性。 创建插件基本结构: wp-content/plugins/health-tracker/ ├── health-tracker.php // 主插件文件 ├── includes/ // 包含文件目录 │ ├── class-database.php // 数据库处理类 │ ├── class-shortcodes.php // 短码处理类 │ ├── class-api.php // API处理类 │ └── class-analysis.php // 数据分析类 ├── admin/ // 后台管理文件 ├── public/ // 前端文件 │ ├── css/ // 样式文件 │ ├── js/ // JavaScript文件 │ └── templates/ // 前端模板 └── assets/ // 静态资源 2.3 插件主文件结构 创建health-tracker.php作为插件入口: <?php /** * Plugin Name: 个人健康数据记录与分析 * Plugin URI: https://yourwebsite.com/health-tracker * Description: 在WordPress网站中添加个人健康数据记录与趋势分析功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: health-tracker */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('HEALTH_TRACKER_VERSION', '1.0.0'); define('HEALTH_TRACKER_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('HEALTH_TRACKER_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once HEALTH_TRACKER_PLUGIN_DIR . 'includes/class-database.php'; require_once HEALTH_TRACKER_PLUGIN_DIR . 'includes/class-shortcodes.php'; require_once HEALTH_TRACKER_PLUGIN_DIR . 'includes/class-api.php'; require_once HEALTH_TRACKER_PLUGIN_DIR . 'includes/class-analysis.php'; class Health_Tracker { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 激活/停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化组件 add_action('init', array($this, 'init')); // 加载文本域 add_action('plugins_loaded', array($this, 'load_textdomain')); } public function activate() { // 创建数据库表 Health_Tracker_Database::create_tables(); // 设置默认选项 update_option('health_tracker_version', HEALTH_TRACKER_VERSION); update_option('health_tracker_installed', time()); } public function deactivate() { // 清理临时数据 // 注意:不删除用户数据 } public function init() { // 初始化短码 Health_Tracker_Shortcodes::init(); // 初始化API Health_Tracker_API::init(); // 加载前端资源 add_action('wp_enqueue_scripts', array($this, 'enqueue_public_scripts')); // 加载后台资源 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } public function load_textdomain() { load_plugin_textdomain('health-tracker', false, dirname(plugin_basename(__FILE__)) . '/languages/'); } public function enqueue_public_scripts() { // 前端资源加载 wp_enqueue_style('health-tracker-public', HEALTH_TRACKER_PLUGIN_URL . 'public/css/health-tracker-public.css', array(), HEALTH_TRACKER_VERSION); wp_enqueue_script('health-tracker-public', HEALTH_TRACKER_PLUGIN_URL . 'public/js/health-tracker-public.js', array('jquery', 'chartjs'), HEALTH_TRACKER_VERSION, true); // 本地化脚本 wp_localize_script('health-tracker-public', 'healthTrackerAjax', array( 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('health_tracker_nonce'), 'user_id' => get_current_user_id() )); } public function enqueue_admin_scripts($hook) { // 仅在我们的设置页面加载 if ('toplevel_page_health-tracker' !== $hook) { return; } wp_enqueue_style('health-tracker-admin', HEALTH_TRACKER_PLUGIN_URL . 'admin/css/health-tracker-admin.css', array(), HEALTH_TRACKER_VERSION); wp_enqueue_script('health-tracker-admin', HEALTH_TRACKER_PLUGIN_URL . 'admin/js/health-tracker-admin.js', array('jquery'), HEALTH_TRACKER_VERSION, true); } } // 启动插件 Health_Tracker::get_instance(); 3. 数据库设计与数据表创建 3.1 数据库表设计 我们需要创建两个主要数据表: wp_health_data - 存储健康数据记录 wp_health_metrics - 存储健康指标定义 3.2 数据库类实现 创建includes/class-database.php: <?php class Health_Tracker_Database { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'health_'; // 健康指标定义表 $metrics_table = $table_prefix . 'metrics'; $metrics_sql = "CREATE TABLE IF NOT EXISTS $metrics_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, metric_name varchar(100) NOT NULL, metric_unit varchar(50) NOT NULL, metric_type varchar(50) NOT NULL, min_value decimal(10,2) DEFAULT NULL, max_value decimal(10,2) DEFAULT NULL, default_value decimal(10,2) DEFAULT NULL, description text, is_active tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY metric_name (metric_name) ) $charset_collate;"; // 健康数据记录表 $data_table = $table_prefix . 'data'; $data_sql = "CREATE TABLE IF NOT EXISTS $data_table ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, metric_id mediumint(9) NOT NULL, metric_value decimal(10,2) NOT NULL, recorded_at datetime NOT NULL, notes text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY metric_id (metric_id), KEY recorded_at (recorded_at), FOREIGN KEY (metric_id) REFERENCES {$metrics_table}(id) ON DELETE CASCADE ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($metrics_sql); dbDelta($data_sql); // 插入默认健康指标 self::insert_default_metrics(); } private static function insert_default_metrics() { global $wpdb; $table_name = $wpdb->prefix . 'health_metrics'; $default_metrics = array( array( 'metric_name' => '体重', 'metric_unit' => 'kg', 'metric_type' => 'numeric', 'min_value' => 30, 'max_value' => 200, 'default_value' => 65, 'description' => '体重记录,单位千克' ), array( 'metric_name' => '收缩压', 'metric_unit' => 'mmHg', 'metric_type' => 'numeric', 'min_value' => 80, 'max_value' => 200, 'default_value' => 120, 'description' => '血压高压值' ), array( 'metric_name' => '舒张压', 'metric_unit' => 'mmHg', 'metric_type' => 'numeric', 'min_value' => 50, 'max_value' => 130, 'default_value' => 80, 'description' => '血压低压值' ), array( 'metric_name' => '空腹血糖', 'metric_unit' => 'mmol/L', 'metric_type' => 'numeric', 'min_value' => 3.0, 'max_value' => 20.0, 'default_value' => 5.5, 'description' => '空腹血糖值' ), array( 'metric_name' => '步数', 'metric_unit' => '步', 'metric_type' => 'numeric', 'min_value' => 0, 'max_value' => 50000, 'default_value' => 8000, 'description' => '每日步数' ), array( 'metric_name' => '睡眠时长', 'metric_unit' => '小时', 'metric_type' => 'numeric', 'min_value' => 0, 'max_value' => 24, 'default_value' => 7.5, 'description' => '每日睡眠时长' ) ); foreach ($default_metrics as $metric) { $wpdb->replace($table_name, $metric); } } public static function get_user_health_data($user_id, $metric_id = null, $start_date = null, $end_date = null, $limit = 100) { global $wpdb; $table_data = $wpdb->prefix . 'health_data'; $table_metrics = $wpdb->prefix . 'health_metrics'; $where_clauses = array("d.user_id = %d"); $where_values = array($user_id); if ($metric_id) { $where_clauses[] = "d.metric_id = %d"; $where_values[] = $metric_id; } if ($start_date) { $where_clauses[] = "d.recorded_at >= %s"; $where_values[] = $start_date; } if ($end_date) { $where_clauses[] = "d.recorded_at <= %s"; $where_values[] = $end_date; } $where_sql = implode(' AND ', $where_clauses); $query = $wpdb->prepare( "SELECT d.*, m.metric_name, m.metric_unit FROM $table_data d JOIN $table_metrics m ON d.metric_id = m.id WHERE $where_sql ORDER BY d.recorded_at DESC LIMIT %d", array_merge($where_values, array($limit)) ); return $wpdb->get_results($query); } public static function add_health_data($user_id, $metric_id, $value, $recorded_at = null, $notes = '') { global $wpdb; $table_name = $wpdb->prefix . 'health_data'; if (!$recorded_at) { $recorded_at = current_time('mysql'); } $data = array( 'user_id' => $user_id, 'metric_id' => $metric_id, 'metric_value' => $value, 'recorded_at' => $recorded_at, 'notes' => $notes ); $format = array('%d', '%d', '%f', '%s', '%s'); return $wpdb->insert($table_name, $data, $format); } public static function get_available_metrics() { global $wpdb; $table_name = $wpdb->prefix . 'health_metrics'; return $wpdb->get_results( "SELECT * FROM $table_name WHERE is_active = 1 ORDER BY metric_name" ); } } 4. 用户界面设计与前端开发 4.1 创建前端模板 在public/templates/目录下创建健康数据记录界面: <!-- public/templates/dashboard.php --> <div class="health-tracker-container"> <div class="health-tracker-header"> <h1><?php _e('个人健康数据记录', 'health-tracker'); ?></h1> <p><?php _e('记录并分析您的健康指标', 'health-tracker'); ?></p> </div> <div class="health-tracker-main"> <!-- 数据录入部分 --> <div class="health-tracker-section"> <h2><?php _e('新增记录', 'health-tracker'); ?></h2> <form id="health-data-form" class="health-data-form"> <div class="form-group"> <label for="metric-select"><?php _e('选择指标', 'health-tracker'); ?></label> <select id="metric-select" name="metric_id" required> <option value=""><?php _e('请选择健康指标', 'health-tracker'); ?></option> <!-- 通过AJAX动态加载 --> </select> </div> <div class="form-group"> <label for="metric-value"><?php _e('数值', 'health-tracker'); ?></label> <input type="number" id="metric-value" name="metric_value" step="0.01" required> <span class="unit-display" id="unit-display"></span> </div> <div class="form-group"> <label for="record-date"><?php _e('记录日期', 'health-tracker'); ?></label> <input type="datetime-local" id="record-date" name="recorded_at" value="<?php echo date('Y-m-dTH:i'); ?>"> </div> <div class="form-group"> <label for="notes"><?php _e('备注', 'health-tracker'); ?></label> <textarea id="notes" name="notes" rows="3" placeholder="<?php _e('可选的备注信息...', 'health-tracker'); ?>"></textarea> </div> <button type="submit" class="submit-btn"> <?php _e('保存记录', 'health-tracker'); ?> </button> </form> </div> <!-- 数据可视化部分 --> <div class="health-tracker-section"> <h2><?php _e('趋势分析', 'health-tracker'); ?></h2> <div class="chart-controls"> <select id="chart-metric-select"> <option value=""><?php _e('选择要分析的指标', 'health-tracker'); ?></option> </select> <select id="chart-period"> <option value="7"><?php _e('最近7天', 'health-tracker'); ?></option> <option value="30" selected><?php _e('最近30天', 'health-tracker'); ?></option> <button type="submit" class="submit-btn"> <?php _e('保存记录', 'health-tracker'); ?> </button> </form> </div> <!-- 数据可视化部分 --> <div class="health-tracker-section"> <h2><?php _e('趋势分析', 'health-tracker'); ?></h2> <div class="chart-controls"> <select id="chart-metric-select"> <option value=""><?php _e('选择要分析的指标', 'health-tracker'); ?></option> </select> <select id="chart-period"> <option value="7"><?php _e('最近7天', 'health-tracker'); ?></option> <option value="30" selected><?php _e('最近30天', 'health-tracker'); ?></option> <option value="90"><?php _e('最近90天', 'health-tracker'); ?></option> <option value="365"><?php _e('最近1年', 'health-tracker'); ?></option> </select> <button id="update-chart" class="btn-secondary"> <?php _e('更新图表', 'health-tracker'); ?> </button> </div> <div class="chart-container"> <canvas id="health-trend-chart"></canvas> </div> <div class="chart-stats" id="chart-stats"> <!-- 统计数据将通过JavaScript动态生成 --> </div> </div> <!-- 历史记录部分 --> <div class="health-tracker-section"> <h2><?php _e('历史记录', 'health-tracker'); ?></h2> <div class="history-controls"> <select id="history-metric-filter"> <option value=""><?php _e('所有指标', 'health-tracker'); ?></option> </select> <input type="date" id="history-start-date" placeholder="<?php _e('开始日期', 'health-tracker'); ?>"> <input type="date" id="history-end-date" placeholder="<?php _e('结束日期', 'health-tracker'); ?>"> <button id="filter-history" class="btn-secondary"> <?php _e('筛选', 'health-tracker'); ?> </button> <button id="export-history" class="btn-secondary"> <?php _e('导出CSV', 'health-tracker'); ?> </button> </div> <div class="history-table-container"> <table id="health-history-table" class="health-history-table"> <thead> <tr> <th><?php _e('指标', 'health-tracker'); ?></th> <th><?php _e('数值', 'health-tracker'); ?></th> <th><?php _e('记录时间', 'health-tracker'); ?></th> <th><?php _e('备注', 'health-tracker'); ?></th> <th><?php _e('操作', 'health-tracker'); ?></th> </tr> </thead> <tbody> <!-- 数据将通过JavaScript动态加载 --> </tbody> </table> <div id="history-pagination" class="pagination"> <!-- 分页控件 --> </div> </div> </div> </div> </div> ### 4.2 前端样式设计 创建`public/css/health-tracker-public.css`: / 健康追踪器主容器 /.health-tracker-container { max-width: 1200px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; } .health-tracker-header { text-align: center; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #4CAF50; } .health-tracker-header h1 { color: #2c3e50; margin-bottom: 10px; } .health-tracker-header p { color: #7f8c8d; font-size: 1.1em; } / 各部分样式 /.health-tracker-section { background: white; border-radius: 10px; padding: 25px; margin-bottom: 30px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); border: 1px solid #e0e0e0; } .health-tracker-section h2 { color: #3498db; margin-top: 0; margin-bottom: 20px; padding-bottom: 10px; border-bottom: 1px solid #eee; } / 表单样式 /.health-data-form .form-group { margin-bottom: 20px; } .health-data-form label { display: block; margin-bottom: 8px; font-weight: 600; color: #34495e; } .health-data-form select,.health-data-form input[type="number"],.health-data-form input[type="datetime-local"],.health-data-form textarea { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; transition: border-color 0.3s; box-sizing: border-box; } .health-data-form select:focus,.health-data-form input:focus,.health-data-form textarea:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2); } .unit-display { margin-left: 10px; color: #7f8c8d; font-style: italic; } .submit-btn { background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); color: white; border: none; padding: 14px 28px; font-size: 16px; border-radius: 6px; cursor: pointer; transition: all 0.3s; font-weight: 600; width: 100%; } .submit-btn:hover { background: linear-gradient(135deg, #45a049 0%, #3d8b40 100%); transform: translateY(-2px); box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } / 图表控制区域 /.chart-controls,.history-controls { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 20px; align-items: center; } .chart-controls select,.history-controls select,.history-controls input { padding: 10px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; } .btn-secondary { background: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; transition: background 0.3s; } .btn-secondary:hover { background: #2980b9; } / 图表容器 /.chart-container { position: relative; height: 400px; margin: 20px 0; background: #f8f9fa; border-radius: 8px; padding: 15px; } .chart-stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 20px; } .stat-card { background: white; padding: 15px; border-radius: 6px; border-left: 4px solid #3498db; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } .stat-card h4 { margin: 0 0 10px 0; color: #7f8c8d; font-size: 14px; text-transform: uppercase; } .stat-card .stat-value { font-size: 24px; font-weight: bold; color: #2c3e50; } .stat-card .stat-change { font-size: 14px; margin-top: 5px; } .stat-change.positive { color: #27ae60; } .stat-change.negative { color: #e74c3c; } / 历史记录表格 /.history-table-container { overflow-x: auto; } .health-history-table { width: 100%; border-collapse: collapse; margin-top: 20px; } .health-history-table th { background: #f8f9fa; padding: 12px; text-align: left; font-weight: 600; color: #2c3e50; border-bottom: 2px solid #dee2e6; } .health-history-table td { padding: 12px; border-bottom: 1px solid #dee2e6; } .health-history-table tr:hover { background: #f8f9fa; } .health-history-table .metric-name { font-weight: 600; color: #3498db; } .health-history-table .metric-value { font-weight: bold; color: #2c3e50; } .health-history-table .metric-unit { color: #7f8c8d; font-size: 0.9em; margin-left: 2px; } .health-history-table .record-time { color: #7f8c8d; font-size: 0.9em; } .health-history-table .action-buttons { display: flex; gap: 5px; } .action-btn { padding: 5px 10px; border: none; border-radius: 3px; cursor: pointer; font-size: 12px; transition: all 0.2s; } .edit-btn { background: #f39c12; color: white; } .delete-btn { background: #e74c3c; color: white; } .action-btn:hover { opacity: 0.9; transform: translateY(-1px); } / 分页样式 /.pagination { display: flex; justify-content: center; align-items: center; margin-top: 20px; gap: 10px; } .pagination button { padding: 8px 16px; border: 1px solid #ddd; background: white; cursor: pointer; border-radius: 4px; transition: all 0.3s; } .pagination button:hover:not(:disabled) { background: #3498db; color: white; border-color: #3498db; } .pagination button:disabled { opacity: 0.5; cursor: not-allowed; } .pagination .page-info { color: #7f8c8d; font-size: 14px; } / 响应式设计 /@media (max-width: 768px) { .health-tracker-container { padding: 10px; } .health-tracker-section { padding: 15px; } .chart-controls, .history-controls { flex-direction: column; align-items: stretch; } .chart-container { height: 300px; } .health-history-table { font-size: 14px; } .health-history-table th, .health-history-table td { padding: 8px; } } / 加载动画 /.loading { display: inline-block; width: 20px; height: 20px; border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } / 消息提示 /.message { padding: 12px; border-radius: 6px; margin: 10px 0; display: none; } .message.success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .message.error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .message.info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; } ### 4.3 前端JavaScript逻辑 创建`public/js/health-tracker-public.js`: (function($) { 'use strict'; // 全局变量 let healthTracker = { metrics: [], currentChart: null, currentPage: 1, itemsPerPage: 10, currentFilter: {} }; // 初始化 $(document).ready(function() { initHealthTracker(); }); function initHealthTracker() { // 加载健康指标 loadMetrics(); // 绑定事件 bindEvents(); // 加载历史记录 loadHistory(); } function loadMetrics() { $.ajax({ url: healthTrackerAjax.ajaxurl, type: 'POST', data: { action: 'health_tracker_get_metrics', nonce: healthTrackerAjax.nonce }, beforeSend: function() { $('#metric-select, #chart-metric-select, #history-metric-filter').append( '<option value="" disabled>加载中...</option>' ); }, success: function(response) { if (response.success) { healthTracker.metrics = response.data; populateMetricSelects(); } else { showMessage('error', '加载指标失败: ' + response.data); } }, error: function() { showMessage('error', '网络错误,请稍后重试'); } }); } function populateMetricSelects() { // 清空所有选择框 $('#metric-select, #chart-metric-select, #history-metric-filter').empty(); // 添加默认选项 $('#metric-select').append('<option value="">请选择健康指标</option>'); $('#chart-metric-select').append('<option value="">选择要分析的指标</option>'); $('#history-metric-filter').append('<option value="">所有指标</option>'); // 添加指标选项 healthTracker.metrics.forEach(function(metric) { $('#metric-select').append( `<option value="${metric.id}" data-unit="${metric.metric_unit}">${metric.metric_name} (${metric.metric_unit})</option>` ); $('#chart-metric-select').append( `<option value="${metric.id}">${metric.metric_name}</option>` ); $('#history-metric-filter').append( `<option value="${metric.id}">${metric.metric_name}</option>` ); }); } function bindEvents() { // 指标选择变化时更新单位显示 $('#metric-select').on('change', function() { const selectedOption = $(this).find('option:selected'); const unit = selectedOption.data('unit'); $('#unit-display').text(unit ? `单位: ${unit}` : ''); }); // 提交健康数据表单 $('#health-data-form').on('submit', function(e) { e.preventDefault(); submitHealthData(); }); // 更新图表 $('#update-chart').on('click', function() { updateChart(); }); // 筛选历史记录 $('#filter-history').on('click', function() { healthTracker.currentPage = 1; updateHistoryFilter(); loadHistory(); }); // 导出历史记录 $('#export-history').on('click', function() { exportHistoryToCSV(); }); // 图表指标选择变化时自动更新图表 $('#chart-metric-select').on('change', function() { if ($(this).val()) { updateChart(); } }); // 分页按钮 $(document).on('click', '.page-btn', function() { const page = $(this).data('page'); if (page) { healthTracker.currentPage = page; loadHistory(); } }); } function submitHealthData() { const formData = $('#health-data-form').serializeArray(); const data = {}; // 转换为对象 formData.forEach(function(item) { data[item.name] = item.value; }); // 添加用户ID data.user_id = healthTrackerAjax.user_id; data.action = 'health_tracker_add_data'; data.nonce = healthTrackerAjax.nonce; $.ajax({ url: healthTrackerAjax.ajaxurl, type: 'POST', data: data, beforeSend: function() { $('.submit-btn').prop('disabled', true).html('<span class="loading"></span> 保存中...'); }, success: function(response) { if (response.success) { showMessage('success', '数据保存成功!'); $('#health-data-form')[0].reset(); $('#unit-display').text(''); // 重新加载历史记录 loadHistory(); // 如果当前图表显示的是刚添加的指标,更新图表 const selectedMetric = $('#chart-metric-select').val(); if (selectedMetric && selectedMetric == data.metric_id) { updateChart(); } } else { showMessage('error', '保存失败: ' + response.data); } }, error: function() { showMessage('error', '网络错误,请稍后重试'); }, complete: function() { $('.submit-btn').prop('disabled', false).text('保存记录'); } }); } function updateChart() { const metric

发表评论

手把手教学,为WordPress集成智能化的网站死链检测与自动修复工具

手把手教学:为WordPress集成智能化的网站死链检测与自动修复工具 引言:为什么WordPress网站需要死链检测与修复工具 在当今互联网时代,网站链接的完整性和可用性直接影响用户体验和搜索引擎排名。根据权威统计,一个拥有超过1000个页面的网站中,平均有5%-10%的链接可能失效或成为"死链"。对于使用WordPress构建的网站而言,随着内容不断积累和更新,死链问题会日益严重。 死链不仅导致访客无法访问目标内容,还会损害网站的专业形象,更重要的是,搜索引擎会将死链数量作为网站质量评估的指标之一,直接影响SEO排名。传统的手动检测方法耗时耗力,且难以实时发现新出现的死链问题。因此,为WordPress集成智能化的死链检测与自动修复工具显得尤为重要。 本文将详细介绍如何通过WordPress代码二次开发,实现一个功能完善的死链检测与自动修复系统,让您的网站始终保持链接健康状态。 第一章:准备工作与环境配置 1.1 开发环境要求 在开始开发之前,我们需要确保具备以下环境条件: WordPress 5.0及以上版本 PHP 7.2及以上版本(推荐PHP 7.4+) MySQL 5.6及以上版本 服务器支持cURL扩展 适当的服务器内存和CPU资源(检测过程可能消耗资源) 1.2 创建插件基础结构 首先,我们需要创建一个独立的WordPress插件来承载我们的死链检测功能: 在WordPress的wp-content/plugins/目录下创建新文件夹smart-link-checker 在该文件夹中创建主插件文件smart-link-checker.php 添加插件基本信息: <?php /** * Plugin Name: Smart Link Checker & Fixer * Plugin URI: https://yourwebsite.com/smart-link-checker * Description: 智能化的网站死链检测与自动修复工具 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: smart-link-checker */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('SLC_VERSION', '1.0.0'); define('SLC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SLC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('SLC_MAX_LINKS_PER_RUN', 100); // 每次运行检测的最大链接数 define('SLC_REQUEST_TIMEOUT', 15); // 请求超时时间(秒) 1.3 创建数据库表结构 我们需要创建数据库表来存储链接检测结果和历史记录: // 在插件激活时创建数据库表 register_activation_hook(__FILE__, 'slc_create_database_tables'); function slc_create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'slc_links'; $history_table = $wpdb->prefix . 'slc_link_history'; // 主链接表 $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, url varchar(1000) NOT NULL, source_id bigint(20) NOT NULL, source_type varchar(50) NOT NULL, last_checked datetime DEFAULT NULL, status_code int(4) DEFAULT NULL, status varchar(50) DEFAULT 'pending', redirect_to varchar(1000) DEFAULT NULL, error_message text, check_count int(11) DEFAULT 0, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY url_index (url(191)), KEY status_index (status), KEY source_index (source_type, source_id) ) $charset_collate;"; // 历史记录表 $sql2 = "CREATE TABLE IF NOT EXISTS $history_table ( id bigint(20) NOT NULL AUTO_INCREMENT, link_id bigint(20) NOT NULL, status_code int(4) DEFAULT NULL, response_time float DEFAULT NULL, checked_at datetime DEFAULT CURRENT_TIMESTAMP, notes text, PRIMARY KEY (id), KEY link_id_index (link_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); dbDelta($sql2); // 添加默认选项 add_option('slc_settings', array( 'check_frequency' => 'daily', 'auto_fix_redirects' => true, 'send_notifications' => true, 'notification_email' => get_option('admin_email'), 'excluded_domains' => array(), 'max_redirects' => 3 )); } 第二章:核心功能模块开发 2.1 链接提取器模块 首先,我们需要从WordPress内容中提取所有链接: class SLC_Link_Extractor { /** * 从所有文章和页面中提取链接 */ public static function extract_all_links() { global $wpdb; $links = array(); // 从文章内容中提取链接 $posts = $wpdb->get_results("SELECT ID, post_content FROM {$wpdb->posts} WHERE post_status = 'publish' AND post_type IN ('post', 'page')"); foreach ($posts as $post) { $post_links = self::extract_from_content($post->post_content); foreach ($post_links as $link) { $links[] = array( 'url' => $link, 'source_id' => $post->ID, 'source_type' => 'post' ); } } // 从评论中提取链接 $comments = $wpdb->get_results("SELECT comment_ID, comment_content FROM {$wpdb->comments} WHERE comment_approved = '1'"); foreach ($comments as $comment) { $comment_links = self::extract_from_content($comment->comment_content); foreach ($comment_links as $link) { $links[] = array( 'url' => $link, 'source_id' => $comment->comment_ID, 'source_type' => 'comment' ); } } return $links; } /** * 从文本内容中提取URL */ private static function extract_from_content($content) { $pattern = '/https?://[^s'"<>]+/i'; preg_match_all($pattern, $content, $matches); $urls = array(); if (!empty($matches[0])) { foreach ($matches[0] as $url) { // 清理URL $url = rtrim($url, '.,;:!?'); $url = html_entity_decode($url); // 排除站内链接(可选) if (!self::is_internal_link($url)) { $urls[] = $url; } } } return array_unique($urls); } /** * 判断是否为站内链接 */ private static function is_internal_link($url) { $site_url = site_url(); $parsed_url = parse_url($url); $parsed_site = parse_url($site_url); if (!isset($parsed_url['host'])) { return false; } return $parsed_url['host'] === $parsed_site['host']; } } 2.2 链接检测器模块 接下来,我们创建链接检测的核心功能: class SLC_Link_Checker { private $timeout; private $user_agent; public function __construct() { $this->timeout = SLC_REQUEST_TIMEOUT; $this->user_agent = 'Mozilla/5.0 (compatible; SmartLinkChecker/1.0; +' . site_url() . ')'; } /** * 检测单个链接的状态 */ public function check_single_link($url) { $start_time = microtime(true); // 初始化cURL $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYHOST => false, CURLOPT_USERAGENT => $this->user_agent, CURLOPT_HEADER => true, CURLOPT_NOBODY => true, // 只获取头部信息,提高速度 )); $response = curl_exec($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $total_time = curl_getinfo($ch, CURLINFO_TOTAL_TIME); $effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); $redirect_count = curl_getinfo($ch, CURLINFO_REDIRECT_COUNT); $error = curl_error($ch); curl_close($ch); $response_time = microtime(true) - $start_time; // 分析结果 $result = array( 'url' => $url, 'http_code' => $http_code, 'response_time' => $response_time, 'effective_url' => $effective_url, 'redirect_count' => $redirect_count, 'error' => $error, 'status' => $this->determine_status($http_code, $error) ); return $result; } /** * 根据HTTP状态码确定链接状态 */ private function determine_status($http_code, $error) { if (!empty($error)) { return 'error'; } if ($http_code >= 200 && $http_code < 300) { return 'working'; } elseif ($http_code >= 300 && $http_code < 400) { return 'redirect'; } elseif ($http_code == 404) { return 'broken'; } elseif ($http_code >= 400 && $http_code < 500) { return 'client_error'; } elseif ($http_code >= 500) { return 'server_error'; } else { return 'unknown'; } } /** * 批量检测链接 */ public function check_batch_links($links, $batch_size = 10) { $results = array(); $batches = array_chunk($links, $batch_size); foreach ($batches as $batch) { $batch_results = $this->check_batch_concurrently($batch); $results = array_merge($results, $batch_results); // 避免对目标服务器造成过大压力 sleep(1); } return $results; } /** * 并发检测(使用curl_multi) */ private function check_batch_concurrently($links) { $mh = curl_multi_init(); $channels = array(); foreach ($links as $i => $link) { $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_URL => $link['url'], CURLOPT_RETURNTRANSFER => true, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 3, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_SSL_VERIFYPEER => false, CURLOPT_USERAGENT => $this->user_agent, CURLOPT_HEADER => true, CURLOPT_NOBODY => true, )); curl_multi_add_handle($mh, $ch); $channels[$i] = array( 'channel' => $ch, 'link' => $link ); } // 执行并发请求 $active = null; do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); while ($active && $mrc == CURLM_OK) { if (curl_multi_select($mh) != -1) { do { $mrc = curl_multi_exec($mh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); } } // 收集结果 $results = array(); foreach ($channels as $i => $channel) { $ch = $channel['channel']; $link = $channel['link']; $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); $effective_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL); $error = curl_error($ch); $results[] = array_merge($link, array( 'http_code' => $http_code, 'effective_url' => $effective_url, 'error' => $error, 'status' => $this->determine_status($http_code, $error) )); curl_multi_remove_handle($mh, $ch); curl_close($ch); } curl_multi_close($mh); return $results; } } 2.3 智能修复模块 检测到死链后,我们需要尝试自动修复: class SLC_Link_Fixer { /** * 尝试自动修复死链 */ public static function try_auto_fix($link_data) { $url = $link_data['url']; $source_id = $link_data['source_id']; $source_type = $link_data['source_type']; // 检查是否为404错误 if ($link_data['http_code'] != 404) { return false; } // 尝试常见的修复策略 $fixed_url = self::try_fix_strategies($url); if ($fixed_url && $fixed_url !== $url) { // 验证修复后的链接是否有效 $checker = new SLC_Link_Checker(); $check_result = $checker->check_single_link($fixed_url); if ($check_result['status'] === 'working') { // 更新数据库中的链接 self::update_link_in_database($link_data['id'], $fixed_url, $check_result['http_code']); // 更新内容中的链接 self::update_link_in_content($source_id, $source_type, $url, $fixed_url); return array( 'original_url' => $url, 'fixed_url' => $fixed_url, 'http_code' => $check_result['http_code'], 'success' => true ); } } return false; } /** * 尝试多种修复策略 */ private static function try_fix_strategies($url) { $strategies = array( 'remove_www' => function($url) { return preg_replace('/^(https?://)www./i', '$1', $url); }, 'add_www' => function($url) { $parsed = parse_url($url); if (!preg_match('/^www./i', $parsed['host'])) { $parsed['host'] = 'www.' . $parsed['host']; return self::build_url($parsed); } return $url; }, 'force_https' => function($url) { return preg_replace('/^http:/i', 'https:', $url); }, 'force_http' => function($url) { return preg_replace('/^https:/i', 'http:', $url); }, 'remove_trailing_slash' => function($url) { return rtrim($url, '/'); }, 'add_trailing_slash' => function($url) { $parsed = parse_url($url); if (!isset($parsed['path']) || substr($parsed['path'], -1) !== '/') { $parsed['path'] = ($parsed['path'] ?? '') . '/'; return self::build_url($parsed); } return $url; } ); foreach ($strategies as $strategy) { $fixed_url = $strategy($url); if ($fixed_url !== $url) { // 简单验证URL格式 if (filter_var($fixed_url, FILTER_VALIDATE_URL)) { return $fixed_url; } } } return false; } /** * 从解析的URL部分重建完整URL */ private static function build_url($parts) { $scheme = isset($parts['scheme']) ? $parts['scheme'] . '://' : ''; $host = $parts['host'] ?? ''; $port = isset($parts['port']) ? ':' . $parts['port'] : ''; $path = $parts['path'] ?? ''; $query = isset($parts['query']) ? '?' . $parts['query'] : ''; $fragment = isset($parts['fragment']) ? '#' . $parts['fragment'] : ''; return $scheme . $host . $port . $path . $query . $fragment; } /** * 更新数据库中的链接记录 */ private static function update_link_in_database($link_id, $new_url, $http_code) { global $wpdb; $table_name = $wpdb->prefix . 'slc_links'; $wpdb->update( $table_name, array( 'url' => $new_url, 'status_code' => $http_code, 'status' => 'fixed', 'last_checked' => current_time('mysql') ), array('id' => $link_id), array('%s', '%d', '%s', '%s'), array('%d') ); // 记录修复历史 $history_table = $wpdb->prefix . 'slc_link_history'; $wpdb->insert( $history_table, array( 'link_id' => $link_id, 'status_code' => $http_code, 'checked_at' => current_time('mysql'), 'notes' => '自动修复: ' . $new_url ) ); } /** * 更新内容中的链接 */ private static function update_link_in_content($source_id, $source_type, $old_url, $new_url) { if ($source_type === 'post') { $post = get_post($source_id); if ($post) { $new_content = str_replace($old_url, $new_url, $post->post_content); wp_update_post(array( 'ID' => $source_id, 'post_content' => $new_content )); } } elseif ($source_type === 'comment') { $comment = get_comment($source_id); if ($comment) { $new_content = str_replace($old_url, $new_url, $comment->comment_content); wp_update_comment(array( 'comment_ID' => $source_id, 'comment_content' => $new_content )); } } } /** * 查找可能的替代链接(通过搜索引擎API) */ public static function find_alternative_url($url, $title = '') { // 这里可以集成搜索引擎API来查找替代链接 // 由于API需要密钥,这里仅提供框架代码 $parsed = parse_url($url); $domain = $parsed['host'] ?? ''; $path = $parsed['path'] ?? ''; // 尝试通过Wayback Machine查找存档 $wayback_url = "https://archive.org/wayback/available?url=" . urlencode($url); $response = wp_remote_get($wayback_url, array('timeout' => 10)); if (!is_wp_error($response)) { $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['archived_snapshots']['closest']['url'])) { return $data['archived_snapshots']['closest']['url']; } } return false; } } ## 第三章:后台管理与用户界面 ### 3.1 创建管理菜单和页面 class SLC_Admin { 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_slc_start_scan', array($this, 'ajax_start_scan')); add_action('wp_ajax_slc_get_stats', array($this, 'ajax_get_stats')); add_action('wp_ajax_slc_fix_link', array($this, 'ajax_fix_link')); } /** * 添加管理菜单 */ public function add_admin_menu() { add_menu_page( '智能死链检测', '死链检测', 'manage_options', 'smart-link-checker', array($this, 'render_main_page'), 'dashicons-admin-links', 30 ); add_submenu_page( 'smart-link-checker', '检测设置', '设置', 'manage_options', 'slc-settings', array($this, 'render_settings_page') ); add_submenu_page( 'smart-link-checker', '检测报告', '报告', 'manage_options', 'slc-reports', array($this, 'render_reports_page') ); } /** * 渲染主页面 */ public function render_main_page() { ?> <div class="wrap slc-admin"> <h1>智能死链检测与修复</h1> <div class="slc-dashboard"> <div class="slc-stats-cards"> <div class="card"> <h3>总链接数</h3> <p class="stat-number" id="total-links">0</p> </div> <div class="card"> <h3>正常链接</h3> <p class="stat-number" id="working-links">0</p> </div> <div class="card"> <h3>死链数</h3> <p class="stat-number" id="broken-links">0</p> </div> <div class="card"> <h3>已修复</h3> <p class="stat-number" id="fixed-links">0</p> </div> </div> <div class="slc-controls"> <button id="start-scan" class="button button-primary button-large"> <span class="dashicons dashicons-search"></span> 开始检测 </button> <button id="stop-scan" class="button button-secondary button-large" disabled> <span class="dashicons dashicons-controls-pause"></span> 停止检测 </button> <button id="export-report" class="button button-secondary"> <span class="dashicons dashicons-download"></span> 导出报告 </button> </div> <div class="slc-progress-container" style="display: none;"> <h3>检测进度</h3> <div class="progress-bar"> <div class="progress-fill" id="scan-progress" style="width: 0%"></div> </div> <p class="progress-text" id="progress-text">准备开始...</p> </div> <div class="slc-results"> <h2>检测结果</h2> <div class="tablenav top"> <div class="alignleft actions"> <select id="status-filter"> <option value="all">所有状态</option> <option value="broken">死链</option> <option value="working">正常</option> <option value="redirect">重定向</option> <option value="error">错误</option> </select> <button id="apply-filter" class="button">筛选</button> </div> <div class="tablenav-pages"> <span class="displaying-num" id="displaying-num">0个项目</span> <div class="pagination"> <button class="button" id="prev-page" disabled>上一页</button> <span id="current-page">1</span> / <span id="total-pages">1</span> <button class="button" id="next-page" disabled>下一页</button> </div> </div> </div> <table class="wp-list-table widefat fixed striped" id="links-table"> <thead> <tr> <th width="5%">ID</th> <th width="30%">URL</th> <th width="10%">状态码</th> <th width="15%">状态</th> <th width="20%">来源</th> <th width="15%">最后检测</th> <th width="15%">操作</th> </tr> </thead> <tbody id="links-tbody"> <!-- 动态加载数据 --> </tbody> </table> </div> </div> </div> <?php } /** * 渲染设置页面 */ public function render_settings_page() { $settings = get_option('slc_settings', array()); ?> <div class="wrap"> <h1>死链检测设置</h1> <form method="post" action="options.php"> <?php settings_fields('slc_settings_group'); ?> <table class="form-table"> <tr> <th scope="row">检测频率</th> <td> <select name="slc_settings[check_frequency]"> <option value="hourly" <?php selected($settings['check_frequency'], 'hourly'); ?>>每小时</option> <option value="twicedaily" <?php selected($settings['check_frequency'], 'twicedaily'); ?>>每天两次</option> <option value="daily" <?php selected($settings['check_frequency'], 'daily'); ?>>每天</option> <option value="weekly" <?php selected($settings['check_frequency'], 'weekly'); ?>>每周</option> <option value="monthly" <?php selected($settings['check_frequency'], 'monthly'); ?>>每月</option> </select> <p class="description">自动检测死链的频率</p> </td> </tr> <tr> <th scope="row">自动修复</th> <td> <label> <input type="checkbox" name="slc_settings[auto_fix_redirects]" value="1" <?php checked($settings['auto_fix_redirects'], true); ?>> 自动尝试修复重定向和简单错误 </label> </td> </tr> <tr> <th scope="row">邮件通知</th> <td> <label> <input type="checkbox" name="slc_settings[send_notifications]" value="1" <?php checked($settings['send_notifications'], true); ?>> 检测到死链时发送邮件通知 </label> <p class="description"> <input type="email" name="slc_settings[notification_email]" value="<?php echo esc_attr($settings['notification_email']); ?>" placeholder="通知邮箱地址"> </p> </td> </tr> <tr> <th scope="row">排除域名</th> <td> <textarea name="slc_settings[excluded_domains]" rows="5" cols="50" placeholder="每行一个域名,例如:example.com"><?php echo esc_textarea(implode("n", $settings['excluded_domains'] ?? array())); ?></textarea> <p class="description">不检测这些域名的链接</p> </td> </tr> <tr> <th scope="row">最大重定向次数</th> <td> <input type="number" name="slc_settings[max_redirects]" value="<?php echo esc_attr($settings['max_redirects']); ?>" min="1" max="10"> <p class="description">检测时允许的最大重定向次数</p> </td> </tr> <tr> <th scope="row">请求超时时间</th> <td> <input type="number" name="slc_settings[request_timeout]" value="<?php echo esc_attr($settings['request_timeout'] ?? SLC_REQUEST_TIMEOUT); ?>" min="5" max="60"> <span>秒</span> <p class="description">检测单个链接的最大等待时间</p> </td> </tr> </table> <?php submit_button(); ?> </form> </div> <?php } /** * 注册设置 */ public function register_settings() { register_setting('slc_settings_group', 'slc_settings', array( 'sanitize_callback' => array($this, 'sanitize_settings') )); } /** * 清理设置数据 */ public function sanitize_settings($input) { $sanitized = array(); $sanitized['check_frequency'] = sanitize_text_field($input['check_frequency']); $sanitized['auto_fix_redirects'] = isset($input['auto_fix_redirects']); $sanitized['send_notifications'] = isset($input['send_notifications']); $sanitized['notification_email'] = sanitize_email($input['notification_email']); // 处理排除域名 $excluded_domains = explode("n", $input['excluded_domains']); $sanitized['excluded_domains'] = array_map('trim', $excluded_domains); $sanitized['excluded_domains'] = array_filter($sanitized['excluded_domains']); $sanitized['max_redirects'] = absint($input['max_redirects']); if ($sanitized['max_redirects'] < 1 || $sanitized['max_redirects'] > 10) { $sanitized['max_redirects'] = 3; } $sanitized['request_timeout'] = absint($input['request_timeout']); if ($sanitized['request_timeout'] < 5 || $sanitized['request_timeout'] > 60) { $sanitized['request_timeout'] = SLC_REQUEST_TIMEOUT; } return $sanitized; } } ### 3.2 AJAX处理与前端交互 // 继续SLC_Admin类中的方法 /** * AJAX开始扫描 */ public function ajax_start_scan() { check_ajax_referer('slc_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('权限不足'); } // 获取所有链接 $links = SLC_Link_Extractor::extract_all_links(); // 保存到数据库 $this->save_links_to_db($links); // 开始后台处理 wp_schedule_single_event(time() + 1, 'slc_process_batch'); wp_send_json_success(array( 'message' => '扫描已开始', 'total_links' => count($links) )); } /** * 保存链接到数据库 */ private function save_links_to_db($links) { global $wpdb; $table_name = $wpdb->prefix . 'slc_links'; // 清空旧数据 $wpdb->query("TRUNCATE TABLE $table_name"); // 批量插入新数据 $values = array(); $placeholders = array(); foreach ($links as $link) { array_push( $values, $link['url'], $link['source_id'], $link['source_type'] ); $placeholders[] = "(%s, %d, %s, 'pending')"; } if (!empty($values)) { $query = "INSERT INTO $table_name (url, source_id, source_type, status) VALUES "; $query .= implode(', ', $placeholders); $wpdb->query($wpdb->prepare($query, $values)); } } /** * AJAX获取统计信息 */ public function ajax_get_stats() { global $wpdb; $table_name = $wpdb->prefix . 'slc_links'; $stats = array( 'total' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name"), 'working' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'working'"), 'broken' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'broken'"), 'fixed' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'fixed'"), 'pending' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'pending'") ); wp_send_json_success($stats); } /** * AJAX修复链接 */ public function ajax_fix_link() { check_ajax_referer('slc_ajax_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('权限不足'); } $link_id = intval($_POST['link_id']); $fix_type = sanitize_text_field($_POST['fix_type']); global $wpdb; $table_name = $wpdb->prefix . 'slc_links'; $link = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $link_id )); if (!$link) { wp_send_json_error('链接不存在'); } $result = false; switch ($fix_type) { case 'auto': $result = SLC_Link_Fixer::try_auto_fix((array)$link); break; case 'manual': $new_url = sanitize_text_field($_POST['new_url']); if ($new_url && filter_var($new_url, FILTER_VALIDATE_URL)) { $result = SLC_Link_Fixer::update_link_in_content( $link->source_id, $link->source_type, $link->url, $new_url ); } break; case 'remove': // 从内容中移除链接 $result = SLC_Link_Fixer::update_link_in_content( $link->source_id, $link->source_type, $link->url, '' ); break; } if ($result) { wp_send_json_success(array( 'message' => '修复成功', 'link_id' => $link_id )); } else { wp_send_json_error('修复失败'); } } /** * 加载管理脚本和样式 */ public function enqueue_admin_scripts($hook) {

发表评论

详细指南,开发网站活动在线抽奖与中奖结果公证展示系统

详细指南:开发网站活动在线抽奖与中奖结果公证展示系统 摘要 随着互联网活动的日益普及,在线抽奖已成为企业营销、社区互动和用户增长的重要手段。然而,如何确保抽奖过程的公平性和中奖结果的公信力,一直是组织者面临的挑战。本文将详细介绍如何通过WordPress程序的代码二次开发,实现一个功能完善的在线抽奖与中奖结果公证展示系统。我们将从系统设计、功能实现、安全性保障到用户体验优化等多个维度进行全面解析,帮助开发者构建一个既实用又可信赖的在线抽奖平台。 一、系统概述与需求分析 1.1 在线抽奖系统的核心价值 在线抽奖系统不仅是一个简单的随机选择工具,更是一个集用户参与、活动管理、结果公证和营销推广于一体的综合平台。一个优秀的在线抽奖系统应具备以下特点: 公平性:确保每个参与者有平等的获奖机会 透明性:抽奖过程和中奖结果可追溯、可验证 易用性:用户参与简单,管理后台操作便捷 可扩展性:支持多种抽奖规则和活动形式 安全性:防止作弊和恶意攻击 1.2 系统功能需求 基于WordPress开发在线抽奖系统,我们需要实现以下核心功能: 用户参与模块:注册、登录、参与抽奖 抽奖活动管理:创建、编辑、删除抽奖活动 抽奖规则设置:参与条件、中奖人数、奖品设置 实时抽奖引擎:公平的随机算法实现 中奖结果展示:可视化展示中奖结果 公证验证系统:提供抽奖过程的可验证证据 数据统计与分析:参与数据、中奖率等统计 1.3 技术选型与架构设计 选择WordPress作为开发基础有以下优势: 成熟的用户管理系统 丰富的插件生态和主题支持 良好的扩展性和二次开发能力 广泛的技术社区支持 系统架构采用WordPress核心+自定义插件+定制主题的模式,确保系统稳定性的同时提供高度定制化功能。 二、开发环境搭建与准备工作 2.1 开发环境配置 在开始开发前,需要准备以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:最新稳定版WordPress 代码编辑器:VS Code、PHPStorm等 版本控制:Git 调试工具:Query Monitor、Debug Bar等WordPress调试插件 2.2 创建自定义插件 我们将创建一个独立插件来管理所有抽奖功能: <?php /** * Plugin Name: 在线抽奖与公证系统 * Plugin URI: https://yourwebsite.com/ * Description: 功能完整的在线抽奖与中奖结果公证展示系统 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('LOTTERY_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('LOTTERY_PLUGIN_URL', plugin_dir_url(__FILE__)); define('LOTTERY_VERSION', '1.0.0'); // 初始化插件 require_once LOTTERY_PLUGIN_PATH . 'includes/class-lottery-core.php'; function lottery_init() { $lottery = new Lottery_Core(); $lottery->init(); } add_action('plugins_loaded', 'lottery_init'); 2.3 数据库设计 我们需要创建自定义数据表来存储抽奖活动、参与记录和中奖信息: // 在插件激活时创建数据表 function lottery_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 抽奖活动表 $table_activities = $wpdb->prefix . 'lottery_activities'; $sql_activities = "CREATE TABLE IF NOT EXISTS $table_activities ( id INT(11) NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, description TEXT, start_date DATETIME NOT NULL, end_date DATETIME NOT NULL, status ENUM('draft', 'active', 'ended', 'cancelled') DEFAULT 'draft', rules TEXT, prizes TEXT, winner_count INT(11) DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 参与记录表 $table_participants = $wpdb->prefix . 'lottery_participants'; $sql_participants = "CREATE TABLE IF NOT EXISTS $table_participants ( id INT(11) NOT NULL AUTO_INCREMENT, activity_id INT(11) NOT NULL, user_id INT(11), user_email VARCHAR(255), user_name VARCHAR(255), ip_address VARCHAR(45), user_agent TEXT, participation_code VARCHAR(100) UNIQUE, status ENUM('valid', 'invalid') DEFAULT 'valid', participated_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX activity_user (activity_id, user_id), FOREIGN KEY (activity_id) REFERENCES $table_activities(id) ON DELETE CASCADE ) $charset_collate;"; // 中奖记录表 $table_winners = $wpdb->prefix . 'lottery_winners'; $sql_winners = "CREATE TABLE IF NOT EXISTS $table_winners ( id INT(11) NOT NULL AUTO_INCREMENT, activity_id INT(11) NOT NULL, participant_id INT(11) NOT NULL, prize_level INT(11), prize_name VARCHAR(255), awarded_at DATETIME DEFAULT CURRENT_TIMESTAMP, verification_hash VARCHAR(255), PRIMARY KEY (id), FOREIGN KEY (activity_id) REFERENCES $table_activities(id) ON DELETE CASCADE, FOREIGN KEY (participant_id) REFERENCES $table_participants(id) ON DELETE CASCADE ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_activities); dbDelta($sql_participants); dbDelta($sql_winners); } register_activation_hook(__FILE__, 'lottery_create_tables'); 三、核心功能模块开发 3.1 抽奖活动管理模块 3.1.1 后台管理界面 创建抽奖活动管理后台页面: class Lottery_Admin { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); } public function add_admin_menu() { add_menu_page( '在线抽奖系统', '抽奖管理', 'manage_options', 'lottery-management', array($this, 'render_admin_page'), 'dashicons-tickets-alt', 30 ); add_submenu_page( 'lottery-management', '抽奖活动', '活动列表', 'manage_options', 'lottery-activities', array($this, 'render_activities_page') ); add_submenu_page( 'lottery-management', '添加抽奖活动', '添加活动', 'manage_options', 'lottery-add-activity', array($this, 'render_add_activity_page') ); } public function render_activities_page() { include LOTTERY_PLUGIN_PATH . 'admin/views/activities-list.php'; } public function render_add_activity_page() { include LOTTERY_PLUGIN_PATH . 'admin/views/activity-form.php'; } public function enqueue_admin_scripts($hook) { if (strpos($hook, 'lottery') !== false) { wp_enqueue_style('lottery-admin-style', LOTTERY_PLUGIN_URL . 'assets/css/admin.css'); wp_enqueue_script('lottery-admin-script', LOTTERY_PLUGIN_URL . 'assets/js/admin.js', array('jquery', 'jquery-ui-datepicker'), LOTTERY_VERSION, true); wp_enqueue_style('jquery-ui-style', '//code.jquery.com/ui/1.12.1/themes/base/jquery-ui.css'); } } } 3.1.2 活动表单与数据保存 // 处理活动表单提交 function lottery_save_activity() { if (!isset($_POST['lottery_nonce']) || !wp_verify_nonce($_POST['lottery_nonce'], 'lottery_save_activity')) { wp_die('安全验证失败'); } global $wpdb; $table_name = $wpdb->prefix . 'lottery_activities'; $data = array( 'title' => sanitize_text_field($_POST['title']), 'description' => wp_kses_post($_POST['description']), 'start_date' => sanitize_text_field($_POST['start_date']), 'end_date' => sanitize_text_field($_POST['end_date']), 'status' => sanitize_text_field($_POST['status']), 'winner_count' => intval($_POST['winner_count']), 'rules' => wp_json_encode($_POST['rules']), 'prizes' => wp_json_encode($_POST['prizes']) ); if (isset($_POST['activity_id']) && !empty($_POST['activity_id'])) { // 更新现有活动 $wpdb->update($table_name, $data, array('id' => intval($_POST['activity_id']))); $message = '活动更新成功'; } else { // 创建新活动 $wpdb->insert($table_name, $data); $message = '活动创建成功'; } wp_redirect(admin_url('admin.php?page=lottery-activities&message=' . urlencode($message))); exit; } add_action('admin_post_lottery_save_activity', 'lottery_save_activity'); 3.2 用户参与模块 3.2.1 前端参与界面 创建短代码让用户可以在任何页面参与抽奖: class Lottery_Shortcodes { public function __construct() { add_shortcode('lottery_participation', array($this, 'render_participation_form')); add_shortcode('lottery_results', array($this, 'render_results_display')); } public function render_participation_form($atts) { $atts = shortcode_atts(array( 'activity_id' => 0, 'title' => '参与抽奖' ), $atts); $activity_id = intval($atts['activity_id']); if (!$activity_id) { return '<p>错误:未指定抽奖活动</p>'; } // 检查活动状态 $activity = $this->get_activity($activity_id); if (!$activity) { return '<p>错误:抽奖活动不存在</p>'; } // 检查活动时间 $current_time = current_time('mysql'); if ($current_time < $activity->start_date) { return '<div class="lottery-notice">抽奖活动尚未开始</div>'; } if ($current_time > $activity->end_date) { return '<div class="lottery-notice">抽奖活动已结束</div>'; } // 检查用户是否已参与 $user_id = get_current_user_id(); $has_participated = $this->check_participation($activity_id, $user_id); if ($has_participated) { return '<div class="lottery-notice">您已参与本次抽奖,请等待开奖结果</div>'; } // 渲染参与表单 ob_start(); include LOTTERY_PLUGIN_PATH . 'public/views/participation-form.php'; return ob_get_clean(); } private function get_activity($activity_id) { global $wpdb; $table_name = $wpdb->prefix . 'lottery_activities'; return $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d AND status = 'active'", $activity_id )); } private function check_participation($activity_id, $user_id) { global $wpdb; $table_name = $wpdb->prefix . 'lottery_participants'; if ($user_id) { return $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE activity_id = %d AND user_id = %d", $activity_id, $user_id )); } return false; } } 3.2.2 参与表单处理 // 处理用户参与请求 function lottery_handle_participation() { // 验证nonce if (!isset($_POST['lottery_nonce']) || !wp_verify_nonce($_POST['lottery_nonce'], 'lottery_participate')) { wp_send_json_error(array('message' => '安全验证失败')); } $activity_id = intval($_POST['activity_id']); // 验证活动 $activity = lottery_get_activity($activity_id); if (!$activity || $activity->status !== 'active') { wp_send_json_error(array('message' => '抽奖活动无效或已结束')); } // 检查时间 $current_time = current_time('mysql'); if ($current_time < $activity->start_date || $current_time > $activity->end_date) { wp_send_json_error(array('message' => '不在抽奖活动时间内')); } // 获取用户信息 $user_id = get_current_user_id(); $user_email = ''; $user_name = ''; if ($user_id) { $user = get_userdata($user_id); $user_email = $user->user_email; $user_name = $user->display_name; } else { // 匿名用户需要提供邮箱 if (empty($_POST['user_email']) || !is_email($_POST['user_email'])) { wp_send_json_error(array('message' => '请输入有效的邮箱地址')); } $user_email = sanitize_email($_POST['user_email']); $user_name = !empty($_POST['user_name']) ? sanitize_text_field($_POST['user_name']) : ''; } // 检查是否已参与 if (lottery_check_participation($activity_id, $user_id, $user_email)) { wp_send_json_error(array('message' => '您已经参与过本次抽奖')); } // 生成参与码 $participation_code = lottery_generate_participation_code(); // 保存参与记录 global $wpdb; $table_name = $wpdb->prefix . 'lottery_participants'; $data = array( 'activity_id' => $activity_id, 'user_id' => $user_id, 'user_email' => $user_email, 'user_name' => $user_name, 'ip_address' => lottery_get_client_ip(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'participation_code' => $participation_code ); $result = $wpdb->insert($table_name, $data); if ($result) { // 发送确认邮件 lottery_send_participation_email($user_email, $activity, $participation_code); wp_send_json_success(array( 'message' => '参与成功!您的参与码是:' . $participation_code, 'code' => $participation_code )); } else { wp_send_json_error(array('message' => '参与失败,请稍后重试')); } } add_action('wp_ajax_lottery_participate', 'lottery_handle_participation'); add_action('wp_ajax_nopriv_lottery_participate', 'lottery_handle_participation'); 3.3 抽奖引擎与随机算法 3.3.1 公平随机算法实现 class Lottery_Draw_Engine { /** * 执行抽奖 * @param int $activity_id 活动ID * @param int $winner_count 中奖人数 * @return array 中奖者列表 */ public function draw_winners($activity_id, $winner_count) { // 获取所有有效参与者 $participants = $this->get_valid_participants($activity_id); if (empty($participants)) { return array('success' => false, 'message' => '没有有效参与者'); } if (count($participants) < $winner_count) { return array('success' => false, 'message' => '参与者数量少于中奖人数'); } // 使用加密安全的随机数生成器 $winners = $this->secure_random_draw($participants, $winner_count); // 记录中奖结果 $result = $this->record_winners($activity_id, $winners); // 生成可验证的抽奖证明 $verification_data = $this->generate_verification_data($activity_id, $participants, $winners); return array( 'success' => true, 'winners' => $winners, 'verification_hash' => $verification_data['hash'], 'verification_data' => $verification_data['data'] ); } /** * 加密安全的随机抽选算法 */ private function secure_random_draw($participants, $winner_count) { $total = count($participants); $winners = array(); $selected_indices = array(); // 使用加密安全的随机数生成器 while (count($winners) < $winner_count) { // 生成随机索引 $random_index = random_int(0, $total - 1); // 确保不重复选择 if (!in_array($random_index, $selected_indices)) { $selected_indices[] = $random_index; $winners[] = $participants[$random_index]; } } return $winners; } /** * 生成可验证的抽奖证明 */ private function generate_verification_data($activity_id, $participants, $winners) { // 创建抽奖数据快照 $snapshot = array( 'activity_id' => $activity_id, 'draw_time' => current_time('mysql'), 'total_participants' => count($participants), 'participant_ids' => array_column($participants, 'id'), 'winner_ids' => array_column($winners, 'id'), 'server_seed' => bin2hex(random_bytes(32)), 'client_seed' => $this->get_client_seed($activity_id) ); // 计算验证哈希 $hash_data = json_encode($snapshot); $verification_hash = hash('sha256', $hash_data); // 保存验证数据 $this->save_verification_data($activity_id, $hash_data, $verification_hash); return array( 'hash' => $verification_hash, 'data' => $snapshot ); } /** * 获取客户端种子(基于活动数据) */ private function get_client_seed($activity_id) { global $wpdb; $table_name = $wpdb->prefix . 'lottery_activities'; $activity = $wpdb->get_row($wpdb->prepare( "SELECT title, start_date, end_date FROM $table_name WHERE id = %d", $activity_id )); if ($activity) { return hash('sha256', $activity->title . $activity->start_date . $activity->end_date); } return hash('sha256', (string) $activity_id); } } 3.3.2 批量抽奖与结果处理 // 后台执行抽奖的接口 function lottery_execute_draw() { if (!current_user_can('manage_options')) { wp_die('权限不足'); } $activity_id = intval($_POST['activity_id']); // 验证活动状态 $activity = lottery_get_activity($activity_id); if (!$activity || $activity->status !== 'active') { wp_send_json_error(array('message' => '活动状态不允许抽奖')); } // 检查是否已开奖 if (lottery_has_winners($activity_id)) { wp_send_json_error(array('message' => '该活动已开奖')); } // 执行抽奖 $draw_engine = new Lottery_Draw_Engine(); $result = $draw_engine->draw_winners($activity_id, $activity->winner_count); if ($result['success']) { // 更新活动状态 lottery_update_activity_status($activity_id, 'ended'); // 发送中奖通知 lottery_send_winner_notifications($activity_id, $result['winners']); wp_send_json_success(array( 'message' => '抽奖完成', 'winners' => $result['winners'], 'verification_hash' => $result['verification_hash'] )); } else { wp_send_json_error(array('message' => $result['message'])); } } add_action('wp_ajax_lottery_execute_draw', 'lottery_execute_draw'); 3.4 中奖结果公证展示系统 3.4.1 可视化结果展示 class Lottery_Results_Display { public function render_results($activity_id) { $activity = lottery_get_activity($activity_id); $winners = lottery_get_winners($activity_id); $verification_data = lottery_get_verification_data($activity_id); ob_start(); ?> <div class="lottery-results-container"> <div class="lottery-results-header"> <h2><?php echo esc_html($activity->title); ?> - 中奖结果</h2> <div class="lottery-meta"> <span class="lottery-date">开奖时间: <?php echo date('Y-m-d H:i:s', strtotime($verification_data['draw_time'])); ?></span> <span class="lottery-participants">参与人数: <?php echo $verification_data['total_participants']; ?></span> </div> </div> <div class="lottery-verification-section"> <h3>抽奖公正性验证</h3> <div class="verification-info"> <p>验证哈希: <code class="verification-hash"><?php echo esc_html($verification_data['hash']); ?></code></p> <button class="btn-verify" onclick="lotteryShowVerificationDetails()">查看验证详情</button> </div> <div id="verification-details" style="display: none;"> <h4>抽奖数据快照</h4> <pre><?php echo json_encode($verification_data['data'], JSON_PRETTY_PRINT); ?></pre> <p>您可以使用SHA256哈希验证器验证上述数据的哈希值是否匹配。</p> </div> </div> <div class="lottery-winners-section"> <h3>中奖名单</h3> <?php if (!empty($winners)): ?> <div class="winners-list"> <?php foreach ($winners as $index => $winner): ?> <div class="winner-item"> <div class="winner-rank"><?php echo $index + 1; ?></div> <div class="winner-info"> <h4><?php echo esc_html($winner->prize_name); ?></h4> <p class="winner-name"> <?php if ($winner->user_name) { echo esc_html(mb_substr($winner->user_name, 0, 1) . '**'); } else { echo esc_html(substr($winner->user_email, 0, 3) . '***' . substr($winner->user_email, strpos($winner->user_email, '@'))); } ?> </p> <p class="winner-code">参与码: <?php echo esc_html($winner->participation_code); ?></p> </div> </div> <?php endforeach; ?> </div> <?php else: ?> <p class="no-winners">暂无中奖者</p> <?php endif; ?> </div> <div class="lottery-statistics"> <h3>抽奖统计</h3> <div class="stats-grid"> <div class="stat-item"> <span class="stat-label">总参与人数</span> <span class="stat-value"><?php echo $verification_data['total_participants']; ?></span> </div> <div class="stat-item"> <span class="stat-label">中奖人数</span> <span class="stat-value"><?php echo count($winners); ?></span> </div> <div class="stat-item"> <span class="stat-label">中奖率</span> <span class="stat-value"> <?php if ($verification_data['total_participants'] > 0) { echo round((count($winners) / $verification_data['total_participants']) * 100, 2) . '%'; } else { echo '0%'; } ?> </span> </div> </div> </div> </div> <script> function lotteryShowVerificationDetails() { var details = document.getElementById('verification-details'); if (details.style.display === 'none') { details.style.display = 'block'; } else { details.style.display = 'none'; } } </script> <?php return ob_get_clean(); } } 3.4.2 验证系统实现 class Lottery_Verification_System { /** * 验证抽奖结果的真实性 */ public function verify_draw_result($activity_id, $user_provided_hash = '') { global $wpdb; // 获取存储的验证数据 $table_name = $wpdb->prefix . 'lottery_verification'; $verification = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE activity_id = %d", $activity_id )); if (!$verification) { return array( 'valid' => false, 'message' => '未找到该活动的验证数据' ); } // 计算哈希 $calculated_hash = hash('sha256', $verification->verification_data); // 验证哈希 if ($user_provided_hash && $user_provided_hash !== $calculated_hash) { return array( 'valid' => false, 'message' => '哈希值不匹配,数据可能被篡改' ); } // 解析验证数据 $verification_data = json_decode($verification->verification_data, true); // 验证参与者数据 $participants_valid = $this->verify_participants( $activity_id, $verification_data['participant_ids'] ); // 验证中奖者数据 $winners_valid = $this->verify_winners( $activity_id, $verification_data['winner_ids'] ); // 重新计算哈希验证 $recalculated_hash = hash('sha256', json_encode($verification_data)); return array( 'valid' => $participants_valid && $winners_valid && ($recalculated_hash === $calculated_hash), 'message' => $participants_valid && $winners_valid ? '验证通过' : '数据验证失败', 'verification_data' => $verification_data, 'stored_hash' => $verification->hash_value, 'calculated_hash' => $calculated_hash ); } /** * 提供公开的验证接口 */ public function public_verification_endpoint() { if (!isset($_GET['activity_id']) || !isset($_GET['hash'])) { wp_send_json_error(array('message' => '缺少必要参数')); } $activity_id = intval($_GET['activity_id']); $hash = sanitize_text_field($_GET['hash']); $result = $this->verify_draw_result($activity_id, $hash); wp_send_json(array( 'success' => $result['valid'], 'data' => $result )); } } 四、前端界面与用户体验优化 4.1 响应式设计实现 /* assets/css/lottery-public.css */ .lottery-container { max-width: 1200px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; } /* 参与表单样式 */ .lottery-participation-form { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 15px; padding: 40px; color: white; box-shadow: 0 20px 40px rgba(0,0,0,0.1); } .lottery-form-title { font-size: 2.5rem; margin-bottom: 10px; text-align: center; } .lottery-form-description { text-align: center; opacity: 0.9; margin-bottom: 30px; } .form-group { margin-bottom: 20px; } .form-control { width: 100%; padding: 12px 15px; border: none; border-radius: 8px; font-size: 16px; background: rgba(255,255,255,0.9); } .btn-participate { background: #ff6b6b; color: white; border: none; padding: 15px 30px; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; width: 100%; transition: transform 0.3s, box-shadow 0.3s; } .btn-participate:hover { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(255,107,107,0.3); } /* 结果展示样式 */ .lottery-results-container { background: white; border-radius: 15px; overflow: hidden; box-shadow: 0 10px 30px rgba(0,0,0,0.1); } .lottery-results-header { background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%); color: white; padding: 30px; text-align: center; } .winners-list { padding: 20px; } .winner-item { display: flex; align-items: center; padding: 20px; margin-bottom: 15px; background: #f8f9fa; border-radius: 10px; border-left: 5px solid #4facfe; transition: transform 0.3s; } .winner-item:hover { transform: translateX(10px); background: #e9ecef; } .winner-rank { font-size: 24px; font-weight: bold; color: #4facfe; margin-right: 20px; min-width: 50px; } .winner-info h4 { margin: 0 0 5px 0; color: #333; } .winner-name { font-size: 18px; font-weight: bold; color: #555; margin: 0 0 5px 0; } .winner-code { font-family: monospace; color: #666; font-size: 14px; margin: 0; } /* 统计信息样式 */ .lottery-statistics { padding: 30px; background: #f8f9fa; } .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; } .stat-item { background: white; padding: 20px; border-radius: 10px; text-align: center; box-shadow: 0 5px 15px rgba(0,0,0,0.05); } .stat-label { display: block; color: #666; font-size: 14px; margin-bottom: 5px; } .stat-value { display: block; font-size: 32px; font-weight: bold; color: #4facfe; } /* 响应式设计 */ @media (max-width: 768px) { .lottery-container { padding: 10px; } .lottery-participation-form { padding: 20px; } .lottery-form-title { font-size: 1.8rem; } .winner-item { flex-direction: column; text-align: center; } .winner-rank { margin-right: 0; margin-bottom: 10px; } .stats-grid { grid-template-columns: 1fr; } } /* 动画效果 */ @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .lottery-results-container { animation: fadeIn 0.6s ease-out; } .winner-item { animation: fadeIn 0.5s ease-out; animation-fill-mode: both; } .winner-item:nth-child(1) { animation-delay: 0.1s; } .winner-item:nth-child(2) { animation-delay: 0.2s; } .winner-item:nth-child(3) { animation-delay: 0.3s; } .winner-item:nth-child(4) { animation-delay: 0.4s; } .winner-item:nth-child(5) { animation-delay: 0.5s; } 4.2 实时状态更新 // assets/js/lottery-public.js jQuery(document).ready(function($) { // 参与抽奖 $('.lottery-participation-form').on('submit', function(e) { e.preventDefault(); var form = $(this); var submitBtn = form.find('.btn-participate'); var originalText = submitBtn.text(); // 显示加载状态 submitBtn.prop('disabled', true).text('提交中...'); // 收集表单数据 var formData = form.serialize(); $.ajax({ url: lottery_ajax.ajax_url, type: 'POST', data: { action: 'lottery_participate', nonce: lottery_ajax.nonce, form_data: formData }, success: function(response) { if (response.success) { // 显示成功消息 form.html(` <div class="participation-success"> <div class="success-icon">🎉</div> <h3>参与成功!</h3>

发表评论

一步步实现,为WordPress打造内嵌的在线Markdown笔记与共享协作工具

一步步实现:为WordPress打造内嵌的在线Markdown笔记与共享协作工具 引言:为什么WordPress需要内嵌Markdown协作工具? 在当今数字化工作环境中,内容创作和团队协作的效率直接影响着项目的成功。WordPress作为全球最流行的内容管理系统,虽然拥有强大的发布功能,但在实时协作和结构化笔记方面却存在明显不足。传统的WordPress编辑器对于需要频繁协作的技术团队、内容创作者和教育机构来说,往往显得笨重且效率低下。 Markdown作为一种轻量级标记语言,以其简洁的语法和清晰的格式呈现,已经成为技术文档、笔记和协作内容的首选格式。将Markdown编辑与实时协作功能集成到WordPress中,不仅可以提升内容创作效率,还能为团队提供无缝的协作体验。 本文将详细介绍如何通过WordPress代码二次开发,构建一个功能完整的内嵌式Markdown笔记与共享协作工具,让您的WordPress网站具备类似Notion、语雀等现代协作平台的核心功能。 第一部分:项目规划与技术选型 1.1 功能需求分析 在开始开发之前,我们需要明确工具的核心功能需求: Markdown编辑器:支持实时预览、语法高亮、常用格式快捷键 实时协作功能:多用户同时编辑、光标位置显示、更改实时同步 笔记管理:文件夹/标签分类、全文搜索、版本历史 权限控制系统:基于角色的访问控制、分享链接设置 数据存储与同步:可靠的数据存储机制、离线编辑支持 WordPress集成:用户系统集成、主题样式兼容、插件化部署 1.2 技术架构设计 为了实现上述功能,我们采用以下技术栈: 前端框架:React + TypeScript(提供良好的组件化开发和类型安全) 实时通信:WebSocket(用于实时协作同步) Markdown解析:Marked.js + highlight.js(轻量且功能强大) 编辑器组件:CodeMirror 6(现代化、可扩展的代码编辑器) 后端框架:WordPress REST API扩展 + 自定义数据库表 数据同步:Operational Transformation(OT)算法解决冲突 存储方案:MySQL自定义表 + 本地存储备份 1.3 开发环境搭建 首先,我们需要设置开发环境: # 创建插件目录结构 mkdir wp-markdown-collab cd wp-markdown-collab # 初始化插件主文件 touch wp-markdown-collab.php # 创建核心目录 mkdir includes mkdir admin mkdir public mkdir assets mkdir build # 初始化package.json用于前端构建 npm init -y # 安装前端依赖 npm install react react-dom typescript @types/react @types/react-dom npm install codemirror @uiw/react-codemirror marked highlight.js npm install socket.io-client npm install --save-dev webpack webpack-cli babel-loader @babel/core @babel/preset-react @babel/preset-typescript 第二部分:构建Markdown编辑器核心 2.1 创建基础编辑器组件 让我们从构建Markdown编辑器开始。首先创建编辑器React组件: // assets/js/components/MarkdownEditor.tsx import React, { useState, useEffect } from 'react'; import CodeMirror from '@uiw/react-codemirror'; import { markdown, markdownLanguage } from '@codemirror/lang-markdown'; import { languages } from '@codemirror/language-data'; import { oneDark } from '@codemirror/theme-one-dark'; import { EditorView } from '@codemirror/view'; import { marked } from 'marked'; import hljs from 'highlight.js'; import 'highlight.js/styles/github-dark.css'; interface MarkdownEditorProps { content: string; onChange: (content: string) => void; readOnly?: boolean; } const MarkdownEditor: React.FC<MarkdownEditorProps> = ({ content, onChange, readOnly = false }) => { const [preview, setPreview] = useState<string>(''); const [activeTab, setActiveTab] = useState<'edit' | 'preview' | 'split'>('split'); // 配置marked解析器 marked.setOptions({ highlight: function(code, lang) { if (lang && hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value; } return hljs.highlightAuto(code).value; }, breaks: true, gfm: true }); // 更新预览内容 useEffect(() => { const renderPreview = async () => { const html = await marked.parse(content || ''); setPreview(html as string); }; renderPreview(); }, [content]); // CodeMirror扩展配置 const extensions = [ markdown({ base: markdownLanguage, codeLanguages: languages }), EditorView.lineWrapping, EditorView.theme({ "&": { height: "100%" }, ".cm-scroller": { overflow: "auto", fontFamily: "'Fira Code', monospace" }, ".cm-content": { padding: "10px 0" } }) ]; return ( <div className="wp-md-editor-container"> <div className="editor-toolbar"> <button className={`tab-btn ${activeTab === 'edit' ? 'active' : ''}`} onClick={() => setActiveTab('edit')} > 编辑 </button> <button className={`tab-btn ${activeTab === 'preview' ? 'active' : ''}`} onClick={() => setActiveTab('preview')} > 预览 </button> <button className={`tab-btn ${activeTab === 'split' ? 'active' : ''}`} onClick={() => setActiveTab('split')} > 分屏 </button> <div className="toolbar-actions"> <button className="toolbar-btn" title="加粗">B</button> <button className="toolbar-btn" title="斜体">I</button> <button className="toolbar-btn" title="链接">🔗</button> <button className="toolbar-btn" title="代码块">{"</>"}</button> <button className="toolbar-btn" title="图片">🖼️</button> </div> </div> <div className={`editor-content ${activeTab}`}> {(activeTab === 'edit' || activeTab === 'split') && ( <div className={`editor-pane ${activeTab === 'split' ? 'half' : 'full'}`}> <CodeMirror value={content} height="100%" theme={oneDark} extensions={extensions} onChange={(value) => onChange(value)} readOnly={readOnly} basicSetup={{ lineNumbers: true, highlightActiveLineGutter: true, bracketMatching: true, closeBrackets: true, autocompletion: true, rectangularSelection: true, crosshairCursor: true, highlightActiveLine: true, foldGutter: true, dropCursor: true, allowMultipleSelections: true, indentOnInput: true, syntaxHighlighting: true, tabSize: 2 }} /> </div> )} {(activeTab === 'preview' || activeTab === 'split') && ( <div className={`preview-pane ${activeTab === 'split' ? 'half' : 'full'}`}> <div className="markdown-preview" dangerouslySetInnerHTML={{ __html: preview }} /> </div> )} </div> </div> ); }; export default MarkdownEditor; 2.2 实现编辑器工具栏功能 接下来,我们需要为编辑器添加实用的工具栏功能: // assets/js/components/EditorToolbar.tsx import React from 'react'; interface EditorToolbarProps { onFormatAction: (action: string, value?: string) => void; onSave: () => void; onExport: (format: 'html' | 'pdf' | 'md') => void; isCollaborating: boolean; collaborators: Array<{id: number, name: string, color: string}>; } const EditorToolbar: React.FC<EditorToolbarProps> = ({ onFormatAction, onSave, onExport, isCollaborating, collaborators }) => { const handleFormatClick = (action: string) => { const actions: Record<string, {prefix: string, suffix: string, placeholder?: string}> = { bold: { prefix: '**', suffix: '**', placeholder: '加粗文字' }, italic: { prefix: '*', suffix: '*', placeholder: '斜体文字' }, link: { prefix: '[', suffix: '](url)', placeholder: '链接文字' }, image: { prefix: '![', suffix: '](image-url)', placeholder: '图片描述' }, code: { prefix: '`', suffix: '`', placeholder: '代码' }, codeBlock: { prefix: '```n', suffix: 'n```', placeholder: '代码块' }, quote: { prefix: '> ', suffix: '', placeholder: '引用文字' }, list: { prefix: '- ', suffix: '', placeholder: '列表项' }, numberedList: { prefix: '1. ', suffix: '', placeholder: '列表项' }, heading1: { prefix: '# ', suffix: '', placeholder: '一级标题' }, heading2: { prefix: '## ', suffix: '', placeholder: '二级标题' }, heading3: { prefix: '### ', suffix: '', placeholder: '三级标题' }, }; onFormatAction(action, actions[action]?.placeholder); }; return ( <div className="editor-toolbar-extended"> <div className="toolbar-section"> <div className="format-buttons"> <button onClick={() => handleFormatClick('heading1')} title="标题1">H1</button> <button onClick={() => handleFormatClick('heading2')} title="标题2">H2</button> <button onClick={() => handleFormatClick('heading3')} title="标题3">H3</button> <div className="separator"></div> <button onClick={() => handleFormatClick('bold')} title="加粗"> <strong>B</strong> </button> <button onClick={() => handleFormatClick('italic')} title="斜体"> <em>I</em> </button> <button onClick={() => handleFormatClick('link')} title="链接">🔗</button> <button onClick={() => handleFormatClick('image')} title="图片">🖼️</button> <div className="separator"></div> <button onClick={() => handleFormatClick('code')} title="行内代码"> {"</>"} </button> <button onClick={() => handleFormatClick('codeBlock')} title="代码块"> {"{ }"} </button> <button onClick={() => handleFormatClick('quote')} title="引用">❝</button> <button onClick={() => handleFormatClick('list')} title="无序列表">•</button> <button onClick={() => handleFormatClick('numberedList')} title="有序列表">1.</button> </div> </div> <div className="toolbar-section"> <div className="action-buttons"> <button onClick={onSave} className="save-btn"> 💾 保存 </button> <div className="export-dropdown"> <button className="export-btn">📥 导出</button> <div className="export-menu"> <button onClick={() => onExport('html')}>HTML</button> <button onClick={() => onExport('pdf')}>PDF</button> <button onClick={() => onExport('md')}>Markdown</button> </div> </div> {isCollaborating && ( <div className="collaborators-indicator"> <span className="collab-icon">👥</span> <span className="collab-count">{collaborators.length + 1}</span> <div className="collaborators-list"> {collaborators.map(collab => ( <div key={collab.id} className="collaborator"> <span className="user-avatar" style={{backgroundColor: collab.color}} > {collab.name.charAt(0)} </span> <span className="user-name">{collab.name}</span> </div> ))} </div> </div> )} </div> </div> </div> ); }; export default EditorToolbar; 第三部分:实现实时协作系统 3.1 WebSocket服务器集成 实时协作需要WebSocket服务器来处理实时通信。我们将在WordPress中集成WebSocket功能: <?php // includes/class-websocket-server.php class WP_Markdown_WebSocket_Server { private $server; private $clients; private $documents; public function __construct() { $this->clients = new SplObjectStorage; $this->documents = []; add_action('init', [$this, 'init_websocket']); } public function init_websocket() { // 检查是否应该启动WebSocket服务器 if (defined('DOING_AJAX') && DOING_AJAX) { return; } // 创建WebSocket服务器 $this->server = new RatchetApp( get_bloginfo('name'), 8080, '0.0.0.0' ); // 注册消息处理类 $this->server->route('/collab', new CollaborationHandler(), ['*']); // 在后台进程中运行服务器 if (php_sapi_name() === 'cli') { $this->server->run(); } } } class CollaborationHandler implements RatchetMessageComponentInterface { protected $clients; protected $documents; public function __construct() { $this->clients = new SplObjectStorage; $this->documents = []; } public function onOpen(RatchetConnectionInterface $conn) { $this->clients->attach($conn); error_log("New connection: {$conn->resourceId}"); // 发送欢迎消息 $conn->send(json_encode([ 'type' => 'welcome', 'message' => 'Connected to collaboration server', 'clientId' => $conn->resourceId ])); } public function onMessage(RatchetConnectionInterface $from, $msg) { $data = json_decode($msg, true); if (!$data || !isset($data['type'])) { return; } switch ($data['type']) { case 'join_document': $this->handleJoinDocument($from, $data); break; case 'text_update': $this->handleTextUpdate($from, $data); break; case 'cursor_move': $this->handleCursorMove($from, $data); break; case 'selection_change': $this->handleSelectionChange($from, $data); break; } } private function handleJoinDocument($conn, $data) { $docId = $data['documentId']; $userId = $data['userId']; $userName = $data['userName']; if (!isset($this->documents[$docId])) { $this->documents[$docId] = [ 'content' => '', 'clients' => [], 'version' => 0 ]; } // 存储客户端信息 $this->documents[$docId]['clients'][$conn->resourceId] = [ 'userId' => $userId, 'userName' => $userName, 'cursor' => null, 'selection' => null, 'color' => $this->generateUserColor($userId) ]; // 发送当前文档状态给新用户 $conn->send(json_encode([ 'type' => 'document_state', 'documentId' => $docId, 'content' => $this->documents[$docId]['content'], 'version' => $this->documents[$docId]['version'], 'clients' => $this->documents[$docId]['clients'] ])); // 通知其他用户有新用户加入 $this->broadcastToDocument($docId, $conn, [ 'type' => 'user_joined', 'clientId' => $conn->resourceId, 'userId' => $userId, 'userName' => $userName, 'color' => $this->documents[$docId]['clients'][$conn->resourceId]['color'] ]); } private function handleTextUpdate($conn, $data) { $docId = $data['documentId']; $operations = $data['operations']; $clientVersion = $data['version']; if (!isset($this->documents[$docId])) { return; } $serverVersion = $this->documents[$docId]['version']; // 检查版本冲突 if ($clientVersion !== $serverVersion) { // 发送冲突解决请求 $conn->send(json_encode([ 'type' => 'version_conflict', 'serverVersion' => $serverVersion, 'serverContent' => $this->documents[$docId]['content'] ])); return; } // 应用操作到文档 $this->applyOperations($docId, $operations); // 增加版本号 $this->documents[$docId]['version']++; // 广播更新给其他用户 $this->broadcastToDocument($docId, $conn, [ 'type' => 'text_updated', 'operations' => $operations, 'version' => $this->documents[$docId]['version'], resourceId, 'userId' => $this->documents[$docId]['clients'][$conn->resourceId]['userId'] ]); } private function applyOperations($docId, $operations) { $content = $this->documents[$docId]['content']; foreach ($operations as $op) { if ($op['type'] === 'insert') { $position = $op['position']; $text = $op['text']; $content = substr($content, 0, $position) . $text . substr($content, $position); } elseif ($op['type'] === 'delete') { $position = $op['position']; $length = $op['length']; $content = substr($content, 0, $position) . substr($content, $position + $length); } } $this->documents[$docId]['content'] = $content; } private function broadcastToDocument($docId, $excludeConn, $message) { foreach ($this->clients as $client) { if ($client !== $excludeConn && isset($this->documents[$docId]['clients'][$client->resourceId])) { $client->send(json_encode($message)); } } } private function generateUserColor($userId) { $colors = [ '#FF6B6B', '#4ECDC4', '#FFD166', '#06D6A0', '#118AB2', '#073B4C', '#EF476F', '#7209B7' ]; return $colors[abs(crc32($userId)) % count($colors)]; } public function onClose(RatchetConnectionInterface $conn) { // 从所有文档中移除客户端 foreach ($this->documents as $docId => $document) { if (isset($document['clients'][$conn->resourceId])) { $userInfo = $document['clients'][$conn->resourceId]; // 通知其他用户该用户已离开 $this->broadcastToDocument($docId, $conn, [ 'type' => 'user_left', 'clientId' => $conn->resourceId, 'userId' => $userInfo['userId'] ]); unset($this->documents[$docId]['clients'][$conn->resourceId]); } } $this->clients->detach($conn); error_log("Connection closed: {$conn->resourceId}"); } public function onError(RatchetConnectionInterface $conn, Exception $e) { error_log("Error: {$e->getMessage()}"); $conn->close(); } } ### 3.2 前端实时协作客户端 现在创建前端的WebSocket客户端来处理实时协作: // assets/js/services/CollaborationClient.tsimport { io, Socket } from 'socket.io-client'; export interface Operation { type: 'insert' | 'delete'; position: number; text?: string; length?: number; clientId?: number; timestamp: number;} export interface ClientInfo { userId: number; userName: string; color: string; cursor: { line: number; ch: number } | null; selection: { from: number; to: number } | null;} export interface DocumentState { content: string; version: number; clients: Record<number, ClientInfo>;} class CollaborationClient { private socket: Socket | null = null; private documentId: string | null = null; private userId: number; private userName: string; private isConnected = false; private operationsBuffer: Operation[] = []; private lastSentVersion = 0; // 事件监听器 private listeners: { textUpdate: ((operations: Operation[], version: number) => void)[]; userJoined: ((clientId: number, userInfo: ClientInfo) => void)[]; userLeft: ((clientId: number, userId: number) => void)[]; cursorMove: ((clientId: number, cursor: any) => void)[]; selectionChange: ((clientId: number, selection: any) => void)[]; connectionChange: ((connected: boolean) => void)[]; } = { textUpdate: [], userJoined: [], userLeft: [], cursorMove: [], selectionChange: [], connectionChange: [] }; constructor(userId: number, userName: string) { this.userId = userId; this.userName = userName; } connect(serverUrl: string): Promise<void> { return new Promise((resolve, reject) => { this.socket = io(serverUrl, { path: '/collab', transports: ['websocket', 'polling'], reconnection: true, reconnectionAttempts: 5, reconnectionDelay: 1000 }); this.socket.on('connect', () => { this.isConnected = true; this.notifyConnectionChange(true); console.log('Connected to collaboration server'); resolve(); }); this.socket.on('disconnect', () => { this.isConnected = false; this.notifyConnectionChange(false); console.log('Disconnected from collaboration server'); }); this.socket.on('connect_error', (error) => { console.error('Connection error:', error); reject(error); }); // 注册消息处理器 this.setupMessageHandlers(); }); } private setupMessageHandlers() { if (!this.socket) return; this.socket.on('welcome', (data: any) => { console.log('Server welcome:', data.message); }); this.socket.on('document_state', (data: any) => { console.log('Received document state, version:', data.version); // 这里可以触发文档状态更新事件 }); this.socket.on('text_updated', (data: any) => { console.log('Text updated by client:', data.clientId); this.listeners.textUpdate.forEach(callback => callback(data.operations, data.version) ); }); this.socket.on('user_joined', (data: any) => { console.log('User joined:', data.userName); this.listeners.userJoined.forEach(callback => callback(data.clientId, { userId: data.userId, userName: data.userName, color: data.color, cursor: null, selection: null }) ); }); this.socket.on('user_left', (data: any) => { console.log('User left:', data.userId); this.listeners.userLeft.forEach(callback => callback(data.clientId, data.userId) ); }); this.socket.on('cursor_moved', (data: any) => { this.listeners.cursorMove.forEach(callback => callback(data.clientId, data.cursor) ); }); this.socket.on('selection_changed', (data: any) => { this.listeners.selectionChange.forEach(callback => callback(data.clientId, data.selection) ); }); this.socket.on('version_conflict', (data: any) => { console.warn('Version conflict detected'); this.handleVersionConflict(data.serverVersion, data.serverContent); }); } joinDocument(documentId: string, initialContent: string = ''): void { this.documentId = documentId; if (!this.socket || !this.isConnected) { console.error('Not connected to server'); return; } this.socket.emit('join_document', { documentId, userId: this.userId, userName: this.userName, initialContent }); } sendOperations(operations: Operation[], currentVersion: number): void { if (!this.socket || !this.documentId || !this.isConnected) { // 缓存操作,等待连接恢复 this.operationsBuffer.push(...operations); return; } this.socket.emit('text_update', { documentId: this.documentId, operations, version: currentVersion, clientId: this.socket.id }); this.lastSentVersion = currentVersion; } sendCursorMove(cursor: { line: number; ch: number }): void { if (!this.socket || !this.documentId || !this.isConnected) return; this.socket.emit('cursor_move', { documentId: this.documentId, cursor, clientId: this.socket.id }); } sendSelectionChange(selection: { from: number; to: number }): void { if (!this.socket || !this.documentId || !this.isConnected) return; this.socket.emit('selection_change', { documentId: this.documentId, selection, clientId: this.socket.id }); } private handleVersionConflict(serverVersion: number, serverContent: string): void { // 这里实现OT冲突解决算法 console.log('Resolving version conflict:', { serverVersion, clientVersion: this.lastSentVersion, serverContentLength: serverContent.length }); // 触发冲突解决事件 // 实际实现中需要更复杂的OT算法 } // 事件监听管理 onTextUpdate(callback: (operations: Operation[], version: number) => void): void { this.listeners.textUpdate.push(callback); } onUserJoined(callback: (clientId: number, userInfo: ClientInfo) => void): void { this.listeners.userJoined.push(callback); } onUserLeft(callback: (clientId: number, userId: number) => void): void { this.listeners.userLeft.push(callback); } onCursorMove(callback: (clientId: number, cursor: any) => void): void { this.listeners.cursorMove.push(callback); } onSelectionChange(callback: (clientId: number, selection: any) => void): void { this.listeners.selectionChange.push(callback); } onConnectionChange(callback: (connected: boolean) => void): void { this.listeners.connectionChange.push(callback); } private notifyConnectionChange(connected: boolean): void { this.listeners.connectionChange.forEach(callback => callback(connected)); } disconnect(): void { if (this.socket) { this.socket.disconnect(); this.socket = null; } this.isConnected = false; this.documentId = null; } getConnectionStatus(): boolean { return this.isConnected; }} export default CollaborationClient; ## 第四部分:WordPress后端集成 ### 4.1 创建数据库表结构 我们需要创建自定义数据库表来存储Markdown笔记和相关数据: <?php// includes/class-database.php class WP_Markdown_Notes_DB { private static $instance = null; private $charset_collate; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { global $wpdb; $this->charset_collate = $wpdb->get_charset_collate(); register_activation_hook(__FILE__, [$this, 'create_tables']); add_action('plugins_loaded', [$this, 'check_tables']); } public function create_tables() { global $wpdb; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); $tables = [ $this->create_notes_table(), $this->create_note_versions_table(), $this->create_collaborators_table(), $this->create_folders_table(), $this->create_tags_table(), $this->create_note_tags_table() ]; foreach ($tables as $sql) { dbDelta($sql); } // 添加默认数据 $this->add_default_data(); } private function create_notes_table() { global $wpdb; $table_name = $wpdb->prefix . 'md_notes'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL DEFAULT '', content LONGTEXT NOT NULL, excerpt TEXT, folder_id BIGINT(20) UNSIGNED DEFAULT 0, author_id BIGINT(20) UNSIGNED NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'draft', is_public TINYINT(1) NOT NULL DEFAULT 0, share_token VARCHAR(32), view_count BIGINT(20) UNSIGNED DEFAULT 0, last_modified_by BIGINT(20) UNSIGNED, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY author_id (author_id), KEY folder_id (folder_id), KEY status (status), KEY share_token (share_token), KEY created_at (created_at), FULLTEXT KEY content_ft (title, content, excerpt) ) {$this->charset_collate};"; return $sql; } private function create_note_versions_table() { global $wpdb; $table_name = $wpdb->prefix . 'md_note_versions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, note_id BIGINT(20) UNSIGNED NOT NULL, version_number INT(11) NOT NULL, title VARCHAR(255), content LONGTEXT, author_id BIGINT(20) UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, change_summary VARCHAR(255), PRIMARY KEY (id), KEY note_id (note_id), KEY version_number (version_number), KEY created_at (created_at), FOREIGN KEY (note_id) REFERENCES {$wpdb->prefix}md_notes(id) ON DELETE CASCADE ) {$this->charset_collate};"; return $sql; } private function create_collaborators_table() { global $wpdb; $table_name = $wpdb->prefix . 'md_collaborators'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, note_id BIGINT(20) UNSIGNED NOT NULL, user_id BIGINT(20) UNSIGNED NOT NULL, permission_level ENUM('view', 'edit', 'admin') NOT NULL DEFAULT 'view', invited_by BIGINT(20) UNSIGNED, invited_at DATETIME DEFAULT CURRENT_TIMESTAMP, last_accessed DATETIME, PRIMARY KEY (id), UNIQUE KEY note_user (note_id, user_id), KEY user_id (user_id), KEY permission_level (permission_level), FOREIGN KEY (note_id) REFERENCES {$wpdb->prefix}md_notes(id) ON DELETE CASCADE ) {$this->charset_collate};"; return $sql; } private function create_folders_table() { global $wpdb; $table_name = $wpdb->prefix . 'md_folders'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(255) NOT NULL, parent_id BIGINT(20) UNSIGNED DEFAULT 0, author_id BIGINT(20) UNSIGNED NOT NULL, color VARCHAR(7), icon VARCHAR(50), sort_order INT(11) DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY parent_id (parent_id), KEY author_id (author_id), KEY sort_order (sort_order) ) {$this->charset_collate};"; return $sql; } private function create_tags_table() { global $wpdb; $table_name = $wpdb->prefix . 'md_tags'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, slug VARCHAR(100) NOT NULL, author_id BIGINT(20) UNSIGNED NOT NULL, color VARCHAR(7), created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY slug (slug), KEY author_id (author_id), KEY name (name) ) {$this->charset_collate};"; return $sql; } private function create_note_tags_table() { global $wpdb; $table_name = $wpdb->prefix . 'md_note_tags'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, note_id BIGINT(20) UNSIGNED NOT NULL, tag_id BIGINT(20) UNSIGNED NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY note_tag (note_id, tag_id), KEY tag_id (tag_id), FOREIGN KEY (note_id) REFERENCES {$wpdb->prefix}md_notes(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES {$wpdb->prefix}md_tags(id) ON DELETE CASCADE ) {$this->charset_collate};"; return $sql; } private function add_default_data() { global $wpdb; // 创建默认文件夹 $user_id = get_current_user_id(); if ($user_id) { $folders_table = $wpdb->prefix . 'md_folders'; $default_folders = [ ['name' => '个人笔记', 'color' => '#4ECDC4', 'icon' => '📝'], ['name' =>

发表评论

WordPress开发教程,集成网站用户贡献内容积分兑换商城系统

WordPress开发教程:集成用户贡献内容积分兑换商城系统与常用互联网小工具功能 引言:WordPress的无限可能性 在当今数字化时代,网站已不仅仅是信息展示的平台,更是用户互动和参与的重要场所。WordPress作为全球最受欢迎的内容管理系统,其真正的强大之处在于其高度的可扩展性和灵活性。本教程将深入探讨如何通过WordPress代码二次开发,实现一个集用户贡献内容、积分系统和兑换商城于一体的综合性平台,同时集成多种实用互联网小工具功能。 通过本教程,您将学习到如何将WordPress从一个简单的博客平台转变为一个功能丰富的互动社区和电子商务系统。无论您是WordPress开发者、网站管理员还是希望扩展网站功能的创业者,这些知识都将为您打开新的可能性。 第一部分:项目规划与环境搭建 1.1 系统需求分析与功能规划 在开始开发之前,我们需要明确项目的核心需求: 用户贡献内容系统:允许用户提交文章、评论、图片等内容 积分管理系统:根据用户行为自动计算和分配积分 积分兑换商城:用户可以使用积分兑换实物或虚拟商品 常用互联网小工具:集成实用工具如短链接生成、二维码创建等 技术架构规划: 主框架:WordPress 5.8+ 开发方式:自定义插件+主题二次开发 数据库:MySQL 5.6+ 前端技术:HTML5, CSS3, JavaScript (jQuery/Vue.js) 安全考虑:数据验证、SQL注入防护、XSS防护 1.2 开发环境配置 首先,确保您的开发环境满足以下要求: // 检查WordPress环境要求 define('WP_DEBUG', true); // 开发阶段开启调试模式 define('WP_DEBUG_LOG', true); // 将错误记录到日志 define('WP_DEBUG_DISPLAY', false); // 不直接显示错误 // 推荐服务器配置 // PHP版本:7.4或更高 // MySQL版本:5.6或更高 // 内存限制:至少256MB 安装必要的开发工具: 本地服务器环境(XAMPP、MAMP或Local by Flywheel) 代码编辑器(VS Code、PHPStorm等) Git版本控制系统 浏览器开发者工具 1.3 创建自定义插件基础结构 我们将创建一个主插件来管理所有功能: /* Plugin Name: 用户积分商城系统 Plugin URI: https://yourwebsite.com/ Description: 集成用户贡献内容、积分系统和兑换商城的综合解决方案 Version: 1.0.0 Author: 您的名称 License: GPL v2 or later Text Domain: user-points-mall */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('UPM_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('UPM_PLUGIN_URL', plugin_dir_url(__FILE__)); define('UPM_VERSION', '1.0.0'); // 初始化插件 function upm_init() { // 加载语言文件 load_plugin_textdomain('user-points-mall', false, dirname(plugin_basename(__FILE__)) . '/languages'); // 检查依赖 upm_check_dependencies(); } add_action('plugins_loaded', 'upm_init'); // 检查系统依赖 function upm_check_dependencies() { $errors = array(); // 检查PHP版本 if (version_compare(PHP_VERSION, '7.4', '<')) { $errors[] = __('需要PHP 7.4或更高版本', 'user-points-mall'); } // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.8', '<')) { $errors[] = __('需要WordPress 5.8或更高版本', 'user-points-mall'); } // 如果有错误,显示通知 if (!empty($errors)) { add_action('admin_notices', function() use ($errors) { echo '<div class="notice notice-error"><p>'; echo __('用户积分商城系统插件无法激活:', 'user-points-mall'); echo '<ul>'; foreach ($errors as $error) { echo '<li>' . $error . '</li>'; } echo '</ul></p></div>'; }); // 停用插件 deactivate_plugins(plugin_basename(__FILE__)); } } 第二部分:用户贡献内容系统开发 2.1 自定义文章类型与用户提交表单 创建用户贡献内容系统,首先需要定义自定义文章类型: // 注册用户贡献内容类型 function upm_register_contribution_type() { $labels = array( 'name' => __('用户贡献', 'user-points-mall'), 'singular_name' => __('贡献内容', 'user-points-mall'), 'menu_name' => __('用户贡献', 'user-points-mall'), 'add_new' => __('添加新贡献', 'user-points-mall'), 'add_new_item' => __('添加新贡献内容', 'user-points-mall'), 'edit_item' => __('编辑贡献内容', 'user-points-mall'), 'new_item' => __('新贡献内容', 'user-points-mall'), 'view_item' => __('查看贡献内容', 'user-points-mall'), 'search_items' => __('搜索贡献内容', 'user-points-mall'), 'not_found' => __('未找到贡献内容', 'user-points-mall'), 'not_found_in_trash' => __('回收站中无贡献内容', 'user-points-mall'), ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'contribution'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-groups', 'supports' => array('title', 'editor', 'author', 'thumbnail', 'excerpt', 'comments'), 'show_in_rest' => true, // 支持Gutenberg编辑器 ); register_post_type('user_contribution', $args); } add_action('init', 'upm_register_contribution_type'); // 添加贡献状态分类 function upm_register_contribution_status() { $labels = array( 'name' => __('贡献状态', 'user-points-mall'), 'singular_name' => __('状态', 'user-points-mall'), 'search_items' => __('搜索状态', 'user-points-mall'), 'all_items' => __('所有状态', 'user-points-mall'), 'parent_item' => __('父状态', 'user-points-mall'), 'parent_item_colon' => __('父状态:', 'user-points-mall'), 'edit_item' => __('编辑状态', 'user-points-mall'), 'update_item' => __('更新状态', 'user-points-mall'), 'add_new_item' => __('添加新状态', 'user-points-mall'), 'new_item_name' => __('新状态名称', 'user-points-mall'), 'menu_name' => __('贡献状态', 'user-points-mall'), ); $args = array( 'hierarchical' => true, 'labels' => $labels, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => array('slug' => 'contribution-status'), 'show_in_rest' => true, ); register_taxonomy('contribution_status', array('user_contribution'), $args); // 默认状态 $default_statuses = array( 'pending' => __('待审核', 'user-points-mall'), 'approved' => __('已通过', 'user-points-mall'), 'rejected' => __('已拒绝', 'user-points-mall'), 'published' => __('已发布', 'user-points-mall'), ); foreach ($default_statuses as $slug => $name) { if (!term_exists($name, 'contribution_status')) { wp_insert_term($name, 'contribution_status', array('slug' => $slug)); } } } add_action('init', 'upm_register_contribution_status'); 2.2 前端用户提交表单 创建用户提交贡献内容的前端表单: // 短代码生成用户提交表单 function upm_contribution_form_shortcode($atts) { // 只有登录用户才能提交 if (!is_user_logged_in()) { return '<div class="upm-alert upm-alert-warning">' . __('请先登录后再提交贡献内容。', 'user-points-mall') . ' <a href="' . wp_login_url(get_permalink()) . '">' . __('登录', 'user-points-mall') . '</a></div>'; } // 处理表单提交 $message = ''; $success = false; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['upm_contribution_nonce'])) { if (wp_verify_nonce($_POST['upm_contribution_nonce'], 'upm_submit_contribution')) { $result = upm_process_contribution_submission($_POST); if ($result['success']) { $message = '<div class="upm-alert upm-alert-success">' . __('贡献提交成功!内容正在审核中。', 'user-points-mall') . '</div>'; $success = true; } else { $message = '<div class="upm-alert upm-alert-error">' . $result['message'] . '</div>'; } } else { $message = '<div class="upm-alert upm-alert-error">' . __('安全验证失败,请重试。', 'user-points-mall') . '</div>'; } } // 如果提交成功,显示成功消息 if ($success) { return $message; } // 输出表单 ob_start(); ?> <div class="upm-contribution-form-wrapper"> <?php echo $message; ?> <form id="upm-contribution-form" method="post" enctype="multipart/form-data"> <?php wp_nonce_field('upm_submit_contribution', 'upm_contribution_nonce'); ?> <div class="upm-form-group"> <label for="upm-contribution-title"><?php _e('标题', 'user-points-mall'); ?> *</label> <input type="text" id="upm-contribution-title" name="contribution_title" required class="upm-form-control" value="<?php echo isset($_POST['contribution_title']) ? esc_attr($_POST['contribution_title']) : ''; ?>"> </div> <div class="upm-form-group"> <label for="upm-contribution-content"><?php _e('内容', 'user-points-mall'); ?> *</label> <?php $content = isset($_POST['contribution_content']) ? wp_kses_post($_POST['contribution_content']) : ''; wp_editor($content, 'upm-contribution-content', array( 'textarea_name' => 'contribution_content', 'textarea_rows' => 10, 'media_buttons' => true, 'teeny' => false, 'quicktags' => true )); ?> </div> <div class="upm-form-group"> <label for="upm-contribution-excerpt"><?php _e('摘要', 'user-points-mall'); ?></label> <textarea id="upm-contribution-excerpt" name="contribution_excerpt" class="upm-form-control" rows="3"><?php echo isset($_POST['contribution_excerpt']) ? esc_textarea($_POST['contribution_excerpt']) : ''; ?></textarea> </div> <div class="upm-form-group"> <label for="upm-contribution-thumbnail"><?php _e('特色图片', 'user-points-mall'); ?></label> <input type="file" id="upm-contribution-thumbnail" name="contribution_thumbnail" accept="image/*" class="upm-form-control"> <p class="upm-help-text"><?php _e('支持JPG、PNG格式,最大2MB', 'user-points-mall'); ?></p> </div> <div class="upm-form-group"> <label for="upm-contribution-category"><?php _e('分类', 'user-points-mall'); ?></label> <select id="upm-contribution-category" name="contribution_category" class="upm-form-control"> <option value=""><?php _e('选择分类', 'user-points-mall'); ?></option> <?php $categories = get_categories(array('hide_empty' => false)); foreach ($categories as $category) { $selected = (isset($_POST['contribution_category']) && $_POST['contribution_category'] == $category->term_id) ? 'selected' : ''; echo '<option value="' . $category->term_id . '" ' . $selected . '>' . esc_html($category->name) . '</option>'; } ?> </select> </div> <div class="upm-form-group"> <label> <input type="checkbox" name="agree_terms" required> <?php _e('我同意遵守网站内容提交规则', 'user-points-mall'); ?> </label> </div> <div class="upm-form-group"> <input type="submit" name="submit_contribution" value="<?php _e('提交贡献', 'user-points-mall'); ?>" class="upm-btn upm-btn-primary"> </div> </form> </div> <style> .upm-contribution-form-wrapper { max-width: 800px; margin: 0 auto; padding: 20px; background: #f9f9f9; border-radius: 8px; } .upm-form-group { margin-bottom: 20px; } .upm-form-control { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box; } .upm-btn { padding: 12px 24px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; } .upm-btn-primary { background: #0073aa; color: white; } .upm-alert { padding: 15px; margin-bottom: 20px; border-radius: 4px; } .upm-alert-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; } .upm-alert-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; } .upm-alert-warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; } </style> <?php return ob_get_clean(); } add_shortcode('upm_contribution_form', 'upm_contribution_form_shortcode'); // 处理表单提交 function upm_process_contribution_submission($data) { $result = array('success' => false, 'message' => ''); // 验证数据 if (empty($data['contribution_title'])) { $result['message'] = __('标题不能为空', 'user-points-mall'); return $result; } if (empty($data['contribution_content'])) { $result['message'] = __('内容不能为空', 'user-points-mall'); return $result; } if (!isset($data['agree_terms'])) { $result['message'] = __('请同意内容提交规则', 'user-points-mall'); return $result; } // 准备文章数据 $post_data = array( 'post_title' => sanitize_text_field($data['contribution_title']), 'post_content' => wp_kses_post($data['contribution_content']), 'post_excerpt' => sanitize_textarea_field($data['contribution_excerpt']), 'post_type' => 'user_contribution', 'post_status' => 'pending', // 初始状态为待审核 'post_author' => get_current_user_id(), ); // 插入文章 $post_id = wp_insert_post($post_data); if (is_wp_error($post_id) || $post_id === 0) { $result['message'] = __('提交失败,请重试', 'user-points-mall'); return $result; } // 设置分类 if (!empty($data['contribution_category'])) { wp_set_post_categories($post_id, array(intval($data['contribution_category']))); } // 设置默认状态为"待审核" wp_set_object_terms($post_id, 'pending', 'contribution_status'); // 处理特色图片上传 thumbnail']['name'])) { $upload = upm_handle_image_upload($post_id, 'contribution_thumbnail'); if ($upload && !is_wp_error($upload)) { set_post_thumbnail($post_id, $upload); } } // 记录用户贡献行为并分配积分 upm_award_points_for_contribution(get_current_user_id(), $post_id); // 发送通知邮件给管理员 upm_send_contribution_notification($post_id); $result['success'] = true; return $result; } // 处理图片上传function upm_handle_image_upload($post_id, $field_name) { if (!function_exists('wp_handle_upload')) { require_once(ABSPATH . 'wp-admin/includes/file.php'); } $uploadedfile = $_FILES[$field_name]; $upload_overrides = array('test_form' => false); // 验证文件类型和大小 $file_type = wp_check_filetype($uploadedfile['name']); $allowed_types = array('jpg' => 'image/jpeg', 'jpeg' => 'image/jpeg', 'png' => 'image/png'); if (!in_array($file_type['type'], $allowed_types)) { return new WP_Error('invalid_type', __('只支持JPG和PNG格式', 'user-points-mall')); } if ($uploadedfile['size'] > 2 * 1024 * 1024) { // 2MB限制 return new WP_Error('file_size', __('文件大小不能超过2MB', 'user-points-mall')); } $movefile = wp_handle_upload($uploadedfile, $upload_overrides); if ($movefile && !isset($movefile['error'])) { $filename = $movefile['file']; $filetype = wp_check_filetype($filename, null); $attachment = array( 'post_mime_type' => $filetype['type'], 'post_title' => sanitize_file_name($uploadedfile['name']), 'post_content' => '', 'post_status' => 'inherit', 'post_parent' => $post_id ); $attach_id = wp_insert_attachment($attachment, $filename, $post_id); require_once(ABSPATH . 'wp-admin/includes/image.php'); $attach_data = wp_generate_attachment_metadata($attach_id, $filename); wp_update_attachment_metadata($attach_id, $attach_data); return $attach_id; } return false; } ### 2.3 用户贡献内容展示与管理 创建用户贡献内容展示页面和管理功能: // 短代码显示用户贡献列表function upm_user_contributions_shortcode($atts) { $atts = shortcode_atts(array( 'user_id' => get_current_user_id(), 'per_page' => 10, 'status' => 'all' ), $atts, 'upm_user_contributions'); if (!is_user_logged_in() && $atts['user_id'] == get_current_user_id()) { return '<div class="upm-alert upm-alert-warning">' . __('请先登录查看您的贡献', 'user-points-mall') . '</div>'; } $paged = get_query_var('paged') ? get_query_var('paged') : 1; $args = array( 'post_type' => 'user_contribution', 'author' => $atts['user_id'], 'posts_per_page' => $atts['per_page'], 'paged' => $paged, 'post_status' => 'any', ); if ($atts['status'] != 'all') { $args['tax_query'] = array( array( 'taxonomy' => 'contribution_status', 'field' => 'slug', 'terms' => $atts['status'] ) ); } $contributions = new WP_Query($args); ob_start(); ?> <div class="upm-user-contributions"> <h3><?php _e('我的贡献', 'user-points-mall'); ?></h3> <?php if ($contributions->have_posts()) : ?> <div class="upm-contributions-list"> <?php while ($contributions->have_posts()) : $contributions->the_post(); ?> <div class="upm-contribution-item"> <h4><a href="<?php the_permalink(); ?>"><?php the_title(); ?></a></h4> <div class="upm-contribution-meta"> <span class="upm-date"><?php echo get_the_date(); ?></span> <span class="upm-status"> <?php $status_terms = get_the_terms(get_the_ID(), 'contribution_status'); if ($status_terms && !is_wp_error($status_terms)) { $status = reset($status_terms); echo '<span class="status-badge status-' . $status->slug . '">' . $status->name . '</span>'; } ?> </span> <span class="upm-points"> <?php $points = get_post_meta(get_the_ID(), '_contribution_points', true); if ($points) { echo sprintf(__('获得积分: %d', 'user-points-mall'), $points); } ?> </span> </div> <div class="upm-contribution-excerpt"> <?php the_excerpt(); ?> </div> </div> <?php endwhile; ?> </div> <!-- 分页 --> <div class="upm-pagination"> <?php echo paginate_links(array( 'total' => $contributions->max_num_pages, 'current' => $paged, 'prev_text' => __('« 上一页', 'user-points-mall'), 'next_text' => __('下一页 »', 'user-points-mall'), )); ?> </div> <?php wp_reset_postdata(); ?> <?php else : ?> <div class="upm-alert upm-alert-info"> <?php _e('暂无贡献内容', 'user-points-mall'); ?> </div> <?php endif; ?> </div> <style> .upm-user-contributions { margin: 20px 0; } .upm-contribution-item { border: 1px solid #e0e0e0; border-radius: 6px; padding: 15px; margin-bottom: 15px; background: white; } .upm-contribution-meta { display: flex; gap: 15px; margin: 10px 0; font-size: 14px; color: #666; } .status-badge { padding: 3px 8px; border-radius: 12px; font-size: 12px; font-weight: bold; } .status-pending { background: #fff3cd; color: #856404; } .status-approved { background: #d4edda; color: #155724; } .status-published { background: #cce5ff; color: #004085; } .status-rejected { background: #f8d7da; color: #721c24; } .upm-pagination { margin-top: 20px; text-align: center; } .upm-pagination a, .upm-pagination span { display: inline-block; padding: 8px 12px; margin: 0 2px; border: 1px solid #ddd; text-decoration: none; } .upm-pagination .current { background: #0073aa; color: white; border-color: #0073aa; } </style> <?php return ob_get_clean(); }add_shortcode('upm_user_contributions', 'upm_user_contributions_shortcode'); ## 第三部分:积分管理系统开发 ### 3.1 积分数据库设计与核心功能 创建积分管理系统的数据库表和核心功能: // 创建积分相关数据库表function upm_create_points_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'upm_user_points'; $log_table_name = $wpdb->prefix . 'upm_points_log'; // 用户积分表 $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, total_points int(11) NOT NULL DEFAULT 0, available_points int(11) NOT NULL DEFAULT 0, frozen_points int(11) NOT NULL DEFAULT 0, last_updated datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY user_id (user_id), KEY total_points (total_points), KEY available_points (available_points) ) $charset_collate;"; // 积分日志表 $sql .= "CREATE TABLE IF NOT EXISTS $log_table_name ( log_id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, points_change int(11) NOT NULL, new_balance int(11) NOT NULL, action_type varchar(50) NOT NULL, action_detail varchar(255) DEFAULT NULL, related_id bigint(20) DEFAULT NULL, related_type varchar(50) DEFAULT NULL, ip_address varchar(45) DEFAULT NULL, user_agent text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (log_id), KEY user_id (user_id), KEY action_type (action_type), KEY created_at (created_at), KEY related_id (related_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); }register_activation_hook(__FILE__, 'upm_create_points_tables'); // 积分管理类class UPM_Points_Manager { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } // 为用户添加积分 public function add_points($user_id, $points, $action_type, $action_detail = '', $related_id = null, $related_type = null) { if ($points <= 0) { return false; } global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; // 获取或创建用户积分记录 $user_points = $this->get_user_points($user_id); if (!$user_points) { $wpdb->insert($table_name, array( 'user_id' => $user_id, 'total_points' => $points, 'available_points' => $points, 'frozen_points' => 0 )); } else { $wpdb->update($table_name, array( 'total_points' => $user_points->total_points + $points, 'available_points' => $user_points->available_points + $points ), array('user_id' => $user_id) ); } // 记录积分日志 $this->log_points_change($user_id, $points, $action_type, $action_detail, $related_id, $related_type); // 更新用户元数据缓存 delete_user_meta($user_id, '_upm_points_data'); return true; } // 扣除用户积分 public function deduct_points($user_id, $points, $action_type, $action_detail = '', $related_id = null, $related_type = null) { if ($points <= 0) { return false; } global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; $user_points = $this->get_user_points($user_id); if (!$user_points || $user_points->available_points < $points) { return false; // 积分不足 } $wpdb->update($table_name, array( 'total_points' => $user_points->total_points - $points, 'available_points' => $user_points->available_points - $points ), array('user_id' => $user_id) ); // 记录积分日志(负值表示扣除) $this->log_points_change($user_id, -$points, $action_type, $action_detail, $related_id, $related_type); // 更新用户元数据缓存 delete_user_meta($user_id, '_upm_points_data'); return true; } // 冻结积分 public function freeze_points($user_id, $points, $action_type, $action_detail = '') { global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; $user_points = $this->get_user_points($user_id); if (!$user_points || $user_points->available_points < $points) { return false; } $wpdb->update($table_name, array( 'available_points' => $user_points->available_points - $points, 'frozen_points' => $user_points->frozen_points + $points ), array('user_id' => $user_id) ); $this->log_points_change($user_id, -$points, $action_type . '_freeze', $action_detail); delete_user_meta($user_id, '_upm_points_data'); return true; } // 解冻积分 public function unfreeze_points($user_id, $points, $action_type, $action_detail = '') { global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; $user_points = $this->get_user_points($user_id); if (!$user_points || $user_points->frozen_points < $points) { return false; } $wpdb->update($table_name, array( 'available_points' => $user_points->available_points + $points, 'frozen_points' => $user_points->frozen_points - $points ), array('user_id' => $user_id) ); $this->log_points_change($user_id, $points, $action_type . '_unfreeze', $action_detail); delete_user_meta($user_id, '_upm_points_data'); return true; } // 获取用户积分信息 public function get_user_points($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'upm_user_points'; // 尝试从缓存获取 $cached = get_user_meta($user_id, '_upm_points_data', true); if ($cached && is_object($cached)) { return $cached; } $result = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE user_id = %d", $user_id )); if ($result) { // 缓存结果 update_user_meta($user_id, '_upm_points_data', $result); } return $result; } // 记录积分变化日志 private function log_points_change($user_id, $points_change, $action_type, $action_detail, $related_id = null, $related_type = null) { global $wpdb; $table_name = $wpdb->prefix . 'upm_points_log'; $user_points = $this->get_user_points($user_id); $new_balance = $user_points ? $user_points->available_points : 0; $data = array( 'user_id' => $user_id, 'points_change' => $points_change, 'new_balance' => $new_balance, 'action_type' => $action_type, 'action_detail' => $action_detail, 'related_id' => $related_id, 'related_type' => $related_type, 'ip_address' => $this->get_client_ip(), 'user_agent' => isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '' ); $wpdb->insert($table_name, $data); // 触发积分变化钩子 do_action('upm_points_changed', $user_id, $points_change, $action_type, $data); } // 获取客户端IP private function get_client_ip() { $ip_keys = array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'); foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : '0.0.0.0'; } // 获取积分排行榜 public function get_points_leaderboard($limit = 10, $offset = 0) { global $wpdb; $table_name = $wpdb->prefix

发表评论

实战教学,为你的网站添加在线团队投票与决策辅助工具

实战教学:为你的网站添加在线团队投票与决策辅助工具 引言:为什么网站需要团队协作工具? 在当今数字化工作环境中,团队协作效率直接影响项目成败。无论是小型创业团队、远程工作小组,还是企业内部部门,都需要高效的工具来收集意见、做出决策。然而,许多团队仍然依赖分散的沟通渠道——微信群讨论、邮件投票、Excel表格统计——这种方式效率低下且容易造成信息丢失。 通过为你的WordPress网站添加在线投票与决策辅助工具,你可以创建一个集中的协作平台,让团队成员能够实时参与决策过程,记录讨论结果,并追踪决策历史。这不仅提高了团队效率,还创造了透明、可追溯的决策文化。 本教程将带你一步步实现这一功能,无需购买昂贵的第三方服务,通过代码二次开发即可拥有完全可控的团队协作工具。 第一部分:准备工作与环境搭建 1.1 开发环境要求 在开始之前,请确保你拥有以下环境: WordPress 5.0及以上版本 PHP 7.2及以上版本(推荐7.4+) MySQL 5.6及以上版本 一个代码编辑器(如VS Code、Sublime Text等) 基础的HTML、CSS、JavaScript和PHP知识 1.2 创建开发用的子主题 为了避免主题更新导致代码丢失,我们首先创建一个子主题: 在WordPress的wp-content/themes/目录下创建新文件夹team-vote-theme 在该文件夹中创建style.css文件,添加以下内容: /* Theme Name: Team Vote Theme Template: 你的父主题名称 Version: 1.0 Description: 用于团队投票功能的子主题 */ /* 导入父主题样式 */ @import url("../父主题名称/style.css"); 创建functions.php文件,添加以下代码: <?php // 子主题初始化 function team_vote_theme_setup() { // 加载父主题样式 add_action('wp_enqueue_scripts', 'enqueue_parent_styles'); } add_action('after_setup_theme', 'team_vote_theme_setup'); function enqueue_parent_styles() { wp_enqueue_style('parent-style', get_template_directory_uri().'/style.css'); } // 在这里添加我们的自定义功能 ?> 在WordPress后台启用这个子主题 1.3 创建插件还是主题功能? 对于这种功能,我们有三种实现方式: 作为主题的一部分(适合特定主题) 创建独立插件(适合多站点或主题无关场景) 使用功能插件(Functions Plugin)模式 本教程将采用功能插件模式,这样即使更换主题,功能也能保留。在wp-content/plugins/目录下创建team-vote-tool文件夹,并创建主文件team-vote-tool.php: <?php /** * Plugin Name: 团队投票与决策辅助工具 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站添加在线团队投票与决策辅助功能 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('TEAM_VOTE_VERSION', '1.0.0'); define('TEAM_VOTE_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('TEAM_VOTE_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once TEAM_VOTE_PLUGIN_DIR . 'includes/init.php'; ?> 第二部分:数据库设计与数据模型 2.1 创建自定义数据库表 团队投票系统需要存储投票活动、选项、用户投票记录等信息。我们在插件激活时创建必要的数据库表: 在插件目录下创建includes/activate.php: <?php function team_vote_activate_plugin() { // 检查必要的WordPress函数 if (!function_exists('dbDelta')) { require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); } global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 投票活动表 $table_name = $wpdb->prefix . 'team_votes'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, creator_id bigint(20) NOT NULL, status varchar(20) DEFAULT 'active', vote_type varchar(50) DEFAULT 'single', max_choices mediumint(9) DEFAULT 1, start_date datetime DEFAULT NULL, end_date datetime DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; dbDelta($sql); // 投票选项表 $table_name = $wpdb->prefix . 'team_vote_options'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, vote_id mediumint(9) NOT NULL, option_text varchar(500) NOT NULL, description text, image_url varchar(500), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY vote_id (vote_id) ) $charset_collate;"; dbDelta($sql); // 投票记录表 $table_name = $wpdb->prefix . 'team_vote_records'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, vote_id mediumint(9) NOT NULL, option_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, comment text, voted_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY user_vote_unique (vote_id, user_id, option_id), KEY vote_id (vote_id), KEY user_id (user_id) ) $charset_collate;"; dbDelta($sql); // 决策讨论表 $table_name = $wpdb->prefix . 'team_decisions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, creator_id bigint(20) NOT NULL, status varchar(20) DEFAULT 'discussion', final_decision text, deadline datetime DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; dbDelta($sql); // 决策评论表 $table_name = $wpdb->prefix . 'team_decision_comments'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, decision_id mediumint(9) NOT NULL, user_id bigint(20) NOT NULL, comment text NOT NULL, sentiment varchar(20) DEFAULT 'neutral', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY decision_id (decision_id), KEY user_id (user_id) ) $charset_collate;"; dbDelta($sql); // 存储插件版本,便于后续更新 add_option('team_vote_db_version', TEAM_VOTE_VERSION); } register_activation_hook(__FILE__, 'team_vote_activate_plugin'); ?> 2.2 创建数据模型类 为了更方便地操作数据,我们创建模型类。在includes/models/目录下创建Vote.php: <?php class TeamVoteModel { private $wpdb; private $table_votes; private $table_options; private $table_records; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->table_votes = $wpdb->prefix . 'team_votes'; $this->table_options = $wpdb->prefix . 'team_vote_options'; $this->table_records = $wpdb->prefix . 'team_vote_records'; } // 创建新投票 public function create_vote($data) { $defaults = array( 'status' => 'active', 'vote_type' => 'single', 'max_choices' => 1, 'created_at' => current_time('mysql') ); $data = wp_parse_args($data, $defaults); $result = $this->wpdb->insert( $this->table_votes, $data ); if ($result) { return $this->wpdb->insert_id; } return false; } // 获取投票详情 public function get_vote($vote_id) { $query = $this->wpdb->prepare( "SELECT * FROM $this->table_votes WHERE id = %d", $vote_id ); return $this->wpdb->get_row($query); } // 获取用户的所有投票 public function get_user_votes($user_id, $status = null) { $where = "WHERE creator_id = %d"; $params = array($user_id); if ($status) { $where .= " AND status = %s"; $params[] = $status; } $query = $this->wpdb->prepare( "SELECT * FROM $this->table_votes $where ORDER BY created_at DESC", $params ); return $this->wpdb->get_results($query); } // 添加投票选项 public function add_option($vote_id, $option_text, $description = '', $image_url = '') { return $this->wpdb->insert( $this->table_options, array( 'vote_id' => $vote_id, 'option_text' => $option_text, 'description' => $description, 'image_url' => $image_url ) ); } // 获取投票的所有选项 public function get_options($vote_id) { $query = $this->wpdb->prepare( "SELECT * FROM $this->table_options WHERE vote_id = %d ORDER BY id ASC", $vote_id ); return $this->wpdb->get_results($query); } // 记录用户投票 public function record_vote($vote_id, $option_id, $user_id, $comment = '') { // 检查是否已经投过票(对于单选投票) $vote = $this->get_vote($vote_id); if ($vote->vote_type === 'single') { $existing = $this->wpdb->get_var($this->wpdb->prepare( "SELECT COUNT(*) FROM $this->table_records WHERE vote_id = %d AND user_id = %d", $vote_id, $user_id )); if ($existing > 0) { return new WP_Error('already_voted', '您已经投过票了'); } } // 检查多选投票是否超过最大选择数 if ($vote->vote_type === 'multiple') { $current_choices = $this->wpdb->get_var($this->wpdb->prepare( "SELECT COUNT(*) FROM $this->table_records WHERE vote_id = %d AND user_id = %d", $vote_id, $user_id )); if ($current_choices >= $vote->max_choices) { return new WP_Error('max_choices', '已达到最大选择数量'); } } return $this->wpdb->insert( $this->table_records, array( 'vote_id' => $vote_id, 'option_id' => $option_id, 'user_id' => $user_id, 'comment' => $comment ) ); } // 获取投票结果 public function get_results($vote_id) { $query = $this->wpdb->prepare( "SELECT o.id, o.option_text, o.description, o.image_url, COUNT(r.id) as vote_count FROM $this->table_options o LEFT JOIN $this->table_records r ON o.id = r.option_id WHERE o.vote_id = %d GROUP BY o.id ORDER BY vote_count DESC", $vote_id ); return $this->wpdb->get_results($query); } // 检查用户是否已投票 public function has_user_voted($vote_id, $user_id) { $count = $this->wpdb->get_var($this->wpdb->prepare( "SELECT COUNT(*) FROM $this->table_records WHERE vote_id = %d AND user_id = %d", $vote_id, $user_id )); return $count > 0; } // 获取用户的投票选择 public function get_user_vote($vote_id, $user_id) { $query = $this->wpdb->prepare( "SELECT r.*, o.option_text FROM $this->table_records r JOIN $this->table_options o ON r.option_id = o.id WHERE r.vote_id = %d AND r.user_id = %d", $vote_id, $user_id ); return $this->wpdb->get_results($query); } } ?> 同样地,创建决策模型类Decision.php,由于篇幅限制,这里不展开详细代码。 第三部分:前端界面设计与实现 3.1 创建投票创建表单 在templates/目录下创建create-vote.php: <?php /** * 创建投票表单模板 */ if (!defined('ABSPATH')) { exit; } // 检查用户是否登录 if (!is_user_logged_in()) { echo '<div class="team-vote-message">请先登录以创建投票</div>'; return; } // 处理表单提交 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['create_vote'])) { // 这里添加表单处理逻辑,将在后面实现 } // 获取当前用户ID $current_user_id = get_current_user_id(); ?> <div class="team-vote-container"> <h2>创建新投票</h2> <form id="create-vote-form" method="POST" action=""> <?php wp_nonce_field('create_vote_action', 'create_vote_nonce'); ?> <div class="form-group"> <label for="vote_title">投票标题 *</label> <input type="text" id="vote_title" name="vote_title" required placeholder="例如:选择团队下次团建地点"> </div> <div class="form-group"> <label for="vote_description">投票描述</label> <textarea id="vote_description" name="vote_description" rows="3" placeholder="详细描述投票的背景和目的"></textarea> </div> <div class="form-row"> <div class="form-group"> <label for="vote_type">投票类型</label> <select id="vote_type" name="vote_type"> <option value="single">单选</option> <option value="multiple">多选</option> </select> </div> <div class="form-group" id="max-choices-container" style="display:none;"> <label for="max_choices">最多可选数量</label> <input type="number" id="max_choices" name="max_choices" min="2" max="10" value="3"> </div> </div> <div class="form-row"> <div class="form-group"> <label for="start_date">开始时间</label> <input type="datetime-local" id="start_date" name="start_date"> </div> <div class="form-group"> <label for="end_date">结束时间</label> <input type="datetime-local" id="end_date" name="end_date"> </div> </div> <div class="form-group"> <label>投票选项 *</label> <div id="vote-options-container"> <div class="option-item"> <input type="text" name="options[]" placeholder="选项内容" required> <textarea name="option_descriptions[]" placeholder="选项描述(可选)"></textarea> <input type="text" name="option_images[]" placeholder="图片URL(可选)"> <button type="button" class="remove-option">删除</button> </div> <div class="option-item"> <input type="text" name="options[]" placeholder="选项内容" required> <textarea name="option_descriptions[]" placeholder="选项描述(可选)"></textarea> <input type="text" name="option_images[]" placeholder="图片URL(可选)"> <button type="button" class="remove-option">删除</button> </div> </div> <button type="button" id="add-option" class="button-secondary">添加选项</button> </div> <div class="form-submit"> <input type="submit" name="create_vote" value="创建投票" class="button-primary"> </div> </form> </div> <script> jQuery(document).ready(function($) { // 显示/隐藏多选数量设置 $('#vote_type').change(function() { if ($(this).val() === 'multiple') { $('#max-choices-container').show(); } else { $('#max-choices-container').hide(); } }); // 添加选项 $('#add-option').click(function() { var optionHtml = ` <div class="option-item"> 选项内容" required> <textarea name="option_descriptions[]" placeholder="选项描述(可选)"></textarea> <input type="text" name="option_images[]" placeholder="图片URL(可选)"> <button type="button" class="remove-option">删除</button> </div>`; $('#vote-options-container').append(optionHtml); }); // 删除选项 $(document).on('click', '.remove-option', function() { if ($('.option-item').length > 2) { $(this).closest('.option-item').remove(); } else { alert('投票至少需要两个选项'); } }); });</script> ### 3.2 创建投票展示与参与界面 在`templates/`目录下创建`vote-display.php`: <?php/** 投票展示与参与模板 */ if (!defined('ABSPATH')) { exit; } // 获取投票ID$vote_id = isset($_GET['vote_id']) ? intval($_GET['vote_id']) : 0;if (!$vote_id) { echo '<div class="team-vote-message">未指定投票</div>'; return; } // 获取投票信息$vote_model = new TeamVoteModel();$vote = $vote_model->get_vote($vote_id);if (!$vote) { echo '<div class="team-vote-message">投票不存在</div>'; return; } // 获取投票选项$options = $vote_model->get_options($vote_id);if (empty($options)) { echo '<div class="team-vote-message">投票选项未设置</div>'; return; } // 检查投票状态$current_time = current_time('mysql');$is_active = true; if ($vote->start_date && $vote->start_date > $current_time) { $is_active = false; $status_message = '投票尚未开始'; } elseif ($vote->end_date && $vote->end_date < $current_time) { $is_active = false; $status_message = '投票已结束'; } elseif ($vote->status !== 'active') { $is_active = false; $status_message = '投票已关闭'; } // 获取当前用户信息$current_user_id = get_current_user_id();$has_voted = $vote_model->has_user_voted($vote_id, $current_user_id);$user_votes = $has_voted ? $vote_model->get_user_vote($vote_id, $current_user_id) : array(); // 获取投票结果$results = $vote_model->get_results($vote_id);$total_votes = 0;foreach ($results as $result) { $total_votes += $result->vote_count; } // 处理投票提交if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_vote']) && $is_active && !$has_voted) { // 这里添加投票处理逻辑 }?> <div class="team-vote-container"> <div class="vote-header"> <h2><?php echo esc_html($vote->title); ?></h2> <div class="vote-meta"> <span class="vote-creator">创建者: <?php echo get_userdata($vote->creator_id)->display_name; ?></span> <span class="vote-date">创建时间: <?php echo date('Y-m-d H:i', strtotime($vote->created_at)); ?></span> <?php if ($vote->end_date): ?> <span class="vote-deadline">截止时间: <?php echo date('Y-m-d H:i', strtotime($vote->end_date)); ?></span> <?php endif; ?> </div> <?php if ($vote->description): ?> <div class="vote-description"> <?php echo wpautop(esc_html($vote->description)); ?> </div> <?php endif; ?> <div class="vote-status"> <?php if ($is_active): ?> <span class="status-active">● 进行中</span> <?php else: ?> <span class="status-inactive">● <?php echo $status_message; ?></span> <?php endif; ?> <span class="total-votes">总投票数: <?php echo $total_votes; ?></span> </div> </div> <?php if ($is_active && !$has_voted): ?> <form id="vote-form" method="POST" action=""> <?php wp_nonce_field('vote_action_' . $vote_id, 'vote_nonce'); ?> <input type="hidden" name="vote_id" value="<?php echo $vote_id; ?>"> <div class="vote-options"> <h3>请选择<?php echo $vote->vote_type === 'multiple' ? '(可多选)' : '(单选)'; ?></h3> <?php foreach ($options as $index => $option): ?> <div class="vote-option"> <label class="option-label"> <?php if ($vote->vote_type === 'multiple'): ?> <input type="checkbox" name="selected_options[]" value="<?php echo $option->id; ?>" class="option-checkbox"> <?php else: ?> <input type="radio" name="selected_options" value="<?php echo $option->id; ?>" class="option-radio" required> <?php endif; ?> <div class="option-content"> <div class="option-text"><?php echo esc_html($option->option_text); ?></div> <?php if ($option->description): ?> <div class="option-description"> <?php echo esc_html($option->description); ?> </div> <?php endif; ?> <?php if ($option->image_url): ?> <div class="option-image"> <img src="<?php echo esc_url($option->image_url); ?>" alt="<?php echo esc_attr($option->option_text); ?>"> </div> <?php endif; ?> </div> </label> </div> <?php endforeach; ?> </div> <div class="vote-comment"> <label for="vote_comment">投票备注(可选)</label> <textarea id="vote_comment" name="vote_comment" placeholder="请说明您选择此选项的理由..."></textarea> </div> <div class="vote-submit"> <button type="submit" name="submit_vote" class="button-primary"> 提交投票 </button> </div> </form> <?php elseif ($has_voted): ?> <div class="already-voted"> <h3>您已参与本次投票</h3> <p>您选择了以下选项:</p> <ul class="user-selections"> <?php foreach ($user_votes as $user_vote): ?> <li><?php echo esc_html($user_vote->option_text); ?></li> <?php endforeach; ?> </ul> <?php if (!empty($user_votes[0]->comment)): ?> <div class="user-comment"> <strong>您的备注:</strong> <p><?php echo esc_html($user_votes[0]->comment); ?></p> </div> <?php endif; ?> </div> <?php endif; ?> <!-- 投票结果展示 --> <div class="vote-results"> <h3>投票结果</h3> <?php if ($total_votes > 0): ?> <div class="results-summary"> <p>总参与人数: <strong><?php echo $total_votes; ?></strong></p> </div> <div class="results-chart"> <?php foreach ($results as $result): $percentage = $total_votes > 0 ? round(($result->vote_count / $total_votes) * 100, 1) : 0; ?> <div class="result-item"> <div class="result-header"> <span class="option-text"><?php echo esc_html($result->option_text); ?></span> <span class="vote-count"><?php echo $result->vote_count; ?> 票 (<?php echo $percentage; ?>%)</span> </div> <div class="result-bar-container"> <div class="result-bar" style="width: <?php echo $percentage; ?>%;"></div> </div> <?php if ($result->description): ?> <div class="result-description"> <?php echo esc_html($result->description); ?> </div> <?php endif; ?> </div> <?php endforeach; ?> </div> <?php else: ?> <p class="no-results">暂无投票结果</p> <?php endif; ?> </div> </div> <?php if ($is_active && !$has_voted): ?><script>jQuery(document).ready(function($) { // 多选投票的最大选择限制 <?php if ($vote->vote_type === 'multiple' && $vote->max_choices > 0): ?> var maxChoices = <?php echo $vote->max_choices; ?>; $('.option-checkbox').change(function() { var checkedCount = $('.option-checkbox:checked').length; if (checkedCount > maxChoices) { $(this).prop('checked', false); alert('最多只能选择 ' + maxChoices + ' 个选项'); } }); <?php endif; ?> // 表单提交验证 $('#vote-form').submit(function(e) { var selectedCount = $('.option-checkbox:checked').length + ($('.option-radio:checked').length ? 1 : 0); if (selectedCount === 0) { e.preventDefault(); alert('请至少选择一个选项'); return false; } <?php if ($vote->vote_type === 'multiple'): ?> if (selectedCount > maxChoices) { e.preventDefault(); alert('最多只能选择 ' + maxChoices + ' 个选项'); return false; } <?php endif; ?> }); });</script><?php endif; ?> ### 3.3 创建决策讨论板界面 在`templates/`目录下创建`decision-board.php`: <?php/** 决策讨论板模板 */ if (!defined('ABSPATH')) { exit; } // 获取决策ID$decision_id = isset($_GET['decision_id']) ? intval($_GET['decision_id']) : 0;if (!$decision_id) { echo '<div class="team-vote-message">未指定决策讨论</div>'; return; } // 获取决策信息$decision_model = new TeamDecisionModel();$decision = $decision_model->get_decision($decision_id);if (!$decision) { echo '<div class="team-vote-message">决策讨论不存在</div>'; return; } // 获取讨论评论$comments = $decision_model->get_comments($decision_id); // 获取当前用户信息$current_user_id = get_current_user_id();$current_user = wp_get_current_user(); // 处理评论提交if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['submit_comment'])) { // 这里添加评论处理逻辑 }?> <div class="decision-container"> <div class="decision-header"> <h2><?php echo esc_html($decision->title); ?></h2> <div class="decision-meta"> <span class="decision-creator">发起人: <?php echo get_userdata($decision->creator_id)->display_name; ?></span> <span class="decision-date">创建时间: <?php echo date('Y-m-d H:i', strtotime($decision->created_at)); ?></span> <?php if ($decision->deadline): ?> <span class="decision-deadline">截止时间: <?php echo date('Y-m-d H:i', strtotime($decision->deadline)); ?></span> <?php endif; ?> <span class="decision-status status-<?php echo $decision->status; ?>"> <?php $status_labels = array( 'discussion' => '讨论中', 'voting' => '投票中', 'decided' => '已决定', 'closed' => '已关闭' ); echo $status_labels[$decision->status] ?? $decision->status; ?> </span> </div> <?php if ($decision->description): ?> <div class="decision-description"> <h3>决策背景</h3> <?php echo wpautop(esc_html($decision->description)); ?> </div> <?php endif; ?> <?php if ($decision->final_decision && $decision->status === 'decided'): ?> <div class="final-decision"> <h3>最终决定</h3> <div class="decision-content"> <?php echo wpautop(esc_html($decision->final_decision)); ?> </div> <div class="decision-meta"> 决定时间: <?php echo date('Y-m-d H:i', strtotime($decision->updated_at)); ?> </div> </div> <?php endif; ?> </div> <!-- 讨论区 --> <div class="discussion-section"> <h3>讨论区 (<?php echo count($comments); ?> 条评论)</h3> <?php if ($decision->status === 'discussion' || $decision->status === 'voting'): ?> <div class="comment-form"> <form method="POST" action=""> <?php wp_nonce_field('comment_action_' . $decision_id, 'comment_nonce'); ?> <div class="form-group"> <label for="comment_content">发表您的观点</label> <textarea id="comment_content" name="comment_content" rows="4" required placeholder="请详细阐述您的观点和建议..."></textarea> </div> <div class="form-group"> <label for="comment_sentiment">观点倾向</label> <select id="comment_sentiment" name="comment_sentiment"> <option value="support">支持</option> <option value="neutral" selected>中立</option> <option value="oppose">反对</option> <option value="question">疑问</option> <option value="suggestion">建议</option> </select> </div> <div class="form-submit"> <button type="submit" name="submit_comment" class="button-primary"> 发表评论 </button> </div> </form> </div> <?php endif; ?> <div class="comments-list"> <?php if (empty($comments)): ?> <p class="no-comments">暂无讨论,快来发表第一个观点吧!</p> <?php else: ?> <?php foreach ($comments as $comment): $comment_user = get_userdata($comment->user_id); $sentiment_classes = array( 'support' => 'sentiment-support', 'oppose' => 'sentiment-oppose', 'question' => 'sentiment-question', 'suggestion' => 'sentiment-suggestion', 'neutral' => 'sentiment-neutral' ); $sentiment_labels = array( 'support' => '支持', 'oppose' => '反对', 'question' => '疑问', 'suggestion' => '建议', 'neutral' => '中立' ); ?> <div class="comment-item <?php echo $sentiment_classes[$comment->sentiment] ?? ''; ?>"> <div class="comment-header"> <div class="comment-author"> <span class="author-avatar"> <?php echo get_avatar($comment->user_id, 40); ?> </span> <span class="author-name"><?php echo $comment_user->display_name; ?></span> <span class="comment-sentiment"> <?php echo $sentiment_labels[$comment->sentiment] ?? $comment->sentiment; ?> </span> </div> <div class="comment-date"> <?php echo date('Y-m-d H:i', strtotime($comment->created_at)); ?> </div> </div> <div class="comment-content"> <?php echo wpautop(esc_html($comment->comment)); ?> </div> </div> <?php endforeach; ?> <?php endif; ?> </div> </div> <!-- 相关投票(如果存在) --> <?php // 获取与决策相关的投票 $related_votes = $decision_model->get_related_votes($decision_id); if (!empty($related_votes)): ?> <div class="related-votes"> <h3>相关投票</h3> <div class="votes-list"> <?php foreach ($related_votes as $vote): ?> <div class="vote-summary"> <h4><?php echo esc_html($vote->title); ?></h4> <p><?php echo esc_html(wp_trim_words($vote->description, 20)); ?></p> <a href="?page=team_vote&vote_id=<?php echo $vote->id; ?>" class="button-small"> 查看投票 </a> </div> <?php endforeach; ?> </div> </div> <?php endif; ?> </div> <style>.sentiment-support { border-left: 4px solid #4CAF50; }.sentiment-oppose { border-left: 4px solid #F44336; }.sentiment-question { border-left: 4px solid #FF9800; }.sentiment-suggestion { border-left: 4px solid #2196F3; }.sentiment-neutral { border-left: 4px solid #9E9E9E; }</style> ## 第四部分:后端逻辑与AJAX处理 ### 4.1 表单处理与数据验证

发表评论

手把手教程,在WordPress中集成网站第三方服务API统一监控面板

手把手教程:在WordPress中集成网站第三方服务API统一监控面板 引言:为什么需要API统一监控面板? 在当今的互联网环境中,网站通常依赖于多个第三方服务API来增强功能、提升用户体验或实现特定业务需求。从支付网关、社交媒体集成、邮件服务到数据分析工具,这些API服务构成了现代网站的核心支撑体系。然而,随着集成服务的增多,管理这些API的状态、监控其性能和可用性变得越来越复杂。 想象一下这样的场景:您的电商网站集成了支付宝、微信支付、物流查询、库存管理、邮件通知等十几种API服务。当某个服务出现故障时,您可能需要逐一检查每个服务的状态,这不仅耗时耗力,而且可能延误问题的解决时间,直接影响用户体验和业务收入。 本教程将手把手教您如何在WordPress中创建一个统一的第三方服务API监控面板,通过代码二次开发实现常用互联网小工具功能,让您能够在一个界面中实时监控所有集成的API服务状态,快速定位问题,提高网站运维效率。 第一部分:准备工作与环境配置 1.1 确定监控需求与目标API 在开始开发之前,首先需要明确您要监控哪些第三方服务API。常见的监控目标包括: 支付网关:支付宝、微信支付、PayPal等 社交媒体:微信、微博、Facebook、Twitter等API 邮件服务:SMTP服务、SendGrid、MailChimp等 云存储:阿里云OSS、腾讯云COS、AWS S3等 地图服务:百度地图、高德地图、Google Maps等 短信服务:阿里云短信、腾讯云短信等 数据分析:Google Analytics、百度统计等 列出所有需要监控的API后,记录每个API的以下信息: API端点URL 认证方式(API密钥、OAuth等) 健康检查方法(专用健康检查端点或模拟实际请求) 预期响应时间和格式 1.2 创建WordPress插件框架 我们将创建一个独立的WordPress插件来实现API监控功能,这样可以确保代码的独立性和可维护性。 在WordPress的wp-content/plugins/目录下创建新文件夹api-monitor-dashboard 在该文件夹中创建主插件文件api-monitor-dashboard.php,并添加以下基础代码: <?php /** * Plugin Name: API统一监控面板 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress中集成第三方服务API统一监控面板 * Version: 1.0.0 * Author: 您的名字 * License: GPL v2 or later * Text Domain: api-monitor-dashboard */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('AMD_VERSION', '1.0.0'); define('AMD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('AMD_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once AMD_PLUGIN_DIR . 'includes/class-api-monitor-core.php'; function amd_init() { $api_monitor = new API_Monitor_Core(); $api_monitor->init(); } add_action('plugins_loaded', 'amd_init'); 1.3 创建数据库表结构 为了存储API监控的历史数据和配置,我们需要创建数据库表。在插件初始化时检查并创建必要的表结构。 在includes/class-api-monitor-core.php中添加数据库创建方法: class API_Monitor_Core { public function init() { // 注册激活钩子 register_activation_hook(__FILE__, array($this, 'activate_plugin')); // 注册管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 加载必要资源 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function activate_plugin() { $this->create_database_tables(); $this->insert_default_data(); } private function create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'api_monitor_services'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, service_name varchar(100) NOT NULL, api_endpoint varchar(500) NOT NULL, api_type varchar(50) NOT NULL, auth_method varchar(50) DEFAULT 'api_key', api_key varchar(500) DEFAULT '', check_interval int DEFAULT 300, last_check datetime DEFAULT '0000-00-00 00:00:00', last_status varchar(20) DEFAULT 'unknown', last_response_time float DEFAULT 0, uptime_percentage float DEFAULT 100, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, is_active tinyint(1) DEFAULT 1, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建监控日志表 $log_table_name = $wpdb->prefix . 'api_monitor_logs'; $sql_log = "CREATE TABLE IF NOT EXISTS $log_table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, service_id mediumint(9) NOT NULL, check_time datetime DEFAULT CURRENT_TIMESTAMP, status varchar(20) NOT NULL, response_time float NOT NULL, response_code int DEFAULT 0, error_message text, PRIMARY KEY (id), KEY service_id (service_id), KEY check_time (check_time) ) $charset_collate;"; dbDelta($sql_log); } private function insert_default_data() { // 可以在这里插入一些默认的API服务配置 } } 第二部分:构建API监控核心功能 2.1 设计API服务检查器 API监控的核心是能够定期检查各个服务的状态。我们将创建一个服务检查器类,负责执行API健康检查。 创建includes/class-api-checker.php: class API_Checker { private $wpdb; private $services_table; private $logs_table; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->services_table = $wpdb->prefix . 'api_monitor_services'; $this->logs_table = $wpdb->prefix . 'api_monitor_logs'; } /** * 检查所有活跃的API服务 */ public function check_all_services() { $services = $this->get_active_services(); foreach ($services as $service) { $this->check_single_service($service); } return count($services); } /** * 获取所有活跃的服务 */ private function get_active_services() { return $this->wpdb->get_results( "SELECT * FROM {$this->services_table} WHERE is_active = 1" ); } /** * 检查单个API服务 */ private function check_single_service($service) { $start_time = microtime(true); $status = 'unknown'; $response_code = 0; $error_message = ''; try { // 根据API类型选择不同的检查方法 switch ($service->api_type) { case 'http_get': $result = $this->check_http_get($service); break; case 'http_post': $result = $this->check_http_post($service); break; case 'oauth': $result = $this->check_oauth_service($service); break; default: $result = $this->check_http_get($service); } $status = $result['status']; $response_code = $result['response_code']; $error_message = $result['error_message'] ?? ''; } catch (Exception $e) { $status = 'error'; $error_message = $e->getMessage(); } $response_time = round((microtime(true) - $start_time) * 1000, 2); // 转换为毫秒 // 记录检查结果 $this->log_check_result($service->id, $status, $response_time, $response_code, $error_message); // 更新服务状态 $this->update_service_status($service->id, $status, $response_time); return array( 'service_id' => $service->id, 'service_name' => $service->service_name, 'status' => $status, 'response_time' => $response_time, 'response_code' => $response_code, 'error_message' => $error_message ); } /** * HTTP GET方式检查 */ private function check_http_get($service) { $args = array( 'timeout' => 10, 'headers' => $this->prepare_headers($service) ); $response = wp_remote_get($service->api_endpoint, $args); return $this->process_http_response($response); } /** * HTTP POST方式检查 */ private function check_http_post($service) { $args = array( 'timeout' => 10, 'headers' => $this->prepare_headers($service), 'body' => $this->prepare_body($service) ); $response = wp_remote_post($service->api_endpoint, $args); return $this->process_http_response($response); } /** * 准备请求头 */ private function prepare_headers($service) { $headers = array(); // 根据认证方式添加相应的请求头 switch ($service->auth_method) { case 'api_key': if (!empty($service->api_key)) { $headers['Authorization'] = 'Bearer ' . $service->api_key; } break; case 'basic_auth': // 处理基本认证 break; case 'oauth': // 处理OAuth认证 break; } $headers['User-Agent'] = 'WordPress API Monitor/1.0'; return $headers; } /** * 处理HTTP响应 */ private function process_http_response($response) { if (is_wp_error($response)) { return array( 'status' => 'error', 'response_code' => 0, 'error_message' => $response->get_error_message() ); } $response_code = wp_remote_retrieve_response_code($response); if ($response_code >= 200 && $response_code < 300) { $status = 'healthy'; } elseif ($response_code >= 400 && $response_code < 500) { $status = 'warning'; } else { $status = 'error'; } return array( 'status' => $status, 'response_code' => $response_code ); } /** * 记录检查结果到数据库 */ private function log_check_result($service_id, $status, $response_time, $response_code, $error_message) { $this->wpdb->insert( $this->logs_table, array( 'service_id' => $service_id, 'status' => $status, 'response_time' => $response_time, 'response_code' => $response_code, 'error_message' => $error_message ), array('%d', '%s', '%f', '%d', '%s') ); } /** * 更新服务状态 */ private function update_service_status($service_id, $status, $response_time) { // 计算正常运行时间百分比 $uptime_percentage = $this->calculate_uptime_percentage($service_id); $this->wpdb->update( $this->services_table, array( 'last_check' => current_time('mysql'), 'last_status' => $status, 'last_response_time' => $response_time, 'uptime_percentage' => $uptime_percentage ), array('id' => $service_id), array('%s', '%s', '%f', '%f'), array('%d') ); } /** * 计算正常运行时间百分比 */ private function calculate_uptime_percentage($service_id) { $total_checks = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT(*) FROM {$this->logs_table} WHERE service_id = %d AND check_time > DATE_SUB(NOW(), INTERVAL 7 DAY)", $service_id ) ); if ($total_checks == 0) { return 100; } $healthy_checks = $this->wpdb->get_var( $this->wpdb->prepare( "SELECT COUNT(*) FROM {$this->logs_table} WHERE service_id = %d AND status = 'healthy' AND check_time > DATE_SUB(NOW(), INTERVAL 7 DAY)", $service_id ) ); return round(($healthy_checks / $total_checks) * 100, 2); } } 2.2 实现定时检查功能 为了定期检查API状态,我们需要使用WordPress的定时任务功能。在class-api-monitor-core.php中添加定时任务管理: // 在API_Monitor_Core类中添加以下方法 public function init() { // ... 其他初始化代码 // 初始化定时任务 add_action('amd_check_apis_event', array($this, 'run_api_checks')); // 注册停用插件时的清理操作 register_deactivation_hook(__FILE__, array($this, 'deactivate_plugin')); } public function schedule_events() { if (!wp_next_scheduled('amd_check_apis_event')) { wp_schedule_event(time(), 'five_minutes', 'amd_check_apis_event'); } } public function deactivate_plugin() { // 清除定时任务 wp_clear_scheduled_hook('amd_check_apis_event'); } public function run_api_checks() { require_once AMD_PLUGIN_DIR . 'includes/class-api-checker.php'; $checker = new API_Checker(); $checked_count = $checker->check_all_services(); // 记录检查日志 error_log("API监控:已检查 {$checked_count} 个服务"); } // 添加自定义时间间隔 public function add_cron_interval($schedules) { $schedules['five_minutes'] = array( 'interval' => 300, 'display' => __('每5分钟', 'api-monitor-dashboard') ); $schedules['thirty_minutes'] = array( 'interval' => 1800, 'display' => __('每30分钟', 'api-monitor-dashboard') ); return $schedules; } 第三部分:创建监控管理界面 3.1 设计管理菜单和主面板 现在我们需要创建一个用户友好的管理界面来显示API监控状态。首先在WordPress后台添加菜单项: // 在API_Monitor_Core类中添加 public function add_admin_menu() { add_menu_page( 'API监控面板', 'API监控', 'manage_options', 'api-monitor-dashboard', array($this, 'render_dashboard_page'), 'dashicons-dashboard', 30 ); add_submenu_page( 'api-monitor-dashboard', 'API服务管理', '服务管理', 'manage_options', 'api-monitor-services', array($this, 'render_services_page') ); add_submenu_page( 'api-monitor-dashboard', '监控日志', '监控日志', 'manage_options', 'api-monitor-logs', array($this, 'render_logs_page') ); add_submenu_page( 'api-monitor-dashboard', '设置', '设置', 'manage_options', 'api-monitor-settings', array($this, 'render_settings_page') ); } 3.2 创建仪表板页面 创建templates/dashboard.php文件,用于显示API监控主面板: <div class="wrap"> <h1 class="wp-heading-inline">API监控面板</h1> <div class="amd-dashboard"> <!-- 概览统计 --> <div class="amd-overview-stats"> <div class="amd-stat-card"> <h3>总服务数</h3> <div class="amd-stat-value"><?php echo $total_services; ?></div> </div> <div class="amd-stat-card"> <h3>正常运行</h3> <div class="amd-stat-value amd-stat-healthy"><?php echo $healthy_services; ?></div> </div> <div class="amd-stat-card"> <h3>警告</h3> <div class="amd-stat-value amd-stat-warning"><?php echo $warning_services; ?></div> </div> <div class="amd-stat-card"> <h3>异常</h3> <div class="amd-stat-value amd-stat-error"><?php echo $error_services; ?></div> </div> <div class="amd-stat-card"> <h3>平均响应时间</h3> <div class="amd-stat-value"><?php echo $avg_response_time; ?>ms</div> </div> </div> <!-- API服务状态表格 --> <div class="amd-services-table"> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>服务名称</th> <th>类型</th> <th>状态</th> <th>最后检查</th> <th>响应时间</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach ($services as $service): ?> <tr> <td> <strong><?php echo esc_html($service->service_name); ?></strong><br> <small><?php echo esc_html($service->api_endpoint); ?></small> </td> <td><?php echo esc_html($this->get_api_type_label($service->api_type)); ?></td> <td> <?php echo $this->get_status_badge($service->last_status); ?> </td> <td> <?php if ($service->last_check != '0000-00-00 00:00:00') { echo human_time_diff(strtotime($service->last_check), current_time('timestamp')) . '前'; } else { echo '从未检查'; } ?> </td> <td> <?php $response_time_class = ''; if ($service->last_response_time > 1000) { $response_time_class = 'amd-response-slow'; } elseif ($service->last_response_time > 500) { $response_time_class = 'amd-response-medium'; } else { $response_time_class = 'amd-response-fast'; } ?> <span class="<?php echo $response_time_class; ?>"> <?php echo number_format($service->last_response_time, 2); ?>ms </span> </td> <td> <div class="amd-uptime-bar"> <div class="amd-uptime-fill" style="width: <?php echo $service->uptime_percentage; ?>%"></div> <span class="amd-uptime-text"><?php echo $service->uptime_percentage; ?>%</span> </div> </td> <td> <button class="button button-small amd-check-now" data-service-id="<?php echo $service->id; ?>"> 立即检查 </button> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=edit&id=' . $service->id); ?>" class="button button-small"> 编辑 </a> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> <!-- 响应时间趋势图 --> <div class="amd-chart-container"> <h3>最近24小时响应时间趋势</h3> <canvas id="amd-response-time-chart" width="800" height="200"></canvas> </div> <!-- 状态分布图 --> <div class="amd-status-distribution"> <h3>服务状态分布</h3> <div class="amd-status-chart-container"> <canvas id="amd-status-chart" width="400" height="400"></canvas> </div> </div> </div> </div> ### 3.3 创建服务管理页面 创建`templates/services.php`,用于管理API服务配置: <div class="wrap"> <h1 class="wp-heading-inline">API服务管理</h1> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=add'); ?>" class="page-title-action">添加新服务</a> <?php if ($action == 'list'): ?> <div class="tablenav top"> <div class="alignleft actions"> <select name="bulk-action"> <option value="-1">批量操作</option> <option value="activate">启用</option> <option value="deactivate">停用</option> <option value="delete">删除</option> </select> <input type="submit" class="button action" value="应用"> </div> <div class="tablenav-pages"> <span class="displaying-num"><?php echo $total_items; ?>个项目</span> </div> </div> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <td class="manage-column column-cb check-column"> <input type="checkbox"> </td> <th>服务名称</th> <th>API端点</th> <th>类型</th> <th>状态</th> <th>检查间隔</th> <th>最后检查</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach ($services as $service): ?> <tr> <th scope="row" class="check-column"> <input type="checkbox" name="service_ids[]" value="<?php echo $service->id; ?>"> </th> <td> <strong> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=edit&id=' . $service->id); ?>"> <?php echo esc_html($service->service_name); ?> </a> </strong> </td> <td><?php echo esc_html($this->truncate_string($service->api_endpoint, 50)); ?></td> <td><?php echo esc_html($service->api_type); ?></td> <td> <?php if ($service->is_active): ?> <span class="amd-status-active">已启用</span> <?php else: ?> <span class="amd-status-inactive">已停用</span> <?php endif; ?> </td> <td><?php echo $this->format_interval($service->check_interval); ?></td> <td> <?php if ($service->last_check != '0000-00-00 00:00:00') { echo date('Y-m-d H:i:s', strtotime($service->last_check)); } else { echo '从未检查'; } ?> </td> <td> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=edit&id=' . $service->id); ?>">编辑</a> | <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=delete&id=' . $service->id); ?>" onclick="return confirm('确定要删除这个服务吗?')">删除</a> | <?php if ($service->is_active): ?> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=deactivate&id=' . $service->id); ?>">停用</a> <?php else: ?> <a href="<?php echo admin_url('admin.php?page=api-monitor-services&action=activate&id=' . $service->id); ?>">启用</a> <?php endif; ?> </td> </tr> <?php endforeach; ?> </tbody> </table> <?php elseif ($action == 'add' || $action == 'edit'): ?> <form method="post" action=""> <?php wp_nonce_field('amd_save_service', 'amd_service_nonce'); ?> <table class="form-table"> <tr> <th scope="row"><label for="service_name">服务名称</label></th> <td> <input type="text" id="service_name" name="service_name" value="<?php echo esc_attr($service_data['service_name'] ?? ''); ?>" class="regular-text" required> <p class="description">显示在监控面板中的服务名称</p> </td> </tr> <tr> <th scope="row"><label for="api_endpoint">API端点</label></th> <td> <input type="url" id="api_endpoint" name="api_endpoint" value="<?php echo esc_attr($service_data['api_endpoint'] ?? ''); ?>" class="regular-text" required> <p class="description">API的完整URL地址</p> </td> </tr> <tr> <th scope="row"><label for="api_type">API类型</label></th> <td> <select id="api_type" name="api_type" class="regular-text"> <option value="http_get" <?php selected($service_data['api_type'] ?? '', 'http_get'); ?>>HTTP GET</option> <option value="http_post" <?php selected($service_data['api_type'] ?? '', 'http_post'); ?>>HTTP POST</option> <option value="oauth" <?php selected($service_data['api_type'] ?? '', 'oauth'); ?>>OAuth</option> <option value="websocket" <?php selected($service_data['api_type'] ?? '', 'websocket'); ?>>WebSocket</option> <option value="custom" <?php selected($service_data['api_type'] ?? '', 'custom'); ?>>自定义</option> </select> </td> </tr> <tr> <th scope="row"><label for="auth_method">认证方式</label></th> <td> <select id="auth_method" name="auth_method" class="regular-text"> <option value="none" <?php selected($service_data['auth_method'] ?? '', 'none'); ?>>无需认证</option> <option value="api_key" <?php selected($service_data['auth_method'] ?? '', 'api_key'); ?>>API密钥</option> <option value="basic_auth" <?php selected($service_data['auth_method'] ?? '', 'basic_auth'); ?>>基本认证</option> <option value="oauth" <?php selected($service_data['auth_method'] ?? '', 'oauth'); ?>>OAuth 2.0</option> </select> </td> </tr> <tr class="auth-field" id="api_key_field"> <th scope="row"><label for="api_key">API密钥</label></th> <td> <input type="password" id="api_key" name="api_key" value="<?php echo esc_attr($service_data['api_key'] ?? ''); ?>" class="regular-text"> <button type="button" class="button button-small toggle-password">显示/隐藏</button> <p class="description">API访问密钥,将安全存储</p> </td> </tr> <tr> <th scope="row"><label for="check_interval">检查间隔</label></th> <td> <select id="check_interval" name="check_interval" class="regular-text"> <option value="60" <?php selected($service_data['check_interval'] ?? 300, 60); ?>>1分钟</option> <option value="300" <?php selected($service_data['check_interval'] ?? 300, 300); ?>>5分钟</option> <option value="900" <?php selected($service_data['check_interval'] ?? 300, 900); ?>>15分钟</option> <option value="1800" <?php selected($service_data['check_interval'] ?? 300, 1800); ?>>30分钟</option> <option value="3600" <?php selected($service_data['check_interval'] ?? 300, 3600); ?>>1小时</option> </select> </td> </tr> <tr> <th scope="row"><label for="expected_response">预期响应</label></th> <td> <textarea id="expected_response" name="expected_response" rows="3" class="large-text"><?php echo esc_textarea($service_data['expected_response'] ?? ''); ?></textarea> <p class="description">可选的预期响应内容(用于验证API返回正确数据)</p> </td> </tr> <tr> <th scope="row"><label for="is_active">状态</label></th> <td> <label> <input type="checkbox" id="is_active" name="is_active" value="1" <?php checked($service_data['is_active'] ?? 1, 1); ?>> 启用此服务的监控 </label> </td> </tr> </table> <p class="submit"> <input type="submit" name="submit" class="button button-primary" value="保存服务"> <a href="<?php echo admin_url('admin.php?page=api-monitor-services'); ?>" class="button">取消</a> </p> </form> <?php endif; ?> </div> ## 第四部分:增强功能与互联网小工具集成 ### 4.1 实现实时通知功能 为了让管理员及时了解API状态变化,我们需要实现通知功能。创建`includes/class-notification-manager.php`: class Notification_Manager { private $notification_methods = array(); public function __construct() { // 初始化通知方法 $this->notification_methods = array( 'email' => array($this, 'send_email_notification'), 'slack' => array($this, 'send_slack_notification'), 'webhook' => array($this, 'send_webhook_notification'), 'sms' => array($this, 'send_sms_notification') ); } /** * 发送状态变化通知 */ public function send_status_change_notification($service_id, $old_status, $new_status, $service_data) { $notification_settings = get_option('amd_notification_settings', array()); // 检查是否需要发送通知 if (!$this->should_send_notification($old_status, $new_status, $notification_settings)) { return; } // 准备通知内容 $message = $this->prepare_notification_message($service_data, $old_status, $new_status); // 发送到所有启用的通知渠道 foreach ($notification_settings['channels'] as $channel => $enabled) { if ($enabled && isset($this->notification_methods[$channel])) { call_user_func($this->notification_methods[$channel], $message, $service_data); } } } /** * 准备通知消息 */ private function prepare_notification_message($service_data, $old_status, $new_status) { $site_name = get_bloginfo('name'); $timestamp = current_time('mysql'); $status_labels = array( 'healthy' => '正常', 'warning' => '警告', 'error' => '异常', 'unknown' => '未知' ); $message = "【{$site_name} API监控通知】nn"; $message .= "服务名称:{$service_data['service_name']}n"; $message .= "API端点:{$service_data['api_endpoint']}n"; $message .= "状态变化:{$status_labels[$old_status]} → {$status_labels[$new_status]}n"; $message .= "发生时间:{$timestamp}n"; if ($new_status == 'error' && !empty($service_data['error_message'])) { $message .= "错误信息:{$service_data['error_message']}n"; } $message .= "n请及时处理。"; return $message; } /** * 发送邮件通知 */ private function send_email_notification($message, $service_data) { $settings = get_option('amd_notification_settings', array()); $to = $settings['email_recipients'] ?? get_option('admin_email'); $subject = "API监控告警:{$service_data['service_name']} 状态异常"; wp_mail($to, $subject, $message); } /** * 发送Slack通知 */ private function send_slack_notification($message, $service_data) { $settings = get_option('amd_notification_settings', array()); if (empty($settings['slack_webhook_url'])) { return; } $payload = array( 'text' => $message, 'username' => 'API监控机器人', 'icon_emoji' => ':warning:', 'attachments' => array( array( 'color' => $this->get_status_color($service_data['last_status']), 'fields' => array( array( 'title' => '服务名称', 'value' => $service_data['service_name'], 'short' => true ), array( 'title' => '当前状态', 'value' => $service_data['last_status'], 'short' => true ) ) ) ) ); $args = array( 'body' => json_encode($payload), 'headers' => array('Content-Type' => 'application/json'), 'timeout' => 5 ); wp_remote_post($settings['slack_webhook_url'], $args); } /** * 获取状态对应的颜色 */ private function get_status_color($status) { $colors = array( 'healthy' => '#36a64f', 'warning' => '#ffcc00', 'error' => '#ff0000', 'unknown' => '#cccccc' ); return $colors[$status] ?? '#cccccc'; } } ### 4.2 集成常用互联网小工具 #### 4.2.1 天气信息小工具 创建`includes/widgets/class-weather-widget.php`: class Weather_Widget extends WP_Widget { public function __construct() { parent::__construct( 'amd_weather_widget', 'API监控 - 天气信息', array('description' => '显示当前天气信息') ); } public function widget($args, $instance) { $weather_data = $this->get_weather_data($instance['city']); echo $args['before_widget']; if (!empty($instance['title'])) { echo $args['before_title'] . apply_filters('widget_title', $instance['title']) . $args['after_title']; } if ($weather_data) { echo '<div class="amd

发表评论

详细教程,为网站打造内嵌的在线简易视频字幕生成与编辑工具

详细教程:为WordPress网站打造内嵌在线简易视频字幕生成与编辑工具 引言:为什么网站需要内置视频字幕工具? 在当今多媒体内容主导的互联网环境中,视频已成为网站吸引和保留用户的重要手段。然而,许多网站运营者面临一个共同挑战:如何高效地为视频内容添加字幕。字幕不仅有助于听力障碍用户访问内容,还能提高视频在静音环境下的观看率,对SEO优化也有显著帮助。 传统上,为视频添加字幕需要依赖第三方工具或专业软件,过程繁琐且耗时。本文将详细介绍如何通过WordPress代码二次开发,为您的网站打造一个内嵌的在线简易视频字幕生成与编辑工具,让字幕创建变得简单高效。 第一部分:项目规划与技术选型 1.1 功能需求分析 在开始开发之前,我们需要明确工具的核心功能: 视频上传与预览:支持常见视频格式上传和实时预览 自动语音识别(ASR):将视频中的语音转换为文字 字幕编辑界面:直观的时间轴编辑功能 字幕格式支持:至少支持SRT、VTT等常用字幕格式 实时预览:编辑字幕时可实时查看效果 导出与集成:将生成的字幕与视频关联或单独导出 1.2 技术架构设计 我们将采用以下技术栈: 前端:HTML5、CSS3、JavaScript(使用Vue.js框架) 视频处理:HTML5 Video API + FFmpeg.wasm(浏览器端处理) 语音识别:Web Speech API(免费基础方案)或集成第三方API(如Google Cloud Speech-to-Text) 后端:WordPress PHP环境,使用REST API处理文件操作 存储:WordPress媒体库 + 自定义数据库表存储字幕数据 1.3 开发环境准备 确保您的WordPress开发环境满足以下条件: WordPress 5.0+(支持REST API) PHP 7.4+(支持最新语法特性) 至少256MB内存限制(用于处理视频文件) 启用文件上传功能(支持视频格式) 安装并启用必要的开发插件(如Query Monitor、Debug Bar) 第二部分:创建WordPress插件框架 2.1 初始化插件结构 首先,在WordPress的wp-content/plugins/目录下创建新文件夹video-subtitle-tool,并创建以下基础文件结构: video-subtitle-tool/ ├── video-subtitle-tool.php # 主插件文件 ├── includes/ │ ├── class-database.php # 数据库处理类 │ ├── class-video-processor.php # 视频处理类 │ ├── class-subtitle-generator.php # 字幕生成类 │ └── class-api-handler.php # REST API处理类 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 后台样式 │ ├── js/ │ │ └── admin-script.js # 后台脚本 │ └── admin-page.php # 管理页面 ├── public/ │ ├── css/ │ │ └── public-style.css # 前端样式 │ ├── js/ │ │ ├── app.js # 主Vue应用 │ │ ├── video-processor.js # 视频处理逻辑 │ │ └── subtitle-editor.js # 字幕编辑器 │ └── shortcode.php # 短代码处理 ├── assets/ │ ├── ffmpeg/ │ │ └── ffmpeg-core.js # FFmpeg.wasm核心文件 │ └── icons/ # 图标资源 └── templates/ └── subtitle-editor.php # 编辑器模板 2.2 主插件文件配置 编辑video-subtitle-tool.php,添加插件基本信息: <?php /** * Plugin Name: 视频字幕生成与编辑工具 * Plugin URI: https://yourwebsite.com/video-subtitle-tool * Description: 为WordPress网站添加内嵌的在线视频字幕生成与编辑功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: video-subtitle-tool */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('VST_VERSION', '1.0.0'); define('VST_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('VST_PLUGIN_URL', plugin_dir_url(__FILE__)); define('VST_MAX_FILE_SIZE', 500 * 1024 * 1024); // 500MB限制 // 自动加载类文件 spl_autoload_register(function ($class) { $prefix = 'VST_'; $base_dir = VST_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { return; } $relative_class = substr($class, $len); $file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function vst_init() { // 检查依赖 if (!function_exists('wp_get_current_user')) { require_once(ABSPATH . 'wp-includes/pluggable.php'); } // 初始化数据库 VST_Database::init(); // 注册短代码 add_shortcode('video_subtitle_editor', array('VST_Shortcode', 'render_editor')); // 初始化REST API add_action('rest_api_init', array('VST_API_Handler', 'register_routes')); // 加载文本域 load_plugin_textdomain('video-subtitle-tool', false, dirname(plugin_basename(__FILE__)) . '/languages/'); } add_action('plugins_loaded', 'vst_init'); // 激活插件时执行 function vst_activate() { require_once VST_PLUGIN_DIR . 'includes/class-database.php'; VST_Database::create_tables(); // 设置默认选项 add_option('vst_default_language', 'zh-CN'); add_option('vst_max_video_duration', 3600); // 默认最大1小时 add_option('vst_enable_auto_generation', true); } register_activation_hook(__FILE__, 'vst_activate'); // 停用插件时清理 function vst_deactivate() { // 清理临时文件 VST_Video_Processor::cleanup_temp_files(); } register_deactivation_hook(__FILE__, 'vst_deactivate'); 第三部分:数据库设计与实现 3.1 创建数据库表 编辑includes/class-database.php: <?php class VST_Database { public static function init() { // 数据库版本 $db_version = '1.0'; $current_version = get_option('vst_db_version', '0'); if ($current_version !== $db_version) { self::create_tables(); update_option('vst_db_version', $db_version); } } public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix . 'vst_'; // 视频项目表 $projects_table = $table_prefix . 'projects'; $projects_sql = "CREATE TABLE IF NOT EXISTS $projects_table ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, title varchar(255) NOT NULL, video_url varchar(1000) NOT NULL, video_duration int(11) DEFAULT 0, language varchar(10) DEFAULT 'zh-CN', 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 user_id (user_id), KEY status (status) ) $charset_collate;"; // 字幕表 $subtitles_table = $table_prefix . 'subtitles'; $subtitles_sql = "CREATE TABLE IF NOT EXISTS $subtitles_table ( id bigint(20) NOT NULL AUTO_INCREMENT, project_id bigint(20) NOT NULL, start_time int(11) NOT NULL, end_time int(11) NOT NULL, text text NOT NULL, confidence float DEFAULT 1.0, is_edited tinyint(1) DEFAULT 0, sequence int(11) NOT NULL, PRIMARY KEY (id), KEY project_id (project_id), KEY sequence (sequence) ) $charset_collate;"; // 项目设置表 $settings_table = $table_prefix . 'project_settings'; $settings_sql = "CREATE TABLE IF NOT EXISTS $settings_table ( project_id bigint(20) NOT NULL, font_family varchar(100) DEFAULT 'Arial', font_size int(11) DEFAULT 24, font_color varchar(7) DEFAULT '#FFFFFF', background_color varchar(7) DEFAULT '#00000080', position varchar(20) DEFAULT 'bottom', max_lines int(11) DEFAULT 2, UNIQUE KEY project_id (project_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($projects_sql); dbDelta($subtitles_sql); dbDelta($settings_sql); } public static function cleanup_temp_files() { // 清理超过24小时的临时文件 $temp_dir = wp_upload_dir()['basedir'] . '/vst_temp/'; if (is_dir($temp_dir)) { $files = glob($temp_dir . '*'); $now = time(); foreach ($files as $file) { if (is_file($file)) { if ($now - filemtime($file) >= 86400) { // 24小时 unlink($file); } } } } } } 第四部分:视频处理与语音识别模块 4.1 视频处理类实现 创建includes/class-video-processor.php: <?php class VST_Video_Processor { /** * 处理上传的视频文件 */ public static function process_upload($file) { // 安全检查 if (!self::validate_file($file)) { return new WP_Error('invalid_file', '无效的视频文件'); } // 创建临时目录 $upload_dir = wp_upload_dir(); $temp_dir = $upload_dir['basedir'] . '/vst_temp/'; if (!is_dir($temp_dir)) { wp_mkdir_p($temp_dir); } // 生成唯一文件名 $file_name = sanitize_file_name($file['name']); $unique_name = wp_unique_filename($temp_dir, $file_name); $temp_path = $temp_dir . $unique_name; // 移动文件 if (!move_uploaded_file($file['tmp_name'], $temp_path)) { return new WP_Error('upload_failed', '文件上传失败'); } // 获取视频信息 $video_info = self::get_video_info($temp_path); return array( 'path' => $temp_path, 'url' => $upload_dir['baseurl'] . '/vst_temp/' . $unique_name, 'name' => $file_name, 'info' => $video_info ); } /** * 验证文件 */ private static function validate_file($file) { $allowed_types = array('mp4', 'avi', 'mov', 'wmv', 'flv', 'webm'); $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); // 检查文件类型 if (!in_array($file_ext, $allowed_types)) { return false; } // 检查文件大小 $max_size = get_option('vst_max_file_size', VST_MAX_FILE_SIZE); if ($file['size'] > $max_size) { return false; } // 检查MIME类型 $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime_type = finfo_file($finfo, $file['tmp_name']); finfo_close($finfo); $allowed_mimes = array( 'video/mp4', 'video/x-msvideo', 'video/quicktime', 'video/x-ms-wmv', 'video/x-flv', 'video/webm' ); return in_array($mime_type, $allowed_mimes); } /** * 获取视频信息 */ private static function get_video_info($file_path) { // 使用FFprobe获取视频信息(需要服务器安装FFmpeg) if (function_exists('shell_exec')) { $cmd = "ffprobe -v quiet -print_format json -show_format -show_streams " . escapeshellarg($file_path); $output = shell_exec($cmd); if ($output) { $info = json_decode($output, true); if ($info) { return array( 'duration' => floatval($info['format']['duration']), 'width' => intval($info['streams'][0]['width']), 'height' => intval($info['streams'][0]['height']), 'bitrate' => intval($info['format']['bit_rate']), 'format' => $info['format']['format_name'] ); } } } // 备用方案:使用PHP的getid3库 if (!class_exists('getID3')) { require_once(ABSPATH . 'wp-admin/includes/media.php'); } $id3 = new getID3(); $file_info = $id3->analyze($file_path); return array( 'duration' => isset($file_info['playtime_seconds']) ? $file_info['playtime_seconds'] : 0, 'width' => isset($file_info['video']['resolution_x']) ? $file_info['video']['resolution_x'] : 0, 'height' => isset($file_info['video']['resolution_y']) ? $file_info['video']['resolution_y'] : 0, 'bitrate' => isset($file_info['bitrate']) ? $file_info['bitrate'] : 0, 'format' => isset($file_info['fileformat']) ? $file_info['fileformat'] : 'unknown' ); } /** * 提取音频用于语音识别 */ public static function extract_audio($video_path, $output_path = null) { if (!$output_path) { $upload_dir = wp_upload_dir(); $output_path = $upload_dir['basedir'] . '/vst_temp/audio_' . uniqid() . '.wav'; } // 使用FFmpeg提取音频 $cmd = "ffmpeg -i " . escapeshellarg($video_path) . " -ac 1 -ar 16000 -vn " . escapeshellarg($output_path) . " 2>&1"; exec($cmd, $output, $return_code); if ($return_code === 0 && file_exists($output_path)) { return $output_path; } return false; } } 4.2 语音识别集成 创建includes/class-subtitle-generator.php: <?php class VST_Subtitle_Generator { /** * 使用Web Speech API进行语音识别(客户端方案) */ public static function get_browser_recognition_code() { // 返回客户端JavaScript代码 return ' <script> class SpeechRecognitionTool { constructor() { this.recognition = null; this.isRecording = false; this.transcript = ""; this.interimResults = []; this.init(); } init() { const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SpeechRecognition) { throw new Error("浏览器不支持语音识别API"); } this.recognition = new SpeechRecognition(); this.recognition.continuous = true; this.recognition.interimResults = true; this.recognition.lang = "zh-CN"; this.recognition.onresult = (event) => { let interimTranscript = ""; let finalTranscript = ""; for (let i = event.resultIndex; i < event.results.length; i++) { const transcript = event.results[i][0].transcript; if (event.results[i].isFinal) { finalTranscript += transcript; } else { interimTranscript += transcript; } } this.interimResults = interimTranscript; this.transcript += finalTranscript; // 触发自定义事件 const transcriptEvent = new CustomEvent("transcriptUpdate", { detail: { final: finalTranscript, interim: interimTranscript, full: this.transcript } }); document.dispatchEvent(transcriptEvent); }; this.recognition.onerror = (event) => { console.error("语音识别错误:", event.error); }; } start() { if (this.recognition && !this.isRecording) { this.recognition.start(); this.isRecording = true; } } stop() { if (this.recognition && this.isRecording) { this.recognition.stop(); this.isRecording = false; } } 第四部分:视频处理与语音识别模块(续) 4.2 语音识别集成(续) setLanguage(lang) { if (this.recognition) { this.recognition.lang = lang; } } getTranscript() { return this.transcript; } reset() { this.transcript = ""; this.interimResults = ""; } } // 导出到全局 window.VSTSpeechRecognition = SpeechRecognitionTool; </script> '; } /** * 使用第三方API进行语音识别(服务器端方案) */ public static function recognize_with_api($audio_path, $language = 'zh-CN') { $api_type = get_option('vst_speech_api', 'google'); switch ($api_type) { case 'google': return self::google_speech_recognition($audio_path, $language); case 'azure': return self::azure_speech_recognition($audio_path, $language); case 'baidu': return self::baidu_speech_recognition($audio_path, $language); default: return new WP_Error('unsupported_api', '不支持的语音识别API'); } } /** * Google Cloud Speech-to-Text */ private static function google_speech_recognition($audio_path, $language) { $api_key = get_option('vst_google_api_key', ''); if (empty($api_key)) { return new WP_Error('missing_api_key', '未配置Google API密钥'); } // 读取音频文件 $audio_content = file_get_contents($audio_path); $base64_audio = base64_encode($audio_content); $url = "https://speech.googleapis.com/v1/speech:recognize?key=" . $api_key; $data = array( 'config' => array( 'encoding' => 'LINEAR16', 'sampleRateHertz' => 16000, 'languageCode' => $language, 'enableWordTimeOffsets' => true, 'enableAutomaticPunctuation' => true ), 'audio' => array( 'content' => $base64_audio ) ); $response = wp_remote_post($url, array( 'headers' => array('Content-Type' => 'application/json'), 'body' => json_encode($data), 'timeout' => 60 )); if (is_wp_error($response)) { return $response; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['error'])) { return new WP_Error('api_error', $body['error']['message']); } return self::format_recognition_results($body['results'], 'google'); } /** * 格式化识别结果 */ private static function format_recognition_results($results, $provider) { $subtitles = array(); $current_time = 0; foreach ($results as $result) { if (!isset($result['alternatives'][0])) { continue; } $alternative = $result['alternatives'][0]; $text = $alternative['transcript']; if ($provider === 'google' && isset($alternative['words'])) { // 基于词语时间戳分割字幕 $words = $alternative['words']; $chunks = self::split_into_subtitles($words); foreach ($chunks as $chunk) { $subtitles[] = array( 'start' => $chunk['start'], 'end' => $chunk['end'], 'text' => $chunk['text'], 'confidence' => $alternative['confidence'] ?? 0.8 ); } } else { // 简单分割:每5秒一句 $duration = 5; // 每句5秒 $sentences = self::split_text_into_sentences($text); foreach ($sentences as $sentence) { $subtitles[] = array( 'start' => $current_time, 'end' => $current_time + $duration, 'text' => $sentence, 'confidence' => 0.7 ); $current_time += $duration; } } } return $subtitles; } /** * 将词语分割成字幕块 */ private static function split_into_subtitles($words, $max_chars = 40) { $chunks = array(); $current_chunk = array( 'text' => '', 'start' => null, 'end' => null, 'word_count' => 0 ); foreach ($words as $word) { $word_text = $word['word']; $word_start = floatval($word['startTime']['seconds']) + (floatval($word['startTime']['nanos'] ?? 0) / 1000000000); $word_end = floatval($word['endTime']['seconds']) + (floatval($word['endTime']['nanos'] ?? 0) / 1000000000); // 如果当前块为空,设置开始时间 if ($current_chunk['start'] === null) { $current_chunk['start'] = $word_start; } // 检查是否应该开始新块 if (strlen($current_chunk['text'] . ' ' . $word_text) > $max_chars || $current_chunk['word_count'] >= 8) { // 保存当前块 if (!empty($current_chunk['text'])) { $current_chunk['end'] = $word_start; // 使用下一个词的开始时间作为结束 $chunks[] = $current_chunk; } // 开始新块 $current_chunk = array( 'text' => $word_text, 'start' => $word_start, 'end' => $word_end, 'word_count' => 1 ); } else { // 添加到当前块 $current_chunk['text'] .= (empty($current_chunk['text']) ? '' : ' ') . $word_text; $current_chunk['end'] = $word_end; $current_chunk['word_count']++; } } // 添加最后一个块 if (!empty($current_chunk['text'])) { $chunks[] = $current_chunk; } return $chunks; } /** * 将文本分割成句子 */ private static function split_text_into_sentences($text) { // 简单的句子分割逻辑 $sentences = preg_split('/(?<=[。!?.!?])/u', $text, -1, PREG_SPLIT_NO_EMPTY); return array_filter(array_map('trim', $sentences)); } } 第五部分:前端编辑器开发 5.1 编辑器HTML结构 创建templates/subtitle-editor.php: <div id="vst-editor-app" class="vst-editor-container"> <!-- 顶部工具栏 --> <div class="vst-toolbar"> <div class="vst-toolbar-left"> <button class="vst-btn vst-btn-primary" @click="uploadVideo"> <i class="vst-icon-upload"></i> 上传视频 </button> <button class="vst-btn" :class="{'vst-btn-active': isRecording}" @click="toggleRecording"> <i class="vst-icon-mic"></i> {{ isRecording ? '停止识别' : '开始识别' }} </button> <select v-model="selectedLanguage" class="vst-select"> <option value="zh-CN">中文(简体)</option> <option value="zh-TW">中文(繁体)</option> <option value="en-US">English</option> <option value="ja-JP">日本語</option> <option value="ko-KR">한국어</option> </select> </div> <div class="vst-toolbar-right"> <button class="vst-btn" @click="exportSubtitles('srt')"> <i class="vst-icon-download"></i> 导出SRT </button> <button class="vst-btn" @click="exportSubtitles('vtt')"> <i class="vst-icon-download"></i> 导出VTT </button> <button class="vst-btn vst-btn-success" @click="saveProject"> <i class="vst-icon-save"></i> 保存项目 </button> </div> </div> <!-- 主编辑区 --> <div class="vst-main-editor"> <!-- 视频播放器 --> <div class="vst-video-container"> <video ref="videoPlayer" :src="videoUrl" controls @timeupdate="updateCurrentTime" @loadedmetadata="onVideoLoaded" ></video> <div class="vst-video-overlay" v-if="subtitles.length > 0"> <div class="vst-subtitle-display" :style="subtitleStyle"> {{ currentSubtitleText }} </div> </div> </div> <!-- 字幕编辑区 --> <div class="vst-subtitle-editor"> <div class="vst-subtitle-header"> <h3>字幕编辑</h3> <div class="vst-subtitle-actions"> <button class="vst-btn vst-btn-small" @click="addSubtitle"> <i class="vst-icon-add"></i> 添加字幕 </button> <button class="vst-btn vst-btn-small" @click="autoSync"> <i class="vst-icon-sync"></i> 自动同步 </button> </div> </div> <!-- 字幕列表 --> <div class="vst-subtitle-list"> <div v-for="(subtitle, index) in subtitles" :key="index" class="vst-subtitle-item" :class="{'vst-subtitle-active': currentSubtitleIndex === index}" @click="selectSubtitle(index)" > <div class="vst-subtitle-index">{{ index + 1 }}</div> <div class="vst-subtitle-time"> <input type="number" v-model="subtitle.start" step="0.1" @change="updateSubtitleTime(index, 'start', $event)" > <span> → </span> <input type="number" v-model="subtitle.end" step="0.1" @change="updateSubtitleTime(index, 'end', $event)" > </div> <div class="vst-subtitle-text"> <textarea v-model="subtitle.text" @input="updateSubtitleText(index, $event)" rows="2" ></textarea> </div> <div class="vst-subtitle-actions"> <button class="vst-btn-icon" @click="playSegment(index)"> <i class="vst-icon-play"></i> </button> <button class="vst-btn-icon" @click="removeSubtitle(index)"> <i class="vst-icon-delete"></i> </button> </div> </div> </div> <!-- 时间轴编辑器 --> <div class="vst-timeline-editor"> <div class="vst-timeline-header"> <span>时间轴</span> <span class="vst-current-time">{{ formatTime(currentTime) }}</span> </div> <div class="vst-timeline-container" ref="timeline"> <div v-for="(subtitle, index) in subtitles" :key="'timeline-' + index" class="vst-timeline-segment" :style="{ left: (subtitle.start / videoDuration * 100) + '%', width: ((subtitle.end - subtitle.start) / videoDuration * 100) + '%' }" @mousedown="startDrag(index, $event)" > <div class="vst-timeline-label">{{ index + 1 }}</div> </div> <div class="vst-timeline-cursor" :style="{ left: (currentTime / videoDuration * 100) + '%' }" ></div> </div> </div> </div> </div> <!-- 设置面板 --> <div class="vst-settings-panel" v-if="showSettings"> <div class="vst-settings-header"> <h3>字幕设置</h3> <button class="vst-btn-icon" @click="showSettings = false"> <i class="vst-icon-close"></i> </button> </div> <div class="vst-settings-content"> <div class="vst-setting-item"> <label>字体</label> <select v-model="settings.fontFamily"> <option value="Arial">Arial</option> <option value="Microsoft YaHei">微软雅黑</option> <option value="SimSun">宋体</option> <option value="SimHei">黑体</option> </select> </div> <div class="vst-setting-item"> <label>字体大小</label> <input type="range" v-model="settings.fontSize" min="12" max="48"> <span>{{ settings.fontSize }}px</span> </div> <div class="vst-setting-item"> <label>字体颜色</label> <input type="color" v-model="settings.fontColor"> </div> <div class="vst-setting-item"> <label>背景颜色</label> <input type="color" v-model="settings.backgroundColor"> <input type="range" v-model="settings.backgroundOpacity" min="0" max="100"> <span>{{ settings.backgroundOpacity }}%</span> </div> <div class="vst-setting-item"> <label>位置</label> <select v-model="settings.position"> <option value="bottom">底部</option> <option value="top">顶部</option> <option value="middle">中间</option> </select> </div> </div> </div> <!-- 上传模态框 --> <div class="vst-modal" v-if="showUploadModal"> <div class="vst-modal-content"> <div class="vst-modal-header"> <h3>上传视频</h3> <button class="vst-btn-icon" @click="showUploadModal = false"> <i class="vst-icon-close"></i> </button> </div> <div class="vst-modal-body"> <div class="vst-upload-area" @dragover.prevent="onDragOver" @dragleave.prevent="onDragLeave" @drop.prevent="onDrop" :class="{'vst-upload-dragover': isDragOver}" > <i class="vst-icon-upload-large"></i> <p>拖放视频文件到这里,或</p> <input type="file" ref="fileInput" accept="video/*" @change="onFileSelected" hidden > <button class="vst-btn" @click="$refs.fileInput.click()"> 选择文件 </button> <p class="vst-upload-hint">支持 MP4, AVI, MOV, WMV, FLV, WebM 格式,最大500MB</p> </div> <div v-if="uploadProgress > 0" class="vst-upload-progress"> <div class="vst-progress-bar"> <div class="vst-progress-fill" :style="{ width: uploadProgress + '%' }"></div> </div> <span>{{ uploadProgress }}%</span> </div> </div> </div> </div> </div> 5.2 编辑器Vue.js应用 创建public/js/app.js: // Vue.js应用主文件 document.addEventListener('DOMContentLoaded', function() { // 检查Vue是否已加载 if (typeof Vue === 'undefined') { console.error('Vue.js未加载,请确保已引入Vue库'); return; } // 创建Vue应用 const app = Vue.createApp({ data() { return { // 视频相关 videoUrl: '', videoDuration: 0, currentTime: 0, videoFile: null, // 字幕数据 subtitles: [], currentSubtitleIndex: -1, currentSubtitleText: '', // 识别状态 isRecording: false, selectedLanguage: 'zh-CN', recognitionTool: null, // 界面状态 showSettings: false, showUploadModal: false, isDragOver: false, uploadProgress: 0, // 设置 settings: { fontFamily: 'Microsoft YaHei', fontSize: 24, fontColor: '#FFFFFF', backgroundColor: '#000000', backgroundOpacity: 50, position: 'bottom' }, // 项目信息 projectId: null, projectTitle: '未命名项目', isSaved: false }; }, computed: { // 计算字幕显示样式 subtitleStyle() { const bgColor = this.hexToRgba( this.settings.backgroundColor, this.settings.backgroundOpacity / 100 );

发表评论

WordPress高级教程,开发集成在线问卷调查结果实时数据看板

WordPress高级教程:开发集成在线问卷调查结果实时数据看板 引言:WordPress作为企业级应用开发平台 WordPress早已超越了简单的博客系统定位,如今已成为一个功能强大的内容管理系统(CMS)和应用程序框架。全球超过43%的网站使用WordPress构建,这得益于其灵活的可扩展性和庞大的开发者社区。在本教程中,我们将探索如何通过WordPress代码二次开发,实现一个专业级的在线问卷调查结果实时数据看板,展示如何将WordPress转变为功能丰富的互联网应用平台。 传统的问卷调查工具往往独立于企业网站存在,导致数据孤岛和用户体验割裂。通过将问卷调查与数据可视化看板直接集成到WordPress中,我们可以创建无缝的用户体验,同时利用WordPress的用户管理、权限控制和内容展示能力。本教程将引导您完成从需求分析到代码实现的完整过程,适合有一定WordPress开发经验的开发者。 第一部分:项目架构设计与技术选型 1.1 需求分析与功能规划 在开始编码之前,我们需要明确项目的核心需求: 问卷调查功能:支持多种题型(单选、多选、矩阵、评分等) 实时数据收集:用户提交问卷后立即更新数据存储 可视化看板:多种图表展示调查结果(柱状图、饼图、趋势图等) 权限管理:不同用户角色查看不同级别的数据 响应式设计:在桌面和移动设备上都能良好显示 数据导出:支持将结果导出为CSV、PDF等格式 1.2 技术栈选择 基于WordPress生态,我们选择以下技术方案: 核心框架:WordPress 5.8+,使用自定义文章类型存储问卷和结果 前端框架:Vue.js 3.0 + Chart.js,实现交互式数据可视化 实时通信:REST API + WebSocket(可选,用于实时更新) 数据库优化:自定义数据库表结构,提高查询效率 缓存机制:Transients API + Object Cache,提升性能 1.3 系统架构设计 我们将采用分层架构设计: 表现层 (前端展示) ↓ 业务逻辑层 (WordPress插件) ↓ 数据访问层 (自定义数据库表+API) ↓ 数据存储层 (MySQL + 缓存) 第二部分:创建WordPress问卷调查插件 2.1 插件基础结构 首先创建插件主文件 wp-survey-dashboard.php: <?php /** * Plugin Name: WordPress问卷调查与数据看板 * Plugin URI: https://yourwebsite.com/ * Description: 集成在线问卷调查和实时数据看板的高级解决方案 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('WPSD_VERSION', '1.0.0'); define('WPSD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WPSD_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 class WP_Survey_Dashboard { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 激活/停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化 add_action('init', array($this, 'init')); // 管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 加载脚本和样式 add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function activate() { // 创建数据库表 $this->create_database_tables(); // 设置默认选项 update_option('wpsd_version', WPSD_VERSION); // 刷新重写规则 flush_rewrite_rules(); } public function deactivate() { // 清理临时数据 // 注意:不删除调查数据 flush_rewrite_rules(); } public function init() { // 注册自定义文章类型 $this->register_post_types(); // 注册短代码 add_shortcode('survey_form', array($this, 'survey_form_shortcode')); add_shortcode('survey_dashboard', array($this, 'survey_dashboard_shortcode')); // 初始化REST API add_action('rest_api_init', array($this, 'register_rest_routes')); } // 其他方法将在后续部分实现 } // 启动插件 WP_Survey_Dashboard::get_instance(); 2.2 创建数据库表结构 在 activate 方法中调用的 create_database_tables 方法: private function create_database_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 调查表 $table_surveys = $wpdb->prefix . 'wpsd_surveys'; $table_results = $wpdb->prefix . 'wpsd_results'; $table_answers = $wpdb->prefix . 'wpsd_answers'; // 创建调查表 $sql_surveys = "CREATE TABLE IF NOT EXISTS $table_surveys ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(255) NOT NULL, description TEXT, settings LONGTEXT, status VARCHAR(20) DEFAULT 'draft', created_by BIGINT(20) UNSIGNED, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY status (status), KEY created_by (created_by) ) $charset_collate;"; // 创建结果表 $sql_results = "CREATE TABLE IF NOT EXISTS $table_results ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, survey_id BIGINT(20) UNSIGNED NOT NULL, user_id BIGINT(20) UNSIGNED, user_ip VARCHAR(45), user_agent TEXT, session_id VARCHAR(100), completed TINYINT(1) DEFAULT 0, started_at DATETIME DEFAULT CURRENT_TIMESTAMP, completed_at DATETIME, metadata LONGTEXT, PRIMARY KEY (id), KEY survey_id (survey_id), KEY user_id (user_id), KEY session_id (session_id), KEY completed (completed) ) $charset_collate;"; // 创建答案表 $sql_answers = "CREATE TABLE IF NOT EXISTS $table_answers ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, result_id BIGINT(20) UNSIGNED NOT NULL, question_id VARCHAR(100) NOT NULL, question_type VARCHAR(50) NOT NULL, question_text TEXT, answer_value LONGTEXT, answer_text TEXT, answered_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY result_id (result_id), KEY question_id (question_id), KEY question_type (question_type) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_surveys); dbDelta($sql_results); dbDelta($sql_answers); } 2.3 注册自定义文章类型和分类 扩展 init 方法中的 register_post_types: private function register_post_types() { // 注册调查自定义文章类型 register_post_type('wpsd_survey', array( 'labels' => array( 'name' => __('问卷调查', 'wp-survey-dashboard'), 'singular_name' => __('调查', 'wp-survey-dashboard'), 'add_new' => __('添加新调查', 'wp-survey-dashboard'), 'add_new_item' => __('添加新调查', 'wp-survey-dashboard'), 'edit_item' => __('编辑调查', 'wp-survey-dashboard'), 'new_item' => __('新调查', 'wp-survey-dashboard'), 'view_item' => __('查看调查', 'wp-survey-dashboard'), 'search_items' => __('搜索调查', 'wp-survey-dashboard'), 'not_found' => __('未找到调查', 'wp-survey-dashboard'), 'not_found_in_trash' => __('回收站中无调查', 'wp-survey-dashboard'), ), 'public' => true, 'has_archive' => true, 'rewrite' => array('slug' => 'surveys'), 'supports' => array('title', 'editor', 'author', 'revisions'), 'menu_icon' => 'dashicons-chart-bar', 'show_in_rest' => true, 'rest_base' => 'wpsd_surveys', 'capability_type' => 'post', 'capabilities' => array( 'create_posts' => 'create_wpsd_surveys', ), 'map_meta_cap' => true, ) ); // 注册调查分类 register_taxonomy('survey_category', 'wpsd_survey', array( 'labels' => array( 'name' => __('调查分类', 'wp-survey-dashboard'), 'singular_name' => __('分类', 'wp-survey-dashboard'), ), 'hierarchical' => true, 'show_in_rest' => true, 'show_admin_column' => true, ) ); } 第三部分:构建问卷调查前端功能 3.1 创建问卷调查表单短代码 实现 survey_form_shortcode 方法: public function survey_form_shortcode($atts) { // 提取短代码属性 $atts = shortcode_atts(array( 'id' => 0, 'title' => 'yes', 'description' => 'yes', ), $atts, 'survey_form'); $survey_id = intval($atts['id']); if (!$survey_id) { return '<div class="wpsd-error">请指定有效的调查ID</div>'; } // 获取调查数据 global $wpdb; $table_surveys = $wpdb->prefix . 'wpsd_surveys'; $survey = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_surveys WHERE id = %d AND status = 'published'", $survey_id )); if (!$survey) { return '<div class="wpsd-error">调查不存在或未发布</div>'; } // 解析调查设置 $settings = json_decode($survey->settings, true); $questions = isset($settings['questions']) ? $settings['questions'] : array(); // 生成唯一会话ID $session_id = $this->generate_session_id(); // 输出调查表单 ob_start(); ?> <div class="wpsd-survey-container" data-survey-id="<?php echo esc_attr($survey_id); ?>"> <div class="wpsd-survey-header"> <?php if ($atts['title'] === 'yes') : ?> <h2 class="wpsd-survey-title"><?php echo esc_html($survey->title); ?></h2> <?php endif; ?> <?php if ($atts['description'] === 'yes' && !empty($survey->description)) : ?> <div class="wpsd-survey-description"> <?php echo wp_kses_post($survey->description); ?> </div> <?php endif; ?> </div> <form id="wpsd-survey-form-<?php echo esc_attr($survey_id); ?>" class="wpsd-survey-form" method="post"> <input type="hidden" name="survey_id" value="<?php echo esc_attr($survey_id); ?>"> <input type="hidden" name="session_id" value="<?php echo esc_attr($session_id); ?>"> <input type="hidden" name="wpsd_nonce" value="<?php echo wp_create_nonce('wpsd_submit_survey'); ?>"> <div class="wpsd-questions-container"> <?php foreach ($questions as $index => $question) : ?> <div class="wpsd-question" data-question-id="<?php echo esc_attr($question['id']); ?>" data-question-type="<?php echo esc_attr($question['type']); ?>"> <div class="wpsd-question-header"> <h3 class="wpsd-question-title"> <span class="wpsd-question-number"><?php echo $index + 1; ?>.</span> <?php echo esc_html($question['title']); ?> <?php if (isset($question['required']) && $question['required']) : ?> <span class="wpsd-required">*</span> <?php endif; ?> </h3> <?php if (!empty($question['description'])) : ?> <div class="wpsd-question-description"> <?php echo esc_html($question['description']); ?> </div> <?php endif; ?> </div> <div class="wpsd-question-body"> <?php echo $this->render_question_field($question); ?> </div> </div> <?php endforeach; ?> </div> <div class="wpsd-form-footer"> <button type="submit" class="wpsd-submit-button"> <?php _e('提交调查', 'wp-survey-dashboard'); ?> </button> <div class="wpsd-form-message"></div> </div> </form> </div> <?php return ob_get_clean(); } private function render_question_field($question) { $field_html = ''; $question_id = esc_attr($question['id']); $field_name = "answers[$question_id]"; switch ($question['type']) { case 'radio': case 'checkbox': $field_html .= '<div class="wpsd-options-container">'; foreach ($question['options'] as $option) { $option_id = esc_attr($option['id']); $input_id = "{$question_id}_{$option_id}"; if ($question['type'] === 'radio') { $field_html .= '<div class="wpsd-option">'; $field_html .= '<input type="radio" id="' . $input_id . '" name="' . $field_name . '" value="' . esc_attr($option['value']) . '">'; $field_html .= '<label for="' . $input_id . '">' . esc_html($option['label']) . '</label>'; $field_html .= '</div>'; } else { $field_html .= '<div class="wpsd-option">'; $field_html .= '<input type="checkbox" id="' . $input_id . '" name="' . $field_name . '[]" value="' . esc_attr($option['value']) . '">'; $field_html .= '<label for="' . $input_id . '">' . esc_html($option['label']) . '</label>'; $field_html .= '</div>'; } } $field_html .= '</div>'; break; case 'select': $field_html .= '<select name="' . $field_name . '" class="wpsd-select">'; if (isset($question['placeholder'])) { $field_html .= '<option value="">' . esc_html($question['placeholder']) . '</option>'; } foreach ($question['options'] as $option) { $field_html .= '<option value="' . esc_attr($option['value']) . '">' . esc_html($option['label']) . '</option>'; } $field_html .= '</select>'; break; case 'rating': $field_html .= '<div class="wpsd-rating-container" data-max-rating="' . esc_attr($question['max_rating']) . '">'; for ($i = 1; $i <= $question['max_rating']; $i++) { $field_html .= '<div class="wpsd-rating-star" data-value="' . $i . '">'; $field_html .= '<span class="wpsd-star-icon">★</span>'; $field_html .= '</div>'; } $field_html .= '<input type="hidden" name="' . $field_name . '" value="">'; $field_html .= '</div>'; break; case 'text': case 'textarea': case 'email': $input_type = $question['type'] === 'textarea' ? 'textarea' : 'text'; $input_type_attr = $question['type'] === 'email' ? 'email' : 'text'; if ($input_type === 'textarea') { $field_html .= '<textarea name="' . $field_name . '" class="wpsd-textarea"'; if (isset($question['placeholder'])) { $field_html .= ' placeholder="' . esc_attr($question['placeholder']) . '"'; } if (isset($question['rows'])) { $field_html .= ' rows="' . esc_attr($question['rows']) . '"'; } $field_html .= '></textarea>'; } else { $field_html .= '<input type="' . $input_type_attr . '" name="' . $field_name . '" class="wpsd-input"'; if (isset($question['placeholder'])) { $field_html .= ' placeholder="' . esc_attr($question['placeholder']) . '"'; } if (isset($question['maxlength'])) { $field_html .= ' maxlength="' . esc_attr($question['maxlength']) . '"'; } $field_html .= '>'; } break; case 'matrix': $field_html .= '<div class="wpsd-matrix-container">'; $field_html .= '<table class="wpsd-matrix-table">'; $field_html .= '<thead><tr><th></th>'; foreach ($question['columns'] as $column) { $field_html .= '<th>' . esc_html($column['label']) . '</th>'; } $field_html .= '</tr></thead>'; $field_html .= '<tbody>'; foreach ($question['rows'] as $row) { $field_html .= '<tr>'; $field_html .= '<td class="wpsd-matrix-row-label">' . esc_html($row['label']) . '</td>'; foreach ($question['columns'] as $column) { $input_id = "{$question_id}_{$row['id']}_{$column['id']}"; $field_html .= '<td class="wpsd-matrix-cell">'; $field_html .= '<input type="radio" id="' . $input_id . '" name="answers[' . $question_id . '][' . $row['id'] . ']" value="' . esc_attr($column['value']) . '">'; $field_html .= '<label for="' . $input_id . '"></label>'; $field_html .= '</td>'; } $field_html .= '</tr>'; } $field_html .= '</tbody></table></div>'; break; default: $field_html .= '<p>不支持的问题类型: ' . esc_html($question['type']) . '</p>'; } return $field_html; } private function generate_session_id() { if (isset($_COOKIE['wpsd_session_id'])) { return sanitize_text_field($_COOKIE['wpsd_session_id']); } $session_id = wp_generate_uuid4(); setcookie('wpsd_session_id', $session_id, time() + 3600 * 24 * 7, COOKIEPATH, COOKIE_DOMAIN); return $session_id; } 3.2 前端脚本和样式 实现前端资源加载方法: public function enqueue_frontend_assets() { // 只在需要时加载 global $post; if (is_a($post, 'WP_Post') && (has_shortcode($post->post_content, 'survey_form') || has_shortcode($post->post_content, 'survey_dashboard'))) { // 加载Vue.js wp_enqueue_script('vue-js', 'https://cdn.jsdelivr.net/npm/vue@3.2.31/dist/vue.global.prod.js', array(), '3.2.31', true); // 加载Chart.js wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js@3.7.1/dist/chart.min.js', array(), '3.7.1', true); // 加载插件主脚本 wp_enqueue_script( 'wpsd-frontend', WPSD_PLUGIN_URL . 'assets/js/frontend.js', array('jquery', 'vue-js', 'chart-js'), WPSD_VERSION, true ); // 本地化脚本 wp_localize_script('wpsd-frontend', 'wpsd_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'rest_url' => rest_url('wpsd/v1/'), 'nonce' => wp_create_nonce('wpsd_ajax_nonce') )); // 加载样式 wp_enqueue_style( 'wpsd-frontend-style', WPSD_PLUGIN_URL . 'assets/css/frontend.css', array(), WPSD_VERSION ); } } 创建前端JavaScript文件 assets/js/frontend.js: (function($) { 'use strict'; // 问卷调查提交处理 $(document).on('submit', '.wpsd-survey-form', function(e) { e.preventDefault(); const $form = $(this); const $submitBtn = $form.find('.wpsd-submit-button'); const $message = $form.find('.wpsd-form-message'); const formData = new FormData(this); // 禁用提交按钮 $submitBtn.prop('disabled', true).text('提交中...'); $message.removeClass('wpsd-success wpsd-error').text(''); // 验证必填字段 const requiredFields = $form.find('.wpsd-required'); let isValid = true; requiredFields.each(function() { const $question = $(this).closest('.wpsd-question'); const questionType = $question.data('question-type'); const questionId = $question.data('question-id'); let isAnswered = false; if (questionType === 'radio' || questionType === 'select') { isAnswered = $form.find(`[name="answers[${questionId}]"]:checked`).length > 0 || $form.find(`[name="answers[${questionId}]"]`).val() !== ''; } else if (questionType === 'checkbox') { isAnswered = $form.find(`[name="answers[${questionId}][]"]:checked`).length > 0; } else if (questionType === 'matrix') { const rowCount = $question.find('.wpsd-matrix-row-label').length; const answeredRows = $question.find('input[type="radio"]:checked').length; isAnswered = answeredRows === rowCount; } else { isAnswered = $form.find(`[name="answers[${questionId}]"]`).val().trim() !== ''; } if (!isAnswered) { $question.addClass('wpsd-error'); isValid = false; } else { $question.removeClass('wpsd-error'); } }); if (!isValid) { $message.addClass('wpsd-error').text('请填写所有必填问题'); $submitBtn.prop('disabled', false).text('提交调查'); return; } // 发送AJAX请求 $.ajax({ url: wpsd_ajax.ajax_url, type: 'POST', data: formData, processData: false, contentType: false, success: function(response) { if (response.success) { $message.addClass('wpsd-success').text(response.data.message); $form[0].reset(); // 更新评分控件 $form.find('.wpsd-rating-container').each(function() { $(this).find('.wpsd-rating-star').removeClass('active'); $(this).find('input[type="hidden"]').val(''); }); // 显示感谢信息 setTimeout(function() { $form.html(` <div class="wpsd-thank-you"> <h3>感谢参与!</h3> <p>您的反馈对我们非常重要。</p> <p>您可以在调查结果页面查看实时统计数据。</p> </div> `); }, 2000); } else { $message.addClass('wpsd-error').text(response.data.message); $submitBtn.prop('disabled', false).text('提交调查'); } }, error: function() { $message.addClass('wpsd-error').text('提交失败,请稍后重试'); $submitBtn.prop('disabled', false).text('提交调查'); } }); }); // 评分控件交互 $(document).on('mouseenter', '.wpsd-rating-star', function() { const $container = $(this).closest('.wpsd-rating-container'); const value = $(this).data('value'); $container.find('.wpsd-rating-star').each(function() { if ($(this).data('value') <= value) { $(this).addClass('hover'); } else { $(this).removeClass('hover'); } }); }); $(document).on('mouseleave', '.wpsd-rating-container', function() { $(this).find('.wpsd-rating-star').removeClass('hover'); }); $(document).on('click', '.wpsd-rating-star', function() { const $container = $(this).closest('.wpsd-rating-container'); const value = $(this).data('value'); const $hiddenInput = $container.find('input[type="hidden"]'); $container.find('.wpsd-rating-star').removeClass('active hover'); $container.find('.wpsd-rating-star').each(function() { if ($(this).data('value') <= value) { $(this).addClass('active'); } }); $hiddenInput.val(value); }); // 数据看板Vue应用 if (typeof Vue !== 'undefined' && $('.wpsd-dashboard-container').length > 0) { const { createApp } = Vue; const DashboardApp = { data() { return { surveyId: 0, loading: true, error: null, surveyData: null, chartData: {}, filters: { dateRange: 'all', startDate: '', endDate: '', questionFilter: 'all' }, realTime: false, refreshInterval: null }; }, mounted() { const container = document.querySelector('.wpsd-dashboard-container'); this.surveyId = container.dataset.surveyId; this.realTime = container.dataset.realtime === 'true'; this.loadSurveyData(); if (this.realTime) { this.startRealTimeUpdates(); } }, methods: { async loadSurveyData() { this.loading = true; this.error = null; try { const response = await fetch( `${wpsd_ajax.rest_url}survey/${this.surveyId}/results?` + new URLSearchParams(this.filters) ); if (!response.ok) { throw new Error('获取数据失败'); } const data = await response.json(); this.surveyData = data; this.prepareChartData(); } catch (err) { this.error = err.message; console.error('加载调查数据失败:', err); } finally { this.loading = false; } }, prepareChartData() { if (!this.surveyData || !this.surveyData.questions) return; this.chartData = {}; this.surveyData.questions.forEach(question => { if (question.type === 'radio' || question.type === 'select' || question.type === 'checkbox') { this.chartData[question.id] = this.createPieChartData(question); } else if (question.type === 'rating') { this.chartData[question.id] = this.createBarChartData(question); } else if (question.type === 'matrix') { this.chartData[question.id] = this.createMatrixChartData(question); } }); // 渲染图表 this.$nextTick(() => { this.renderCharts(); }); }, createPieChartData(question) { const labels = []; const data = []; const backgroundColors = []; question.options.forEach((option, index) => { labels.push(option.label); data.push(option.count || 0); backgroundColors.push(this.getColor(index)); }); return { type: 'pie', data: { labels: labels, datasets: [{ data: data, backgroundColor: backgroundColors, borderWidth: 1 }] }, options: { responsive: true, plugins: { legend: { position: 'right' }, title: { display: true, text: question.title } } } }; }, createBarChartData(question) { const labels = []; const data = []; for (let i = 1; i <= question.max_rating; i++) { labels.push(`${i}星`); const count = question.ratings ? (question.ratings[i] || 0) : 0; data.push(count); } return { type: 'bar', data: { labels: labels, datasets: [{ label: '评分分布', data: data, backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } }, plugins: { title: { display: true, text: question.title } } } }; }, createMatrixChartData(question) { const labels = question.rows.map(row => row.label); const datasets = []; question.columns.forEach((column, colIndex) => { const data = question.rows.map(row => { const cell = question.matrix_data?.find( d => d.row_id === row.id && d.column_id === column.id ); return cell ? cell.count : 0; }); datasets.push({ label: column.label, data: data, backgroundColor: this.getColor(colIndex, 0.5), borderColor: this.getColor(colIndex, 1), borderWidth: 1 }); }); return { type: 'bar', data: { labels: labels, datasets: datasets }, options: { responsive: true, scales: { x: { stacked: true }, y: { stacked: true, beginAtZero: true } }, plugins: { title: { display: true, text: question.title } } } }; }, renderCharts() { Object.keys(this.chartData).forEach(questionId => { const canvas = document.getElementById(`chart-${questionId}`); if (!canvas) return; const ctx = canvas.getContext('2d'); const chartConfig = this.chartData[questionId]; // 销毁现有图表实例 if (canvas.chart) { canvas.chart.destroy(); } // 创建新图表 canvas.chart = new Chart(ctx, chartConfig); }); }, getColor(index, alpha = 1) { const colors = [ `rgba(255, 99, 132, ${alpha})`, `rgba(54, 162, 235, ${alpha})`, `rgba(255, 206, 86, ${alpha})`, `rgba(75, 192, 192, ${alpha})`, `rgba(153, 102, 255, ${alpha})`, `rgba(255, 159, 64, ${alpha})`, `rgba(199, 199, 199, ${alpha})`, `rgba(83, 102, 255, ${alpha})`, `rgba(40, 159, 64, ${alpha})`, `rgba(210, 199, 199, ${alpha})` ]; return colors[index % colors.length]; }, applyFilters() { this.loadSurveyData(); }, resetFilters() { this.filters = { dateRange: 'all', startDate: '', endDate: '', questionFilter: 'all' }; this.loadSurveyData(); }, startRealTimeUpdates() { // 每30秒更新一次数据 this.refreshInterval = setInterval(() => { this.loadSurveyData(); }, 30000); }, stopRealTimeUpdates() { if (this.refreshInterval) { clearInterval(this.refreshInterval); this.refreshInterval = null; } }, exportData(format) { const url = `${wpsd_ajax.rest_url}survey/${this.surveyId}/export?format=${format}`; window.open(url, '_blank'); } }, beforeUnmount() { this.stopRealTimeUpdates(); }, template: ` <div class="wpsd-dashboard"> <div v-if="loading" class="wpsd-loading"> 加载中... </div> <div v-else-if="error" class="wpsd-error"> {{ error }} </div> <div v-else> <!-- 控制面板 --> <div class="wpsd-dashboard-controls"> <div class="wpsd-filters"> <div class="wpsd-filter-group"> <label>时间范围:</label> <select v-model="filters.dateRange" @change="applyFilters"> <option value="all">全部时间</option> <option value="today">今天</option> <option value="yesterday">昨天</option> <option value="week">本周</option> <option value="month">本月</option> <option value="custom">自定义</option> </select> </div> <div v-if="filters.dateRange === 'custom'" class="wpsd-filter-group">

发表评论