跳至内容

分类: 网站建设

WordPress插件开发教程,实现网站内容自动生成语音并转换为播客

WordPress插件开发教程:实现网站内容自动生成语音并转换为播客 引言:当文字遇见声音 在信息爆炸的数字时代,内容呈现方式的多样性变得至关重要。据统计,全球播客听众数量已从2019年的3.32亿增长到2023年的4.64亿,预计到2024年将超过5亿。与此同时,语音技术的发展使得文字转语音的质量达到了前所未有的自然度。对于WordPress网站运营者而言,将文字内容自动转换为语音并生成播客,不仅能提升用户体验,还能扩大内容传播渠道,吸引更多受众。 本教程将深入讲解如何通过WordPress插件开发,实现网站内容自动生成语音并转换为播客的功能。我们将从基础概念讲起,逐步深入到代码实现,最终打造一个功能完整的插件。无论您是WordPress开发者还是有一定技术基础的内容创作者,都能通过本教程掌握这一实用技能。 第一部分:准备工作与环境搭建 1.1 理解WordPress插件架构 WordPress插件系统基于PHP构建,采用事件驱动的钩子(Hooks)机制。插件通过动作钩子(Actions)和过滤器钩子(Filters)与WordPress核心交互。理解这一机制是开发任何WordPress插件的基础。 一个标准的WordPress插件通常包含以下结构: 主插件文件(plugin-name.php):包含插件元信息 功能类文件:实现核心功能 资源文件:CSS、JavaScript、图像等 语言文件:用于国际化 1.2 开发环境配置 在开始开发前,需要搭建合适的开发环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel 代码编辑器:VS Code、PHPStorm或Sublime Text WordPress安装:最新版本的WordPress 调试工具:安装Query Monitor、Debug Bar等调试插件 版本控制:使用Git进行代码管理 1.3 创建插件基础结构 首先,在WordPress的wp-content/plugins/目录下创建新文件夹auto-podcast-generator,然后创建主插件文件: <?php /** * Plugin Name: Auto Podcast Generator * Plugin URI: https://yourwebsite.com/auto-podcast-generator * Description: 自动将WordPress文章转换为语音播客 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: auto-podcast-generator */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('APG_VERSION', '1.0.0'); define('APG_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('APG_PLUGIN_URL', plugin_dir_url(__FILE__)); define('APG_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 初始化插件 require_once APG_PLUGIN_DIR . 'includes/class-auto-podcast-generator.php'; function run_auto_podcast_generator() { $plugin = new Auto_Podcast_Generator(); $plugin->run(); } run_auto_podcast_generator(); 第二部分:核心功能设计与实现 2.1 语音生成引擎选择与集成 目前市场上有多种文字转语音(TTS)服务可供选择: Google Cloud Text-to-Speech:质量高,支持多种语言和声音 Amazon Polly:提供逼真的语音合成 Microsoft Azure Cognitive Services:语音自然度高 IBM Watson Text to Speech:企业级解决方案 开源方案:如eSpeak、Festival(质量较低) 本教程将使用Google Cloud Text-to-Speech API作为示例,因为它提供了高质量的语音合成和相对友好的免费额度。 2.1.1 配置Google Cloud TTS API 首先,需要在Google Cloud Console中创建项目并启用Text-to-Speech API,然后创建服务账号密钥。 在插件中创建API配置类: <?php // includes/class-tts-engine.php class APG_TTS_Engine { private $api_key; private $language_code; private $voice_name; public function __construct() { $options = get_option('apg_settings'); $this->api_key = isset($options['google_tts_api_key']) ? $options['google_tts_api_key'] : ''; $this->language_code = isset($options['tts_language']) ? $options['tts_language'] : 'en-US'; $this->voice_name = isset($options['tts_voice']) ? $options['tts_voice'] : 'en-US-Wavenet-D'; } /** * 将文本转换为语音 * * @param string $text 要转换的文本 * @param int $post_id 文章ID * @return string|bool 返回音频文件路径或false */ public function text_to_speech($text, $post_id) { if (empty($this->api_key)) { error_log('Auto Podcast Generator: Google TTS API key not configured'); return false; } // 清理文本,移除HTML标签 $clean_text = wp_strip_all_tags($text); // 限制文本长度(Google TTS API限制为5000字符) if (strlen($clean_text) > 5000) { $clean_text = $this->truncate_text($clean_text, 5000); } // 准备API请求 $url = 'https://texttospeech.googleapis.com/v1/text:synthesize?key=' . $this->api_key; $data = array( 'input' => array( 'text' => $clean_text ), 'voice' => array( 'languageCode' => $this->language_code, 'name' => $this->voice_name ), 'audioConfig' => array( 'audioEncoding' => 'MP3', 'speakingRate' => 1.0, 'pitch' => 0, 'volumeGainDb' => 0 ) ); $args = array( 'body' => json_encode($data), 'headers' => array( 'Content-Type' => 'application/json' ), 'timeout' => 30 ); // 发送请求 $response = wp_remote_post($url, $args); if (is_wp_error($response)) { error_log('Auto Podcast Generator: TTS API request failed - ' . $response->get_error_message()); return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['error'])) { error_log('Auto Podcast Generator: TTS API error - ' . $data['error']['message']); return false; } if (!isset($data['audioContent'])) { error_log('Auto Podcast Generator: No audio content in response'); return false; } // 解码base64音频数据 $audio_data = base64_decode($data['audioContent']); // 保存音频文件 $upload_dir = wp_upload_dir(); $podcast_dir = $upload_dir['basedir'] . '/apg-podcasts/'; if (!file_exists($podcast_dir)) { wp_mkdir_p($podcast_dir); } $filename = 'podcast-' . $post_id . '-' . time() . '.mp3'; $filepath = $podcast_dir . $filename; if (file_put_contents($filepath, $audio_data)) { // 保存文件信息到文章元数据 $file_url = $upload_dir['baseurl'] . '/apg-podcasts/' . $filename; update_post_meta($post_id, '_apg_audio_file', $filepath); update_post_meta($post_id, '_apg_audio_url', $file_url); update_post_meta($post_id, '_apg_audio_generated', current_time('mysql')); return $filepath; } return false; } /** * 截断文本,尽量在句子结束处截断 */ private function truncate_text($text, $max_length) { if (strlen($text) <= $max_length) { return $text; } $truncated = substr($text, 0, $max_length); $last_period = strrpos($truncated, '.'); $last_question = strrpos($truncated, '?'); $last_exclamation = strrpos($truncated, '!'); $last_sentence_end = max($last_period, $last_question, $last_exclamation); if ($last_sentence_end > 0) { return substr($text, 0, $last_sentence_end + 1); } return $truncated . '...'; } /** * 获取可用的语音列表 */ public function get_available_voices() { // 这里可以缓存语音列表以提高性能 $voices = get_transient('apg_google_voices'); if (false === $voices) { $url = 'https://texttospeech.googleapis.com/v1/voices?key=' . $this->api_key; $response = wp_remote_get($url); if (!is_wp_error($response)) { $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['voices'])) { $voices = $data['voices']; set_transient('apg_google_voices', $voices, WEEK_IN_SECONDS); } } } return $voices; } } 2.2 文章内容处理与优化 直接使用文章原始内容生成语音可能不是最佳体验,我们需要对内容进行处理: <?php // includes/class-content-processor.php class APG_Content_Processor { /** * 处理文章内容,准备用于语音合成 */ public function prepare_content_for_tts($post_id) { $post = get_post($post_id); if (!$post || $post->post_status !== 'publish') { return false; } $content = $post->post_content; // 应用the_content过滤器,处理短代码等 $content = apply_filters('the_content', $content); // 移除不需要的元素 $content = $this->clean_content($content); // 添加文章标题作为介绍 $title = get_the_title($post_id); $final_content = "文章标题: " . $title . ". " . $content; // 添加结尾语 $final_content .= " 本文由自动播客生成器为您朗读。访问我们的网站获取更多内容。"; return $final_content; } /** * 清理HTML内容,提取纯文本 */ private function clean_content($content) { // 移除脚本和样式标签 $content = preg_replace('/<scriptb[^>]*>(.*?)</script>/is', '', $content); $content = preg_replace('/<styleb[^>]*>(.*?)</style>/is', '', $content); // 移除注释 $content = preg_replace('/<!--(.*?)-->/', '', $content); // 替换HTML标签为适当的停顿 $content = preg_replace('/</h[1-6]>/', '. ', $content); $content = preg_replace('/</p>/', '. ', $content); $content = preg_replace('/</div>/', '. ', $content); $content = preg_replace('/<brs*/?>/', '. ', $content); // 移除所有剩余HTML标签 $content = wp_strip_all_tags($content); // 规范化空格和标点 $content = preg_replace('/s+/', ' ', $content); $content = preg_replace('/s*.s*/', '. ', $content); $content = preg_replace('/s*,s*/', ', ', $content); $content = preg_replace('/s*!s*/', '! ', $content); $content = preg_replace('/s*?s*/', '? ', $content); // 解码HTML实体 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); return trim($content); } /** * 估算朗读时间 */ public function estimate_reading_time($content) { // 平均阅读速度:每分钟150-200字 $word_count = str_word_count(strip_tags($content)); $minutes = ceil($word_count / 180); // 使用180字/分钟作为平均值 return $minutes; } } 2.3 播客RSS Feed生成 播客的核心是RSS Feed,它使音频内容能够被播客客户端订阅: <?php // includes/class-podcast-feed.php class APG_Podcast_Feed { private $feed_slug = 'podcast-feed'; public function __construct() { add_action('init', array($this, 'add_podcast_feed')); add_action('do_feed_' . $this->feed_slug, array($this, 'generate_podcast_feed'), 10, 1); } /** * 注册自定义RSS Feed */ public function add_podcast_feed() { add_feed($this->feed_slug, array($this, 'generate_podcast_feed')); } /** * 生成播客RSS Feed */ public function generate_podcast_feed() { // 设置内容类型为XML header('Content-Type: ' . feed_content_type('rss2') . '; charset=' . get_option('blog_charset'), true); // 获取播客设置 $options = get_option('apg_settings'); // 查询有音频的文章 $args = array( 'post_type' => 'post', 'post_status' => 'publish', 'meta_key' => '_apg_audio_url', 'meta_compare' => 'EXISTS', 'posts_per_page' => 20, 'orderby' => 'date', 'order' => 'DESC' ); $podcast_posts = new WP_Query($args); echo '<?xml version="1.0" encoding="' . get_option('blog_charset') . '"?>'; ?> <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"> <channel> <title><?php echo esc_xml(get_bloginfo('name') . ' Podcast'); ?></title> <atom:link href="<?php self_link(); ?>" rel="self" type="application/rss+xml" /> <link><?php echo esc_url(home_url('/')); ?></link> <description><?php echo esc_xml(get_bloginfo('description')); ?></description> <lastBuildDate><?php echo mysql2date('D, d M Y H:i:s +0000', get_lastpostmodified('GMT'), false); ?></lastBuildDate> <language><?php echo get_option('rss_language'); ?></language> <sy:updatePeriod><?php echo apply_filters('rss_update_period', 'hourly'); ?></sy:updatePeriod> <sy:updateFrequency><?php echo apply_filters('rss_update_frequency', '1'); ?></sy:updateFrequency> <generator>Auto Podcast Generator for WordPress</generator> <!-- iTunes播客特定标签 --> <itunes:subtitle><?php echo esc_xml($options['podcast_subtitle'] ?? get_bloginfo('description')); ?></itunes:subtitle> <itunes:summary><?php echo esc_xml($options['podcast_summary'] ?? get_bloginfo('description')); ?></itunes:summary> <itunes:author><?php echo esc_xml($options['podcast_author'] ?? get_bloginfo('name')); ?></itunes:author> <?php if (!empty($options['podcast_image'])): ?> <itunes:image href="<?php echo esc_url($options['podcast_image']); ?>" /> <image> <url><?php echo esc_url($options['podcast_image']); ?></url> <title><?php echo esc_xml(get_bloginfo('name') . ' Podcast'); ?></title> <link><?php echo esc_url(home_url('/')); ?></link> </image> <?php endif; ?> <itunes:explicit><?php echo (!empty($options['podcast_explicit']) && $options['podcast_explicit'] === 'yes') ? 'yes' : 'no'; ?></itunes:explicit> <itunes:category text="<?php echo esc_attr($options['podcast_category'] ?? 'Technology'); ?>"> <?php if (!empty($options['podcast_subcategory'])): ?> <itunes:category text="<?php echo esc_attr($options['podcast_subcategory']); ?>" /> <?php endif; ?> </itunes:category> ): ?> <?php while ($podcast_posts->have_posts()): $podcast_posts->the_post(); ?> <?php $audio_url = get_post_meta(get_the_ID(), '_apg_audio_url', true); $audio_file = get_post_meta(get_the_ID(), '_apg_audio_file', true); $audio_duration = get_post_meta(get_the_ID(), '_apg_audio_duration', true); if (empty($audio_url)) { continue; } // 获取音频文件大小 $file_size = 0; if (file_exists($audio_file)) { $file_size = filesize($audio_file); } // 获取文章特色图像 $thumbnail_url = get_the_post_thumbnail_url(get_the_ID(), 'full'); ?> <item> <title><?php echo esc_xml(get_the_title()); ?></title> <link><?php echo esc_url(get_permalink()); ?></link> <pubDate><?php echo mysql2date('D, d M Y H:i:s +0000', get_post_time('Y-m-d H:i:s', true), false); ?></pubDate> <dc:creator><![CDATA[<?php the_author(); ?>]]></dc:creator> <guid isPermaLink="false"><?php the_guid(); ?></guid> <description><![CDATA[<?php echo esc_xml(get_the_excerpt()); ?>]]></description> <content:encoded><![CDATA[<?php echo esc_xml(get_the_content()); ?>]]></content:encoded> <!-- 播客特定元素 --> <enclosure url="<?php echo esc_url($audio_url); ?>" length="<?php echo esc_attr($file_size); ?>" type="audio/mpeg" /> <itunes:author><?php the_author(); ?></itunes:author> <itunes:subtitle><?php echo esc_xml(wp_trim_words(get_the_excerpt(), 20)); ?></itunes:subtitle> <itunes:summary><![CDATA[<?php echo esc_xml(get_the_excerpt()); ?>]]></itunes:summary> <?php if (!empty($thumbnail_url)): ?> <itunes:image href="<?php echo esc_url($thumbnail_url); ?>" /> <?php endif; ?> <?php if (!empty($audio_duration)): ?> <itunes:duration><?php echo esc_attr($audio_duration); ?></itunes:duration> <?php endif; ?> <itunes:explicit><?php echo (!empty($options['podcast_explicit']) && $options['podcast_explicit'] === 'yes') ? 'yes' : 'no'; ?></itunes:explicit> </item> <?php endwhile; ?> <?php wp_reset_postdata(); ?> <?php endif; ?> </channel> </rss> <?php } /** * 获取播客Feed URL */ public function get_feed_url() { return home_url('/feed/' . $this->feed_slug); } } ## 第三部分:插件核心类与集成 ### 3.1 主插件类实现 <?php// includes/class-auto-podcast-generator.php class Auto_Podcast_Generator { private $loader; private $tts_engine; private $content_processor; private $podcast_feed; private $admin; public function __construct() { $this->load_dependencies(); $this->define_admin_hooks(); $this->define_public_hooks(); } private function load_dependencies() { require_once APG_PLUGIN_DIR . 'includes/class-tts-engine.php'; require_once APG_PLUGIN_DIR . 'includes/class-content-processor.php'; require_once APG_PLUGIN_DIR . 'includes/class-podcast-feed.php'; require_once APG_PLUGIN_DIR . 'admin/class-admin.php'; $this->tts_engine = new APG_TTS_Engine(); $this->content_processor = new APG_Content_Processor(); $this->podcast_feed = new APG_Podcast_Feed(); $this->admin = new APG_Admin(); } private function define_admin_hooks() { add_action('admin_menu', array($this->admin, 'add_admin_menu')); add_action('admin_init', array($this->admin, 'register_settings')); add_action('add_meta_boxes', array($this, 'add_podcast_meta_box')); add_action('save_post', array($this, 'save_post_handler'), 10, 2); add_action('admin_enqueue_scripts', array($this->admin, 'enqueue_admin_scripts')); } private function define_public_hooks() { // 在文章内容后添加音频播放器 add_filter('the_content', array($this, 'add_audio_player_to_content')); // 添加播客订阅链接到页面 add_action('wp_footer', array($this, 'add_podcast_subscription_links')); // 短代码支持 add_shortcode('apg_podcast_player', array($this, 'podcast_player_shortcode')); } /** * 添加播客元数据框 */ public function add_podcast_meta_box() { $post_types = apply_filters('apg_supported_post_types', array('post')); foreach ($post_types as $post_type) { add_meta_box( 'apg_podcast_meta', __('播客设置', 'auto-podcast-generator'), array($this, 'render_podcast_meta_box'), $post_type, 'side', 'default' ); } } /** * 渲染播客元数据框 */ public function render_podcast_meta_box($post) { wp_nonce_field('apg_podcast_meta', 'apg_podcast_meta_nonce'); $audio_url = get_post_meta($post->ID, '_apg_audio_url', true); $audio_generated = get_post_meta($post->ID, '_apg_audio_generated', true); $generate_audio = get_post_meta($post->ID, '_apg_generate_audio', true); if (empty($generate_audio)) { $generate_audio = 'auto'; } ?> <div class="apg-meta-box"> <p> <label for="apg_generate_audio"> <strong><?php _e('生成音频', 'auto-podcast-generator'); ?></strong> </label> </p> <p> <select name="apg_generate_audio" id="apg_generate_audio" style="width:100%;"> <option value="auto" <?php selected($generate_audio, 'auto'); ?>> <?php _e('自动(发布时生成)', 'auto-podcast-generator'); ?> </option> <option value="manual" <?php selected($generate_audio, 'manual'); ?>> <?php _e('手动生成', 'auto-podcast-generator'); ?> </option> <option value="disabled" <?php selected($generate_audio, 'disabled'); ?>> <?php _e('不生成', 'auto-podcast-generator'); ?> </option> </select> </p> <?php if (!empty($audio_url)): ?> <p> <strong><?php _e('音频状态:', 'auto-podcast-generator'); ?></strong><br> <?php _e('已生成', 'auto-podcast-generator'); ?> <?php if ($audio_generated): ?> <br><small><?php echo date_i18n(get_option('date_format') . ' ' . get_option('time_format'), strtotime($audio_generated)); ?></small> <?php endif; ?> </p> <p> <audio controls style="width:100%;"> <source src="<?php echo esc_url($audio_url); ?>" type="audio/mpeg"> <?php _e('您的浏览器不支持音频播放。', 'auto-podcast-generator'); ?> </audio> </p> <p> <a href="<?php echo esc_url($audio_url); ?>" target="_blank" class="button button-small"> <?php _e('下载音频', 'auto-podcast-generator'); ?> </a> <button type="button" class="button button-small button-secondary" id="apg_regenerate_audio"> <?php _e('重新生成', 'auto-podcast-generator'); ?> </button> </p> <?php else: ?> <p> <strong><?php _e('音频状态:', 'auto-podcast-generator'); ?></strong><br> <?php _e('未生成', 'auto-podcast-generator'); ?> </p> <?php endif; ?> <div id="apg_audio_generation_message" style="display:none; margin-top:10px; padding:5px; background:#f5f5f5; border-left:4px solid #46b450;"></div> </div> <script> jQuery(document).ready(function($) { $('#apg_regenerate_audio').on('click', function(e) { e.preventDefault(); var button = $(this); var messageDiv = $('#apg_audio_generation_message'); button.prop('disabled', true).text('<?php _e("生成中...", "auto-podcast-generator"); ?>'); messageDiv.hide().removeClass('notice-success notice-error'); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'apg_generate_audio', post_id: <?php echo $post->ID; ?>, nonce: '<?php echo wp_create_nonce("apg_generate_audio_" . $post->ID); ?>' }, success: function(response) { if (response.success) { messageDiv.addClass('notice-success').html('<p><?php _e("音频生成成功!页面将重新加载...", "auto-podcast-generator"); ?></p>').show(); setTimeout(function() { location.reload(); }, 2000); } else { messageDiv.addClass('notice-error').html('<p>' + response.data + '</p>').show(); button.prop('disabled', false).text('<?php _e("重新生成", "auto-podcast-generator"); ?>'); } }, error: function() { messageDiv.addClass('notice-error').html('<p><?php _e("生成失败,请重试。", "auto-podcast-generator"); ?></p>').show(); button.prop('disabled', false).text('<?php _e("重新生成", "auto-podcast-generator"); ?>'); } }); }); }); </script> <?php } /** * 保存文章时的处理 */ public function save_post_handler($post_id, $post) { // 检查权限 if (!current_user_can('edit_post', $post_id)) { return; } // 验证nonce if (!isset($_POST['apg_podcast_meta_nonce']) || !wp_verify_nonce($_POST['apg_podcast_meta_nonce'], 'apg_podcast_meta')) { return; } // 防止自动保存时处理 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } // 保存生成选项 if (isset($_POST['apg_generate_audio'])) { update_post_meta($post_id, '_apg_generate_audio', sanitize_text_field($_POST['apg_generate_audio'])); } // 如果是发布状态且设置为自动生成,则生成音频 if ($post->post_status === 'publish' && isset($_POST['apg_generate_audio']) && $_POST['apg_generate_audio'] === 'auto') { $this->generate_audio_for_post($post_id); } } /** * 为文章生成音频 */ public function generate_audio_for_post($post_id) { // 检查是否已生成 $existing_audio = get_post_meta($post_id, '_apg_audio_url', true); if (!empty($existing_audio)) { return true; } // 准备内容 $content = $this->content_processor->prepare_content_for_tts($post_id); if (empty($content)) { return false; } // 生成音频 $audio_file = $this->tts_engine->text_to_speech($content, $post_id); if ($audio_file) { // 计算音频时长 $this->calculate_audio_duration($post_id, $audio_file); // 触发动作,允许其他插件响应 do_action('apg_audio_generated', $post_id, $audio_file); return true; } return false; } /** * 计算音频时长 */ private function calculate_audio_duration($post_id, $audio_file) { if (!file_exists($audio_file)) { return; } // 使用getID3库获取音频信息 if (!class_exists('getID3')) { require_once(ABSPATH . 'wp-admin/includes/media.php'); } try { $id3 = new getID3(); $file_info = $id3->analyze($audio_file); if (isset($file_info['playtime_seconds'])) { $duration_seconds = floor($file_info['playtime_seconds']); $duration_formatted = sprintf('%02d:%02d:%02d', floor($duration_seconds / 3600), floor(($duration_seconds % 3600) / 60), $duration_seconds % 60 ); update_post_meta($post_id, '_apg_audio_duration', $duration_formatted); update_post_meta($post_id, '_apg_audio_duration_seconds', $duration_seconds); } } catch (Exception $e) { error_log('Auto Podcast Generator: Could not calculate audio duration - ' . $e->getMessage()); } } /** * 在文章内容后添加音频播放器 */ public function add_audio_player_to_content($content) { if (!is_single() || !in_the_loop() || !is_main_query()) { return $content; } $post_id = get_the_ID(); $audio_url = get_post_meta($post_id, '_apg_audio_url', true); if (empty($audio_url)) { return $content; } $options = get_option('apg_settings'); $show_player = isset($options['show_player_in_content']) ? $options['show_player_in_content'] : 'yes'; if ($show_player !== 'yes') { return $content; } $player_position = isset($options['player_position']) ? $options['player_position'] : 'after'; $player_html = $this->get_audio_player_html($post_id); if ($player_position === 'before') { return $player_html . $content; } else { return $content . $player_html; } } /** * 获取音频播放器HTML */ private function get_audio_player_html($post_id) { $audio_url = get_post_meta($post_id, '_apg_audio_url', true); $audio_duration = get_post_meta($post_id, '_apg_audio_duration', true); ob_start(); ?> <div class="apg-audio-player" style="margin: 30px 0; padding: 20px; background: #f9f9f9; border-radius: 8px; border-left: 4px solid #0073aa;"> <h3 style="margin-top: 0; color: #333;"> <?php _e('收听本文音频版', 'auto-podcast-generator'); ?> <?php if ($audio_duration): ?> <small style="font-size: 14px; color: #666; font-weight: normal;"> (<?php echo esc_html($audio_duration); ?>) </small> <?php endif; ?> </h3> <audio controls style="width:100%;"> <source src="<?php echo esc_url($audio_url); ?>" type="audio/mpeg"> <?php _e('您的浏览器不支持音频播放。', 'auto-podcast-generator'); ?> </audio> <div style="margin-top: 15px; font-size: 14px; color: #666;"> <p style="margin: 5px 0;"> <?php _e('您也可以:', 'auto-podcast-generator'); ?> <a href="<?php echo esc_url($audio_url); ?>" download style="margin-left: 10px;"> <?php _e('下载音频', 'auto-podcast-generator'); ?> </a> <a href="<?php echo esc_url($this->podcast_feed->get_feed_url()); ?>" style="margin-left: 10px;" target="_blank"> <?php _e('订阅播客', 'auto-podcast-generator'); ?> </a> </p> </div> </div> <?php return ob_get_clean(); } /** * 添加播客订阅链接 */ public function add_podcast_subscription_links() { $options = get_option('apg_settings'); $show_subscription_links = isset($options['show_subscription_links']) ? $options['show_subscription_links'] : 'yes'; if ($show

发表评论

实战教程,在网站中添加在线个人财务记账与预算管理小程序

实战教程:在WordPress网站中添加在线个人财务记账与预算管理小程序 引言:为什么网站需要个人财务工具? 在当今数字化时代,个人财务管理已成为许多人日常生活中的重要组成部分。随着在线消费、数字支付和远程工作的普及,人们越来越需要便捷的工具来跟踪和管理个人财务。对于网站运营者而言,在网站上集成实用的个人财务工具不仅能增加用户粘性,还能提升网站的价值和实用性。 WordPress作为全球最流行的内容管理系统,其强大的可扩展性使其成为实现此类功能的理想平台。通过代码二次开发,我们可以在WordPress网站上添加一个完整的在线个人财务记账与预算管理小程序,为用户提供实用的互联网小工具。 本教程将详细指导您如何通过WordPress代码二次开发,实现一个功能完整的个人财务管理系统,涵盖记账、预算管理、数据可视化等核心功能。 第一部分:项目规划与环境准备 1.1 功能需求分析 在开始开发之前,我们需要明确个人财务管理系统应包含的核心功能: 用户认证与数据隔离:确保每个用户只能访问自己的财务数据 收入与支出记录:支持添加、编辑、删除和分类财务记录 预算管理:允许用户设置月度/年度预算并跟踪执行情况 数据可视化:通过图表展示财务趋势和分类占比 数据导出:支持将财务数据导出为CSV或Excel格式 报表生成:自动生成月度/年度财务报告 1.2 开发环境搭建 为了进行WordPress二次开发,我们需要准备以下环境: 本地开发环境:安装XAMPP、MAMP或Local by Flywheel WordPress安装:最新版本的WordPress核心文件 代码编辑器:VS Code、Sublime Text或PHPStorm 浏览器开发者工具:用于调试前端代码 1.3 创建自定义插件 我们将通过创建自定义插件的方式实现财务管理系统,这样可以确保代码的独立性和可维护性。 在WordPress的wp-content/plugins目录下创建新文件夹personal-finance-manager,并在其中创建主插件文件: <?php /** * Plugin Name: 个人财务记账与预算管理系统 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress网站中添加在线个人财务记账与预算管理功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('PFM_VERSION', '1.0.0'); define('PFM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('PFM_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once PFM_PLUGIN_DIR . 'includes/class-pfm-init.php'; 第二部分:数据库设计与数据模型 2.1 创建自定义数据库表 个人财务数据需要专门的数据库表来存储。我们将在插件激活时创建这些表: // 在includes/class-pfm-db.php中 class PFM_DB { public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 财务记录表 $table_name = $wpdb->prefix . 'pfm_transactions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, type varchar(20) NOT NULL COMMENT 'income/expense', category varchar(100) NOT NULL, amount decimal(10,2) NOT NULL, description text, transaction_date date NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY transaction_date (transaction_date), KEY category (category) ) $charset_collate;"; // 预算表 $budget_table = $wpdb->prefix . 'pfm_budgets'; $budget_sql = "CREATE TABLE IF NOT EXISTS $budget_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, category varchar(100) NOT NULL, budget_amount decimal(10,2) NOT NULL, period varchar(20) NOT NULL COMMENT 'monthly/yearly', start_date date NOT NULL, end_date date, created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY user_id (user_id), KEY period (period) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); dbDelta($budget_sql); } } 2.2 数据模型类 创建数据模型类来处理财务数据的CRUD操作: // 在includes/class-pfm-transaction.php中 class PFM_Transaction { private $db; private $table_name; public function __construct() { global $wpdb; $this->db = $wpdb; $this->table_name = $wpdb->prefix . 'pfm_transactions'; } // 添加财务记录 public function add($user_id, $data) { $defaults = array( 'type' => 'expense', 'category' => '其他', 'amount' => 0.00, 'description' => '', 'transaction_date' => current_time('Y-m-d') ); $data = wp_parse_args($data, $defaults); return $this->db->insert( $this->table_name, array( 'user_id' => $user_id, 'type' => sanitize_text_field($data['type']), 'category' => sanitize_text_field($data['category']), 'amount' => floatval($data['amount']), 'description' => sanitize_textarea_field($data['description']), 'transaction_date' => sanitize_text_field($data['transaction_date']) ), array('%d', '%s', '%s', '%f', '%s', '%s') ); } // 获取用户的财务记录 public function get_user_transactions($user_id, $filters = array()) { $where = array('user_id = %d'); $params = array($user_id); if (!empty($filters['type'])) { $where[] = 'type = %s'; $params[] = $filters['type']; } if (!empty($filters['category'])) { $where[] = 'category = %s'; $params[] = $filters['category']; } if (!empty($filters['start_date'])) { $where[] = 'transaction_date >= %s'; $params[] = $filters['start_date']; } if (!empty($filters['end_date'])) { $where[] = 'transaction_date <= %s'; $params[] = $filters['end_date']; } $where_clause = implode(' AND ', $where); $sql = $this->db->prepare( "SELECT * FROM {$this->table_name} WHERE {$where_clause} ORDER BY transaction_date DESC", $params ); return $this->db->get_results($sql); } // 更多方法:更新、删除、统计等 } 第三部分:用户界面与前端开发 3.1 创建管理页面 在WordPress后台添加财务管理菜单项: // 在includes/class-pfm-admin.php中 class PFM_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( '个人财务管理', '财务记账', 'read', // 所有登录用户都可以访问 'personal-finance', array($this, 'render_dashboard'), 'dashicons-chart-area', 30 ); add_submenu_page( 'personal-finance', '财务概览', '概览', 'read', 'personal-finance', array($this, 'render_dashboard') ); add_submenu_page( 'personal-finance', '添加记录', '添加记录', 'read', 'pfm-add-transaction', array($this, 'render_add_transaction_page') ); add_submenu_page( 'personal-finance', '预算管理', '预算管理', 'read', 'pfm-budgets', array($this, 'render_budgets_page') ); add_submenu_page( 'personal-finance', '财务报告', '报告', 'read', 'pfm-reports', array($this, 'render_reports_page') ); } public function render_dashboard() { include PFM_PLUGIN_DIR . 'templates/dashboard.php'; } // 其他页面渲染方法 } 3.2 前端模板设计 创建财务概览页面模板: <!-- 在templates/dashboard.php中 --> <div class="wrap pfm-dashboard"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <div class="pfm-stats-container"> <div class="pfm-stat-card"> <h3>本月收入</h3> <div class="pfm-stat-amount income">¥<?php echo number_format($monthly_income, 2); ?></div> </div> <div class="pfm-stat-card"> <h3>本月支出</h3> <div class="pfm-stat-amount expense">¥<?php echo number_format($monthly_expense, 2); ?></div> </div> <div class="pfm-stat-card"> <h3>本月结余</h3> <div class="pfm-stat-amount balance">¥<?php echo number_format($monthly_balance, 2); ?></div> </div> <div class="pfm-stat-card"> <h3>预算执行率</h3> <div class="pfm-stat-amount"><?php echo $budget_usage; ?>%</div> </div> </div> <div class="pfm-chart-container"> <h2>月度收支趋势</h2> <canvas id="pfm-monthly-trend-chart" width="400" height="200"></canvas> </div> <div class="pfm-recent-transactions"> <h2>最近交易记录</h2> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>日期</th> <th>类别</th> <th>类型</th> <th>描述</th> <th>金额</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach($recent_transactions as $transaction): ?> <tr> <td><?php echo esc_html($transaction->transaction_date); ?></td> <td><?php echo esc_html($transaction->category); ?></td> <td> <span class="pfm-type-badge <?php echo $transaction->type; ?>"> <?php echo $transaction->type == 'income' ? '收入' : '支出'; ?> </span> </td> <td><?php echo esc_html($transaction->description); ?></td> <td class="amount <?php echo $transaction->type; ?>"> <?php echo ($transaction->type == 'income' ? '+' : '-') . number_format($transaction->amount, 2); ?> </td> <td> <button class="button button-small edit-transaction" data-id="<?php echo $transaction->id; ?>">编辑</button> <button class="button button-small delete-transaction" data-id="<?php echo $transaction->id; ?>">删除</button> </td> </tr> <?php endforeach; ?> </tbody> </table> </div> </div> 3.3 添加记录表单 创建添加财务记录的表单界面: <!-- 在templates/add-transaction.php中 --> <div class="wrap pfm-add-transaction"> <h1>添加财务记录</h1> <form id="pfm-transaction-form" method="post"> <?php wp_nonce_field('pfm_add_transaction', 'pfm_nonce'); ?> <div class="pfm-form-section"> <h2>基本信息</h2> <div class="pfm-form-row"> <label for="transaction-type">类型</label> <select id="transaction-type" name="type" required> <option value="income">收入</option> <option value="expense" selected>支出</option> </select> </div> <div class="pfm-form-row"> <label for="transaction-category">类别</label> <select id="transaction-category" name="category" required> <option value="餐饮">餐饮</option> <option value="交通">交通</option> <option value="购物">购物</option> <option value="娱乐">娱乐</option> <option value="住房">住房</option> <option value="医疗">医疗</option> <option value="教育">教育</option> <option value="投资">投资</option> <option value="工资收入">工资收入</option> <option value="其他收入">其他收入</option> <option value="其他">其他</option> </select> <input type="text" id="new-category" name="new_category" placeholder="或输入新类别" style="display:none;"> <button type="button" id="add-category-btn" class="button">+</button> </div> <div class="pfm-form-row"> <label for="transaction-amount">金额 (¥)</label> <input type="number" id="transaction-amount" name="amount" step="0.01" min="0" required> </div> <div class="pfm-form-row"> <label for="transaction-date">日期</label> <input type="date" id="transaction-date" name="transaction_date" value="<?php echo date('Y-m-d'); ?>" required> </div> </div> <div class="pfm-form-section"> <h2>详细信息</h2> <div class="pfm-form-row"> <label for="transaction-description">描述</label> <textarea id="transaction-description" name="description" rows="3" placeholder="可选的描述信息..."></textarea> </div> <div class="pfm-form-row"> <label for="transaction-tags">标签</label> <input type="text" id="transaction-tags" name="tags" placeholder="用逗号分隔标签"> </div> </div> <div class="pfm-form-actions"> <button type="submit" class="button button-primary">保存记录</button> <button type="button" id="save-and-add-another" class="button">保存并继续添加</button> <a href="<?php echo admin_url('admin.php?page=personal-finance'); ?>" class="button">返回概览</a> </div> </form> </div> 第四部分:预算管理功能实现 4.1 预算数据模型 创建预算管理的数据模型类: // 在includes/class-pfm-budget.php中 class PFM_Budget { private $db; private $table_name; public function __construct() { global $wpdb; $this->db = $wpdb; $this->table_name = $wpdb->prefix . 'pfm_budgets'; } // 设置预算 public function set_budget($user_id, $data) { $defaults = array( 'category' => '', 'budget_amount' => 0.00, 'period' => 'monthly', 'start_date' => date('Y-m-01'), // 当月第一天 'end_date' => null ); $data = wp_parse_args($data, $defaults); // 检查是否已存在该类别同期的预算 $existing = $this->db->get_var($this->db->prepare( "SELECT id FROM {$this->table_name} WHERE user_id = %d AND category = %s AND period = %s AND start_date = %s", $user_id, $data['category'], $data['period'], $data['start_date'] )); if ($existing) { // 更新现有预算 return $this->db->update( $this->table_name, array('budget_amount' => floatval($data['budget_amount'])), array('id' => $existing), array('%f'), array('%d') ); } else { // 插入新预算 return $this->db->insert( $this->table_name, array( 'user_id' => $user_id, 'category' => sanitize_text_field($data['category']), 'budget_amount' => floatval($data['budget_amount']), 'period' => sanitize_text_field($data['period']), 'start_date' => sanitize_text_field($data['start_date']), 'end_date' => $data['end_date'] ? sanitize_text_field($data['end_date']) : null ), array('%d', '%s', '%f', '%s', '%s', '%s') ); } } // 获取用户预算执行情况 public function get_budget_progress($user_id, $period = 'current_month') { global $wpdb; // 确定日期范围 if ($period === 'current_month') { $start_date = date('Y-m-01'); $end_date = date('Y-m-t'); } elseif ($period === 'current_year') { $start_date = date('Y-01-01'); $end_date = date('Y-12-31'); } else { // 自定义日期范围 $date_range = explode('_to_', $period); $start_date = $date_range[0]; $end_date = $date_range[1]; } // 获取预算数据 $budgets = $wpdb->get_results($wpdb->prepare( "SELECT category, budget_amount, period FROM {$this->table_name} WHERE user_id = %d AND start_date <= %s AND (end_date >= %s OR end_date IS NULL)", $user_id, $end_date, $start_date )); $progress_data = array(); foreach ($budgets as $budget) { // 获取该类别在指定期间的支出总额 $transaction_table = $wpdb->prefix . 'pfm_transactions'; $actual_spent = $wpdb->get_var($wpdb->prepare( "SELECT SUM(amount) FROM {$transaction_table} WHERE user_id = %d AND category = %s AND type = 'expense' AND transaction_date BETWEEN %s AND %s", $user_id, $budget->category, $start_date, $end_date )) ?: 0; $progress_data[] = array( 'category' => $budget->category, 'budget_amount' => floatval($budget->budget_amount), 'actual_spent' => floatval($actual_spent), 'remaining' => floatval($budget->budget_amount) - floatval($actual_spent), 'usage_percentage' => $budget->budget_amount > 0 ? (floatval($actual_spent) / floatval($budget->budget_amount)) * 100 : 0 ); } return $progress_data; } } 4.2 预算管理界面 创建预算管理页面模板: <!-- 在templates/budgets.php中 --> <div class="wrap pfm-budgets"> <h1>预算管理</h1> <div class="pfm-tabs"> <button class="pfm-tab-button active" data-tab="current-budgets">当前预算</button> <button class="pfm-tab-button" data-tab="set-budget">设置预算</button> <button class="pfm-tab-button" data-tab="budget-history">历史预算</button> </div> <div id="current-budgets" class="pfm-tab-content active"> <h2>当前预算执行情况</h2> <div class="pfm-period-selector"> <label for="budget-period">查看期间:</label> <select id="budget-period"> <option value="current_month">本月</option> <option value="current_year">本年</option> <option value="last_month">上月</option> <option value="last_year">去年</option> </select> </div> <div class="pfm-budget-progress-container"> <?php foreach ($budget_progress as $item): ?> <div class="pfm-budget-item"> <div class="pfm-budget-header"> <h3><?php echo esc_html($item['category']); ?></h3> <span class="pfm-budget-amount">预算:¥<?php echo number_format($item['budget_amount'], 2); ?></span> </div> <div class="pfm-progress-bar-container"> <div class="pfm-progress-bar"> <div class="pfm-progress-fill" style="width: <?php echo min($item['usage_percentage'], 100); ?>%; background-color: <?php echo $item['usage_percentage'] > 90 ? '#f44336' : ($item['usage_percentage'] > 70 ? '#ff9800' : '#4CAF50'); ?>"> </div> </div> <div class="pfm-progress-text"> <span>已用:¥<?php echo number_format($item['actual_spent'], 2); ?></span> <span>剩余:¥<?php echo number_format($item['remaining'], 2); ?></span> <span>使用率:<?php echo number_format($item['usage_percentage'], 1); ?>%</span> </div> </div> <?php if ($item['usage_percentage'] > 100): ?> <div class="pfm-budget-alert"> <span class="dashicons dashicons-warning"></span> 已超出预算 <?php echo number_format($item['usage_percentage'] - 100, 1); ?>% </div> <?php endif; ?> </div> <?php endforeach; ?> </div> </div> <div id="set-budget" class="pfm-tab-content"> <h2>设置新预算</h2> <form id="pfm-budget-form" method="post"> <?php wp_nonce_field('pfm_set_budget', 'pfm_budget_nonce'); ?> <div class="pfm-form-row"> <label for="budget-category">预算类别</label> <select id="budget-category" name="category" required> <option value="">选择类别</option> <option value="餐饮">餐饮</option> <option value="交通">交通</option> <option value="购物">购物</option> <option value="娱乐">娱乐</option> <option value="住房">住房</option> <option value="医疗">医疗</option> <option value="教育">教育</option> <option value="其他">其他</option> </select> </div> <div class="pfm-form-row"> <label for="budget-amount">预算金额 (¥)</label> <input type="number" id="budget-amount" name="budget_amount" step="0.01" min="0" required> </div> <div class="pfm-form-row"> <label for="budget-period">预算周期</label> <select id="budget-period" name="period" required> <option value="monthly">月度预算</option> <option value="yearly">年度预算</option> <option value="custom">自定义周期</option> </select> </div> <div id="custom-period-fields" style="display: none;"> <div class="pfm-form-row"> <label for="budget-start-date">开始日期</label> <input type="date" id="budget-start-date" name="start_date"> </div> <div class="pfm-form-row"> <label for="budget-end-date">结束日期</label> <input type="date" id="budget-end-date" name="end_date"> </div> </div> <div class="pfm-form-actions"> <button type="submit" class="button button-primary">保存预算</button> <button type="reset" class="button">重置</button> </div> </form> </div> </div> 第五部分:数据可视化与报表功能 5.1 图表数据接口 创建API接口提供图表数据: // 在includes/class-pfm-api.php中 class PFM_API { public function __construct() { add_action('wp_ajax_pfm_get_chart_data', array($this, 'get_chart_data')); add_action('wp_ajax_nopriv_pfm_get_chart_data', array($this, 'require_login')); } public function get_chart_data() { // 验证用户权限 if (!is_user_logged_in()) { wp_die('请先登录'); } $user_id = get_current_user_id(); $chart_type = sanitize_text_field($_POST['chart_type']); $period = sanitize_text_field($_POST['period']); switch ($chart_type) { case 'monthly_trend': $data = $this->get_monthly_trend_data($user_id, $period); break; case 'category_distribution': $data = $this->get_category_distribution_data($user_id, $period); break; case 'budget_vs_actual': $data = $this->get_budget_vs_actual_data($user_id, $period); break; default: $data = array('error' => '无效的图表类型'); } wp_send_json_success($data); } private function get_monthly_trend_data($user_id, $months = 6) { global $wpdb; $end_date = date('Y-m-d'); $start_date = date('Y-m-d', strtotime("-$months months")); $transaction_table = $wpdb->prefix . 'pfm_transactions'; // 获取月度收入支出数据 $results = $wpdb->get_results($wpdb->prepare( "SELECT DATE_FORMAT(transaction_date, '%%Y-%%m') as month, SUM(CASE WHEN type = 'income' THEN amount ELSE 0 END) as income, SUM(CASE WHEN type = 'expense' THEN amount ELSE 0 END) as expense FROM {$transaction_table} WHERE user_id = %d AND transaction_date BETWEEN %s AND %s GROUP BY DATE_FORMAT(transaction_date, '%%Y-%%m') ORDER BY month", $user_id, $start_date, $end_date )); $labels = array(); $income_data = array(); $expense_data = array(); foreach ($results as $row) { $labels[] = $row->month; $income_data[] = floatval($row->income); $expense_data[] = floatval($row->expense); } return array( 'labels' => $labels, 'datasets' => array( array( 'label' => '收入', 'data' => $income_data, 'borderColor' => '#4CAF50', 'backgroundColor' => 'rgba(76, 175, 80, 0.1)', 'fill' => true ), array( 'label' => '支出', 'data' => $expense_data, 'borderColor' => '#f44336', 'backgroundColor' => 'rgba(244, 67, 54, 0.1)', 'fill' => true ) ) ); } private function get_category_distribution_data($user_id, $period = 'current_month') { global $wpdb; // 确定日期范围 if ($period === 'current_month') { $start_date = date('Y-m-01'); $end_date = date('Y-m-d'); } else { $start_date = date('Y-01-01'); $end_date = date('Y-12-31'); } $transaction_table = $wpdb->prefix . 'pfm_transactions'; $results = $wpdb->get_results($wpdb->prepare( "SELECT category, SUM(amount) as total FROM {$transaction_table} WHERE user_id = %d AND type = 'expense' AND transaction_date BETWEEN %s AND %s GROUP BY category ORDER BY total DESC", $user_id, $start_date, $end_date )); $labels = array(); $data = array(); $background_colors = array(); $border_colors = array(); // 预定义颜色方案 $color_palette = array( '#FF6384', '#36A2EB', '#FFCE56', '#4BC0C0', '#9966FF', '#FF9F40', '#8AC926', '#1982C4', '#6A4C93', '#F15BB5', '#00BBF9', '#00F5D4' ); foreach ($results as $index => $row) { $labels[] = $row->category; $data[] = floatval($row->total); $color_index = $index % count($color_palette); $background_colors[] = $color_palette[$color_index] . '80'; // 添加透明度 $border_colors[] = $color_palette[$color_index]; } return array( 'labels' => $labels, 'datasets' => array( array( 'data' => $data, 'backgroundColor' => $background_colors, 'borderColor' => $border_colors, 'borderWidth' => 1 ) ) ); } } 5.2 图表渲染与交互 创建JavaScript代码来渲染图表: // 在assets/js/pfm-charts.js中 jQuery(document).ready(function($) { // 图表实例存储 var pfmCharts = {}; // 初始化月度趋势图表 function initMonthlyTrendChart() { var ctx = document.getElementById('pfm-monthly-trend-chart').getContext('2d'); // 获取数据 $.ajax({ url: pfm_ajax.ajax_url, type: 'POST', data: { action: 'pfm_get_chart_data', chart_type: 'monthly_trend', period: '6_months', nonce: pfm_ajax.nonce }, success: function(response) { if (response.success) { pfmCharts.monthlyTrend = new Chart(ctx, { type: 'line', data: response.data, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', }, tooltip: { callbacks: { label: function(context) { return context.dataset.label + ': ¥' + context.parsed.y.toFixed(2); } } } }, scales: { y: { beginAtZero: true, ticks: { callback: function(value) { return '¥' + value; } } } } } }); } } }); } // 初始化分类分布图表 function initCategoryDistributionChart() { var ctx = document.getElementById('pfm-category-chart').getContext('2d'); $.ajax({ url: pfm_ajax.ajax_url, type: 'POST', data: { action: 'pfm_get_chart_data', chart_type: 'category_distribution', period: 'current_month', nonce: pfm_ajax.nonce }, success: function(response) { if (response.success) { pfmCharts.categoryDistribution = new Chart(ctx, { type: 'doughnut', data: response.data, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', }, tooltip: { callbacks: { label: function(context) { var label = context.label || ''; var value = context.parsed || 0; var total = context.dataset.data.reduce((a, b) => a + b, 0); var percentage = Math.round((value / total) * 100); return label + ': ¥' + value.toFixed(2) + ' (' + percentage + '%)'; } } } } } }); } } }); } // 更新图表数据 function updateChart(chartType, period) { if (pfmCharts[chartType]) { $.ajax({ url: pfm_ajax.ajax_url, type: 'POST', data: { action: 'pfm_get_chart_data', chart_type: chartType, period: period, nonce: pfm_ajax.nonce }, success: function(response) { if (response.success) { pfmCharts[chartType].data = response.data; pfmCharts[chartType].update(); } } }); } } // 初始化所有图表 if ($('#pfm-monthly-trend-chart').length) { initMonthlyTrendChart(); } if ($('#pfm-category-chart').length) { initCategoryDistributionChart(); } // 图表控制事件 $('.pfm-chart-controls select').on('change', function() { var chartType = $(this).data('chart-type'); var period = $(this).val(); updateChart(chartType, period); }); }); 5.3 报表生成功能 创建报表生成功能: // 在includes/class-pfm-reports.php中 class PFM_Reports { public function generate_monthly_report($user_id, $year, $month) { global $wpdb; $start_date = sprintf('%04d-%02d-01', $year, $month); $end_date = date('Y-m-t', strtotime($start_date)); $transaction_table = $wpdb->prefix . 'pfm_transactions'; $budget_table = $wpdb->prefix . 'pfm_budgets'; // 获取月度汇总数据 $summary = $wpdb->get_row($wpdb->prepare( "SELECT

发表评论

手把手教学,为WordPress集成智能化的网站表单数据清洗与去重工具

手把手教学:为WordPress集成智能化的网站表单数据清洗与去重工具 引言:为什么WordPress网站需要数据清洗与去重工具 在当今数字化时代,网站表单已成为企业与用户互动、收集信息的重要渠道。无论是联系表单、注册表单、调查问卷还是订单提交,WordPress网站每天都会通过各类表单收集大量用户数据。然而,这些数据往往存在各种质量问题:重复提交、格式不规范、信息不完整甚至包含恶意内容。 数据质量问题不仅影响后续的数据分析和业务决策,还可能导致营销资源浪费、客户体验下降以及系统性能问题。例如,同一用户多次提交相同信息会占用数据库空间,发送重复邮件可能引起用户反感,而不规范的数据格式则使数据分析变得困难。 传统上,WordPress管理员需要手动处理这些问题,耗时耗力且容易出错。因此,为WordPress集成智能化的数据清洗与去重工具显得尤为重要。本文将手把手教您如何通过WordPress代码二次开发,实现这一常用互联网小工具功能。 第一部分:理解WordPress表单数据处理机制 1.1 WordPress表单插件的工作原理 WordPress本身不提供强大的表单功能,大多数网站都依赖第三方表单插件,如Contact Form 7、Gravity Forms、WPForms等。这些插件通常遵循相似的工作流程: 前端渲染表单HTML 用户填写并提交表单 服务器端验证和处理数据 存储数据到数据库或发送到指定邮箱 显示成功或错误消息 理解这一流程是进行二次开发的基础。大多数表单插件都提供了丰富的钩子(Hooks)和过滤器(Filters),允许开发者在关键节点插入自定义代码。 1.2 WordPress数据存储方式分析 WordPress表单数据通常以以下几种方式存储: 自定义数据库表:专业表单插件如Gravity Forms会创建专用表存储提交数据 文章类型(Post Type):部分插件将表单提交存储为自定义文章类型 选项(Options):简单表单可能将数据存储在wp_options表中 直接发送到邮箱:不存储数据库,仅通过邮件发送 了解数据存储方式对于设计清洗和去重策略至关重要。我们的工具需要能够适应不同的存储方式,提供统一的处理接口。 第二部分:设计智能化数据清洗与去重系统 2.1 系统架构设计 一个完整的数据清洗与去重系统应包含以下模块: 数据采集模块:从不同来源收集表单数据 预处理模块:标准化数据格式 清洗规则引擎:根据预设规则清洗数据 去重算法模块:识别和合并重复数据 数据验证模块:验证数据有效性和完整性 日志与报告模块:记录处理过程和结果 管理界面模块:提供后台配置和监控界面 2.2 关键技术选择 在WordPress环境中实现这一系统,我们需要选择合适的技术方案: PHP作为主要开发语言:WordPress核心使用PHP,所有插件和主题都基于PHP MySQL数据库操作:直接操作WordPress数据库进行数据查询和更新 WordPress钩子系统:利用Action和Filter钩子集成到现有表单处理流程 JavaScript/AJAX:增强前端交互体验 正则表达式:用于复杂的数据格式匹配和清洗 2.3 数据清洗规则设计 数据清洗规则应根据具体业务需求定制,常见规则包括: 格式标准化:统一电话号码、日期、邮箱等格式 去除无效字符:清理HTML标签、特殊字符等 拼写校正:常见词汇的自动校正 缺失值处理:识别并标记或填充缺失数据 异常值检测:识别不符合业务逻辑的数据 敏感信息过滤:移除或加密敏感个人信息 2.4 去重算法设计 去重算法需要平衡准确性和性能: 精确匹配:完全相同的记录 模糊匹配:考虑拼写错误、缩写、同义词等 基于规则的匹配:根据业务规则定义重复条件 机器学习方法:使用相似度算法识别潜在重复 在WordPress环境中,考虑到性能限制,我们主要采用前三种方法。 第三部分:手把手实现数据清洗功能 3.1 创建WordPress插件框架 首先,我们创建一个基本的WordPress插件框架: <?php /** * Plugin Name: 智能表单数据清洗与去重工具 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress表单提供智能数据清洗和去重功能 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('SFDCD_VERSION', '1.0.0'); define('SFDCD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('SFDCD_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 class Smart_Form_Data_Cleaner { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 初始化钩子 add_action('plugins_loaded', array($this, 'load_textdomain')); add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_scripts')); // 表单处理钩子 - 以Contact Form 7为例 add_filter('wpcf7_posted_data', array($this, 'clean_cf7_data'), 10, 1); add_action('wpcf7_before_send_mail', array($this, 'check_duplicates_cf7'), 10, 3); } public function load_textdomain() { load_plugin_textdomain('smart-form-data-cleaner', false, dirname(plugin_basename(__FILE__)) . '/languages'); } // 其他方法将在后续步骤中添加 } // 启动插件 Smart_Form_Data_Cleaner::get_instance(); 3.2 实现基础数据清洗功能 接下来,我们实现基础的数据清洗功能: class Data_Cleaner_Engine { /** * 清洗表单数据 * @param array $form_data 原始表单数据 * @return array 清洗后的数据 */ public static function clean_form_data($form_data) { if (!is_array($form_data) || empty($form_data)) { return $form_data; } $cleaned_data = array(); foreach ($form_data as $field => $value) { // 跳过空值 if (is_null($value) || $value === '') { $cleaned_data[$field] = $value; continue; } // 根据字段类型应用不同的清洗规则 $field_type = self::detect_field_type($field, $value); $cleaned_value = self::apply_cleaning_rules($value, $field_type); $cleaned_data[$field] = $cleaned_value; } return $cleaned_data; } /** * 检测字段类型 */ private static function detect_field_type($field_name, $value) { $field_name = strtolower($field_name); // 根据字段名关键词判断类型 if (strpos($field_name, 'email') !== false) { return 'email'; } elseif (strpos($field_name, 'phone') !== false || strpos($field_name, 'tel') !== false) { return 'phone'; } elseif (strpos($field_name, 'date') !== false) { return 'date'; } elseif (strpos($field_name, 'name') !== false) { return 'name'; } elseif (strpos($field_name, 'address') !== false) { return 'address'; } elseif (strpos($field_name, 'url') !== false || strpos($field_name, 'website') !== false) { return 'url'; } else { // 根据值内容进一步判断 if (is_email($value)) { return 'email'; } elseif (preg_match('/^d{10,}$/', preg_replace('/[^0-9]/', '', $value))) { return 'phone'; } elseif (strtotime($value) !== false) { return 'date'; } else { return 'text'; } } } /** * 应用清洗规则 */ private static function apply_cleaning_rules($value, $field_type) { // 通用清洗:去除首尾空格、HTML标签、多余空白字符 $value = trim($value); $value = strip_tags($value); $value = preg_replace('/s+/', ' ', $value); // 根据字段类型应用特定清洗规则 switch ($field_type) { case 'email': $value = self::clean_email($value); break; case 'phone': $value = self::clean_phone($value); break; case 'date': $value = self::clean_date($value); break; case 'name': $value = self::clean_name($value); break; case 'url': $value = self::clean_url($value); break; case 'address': $value = self::clean_address($value); break; default: // 对普通文本进行基本清洗 $value = self::clean_text($value); } return $value; } /** * 清洗邮箱地址 */ private static function clean_email($email) { $email = strtolower(trim($email)); // 移除邮箱地址中的多余字符 $email = filter_var($email, FILTER_SANITIZE_EMAIL); // 验证邮箱格式 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { // 尝试修复常见邮箱错误 $email = preg_replace('/s+/', '', $email); $email = preg_replace('/[,;]/', '.', $email); // 再次验证 if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { // 如果仍然无效,记录错误但保留原值 error_log('无效的邮箱格式: ' . $email); } } return $email; } /** * 清洗电话号码 */ private static function clean_phone($phone) { // 移除非数字字符 $digits = preg_replace('/[^0-9]/', '', $phone); // 根据不同国家/地区格式标准化 $country_code = self::detect_phone_country_code($digits); switch ($country_code) { case '86': // 中国 if (strlen($digits) === 11) { return preg_replace('/(d{3})(d{4})(d{4})/', '$1 $2 $3', $digits); } break; case '1': // 美国/加拿大 if (strlen($digits) === 10) { return preg_replace('/(d{3})(d{3})(d{4})/', '($1) $2-$3', $digits); } elseif (strlen($digits) === 11) { return preg_replace('/(d{1})(d{3})(d{3})(d{4})/', '+$1 ($2) $3-$4', $digits); } break; default: // 通用格式化:每3-4位加空格 if (strlen($digits) > 6) { $formatted = ''; while (strlen($digits) > 4) { $formatted .= substr($digits, 0, 3) . ' '; $digits = substr($digits, 3); } $formatted .= $digits; return $formatted; } } return $phone; // 如果无法标准化,返回原值 } // 其他清洗方法... } 3.3 集成到Contact Form 7 现在我们将清洗引擎集成到Contact Form 7中: // 在Smart_Form_Data_Cleaner类中添加以下方法 public function clean_cf7_data($posted_data) { // 应用数据清洗 $cleaned_data = Data_Cleaner_Engine::clean_form_data($posted_data); // 记录清洗日志 $this->log_cleaning_process($posted_data, $cleaned_data); return $cleaned_data; } public function check_duplicates_cf7($contact_form, &$abort, $submission) { $posted_data = $submission->get_posted_data(); // 检查重复提交 $is_duplicate = $this->check_for_duplicates($posted_data, 'cf7'); if ($is_duplicate) { // 如果是重复数据,可以阻止提交或标记为重复 $abort = true; $submission->set_response(__('检测到重复提交,请勿重复提交表单。', 'smart-form-data-cleaner')); // 或者可以选择不阻止,但记录重复 // $this->log_duplicate_submission($posted_data); } } private function check_for_duplicates($form_data, $form_type) { global $wpdb; // 根据表单类型确定检查策略 $threshold_time = apply_filters('sfdcd_duplicate_threshold', 24 * HOUR_IN_SECONDS); // 默认24小时内 // 构建查询条件 $conditions = array(); $values = array(); // 检查关键字段:邮箱、电话等 foreach ($form_data as $field => $value) { if ($this->is_key_field($field)) { $conditions[] = "meta_value LIKE %s"; $values[] = '%' . $wpdb->esc_like($value) . '%'; } } if (empty($conditions)) { return false; } // 查询数据库检查重复 $query = "SELECT COUNT(*) FROM {$wpdb->prefix}postmeta WHERE meta_key IN ('_form_data', '_submission_data') AND (" . implode(' OR ', $conditions) . ") AND post_id IN ( SELECT ID FROM {$wpdb->prefix}posts WHERE post_type = 'wpcf7_contact_form' AND post_date > DATE_SUB(NOW(), INTERVAL %d SECOND) )"; $values[] = $threshold_time; $count = $wpdb->get_var($wpdb->prepare($query, $values)); return $count > 0; } private function is_key_field($field_name) { $key_fields = array('email', 'phone', 'tel', 'mobile', 'e-mail'); foreach ($key_fields as $key) { if (stripos($field_name, $key) !== false) { return true; } } return false; } 第四部分:实现智能去重功能 4.1 设计去重算法 去重功能比简单清洗更复杂,需要智能识别相似但不完全相同的数据: class Deduplication_Engine { /** * 查找重复记录 */ public static function find_duplicates($new_data, $form_type, $threshold = 0.8) { global $wpdb; // 获取历史数据进行比较 $historical_data = self::get_historical_data($form_type); $duplicates = array(); foreach ($historical_data as $record) { $similarity = self::calculate_similarity($new_data, $record['data']); if ($similarity >= $threshold) { $duplicates[] = array( 'id' => $record['id'], 'similarity' => $similarity, 'data' => $record['data'] ); } } return $duplicates; } /** * 计算两条记录之间的相似度 */ private static function calculate_similarity($data1, $data2) { $total_weight = 0; $similarity_score = 0; // 定义字段权重 $field_weights = array( 'email' => 0.4, 'phone' => 0.3, 'name' => 0.2, 'address' => 0.1 ); foreach ($data1 as $field => $value1) { if (!isset($data2[$field])) { continue; } $value2 = $data2[$field]; // 确定字段类型和权重 $field_type = Data_Cleaner_Engine::detect_field_type($field, $value1); $weight = isset($field_weights[$field_type]) ? $field_weights[$field_type] : 0.05; // 计算字段相似度 $field_similarity = self::compare_field_values($value1, $value2, $field_type); $similarity_score += $field_similarity * $weight; $total_weight += $weight; } // 避免除以零 if ($total_weight == 0) { return 0; } return $similarity_score / $total_weight; } /** * 比较字段值相似度 */ private static function compare_field_values($value1, $value2, $field_type) { if ($value1 === $value2) { return 1.0; } // 空值处理 if (empty($value1) || empty($value2)) { return 0.2; // 部分匹配分数 } switch ($field_type) { case 'email': // 邮箱比较:考虑用户名和域名分开比较 list($user1, $domain1) = explode('@', strtolower($value1)); list($user2, $domain2) = explode('@', strtolower($value2)); $user_similarity = self::string_similarity($user1, $user2); $domain_similarity = ($domain1 === $domain2) ? 1.0 : 0; return ($user_similarity * 0.7) + ($domain_similarity * 0.3); case 'phone': // 电话号码比较:提取数字后比较 $digits1 = preg_replace('/[^0-9]/', '', $value1); $digits2 = preg_replace('/[^0-9]/', '', $value2); // 考虑国际区号差异 if (strlen($digits1) > 10 && strlen($digits2) > 10) { // 都有国际区号,比较后8-10位 $compare1 = substr($digits1, -10); $compare2 = substr($digits2, -10); return ($compare1 === $compare2) ? 0.9 : 0; } else { // 直接比较 return ($digits1 === $digits2) ? 1.0 : self::string_similarity($digits1, $digits2); } case 'name': // 姓名比较:考虑中英文、顺序、缩写 return self::name_similarity($value1, $value2); default: // 普通文本比较 return self::string_similarity($value1, $value2); } } /** * 字符串相似度计算(使用Levenshtein距离) */ private static function string_similarity($str1, $str2) { $len1 = strlen($str1); $len2 = strlen($str2); if ($len1 === 0 || $len2 === 0) { return 0; } // 计算Levenshtein距离 $distance = levenshtein(strtolower($str1), strtolower($str2)); $max_len = max($len1, $len2); return 1 - ($distance / $max_len); } /** * 姓名相似度计算 */ private static function name_similarity($name1, $name2) { // 转换为小写 $name1 = strtolower(trim($name1)); $name2 = strtolower(trim($name2)); // 完全相等 if ($name1 === $name2) { return 1.0; } // 分割姓名组成部分 $parts1 = preg_split('/[s.,]+/', $name1); $parts2 = preg_split('/[s.,]+/', $name2); // 检查是否只是顺序不同 if (count(array_intersect($parts1, $parts2)) === count($parts1) && count($parts1) === count($parts2)) { return 0.9; } // 检查缩写匹配 $initials1 = array_map(function($part) { return substr($part, 0, 1); }, $parts1); $initials2 = array_map(function($part) { return substr($part, 0, 1); }, $parts2); if (implode('', $initials1) === implode('', $initials2)) { return 0.8; } // 计算平均相似度 $total_similarity = 0; $comparisons = 0; foreach ($parts1 as $part1) { foreach ($parts2 as $part2) { $similarity = self::string_similarity($part1, $part2); if ($similarity > 0.7) { $total_similarity += $similarity; $comparisons++; } } } if ($comparisons > 0) { return $total_similarity / $comparisons; } return self::string_similarity($name1, $name2); } /** * 获取历史数据 */ private static function get_historical_data($form_type, $limit = 100) { global $wpdb; $table_name = $wpdb->prefix . 'sfdcd_submissions'; // 检查表是否存在,如果不存在则返回空数组 if ($wpdb->get_var("SHOW TABLES LIKE '$table_name'") != $table_name) { return array(); } $query = $wpdb->prepare( "SELECT id, form_data FROM $table_name WHERE form_type = %s ORDER BY submission_date DESC LIMIT %d", $form_type, $limit ); $results = $wpdb->get_results($query, ARRAY_A); $historical_data = array(); foreach ($results as $row) { $historical_data[] = array( 'id' => $row['id'], 'data' => maybe_unserialize($row['form_data']) ); } return $historical_data; } } ### 4.2 创建数据存储表 为了有效进行去重,我们需要创建专门的表来存储表单提交记录: class Database_Manager { /** * 创建数据表 */ public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'sfdcd_submissions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, form_type varchar(100) NOT NULL, form_id varchar(100) DEFAULT NULL, form_data longtext NOT NULL, cleaned_data longtext NOT NULL, duplicate_check_hash varchar(64) DEFAULT NULL, is_duplicate tinyint(1) DEFAULT 0, duplicate_of bigint(20) DEFAULT NULL, submission_date datetime DEFAULT CURRENT_TIMESTAMP, processed_date datetime DEFAULT NULL, PRIMARY KEY (id), KEY form_type (form_type), KEY duplicate_check_hash (duplicate_check_hash), KEY submission_date (submission_date), KEY is_duplicate (is_duplicate) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建清洗规则表 $rules_table = $wpdb->prefix . 'sfdcd_cleaning_rules'; $sql = "CREATE TABLE IF NOT EXISTS $rules_table ( id bigint(20) NOT NULL AUTO_INCREMENT, rule_name varchar(200) NOT NULL, rule_type varchar(50) NOT NULL, field_pattern varchar(200) DEFAULT NULL, condition text NOT NULL, action text NOT NULL, priority int(11) DEFAULT 10, is_active tinyint(1) DEFAULT 1, created_date datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY rule_type (rule_type), KEY is_active (is_active) ) $charset_collate;"; dbDelta($sql); } /** * 存储表单提交 */ public static function store_submission($form_type, $form_id, $original_data, $cleaned_data) { global $wpdb; $table_name = $wpdb->prefix . 'sfdcd_submissions'; // 生成去重检查哈希 $hash_data = self::prepare_data_for_hashing($cleaned_data); $duplicate_hash = md5(serialize($hash_data)); // 检查是否已存在相同哈希的记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE duplicate_check_hash = %s AND form_type = %s AND submission_date > DATE_SUB(NOW(), INTERVAL 7 DAY)", $duplicate_hash, $form_type )); $is_duplicate = $existing ? 1 : 0; $duplicate_of = $existing ? $existing : null; // 插入新记录 $result = $wpdb->insert( $table_name, array( 'form_type' => $form_type, 'form_id' => $form_id, 'form_data' => maybe_serialize($original_data), 'cleaned_data' => maybe_serialize($cleaned_data), 'duplicate_check_hash' => $duplicate_hash, 'is_duplicate' => $is_duplicate, 'duplicate_of' => $duplicate_of, 'processed_date' => current_time('mysql') ), array('%s', '%s', '%s', '%s', '%s', '%d', '%d', '%s') ); return array( 'success' => $result !== false, 'submission_id' => $wpdb->insert_id, 'is_duplicate' => $is_duplicate, 'duplicate_of' => $duplicate_of ); } /** * 准备用于哈希计算的数据 */ private static function prepare_data_for_hashing($data) { $hash_data = array(); // 只选择关键字段进行哈希计算 $key_fields = array('email', 'phone', 'name', 'user_login', 'e-mail'); foreach ($data as $field => $value) { $field_lower = strtolower($field); foreach ($key_fields as $key) { if (strpos($field_lower, $key) !== false) { $hash_data[$field] = $value; break; } } } // 如果没有关键字段,使用所有字段 if (empty($hash_data)) { $hash_data = $data; } // 对值进行标准化处理 foreach ($hash_data as &$value) { if (is_string($value)) { $value = strtolower(trim($value)); $value = preg_replace('/s+/', ' ', $value); } } // 按键名排序确保一致性 ksort($hash_data); return $hash_data; } } ## 第五部分:创建管理界面 ### 5.1 后台管理菜单 // 在Smart_Form_Data_Cleaner类中添加管理界面方法 public function add_admin_menu() { // 主菜单 add_menu_page( __('智能表单数据清洗', 'smart-form-data-cleaner'), __('数据清洗', 'smart-form-data-cleaner'), 'manage_options', 'smart-form-data-cleaner', array($this, 'render_main_page'), 'dashicons-filter', 30 ); // 子菜单 add_submenu_page( 'smart-form-data-cleaner', __('数据清洗记录', 'smart-form-data-cleaner'), __('清洗记录', 'smart-form-data-cleaner'), 'manage_options', 'sfdcd-cleaning-logs', array($this, 'render_cleaning_logs_page') ); add_submenu_page( 'smart-form-data-cleaner', __('重复数据管理', 'smart-form-data-cleaner'), __('重复数据', 'smart-form-data-cleaner'), 'manage_options', 'sfdcd-duplicates', array($this, 'render_duplicates_page') ); add_submenu_page( 'smart-form-data-cleaner', __('清洗规则设置', 'smart-form-data-cleaner'), __('清洗规则', 'smart-form-data-cleaner'), 'manage_options', 'sfdcd-cleaning-rules', array($this, 'render_rules_page') ); add_submenu_page( 'smart-form-data-cleaner', __('系统设置', 'smart-form-data-cleaner'), __('系统设置', 'smart-form-data-cleaner'), 'manage_options', 'sfdcd-settings', array($this, 'render_settings_page') ); } public function enqueue_admin_scripts($hook) { // 只在我们插件页面加载资源 if (strpos($hook, 'smart-form-data-cleaner') === false && strpos($hook, 'sfdcd-') === false) { return; } // 加载CSS wp_enqueue_style( 'sfdcd-admin-style', SFDCD_PLUGIN_URL . 'assets/css/admin.css', array(), SFDCD_VERSION ); // 加载JavaScript wp_enqueue_script( 'sfdcd-admin-script', SFDCD_PLUGIN_URL . 'assets/js/admin.js', array('jquery', 'jquery-ui-sortable', 'jquery-ui-dialog'), SFDCD_VERSION, true ); // 本地化脚本 wp_localize_script('sfdcd-admin-script', 'sfdcd_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('sfdcd_ajax_nonce'), 'confirm_delete' => __('确定要删除这条记录吗?', 'smart-form-data-cleaner'), 'processing' => __('处理中...', 'smart-form-data-cleaner') )); } ### 5.2 主页面模板 public function render_main_page() { // 检查数据库表是否存在,如果不存在则创建 Database_Manager::create_tables(); // 获取统计数据 $stats = $this->get_system_stats(); ?> <div class="wrap sfdcd-dashboard"> <h1><?php _e('智能表单数据清洗与去重工具', 'smart-form-data-cleaner'); ?></h1> <div class="sfdcd-stats-container"> <div class="sfdcd-stat-card"> <h3><?php _e('今日提交', 'smart-form-data-cleaner'); ?></h3> <div class="stat-number"><?php echo esc_html($stats['today_submissions']); ?></div> </div> <div class="sfdcd-stat-card"> <h3><?php _e('重复数据', 'smart-form-data-cleaner'); ?></h3> <div class="stat-number"><?php echo esc_html($stats['duplicate_count']); ?></div> <div class="stat-percentage"><?php echo esc_html($stats['duplicate_percentage']); ?>%</div> </div> <div class="sfdcd-stat-card"> <h3><?php _e('数据清洗', 'smart-form-data-cleaner'); ?></h3> <div class="stat-number"><?php echo esc_html($stats['cleaned_count']); ?></div> <div class="stat-percentage"><?php echo esc_html($stats['cleaning_rate']); ?>%</div> </div> <div class="sfdcd-stat-card"> <h3><?php _e('活跃规则', 'smart-form-data-cleaner'); ?></h3> <div class="stat-number"><?php echo esc_html($stats['active_rules']); ?></div> </div> </div> <div class="sfdcd-dashboard-content"> <div class="sfdcd-dashboard-column"> <div class="sfdcd-card"> <h2><?php _e('最近提交', 'smart-form-data-cleaner'); ?></h2> <?php $this->render_recent_submissions(); ?> </div> <div class="sfdcd-card"> <h2><?php _e('快速操作', 'smart-form-data-cleaner'); ?></h2> <div class="sfdcd-quick-actions"> <button class="button button-primary" onclick="location.href='?page=sfdcd-cleaning-rules&action=add'"> <?php _e('添加清洗规则', 'smart-form-data-cleaner'); ?> </button> <button class="button" onclick="sfdcd_run_manual_cleanup()"> <?php _e('手动清洗数据', 'smart-form-data-cleaner'); ?> </button> <button class="button" onclick="sfdcd_check_duplicates()"> <?php _e('检查重复数据', 'smart-form-data-cleaner'); ?> </button> <button class="button" onclick="location.href='?page=sfdcd-settings'"> <?php _e('系统设置', 'smart-form-data-cleaner'); ?> </button> </div> </div> </div> <div class="sfdcd-dashboard-column"> <div class="sfdcd-card"> <h2><?php _e('系统状态', 'smart-form-data-cleaner'); ?></h2> <div class="sfdcd-system-status"> <div class="status-item"> <span class="status-label"><?php _e('数据库表', 'smart-form-data-cleaner'); ?>:</span> <span class="status-value status-ok"><?php _e('正常', 'smart-form-data-cleaner'); ?></span> </div> <div class="status-item"> <span class="status-label"><?php _e('最后运行', 'smart-form-data-cleaner'); ?>:</span> <span class="status-value"><?php echo esc_html($stats['last_run']); ?></span> </div> <div class="status-item"> <span class="status-label"><?php _e('支持的表单插件', 'smart-form-data-cleaner'); ?>:</

发表评论

详细指南,开发网站会员签到打卡与连续奖励积分系统

详细指南:开发网站会员签到打卡与连续奖励积分系统 摘要 在当今互联网时代,用户参与度和忠诚度是网站成功的关键因素之一。会员签到打卡与连续奖励积分系统作为一种有效的用户激励工具,已被广泛应用于各类网站和应用程序中。本文将详细介绍如何通过WordPress程序的代码二次开发,实现一个功能完善的会员签到打卡与连续奖励积分系统。我们将从系统设计、数据库结构、核心功能实现、用户界面设计到系统测试与优化,全面解析开发过程,帮助开发者快速掌握这一常用互联网小工具的实现方法。 第一章:系统概述与设计思路 1.1 签到打卡系统的价值 签到打卡系统是一种简单而有效的用户参与机制,通过每日签到获取积分奖励的方式,鼓励用户养成访问网站的习惯。这种系统不仅提高了用户粘性,还能促进内容消费和社区互动。结合连续签到奖励机制,用户为了获得更高的积分奖励,会努力保持连续签到记录,从而形成良性的用户行为循环。 1.2 系统功能需求分析 一个完整的会员签到打卡与连续奖励积分系统应包含以下核心功能: 用户签到功能:允许用户每日签到一次,记录签到时间 连续签到追踪:跟踪用户连续签到天数,并在中断时重置 积分奖励机制:根据签到情况奖励相应积分,连续签到天数越多奖励越高 签到日历展示:可视化展示用户的签到历史记录 奖励规则配置:管理员可灵活配置积分奖励规则 用户数据统计:展示用户的签到统计信息 积分兑换功能:允许用户使用积分兑换虚拟或实物奖励 1.3 WordPress二次开发的优势 WordPress作为全球最流行的内容管理系统,拥有以下优势: 成熟的用户管理系统 丰富的插件架构和钩子机制 庞大的开发者社区支持 灵活的主题和插件扩展能力 良好的安全性和稳定性 通过WordPress二次开发实现签到系统,可以充分利用现有用户体系,减少重复开发工作,提高开发效率。 第二章:数据库设计与数据模型 2.1 数据表结构设计 为了实现签到打卡系统,我们需要在WordPress数据库中添加以下自定义数据表: -- 用户签到记录表 CREATE TABLE wp_signin_records ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL, signin_date DATE NOT NULL, signin_time DATETIME NOT NULL, continuous_days INT(11) DEFAULT 1, points_earned INT(11) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY user_date (user_id, signin_date), KEY user_id (user_id), KEY signin_date (signin_date) ); -- 用户积分总表 CREATE TABLE wp_user_points ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL, total_points INT(11) DEFAULT 0, available_points INT(11) DEFAULT 0, last_updated DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY user_id (user_id) ); -- 积分奖励规则表 CREATE TABLE wp_points_rules ( id INT(11) NOT NULL AUTO_INCREMENT, rule_name VARCHAR(100) NOT NULL, continuous_days INT(11) NOT NULL, points_award INT(11) NOT NULL, is_active TINYINT(1) DEFAULT 1, PRIMARY KEY (id) ); -- 积分兑换记录表 CREATE TABLE wp_points_redemption ( id INT(11) NOT NULL AUTO_INCREMENT, user_id INT(11) NOT NULL, redemption_date DATETIME NOT NULL, points_used INT(11) NOT NULL, item_name VARCHAR(200) NOT NULL, status VARCHAR(50) DEFAULT 'pending', PRIMARY KEY (id), KEY user_id (user_id) ); 2.2 数据关系与索引优化 为了提高查询效率,我们在关键字段上建立了索引: wp_signin_records表的user_id和signin_date字段建立联合唯一索引,防止重复签到 各表的user_id字段建立普通索引,加快用户相关查询 signin_date字段建立索引,便于按日期范围查询统计 2.3 与WordPress用户表的关联 我们的签到系统需要与WordPress核心用户表wp_users进行关联,通过user_id字段建立外键关系,确保数据一致性。同时,我们可以利用WordPress的用户元数据表wp_usermeta存储一些额外的用户签到相关信息。 第三章:核心功能实现 3.1 签到功能模块 3.1.1 签到逻辑实现 <?php /** * 处理用户签到 */ function handle_user_signin($user_id) { global $wpdb; // 检查用户是否已签到 $today = date('Y-m-d'); $table_name = $wpdb->prefix . 'signin_records'; $already_signed = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d AND signin_date = %s", $user_id, $today )); if ($already_signed > 0) { return array( 'success' => false, 'message' => '今日已签到,请明天再来!' ); } // 获取用户昨日签到记录,计算连续签到天数 $yesterday = date('Y-m-d', strtotime('-1 day')); $yesterday_record = $wpdb->get_row($wpdb->prepare( "SELECT continuous_days FROM $table_name WHERE user_id = %d AND signin_date = %s", $user_id, $yesterday )); $continuous_days = 1; if ($yesterday_record) { $continuous_days = $yesterday_record->continuous_days + 1; } // 根据连续签到天数计算奖励积分 $points_earned = calculate_signin_points($continuous_days); // 插入签到记录 $result = $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'signin_date' => $today, 'signin_time' => current_time('mysql'), 'continuous_days' => $continuous_days, 'points_earned' => $points_earned ), array('%d', '%s', '%s', '%d', '%d') ); if ($result) { // 更新用户总积分 update_user_points($user_id, $points_earned); return array( 'success' => true, 'message' => '签到成功!', 'continuous_days' => $continuous_days, 'points_earned' => $points_earned ); } else { return array( 'success' => false, 'message' => '签到失败,请稍后重试!' ); } } /** * 根据连续签到天数计算奖励积分 */ function calculate_signin_points($continuous_days) { $base_points = 10; // 基础积分 // 连续签到额外奖励 $extra_points = 0; if ($continuous_days >= 7) { $extra_points = 30; } elseif ($continuous_days >= 30) { $extra_points = 100; } elseif ($continuous_days >= 100) { $extra_points = 500; } // 特殊日期额外奖励(如节假日) $special_bonus = check_special_date_bonus(); return $base_points + $extra_points + $special_bonus; } ?> 3.1.2 AJAX签到接口 <?php /** * 注册AJAX签到接口 */ add_action('wp_ajax_user_signin', 'ajax_handle_signin'); add_action('wp_ajax_nopriv_user_signin', 'ajax_handle_signin_no_priv'); function ajax_handle_signin() { // 验证用户登录状态 if (!is_user_logged_in()) { wp_send_json_error('请先登录!'); } $user_id = get_current_user_id(); $result = handle_user_signin($user_id); if ($result['success']) { wp_send_json_success($result); } else { wp_send_json_error($result['message']); } } function ajax_handle_signin_no_priv() { wp_send_json_error('请先登录!'); } ?> 3.2 连续签到追踪模块 <?php /** * 获取用户连续签到天数 */ function get_user_continuous_days($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'signin_records'; $today = date('Y-m-d'); // 获取今日签到记录 $today_record = $wpdb->get_row($wpdb->prepare( "SELECT continuous_days FROM $table_name WHERE user_id = %d AND signin_date = %s", $user_id, $today )); if ($today_record) { return $today_record->continuous_days; } // 如果今日未签到,检查昨日是否签到 $yesterday = date('Y-m-d', strtotime('-1 day')); $yesterday_record = $wpdb->get_row($wpdb->prepare( "SELECT continuous_days FROM $table_name WHERE user_id = %d AND signin_date = %s", $user_id, $yesterday )); if ($yesterday_record) { return 0; // 今日未签到,但昨日已签到,连续天数中断 } // 检查连续签到是否中断 return check_continuous_break($user_id); } /** * 检查连续签到是否中断 */ function check_continuous_break($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'signin_records'; // 获取最近一次签到记录 $latest_record = $wpdb->get_row($wpdb->prepare( "SELECT signin_date, continuous_days FROM $table_name WHERE user_id = %d ORDER BY signin_date DESC LIMIT 1", $user_id )); if (!$latest_record) { return 0; // 从未签到 } $last_signin_date = $latest_record->signin_date; $days_since_last = floor((time() - strtotime($last_signin_date)) / (60 * 60 * 24)); // 如果距离上次签到超过1天,则连续签到中断 if ($days_since_last > 1) { return 0; } return $latest_record->continuous_days; } ?> 3.3 积分管理系统 <?php /** * 更新用户积分 */ function update_user_points($user_id, $points_to_add) { global $wpdb; $table_name = $wpdb->prefix . 'user_points'; // 检查用户积分记录是否存在 $existing_record = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE user_id = %d", $user_id )); if ($existing_record) { // 更新现有记录 $new_total = $existing_record->total_points + $points_to_add; $new_available = $existing_record->available_points + $points_to_add; $wpdb->update( $table_name, array( 'total_points' => $new_total, 'available_points' => $new_available, 'last_updated' => current_time('mysql') ), array('user_id' => $user_id), array('%d', '%d', '%s'), array('%d') ); } else { // 创建新记录 $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'total_points' => $points_to_add, 'available_points' => $points_to_add, 'last_updated' => current_time('mysql') ), array('%d', '%d', '%d', '%s') ); } // 记录积分变动日志 log_points_change($user_id, $points_to_add, 'signin_reward', '签到奖励'); } /** * 积分兑换功能 */ function redeem_points($user_id, $item_id, $points_required) { global $wpdb; // 检查用户可用积分 $user_points = $wpdb->get_row($wpdb->prepare( "SELECT available_points FROM {$wpdb->prefix}user_points WHERE user_id = %d", $user_id )); if (!$user_points || $user_points->available_points < $points_required) { return array( 'success' => false, 'message' => '积分不足,无法兑换!' ); } // 获取兑换物品信息 $item_info = get_redemption_item($item_id); if (!$item_info) { return array( 'success' => false, 'message' => '兑换物品不存在!' ); } // 开始事务处理 $wpdb->query('START TRANSACTION'); try { // 扣除用户积分 $wpdb->query($wpdb->prepare( "UPDATE {$wpdb->prefix}user_points SET available_points = available_points - %d WHERE user_id = %d", $points_required, $user_id )); // 记录兑换记录 $wpdb->insert( $wpdb->prefix . 'points_redemption', array( 'user_id' => $user_id, 'redemption_date' => current_time('mysql'), 'points_used' => $points_required, 'item_name' => $item_info['name'], 'status' => 'pending' ), array('%d', '%s', '%d', '%s', '%s') ); $wpdb->query('COMMIT'); return array( 'success' => true, 'message' => '兑换成功,请等待管理员处理!' ); } catch (Exception $e) { $wpdb->query('ROLLBACK'); return array( 'success' => false, 'message' => '兑换失败:' . $e->getMessage() ); } } ?> 第四章:用户界面设计与实现 4.1 签到界面设计 <?php /** * 签到界面短代码 */ add_shortcode('signin_system', 'display_signin_system'); function display_signin_system() { if (!is_user_logged_in()) { return '<div class="signin-notice">请先登录以使用签到功能</div>'; } $user_id = get_current_user_id(); $user_info = get_userdata($user_id); $continuous_days = get_user_continuous_days($user_id); $today_signed = check_today_signin($user_id); ob_start(); ?> <div class="signin-container"> <div class="signin-header"> <h2>每日签到</h2> <p>连续签到可获得更多积分奖励</p> </div> <div class="user-stats"> <div class="stat-item"> <span class="stat-label">用户名</span> <span class="stat-value"><?php echo esc_html($user_info->display_name); ?></span> </div> <div class="stat-item"> <span class="stat-label">连续签到</span> <span class="stat-value"><?php echo $continuous_days; ?> 天</span> </div> <div class="stat-item"> <span class="stat-label">总积分</span> <span class="stat-value"><?php echo get_user_total_points($user_id); ?></span> </div> </div> <div class="signin-main"> <?php if ($today_signed): ?> <div class="signin-already"> <div class="signin-icon">✓</div> <h3>今日已签到</h3> <p>您已连续签到 <?php echo $continuous_days; ?> 天</p> <p>明日再来可获得更多积分!</p> </div> <?php else: ?> <div class="signin-available" id="signin-box"> <div class="signin-icon">📅</div> <h3>立即签到</h3> <p>今日签到可获得 <span class="points-preview"><?php echo calculate_signin_points($continuous_days + 1); ?></span> 积分</p> <button class="signin-button" id="signin-button">立即签到</button> <div class="signin-loading" id="signin-loading" style="display:none;"> 处理中... </div> </div> <?php endif; ?> </div> <div class="signin-calendar"> <h3>签到日历</h3> <?php display_signin_calendar($user_id); ?> </div> <div class="rewards-info"> <h3>连续签到奖励规则</h3> <ul class="rewards-list"> <li>第1-6天:每天10积分</li> <li>第7天:额外奖励30积分</li> <li>第30天:额外奖励100积分</li> <li>第100天:额外奖励500积分</li> <li>节假日:额外奖励20积分</li> </ul> </div> </div> <script> jQuery(document).ready(function($) { $('#signin-button').click(function() { var button = $(this); var loading = $('#signin-loading'); button.prop('disabled', true).text('签到中...'); loading.show(); $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'user_signin', nonce: '<?php echo wp_create_nonce('signin_nonce'); ?>' }, success: function(response) { if (response.success) { // 更新界面显示 $('#signin-box').html(` <div class="signin-success"> <div class="signin-icon">🎉</div> <h3>签到成功!</h3> <p>获得 <span class="points-earned">${response.data.points_earned}</span> 积分</p> <p>连续签到天数:${response.data.continuous_days} 天</p> <p>明日签到可获得更多积分!</p> </div> `); // 更新用户统计信息 setTimeout(function() { location.reload(); }, 2000); } else { alert(response.data); button.prop('disabled', false).text('立即签到'); loading.hide(); } }, error: function() { alert('签到失败,请稍后重试!'); button.prop('disabled', false).text('立即签到'); loading.hide(); } }); }); }); </script> <?php return ob_get_clean(); } /** 显示签到日历 */ function display_signin_calendar($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'signin_records'; $current_month = date('n'); $current_year = date('Y'); // 获取本月签到记录 $signin_records = $wpdb->get_results($wpdb->prepare( "SELECT DAY(signin_date) as day, points_earned FROM $table_name WHERE user_id = %d AND MONTH(signin_date) = %d AND YEAR(signin_date) = %d", $user_id, $current_month, $current_year )); // 创建签到记录映射 $signin_map = array(); foreach ($signin_records as $record) { $signin_map[$record->day] = $record->points_earned; } // 生成日历 $first_day = mktime(0, 0, 0, $current_month, 1, $current_year); $days_in_month = date('t', $first_day); $first_day_of_week = date('w', $first_day); ob_start(); ?> <div class="calendar"> <div class="calendar-header"> <span class="month-year"><?php echo date('Y年n月'); ?></span> </div> <div class="calendar-weekdays"> <div>日</div> <div>一</div> <div>二</div> <div>三</div> <div>四</div> <div>五</div> <div>六</div> </div> <div class="calendar-days"> <?php // 填充空白 for ($i = 0; $i < $first_day_of_week; $i++) { echo '<div class="empty"></div>'; } // 填充日期 for ($day = 1; $day <= $days_in_month; $day++) { $is_today = ($day == date('j')); $has_signed = isset($signin_map[$day]); $class = 'day'; if ($is_today) $class .= ' today'; if ($has_signed) $class .= ' signed'; echo '<div class="' . $class . '">'; echo '<span class="day-number">' . $day . '</span>'; if ($has_signed) { echo '<span class="signin-indicator" title="获得' . $signin_map[$day] . '积分">✓</span>'; } echo '</div>'; } ?> </div> </div> <?php return ob_get_clean(); }?> ### 4.2 用户积分中心 <?php/** 用户积分中心短代码 */ add_shortcode('points_center', 'display_points_center'); function display_points_center() { if (!is_user_logged_in()) { return '<div class="points-center-notice">请先登录以查看积分中心</div>'; } $user_id = get_current_user_id(); $points_data = get_user_points_data($user_id); $redemption_items = get_redemption_items(); $points_history = get_points_history($user_id, 10); ob_start(); ?> <div class="points-center-container"> <div class="points-overview"> <div class="points-card"> <h3>我的积分</h3> <div class="points-total"> <span class="points-number"><?php echo $points_data['available_points']; ?></span> <span class="points-label">可用积分</span> </div> <div class="points-details"> <div class="detail-item"> <span>总获得积分</span> <span><?php echo $points_data['total_points']; ?></span> </div> <div class="detail-item"> <span>已使用积分</span> <span><?php echo $points_data['total_points'] - $points_data['available_points']; ?></span> </div> </div> </div> <div class="points-actions"> <h3>快速操作</h3> <button class="btn-checkin" onclick="location.href='#signin-system'">每日签到</button> <button class="btn-history" id="view-history">查看积分记录</button> <button class="btn-redeem" id="show-redeem-modal">积分兑换</button> </div> </div> <div class="points-history-section"> <h3>最近积分记录</h3> <table class="points-history-table"> <thead> <tr> <th>日期</th> <th>类型</th> <th>积分变动</th> <th>说明</th> </tr> </thead> <tbody> <?php foreach ($points_history as $record): ?> <tr> <td><?php echo date('Y-m-d H:i', strtotime($record->change_time)); ?></td> <td><?php echo get_points_change_type_label($record->change_type); ?></td> <td class="<?php echo $record->points_change >= 0 ? 'points-increase' : 'points-decrease'; ?>"> <?php echo ($record->points_change >= 0 ? '+' : '') . $record->points_change; ?> </td> <td><?php echo esc_html($record->description); ?></td> </tr> <?php endforeach; ?> </tbody> </table> </div> <div class="redemption-section"> <h3>积分兑换商城</h3> <div class="redemption-items"> <?php foreach ($redemption_items as $item): ?> <div class="redemption-item"> <div class="item-image"> <img src="<?php echo esc_url($item['image']); ?>" alt="<?php echo esc_attr($item['name']); ?>"> </div> <div class="item-info"> <h4><?php echo esc_html($item['name']); ?></h4> <p class="item-description"><?php echo esc_html($item['description']); ?></p> <div class="item-points"> <span class="points-required"><?php echo $item['points_required']; ?> 积分</span> <?php if ($points_data['available_points'] >= $item['points_required']): ?> <button class="btn-redeem-item" data-item-id="<?php echo $item['id']; ?>" data-item-name="<?php echo esc_attr($item['name']); ?>" data-points-required="<?php echo $item['points_required']; ?>"> 立即兑换 </button> <?php else: ?> <button class="btn-redeem-item disabled" disabled>积分不足</button> <?php endif; ?> </div> </div> </div> <?php endforeach; ?> </div> </div> </div> <!-- 兑换确认模态框 --> <div id="redeem-modal" class="modal"> <div class="modal-content"> <span class="close-modal">&times;</span> <h3>确认兑换</h3> <div id="redeem-item-info"></div> <div class="modal-actions"> <button id="confirm-redeem" class="btn-confirm">确认兑换</button> <button id="cancel-redeem" class="btn-cancel">取消</button> </div> </div> </div> <script> jQuery(document).ready(function($) { // 兑换按钮点击事件 $('.btn-redeem-item').click(function() { var itemId = $(this).data('item-id'); var itemName = $(this).data('item-name'); var pointsRequired = $(this).data('points-required'); $('#redeem-item-info').html(` <p>您要兑换:<strong>${itemName}</strong></p> <p>需要积分:<strong>${pointsRequired}</strong></p> <p>兑换后积分将立即扣除,兑换物品将在3个工作日内处理。</p> `); $('#redeem-modal').data('item-id', itemId); $('#redeem-modal').data('points-required', pointsRequired); $('#redeem-modal').show(); }); // 确认兑换 $('#confirm-redeem').click(function() { var itemId = $('#redeem-modal').data('item-id'); var pointsRequired = $('#redeem-modal').data('points-required'); $.ajax({ url: '<?php echo admin_url('admin-ajax.php'); ?>', type: 'POST', data: { action: 'redeem_points', item_id: itemId, points_required: pointsRequired, nonce: '<?php echo wp_create_nonce('redeem_nonce'); ?>' }, success: function(response) { if (response.success) { alert('兑换成功!'); location.reload(); } else { alert('兑换失败:' + response.data); } }, error: function() { alert('兑换失败,请稍后重试!'); } }); }); // 关闭模态框 $('.close-modal, #cancel-redeem').click(function() { $('#redeem-modal').hide(); }); }); </script> <?php return ob_get_clean(); }?> --- ## 第五章:后台管理功能 ### 5.1 管理菜单与界面 <?php/** 添加管理菜单 */ add_action('admin_menu', 'register_signin_admin_menu'); function register_signin_admin_menu() { add_menu_page( '签到管理系统', '签到管理', 'manage_options', 'signin-management', 'display_signin_admin_page', 'dashicons-calendar-alt', 30 ); add_submenu_page( 'signin-management', '签到记录', '签到记录', 'manage_options', 'signin-records', 'display_signin_records_page' ); add_submenu_page( 'signin-management', '积分规则', '积分规则', 'manage_options', 'points-rules', 'display_points_rules_page' ); add_submenu_page( 'signin-management', '兑换管理', '兑换管理', 'manage_options', 'redemption-management', 'display_redemption_management_page' ); add_submenu_page( 'signin-management', '数据统计', '数据统计', 'manage_options', 'signin-statistics', 'display_signin_statistics_page' ); } /** 显示签到记录管理页面 */ function display_signin_records_page() { global $wpdb; $table_name = $wpdb->prefix . 'signin_records'; $users_table = $wpdb->users; // 处理搜索和筛选 $search = isset($_GET['s']) ? sanitize_text_field($_GET['s']) : ''; $date_from = isset($_GET['date_from']) ? sanitize_text_field($_GET['date_from']) : ''; $date_to = isset($_GET['date_to']) ? sanitize_text_field($_GET['date_to']) : ''; // 构建查询条件 $where = array('1=1'); $query_params = array(); if (!empty($search)) { $where[] = "(u.user_login LIKE %s OR u.display_name LIKE %s)"; $query_params[] = '%' . $wpdb->esc_like($search) . '%'; $query_params[] = '%' . $wpdb->esc_like($search) . '%'; } if (!empty($date_from)) { $where[] = "s.signin_date >= %s"; $query_params[] = $date_from; } if (!empty($date_to)) { $where[] = "s.signin_date <= %s"; $query_params[] = $date_to; } $where_clause = implode(' AND ', $where); // 获取总记录数 $total_query = "SELECT COUNT(*) FROM $table_name s INNER JOIN $users_table u ON s.user_id = u.ID WHERE $where_clause"; if (!empty($query_params)) { $total_query = $wpdb->prepare($total_query, $query_params); } $total_items = $wpdb->get_var($total_query); // 分页设置 $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $offset = ($current_page - 1) * $per_page; // 获取记录数据 $query = "SELECT s.*, u.user_login, u.display_name FROM $table_name s INNER JOIN $users_table u ON s.user_id = u.ID WHERE $where_clause ORDER BY s.signin_date DESC, s.signin_time DESC LIMIT %d OFFSET %d"; $query_params[] = $per_page; $query_params[] = $offset; $records = $wpdb->get_results($wpdb->prepare($query, $query_params)); ob_start(); ?> <div class="wrap"> <h1 class="wp-heading-inline">签到记录管理</h1> <form method="get" action="" class="search-form"> <input type="hidden" name="page" value="signin-records"> <div class="tablenav top"> <div class="alignleft actions"> <input type="text" name="s" value="<?php echo esc_attr($search); ?>" placeholder="搜索用户名或显示名称"> <label>从:<input type="date" name="date_from" value="<?php echo esc_attr($date_from); ?>"></label> <label>到:<input type="date" name="date_to" value="<?php echo esc_attr($date_to); ?>"></label> <input type="submit" class="button" value="筛选"> <?php if (!empty($search) || !empty($date_from) || !empty($date_to)): ?> <a href="?page=signin-records" class="button">清除筛选</a> <?php endif; ?> </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' => '&laquo;', 'next_text' => '&raquo;', 'total' => $total_pages, 'current' => $current_page )); ?> </div> </div> </form> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>用户</th> <th>签到日期</th> <th>签到时间</th> <th>连续天数</th> <th>获得积分</th> <th>操作</th> </tr> </thead> <tbody> <?php if (empty($records)): ?> <tr> <td colspan="6" style="text-align: center;">暂无签到记录</td> </tr> <?php else: ?> <?php foreach ($records as $record): ?> <tr> <td> <strong><?php echo esc_html($record->display_name); ?></strong><br> <small><?php echo esc_html($record->user_login); ?></small> </td> <td><?php echo esc_html($record->signin_date); ?></td> <td><?php echo esc_html($record->signin_time); ?></td> <td><?php echo esc_html($record->continuous_days); ?></td> <td><?php echo esc_html($record->points_earned); ?></td> <td> <button class="button button-small" onclick="deleteSigninRecord(<?php echo $record->id; ?

发表评论

一步步实现,为WordPress打造内嵌的在线流程图绘制与原型设计工具

一步步实现:为WordPress打造内嵌的在线流程图绘制与原型设计工具 引言:为什么WordPress需要内置流程图与原型设计工具? 在当今数字化时代,可视化沟通已成为网站开发、项目管理和内容创作中不可或缺的一环。无论是网站架构规划、用户流程设计,还是产品原型展示,流程图和原型设计工具都能显著提高工作效率和沟通效果。 然而,大多数WordPress用户目前面临一个共同困境:当需要在文章中插入流程图或原型设计时,他们不得不依赖外部工具(如Lucidchart、Figma或Draw.io)创建图表,然后以图片形式导入WordPress。这种方法存在明显缺陷:无法直接编辑、图片质量损失、缺乏交互性,并且破坏了内容创作的一体化体验。 本文将通过详细的代码实现步骤,展示如何为WordPress打造一个完全内嵌的在线流程图绘制与原型设计工具,让用户无需离开WordPress后台即可创建、编辑和发布专业的可视化图表。 第一部分:项目规划与技术选型 1.1 功能需求分析 在开始编码之前,我们需要明确工具的核心功能: 基本绘图功能:支持常见流程图元素(矩形、圆形、菱形、箭头等) 原型设计组件:按钮、输入框、导航栏等UI元素库 实时协作:支持多用户同时编辑(可选高级功能) 导出与分享:支持PNG、SVG、JSON等多种格式导出 WordPress集成:与文章编辑器无缝对接,支持短代码嵌入 版本控制:保存编辑历史,支持版本回退 响应式设计:确保在不同设备上都能良好工作 1.2 技术架构设计 考虑到开发效率和功能完整性,我们采用混合技术方案: 前端绘图库:使用开源的Draw.io(mxGraph)核心库,这是一个功能强大且成熟的图形绘制库 WordPress集成:通过自定义插件方式集成到WordPress 数据存储:使用WordPress自定义数据表结合文章元数据 后端API:REST API处理图表保存、加载和用户权限管理 1.3 开发环境准备 在开始开发前,确保你的环境满足以下要求: WordPress 5.0+(支持Gutenberg编辑器) PHP 7.4+ MySQL 5.6+ 基本的Web开发知识(HTML、CSS、JavaScript、PHP) 第二部分:创建WordPress插件基础框架 2.1 初始化插件文件结构 首先,在WordPress的wp-content/plugins/目录下创建插件文件夹wp-flowchart-designer,并建立以下文件结构: wp-flowchart-designer/ ├── wp-flowchart-designer.php # 主插件文件 ├── includes/ │ ├── class-database.php # 数据库处理类 │ ├── class-shortcode.php # 短代码处理器 │ ├── class-rest-api.php # REST API处理器 │ └── class-admin.php # 后台管理类 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 后台样式 │ └── js/ │ └── admin-script.js # 后台脚本 ├── public/ │ ├── css/ │ │ └── public-style.css # 前台样式 │ ├── js/ │ │ ├── public-script.js # 前台脚本 │ │ └── mxgraph/ # mxGraph库文件 │ └── partials/ # 公共模板部分 ├── assets/ # 静态资源 └── uninstall.php # 插件卸载脚本 2.2 编写主插件文件 创建主插件文件wp-flowchart-designer.php: <?php /** * Plugin Name: WordPress流程图与原型设计工具 * Plugin URI: https://yourwebsite.com/wp-flowchart-designer * Description: 为WordPress添加内嵌的在线流程图绘制与原型设计功能 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later * Text Domain: wp-flowchart-designer */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('WPFD_VERSION', '1.0.0'); define('WPFD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('WPFD_PLUGIN_URL', plugin_dir_url(__FILE__)); define('WPFD_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class_name) { $prefix = 'WPFD_'; $base_dir = WPFD_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class_name, $len) !== 0) { return; } $relative_class = substr($class_name, $len); $file = $base_dir . 'class-' . strtolower(str_replace('_', '-', $relative_class)) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 function wpfd_init() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('WordPress流程图与原型设计工具需要WordPress 5.0或更高版本。', 'wp-flowchart-designer'); echo '</p></div>'; }); return; } // 初始化各个组件 WPFD_Database::init(); WPFD_Shortcode::init(); WPFD_REST_API::init(); if (is_admin()) { WPFD_Admin::init(); } } add_action('plugins_loaded', 'wpfd_init'); // 激活插件时创建数据库表 register_activation_hook(__FILE__, ['WPFD_Database', 'create_tables']); // 卸载插件时清理数据 register_uninstall_hook(__FILE__, ['WPFD_Database', 'drop_tables']); 第三部分:实现数据库层 3.1 创建数据库表 在includes/class-database.php中,我们创建存储图表数据的数据表: <?php class WPFD_Database { private static $table_name; public static function init() { global $wpdb; self::$table_name = $wpdb->prefix . 'wpfd_diagrams'; } public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS " . self::$table_name . " ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, post_id bigint(20) UNSIGNED DEFAULT NULL, user_id bigint(20) UNSIGNED NOT NULL, title varchar(255) NOT NULL, diagram_data longtext NOT NULL, diagram_type varchar(50) DEFAULT 'flowchart', settings text, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY post_id (post_id), KEY user_id (user_id), KEY diagram_type (diagram_type) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 添加版本选项,便于后续升级 add_option('wpfd_db_version', '1.0'); } public static function drop_tables() { global $wpdb; $sql = "DROP TABLE IF EXISTS " . self::$table_name; $wpdb->query($sql); delete_option('wpfd_db_version'); } public static function save_diagram($data) { global $wpdb; $defaults = [ 'post_id' => null, 'user_id' => get_current_user_id(), 'title' => '未命名图表', 'diagram_data' => '', 'diagram_type' => 'flowchart', 'settings' => '{}' ]; $data = wp_parse_args($data, $defaults); if (isset($data['id']) && $data['id'] > 0) { // 更新现有图表 $wpdb->update( self::$table_name, [ 'title' => sanitize_text_field($data['title']), 'diagram_data' => wp_json_encode($data['diagram_data']), 'diagram_type' => sanitize_text_field($data['diagram_type']), 'settings' => wp_json_encode($data['settings']), 'updated_at' => current_time('mysql') ], ['id' => $data['id']], ['%s', '%s', '%s', '%s', '%s'], ['%d'] ); return $data['id']; } else { // 插入新图表 $wpdb->insert( self::$table_name, [ 'post_id' => $data['post_id'], 'user_id' => $data['user_id'], 'title' => sanitize_text_field($data['title']), 'diagram_data' => wp_json_encode($data['diagram_data']), 'diagram_type' => sanitize_text_field($data['diagram_type']), 'settings' => wp_json_encode($data['settings']), 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ], ['%d', '%d', '%s', '%s', '%s', '%s', '%s', '%s'] ); return $wpdb->insert_id; } } public static function get_diagram($id) { global $wpdb; $result = $wpdb->get_row( $wpdb->prepare( "SELECT * FROM " . self::$table_name . " WHERE id = %d", $id ), ARRAY_A ); if ($result) { $result['diagram_data'] = json_decode($result['diagram_data'], true); $result['settings'] = json_decode($result['settings'], true); } return $result; } public static function get_user_diagrams($user_id = null, $limit = 20, $offset = 0) { global $wpdb; if (!$user_id) { $user_id = get_current_user_id(); } $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM " . self::$table_name . " WHERE user_id = %d ORDER BY updated_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset ), ARRAY_A ); foreach ($results as &$result) { $result['diagram_data'] = json_decode($result['diagram_data'], true); $result['settings'] = json_decode($result['settings'], true); } return $results; } public static function delete_diagram($id) { global $wpdb; return $wpdb->delete( self::$table_name, ['id' => $id], ['%d'] ); } } 第四部分:集成Draw.io编辑器 4.1 引入mxGraph库 由于Draw.io基于mxGraph库,我们需要将其集成到插件中。可以从GitHub获取mxGraph库: 访问 https://github.com/jgraph/mxgraph 下载最新版本 将javascript目录复制到public/js/mxgraph/ 4.2 创建编辑器界面 在admin/js/admin-script.js中,我们创建编辑器初始化代码: (function($) { 'use strict'; // 全局编辑器实例 var wpfdEditor = null; // 初始化流程图编辑器 function initFlowchartEditor(containerId, initialData) { // 检查mxGraph库是否已加载 if (typeof mxClient === 'undefined') { console.error('mxGraph库未加载'); return; } // 创建编辑器容器 var container = document.getElementById(containerId); if (!container) { console.error('找不到容器: ' + containerId); return; } // 禁用右键菜单 mxEvent.disableContextMenu(container); // 创建图形模型 var model = new mxGraphModel(); var graph = new mxGraph(container, model); // 配置图形 graph.setConnectable(true); graph.setMultigraph(false); graph.setAllowDanglingEdges(false); graph.setDropEnabled(true); graph.setPanning(true); graph.setTooltips(true); // 启用缩放 new mxKeyHandler(graph); new mxRubberband(graph); // 设置样式 var style = graph.getStylesheet().getDefaultVertexStyle(); style[mxConstants.STYLE_FONTFAMILY] = 'Helvetica, Arial, sans-serif'; style[mxConstants.STYLE_FONTSIZE] = '12'; style[mxConstants.STYLE_STROKECOLOR] = '#2D3748'; style[mxConstants.STYLE_FILLCOLOR] = '#EDF2F7'; // 加载初始数据 if (initialData && initialData.xml) { try { var doc = mxUtils.parseXml(initialData.xml); var codec = new mxCodec(doc); codec.decode(doc.documentElement, graph.getModel()); } catch (e) { console.error('解析图表数据失败:', e); } } // 保存编辑器实例 wpfdEditor = { graph: graph, model: model, container: container }; return wpfdEditor; } // 获取图表数据 function getDiagramData() { if (!wpfdEditor) { return null; } var encoder = new mxCodec(); var node = encoder.encode(wpfdEditor.model); return { xml: mxUtils.getXml(node), bounds: wpfdEditor.graph.getGraphBounds(), cells: wpfdEditor.model.getDescendants(wpfdEditor.model.getRoot()) }; } // 导出为图片 function exportAsImage(format, bgColor) { if (!wpfdEditor) { return null; } var bounds = wpfdEditor.graph.getGraphBounds(); var scale = wpfdEditor.graph.view.scale; // 创建临时canvas var canvas = document.createElement('canvas'); canvas.width = bounds.width * scale; canvas.height = bounds.height * scale; var imgExport = new mxImageExport(); var ctx = canvas.getContext('2d'); // 设置背景色 if (bgColor) { ctx.fillStyle = bgColor; ctx.fillRect(0, 0, canvas.width, canvas.height); } // 导出图形 imgExport.drawState(wpfdEditor.graph.getView().getState(wpfdEditor.model.getRoot()), ctx); return canvas.toDataURL('image/' + format); } // 添加形状到工具栏 function addShapeToToolbar(toolbarId, shapeConfig) { var toolbar = document.getElementById(toolbarId); if (!toolbar) return; var button = document.createElement('button'); button.type = 'button'; button.className = 'wpfd-toolbar-btn'; button.innerHTML = shapeConfig.icon || shapeConfig.label; button.title = shapeConfig.label; button.addEventListener('click', function() { if (!wpfdEditor) return; wpfdEditor.graph.stopEditing(); var parent = wpfdEditor.graph.getDefaultParent(); wpfdEditor.model.beginUpdate(); try { var vertex = wpfdEditor.graph.insertVertex( parent, null, shapeConfig.label, 20, 20, shapeConfig.width || 120, shapeConfig.height || 60, shapeConfig.style || '' ); // 将新形状置于视图中心 var geo = wpfdEditor.model.getGeometry(vertex); var bounds = wpfdEditor.graph.getGraphBounds(); geo.x = Math.max(20, (bounds.width - geo.width) / 2); geo.y = Math.max(20, (bounds.height - geo.height) / 2); wpfdEditor.graph.setSelectionCell(vertex); } finally { wpfdEditor.model.endUpdate(); } }); toolbar.appendChild(button); } // 初始化工具栏 function initToolbar(toolbarId) { // 预定义形状 var shapes = [ { label: '开始/结束', style: 'shape=ellipse', width: 100, height: 60 }, { label: '过程', style: '', width: 120, height: 60 }, { label: '判断', style: 'shape=rhombus', width: 100, height: 80 }, { label: '数据', style: 'shape=parallelogram', width: 120, height: 60 }, { label: '文档', style: 'shape=document', width: 100, height: 80 }, { label: '子流程', style: 'shape=process;perimeter=rectanglePerimeter', width: 140, height: 80 } ]; shapes.forEach(function(shape) { addShapeToToolbar(toolbarId, shape); }); } // WordPress集成 $(document).ready(function() { // 初始化编辑器 if ($('#wpfd-editor-container').length) { var initialData = window.wpfdInitialData || {}; wpfdEditor = initFlowchartEditor('wpfd-editor-container', initialData); ('#wpfd-toolbar').length) { initToolbar('wpfd-toolbar'); } } // 保存图表 $('#wpfd-save-diagram').on('click', function() { var diagramData = getDiagramData(); var title = $('#wpfd-diagram-title').val() || '未命名图表'; if (!diagramData) { alert('无法获取图表数据'); return; } // 显示保存中状态 var $button = $(this); var originalText = $button.text(); $button.text('保存中...').prop('disabled', true); // 发送保存请求 $.ajax({ url: wpfd_ajax.ajax_url, type: 'POST', data: { action: 'wpfd_save_diagram', nonce: wpfd_ajax.nonce, title: title, diagram_data: JSON.stringify(diagramData), diagram_type: 'flowchart' }, success: function(response) { if (response.success) { alert('图表保存成功!'); if (response.data.redirect) { window.location.href = response.data.redirect; } } else { alert('保存失败: ' + response.data.message); } }, error: function() { alert('保存请求失败,请检查网络连接'); }, complete: function() { $button.text(originalText).prop('disabled', false); } }); }); // 导出功能 $('.wpfd-export-btn').on('click', function() { var format = $(this).data('format'); var imageData = exportAsImage(format, '#ffffff'); if (imageData) { // 创建下载链接 var link = document.createElement('a'); link.download = 'diagram.' + format; link.href = imageData; document.body.appendChild(link); link.click(); document.body.removeChild(link); } }); // 插入到文章 $('#wpfd-insert-to-post').on('click', function() { var diagramData = getDiagramData(); if (!diagramData) { alert('无法获取图表数据'); return; } // 生成短代码 var shortcode = '[wpfd_diagram id="' + wpfd_ajax.diagram_id + '"]'; // 发送到编辑器 if (typeof wp !== 'undefined' && wp.media && wp.media.editor) { wp.media.editor.insert(shortcode); } else { // 备用方案:复制到剪贴板 var tempInput = document.createElement('input'); tempInput.value = shortcode; document.body.appendChild(tempInput); tempInput.select(); document.execCommand('copy'); document.body.removeChild(tempInput); alert('短代码已复制到剪贴板'); } }); }); // 暴露公共API window.WPFD_Editor = { init: initFlowchartEditor, getData: getDiagramData, exportImage: exportAsImage, addShape: addShapeToToolbar }; })(jQuery); ### 4.3 创建编辑器CSS样式 在`admin/css/admin-style.css`中添加编辑器样式: / 流程图编辑器主容器 /.wpfd-editor-wrapper { display: flex; flex-direction: column; height: 800px; border: 1px solid #ccd0d4; border-radius: 4px; overflow: hidden; background: #f8f9fa; } / 编辑器工具栏 /.wpfd-editor-toolbar { background: #fff; border-bottom: 1px solid #ccd0d4; padding: 10px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; min-height: 60px; } .wpfd-toolbar-group { display: flex; align-items: center; gap: 4px; padding: 0 10px; border-right: 1px solid #e0e0e0; } .wpfd-toolbar-group:last-child { border-right: none; } .wpfd-toolbar-btn { background: #f0f0f0; border: 1px solid #ddd; border-radius: 3px; padding: 6px 12px; cursor: pointer; font-size: 13px; color: #333; transition: all 0.2s; display: flex; align-items: center; gap: 5px; } .wpfd-toolbar-btn:hover { background: #e0e0e0; border-color: #ccc; } .wpfd-toolbar-btn.active { background: #007cba; color: white; border-color: #007cba; } .wpfd-toolbar-btn i { font-size: 16px; } / 编辑器主区域 /.wpfd-editor-main { display: flex; flex: 1; overflow: hidden; } / 左侧形状面板 /.wpfd-shapes-panel { width: 200px; background: #fff; border-right: 1px solid #ccd0d4; padding: 15px; overflow-y: auto; } .wpfd-shapes-category { margin-bottom: 20px; } .wpfd-shapes-category h4 { margin: 0 0 10px 0; font-size: 13px; color: #666; text-transform: uppercase; font-weight: 600; } .wpfd-shapes-list { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; } .wpfd-shape-item { background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 4px; padding: 10px; cursor: move; text-align: center; font-size: 12px; transition: all 0.2s; } .wpfd-shape-item:hover { background: #e9ecef; border-color: #007cba; } / 画布区域 /.wpfd-canvas-container { flex: 1; position: relative; overflow: hidden; background: #fff; } wpfd-editor-container { width: 100%; height: 100%; min-height: 600px; } / 右侧属性面板 /.wpfd-properties-panel { width: 300px; background: #fff; border-left: 1px solid #ccd0d4; padding: 15px; overflow-y: auto; } .wpfd-property-group { margin-bottom: 20px; padding-bottom: 15px; border-bottom: 1px solid #eee; } .wpfd-property-group:last-child { border-bottom: none; margin-bottom: 0; } .wpfd-property-group h4 { margin: 0 0 12px 0; font-size: 14px; color: #333; font-weight: 600; } .wpfd-property-item { margin-bottom: 10px; } .wpfd-property-item label { display: block; margin-bottom: 5px; font-size: 13px; color: #555; } .wpfd-property-item input[type="text"],.wpfd-property-item input[type="number"],.wpfd-property-item select { width: 100%; padding: 6px 8px; border: 1px solid #ddd; border-radius: 3px; font-size: 13px; } .wpfd-property-item input[type="color"] { width: 100%; height: 35px; padding: 2px; border: 1px solid #ddd; border-radius: 3px; } / 编辑器底部状态栏 /.wpfd-editor-statusbar { background: #fff; border-top: 1px solid #ccd0d4; padding: 8px 15px; font-size: 12px; color: #666; display: flex; justify-content: space-between; align-items: center; } / 响应式设计 /@media (max-width: 1200px) { .wpfd-editor-wrapper { height: 600px; } .wpfd-shapes-panel { width: 180px; } .wpfd-properties-panel { width: 250px; } } @media (max-width: 768px) { .wpfd-editor-main { flex-direction: column; } .wpfd-shapes-panel { width: 100%; height: 150px; border-right: none; border-bottom: 1px solid #ccd0d4; } .wpfd-properties-panel { width: 100%; height: 200px; border-left: none; border-top: 1px solid #ccd0d4; } .wpfd-shapes-list { grid-template-columns: repeat(4, 1fr); } } / 图标字体 /@font-face { font-family: 'wpfd-icons'; src: url('../fonts/wpfd-icons.woff2') format('woff2'), url('../fonts/wpfd-icons.woff') format('woff'); font-weight: normal; font-style: normal; } .wpfd-icon { font-family: 'wpfd-icons'; speak: never; font-style: normal; font-weight: normal; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } .wpfd-icon-rectangle:before { content: "e900"; }.wpfd-icon-circle:before { content: "e901"; }.wpfd-icon-diamond:before { content: "e902"; }.wpfd-icon-arrow:before { content: "e903"; }.wpfd-icon-text:before { content: "e904"; }.wpfd-icon-line:before { content: "e905"; }.wpfd-icon-save:before { content: "e906"; }.wpfd-icon-export:before { content: "e907"; }.wpfd-icon-undo:before { content: "e908"; }.wpfd-icon-redo:before { content: "e909"; }.wpfd-icon-zoom-in:before { content: "e90a"; }.wpfd-icon-zoom-out:before { content: "e90b"; } ## 第五部分:实现REST API接口 ### 5.1 创建REST API处理器 在`includes/class-rest-api.php`中实现API接口: <?phpclass WPFD_REST_API { public static function init() { add_action('rest_api_init', [__CLASS__, 'register_routes']); } public static function register_routes() { // 图表CRUD接口 register_rest_route('wpfd/v1', '/diagrams', [ [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_diagrams'], 'permission_callback' => [__CLASS__, 'check_permission'], 'args' => [ 'per_page' => [ 'default' => 20, 'validate_callback' => function($param) { return is_numeric($param) && $param > 0 && $param <= 100; } ], 'page' => [ 'default' => 1, 'validate_callback' => function($param) { return is_numeric($param) && $param > 0; } ] ] ], [ 'methods' => 'POST', 'callback' => [__CLASS__, 'create_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'] ] ]); register_rest_route('wpfd/v1', '/diagrams/(?P<id>d+)', [ [ 'methods' => 'GET', 'callback' => [__CLASS__, 'get_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'] ], [ 'methods' => 'PUT', 'callback' => [__CLASS__, 'update_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'] ], [ 'methods' => 'DELETE', 'callback' => [__CLASS__, 'delete_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'] ] ]); // 导出接口 register_rest_route('wpfd/v1', '/export/(?P<id>d+)', [ [ 'methods' => 'GET', 'callback' => [__CLASS__, 'export_diagram'], 'permission_callback' => [__CLASS__, 'check_permission'], 'args' => [ 'format' => [ 'default' => 'png', 'validate_callback' => function($param) { return in_array($param, ['png', 'jpg', 'svg', 'pdf']); } ] ] ] ]); // 搜索接口 register_rest_route('wpfd/v1', '/search', [ [ 'methods' => 'GET', 'callback' => [__CLASS__, 'search_diagrams'], 'permission_callback' => [__CLASS__, 'check_permission'] ] ]); } public static function check_permission($request) { // 检查用户是否登录 if (!is_user_logged_in()) { return new WP_Error('rest_forbidden', __('请先登录'), ['status' => 401]); } // 检查用户权限 $method = $request->get_method(); $user_id = get_current_user_id(); // 对于GET请求,允许所有登录用户访问 if ($method === 'GET') { return true; } // 对于其他请求,需要编辑权限 if (!current_user_can('edit_posts')) { return new WP_Error('rest_forbidden', __('权限不足'), ['status' => 403]); } return true; } public static function get_diagrams($request) { $params = $request->get_params(); $per_page = intval($params['per_page']); $page = intval($params['page']); $offset = ($page - 1) * $per_page; global $wpdb; $table_name = $wpdb->prefix . 'wpfd_diagrams'; $user_id = get_current_user_id(); // 获取图表总数 $total = $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table_name} WHERE user_id = %d", $user_id ) ); // 获取图表列表 $results = $wpdb->get_results( $wpdb->prepare( "SELECT id, title, diagram_type, created_at, updated_at FROM {$table_name} WHERE user_id = %d ORDER BY updated_at DESC LIMIT %d OFFSET %d", $user_id, $per_page, $offset ), ARRAY_A ); // 准备响应数据 $diagrams = []; foreach ($results as $row) { $diagrams[] = [ 'id' => intval($row['id']), 'title' => $row['title'], 'type' => $row['diagram_type'], 'created_at' => $row['created_at'], 'updated_at' => $row['updated_at'], 'edit_url' => admin_url('admin.php?page=wpfd-edit&id=' . $row['id']) ]; } $response = new WP_REST_Response($diagrams); $response->header('X-WP-Total', $total); $response->header('X-WP-TotalPages', ceil($total / $per_page)); return $response; } public static function get_diagram($request) { $id = intval($request['id']); $diagram = WPFD_Database::get_diagram($id); if (!$diagram) { return new WP_Error('not_found', __('图表不存在'), ['status' => 404]); } // 检查权限:只能访问自己的图表 if ($diagram['user_id'] != get_current_user_id() && !current_user_can('manage_options')) { return new WP_Error('forbidden', __('无权访问此图表'), ['status' => 403]); } return rest_ensure_response($diagram); } public static function create_diagram($request) { $data = $request->get_json_params(); // 验证数据 if (empty($data['title'])) { return new WP_Error('invalid_data', __('标题不能为空'), ['status' => 400]); } if (empty($data['diagram_data'])) { return new WP_Error('invalid_data', __('图表数据不能为空'), ['status' => 400]); } // 准备保存数据 $save_data = [ 'title' => sanitize_text_field($data['title']), 'diagram_data' => $data['diagram_data'], 'diagram_type' => isset($data['diagram_type']) ? sanitize_text_field($data['diagram_type']) : 'flowchart', 'settings' => isset($data['settings']) ? $data['settings'] : [] ]; // 如果有post_id,关联到文章 if (isset($data['post_id']) && $data['post_id

发表评论

WordPress开发教程,集成网站自动化社交媒体舆情监控与警报

WordPress开发教程:集成网站自动化社交媒体舆情监控与警报,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:WordPress的无限可能 在当今数字化时代,网站已不仅仅是信息展示的平台,更是企业与用户互动、品牌传播和业务拓展的核心阵地。作为全球最受欢迎的内容管理系统,WordPress以其开源特性、灵活的可扩展性和庞大的开发者社区,占据了互联网超过43%的网站市场份额。然而,许多WordPress用户仅停留在使用现成主题和插件的层面,未能充分挖掘其深层潜力。 本教程将深入探讨如何通过WordPress代码二次开发,将您的网站从一个被动的内容发布平台,转变为一个集成了自动化社交媒体舆情监控与警报系统的智能工具。我们将逐步引导您实现这一复杂功能,同时在这个过程中,掌握如何通过自定义开发为WordPress添加各种实用的小工具功能,从而大幅提升网站的价值和效率。 第一部分:WordPress开发基础与环境配置 1.1 WordPress开发环境搭建 在开始任何WordPress开发项目之前,建立一个合适的开发环境至关重要。我们推荐使用本地开发环境,如Local by Flywheel、XAMPP或MAMP,这些工具可以快速在本地计算机上搭建WordPress运行所需的PHP、MySQL和Web服务器环境。 对于本教程涉及的开发工作,您需要确保环境满足以下要求: PHP 7.4或更高版本(建议8.0+) MySQL 5.6或更高版本或MariaDB 10.1+ WordPress 5.8或更高版本 代码编辑器(如VS Code、PHPStorm等) Git版本控制系统 1.2 子主题创建与最佳实践 为了避免直接修改主题文件导致更新时丢失自定义代码,我们始终建议使用子主题进行开发。创建子主题只需在wp-content/themes目录下新建一个文件夹,并包含以下基本文件: style.css - 子主题样式表,必须包含特定的头部信息: /* Theme Name: 我的自定义子主题 Template: parent-theme-folder-name Version: 1.0.0 */ functions.php - 子主题功能文件,用于添加自定义代码: <?php // 子主题functions.php add_action('wp_enqueue_scripts', 'my_child_theme_scripts'); function my_child_theme_scripts() { // 加载父主题样式 wp_enqueue_style('parent-style', get_template_directory_uri() . '/style.css'); // 加载子主题样式 wp_enqueue_style('child-style', get_stylesheet_directory_uri() . '/style.css', array('parent-style')); } 1.3 WordPress钩子(Hooks)系统理解 WordPress的钩子系统是扩展其功能的核心机制,分为两种类型: 动作(Actions):在特定时刻执行自定义代码 过滤器(Filters):修改数据后再返回 理解并熟练使用钩子是高级WordPress开发的基础。例如,我们将在舆情监控系统中使用wp_cron钩子来定期执行监控任务。 第二部分:社交媒体API集成基础 2.1 社交媒体API概览与申请 要实现社交媒体舆情监控,首先需要获取各大社交平台的API访问权限。以下是主要平台的API申请要点: Twitter API(现为X平台): 访问developer.twitter.com申请开发者账户 创建项目和应用获取API密钥和访问令牌 注意:Twitter API v2有严格的访问限制和费用结构 Facebook Graph API: 通过Facebook开发者平台创建应用 需要应用审核才能访问某些接口 获取长期访问令牌以实现自动化 Instagram Basic Display API: 只能访问用户自己的内容 需要用户授权流程 适用于监控品牌自己的Instagram账户 YouTube Data API: 通过Google Cloud Console启用 每日有免费配额限制 可以搜索视频和评论 Reddit API: 相对宽松的访问政策 需要遵守API使用规则 可以访问公开的帖子和评论 2.2 WordPress中安全存储API密钥 绝对不要在代码中硬编码API密钥。WordPress提供了安全存储敏感数据的方法: // 在主题或插件中安全存储和获取API密钥 function save_social_api_keys() { if (isset($_POST['twitter_api_key'])) { update_option('twitter_api_key', sanitize_text_field($_POST['twitter_api_key'])); } } function get_twitter_api_key() { return get_option('twitter_api_key', ''); } // 或者使用更安全的wp-config.php常量定义 // define('TWITTER_API_KEY', 'your_actual_key_here'); 2.3 API请求处理与错误处理 在WordPress中发起API请求时,应使用内置的HTTP函数: function make_api_request($url, $args = array()) { $defaults = array( 'timeout' => 30, 'headers' => array( 'Authorization' => 'Bearer ' . get_option('twitter_bearer_token') ) ); $args = wp_parse_args($args, $defaults); $response = wp_remote_get($url, $args); if (is_wp_error($response)) { // 记录错误日志 error_log('API请求失败: ' . $response->get_error_message()); return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { error_log('JSON解析错误: ' . json_last_error_msg()); return false; } return $data; } 第三部分:构建舆情监控系统核心 3.1 数据库设计与数据模型 我们需要创建自定义数据库表来存储监控到的社交媒体内容: function create_social_monitoring_tables() { global $wpdb; $table_name = $wpdb->prefix . 'social_mentions'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, platform varchar(50) NOT NULL, post_id varchar(255) NOT NULL, author_name varchar(255), author_username varchar(255), content text NOT NULL, url varchar(500), sentiment_score float DEFAULT 0, engagement_count int DEFAULT 0, mention_date datetime DEFAULT CURRENT_TIMESTAMP, processed tinyint(1) DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY post_platform (post_id, platform), KEY platform_index (platform), KEY sentiment_index (sentiment_score), KEY date_index (mention_date) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } add_action('after_setup_theme', 'create_social_monitoring_tables'); 3.2 多平台数据采集引擎 创建一个统一的数据采集引擎,支持多个社交平台: class SocialMediaMonitor { private $platforms = array(); public function __construct() { $this->platforms = array( 'twitter' => new TwitterMonitor(), 'facebook' => new FacebookMonitor(), // 可以轻松扩展其他平台 ); } public function collect_mentions($keywords, $hours = 24) { $all_mentions = array(); foreach ($this->platforms as $platform => $monitor) { if ($monitor->is_enabled()) { $mentions = $monitor->search_mentions($keywords, $hours); $all_mentions = array_merge($all_mentions, $mentions); // 存储到数据库 $this->store_mentions($mentions, $platform); } } return $all_mentions; } private function store_mentions($mentions, $platform) { global $wpdb; $table_name = $wpdb->prefix . 'social_mentions'; foreach ($mentions as $mention) { $wpdb->replace( $table_name, array( 'platform' => $platform, 'post_id' => $mention['id'], 'author_name' => $mention['author_name'], 'author_username' => $mention['author_username'], 'content' => $mention['content'], 'url' => $mention['url'], 'mention_date' => $mention['created_at'], 'engagement_count' => $mention['engagement'] ), array('%s', '%s', '%s', '%s', '%s', '%s', '%s', '%d') ); } } } 3.3 情感分析与关键词识别 集成情感分析功能,自动判断提及内容的情感倾向: class SentimentAnalyzer { public function analyze($text) { // 使用简单的词典方法进行情感分析 // 实际项目中可以考虑使用机器学习API如Google Natural Language API $positive_words = array('好', '优秀', '推荐', '喜欢', '满意', '棒', '赞'); $negative_words = array('差', '糟糕', '讨厌', '失望', '垃圾', '差评', '投诉'); $score = 0; $words = $this->segment_text($text); // 中文需要分词 foreach ($words as $word) { if (in_array($word, $positive_words)) { $score += 1; } elseif (in_array($word, $negative_words)) { $score -= 1; } } // 归一化到-1到1之间 $normalized_score = tanh($score / 10); return array( 'score' => $normalized_score, 'sentiment' => $this->get_sentiment_label($normalized_score) ); } private function get_sentiment_label($score) { if ($score > 0.3) return '积极'; if ($score < -0.3) return '消极'; return '中性'; } private function segment_text($text) { // 简单的中文分词,实际项目应使用专业分词库如jieba-php return preg_split('/s+/', $text); } } 第四部分:实时警报系统实现 4.1 警报规则引擎设计 创建一个灵活的警报规则系统,允许用户自定义触发条件: class AlertEngine { private $rules = array(); public function __construct() { $this->load_rules(); } public function check_mention($mention) { $alerts_triggered = array(); foreach ($this->rules as $rule) { if ($this->evaluate_rule($rule, $mention)) { $alerts_triggered[] = $rule['id']; $this->trigger_alert($rule, $mention); } } return $alerts_triggered; } private function evaluate_rule($rule, $mention) { $conditions_met = 0; foreach ($rule['conditions'] as $condition) { if ($this->check_condition($condition, $mention)) { $conditions_met++; } } // 根据规则类型判断是否触发 if ($rule['type'] === 'all' && $conditions_met === count($rule['conditions'])) { return true; } elseif ($rule['type'] === 'any' && $conditions_met > 0) { return true; } return false; } private function check_condition($condition, $mention) { switch ($condition['field']) { case 'sentiment': return $this->compare_sentiment($mention['sentiment_score'], $condition['operator'], $condition['value']); case 'engagement': return $this->compare_number($mention['engagement_count'], $condition['operator'], $condition['value']); case 'keyword': return $this->check_keyword($mention['content'], $condition['value']); default: return false; } } } 4.2 多渠道通知系统 实现通过多种渠道发送警报通知: class NotificationSystem { public function send_alert($alert_data, $channels) { foreach ($channels as $channel) { switch ($channel) { case 'email': $this->send_email_alert($alert_data); break; case 'slack': $this->send_slack_alert($alert_data); break; case 'webhook': $this->send_webhook_alert($alert_data); break; case 'sms': $this->send_sms_alert($alert_data); break; } } } private function send_email_alert($alert_data) { $to = get_option('alert_email_recipient', get_option('admin_email')); $subject = '社交媒体警报: ' . $alert_data['title']; $message = $this->build_email_template($alert_data); $headers = array('Content-Type: text/html; charset=UTF-8'); wp_mail($to, $subject, $message, $headers); } private function send_slack_alert($alert_data) { $webhook_url = get_option('slack_webhook_url'); if (!$webhook_url) return; $payload = array( 'text' => $alert_data['title'], 'blocks' => array( array( 'type' => 'section', 'text' => array( 'type' => 'mrkdwn', 'text' => "*新警报:* " . $alert_data['title'] ) ), array( 'type' => 'section', 'text' => array( 'type' => 'mrkdwn', 'text' => $alert_data['content'] ) ) ) ); wp_remote_post($webhook_url, array( 'body' => json_encode($payload), 'headers' => array('Content-Type' => 'application/json') )); } } 4.3 WordPress Cron定时任务集成 使用WordPress内置的Cron系统定期执行监控任务: class MonitoringScheduler { public function __construct() { add_action('social_monitoring_cron', array($this, 'run_monitoring')); add_filter('cron_schedules', array($this, 'add_custom_schedules')); } public function activate() { if (!wp_next_scheduled('social_monitoring_cron')) { wp_schedule_event(time(), 'every_15_minutes', 'social_monitoring_cron'); } } public function deactivate() { wp_clear_scheduled_hook('social_monitoring_cron'); } public function add_custom_schedules($schedules) { $schedules['every_15_minutes'] = array( 'interval' => 15 * 60, 'display' => __('每15分钟') ); $schedules['every_hour'] = array( 'interval' => 60 * 60, 'display' => __('每小时') ); return $schedules; } public function run_monitoring() { $monitor = new SocialMediaMonitor(); $keywords = get_option('monitoring_keywords', array()); if (empty($keywords)) { error_log('未设置监控关键词'); return; } $mentions = $monitor->collect_mentions($keywords, 1); // 监控最近1小时的内容 $alert_engine = new AlertEngine(); foreach ($mentions as $mention) { $alert_engine->check_mention($mention); } // 记录执行日志 $this->log_execution(count($mentions)); } } 第五部分:管理界面与可视化仪表板 5.1 WordPress管理菜单集成 创建用户友好的管理界面: class MonitoringAdmin { 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', 'social-monitoring', array($this, 'render_dashboard'), 'dashicons-share', 30 ); add_submenu_page( 'social-monitoring', '监控仪表板', '仪表板', 'manage_options', 'social-monitoring', array($this, 'render_dashboard') ); add_submenu_page( 'social-monitoring', '警报规则', '警报规则', 'manage_options', 'social-monitoring-rules', array($this, 'render_rules_page') ); add_submenu_page( 'social-monitoring', '设置', '设置', 'manage_options', 'social-monitoring-settings', array($this, 'render_settings_page') ); } public function render_dashboard() { include plugin_dir_path(__FILE__) . 'templates/dashboard.php'; } } 5.2 数据可视化与图表 使用Chart.js或ECharts创建交互式数据可视化: public function enqueue_admin_scripts($hook) { if (strpos($hook, 'social-monitoring') === false) { return; } // 加载Chart.js wp_enqueue_script( 'chart-js', 'https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js', array(), '3.7.0', true ); // 加载自定义仪表板脚本 wp_enqueue_script( 'monitoring-dashboard', plugin_dir_url(__FILE__) . 'js/dashboard.js', array('jquery', 'chart-js'), '1.0.0', ); // 传递数据到前端 wp_localize_script('monitoring-dashboard', 'monitoringData', array( 'sentimentData' => $this->get_sentiment_chart_data(), 'platformData' => $this->get_platform_distribution_data(), 'timelineData' => $this->get_mentions_timeline_data() )); } private function get_sentiment_chart_data() { global $wpdb; $table_name = $wpdb->prefix . 'social_mentions'; $results = $wpdb->get_results(" SELECT CASE WHEN sentiment_score > 0.3 THEN '积极' WHEN sentiment_score < -0.3 THEN '消极' ELSE '中性' END as sentiment, COUNT(*) as count FROM $table_name WHERE mention_date >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY CASE WHEN sentiment_score > 0.3 THEN '积极' WHEN sentiment_score < -0.3 THEN '消极' ELSE '中性' END "); $data = array( 'labels' => array('积极', '中性', '消极'), 'datasets' => array( array( 'data' => array(0, 0, 0), 'backgroundColor' => array('#4CAF50', '#2196F3', '#F44336') ) ) ); foreach ($results as $row) { $index = array_search($row->sentiment, $data['labels']); if ($index !== false) { $data['datasets'][0]['data'][$index] = (int)$row->count; } } return $data; } #### 5.3 实时数据更新与AJAX集成 实现无需刷新页面的实时数据更新: class RealTimeUpdater { public function __construct() { add_action('wp_ajax_get_recent_mentions', array($this, 'ajax_get_recent_mentions')); add_action('wp_ajax_nopriv_get_recent_mentions', array($this, 'ajax_no_permission')); add_action('wp_ajax_update_alert_status', array($this, 'ajax_update_alert_status')); } public function ajax_get_recent_mentions() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'monitoring_ajax_nonce')) { wp_die('权限验证失败'); } global $wpdb; $table_name = $wpdb->prefix . 'social_mentions'; $limit = intval($_POST['limit'] ?? 10); $offset = intval($_POST['offset'] ?? 0); $mentions = $wpdb->get_results($wpdb->prepare(" SELECT * FROM $table_name ORDER BY mention_date DESC LIMIT %d OFFSET %d ", $limit, $offset)); // 格式化数据 $formatted_mentions = array(); foreach ($mentions as $mention) { $formatted_mentions[] = array( 'id' => $mention->id, 'platform' => $mention->platform, 'author' => $mention->author_name ?: $mention->author_username, 'content' => wp_trim_words($mention->content, 20), 'sentiment' => $this->get_sentiment_label($mention->sentiment_score), 'sentiment_score' => $mention->sentiment_score, 'engagement' => $mention->engagement_count, 'time' => human_time_diff(strtotime($mention->mention_date), current_time('timestamp')), 'url' => $mention->url ); } wp_send_json_success(array( 'mentions' => $formatted_mentions, 'total' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name") )); } public function ajax_update_alert_status() { if (!current_user_can('manage_options')) { wp_send_json_error('权限不足'); } $alert_id = intval($_POST['alert_id']); $status = sanitize_text_field($_POST['status']); // 更新警报状态逻辑 $result = $this->update_alert_in_database($alert_id, $status); if ($result) { wp_send_json_success('状态更新成功'); } else { wp_send_json_error('更新失败'); } } } ### 第六部分:实用小工具功能扩展 #### 6.1 短代码(Shortcode)系统开发 创建灵活的短代码系统,让用户可以在文章或页面中嵌入监控数据: class MonitoringShortcodes { public function __construct() { add_shortcode('social_mentions', array($this, 'render_mentions_shortcode')); add_shortcode('sentiment_chart', array($this, 'render_sentiment_chart')); add_shortcode('top_influencers', array($this, 'render_influencers_list')); } public function render_mentions_shortcode($atts) { $atts = shortcode_atts(array( 'limit' => 5, 'platform' => 'all', 'sentiment' => 'all', 'days' => 7 ), $atts); global $wpdb; $table_name = $wpdb->prefix . 'social_mentions'; $where_clauses = array("mention_date >= DATE_SUB(NOW(), INTERVAL %d DAY)"); $where_values = array(intval($atts['days'])); if ($atts['platform'] !== 'all') { $where_clauses[] = "platform = %s"; $where_values[] = sanitize_text_field($atts['platform']); } if ($atts['sentiment'] !== 'all') { $sentiment_map = array( 'positive' => 'sentiment_score > 0.3', 'negative' => 'sentiment_score < -0.3', 'neutral' => 'sentiment_score BETWEEN -0.3 AND 0.3' ); if (isset($sentiment_map[$atts['sentiment']])) { $where_clauses[] = $sentiment_map[$atts['sentiment']]; } } $where_sql = implode(' AND ', $where_clauses); $mentions = $wpdb->get_results($wpdb->prepare(" SELECT * FROM $table_name WHERE $where_sql ORDER BY engagement_count DESC LIMIT %d ", array_merge($where_values, array(intval($atts['limit']))))); ob_start(); ?> <div class="social-mentions-widget"> <h3>最新社交媒体提及</h3> <div class="mentions-list"> <?php foreach ($mentions as $mention): ?> <div class="mention-item"> <div class="mention-platform platform-<?php echo esc_attr($mention->platform); ?>"> <?php echo esc_html(ucfirst($mention->platform)); ?> </div> <div class="mention-content"> <?php echo esc_html(wp_trim_words($mention->content, 15)); ?> </div> <div class="mention-meta"> <span class="mention-author">@<?php echo esc_html($mention->author_username); ?></span> <span class="mention-time"><?php echo human_time_diff(strtotime($mention->mention_date), current_time('timestamp')); ?>前</span> </div> </div> <?php endforeach; ?> </div> </div> <style> .social-mentions-widget { border: 1px solid #ddd; padding: 15px; border-radius: 5px; } .mention-item { border-bottom: 1px solid #eee; padding: 10px 0; } .mention-platform { display: inline-block; padding: 2px 8px; border-radius: 3px; font-size: 12px; color: white; } .platform-twitter { background: #1DA1F2; } .platform-facebook { background: #4267B2; } .mention-meta { font-size: 12px; color: #666; margin-top: 5px; } </style> <?php return ob_get_clean(); } } #### 6.2 WordPress小工具(Widget)开发 创建可拖拽的侧边栏小工具: class SocialMonitoringWidget extends WP_Widget { public function __construct() { parent::__construct( 'social_monitoring_widget', '社交媒体监控', array('description' => '显示最新的社交媒体提及和情感分析') ); } public function widget($args, $instance) { echo $args['before_widget']; $title = apply_filters('widget_title', $instance['title']); if (!empty($title)) { echo $args['before_title'] . $title . $args['after_title']; } // 获取数据 $data = $this->get_widget_data($instance); // 渲染小工具内容 $this->render_widget_content($data, $instance); echo $args['after_widget']; } public function form($instance) { $title = $instance['title'] ?? '社交媒体监控'; $limit = $instance['limit'] ?? 5; $show_chart = $instance['show_chart'] ?? true; ?> <p> <label for="<?php echo $this->get_field_id('title'); ?>">标题:</label> <input class="widefat" id="<?php echo $this->get_field_id('title'); ?>" name="<?php echo $this->get_field_name('title'); ?>" type="text" value="<?php echo esc_attr($title); ?>"> </p> <p> <label for="<?php echo $this->get_field_id('limit'); ?>">显示数量:</label> <input class="tiny-text" id="<?php echo $this->get_field_id('limit'); ?>" name="<?php echo $this->get_field_name('limit'); ?>" type="number" value="<?php echo esc_attr($limit); ?>" min="1" max="20"> </p> <p> <input class="checkbox" type="checkbox" id="<?php echo $this->get_field_id('show_chart'); ?>" name="<?php echo $this->get_field_name('show_chart'); ?>" <?php checked($show_chart); ?>> <label for="<?php echo $this->get_field_id('show_chart'); ?>">显示情感图表</label> </p> <?php } public function update($new_instance, $old_instance) { $instance = array(); $instance['title'] = sanitize_text_field($new_instance['title'] ?? ''); $instance['limit'] = intval($new_instance['limit'] ?? 5); $instance['show_chart'] = isset($new_instance['show_chart']); return $instance; } } // 注册小工具add_action('widgets_init', function() { register_widget('SocialMonitoringWidget'); }); #### 6.3 REST API端点创建 为监控系统创建REST API,支持与其他系统集成: class MonitoringRESTAPI { public function __construct() { add_action('rest_api_init', array($this, 'register_routes')); } public function register_routes() { register_rest_route('social-monitoring/v1', '/mentions', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_mentions'), 'permission_callback' => array($this, 'check_api_permission'), 'args' => $this->get_mentions_args() ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array($this, 'create_mention'), 'permission_callback' => array($this, 'check_api_permission') ) )); register_rest_route('social-monitoring/v1', '/analytics', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_analytics'), 'permission_callback' => array($this, 'check_api_permission') )); register_rest_route('social-monitoring/v1', '/alerts', array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_alerts'), 'permission_callback' => array($this, 'check_api_permission') )); } public function get_mentions(WP_REST_Request $request) { $params = $request->get_params(); global $wpdb; $table_name = $wpdb->prefix . 'social_mentions'; $page = max(1, intval($params['page'] ?? 1)); $per_page = min(100, intval($params['per_page'] ?? 20)); $offset = ($page - 1) * $per_page; // 构建查询条件 $where = array('1=1'); $query_params = array(); if (!empty($params['platform'])) { $where[] = 'platform = %s'; $query_params[] = sanitize_text_field($params['platform']); } if (!empty($params['start_date'])) { $where[] = 'mention_date >= %s'; $query_params[] = sanitize_text_field($params['start_date']); } if (!empty($params['end_date'])) { $where[] = 'mention_date <= %s'; $query_params[] = sanitize_text_field($params['end_date']); } if (!empty($params['sentiment'])) { if ($params['sentiment'] === 'positive') { $where[] = 'sentiment_score > 0.3'; } elseif ($params['sentiment'] === 'negative') { $where[] = 'sentiment_score < -0.3'; } else { $where[] = 'sentiment_score BETWEEN -0.3 AND 0.3'; } } $where_sql = implode(' AND ', $where); // 获取总数 $count_query = "SELECT COUNT(*) FROM $table_name WHERE $where_sql"; if (!empty($query_params)) { $count_query = $wpdb->prepare($count_query, $query_params); } $total = $wpdb->get_var($count_query); // 获取数据 $data_query = "SELECT * FROM $table_name WHERE $where_sql ORDER BY mention_date DESC LIMIT %d OFFSET %d"; $query_params[] = $per_page; $query_params[] = $offset; $data = $wpdb->get_results($wpdb->prepare($data_query, $query_params)); // 格式化响应 $formatted_data = array(); foreach ($data as $item) { $formatted_data[] = array( 'id' => $item->id, 'platform' => $item->platform, 'author' => array( 'name' => $item->author_name, 'username' => $item->author_username ), 'content' => $item->content, 'url' => $item->url, 'sentiment' => array( 'score' => floatval($item->sentiment_score), 'label' => $this->get_sentiment_label(floatval($item->sentiment_score)) ), 'engagement' => intval($item->engagement_count), 'date' => $item->mention_date ); } return new WP_REST_Response(array( 'data' => $formatted_data, 'pagination' => array( 'page' => $page, 'per_page' => $per_page, 'total' => intval($total), 'total_pages' => ceil($total / $per_page) ) ), 200); } } ### 第七部分:性能优化与安全加固 #### 7.1 数据库查询优化 优化监控系统的数据库性能: class DatabaseOptimizer { public function optimize_tables() { global $wpdb; // 定期清理旧数据 $retention_days = get_option('data_retention_days', 90); $table_name = $wpdb->prefix . 'social_mentions'; $wpdb->query($wpdb->prepare(" DELETE FROM $table_name WHERE mention_date < DATE_SUB(NOW(), INTERVAL %d DAY) ", $retention_days)); // 优化表 $wpdb->query("OPTIMIZE TABLE $table_name"); // 创建和维护索引 $this->maintain_indexes(); } private function maintain_indexes() { global $wpdb; $table_name = $wpdb->prefix . 'social_mentions'; // 检查并添加缺失的索引 $indexes = $wpdb->get_results("SHOW INDEX FROM $table_name"); $existing_indexes = array(); foreach ($indexes as $index) { $existing_indexes[] = $index->Key_name; } // 添加常用查询的复合索引 if (!in_array('idx_platform_date', $existing_indexes)) { $wpdb->query("CREATE INDEX idx_platform_date ON $table_name (platform, mention_date)"); } if (!in_array('idx_sentiment_engagement', $existing_indexes)) { $wpdb->query("CREATE INDEX idx_sentiment_engagement ON $table_name (sentiment_score, engagement_count)"); } } public function add_query_cache() { // 使用WordPress瞬

发表评论

实战教学,为你的网站添加在线迷你游戏以提升用户互动与留存

实战教学:为你的WordPress网站添加在线迷你游戏以提升用户互动与留存 引言:为什么网站需要迷你游戏? 在当今互联网环境中,用户注意力已成为最稀缺的资源之一。网站运营者面临着一个共同的挑战:如何让访客停留更长时间,提高用户参与度,并最终实现转化率的提升?传统的内容展示方式已难以满足现代用户的需求,而互动元素的加入正成为解决这一问题的有效途径。 在线迷你游戏作为一种轻量级互动形式,具有以下优势: 提升用户停留时间:有趣的游戏体验能有效延长用户在网站的停留时间 增强品牌记忆:通过游戏化体验加深用户对品牌的印象 促进社交分享:用户乐于分享游戏成绩和体验,带来自然流量 收集用户数据:游戏过程中可以收集有价值的用户行为数据 提高转化率:游戏化元素可以作为引导用户完成特定动作的有效手段 本文将详细介绍如何通过WordPress代码二次开发,为你的网站添加实用的在线迷你游戏和小工具功能,从而显著提升用户互动与留存率。 第一部分:准备工作与环境搭建 1.1 选择合适的开发环境 在开始开发之前,确保你拥有以下环境: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel WordPress安装:最新版本的WordPress(建议5.8以上) 代码编辑器:VS Code、Sublime Text或PHPStorm 浏览器开发者工具:用于调试JavaScript和CSS 1.2 创建子主题保护核心文件 为了避免主题更新导致自定义代码丢失,我们首先创建一个子主题: /* Theme Name: 我的游戏化子主题 Template: twentytwentythree Version: 1.0 Description: 为网站添加迷你游戏功能的子主题 */ // 引入父主题样式表 add_action('wp_enqueue_scripts', 'my_gamification_theme_enqueue_styles'); function my_gamification_theme_enqueue_styles() { wp_enqueue_style('parent-style', get_template_directory_uri() . '/style.css'); wp_enqueue_style('child-style', get_stylesheet_directory_uri() . '/style.css', array('parent-style')); } 1.3 创建必要的目录结构 在你的子主题目录中创建以下文件夹结构: /my-gamification-theme/ ├── games/ │ ├── js/ │ ├── css/ │ └── assets/ ├── includes/ ├── templates/ └── functions.php 第二部分:实现经典记忆配对游戏 2.1 游戏设计与功能规划 记忆配对游戏是一种简单但有效的互动游戏,适合各种类型的网站。我们将实现以下功能: 可配置的卡片数量(4x4、4x5、5x6等) 计时器和步数计数器 得分系统 社交分享功能 保存最高分记录 2.2 创建游戏短代码 在functions.php中添加短代码,使游戏可以轻松插入到任何文章或页面中: // 注册记忆游戏短代码 add_shortcode('memory_game', 'memory_game_shortcode'); function memory_game_shortcode($atts) { // 短代码属性 $atts = shortcode_atts( array( 'columns' => 4, 'rows' => 4, 'theme' => 'default' ), $atts, 'memory_game' ); // 生成唯一游戏ID $game_id = 'memory_game_' . uniqid(); // 输出游戏容器 ob_start(); ?> <div id="<?php echo esc_attr($game_id); ?>" class="memory-game-container" data-columns="<?php echo esc_attr($atts['columns']); ?>" data-rows="<?php echo esc_attr($atts['rows']); ?>" data-theme="<?php echo esc_attr($atts['theme']); ?>"> <div class="game-controls"> <div class="game-stats"> <span class="timer">时间: <span class="time-value">00:00</span></span> <span class="moves">步数: <span class="moves-value">0</span></span> <span class="score">得分: <span class="score-value">0</span></span> </div> <div class="game-buttons"> <button class="restart-game">重新开始</button> <button class="pause-game">暂停</button> </div> </div> <div class="game-board"></div> <div class="game-result" style="display:none;"> <h3>游戏结束!</h3> <p>你的得分: <span class="final-score">0</span></p> <p>用时: <span class="final-time">00:00</span></p> <p>步数: <span class="final-moves">0</span></p> <button class="play-again">再玩一次</button> <button class="share-score">分享成绩</button> </div> </div> <?php return ob_get_clean(); } 2.3 实现游戏JavaScript逻辑 创建 /games/js/memory-game.js 文件: class MemoryGame { constructor(containerId) { this.container = document.getElementById(containerId); this.columns = parseInt(this.container.dataset.columns) || 4; this.rows = parseInt(this.container.dataset.rows) || 4; this.theme = this.container.dataset.theme || 'default'; this.totalPairs = (this.columns * this.rows) / 2; this.cards = []; this.flippedCards = []; this.matchedPairs = 0; this.moves = 0; this.score = 0; this.gameStarted = false; this.gamePaused = false; this.startTime = null; this.timerInterval = null; this.elapsedTime = 0; this.init(); } init() { this.createCards(); this.renderBoard(); this.setupEventListeners(); this.updateStats(); } createCards() { // 创建卡片对 const symbols = ['★', '❤', '♦', '♠', '♣', '☀', '☁', '☂', '☃', '♫', '⚓', '✈']; const usedSymbols = symbols.slice(0, this.totalPairs); // 每对卡片重复一次 let cardValues = [...usedSymbols, ...usedSymbols]; // 随机排序 cardValues = this.shuffleArray(cardValues); // 创建卡片对象 this.cards = cardValues.map((value, index) => ({ id: index, value: value, flipped: false, matched: false })); } shuffleArray(array) { for (let i = array.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [array[i], array[j]] = [array[j], array[i]]; } return array; } renderBoard() { const board = this.container.querySelector('.game-board'); board.innerHTML = ''; // 设置网格布局 board.style.gridTemplateColumns = `repeat(${this.columns}, 1fr)`; board.style.gridTemplateRows = `repeat(${this.rows}, 1fr)`; // 创建卡片元素 this.cards.forEach(card => { const cardElement = document.createElement('div'); cardElement.className = 'memory-card'; cardElement.dataset.id = card.id; const frontFace = document.createElement('div'); frontFace.className = 'card-front'; frontFace.textContent = card.value; const backFace = document.createElement('div'); backFace.className = 'card-back'; backFace.textContent = '?'; cardElement.appendChild(frontFace); cardElement.appendChild(backFace); board.appendChild(cardElement); }); } setupEventListeners() { // 卡片点击事件 this.container.addEventListener('click', (e) => { const cardElement = e.target.closest('.memory-card'); if (!cardElement || this.gamePaused) return; const cardId = parseInt(cardElement.dataset.id); this.flipCard(cardId); }); // 重新开始按钮 const restartBtn = this.container.querySelector('.restart-game'); restartBtn.addEventListener('click', () => this.restartGame()); // 暂停按钮 const pauseBtn = this.container.querySelector('.pause-game'); pauseBtn.addEventListener('click', () => this.togglePause()); // 再玩一次按钮 const playAgainBtn = this.container.querySelector('.play-again'); if (playAgainBtn) { playAgainBtn.addEventListener('click', () => this.restartGame()); } // 分享按钮 const shareBtn = this.container.querySelector('.share-score'); if (shareBtn) { shareBtn.addEventListener('click', () => this.shareScore()); } } flipCard(cardId) { // 如果游戏未开始,开始计时 if (!this.gameStarted) { this.startGame(); } const card = this.cards.find(c => c.id === cardId); // 如果卡片已匹配或已翻转,忽略点击 if (card.matched || card.flipped || this.flippedCards.length >= 2) { return; } // 翻转卡片 card.flipped = true; this.flippedCards.push(card); // 更新UI this.updateCardUI(cardId); // 如果翻转了两张卡片,检查是否匹配 if (this.flippedCards.length === 2) { this.moves++; this.updateStats(); const [card1, card2] = this.flippedCards; if (card1.value === card2.value) { // 匹配成功 card1.matched = true; card2.matched = true; this.matchedPairs++; this.score += 100; // 更新分数 this.updateStats(); // 清空翻转卡片数组 this.flippedCards = []; // 检查游戏是否结束 if (this.matchedPairs === this.totalPairs) { this.endGame(); } } else { // 不匹配,稍后翻转回来 setTimeout(() => { card1.flipped = false; card2.flipped = false; this.flippedCards = []; this.updateCardUI(card1.id); this.updateCardUI(card2.id); }, 1000); } } } updateCardUI(cardId) { const cardElement = this.container.querySelector(`[data-id="${cardId}"]`); const card = this.cards.find(c => c.id === cardId); if (card.flipped || card.matched) { cardElement.classList.add('flipped'); } else { cardElement.classList.remove('flipped'); } } startGame() { this.gameStarted = true; this.startTime = Date.now(); // 开始计时器 this.timerInterval = setInterval(() => { if (!this.gamePaused) { this.elapsedTime = Date.now() - this.startTime; this.updateStats(); } }, 1000); } togglePause() { this.gamePaused = !this.gamePaused; const pauseBtn = this.container.querySelector('.pause-game'); if (this.gamePaused) { pauseBtn.textContent = '继续'; } else { pauseBtn.textContent = '暂停'; // 如果游戏暂停后继续,调整开始时间 if (this.gameStarted) { this.startTime = Date.now() - this.elapsedTime; } } } updateStats() { // 更新时间显示 const timeElement = this.container.querySelector('.time-value'); const minutes = Math.floor(this.elapsedTime / 60000); const seconds = Math.floor((this.elapsedTime % 60000) / 1000); timeElement.textContent = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; // 更新步数 const movesElement = this.container.querySelector('.moves-value'); movesElement.textContent = this.moves; // 更新分数 const scoreElement = this.container.querySelector('.score-value'); scoreElement.textContent = this.score; } endGame() { clearInterval(this.timerInterval); // 计算最终得分(考虑时间和步数) const timeBonus = Math.max(0, 300 - Math.floor(this.elapsedTime / 1000)) * 10; const movesBonus = Math.max(0, 50 - this.moves) * 5; this.score += timeBonus + movesBonus; // 显示结果 const resultElement = this.container.querySelector('.game-result'); resultElement.querySelector('.final-score').textContent = this.score; resultElement.querySelector('.final-time').textContent = this.container.querySelector('.time-value').textContent; resultElement.querySelector('.final-moves').textContent = this.moves; resultElement.style.display = 'block'; // 保存最高分到本地存储 this.saveHighScore(); } saveHighScore() { const highScores = JSON.parse(localStorage.getItem('memoryGameHighScores') || '[]'); highScores.push({ score: this.score, time: this.elapsedTime, moves: this.moves, date: new Date().toISOString(), grid: `${this.columns}x${this.rows}` }); // 按分数排序,只保留前10名 highScores.sort((a, b) => b.score - a.score); const topScores = highScores.slice(0, 10); localStorage.setItem('memoryGameHighScores', JSON.stringify(topScores)); } shareScore() { const text = `我在记忆配对游戏中获得了${this.score}分!用时${this.container.querySelector('.time-value').textContent},用了${this.moves}步。`; if (navigator.share) { navigator.share({ title: '我的游戏成绩', text: text, url: window.location.href }); } else { // 备用方案:复制到剪贴板 navigator.clipboard.writeText(text).then(() => { alert('成绩已复制到剪贴板,快去分享吧!'); }); } } restartGame() { // 重置游戏状态 this.matchedPairs = 0; this.moves = 0; this.score = 0; this.gameStarted = false; this.gamePaused = false; this.flippedCards = []; this.elapsedTime = 0; clearInterval(this.timerInterval); // 重新创建卡片 this.createCards(); this.renderBoard(); // 隐藏结果 const resultElement = this.container.querySelector('.game-result'); resultElement.style.display = 'none'; // 更新统计 this.updateStats(); } } // 初始化所有记忆游戏实例 document.addEventListener('DOMContentLoaded', () => { document.querySelectorAll('.memory-game-container').forEach(container => { new MemoryGame(container.id); }); }); 2.4 添加游戏样式 创建 /games/css/memory-game.css 文件: .memory-game-container { max-width: 800px; margin: 20px auto; padding: 20px; background: linear-gradient(135deg, #6a11cb 0%, #2575fc 100%); border-radius: 15px; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); color: white; font-family: 'Arial', sans-serif; } .game-controls { display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; padding: 15px; background: rgba(255, 255, 255, 0.1); border-radius: 10px; flex-wrap: wrap; } .game-stats { display: flex; gap: 20px; font-size: 18px; font-weight: bold; } .game-stats span { background: rgba(0, 0, 0, 0.2); padding: 8px 15px; border-radius: 5px; } .game-buttons { display: flex; gap: 10px; } .game-buttons button { padding: 10px 20px; border: none; border-radius: 5px; background: #4CAF50; color: white; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } .game-buttons button:hover { background: #45a049; transform: translateY(-2px); } .game-buttons .pause-game { background: #ff9800; } .game-buttons .pause-game:hover { background: #e68900; } .game-board { display: grid; gap: 10px; margin: 20px 0; perspective: 1000px; } .memory-card { height: 100px; position: relative; transform-style: preserve-3d; transition: transform 0.6s; cursor: pointer; border-radius: 10px; box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); } .memory-card.flipped { transform: rotateY(180deg); } .memory-card .card-front, .memory-card .card-back { position: absolute; width: 100%; height: 100%; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 2em; font-weight: bold; } .memory-card .card-back { background: linear-gradient(45deg, #2196F3, #21CBF3); color: white; transform: rotateY(0deg); } .memory-card .card-front { background: linear-gradient(45deg, #FF9800, #FFC107); color: white; transform: rotateY(180deg); } .memory-card.matched .card-front { background: linear-gradient(45deg, #4CAF50, #8BC34A); } .game-result { text-align: center; padding: 30px; background: rgba(255, 255, 255, 0.1); border-radius: 10px; margin-top: 20px; animation: fadeIn 0.5s ease; } .game-result h3 { font-size: 28px; margin-bottom: 20px; color: #FFEB3B; } .game-result p { font-size: 18px; margin: 10px 0; } .game-result button { margin: 10px; padding: 12px 25px; border: none; border-radius: 5px; font-size: 16px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } .game-result .play-again { background: #4CAF50; color: white; } .game-result .share-score { background: #2196F3; color: white; } .game-result button:hover { transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); } @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } /* 响应式设计 */ @media (max-width: 768px) { .game-controls { flex-direction: column; gap: 15px; } .game-stats { flex-wrap: wrap; justify-content: center; } .memory-card { height: 80px; } } @media (max-width: 480px) { .memory-card { height: 60px; } .memory-card .card-front, .memory-card .card-back { font-size: 1.5em; } } 2.5 在WordPress中注册游戏资源 在functions.php中添加以下代码,确保游戏脚本和样式正确加载: // 注册并加载记忆游戏资源 add_action('wp_enqueue_scripts', 'register_memory_game_assets'); function register_memory_game_assets() { // 只在需要时加载游戏资源 global $post; if (is_a($post, 'WP_Post') && has_shortcode($post->post_content, 'memory_game')) { // 游戏样式 wp_enqueue_style( 'memory-game-css', get_stylesheet_directory_uri() . '/games/css/memory-game.css', array(), '1.0.0' ); // 游戏脚本 wp_enqueue_script( 'memory-game-js', get_stylesheet_directory_uri() . '/games/js/memory-game.js', array(), '1.0.0', true ); } } 第三部分:创建简易抽奖转盘游戏 3.1 转盘游戏设计与实现 抽奖转盘是另一种受欢迎的互动形式,特别适合电商网站和营销活动。我们将创建一个可配置的转盘游戏: // 注册转盘游戏短代码 add_shortcode('wheel_of_fortune', 'wheel_of_fortune_shortcode'); function wheel_of_fortune_shortcode($atts) { $atts = shortcode_atts( array( 'segments' => '优惠券10%,谢谢参与,优惠券20%,再来一次,折扣30%,幸运奖,优惠券15%,大奖', 'colors' => '#FF6384,#36A2EB,#FFCE56,#4BC0C0,#9966FF,#FF9F40,#FF6384,#36A2EB', 'prize_text' => '恭喜您获得:', 'button_text' => '开始抽奖' ), $atts, 'wheel_of_fortune' ); $game_id = 'wheel_game_' . uniqid(); $segments = explode(',', $atts['segments']); $colors = explode(',', $atts['colors']); ob_start(); ?> <div id="<?php echo esc_attr($game_id); ?>" class="wheel-game-container"> <div class="wheel-header"> <h3>幸运大转盘</h3> <p>试试你的运气,赢取惊喜奖励!</p> </div> <div class="wheel-content"> <div class="wheel-wrapper"> <canvas id="<?php echo esc_attr($game_id); ?>_canvas" class="wheel-canvas" width="400" height="400"></canvas> <div class="wheel-pointer"></div> </div> <div class="wheel-controls"> <div class="wheel-stats"> <p>剩余抽奖次数: <span class="spins-left">3</span></p> <p class="prize-result"></p> </div> <button class="spin-button"><?php echo esc_html($atts['button_text']); ?></button> <button class="reset-spins">重置次数</button> <div class="wheel-segments"> <h4>奖项设置:</h4> <ul> <?php foreach ($segments as $index => $segment): ?> <li> <span class="segment-color" style="background-color: <?php echo esc_attr($colors[$index % count($colors)]); ?>"></span> <?php echo esc_html($segment); ?> </li> <?php endforeach; ?> </ul> </div> </div> </div> <div class="wheel-history"> <h4>中奖记录</h4> <ul class="history-list"></ul> </div> </div> <script type="text/javascript"> document.addEventListener('DOMContentLoaded', function() { new WheelOfFortune( '<?php echo esc_js($game_id); ?>', <?php echo json_encode($segments); ?>, <?php echo json_encode($colors); ?>, '<?php echo esc_js($atts['prize_text']); ?>' ); }); </script> <?php return ob_get_clean(); } 3.2 转盘游戏JavaScript实现 创建 /games/js/wheel-game.js 文件: class WheelOfFortune { constructor(containerId, segments, colors, prizeText) { this.container = document.getElementById(containerId); this.canvas = this.container.querySelector('.wheel-canvas'); this.ctx = this.canvas.getContext('2d'); this.spinButton = this.container.querySelector('.spin-button'); this.resetButton = this.container.querySelector('.reset-spins'); this.prizeResult = this.container.querySelector('.prize-result'); this.spinsLeftElement = this.container.querySelector('.spins-left'); this.historyList = this.container.querySelector('.history-list'); this.segments = segments; this.colors = colors; this.prizeText = prizeText; // 游戏状态 this.spinsLeft = 3; this.isSpinning = false; this.currentRotation = 0; this.segmentAngle = (2 * Math.PI) / this.segments.length; this.init(); } init() { this.drawWheel(); this.setupEventListeners(); this.updateSpinsDisplay(); this.loadHistory(); } drawWheel() { const centerX = this.canvas.width / 2; const centerY = this.canvas.height / 2; const radius = Math.min(centerX, centerY) - 10; // 清除画布 this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); // 绘制每个扇形 for (let i = 0; i < this.segments.length; i++) { const startAngle = this.currentRotation + (i * this.segmentAngle); const endAngle = startAngle + this.segmentAngle; // 绘制扇形 this.ctx.beginPath(); this.ctx.moveTo(centerX, centerY); this.ctx.arc(centerX, centerY, radius, startAngle, endAngle); this.ctx.closePath(); // 填充颜色 this.ctx.fillStyle = this.colors[i % this.colors.length]; this.ctx.fill(); // 绘制边框 this.ctx.strokeStyle = '#FFFFFF'; this.ctx.lineWidth = 2; this.ctx.stroke(); // 绘制文本 this.ctx.save(); this.ctx.translate(centerX, centerY); this.ctx.rotate(startAngle + this.segmentAngle / 2); this.ctx.textAlign = 'right'; this.ctx.fillStyle = '#FFFFFF'; this.ctx.font = 'bold 14px Arial'; this.ctx.fillText(this.segments[i], radius - 20, 5); this.ctx.restore(); } // 绘制中心圆 this.ctx.beginPath(); this.ctx.arc(centerX, centerY, 20, 0, 2 * Math.PI); this.ctx.fillStyle = '#333333'; this.ctx.fill(); this.ctx.strokeStyle = '#FFFFFF'; this.ctx.lineWidth = 3; this.ctx.stroke(); } setupEventListeners() { this.spinButton.addEventListener('click', () => this.spinWheel()); this.resetButton.addEventListener('click', () => this.resetSpins()); } spinWheel() { if (this.isSpinning || this.spinsLeft <= 0) return; this.isSpinning = true; this.spinsLeft--; this.updateSpinsDisplay(); // 随机决定停止位置 const spinDuration = 3000 + Math.random() * 2000; // 3-5秒 const extraRotation = 5 + Math.random() * 5; // 额外旋转5-10圈 const totalRotation = (extraRotation * 2 * Math.PI) + (Math.random() * this.segmentAngle); // 动画开始时间 const startTime = Date.now(); const animate = () => { const elapsed = Date.now() - startTime; const progress = Math.min(elapsed / spinDuration, 1); // 缓动函数:先快后慢 const easeOut = 1 - Math.pow(1 - progress, 3); // 更新旋转角度 this.currentRotation = easeOut * totalRotation; this.drawWheel(); if (progress < 1) { requestAnimationFrame(animate); } else { // 动画结束 this.isSpinning = false; this.determinePrize(); } }; animate(); } determinePrize() { // 计算指针指向的扇形 const normalizedRotation = this.currentRotation % (2 * Math.PI); const segmentIndex = Math.floor( ((2 * Math.PI - normalizedRotation) % (2 * Math.PI)) / this.segmentAngle ); const prize = this.segments[segmentIndex]; // 显示结果 this.prizeResult.textContent = `${this.prizeText} ${prize}`; this.prizeResult.style.color = this.colors[segmentIndex % this.colors.length]; // 添加到历史记录 this.addToHistory(prize); // 保存到本地存储 this.saveHistory(prize); // 如果是"再来一次",增加一次抽奖机会 if (prize === '再来一次') { this.spinsLeft++; this.updateSpinsDisplay(); } } addToHistory(prize) { const now = new Date(); const timeString = now.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); const historyItem = document.createElement('li'); historyItem.innerHTML = ` <span class="history-time">${timeString}</span> <span class="history-prize">${prize}</span> `; this.historyList.insertBefore(historyItem, this.historyList.firstChild); // 限制历史记录数量 if (this.historyList.children.length > 10) { this.historyList.removeChild(this.historyList.lastChild); } } saveHistory(prize) { const history = JSON.parse(localStorage.getItem('wheelGameHistory') || '[]'); history.unshift({ prize: prize, timestamp: new Date().toISOString() }); // 只保留最近20条记录 const recentHistory = history.slice(0, 20); localStorage.setItem('wheelGameHistory', JSON.stringify(recentHistory)); } loadHistory() { const history = JSON.parse(localStorage.getItem('wheelGameHistory') || '[]'); history.forEach(item => { const date = new Date(item.timestamp); const timeString = date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'}); const historyItem = document.createElement('li'); historyItem.innerHTML = ` <span class="history-time">${timeString}</span> <span class="history-prize">${item.prize}</span> `; this.historyList.appendChild(historyItem); }); } updateSpinsDisplay() { this.spinsLeftElement.textContent = this.spinsLeft; if (this.spinsLeft <= 0) { this.spinButton.disabled = true; this.spinButton.textContent = '次数已用完'; } else { this.spinButton.disabled = false; this.spinButton.textContent = '开始抽奖'; } } resetSpins() { this.spinsLeft = 3; this.updateSpinsDisplay(); this.prizeResult.textContent = ''; } } 3.3 转盘游戏样式设计 创建 /games/css/wheel-game.css 文件: .wheel-game-container { max-width: 900px; margin: 30px auto; padding: 25px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 20px; box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2); color: white; font-family: 'Arial', sans-serif; } .wheel-header { text-align: center; margin-bottom: 30px; } .wheel-header h3 { font-size: 32px; margin-bottom: 10px; color: #FFD700; text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.3); } .wheel-header p { font-size: 18px; opacity: 0.9; } .wheel-content { display: flex; flex-wrap: wrap; gap: 40px; align-items: center; justify-content: center; } .wheel-wrapper { position: relative; flex: 0 0 auto; } .wheel-canvas { background: white; border-radius: 50%; box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3); border: 8px solid #FFD700; } .wheel-pointer { position: absolute; top: -20px; left: 50%; transform: translateX(-50%); width: 0; height: 0; border-left: 20px solid transparent; border-right: 20px solid transparent; border-top: 40px solid #FF0000; filter: drop-shadow(0 5px 5px rgba(0, 0, 0, 0.3)); z-index: 10; } .wheel-controls { flex: 1; min-width: 300px; background: rgba(255, 255, 255, 0.1); padding: 25px; border-radius: 15px; backdrop-filter: blur(10px); } .wheel-stats { margin-bottom: 25px; padding: 15px; background: rgba(0, 0, 0, 0.2); border-radius: 10px; } .wheel-stats p { font-size: 18px; margin: 10px 0; } .prize-result { font-size: 22px !important; font-weight: bold; color: #FFD700 !important; min-height: 30px; margin-top: 15px !important; } .wheel-controls button { display: block; width: 100%; padding: 15px; margin: 10px 0; border: none; border-radius: 8px; font-size: 18px; font-weight: bold; cursor: pointer; transition: all 0.3s ease; } .spin-button { background: linear-gradient(45deg, #FF416C, #FF4B2B); color: white; } .spin-button:hover:not(:disabled) { transform: translateY(-3px);

发表评论

手把手教程,在WordPress中集成网站Cookie合规管理与用户同意横幅

手把手教程:在WordPress中集成网站Cookie合规管理与用户同意横幅,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:为什么Cookie合规管理如此重要? 在当今数字时代,数据隐私已成为全球关注的焦点。随着欧盟《通用数据保护条例》(GDPR)、加州消费者隐私法案(CCPA)以及中国《个人信息保护法》等法规的实施,网站所有者必须确保其在线平台符合数据保护要求。Cookie作为网站跟踪用户行为、存储偏好设置的关键工具,其使用必须透明且获得用户明确同意。 WordPress作为全球最流行的内容管理系统,驱动着超过40%的网站。然而,许多WordPress网站所有者并未充分意识到Cookie合规的重要性,或不知道如何正确实施合规解决方案。本教程将手把手指导您通过代码二次开发,在WordPress中集成完整的Cookie合规管理系统,包括用户同意横幅、偏好设置中心和常用互联网小工具功能。 第一部分:理解Cookie合规的基本要求 1.1 主要数据保护法规概述 在开始技术实施之前,了解相关法规的基本要求至关重要: GDPR(欧盟通用数据保护条例):要求网站在使用非必要Cookie前获得用户明确、知情的同意 CCPA(加州消费者隐私法案):赋予加州居民了解其个人信息被收集、拒绝出售个人信息的权利 ePrivacy指令:专门规范电子通信隐私,包括Cookie使用 中国《个人信息保护法》:规定个人信息处理应取得个人同意,并遵循最小必要原则 1.2 Cookie分类与合规要求 根据功能,Cookie通常分为以下几类: 必要Cookie:确保网站基本功能运行,无需用户同意 偏好Cookie:记住用户选择(如语言、地区),需要用户同意 统计Cookie:收集匿名数据用于分析,需要用户同意 营销Cookie:跟踪用户行为用于广告定向,需要明确同意 1.3 WordPress网站Cookie合规现状 大多数WordPress网站通过以下方式使用Cookie: 核心WordPress:使用登录认证Cookie 主题和插件:添加各种功能性和跟踪Cookie 第三方服务:如Google Analytics、Facebook像素等 第二部分:规划Cookie合规解决方案架构 2.1 系统需求分析 一个完整的Cookie合规管理系统应包含: 可定制的同意横幅:清晰说明Cookie使用目的 同意管理平台:允许用户查看和修改偏好设置 Cookie分类拦截:在获得同意前阻止非必要脚本 同意记录:存储用户同意状态和偏好 定期重新同意:根据法规要求定期更新同意 2.2 技术架构设计 我们将构建一个轻量级但功能完整的解决方案: 前端组件:使用HTML、CSS和JavaScript创建响应式横幅和设置面板 后端处理:使用PHP处理同意状态存储和脚本管理 数据库设计:存储用户同意偏好和日志 集成机制:与WordPress核心和第三方插件无缝集成 第三部分:创建WordPress插件基础结构 3.1 初始化插件文件 首先,在wp-content/plugins目录下创建新文件夹"cookie-compliance-manager",然后创建主插件文件: <?php /** * Plugin Name: Cookie Compliance Manager * Plugin URI: https://yourwebsite.com/ * Description: 完整的Cookie合规管理与用户同意解决方案 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: cookie-compliance-manager */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('CCM_VERSION', '1.0.0'); define('CCM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('CCM_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once CCM_PLUGIN_DIR . 'includes/class-cookie-compliance-manager.php'; function run_cookie_compliance_manager() { $plugin = new Cookie_Compliance_Manager(); $plugin->run(); } run_cookie_compliance_manager(); 3.2 创建主管理类 在includes目录下创建主类文件: <?php class Cookie_Compliance_Manager { private $loader; public function __construct() { $this->load_dependencies(); $this->define_admin_hooks(); $this->define_public_hooks(); } private function load_dependencies() { require_once CCM_PLUGIN_DIR . 'includes/class-ccm-loader.php'; require_once CCM_PLUGIN_DIR . 'includes/class-ccm-i18n.php'; require_once CCM_PLUGIN_DIR . 'admin/class-ccm-admin.php'; require_once CCM_PLUGIN_DIR . 'public/class-ccm-public.php'; $this->loader = new CCM_Loader(); } private function define_admin_hooks() { $plugin_admin = new CCM_Admin(); $this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_styles'); $this->loader->add_action('admin_enqueue_scripts', $plugin_admin, 'enqueue_scripts'); $this->loader->add_action('admin_menu', $plugin_admin, 'add_admin_menu'); $this->loader->add_action('admin_init', $plugin_admin, 'register_settings'); } private function define_public_hooks() { $plugin_public = new CCM_Public(); $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_styles'); $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_scripts'); $this->loader->add_action('wp_head', $plugin_public, 'insert_cookie_consent_banner'); $this->loader->add_action('wp_footer', $plugin_public, 'insert_cookie_settings_modal'); $this->loader->add_action('init', $plugin_public, 'handle_ajax_requests'); } public function run() { $this->loader->run(); } } 第四部分:构建Cookie同意横幅前端界面 4.1 设计响应式Cookie横幅 创建public/css/ccm-public.css文件: /* Cookie同意横幅基础样式 */ .ccm-cookie-banner { position: fixed; bottom: 0; left: 0; right: 0; background: #2c3e50; color: #ecf0f1; padding: 20px; z-index: 999999; box-shadow: 0 -2px 10px rgba(0,0,0,0.1); display: none; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; } .ccm-banner-content { max-width: 1200px; margin: 0 auto; display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; } .ccm-banner-text { flex: 1; min-width: 300px; margin-right: 20px; } .ccm-banner-text h3 { margin: 0 0 10px 0; color: #3498db; font-size: 1.2em; } .ccm-banner-text p { margin: 0 0 15px 0; line-height: 1.5; font-size: 14px; } .ccm-banner-text a { color: #3498db; text-decoration: underline; } .ccm-banner-actions { display: flex; gap: 10px; flex-wrap: wrap; } .ccm-btn { padding: 10px 20px; border: none; border-radius: 4px; cursor: pointer; font-weight: 600; transition: all 0.3s ease; font-size: 14px; } .ccm-btn-accept-all { background: #27ae60; color: white; } .ccm-btn-accept-all:hover { background: #219653; } .ccm-btn-settings { background: #3498db; color: white; } .ccm-btn-settings:hover { background: #2980b9; } .ccm-btn-reject-all { background: #e74c3c; color: white; } .ccm-btn-reject-all:hover { background: #c0392b; } /* 响应式设计 */ @media (max-width: 768px) { .ccm-banner-content { flex-direction: column; text-align: center; } .ccm-banner-text { margin-right: 0; margin-bottom: 15px; } .ccm-banner-actions { justify-content: center; } } 4.2 创建Cookie设置模态窗口 添加设置面板的HTML结构和CSS: /* Cookie设置模态窗口 */ .ccm-settings-modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.8); z-index: 1000000; overflow-y: auto; } .ccm-modal-content { background: white; margin: 50px auto; max-width: 800px; border-radius: 8px; box-shadow: 0 5px 30px rgba(0,0,0,0.3); color: #333; } .ccm-modal-header { padding: 20px 30px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; } .ccm-modal-header h2 { margin: 0; color: #2c3e50; } .ccm-close-modal { background: none; border: none; font-size: 24px; cursor: pointer; color: #7f8c8d; } .ccm-modal-body { padding: 30px; } .ccm-cookie-category { margin-bottom: 25px; padding: 20px; border: 1px solid #ddd; border-radius: 6px; background: #f9f9f9; } .ccm-category-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .ccm-category-header h3 { margin: 0; color: #2c3e50; } .ccm-category-toggle { position: relative; display: inline-block; width: 50px; height: 24px; } .ccm-category-toggle input { opacity: 0; width: 0; height: 0; } .ccm-toggle-slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; transition: .4s; border-radius: 24px; } .ccm-toggle-slider:before { position: absolute; content: ""; height: 16px; width: 16px; left: 4px; bottom: 4px; background-color: white; transition: .4s; border-radius: 50%; } input:checked + .ccm-toggle-slider { background-color: #27ae60; } input:checked + .ccm-toggle-slider:before { transform: translateX(26px); } .ccm-category-description { color: #666; font-size: 14px; line-height: 1.5; } .ccm-cookie-list { margin-top: 15px; font-size: 13px; } .ccm-cookie-item { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #eee; } .ccm-modal-footer { padding: 20px 30px; border-top: 1px solid #eee; text-align: right; background: #f9f9f9; border-radius: 0 0 8px 8px; } 第五部分:实现JavaScript交互功能 5.1 创建主JavaScript文件 在public/js/ccm-public.js中实现核心交互逻辑: (function($) { 'use strict'; var CCM = { // 初始化 init: function() { this.bindEvents(); this.checkConsent(); this.loadBlockedScripts(); }, // 绑定事件 bindEvents: function() { // 接受所有Cookie $(document).on('click', '.ccm-btn-accept-all', function(e) { e.preventDefault(); CCM.saveConsent('all'); CCM.hideBanner(); }); // 拒绝所有非必要Cookie $(document).on('click', '.ccm-btn-reject-all', function(e) { e.preventDefault(); CCM.saveConsent('necessary'); CCM.hideBanner(); }); // 打开设置 $(document).on('click', '.ccm-btn-settings', function(e) { e.preventDefault(); CCM.showSettingsModal(); }); // 关闭模态窗口 $(document).on('click', '.ccm-close-modal', function() { CCM.hideSettingsModal(); }); // 保存设置 $(document).on('click', '.ccm-save-settings', function() { CCM.saveCustomConsent(); }); // 阻止模态窗口外部点击关闭 $(document).on('click', '.ccm-settings-modal', function(e) { if ($(e.target).hasClass('ccm-settings-modal')) { CCM.hideSettingsModal(); } }); }, // 显示Cookie横幅 showBanner: function() { $('.ccm-cookie-banner').fadeIn(300); $('body').addClass('ccm-banner-visible'); }, // 隐藏Cookie横幅 hideBanner: function() { $('.ccm-cookie-banner').fadeOut(300); $('body').removeClass('ccm-banner-visible'); }, // 显示设置模态窗口 showSettingsModal: function() { $('.ccm-settings-modal').fadeIn(300); $('body').addClass('ccm-modal-visible'); this.hideBanner(); }, // 隐藏设置模态窗口 hideSettingsModal: function() { $('.ccm-settings-modal').fadeOut(300); $('body').removeClass('ccm-modal-visible'); }, // 检查同意状态 checkConsent: function() { var consent = this.getConsent(); if (!consent || consent.status === 'pending') { this.showBanner(); } else { this.hideBanner(); } }, // 获取同意状态 getConsent: function() { var consentCookie = this.getCookie('ccm_consent'); if (consentCookie) { try { return JSON.parse(decodeURIComponent(consentCookie)); } catch (e) { return null; } } return null; }, // 保存同意设置 saveConsent: function(type) { var consent = { version: '1.0', date: new Date().toISOString(), status: 'given' }; switch(type) { case 'all': consent.categories = { necessary: true, preferences: true, statistics: true, marketing: true }; break; case 'necessary': consent.categories = { necessary: true, preferences: false, statistics: false, marketing: false }; break; default: // 自定义设置 consent.categories = this.getCategorySelections(); } // 设置Cookie(365天过期) this.setCookie('ccm_consent', JSON.stringify(consent), 365); // 触发同意更改事件 $(document).trigger('ccm_consent_changed', [consent]); // 重新加载被阻止的脚本 this.loadBlockedScripts(); }, // 保存自定义同意设置 saveCustomConsent: function() { this.saveConsent('custom'); this.hideSettingsModal(); this.showNotification('设置已保存成功!'); }, // 获取类别选择 getCategorySelections: function() { return { necessary: true, // 必要Cookie始终启用 preferences: $('#ccm-category-preferences').is(':checked'), statistics: $('#ccm-category-statistics').is(':checked'), marketing: $('#ccm-category-marketing').is(':checked') }; }, // 加载被阻止的脚本 loadBlockedScripts: function() { var consent = this.getConsent(); if (!consent || !consent.categories) { return; } // 根据同意类别加载脚本 if (consent.categories.statistics) { this.loadStatisticsScripts(); } if (consent.categories.marketing) { this.loadMarketingScripts(); } }, // 加载统计脚本 loadStatisticsScripts: function() { // 加载Google Analytics if (window.ccmConfig.gaTrackingId) { this.loadGoogleAnalytics(window.ccmConfig.gaTrackingId); } // 这里可以添加其他统计脚本 }, // 加载Google Analytics loadGoogleAnalytics: function(trackingId) { window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('config', trackingId, { 'anonymize_ip': true }); var script = document.createElement('script'); script.async = true; script.src = 'https://www.googletagmanager.com/gtag/js?id=' + trackingId; document.head.appendChild(script); }, // 加载营销脚本 loadMarketingScripts: function() { // 加载Facebook像素 if (window.ccmConfig.fbPixelId) { this.loadFacebookPixel(window.ccmConfig.fbPixelId); } // 这里可以添加其他营销脚本 }, // 加载Facebook像素 loadFacebookPixel: function(pixelId) { !function(f,b,e,v,n,t,s) {if(f.fbq)return;n=f.fbq=function(){n.callMethod? n.callMethod.apply(n,arguments):n.queue.push(arguments)}; if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0'; n.queue=[];t=b.createElement(e);t.async=!0; t.src=v;s=b.getElementsByTagName(e)[0]; s.parentNode.insertBefore(t,s)}(window, document,'script', 'https://connect.facebook.net/en_US/fbevents.js'); fbq('init', pixelId); fbq('track', 'PageView'); }, // Cookie操作辅助函数 setCookie: function(name, value, days) { var expires = ""; if (days) { var date = new Date(); date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); expires = "; expires=" + date.toUTCString(); } document.cookie = name + "=" + encodeURIComponent(value) + expires + "; path=/; SameSite=Lax"; }, getCookie: function(name) { var nameEQ = name + "="; var ca = document.cookie.split(';'); for(var i = 0; i < ca.length; i++) { var c = ca[i]; while (c.charAt(0) == ' ') c = c.substring(1, c.length); if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length, c.length); } return null; }, // 显示通知 showNotification: function(message) { var notification = $('<div class="ccm-notification">' + message + '</div>'); $('body').append(notification); notification.css({ position: 'fixed', top: '20px', right: '20px', background: '#27ae60', color: 'white', padding: '15px 20px', borderRadius: '4px', zIndex: '1000001', boxShadow: '0 2px 10px rgba(0,0,0,0.2)' }); setTimeout(function() { notification.fadeOut(300, function() { $(this).remove(); }); }, 3000); } }; // 文档加载完成后初始化 $(document).ready(function() { CCM.init(); }); // 暴露到全局作用域 window.CookieComplianceManager = CCM; })(jQuery); ### 第六部分:实现PHP后端处理逻辑 #### 6.1 创建公共功能类 在public/class-ccm-public.php中实现后端逻辑: <?phpclass CCM_Public { private $plugin_name; private $version; public function __construct() { $this->plugin_name = 'cookie-compliance-manager'; $this->version = CCM_VERSION; } // 注册前端样式和脚本 public function enqueue_styles() { wp_enqueue_style( $this->plugin_name, CCM_PLUGIN_URL . 'public/css/ccm-public.css', array(), $this->version, 'all' ); } public function enqueue_scripts() { wp_enqueue_script( 'jquery' ); wp_enqueue_script( $this->plugin_name, CCM_PLUGIN_URL . 'public/js/ccm-public.js', array('jquery'), $this->version, true ); // 传递配置到JavaScript wp_localize_script($this->plugin_name, 'ccmConfig', array( 'ajax_url' => admin_url('admin-ajax.php'), 'gaTrackingId' => get_option('ccm_ga_tracking_id', ''), 'fbPixelId' => get_option('ccm_fb_pixel_id', ''), 'nonce' => wp_create_nonce('ccm_nonce') )); } // 插入Cookie同意横幅 public function insert_cookie_consent_banner() { if ($this->should_show_banner()) { ?> <div class="ccm-cookie-banner" id="ccm-cookie-banner"> <div class="ccm-banner-content"> <div class="ccm-banner-text"> <h3>Cookie设置</h3> <p>我们使用Cookie来提升您的浏览体验、分析网站流量并个性化内容。点击"接受所有"即表示您同意我们使用所有Cookie。您可以通过"Cookie设置"管理您的偏好。了解更多,请查看我们的<a href="<?php echo get_privacy_policy_url(); ?>">隐私政策</a>。</p> </div> <div class="ccm-banner-actions"> <button type="button" class="ccm-btn ccm-btn-accept-all">接受所有</button> <button type="button" class="ccm-btn ccm-btn-settings">Cookie设置</button> <button type="button" class="ccm-btn ccm-btn-reject-all">拒绝非必要</button> </div> </div> </div> <?php } } // 插入Cookie设置模态窗口 public function insert_cookie_settings_modal() { ?> <div class="ccm-settings-modal" id="ccm-settings-modal"> <div class="ccm-modal-content"> <div class="ccm-modal-header"> <h2>Cookie偏好设置</h2> <button type="button" class="ccm-close-modal">&times;</button> </div> <div class="ccm-modal-body"> <p>您可以选择接受或拒绝不同类型的Cookie。必要Cookie对于网站基本功能是必需的,无法被拒绝。</p> <div class="ccm-cookie-category"> <div class="ccm-category-header"> <h3>必要Cookie</h3> <label class="ccm-category-toggle"> <input type="checkbox" id="ccm-category-necessary" checked disabled> <span class="ccm-toggle-slider"></span> </label> </div> <div class="ccm-category-description"> <p>这些Cookie对于网站的基本功能是必需的,无法被禁用。它们通常仅针对您所做的操作(例如设置隐私偏好、登录或填写表单)而设置。</p> </div> </div> <div class="ccm-cookie-category"> <div class="ccm-category-header"> <h3>偏好Cookie</h3> <label class="ccm-category-toggle"> <input type="checkbox" id="ccm-category-preferences"> <span class="ccm-toggle-slider"></span> </label> </div> <div class="ccm-category-description"> <p>这些Cookie使网站能够记住您所做的选择(例如用户名、语言或地区),并提供增强的个性化功能。</p> </div> </div> <div class="ccm-cookie-category"> <div class="ccm-category-header"> <h3>统计Cookie</h3> <label class="ccm-category-toggle"> <input type="checkbox" id="ccm-category-statistics"> <span class="ccm-toggle-slider"></span> </label> </div> <div class="ccm-category-description"> <p>这些Cookie帮助我们了解访问者如何与网站互动,收集匿名信息用于改进网站功能。</p> <div class="ccm-cookie-list"> <div class="ccm-cookie-item"> <span>Google Analytics</span> <span>分析用户行为</span> </div> </div> </div> </div> <div class="ccm-cookie-category"> <div class="ccm-category-header"> <h3>营销Cookie</h3> <label class="ccm-category-toggle"> <input type="checkbox" id="ccm-category-marketing"> <span class="ccm-toggle-slider"></span> </label> </div> <div class="ccm-category-description"> <p>这些Cookie用于跟踪访问者跨网站的浏览习惯,以显示更相关的广告。</p> <div class="ccm-cookie-list"> <div class="ccm-cookie-item"> <span>Facebook Pixel</span> <span>广告效果跟踪</span> </div> </div> </div> </div> </div> <div class="ccm-modal-footer"> <button type="button" class="ccm-btn ccm-btn-accept-all">接受所有</button> <button type="button" class="ccm-btn ccm-btn-save-settings">保存设置</button> <button type="button" class="ccm-btn ccm-btn-reject-all">仅接受必要</button> </div> </div> </div> <?php } // 处理AJAX请求 public function handle_ajax_requests() { add_action('wp_ajax_ccm_save_consent', array($this, 'ajax_save_consent')); add_action('wp_ajax_nopriv_ccm_save_consent', array($this, 'ajax_save_consent')); add_action('wp_ajax_ccm_get_consent', array($this, 'ajax_get_consent')); add_action('wp_ajax_nopriv_ccm_get_consent', array($this, 'ajax_get_consent')); } // AJAX保存同意设置 public function ajax_save_consent() { check_ajax_referer('ccm_nonce', 'nonce'); $consent_data = array( 'version' => '1.0', 'date' => current_time('mysql'), 'ip_address' => $this->get_user_ip(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'categories' => array( 'necessary' => true, 'preferences' => isset($_POST['preferences']) ? filter_var($_POST['preferences'], FILTER_VALIDATE_BOOLEAN) : false, 'statistics' => isset($_POST['statistics']) ? filter_var($_POST['statistics'], FILTER_VALIDATE_BOOLEAN) : false, 'marketing' => isset($_POST['marketing']) ? filter_var($_POST['marketing'], FILTER_VALIDATE_BOOLEAN) : false ) ); // 保存到数据库 $this->save_consent_to_db($consent_data); // 设置Cookie $this->set_consent_cookie($consent_data); wp_send_json_success(array( 'message' => '同意设置已保存', 'consent' => $consent_data )); } // AJAX获取同意设置 public function ajax_get_consent() { $consent = $this->get_user_consent(); if ($consent) { wp_send_json_success($consent); } else { wp_send_json_error('未找到同意记录'); } } // 保存同意到数据库 private function save_consent_to_db($consent_data) { global $wpdb; $table_name = $wpdb->prefix . 'ccm_consents'; $data = array( 'user_id' => get_current_user_id(), 'ip_address' => $consent_data['ip_address'], 'user_agent' => $consent_data['user_agent'], 'consent_data' => json_encode($consent_data), 'created_at' => current_time('mysql') ); $wpdb->insert($table_name, $data); } // 设置同意Cookie private function set_consent_cookie($consent_data) { $cookie_value = json_encode($consent_data); $expiry = time() + (365 * 24 * 60 * 60); // 1年 setcookie('ccm_consent', $cookie_value, $expiry, '/', '', is_ssl(), true); } // 获取用户同意状态 private function get_user_consent() { if (isset($_COOKIE['ccm_consent'])) { return json_decode(stripslashes($_COOKIE['ccm_consent']), true); } return null; } // 检查是否应该显示横幅 private function should_show_banner() { // 如果用户已经做出选择,不显示横幅 $consent = $this->get_user_consent(); if ($consent && isset($consent['date'])) { return false; } // 检查是否在管理页面 if (is_admin()) { return false; } // 检查是否在特定页面排除 $excluded_pages = get_option('ccm_excluded_pages', array()); if (is_page($excluded_pages)) { return false; } return true; } // 获取用户IP地址 private function get_user_ip() { $ip_keys = array('HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'HTTP_X_FORWARDED', 'HTTP_X_CLUSTER_CLIENT_IP', 'HTTP_FORWARDED_FOR', 'HTTP_FORWARDED', 'REMOTE_ADDR'); foreach ($ip_keys as $key) { if (array_key_exists($key, $_SERVER) === true) { foreach (explode(',', $_SERVER[$key]) as $ip) { $ip = trim($ip); if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE) !== false) { return $ip; } } } } return $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; } } ### 第七部分:创建管理后台界面 #### 7.1 构建管理设置页面 在admin/class-ccm-admin.php中创建管理界面: <?phpclass CCM_Admin { private $plugin_name; private $version; public function __construct() { $this->plugin_name = 'cookie-compliance-manager'; $this->version = CCM_VERSION; } // 注册管理菜单 public function add_admin_menu() { add_menu_page( 'Cookie合规管理', 'Cookie合规', 'manage_options', 'cookie-compliance-manager', array($this, 'display_admin_page'), 'dashicons-shield', 80 ); add_submenu_page( 'cookie-compliance-manager', '设置', '设置', 'manage_options', 'cookie-compliance-settings', array($this, 'display_settings_page') ); add_submenu_page( 'cookie-compliance-manager', '同意记录', '同意记录', 'manage_options', 'cookie-compliance-consents', array($this, 'display_consents_page') ); } // 显示主管理页面 public function display_admin_page() { ?> <div class="wrap"> <h1>Cookie合规管理</h1> <div class="ccm-admin-container"> <div class="ccm-admin-header"> <div class="ccm-stats-cards"> <div class="ccm-stat-card"> <h3>总同意数</h3> <p class="ccm-stat-number"><?php echo $this->get_total_consents(); ?></p> </div> <div class="ccm-stat-card"> <h3>今日同意</h3> <p class="ccm-stat-number"><?php echo $this->get_today_consents(); ?></p> </div> <div class="ccm-stat-card"> <h3>接受率</h3> <p class="ccm-stat-number"><?php echo $this->get_acceptance_rate(); ?>%</p> </div> </div> </div> <div class="ccm-admin-content"> <div class="ccm-welcome-panel"> <h2>欢迎使用Cookie合规管理器</h2> <p>确保您的网站符合GDPR、CCPA等数据保护法规要求。</p> <div class="ccm-quick-links"> <a href="?page=cookie-compliance-settings" class="button button-primary">配置设置</a> <a href="?page=cookie-compliance-consents" class="button">查看同意记录</a> <a href="#" class="button">生成隐私政策</a> </div> </div> <div class="ccm-compliance-status"> <h3>合规状态检查</h3> <ul class="ccm-status-list"> <li class="ccm-status-item <?php echo $this->check_banner_status() ? 'ccm-status-ok' : 'ccm-status-error'; ?>"> <span

发表评论

详细教程,为网站打造内嵌的在线视频剪辑与简易制作工具

详细教程:为网站打造内嵌的在线视频剪辑与简易制作工具,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:为什么网站需要内嵌视频编辑工具? 在当今数字内容为王的时代,视频已成为最受欢迎的内容形式之一。据统计,全球互联网用户每天观看的视频时长超过10亿小时,而超过85%的企业使用视频作为营销工具。然而,对于许多网站运营者来说,视频制作一直是个门槛——用户需要离开网站,使用专业软件编辑视频,再上传回网站,这一流程既繁琐又影响用户体验。 想象一下,如果您的WordPress网站能够直接提供在线视频剪辑功能,让用户无需离开页面即可完成视频裁剪、合并、添加字幕和特效等操作,这将是多么强大的竞争优势!无论是教育平台让学生编辑课程录像,电商网站让卖家制作产品展示视频,还是社交平台让用户创作内容,内嵌视频工具都能显著提升用户参与度和内容产出效率。 本教程将详细指导您如何通过WordPress代码二次开发,为您的网站打造一个功能完整的在线视频剪辑与制作工具。我们将从基础原理讲起,逐步实现核心功能,最终整合成一个可直接使用的解决方案。 第一部分:准备工作与环境搭建 1.1 理解WordPress插件开发基础 在开始之前,我们需要了解WordPress插件的基本结构。WordPress插件是独立的代码模块,可以扩展WordPress的功能而不修改核心代码。一个基本的插件至少包含: 主PHP文件(包含插件头信息) 必要的JavaScript和CSS文件 可选的资源文件(如图像、字体等) 插件头信息示例: <?php /** * Plugin Name: 在线视频剪辑工具 * Plugin URI: https://yourwebsite.com/video-editor * Description: 为WordPress网站添加在线视频剪辑功能 * Version: 1.0.0 * Author: 您的名字 * License: GPL v2 or later */ 1.2 开发环境配置 本地开发环境:建议使用Local by Flywheel、XAMPP或MAMP搭建本地WordPress环境 代码编辑器:推荐VS Code、PHPStorm或Sublime Text 浏览器开发者工具:Chrome或Firefox的开发者工具是调试前端代码的必备 版本控制:初始化Git仓库管理代码版本 1.3 关键技术栈选择 为了实现在线视频编辑,我们需要选择合适的技术方案: 前端视频处理:FFmpeg.js或WebAssembly版本的FFmpeg 前端框架:React或Vue.js(本教程使用纯JavaScript以降低复杂度) 视频播放器:Video.js或plyr.js UI组件:自定义CSS或使用轻量级UI库 后端处理:PHP + WordPress REST API 1.4 创建插件基本结构 在wp-content/plugins目录下创建"video-editor-tool"文件夹,并建立以下结构: video-editor-tool/ ├── video-editor.php # 主插件文件 ├── includes/ # PHP类文件 │ ├── class-video-processor.php │ ├── class-video-library.php │ └── class-ajax-handler.php ├── admin/ # 后台相关文件 │ ├── css/ │ ├── js/ │ └── admin-page.php ├── public/ # 前端相关文件 │ ├── css/ │ ├── js/ │ ├── libs/ # 第三方库 │ └── templates/ # 前端模板 ├── assets/ # 静态资源 │ ├── images/ │ └── fonts/ └── vendor/ # 第三方PHP库 第二部分:核心视频处理功能实现 2.1 集成FFmpeg.js进行客户端视频处理 FFmpeg.js是FFmpeg的JavaScript端口,允许在浏览器中直接处理视频文件。由于视频处理是计算密集型任务,我们将在客户端进行基本操作以减少服务器压力。 步骤1:引入FFmpeg.js <!-- 在编辑器页面添加 --> <script src="<?php echo plugin_dir_url(__FILE__); ?>public/libs/ffmpeg/ffmpeg.min.js"></script> 步骤2:创建视频处理管理器 // public/js/video-processor.js class VideoProcessor { constructor() { this.ffmpeg = null; this.videoElements = []; this.isFFmpegLoaded = false; this.initFFmpeg(); } async initFFmpeg() { try { // 加载FFmpeg const { createFFmpeg, fetchFile } = FFmpeg; this.ffmpeg = createFFmpeg({ log: true }); await this.ffmpeg.load(); this.isFFmpegLoaded = true; console.log('FFmpeg加载成功'); } catch (error) { console.error('FFmpeg加载失败:', error); } } // 裁剪视频 async trimVideo(inputFile, startTime, endTime) { if (!this.isFFmpegLoaded) { throw new Error('FFmpeg未加载完成'); } // 将视频文件写入FFmpeg文件系统 this.ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(inputFile)); // 执行裁剪命令 await this.ffmpeg.run( '-i', 'input.mp4', '-ss', startTime.toString(), '-to', endTime.toString(), '-c', 'copy', 'output.mp4' ); // 读取输出文件 const data = this.ffmpeg.FS('readFile', 'output.mp4'); return new Blob([data.buffer], { type: 'video/mp4' }); } // 合并多个视频 async mergeVideos(videoFiles) { // 创建文件列表 const fileList = videoFiles.map((file, index) => { const filename = `input${index}.mp4`; this.ffmpeg.FS('writeFile', filename, await fetchFile(file)); return `file '${filename}'`; }).join('n'); // 写入文件列表到FFmpeg文件系统 this.ffmpeg.FS('writeFile', 'filelist.txt', fileList); // 执行合并命令 await this.ffmpeg.run( '-f', 'concat', '-safe', '0', '-i', 'filelist.txt', '-c', 'copy', 'output.mp4' ); const data = this.ffmpeg.FS('readFile', 'output.mp4'); return new Blob([data.buffer], { type: 'video/mp4' }); } // 提取音频 async extractAudio(videoFile) { this.ffmpeg.FS('writeFile', 'input.mp4', await fetchFile(videoFile)); await this.ffmpeg.run( '-i', 'input.mp4', '-vn', '-acodec', 'libmp3lame', 'output.mp3' ); const data = this.ffmpeg.FS('readFile', 'output.mp3'); return new Blob([data.buffer], { type: 'audio/mpeg' }); } // 添加水印 async addWatermark(videoFile, watermarkImage, position = 'bottom-right') { // 实现水印添加逻辑 // 注意:这需要更复杂的FFmpeg命令 } } 2.2 视频时间轴与预览组件 时间轴是视频编辑器的核心组件,允许用户可视化地操作视频。 // public/js/timeline-editor.js class TimelineEditor { constructor(containerId, options = {}) { this.container = document.getElementById(containerId); this.videoElement = null; this.timelineCanvas = null; this.duration = 0; this.currentTime = 0; this.videoClips = []; this.isDragging = false; this.init(options); } init(options) { // 创建时间轴HTML结构 this.container.innerHTML = ` <div class="video-preview-container"> <video id="preview-video" controls></video> </div> <div class="timeline-container"> <canvas id="timeline-canvas"></canvas> <div class="timeline-controls"> <div class="playhead" id="playhead"></div> <div class="trim-handle left-handle" id="trim-left"></div> <div class="trim-handle right-handle" id="trim-right"></div> </div> </div> <div class="timeline-toolbar"> <button class="btn-cut" title="剪切">✂️</button> <button class="btn-split" title="分割">🔪</button> <button class="btn-delete" title="删除">🗑️</button> <button class="btn-add-text" title="添加文字">T</button> <button class="btn-add-transition" title="添加转场">✨</button> </div> `; this.videoElement = document.getElementById('preview-video'); this.timelineCanvas = document.getElementById('timeline-canvas'); this.playhead = document.getElementById('playhead'); this.setupEventListeners(); this.setupCanvas(); } setupCanvas() { const ctx = this.timelineCanvas.getContext('2d'); const width = this.container.offsetWidth; const height = 120; this.timelineCanvas.width = width; this.timelineCanvas.height = height; // 绘制时间轴背景 this.drawTimeline(ctx, width, height); } drawTimeline(ctx, width, height) { // 绘制背景 ctx.fillStyle = '#2d2d2d'; ctx.fillRect(0, 0, width, height); // 绘制时间刻度 const seconds = this.duration; const pixelsPerSecond = width / seconds; ctx.strokeStyle = '#555'; ctx.lineWidth = 1; ctx.fillStyle = '#888'; ctx.font = '10px Arial'; for (let i = 0; i <= seconds; i++) { const x = i * pixelsPerSecond; // 主刻度(每秒) ctx.beginPath(); ctx.moveTo(x, height - 20); ctx.lineTo(x, height); ctx.stroke(); // 时间标签 if (i % 5 === 0) { const timeText = this.formatTime(i); ctx.fillText(timeText, x - 10, height - 25); } // 次刻度(每0.5秒) if (i < seconds) { const midX = x + pixelsPerSecond / 2; ctx.beginPath(); ctx.moveTo(midX, height - 15); ctx.lineTo(midX, height); ctx.stroke(); } } // 绘制视频片段 this.videoClips.forEach((clip, index) => { this.drawVideoClip(ctx, clip, index, pixelsPerSecond, height); }); } drawVideoClip(ctx, clip, index, pixelsPerSecond, height) { const startX = clip.startTime * pixelsPerSecond; const clipWidth = (clip.endTime - clip.startTime) * pixelsPerSecond; // 绘制片段背景 ctx.fillStyle = index % 2 === 0 ? '#4a9eff' : '#6bb7ff'; ctx.fillRect(startX, 10, clipWidth, height - 40); // 绘制片段边框 ctx.strokeStyle = '#2a7fff'; ctx.lineWidth = 2; ctx.strokeRect(startX, 10, clipWidth, height - 40); // 绘制片段标签 ctx.fillStyle = 'white'; ctx.font = '12px Arial'; ctx.fillText(`片段 ${index + 1}`, startX + 5, 30); // 绘制时长 const durationText = this.formatTime(clip.endTime - clip.startTime); ctx.fillText(durationText, startX + 5, 50); } formatTime(seconds) { const mins = Math.floor(seconds / 60); const secs = Math.floor(seconds % 60); return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; } setupEventListeners() { // 视频时间更新事件 this.videoElement.addEventListener('timeupdate', () => { this.updatePlayheadPosition(); }); // 时间轴点击事件 this.timelineCanvas.addEventListener('click', (e) => { const rect = this.timelineCanvas.getBoundingClientRect(); const x = e.clientX - rect.left; const time = (x / this.timelineCanvas.width) * this.duration; this.videoElement.currentTime = time; this.updatePlayheadPosition(); }); // 拖拽事件 this.setupDragEvents(); } updatePlayheadPosition() { if (!this.duration) return; const percentage = (this.videoElement.currentTime / this.duration) * 100; this.playhead.style.left = `${percentage}%`; } setupDragEvents() { // 实现拖拽逻辑 // 包括播放头拖拽、片段拖拽、裁剪手柄拖拽等 } loadVideo(videoFile) { return new Promise((resolve, reject) => { const url = URL.createObjectURL(videoFile); this.videoElement.src = url; this.videoElement.onloadedmetadata = () => { this.duration = this.videoElement.duration; this.videoClips = [{ startTime: 0, endTime: this.duration, file: videoFile }]; this.setupCanvas(); resolve(); }; this.videoElement.onerror = reject; }); } } 2.3 文字与特效添加功能 // public/js/text-effects-editor.js class TextEffectsEditor { constructor() { this.textTracks = []; this.effects = []; this.currentText = null; } // 添加文字轨道 addTextTrack(text, options = {}) { const textTrack = { id: Date.now(), text: text, startTime: options.startTime || 0, duration: options.duration || 5, style: { fontSize: options.fontSize || '24px', fontFamily: options.fontFamily || 'Arial', color: options.color || '#ffffff', backgroundColor: options.backgroundColor || 'rgba(0,0,0,0.5)', position: options.position || 'bottom-center', animation: options.animation || 'fade' } }; this.textTracks.push(textTrack); return textTrack; } // 渲染文字到视频 renderTextToVideo(videoElement, textTrack) { const overlay = document.createElement('div'); overlay.className = 'text-overlay'; Object.assign(overlay.style, { position: 'absolute', color: textTrack.style.color, fontSize: textTrack.style.fontSize, fontFamily: textTrack.style.fontFamily, backgroundColor: textTrack.style.backgroundColor, padding: '10px', borderRadius: '5px', ...this.getPositionStyle(textTrack.style.position) }); overlay.textContent = textTrack.text; // 添加动画类 overlay.classList.add(`text-animation-${textTrack.style.animation}`); // 添加到视频容器 const videoContainer = videoElement.parentElement; videoContainer.style.position = 'relative'; videoContainer.appendChild(overlay); // 设置显示时间 setTimeout(() => { overlay.style.display = 'block'; }, textTrack.startTime * 1000); setTimeout(() => { overlay.style.display = 'none'; overlay.remove(); }, (textTrack.startTime + textTrack.duration) * 1000); } getPositionStyle(position) { const positions = { 'top-left': { top: '10px', left: '10px' }, 'top-center': { top: '10px', left: '50%', transform: 'translateX(-50%)' }, 'top-right': { top: '10px', right: '10px' }, 'middle-left': { top: '50%', left: '10px', transform: 'translateY(-50%)' }, 'middle-center': { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' }, 'middle-right': { top: '50%', right: '10px', transform: 'translateY(-50%)' }, 'bottom-left': { bottom: '10px', left: '10px' }, 'bottom-center': { bottom: '10px', left: '50%', transform: 'translateX(-50%)' }, 'bottom-right': { bottom: '10px', right: '10px' } }; return positions[position] || positions['bottom-center']; } // 添加转场效果 addTransitionEffect(videoClips, transitionType = 'fade') { const transitions = { 'fade': { duration: 1, type: 'fade' }, 'slide': { duration: 1, type: 'slide', direction: 'right' }, 'zoom': { duration: 1, type: 'zoom' }, 'rotate': { duration: 1, type: 'rotate' } }; this.effects.push({ type: 'transition', transition: transitions[transitionType], position: videoClips.length > 0 ? videoClips.length - 1 : 0 }); } } 第三部分:WordPress后端集成 3.1 创建视频处理API端点 <?php // includes/class-rest-api.php class Video_Editor_REST_API { private $namespace = 'video-editor/v1'; public function __construct() { add_action('rest_api_init', [$this, 'register_routes']); } public function register_routes() { // 上传视频端点 register_rest_route($this->namespace, '/upload', [ 'methods' => 'POST', 'callback' => [$this, 'handle_video_upload'], 'permission_callback' => [$this, 'check_permission'], 'args' => [ 'file' => [ 'required' => true, 'validate_callback' => function($file) { return !empty($file); } ], 'title' => [ 'required' => false, 'sanitize_callback' => 'sanitize_text_field' ] ] ]); // 保存项目端点 register_rest_route($this->namespace, '/project/save', [ 'methods' => 'POST', 'callback' => [$this, 'save_project'], 'permission_callback' => [$this, 'check_permission'] ]); // 获取项目列表 register_rest_route($this->namespace, '/projects', [ 'methods' => 'GET', 'callback' => [$this, 'get_projects'], 'permission_callback' => [$this, 'check_permission'] ]); // 服务器端视频处理(用于复杂操作) register_rest_route($this->namespace, '/process', [ 'methods' => 'POST', 'callback' => [$this, 'process_video'], 'permission_callback' => [$this, 'check_permission'] ]); } public function handle_video_upload($request) { $files = $request->get_file_params(); if (empty($files['video_file'])) { return new WP_Error('no_file', '没有上传文件', ['status' => 400]); } $file = $files['video_file']; // 检查文件类型 $allowed_types = ['video/mp4', 'video/webm', 'video/ogg', 'video/quicktime']; if (!in_array($file['type'], $allowed_types)) { return new WP_Error('invalid_type', '不支持的文件格式', ['status' => 400]); } // 检查文件大小(限制为500MB) $max_size = 500 * 1024 * 1024; if ($file['size'] > $max_size) { return new WP_Error('file_too_large', '文件太大,最大支持500MB', ['status' => 400]); } // 创建上传目录 $upload_dir = wp_upload_dir(); $video_dir = $upload_dir['basedir'] . '/video-editor'; if (!file_exists($video_dir)) { wp_mkdir_p($video_dir); } // 生成唯一文件名 $filename = wp_unique_filename($video_dir, $file['name']); $filepath = $video_dir . '/' . $filename; // 移动文件 if (move_uploaded_file($file['tmp_name'], $filepath)) { // 保存到媒体库 $attachment = [ 'post_mime_type' => $file['type'], 'post_title' => $request->get_param('title') ?: preg_replace('/.[^.]+$/', '', $file['name']), 'post_content' => '', 'post_status' => 'inherit', 'guid' => $upload_dir['baseurl'] . '/video-editor/' . $filename ]; $attach_id = wp_insert_attachment($attachment, $filepath); // 生成视频元数据 require_once(ABSPATH . 'wp-admin/includes/image.php'); $attach_data = wp_generate_attachment_metadata($attach_id, $filepath); wp_update_attachment_metadata($attach_id, $attach_data); // 获取视频信息 $video_info = $this->get_video_info($filepath); return [ 'success' => true, 'id' => $attach_id, 'url' => wp_get_attachment_url($attach_id), 'thumbnail' => $this->generate_thumbnail($attach_id, $filepath), 'duration' => $video_info['duration'] ?? 0, 'dimensions' => $video_info['dimensions'] ?? ['width' => 0, 'height' => 0] ]; } return new WP_Error('upload_failed', '文件上传失败', ['status' => 500]); } private function get_video_info($filepath) { if (!function_exists('exec')) { return ['duration' => 0, 'dimensions' => ['width' => 0, 'height' => 0]]; } // 使用FFmpeg获取视频信息 $cmd = "ffprobe -v error -show_entries format=duration -show_entries stream=width,height -of json " . escapeshellarg($filepath); @exec($cmd, $output, $return_var); if ($return_var === 0 && !empty($output)) { $data = json_decode(implode('', $output), true); return [ 'duration' => $data['format']['duration'] ?? 0, 'dimensions' => [ 'width' => $data['streams'][0]['width'] ?? 0, 'height' => $data['streams'][0]['height'] ?? 0 ] ]; } return ['duration' => 0, 'dimensions' => ['width' => 0, 'height' => 0]]; } private function generate_thumbnail($attach_id, $filepath) { $upload_dir = wp_upload_dir(); $thumb_dir = $upload_dir['basedir'] . '/video-editor/thumbs'; if (!file_exists($thumb_dir)) { wp_mkdir_p($thumb_dir); } $thumb_name = 'thumb-' . $attach_id . '.jpg'; $thumb_path = $thumb_dir . '/' . $thumb_name; // 使用FFmpeg生成缩略图 $cmd = "ffmpeg -i " . escapeshellarg($filepath) . " -ss 00:00:01 -vframes 1 -q:v 2 " . escapeshellarg($thumb_path); @exec($cmd, $output, $return_var); if ($return_var === 0 && file_exists($thumb_path)) { return $upload_dir['baseurl'] . '/video-editor/thumbs/' . $thumb_name; } // 如果FFmpeg失败,使用默认缩略图 return plugin_dir_url(__FILE__) . '../assets/images/default-thumb.jpg'; } public function save_project($request) { $user_id = get_current_user_id(); $project_data = $request->get_json_params(); if (!$user_id) { return new WP_Error('unauthorized', '用户未登录', ['status' => 401]); } $project_id = wp_insert_post([ 'post_title' => $project_data['title'] ?: '未命名项目', 'post_content' => wp_json_encode($project_data['content']), 'post_status' => 'draft', 'post_type' => 'video_project', 'post_author' => $user_id, 'meta_input' => [ '_video_project_data' => $project_data['timeline'], '_video_duration' => $project_data['duration'], '_video_thumbnail' => $project_data['thumbnail'] ] ]); if (is_wp_error($project_id)) { return $project_id; } return [ 'success' => true, 'project_id' => $project_id, 'message' => '项目保存成功' ]; } public function get_projects($request) { $user_id = get_current_user_id(); if (!$user_id) { return new WP_Error('unauthorized', '用户未登录', ['status' => 401]); } $args = [ 'post_type' => 'video_project', 'author' => $user_id, 'posts_per_page' => 20, 'post_status' => ['draft', 'publish'] ]; $projects = get_posts($args); $formatted_projects = []; foreach ($projects as $project) { $formatted_projects[] = [ 'id' => $project->ID, 'title' => $project->post_title, 'created' => $project->post_date, 'modified' => $project->post_modified, 'thumbnail' => get_post_meta($project->ID, '_video_thumbnail', true), 'duration' => get_post_meta($project->ID, '_video_duration', true) ]; } return $formatted_projects; } public function process_video($request) { // 服务器端复杂视频处理 $data = $request->get_json_params(); $operation = $data['operation'] ?? ''; switch ($operation) { case 'concat': return $this->concatenate_videos($data['videos']); case 'compress': return $this->compress_video($data['video'], $data['options']); case 'add_watermark': return $this->add_watermark_server($data['video'], $data['watermark']); default: return new WP_Error('invalid_operation', '不支持的操作', ['status' => 400]); } } private function concatenate_videos($video_urls) { // 实现服务器端视频合并 // 注意:这需要服务器安装FFmpeg } public function check_permission($request) { // 检查用户权限 return current_user_can('edit_posts') || apply_filters('video_editor_allow_anonymous', false); } } 3.2 创建自定义文章类型存储视频项目 <?php // includes/class-custom-post-type.php class Video_Project_Post_Type { public function __construct() { add_action('init', [$this, 'register_post_type']); add_action('add_meta_boxes', [$this, 'add_meta_boxes']); add_action('save_post_video_project', [$this, 'save_meta_data']); } public function register_post_type() { $labels = [ '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 = [ 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => ['slug' => 'video-project'], 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 20, 'menu_icon' => 'dashicons-video-alt3', 'supports' => ['title', 'editor', 'author', 'thumbnail'], 'show_in_rest' => true ]; register_post_type('video_project', $args); } public function add_meta_boxes() { add_meta_box( 'video_project_meta', '项目详情', [$this, 'render_meta_box'], 'video_project', 'normal', 'high' ); add_meta_box( 'video_project_preview', '视频预览', [$this, 'render_preview_box'], 'video_project', 'side', 'high' ); } public function render_meta_box($post) { wp_nonce_field('video_project_meta', 'video_project_nonce'); $project_data = get_post_meta($post->ID, '_video_project_data', true); $duration = get_post_meta($post->ID, '_video_duration', true); $video_url = get_post_meta($post->ID, '_video_final_url', true); ?> <div class="video-project-meta"> <p> <label for="video_duration">视频时长:</label> <input type="text" id="video_duration" name="video_duration" value="<?php echo esc_attr($duration); ?>" readonly> <span>秒</span> </p> <p> <label for="video_url">最终视频URL:</label> <input type="url" id="video_url" name="video_url" value="<?php echo esc_url($video_url); ?>" class="widefat"> </p> <p> <label for="project_status">项目状态:</label> <select id="project_status" name="project_status"> <option value="draft" <?php selected(get_post_status($post->ID), 'draft'); ?>>草稿</option> <option value="publish" <?php selected(get_post_status($post->ID), 'publish'); ?>>发布</option> <option value="processing" <?php selected(get_post_meta($post->ID, '_project_status', true), 'processing'); ?>>处理中</option> <option value="completed" <?php selected(get_post_meta($post->ID, '_project_status', true), 'completed'); ?>>已完成</option> </select> </p> <div class="project-data"> <h4>项目数据(JSON格式)</h4> <textarea name="project_data" rows="10" class="widefat" readonly><?php echo esc_textarea($project_data); ?></textarea> </div> </div> <style> .video-project-meta p { margin: 15px 0; } .video-project-meta label { display: inline-block; width: 120px; font-weight: bold; } .project-data textarea { font-family: monospace; font-size: 12px; } </style> <?php } public function render_preview_box($post) { $thumbnail = get_post_meta($post->ID, '_video_thumbnail', true); $video_url = get_post_meta($post->ID, '_video_final_url', true); ?> <div class="video-preview-sidebar"> <?php if ($thumbnail): ?> <div class="video-thumbnail"> <img src="<?php echo esc_url($thumbnail); ?>" alt="视频缩略图" style="max-width:100%; height:auto;"> </div> <?php endif; ?> <?php if ($video_url): ?> <div class="video-preview"> <video controls style="width:100%; max-height:200px;"> <source src="<?php echo esc_url($video_url); ?>" type="video/mp4"> 您的浏览器不支持视频播放 </video> </div> <p style="text-align:center; margin-top:10px;"> <a href="<?php echo esc_url($video_url); ?>" class="button button-primary" target="_blank"> <span class="dashicons dashicons-external"></span> 查看完整视频 </a> </p> <?php else: ?> <p class="description">视频尚未生成</p> <button type="button" id="generate_video" class="button button-secondary"> <span class="dashicons dashicons-video-alt3"></span> 生成最终视频 </button> <?php endif; ?> </div> <script> jQuery(document).ready(function($) { $('#generate_video').on('click', function() { var button = $(this); var postId = <?php echo $post->ID; ?>; button.prop('disabled', true).text('生成中...'); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'generate_final_video', post_id: postId, nonce: '<?php echo wp_create_nonce('generate_video_' . $post->ID); ?>' }, success: function(response) { if (response.success) { alert('视频生成成功!'); location.reload(); } else { alert('生成失败:' + response.data); button.prop('disabled', false).text('生成最终视频'); } }, error: function() { alert('请求失败,请重试'); button.prop('disabled', false).text('生成最终视频'); } }); }); }); </script> <?php } public function save_meta_data($post_id) { // 验证nonce if (!isset($_POST['video_project_nonce']) || !wp_verify_nonce($_POST['video_project_nonce'], 'video_project_meta')) { return; } // 检查自动保存 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } // 检查权限 if (!current_user_can('edit_post', $post_id)) { return; } // 保存元数据

发表评论

WordPress高级教程,开发集成在线问卷调查与自动报告生成器

WordPress高级教程:开发集成在线问卷调查与自动报告生成器 引言:WordPress作为企业级应用开发平台 WordPress早已超越了简单的博客系统范畴,成为全球最受欢迎的内容管理系统(CMS),驱动着超过40%的网站。其强大的插件架构、丰富的API接口和庞大的开发者社区,使得WordPress能够胜任各种复杂的企业级应用开发。本教程将深入探讨如何通过WordPress代码二次开发,实现一个集在线问卷调查与自动报告生成器于一体的高级功能模块。 在当今数据驱动的商业环境中,问卷调查和数据分析工具已成为企业决策的重要支撑。传统上,企业可能需要购买昂贵的专业调查工具或委托定制开发,而通过WordPress二次开发,我们可以以更低的成本、更高的集成度实现这一功能,同时充分利用WordPress的用户管理、权限控制和内容展示能力。 系统架构设计 功能需求分析 在开始开发之前,我们需要明确系统的核心需求: 问卷调查功能: 支持多种题型(单选、多选、文本输入、评分等) 问题逻辑跳转 问卷分页与进度保存 响应式设计,适配移动设备 报告生成功能: 基于问卷结果的自动分析 可视化图表生成 可定制的报告模板 多种格式导出(PDF、Word、HTML) 管理功能: 问卷创建与管理界面 数据收集与统计分析 用户权限与访问控制 报告模板管理 技术架构设计 我们将采用分层架构设计,确保系统的可维护性和扩展性: 数据层:使用WordPress自定义数据表存储问卷、回答和报告数据 业务逻辑层:处理问卷逻辑、数据分析和报告生成 表示层:前端界面和用户交互 集成层:与WordPress核心功能(用户、权限、主题)的集成 数据库设计与实现 自定义数据表设计 虽然WordPress提供了自定义文章类型(CPT)和元数据存储机制,但对于问卷调查这种结构化数据,我们建议创建专门的数据表以提高查询效率。 -- 问卷主表 CREATE TABLE wp_surveys ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, status ENUM('draft', 'published', 'closed') DEFAULT 'draft', created_by INT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, settings TEXT, -- JSON格式存储问卷设置 FOREIGN KEY (created_by) REFERENCES wp_users(ID) ); -- 问题表 CREATE TABLE wp_survey_questions ( id INT AUTO_INCREMENT PRIMARY KEY, survey_id INT NOT NULL, question_text TEXT NOT NULL, question_type ENUM('single_choice', 'multiple_choice', 'text', 'rating', 'matrix') NOT NULL, options TEXT, -- JSON格式存储选项 is_required BOOLEAN DEFAULT FALSE, sort_order INT DEFAULT 0, logic_rules TEXT, -- JSON格式存储逻辑跳转规则 FOREIGN KEY (survey_id) REFERENCES wp_surveys(id) ON DELETE CASCADE ); -- 回答表 CREATE TABLE wp_survey_responses ( id INT AUTO_INCREMENT PRIMARY KEY, survey_id INT NOT NULL, user_id INT, session_id VARCHAR(100), started_at DATETIME, completed_at DATETIME, ip_address VARCHAR(45), user_agent TEXT, FOREIGN KEY (survey_id) REFERENCES wp_surveys(id), FOREIGN KEY (user_id) REFERENCES wp_users(ID) ); -- 答案详情表 CREATE TABLE wp_survey_answers ( id INT AUTO_INCREMENT PRIMARY KEY, response_id INT NOT NULL, question_id INT NOT NULL, answer_value TEXT, FOREIGN KEY (response_id) REFERENCES wp_survey_responses(id) ON DELETE CASCADE, FOREIGN KEY (question_id) REFERENCES wp_survey_questions(id) ); -- 报告表 CREATE TABLE wp_survey_reports ( id INT AUTO_INCREMENT PRIMARY KEY, survey_id INT NOT NULL, title VARCHAR(255) NOT NULL, report_type ENUM('summary', 'individual', 'comparative') NOT NULL, template_id INT, generated_at DATETIME DEFAULT CURRENT_TIMESTAMP, content LONGTEXT, -- 报告内容,可以是HTML或序列化数据 file_path VARCHAR(500), -- 导出文件路径 FOREIGN KEY (survey_id) REFERENCES wp_surveys(id) ); 数据库操作类封装 为了与WordPress数据库操作保持一致,我们创建一个专门的数据库操作类: <?php /** * 问卷调查数据库操作类 */ class Survey_DB { private $wpdb; private $charset_collate; public function __construct() { global $wpdb; $this->wpdb = $wpdb; $this->charset_collate = $wpdb->get_charset_collate(); } /** * 创建数据表 */ public function create_tables() { require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); // 问卷主表 $sql = "CREATE TABLE {$this->wpdb->prefix}surveys ( id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(255) NOT NULL, description TEXT, status ENUM('draft', 'published', 'closed') DEFAULT 'draft', created_by INT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, settings TEXT, FOREIGN KEY (created_by) REFERENCES {$this->wpdb->prefix}users(ID) ) {$this->charset_collate};"; dbDelta($sql); // 其他表的创建代码类似... } /** * 获取问卷列表 */ public function get_surveys($args = array()) { $defaults = array( 'status' => 'published', 'per_page' => 10, 'page' => 1, 'orderby' => 'created_at', 'order' => 'DESC' ); $args = wp_parse_args($args, $defaults); $offset = ($args['page'] - 1) * $args['per_page']; $sql = $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->prefix}surveys WHERE status = %s ORDER BY {$args['orderby']} {$args['order']} LIMIT %d OFFSET %d", $args['status'], $args['per_page'], $offset ); return $this->wpdb->get_results($sql); } /** * 保存问卷回答 */ public function save_response($survey_id, $user_id, $answers) { // 开启事务 $this->wpdb->query('START TRANSACTION'); try { // 保存回答主记录 $response_data = array( 'survey_id' => $survey_id, 'user_id' => $user_id, 'completed_at' => current_time('mysql'), 'ip_address' => $this->get_client_ip(), 'user_agent' => $_SERVER['HTTP_USER_AGENT'] ); $this->wpdb->insert( "{$this->wpdb->prefix}survey_responses", $response_data ); $response_id = $this->wpdb->insert_id; // 保存每个问题的答案 foreach ($answers as $question_id => $answer) { $answer_data = array( 'response_id' => $response_id, 'question_id' => $question_id, 'answer_value' => is_array($answer) ? json_encode($answer) : $answer ); $this->wpdb->insert( "{$this->wpdb->prefix}survey_answers", $answer_data ); } // 提交事务 $this->wpdb->query('COMMIT'); return $response_id; } catch (Exception $e) { // 回滚事务 $this->wpdb->query('ROLLBACK'); return false; } } /** * 获取客户端IP */ 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; } } ?> 问卷创建与管理模块 后台管理界面开发 我们将创建一个完整的后台管理界面,用于问卷的创建、编辑和管理: <?php /** * 问卷调查管理类 */ class Survey_Admin { private $db; public function __construct() { $this->db = new Survey_DB(); // 添加管理菜单 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', 'survey-manager', array($this, 'render_main_page'), 'dashicons-feedback', 30 ); // 子菜单 add_submenu_page( 'survey-manager', '所有问卷', '所有问卷', 'manage_options', 'survey-manager', array($this, 'render_main_page') ); add_submenu_page( 'survey-manager', '新建问卷', '新建问卷', 'manage_options', 'survey-new', array($this, 'render_new_survey_page') ); add_submenu_page( 'survey-manager', '报告生成', '报告生成', 'manage_options', 'survey-reports', array($this, 'render_reports_page') ); add_submenu_page( 'survey-manager', '设置', '设置', 'manage_options', 'survey-settings', array($this, 'render_settings_page') ); } /** * 渲染主页面 */ public function render_main_page() { // 获取问卷列表 $surveys = $this->db->get_surveys(); // 加载模板 include SURVEY_PLUGIN_DIR . 'templates/admin/survey-list.php'; } /** * 渲染新建问卷页面 */ public function render_new_survey_page() { // 处理表单提交 if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_survey'])) { $this->save_survey($_POST); } // 加载模板 include SURVEY_PLUGIN_DIR . 'templates/admin/survey-editor.php'; } /** * 保存问卷 */ private function save_survey($data) { // 验证和清理数据 $survey_data = array( 'title' => sanitize_text_field($data['title']), 'description' => wp_kses_post($data['description']), 'status' => sanitize_text_field($data['status']), 'created_by' => get_current_user_id(), 'settings' => json_encode(array( 'require_login' => isset($data['require_login']) ? 1 : 0, 'limit_responses' => isset($data['limit_responses']) ? intval($data['limit_responses']) : 0, 'show_progress' => isset($data['show_progress']) ? 1 : 0, 'randomize_questions' => isset($data['randomize_questions']) ? 1 : 0 )) ); // 保存到数据库 global $wpdb; if (isset($data['survey_id']) && $data['survey_id']) { // 更新现有问卷 $wpdb->update( "{$wpdb->prefix}surveys", $survey_data, array('id' => intval($data['survey_id'])) ); $survey_id = $data['survey_id']; } else { // 创建新问卷 $wpdb->insert( "{$wpdb->prefix}surveys", $survey_data ); $survey_id = $wpdb->insert_id; } // 保存问题 if (isset($data['questions']) && is_array($data['questions'])) { $this->save_questions($survey_id, $data['questions']); } // 重定向到编辑页面 wp_redirect(admin_url('admin.php?page=survey-new&survey_id=' . $survey_id . '&saved=1')); exit; } /** * 注册管理页面脚本 */ public function enqueue_admin_scripts($hook) { // 只在我们的管理页面加载脚本 if (strpos($hook, 'survey-') === false) { return; } // 加载Vue.js用于动态界面 wp_enqueue_script('vue', 'https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js', array(), '2.6.14'); // 加载Element UI组件库 wp_enqueue_style('element-ui', 'https://unpkg.com/element-ui/lib/theme-chalk/index.css'); wp_enqueue_script('element-ui', 'https://unpkg.com/element-ui/lib/index.js', array('vue'), '2.15.6'); // 加载自定义脚本 wp_enqueue_script( 'survey-admin', SURVEY_PLUGIN_URL . 'assets/js/admin.js', array('vue', 'element-ui', 'jquery'), '1.0.0', true ); // 加载自定义样式 wp_enqueue_style( 'survey-admin', SURVEY_PLUGIN_URL . 'assets/css/admin.css', array('element-ui'), '1.0.0' ); // 传递数据到JavaScript wp_localize_script('survey-admin', 'survey_admin_data', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('survey_admin_nonce') )); } } ?> 问卷编辑器前端实现 使用Vue.js和Element UI创建动态问卷编辑器: // assets/js/admin.js (function($) { 'use strict'; // 创建Vue应用 new Vue({ el: '#survey-editor', data: { survey: { title: '', description: '', status: 'draft', settings: { require_login: false, limit_responses: 0, show_progress: true, randomize_questions: false } }, questions: [], questionTypes: [ { value: 'single_choice', label: '单选题' }, { value: 'multiple_choice', label: '多选题' }, { value: 'text', label: '文本题' }, { value: 'rating', label: '评分题' }, { value: 'matrix', label: '矩阵题' } ], activeQuestionIndex: 0 }, methods: { // 添加新问题 addQuestion: function(type) { const newQuestion = { id: Date.now(), // 临时ID question_text: '', question_type: type, options: type === 'rating' ? [{value: 1, label: '1'}, {value: 2, label: '2'}, {value: 3, label: '3'}, {value: 4, label: '4'}, {value: 5, label: '5'}] : type === 'single_choice' || type === 'multiple_choice' ? [{value: 'option1', label: '选项1'}, {value: 'option2', label: '选项2'}] : [], is_required: false, sort_order: this.questions.length, logic_rules: [] }; this.questions.push(newQuestion); this.activeQuestionIndex = this.questions.length - 1; }, // 删除问题 removeQuestion: function(index) { this.questions.splice(index, 1); if (this.activeQuestionIndex >= index && this.activeQuestionIndex > 0) { this.activeQuestionIndex--; } }, // 上移问题 moveQuestionUp: function(index) { if (index > 0) { const temp = this.questions[index]; this.$set(this.questions, index, this.questions[index - 1]); this.$set(this.questions, index - 1, temp); this.activeQuestionIndex = index - 1; } }, // 下移问题 moveQuestionDown: function(index) { if (index < this.questions.length - 1) { const temp = this.questions[index]; this.$set(this.questions, index, this.questions[index + 1]); this.activeQuestionIndex = index + 1; } }, // 添加选项 addOption: function(questionIndex) { if (!this.questions[questionIndex].options) { this.$set(this.questions[questionIndex], 'options', []); } const optionCount = this.questions[questionIndex].options.length + 1; this.questions[questionIndex].options.push({ value: 'option' + optionCount, label: '选项' + optionCount }); }, // 删除选项 removeOption: function(questionIndex, optionIndex) { this.questions[questionIndex].options.splice(optionIndex, 1); }, // 保存问卷 saveSurvey: function() { const formData = new FormData(); formData.append('action', 'save_survey'); formData.append('nonce', survey_admin_data.nonce); formData.append('survey', JSON.stringify(this.survey)); formData.append('questions', JSON.stringify(this.questions)); $.ajax({ url: survey_admin_data.ajax_url, type: 'POST', data: formData, processData: false, contentType: false, success: function(response) { if (response.success) { this.$message.success('问卷保存成功'); if (response.data.redirect) { window.location.href = response.data.redirect; } } else { this.$message.error('保存失败:' + response.data.message); } }.bind(this), error: function() { this.$message.error('网络错误,请重试'); }.bind(this) }); }, // 预览问卷 previewSurvey: function() { // 在新窗口打开预览 const previewUrl = survey_admin_data.site_url + '?survey_preview=' + (this.survey.id || 'new'); window.open(previewUrl, '_blank'); } }, mounted: function() { // 如果有现有问卷数据,加载它 if (window.surveyData) { this.survey = window.surveyData.survey; this.questions = window.surveyData.questions; } } }); })(jQuery); 前端问卷展示与交互 短代码系统实现 为了让用户能够轻松地在文章或页面中插入问卷,我们实现一个短代码系统: <?php /** * 问卷调查短代码类 */ class Survey_Shortcode { private $db; public function __construct() { $this->db = new Survey_DB(); // 注册短代码 add_shortcode('survey', array($this, 'render_survey')); // 注册AJAX处理 add_action('wp_ajax_submit_survey', array($this, 'handle_survey_submission')); add_action('wp_ajax_nopriv_submit_survey', array($this, 'handle_survey_submission')); } /** * 渲染问卷短代码 */ public function render_survey($atts) { $atts = shortcode_atts(array( 'id' => 0, 'title' => '', 'width' => '100%', 'height' => 'auto' ), $atts, 'survey'); $survey_id = intval($atts['id']); if (!$survey_id) { return '<div class="survey-error">请指定问卷ID</div>'; } // 获取问卷数据 global $wpdb; $survey = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}surveys WHERE id = %d AND status = 'published'", $survey_id )); if (!$survey) { return '<div class="survey-error">问卷不存在或未发布</div>'; } // 检查访问权限 $settings = json_decode($survey->settings, true); if ($settings['require_login'] && !is_user_logged_in()) { return '<div class="survey-login-required">请先登录后再填写问卷</div>'; } // 检查回答限制 if ($settings['limit_responses'] > 0) { $response_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}survey_responses WHERE survey_id = %d", $survey_id )); if ($response_count >= $settings['limit_responses']) { return '<div class="survey-closed">本问卷已收集足够数据,感谢参与</div>'; } } // 获取问题列表 $questions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}survey_questions WHERE survey_id = %d ORDER BY sort_order ASC", $survey_id )); // 解析问题选项 foreach ($questions as &$question) { if ($question->options) { $question->options = json_decode($question->options, true); } if ($question->logic_rules) { $question->logic_rules = json_decode($question->logic_rules, true); } } // 生成唯一会话ID $session_id = $this->generate_session_id(); // 输出问卷HTML ob_start(); include SURVEY_PLUGIN_DIR . 'templates/frontend/survey-form.php'; return ob_get_clean(); } /** * 处理问卷提交 */ public function handle_survey_submission() { // 验证nonce if (!check_ajax_referer('survey_submission', 'nonce', false)) { wp_die(json_encode(array( 'success' => false, 'message' => '安全验证失败' ))); } $survey_id = intval($_POST['survey_id']); $answers = $_POST['answers']; $session_id = sanitize_text_field($_POST['session_id']); // 验证问卷是否存在且开放 global $wpdb; $survey = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}surveys WHERE id = %d AND status = 'published'", $survey_id )); if (!$survey) { wp_die(json_encode(array( 'success' => false, 'message' => '问卷不存在' ))); } // 检查是否已经提交过 $settings = json_decode($survey->settings, true); if ($settings['prevent_duplicate']) { $user_id = get_current_user_id(); $check_field = $user_id ? 'user_id' : 'session_id'; $check_value = $user_id ? $user_id : $session_id; $existing = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM {$wpdb->prefix}survey_responses WHERE survey_id = %d AND {$check_field} = %s", $survey_id, $check_value )); if ($existing > 0) { wp_die(json_encode(array( 'success' => false, 'message' => '您已经提交过本问卷' ))); } } // 保存回答 $user_id = get_current_user_id(); $response_id = $this->db->save_response($survey_id, $user_id, $answers); if ($response_id) { // 触发完成动作 do_action('survey_completed', $survey_id, $response_id, $user_id); wp_die(json_encode(array( 'success' => true, 'message' => '提交成功', 'response_id' => $response_id ))); } else { wp_die(json_encode(array( 'success' => false, 'message' => '提交失败,请重试' ))); } } /** * 生成会话ID */ private function generate_session_id() { if (isset($_COOKIE['survey_session_id'])) { return $_COOKIE['survey_session_id']; } $session_id = wp_generate_uuid4(); setcookie('survey_session_id', $session_id, time() + 3600 * 24 * 30, '/'); return $session_id; } } ?> 前端问卷样式与交互 /* assets/css/frontend.css */ .survey-container { max-width: 800px; margin: 0 auto; padding: 30px; background: #fff; border-radius: 10px; box-shadow: 0 5px 20px rgba(0, 0, 0, 0.1); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } .survey-header { margin-bottom: 30px; padding-bottom: 20px; border-bottom: 2px solid #f0f0f0; } .survey-title { font-size: 28px; font-weight: 600; color: #333; margin-bottom: 10px; } .survey-description { font-size: 16px; color: #666; line-height: 1.6; } .survey-progress { margin-bottom: 30px; } .progress-bar { height: 8px; background: #f0f0f0; border-radius: 4px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #4CAF50, #8BC34A); transition: width 0.3s ease; } .progress-text { text-align: right; font-size: 14px; color: #666; margin-top: 5px; } .survey-questions { margin-bottom: 40px; } .question-item { margin-bottom: 30px; padding: 25px; background: #f9f9f9; border-radius: 8px; border-left: 4px solid #4CAF50; transition: all 0.3s ease; } .question-item.required { border-left-color: #F44336; } .question-item.required .question-text::after { content: " *"; color: #F44336; } .question-text { font-size: 18px; font-weight: 500; color: #333; margin-bottom: 20px; line-height: 1.5; } .question-hint { font-size: 14px; color: #888; margin-top: 5px; font-style: italic; } .options-container { display: flex; flex-direction: column; gap: 12px; } .option-item { display: flex; align-items: center; padding: 12px 15px; background: #fff; border: 2px solid #e0e0e0; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; } .option-item:hover { border-color: #4CAF50; background: #f8fff8; } .option-item.selected { border-color: #4CAF50; background: #f0f9f0; } .option-radio, .option-checkbox { margin-right: 12px; width: 20px; height: 20px; } .option-label { font-size: 16px; color: #333; flex: 1; } .text-input { width: 100%; padding: 12px 15px; font-size: 16px; border: 2px solid #e0e0e0; border-radius: 6px; transition: border-color 0.2s ease; } .text-input:focus { outline: none; border-color: #4CAF50; } .rating-container { display: flex; gap: 10px; justify-content: center; } .rating-star { font-size: 32px; color: #ddd; cursor: pointer; transition: color 0.2s ease; } .rating-star.active { color: #FFC107; } .rating-star:hover { color: #FFC107; } .matrix-table { width: 100%; border-collapse: collapse; margin-top: 10px; } .matrix-table th, .matrix-table td { padding: 12px; text-align: center; border: 1px solid #e0e0e0; } .matrix-table th { background: #f5f5f5; font-weight: 500; } .matrix-option { display: inline-block; margin: 0 5px; } .survey-navigation { display: flex; justify-content: space-between; align-items: center; padding-top: 30px; border-top: 2px solid #f0f0f0; } .nav-button { padding: 12px 30px; font-size: 16px; font-weight: 500; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; } .nav-button.prev { background: #f5f5f5; color: #666; } .nav-button.prev:hover { background: #e0e0e0; } .nav-button.next { background: #4CAF50; color: white; } .nav-button.next:hover { background: #45a049; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3); } .nav-button.submit { background: #2196F3; color: white; } .nav-button.submit:hover { background: #1976D2; transform: translateY(-2px); box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3); } .survey-footer { margin-top: 30px; text-align: center; font-size: 14px; color: #888; } /* 响应式设计 */ @media (max-width: 768px) { .survey-container { padding: 20px; margin: 10px; } .survey-title { font-size: 24px; } .question-item { padding: 20px; } .question-text { font-size: 16px; } .nav-button { padding: 10px 20px; font-size: 14px; } .matrix-table { font-size: 14px; } .matrix-table th, .matrix-table td { padding: 8px; } } /* 动画效果 */ @keyframes fadeIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } .question-item { animation: fadeIn 0.5s ease forwards; } .question-item:nth-child(odd) { animation-delay: 0.1s; } .question-item:nth-child(even) { animation-delay: 0.2s; } 数据统计与分析模块 数据可视化组件 <?php /** * 数据统计与分析类 */ class Survey_Analytics { private $db; public function __construct() { $this->db = new Survey_DB(); // 注册AJAX端点 add_action('wp_ajax_get_survey_stats', array($this, 'get_survey_statistics')); } /** * 获取问卷统计数据 */ public function get_survey_statistics($survey_id) { global $wpdb; $stats = array( 'summary' => array(), 'questions' => array(), 'demographics' => array(), 'timeline' => array() ); // 基础统计 $stats['summary'] = array( 'total_responses' => $this->get_total_responses($survey_id), 'completion_rate' => $this->get_completion_rate($survey_id), 'average_time' => $this->get_average_completion_time($survey_id), 'daily_average' => $this->get_daily_average($survey_id) ); // 问题分析 $questions = $wpdb->get_results($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}survey_questions WHERE survey_id = %d ORDER BY sort_order", $survey_id )); foreach ($questions as $question) { $question_stats = $this->analyze_question($question->id, $question->question_type); $stats['questions'][] = array( 'id' => $question->id, 'text' => $question->question_text, 'type' => $question->question_type, 'stats' => $question_stats ); } // 用户画像(如果收集了用户信息) $stats['demographics'] = $this->analyze_demographics($survey_id); // 时间线数据 $stats['timeline'] = $this->get_response_timeline($survey_id); return $stats; } /** * 分析单个问题 */ private function analyze_question($question_id, $question_type) { global $wpdb; $stats = array(); switch ($question_type) { case 'single_choice': case 'multiple_choice': // 获取选项统计 $answers = $wpdb->get_results($wpdb->prepare( "SELECT answer_value, COUNT(*) as count

发表评论