手把手教程:为WordPress实现自动化内容翻译与本地化适配工具 引言:全球化时代下的WordPress本地化挑战 在当今互联网全球化浪潮中,网站内容的多语言支持已成为吸引国际用户、拓展全球市场的关键因素。根据W3Techs的数据,超过43%的网站使用WordPress作为内容管理系统,其中许多网站面临着内容本地化的迫切需求。然而,传统的人工翻译方式不仅成本高昂、效率低下,而且难以保持内容更新的同步性。 本教程将深入探讨如何通过WordPress代码二次开发,构建一个自动化内容翻译与本地化适配工具。我们将从基础概念入手,逐步实现一个功能完整的解决方案,帮助您的WordPress网站轻松跨越语言障碍,实现真正的全球化内容管理。 第一部分:理解WordPress本地化与翻译机制 1.1 WordPress国际化(i18n)与本地化(l10n)基础 WordPress从设计之初就考虑了国际化需求,其核心采用gettext框架实现文本翻译。理解这一机制是进行二次开发的基础: // WordPress标准翻译函数示例 $text = __('Hello World', 'text-domain'); $text = _e('Hello World', 'text-domain'); // 直接输出 WordPress使用.po和.mo文件存储翻译字符串,但这种方式主要适用于主题和插件的静态文本翻译,对于动态生成的内容支持有限。 1.2 现有翻译插件的局限性 市场上存在多种WordPress翻译插件,如WPML、Polylang等,它们提供了不错的解决方案,但也存在一些限制: 高昂的许可费用 复杂的配置过程 对自定义内容类型的支持有限 自动化程度不足,仍需大量人工干预 难以与第三方翻译API深度集成 通过自主开发,我们可以创建更贴合特定需求、成本更低且高度自动化的解决方案。 第二部分:系统架构设计与技术选型 2.1 整体架构设计 我们的自动化翻译系统将采用模块化设计,主要包括以下组件: 内容捕获模块:监测新发布或更新的内容 翻译处理模块:调用翻译API进行内容转换 本地化适配模块:处理日期、货币、数字格式等本地化元素 存储管理模块:管理多语言内容之间的关系 前端展示模块:根据用户语言偏好显示相应内容 2.2 技术选型与准备 2.2.1 翻译API选择 我们将使用以下翻译API之一(本教程以Google Cloud Translation API为例): Google Cloud Translation API(质量高,支持100+语言) DeepL API(欧洲语言质量优秀) Microsoft Azure Translator 百度翻译API(中文相关翻译效果好) 2.2.2 开发环境要求 WordPress 5.0+ PHP 7.4+ 基本的WordPress插件开发知识 熟悉REST API和AJAX 获取所选翻译API的访问密钥 第三部分:创建基础插件框架 3.1 初始化插件结构 首先创建插件主文件 auto-translate-localize.php: <?php /** * Plugin Name: 自动化翻译与本地化工具 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress提供自动化内容翻译与本地化适配功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: auto-translate-localize */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('ATL_VERSION', '1.0.0'); define('ATL_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ATL_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once ATL_PLUGIN_DIR . 'includes/class-atl-core.php'; function atl_init() { $plugin = new ATL_Core(); $plugin->run(); } add_action('plugins_loaded', 'atl_init'); 3.2 创建核心类 在 includes/class-atl-core.php 中创建核心类: class ATL_Core { private $loader; private $version; public function __construct() { $this->version = ATL_VERSION; $this->load_dependencies(); $this->define_admin_hooks(); $this->define_public_hooks(); } private function load_dependencies() { require_once ATL_PLUGIN_DIR . 'includes/class-atl-loader.php'; require_once ATL_PLUGIN_DIR . 'includes/class-atl-translator.php'; require_once ATL_PLUGIN_DIR . 'includes/class-atl-localizer.php'; require_once ATL_PLUGIN_DIR . 'includes/class-atl-content-manager.php'; require_once ATL_PLUGIN_DIR . 'admin/class-atl-admin.php'; require_once ATL_PLUGIN_DIR . 'public/class-atl-public.php'; $this->loader = new ATL_Loader(); } private function define_admin_hooks() { $admin = new ATL_Admin($this->version); $this->loader->add_action('admin_menu', $admin, 'add_admin_menu'); $this->loader->add_action('admin_init', $admin, 'register_settings'); $this->loader->add_action('save_post', $admin, 'handle_post_save', 10, 3); } private function define_public_hooks() { $public = new ATL_Public($this->version); $this->loader->add_action('wp_enqueue_scripts', $public, 'enqueue_scripts'); $this->loader->add_filter('the_content', $public, 'filter_content_by_language'); $this->loader->add_action('init', $public, 'register_language_switcher'); } public function run() { $this->loader->run(); } } 第四部分:实现内容捕获与翻译模块 4.1 内容捕获机制 我们需要捕获WordPress中的内容更新事件,自动触发翻译流程: class ATL_Content_Manager { private $supported_post_types = ['post', 'page']; private $translation_languages = []; public function __construct() { $this->translation_languages = get_option('atl_target_languages', ['es', 'fr', 'de']); } /** * 处理文章保存事件 */ public function handle_post_save($post_id, $post, $update) { // 避免无限循环 if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { return; } // 检查是否支持的文章类型 if (!in_array($post->post_type, $this->supported_post_types)) { return; } // 检查是否需要翻译(避免重复翻译) $already_translated = get_post_meta($post_id, '_atl_translated', true); if (!$already_translated || isset($_POST['force_translate'])) { $this->translate_post($post_id, $post); } } /** * 翻译文章内容 */ private function translate_post($post_id, $post) { $translator = new ATL_Translator(); $source_lang = get_option('atl_source_language', 'en'); foreach ($this->translation_languages as $target_lang) { // 翻译标题 $translated_title = $translator->translate_text( $post->post_title, $source_lang, $target_lang ); // 翻译内容 $translated_content = $translator->translate_text( $post->post_content, $source_lang, $target_lang ); // 创建翻译后的文章 $this->create_translated_post( $post_id, $translated_title, $translated_content, $target_lang ); } // 标记原文已翻译 update_post_meta($post_id, '_atl_translated', true); } } 4.2 集成翻译API 创建翻译器类,集成Google Cloud Translation API: class ATL_Translator { private $api_key; private $api_url = 'https://translation.googleapis.com/language/translate/v2'; public function __construct() { $this->api_key = get_option('atl_google_api_key', ''); if (empty($this->api_key)) { error_log('ATL: Google Translation API key not set'); } } /** * 翻译文本 */ public function translate_text($text, $source_lang, $target_lang) { if (empty($text) || $source_lang === $target_lang) { return $text; } // 检查是否有缓存 $cache_key = 'atl_trans_' . md5($text . $source_lang . $target_lang); $cached = get_transient($cache_key); if ($cached !== false) { return $cached; } // 调用API $response = $this->call_translation_api($text, $source_lang, $target_lang); if ($response && isset($response['data']['translations'][0]['translatedText'])) { $translated_text = $response['data']['translations'][0]['translatedText']; // 缓存结果(24小时) set_transient($cache_key, $translated_text, DAY_IN_SECONDS); return $translated_text; } return $text; // 翻译失败,返回原文 } /** * 调用Google翻译API */ private function call_translation_api($text, $source_lang, $target_lang) { $args = [ 'body' => [ 'q' => $text, 'source' => $source_lang, 'target' => $target_lang, 'format' => 'html', 'key' => $this->api_key ], 'timeout' => 30 ]; $response = wp_remote_post($this->api_url, $args); if (is_wp_error($response)) { error_log('ATL Translation API error: ' . $response->get_error_message()); return false; } return json_decode(wp_remote_retrieve_body($response), true); } /** * 批量翻译(提高效率) */ public function translate_batch($texts, $source_lang, $target_lang) { // 实现批量翻译逻辑 // ... } } 第五部分:实现本地化适配模块 5.1 本地化内容适配 翻译不仅仅是文字转换,还包括本地化元素的适配: class ATL_Localizer { /** * 本地化日期格式 */ public function localize_date($date_string, $target_lang) { $date_formats = [ 'en' => 'F j, Y', 'es' => 'j de F de Y', 'fr' => 'j F Y', 'de' => 'j. F Y', 'ja' => 'Y年m月d日', 'zh-CN' => 'Y年m月d日' ]; $format = isset($date_formats[$target_lang]) ? $date_formats[$target_lang] : $date_formats['en']; $timestamp = strtotime($date_string); return date_i18n($format, $timestamp); } /** * 本地化数字格式 */ public function localize_number($number, $target_lang) { $number_formats = [ 'en' => ['decimal' => '.', 'thousands' => ','], 'es' => ['decimal' => ',', 'thousands' => '.'], 'fr' => ['decimal' => ',', 'thousands' => ' '], 'de' => ['decimal' => ',', 'thousands' => '.'], ]; $format = isset($number_formats[$target_lang]) ? $number_formats[$target_lang] : $number_formats['en']; return number_format( $number, 2, $format['decimal'], $format['thousands'] ); } /** * 本地化货币 */ public function localize_currency($amount, $currency, $target_lang) { $currency_formats = [ 'en' => ['symbol' => '$', 'position' => 'before'], 'es' => ['symbol' => '€', 'position' => 'after'], 'fr' => ['symbol' => '€', 'position' => 'after'], 'de' => ['symbol' => '€', 'position' => 'after'], 'ja' => ['symbol' => '¥', 'position' => 'before'], 'zh-CN' => ['symbol' => '¥', 'position' => 'before'] ]; $format = isset($currency_formats[$target_lang]) ? $currency_formats[$target_lang] : $currency_formats['en']; $localized_amount = $this->localize_number($amount, $target_lang); if ($format['position'] === 'before') { return $format['symbol'] . $localized_amount; } else { return $localized_amount . ' ' . $format['symbol']; } } /** * 处理内容中的本地化元素 */ public function process_content($content, $target_lang) { // 处理日期 $content = preg_replace_callback( '/{date:(.+?)}/', function($matches) use ($target_lang) { return $this->localize_date($matches[1], $target_lang); }, $content ); // 处理货币 $content = preg_replace_callback( '/{currency:(d+.?d*):(w+)}/', function($matches) use ($target_lang) { return $this->localize_currency( $matches[1], $matches[2], $target_lang ); }, $content ); // 处理测量单位转换(如英里到公里) $content = $this->convert_measurements($content, $target_lang); return $content; } } 第六部分:创建管理界面与设置 6.1 管理菜单与设置页面 class ATL_Admin { private $version; public function __construct($version) { $this->version = $version; } /** * 添加管理菜单 */ public function add_admin_menu() { add_menu_page( '自动化翻译设置', '翻译工具', 'manage_options', 'atl-settings', [$this, 'display_settings_page'], 'dashicons-translation', 80 ); add_submenu_page( 'atl-settings', '翻译统计', '统计', 'manage_options', 'atl-stats', [$this, 'display_stats_page'] ); } /** * 显示设置页面 */ public function display_settings_page() { ?> <div class="wrap"> <h1>自动化翻译与本地化设置</h1> <form method="post" action="options.php"> <?php settings_fields('atl_settings_group'); do_settings_sections('atl-settings'); submit_button(); ?> </form> <div class="atl-actions"> <h2>批量操作</h2> <button id="atl-batch-translate" class="button button-primary"> 批量翻译现有内容 </button> <button id="atl-sync-translations" class="button"> 同步翻译内容 </button> </div> </div> <?php } /** * 注册设置 */ public function register_settings() { register_setting('atl_settings_group', 'atl_google_api_key'); register_setting('atl_settings_group', 'atl_source_language'); register_setting('atl_settings_group', 'atl_target_languages'); register_setting('atl_settings_group', 'atl_auto_translate'); register_setting('atl_settings_group', 'atl_post_types'); add_settings_section( 'atl_main_section', 'API配置', null, 'atl-settings' ); add_settings_field( 'atl_google_api_key', 'Google Cloud API密钥', [$this, 'render_api_key_field'], 'atl-settings', 'atl_main_section' ); add_settings_field( 'atl_source_language', '源语言', [$this, 'render_source_language_field'], 'atl-settings', 'atl_main_section' ); add_settings_field( 'atl_target_languages', '目标语言', [$this, 'render_target_languages_field'], 'atl-settings', 'atl_main_section' ); } /** * 渲染API密钥字段 */ public function render_api_key_field() { $value = get_option('atl_google_api_key', ''); ?> <input type="password" name="atl_google_api_key" value="<?php echo esc_attr($value); ?>" class="regular-text"> <p class="description">从Google Cloud Console获取Translation API密钥</p> <?php } /** * 渲染目标语言选择字段 */ public function render_target_languages_field() { $languages = [ 'es' => '西班牙语', 'fr' => '法语', 'de' => '德语', 'ja' => '日语', zh-CN' => '简体中文', 'zh-TW' => '繁体中文', 'ko' => '韩语', 'ru' => '俄语', 'ar' => '阿拉伯语' ]; $selected = get_option('atl_target_languages', ['es', 'fr', 'de']); ?> <select name="atl_target_languages[]" multiple="multiple" class="regular-text" style="height: 150px;"> <?php foreach ($languages as $code => $name): ?> <option value="<?php echo esc_attr($code); ?>" <?php echo in_array($code, $selected) ? 'selected' : ''; ?>> <?php echo esc_html($name . ' (' . $code . ')'); ?> </option> <?php endforeach; ?> </select> <p class="description">按住Ctrl键可多选</p> <?php } } ### 6.2 批量翻译功能 class ATL_Batch_Processor { /** * 批量翻译现有内容 */ public function process_batch_translation() { $post_types = get_option('atl_post_types', ['post', 'page']); $languages = get_option('atl_target_languages', ['es', 'fr', 'de']); $args = [ 'post_type' => $post_types, 'post_status' => 'publish', 'posts_per_page' => -1, 'meta_query' => [ [ 'key' => '_atl_translated', 'compare' => 'NOT EXISTS' ] ] ]; $posts = get_posts($args); $results = [ 'total' => count($posts), 'processed' => 0, 'errors' => [] ]; foreach ($posts as $post) { try { $this->translate_single_post($post->ID, $post, $languages); $results['processed']++; // 更新进度 if ($results['processed'] % 10 === 0) { update_option('atl_batch_progress', [ 'current' => $results['processed'], 'total' => $results['total'] ]); } } catch (Exception $e) { $results['errors'][] = [ 'post_id' => $post->ID, 'error' => $e->getMessage() ]; } } return $results; } /** * AJAX批量翻译处理 */ public function ajax_batch_translate() { check_ajax_referer('atl_batch_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_die('权限不足'); } $page = isset($_POST['page']) ? intval($_POST['page']) : 1; $batch_size = 5; // 每批处理5篇文章 $args = [ 'post_type' => get_option('atl_post_types', ['post', 'page']), 'post_status' => 'publish', 'posts_per_page' => $batch_size, 'paged' => $page, 'meta_query' => [ [ 'key' => '_atl_translated', 'compare' => 'NOT EXISTS' ] ] ]; $posts = get_posts($args); $total_posts = wp_count_posts()->publish; foreach ($posts as $post) { $this->translate_single_post($post->ID, $post); } $progress = ($page * $batch_size) / $total_posts * 100; $progress = min(100, $progress); wp_send_json_success([ 'progress' => $progress, 'message' => sprintf('已处理 %d/%d', $page * $batch_size, $total_posts), 'completed' => $progress >= 100 ]); } } ## 第七部分:前端语言切换与内容展示 ### 7.1 语言切换器实现 class ATL_Public { private $version; private $current_language; public function __construct($version) { $this->version = $version; $this->current_language = $this->get_user_language(); } /** * 获取用户语言偏好 */ private function get_user_language() { // 1. 检查URL参数 if (isset($_GET['lang'])) { $lang = sanitize_text_field($_GET['lang']); setcookie('atl_user_language', $lang, time() + YEAR_IN_SECONDS, '/'); return $lang; } // 2. 检查Cookie if (isset($_COOKIE['atl_user_language'])) { return $_COOKIE['atl_user_language']; } // 3. 检查浏览器语言 $browser_lang = substr($_SERVER['HTTP_ACCEPT_LANGUAGE'], 0, 2); $supported_langs = get_option('atl_target_languages', []); if (in_array($browser_lang, $supported_langs)) { return $browser_lang; } // 4. 默认语言 return get_option('atl_source_language', 'en'); } /** * 注册语言切换器短代码 */ public function register_language_switcher() { add_shortcode('language_switcher', [$this, 'render_language_switcher']); } /** * 渲染语言切换器 */ public function render_language_switcher($atts) { $atts = shortcode_atts([ 'type' => 'dropdown', // dropdown, flags, list 'show_names' => true, 'show_flags' => true ], $atts); $languages = [ 'en' => ['name' => 'English', 'flag' => '🇺🇸'], 'es' => ['name' => 'Español', 'flag' => '🇪🇸'], 'fr' => ['name' => 'Français', 'flag' => '🇫🇷'], 'de' => ['name' => 'Deutsch', 'flag' => '🇩🇪'], 'zh-CN' => ['name' => '简体中文', 'flag' => '🇨🇳'] ]; $current_url = home_url(add_query_arg(null, null)); ob_start(); ?> <div class="atl-language-switcher atl-type-<?php echo esc_attr($atts['type']); ?>"> <?php if ($atts['type'] === 'dropdown'): ?> <select class="atl-lang-select" onchange="window.location.href=this.value"> <?php foreach ($languages as $code => $lang): ?> <option value="<?php echo esc_url(add_query_arg('lang', $code, $current_url)); ?>" <?php echo $code === $this->current_language ? 'selected' : ''; ?>> <?php if ($atts['show_flags']): ?> <?php echo esc_html($lang['flag']); ?> <?php endif; ?> <?php if ($atts['show_names']): ?> <?php echo esc_html($lang['name']); ?> <?php endif; ?> </option> <?php endforeach; ?> </select> <?php else: ?> <ul class="atl-lang-list"> <?php foreach ($languages as $code => $lang): ?> <li class="<?php echo $code === $this->current_language ? 'current' : ''; ?>"> <a href="<?php echo esc_url(add_query_arg('lang', $code, $current_url)); ?>"> <?php if ($atts['show_flags']): ?> <span class="atl-flag"><?php echo esc_html($lang['flag']); ?></span> <?php endif; ?> <?php if ($atts['show_names']): ?> <span class="atl-name"><?php echo esc_html($lang['name']); ?></span> <?php endif; ?> </a> </li> <?php endforeach; ?> </ul> <?php endif; ?> </div> <?php return ob_get_clean(); } /** * 根据语言过滤内容 */ public function filter_content_by_language($content) { global $post; if (is_admin() || !$post) { return $content; } // 如果是源语言,直接返回 if ($this->current_language === get_option('atl_source_language', 'en')) { return $content; } // 查找翻译版本 $translated_post_id = $this->get_translated_post_id($post->ID, $this->current_language); if ($translated_post_id) { $translated_post = get_post($translated_post_id); if ($translated_post && $translated_post->post_status === 'publish') { $localizer = new ATL_Localizer(); $content = $localizer->process_content($translated_post->post_content, $this->current_language); // 应用WordPress过滤器 $content = apply_filters('the_content', $content); } } return $content; } /** * 获取翻译后的文章ID */ private function get_translated_post_id($original_id, $language) { $args = [ 'post_type' => get_post_type($original_id), 'meta_query' => [ [ 'key' => '_atl_original_post', 'value' => $original_id ], [ 'key' => '_atl_language', 'value' => $language ] ], 'posts_per_page' => 1 ]; $posts = get_posts($args); return $posts ? $posts[0]->ID : false; } } ### 7.2 前端样式与脚本 / assets/css/atl-public.css /.atl-language-switcher { margin: 10px 0; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; } .atl-language-switcher select.atl-lang-select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; background: white; font-size: 14px; min-width: 150px; } .atl-language-switcher ul.atl-lang-list { list-style: none; margin: 0; padding: 0; display: flex; gap: 10px; } .atl-language-switcher ul.atl-lang-list li { margin: 0; } .atl-language-switcher ul.atl-lang-list li a { display: flex; align-items: center; gap: 5px; padding: 5px 10px; text-decoration: none; color: #333; border: 1px solid #ddd; border-radius: 4px; transition: all 0.3s ease; } .atl-language-switcher ul.atl-lang-list li a:hover { background: #f5f5f5; border-color: #0073aa; } .atl-language-switcher ul.atl-lang-list li.current a { background: #0073aa; color: white; border-color: #0073aa; } .atl-flag { font-size: 18px; } .atl-name { font-size: 14px; } // assets/js/atl-public.jsjQuery(document).ready(function($) { // 语言切换器交互 $('.atl-language-switcher').on('change', '.atl-lang-select', function() { window.location.href = $(this).val(); }); // 检测内容更新并提示翻译 if (typeof wp !== 'undefined' && wp.data && wp.data.subscribe) { wp.data.subscribe(function() { var isSavingPost = wp.data.select('core/editor').isSavingPost(); var isAutosavingPost = wp.data.select('core/editor').isAutosavingPost(); if (isSavingPost && !isAutosavingPost) { // 文章保存后显示翻译提示 setTimeout(function() { if (confirm('是否立即翻译这篇文章到其他语言?')) { $('#atl-force-translate').click(); } }, 1000); } }); } }); ## 第八部分:高级功能与优化 ### 8.1 SEO优化与hreflang标签 class ATL_SEO_Optimizer { /** * 添加hreflang标签 */ public function add_hreflang_tags() { if (is_singular()) { global $post; $original_id = get_post_meta($post->ID, '_atl_original_post', true); $original_id = $original_id ?: $post->ID; $translations = $this->get_post_translations($original_id); foreach ($translations as $lang => $url) { echo '<link rel="alternate" hreflang="' . esc_attr($lang) . '" href="' . esc_url($url) . '" />' . "n"; } // 添加x-default $default_url = get_permalink($original_id); echo '<link rel="alternate" hreflang="x-default" href="' . esc_url($default_url) . '" />' . "n"; } } /** * 获取文章的所有翻译版本 */ private function get_post_translations($post_id) { $translations = []; $source_lang = get_option('atl_source_language', 'en'); // 添加源语言 $translations[$source_lang] = get_permalink($post_id); // 查找翻译版本 $args = [ 'post_type' => get_post_type($post_id), 'meta_query' => [ [ 'key' => '_atl_original_post', 'value' => $post_id ] ], 'posts_per_page' => -1 ]; $translated_posts = get_posts($args); foreach ($translated_posts as $translated_post) { $lang = get_post_meta($translated_post->ID, '_atl_language', true); if ($lang) { $translations[$lang] = get_permalink($translated_post->ID); } } return $translations; } /** * 优化翻译内容的SEO元数据 */ public function optimize_seo_metadata($post_id, $language) { $original_id = get_post_meta($post_id, '_atl_original_post', true); if ($original_id) { // 复制SEO数据 $meta_keys = [ '_yoast_wpseo_title', '_yoast_wpseo_metadesc', '_yoast_wpseo_focuskw', '_aioseo_title', '_aioseo_description' ]; foreach ($meta_keys as $meta_key) { $value = get_post_meta($original_id, $meta_key, true); if ($value) { // 翻译SEO数据 $translator = new ATL_Translator(); $translated_value = $translator->translate_text( $value, get_option('atl_source_language', 'en'), $language ); update_post_meta($post_id, $meta_key, $translated_value); } } } } } ### 8.2 缓存优化策略 class ATL_Cache_Manager { /** * 智能缓存策略 */ public function setup_caching() { // 按语言缓存页面 add_action('template_redirect', [$this, 'set_cache_headers']); // 清除翻译缓存 add_action('atl_translation_updated', [$this, 'clear_translation_cache']); // 预缓存热门翻译 $this->precache_popular_content(); } /** * 设置缓存头 */ public function set_cache_headers() { if (!is_admin() && !is_user_logged_in()) { $language = $this->get_current_language(); header('Cache-Control: public, max-age=3600'); header('Vary: Accept-Language, Cookie'); // 为CDN设置缓存键 if (function_exists('wp_cache_add')) { wp_cache_add('atl_language', $language); } } } /** * 清除翻译缓存 */ public function clear_translation_cache($post_id) { // 清除文章缓存 clean_post_cache($post_id); // 清除相关翻译缓存 $translated_posts = $this->get_translated_posts($post_id); foreach ($translated_posts as $translated_post) { clean_post_cache($translated_post->ID); } // 清除API缓存 $this->clear_api_cache($post_id); } /** * 预缓存热门内容 */ private function precache_popular_content() { if (false === ($popular_posts = get_transient('atl_popular_posts'))) { $args = [ 'post_type' => get_option('atl_post_types', ['post', 'page']), 'posts_per_page' => 10, 'meta_key' => 'post_views_count', 'orderby' => 'meta_value_num', 'order' => 'DESC' ]; $popular_posts = get_posts($args); set_transient('atl_popular_posts', $popular_posts, HOUR_IN_SECONDS); } // 预翻译热门文章 foreach ($popular
发表评论分类: 应用软件
开发指南:打造网站内嵌的在线问卷调查与可视化数据分析报告 摘要 在当今数据驱动的互联网时代,网站内嵌的在线问卷调查与可视化数据分析功能已成为企业与用户互动、收集反馈、优化产品的重要工具。本文详细介绍了如何通过WordPress程序的代码二次开发,实现这一常用互联网小工具功能。我们将从需求分析、技术选型、开发流程到最终部署,全面解析打造专业级问卷调查与数据分析系统的全过程。 一、需求分析与功能规划 1.1 市场需求背景 随着互联网技术的普及,数据收集与分析已成为企业决策的重要依据。传统问卷调查方式存在诸多局限性,如数据整理繁琐、分析效率低下、可视化程度不足等。而网站内嵌的在线问卷调查系统能够实时收集用户反馈,结合可视化数据分析报告,为企业提供直观、高效的数据洞察工具。 1.2 核心功能需求 一个完整的网站内嵌问卷调查与数据分析系统应包含以下核心功能: 问卷创建与管理:支持多种题型(单选、多选、文本、评分等),可自定义问卷样式 响应式设计:适配各种设备,确保移动端用户体验 数据收集与存储:安全高效地收集和存储用户提交的数据 实时数据分析:对收集的数据进行实时统计分析 可视化报告:将分析结果以图表形式直观展示 数据导出:支持将数据导出为Excel、CSV等格式 权限管理:不同用户角色拥有不同的数据访问和操作权限 集成能力:能够与WordPress用户系统、邮件系统等无缝集成 1.3 技术可行性分析 WordPress作为全球最流行的内容管理系统,拥有强大的扩展性和丰富的API接口,通过二次开发完全可以实现上述功能需求。其优势包括: 成熟的用户管理系统 丰富的插件生态和开发文档 灵活的数据库结构 强大的主题定制能力 活跃的开发者社区 二、技术架构设计 2.1 系统架构概览 我们将采用分层架构设计,确保系统的可扩展性和可维护性: ┌─────────────────────────────────────────┐ │ 用户界面层 (UI Layer) │ ├─────────────────────────────────────────┤ │ 业务逻辑层 (Logic Layer) │ ├─────────────────────────────────────────┤ │ 数据访问层 (Data Layer) │ ├─────────────────────────────────────────┤ │ WordPress核心系统 │ └─────────────────────────────────────────┘ 2.2 数据库设计 在WordPress默认数据库结构基础上,我们需要新增以下数据表: 问卷表 (wp_surveys):存储问卷基本信息 问题表 (wp_survey_questions):存储问卷中的各个问题 选项表 (wp_survey_options):存储选择题的选项 回答表 (wp_survey_responses):存储用户提交的回答 报告表 (wp_survey_reports):存储生成的分析报告 2.3 技术栈选择 后端:PHP 7.4+,WordPress REST API,MySQL 5.7+ 前端:HTML5,CSS3,JavaScript (ES6+),Chart.js/D3.js(数据可视化) 开发框架:基于WordPress插件架构开发 第三方库:jQuery(兼容性),Bootstrap(响应式布局),PHPSpreadsheet(数据导出) 三、开发环境搭建与配置 3.1 本地开发环境配置 安装本地服务器环境:推荐使用XAMPP、MAMP或Local by Flywheel 安装WordPress:下载最新版WordPress并完成基本配置 创建插件目录:在wp-content/plugins/目录下创建survey-analytics-tool文件夹 配置开发工具:安装代码编辑器(如VS Code),配置Git版本控制 3.2 插件基础结构 创建插件主文件survey-analytics-tool.php: <?php /** * Plugin Name: 问卷调查与数据分析工具 * Plugin URI: https://yourwebsite.com/ * Description: 网站内嵌的在线问卷调查与可视化数据分析报告系统 * Version: 1.0.0 * Author: 开发者名称 * License: GPL v2 or later * Text Domain: survey-analytics-tool */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('SAT_VERSION', '1.0.0'); define('SAT_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SAT_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once SAT_PLUGIN_DIR . 'includes/class-survey-analytics-tool.php'; // 实例化主类 function sat_init() { $plugin = new Survey_Analytics_Tool(); $plugin->run(); } add_action('plugins_loaded', 'sat_init'); 3.3 数据库表创建 创建数据库表安装脚本: // 在includes/class-install.php中 class SAT_Install { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 问卷表 $surveys_table = $wpdb->prefix . 'surveys'; $sql1 = "CREATE TABLE IF NOT EXISTS $surveys_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, description text, status varchar(20) DEFAULT 'draft', created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, settings text, PRIMARY KEY (id) ) $charset_collate;"; // 问题表 $questions_table = $wpdb->prefix . 'survey_questions'; $sql2 = "CREATE TABLE IF NOT EXISTS $questions_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, survey_id mediumint(9) NOT NULL, question_text text NOT NULL, question_type varchar(50) NOT NULL, is_required tinyint(1) DEFAULT 0, sort_order int(11) DEFAULT 0, options text, PRIMARY KEY (id), KEY survey_id (survey_id) ) $charset_collate;"; // 更多表创建语句... require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); // 执行其他表的创建... } } 四、核心功能模块开发 4.1 问卷创建与管理模块 4.1.1 后台管理界面 创建问卷管理后台页面: // 在admin/class-admin-survey.php中 class SAT_Admin_Survey { public function add_admin_menu() { add_menu_page( '问卷调查管理', '问卷调查', 'manage_options', 'survey-analytics', array($this, 'render_survey_list'), 'dashicons-feedback', 30 ); add_submenu_page( 'survey-analytics', '创建新问卷', '创建问卷', 'manage_options', 'survey-analytics-new', array($this, 'render_survey_editor') ); } public function render_survey_list() { // 显示问卷列表 include SAT_PLUGIN_DIR . 'admin/views/survey-list.php'; } public function render_survey_editor() { // 显示问卷编辑器 include SAT_PLUGIN_DIR . 'admin/views/survey-editor.php'; } } 4.1.2 问卷编辑器前端实现 创建交互式问卷编辑器: <!-- admin/views/survey-editor.php --> <div class="wrap"> <h1>问卷编辑器</h1> <div id="survey-builder"> <!-- 问卷基本信息 --> <div class="survey-basic-info"> <input type="text" id="survey-title" placeholder="问卷标题"> <textarea id="survey-description" placeholder="问卷描述"></textarea> </div> <!-- 问题列表 --> <div id="questions-container"> <!-- 问题将通过JavaScript动态添加 --> </div> <!-- 添加问题按钮 --> <button id="add-question" class="button button-primary">添加问题</button> <!-- 保存问卷 --> <button id="save-survey" class="button button-primary">保存问卷</button> </div> </div> <script> // 问题类型模板 const questionTemplates = { 'single_choice': ` <div class="question-item" data-type="single_choice"> <div class="question-header"> <input type="text" class="question-text" placeholder="请输入问题"> <select class="question-required"> <option value="0">非必填</option> <option value="1">必填</option> </select> <button class="remove-question">删除</button> </div> <div class="options-container"> <div class="option-item"> <input type="radio" disabled> <input type="text" class="option-text" placeholder="选项文本"> <button class="remove-option">删除</button> </div> </div> <button class="add-option">添加选项</button> </div> `, // 其他问题类型模板... }; // 初始化问卷编辑器 jQuery(document).ready(function($) { // 添加问题 $('#add-question').on('click', function() { const questionType = 'single_choice'; // 默认单选,实际中可以让用户选择 $('#questions-container').append(questionTemplates[questionType]); }); // 保存问卷 $('#save-survey').on('click', function() { const surveyData = { title: $('#survey-title').val(), description: $('#survey-description').val(), questions: [] }; // 收集所有问题数据 $('.question-item').each(function() { const question = { text: $(this).find('.question-text').val(), type: $(this).data('type'), required: $(this).find('.question-required').val(), options: [] }; // 收集选项数据 $(this).find('.option-item').each(function() { question.options.push($(this).find('.option-text').val()); }); surveyData.questions.push(question); }); // 发送到服务器保存 $.ajax({ url: sat_ajax.ajax_url, type: 'POST', data: { action: 'save_survey', nonce: sat_ajax.nonce, survey: surveyData }, success: function(response) { if (response.success) { alert('问卷保存成功!'); } else { alert('保存失败:' + response.data.message); } } }); }); }); </script> 4.2 前端问卷展示与数据收集 4.2.1 创建短代码显示问卷 // 在includes/class-shortcodes.php中 class SAT_Shortcodes { public function register_shortcodes() { add_shortcode('survey', array($this, 'render_survey')); } public function render_survey($atts) { $atts = shortcode_atts(array( 'id' => 0, 'title' => '问卷调查' ), $atts); $survey_id = intval($atts['id']); if (!$survey_id) { return '<p>请指定有效的问卷ID</p>'; } // 获取问卷数据 $survey = $this->get_survey_data($survey_id); if (!$survey) { return '<p>问卷不存在或已删除</p>'; } // 渲染问卷HTML ob_start(); include SAT_PLUGIN_DIR . 'public/views/survey-form.php'; return ob_get_clean(); } private function get_survey_data($survey_id) { global $wpdb; $table = $wpdb->prefix . 'surveys'; $survey = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table WHERE id = %d AND status = 'published'", $survey_id )); if ($survey) { // 获取问题数据 $questions_table = $wpdb->prefix . 'survey_questions'; $survey->questions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $questions_table WHERE survey_id = %d ORDER BY sort_order ASC", $survey_id )); // 解析问题选项 foreach ($survey->questions as &$question) { if ($question->options) { $question->options = maybe_unserialize($question->options); } } } return $survey; } } 4.2.2 问卷表单前端实现 <!-- public/views/survey-form.php --> <div class="sat-survey-container" data-survey-id="<?php echo $survey->id; ?>"> <h2 class="survey-title"><?php echo esc_html($survey->title); ?></h2> <?php if ($survey->description): ?> <div class="survey-description"> <?php echo wp_kses_post($survey->description); ?> </div> <?php endif; ?> <form id="sat-survey-form-<?php echo $survey->id; ?>" class="sat-survey-form"> <?php foreach ($survey->questions as $index => $question): ?> <div class="survey-question" data-question-id="<?php echo $question->id; ?>"> <div class="question-text"> <label> <?php echo ($index + 1) . '. ' . esc_html($question->question_text); ?> <?php if ($question->is_required): ?> <span class="required">*</span> <?php endif; ?> </label> </div> <div class="question-input"> <?php switch($question->question_type): case 'single_choice': ?> <?php foreach ($question->options as $option_index => $option): ?> <div class="option-item"> <input type="radio" id="q<?php echo $question->id; ?>_o<?php echo $option_index; ?>" name="question_<?php echo $question->id; ?>" value="<?php echo esc_attr($option); ?>" <?php echo $question->is_required ? 'required' : ''; ?>> <label for="q<?php echo $question->id; ?>_o<?php echo $option_index; ?>"> <?php echo esc_html($option); ?> </label> </div> <?php endforeach; ?> <?php break; case 'multiple_choice': ?> <!-- 多选实现 --> <?php break; case 'text': ?> <textarea name="question_<?php echo $question->id; ?>" <?php echo $question->is_required ? 'required' : ''; ?> rows="3"></textarea> <?php break; case 'rating': ?> <!-- 评分题实现 --> <?php break; endswitch; ?> </div> </div> <?php endforeach; ?> <div class="survey-submit"> <button type="submit" class="submit-button">提交问卷</button> </div> </form> <div id="survey-thankyou" style="display:none;"> <p>感谢您参与本次问卷调查!</p> </div> </div> <script> jQuery(document).ready(function($) { $('#sat-survey-form-<?php echo $survey->id; ?>').on('submit', function(e) { e.preventDefault(); const formData = $(this).serializeArray(); const surveyId = $(this).closest('.sat-survey-container').data('survey-id'); // 添加survey_id到数据中 formData.push({name: 'survey_id', value: surveyId}); $.ajax({ url: sat_public.ajax_url, type: 'POST', data: { action: 'submit_survey', nonce: sat_public.nonce, form_data: formData }, success: function(response) { if (response.success) { // 显示感谢信息 $('#sat-survey-form-<?php echo $survey->id; ?>').hide(); $('#survey-thankyou').show(); } else { alert('提交失败:' + response.data.message); } } }); }); }); </script> 4.3 数据存储与处理模块 4.3.1 数据提交处理 // 在includes/class-data-handler.php中 class SAT_Data_Handler { public function handle_survey_submission() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'sat_public_nonce')) { wp_die('安全验证失败'); } $survey_id = intval($_POST['survey_id']); $form_data = $_POST['form_data']; // 验证问卷是否存在且处于发布状态 $survey = $this->validate_survey($survey_id); if (!$survey) { wp_send_json_error(array('message' => '问卷不存在或已关闭')); } // 处理表单数据 $processed_data = array(); foreach ($form_data as $item) { if (strpos($item['name'], 'question_') === 0) { $question_id = str_replace('question_', '', $item['name']); $processed_data[$question_id] = $item['value']; } 4.3.2 数据验证与存储 // 验证必填问题 foreach ($survey->questions as $question) { if ($question->is_required && empty($processed_data[$question->id])) { wp_send_json_error(array( 'message' => '请填写所有必填问题', 'question_id' => $question->id )); } } // 存储回答数据 $response_id = $this->store_response($survey_id, $processed_data); if ($response_id) { // 触发数据收集完成钩子 do_action('sat_survey_completed', $survey_id, $response_id, $processed_data); wp_send_json_success(array( 'message' => '提交成功', 'response_id' => $response_id )); } else { wp_send_json_error(array('message' => '数据存储失败')); } } private function store_response($survey_id, $data) { global $wpdb; // 插入主回答记录 $responses_table = $wpdb->prefix . 'survey_responses'; $wpdb->insert( $responses_table, array( 'survey_id' => $survey_id, 'user_id' => get_current_user_id(), 'ip_address' => $this->get_client_ip(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'created_at' => current_time('mysql') ), array('%d', '%d', '%s', '%s', '%s') ); $response_id = $wpdb->insert_id; if (!$response_id) { return false; } // 存储每个问题的回答 $answers_table = $wpdb->prefix . 'survey_answers'; foreach ($data as $question_id => $answer_value) { $wpdb->insert( $answers_table, array( 'response_id' => $response_id, 'question_id' => $question_id, 'answer_value' => is_array($answer_value) ? serialize($answer_value) : $answer_value, 'created_at' => current_time('mysql') ), array('%d', '%d', '%s', '%s') ); } return $response_id; } private function get_client_ip() { $ipaddress = ''; if (isset($_SERVER['HTTP_CLIENT_IP'])) $ipaddress = $_SERVER['HTTP_CLIENT_IP']; else if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) $ipaddress = $_SERVER['HTTP_X_FORWARDED_FOR']; else if(isset($_SERVER['HTTP_X_FORWARDED'])) $ipaddress = $_SERVER['HTTP_X_FORWARDED']; else if(isset($_SERVER['HTTP_FORWARDED_FOR'])) $ipaddress = $_SERVER['HTTP_FORWARDED_FOR']; else if(isset($_SERVER['HTTP_FORWARDED'])) $ipaddress = $_SERVER['HTTP_FORWARDED']; else if(isset($_SERVER['REMOTE_ADDR'])) $ipaddress = $_SERVER['REMOTE_ADDR']; else $ipaddress = 'UNKNOWN'; return $ipaddress; } } 4.4 数据分析与统计模块 4.4.1 数据统计功能 // 在includes/class-analytics.php中 class SAT_Analytics { public function get_survey_statistics($survey_id) { global $wpdb; $statistics = array( 'basic' => $this->get_basic_stats($survey_id), 'questions' => array(), 'trends' => $this->get_response_trends($survey_id) ); // 获取所有问题 $questions_table = $wpdb->prefix . 'survey_questions'; $questions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $questions_table WHERE survey_id = %d ORDER BY sort_order ASC", $survey_id )); // 为每个问题生成统计 foreach ($questions as $question) { $statistics['questions'][$question->id] = $this->analyze_question($question); } return $statistics; } private function get_basic_stats($survey_id) { global $wpdb; $responses_table = $wpdb->prefix . 'survey_responses'; // 总回答数 $total_responses = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $responses_table WHERE survey_id = %d", $survey_id )); // 今日回答数 $today_responses = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $responses_table WHERE survey_id = %d AND DATE(created_at) = CURDATE()", $survey_id )); // 平均完成时间(如果有记录时间的话) $avg_completion_time = null; return array( 'total_responses' => (int)$total_responses, 'today_responses' => (int)$today_responses, 'completion_rate' => $this->calculate_completion_rate($survey_id), 'avg_completion_time' => $avg_completion_time ); } private function analyze_question($question) { global $wpdb; $answers_table = $wpdb->prefix . 'survey_answers'; $responses_table = $wpdb->prefix . 'survey_responses'; $analysis = array( 'question_id' => $question->id, 'question_text' => $question->question_text, 'question_type' => $question->question_type, 'total_answers' => 0, 'data' => array() ); switch ($question->question_type) { case 'single_choice': case 'multiple_choice': $analysis = $this->analyze_choice_question($question, $analysis); break; case 'rating': $analysis = $this->analyze_rating_question($question, $analysis); break; case 'text': $analysis = $this->analyze_text_question($question, $analysis); break; } return $analysis; } private function analyze_choice_question($question, $analysis) { global $wpdb; $answers_table = $wpdb->prefix . 'survey_answers'; $options = maybe_unserialize($question->options); // 获取每个选项的选择次数 $option_counts = array(); foreach ($options as $index => $option) { $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $answers_table WHERE question_id = %d AND answer_value LIKE %s", $question->id, '%' . $wpdb->esc_like($option) . '%' )); $option_counts[$option] = (int)$count; } // 计算百分比 $total = array_sum($option_counts); $analysis['total_answers'] = $total; foreach ($option_counts as $option => $count) { $percentage = $total > 0 ? round(($count / $total) * 100, 2) : 0; $analysis['data'][] = array( 'option' => $option, 'count' => $count, 'percentage' => $percentage ); } return $analysis; } private function get_response_trends($survey_id) { global $wpdb; $responses_table = $wpdb->prefix . 'survey_responses'; // 获取最近30天的回答趋势 $trends = $wpdb->get_results($wpdb->prepare( "SELECT DATE(created_at) as date, COUNT(*) as count FROM $responses_table WHERE survey_id = %d AND created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(created_at) ORDER BY date ASC", $survey_id )); return $trends; } } 4.5 可视化报告生成模块 4.5.1 图表生成与展示 <!-- admin/views/analytics-dashboard.php --> <div class="wrap"> <h1>问卷数据分析报告</h1> <div class="sat-analytics-dashboard"> <!-- 基本统计 --> <div class="basic-stats"> <div class="stat-card"> <h3>总回答数</h3> <div class="stat-value" id="total-responses">0</div> </div> <div class="stat-card"> <h3>今日回答</h3> <div class="stat-value" id="today-responses">0</div> </div> <div class="stat-card"> <h3>完成率</h3> <div class="stat-value" id="completion-rate">0%</div> </div> </div> <!-- 回答趋势图 --> <div class="chart-container"> <h3>回答趋势</h3> <canvas id="response-trend-chart" width="800" height="300"></canvas> </div> <!-- 问题分析 --> <div id="question-analytics"> <!-- 每个问题的分析将通过JavaScript动态加载 --> </div> <!-- 数据导出 --> <div class="export-options"> <button id="export-csv" class="button">导出CSV</button> <button id="export-excel" class="button">导出Excel</button> <button id="export-pdf" class="button">导出PDF报告</button> </div> </div> </div> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script> jQuery(document).ready(function($) { const surveyId = <?php echo $survey_id; ?>; // 加载统计数据 function loadAnalytics() { $.ajax({ url: sat_admin.ajax_url, type: 'GET', data: { action: 'get_survey_analytics', survey_id: surveyId, nonce: sat_admin.nonce }, success: function(response) { if (response.success) { renderAnalytics(response.data); } } }); } function renderAnalytics(data) { // 更新基本统计 $('#total-responses').text(data.basic.total_responses); $('#today-responses').text(data.basic.today_responses); $('#completion-rate').text(data.basic.completion_rate + '%'); // 渲染趋势图 renderTrendChart(data.trends); // 渲染问题分析 renderQuestionAnalytics(data.questions); } function renderTrendChart(trends) { const dates = trends.map(item => item.date); const counts = trends.map(item => parseInt(item.count)); const ctx = document.getElementById('response-trend-chart').getContext('2d'); new Chart(ctx, { type: 'line', data: { labels: dates, datasets: [{ label: '回答数量', data: counts, borderColor: 'rgb(75, 192, 192)', tension: 0.1, fill: true, backgroundColor: 'rgba(75, 192, 192, 0.2)' }] }, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: '回答趋势图' } } } }); } function renderQuestionAnalytics(questions) { const container = $('#question-analytics'); container.empty(); Object.values(questions).forEach(question => { const questionHtml = ` <div class="question-analysis" data-question-id="${question.question_id}"> <h4>${question.question_text}</h4> <div class="chart-container"> <canvas id="chart-${question.question_id}" width="400" height="200"></canvas> </div> <div class="analysis-table"> <table> <thead> <tr> <th>选项</th> <th>数量</th> <th>百分比</th> </tr> </thead> <tbody> ${question.data.map(item => ` <tr> <td>${item.option}</td> <td>${item.count}</td> <td>${item.percentage}%</td> </tr> `).join('')} </tbody> </table> </div> </div> `; container.append(questionHtml); // 为每个问题渲染图表 renderQuestionChart(question); }); } function renderQuestionChart(question) { const ctx = document.getElementById(`chart-${question.question_id}`).getContext('2d'); const labels = question.data.map(item => item.option); const data = question.data.map(item => item.count); const backgroundColors = generateColors(data.length); new Chart(ctx, { type: question.question_type === 'rating' ? 'bar' : 'pie', data: { labels: labels, datasets: [{ data: data, backgroundColor: backgroundColors, borderWidth: 1 }] }, options: { responsive: true, plugins: { legend: { position: 'right', }, title: { display: true, text: question.question_text.substring(0, 50) + '...' } } } }); } function generateColors(count) { const colors = []; for (let i = 0; i < count; i++) { const hue = (i * 360 / count) % 360; colors.push(`hsl(${hue}, 70%, 60%)`); } return colors; } // 初始化加载 loadAnalytics(); // 设置自动刷新 setInterval(loadAnalytics, 300000); // 每5分钟刷新一次 }); </script> 4.5.2 数据导出功能 // 在includes/class-export.php中 class SAT_Export { public function export_csv($survey_id) { global $wpdb; // 获取问卷数据 $survey = $this->get_survey_with_responses($survey_id); if (!$survey) { return false; } // 设置CSV头部 $filename = 'survey_' . $survey_id . '_' . date('Ymd_His') . '.csv'; header('Content-Type: text/csv; charset=utf-8'); header('Content-Disposition: attachment; filename="' . $filename . '"'); $output = fopen('php://output', 'w'); // 写入标题行 $headers = array('回答ID', '提交时间', '用户ID', 'IP地址'); foreach ($survey->questions as $question) { $headers[] = $question->question_text; } fputcsv($output, $headers); // 写入数据行 foreach ($survey->responses as $response) { $row = array( $response->id, $response->created_at, $response->user_id, $response->ip_address ); foreach ($survey->questions as $question) { $answer = $this->find_answer($response->answers, $question->id); $row[] = $answer ? $answer->answer_value : ''; } fputcsv($output, $row); } fclose($output); exit; } public function export_pdf_report($survey_id) { // 使用TCPDF或DomPDF生成PDF报告 require_once SAT_PLUGIN_DIR . 'vendor/autoload.php'; $analytics = SAT_Analytics::get_survey_statistics($survey_id); $survey = $this->get_survey_data($survey_id); // 创建PDF $pdf = new TCPDF(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); // 设置文档信息 $pdf->SetCreator('Survey Analytics Tool'); $pdf->SetAuthor('WordPress'); $pdf->SetTitle('问卷分析报告 - ' . $survey->title); // 添加页面 $pdf->AddPage(); // 添加标题 $pdf->SetFont('stsongstdlight', 'B', 16); $pdf->Cell(0, 10, $survey->title, 0, 1, 'C'); // 添加基本统计 $pdf->SetFont('stsongstdlight', '', 12); $pdf->Ln(10); $pdf->Cell(0, 10, '基本统计:', 0, 1); $pdf->Cell(0, 10, '总回答数: ' . $analytics['basic']['total_responses'], 0, 1); $pdf->Cell(0, 10, '今日回答: ' . $analytics['basic']['today_responses'], 0, 1); // 添加问题分析 foreach ($analytics['questions'] as $question) { $pdf->AddPage(); $pdf->SetFont('stsongstdlight', 'B', 14); $pdf->Cell(0, 10, $question['question_text'], 0, 1); $pdf->SetFont('stsongstdlight', '', 12); // 添加图表或表格... } // 输出PDF
发表评论WordPress集成教程:连接智能硬件并展示物联网设备数据面板,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:当WordPress遇见物联网 在当今万物互联的时代,物联网设备正以前所未有的速度渗透到我们生活的方方面面。从智能家居的温度传感器到工业环境的监控设备,海量数据正源源不断地产生。然而,这些数据的价值往往受限于展示和分析的平台。与此同时,WordPress作为全球最流行的内容管理系统,已经超越了简单的博客平台,发展成为功能强大的网站构建工具。本教程将深入探讨如何将这两者结合——通过WordPress的代码二次开发,连接智能硬件并创建专业的物联网设备数据面板,同时实现多种常用互联网小工具功能。 第一部分:准备工作与环境搭建 1.1 WordPress开发环境配置 在开始集成物联网设备之前,我们需要建立一个适合开发的WordPress环境。建议使用本地开发环境如XAMPP、MAMP或Local by Flywheel,这些工具可以快速搭建包含Apache、MySQL和PHP的完整环境。 对于物联网开发,我们需要确保WordPress安装满足以下条件: WordPress 5.0及以上版本 PHP 7.4及以上(建议8.0+以获得更好性能) 启用REST API功能 安装并激活调试插件,如Query Monitor和Debug Bar 1.2 物联网硬件连接基础 物联网设备与WordPress的通信通常通过以下几种方式实现: HTTP/HTTPS请求:设备直接向WordPress REST API端点发送数据 MQTT协议:轻量级的发布/订阅消息传输协议,适合低带宽环境 WebSocket连接:实现实时双向通信 第三方物联网平台中转:通过如ThingsBoard、AWS IoT等平台收集数据,再同步到WordPress 在本教程中,我们将主要使用HTTP REST API方式,因为它与WordPress的集成最为直接,且大多数智能硬件都支持HTTP客户端库。 1.3 必要的开发工具和库 Postman或Insomnia:用于测试API端点 Arduino IDE或PlatformIO:用于嵌入式设备编程 Node-RED(可选):可视化物联网编程工具,可用于数据预处理 Chart.js或D3.js:用于数据可视化 Vue.js或React(可选):用于构建交互式前端面板 第二部分:创建WordPress REST API端点接收物联网数据 2.1 理解WordPress REST API架构 WordPress REST API提供了一套标准的、易于使用的接口,允许外部应用与WordPress进行数据交互。默认情况下,WordPress已经为文章、页面、用户等核心内容类型提供了API端点。 对于物联网数据,我们需要创建自定义端点来接收和存储设备数据。这可以通过两种方式实现: 使用register_rest_route函数创建全新端点 扩展现有端点(如文章类型)来存储设备数据 2.2 创建自定义REST API端点 下面是一个完整的插件示例,用于创建接收物联网设备数据的API端点: <?php /** * Plugin Name: IoT Device Data Manager * Description: 接收和展示物联网设备数据的WordPress插件 * Version: 1.0.0 * Author: Your Name */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } class IoT_Device_Manager { private $table_name; public function __construct() { global $wpdb; $this->table_name = $wpdb->prefix . 'iot_device_data'; // 注册激活钩子 register_activation_hook(__FILE__, array($this, 'create_database_table')); // 注册REST API路由 add_action('rest_api_init', array($this, 'register_rest_routes')); // 注册管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); // 注册短代码 add_shortcode('iot_dashboard', array($this, 'iot_dashboard_shortcode')); } // 创建数据库表 public function create_database_table() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS {$this->table_name} ( id mediumint(9) NOT NULL AUTO_INCREMENT, device_id varchar(100) NOT NULL, device_type varchar(50) NOT NULL, temperature decimal(5,2), humidity decimal(5,2), pressure decimal(7,2), pm25 int(11), voltage decimal(6,3), status varchar(20) DEFAULT 'active', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), INDEX device_index (device_id), INDEX time_index (created_at) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } // 注册REST API路由 public function register_rest_routes() { // 接收设备数据的端点 register_rest_route('iot/v1', '/data', array( 'methods' => 'POST', 'callback' => array($this, 'handle_device_data'), 'permission_callback' => array($this, 'verify_device_token'), 'args' => array( 'device_id' => array( 'required' => true, 'validate_callback' => function($param) { return is_string($param) && strlen($param) <= 100; } ), 'temperature' => array( 'required' => false, 'validate_callback' => function($param) { return is_numeric($param) && $param >= -50 && $param <= 100; } ), 'humidity' => array( 'required' => false, 'validate_callback' => function($param) { return is_numeric($param) && $param >= 0 && $param <= 100; } ), // 其他参数... ) )); // 获取设备数据的端点 register_rest_route('iot/v1', '/data/(?P<device_id>[a-zA-Z0-9_-]+)', array( 'methods' => 'GET', 'callback' => array($this, 'get_device_data'), 'permission_callback' => function() { return current_user_can('edit_posts'); } )); } // 处理设备数据 public function handle_device_data($request) { global $wpdb; $parameters = $request->get_params(); // 验证必要参数 if (empty($parameters['device_id'])) { return new WP_Error('missing_device_id', '设备ID是必需的', array('status' => 400)); } // 准备插入数据 $data = array( 'device_id' => sanitize_text_field($parameters['device_id']), 'device_type' => sanitize_text_field($parameters['device_type'] ?? 'unknown'), 'temperature' => isset($parameters['temperature']) ? floatval($parameters['temperature']) : null, 'humidity' => isset($parameters['humidity']) ? floatval($parameters['humidity']) : null, 'pressure' => isset($parameters['pressure']) ? floatval($parameters['pressure']) : null, 'pm25' => isset($parameters['pm25']) ? intval($parameters['pm25']) : null, 'voltage' => isset($parameters['voltage']) ? floatval($parameters['voltage']) : null, 'status' => 'active' ); // 插入数据库 $result = $wpdb->insert($this->table_name, $data); if ($result === false) { return new WP_Error('db_insert_error', '数据插入失败', array('status' => 500)); } // 触发数据接收动作,供其他插件使用 do_action('iot_device_data_received', $data, $wpdb->insert_id); return rest_ensure_response(array( 'success' => true, 'message' => '数据接收成功', 'data_id' => $wpdb->insert_id, 'timestamp' => current_time('mysql') )); } // 验证设备令牌 public function verify_device_token($request) { // 从请求头获取API密钥 $api_key = $request->get_header('X-IoT-API-Key'); if (empty($api_key)) { return false; } // 验证API密钥(这里应该从数据库或配置中获取有效密钥) $valid_keys = get_option('iot_valid_api_keys', array()); return in_array($api_key, $valid_keys); } // 获取设备数据 public function get_device_data($request) { global $wpdb; $device_id = $request->get_param('device_id'); $limit = $request->get_param('limit') ? intval($request->get_param('limit')) : 100; $query = $wpdb->prepare( "SELECT * FROM {$this->table_name} WHERE device_id = %s ORDER BY created_at DESC LIMIT %d", $device_id, $limit ); $results = $wpdb->get_results($query, ARRAY_A); if (empty($results)) { return rest_ensure_response(array( 'success' => false, 'message' => '未找到设备数据' )); } return rest_ensure_response(array( 'success' => true, 'data' => $results, 'count' => count($results) )); } // 添加管理菜单 public function add_admin_menu() { add_menu_page( '物联网设备管理', 'IoT设备', 'manage_options', 'iot-device-manager', array($this, 'display_admin_page'), 'dashicons-admin-generic', 30 ); add_submenu_page( 'iot-device-manager', '设备数据', '数据查看', 'manage_options', 'iot-device-data', array($this, 'display_device_data_page') ); } // 显示管理页面 public function display_admin_page() { ?> <div class="wrap"> <h1>物联网设备管理</h1> <div class="card"> <h2>API端点信息</h2> <p><strong>数据提交URL:</strong> <?php echo rest_url('iot/v1/data'); ?></p> <p><strong>请求方法:</strong> POST</p> <p><strong>内容类型:</strong> application/json</p> <h3>请求示例</h3> <pre> { "device_id": "sensor_001", "device_type": "environment", "temperature": 23.5, "humidity": 65.2, "pressure": 1013.25, "pm25": 35 } </pre> </div> </div> <?php } // 短代码显示仪表板 public function iot_dashboard_shortcode($atts) { $atts = shortcode_atts(array( 'device_id' => '', 'title' => '物联网设备数据面板', 'show_charts' => 'true' ), $atts, 'iot_dashboard'); // 生成唯一ID用于JavaScript $dashboard_id = 'iot-dashboard-' . uniqid(); ob_start(); ?> <div id="<?php echo esc_attr($dashboard_id); ?>" class="iot-dashboard"> <h3><?php echo esc_html($atts['title']); ?></h3> <div class="iot-stats-grid"> <div class="iot-stat-card"> <div class="stat-label">当前温度</div> <div class="stat-value temperature">--</div> <div class="stat-unit">°C</div> </div> <div class="iot-stat-card"> <div class="stat-label">当前湿度</div> <div class="stat-value humidity">--</div> <div class="stat-unit">%</div> </div> <div class="iot-stat-card"> <div class="stat-label">空气质量</div> <div class="stat-value pm25">--</div> <div class="stat-unit">PM2.5</div> </div> <div class="iot-stat-card"> <div class="stat-label">设备状态</div> <div class="stat-value status">--</div> <div class="stat-unit"></div> </div> </div> <?php if ($atts['show_charts'] === 'true') : ?> <div class="iot-charts-container"> <div class="chart-container"> <canvas id="<?php echo esc_attr($dashboard_id); ?>-temp-chart"></canvas> </div> <div class="chart-container"> <canvas id="<?php echo esc_attr($dashboard_id); ?>-humidity-chart"></canvas> </div> </div> <?php endif; ?> <div class="iot-data-table"> <table> <thead> <tr> <th>时间</th> <th>温度(°C)</th> <th>湿度(%)</th> <th>气压(hPa)</th> <th>PM2.5</th> </tr> </thead> <tbody> <!-- 数据将通过JavaScript动态加载 --> </tbody> </table> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { // 初始化物联网仪表板 initIoTDashboard('<?php echo esc_js($dashboard_id); ?>', '<?php echo esc_js($atts['device_id']); ?>'); }); </script> <?php return ob_get_clean(); } } // 初始化插件 new IoT_Device_Manager(); // 添加前端样式和脚本 add_action('wp_enqueue_scripts', function() { if (has_shortcode(get_post()->post_content, 'iot_dashboard')) { // 引入Chart.js用于数据可视化 wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', array(), '3.7.0', true); // 引入自定义脚本 wp_enqueue_script('iot-dashboard', plugin_dir_url(__FILE__) . 'js/iot-dashboard.js', array('jquery', 'chart-js'), '1.0.0', true); // 传递AJAX URL到前端 wp_localize_script('iot-dashboard', 'iot_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'rest_url' => rest_url('iot/v1/data/'), 'nonce' => wp_create_nonce('iot_nonce') )); // 添加样式 wp_enqueue_style('iot-dashboard-style', plugin_dir_url(__FILE__) . 'css/iot-dashboard.css'); } }); // 添加AJAX处理 add_action('wp_ajax_get_iot_data', 'handle_iot_data_ajax'); add_action('wp_ajax_nopriv_get_iot_data', 'handle_iot_data_ajax'); function handle_iot_data_ajax() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'iot_nonce')) { wp_die('权限验证失败'); } global $wpdb; $table_name = $wpdb->prefix . 'iot_device_data'; $device_id = sanitize_text_field($_POST['device_id']); $limit = intval($_POST['limit'] ?? 50); $data = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$table_name} WHERE device_id = %s ORDER BY created_at DESC LIMIT %d", $device_id, $limit ), ARRAY_A); wp_send_json_success($data); } 2.3 前端JavaScript代码 (iot-dashboard.js) // iot-dashboard.js function initIoTDashboard(dashboardId, deviceId) { const dashboard = document.getElementById(dashboardId); if (!dashboard) return; // 初始化图表 let tempChart = null; let humidityChart = null; // 获取数据并更新仪表板 function updateDashboard() { if (!deviceId) { console.error('设备ID未指定'); return; } // 使用REST API获取数据 fetch(`${iot_ajax.rest_url}${deviceId}?limit=50`) .then(response => response.json()) .then(data => { if (data.success && data.data.length > 0) { updateStats(data.data[0]); // 更新最新数据 updateCharts(data.data); // 更新图表 updateTable(data.data); // 更新表格 } }) .catch(error => console.error('获取数据失败:', error)); } // 更新统计数据 function updateStats(latestData) { const tempElement = dashboard.querySelector('.temperature'); const humidityElement = dashboard.querySelector('.humidity'); const pm25Element = dashboard.querySelector('.pm25'); const statusElement = dashboard.querySelector('.status'); if (tempElement && latestData.temperature !== null) { tempElement.textContent = latestData.temperature.toFixed(1); // 根据温度设置颜色 tempElement.style.color = getTemperatureColor(latestData.temperature); } if (humidityElement && latestData.humidity !== null) { humidityElement.textContent = latestData.humidity.toFixed(1); } if (pm25Element && latestData.pm25 !== null) { pm25Element.textContent = latestData.pm25; // 根据PM2.5设置颜色 pm25Element.style.color = getPM25Color(latestData.pm25); } statusElement && latestData.status) { statusElement.textContent = latestData.status; statusElement.className = `status status-${latestData.status}`; } } // 更新图表 function updateCharts(data) { // 反转数据,使时间从旧到新 const reversedData = [...data].reverse(); // 准备图表数据 const labels = reversedData.map(item => { const date = new Date(item.created_at); return `${date.getHours()}:${date.getMinutes().toString().padStart(2, '0')}`; }); const temperatures = reversedData.map(item => item.temperature); const humidities = reversedData.map(item => item.humidity); // 温度图表 const tempCanvas = document.getElementById(`${dashboardId}-temp-chart`); if (tempCanvas) { if (tempChart) { tempChart.destroy(); } tempChart = new Chart(tempCanvas.getContext('2d'), { type: 'line', data: { labels: labels, datasets: [{ label: '温度 (°C)', data: temperatures, borderColor: 'rgb(255, 99, 132)', backgroundColor: 'rgba(255, 99, 132, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: '温度变化趋势' } }, scales: { y: { beginAtZero: false, title: { display: true, text: '温度 (°C)' } } } } }); } // 湿度图表 const humidityCanvas = document.getElementById(`${dashboardId}-humidity-chart`); if (humidityCanvas) { if (humidityChart) { humidityChart.destroy(); } humidityChart = new Chart(humidityCanvas.getContext('2d'), { type: 'line', data: { labels: labels, datasets: [{ label: '湿度 (%)', data: humidities, borderColor: 'rgb(54, 162, 235)', backgroundColor: 'rgba(54, 162, 235, 0.1)', tension: 0.4, fill: true }] }, options: { responsive: true, plugins: { legend: { position: 'top', }, title: { display: true, text: '湿度变化趋势' } }, scales: { y: { beginAtZero: false, min: 0, max: 100, title: { display: true, text: '湿度 (%)' } } } } }); } } // 更新数据表格 function updateTable(data) { const tbody = dashboard.querySelector('.iot-data-table tbody'); if (!tbody) return; tbody.innerHTML = ''; data.forEach(item => { const row = document.createElement('tr'); const date = new Date(item.created_at); const timeString = `${date.getFullYear()}-${(date.getMonth()+1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')} ${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`; row.innerHTML = ` <td>${timeString}</td> <td>${item.temperature !== null ? item.temperature.toFixed(1) : '--'}</td> <td>${item.humidity !== null ? item.humidity.toFixed(1) : '--'}</td> <td>${item.pressure !== null ? item.pressure.toFixed(1) : '--'}</td> <td>${item.pm25 !== null ? item.pm25 : '--'}</td> `; tbody.appendChild(row); }); } // 辅助函数:根据温度获取颜色 function getTemperatureColor(temp) { if (temp < 10) return '#3498db'; // 冷 - 蓝色 if (temp < 20) return '#2ecc71'; // 凉爽 - 绿色 if (temp < 28) return '#f1c40f'; // 舒适 - 黄色 if (temp < 35) return '#e67e22'; // 温暖 - 橙色 return '#e74c3c'; // 热 - 红色 } // 辅助函数:根据PM2.5获取颜色 function getPM25Color(pm25) { if (pm25 <= 35) return '#2ecc71'; // 优 - 绿色 if (pm25 <= 75) return '#f1c40f'; // 良 - 黄色 if (pm25 <= 115) return '#e67e22'; // 轻度污染 - 橙色 if (pm25 <= 150) return '#e74c3c'; // 中度污染 - 红色 return '#8e44ad'; // 重度污染 - 紫色 } // 初始加载数据 updateDashboard(); // 设置定时刷新(每30秒) setInterval(updateDashboard, 30000); // 添加手动刷新按钮 const refreshButton = document.createElement('button'); refreshButton.textContent = '刷新数据'; refreshButton.className = 'iot-refresh-btn'; refreshButton.onclick = updateDashboard; const header = dashboard.querySelector('h3'); if (header) { header.appendChild(refreshButton); } } #### 2.4 前端CSS样式 (iot-dashboard.css) / iot-dashboard.css /.iot-dashboard { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; padding: 20px; background: #f8f9fa; border-radius: 10px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } .iot-dashboard h3 { margin-top: 0; color: #2c3e50; display: flex; justify-content: space-between; align-items: center; } .iot-stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0; } .iot-stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); text-align: center; transition: transform 0.3s ease; } .iot-stat-card:hover { transform: translateY(-5px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); } .stat-label { font-size: 14px; color: #7f8c8d; margin-bottom: 10px; text-transform: uppercase; letter-spacing: 1px; } .stat-value { font-size: 36px; font-weight: bold; color: #2c3e50; margin: 10px 0; transition: color 0.3s ease; } .stat-unit { font-size: 14px; color: #95a5a6; } .iot-charts-container { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 30px 0; } .chart-container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); } .iot-data-table { margin-top: 30px; overflow-x: auto; } .iot-data-table table { width: 100%; border-collapse: collapse; background: white; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 5px rgba(0,0,0,0.05); } .iot-data-table th { background: #34495e; color: white; padding: 15px; text-align: left; font-weight: 600; } .iot-data-table td { padding: 12px 15px; border-bottom: 1px solid #ecf0f1; } .iot-data-table tr:hover { background: #f8f9fa; } .iot-refresh-btn { background: #3498db; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background 0.3s ease; } .iot-refresh-btn:hover { background: #2980b9; } / 状态指示器 /.status-active { color: #27ae60; } .status-inactive { color: #e74c3c; } .status-warning { color: #f39c12; } / 响应式设计 /@media (max-width: 768px) { .iot-stats-grid { grid-template-columns: repeat(2, 1fr); } .iot-charts-container { grid-template-columns: 1fr; } } @media (max-width: 480px) { .iot-stats-grid { grid-template-columns: 1fr; } .iot-dashboard { padding: 10px; } } ### 第三部分:智能硬件端代码实现 #### 3.1 Arduino ESP8266/ESP32示例代码 // iot_sensor_to_wordpress.ino include <ESP8266WiFi.h> // 对于ESP8266 // #include <WiFi.h> // 对于ESP32 include <ArduinoJson.h> include <DHT.h> // 温湿度传感器库 // WiFi配置const char* ssid = "你的WiFi名称";const char* password = "你的WiFi密码"; // WordPress站点配置const char* wordpress_host = "你的网站域名";const int wordpress_port = 443; // HTTPS端口const char* api_endpoint = "/wp-json/iot/v1/data";const char* api_key = "你的API密钥"; // 传感器配置 define DHTPIN D4 // DHT传感器数据引脚 define DHTTYPE DHT22 // DHT22传感器类型 DHT dht(DHTPIN, DHTTYPE); // 设备ID(每个设备唯一)const String device_id = "sensor_001";const String device_type = "environment"; // 定时器unsigned long previousMillis = 0;const long interval = 30000; // 30秒发送一次数据 void setup() { Serial.begin(115200); delay(1000); Serial.println("物联网设备启动中..."); // 初始化传感器 dht.begin(); // 连接WiFi connectToWiFi(); Serial.println("设备准备就绪");} void loop() { unsigned long currentMillis = millis(); // 定时发送数据 if (currentMillis - previousMillis >= interval) { previousMillis = currentMillis; // 读取传感器数据 float temperature = dht.readTemperature(); float humidity = dht.readHumidity(); // 检查读取是否成功 if (isnan(temperature) || isnan(humidity)) { Serial.println("读取传感器数据失败"); return; } // 发送数据到WordPress sendToWordPress(temperature, humidity); } // 其他任务... delay(1000);} void connectToWiFi() { Serial.print("连接WiFi: "); Serial.println(ssid); WiFi.begin(ssid, password); int attempts = 0; while (WiFi.status() != WL_CONNECTED && attempts < 20) { delay(500); Serial.print("."); attempts++; } if (WiFi.status() == WL_CONNECTED) { Serial.println("nWiFi连接成功!"); Serial.print("IP地址: "); Serial.println(WiFi.localIP()); } else { Serial.println("nWiFi连接失败"); }} void sendToWordPress(float temperature, float humidity) { Serial.println("准备发送数据到WordPress..."); // 创建JSON数据 DynamicJsonDocument doc(256); doc["device_id"] = device_id; doc["device_type"] = device_type; doc["temperature"] = temperature; doc["humidity"] = humidity; doc["voltage"] = getBatteryVoltage(); // 假设有电池电压读取函数 String jsonData; serializeJson(doc, jsonData); // 使用WiFiClientSecure进行HTTPS连接 WiFiClientSecure client; client.setInsecure(); // 跳过证书验证(生产环境应使用证书验证) if (!client.connect(wordpress_host, wordpress_port)) { Serial.println("连接WordPress失败"); return; } // 构建HTTP请求 String request = String("POST ") + api_endpoint + " HTTP/1.1rn" + "Host: " + wordpress_host + "rn" + "User-Agent: IoT-Device/1.0rn" + "X-IoT-API-Key: " + api_key + "rn" + "Content-Type: application/jsonrn" + "Content-Length: " + jsonData.length() + "rn" + "Connection: closernrn" + jsonData; // 发送请求 client.print(request); // 等待响应 unsigned long timeout = millis(); while (client.available() == 0) { if (millis() - timeout > 5000) { Serial.println("请求超时"); client.stop(); return; } } // 读取响应 while (client.available()) { String line = client.readStringUntil('r'); Serial.print(line); } Serial.println("n数据发送完成"); client.stop();} float getBatteryVoltage() { // 模拟读取电池电压(实际实现取决于硬件) return 3.7; // 示例值} #### 3.2 Raspberry Pi Python示例代码 wordpress_iot_client.py import requestsimport jsonimport timeimport loggingfrom datetime import datetimeimport boardimport adafruit_dht # 对于DHT传感器import psutil # 用于系统监控 配置日志 logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s' )logger = logging.getLogger(__name__) class WordPressIoTClient: def __init__(self, config): self.config = config self.device_id = config['device_id'] self.api_url = f"https://{config['wordpress_host']}/wp-json/iot/v1/data" self.headers = { 'X-IoT-API-Key': config['api_key'], 'Content-Type': 'application/json' } # 初始化传感器 self.init_sensors() def init_sensors(self): """初始化传感器""" try: # DHT22温湿度传感器 self.dht_device = adafruit_dht.DHT22(board.D4) logger.info("DHT22传感器初始化成功") except Exception as e: logger.error(f"传感器初始化失败: {e}") self.dht_device = None def read_sensor_data(self): """读取传感器数据""" data = { 'device_id': self.device_id, 'device_type': 'raspberry_pi', 'timestamp': datetime.now().isoformat() } # 读取温湿度 if self.dht_device: try: temperature = self.dht_device.temperature humidity = self.dht_device.humidity if temperature is not None and humidity is not None: data['temperature'] = round(temperature, 2) data['humidity'] = round(humidity, 2) else: logger.warning("读取传感器数据失败") except RuntimeError as e: logger.error(f"读取传感器错误: {e}") # 读取系统信息 data['cpu_usage'] = psutil.cpu_percent() data['memory_usage'] = psutil.virtual_memory().percent data['disk_usage'] = psutil.disk_usage('/').percent # 读取GPIO状态(示例) data['gpio_status'] = self.read_gpio_status() return data def read_gpio_status(self): """读取GPIO状态""" # 这里可以添加实际的GPIO读取逻辑 return { 'pin_17': 1, 'pin_18': 0, 'pin_27': 1 } def send_data(self, data): """发送数据到WordPress""" try: response = requests.post( self.api_url, headers=self.headers, json=data, timeout=10 ) if response.status_code == 200: logger.info(f"数据发送成功: {response.json()}") return True else: logger.error(f"数据发送失败: {response.status_code} - {response.text}") return False except requests.exceptions.RequestException as e: logger.error(f"请求异常: {e}") return False def run(self, interval=30): """主循环""" logger
发表评论WordPress网站活动抽奖与互动游戏化组件开发详细教程 引言:为什么WordPress网站需要游戏化组件 在当今互联网环境中,用户参与度和互动性已成为网站成功的关键指标。无论是电商平台、内容网站还是企业官网,通过游戏化元素如抽奖、积分系统、进度条等互动组件,可以显著提升用户粘性、延长停留时间并促进转化。WordPress作为全球最流行的内容管理系统,拥有强大的扩展能力,通过代码二次开发,我们可以为网站添加各种互联网常用的小工具功能。 本教程将详细指导您如何为WordPress网站开发活动抽奖与互动游戏化组件,从环境搭建到功能实现,涵盖完整的技术流程。无论您是WordPress开发者还是有一定技术基础的网站管理员,都能通过本教程掌握相关技能。 第一章:开发环境准备与基础架构 1.1 开发环境配置 在开始开发之前,我们需要准备合适的开发环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:最新版本的WordPress(建议5.8+) 代码编辑器:VS Code、Sublime Text或PHPStorm 浏览器开发者工具:Chrome DevTools或Firefox Developer Tools 1.2 创建自定义插件 为了避免主题更新导致功能丢失,我们将创建一个独立的插件来实现所有功能: <?php /** * Plugin Name: 互动游戏化组件套件 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站添加抽奖、游戏化互动功能 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('GAMIFICATION_PLUGIN_PATH', plugin_dir_path(__FILE__)); define('GAMIFICATION_PLUGIN_URL', plugin_dir_url(__FILE__)); define('GAMIFICATION_VERSION', '1.0.0'); // 初始化插件 require_once GAMIFICATION_PLUGIN_PATH . 'includes/class-gamification-core.php'; function run_gamification_plugin() { $plugin = new Gamification_Core(); $plugin->run(); } run_gamification_plugin(); 1.3 数据库表设计 我们需要创建几个数据库表来存储游戏化组件的数据: // 在插件激活时创建数据库表 register_activation_hook(__FILE__, 'gamification_create_tables'); function gamification_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 抽奖活动表 $table_name = $wpdb->prefix . 'gamification_lotteries'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL, description text, start_date datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, end_date datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, status varchar(20) DEFAULT 'draft', settings longtext, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 抽奖奖品表 $table_name_prizes = $wpdb->prefix . 'gamification_prizes'; $sql2 = "CREATE TABLE IF NOT EXISTS $table_name_prizes ( id mediumint(9) NOT NULL AUTO_INCREMENT, lottery_id mediumint(9) NOT NULL, name varchar(200) NOT NULL, description text, image_url varchar(500), quantity int NOT NULL DEFAULT 1, probability decimal(5,4) NOT NULL DEFAULT 0.0, remaining int NOT NULL DEFAULT 0, type varchar(50) DEFAULT 'physical', PRIMARY KEY (id), KEY lottery_id (lottery_id) ) $charset_collate;"; // 用户参与记录表 $table_name_participants = $wpdb->prefix . 'gamification_participants'; $sql3 = "CREATE TABLE IF NOT EXISTS $table_name_participants ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned, lottery_id mediumint(9) NOT NULL, prize_id mediumint(9), win_status varchar(20) DEFAULT 'pending', participant_data longtext, ip_address varchar(45), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY lottery_id (lottery_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); dbDelta($sql2); dbDelta($sql3); } 第二章:抽奖系统核心功能开发 2.1 抽奖活动管理后台 首先创建抽奖活动的管理界面: // 在includes/class-gamification-admin.php中 class Gamification_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', 'gamification', array($this, 'display_main_page'), 'dashicons-games', 30 ); add_submenu_page( 'gamification', '抽奖活动', '抽奖活动', 'manage_options', 'gamification-lotteries', array($this, 'display_lotteries_page') ); add_submenu_page( 'gamification', '添加新抽奖', '添加新抽奖', 'manage_options', 'gamification-add-lottery', array($this, 'display_add_lottery_page') ); } public function display_lotteries_page() { include GAMIFICATION_PLUGIN_PATH . 'admin/views/lotteries-list.php'; } public function display_add_lottery_page() { include GAMIFICATION_PLUGIN_PATH . 'admin/views/lottery-edit.php'; } public function enqueue_admin_scripts($hook) { if (strpos($hook, 'gamification') !== false) { wp_enqueue_style('gamification-admin', GAMIFICATION_PLUGIN_URL . 'assets/css/admin.css', array(), GAMIFICATION_VERSION); wp_enqueue_script('gamification-admin', GAMIFICATION_PLUGIN_URL . 'assets/js/admin.js', array('jquery', 'jquery-ui-sortable'), GAMIFICATION_VERSION, true); // 本地化脚本 wp_localize_script('gamification-admin', 'gamification_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('gamification_nonce') )); } } } 2.2 抽奖活动前端展示 创建前端抽奖界面: // 短代码处理 add_shortcode('lottery_wheel', 'gamification_lottery_wheel_shortcode'); function gamification_lottery_wheel_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'title' => '幸运大转盘', 'width' => '500', 'height' => '500' ), $atts, 'lottery_wheel'); if (!$atts['id']) { return '<p>请指定抽奖活动ID</p>'; } // 获取抽奖活动数据 global $wpdb; $table_name = $wpdb->prefix . 'gamification_lotteries'; $lottery = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d AND status = 'active'", $atts['id'] )); if (!$lottery) { return '<p>抽奖活动不存在或已结束</p>'; } // 获取奖品数据 $table_name_prizes = $wpdb->prefix . 'gamification_prizes'; $prizes = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name_prizes WHERE lottery_id = %d ORDER BY id ASC", $atts['id'] )); if (empty($prizes)) { return '<p>抽奖活动暂无奖品</p>'; } // 检查用户是否已参与 $user_id = get_current_user_id(); $table_name_participants = $wpdb->prefix . 'gamification_participants'; $has_participated = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name_participants WHERE lottery_id = %d AND user_id = %d", $atts['id'], $user_id )); ob_start(); ?> <div class="lottery-container" data-lottery-id="<?php echo esc_attr($atts['id']); ?>"> <h3 class="lottery-title"><?php echo esc_html($lottery->title); ?></h3> <div class="lottery-description"><?php echo wp_kses_post($lottery->description); ?></div> <div class="lottery-wheel-container" style="width: <?php echo esc_attr($atts['width']); ?>px; height: <?php echo esc_attr($atts['height']); ?>px;"> <canvas id="lottery-wheel-<?php echo esc_attr($atts['id']); ?>" width="<?php echo esc_attr($atts['width']); ?>" height="<?php echo esc_attr($atts['height']); ?>"></canvas> <div class="wheel-pointer"></div> </div> <div class="lottery-controls"> <?php if ($has_participated): ?> <button class="lottery-button" disabled>您已参与过本次抽奖</button> <?php else: ?> <button class="lottery-button" id="spin-button-<?php echo esc_attr($atts['id']); ?>">开始抽奖</button> <?php endif; ?> </div> <div class="lottery-prizes"> <h4>奖品列表</h4> <ul> <?php foreach ($prizes as $prize): ?> <li> <span class="prize-name"><?php echo esc_html($prize->name); ?></span> <span class="prize-probability">中奖概率: <?php echo esc_html($prize->probability * 100); ?>%</span> <span class="prize-remaining">剩余: <?php echo esc_html($prize->remaining); ?>份</span> </li> <?php endforeach; ?> </ul> </div> </div> <script type="text/javascript"> var lotteryData_<?php echo esc_attr($atts['id']); ?> = { prizes: <?php echo json_encode($prizes); ?>, lotteryId: <?php echo esc_attr($atts['id']); ?>, userId: <?php echo $user_id ?: 0; ?>, ajaxUrl: '<?php echo admin_url('admin-ajax.php'); ?>', nonce: '<?php echo wp_create_nonce('lottery_spin_' . $atts['id']); ?>' }; </script> <?php return ob_get_clean(); } 2.3 抽奖转盘Canvas绘制与动画 创建转盘的JavaScript绘制逻辑: // assets/js/lottery-wheel.js class LotteryWheel { constructor(canvasId, options) { this.canvas = document.getElementById(canvasId); this.ctx = this.canvas.getContext('2d'); this.options = options; this.prizes = options.prizes; this.colors = ['#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#FF6384', '#C9CBCF']; this.isSpinning = false; this.currentRotation = 0; this.spinDuration = 4000; // 旋转持续时间(毫秒) this.init(); } init() { this.drawWheel(); this.drawPointer(); } drawWheel() { const ctx = this.ctx; const width = this.canvas.width; const height = this.canvas.height; const centerX = width / 2; const centerY = height / 2; const radius = Math.min(width, height) / 2 - 10; // 清空画布 ctx.clearRect(0, 0, width, height); // 绘制转盘背景 ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, 2 * Math.PI); ctx.fillStyle = '#f0f0f0'; ctx.fill(); ctx.strokeStyle = '#ddd'; ctx.lineWidth = 2; ctx.stroke(); // 计算每个扇形的角度 const prizeCount = this.prizes.length; const anglePerPrize = (2 * Math.PI) / prizeCount; // 绘制每个扇形 for (let i = 0; i < prizeCount; i++) { const startAngle = i * anglePerPrize + this.currentRotation; const endAngle = (i + 1) * anglePerPrize + this.currentRotation; // 绘制扇形 ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.arc(centerX, centerY, radius, startAngle, endAngle); ctx.closePath(); ctx.fillStyle = this.colors[i % this.colors.length]; ctx.fill(); ctx.strokeStyle = '#fff'; ctx.lineWidth = 1; ctx.stroke(); // 绘制奖品文字 ctx.save(); ctx.translate(centerX, centerY); ctx.rotate(startAngle + anglePerPrize / 2); ctx.textAlign = 'right'; ctx.fillStyle = '#fff'; ctx.font = 'bold 14px Arial'; ctx.fillText(this.prizes[i].name, radius - 20, 5); ctx.restore(); } // 绘制中心圆 ctx.beginPath(); ctx.arc(centerX, centerY, 20, 0, 2 * Math.PI); ctx.fillStyle = '#fff'; ctx.fill(); ctx.strokeStyle = '#ddd'; ctx.lineWidth = 3; ctx.stroke(); } drawPointer() { const ctx = this.ctx; const width = this.canvas.width; const height = this.canvas.height; const centerX = width / 2; const centerY = height / 2; // 绘制指针三角形 ctx.beginPath(); ctx.moveTo(centerX, 10); ctx.lineTo(centerX - 15, 40); ctx.lineTo(centerX + 15, 40); ctx.closePath(); ctx.fillStyle = '#ff4444'; ctx.fill(); ctx.strokeStyle = '#cc0000'; ctx.lineWidth = 2; ctx.stroke(); } spin(prizeIndex, callback) { if (this.isSpinning) return; this.isSpinning = true; // 计算目标旋转角度(确保旋转多圈后停在指定奖品) const prizeCount = this.prizes.length; const anglePerPrize = (2 * Math.PI) / prizeCount; const targetPrizeAngle = prizeIndex * anglePerPrize; // 计算总旋转角度(多转几圈) const fullRotations = 5; // 完整旋转圈数 const targetRotation = fullRotations * 2 * Math.PI + targetPrizeAngle; // 动画开始时间 const startTime = Date.now(); // 缓动函数 const easeOut = (t) => 1 - Math.pow(1 - t, 3); // 动画函数 const animate = () => { const currentTime = Date.now(); const elapsed = currentTime - startTime; const progress = Math.min(elapsed / this.spinDuration, 1); // 应用缓动函数 const easedProgress = easeOut(progress); // 计算当前旋转角度 this.currentRotation = easedProgress * targetRotation; // 重绘转盘 this.drawWheel(); this.drawPointer(); if (progress < 1) { requestAnimationFrame(animate); } else { this.isSpinning = false; if (callback) callback(); } }; // 开始动画 animate(); } } 第三章:AJAX交互与抽奖逻辑处理 3.1 抽奖逻辑与概率算法 // includes/class-lottery-engine.php class Lottery_Engine { public function process_spin($lottery_id, $user_id) { global $wpdb; // 验证抽奖活动 $lottery = $this->get_lottery($lottery_id); if (!$lottery || $lottery->status !== 'active') { return array('success' => false, 'message' => '抽奖活动不存在或已结束'); } // 检查活动时间 $current_time = current_time('mysql'); if ($current_time < $lottery->start_date || $current_time > $lottery->end_date) { return array('success' => false, 'message' => '抽奖活动未开始或已结束'); } 用户参与次数 $participation_count = $this->get_user_participation_count($lottery_id, $user_id); $settings = maybe_unserialize($lottery->settings); $max_participations = isset($settings['max_participations']) ? intval($settings['max_participations']) : 1; if ($participation_count >= $max_participations) { return array('success' => false, 'message' => '您已达到最大参与次数'); } // 获取奖品列表 $prizes = $this->get_prizes($lottery_id); if (empty($prizes)) { return array('success' => false, 'message' => '抽奖活动暂无奖品'); } // 执行抽奖算法 $winning_prize = $this->calculate_winner($prizes); if (!$winning_prize) { return array('success' => false, 'message' => '抽奖失败,请重试'); } // 减少奖品库存 $this->decrease_prize_stock($winning_prize->id); // 记录用户参与 $participation_id = $this->record_participation($lottery_id, $user_id, $winning_prize->id); return array( 'success' => true, 'prize' => $winning_prize, 'participation_id' => $participation_id, 'message' => '恭喜您中奖了!' ); } private function calculate_winner($prizes) { // 计算总概率 $total_probability = 0; $available_prizes = array(); foreach ($prizes as $prize) { if ($prize->remaining > 0) { $total_probability += floatval($prize->probability); $available_prizes[] = $prize; } } if (empty($available_prizes)) { return null; } // 如果总概率小于1,添加"未中奖"选项 if ($total_probability < 1) { $no_prize = new stdClass(); $no_prize->id = 0; $no_prize->name = '未中奖'; $no_prize->probability = 1 - $total_probability; $available_prizes[] = $no_prize; $total_probability = 1; } // 生成随机数 $random = mt_rand() / mt_getrandmax() * $total_probability; // 确定中奖奖品 $cumulative_probability = 0; foreach ($available_prizes as $prize) { $cumulative_probability += floatval($prize->probability); if ($random <= $cumulative_probability) { return $prize->id == 0 ? null : $prize; } } return null; } private function record_participation($lottery_id, $user_id, $prize_id) { global $wpdb; $table_name = $wpdb->prefix . 'gamification_participants'; $user_ip = $_SERVER['REMOTE_ADDR']; $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'lottery_id' => $lottery_id, 'prize_id' => $prize_id, 'win_status' => $prize_id ? 'won' : 'lost', 'ip_address' => $user_ip, 'created_at' => current_time('mysql') ), array('%d', '%d', '%d', '%s', '%s', '%s') ); return $wpdb->insert_id; } } ### 3.2 AJAX处理程序 // 添加AJAX处理add_action('wp_ajax_gamification_spin_wheel', 'handle_spin_wheel_ajax');add_action('wp_ajax_nopriv_gamification_spin_wheel', 'handle_spin_wheel_ajax'); function handle_spin_wheel_ajax() { // 验证nonce if (!check_ajax_referer('lottery_spin_' . $_POST['lottery_id'], 'nonce', false)) { wp_die(json_encode(array( 'success' => false, 'message' => '安全验证失败' ))); } $lottery_id = intval($_POST['lottery_id']); $user_id = get_current_user_id(); // 如果用户未登录,可以创建临时用户或使用IP限制 if (!$user_id) { // 可以根据需要处理未登录用户 wp_die(json_encode(array( 'success' => false, 'message' => '请先登录' ))); } $engine = new Lottery_Engine(); $result = $engine->process_spin($lottery_id, $user_id); wp_die(json_encode($result)); } ### 3.3 前端AJAX调用 // assets/js/lottery-frontend.jsjQuery(document).ready(function($) { $('.lottery-button').on('click', function() { var button = $(this); var container = button.closest('.lottery-container'); var lotteryId = container.data('lottery-id'); var canvasId = 'lottery-wheel-' + lotteryId; // 禁用按钮防止重复点击 button.prop('disabled', true).text('抽奖中...'); // 获取转盘实例 var wheel = window['lotteryWheel_' + lotteryId]; // 发送AJAX请求 $.ajax({ url: gamification_ajax.ajax_url, type: 'POST', data: { action: 'gamification_spin_wheel', lottery_id: lotteryId, nonce: gamification_ajax.nonce }, dataType: 'json', success: function(response) { if (response.success) { // 计算奖品在转盘上的位置 var prizeIndex = 0; if (response.prize) { // 这里需要根据实际奖品列表计算索引 prizeIndex = calculatePrizeIndex(lotteryId, response.prize.id); } else { // 未中奖的位置 prizeIndex = Math.floor(Math.random() * 8); // 随机位置 } // 开始转盘动画 wheel.spin(prizeIndex, function() { // 动画完成后显示结果 showPrizeResult(response); button.text('已参与'); }); } else { alert(response.message); button.prop('disabled', false).text('开始抽奖'); } }, error: function() { alert('网络错误,请重试'); button.prop('disabled', false).text('开始抽奖'); } }); }); function calculatePrizeIndex(lotteryId, prizeId) { // 根据lotteryData计算奖品索引 var lotteryData = window['lotteryData_' + lotteryId]; if (lotteryData && lotteryData.prizes) { for (var i = 0; i < lotteryData.prizes.length; i++) { if (lotteryData.prizes[i].id == prizeId) { return i; } } } return 0; } function showPrizeResult(response) { var message = response.message; var prizeName = response.prize ? response.prize.name : '未中奖'; // 使用SweetAlert或自定义模态框显示结果 if (typeof Swal !== 'undefined') { Swal.fire({ title: prizeName, text: message, icon: response.prize ? 'success' : 'info', confirmButtonText: '确定' }); } else { // 简单的alert显示 alert(prizeName + ' - ' + message); } } }); ## 第四章:积分系统与用户进度追踪 ### 4.1 积分系统数据库设计 // 扩展数据库表function gamification_create_points_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 用户积分表 $table_name_points = $wpdb->prefix . 'gamification_user_points'; $sql = "CREATE TABLE IF NOT EXISTS $table_name_points ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NOT NULL, points int NOT NULL DEFAULT 0, level int NOT NULL DEFAULT 1, total_earned int NOT NULL DEFAULT 0, last_updated datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY user_id (user_id), KEY points (points) ) $charset_collate;"; // 积分记录表 $table_name_points_log = $wpdb->prefix . 'gamification_points_log'; $sql2 = "CREATE TABLE IF NOT EXISTS $table_name_points_log ( id bigint(20) NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NOT NULL, points_change int NOT NULL, action_type varchar(100) NOT NULL, action_id bigint(20), description text, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY action_type (action_type), KEY created_at (created_at) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); dbDelta($sql2); } // 在插件激活时调用register_activation_hook(__FILE__, 'gamification_create_points_tables'); ### 4.2 积分管理类 // includes/class-points-manager.phpclass Points_Manager { public static function add_points($user_id, $points, $action_type, $action_id = null, $description = '') { global $wpdb; if (!$user_id || $points == 0) { return false; } // 记录积分变更 $table_log = $wpdb->prefix . 'gamification_points_log'; $wpdb->insert( $table_log, array( 'user_id' => $user_id, 'points_change' => $points, 'action_type' => $action_type, 'action_id' => $action_id, 'description' => $description, 'created_at' => current_time('mysql') ), array('%d', '%d', '%s', '%d', '%s', '%s') ); // 更新用户总积分 $table_points = $wpdb->prefix . 'gamification_user_points'; // 检查用户是否已有积分记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_points WHERE user_id = %d", $user_id )); if ($existing) { // 更新现有记录 $wpdb->query($wpdb->prepare( "UPDATE $table_points SET points = points + %d, total_earned = total_earned + %d, level = FLOOR(SQRT(total_earned + %d) / 10) + 1 WHERE user_id = %d", $points, max($points, 0), max($points, 0), $user_id )); } else { // 创建新记录 $wpdb->insert( $table_points, array( 'user_id' => $user_id, 'points' => max($points, 0), 'total_earned' => max($points, 0), 'level' => 1, 'last_updated' => current_time('mysql') ), array('%d', '%d', '%d', '%d', '%s') ); } // 检查是否达到升级条件 self::check_level_up($user_id); return true; } public static function get_user_points($user_id) { global $wpdb; $table_points = $wpdb->prefix . 'gamification_user_points'; $result = $wpdb->get_row($wpdb->prepare( "SELECT points, level, total_earned FROM $table_points WHERE user_id = %d", $user_id )); if (!$result) { return array( 'points' => 0, 'level' => 1, 'total_earned' => 0 ); } return array( 'points' => intval($result->points), 'level' => intval($result->level), 'total_earned' => intval($result->total_earned) ); } private static function check_level_up($user_id) { $user_points = self::get_user_points($user_id); $old_level = $user_points['level']; // 计算新等级(每1000积分升一级) $new_level = floor($user_points['total_earned'] / 1000) + 1; if ($new_level > $old_level) { // 触发等级提升事件 do_action('gamification_level_up', $user_id, $old_level, $new_level); // 发送通知 self::send_level_up_notification($user_id, $old_level, $new_level); } } private static function send_level_up_notification($user_id, $old_level, $new_level) { $user = get_user_by('id', $user_id); if (!$user) return; $subject = sprintf('恭喜您升级到 %d 级!', $new_level); $message = sprintf( '亲爱的 %s,恭喜您从 %d 级升级到 %d 级!继续努力获取更多积分吧!', $user->display_name, $old_level, $new_level ); wp_mail($user->user_email, $subject, $message); // 也可以添加站内通知 if (function_exists('bp_notifications_add_notification')) { // BuddyPress 集成 bp_notifications_add_notification(array( 'user_id' => $user_id, 'item_id' => $new_level, 'component_name' => 'gamification', 'component_action' => 'level_up', 'date_notified' => bp_core_current_time(), 'is_new' => 1, )); } } } ### 4.3 积分获取规则 // 定义积分获取规则class Points_Rules { private static $rules = array( 'daily_login' => array( 'points' => 10, 'limit' => 'daily', 'description' => '每日登录奖励' ), 'post_comment' => array( 'points' => 5, 'limit' => 'none', 'description' => '发表评论' ), 'lottery_participation' => array( 'points' => 2, 'limit' => 'daily', 'description' => '参与抽奖' ), 'lottery_win' => array( 'points' => 20, 'limit' => 'none', 'description' => '抽奖中奖' ), 'share_content' => array( 'points' => 15, 'limit' => 'daily', 'description' => '分享内容' ), 'complete_profile' => array( 'points' => 50, 'limit' => 'once', 'description' => '完善个人资料' ) ); public static function apply_rule($user_id, $rule_name, $action_id = null) { if (!isset(self::$rules[$rule_name])) { return false; } $rule = self::$rules[$rule_name]; // 检查限制条件 if (!self::check_limit($user_id, $rule_name, $rule['limit'])) { return false; } // 添加积分 Points_Manager::add_points( $user_id, $rule['points'], $rule_name, $action_id, $rule['description'] ); return true; } private static function check_limit($user_id, $rule_name, $limit_type) { global $wpdb; $table_log = $wpdb->prefix . 'gamification_points_log'; switch ($limit_type) { case 'daily': // 检查今天是否已经获得过 $today = date('Y-m-d'); $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_log WHERE user_id = %d AND action_type = %s AND DATE(created_at) = %s", $user_id, $rule_name, $today )); return $count == 0; case 'once': // 检查是否已经获得过 $count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_log WHERE user_id = %d AND action_type = %s", $user_id, $rule_name )); return $count == 0; case 'none': default: return true; } } } ## 第五章:进度条与成就系统 ### 5.1 进度条组件 // 进度条短代码add_shortcode('progress_bar', 'gamification_progress_bar_shortcode'); function gamification_progress_bar_shortcode($atts) { $atts = shortcode_atts(array( 'type' => 'points', // points, level, custom 'target' => 1000, 'current' => '', 'height' => '20
发表评论一步步教你,集成在线设计工具与模板编辑器到你的网站,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:为什么你的网站需要集成设计工具与编辑器? 在当今数字化时代,用户体验已成为网站成功的关键因素。无论是电商平台、内容发布网站还是企业官网,用户都期望获得更加个性化、互动性强的在线体验。集成在线设计工具与模板编辑器,可以让你的网站从“静态展示”转变为“动态创作平台”,从而显著提升用户参与度、延长停留时间并提高转化率。 想象一下,一个服装电商网站允许用户自定义T恤图案;一个营销公司网站让客户在线设计宣传海报;一个博客平台提供个性化的文章模板编辑器——这些功能不仅提升了网站的实用价值,也创造了独特的竞争优势。通过WordPress这一全球最流行的内容管理系统,我们可以通过代码二次开发,相对高效地实现这些高级功能。 本文将详细指导你如何一步步在WordPress网站中集成在线设计工具与模板编辑器,并实现一系列常用互联网小工具功能。无论你是WordPress开发者、网站管理员还是有一定技术基础的企业主,都能从本指南中获得实用的解决方案。 第一部分:前期准备与环境搭建 1.1 理解核心概念:什么是在线设计工具与模板编辑器? 在线设计工具是一种基于Web的应用程序,允许用户在浏览器中直接创建和编辑视觉内容,无需安装专业设计软件。常见的功能包括:拖放元素、调整尺寸、更改颜色、添加文字和上传图片等。而模板编辑器则是预设了布局和样式的设计工具,用户可以在模板基础上进行个性化修改,大大降低了设计门槛。 在WordPress中集成这类工具,意味着将这些功能无缝嵌入到你的网站页面中,让访问者能够直接使用。这通常需要结合前端界面库、后端处理逻辑和数据库存储等技术组件。 1.2 开发环境准备 在开始编码之前,你需要搭建合适的开发环境: 本地开发环境:建议使用Local by Flywheel、XAMPP或MAMP等工具,在本地计算机上搭建WordPress运行环境。这样可以避免在线上网站直接调试可能带来的风险。 代码编辑器:选择一款强大的代码编辑器,如Visual Studio Code、PHPStorm或Sublime Text。确保安装相关插件,如PHP智能提示、CSS自动补全和Git集成等。 浏览器开发者工具:熟悉Chrome或Firefox的开发者工具,这对前端调试至关重要。 版本控制系统:初始化Git仓库来管理代码变更,这是专业开发的基本实践。 备份方案:确保你有完整的网站备份,包括文件和数据库,以防开发过程中出现不可逆的错误。 1.3 创建子主题或自定义插件 为了避免主题更新覆盖你的修改,最佳实践是创建一个子主题或自定义插件: 创建子主题的方法: 在/wp-content/themes/目录下创建新文件夹,如my-custom-design-tool 创建style.css文件,添加主题信息头: /* Theme Name: 我的设计工具子主题 Template: parent-theme-folder-name */ 创建functions.php文件,用于添加自定义功能 创建自定义插件的方法: 在/wp-content/plugins/目录下创建新文件夹,如design-tool-integration 创建主插件文件design-tool-integration.php: <?php /** * Plugin Name: 设计工具集成插件 * Description: 为WordPress网站集成在线设计工具和模板编辑器 */ 第二部分:集成基础在线设计工具 2.1 选择合适的前端设计库 集成设计工具的第一步是选择合适的前端库。以下是几个优秀的选择: Fabric.js:一个强大的Canvas库,专门用于创建交互式设计工具。它提供了丰富的对象模型、序列化功能和交互控制。 Konva.js:另一个基于Canvas的2D绘图库,性能优异,支持桌面和移动设备。 tldraw:开源的绘图库,提供流畅的绘图体验和丰富的图形工具。 PixiJS:专注于性能的2D渲染引擎,适合需要处理大量图形元素的复杂设计工具。 对于大多数应用场景,Fabric.js提供了最全面的功能集和活跃的社区支持。我们将以Fabric.js为例进行后续讲解。 2.2 在WordPress中引入Fabric.js 有几种方法可以将Fabric.js引入WordPress: 方法一:使用CDN链接(最简单)在主题或插件的PHP文件中添加: function enqueue_design_tool_scripts() { wp_enqueue_script('fabric-js', 'https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.5.0/fabric.min.js', array(), '4.5.0', true); wp_enqueue_script('design-tool-main', get_stylesheet_directory_uri() . '/js/design-tool.js', array('fabric-js', 'jquery'), '1.0.0', true); wp_enqueue_style('design-tool-style', get_stylesheet_directory_uri() . '/css/design-tool.css'); } add_action('wp_enqueue_scripts', 'enqueue_design_tool_scripts'); 方法二:本地托管(推荐,更稳定) 从Fabric.js官网下载最新版本 将文件放入主题的/js/目录 修改上述代码中的CDN链接为本地路径 2.3 创建基础画布编辑器 接下来,我们创建一个基础的设计画布。首先,在页面模板中添加画布容器: // 在主题模板文件中添加 function render_design_tool() { ob_start(); ?> <div class="design-tool-container"> <div class="tool-header"> <h2>在线设计工具</h2> </div> <div class="tool-body"> <div class="tool-sidebar"> <!-- 工具选项将在这里添加 --> </div> <div class="tool-main"> <canvas id="design-canvas" width="800" height="600"></canvas> </div> </div> <div class="tool-footer"> <button id="save-design" class="btn btn-primary">保存设计</button> <button id="export-png" class="btn btn-secondary">导出PNG</button> </div> </div> <?php return ob_get_clean(); } add_shortcode('design_tool', 'render_design_tool'); 然后,创建JavaScript文件初始化画布: // js/design-tool.js jQuery(document).ready(function($) { // 初始化画布 var canvas = new fabric.Canvas('design-canvas', { backgroundColor: '#ffffff', preserveObjectStacking: true }); // 设置画布尺寸 canvas.setDimensions({ width: 800, height: 600 }, { cssOnly: false }); // 添加矩形工具 $('#add-rectangle').on('click', function() { var rect = new fabric.Rect({ left: 100, top: 100, fill: '#ff0000', width: 100, height: 100 }); canvas.add(rect); canvas.setActiveObject(rect); }); // 添加文字工具 $('#add-text').on('click', function() { var text = new fabric.Textbox('双击编辑文字', { left: 50, top: 50, width: 200, fontSize: 20, fill: '#000000' }); canvas.add(text); canvas.setActiveObject(text); }); // 保存设计到服务器 $('#save-design').on('click', function() { var designData = JSON.stringify(canvas.toJSON()); $.ajax({ url: designToolAjax.ajax_url, type: 'POST', data: { action: 'save_design', design_data: designData, nonce: designToolAjax.nonce }, success: function(response) { if(response.success) { alert('设计已保存!'); } else { alert('保存失败:' + response.data); } } }); }); // 导出为PNG $('#export-png').on('click', function() { var dataURL = canvas.toDataURL({ format: 'png', quality: 1 }); // 创建下载链接 var link = document.createElement('a'); link.download = 'my-design.png'; link.href = dataURL; document.body.appendChild(link); link.click(); document.body.removeChild(link); }); }); 2.4 实现后端保存功能 设计数据需要保存到服务器。在WordPress中,我们使用AJAX处理: // 在functions.php或插件文件中添加 function save_design_callback() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'design_tool_nonce')) { wp_die('安全验证失败'); } $design_data = json_decode(stripslashes($_POST['design_data']), true); // 获取当前用户ID $user_id = get_current_user_id(); // 创建或更新设计 $design_id = wp_insert_post(array( 'post_type' => 'design', 'post_title' => '用户设计 ' . current_time('mysql'), 'post_status' => 'publish', 'post_author' => $user_id, 'meta_input' => array( '_design_data' => $design_data, '_design_preview' => generate_design_preview($design_data) // 生成预览图 ) )); if ($design_id) { wp_send_json_success(array( 'design_id' => $design_id, 'message' => '设计保存成功' )); } else { wp_send_json_error('保存失败'); } } add_action('wp_ajax_save_design', 'save_design_callback'); add_action('wp_ajax_nopriv_save_design', 'save_design_callback'); // 如果允许未登录用户保存 // 注册设计自定义文章类型 function register_design_post_type() { register_post_type('design', array( 'labels' => array( 'name' => __('设计作品'), 'singular_name' => __('设计') ), 'public' => true, 'has_archive' => true, 'supports' => array('title', 'thumbnail'), 'show_in_rest' => true // 启用Gutenberg编辑器支持 ) ); } add_action('init', 'register_design_post_type'); 第三部分:构建模板编辑器系统 3.1 设计模板数据结构 模板编辑器需要一种结构化的方式来定义模板。我们使用JSON格式存储模板配置: // 示例模板数据结构 $template_structure = array( 'name' => '社交媒体海报模板', 'width' => 1200, 'height' => 630, 'backgroundColor' => '#ffffff', 'objects' => array( array( 'type' => 'image', 'src' => '/templates/images/placeholder.jpg', 'left' => 100, 'top' => 100, 'width' => 400, 'height' => 300, 'editable' => true, 'properties' => array( 'replaceable' => true, 'minWidth' => 100, 'minHeight' => 100 ) ), array( 'type' => 'textbox', 'text' => '点击编辑标题', 'left' => 550, 'top' => 150, 'fontSize' => 48, 'fontFamily' => 'Arial', 'fill' => '#333333', 'editable' => true, 'properties' => array( 'maxLength' => 100, 'textAlign' => 'center' ) ) ) ); 3.2 创建模板管理界面 我们需要一个管理界面来创建和管理模板。使用WordPress的Admin API创建管理页面: // 添加模板管理菜单 function add_template_admin_menu() { add_menu_page( '设计模板管理', '设计模板', 'manage_options', 'design-templates', 'render_template_admin_page', 'dashicons-layout', 30 ); add_submenu_page( 'design-templates', '添加新模板', '添加模板', 'manage_options', 'add-design-template', 'render_add_template_page' ); } add_action('admin_menu', 'add_template_admin_menu'); // 渲染模板管理页面 function render_template_admin_page() { ?> <div class="wrap"> <h1 class="wp-heading-inline">设计模板管理</h1> <a href="<?php echo admin_url('admin.php?page=add-design-template'); ?>" class="page-title-action">添加新模板</a> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>ID</th> <th>模板名称</th> <th>尺寸</th> <th>创建时间</th> <th>操作</th> </tr> </thead> <tbody> <?php $templates = get_posts(array( 'post_type' => 'design_template', 'posts_per_page' => -1 )); foreach ($templates as $template) { $template_data = get_post_meta($template->ID, '_template_data', true); ?> <tr> <td><?php echo $template->ID; ?></td> <td><?php echo $template->post_title; ?></td> <td><?php echo isset($template_data['width']) ? $template_data['width'] . '×' . $template_data['height'] : '未设置'; ?></td> <td><?php echo $template->post_date; ?></td> <td> <a href="<?php echo admin_url('admin.php?page=add-design-template&template_id=' . $template->ID); ?>">编辑</a> | <a href="#" class="delete-template" data-id="<?php echo $template->ID; ?>">删除</a> | <a href="#" class="preview-template" data-id="<?php echo $template->ID; ?>">预览</a> </td> </tr> <?php } ?> </tbody> </table> </div> <?php } 3.3 实现模板加载与应用 在前端,我们需要从服务器加载模板并应用到画布: // 加载模板功能 function loadTemplate(templateId) { $.ajax({ url: designToolAjax.ajax_url, type: 'GET', data: { action: 'load_template', template_id: templateId }, success: function(response) { if(response.success) { // 清空当前画布 canvas.clear(); // 设置画布尺寸 canvas.setWidth(response.data.width); canvas.setHeight(response.data.height); // 设置背景色 canvas.setBackgroundColor(response.data.backgroundColor, canvas.renderAll.bind(canvas)); // 加载模板对象 fabric.util.enlivenObjects(response.data.objects, function(objects) { objects.forEach(function(obj) { canvas.add(obj); }); canvas.renderAll(); }); } } }); } // 模板选择器UI function renderTemplateSelector() { $.ajax({ url: designToolAjax.ajax_url, type: 'GET', data: { action: 'get_templates' }, success: function(response) { if(response.success) { var $selector = $('#template-selector'); $selector.empty(); response.data.forEach(function(template) { $selector.append( '<div class="template-thumbnail" data-id="' + template.id + '">' + '<img src="' + template.thumbnail + '" alt="' + template.name + '">' + '<div class="template-name">' + template.name + '</div>' + '</div>' ); }); // 绑定点击事件 $('.template-thumbnail').on('click', function() { var templateId = $(this).data('id'); loadTemplate(templateId); }); } } }); } 第四部分:集成常用互联网小工具功能 4.1 图像处理工具集成 现代设计工具离不开图像处理功能。我们可以集成一些常见的图像处理工具: // 图像处理功能后端 function process_image_callback() { $action = $_POST['action_type']; $image_data = $_POST['image_data']; // 解码base64图像 $image_data = str_replace('data:image/png;base64,', '', $image_data); $image_data = str_replace(' ', '+', $image_data); $image_binary = base64_decode($image_data); // 使用GD库处理图像 $image = imagecreatefromstring($image_binary); switch($action) { case 'grayscale': imagefilter($image, IMG_FILTER_GRAYSCALE); break; case 'sepia': // 实现棕褐色滤镜 imagefilter($image, IMG_FILTER_GRAYSCALE); imagefilter($image, IMG_FILTER_COLORIZE, 112, 66, 20); break; case 'brightness': $level = intval($_POST['level']); imagefilter($image, IMG_FILTER_BRIGHTNESS, $level); break; case 'crop': $x = intval($_POST['x']); $y = intval($_POST['y']); $width = intval($_POST['width']); $height = intval($_POST['height']); $image = imagecrop($image, ['x' => $x, 'y' => $y, 'width' => $width, 'height' => $height]); 4.2 字体管理与Google Fonts集成 设计工具中字体选择是核心功能。我们可以集成Google Fonts API,提供丰富的字体选择: // 字体管理功能 class Font_Manager { private $google_fonts_api_key = 'YOUR_GOOGLE_API_KEY'; public function __construct() { add_action('wp_ajax_get_google_fonts', array($this, 'get_google_fonts_callback')); add_action('wp_ajax_load_font_to_canvas', array($this, 'load_font_to_canvas_callback')); } // 获取Google Fonts列表 public function get_google_fonts_callback() { $transient_key = 'google_fonts_list'; $fonts = get_transient($transient_key); if (false === $fonts) { $response = wp_remote_get( 'https://www.googleapis.com/webfonts/v1/webfonts?key=' . $this->google_fonts_api_key . '&sort=popularity' ); if (!is_wp_error($response)) { $body = wp_remote_retrieve_body($response); $fonts_data = json_decode($body, true); $fonts = array(); foreach ($fonts_data['items'] as $font) { $fonts[] = array( 'family' => $font['family'], 'variants' => $font['variants'], 'category' => $font['category'] ); } // 缓存24小时 set_transient($transient_key, $fonts, DAY_IN_SECONDS); } } wp_send_json_success($fonts); } // 动态加载字体到画布 public function load_font_to_canvas_callback() { $font_family = sanitize_text_field($_POST['font_family']); $font_variant = sanitize_text_field($_POST['font_variant']); // 生成字体CSS URL $font_url = $this->generate_font_css_url($font_family, $font_variant); // 将字体添加到页面 $font_face_css = " @font-face { font-family: '{$font_family}'; src: url('{$font_url}'); }"; wp_send_json_success(array( 'font_css' => $font_face_css, 'font_family' => $font_family )); } private function generate_font_css_url($family, $variant) { $family_encoded = str_replace(' ', '+', $family); return "https://fonts.googleapis.com/css2?family={$family_encoded}:wght@{$variant}&display=swap"; } } new Font_Manager(); 前端字体选择器实现: // 字体选择器组件 class FontSelector { constructor(canvas) { this.canvas = canvas; this.fonts = []; this.init(); } async init() { await this.loadFonts(); this.renderFontSelector(); } async loadFonts() { try { const response = await $.ajax({ url: designToolAjax.ajax_url, type: 'GET', data: { action: 'get_google_fonts' } }); if (response.success) { this.fonts = response.data; this.populateFontSelect(); } } catch (error) { console.error('加载字体失败:', error); } } populateFontSelect() { const $fontSelect = $('#font-family-select'); $fontSelect.empty(); // 添加系统字体 const systemFonts = ['Arial', 'Helvetica', 'Times New Roman', 'Georgia', 'Courier New']; systemFonts.forEach(font => { $fontSelect.append(`<option value="${font}">${font}</option>`); }); // 添加Google Fonts this.fonts.slice(0, 50).forEach(font => { // 限制显示数量 $fontSelect.append(`<option value="${font.family}">${font.family}</option>`); }); } renderFontSelector() { const fontControls = ` <div class="font-controls"> <div class="font-control-group"> <label>字体:</label> <select id="font-family-select" class="font-select"> <option value="Arial">Arial</option> </select> </div> <div class="font-control-group"> <label>字号:</label> <input type="range" id="font-size-slider" min="8" max="120" value="24"> <input type="number" id="font-size-input" value="24" min="8" max="120"> </div> <div class="font-control-group"> <label>颜色:</label> <input type="color" id="font-color-picker" value="#000000"> </div> <div class="font-control-group"> <button id="bold-btn" class="font-style-btn">B</button> <button id="italic-btn" class="font-style-btn">I</button> <button id="underline-btn" class="font-style-btn">U</button> </div> </div>`; $('.tool-sidebar').append(fontControls); this.bindEvents(); } bindEvents() { // 字体选择变化 $('#font-family-select').on('change', async (e) => { const fontFamily = $(e.target).val(); const activeObject = this.canvas.getActiveObject(); if (activeObject && activeObject.type === 'textbox') { // 如果是Google Font,先加载 if (this.isGoogleFont(fontFamily)) { await this.loadGoogleFont(fontFamily, '400'); } activeObject.set('fontFamily', fontFamily); this.canvas.renderAll(); } }); // 字号调整 $('#font-size-slider, #font-size-input').on('input change', (e) => { const fontSize = $(e.target).val(); const activeObject = this.canvas.getActiveObject(); if (activeObject && activeObject.type === 'textbox') { activeObject.set('fontSize', parseInt(fontSize)); this.canvas.renderAll(); } // 同步两个输入框的值 if (e.target.id === 'font-size-slider') { $('#font-size-input').val(fontSize); } else { $('#font-size-slider').val(fontSize); } }); } isGoogleFont(fontFamily) { return this.fonts.some(font => font.family === fontFamily); } async loadGoogleFont(fontFamily, variant) { try { const response = await $.ajax({ url: designToolAjax.ajax_url, type: 'POST', data: { action: 'load_font_to_canvas', font_family: fontFamily, font_variant: variant } }); if (response.success) { // 动态添加字体CSS到页面 $('head').append(`<style>${response.data.font_css}</style>`); return true; } } catch (error) { console.error('加载Google字体失败:', error); } return false; } } // 初始化字体选择器 const fontSelector = new FontSelector(canvas); 4.3 素材库与资源管理器 创建可搜索的素材库,包含图标、背景、贴纸等资源: // 素材库管理 class Asset_Library { private $asset_categories = array( 'icons' => '图标', 'backgrounds' => '背景', 'stickers' => '贴纸', 'shapes' => '形状', 'frames' => '边框' ); public function __construct() { add_action('init', array($this, 'register_asset_post_type')); add_action('wp_ajax_search_assets', array($this, 'search_assets_callback')); add_action('wp_ajax_upload_asset', array($this, 'upload_asset_callback')); } public function register_asset_post_type() { register_post_type('design_asset', array( 'labels' => array( 'name' => __('设计素材'), 'singular_name' => __('素材') ), 'public' => true, 'has_archive' => false, 'show_in_menu' => true, 'menu_position' => 31, 'menu_icon' => 'dashicons-images-alt2', 'supports' => array('title', 'thumbnail'), 'taxonomies' => array('asset_category'), 'show_in_rest' => true ) ); // 注册素材分类 register_taxonomy('asset_category', 'design_asset', array( 'labels' => array( 'name' => __('素材分类'), 'singular_name' => __('分类') ), 'hierarchical' => true, 'show_in_rest' => true ) ); } public function search_assets_callback() { $search_term = sanitize_text_field($_GET['search']); $category = sanitize_text_field($_GET['category']); $page = intval($_GET['page']) ?: 1; $per_page = 20; $args = array( 'post_type' => 'design_asset', 'posts_per_page' => $per_page, 'paged' => $page, 'post_status' => 'publish' ); if (!empty($search_term)) { $args['s'] = $search_term; } if (!empty($category) && $category !== 'all') { $args['tax_query'] = array( array( 'taxonomy' => 'asset_category', 'field' => 'slug', 'terms' => $category ) ); } $query = new WP_Query($args); $assets = array(); while ($query->have_posts()) { $query->the_post(); $post_id = get_the_ID(); $assets[] = array( 'id' => $post_id, 'title' => get_the_title(), 'thumbnail' => get_the_post_thumbnail_url($post_id, 'medium'), 'full_url' => wp_get_attachment_url(get_post_thumbnail_id($post_id)), 'type' => $this->get_asset_type($post_id), 'category' => wp_get_post_terms($post_id, 'asset_category', array('fields' => 'names')) ); } wp_reset_postdata(); wp_send_json_success(array( 'assets' => $assets, 'total_pages' => $query->max_num_pages, 'current_page' => $page )); } private function get_asset_type($post_id) { $mime_type = get_post_mime_type(get_post_thumbnail_id($post_id)); if (strpos($mime_type, 'image/svg') !== false) { return 'svg'; } elseif (strpos($mime_type, 'image/') !== false) { return 'image'; } else { return 'other'; } } public function upload_asset_callback() { // 检查用户权限 if (!current_user_can('upload_files')) { wp_send_json_error('没有上传权限'); } // 处理文件上传 if (!function_exists('wp_handle_upload')) { require_once(ABSPATH . 'wp-admin/includes/file.php'); } $uploadedfile = $_FILES['asset_file']; $category = sanitize_text_field($_POST['category']); $upload_overrides = array('test_form' => false); $movefile = wp_handle_upload($uploadedfile, $upload_overrides); if ($movefile && !isset($movefile['error'])) { // 创建附件 $attachment = array( 'post_mime_type' => $movefile['type'], 'post_title' => sanitize_file_name($_POST['title']), 'post_content' => '', 'post_status' => 'inherit' ); $attach_id = wp_insert_attachment($attachment, $movefile['file']); // 生成缩略图 require_once(ABSPATH . 'wp-admin/includes/image.php'); $attach_data = wp_generate_attachment_metadata($attach_id, $movefile['file']); wp_update_attachment_metadata($attach_id, $attach_data); // 创建设计素材文章 $asset_post_id = wp_insert_post(array( 'post_type' => 'design_asset', 'post_title' => sanitize_text_field($_POST['title']), 'post_status' => 'publish', 'meta_input' => array( '_asset_file_id' => $attach_id ) )); // 设置特色图片 set_post_thumbnail($asset_post_id, $attach_id); // 设置分类 if (!empty($category)) { wp_set_post_terms($asset_post_id, array($category), 'asset_category'); } wp_send_json_success(array( 'message' => '素材上传成功', 'asset_id' => $asset_post_id, 'url' => $movefile['url'] )); } else { wp_send_json_error($movefile['error']); } } } new Asset_Library(); 前端素材库界面: // 素材库组件 class AssetLibrary { constructor(canvas) { this.canvas = canvas; this.currentPage = 1; this.totalPages = 1; this.currentCategory = 'all'; this.searchTerm = ''; this.init(); } init() { this.renderLibraryUI(); this.loadAssets(); } renderLibraryUI() { const libraryHTML = ` <div class="asset-library"> <div class="library-header"> <h3>素材库</h3> <div class="library-controls"> <input type="text" id="asset-search" placeholder="搜索素材..."> <select id="asset-category"> <option value="all">所有分类</option> <option value="icons">图标</option> <option value="backgrounds">背景</option> <option value="stickers">贴纸</option> <option value="shapes">形状</option> <option value="frames">边框</option> </select> <button id="upload-asset-btn" class="btn-small">上传素材</button> </div> </div> <div class="library-content" id="asset-grid"> <!-- 素材将在这里显示 --> </div> <div class="library-footer"> <div class="pagination"> <button id="prev-page" disabled>上一页</button> <span id="page-info">第 1 页 / 共 1 页</span> <button id="next-page" disabled>下一页</button> </div> </div> </div>`; $('.tool-sidebar').append(libraryHTML); this.bindEvents(); } bindEvents() { // 搜索功能 $('#asset-search').on('keyup', debounce(() => { this.searchTerm = $('#asset-search').val(); this.currentPage = 1; this.loadAssets(); }, 500)); // 分类筛选 $('#asset-category').on('change', () => { this.currentCategory = $('#asset-category').val(); this.currentPage = 1; this.loadAssets(); }); // 分页 $('#prev-page').on('click', () => { if (this.currentPage > 1) { this.currentPage--; this.loadAssets(); } }); $('#next-page').on('click', () => { if (this.currentPage < this.totalPages) { this.currentPage++; this.loadAssets(); } }); // 上传素材 $('#upload-asset-btn').on('click', () => { this.showUploadModal(); }); } async loadAssets() { try { const response = await $.ajax({ url: designToolAjax.ajax_url, type: 'GET', data: { action: 'search_assets', search: this.searchTerm, category: this.currentCategory, page: this.currentPage } }); if (response.success) { this.renderAssets(response.data.assets); this.updatePagination(response.data); } } catch (error) { console.error('加载素材失败:', error); } } renderAssets(assets) { const $assetGrid = $('#asset-grid'); $assetGrid.empty(); if (assets.length === 0) { $assetGrid.html('<div class="no-assets">未找到素材</div>'); return; } assets.forEach(asset => { const assetItem = ` <div class="asset-item" data-id="${asset.id}" data-url="${asset.full_url}" data-type="${asset.type}"> <div class="asset-thumbnail"> <img src="${asset.thumbnail}" alt="${asset.title}" loading="lazy"> </div> <div class="asset-info"> <div class="asset-title">${asset.title}</div> <div class="asset-category">${asset.category.join(', ')}</div> </div> </div>`; $assetGrid.append(assetItem); }); // 绑定点击事件 $('.asset-item').on('click', (e) => { const $item = $(e.currentTarget); this.addAssetToCanvas( $item.data('url'), $item.data('type') ); }); } addAssetToCanvas(url, type) {
发表评论WordPress插件开发教程:实现多维度网站用户分群与精准推送 引言:为什么需要用户分群与精准推送? 在当今互联网时代,网站访问者不再满足于千篇一律的内容体验。根据Salesforce的研究,个性化内容可以将转化率提高20%。对于WordPress网站运营者而言,理解用户行为并提供定制化内容已成为提升用户参与度和转化率的关键策略。 传统的WordPress网站通常向所有访问者展示相同的内容,无论他们的兴趣、行为或背景如何。这种"一刀切"的方法已经无法满足现代用户的需求。通过开发自定义插件实现多维度用户分群与精准推送,我们可以: 提高用户参与度和页面停留时间 增加内容相关性和用户满意度 提升转化率和商业价值 收集有价值的用户行为数据 本教程将引导您从零开始开发一个功能完整的WordPress插件,实现多维度用户分群与精准推送功能,同时融入常用互联网小工具,增强网站互动性。 第一章:开发环境准备与插件基础框架 1.1 开发环境配置 在开始插件开发前,确保您已准备好以下环境: WordPress 5.0+ 本地或测试环境 PHP 7.2+ 版本 MySQL 5.6+ 或 MariaDB 10.0+ 代码编辑器(推荐VS Code或PHPStorm) 浏览器开发者工具 1.2 创建插件基础结构 首先,在WordPress的wp-content/plugins/目录下创建新文件夹user-segmentation-tools,并创建以下基础文件: user-segmentation-tools/ ├── user-segmentation-tools.php # 主插件文件 ├── includes/ # 包含核心功能文件 │ ├── class-user-segmenter.php # 用户分群核心类 │ ├── class-content-pusher.php # 内容推送类 │ ├── class-data-collector.php # 数据收集类 │ └── class-tools-integration.php # 小工具集成类 ├── admin/ # 后台管理文件 │ ├── css/ │ ├── js/ │ └── views/ ├── public/ # 前端文件 │ ├── css/ │ ├── js/ │ └── views/ ├── assets/ # 静态资源 └── uninstall.php # 插件卸载脚本 1.3 主插件文件配置 编辑user-segmentation-tools.php,添加插件基本信息: <?php /** * Plugin Name: 用户分群与精准推送工具 * Plugin URI: https://yourwebsite.com/ * Description: 实现多维度网站用户分群与精准推送功能,集成常用互联网小工具 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: user-segmentation-tools * Domain Path: /languages */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('UST_VERSION', '1.0.0'); define('UST_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('UST_PLUGIN_URL', plugin_dir_url(__FILE__)); define('UST_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { $prefix = 'UST_'; $base_dir = UST_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class_name, $len) !== 0) { return; } $relative_class = substr($class_name, $len); $file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function ust_init_plugin() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('用户分群与精准推送工具需要WordPress 5.0或更高版本。', 'user-segmentation-tools'); echo '</p></div>'; }); return; } // 初始化核心类 $user_segmenter = new UST_User_Segmenter(); $content_pusher = new UST_Content_Pusher(); $data_collector = new UST_Data_Collector(); $tools_integration = new UST_Tools_Integration(); // 注册激活/停用钩子 register_activation_hook(__FILE__, ['UST_User_Segmenter', 'activate']); register_deactivation_hook(__FILE__, ['UST_User_Segmenter', 'deactivate']); // 加载文本域 load_plugin_textdomain('user-segmentation-tools', false, dirname(UST_PLUGIN_BASENAME) . '/languages'); } add_action('plugins_loaded', 'ust_init_plugin'); 第二章:用户数据收集与存储机制 2.1 设计用户数据表结构 为了实现多维度用户分群,我们需要收集和存储用户行为数据。在class-user-segmenter.php中创建数据库表: class UST_User_Segmenter { public static function activate() { self::create_tables(); self::create_default_segments(); } private static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name_users = $wpdb->prefix . 'ust_user_data'; $table_name_behavior = $wpdb->prefix . 'ust_user_behavior'; $table_name_segments = $wpdb->prefix . 'ust_user_segments'; // 用户数据表 $sql_users = "CREATE TABLE IF NOT EXISTS $table_name_users ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT(20) UNSIGNED DEFAULT NULL, session_id VARCHAR(64) NOT NULL, first_visit DATETIME NOT NULL, last_visit DATETIME NOT NULL, visit_count INT(11) DEFAULT 1, device_type VARCHAR(50), browser VARCHAR(100), location VARCHAR(200), referrer VARCHAR(500), interests TEXT, PRIMARY KEY (id), UNIQUE KEY session_id (session_id), KEY user_id (user_id), KEY last_visit (last_visit) ) $charset_collate;"; // 用户行为表 $sql_behavior = "CREATE TABLE IF NOT EXISTS $table_name_behavior ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, user_data_id BIGINT(20) UNSIGNED NOT NULL, behavior_type VARCHAR(50) NOT NULL, behavior_value TEXT, page_url VARCHAR(500), timestamp DATETIME NOT NULL, metadata TEXT, PRIMARY KEY (id), KEY user_data_id (user_data_id), KEY behavior_type (behavior_type), KEY timestamp (timestamp) ) $charset_collate;"; // 用户分群表 $sql_segments = "CREATE TABLE IF NOT EXISTS $table_name_segments ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, segment_name VARCHAR(100) NOT NULL, segment_slug VARCHAR(100) NOT NULL, segment_rules TEXT NOT NULL, user_count INT(11) DEFAULT 0, created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, is_active TINYINT(1) DEFAULT 1, PRIMARY KEY (id), UNIQUE KEY segment_slug (segment_slug) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_users); dbDelta($sql_behavior); dbDelta($sql_segments); } } 2.2 实现用户数据收集 创建class-data-collector.php,实现用户行为跟踪: class UST_Data_Collector { private $user_data_id; private $session_id; public function __construct() { $this->init_session(); $this->track_user(); add_action('wp_footer', [$this, 'add_tracking_script']); } private function init_session() { if (!session_id()) { session_start(); } if (!isset($_SESSION['ust_session_id'])) { $_SESSION['ust_session_id'] = $this->generate_session_id(); } $this->session_id = $_SESSION['ust_session_id']; } private function generate_session_id() { return md5(uniqid(mt_rand(), true) . $_SERVER['REMOTE_ADDR'] . $_SERVER['HTTP_USER_AGENT']); } private function track_user() { global $wpdb; // 获取用户信息 $user_id = is_user_logged_in() ? get_current_user_id() : null; $current_time = current_time('mysql'); // 检查是否已存在该会话记录 $table_name = $wpdb->prefix . 'ust_user_data'; $existing = $wpdb->get_row($wpdb->prepare( "SELECT id FROM $table_name WHERE session_id = %s", $this->session_id )); if ($existing) { $this->user_data_id = $existing->id; // 更新最后访问时间 $wpdb->update( $table_name, [ 'last_visit' => $current_time, 'visit_count' => $wpdb->get_var($wpdb->prepare( "SELECT visit_count + 1 FROM $table_name WHERE id = %d", $this->user_data_id )) ], ['id' => $this->user_data_id] ); } else { // 收集设备信息 $device_info = $this->get_device_info(); $location = $this->get_user_location(); // 插入新用户记录 $wpdb->insert( $table_name, [ 'user_id' => $user_id, 'session_id' => $this->session_id, 'first_visit' => $current_time, 'last_visit' => $current_time, 'device_type' => $device_info['device_type'], 'browser' => $device_info['browser'], 'location' => $location, 'referrer' => isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '直接访问' ] ); $this->user_data_id = $wpdb->insert_id; } // 记录页面访问行为 $this->record_behavior('page_view', [ 'page_title' => wp_get_document_title(), 'page_url' => home_url($_SERVER['REQUEST_URI']) ]); } private function get_device_info() { $user_agent = $_SERVER['HTTP_USER_AGENT']; // 简单设备检测 $device_type = 'desktop'; if (preg_match('/(android|webos|iphone|ipad|ipod|blackberry|windows phone)/i', $user_agent)) { $device_type = 'mobile'; } elseif (preg_match('/(tablet|ipad|playbook|silk)/i', $user_agent)) { $device_type = 'tablet'; } // 浏览器检测 $browser = '未知浏览器'; if (preg_match('/MSIE|Trident/i', $user_agent)) { $browser = 'Internet Explorer'; } elseif (preg_match('/Firefox/i', $user_agent)) { $browser = 'Firefox'; } elseif (preg_match('/Chrome/i', $user_agent)) { $browser = 'Chrome'; } elseif (preg_match('/Safari/i', $user_agent)) { $browser = 'Safari'; } elseif (preg_match('/Edge/i', $user_agent)) { $browser = 'Edge'; } return [ 'device_type' => $device_type, 'browser' => $browser ]; } private function get_user_location() { // 简化版地理位置获取 // 实际应用中可使用IP地理位置API $ip = $_SERVER['REMOTE_ADDR']; // 如果是本地环境,返回模拟位置 if ($ip === '127.0.0.1' || $ip === '::1') { return '本地环境'; } // 这里可以集成IP地理位置API // 例如使用ipapi.co或ipstack.com的API return '未知位置'; } public function record_behavior($type, $value = null, $metadata = []) { global $wpdb; if (!$this->user_data_id) { return false; } $table_name = $wpdb->prefix . 'ust_user_behavior'; return $wpdb->insert( $table_name, [ 'user_data_id' => $this->user_data_id, 'behavior_type' => $type, 'behavior_value' => is_array($value) ? json_encode($value) : $value, 'page_url' => home_url($_SERVER['REQUEST_URI']), 'timestamp' => current_time('mysql'), 'metadata' => json_encode($metadata) ] ); } public function add_tracking_script() { ?> <script type="text/javascript"> (function() { // 跟踪用户交互行为 document.addEventListener('DOMContentLoaded', function() { // 跟踪链接点击 document.querySelectorAll('a').forEach(function(link) { link.addEventListener('click', function(e) { var linkData = { href: this.href, text: this.textContent.substring(0, 100), class: this.className }; // 发送AJAX请求记录行为 fetch('<?php echo admin_url('admin-ajax.php'); ?>', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'action=ust_track_behavior&type=link_click&value=' + encodeURIComponent(JSON.stringify(linkData)) }); }); }); // 跟踪表单交互 document.querySelectorAll('form').forEach(function(form) { form.addEventListener('submit', function(e) { var formData = { form_id: this.id || 'unnamed_form', action: this.action }; fetch('<?php echo admin_url('admin-ajax.php'); ?>', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'action=ust_track_behavior&type=form_submit&value=' + encodeURIComponent(JSON.stringify(formData)) }); }); }); // 跟踪滚动深度 var scrollTracked = false; window.addEventListener('scroll', function() { var scrollPercent = (window.scrollY / (document.documentElement.scrollHeight - window.innerHeight)) * 100; if (scrollPercent > 50 && !scrollTracked) { scrollTracked = true; fetch('<?php echo admin_url('admin-ajax.php'); ?>', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'action=ust_track_behavior&type=scroll_depth&value=50' }); } }); }); })(); </script> <?php } } 第三章:多维度用户分群算法实现 3.1 设计分群规则引擎 在class-user-segmenter.php中扩展分群功能: class UST_User_Segmenter { private $segments = []; public function __construct() { $this->load_segments(); add_action('init', [$this, 'evaluate_user_segments']); add_action('admin_menu', [$this, 'add_admin_menu']); } private function load_segments() { global $wpdb; $table_name = $wpdb->prefix . 'ust_user_segments'; $this->segments = $wpdb->get_results( "SELECT * FROM $table_name WHERE is_active = 1" ); } public function evaluate_user_segments() { if (is_admin()) { return; } $user_segments = $this->get_user_segments(); setcookie('ust_user_segments', json_encode($user_segments), time() + 86400 * 30, '/'); $_SESSION['ust_user_segments'] = $user_segments; } private function get_user_segments() { global $wpdb; $user_segments = []; $session_id = isset($_SESSION['ust_session_id']) ? $_SESSION['ust_session_id'] : ''; if (empty($session_id)) { return $user_segments; } // 获取当前用户数据ID $user_data_table = $wpdb->prefix . 'ust_user_data'; $user_data = $wpdb->get_row($wpdb->prepare( "SELECT id, user_id, visit_count, device_type, location FROM $user_data_table WHERE session_id = %s", $session_id )); if (!$user_data) { return $user_segments; } // 获取用户行为数据 $behavior_table = $wpdb->prefix . 'ust_user_behavior'; get_results($wpdb->prepare( "SELECT behavior_type, COUNT(*) as count, MAX(timestamp) as last_time FROM $behavior_table WHERE user_data_id = %d GROUP BY behavior_type", $user_data->id )); $behavior_summary = []; foreach ($user_behaviors as $behavior) { $behavior_summary[$behavior->behavior_type] = [ 'count' => $behavior->count, 'last_time' => $behavior->last_time ]; } // 评估每个分群规则 foreach ($this->segments as $segment) { $rules = json_decode($segment->segment_rules, true); if ($this->evaluate_rules($rules, $user_data, $behavior_summary)) { $user_segments[] = $segment->segment_slug; // 记录用户分群关系 $this->record_user_segment($user_data->id, $segment->id); } } return $user_segments; } private function evaluate_rules($rules, $user_data, $behavior_summary) { foreach ($rules as $rule_group) { $group_result = true; foreach ($rule_group as $rule) { $rule_result = $this->evaluate_single_rule($rule, $user_data, $behavior_summary); if (!$rule_result) { $group_result = false; break; } } if ($group_result) { return true; } } return false; } private function evaluate_single_rule($rule, $user_data, $behavior_summary) { $field = $rule['field']; $operator = $rule['operator']; $value = $rule['value']; switch ($field) { case 'visit_count': $actual_value = $user_data->visit_count; return $this->compare_values($actual_value, $operator, $value); case 'device_type': $actual_value = $user_data->device_type; return $this->compare_strings($actual_value, $operator, $value); case 'user_role': $actual_value = $user_data->user_id ? $this->get_user_role($user_data->user_id) : 'guest'; return $this->compare_strings($actual_value, $operator, $value); case 'behavior_count': $behavior_type = $rule['behavior_type'] ?? 'page_view'; $actual_value = isset($behavior_summary[$behavior_type]) ? $behavior_summary[$behavior_type]['count'] : 0; return $this->compare_values($actual_value, $operator, $value); case 'last_visit_days': $last_visit = strtotime($user_data->last_visit); $days_diff = floor((time() - $last_visit) / (60 * 60 * 24)); return $this->compare_values($days_diff, $operator, $value); case 'location': $actual_value = $user_data->location; return $this->compare_strings($actual_value, $operator, $value); default: return false; } } private function compare_values($actual, $operator, $expected) { switch ($operator) { case 'equals': return $actual == $expected; case 'not_equals': return $actual != $expected; case 'greater_than': return $actual > $expected; case 'less_than': return $actual < $expected; case 'greater_than_equals': return $actual >= $expected; case 'less_than_equals': return $actual <= $expected; default: return false; } } private function compare_strings($actual, $operator, $expected) { switch ($operator) { case 'equals': return strtolower($actual) == strtolower($expected); case 'not_equals': return strtolower($actual) != strtolower($expected); case 'contains': return stripos($actual, $expected) !== false; case 'not_contains': return stripos($actual, $expected) === false; case 'starts_with': return stripos($actual, $expected) === 0; case 'ends_with': $expected_length = strlen($expected); return substr_compare($actual, $expected, -$expected_length, $expected_length, true) === 0; default: return false; } } private function get_user_role($user_id) { $user = get_userdata($user_id); if ($user && !empty($user->roles)) { return $user->roles[0]; } return 'guest'; } private function record_user_segment($user_data_id, $segment_id) { global $wpdb; $table_name = $wpdb->prefix . 'ust_user_segment_relationships'; // 检查是否已存在关系 $existing = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_data_id = %d AND segment_id = %d", $user_data_id, $segment_id )); if (!$existing) { $wpdb->insert( $table_name, [ 'user_data_id' => $user_data_id, 'segment_id' => $segment_id, 'assigned_at' => current_time('mysql') ] ); } } public function add_admin_menu() { add_menu_page( '用户分群管理', '用户分群', 'manage_options', 'user-segmentation', [$this, 'render_admin_page'], 'dashicons-groups', 30 ); add_submenu_page( 'user-segmentation', '分群规则管理', '分群规则', 'manage_options', 'user-segmentation-rules', [$this, 'render_rules_page'] ); add_submenu_page( 'user-segmentation', '用户行为分析', '行为分析', 'manage_options', 'user-segmentation-analytics', [$this, 'render_analytics_page'] ); } public function render_admin_page() { include UST_PLUGIN_DIR . 'admin/views/admin-main.php'; } public function render_rules_page() { include UST_PLUGIN_DIR . 'admin/views/rules-management.php'; } public function render_analytics_page() { include UST_PLUGIN_DIR . 'admin/views/analytics-dashboard.php'; } } ## 第四章:精准内容推送系统 ### 4.1 内容推送引擎设计 创建`class-content-pusher.php`: class UST_Content_Pusher { private $push_rules = []; public function __construct() { $this->load_push_rules(); add_filter('the_content', [$this, 'inject_targeted_content']); add_action('wp_footer', [$this, 'inject_floating_widgets']); add_action('admin_menu', [$this, 'add_push_rules_menu']); } private function load_push_rules() { global $wpdb; $table_name = $wpdb->prefix . 'ust_push_rules'; $this->push_rules = $wpdb->get_results( "SELECT * FROM $table_name WHERE is_active = 1 ORDER BY priority DESC" ); } public function inject_targeted_content($content) { if (is_admin() || !is_singular()) { return $content; } $user_segments = $this->get_current_user_segments(); $post_id = get_the_ID(); $post_type = get_post_type(); foreach ($this->push_rules as $rule) { if ($this->should_apply_rule($rule, $user_segments, $post_id, $post_type)) { $content = $this->apply_push_rule($content, $rule); } } return $content; } private function should_apply_rule($rule, $user_segments, $post_id, $post_type) { $rule_data = json_decode($rule->rule_conditions, true); // 检查目标分群 if (!empty($rule_data['target_segments'])) { $intersection = array_intersect($user_segments, $rule_data['target_segments']); if (empty($intersection)) { return false; } } // 检查内容类型 if (!empty($rule_data['post_types']) && !in_array($post_type, $rule_data['post_types'])) { return false; } // 检查特定文章/页面 if (!empty($rule_data['specific_posts']) && !in_array($post_id, $rule_data['specific_posts'])) { return false; } // 检查分类条件 if (!empty($rule_data['categories'])) { $post_categories = wp_get_post_categories($post_id, ['fields' => 'ids']); $intersection = array_intersect($post_categories, $rule_data['categories']); if (empty($intersection)) { return false; } } // 检查标签条件 if (!empty($rule_data['tags'])) { $post_tags = wp_get_post_tags($post_id, ['fields' => 'ids']); $intersection = array_intersect($post_tags, $rule_data['tags']); if (empty($intersection)) { return false; } } // 检查显示频率限制 if ($rule->display_limit > 0) { $display_count = $this->get_rule_display_count($rule->id); if ($display_count >= $rule->display_limit) { return false; } } return true; } private function apply_push_rule($content, $rule) { $rule_data = json_decode($rule->rule_conditions, true); $push_content = $this->get_push_content($rule->content_type, $rule->content_value); if (!$push_content) { return $content; } // 记录显示次数 $this->record_rule_display($rule->id); // 根据位置插入内容 switch ($rule_data['position']) { case 'before_content': return $push_content . $content; case 'after_content': return $content . $push_content; case 'after_paragraph': $paragraph_number = $rule_data['paragraph_number'] ?? 3; return $this->insert_after_paragraph($content, $push_content, $paragraph_number); case 'replace_content': $selector = $rule_data['css_selector'] ?? ''; if ($selector) { return $this->replace_by_selector($content, $push_content, $selector); } return $content; default: return $content; } } private function get_push_content($type, $value) { switch ($type) { case 'custom_html': return do_shortcode($value); case 'shortcode': return do_shortcode($value); case 'post': $post = get_post($value); if ($post) { return apply_filters('the_content', $post->post_content); } break; case 'widget': return $this->render_widget($value); case 'call_to_action': return $this->render_cta($value); } return ''; } private function insert_after_paragraph($content, $insertion, $paragraph_number) { $paragraphs = explode('</p>', $content); if (count($paragraphs) >= $paragraph_number) { $paragraphs[$paragraph_number - 1] .= $insertion; return implode('</p>', $paragraphs); } return $content . $insertion; } public function inject_floating_widgets() { if (is_admin()) { return; } $user_segments = $this->get_current_user_segments(); foreach ($this->push_rules as $rule) { $rule_data = json_decode($rule->rule_conditions, true); if ($rule_data['display_type'] === 'floating' && $this->should_apply_rule($rule, $user_segments, get_the_ID(), get_post_type())) { $this->render_floating_widget($rule); } } } private function render_floating_widget($rule) { $content = $this->get_push_content($rule->content_type, $rule->content_value); $rule_data = json_decode($rule->rule_conditions, true); ?> <div class="ust-floating-widget" data-position="<?php echo esc_attr($rule_data['float_position']); ?>" data-rule-id="<?php echo esc_attr($rule->id); ?>"> <div class="ust-floating-content"> <?php echo $content; ?> </div> <button class="ust-close-widget" aria-label="关闭">×</button> </div> <style> .ust-floating-widget { position: fixed; z-index: 999999; max-width: 300px; background: white; border-radius: 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.15); padding: 20px; animation: ust-fade-in 0.3s ease; } .ust-floating-widget[data-position="bottom-right"] { bottom: 20px; right: 20px; } .ust-floating-widget[data-position="bottom-left"] { bottom: 20px; left: 20px; } .ust-close-widget { position: absolute; top: 5px; right: 10px; background: none; border: none; font-size: 20px; cursor: pointer; color: #666; } @keyframes ust-fade-in { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } </style> <script> document.addEventListener('DOMContentLoaded', function() { document.querySelectorAll('.ust-close-widget').forEach(function(button) { button.addEventListener('click', function() { var widget = this.closest('.ust-floating-widget'); widget.style.display = 'none'; // 记录关闭行为 var ruleId = widget.getAttribute('data-rule-id'); fetch('<?php echo admin_url('admin-ajax.php'); ?>', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'action=ust_track_widget_close&rule_id=' + ruleId }); }); }); }); </script> <?php } private function get_current_user_segments() { if (isset($_SESSION['ust_user_segments'])) { return $_SESSION['ust_user_segments']; } if (isset($_COOKIE['ust_user_segments'])) { return json_decode(stripslashes($_COOKIE['ust_user_segments']), true); } return []; } } ## 第五章:常用互联网小工具集成 ### 5.1 小工具管理器实现 创建`class-tools-integration.php`: class UST_Tools_Integration { private $available_tools = []; public function __construct() { $this->register_tools(); add_action('widgets_init', [$this, 'register_widgets']); add_shortcode('ust_tool', [$this, 'render_tool_shortcode']); add_action('wp_ajax_ust_tool_action', [$this, 'handle_tool_ajax']); add_action('wp_ajax_nopriv_ust_tool_action', [$this, 'handle_tool_ajax']); } private function register_tools() { $this->available_tools = [ 'poll' => [ 'name' => '投票工具', 'description' => '创建简单的投票调查', 'callback' => [$this, 'render_poll_tool'], 'settings' => [ 'question' => ['type' => 'text', 'label' => '问题'], 'options' => ['type' => 'textarea', 'label' => '选项(每行一个)'], 'multiple' => ['type' => 'checkbox', 'label' => '允许多选'] ] ], 'calculator' => [ 'name' => '计算器工具', 'description' => '简单的计算器功能', 'callback' => [$this, 'render_calculator_tool'], 'settings' => [ 'type' => [ 'type' => 'select', 'label' => '计算器类型', 'options' => [ 'basic' => '基础计算器', 'loan' => '贷款计算器', 'currency' => '货币换算器' ] ] ] ], 'countdown' => [ 'name' => '倒计时工具', 'description' => '创建活动倒计时', 'callback' => [$this, 'render_countdown_tool'], 'settings' => [ 'target_date' => ['type' => 'date', 'label' => '目标日期'], 'title' => ['type' => 'text', 'label' => '标题'], 'show_days' => ['type' => 'checkbox', 'label' => '显示天数', 'default' => true] ] ], 'social_share' => [ 'name' => '社交分享工具', 'description' => '增强的社交分享按钮', 'callback' => [$this, 'render_social_share_tool'],
发表评论实战教程:在WordPress网站中添加基于位置的附近门店与服务查询功能 引言:位置服务在互联网应用中的重要性 在移动互联网时代,基于位置的服务(LBS)已成为各类网站和应用程序的标配功能。无论是电商平台、服务型企业还是内容网站,为用户提供附近门店查询、地理位置搜索等功能,都能显著提升用户体验和商业转化率。根据谷歌的研究,超过80%的消费者在寻找本地服务时会使用“附近”搜索功能,而其中76%的用户会在一天内访问相关门店。 WordPress作为全球最流行的内容管理系统,占据了互联网上超过43%的网站份额。然而,许多WordPress网站所有者并未充分利用位置服务的潜力。本教程将详细介绍如何通过代码二次开发,在WordPress网站中实现专业的附近门店与服务查询功能,无需依赖昂贵的第三方插件,同时保持对功能的完全控制。 第一部分:前期准备与环境配置 1.1 功能需求分析与设计 在开始编码之前,我们需要明确功能需求。一个完整的附近门店查询系统应包含以下核心功能: 门店信息管理(名称、地址、坐标、联系方式、营业时间等) 用户位置获取(自动检测或手动输入) 距离计算与排序算法 地图可视化展示 筛选与搜索功能 响应式设计,支持移动设备 1.2 开发环境搭建 确保你的WordPress开发环境满足以下要求: WordPress 5.0以上版本 PHP 7.4以上(推荐8.0+) MySQL 5.6以上或MariaDB 10.1以上 启用Apache/Nginx的rewrite模块 代码编辑器(VS Code、PHPStorm等) 1.3 获取地图API密钥 我们将使用Google Maps API作为地图服务,你也可以选择百度地图、高德地图等国内服务。以Google Maps为例: 访问Google Cloud Console(console.cloud.google.com) 创建新项目或选择现有项目 启用"Maps JavaScript API"、"Places API"和"Geocoding API" 创建API密钥并设置限制(推荐限制HTTP引用来源) 第二部分:数据库设计与门店管理 2.1 创建自定义数据表 虽然可以使用WordPress的自定义文章类型(CPT)存储门店信息,但为了更好的性能和查询效率,我们创建独立的数据表: // 在插件激活时创建数据表 function create_store_locations_table() { global $wpdb; $table_name = $wpdb->prefix . 'store_locations'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, store_name varchar(255) NOT NULL, address text NOT NULL, city varchar(100), state varchar(100), country varchar(100), postal_code varchar(20), latitude decimal(10,8) NOT NULL, longitude decimal(11,8) NOT NULL, phone varchar(30), email varchar(100), website varchar(255), description text, opening_hours text, categories varchar(255), featured tinyint(1) DEFAULT 0, active tinyint(1) DEFAULT 1, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY latitude_longitude (latitude, longitude), KEY city_state (city, state), KEY categories (categories(191)) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } register_activation_hook(__FILE__, 'create_store_locations_table'); 2.2 创建后台管理界面 为方便管理门店信息,我们需要在WordPress后台创建管理界面: // 添加管理菜单 add_action('admin_menu', 'register_store_locations_menu'); function register_store_locations_menu() { add_menu_page( '门店管理', '门店位置', 'manage_options', 'store-locations', 'store_locations_admin_page', 'dashicons-location-alt', 30 ); add_submenu_page( 'store-locations', '添加新门店', '添加新门店', 'manage_options', 'add-new-store', 'add_new_store_page' ); add_submenu_page( 'store-locations', '门店分类', '分类管理', 'manage_options', 'store-categories', 'store_categories_page' ); } // 主管理页面 function store_locations_admin_page() { global $wpdb; $table_name = $wpdb->prefix . 'store_locations'; // 处理批量操作 if (isset($_POST['action']) && $_POST['action'] == 'bulk_delete') { // 验证nonce和权限 check_admin_referer('bulk_action_nonce'); if (!current_user_can('manage_options')) { wp_die('权限不足'); } if (isset($_POST['store_ids'])) { $ids = array_map('intval', $_POST['store_ids']); $ids_placeholder = implode(',', array_fill(0, count($ids), '%d')); $wpdb->query($wpdb->prepare( "DELETE FROM $table_name WHERE id IN ($ids_placeholder)", $ids )); echo '<div class="notice notice-success"><p>已删除选中的门店</p></div>'; } } // 分页查询 $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $offset = ($current_page - 1) * $per_page; $total_items = $wpdb->get_var("SELECT COUNT(*) FROM $table_name"); $stores = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name ORDER BY created_at DESC LIMIT %d OFFSET %d", $per_page, $offset )); // 显示管理界面 ?> <div class="wrap"> <h1 class="wp-heading-inline">门店管理</h1> <a href="<?php echo admin_url('admin.php?page=add-new-store'); ?>" class="page-title-action">添加新门店</a> <form method="post" action=""> <?php wp_nonce_field('bulk_action_nonce'); ?> <input type="hidden" name="action" value="bulk_delete"> <div class="tablenav top"> <div class="alignleft actions bulkactions"> <select name="bulk_action"> <option value="-1">批量操作</option> <option value="delete">删除</option> </select> <input type="submit" class="button action" value="应用"> </div> <div class="tablenav-pages"> <?php $total_pages = ceil($total_items / $per_page); echo paginate_links(array( 'base' => add_query_arg('paged', '%#%'), 'format' => '', 'prev_text' => '«', 'next_text' => '»', 'total' => $total_pages, 'current' => $current_page )); ?> </div> </div> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <td class="manage-column column-cb check-column"> <input type="checkbox" id="cb-select-all-1"> </td> <th>ID</th> <th>门店名称</th> <th>地址</th> <th>城市</th> <th>电话</th> <th>状态</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach ($stores as $store): ?> <tr> <th scope="row" class="check-column"> <input type="checkbox" name="store_ids[]" value="<?php echo $store->id; ?>"> </th> <td><?php echo $store->id; ?></td> <td> <strong><?php echo esc_html($store->store_name); ?></strong> <?php if ($store->featured): ?> <span class="dashicons dashicons-star-filled" style="color:#ffb900;"></span> <?php endif; ?> </td> <td><?php echo esc_html($store->address); ?></td> <td><?php echo esc_html($store->city); ?></td> <td><?php echo esc_html($store->phone); ?></td> <td> <?php if ($store->active): ?> <span class="status-active">启用</span> <?php else: ?> <span class="status-inactive">禁用</span> <?php endif; ?> </td> <td> <a href="<?php echo admin_url('admin.php?page=add-new-store&id=' . $store->id); ?>">编辑</a> | <a href="#" class="delete-store" data-id="<?php echo $store->id; ?>">删除</a> </td> </tr> <?php endforeach; ?> </tbody> </table> </form> </div> <script> jQuery(document).ready(function($) { $('.delete-store').on('click', function(e) { e.preventDefault(); if (confirm('确定要删除这个门店吗?')) { var storeId = $(this).data('id'); $.post(ajaxurl, { action: 'delete_store', store_id: storeId, nonce: '<?php echo wp_create_nonce("delete_store_nonce"); ?>' }, function(response) { if (response.success) { location.reload(); } else { alert('删除失败:' + response.data); } }); } }); }); </script> <?php } 第三部分:核心功能开发 3.1 地理位置编码与坐标获取 将地址转换为地理坐标(经纬度)是位置服务的基础: class LocationGeocoder { private $api_key; private $cache_time = 604800; // 缓存7天 public function __construct($api_key) { $this->api_key = $api_key; } public function geocode_address($address) { // 检查缓存 $cache_key = 'geocode_' . md5($address); $cached = get_transient($cache_key); if ($cached !== false) { return $cached; } // 调用Google Geocoding API $url = 'https://maps.googleapis.com/maps/api/geocode/json'; $params = array( 'address' => urlencode($address), 'key' => $this->api_key, 'language' => 'zh-CN' ); $response = wp_remote_get($url . '?' . http_build_query($params)); if (is_wp_error($response)) { return array( 'success' => false, 'error' => $response->get_error_message() ); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if ($data['status'] !== 'OK') { return array( 'success' => false, 'error' => '地理编码失败:' . $data['status'] ); } $result = $data['results'][0]; $location = array( 'success' => true, 'latitude' => $result['geometry']['location']['lat'], 'longitude' => $result['geometry']['location']['lng'], 'formatted_address' => $result['formatted_address'], 'address_components' => $this->parse_address_components($result['address_components']) ); // 缓存结果 set_transient($cache_key, $location, $this->cache_time); return $location; } private function parse_address_components($components) { $parsed = array(); foreach ($components as $component) { foreach ($component['types'] as $type) { $parsed[$type] = $component['long_name']; } } return $parsed; } public function calculate_distance($lat1, $lon1, $lat2, $lon2, $unit = 'km') { $theta = $lon1 - $lon2; $dist = sin(deg2rad($lat1)) * sin(deg2rad($lat2)) + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * cos(deg2rad($theta)); $dist = acos($dist); $dist = rad2deg($dist); $miles = $dist * 60 * 1.1515; switch ($unit) { case 'km': return $miles * 1.609344; case 'm': return $miles * 1.609344 * 1000; default: return $miles; } } } 3.2 附近门店查询算法 实现高效的附近门店搜索算法: class NearbyStoresFinder { private $db; private $earth_radius = 6371; // 地球半径,单位公里 public function __construct() { global $wpdb; $this->db = $wpdb; } public function find_nearby_stores($latitude, $longitude, $radius_km = 10, $limit = 20, $category = null) { $table_name = $this->db->prefix . 'store_locations'; // 使用Haversine公式计算距离 $haversine = "( $this->earth_radius * acos( cos(radians(%f)) * cos(radians(latitude)) * cos(radians(longitude) - radians(%f)) + sin(radians(%f)) * sin(radians(latitude)) ) )"; $query = "SELECT *, $haversine AS distance FROM $table_name WHERE active = 1"; $params = array($latitude, $longitude, $latitude); // 添加分类筛选 if ($category) { $query .= " AND FIND_IN_SET(%s, categories)"; $params[] = $category; } $query .= " HAVING distance <= %d ORDER BY distance ASC LIMIT %d"; $params[] = $radius_km; $params[] = $limit; $prepared_query = $this->db->prepare($query, $params); return $this->db->get_results($prepared_query); } public function find_stores_by_bounds($north, $south, $east, $west, $category = null) { $table_name = $this->db->prefix . 'store_locations'; $query = "SELECT * FROM $table_name WHERE active = 1 AND latitude BETWEEN %f AND %f AND longitude BETWEEN %f AND %f"; $params = array($south, $north, $west, $east); if ($category) { $query .= " AND FIND_IN_SET(%s, categories)"; $params[] = $category; } $query .= " ORDER BY featured DESC, store_name ASC LIMIT 100"; $prepared_query = $this->db->prepare($query, $params); return $this->db->get_results($prepared_query); } } 第四部分:前端界面与用户体验 4.1 创建搜索表单与结果展示 class StoreLocatorFrontend { private $api_key; public function __construct($api_key) { $this->api_key = $api_key; add_shortcode('store_locator', array($this, 'render_store_locator')); add_action('wp_enqueue_scripts', array($this, 'enqueue_scripts')); add_action('wp_ajax_search_nearby_stores', array($this, 'ajax_search_nearby_stores')); add_action('wp_ajax_nopriv_search_nearby_stores', array($this, 'ajax_search_nearby_stores')); } public function enqueue_scripts() { global $post; if (has_shortcode($post->post_content, 'store_locator')) { // 加载Google Maps API wp_enqueue_script( 'google-maps-api', 'https://maps.googleapis.com/maps/api/js?key=' . $this->api_key . '&libraries=places&language=zh-CN', array(), null, true ); // 加载自定义脚本 wp_enqueue_script( 'store-locator-frontend', plugin_dir_url(__FILE__) . 'js/store-locator-frontend.js', array('jquery', 'google-maps-api'), '1.0.0', true ); // 加载样式 wp_enqueue_style( 'store-locator-style', plugin_dir_url(__FILE__) . 'css/store-locator.css', array(), '1.0.0' ); // 传递数据到前端 wp_localize_script('store-locator-frontend', 'storeLocatorData', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('store_locator_nonce'), 'default_lat' => 39.9042, // 默认坐标(北京) 'default_lng' => 116.4074, 'default_radius' => 10, 4.2 构建响应式前端界面 public function render_store_locator($atts) { $atts = shortcode_atts(array( 'default_radius' => 10, 'max_results' => 50, 'show_categories' => true, 'show_filters' => true ), $atts); // 获取所有门店分类 $categories = $this->get_store_categories(); ob_start(); ?> <div class="store-locator-container"> <div class="store-locator-header"> <h2>附近门店查询</h2> <p>输入地址或使用当前位置查找附近的门店</p> </div> <div class="store-locator-main"> <!-- 搜索面板 --> <div class="search-panel"> <div class="search-form"> <div class="form-group"> <label for="store-search-input">搜索地址或位置:</label> <div class="search-input-wrapper"> <input type="text" id="store-search-input" class="search-input" placeholder="输入地址、城市或邮政编码"> <button type="button" id="use-current-location" class="location-btn"> <span class="dashicons dashicons-location"></span> 使用当前位置 </button> </div> </div> <?php if ($atts['show_categories'] && !empty($categories)): ?> <div class="form-group"> <label>服务分类:</label> <div class="categories-filter"> <select id="store-category" class="category-select"> <option value="">所有分类</option> <?php foreach ($categories as $category): ?> <option value="<?php echo esc_attr($category); ?>"> <?php echo esc_html($category); ?> </option> <?php endforeach; ?> </select> </div> </div> <?php endif; ?> <div class="form-group"> <label>搜索半径:</label> <div class="radius-slider"> <input type="range" id="search-radius" min="1" max="50" value="<?php echo esc_attr($atts['default_radius']); ?>" class="radius-range"> <span id="radius-value"><?php echo esc_attr($atts['default_radius']); ?> 公里</span> </div> </div> <div class="form-actions"> <button type="button" id="search-stores" class="search-btn"> 搜索附近门店 </button> <button type="button" id="reset-search" class="reset-btn"> 重置搜索 </button> </div> </div> <!-- 搜索结果列表 --> <div class="results-panel"> <div class="results-header"> <h3>搜索结果</h3> <div class="results-count"> 找到 <span id="results-count">0</span> 个门店 </div> </div> <div class="results-list" id="stores-results"> <div class="no-results"> <p>请输入位置开始搜索</p> </div> </div> <div class="results-pagination" id="results-pagination" style="display: none;"> <button id="prev-page" class="pagination-btn" disabled>上一页</button> <span id="page-info">第 1 页</span> <button id="next-page" class="pagination-btn" disabled>下一页</button> </div> </div> </div> <!-- 地图显示区域 --> <div class="map-panel"> <div id="store-locator-map"></div> <div class="map-controls"> <button id="zoom-in" class="map-control-btn">+</button> <button id="zoom-out" class="map-control-btn">-</button> <button id="reset-map" class="map-control-btn">重置视图</button> </div> </div> </div> <!-- 门店详情模态框 --> <div id="store-details-modal" class="store-modal"> <div class="modal-content"> <div class="modal-header"> <h3 id="modal-store-name"></h3> <button class="modal-close">×</button> </div> <div class="modal-body"> <div class="store-info-grid"> <div class="info-item"> <span class="dashicons dashicons-location"></span> <div> <strong>地址:</strong> <span id="modal-store-address"></span> </div> </div> <div class="info-item"> <span class="dashicons dashicons-phone"></span> <div> <strong>电话:</strong> <span id="modal-store-phone"></span> </div> </div> <div class="info-item"> <span class="dashicons dashicons-clock"></span> <div> <strong>营业时间:</strong> <div id="modal-store-hours"></div> </div> </div> <div class="info-item"> <span class="dashicons dashicons-admin-site"></span> <div> <strong>距离:</strong> <span id="modal-store-distance"></span> </div> </div> </div> <div class="store-description"> <h4>门店介绍</h4> <p id="modal-store-description"></p> </div> <div class="store-actions"> <a href="#" id="modal-get-directions" class="action-btn" target="_blank"> <span class="dashicons dashicons-car"></span> 获取路线 </a> <a href="#" id="modal-call-store" class="action-btn"> <span class="dashicons dashicons-phone"></span> 拨打电话 </a> <a href="#" id="modal-visit-website" class="action-btn" target="_blank"> <span class="dashicons dashicons-external"></span> 访问网站 </a> </div> </div> </div> </div> </div> <!-- 加载状态 --> <div id="loading-overlay" style="display: none;"> <div class="loading-spinner"></div> <p>正在搜索附近门店...</p> </div> <?php return ob_get_clean(); } private function get_store_categories() { global $wpdb; $table_name = $wpdb->prefix . 'store_locations'; $categories = $wpdb->get_col( "SELECT DISTINCT categories FROM $table_name WHERE active = 1 AND categories IS NOT NULL" ); $all_categories = array(); foreach ($categories as $category_string) { $cat_array = explode(',', $category_string); foreach ($cat_array as $category) { $category = trim($category); if ($category && !in_array($category, $all_categories)) { $all_categories[] = $category; } } } sort($all_categories); return $all_categories; } 4.3 前端JavaScript交互逻辑 // store-locator-frontend.js (function($) { 'use strict'; class StoreLocator { constructor() { this.map = null; this.markers = []; this.infoWindow = null; this.currentLocation = null; this.currentRadius = parseInt(storeLocatorData.default_radius); this.currentCategory = ''; this.currentPage = 1; this.storesPerPage = 10; this.allStores = []; this.init(); } init() { this.initMap(); this.bindEvents(); this.initAutocomplete(); } initMap() { const defaultLocation = { lat: parseFloat(storeLocatorData.default_lat), lng: parseFloat(storeLocatorData.default_lng) }; this.map = new google.maps.Map(document.getElementById('store-locator-map'), { zoom: 12, center: defaultLocation, mapTypeControl: true, streetViewControl: false, fullscreenControl: true, styles: this.getMapStyles() }); this.infoWindow = new google.maps.InfoWindow({ maxWidth: 300 }); // 添加初始标记 this.addInitialMarker(defaultLocation); } getMapStyles() { return [ { featureType: 'poi.business', stylers: [{ visibility: 'on' }] }, { featureType: 'transit', elementType: 'labels.icon', stylers: [{ visibility: 'off' }] } ]; } addInitialMarker(position) { const marker = new google.maps.Marker({ position: position, map: this.map, icon: { path: google.maps.SymbolPath.CIRCLE, scale: 8, fillColor: '#4285F4', fillOpacity: 1, strokeColor: '#FFFFFF', strokeWeight: 2 }, title: '中心位置' }); this.markers.push(marker); } bindEvents() { // 搜索按钮 $('#search-stores').on('click', () => this.searchStores()); // 使用当前位置 $('#use-current-location').on('click', () => this.getCurrentLocation()); // 半径滑块 $('#search-radius').on('input', (e) => { const radius = parseInt(e.target.value); $('#radius-value').text(radius + ' 公里'); this.currentRadius = radius; }); // 分类选择 $('#store-category').on('change', (e) => { this.currentCategory = e.target.value; }); // 重置搜索 $('#reset-search').on('click', () => this.resetSearch()); // 分页按钮 $('#prev-page').on('click', () => this.changePage(-1)); $('#next-page').on('click', () => this.changePage(1)); // 地图控制 $('#zoom-in').on('click', () => this.map.setZoom(this.map.getZoom() + 1)); $('#zoom-out').on('click', () => this.map.setZoom(this.map.getZoom() - 1)); $('#reset-map').on('click', () => this.resetMapView()); // 模态框关闭 $('.modal-close, #store-details-modal').on('click', (e) => { if (e.target === e.currentTarget) { this.closeModal(); } }); // 键盘事件 $(document).on('keyup', (e) => { if (e.key === 'Escape') this.closeModal(); }); } initAutocomplete() { const input = document.getElementById('store-search-input'); const autocomplete = new google.maps.places.Autocomplete(input, { types: ['geocode', 'establishment'], componentRestrictions: { country: 'cn' } }); autocomplete.addListener('place_changed', () => { const place = autocomplete.getPlace(); if (!place.geometry) { alert('未找到该位置,请尝试更详细的地址'); return; } this.currentLocation = { lat: place.geometry.location.lat(), lng: place.geometry.location.lng() }; this.updateMapCenter(this.currentLocation); this.searchStores(); }); } getCurrentLocation() { if (!navigator.geolocation) { alert('您的浏览器不支持地理位置功能'); return; } $('#use-current-location').prop('disabled', true).text('定位中...'); navigator.geolocation.getCurrentPosition( (position) => { this.currentLocation = { lat: position.coords.latitude, lng: position.coords.longitude }; // 反向地理编码获取地址 const geocoder = new google.maps.Geocoder(); geocoder.geocode({ location: this.currentLocation }, (results, status) => { if (status === 'OK' && results[0]) { $('#store-search-input').val(results[0].formatted_address); } }); this.updateMapCenter(this.currentLocation); this.searchStores(); $('#use-current-location').prop('disabled', false).html( '<span class="dashicons dashicons-location"></span> 使用当前位置' ); }, (error) => { let errorMessage = '无法获取您的位置:'; switch(error.code) { case error.PERMISSION_DENIED: errorMessage += '用户拒绝了位置请求'; break; case error.POSITION_UNAVAILABLE: errorMessage += '位置信息不可用'; break; case error.TIMEOUT: errorMessage += '获取位置超时'; break; default: errorMessage += '未知错误'; } alert(errorMessage); $('#use-current-location').prop('disabled', false).html( '<span class="dashicons dashicons-location"></span> 使用当前位置' ); }, { enableHighAccuracy: true, timeout: 10000, maximumAge: 0 } ); } updateMapCenter(location) { this.map.setCenter(location); // 更新中心标记 if (this.markers.length > 0) { this.markers[0].setPosition(location); } } async searchStores() { if (!this.currentLocation) { alert('请先选择或输入一个位置'); return; } this.showLoading(true); try { const response = await $.ajax({ url: storeLocatorData.ajax_url, method: 'POST', data: { action: 'search_nearby_stores', nonce: storeLocatorData.nonce, latitude: this.currentLocation.lat, longitude: this.currentLocation.lng, radius: this.currentRadius, category: this.currentCategory } }); if (response.success) { this.allStores = response.data.stores; this.currentPage = 1; this.displayResults(); this.displayMarkers(); this.updatePagination(); } else { throw new Error(response.data); } } catch (error) { console.error('搜索失败:', error); alert('搜索失败,请稍后重试'); } finally { this.showLoading(false); } } displayResults() { const $resultsContainer = $('#stores-results'); const startIndex = (this.currentPage - 1) * this.storesPerPage; const endIndex = startIndex + this.storesPerPage; const currentStores = this.allStores.slice(startIndex, endIndex); if (currentStores.length === 0) { $resultsContainer.html(` <div class="no-results"> <p>在指定范围内未找到门店</p> <p>尝试扩大搜索半径或选择其他位置</p> </div> `); return; } let html = ''; currentStores.forEach((store, index) => { html += this.getStoreCardHtml(store, startIndex + index + 1); }); $resultsContainer.html(html); $('#results-count').text(this.allStores.length); // 绑定卡片点击事件 $('.store-card').on('click', (e) => { const storeId = $(e.currentTarget).data('store-id'); const store = this.allStores.find(s => s.id == storeId); if (store) { this.showStoreDetails(store); } }); } getStoreCardHtml(store, index) { const distance = parseFloat(store.distance).toFixed(1); const featuredClass = store.featured ? 'featured' : ''; return ` <div class="store-card ${featuredClass}" data-store-id="${store.id}"> <div class="store-card-header"> <span class="store-index">${index}</span> <h4 class="store-name">${this.escapeHtml(store.store_name)}</h4> ${store.featured ? '<span class="featured-badge">推荐</span>' : ''} </div> <div class="store-card-body"> <div class="store-info"> <p class="store-address"> <span class="dashicons dashicons-location"></span> ${this.escapeHtml(store.address)} </p> ${store.phone ? ` <p class="store-phone"> <span class="dashicons dashicons-phone"></span> ${this.escapeHtml(store.phone)} </p> ` : ''} <p class="store-distance"> <span class="dashicons dashicons-admin-site"></span> 距离:${distance} 公里 </p> </div> <div class="store-actions"> <button class="btn-details" data-store-id="${store.id}"> 查看详情 </button> <button class="btn-directions" data-store-id="${store.id}"> 路线规划 </button> </div> </div> </div> `; } displayMarkers() { // 清除现有标记 this.clearMarkers(); // 添加新标记 this.allStores.forEach(store => { const marker = new google.maps.Marker({ position: { lat: parseFloat(store.latitude), lng: parseFloat(store.longitude) }, map: this.map, title: store.store_name, icon: this.getMarkerIcon(store.featured) });
发表评论手把手教学:为WordPress集成智能化的网站内容合规性检查工具 引言:内容合规性检查在数字时代的重要性 在当今数字化时代,网站内容合规性已成为网站运营者不可忽视的重要议题。随着全球各地对网络内容监管的加强,从数据隐私保护到版权管理,从敏感信息过滤到行业规范遵守,网站内容合规性检查已成为确保网站安全运营、避免法律风险的关键环节。 对于使用WordPress构建的网站而言,作为全球最流行的内容管理系统,它承载着互联网上超过40%的网站内容。然而,WordPress本身并未内置全面的内容合规性检查功能,这给网站管理员带来了不小的挑战。手动检查每一条发布的内容不仅耗时耗力,而且容易遗漏潜在问题。 本文将详细介绍如何通过WordPress代码二次开发,集成智能化的网站内容合规性检查工具,帮助网站管理员自动化完成内容审核,确保网站内容符合法律法规和行业标准。 第一部分:理解WordPress内容合规性检查的核心需求 1.1 内容合规性的多维度考量 网站内容合规性检查涉及多个维度,主要包括: 法律法规合规性:确保内容不违反国家法律法规,不包含违法信息 版权合规性:检查内容是否侵犯他人知识产权 敏感信息过滤:识别并处理个人隐私信息、商业秘密等敏感内容 行业特定规范:根据不同行业要求检查内容合规性(如医疗、金融等行业) 平台政策遵守:确保内容符合社交媒体平台、搜索引擎等的要求 1.2 WordPress内容发布流程分析 要有效集成合规性检查工具,首先需要理解WordPress的内容发布流程: 内容创建与编辑阶段 预览与保存草稿阶段 发布与更新阶段 已发布内容的定期检查阶段 理想的合规性检查工具应在这些关键节点介入,提供实时或批量的内容检查功能。 1.3 现有解决方案的局限性 目前市场上有一些WordPress合规性检查插件,但它们往往存在以下问题: 功能单一,无法满足多维度检查需求 缺乏智能化,误报率较高 定制化程度低,难以适应特定行业需求 性能影响较大,拖慢网站速度 数据隐私保护不足,可能将内容发送到第三方服务器 因此,通过代码二次开发实现定制化的合规性检查工具,成为许多专业网站的选择。 第二部分:构建WordPress合规性检查工具的技术基础 2.1 WordPress插件开发基础 在开始开发合规性检查工具之前,需要掌握以下WordPress开发基础知识: WordPress插件结构:了解插件文件组织方式、主文件编写规范 动作钩子(Action Hooks)和过滤器钩子(Filter Hooks):这是WordPress扩展功能的核心机制 自定义数据库表:用于存储检查结果和历史记录 WordPress REST API:为前后端交互提供接口 安全性最佳实践:包括数据验证、权限检查、非ce验证等 2.2 合规性检查的技术实现方案 合规性检查可以通过以下技术方案实现: 本地规则引擎:基于正则表达式和关键词库的规则匹配 机器学习模型:使用预训练的NLP模型识别敏感内容 第三方API集成:调用专业合规性检查服务的API 混合方案:结合本地规则和云端智能检查 考虑到性能和数据隐私,我们建议采用混合方案,将基础检查放在本地,复杂分析通过API完成。 2.3 开发环境搭建 开始开发前,需要准备以下环境: 本地WordPress开发环境(推荐使用Local by Flywheel或Docker) 代码编辑器(VS Code、PHPStorm等) 版本控制系统(Git) PHP 7.4+和MySQL 5.6+ 必要的调试工具(Query Monitor、Debug Bar等) 第三部分:手把手创建WordPress合规性检查插件 3.1 插件基础结构创建 首先,创建插件的基本文件结构: wp-content/plugins/content-compliance-checker/ ├── content-compliance-checker.php # 主插件文件 ├── includes/ │ ├── class-compliance-checker.php # 主功能类 │ ├── class-rule-engine.php # 规则引擎类 │ ├── class-api-handler.php # API处理器类 │ └── class-db-manager.php # 数据库管理类 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 后台样式 │ ├── js/ │ │ └── admin-script.js # 后台脚本 │ └── partials/ │ └── admin-display.php # 后台界面 ├── public/ │ ├── css/ │ │ └── public-style.css # 前台样式 │ └── js/ │ └── public-script.js # 前台脚本 ├── assets/ │ └── images/ # 图片资源 └── languages/ # 国际化文件 3.2 主插件文件编写 创建主插件文件content-compliance-checker.php: <?php /** * Plugin Name: 内容合规性检查工具 * Plugin URI: https://yourwebsite.com/content-compliance-checker * Description: 智能化的WordPress网站内容合规性检查工具 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later * Text Domain: content-compliance-checker */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('CCC_VERSION', '1.0.0'); define('CCC_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('CCC_PLUGIN_URL', plugin_dir_url(__FILE__)); define('CCC_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { $prefix = 'CCC_'; $base_dir = CCC_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class_name, $len) !== 0) { return; } $relative_class = substr($class_name, $len); $file = $base_dir . 'class-' . str_replace('_', '-', strtolower($relative_class)) . '.php'; if (file_exists($file)) { require_once $file; } }); // 初始化插件 function ccc_init_plugin() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('内容合规性检查工具需要WordPress 5.0或更高版本。', 'content-compliance-checker'); echo '</p></div>'; }); return; } // 实例化主类 $compliance_checker = new CCC_Compliance_Checker(); $compliance_checker->init(); } add_action('plugins_loaded', 'ccc_init_plugin'); // 激活和停用钩子 register_activation_hook(__FILE__, 'ccc_activate_plugin'); register_deactivation_hook(__FILE__, 'ccc_deactivate_plugin'); function ccc_activate_plugin() { require_once CCC_PLUGIN_DIR . 'includes/class-db-manager.php'; CCC_DB_Manager::create_tables(); // 设置默认选项 $default_options = array( 'enable_auto_check' => true, 'check_on_publish' => true, 'check_on_update' => true, 'check_on_draft' => false, 'block_on_violation' => true, 'notify_admin' => true, 'sensitivity_level' => 'medium', 'api_key' => '', 'api_service' => 'local', ); add_option('ccc_settings', $default_options); // 创建默认规则 ccc_create_default_rules(); } function ccc_deactivate_plugin() { // 清理临时数据 wp_clear_scheduled_hook('ccc_daily_compliance_check'); } function ccc_create_default_rules() { $default_rules = array( array( 'rule_name' => '敏感词检测', 'rule_type' => 'keyword', 'rule_pattern' => '赌博|毒品|色情|暴力|恐怖主义', 'rule_action' => 'block', 'rule_priority' => 10, 'is_active' => 1 ), array( 'rule_name' => '电话号码检测', 'rule_type' => 'regex', 'rule_pattern' => '/bd{3}[-.]?d{3}[-.]?d{4}b/', 'rule_action' => 'flag', 'rule_priority' => 5, 'is_active' => 1 ), array( 'rule_name' => '邮箱地址检测', 'rule_type' => 'regex', 'rule_pattern' => '/b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Z|a-z]{2,}b/', 'rule_action' => 'flag', 'rule_priority' => 5, 'is_active' => 1 ) ); $existing_rules = get_option('ccc_rules', array()); if (empty($existing_rules)) { update_option('ccc_rules', $default_rules); } } 3.3 主功能类实现 创建includes/class-compliance-checker.php: <?php class CCC_Compliance_Checker { private $rule_engine; private $api_handler; private $db_manager; public function __construct() { $this->rule_engine = new CCC_Rule_Engine(); $this->api_handler = new CCC_API_Handler(); $this->db_manager = new CCC_DB_Manager(); } public function init() { // 添加内容检查钩子 add_filter('wp_insert_post_data', array($this, 'check_post_content'), 99, 2); add_action('save_post', array($this, 'check_post_on_save'), 10, 3); add_action('publish_post', array($this, 'check_post_on_publish'), 10, 2); // 添加上传文件检查 add_filter('wp_handle_upload_prefilter', array($this, 'check_uploaded_file')); // 添加评论检查 add_filter('preprocess_comment', array($this, 'check_comment_content')); // 添加后台管理界面 if (is_admin()) { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); add_action('add_meta_boxes', array($this, 'add_compliance_meta_box')); } // 添加定期检查任务 add_action('ccc_daily_compliance_check', array($this, 'run_daily_compliance_check')); if (!wp_next_scheduled('ccc_daily_compliance_check')) { wp_schedule_event(time(), 'daily', 'ccc_daily_compliance_check'); } // 初始化REST API端点 add_action('rest_api_init', array($this, 'register_rest_routes')); } public function check_post_content($data, $postarr) { $settings = get_option('ccc_settings'); // 检查是否启用自动检查 if (!$settings['enable_auto_check']) { return $data; } // 检查草稿(如果设置允许) if ($data['post_status'] === 'draft' && !$settings['check_on_draft']) { return $data; } $post_id = isset($postarr['ID']) ? $postarr['ID'] : 0; $content_to_check = $data['post_content'] . ' ' . $data['post_title']; // 执行合规性检查 $result = $this->perform_compliance_check($content_to_check, $post_id); if ($result['has_violation']) { if ($settings['block_on_violation']) { // 阻止发布并显示错误 add_filter('redirect_post_location', function($location) use ($result) { return add_query_arg('ccc_error', urlencode($result['message']), $location); }); // 将文章状态改为草稿 $data['post_status'] = 'draft'; } // 记录违规 $this->db_manager->log_violation(array( 'post_id' => $post_id, 'violation_type' => $result['violation_type'], 'violation_details' => json_encode($result['violations']), 'check_date' => current_time('mysql') )); // 发送通知(如果启用) if ($settings['notify_admin']) { $this->send_notification($post_id, $result); } } return $data; } public function check_post_on_save($post_id, $post, $update) { // 跳过自动保存和修订 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } if (wp_is_post_revision($post_id)) { return; } // 检查用户权限 if (!current_user_can('edit_post', $post_id)) { return; } $settings = get_option('ccc_settings'); // 检查更新(如果设置允许) if ($update && !$settings['check_on_update']) { return; } // 如果文章已发布,执行额外检查 if ($post->post_status === 'publish') { $this->perform_compliance_check($post->post_content . ' ' . $post->post_title, $post_id); } } public function check_post_on_publish($post_id, $post) { $settings = get_option('ccc_settings'); if (!$settings['check_on_publish']) { return; } $result = $this->perform_compliance_check($post->post_content . ' ' . $post->post_title, $post_id); if ($result['has_violation']) { // 记录发布时的违规 update_post_meta($post_id, '_ccc_publish_violation', $result); } } public function check_uploaded_file($file) { $settings = get_option('ccc_settings'); if (!$settings['enable_auto_check']) { return $file; } // 检查文件类型 $allowed_types = array('jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx'); $file_ext = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); if (!in_array($file_ext, $allowed_types)) { $file['error'] = '不支持的文件类型。只允许上传:' . implode(', ', $allowed_types); return $file; } // 检查文件内容(如果是图片,可以检查EXIF数据等) if (in_array($file_ext, array('jpg', 'jpeg', 'png', 'gif'))) { // 这里可以添加图片内容检查逻辑 // 例如检查图片是否包含敏感内容 } return $file; } public function check_comment_content($commentdata) { $settings = get_option('ccc_settings'); if (!$settings['enable_auto_check']) { return $commentdata; } $content_to_check = $commentdata['comment_content']; $result = $this->perform_compliance_check($content_to_check, 0, 'comment'); if ($result['has_violation']) { if ($settings['block_on_violation']) { wp_die(__('您的评论包含不合规内容,无法提交。', 'content-compliance-checker')); } else { // 标记评论为待审核 $commentdata['comment_approved'] = 0; add_comment_meta($commentdata['comment_ID'], '_ccc_flagged', true, true); } } return $commentdata; } private function perform_compliance_check($content, $post_id = 0, $content_type = 'post') { $result = array( 'has_violation' => false, 'violation_type' => '', 'violations' => array(), 'message' => '' ); // 1. 使用本地规则引擎检查 $rule_violations = $this->rule_engine->check_content($content); if (!empty($rule_violations)) { $result['has_violation'] = true; $result['violation_type'] = 'rule_violation'; $result['violations'] = array_merge($result['violations'], $rule_violations); } // 2. 使用API检查(如果配置了API) $settings = get_option('ccc_settings'); if (!empty($settings['api_key']) && $settings['api_service'] !== 'local') { $api_result = $this->api_handler->check_content($content, $content_type); if ($api_result['has_violation']) { $result['has_violation'] = true; $result['violation_type'] = $api_result['violation_type']; $result['violations'] = array_merge($result['violations'], $api_result['violations']); } } // 3. 生成用户友好的消息 if ($result['has_violation']) { $violation_count = count($result['violations']); $result['message'] = sprintf( __('内容包含%d处不合规问题:', 'content-compliance-checker'), $violation_count ); foreach ($result['violations'] as $violation) { $result['message'] .= "n- " . $violation['description']; } } // 4. 记录检查结果 $this->db_manager->log_check_result(array( 'post_id' => $post_id, 'content_type' => $content_type, 'check_result' => json_encode($result), 'check_date' => current_time('mysql') )); return $result; } private function send_notification($post_id, $check_result) { $admin_email = get_option('admin_email'); $post_title = get_the_title($post_id); $post_edit_url = admin_url('post.php?post=' . $post_id . '&action=edit'); $subject = sprintf(__('[合规性警报] 文章 "%s" 包含不合规内容', 'content-compliance-checker'), $post_title); $message = sprintf(__('文章 "%s" 在发布时检测到不合规内容。', 'content-compliance-checker'), $post_title) . "nn"; $message .= __('检测到的问题:', 'content-compliance-checker') . "n"; foreach ($check_result['violations'] as $violation) { $message .= '- ' . $violation['description'] . "n"; } $message .= "n" . __('文章编辑链接:', 'content-compliance-checker') . "n"; $message .= $post_edit_url . "nn"; $message .= __('此邮件由内容合规性检查工具自动发送。', 'content-compliance-checker'); wp_mail($admin_email, $subject, $message); } public function add_admin_menu() { add_menu_page( __('内容合规性检查', 'content-compliance-checker'), __('合规性检查', 'content-compliance-checker'), 'manage_options', 'ccc-dashboard', array($this, 'display_dashboard_page'), 'dashicons-shield', 30 ); add_submenu_page( 'ccc-dashboard', __('合规性设置', 'content-compliance-checker'), __('设置', 'content-compliance-checker'), 'manage_options', 'ccc-settings', array($this, 'display_settings_page') ); add_submenu_page( 'ccc-dashboard', __('检查规则', 'content-compliance-checker'), __('规则管理', 'content-compliance-checker'), 'manage_options', 'ccc-rules', array($this, 'display_rules_page') ); add_submenu_page( 'ccc-dashboard', __('检查记录', 'content-compliance-checker'), __('检查记录', 'content-compliance-checker'), 'manage_options', 'ccc-logs', array($this, 'display_logs_page') ); add_submenu_page( 'ccc-dashboard', __('批量检查', 'content-compliance-checker'), __('批量检查', 'content-compliance-checker'), 'manage_options', 'ccc-bulk-check', array($this, 'display_bulk_check_page') ); } public function display_dashboard_page() { include CCC_PLUGIN_DIR . 'admin/partials/dashboard.php'; } public function display_settings_page() { include CCC_PLUGIN_DIR . 'admin/partials/settings.php'; } public function display_rules_page() { include CCC_PLUGIN_DIR . 'admin/partials/rules.php'; } public function display_logs_page() { include CCC_PLUGIN_DIR . 'admin/partials/logs.php'; } public function display_bulk_check_page() { include CCC_PLUGIN_DIR . 'admin/partials/bulk-check.php'; } public function enqueue_admin_scripts($hook) { if (strpos($hook, 'ccc-') === false) { return; } wp_enqueue_style( 'ccc-admin-style', CCC_PLUGIN_URL . 'admin/css/admin-style.css', array(), CCC_VERSION ); wp_enqueue_script( 'ccc-admin-script', CCC_PLUGIN_URL . 'admin/js/admin-script.js', array('jquery', 'wp-util'), CCC_VERSION, true ); wp_localize_script('ccc-admin-script', 'ccc_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('ccc_ajax_nonce') )); } public function add_compliance_meta_box() { $post_types = get_post_types(array('public' => true), 'names'); foreach ($post_types as $post_type) { add_meta_box( 'ccc-compliance-meta-box', __('内容合规性检查', 'content-compliance-checker'), array($this, 'render_compliance_meta_box'), $post_type, 'side', 'high' ); } } public function render_compliance_meta_box($post) { $check_results = $this->db_manager->get_post_check_results($post->ID); echo '<div id="ccc-compliance-status">'; if (empty($check_results)) { echo '<p>' . __('尚未检查此内容。', 'content-compliance-checker') . '</p>'; echo '<button type="button" class="button button-secondary" id="ccc-check-now">' . __('立即检查', 'content-compliance-checker') . '</button>'; } else { $latest_result = end($check_results); $result_data = json_decode($latest_result->check_result, true); if ($result_data['has_violation']) { echo '<div class="ccc-alert ccc-alert-error">'; echo '<p><strong>' . __('检测到不合规内容', 'content-compliance-checker') . '</strong></p>'; echo '<ul>'; foreach ($result_data['violations'] as $violation) { echo '<li>' . esc_html($violation['description']) . '</li>'; } echo '</ul>'; echo '</div>'; } else { echo '<div class="ccc-alert ccc-alert-success">'; echo '<p><strong>' . __('内容合规', 'content-compliance-checker') . '</strong></p>'; echo '<p>' . sprintf(__('最后检查时间:%s', 'content-compliance-checker'), $latest_result->check_date) . '</p>'; echo '</div>'; } echo '<button type="button" class="button button-secondary" id="ccc-recheck-now">' . __('重新检查', 'content-compliance-checker') . '</button>'; } echo '</div>'; } public function run_daily_compliance_check() { // 获取所有已发布的文章 $args = array( 'post_type' => 'post', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids' ); $post_ids = get_posts($args); foreach ($post_ids as $post_id) { $post = get_post($post_id); $this->perform_compliance_check($post->post_content . ' ' . $post->post_title, $post_id); } // 记录批量检查完成 $this->db_manager->log_bulk_check(array( 'check_type' => 'daily', 'items_checked' => count($post_ids), 'check_date' => current_time('mysql') )); } public function register_rest_routes() { register_rest_route('ccc/v1', '/check-content', array( 'methods' => 'POST', 'callback' => array($this, 'rest_check_content'), 'permission_callback' => function() { return current_user_can('edit_posts'); }, 'args' => array( 'content' => array( 'required' => true, 'validate_callback' => function($param) { return is_string($param) && !empty($param); } ), 'content_type' => array( 'required' => false, 'default' => 'post' ) ) )); register_rest_route('ccc/v1', '/get-stats', array( 'methods' => 'GET', 'callback' => array($this, 'rest_get_stats'), 'permission_callback' => function() { return current_user_can('manage_options'); } )); } public function rest_check_content($request) { $content = $request->get_param('content'); $content_type = $request->get_param('content_type'); $result = $this->perform_compliance_check($content, 0, $content_type); return rest_ensure_response($result); } public function rest_get_stats() { $stats = $this->db_manager->get_compliance_stats(); return rest_ensure_response($stats); } } ### 3.4 规则引擎类实现 创建`includes/class-rule-engine.php`: <?phpclass CCC_Rule_Engine { private $rules; public function __construct() { $this->load_rules(); } private function load_rules() { $this->rules = get_option('ccc_rules', array()); // 按优先级排序 usort($this->rules, function($a, $b) { return $b['rule_priority'] - $a['rule_priority']; }); } public function check_content($content) { $violations = array(); foreach ($this->rules as $rule) { if (!$rule['is_active']) { continue; } $matches = $this->apply_rule($rule, $content); if (!empty($matches)) { $violations[] = array( 'rule_id' => $rule['rule_id'] ?? uniqid(), 'rule_name' => $rule['rule_name'], 'rule_type' => $rule['rule_type'], 'matches' => $matches, 'description' => $this->generate_violation_description($rule, $matches) ); } } return $violations; } private function apply_rule($rule, $content) { $matches = array(); switch ($rule['rule_type']) { case 'keyword': $keywords = explode('|', $rule['rule_pattern']); foreach ($keywords as $keyword) { $keyword = trim($keyword); if (stripos($content, $keyword) !== false) { $matches[] = array( 'type' => 'keyword', 'value' => $keyword, 'position' => stripos($content, $keyword) ); } } break; case 'regex': if (@preg_match_all($rule['rule_pattern'], $content, $regex_matches, PREG_OFFSET_CAPTURE)) { foreach ($regex_matches[0] as $match) { $matches[] = array( 'type' => 'regex', 'value' => $match[0], 'position' => $match[1] ); } } break; case 'ml': // 机器学习规则 - 可以集成TensorFlow PHP或调用外部API // 这里是一个简单的实现示例 $sentiment = $this->analyze_sentiment($content); if ($sentiment['score'] < -0.5) { // 非常负面的情绪 $matches[] = array( 'type' => 'sentiment', 'value' => $sentiment['label'], 'score' => $sentiment['score'] ); } break; } return $matches; } private function analyze_sentiment($text) { // 简化的情感分析 $negative_words = array('糟糕', '差劲', '讨厌', '恶心', '垃圾', '骗子', '诈骗'); $positive_words = array('优秀', '很好', '喜欢', '满意', '推荐', '超值', '完美'); $negative_count = 0; $positive_count = 0; $total_words = str_word_count($text); if ($total_words === 0) { return array('label' => '中性', 'score' => 0); } foreach ($negative_words as $word) { $negative_count += substr_count($text, $word); } foreach ($positive_words as $word) { $positive_count += substr_count($text, $word); } $score = ($positive_count - $negative_count) / max(1, $total_words); if ($score > 0.1) { $label = '积极'; } elseif ($score < -0.1) { $label = '消极'; } else { $label = '中性'; } return array('label' => $label, 'score' => $score); } private function generate_violation_description($rule, $matches) { $description = sprintf(__('违反规则 "%s"', 'content-compliance-checker'), $rule['rule_name']); if (count($matches) <= 3) { $description .= ':'; $match_values = array(); foreach ($matches as $match) { $match_values[] = $match['value']; } $description .= implode('、', array_unique($match_values)); } else { $description .= sprintf(__('(发现%d处匹配)', 'content-compliance-checker'), count($matches)); } return $description; } public function add_rule($rule_data) { $rule_id = uniqid(); $rule_data['rule_id'] = $rule_id; $rule_data['is_active'] = $rule_data['is_active'] ?? 1; $this->rules[] = $rule_data; $this->save_rules(); return $rule_id; } public function update_rule($rule_id, $rule_data) { foreach ($this->rules as &$rule) { if ($rule['rule_id'] === $rule_id) { $rule = array_merge($rule, $rule_data); $this->save_rules(); return true; } } return false; } public function delete_rule($rule_id) { $this->rules = array_filter($this->rules, function($rule) use ($rule_id) { return $rule['rule_id'] !== $rule_id; }); $this->save_rules(); return true; } public function get_rules() { return $this->rules; } public function get_rule($rule_id) { foreach ($this->rules as $rule) { if ($rule['rule_id'] === $rule_id) { return $rule; } } return null; } private function save_rules() { update_option('ccc_rules', $this->rules); } } ### 3.5 API处理器类实现 创建`includes/class-api-handler.php`: <?phpclass CCC_API_Handler { private $api_services = array( 'google_safe_browsing' => array( 'name' => 'Google Safe Browsing', 'endpoint' => 'https://safebrowsing.googleapis.com/v4/threatMatches:find', 'requires_key' => true ), 'microsoft_presidio' => array( 'name' => 'Microsoft Presidio', 'endpoint' => 'https://presidio.azurewebsites.net/analyze', 'requires_key' => false ), 'openai_moderation' => array( 'name' => 'OpenAI Moderation', 'endpoint' => 'https://api.openai.com/v1/moderations', 'requires_key' => true ) ); public function check_content($content, $content_type = 'post') { $settings = get_option('ccc_settings'); $api_service = $settings['api_service']; $api_key = $settings['api_key']; if ($api_service === 'local' || empty($api_key)) { return array('has_violation' => false, 'violations' => array()); } if (!isset($this->api_services[$api_service])) { return array( 'has_violation' => false, 'violations' => array(), 'error' => '未知的API服务' ); } $service_config = $this->api_services[$api_service]; try { switch ($api_service) { case 'google_safe_browsing': return $this->check_google_safe_browsing($content, $api_key); case 'openai_moderation': return $this->check_openai_moderation($content, $api_key); case 'microsoft_presidio': return $this->check_microsoft_presidio($content); default: return array('has_violation' => false, 'violations' => array()); } } catch (Exception $e) { error_log('合规性检查API错误:' . $e->getMessage()); return array( 'has_violation' => false, 'violations' => array(), 'error' => 'API检查失败:' . $e->getMessage() ); } } private function check_google_safe_browsing($content, $api_key) { $urls = $this->extract_urls($content); if (empty($urls)) { return array('has_violation' => false, 'violations' => array()); } $client_id = 'wordpress-compliance-checker'; $client_version = '1.0'; $threat_entries = array(); foreach ($urls as $url) { $threat_entries[] = array('url' => $url); }
发表评论详细指南:开发网站内嵌的在线预约课程与虚拟教室系统 摘要 随着在线教育的蓬勃发展,越来越多的教育机构和个人教师需要在自己的网站上集成在线预约课程与虚拟教室功能。本指南将详细介绍如何通过WordPress程序的代码二次开发,实现一个功能完整的在线教育系统。我们将从系统设计、功能规划、技术实现到测试部署,全面解析开发过程,帮助您打造一个集课程预约、支付管理、虚拟教室和学员管理于一体的专业在线教育平台。 一、项目概述与需求分析 1.1 在线教育市场趋势 近年来,全球在线教育市场呈现爆发式增长。根据市场研究数据显示,到2025年,全球在线教育市场规模预计将达到3500亿美元。这一趋势推动了教育机构和个人教师对在线教学平台的需求,特别是能够与现有网站无缝集成的解决方案。 1.2 系统核心需求 我们的在线预约课程与虚拟教室系统需要满足以下核心需求: 课程展示与管理:教师可以创建、编辑和发布课程信息 在线预约系统:学员可以浏览课程并预约上课时间 支付集成:支持多种支付方式完成课程费用支付 虚拟教室:集成视频会议功能,支持实时互动教学 学员管理:教师可以管理学员信息、跟踪学习进度 通知系统:自动发送预约确认、上课提醒等通知 评价与反馈:学员可以对课程和教师进行评价 1.3 WordPress作为开发平台的优势 WordPress作为全球最流行的内容管理系统,具有以下优势: 庞大的插件生态系统和主题资源 成熟的用户管理和权限控制系统 强大的扩展性和自定义能力 活跃的开发者社区和丰富的文档资源 良好的SEO基础和移动端适配 二、系统架构设计 2.1 整体架构设计 我们的系统将采用分层架构设计: 前端展示层 (WordPress主题) ↓ 业务逻辑层 (自定义插件) ↓ 数据访问层 (WordPress数据库) ↓ 第三方服务层 (支付、视频会议API) 2.2 数据库设计 我们需要扩展WordPress的数据库结构,添加以下自定义表: 课程表 (wp_courses):存储课程基本信息 课程安排表 (wp_course_schedules):存储课程的具体时间安排 预约记录表 (wp_bookings):存储学员的预约记录 支付记录表 (wp_payments):存储支付相关信息 虚拟教室表 (wp_virtual_classes):存储虚拟教室的会议信息 学员进度表 (wp_student_progress):跟踪学员学习进度 2.3 技术栈选择 后端:PHP 7.4+,WordPress 5.8+ 前端:HTML5,CSS3,JavaScript (ES6+),jQuery 数据库:MySQL 5.7+ 或 MariaDB 10.3+ 视频会议:集成Zoom API或Jitsi Meet 支付网关:Stripe,PayPal或国内支付平台API 实时通信:WebSocket或Socket.io for实时通知 三、开发环境搭建与准备工作 3.1 本地开发环境配置 安装本地服务器环境: 使用XAMPP、MAMP或Local by Flywheel 确保PHP版本≥7.4,MySQL≥5.7 安装WordPress: # 下载最新版WordPress wget https://wordpress.org/latest.zip unzip latest.zip # 配置数据库连接信息 配置开发工具: 代码编辑器:VS Code或PHPStorm 版本控制:Git 调试工具:Query Monitor、Debug Bar插件 3.2 创建自定义插件结构 在wp-content/plugins/目录下创建插件文件夹结构: online-education-system/ ├── online-education-system.php # 主插件文件 ├── includes/ # 核心功能文件 │ ├── class-database.php # 数据库操作类 │ ├── class-course-manager.php # 课程管理类 │ ├── class-booking-system.php # 预约系统类 │ ├── class-payment-handler.php # 支付处理类 │ ├── class-virtual-classroom.php # 虚拟教室类 │ └── class-notification.php # 通知系统类 ├── admin/ # 后台管理文件 │ ├── css/ │ ├── js/ │ └── partials/ ├── public/ # 前端文件 │ ├── css/ │ ├── js/ │ └── templates/ ├── assets/ # 静态资源 └── vendor/ # 第三方库 3.3 插件主文件配置 创建主插件文件online-education-system.php: <?php /** * Plugin Name: 在线教育系统 * Plugin URI: https://yourwebsite.com/ * Description: 在线预约课程与虚拟教室系统 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: online-education */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('OES_VERSION', '1.0.0'); define('OES_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('OES_PLUGIN_URL', plugin_dir_url(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { if (strpos($class_name, 'OES_') === 0) { $file = OES_PLUGIN_DIR . 'includes/class-' . strtolower(str_replace('_', '-', $class_name)) . '.php'; if (file_exists($file)) { require_once $file; } } }); // 初始化插件 function oes_init() { // 检查依赖 if (!function_exists('get_bloginfo')) { wp_die('此插件需要WordPress环境。'); } // 初始化核心类 if (class_exists('OES_Database')) { $database = new OES_Database(); $database->create_tables(); } // 加载文本域 load_plugin_textdomain('online-education', false, dirname(plugin_basename(__FILE__)) . '/languages'); } add_action('plugins_loaded', 'oes_init'); // 激活插件时执行的操作 register_activation_hook(__FILE__, function() { require_once OES_PLUGIN_DIR . 'includes/class-database.php'; $database = new OES_Database(); $database->create_tables(); // 创建必要的页面 oes_create_pages(); // 设置默认选项 update_option('oes_version', OES_VERSION); update_option('oes_currency', 'CNY'); update_option('oes_timezone', 'Asia/Shanghai'); }); // 停用插件时执行的操作 register_deactivation_hook(__FILE__, function() { // 清理临时数据 wp_clear_scheduled_hook('oes_daily_maintenance'); }); // 创建系统必要页面 function oes_create_pages() { $pages = [ 'course-list' => [ 'title' => '课程列表', 'content' => '[oes_course_list]' ], 'booking' => [ 'title' => '课程预约', 'content' => '[oes_booking_form]' ], 'student-dashboard' => [ 'title' => '学员中心', 'content' => '[oes_student_dashboard]' ], 'teacher-dashboard' => [ 'title' => '教师中心', 'content' => '[oes_teacher_dashboard]' ] ]; foreach ($pages as $slug => $page) { if (!get_page_by_path($slug)) { wp_insert_post([ 'post_title' => $page['title'], 'post_content' => $page['content'], 'post_status' => 'publish', 'post_type' => 'page', 'post_name' => $slug ]); } } } 四、核心功能模块开发 4.1 课程管理系统 4.1.1 数据库表结构 创建课程相关数据库表: // includes/class-database.php class OES_Database { public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 课程表 $courses_table = $wpdb->prefix . 'oes_courses'; $sql = "CREATE TABLE IF NOT EXISTS $courses_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL, description text, teacher_id bigint(20) NOT NULL, category varchar(100), price decimal(10,2) DEFAULT 0.00, duration int DEFAULT 60, max_students int DEFAULT 10, thumbnail_url varchar(500), 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 teacher_id (teacher_id), KEY status (status) ) $charset_collate;"; // 课程安排表 $schedules_table = $wpdb->prefix . 'oes_course_schedules'; $sql .= "CREATE TABLE IF NOT EXISTS $schedules_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, course_id mediumint(9) NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, recurring_type varchar(20) DEFAULT 'once', recurring_days varchar(100), max_capacity int DEFAULT 10, booked_count int DEFAULT 0, status varchar(20) DEFAULT 'available', meeting_id varchar(200), meeting_password varchar(100), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY course_id (course_id), KEY start_time (start_time), KEY status (status) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } } 4.1.2 课程管理后台界面 创建课程管理后台界面: // admin/partials/course-management.php class OES_Course_Manager { public function add_admin_menu() { add_menu_page( '课程管理', '在线教育', 'manage_options', 'oes-courses', [$this, 'render_course_list'], 'dashicons-welcome-learn-more', 30 ); add_submenu_page( 'oes-courses', '添加新课程', '添加课程', 'manage_options', 'oes-add-course', [$this, 'render_add_course'] ); add_submenu_page( 'oes-courses', '课程安排', '课程安排', 'manage_options', 'oes-schedules', [$this, 'render_schedule_management'] ); } public function render_course_list() { ?> <div class="wrap"> <h1 class="wp-heading-inline">课程管理</h1> <a href="<?php echo admin_url('admin.php?page=oes-add-course'); ?>" class="page-title-action">添加新课程</a> <div class="oes-course-list"> <?php global $wpdb; $courses_table = $wpdb->prefix . 'oes_courses'; $courses = $wpdb->get_results("SELECT * FROM $courses_table ORDER BY created_at DESC"); if ($courses) { echo '<table class="wp-list-table widefat fixed striped">'; echo '<thead><tr> <th>ID</th> <th>课程名称</th> <th>教师</th> <th>价格</th> <th>状态</th> <th>操作</th> </tr></thead>'; echo '<tbody>'; foreach ($courses as $course) { $teacher = get_userdata($course->teacher_id); echo '<tr> <td>' . $course->id . '</td> <td><strong>' . esc_html($course->title) . '</strong></td> <td>' . ($teacher ? $teacher->display_name : '未知') . '</td> <td>' . number_format($course->price, 2) . ' ' . get_option('oes_currency', 'CNY') . '</td> <td><span class="oes-status ' . $course->status . '">' . $this->get_status_text($course->status) . '</span></td> <td> <a href="' . admin_url('admin.php?page=oes-add-course&id=' . $course->id) . '">编辑</a> | <a href="#" class="oes-delete-course" data-id="' . $course->id . '">删除</a> </td> </tr>'; } echo '</tbody></table>'; } else { echo '<div class="notice notice-info"><p>暂无课程,请添加第一个课程。</p></div>'; } ?> </div> </div> <?php } public function render_add_course() { $course_id = isset($_GET['id']) ? intval($_GET['id']) : 0; $course = null; if ($course_id) { global $wpdb; $courses_table = $wpdb->prefix . 'oes_courses'; $course = $wpdb->get_row($wpdb->prepare("SELECT * FROM $courses_table WHERE id = %d", $course_id)); } ?> <div class="wrap"> <h1><?php echo $course_id ? '编辑课程' : '添加新课程'; ?></h1> <form method="post" action="" id="oes-course-form"> <?php wp_nonce_field('oes_save_course', 'oes_course_nonce'); ?> <input type="hidden" name="course_id" value="<?php echo $course_id; ?>"> <table class="form-table"> <tr> <th scope="row"><label for="course_title">课程标题</label></th> <td><input type="text" id="course_title" name="course_title" class="regular-text" value="<?php echo $course ? esc_attr($course->title) : ''; ?>" required></td> </tr> <tr> <th scope="row"><label for="course_description">课程描述</label></th> <td> <?php $description = $course ? $course->description : ''; wp_editor($description, 'course_description', [ 'textarea_name' => 'course_description', 'media_buttons' => true, 'textarea_rows' => 10 ]); ?> </td> </tr> <tr> <th scope="row"><label for="course_teacher">授课教师</label></th> <td> <select id="course_teacher" name="course_teacher" required> <option value="">选择教师</option> <?php $teachers = get_users(['role__in' => ['administrator', 'teacher']]); foreach ($teachers as $teacher) { $selected = $course && $course->teacher_id == $teacher->ID ? 'selected' : ''; echo '<option value="' . $teacher->ID . '" ' . $selected . '>' . $teacher->display_name . '</option>'; } ?> </select> </td> </tr> <tr> <th scope="row"><label for="course_price">课程价格</label></th> <td> <input type="number" id="course_price" name="course_price" step="0.01" min="0" value="<?php echo $course ? number_format($course->price, 2) : '0.00'; ?>" required> <span class="description"><?php echo get_option('oes_currency', 'CNY'); ?></span> </td> </tr> <tr> <th scope="row"><label for="course_duration">课程时长</label></th> <td> <input type="number" id="course_duration" name="course_duration" min="15" max="480" value="<?php echo $course ? $course->duration : 60; ?>" required> <span class="description">分钟</span> </td> </tr> <tr> <th scope="row"><label for="max_students">最大学员数</label></th> <td> <input type="number" id="max_students" name="max_students" min="1" max="100" value="<?php echo $course ? $course->max_students : 10; ?>" required> </td> </tr> <tr> <th scope="row"><label for="course_status">课程状态</label></th> <td> <select id="course_status" name="course_status"> <option value="draft" <?php echo $course && $course->status == 'draft' ? 'selected' : ''; ?>>草稿</option> <option value="published" <?php echo $course && $course->status == 'published' ? 'selected' : ''; ?>>已发布</option> <option value="archived" <?php echo $course && $course->status == 'archived' ? 'selected' : ''; ?>>已归档</option> </select> </td> </tr> </table> <p class="submit"> type="submit" name="submit_course" class="button button-primary" value="保存课程"> </p> </form> </div> <?php } private function get_status_text($status) { $statuses = [ 'draft' => '草稿', 'published' => '已发布', 'archived' => '已归档' ]; return isset($statuses[$status]) ? $statuses[$status] : $status; } } ### 4.2 在线预约系统 #### 4.2.1 预约数据库表设计 // 在class-database.php中继续添加class OES_Database { public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 预约记录表 $bookings_table = $wpdb->prefix . 'oes_bookings'; $sql = "CREATE TABLE IF NOT EXISTS $bookings_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, schedule_id mediumint(9) NOT NULL, student_id bigint(20) NOT NULL, booking_time datetime DEFAULT CURRENT_TIMESTAMP, status varchar(20) DEFAULT 'pending', payment_status varchar(20) DEFAULT 'unpaid', payment_id varchar(100), notes text, attended tinyint(1) DEFAULT 0, rating int DEFAULT 0, review text, PRIMARY KEY (id), KEY schedule_id (schedule_id), KEY student_id (student_id), KEY status (status), KEY payment_status (payment_status) ) $charset_collate;"; // 支付记录表 $payments_table = $wpdb->prefix . 'oes_payments'; $sql .= "CREATE TABLE IF NOT EXISTS $payments_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, booking_id mediumint(9) NOT NULL, amount decimal(10,2) NOT NULL, currency varchar(10) DEFAULT 'CNY', payment_method varchar(50), transaction_id varchar(100), status varchar(20) DEFAULT 'pending', payment_time datetime, refund_amount decimal(10,2) DEFAULT 0.00, refund_time datetime, PRIMARY KEY (id), KEY booking_id (booking_id), KEY transaction_id (transaction_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } } #### 4.2.2 前端预约界面 创建前端预约短代码和模板: // includes/class-booking-system.phpclass OES_Booking_System { public function register_shortcodes() { add_shortcode('oes_course_list', [$this, 'render_course_list']); add_shortcode('oes_booking_form', [$this, 'render_booking_form']); add_shortcode('oes_student_dashboard', [$this, 'render_student_dashboard']); } public function render_course_list($atts) { global $wpdb; $atts = shortcode_atts([ 'category' => '', 'teacher' => '', 'limit' => 10 ], $atts); $courses_table = $wpdb->prefix . 'oes_courses'; $where = "WHERE status = 'published'"; if (!empty($atts['category'])) { $where .= $wpdb->prepare(" AND category = %s", $atts['category']); } if (!empty($atts['teacher'])) { $where .= $wpdb->prepare(" AND teacher_id = %d", $atts['teacher']); } $courses = $wpdb->get_results("SELECT * FROM $courses_table $where ORDER BY created_at DESC LIMIT " . intval($atts['limit'])); ob_start(); ?> <div class="oes-course-list-container"> <div class="oes-course-filters"> <form method="get" class="oes-filter-form"> <input type="text" name="search" placeholder="搜索课程..." value="<?php echo isset($_GET['search']) ? esc_attr($_GET['search']) : ''; ?>"> <select name="category"> <option value="">所有分类</option> <?php $categories = $wpdb->get_col("SELECT DISTINCT category FROM $courses_table WHERE category IS NOT NULL AND category != ''"); foreach ($categories as $category) { $selected = isset($_GET['category']) && $_GET['category'] == $category ? 'selected' : ''; echo '<option value="' . esc_attr($category) . '" ' . $selected . '>' . esc_html($category) . '</option>'; } ?> </select> <button type="submit" class="oes-filter-button">筛选</button> </form> </div> <div class="oes-courses-grid"> <?php if ($courses): ?> <?php foreach ($courses as $course): ?> <?php $teacher = get_userdata($course->teacher_id); $schedules_table = $wpdb->prefix . 'oes_course_schedules'; $next_schedule = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $schedules_table WHERE course_id = %d AND start_time > NOW() AND status = 'available' ORDER BY start_time ASC LIMIT 1", $course->id )); ?> <div class="oes-course-card"> <div class="oes-course-thumbnail"> <?php if ($course->thumbnail_url): ?> <img src="<?php echo esc_url($course->thumbnail_url); ?>" alt="<?php echo esc_attr($course->title); ?>"> <?php else: ?> <div class="oes-course-thumbnail-placeholder"> <span class="dashicons dashicons-welcome-learn-more"></span> </div> <?php endif; ?> </div> <div class="oes-course-content"> <h3 class="oes-course-title"><?php echo esc_html($course->title); ?></h3> <div class="oes-course-meta"> <span class="oes-course-teacher"> <span class="dashicons dashicons-businessperson"></span> <?php echo $teacher ? esc_html($teacher->display_name) : '未知教师'; ?> </span> <span class="oes-course-duration"> <span class="dashicons dashicons-clock"></span> <?php echo intval($course->duration); ?>分钟 </span> <span class="oes-course-price"> <span class="dashicons dashicons-money-alt"></span> <?php echo number_format($course->price, 2); ?> <?php echo get_option('oes_currency', 'CNY'); ?> </span> </div> <div class="oes-course-excerpt"> <?php echo wp_trim_words(strip_tags($course->description), 30); ?> </div> <div class="oes-course-actions"> <?php if ($next_schedule): ?> <a href="<?php echo add_query_arg(['course_id' => $course->id], get_permalink(get_page_by_path('booking'))); ?>" class="oes-book-button button"> 立即预约 </a> <?php else: ?> <button class="oes-book-button button" disabled>暂无排课</button> <?php endif; ?> <a href="<?php echo add_query_arg(['course_id' => $course->id], get_permalink()); ?>" class="oes-details-button button"> 查看详情 </a> </div> </div> </div> <?php endforeach; ?> <?php else: ?> <div class="oes-no-courses"> <p>暂无可用课程。</p> </div> <?php endif; ?> </div> </div> <?php return ob_get_clean(); } public function render_booking_form() { if (!is_user_logged_in()) { return '<div class="oes-login-required"><p>请先<a href="' . wp_login_url(get_permalink()) . '">登录</a>以预约课程。</p></div>'; } $course_id = isset($_GET['course_id']) ? intval($_GET['course_id']) : 0; if (!$course_id) { return '<div class="oes-booking-error"><p>请选择要预约的课程。</p></div>'; } global $wpdb; $courses_table = $wpdb->prefix . 'oes_courses'; $course = $wpdb->get_row($wpdb->prepare("SELECT * FROM $courses_table WHERE id = %d AND status = 'published'", $course_id)); if (!$course) { return '<div class="oes-booking-error"><p>课程不存在或不可用。</p></div>'; } $schedules_table = $wpdb->prefix . 'oes_course_schedules'; $schedules = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $schedules_table WHERE course_id = %d AND start_time > NOW() AND status = 'available' ORDER BY start_time ASC", $course_id )); ob_start(); ?> <div class="oes-booking-container"> <div class="oes-booking-header"> <h2>预约课程: <?php echo esc_html($course->title); ?></h2> <div class="oes-course-info"> <p><strong>教师:</strong> <?php echo get_userdata($course->teacher_id)->display_name; ?></p> <p><strong>价格:</strong> <?php echo number_format($course->price, 2); ?> <?php echo get_option('oes_currency', 'CNY'); ?></p> <p><strong>时长:</strong> <?php echo intval($course->duration); ?> 分钟</p> </div> </div> <?php if ($schedules): ?> <form id="oes-booking-form" method="post" action="<?php echo admin_url('admin-ajax.php'); ?>"> <?php wp_nonce_field('oes_process_booking', 'oes_booking_nonce'); ?> <input type="hidden" name="action" value="oes_process_booking"> <input type="hidden" name="course_id" value="<?php echo $course_id; ?>"> <div class="oes-schedule-selection"> <h3>选择上课时间</h3> <div class="oes-schedules-list"> <?php foreach ($schedules as $schedule): ?> <?php $available_seats = $schedule->max_capacity - $schedule->booked_count; $start_time = strtotime($schedule->start_time); $end_time = strtotime($schedule->end_time); ?> <div class="oes-schedule-item"> <input type="radio" id="schedule_<?php echo $schedule->id; ?>" name="schedule_id" value="<?php echo $schedule->id; ?>" <?php echo $available_seats <= 0 ? 'disabled' : ''; ?> required> <label for="schedule_<?php echo $schedule->id; ?>"> <div class="oes-schedule-date"> <?php echo date('Y年m月d日', $start_time); ?> </div> <div class="oes-schedule-time"> <?php echo date('H:i', $start_time); ?> - <?php echo date('H:i', $end_time); ?> </div> <div class="oes-schedule-availability"> <?php if ($available_seats > 0): ?> <span class="oes-available">剩余 <?php echo $available_seats; ?> 个名额</span> <?php else: ?> <span class="oes-full">已满额</span> <?php endif; ?> </div> </label> </div> <?php endforeach; ?> </div> </div> <div class="oes-booking-notes"> <h3>备注信息 (可选)</h3> <textarea name="booking_notes" rows="3" placeholder="如有特殊要求,请在此说明..."></textarea> </div> <div class="oes-payment-method"> <h3>选择支付方式</h3> <div class="oes-payment-options"> <label class="oes-payment-option"> <input type="radio" name="payment_method" value="alipay" checked> <span class="oes-payment-icon">支付宝</span> </label> <label class="oes-payment-option"> <input type="radio" name="payment_method" value="wechat"> <span class="oes-payment-icon">微信支付</span> </label> <label class="oes-payment-option"> <input type="radio" name="payment_method" value="paypal"> <span class="oes-payment-icon">PayPal</span> </label> </div> </div> <div class="oes-booking-summary"> <h3>订单摘要</h3> <table class="oes-summary-table"> <tr> <td>课程费用:</td> <td class="oes-price"><?php echo number_format($course->price, 2); ?> <?php echo get_option('oes_currency', 'CNY'); ?></td> </tr> <tr> <td>服务费:</td> <td class="oes-price">0.00 <?php echo get_option('oes_currency', 'CNY'); ?></td> </tr> <tr class="oes-total"> <td>总计:</td> <td class="oes-price"><?php echo number_format($course->price, 2); ?> <?php echo get_option('oes_currency', 'CNY'); ?></td> </tr> </table> </div> <div class="oes-booking-submit"> <button type="submit" class="oes-submit-button button button-primary"> 确认预约并支付 </button> <p class="oes-terms-notice"> 点击确认即表示您同意我们的<a href="<?php echo get_permalink(get_page_by_path('terms')); ?>">服务条款</a> </p> </div> </form> <div id="oes-booking-result" style="display:none;"></div> <?php else: ?> <div class="oes-no-schedules"> <p>该课程暂无可用时间安排。</p> <a href="<?php echo get_permalink(get_page_by_path('course-list')); ?>" class="button"> 返回课程列表 </a> </div> <?php endif; ?> </div> <?php return ob_get_clean(); } } ### 4.3 虚拟教室集成 #### 4.3.1 Zoom API集成 // includes/class-virtual-classroom.phpclass OES_Virtual_Classroom { private $zoom_api_key; private $zoom_api_secret; private $zoom_account_id; public function __construct() { $this->zoom_api_key = get_option('oes_zoom_api_key', ''); $this->zoom_api_secret = get_option('oes_zoom_api_secret', ''); $this->zoom_account_id = get_option('oes_zoom_account_id', ''); add_action('wp_ajax_oes_create_meeting', [$this, 'create_meeting']); add_action('wp_ajax_oes_get_meeting_info', [$this, 'get_meeting_info']); } public function create_meeting_for_schedule($schedule_id) { global $wpdb; $schedules_table = $wpdb->prefix . 'oes_course_schedules'; $courses_table = $wpdb->prefix . 'oes_courses'; $schedule = $wpdb->get_row($wpdb->prepare( "SELECT s.*, c.title as course_title, c.teacher_id FROM $schedules_table s LEFT JOIN $courses_table c ON s.course_id = c.id WHERE s.id = %d", $schedule_id )); if (!$schedule) { return false; } $teacher = get_userdata($schedule->teacher_id); $start_time = new DateTime($schedule->start_time, new DateTimeZone('UTC')); $meeting_data = [ 'topic' => $schedule->course_title . ' - ' . $start_time->format('Y-m-d H:i'), 'type' => 2, // 预定会议 'start_time' => $start_time->format('Y-m-dTH:i:sZ'), 'duration' => intval($schedule->end_time) - intval($schedule->start_time), 'timezone' => get_option('timezone_string', 'Asia/Shanghai'), 'password' => wp_generate_password(6, false), 'settings' => [ 'host_video' => true, 'participant_video' => true, 'join_before_host' => false, 'mute_upon_entry' => true, 'waiting_room' => true, 'auto_recording' => 'cloud' ] ]; $response = $this->call_zoom_api('users/me/meetings', 'POST', $meeting_data); if ($response && isset($response['id'])) { $wpdb->update( $schedules_table, [ 'meeting_id' => $response['id'], 'meeting_password' => $response['password'] ], ['id' => $schedule_id] ); return [ 'meeting_id' => $response['id'], 'join_url' => $response['join_url'], 'password' => $response['password'] ]; } return false; } private function call_zoom_api
发表评论一步步实现:为WordPress打造内部工单系统与客户支持门户 引言:为什么选择WordPress作为工单系统平台? 在当今数字化商业环境中,高效的客户支持和内部协作系统是企业成功的关键因素。许多企业每年花费大量资金购买第三方客户支持软件,如Zendesk、Freshdesk等,但这些解决方案往往价格昂贵且缺乏定制灵活性。 WordPress作为全球最流行的内容管理系统,占据了互联网上超过40%的网站份额。其强大的插件生态和开放源代码特性,使其成为构建定制化业务工具的绝佳平台。通过WordPress二次开发,我们可以以较低成本打造一个完全符合企业特定需求的内部工单系统与客户支持门户。 本文将详细介绍如何通过WordPress代码二次开发,实现一个功能完整的工单系统,同时集成多种常用互联网小工具,提升客户支持效率和团队协作能力。 第一章:项目规划与需求分析 1.1 确定系统核心功能 在开始开发前,我们需要明确工单系统的基本功能需求: 用户端功能: 用户注册与登录 工单提交表单 工单状态跟踪 历史工单查看 文件上传功能 实时通知系统 管理端功能: 工单分配与转交 优先级管理 分类与标签系统 知识库集成 团队绩效统计 自动化规则设置 系统集成需求: 电子邮件通知 实时聊天小工具 常见问题(FAQ)模块 客户满意度调查 报表与分析工具 1.2 技术架构设计 基于WordPress的工单系统将采用以下技术架构: 前端界面:使用WordPress主题模板系统,结合Bootstrap框架确保响应式设计 数据存储:利用WordPress自定义文章类型(CPT)存储工单数据 用户管理:扩展WordPress原生用户系统,添加支持人员角色 通信机制:结合AJAX实现无刷新交互,使用WP Mail函数处理邮件通知 安全措施:实施WordPress非ce验证、数据消毒和权限检查 第二章:搭建基础开发环境 2.1 创建自定义插件 首先,我们需要创建一个独立的插件来容纳所有工单系统功能: <?php /** * Plugin Name: 企业工单与支持系统 * Plugin URI: https://yourcompany.com/ * Description: 基于WordPress的定制化工单与客户支持系统 * Version: 1.0.0 * Author: 您的公司 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('TICKET_SYSTEM_VERSION', '1.0.0'); define('TICKET_SYSTEM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('TICKET_SYSTEM_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once TICKET_SYSTEM_PLUGIN_DIR . 'includes/class-ticket-system.php'; function run_ticket_system() { $plugin = new Ticket_System(); $plugin->run(); } run_ticket_system(); 2.2 创建自定义文章类型存储工单 在插件中创建自定义文章类型来存储工单信息: class Ticket_System { public function __construct() { // 初始化钩子 add_action('init', array($this, 'register_ticket_post_type')); add_action('init', array($this, 'register_ticket_taxonomies')); } public function register_ticket_post_type() { $labels = array( 'name' => '工单', 'singular_name' => '工单', 'menu_name' => '工单系统', 'add_new' => '新建工单', 'add_new_item' => '添加新工单', 'edit_item' => '编辑工单', 'new_item' => '新工单', 'view_item' => '查看工单', 'search_items' => '搜索工单', 'not_found' => '未找到工单', 'not_found_in_trash' => '回收站中无工单' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'ticket'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-tickets-alt', 'supports' => array('title', 'editor', 'author', 'comments'), 'show_in_rest' => true, // 启用Gutenberg编辑器支持 ); register_post_type('ticket', $args); } public function register_ticket_taxonomies() { // 工单状态分类 register_taxonomy( 'ticket_status', 'ticket', array( 'labels' => array( 'name' => '工单状态', 'singular_name' => '状态', 'search_items' => '搜索状态', 'all_items' => '所有状态', 'edit_item' => '编辑状态', 'update_item' => '更新状态', 'add_new_item' => '添加新状态', 'new_item_name' => '新状态名称', 'menu_name' => '状态' ), 'hierarchical' => true, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => array('slug' => 'ticket-status'), 'default_terms' => array('open', 'in-progress', 'resolved', 'closed') ) ); // 工单优先级分类 register_taxonomy( 'ticket_priority', 'ticket', array( 'labels' => array( 'name' => '优先级', 'singular_name' => '优先级', 'search_items' => '搜索优先级', 'all_items' => '所有优先级', 'edit_item' => '编辑优先级', 'update_item' => '更新优先级', 'add_new_item' => '添加新优先级', 'new_item_name' => '新优先级名称', 'menu_name' => '优先级' ), 'hierarchical' => true, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => array('slug' => 'ticket-priority'), 'default_terms' => array('low', 'medium', 'high', 'urgent') ) ); } } 第三章:构建用户前端界面 3.1 创建工单提交表单 在前端创建用户提交工单的表单: class Ticket_Frontend { public function __construct() { add_shortcode('ticket_submission_form', array($this, 'render_ticket_form')); add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_scripts')); add_action('wp_ajax_submit_ticket', array($this, 'handle_ticket_submission')); add_action('wp_ajax_nopriv_submit_ticket', array($this, 'handle_ticket_submission')); } public function render_ticket_form() { // 检查用户是否登录 if (!is_user_logged_in()) { return '<div class="ticket-login-required">请先<a href="' . wp_login_url(get_permalink()) . '">登录</a>以提交工单</div>'; } ob_start(); ?> <div class="ticket-submission-container"> <h2>提交新工单</h2> <form id="ticket-submission-form" method="post" enctype="multipart/form-data"> <?php wp_nonce_field('submit_ticket_action', 'ticket_nonce'); ?> <div class="form-group"> <label for="ticket_title">问题标题 *</label> <input type="text" id="ticket_title" name="ticket_title" required class="form-control" placeholder="简要描述您的问题"> </div> <div class="form-group"> <label for="ticket_description">详细描述 *</label> <textarea id="ticket_description" name="ticket_description" rows="6" required class="form-control" placeholder="请详细描述您遇到的问题,包括步骤、期望结果和实际结果"></textarea> </div> <div class="form-row"> <div class="form-group col-md-6"> <label for="ticket_category">问题分类</label> <select id="ticket_category" name="ticket_category" class="form-control"> <option value="technical">技术问题</option> <option value="billing">账单问题</option> <option value="account">账户问题</option> <option value="feature">功能建议</option> <option value="other">其他</option> </select> </div> <div class="form-group col-md-6"> <label for="ticket_priority">优先级</label> <select id="ticket_priority" name="ticket_priority" class="form-control"> <option value="low">低 - 常规问题</option> <option value="medium" selected>中 - 需要尽快解决</option> <option value="high">高 - 严重影响使用</option> <option value="urgent">紧急 - 系统完全无法使用</option> </select> </div> </div> <div class="form-group"> <label for="ticket_attachments">附件 (可选)</label> <input type="file" id="ticket_attachments" name="ticket_attachments[]" multiple class="form-control-file"> <small class="form-text text-muted">支持图片、文档等格式,单个文件不超过5MB</small> </div> <div class="form-group"> <button type="submit" class="btn btn-primary btn-submit-ticket">提交工单</button> <div class="spinner-border text-primary d-none" role="status" id="ticket-submit-spinner"> <span class="sr-only">提交中...</span> </div> </div> <div id="ticket-submission-response" class="alert d-none"></div> </form> </div> <?php return ob_get_clean(); } public function handle_ticket_submission() { // 验证nonce if (!isset($_POST['ticket_nonce']) || !wp_verify_nonce($_POST['ticket_nonce'], 'submit_ticket_action')) { wp_die('安全验证失败'); } // 验证用户权限 if (!is_user_logged_in()) { wp_send_json_error(array('message' => '请先登录')); } $current_user = wp_get_current_user(); // 准备工单数据 $ticket_data = array( 'post_title' => sanitize_text_field($_POST['ticket_title']), 'post_content' => wp_kses_post($_POST['ticket_description']), 'post_status' => 'publish', 'post_type' => 'ticket', 'post_author' => $current_user->ID, 'meta_input' => array( '_ticket_category' => sanitize_text_field($_POST['ticket_category']), '_ticket_priority' => sanitize_text_field($_POST['ticket_priority']), '_ticket_status' => 'open' ) ); // 插入工单 $ticket_id = wp_insert_post($ticket_data); if (is_wp_error($ticket_id)) { wp_send_json_error(array('message' => '创建工单失败: ' . $ticket_id->get_error_message())); } // 处理附件上传 $this->handle_attachments($ticket_id); // 设置分类术语 wp_set_object_terms($ticket_id, sanitize_text_field($_POST['ticket_priority']), 'ticket_priority'); wp_set_object_terms($ticket_id, 'open', 'ticket_status'); // 发送通知邮件 $this->send_ticket_notification($ticket_id, $current_user); wp_send_json_success(array( 'message' => '工单提交成功!', 'ticket_id' => $ticket_id, 'redirect_url' => add_query_arg('ticket_id', $ticket_id, get_permalink(get_page_by_path('my-tickets'))) )); } } 3.2 创建用户工单管理面板 为用户创建查看和管理工单的面板: class Ticket_Dashboard { public function __construct() { add_shortcode('ticket_dashboard', array($this, 'render_ticket_dashboard')); } public function render_ticket_dashboard() { if (!is_user_logged_in()) { return '<p>请先登录查看您的工单。</p>'; } $current_user = wp_get_current_user(); $user_tickets = $this->get_user_tickets($current_user->ID); ob_start(); ?> <div class="ticket-dashboard-container"> <h2>我的工单</h2> <div class="dashboard-actions mb-4"> <a href="<?php echo get_permalink(get_page_by_path('submit-ticket')); ?>" class="btn btn-success"> <i class="fas fa-plus-circle"></i> 提交新工单 </a> <button class="btn btn-outline-secondary" id="refresh-tickets"> <i class="fas fa-sync-alt"></i> 刷新列表 </button> </div> <div class="ticket-filters mb-3"> <div class="btn-group" role="group"> <button type="button" class="btn btn-outline-primary active" data-filter="all">全部</button> <button type="button" class="btn btn-outline-warning" data-filter="open">进行中</button> <button type="button" class="btn btn-outline-success" data-filter="resolved">已解决</button> <button type="button" class="btn btn-outline-secondary" data-filter="closed">已关闭</button> </div> </div> <div class="table-responsive"> <table class="table table-hover ticket-table"> <thead> <tr> <th>工单ID</th> <th>标题</th> <th>状态</th> <th>优先级</th> <th>创建时间</th> <th>最后更新</th> <th>操作</th> </tr> </thead> <tbody> <?php if (empty($user_tickets)): ?> <tr> <td colspan="7" class="text-center">暂无工单记录</td> </tr> <?php else: ?> <?php foreach ($user_tickets as $ticket): ?> <?php $status = wp_get_post_terms($ticket->ID, 'ticket_status', array('fields' => 'names')); $priority = wp_get_post_terms($ticket->ID, 'ticket_priority', array('fields' => 'names')); $status_class = $this->get_status_class($status[0] ?? 'open'); $priority_class = $this->get_priority_class($priority[0] ?? 'medium'); ?> <tr class="ticket-row" data-status="<?php echo strtolower($status[0] ?? ''); ?>"> <td>#<?php echo $ticket->ID; ?></td> <td> <a href="<?php echo get_permalink($ticket->ID); ?>" class="ticket-title"> <?php echo esc_html($ticket->post_title); ?> </a> </td> <td> <span class="badge badge-<?php echo $status_class; ?>"> <?php echo $status[0] ?? 'Open'; ?> </span> </td> <td> <span class="badge badge-<?php echo $priority_class; ?>"> <?php echo $priority[0] ?? 'Medium'; ?> </span> </td> <td><?php echo get_the_date('Y-m-d H:i', $ticket->ID); ?></td> <td><?php echo get_the_modified_date('Y-m-d H:i', $ticket->ID); ?></td> <td> <a href="<?php echo get_permalink($ticket->ID); ?>" class="btn btn-sm btn-outline-primary"> <i class="fas fa-eye"></i> 查看 </a> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <div class="ticket-statistics mt-4 p-3 bg-light rounded"> <h5>工单统计</h5> <div class="row"> <div class="col-md-3"> <div class="stat-box text-center"> <div class="stat-number"><?php echo count($user_tickets); ?></div> <div class="stat-label">总工单数</div> </div> </div> <div class="col-md-3"> <div class="stat-box text-center"> <div class="stat-number"><?php echo $this->count_tickets_by_status($user_tickets, 'open'); ?></div> <div class="stat-label">进行中</div> </div> </div> <div class="col-md-3"> <div class="stat-box text-center"> <div class="stat-number"><?php echo $this->count_tickets_by_status($user_tickets, 'resolved'); ?></div> <div class="stat-label">已解决</div> </div> </div> <div class="col-md-3"> <div class="stat-box text-center"> <div class="stat-number"><?php echo $this->count_tickets_by_status($user_tickets, 'closed'); ?></div> <div class="stat-label">已关闭</div> </div> </div> </div> </div> </div> <script> jQuery(document).ready(function($) { // 工单筛选功能 $('.ticket-filters button').on('click', function() { var filter = $(this).data('filter'); // 更新按钮状态 $('.ticket-filters button').removeClass('active'); $(this).addClass('active'); // 筛选工单行 if (filter === 'all') { $('.ticket-row').show(); } else { $('.ticket-row').hide(); $('.ticket-row[data-status="' + filter + '"]').show(); } }); // 刷新工单列表 $('#refresh-tickets').on('click', function() { location.reload(); }); }); </script> <?php return ob_get_clean(); } private function get_user_tickets($user_id) { $args = array( 'post_type' => 'ticket', 'author' => $user_id, 'posts_per_page' => -1, 'orderby' => 'date', 'order' => 'DESC' ); return get_posts($args); } private function count_tickets_by_status($tickets, $status) { $count = 0; foreach ($tickets as $ticket) { $ticket_status = wp_get_post_terms($ticket->ID, 'ticket_status', array('fields' => 'names')); if (!empty($ticket_status) && strtolower($ticket_status[0]) === $status) { $count++; } } return $count; } private function get_status_class($status) { $status = strtolower($status); switch ($status) { case 'open': return 'warning'; case 'in-progress': return 'info'; case 'resolved': return 'success'; case 'closed': return 'secondary'; default: return 'light'; } } private function get_priority_class($priority) { $priority = strtolower($priority); switch ($priority) { case 'low': return 'info'; case 'medium': return 'primary'; case 'high': return 'warning'; case 'urgent': return 'danger'; default: return 'light'; } } } ## 第四章:构建管理后台功能 ### 4.1 创建工单管理界面 为支持团队创建专门的管理界面: class Ticket_Admin { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); add_filter('manage_ticket_posts_columns', array($this, 'add_ticket_columns')); add_action('manage_ticket_posts_custom_column', array($this, 'manage_ticket_columns'), 10, 2); add_action('add_meta_boxes', array($this, 'add_ticket_meta_boxes')); add_action('save_post_ticket', array($this, 'save_ticket_meta')); } public function add_admin_menu() { add_menu_page( '工单系统', '工单系统', 'manage_options', 'ticket-system', array($this, 'render_admin_dashboard'), 'dashicons-tickets-alt', 30 ); add_submenu_page( 'ticket-system', '工单统计', '统计报表', 'manage_options', 'ticket-statistics', array($this, 'render_statistics_page') ); add_submenu_page( 'ticket-system', '系统设置', '设置', 'manage_options', 'ticket-settings', array($this, 'render_settings_page') ); } public function render_admin_dashboard() { global $wpdb; // 获取统计数据 $total_tickets = wp_count_posts('ticket'); $open_tickets = $total_tickets->publish ?? 0; // 获取最近工单 $recent_tickets = get_posts(array( 'post_type' => 'ticket', 'posts_per_page' => 10, 'orderby' => 'date', 'order' => 'DESC' )); // 获取支持人员绩效 $support_performance = $this->get_support_performance(); ob_start(); ?> <div class="wrap ticket-admin-dashboard"> <h1 class="wp-heading-inline">工单系统仪表板</h1> <div class="dashboard-widgets"> <div class="row"> <div class="col-md-3"> <div class="card text-white bg-primary mb-3"> <div class="card-body"> <h5 class="card-title">总工单数</h5> <p class="card-text display-4"><?php echo array_sum((array)$total_tickets); ?></p> </div> </div> </div> <div class="col-md-3"> <div class="card text-white bg-warning mb-3"> <div class="card-body"> <h5 class="card-title">进行中</h5> <p class="card-text display-4"><?php echo $open_tickets; ?></p> </div> </div> </div> <div class="col-md-3"> <div class="card text-white bg-success mb-3"> <div class="card-body"> <h5 class="card-title">已解决</h5> <p class="card-text display-4"><?php echo $total_tickets->resolved ?? 0; ?></p> </div> </div> </div> <div class="col-md-3"> <div class="card text-white bg-info mb-3"> <div class="card-body"> <h5 class="card-title">平均响应时间</h5> <p class="card-text display-4"><?php echo $this->get_average_response_time(); ?>h</p> </div> </div> </div> </div> <div class="row"> <div class="col-md-8"> <div class="card mb-3"> <div class="card-header"> <h5>最近工单</h5> </div> <div class="card-body"> <table class="table table-hover"> <thead> <tr> <th>ID</th> <th>标题</th> <th>客户</th> <th>状态</th> <th>分配至</th> <th>创建时间</th> </tr> </thead> <tbody> <?php foreach ($recent_tickets as $ticket): ?> <?php $author = get_userdata($ticket->post_author); $assigned_to = get_post_meta($ticket->ID, '_ticket_assigned_to', true); $assigned_user = $assigned_to ? get_userdata($assigned_to) : null; $status = wp_get_post_terms($ticket->ID, 'ticket_status', array('fields' => 'names')); ?> <tr> <td>#<?php echo $ticket->ID; ?></td> <td> <a href="<?php echo get_edit_post_link($ticket->ID); ?>"> <?php echo esc_html($ticket->post_title); ?> </a> </td> <td><?php echo $author ? $author->display_name : '未知用户'; ?></td> <td> <span class="badge badge-<?php echo $this->get_status_class($status[0] ?? 'open'); ?>"> <?php echo $status[0] ?? 'Open'; ?> </span> </td> <td> <?php if ($assigned_user): ?> <?php echo $assigned_user->display_name; ?> <?php else: ?> <span class="text-muted">未分配</span> <?php endif; ?> </td> <td><?php echo get_the_date('m-d H:i', $ticket->ID); ?></td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> </div> <div class="col-md-4"> <div class="card mb-3"> <div class="card-header"> <h5>支持人员绩效</h5> </div> <div class="card-body"> <table class="table table-sm"> <thead> <tr> <th>姓名</th> <th>处理数</th> <th>解决率</th> </tr> </thead> <tbody> <?php foreach ($support_performance as $performance): ?> <tr> <td><?php echo $performance['name']; ?></td> <td><?php echo $performance['ticket_count']; ?></td> <td> <div class="progress"> <div class="progress-bar" role="progressbar" style="width: <?php echo $performance['resolution_rate']; ?>%"> <?php echo number_format($performance['resolution_rate'], 1); ?>% </div> </div> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> <div class="card"> <div class="card-header"> <h5>快速操作</h5> </div> <div class="card-body"> <a href="<?php echo admin_url('post-new.php?post_type=ticket'); ?>" class="btn btn-primary btn-block mb-2"> 创建新工单 </a> <a href="<?php echo admin_url('edit.php?post_type=ticket'); ?>" class="btn btn-secondary btn-block mb-2"> 查看所有工单 </a> <a href="<?php echo admin_url('admin.php?page=ticket-statistics'); ?>" class="btn btn-info btn-block"> 查看详细统计 </a> </div> </div> </div> </div> </div> </div> <?php echo ob_get_clean(); } private function get_support_performance() { // 获取所有支持人员 $support_users = get_users(array( 'role__in' => array('administrator', 'editor', 'support_agent'), 'fields' => array('ID', 'display_name') )); $performance_data = array(); foreach ($support_users as $user) { // 获取分配给该用户的工单 $assigned_tickets = get_posts(array( 'post_type' => 'ticket', 'meta_key' => '_ticket_assigned_to', 'meta_value' => $user->ID, 'posts_per_page' => -1 )); $resolved_count = 0; foreach ($assigned_tickets as $ticket) { $status = wp_get_post_terms($ticket->ID, 'ticket_status', array('fields' => 'names')); if (!empty($status) && in_array(strtolower($status[0]), array('resolved', 'closed'))) { $resolved_count++; } } $total_count = count($assigned_tickets); $resolution_rate = $total_count > 0 ? ($resolved_count / $total_count) * 100 : 0; $performance_data[] = array( 'name' => $user->display_name, 'ticket_count' => $total_count, 'resolved_count' => $resolved_count, 'resolution_rate' => $resolution_rate ); } return $performance_data; } } ### 4.2 添加工单分配与跟踪功能 class Ticket_Assignment { public function __construct() { add_action('add_meta_boxes_ticket', array($this, 'add_assignment_meta_box')); add_action('wp_ajax_assign_ticket', array($this, 'handle_ticket_assignment')); add_action('wp_ajax_get_available_agents', array($this, 'get_available_agents')); } public function add_assignment_meta_box() { add_meta_box( 'ticket_assignment', '工单分配', array($this, 'render_assignment_meta_box'), 'ticket', 'side', 'high' ); } public function render_assignment_meta_box($post) { $assigned_to = get_post_meta($post->ID, '_ticket_assigned_to', true); $current_assignee = $assigned_to ? get_userdata($assigned_to) : null; // 获取可分配的支持人员 $available_agents = get_users(array( 'role__in' => array('administrator', 'editor', 'support_agent'), 'fields' => array('ID', 'display_name', 'user_email') )); wp_nonce_field('assign_ticket_action', 'assignment_nonce'); ?> <div class="ticket-assignment-container"> <div class="current-assignee mb-3"> <strong>当前负责人:</strong> <?php if ($current_assignee): ?> <div class="assignee-info mt-1 p-2 bg-light rounded"> <div class="d-flex align-items-center"> <div class="assignee-avatar mr-2"> <?php echo get_avatar($current_assignee->ID, 32); ?> </div> <div> <div class="assignee-name"><?php echo $current_assignee->display_name; ?></div> <div class="assignee-email text-muted small"><?php echo $current_assignee->user_email; ?></div> </div> </div> </div> <?php else: ?> <div class="text-muted mt-1">未分配</div> <?php endif; ?> </div> <div class="assignment-form"> <label for="assign_to"><strong>分配给:</strong></label> <select id="assign_to" name="assign_to" class="form-control"> <option value="">选择支持人员...</option> <?php foreach ($available_agents as $agent): ?> <option value="<?php echo $agent->ID; ?>" <?php selected($assigned_to, $agent->ID); ?>> <?php echo $agent->display_name; ?> (<?php echo $agent->user_email; ?>) </option> <?php endforeach; ?> </select> <div class="assignment-actions mt-3"> <button type="button" class="button button-primary" id="assign-ticket-btn"> 分配工单 </button> <?php if ($assigned_to): ?> <button type="button" class="button button-link" id="unassign-ticket-btn"> 取消分配 </button> <?php endif; ?> </div> <div class="assignment-history mt-3"> <strong>分配历史:</strong> <?php $this->display_assignment_history($post->ID); ?> </div> </div> <div id="assignment-response" class="mt-2"></div> </div> <script> jQuery(document).ready(function($) { $('#assign-ticket-btn').on('click', function() { var ticketId = <?php echo $post->ID; ?>; var assignTo = $('#assign_to').val(); var nonce = $('#assignment_nonce').val(); if (!assignTo) { alert('请选择要分配的支持人员'); return; } $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'assign_ticket', ticket_id: ticketId, assign_to: assignTo, nonce: nonce }, beforeSend: function() { $('#assign-ticket-btn').prop('disabled', true).text('分配中...'); }, success: function(response) { if (response.success) { $('#assignment-response').html('<div class="notice notice-success"><p>' + response.data.message + '</p></div>'); location.reload(); } else { $('#assignment-response').html('<div class="notice notice-error"><p>' + response.data.message + '</p></div>'); } }, complete: function() { $('#assign-ticket-btn').prop('disabled', false).text('分配工单'); } }); }); }); </script> <?php } public function handle_ticket_assignment() { // 验证nonce和权限 if (!check_ajax_referer('assign_ticket_action', 'nonce', false)) { wp_send_json_error(array('
发表评论