跳至内容

分类: 网站建设

WordPress插件开发教程,实现网站内容自动分发至音频播客平台

WordPress插件开发教程:实现网站内容自动分发至音频播客平台 引言:内容多平台分发的时代需求 在当今数字化内容爆炸的时代,内容创作者面临着一个共同的挑战:如何高效地将优质内容分发到多个平台,以最大化覆盖面和影响力。对于WordPress网站运营者而言,博客文章不仅是文字内容,更可以转化为音频、视频等多种形式,满足不同用户群体的消费习惯。 本教程将深入讲解如何开发一个WordPress插件,实现将网站内容自动转换为音频格式并分发至主流播客平台的功能。通过这个案例,您不仅能掌握WordPress插件开发的核心技术,还能学习如何通过代码二次开发实现实用的互联网小工具功能。 第一部分:WordPress插件开发基础 1.1 WordPress插件架构概述 WordPress插件本质上是一组PHP文件,通过WordPress提供的API和钩子(Hooks)系统与核心程序交互。一个标准的WordPress插件至少包含: 主插件文件(必须):包含插件元信息的PHP文件 功能文件:实现具体功能的PHP文件 资源文件:CSS、JavaScript、图像等 语言文件:国际化支持 1.2 创建插件基本结构 首先,在WordPress的wp-content/plugins/目录下创建一个新文件夹,命名为auto-podcast-distributor。在该文件夹中创建主插件文件auto-podcast-distributor.php: <?php /** * Plugin Name: Auto Podcast Distributor * Plugin URI: https://yourwebsite.com/auto-podcast-distributor * Description: 自动将WordPress文章转换为音频并分发到播客平台 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: auto-podcast-distributor */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('APD_VERSION', '1.0.0'); define('APD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('APD_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once APD_PLUGIN_DIR . 'includes/class-apd-core.php'; function apd_init() { $plugin = new APD_Core(); $plugin->run(); } add_action('plugins_loaded', 'apd_init'); 1.3 WordPress钩子系统详解 WordPress的钩子系统是插件开发的核心,分为两种类型: 动作钩子(Action Hooks):在特定时间点执行代码 过滤器钩子(Filter Hooks):修改数据后再传递 例如,我们可以使用publish_post动作钩子在文章发布时触发音频转换: add_action('publish_post', 'apd_convert_post_to_audio', 10, 2); function apd_convert_post_to_audio($post_id, $post) { // 检查文章类型和状态 if ($post->post_type !== 'post' || $post->post_status !== 'publish') { return; } // 调用音频转换函数 apd_generate_audio($post_id); } 第二部分:文本转音频技术实现 2.1 选择文本转语音引擎 将文本转换为高质量的音频是插件的核心功能。目前有多种解决方案: 本地TTS引擎:如eSpeak、Festival,免费但音质一般 云TTS服务:如Google Cloud Text-to-Speech、Amazon Polly、微软Azure语音服务,音质好但需要API调用 本教程将使用Amazon Polly作为示例,因为它提供高质量的语音合成和免费的套餐额度。 2.2 集成Amazon Polly API 首先,在插件目录中创建includes/class-apd-tts-engine.php文件: <?php class APD_TTS_Engine { private $aws_access_key; private $aws_secret_key; private $aws_region; public function __construct() { $options = get_option('apd_settings'); $this->aws_access_key = $options['aws_access_key'] ?? ''; $this->aws_secret_key = $options['aws_secret_key'] ?? ''; $this->aws_region = $options['aws_region'] ?? 'us-east-1'; } /** * 将文本转换为音频 */ public function text_to_speech($text, $post_id) { // 清理文本,移除HTML标签 $clean_text = wp_strip_all_tags($text); // 限制文本长度(Polly单次请求限制为3000字符) if (strlen($clean_text) > 3000) { $clean_text = $this->truncate_text($clean_text, 3000); } // 生成音频文件名 $filename = 'podcast-' . $post_id . '-' . time() . '.mp3'; $filepath = APD_PLUGIN_DIR . 'audio/' . $filename; // 确保目录存在 if (!file_exists(APD_PLUGIN_DIR . 'audio')) { wp_mkdir_p(APD_PLUGIN_DIR . 'audio'); } // 调用AWS Polly SDK try { $polly = new AwsPollyPollyClient([ 'version' => 'latest', 'region' => $this->aws_region, 'credentials' => [ 'key' => $this->aws_access_key, 'secret' => $this->aws_secret_key ] ]); $result = $polly->synthesizeSpeech([ 'Text' => $clean_text, 'OutputFormat' => 'mp3', 'VoiceId' => 'Joanna', // 选择声音 'Engine' => 'neural' // 使用神经引擎提高质量 ]); // 保存音频文件 file_put_contents($filepath, $result['AudioStream']); // 将音频文件保存到媒体库 $attachment_id = $this->save_to_media_library($filepath, $filename, $post_id); return [ 'success' => true, 'filepath' => $filepath, 'url' => wp_get_attachment_url($attachment_id), 'attachment_id' => $attachment_id ]; } catch (Exception $e) { error_log('Polly TTS Error: ' . $e->getMessage()); return [ 'success' => false, 'error' => $e->getMessage() ]; } } /** * 将音频文件保存到WordPress媒体库 */ private function save_to_media_library($filepath, $filename, $post_id) { require_once(ABSPATH . 'wp-admin/includes/media.php'); require_once(ABSPATH . 'wp-admin/includes/file.php'); require_once(ABSPATH . 'wp-admin/includes/image.php'); $file_array = [ 'name' => $filename, 'tmp_name' => $filepath ]; // 上传文件到媒体库 $attachment_id = media_handle_sideload($file_array, $post_id, 'Audio for post ' . $post_id); if (is_wp_error($attachment_id)) { error_log('Media upload error: ' . $attachment_id->get_error_message()); return 0; } // 更新附件元数据 update_post_meta($attachment_id, '_apd_generated_audio', 'yes'); update_post_meta($attachment_id, '_apd_source_post', $post_id); return $attachment_id; } /** * 截断文本,保留完整句子 */ private function truncate_text($text, $max_length) { if (strlen($text) <= $max_length) { return $text; } $truncated = substr($text, 0, $max_length); $last_period = strrpos($truncated, '.'); if ($last_period !== false) { return substr($truncated, 0, $last_period + 1); } return $truncated; } } 2.3 音频后期处理 为了提高音频质量,我们可以添加简单的后期处理功能: class APD_Audio_Processor { /** * 为音频添加介绍和结尾 */ public function add_intro_outro($audio_path, $post_id) { $post = get_post($post_id); // 生成介绍音频 $intro_text = "欢迎收听本期节目,今天为您带来:" . $post->post_title; $intro_audio = $this->generate_audio_segment($intro_text); // 生成结尾音频 $outro_text = "感谢收听本期节目,更多内容请访问我们的网站。"; $outro_audio = $this->generate_audio_segment($outro_text); // 合并音频文件 $final_audio = $this->merge_audio_files([ $intro_audio, $audio_path, $outro_audio ]); return $final_audio; } /** * 合并多个音频文件 */ private function merge_audio_files($audio_files) { // 这里可以使用FFmpeg或音频处理库 // 简化示例:返回第一个文件(实际开发中需要实现合并逻辑) return $audio_files[0]; } /** * 生成音频片段 */ private function generate_audio_segment($text) { // 调用TTS引擎生成短音频 $tts_engine = new APD_TTS_Engine(); $temp_file = tempnam(sys_get_temp_dir(), 'apd_'); // 简化示例,实际需要调用TTS引擎 file_put_contents($temp_file, ''); // 实际应保存音频内容 return $temp_file; } } 第三部分:播客平台分发机制 3.1 播客RSS标准解析 播客平台通常通过RSS feed获取内容。一个标准的播客RSS需要包含特定标签: class APD_Podcast_RSS { /** * 生成播客RSS Feed */ public function generate_rss_feed() { $rss = '<?xml version="1.0" encoding="UTF-8"?>'; $rss .= '<rss version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:content="http://purl.org/rss/1.0/modules/content/">'; $rss .= '<channel>'; // 频道信息 $rss .= $this->generate_channel_info(); // 剧集列表 $rss .= $this->generate_episodes(); $rss .= '</channel>'; $rss .= '</rss>'; return $rss; } /** * 生成频道信息 */ private function generate_channel_info() { $output = '<title>' . get_bloginfo('name') . ' Podcast</title>'; $output .= '<link>' . get_bloginfo('url') . '</link>'; $output .= '<description>' . get_bloginfo('description') . '</description>'; $output .= '<language>' . get_bloginfo('language') . '</language>'; $output .= '<itunes:author>' . get_bloginfo('name') . '</itunes:author>'; $output .= '<itunes:category text="Education"></itunes:category>'; return $output; } /** * 生成剧集列表 */ private function generate_episodes() { $output = ''; $args = [ 'post_type' => 'post', 'posts_per_page' => 20, 'meta_query' => [ [ 'key' => '_apd_audio_attachment', 'compare' => 'EXISTS' ] ] ]; $episodes = new WP_Query($args); while ($episodes->have_posts()) { $episodes->the_post(); $post_id = get_the_ID(); $audio_url = get_post_meta($post_id, '_apd_audio_url', true); if (!$audio_url) { continue; } $output .= '<item>'; $output .= '<title>' . get_the_title() . '</title>'; $output .= '<description>' . get_the_excerpt() . '</description>'; $output .= '<pubDate>' . get_the_date('r') . '</pubDate>'; $output .= '<enclosure url="' . esc_url($audio_url) . '" length="0" type="audio/mpeg" />'; $output .= '<itunes:duration>30:00</itunes:duration>'; $output .= '</item>'; } wp_reset_postdata(); return $output; } } 3.2 集成主流播客平台API 3.2.1 Apple Podcasts Connect Apple Podcasts不直接提供API,但可以通过RSS feed提交。我们可以创建一个自动提交功能: class APD_Apple_Podcasts { /** * 提交RSS到Apple Podcasts */ public function submit_rss($rss_feed_url) { // Apple Podcasts Connect需要手动提交 // 这里可以生成提交指南或发送通知邮件 $admin_email = get_option('admin_email'); $subject = 'Apple Podcasts RSS Feed 已就绪'; $message = "您的播客RSS Feed已生成,请登录Apple Podcasts Connect提交以下URL:nn"; $message .= $rss_feed_url . "nn"; $message .= "此邮件由Auto Podcast Distributor插件自动发送。"; wp_mail($admin_email, $subject, $message); return true; } } 3.2.2 Spotify Podcast API Spotify提供了Podcast API,可以自动提交播客: class APD_Spotify_Podcasts { private $client_id; private $client_secret; private $access_token; public function __construct() { $options = get_option('apd_settings'); $this->client_id = $options['spotify_client_id'] ?? ''; $this->client_secret = $options['spotify_client_secret'] ?? ''; } /** * 获取访问令牌 */ private function get_access_token() { $response = wp_remote_post('https://accounts.spotify.com/api/token', [ 'headers' => [ 'Authorization' => 'Basic ' . base64_encode($this->client_id . ':' . $this->client_secret), 'Content-Type' => 'application/x-www-form-urlencoded' ], 'body' => [ 'grant_type' => 'client_credentials' ] ]); if (is_wp_error($response)) { error_log('Spotify API Error: ' . $response->get_error_message()); return false; } $body = json_decode(wp_remote_retrieve_body($response), true); $this->access_token = $body['access_token'] ?? ''; return !empty($this->access_token); } /** * 提交播客到Spotify */ public function submit_podcast($rss_feed_url) { if (!$this->get_access_token()) { return [ 'success' => false, 'error' => '无法获取Spotify访问令牌' ]; } $response = wp_remote_post('https://api.spotify.com/v1/shows', [ 'headers' => [ 'Authorization' => 'Bearer ' . $this->access_token, 'Content-Type' => 'application/json' ], 'body' => json_encode([ 'rss_feed' => $rss_feed_url ]) ]); if (is_wp_error($response)) { return [ 'success' => false, 'error' => $response->get_error_message() ]; } $status_code = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($status_code === 201) { return [ 'success' => true, 'show_id' => $body['id'] ?? '', 'message' => '播客已成功提交到Spotify' ]; } else { return [ 'success' => false, 'error' => $body['error']['message'] ?? '未知错误' ]; } } } 第四部分:WordPress后台管理界面 4.1 创建插件设置页面 class APD_Admin { /** * 初始化管理界面 */ public function init() { add_action('admin_menu', [$this, 'add_admin_menu']); add_action('admin_init', [$this, 'register_settings']); add_action('add_meta_boxes', [$this, 'add_post_meta_box']); add_action('save_post', [$this, 'save_post_meta'], 10, 2); } /** * 添加管理菜单 */ public function add_admin_menu() { add_menu_page( '自动播客分发', '播客分发', 'manage_options', 'apd-settings', [$this, 'display_settings_page'], 'dashicons-microphone', 30 ); add_submenu_page( 'apd-settings', '分发日志', '分发日志', 'manage_options', 'apd-logs', [$this, 'display_logs_page'] ); } /** * 显示设置页面 */ public function display_settings_page() { if (!current_user_can('manage_options')) { return; } ?> <div class="wrap"> esc_html(get_admin_page_title()); ?></h1> <form action="options.php" method="post"> <?php settings_fields('apd_settings_group'); do_settings_sections('apd-settings'); submit_button('保存设置'); ?> </form> <div class="apd-test-section"> <h2>功能测试</h2> <button id="apd-test-tts" class="button button-secondary">测试文本转语音</button> <button id="apd-test-rss" class="button button-secondary">生成RSS Feed</button> <div id="apd-test-result" style="margin-top: 15px; padding: 10px; background: #f5f5f5; display: none;"></div> </div> </div> <?php } /** * 注册设置选项 */ public function register_settings() { register_setting('apd_settings_group', 'apd_settings', [$this, 'sanitize_settings']); // AWS设置部分 add_settings_section( 'apd_aws_section', 'AWS Polly设置', [$this, 'aws_section_callback'], 'apd-settings' ); add_settings_field( 'aws_access_key', 'AWS访问密钥', [$this, 'text_field_callback'], 'apd-settings', 'apd_aws_section', [ 'label_for' => 'aws_access_key', 'description' => 'Amazon Polly服务的访问密钥' ] ); add_settings_field( 'aws_secret_key', 'AWS秘密密钥', [$this, 'password_field_callback'], 'apd-settings', 'apd_aws_section', [ 'label_for' => 'aws_secret_key', 'description' => 'Amazon Polly服务的秘密密钥' ] ); // 播客平台设置部分 add_settings_section( 'apd_platforms_section', '播客平台设置', [$this, 'platforms_section_callback'], 'apd-settings' ); add_settings_field( 'auto_submit_spotify', '自动提交到Spotify', [$this, 'checkbox_field_callback'], 'apd-settings', 'apd_platforms_section', [ 'label_for' => 'auto_submit_spotify', 'description' => '文章发布时自动提交到Spotify播客' ] ); } /** * 清理设置数据 */ public function sanitize_settings($input) { $sanitized = []; // 清理AWS密钥 $sanitized['aws_access_key'] = sanitize_text_field($input['aws_access_key'] ?? ''); $sanitized['aws_secret_key'] = sanitize_text_field($input['aws_secret_key'] ?? ''); $sanitized['aws_region'] = sanitize_text_field($input['aws_region'] ?? 'us-east-1'); // 清理平台设置 $sanitized['auto_submit_spotify'] = isset($input['auto_submit_spotify']) ? 1 : 0; return $sanitized; } /** * 添加文章元数据框 */ public function add_post_meta_box() { add_meta_box( 'apd_podcast_meta', '播客分发设置', [$this, 'render_post_meta_box'], 'post', 'side', 'high' ); } /** * 渲染文章元数据框 */ public function render_post_meta_box($post) { wp_nonce_field('apd_save_post_meta', 'apd_meta_nonce'); $generate_audio = get_post_meta($post->ID, '_apd_generate_audio', true); $audio_status = get_post_meta($post->ID, '_apd_audio_status', true); $audio_url = get_post_meta($post->ID, '_apd_audio_url', true); ?> <div class="apd-meta-box"> <p> <label> <input type="checkbox" name="apd_generate_audio" value="1" <?php checked($generate_audio, '1'); ?>> 生成音频版本 </label> </p> <?php if ($audio_status): ?> <div class="apd-audio-status"> <p><strong>状态:</strong> <?php echo esc_html($audio_status); ?></p> <?php if ($audio_url): ?> <p><strong>音频文件:</strong> <a href="<?php echo esc_url($audio_url); ?>" target="_blank">预览</a> </p> <audio controls style="width: 100%; margin-top: 10px;"> <source src="<?php echo esc_url($audio_url); ?>" type="audio/mpeg"> </audio> <?php endif; ?> </div> <?php endif; ?> <button type="button" id="apd-generate-now" class="button button-secondary"> 立即生成音频 </button> </div> <script> jQuery(document).ready(function($) { $('#apd-generate-now').click(function() { var post_id = <?php echo $post->ID; ?>; var button = $(this); button.text('生成中...').prop('disabled', true); $.post(ajaxurl, { action: 'apd_generate_audio', post_id: post_id, nonce: '<?php echo wp_create_nonce("apd_generate_audio"); ?>' }, function(response) { if (response.success) { alert('音频生成成功!'); location.reload(); } else { alert('生成失败:' + response.data); } button.text('立即生成音频').prop('disabled', false); }); }); }); </script> <?php } /** * 保存文章元数据 */ public function save_post_meta($post_id, $post) { // 检查权限 if (!current_user_can('edit_post', $post_id)) { return; } // 验证nonce if (!isset($_POST['apd_meta_nonce']) || !wp_verify_nonce($_POST['apd_meta_nonce'], 'apd_save_post_meta')) { return; } // 保存设置 $generate_audio = isset($_POST['apd_generate_audio']) ? 1 : 0; update_post_meta($post_id, '_apd_generate_audio', $generate_audio); // 如果勾选了生成音频且文章已发布 if ($generate_audio && $post->post_status === 'publish') { $this->schedule_audio_generation($post_id); } } /** * 调度音频生成任务 */ private function schedule_audio_generation($post_id) { // 使用WordPress的定时任务系统 if (!wp_next_scheduled('apd_generate_audio_event', [$post_id])) { wp_schedule_single_event(time() + 10, 'apd_generate_audio_event', [$post_id]); } } } ### 4.2 AJAX处理与实时反馈 class APD_Ajax_Handler { /** * 初始化AJAX处理 */ public function init() { add_action('wp_ajax_apd_generate_audio', [$this, 'handle_generate_audio']); add_action('wp_ajax_apd_test_tts', [$this, 'handle_test_tts']); add_action('wp_ajax_apd_get_logs', [$this, 'handle_get_logs']); } /** * 处理音频生成请求 */ public function handle_generate_audio() { // 安全检查 check_ajax_referer('apd_generate_audio', 'nonce'); if (!current_user_can('edit_posts')) { wp_die('权限不足'); } $post_id = intval($_POST['post_id']); $post = get_post($post_id); if (!$post) { wp_send_json_error('文章不存在'); } // 调用音频生成函数 $result = apd_generate_audio($post_id); if ($result['success']) { // 记录日志 $this->log_activity('音频生成成功', [ 'post_id' => $post_id, 'audio_url' => $result['url'] ]); wp_send_json_success('音频生成成功'); } else { $this->log_activity('音频生成失败', [ 'post_id' => $post_id, 'error' => $result['error'] ]); wp_send_json_error($result['error']); } } /** * 记录活动日志 */ private function log_activity($message, $data = []) { $logs = get_option('apd_activity_logs', []); $log_entry = [ 'time' => current_time('mysql'), 'message' => $message, 'data' => $data ]; // 只保留最近100条日志 array_unshift($logs, $log_entry); $logs = array_slice($logs, 0, 100); update_option('apd_activity_logs', $logs); } } ## 第五部分:高级功能与优化 ### 5.1 批量处理与队列系统 对于有大量历史文章需要转换的用户,我们需要实现批量处理功能: class APD_Batch_Processor { /** * 批量转换文章为音频 */ public function batch_convert_posts($post_ids) { $results = []; foreach ($post_ids as $post_id) { // 检查是否已生成音频 $existing_audio = get_post_meta($post_id, '_apd_audio_url', true); if ($existing_audio) { $results[$post_id] = [ 'success' => false, 'message' => '已存在音频文件' ]; continue; } // 生成音频 $result = apd_generate_audio($post_id); $results[$post_id] = $result; // 避免API限制,添加延迟 sleep(1); } return $results; } /** * 获取可批量处理的文章列表 */ public function get_eligible_posts($limit = 50) { $args = [ 'post_type' => 'post', 'posts_per_page' => $limit, 'meta_query' => [ [ 'key' => '_apd_audio_url', 'compare' => 'NOT EXISTS' ] ] ]; $query = new WP_Query($args); $posts = []; while ($query->have_posts()) { $query->the_post(); $posts[] = [ 'id' => get_the_ID(), 'title' => get_the_title(), 'date' => get_the_date() ]; } wp_reset_postdata(); return $posts; } } ### 5.2 音频内容优化 为了提高音频质量,我们可以添加内容优化功能: class APD_Content_Optimizer { /** * 优化文本内容,提高TTS质量 */ public function optimize_for_tts($content) { // 移除短代码 $content = strip_shortcodes($content); // 转换HTML实体 $content = html_entity_decode($content, ENT_QUOTES | ENT_HTML5, 'UTF-8'); // 处理特殊字符 $content = $this->process_special_characters($content); // 优化段落结构 $content = $this->optimize_paragraphs($content); // 添加自然停顿 $content = $this->add_pauses($content); return $content; } /** * 处理特殊字符 */ private function process_special_characters($text) { $replacements = [ '&nbsp;' => ' ', '&amp;' => '和', '&quot;' => '"', '&#8217;' => "'", '...' => '。', '..' => '。', ]; return str_replace(array_keys($replacements), array_values($replacements), $text); } /** * 优化段落结构 */ private function optimize_paragraphs($text) { // 将多个换行转换为单个换行 $text = preg_replace('/ns*n/', "nn", $text); // 确保每个段落以句号结束 $paragraphs = explode("nn", $text); foreach ($paragraphs as &$paragraph) { $paragraph = trim($paragraph); if (!empty($paragraph) && !preg_match('/[。.!?]$/u', $paragraph)) { $paragraph .= '。'; } } return implode("nn", $paragraphs); } /** * 添加自然停顿 */ private function add_pauses($text) { // 在标点符号后添加额外空格,模拟停顿 $text = preg_replace('/([。.!?;;])/', '$1 ', $text); return $text; } } ### 5.3 性能优化与缓存 class APD_Performance_Optimizer { /** * 实现音频文件缓存 */ public function get_cached_audio($post_id) { $cache_key = 'apd_audio_' . $post_id; $cached = wp_cache_get($cache_key, 'apd'); if ($cached !== false) { return $cached; } // 从数据库获取 $audio_url = get_post_meta($post_id, '_apd_audio_url', true); if ($audio_url) { $audio_data = [ 'url' => $audio_url, 'duration' => get_post_meta($post_id, '_apd_audio_duration', true), 'size' => get_post_meta($post_id, '_apd_audio_size', true) ]; wp_cache_set($cache_key, $audio_data, 'apd', 3600); // 缓存1小时 return $audio_data; } return null; } /** * 清理过期缓存 */ public function cleanup_cache() { global $wpdb; // 清理一周前的日志 $one_week_ago = date('Y-m-d H:i:s', strtotime('-1 week')); $wpdb->query($wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE 'apd_temp_%' AND option_value < %s", $one_week_ago )); } } ## 第六部分:插件部署与维护 ### 6.1 国际化与本地化 class APD_i18n { /** * 加载文本域 */ public function load_textdomain() { load_plugin_textdomain( 'auto-podcast-distributor', false, dirname(plugin_basename(APD_PLUGIN_DIR)) . '/languages' ); } /** * 生成翻译文件 */ public function generate_pot_file() { // 在实际开发中,这里会调用gettext工具生成POT文件 // 供翻译人员使用 } } ### 6.2 插件更新机制 class APD_Updater { private $current_version; private $update_url = 'https://yourwebsite.com/updates/'; public function __construct() { $this->current_version = APD_VERSION; add_filter('pre_set_site_transient_update_plugins', [$this, 'check_for_update']); add_filter('plugins_api', [$this, 'plugin_info'], 20, 3); } /** * 检查更新 */ public function check_for_update($transient) { if (empty($transient->checked)) { return $transient; } $remote_version = $this->get_remote_version(); if ($remote_version && version_compare($this->current_version, $remote_version, '<')) { $plugin_slug = plugin_basename(APD_PLUGIN_DIR); $obj = new stdClass(); $obj->slug = $plugin_slug; $obj->new_version = $remote_version; $obj->url = $this->update_url; $obj->package = $this->update_url . 'download/'; $transient->response[$plugin_slug] = $obj; } return $transient; } /** * 获取远程版本信息 */ private function get_remote_version() { $response = wp_remote_get($this->update_url . 'version.json'); if (is_wp_error($response)) { return false; } $data = json_decode(wp_remote_retrieve_body($response)); return $data->version ?? false; } } ### 6.3 错误处理与日志系统 class APD_Logger { private $log_file; public function __construct() { $upload_dir = wp_upload_dir(); $this->log_file = $upload_dir['basedir'] . '/apd-logs/apd-' . date('Y-m-d') . '.log'; // 确保日志目录存在 $log_dir = dirname($this->log_file); if (!

发表评论

实战教程,在网站中添加在线个人习惯追踪与目标管理小程序

实战教程:在WordPress网站中添加在线个人习惯追踪与目标管理小程序 引言:为什么网站需要个人习惯追踪功能? 在当今快节奏的数字时代,个人效率管理和习惯养成已成为许多人关注的焦点。研究表明,习惯追踪能够提高目标达成率高达42%,而将这一功能集成到个人或专业网站中,不仅能提升用户体验,还能增加网站粘性和实用价值。 WordPress作为全球最流行的内容管理系统,其强大的可扩展性使其成为实现此类功能的理想平台。本教程将指导您通过代码二次开发,在WordPress网站中集成一个完整的在线个人习惯追踪与目标管理小程序,让您的网站从单纯的内容展示平台转变为实用的个人成长工具。 第一部分:项目规划与准备工作 1.1 功能需求分析 在开始编码之前,我们需要明确小程序的核心功能: 用户习惯管理:创建、编辑、删除个人习惯 进度追踪:每日习惯打卡与进度可视化 目标设定:短期与长期目标管理 数据统计:习惯坚持率、完成趋势分析 提醒功能:邮件或站内消息提醒 社交分享:成就分享到社交媒体 数据导出:支持CSV格式数据导出 1.2 技术栈选择 前端:HTML5、CSS3、JavaScript (使用Vue.js或React简化开发) 后端:PHP (WordPress原生支持) 数据库:MySQL (WordPress数据库) 图表库:Chart.js 或 Google Charts 日期处理:Moment.js 开发环境:本地WordPress安装或开发服务器 1.3 开发环境搭建 安装本地服务器环境(如XAMPP、MAMP或Local by Flywheel) 下载最新版WordPress并完成安装 创建子主题或自定义插件目录结构 安装代码编辑器(如VS Code)并配置PHP开发环境 第二部分:数据库设计与数据模型 2.1 自定义数据库表设计 为了存储习惯追踪数据,我们需要在WordPress数据库中添加几个自定义表: -- 习惯表 CREATE TABLE wp_habits ( habit_id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) UNSIGNED NOT NULL, habit_name VARCHAR(255) NOT NULL, habit_description TEXT, habit_category VARCHAR(100), frequency ENUM('daily', 'weekly', 'monthly') DEFAULT 'daily', target_days INT DEFAULT 7, start_date DATE NOT NULL, end_date DATE, color_code VARCHAR(7) DEFAULT '#3498db', is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ); -- 习惯记录表 CREATE TABLE wp_habit_logs ( log_id INT AUTO_INCREMENT PRIMARY KEY, habit_id INT NOT NULL, user_id BIGINT(20) UNSIGNED NOT NULL, log_date DATE NOT NULL, status ENUM('completed', 'skipped', 'failed') DEFAULT 'completed', notes TEXT, duration INT COMMENT '完成习惯所用时间(分钟)', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_habit_date (habit_id, log_date), FOREIGN KEY (habit_id) REFERENCES wp_habits(habit_id), FOREIGN KEY (user_id) REFERENCES wp_users(ID) ); -- 目标表 CREATE TABLE wp_goals ( goal_id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) UNSIGNED NOT NULL, goal_name VARCHAR(255) NOT NULL, goal_description TEXT, goal_type ENUM('habit_based', 'milestone', 'quantitative') DEFAULT 'habit_based', target_value DECIMAL(10,2), current_value DECIMAL(10,2) DEFAULT 0, start_date DATE NOT NULL, target_date DATE, status ENUM('active', 'completed', 'abandoned') DEFAULT 'active', related_habit_id INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES wp_users(ID), FOREIGN KEY (related_habit_id) REFERENCES wp_habits(habit_id) ); 2.2 WordPress数据表集成策略 虽然我们创建了自定义表,但仍需确保与WordPress用户系统的无缝集成: 使用WordPress的$wpdb类进行数据库操作 利用WordPress的用户认证系统 遵循WordPress的数据转义和安全规范 考虑使用自定义Post Type作为替代方案(适合简单需求) 第三部分:创建WordPress插件框架 3.1 插件基础结构 创建插件目录wp-content/plugins/habit-tracker/,并添加以下文件: <?php /* Plugin Name: 个人习惯追踪与目标管理 Plugin URI: https://yourwebsite.com/habit-tracker Description: 在WordPress网站中添加在线个人习惯追踪与目标管理功能 Version: 1.0.0 Author: 您的名称 License: GPL v2 or later Text Domain: habit-tracker */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('HABIT_TRACKER_VERSION', '1.0.0'); define('HABIT_TRACKER_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('HABIT_TRACKER_PLUGIN_URL', plugin_dir_url(__FILE__)); // 数据库表版本 define('HABIT_TRACKER_DB_VERSION', '1.0'); // 包含必要文件 require_once HABIT_TRACKER_PLUGIN_DIR . 'includes/class-database.php'; require_once HABIT_TRACKER_PLUGIN_DIR . 'includes/class-habit-tracker.php'; require_once HABIT_TRACKER_PLUGIN_DIR . 'includes/class-shortcodes.php'; require_once HABIT_TRACKER_PLUGIN_DIR . 'includes/class-ajax-handler.php'; // 初始化插件 function habit_tracker_init() { $plugin = new Habit_Tracker(); $plugin->run(); } add_action('plugins_loaded', 'habit_tracker_init'); // 激活插件时创建数据库表 register_activation_hook(__FILE__, array('Habit_Tracker_Database', 'create_tables')); // 停用插件时的清理操作 register_deactivation_hook(__FILE__, array('Habit_Tracker_Database', 'cleanup')); 3.2 数据库操作类 创建includes/class-database.php文件: <?php class Habit_Tracker_Database { private static $table_prefix; public static function init() { global $wpdb; self::$table_prefix = $wpdb->prefix . 'habit_'; } // 创建数据库表 public static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $sql = array(); // 创建习惯表 $sql[] = "CREATE TABLE " . self::$table_prefix . "habits ( habit_id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) UNSIGNED NOT NULL, habit_name VARCHAR(255) NOT NULL, habit_description TEXT, habit_category VARCHAR(100), frequency ENUM('daily', 'weekly', 'monthly') DEFAULT 'daily', target_days INT DEFAULT 7, start_date DATE NOT NULL, end_date DATE, color_code VARCHAR(7) DEFAULT '#3498db', is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) $charset_collate;"; // 创建习惯记录表 $sql[] = "CREATE TABLE " . self::$table_prefix . "logs ( log_id INT AUTO_INCREMENT PRIMARY KEY, habit_id INT NOT NULL, user_id BIGINT(20) UNSIGNED NOT NULL, log_date DATE NOT NULL, status ENUM('completed', 'skipped', 'failed') DEFAULT 'completed', notes TEXT, duration INT COMMENT '完成习惯所用时间(分钟)', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_habit_date (habit_id, log_date) ) $charset_collate;"; // 创建目标表 $sql[] = "CREATE TABLE " . self::$table_prefix . "goals ( goal_id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) UNSIGNED NOT NULL, goal_name VARCHAR(255) NOT NULL, goal_description TEXT, goal_type ENUM('habit_based', 'milestone', 'quantitative') DEFAULT 'habit_based', target_value DECIMAL(10,2), current_value DECIMAL(10,2) DEFAULT 0, start_date DATE NOT NULL, target_date DATE, status ENUM('active', 'completed', 'abandoned') DEFAULT 'active', related_habit_id INT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); foreach ($sql as $query) { dbDelta($query); } add_option('habit_tracker_db_version', HABIT_TRACKER_DB_VERSION); } // 获取用户的所有习惯 public static function get_user_habits($user_id, $active_only = true) { global $wpdb; $table_name = self::$table_prefix . 'habits'; $condition = $active_only ? " AND is_active = 1" : ""; return $wpdb->get_results( $wpdb->prepare( "SELECT * FROM $table_name WHERE user_id = %d $condition ORDER BY created_at DESC", $user_id ) ); } // 添加更多数据库操作方法... } 第四部分:前端界面开发 4.1 主界面HTML结构 创建templates/dashboard.php文件: <div class="habit-tracker-container"> <div class="habit-tracker-header"> <h1>个人习惯追踪与目标管理</h1> <div class="date-navigation"> <button class="prev-date">← 前一天</button> <span class="current-date"><?php echo date('Y年m月d日'); ?></span> <button class="next-date">后一天 →</button> </div> </div> <div class="habit-tracker-main"> <!-- 左侧习惯管理区域 --> <div class="habits-section"> <div class="section-header"> <h2>我的习惯</h2> <button class="btn-add-habit">+ 添加新习惯</button> </div> <div class="habits-list"> <!-- 习惯项将通过JavaScript动态加载 --> <div class="loading-habits">加载习惯中...</div> </div> <div class="habits-stats"> <div class="stat-card"> <div class="stat-value" id="current-streak">0</div> <div class="stat-label">当前连续天数</div> </div> <div class="stat-card"> <div class="stat-value" id="completion-rate">0%</div> <div class="stat-label">本月完成率</div> </div> <div class="stat-card"> <div class="stat-value" id="total-habits">0</div> <div class="stat-label">活跃习惯</div> </div> </div> </div> <!-- 右侧目标与统计区域 --> <div class="goals-stats-section"> <div class="goals-section"> <div class="section-header"> <h2>我的目标</h2> <button class="btn-add-goal">+ 添加新目标</button> </div> <div class="goals-list"> <!-- 目标项将通过JavaScript动态加载 --> </div> </div> <div class="stats-section"> <div class="section-header"> <h2>习惯统计</h2> <select id="stats-period"> <option value="week">本周</option> <option value="month" selected>本月</option> <option value="year">今年</option> <option value="all">全部</option> </select> </div> <div class="charts-container"> <canvas id="completionChart" width="400" height="200"></canvas> <canvas id="categoryChart" width="400" height="200"></canvas> </div> </div> </div> </div> <!-- 添加习惯模态框 --> <div class="modal" id="addHabitModal"> <div class="modal-content"> <span class="close-modal">&times;</span> <h3>添加新习惯</h3> <form id="habitForm"> <div class="form-group"> <label for="habitName">习惯名称 *</label> <input type="text" id="habitName" name="habit_name" required> </div> <div class="form-group"> <label for="habitDescription">习惯描述</label> <textarea id="habitDescription" name="habit_description" rows="3"></textarea> </div> <div class="form-row"> <div class="form-group"> <label for="habitCategory">分类</label> <select id="habitCategory" name="habit_category"> <option value="health">健康</option> <option value="work">工作</option> <option value="learning">学习</option> <option value="finance">财务</option> <option value="relationship">人际关系</option> <option value="hobby">兴趣爱好</option> </select> </div> <div class="form-group"> <label for="habitFrequency">频率</label> <select id="habitFrequency" name="frequency"> <option value="daily">每日</option> <option value="weekly">每周</option> <option value="monthly">每月</option> </select> </div> </div> <div class="form-row"> <div class="form-group"> <label for="targetDays">目标天数 (周)</label> <input type="number" id="targetDays" name="target_days" min="1" max="7" value="7"> </div> <div class="form-group"> <label for="habitColor">颜色标识</label> <input type="color" id="habitColor" name="color_code" value="#3498db"> </div> </div> <div class="form-actions"> <button type="button" class="btn-cancel">取消</button> <button type="submit" class="btn-submit">创建习惯</button> </div> </form> </div> </div> </div> 4.2 CSS样式设计 创建assets/css/habit-tracker.css文件: /* 主容器样式 */ .habit-tracker-container { max-width: 1200px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; } .habit-tracker-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } .habit-tracker-header h1 { margin: 0; color: #2c3e50; font-size: 28px; } .date-navigation { display: flex; align-items: center; gap: 15px; } .date-navigation button { background: #f8f9fa; border: 1px solid #ddd; padding: 8px 15px; border-radius: 4px; cursor: pointer; transition: all 0.3s; } .date-navigation button:hover { background: #e9ecef; } .current-date { font-weight: 600; color: #495057; } /* 主内容区域 */ .habit-tracker-main { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; } @media (max-width: 992px) { .habit-tracker-main { grid-template-columns: 1fr; } } /* 习惯列表样式 */ .habits-section, .goals-section, .stats-section { background: white; border-radius: 8px; padding: 25px; box-shadow: 0 2px 10px rgba(0,0,0,0.08); } .section-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 25px; } .section-header h2 { margin: 0; color: #2c3e50; font-size: 22px; } .btn-add-habit, .btn-add-goal { background: #3498db; color: white; border: none; padding: 10px 20px; border-radius: 4px; cursor: pointer; font-weight: 600; transition: background 0.3s; } .btn-add-habit:hover, .btn-add-goal:hover { background: #2980b9; } /* 习惯项样式 */ .habit-item { display: flex; align-items: center; justify-content: space-between; padding: 15px; margin-bottom: 15px; border-radius: 6px; background: #f8f9fa; border-left: 4px solid #3498db; transition: transform 0.2s; } .habit-item:hover { transform: translateY(-2px); (接上文) .habit-info { flex: 1; } .habit-name { font-weight: 600; color: #2c3e50; margin-bottom: 5px; font-size: 16px; } .habit-meta { display: flex; gap: 15px; font-size: 14px; color: #6c757d; } .habit-category { background: #e9ecef; padding: 2px 8px; border-radius: 12px; font-size: 12px; } .habit-actions { display: flex; gap: 10px; align-items: center; } .check-habit { width: 24px; height: 24px; border: 2px solid #dee2e6; border-radius: 50%; cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all 0.3s; } .check-habit.checked { background: #28a745; border-color: #28a745; color: white; } .check-habit.checked::after { content: "✓"; font-size: 14px; } /* 统计卡片样式 */ .habits-stats { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-top: 25px; } .stat-card { background: white; border-radius: 8px; padding: 20px; text-align: center; box-shadow: 0 2px 5px rgba(0,0,0,0.05); border: 1px solid #eaeaea; } .stat-value { font-size: 32px; font-weight: 700; color: #3498db; margin-bottom: 5px; } .stat-label { font-size: 14px; color: #6c757d; } /* 目标项样式 */ .goal-item { padding: 15px; margin-bottom: 15px; background: #f8f9fa; border-radius: 6px; border-left: 4px solid #9b59b6; } .goal-progress { margin-top: 10px; } .progress-bar { height: 8px; background: #e9ecef; border-radius: 4px; overflow: hidden; } .progress-fill { height: 100%; background: linear-gradient(90deg, #9b59b6, #8e44ad); border-radius: 4px; transition: width 0.5s ease; } .goal-details { display: flex; justify-content: space-between; margin-top: 5px; font-size: 14px; color: #6c757d; } /* 图表容器 */ .charts-container { margin-top: 20px; } .charts-container canvas { max-width: 100%; margin-bottom: 20px; } /* 模态框样式 */ .modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 1000; align-items: center; justify-content: center; } .modal-content { background: white; border-radius: 8px; width: 90%; max-width: 500px; padding: 30px; position: relative; animation: modalSlideIn 0.3s ease; } @keyframes modalSlideIn { from { opacity: 0; transform: translateY(-50px); } to { opacity: 1; transform: translateY(0); } } .close-modal { position: absolute; top: 15px; right: 20px; font-size: 24px; cursor: pointer; color: #6c757d; } .close-modal:hover { color: #343a40; } /* 表单样式 */ .form-group { margin-bottom: 20px; } .form-group label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .form-group input[type="text"], .form-group input[type="number"], .form-group select, .form-group textarea { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; font-size: 16px; transition: border-color 0.3s; } .form-group input:focus, .form-group select:focus, .form-group textarea:focus { outline: none; border-color: #3498db; box-shadow: 0 0 0 3px rgba(52,152,219,0.1); } .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; } .form-actions { display: flex; justify-content: flex-end; gap: 15px; margin-top: 30px; } .btn-cancel, .btn-submit { padding: 12px 25px; border-radius: 4px; font-weight: 600; cursor: pointer; border: none; transition: all 0.3s; } .btn-cancel { background: #f8f9fa; color: #6c757d; border: 1px solid #dee2e6; } .btn-cancel:hover { background: #e9ecef; } .btn-submit { background: #3498db; color: white; } .btn-submit:hover { background: #2980b9; } /* 响应式调整 */ @media (max-width: 768px) { .habit-tracker-header { flex-direction: column; align-items: flex-start; gap: 15px; } .form-row { grid-template-columns: 1fr; } .habits-stats { grid-template-columns: 1fr; } } 第五部分:JavaScript交互功能 5.1 主JavaScript文件 创建assets/js/habit-tracker.js文件: // 习惯追踪器主应用 class HabitTracker { constructor() { this.currentDate = new Date(); this.habits = []; this.goals = []; this.stats = {}; this.init(); } init() { // 绑定事件监听器 this.bindEvents(); // 加载初始数据 this.loadUserData(); // 初始化图表 this.initCharts(); } bindEvents() { // 日期导航 document.querySelector('.prev-date')?.addEventListener('click', () => this.changeDate(-1)); document.querySelector('.next-date')?.addEventListener('click', () => this.changeDate(1)); // 添加习惯按钮 document.querySelector('.btn-add-habit')?.addEventListener('click', () => this.showAddHabitModal()); // 模态框关闭 document.querySelector('.close-modal')?.addEventListener('click', () => this.hideModal()); document.querySelector('.btn-cancel')?.addEventListener('click', () => this.hideModal()); // 习惯表单提交 document.getElementById('habitForm')?.addEventListener('submit', (e) => this.handleAddHabit(e)); // 统计周期选择 document.getElementById('stats-period')?.addEventListener('change', (e) => this.updateStats(e.target.value)); // 点击模态框外部关闭 document.querySelector('.modal')?.addEventListener('click', (e) => { if (e.target.classList.contains('modal')) { this.hideModal(); } }); } // 加载用户数据 async loadUserData() { try { // 加载习惯 const habitsResponse = await this.ajaxRequest('get_user_habits'); this.habits = habitsResponse.data || []; // 加载目标 const goalsResponse = await this.ajaxRequest('get_user_goals'); this.goals = goalsResponse.data || []; // 加载今日习惯状态 const todayStatus = await this.ajaxRequest('get_today_status'); // 渲染数据 this.renderHabits(); this.renderGoals(); this.updateStats('month'); } catch (error) { console.error('加载数据失败:', error); this.showMessage('加载数据失败,请刷新页面重试', 'error'); } } // 渲染习惯列表 renderHabits() { const habitsList = document.querySelector('.habits-list'); if (!habitsList) return; if (this.habits.length === 0) { habitsList.innerHTML = ` <div class="empty-state"> <p>还没有添加任何习惯</p> <button class="btn-add-habit">添加第一个习惯</button> </div> `; return; } let html = ''; this.habits.forEach(habit => { const isChecked = habit.today_status === 'completed'; html += ` <div class="habit-item" data-habit-id="${habit.habit_id}" style="border-left-color: ${habit.color_code}"> <div class="habit-info"> <div class="habit-name">${habit.habit_name}</div> <div class="habit-meta"> <span class="habit-category">${this.getCategoryName(habit.habit_category)}</span> <span>已坚持 ${habit.current_streak || 0} 天</span> <span>${habit.completion_rate || 0}% 完成率</span> </div> </div> <div class="habit-actions"> <div class="check-habit ${isChecked ? 'checked' : ''}" onclick="habitTracker.toggleHabit(${habit.habit_id})"> </div> <button class="btn-habit-detail" onclick="habitTracker.showHabitDetail(${habit.habit_id})"> 详情 </button> </div> </div> `; }); habitsList.innerHTML = html; // 更新统计卡片 this.updateStatCards(); } // 渲染目标列表 renderGoals() { const goalsList = document.querySelector('.goals-list'); if (!goalsList) return; if (this.goals.length === 0) { goalsList.innerHTML = ` <div class="empty-state"> <p>还没有设置任何目标</p> <button class="btn-add-goal">添加第一个目标</button> </div> `; return; } let html = ''; this.goals.forEach(goal => { const progress = goal.target_value > 0 ? (goal.current_value / goal.target_value * 100) : 0; const progressPercent = Math.min(100, Math.round(progress)); html += ` <div class="goal-item" data-goal-id="${goal.goal_id}"> <div class="goal-info"> <div class="goal-name">${goal.goal_name}</div> <div class="goal-description">${goal.goal_description || ''}</div> </div> <div class="goal-progress"> <div class="progress-bar"> <div class="progress-fill" style="width: ${progressPercent}%"></div> </div> <div class="goal-details"> <span>${goal.current_value} / ${goal.target_value}</span> <span>${progressPercent}%</span> </div> </div> </div> `; }); goalsList.innerHTML = html; } // 切换习惯完成状态 async toggleHabit(habitId) { try { const response = await this.ajaxRequest('toggle_habit_status', { habit_id: habitId, date: this.formatDate(this.currentDate) }); if (response.success) { // 更新本地数据 const habit = this.habits.find(h => h.habit_id == habitId); if (habit) { habit.today_status = habit.today_status === 'completed' ? null : 'completed'; habit.current_streak = response.data.current_streak; habit.completion_rate = response.data.completion_rate; } // 重新渲染 this.renderHabits(); this.updateStats(); this.showMessage('习惯状态已更新', 'success'); } } catch (error) { console.error('更新习惯状态失败:', error); this.showMessage('更新失败,请重试', 'error'); } } // 显示添加习惯模态框 showAddHabitModal() { const modal = document.getElementById('addHabitModal'); if (modal) { modal.style.display = 'flex'; document.getElementById('habitName').focus(); } } // 隐藏模态框 hideModal() { const modal = document.getElementById('addHabitModal'); if (modal) { modal.style.display = 'none'; document.getElementById('habitForm').reset(); } } // 处理添加习惯表单提交 async handleAddHabit(e) { e.preventDefault(); const formData = new FormData(e.target); const habitData = { habit_name: formData.get('habit_name'), habit_description: formData.get('habit_description'), habit_category: formData.get('habit_category'), frequency: formData.get('frequency'), target_days: parseInt(formData.get('target_days')), color_code: formData.get('color_code'), start_date: this.formatDate(new Date()) }; try { const response = await this.ajaxRequest('add_habit', habitData); if (response.success) { // 添加新习惯到列表 this.habits.unshift(response.data); // 重新渲染 this.renderHabits(); this.hideModal(); this.showMessage('习惯添加成功', 'success'); } } catch (error) { console.error('添加习惯失败:', error); this.showMessage('添加失败,请重试', 'error'); } } // 更新统计卡片 updateStatCards() { if (this.habits.length === 0) return; // 计算最长连续天数 const longestStreak = Math.max(...this.habits.map(h => h.current_streak || 0)); // 计算本月完成率 const completedHabits = this.habits.filter(h => h.today_status === 'completed').length; const completionRate = this.habits.length > 0 ? Math.round((completedHabits / this.habits.length) * 100) : 0; // 更新DOM document.getElementById('current-streak').textContent = longestStreak; document.getElementById('completion-rate').textContent = `${completionRate}%`; document.getElementById('total-habits').textContent = this.habits.length; } // 更新统计图表 async updateStats(period = 'month') { try { const response = await this.ajaxRequest('get_stats', { period }); this.stats = response.data || {}; this.updateCharts(); } catch (error) { console.error('加载统计数据失败:', error); } } // 初始化图表 initCharts() { // 完成率图表 const completionCtx = document.getElementById('completionChart')?.getContext('2d'); if (completionCtx) { this.completionChart = new Chart(completionCtx, { type: 'line', data: { labels: [], datasets: [{ label: '完成率', data: [], borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)', fill: true, tension: 0.4 }] }, options: { responsive: true, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, max: 100, ticks: { callback: function(value) { return value + '%'; } } } } } }); } // 分类分布图表 const categoryCtx = document.getElementById('categoryChart')?.getContext('2d'); if (categoryCtx) { this.categoryChart = new Chart(categoryCtx, { type: 'doughnut', data: { labels: [], datasets: [{ data: [], backgroundColor: [ '#3498db', '#2ecc71', '#9b59b6', '#e74c3c', '#f39c12', '#1abc9c' ] }] }, options: { responsive: true, plugins: { legend: { position: 'bottom' } } } }); } } // 更新图表数据 updateCharts() { // 更新完成率图表 if (this.completionChart && this.stats.completion_trend) { const trend = this.stats.completion_trend; this.completionChart.data.labels = trend.labels; this.completionChart.data.datasets[0].data = trend.data; this.completionChart.update(); } // 更新分类分布图表 if (this.categoryChart && this.stats.category_distribution) { const distribution = this.stats.category_distribution; this.categoryChart.data.labels = distribution.labels; this.categoryChart.data.datasets[0].data = distribution.data; this.categoryChart.update(); } } // 辅助方法:AJAX请求 async ajaxRequest(action, data = {}) { return new Promise((resolve, reject) => { jQuery.ajax({ url: habitTrackerAjax.ajax_url,

发表评论

手把手教学,为WordPress集成智能化的网站图片懒加载与CDN加速工具

手把手教学:为WordPress集成智能化的网站图片懒加载与CDN加速工具 引言:为什么WordPress网站需要图片懒加载与CDN加速? 在当今互联网环境中,网站加载速度已成为影响用户体验、搜索引擎排名和转化率的关键因素。据统计,页面加载时间每增加1秒,转化率就会下降7%。对于WordPress网站而言,图片通常是页面中最大的资源,占用了大量的带宽和加载时间。 图片懒加载和CDN加速是解决这一问题的两个关键技术: 图片懒加载:延迟加载非视口内的图片,只有当用户滚动到图片附近时才加载,显著减少初始页面加载时间。 CDN加速:通过全球分布的服务器网络分发静态资源,减少用户与服务器之间的物理距离,加快资源加载速度。 本文将手把手教您如何通过WordPress代码二次开发,集成智能化的图片懒加载与CDN加速功能,无需依赖臃肿的插件,实现轻量高效的性能优化。 第一部分:理解WordPress图片加载机制 1.1 WordPress默认图片处理方式 WordPress通过the_content()函数和特色图像功能输出图片时,会生成标准的HTML <img>标签。例如: <img src="https://example.com/wp-content/uploads/2023/05/image.jpg" alt="示例图片" width="800" height="600" class="aligncenter size-full wp-image-123"> 这种传统方式会在页面加载时立即请求所有图片,无论用户是否能看到它们。 1.2 现代图片优化技术 现代网站优化通常采用以下技术: 响应式图片:根据设备屏幕尺寸提供不同大小的图片 下一代图片格式:WebP、AVIF等更高效的格式 懒加载:延迟加载非视口图片 CDN分发:通过边缘节点快速交付图片 第二部分:实现智能图片懒加载功能 2.1 懒加载的基本原理 懒加载的核心思想是:将图片的src属性替换为data-src属性,当图片进入视口时,再将data-src的值赋给src属性,触发图片加载。 2.2 创建懒加载函数 在您的WordPress主题的functions.php文件中添加以下代码: /** * 为WordPress图片添加懒加载功能 */ function add_lazy_loading_to_images($content) { // 如果内容为空,直接返回 if (empty($content)) { return $content; } // 创建DOM文档对象 $dom = new DOMDocument(); // 抑制HTML解析错误警告 libxml_use_internal_errors(true); // 加载HTML内容,指定编码为UTF-8 $dom->loadHTML('<?xml encoding="UTF-8">' . $content); // 清除错误 libxml_clear_errors(); // 获取所有图片标签 $images = $dom->getElementsByTagName('img'); // 遍历所有图片 foreach ($images as $image) { // 获取原始src $src = $image->getAttribute('src'); // 如果src为空,跳过 if (empty($src)) { continue; } // 获取图片类名 $classes = $image->getAttribute('class'); // 排除某些不需要懒加载的图片(如首屏图片) if (strpos($classes, 'no-lazy') !== false) { continue; } // 将src转换为data-src $image->setAttribute('data-src', $src); // 设置占位符(1x1像素的透明GIF) $image->setAttribute('src', 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='); // 添加懒加载类 $new_classes = $classes . ' lazy-load'; $image->setAttribute('class', trim($new_classes)); // 添加noscript标签作为降级方案 $noscript = $dom->createElement('noscript'); $noscript_img = $dom->createElement('img'); $noscript_img->setAttribute('src', $src); // 复制所有原始属性到noscript图片 foreach ($image->attributes as $attr) { if ($attr->nodeName !== 'data-src' && $attr->nodeName !== 'class') { $noscript_img->setAttribute($attr->nodeName, $attr->nodeValue); } } $noscript->appendChild($noscript_img); $image->parentNode->insertBefore($noscript, $image->nextSibling); } // 保存修改后的HTML $content = $dom->saveHTML(); // 提取body内容 $content = preg_replace('/^<!DOCTYPE.+?>/', '', $content); $content = str_replace(array('<html>', '</html>', '<body>', '</body>'), array('', '', '', ''), $content); return $content; } // 将懒加载应用到文章内容 add_filter('the_content', 'add_lazy_loading_to_images', 99); // 将懒加载应用到文章摘要 add_filter('get_the_excerpt', 'add_lazy_loading_to_images', 99); // 将懒加载应用到小工具内容 add_filter('widget_text', 'add_lazy_loading_to_images', 99); 2.3 添加懒加载JavaScript 在主题的footer.php文件之前或使用wp_footer钩子添加JavaScript代码: /** * 添加懒加载JavaScript */ function add_lazy_load_script() { ?> <script> (function() { 'use strict'; // 懒加载配置 var lazyLoadConfig = { // 图片进入视口前多少像素开始加载 rootMargin: '50px 0px', // 图片加载阈值 threshold: 0.01 }; // 检查浏览器是否支持IntersectionObserver if ('IntersectionObserver' in window) { // 使用IntersectionObserver API实现懒加载 var lazyImages = document.querySelectorAll('img.lazy-load'); var imageObserver = new IntersectionObserver(function(entries, observer) { entries.forEach(function(entry) { if (entry.isIntersecting) { var lazyImage = entry.target; // 替换data-src为src if (lazyImage.dataset.src) { lazyImage.src = lazyImage.dataset.src; } // 替换data-srcset为srcset(响应式图片) if (lazyImage.dataset.srcset) { lazyImage.srcset = lazyImage.dataset.srcset; } // 加载完成后移除懒加载类 lazyImage.onload = function() { lazyImage.classList.remove('lazy-load'); lazyImage.classList.add('lazy-loaded'); }; // 处理加载错误 lazyImage.onerror = function() { lazyImage.classList.remove('lazy-load'); lazyImage.classList.add('lazy-error'); console.error('图片加载失败: ' + lazyImage.dataset.src); }; // 停止观察该图片 imageObserver.unobserve(lazyImage); } }); }, lazyLoadConfig); // 开始观察所有懒加载图片 lazyImages.forEach(function(lazyImage) { imageObserver.observe(lazyImage); }); } else { // 不支持IntersectionObserver的降级方案 var lazyImages = document.querySelectorAll('img.lazy-load'); var lazyLoadThrottleTimeout; function lazyLoad() { if (lazyLoadThrottleTimeout) { clearTimeout(lazyLoadThrottleTimeout); } lazyLoadThrottleTimeout = setTimeout(function() { var scrollTop = window.pageYOffset; lazyImages.forEach(function(lazyImage) { if (lazyImage.offsetTop < (window.innerHeight + scrollTop)) { // 替换data-src为src if (lazyImage.dataset.src) { lazyImage.src = lazyImage.dataset.src; } // 替换data-srcset为srcset if (lazyImage.dataset.srcset) { lazyImage.srcset = lazyImage.dataset.srcset; } // 加载完成后移除懒加载类 lazyImage.onload = function() { lazyImage.classList.remove('lazy-load'); lazyImage.classList.add('lazy-loaded'); }; // 从数组中移除已处理的图片 lazyImages = Array.prototype.filter.call(lazyImages, function(image) { return image !== lazyImage; }); // 如果所有图片都已加载,移除滚动事件监听 if (lazyImages.length === 0) { document.removeEventListener('scroll', lazyLoad); window.removeEventListener('resize', lazyLoad); window.removeEventListener('orientationchange', lazyLoad); } } }); }, 20); } // 监听滚动、调整大小和方向变化事件 document.addEventListener('scroll', lazyLoad); window.addEventListener('resize', lazyLoad); window.addEventListener('orientationchange', lazyLoad); // 初始加载 lazyLoad(); } // 添加CSS样式 var style = document.createElement('style'); style.textContent = ` img.lazy-load { opacity: 0; transition: opacity 0.3s; } img.lazy-loaded { opacity: 1; } img.lazy-error { opacity: 0.5; filter: grayscale(100%); } `; document.head.appendChild(style); })(); </script> <?php } add_action('wp_footer', 'add_lazy_load_script', 99); 第三部分:集成CDN加速功能 3.1 CDN加速原理 CDN(内容分发网络)通过将静态资源(如图片、CSS、JavaScript)缓存到全球分布的边缘服务器上,使用户可以从地理位置上最近的服务器获取资源,从而显著减少延迟。 3.2 配置WordPress使用CDN 在functions.php中添加以下代码,将本地资源URL替换为CDN URL: /** * 配置WordPress使用CDN */ function setup_cdn_for_wordpress() { // CDN配置 $cdn_config = array( // 启用CDN 'enabled' => true, // CDN域名(请替换为您的CDN域名) 'cdn_domain' => 'cdn.yourdomain.com', // 本地域名(您的WordPress网站域名) 'local_domain' => $_SERVER['HTTP_HOST'], // 要使用CDN的资源类型 'file_extensions' => array('jpg', 'jpeg', 'png', 'gif', 'webp', 'ico', 'svg', 'css', 'js', 'pdf', 'zip', 'mp4', 'mp3', 'woff', 'woff2', 'ttf', 'eot'), // 排除的路径(某些路径下的资源不使用CDN) 'exclude_paths' => array('/wp-admin/', '/wp-includes/', '/wp-content/plugins/', '/wp-content/themes/'), // 是否对管理员禁用CDN 'disable_for_admin' => true ); // 如果CDN未启用,直接返回 if (!$cdn_config['enabled']) { return; } // 对管理员禁用CDN if ($cdn_config['disable_for_admin'] && current_user_can('manage_options')) { return; } // 返回配置 return $cdn_config; } /** * 将本地URL替换为CDN URL */ function replace_urls_with_cdn($url) { // 获取CDN配置 $cdn_config = setup_cdn_for_wordpress(); // 如果没有配置或CDN未启用,返回原URL if (!$cdn_config || !$cdn_config['enabled']) { return $url; } // 解析URL $parsed_url = parse_url($url); // 如果不是HTTP/HTTPS协议,返回原URL if (!isset($parsed_url['scheme']) || !in_array($parsed_url['scheme'], array('http', 'https'))) { return $url; } // 检查主机名是否匹配本地域名 if (isset($parsed_url['host']) && $parsed_url['host'] === $cdn_config['local_domain']) { // 检查路径是否在排除列表中 if (isset($parsed_url['path'])) { foreach ($cdn_config['exclude_paths'] as $exclude_path) { if (strpos($parsed_url['path'], $exclude_path) === 0) { return $url; } } } // 检查文件扩展名 $path = isset($parsed_url['path']) ? $parsed_url['path'] : ''; $extension = pathinfo($path, PATHINFO_EXTENSION); if (empty($extension) || !in_array(strtolower($extension), $cdn_config['file_extensions'])) { return $url; } // 替换域名为CDN域名 $url = str_replace( $cdn_config['local_domain'], $cdn_config['cdn_domain'], $url ); } return $url; } // 应用CDN替换到各种URL add_filter('wp_get_attachment_url', 'replace_urls_with_cdn'); add_filter('the_content', 'cdn_replace_content_urls', 99); add_filter('widget_text', 'cdn_replace_content_urls', 99); add_filter('stylesheet_uri', 'replace_urls_with_cdn'); add_filter('script_loader_src', 'replace_urls_with_cdn'); add_filter('style_loader_src', 'replace_urls_with_cdn'); /** * 替换内容中的URL为CDN URL */ function cdn_replace_content_urls($content) { // 获取CDN配置 $cdn_config = setup_cdn_for_wordpress(); // 如果没有配置或CDN未启用,返回原内容 if (!$cdn_config || !$cdn_config['enabled']) { return $content; } // 正则表达式匹配URL $local_domain = preg_quote($cdn_config['local_domain'], '/'); $cdn_domain = $cdn_config['cdn_domain']; // 匹配图片、CSS、JS等资源的URL $pattern = '/https?://' . $local_domain . '/[^"'s]*.(' . implode('|', $cdn_config['file_extensions']) . ')(?:?[^"'s]*)?/i'; // 替换URL $content = preg_replace_callback($pattern, function($matches) use ($cdn_config) { $url = $matches[0]; // 检查路径是否在排除列表中 foreach ($cdn_config['exclude_paths'] as $exclude_path) { if (strpos($url, $exclude_path) !== false) { return $url; } } // 替换域名为CDN域名 return str_replace( '//' . $cdn_config['local_domain'], '//' . $cdn_config['cdn_domain'], $url ); }, $content); return $content; } 3.3 高级CDN功能:自动WebP转换 现代CDN通常支持自动图片格式转换。以下代码可以检测浏览器是否支持WebP,并相应调整图片URL: /** * 根据浏览器支持自动提供WebP格式图片 */ function provide_webp_when_supported($url, $attachment_id) { // 获取CDN配置 $cdn_config = setup_cdn_for_wordpress(); // 如果没有配置或CDN未启用,返回原URL if (!$cdn_config || !$cdn_config['enabled']) { return $url; } // 检查URL是否已经是CDN URL if (strpos($url, $cdn_config['cdn_domain']) === false) { return $url; } // 检查文件扩展名 $extension = pathinfo($url, PATHINFO_EXTENSION); $supported_extensions = array('jpg', 'jpeg', 'png'); if (!in_array(strtolower($extension), $supported_extensions)) { return $url; } // 检查浏览器是否支持WebP if (isset($_SERVER['HTTP_ACCEPT']) && strpos($_SERVER['HTTP_ACCEPT'], 'image/webp') !== false) { // 在URL中添加WebP参数或修改扩展名 // 这取决于您的CDN提供商如何配置WebP转换 // 示例:在URL末尾添加?format=webp if (strpos($url, '?') === false) { $url .= '?format=webp'; } else { $url .= '&format=webp'; } } return $url; } // 为图片URL添加WebP支持检测 add_filter('wp_get_attachment_url', 'provide_webp_when_supported', 10, 2); add_filter('the_content', 'add_webp_support_to_content', 99); /** * 为内容中的图片添加WebP支持 */ function add_webp_support_to_content($content) { // 如果浏览器不支持WebP,直接返回 if (!isset($_SERVER['HTTP_ACCEPT']) || strpos($_SERVER['HTTP_ACCEPT'], 'image/webp') === false) { return $content; } // 获取CDN配置 $cdn_config = setup_cdn_for_wordpress(); // 如果没有配置或CDN未启用,返回原内容 if (!$cdn_config || !$cdn_config['enabled']) { return $content; } 图片URL并添加WebP参数 $cdn_domain = preg_quote($cdn_config['cdn_domain'], '/'); $pattern = '/https?://' . $cdn_domain . '/[^"'s]*.(jpg|jpeg|png)(?:?[^"'s]*)?/i'; $content = preg_replace_callback($pattern, function($matches) { $url = $matches[0]; // 添加WebP格式参数 if (strpos($url, '?') === false) { return $url . '?format=webp'; } else { return $url . '&format=webp'; } }, $content); return $content; } ## 第四部分:集成响应式图片支持 ### 4.1 WordPress响应式图片基础 WordPress 4.4+ 已内置响应式图片支持,但我们可以进一步优化: /** 增强WordPress响应式图片功能 */ function enhance_responsive_images($html, $attachment_id, $size, $icon, $attr) { // 获取原始图片URL $image_url = wp_get_attachment_url($attachment_id); if (!$image_url) { return $html; } // 获取图片元数据 $image_meta = wp_get_attachment_metadata($attachment_id); if (!$image_meta) { return $html; } // 获取CDN配置 $cdn_config = setup_cdn_for_wordpress(); // 如果是CDN URL,确保响应式图片srcset也使用CDN if ($cdn_config && $cdn_config['enabled']) { $html = str_replace( 'srcset="' . $cdn_config['local_domain'], 'srcset="' . $cdn_config['cdn_domain'], $html ); } // 添加懒加载属性 $html = preg_replace('/<img(.*?)src=/', '<img$1data-src=', $html); $html = preg_replace('/srcset=/', 'data-srcset=', $html); // 添加懒加载类 if (strpos($html, 'class="') !== false) { $html = preg_replace('/class="(.*?)"/', 'class="$1 lazy-load"', $html); } else { $html = preg_replace('/<img/', '<img class="lazy-load"', $html); } // 添加noscript回退 $noscript_html = preg_replace('/data-src=/', 'src=', $html); $noscript_html = preg_replace('/data-srcset=/', 'srcset=', $noscript_html); $noscript_html = preg_replace('/class="(.*?)lazy-load(.*?)"/', 'class="$1$2"', $noscript_html); $html .= '<noscript>' . $noscript_html . '</noscript>'; return $html; } // 应用增强的响应式图片功能add_filter('wp_get_attachment_image', 'enhance_responsive_images', 10, 5); ### 4.2 自定义图片尺寸生成 /** 添加自定义图片尺寸用于响应式设计 */ function add_custom_image_sizes() { // 添加各种设备适用的图片尺寸 add_image_size('retina_large', 1920, 0, false); // 大屏Retina设备 add_image_size('desktop_large', 1200, 0, false); // 桌面大屏 add_image_size('desktop_medium', 800, 0, false); // 桌面中屏 add_image_size('tablet_large', 600, 0, false); // 平板大屏 add_image_size('tablet_small', 400, 0, false); // 平板小屏 add_image_size('mobile_large', 300, 0, false); // 手机大屏 add_image_size('mobile_small', 150, 0, false); // 手机小屏 } add_action('after_setup_theme', 'add_custom_image_sizes'); /** 自定义图片尺寸的srcset */ function custom_image_srcset($sources, $size_array, $image_src, $image_meta, $attachment_id) { // 获取CDN配置 $cdn_config = setup_cdn_for_wordpress(); // 自定义尺寸映射 $custom_sizes = array( 'retina_large' => 1920, 'desktop_large' => 1200, 'desktop_medium' => 800, 'tablet_large' => 600, 'tablet_small' => 400, 'mobile_large' => 300, 'mobile_small' => 150 ); foreach ($custom_sizes as $size_name => $width) { // 检查是否有该尺寸的图片 if (isset($image_meta['sizes'][$size_name])) { $image_url = wp_get_attachment_image_url($attachment_id, $size_name); // 应用CDN if ($cdn_config && $cdn_config['enabled']) { $image_url = replace_urls_with_cdn($image_url); } $sources[$width] = array( 'url' => $image_url, 'descriptor' => 'w', 'value' => $width, ); } } // 按宽度排序 ksort($sources); return $sources; } add_filter('wp_calculate_image_srcset', 'custom_image_srcset', 10, 5); ## 第五部分:性能优化与缓存策略 ### 5.1 浏览器缓存优化 /** 添加浏览器缓存头 */ function add_cache_headers() { // 如果不是管理页面 if (!is_admin()) { // 设置静态资源缓存时间(1年) $cache_time = 31536000; // 60*60*24*365 // 获取当前请求的扩展名 $request_uri = $_SERVER['REQUEST_URI']; $extension = pathinfo($request_uri, PATHINFO_EXTENSION); // 静态资源扩展名列表 $static_extensions = array( 'jpg', 'jpeg', 'png', 'gif', 'webp', 'ico', 'svg', 'css', 'js', 'pdf', 'zip', 'mp4', 'mp3', 'woff', 'woff2', 'ttf', 'eot' ); if (in_array(strtolower($extension), $static_extensions)) { header("Cache-Control: public, max-age={$cache_time}, immutable"); header("Expires: " . gmdate('D, d M Y H:i:s', time() + $cache_time) . ' GMT'); // 添加ETag $etag = md5($request_uri . filemtime(ABSPATH . $request_uri)); header("ETag: {$etag}"); } } } add_action('send_headers', 'add_cache_headers'); ### 5.2 资源预加载与预连接 /** 添加资源提示(Resource Hints) */ function add_resource_hints() { // 获取CDN配置 $cdn_config = setup_cdn_for_wordpress(); if ($cdn_config && $cdn_config['enabled']) { // 预连接到CDN域名 echo '<link rel="preconnect" href="https://' . esc_attr($cdn_config['cdn_domain']) . '" crossorigin>'; // DNS预获取 echo '<link rel="dns-prefetch" href="//' . esc_attr($cdn_config['cdn_domain']) . '">'; } // 预加载关键资源 echo '<link rel="preload" href="' . get_template_directory_uri() . '/assets/css/critical.css" as="style">'; echo '<link rel="preload" href="' . get_template_directory_uri() . '/assets/js/lazyload.js" as="script">'; } add_action('wp_head', 'add_resource_hints', 1); ## 第六部分:监控与调试功能 ### 6.1 性能监控 /** 添加性能监控 */ class PerformanceMonitor { private $start_time; private $queries = array(); public function __construct() { $this->start_time = microtime(true); // 监控数据库查询 if (defined('SAVEQUERIES') && SAVEQUERIES) { add_filter('query', array($this, 'log_query')); } } public function log_query($query) { $this->queries[] = array( 'query' => $query, 'time' => microtime(true) ); return $query; } public function get_performance_data() { $end_time = microtime(true); $load_time = ($end_time - $this->start_time) * 1000; // 转换为毫秒 $data = array( 'load_time' => round($load_time, 2), 'memory_usage' => round(memory_get_peak_usage() / 1024 / 1024, 2), 'query_count' => count($this->queries), 'cdn_enabled' => false ); // 检查CDN状态 $cdn_config = setup_cdn_for_wordpress(); if ($cdn_config && $cdn_config['enabled']) { $data['cdn_enabled'] = true; $data['cdn_domain'] = $cdn_config['cdn_domain']; } return $data; } } // 初始化性能监控$performance_monitor = new PerformanceMonitor(); /** 在页脚显示性能数据(仅管理员可见) */ function display_performance_data() { global $performance_monitor; if (current_user_can('manage_options')) { $data = $performance_monitor->get_performance_data(); echo '<div style="position:fixed;bottom:10px;right:10px;background:rgba(0,0,0,0.8);color:#fff;padding:10px;font-size:12px;z-index:9999;border-radius:5px;">'; echo '<strong>性能数据:</strong><br>'; echo '加载时间: ' . $data['load_time'] . 'ms<br>'; echo '内存使用: ' . $data['memory_usage'] . 'MB<br>'; echo '数据库查询: ' . $data['query_count'] . '<br>'; echo 'CDN: ' . ($data['cdn_enabled'] ? '已启用 (' . $data['cdn_domain'] . ')' : '未启用'); echo '</div>'; } } add_action('wp_footer', 'display_performance_data'); ### 6.2 图片加载错误处理 /** 图片加载错误处理 */ function handle_image_errors() { ?> <script> document.addEventListener('DOMContentLoaded', function() { // 监听图片加载错误 document.addEventListener('error', function(e) { if (e.target.tagName === 'IMG') { var img = e.target; // 如果是懒加载图片 if (img.classList.contains('lazy-load')) { // 尝试加载原始src(非CDN版本) var originalSrc = img.dataset.src; if (originalSrc) { // 移除CDN域名,尝试加载原始图片 var localSrc = originalSrc.replace('cdn.yourdomain.com', 'yourdomain.com'); img.src = localSrc; // 标记为回退加载 img.dataset.fallback = 'true'; } } // 添加错误类 img.classList.add('image-error'); // 可选:显示占位图 if (!img.dataset.placeholderSet) { img.style.backgroundColor = '#f0f0f0'; img.style.minHeight = '100px'; img.dataset.placeholderSet = 'true'; } } }, true); // 使用捕获阶段 }); </script> <style> .image-error { opacity: 0.5; filter: grayscale(100%); } .image-error::after { content: '图片加载失败'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: #999; font-size: 12px; } </style> <?php } add_action('wp_footer', 'handle_image_errors'); ## 第七部分:完整集成与配置 ### 7.1 创建配置页面 /** 创建懒加载和CDN配置页面 */ class LazyLoadCDN_Settings { public function __construct() { add_action('admin_menu', array($this, 'add_admin_menu')); add_action('admin_init', array($this, 'settings_init')); } public function add_admin_menu() { add_options_page( '懒加载与CDN设置', '图片优化', 'manage_options', 'lazyload-cdn-settings', array($this, 'settings_page') ); } public function settings_init() { register_setting('lazyload_cdn', 'lazyload_cdn_settings'); add_settings_section( 'lazyload_cdn_section', '懒加载与CDN配置', array($this, 'settings_section_callback'), 'lazyload_cdn' ); // CDN域名设置 add_settings_field( 'cdn_domain', 'CDN域名', array($this, 'cdn_domain_render'), 'lazyload_cdn', 'lazyload_cdn_section' ); // 懒加载启用设置 add_settings_field( 'lazyload_enabled', '启用懒加载', array($this, 'lazyload_enabled_render'), 'lazyload_cdn', 'lazyload_cdn_section' ); // WebP支持设置 add_settings_field( 'webp_support', '启用WebP支持', array($this, 'webp_support_render'), 'lazyload_cdn', 'lazyload_cdn_section' ); // 排除路径设置 add_settings_field( 'exclude_paths', '排除路径', array($this, 'exclude_paths_render'), 'lazyload_cdn', 'lazyload_cdn_section' ); } public function cdn_domain_render() { $options = get_option('lazyload_cdn_settings'); ?> <input type="text" name="lazyload_cdn_settings[cdn_domain]" value="<?php echo isset($options['cdn_domain']) ? esc_attr($options['cdn_domain']) : ''; ?>" placeholder="cdn.yourdomain.com"> <p class="description">请输入您的CDN域名,例如:cdn.yourdomain.com</p> <?php } public function lazyload_enabled_render() { $options = get_option('lazyload_cdn_settings'); ?> <input type="checkbox" name="lazyload_cdn_settings[lazyload_enabled]" value="1" <?php checked(isset($options['lazyload_enabled']) && $options['lazyload_enabled']); ?>> <label>启用图片懒加载功能</label> <?php } public function webp_support_render() { $options = get_option('lazyload_cdn_settings'); ?> <input type="checkbox" name="lazyload_cdn_settings[webp_support]" value="1" <?php checked(isset($options['webp_support']) && $options['webp_support']); ?>> <label>为支持WebP的浏览器自动提供WebP格式图片</label> <?php } public function exclude_paths_render() { $options = get_option('lazyload_cdn_settings'); $paths = isset($options['exclude_paths']) ? $options['exclude_paths'] : "/wp-admin/n/wp-includes/"; ?> <textarea name="lazyload_cdn_settings[exclude_paths]" rows="5" cols="50"><?php echo esc_textarea($paths); ?></textarea> <p class="description">每行一个路径,这些路径下的资源将不使用CDN</p> <?php } public function settings_section_callback() { echo '配置图片懒加载和CDN加速功能'; } public function settings_page() { ?> <div class="wrap"> <h1>懒加载与CDN设置</h1> <form action="options.php" method="post"> <?php settings_fields('lazyload_cdn'); do_settings_sections('lazyload_cdn'); submit_button(); ?> </form> <div class="card"> <h2>使用说明</h2> <ol> <li>配置CDN域名后,所有静态资源将通过CDN加速</li> <li>懒加载功能会延迟加载非视口内的图片</li> <li>WebP支持会自动为兼容浏览器提供更小的图片格式</li> <li>保存设置后,请清除缓存以查看效果</li> </ol> </div> </div> <?php } } // 初始化设置页面if (is_admin()) { new LazyLoadCDN_Settings(); } ### 7.2 更新配置获取函数 /** 更新CDN配置获取函数,

发表评论

详细指南,开发网站线上活动抽奖与实时弹幕互动展示系统

详细指南:开发网站线上活动抽奖与实时弹幕互动展示系统 摘要 在当今数字化时代,线上活动已成为企业与用户互动的重要方式。本文将详细介绍如何通过WordPress程序的代码二次开发,实现一个集线上抽奖与实时弹幕互动展示于一体的系统。我们将从系统设计、技术选型、代码实现到部署测试,全面解析这一常用互联网小工具功能的开发过程,帮助您为网站增添互动性与趣味性。 第一章:系统概述与需求分析 1.1 项目背景与意义 随着互联网技术的快速发展,线上活动已成为企业营销、社区互动和用户参与的重要手段。抽奖活动能够有效提升用户参与度,而实时弹幕互动则能增强活动的趣味性和即时互动性。将这两种功能结合,可以为线上活动带来全新的体验。 传统的线上活动工具往往功能单一,且与网站集成度不高。通过WordPress二次开发实现这一系统,不仅可以充分利用WordPress庞大的用户基础和成熟的生态系统,还能根据具体需求进行深度定制,实现与网站的无缝集成。 1.2 系统功能需求 本系统需要实现以下核心功能: 抽奖系统功能: 支持多种抽奖模式(随机抽取、按条件筛选抽取) 灵活的奖品设置与管理 参与资格验证与限制 中奖结果实时展示与通知 中奖记录管理与导出 实时弹幕互动功能: 用户实时发送弹幕消息 弹幕样式自定义(颜色、大小、位置) 弹幕内容审核与过滤 弹幕数据统计与分析 弹幕显示控制(速度、密度、显示区域) 管理后台功能: 活动创建与配置 参与用户管理 中奖记录查看与操作 弹幕内容审核与管理 数据统计与报表生成 1.3 技术选型与架构设计 技术栈选择: 后端:WordPress + PHP 7.4+ 前端:HTML5 + CSS3 + JavaScript (ES6+) 实时通信:WebSocket (Ratchet或Swoole) 数据库:MySQL 5.7+ 缓存:Redis (可选,用于提升性能) 系统架构:采用前后端分离的设计思路,前端通过AJAX与WordPress REST API交互,实时功能通过WebSocket实现。抽奖逻辑在服务器端执行确保公平性,弹幕数据通过WebSocket广播实现实时展示。 第二章:开发环境搭建与准备工作 2.1 WordPress环境配置 首先需要搭建一个标准的WordPress开发环境: // 推荐使用本地开发环境 // 1. 安装XAMPP/MAMP/WAMP等集成环境 // 2. 下载最新版WordPress // 3. 创建数据库并完成WordPress安装 // 4. 启用调试模式,在wp-config.php中添加: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); // 5. 安装必要的开发插件: // - Query Monitor (性能调试) // - Show Current Template (模板调试) // - Advanced Custom Fields (字段管理) 2.2 创建自定义插件 为保持代码的可维护性和可移植性,我们将所有功能封装为一个独立的WordPress插件: /* Plugin Name: 线上活动抽奖与弹幕系统 Plugin URI: https://yourwebsite.com/ Description: 提供线上抽奖与实时弹幕互动功能 Version: 1.0.0 Author: Your Name License: GPL v2 or later Text Domain: lottery-danmaku */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('LD_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('LD_PLUGIN_URL', plugin_dir_url(__FILE__)); define('LD_VERSION', '1.0.0'); // 初始化插件 require_once LD_PLUGIN_DIR . 'includes/class-core.php'; require_once LD_PLUGIN_DIR . 'includes/class-database.php'; require_once LD_PLUGIN_DIR . 'includes/class-websocket.php'; function ld_init() { $plugin = Lottery_Danmaku_Core::get_instance(); $plugin->init(); } add_action('plugins_loaded', 'ld_init'); 2.3 数据库设计 创建必要的数据库表来存储活动、参与记录、弹幕等数据: // includes/class-database.php class Lottery_Danmaku_Database { public function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 活动表 $table_activities = $wpdb->prefix . 'ld_activities'; $sql_activities = "CREATE TABLE IF NOT EXISTS $table_activities ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, title VARCHAR(200) NOT NULL, description TEXT, type ENUM('lottery', 'danmaku', 'both') DEFAULT 'both', start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status ENUM('draft', 'active', 'ended', 'archived') DEFAULT 'draft', settings LONGTEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; // 抽奖奖品表 $table_prizes = $wpdb->prefix . 'ld_prizes'; $sql_prizes = "CREATE TABLE IF NOT EXISTS $table_prizes ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, activity_id BIGINT(20) UNSIGNED NOT NULL, name VARCHAR(200) NOT NULL, description TEXT, quantity INT(11) NOT NULL DEFAULT 1, level INT(11) DEFAULT 1, probability DECIMAL(5,4) DEFAULT 0.0, remaining INT(11) NOT NULL DEFAULT 0, PRIMARY KEY (id), FOREIGN KEY (activity_id) REFERENCES $table_activities(id) ON DELETE CASCADE ) $charset_collate;"; // 参与记录表 $table_participants = $wpdb->prefix . 'ld_participants'; $sql_participants = "CREATE TABLE IF NOT EXISTS $table_participants ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, activity_id BIGINT(20) UNSIGNED NOT NULL, user_id BIGINT(20) UNSIGNED, user_email VARCHAR(100), user_name VARCHAR(100), joined_at DATETIME DEFAULT CURRENT_TIMESTAMP, ip_address VARCHAR(45), user_agent TEXT, is_winner TINYINT(1) DEFAULT 0, prize_id BIGINT(20) UNSIGNED, PRIMARY KEY (id), INDEX activity_user (activity_id, user_id), FOREIGN KEY (activity_id) REFERENCES $table_activities(id) ON DELETE CASCADE ) $charset_collate;"; // 弹幕消息表 $table_danmaku = $wpdb->prefix . 'ld_danmaku'; $sql_danmaku = "CREATE TABLE IF NOT EXISTS $table_danmaku ( id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, activity_id BIGINT(20) UNSIGNED NOT NULL, user_id BIGINT(20) UNSIGNED, content TEXT NOT NULL, color VARCHAR(7) DEFAULT '#FFFFFF', size INT(11) DEFAULT 24, position ENUM('top', 'bottom', 'fly') DEFAULT 'fly', status ENUM('pending', 'approved', 'rejected') DEFAULT 'pending', sent_at DATETIME DEFAULT CURRENT_TIMESTAMP, displayed TINYINT(1) DEFAULT 0, PRIMARY KEY (id), INDEX activity_status (activity_id, status), FOREIGN KEY (activity_id) REFERENCES $table_activities(id) ON DELETE CASCADE ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql_activities); dbDelta($sql_prizes); dbDelta($sql_participants); dbDelta($sql_danmaku); } } 第三章:抽奖系统核心功能实现 3.1 活动管理模块 创建活动管理后台界面,使用WordPress的Settings API和Custom Post Type: // includes/class-admin.php class Lottery_Danmaku_Admin { public function register_post_types() { // 注册活动自定义文章类型 $args = array( 'labels' => array( 'name' => __('活动管理', 'lottery-danmaku'), 'singular_name' => __('活动', 'lottery-danmaku'), ), 'public' => false, 'show_ui' => true, 'show_in_menu' => true, 'menu_position' => 30, 'menu_icon' => 'dashicons-megaphone', 'supports' => array('title', 'editor'), 'capability_type' => 'post', 'capabilities' => array( 'create_posts' => 'manage_options', ), 'map_meta_cap' => true, ); register_post_type('ld_activity', $args); // 添加活动管理页面 add_menu_page( __('抽奖弹幕系统', 'lottery-danmaku'), __('抽奖弹幕', 'lottery-danmaku'), 'manage_options', 'lottery-danmaku', array($this, 'admin_page'), 'dashicons-awards', 30 ); } public function admin_page() { ?> <div class="wrap"> <h1><?php echo esc_html(get_admin_page_title()); ?></h1> <div class="ld-admin-container"> <div class="ld-tabs"> <button class="ld-tab active" data-target="activities">活动管理</button> <button class="ld-tab" data-target="prizes">奖品管理</button> <button class="ld-tab" data-target="participants">参与记录</button> <button class="ld-tab" data-target="danmaku">弹幕审核</button> <button class="ld-tab" data-target="settings">系统设置</button> </div> <div class="ld-tab-content active" id="activities"> <!-- 活动管理界面 --> <?php $this->render_activities_table(); ?> </div> <div class="ld-tab-content" id="prizes"> <!-- 奖品管理界面 --> <?php $this->render_prizes_table(); ?> </div> <!-- 其他标签页内容 --> </div> </div> <?php } } 3.2 抽奖算法实现 实现公平、高效的抽奖算法: // includes/class-lottery.php class Lottery_Engine { /** * 执行抽奖 * @param int $activity_id 活动ID * @param int $user_id 用户ID * @return array 抽奖结果 */ public function draw($activity_id, $user_id) { global $wpdb; // 验证活动状态 $activity = $this->get_activity($activity_id); if (!$activity || $activity->status !== 'active') { return array('success' => false, 'message' => '活动未开始或已结束'); } // 验证用户参与资格 if (!$this->check_eligibility($activity_id, $user_id)) { return array('success' => false, 'message' => '您不符合参与条件'); } // 检查用户是否已参与 if ($this->has_participated($activity_id, $user_id)) { return array('success' => false, 'message' => '您已参与过本次活动'); } // 获取奖品列表 $prizes = $this->get_available_prizes($activity_id); if (empty($prizes)) { return array('success' => false, 'message' => '奖品已抽完'); } // 执行抽奖算法 $result = $this->calculate_prize($prizes); if ($result['prize_id']) { // 用户中奖 $this->record_winner($activity_id, $user_id, $result['prize_id']); $prize_info = $this->get_prize_info($result['prize_id']); return array( 'success' => true, 'is_winner' => true, 'prize' => $prize_info, 'message' => '恭喜您中奖!' ); } else { // 用户未中奖 $this->record_participant($activity_id, $user_id); return array( 'success' => true, 'is_winner' => false, 'message' => '很遗憾,您未中奖' ); } } /** * 概率算法计算中奖奖品 * @param array $prizes 奖品列表 * @return array 抽奖结果 */ private function calculate_prize($prizes) { $total_probability = 0; $prize_list = array(); // 构建奖品概率数组 foreach ($prizes as $prize) { if ($prize->remaining > 0) { $total_probability += $prize->probability; $prize_list[] = array( 'id' => $prize->id, 'probability' => $prize->probability, 'cumulative' => $total_probability ); } } // 生成随机数 $random = mt_rand() / mt_getrandmax(); $selected_prize = 0; // 根据概率选择奖品 foreach ($prize_list as $prize) { if ($random <= $prize['cumulative']) { $selected_prize = $prize['id']; break; } } return array('prize_id' => $selected_prize); } /** * 记录中奖者 */ private function record_winner($activity_id, $user_id, $prize_id) { global $wpdb; $table = $wpdb->prefix . 'ld_participants'; // 记录参与信息 $wpdb->insert($table, array( 'activity_id' => $activity_id, 'user_id' => $user_id, 'is_winner' => 1, 'prize_id' => $prize_id, 'joined_at' => current_time('mysql') )); // 减少奖品剩余数量 $prize_table = $wpdb->prefix . 'ld_prizes'; $wpdb->query($wpdb->prepare( "UPDATE $prize_table SET remaining = remaining - 1 WHERE id = %d AND remaining > 0", $prize_id )); // 发送中奖通知 $this->send_winner_notification($user_id, $prize_id); } } 3.3 前端抽奖界面 创建用户参与抽奖的前端界面: <!-- templates/lottery-frontend.php --> <div class="lottery-container" data-activity-id="<?php echo $activity_id; ?>"> <div class="lottery-header"> <h2><?php echo esc_html($activity_title); ?></h2> <p class="lottery-description"><?php echo esc_html($activity_description); ?></p> <div class="lottery-timer" id="lottery-timer"> <span>活动倒计时: </span> <span class="countdown" data-end="<?php echo $end_time; ?>"></span> </div> </div> <div class="lottery-prizes"> <h3>活动奖品</h3> <div class="prizes-grid"> <?php foreach ($prizes as $prize): ?> <div class="prize-item" data-level="<?php echo $prize->level; ?>"> <div class="prize-icon">🏆</div> <h4><?php echo esc_html($prize->name); ?></h4> <p><?php echo esc_html($prize->description); ?></p> <div class="prize-quantity"> 剩余: <span class="remaining"><?php echo $prize->remaining; ?></span>/<?php echo $prize->quantity; ?> </div> </div> <?php endforeach; ?> </div> </div> <div class="lottery-action"> <?php if (is_user_logged_in()): ?> <button class="btn-draw" id="btn-draw" <?php echo $can_participate ? '' : 'disabled'; ?>> <?php echo $can_participate ? '立即抽奖' : '已参与'; ?> </button> <div class="lottery-result" id="lottery-result"></div> <?php else: ?> <div class="login-required"> <p>请先登录参与抽奖</p> <a href="<?php echo wp_login_url(get_permalink()); ?>" class="btn-login">登录</a> </div> <?php endif; ?> </div> <div class="lottery-winners"> <h3>中奖名单</h3> <div class="winners-list" id="winners-list"> <!-- 通过AJAX动态加载中奖名单 --> </div> </div> </div> <script> jQuery(document).ready(function($) { // 抽奖按钮点击事件 draw').on('click', function() { const $btn = $(this); const activityId = $('.lottery-container').data('activity-id'); if ($btn.hasClass('processing')) return; $btn.addClass('processing').text('抽奖中...'); // 发送抽奖请求 $.ajax({ url: ld_ajax.ajax_url, type: 'POST', data: { action: 'ld_perform_draw', activity_id: activityId, nonce: ld_ajax.nonce }, success: function(response) { if (response.success) { if (response.data.is_winner) { // 显示中奖动画 showWinnerAnimation(response.data.prize); // 更新中奖名单 loadWinnersList(); } else { $('#lottery-result').html( '<div class="result-message not-winner">' + '<p>很遗憾,您未中奖</p>' + '<p>感谢参与!</p>' + '</div>' ); } $btn.prop('disabled', true).text('已参与'); } else { alert(response.data.message); $btn.removeClass('processing').text('立即抽奖'); } }, error: function() { alert('抽奖失败,请稍后重试'); $btn.removeClass('processing').text('立即抽奖'); } }); }); // 加载中奖名单 function loadWinnersList() { $.ajax({ url: ld_ajax.ajax_url, type: 'GET', data: { action: 'ld_get_winners', activity_id: activityId }, success: function(response) { if (response.success) { $('#winners-list').html(response.data.html); } } }); } // 中奖动画效果 function showWinnerAnimation(prize) { const $result = $('#lottery-result'); $result.html(` <div class="winner-animation"> <div class="confetti"></div> <div class="prize-reveal"> <h3>🎉 恭喜您中奖了! 🎉</h3> <div class="prize-details"> <h4>${prize.name}</h4> <p>${prize.description}</p> </div> <p class="winner-instructions">请查看您的注册邮箱获取领奖方式</p> </div> </div> `); // 触发WebSocket广播中奖消息 if (window.ldWebSocket && window.ldWebSocket.readyState === WebSocket.OPEN) { const message = { type: 'winner_announcement', data: { prize: prize.name, timestamp: new Date().toISOString() } }; window.ldWebSocket.send(JSON.stringify(message)); } } });</script> --- ## 第四章:实时弹幕系统实现 ### 4.1 WebSocket服务器搭建 使用PHP Ratchet实现WebSocket服务器: // includes/class-websocket-server.phpuse RatchetMessageComponentInterface;use RatchetConnectionInterface;use RatchetServerIoServer;use RatchetHttpHttpServer;use RatchetWebSocketWsServer; class DanmakuWebSocket implements MessageComponentInterface { protected $clients; protected $activityConnections; public function __construct() { $this->clients = new SplObjectStorage; $this->activityConnections = []; } public function onOpen(ConnectionInterface $conn) { $this->clients->attach($conn); echo "新连接: {$conn->resourceId}n"; } public function onMessage(ConnectionInterface $from, $msg) { $data = json_decode($msg, true); if (!$data || !isset($data['type'])) { return; } switch ($data['type']) { case 'subscribe': // 订阅特定活动 $activityId = $data['activity_id']; if (!isset($this->activityConnections[$activityId])) { $this->activityConnections[$activityId] = []; } $this->activityConnections[$activityId][$from->resourceId] = $from; $from->activityId = $activityId; break; case 'danmaku': // 处理弹幕消息 $this->handleDanmaku($from, $data); break; case 'heartbeat': // 心跳检测 $from->send(json_encode(['type' => 'pong'])); break; } } private function handleDanmaku($from, $data) { global $wpdb; $activityId = $data['activity_id']; $content = sanitize_text_field($data['content']); $userId = isset($data['user_id']) ? intval($data['user_id']) : 0; // 保存到数据库 $table = $wpdb->prefix . 'ld_danmaku'; $wpdb->insert($table, [ 'activity_id' => $activityId, 'user_id' => $userId, 'content' => $content, 'color' => $data['color'] ?? '#FFFFFF', 'size' => $data['size'] ?? 24, 'position' => $data['position'] ?? 'fly', 'status' => 'pending', // 需要审核 'sent_at' => current_time('mysql') ]); $danmakuId = $wpdb->insert_id; // 如果是管理员或自动审核通过的消息,立即广播 if ($this->shouldAutoApprove($userId)) { $this->broadcastDanmaku($activityId, [ 'id' => $danmakuId, 'content' => $content, 'color' => $data['color'] ?? '#FFFFFF', 'size' => $data['size'] ?? 24, 'position' => $data['position'] ?? 'fly', 'timestamp' => time(), 'user' => $this->getUserInfo($userId) ]); // 更新状态为已批准 $wpdb->update($table, ['status' => 'approved'], ['id' => $danmakuId] ); } } private function broadcastDanmaku($activityId, $danmaku) { if (!isset($this->activityConnections[$activityId])) { return; } $message = json_encode([ 'type' => 'danmaku', 'data' => $danmaku ]); foreach ($this->activityConnections[$activityId] as $client) { $client->send($message); } } public function onClose(ConnectionInterface $conn) { $this->clients->detach($conn); // 从活动连接中移除 if (isset($conn->activityId)) { unset($this->activityConnections[$conn->activityId][$conn->resourceId]); } echo "连接关闭: {$conn->resourceId}n"; } public function onError(ConnectionInterface $conn, Exception $e) { echo "错误: {$e->getMessage()}n"; $conn->close(); } } // WebSocket服务器启动脚本class WebSocketServer { public function start() { $port = get_option('ld_websocket_port', 8080); $server = IoServer::factory( new HttpServer( new WsServer( new DanmakuWebSocket() ) ), $port ); echo "WebSocket服务器运行在端口 {$port}n"; $server->run(); } } ### 4.2 前端弹幕展示系统 创建弹幕展示前端: <!-- templates/danmaku-display.php --><div class="danmaku-container" data-activity-id="<?php echo $activity_id; ?>"> <div class="danmaku-stage" id="danmaku-stage"> <!-- 弹幕将在这里显示 --> </div> <div class="danmaku-controls"> <div class="danmaku-input-area"> <input type="text" id="danmaku-input" placeholder="输入弹幕内容..." maxlength="100"> <div class="danmaku-style-controls"> <div class="color-picker"> <label>颜色:</label> <input type="color" id="danmaku-color" value="#FFFFFF"> </div> <div class="size-selector"> <label>大小:</label> <select id="danmaku-size"> <option value="20">小</option> <option value="24" selected>中</option> <option value="28">大</option> </select> </div> <div class="position-selector"> <label>位置:</label> <select id="danmaku-position"> <option value="fly" selected>滚动</option> <option value="top">顶部</option> <option value="bottom">底部</option> </select> </div> </div> <button id="send-danmaku" class="btn-send"> <span class="dashicons dashicons-arrow-right-alt"></span> 发送 </button> </div> <div class="danmaku-settings"> <label> <input type="checkbox" id="danmaku-auto-scroll" checked> 自动滚动 </label> <label> 速度: <input type="range" id="danmaku-speed" min="1" max="10" value="5"> </label> <label> 透明度: <input type="range" id="danmaku-opacity" min="0.1" max="1" step="0.1" value="0.8"> </label> <button id="clear-danmaku" class="btn-clear">清屏</button> </div> </div> <div class="danmaku-stats"> <span>在线人数: <span id="online-count">0</span></span> <span>弹幕数量: <span id="danmaku-count">0</span></span> </div> </div> <script>class DanmakuDisplay { constructor(container, activityId) { this.container = container; this.activityId = activityId; this.stage = container.querySelector('#danmaku-stage'); this.danmakuPool = []; this.activeDanmaku = []; this.onlineCount = 0; this.danmakuCount = 0; this.settings = { speed: 5, opacity: 0.8, autoScroll: true, maxDanmaku: 100 }; this.initWebSocket(); this.initControls(); this.loadHistory(); this.startRenderLoop(); } initWebSocket() { const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; const host = window.location.hostname; const port = ld_settings.websocket_port || 8080; const wsUrl = `${protocol}//${host}:${port}`; this.ws = new WebSocket(wsUrl); this.ws.onopen = () => { console.log('WebSocket连接已建立'); // 订阅活动 this.ws.send(JSON.stringify({ type: 'subscribe', activity_id: this.activityId })); // 开始心跳 this.startHeartbeat(); }; this.ws.onmessage = (event) => { const data = JSON.parse(event.data); this.handleMessage(data); }; this.ws.onclose = () => { console.log('WebSocket连接关闭'); setTimeout(() => this.initWebSocket(), 3000); }; } handleMessage(data) { switch (data.type) { case 'danmaku': this.addDanmaku(data.data); break; case 'online_count': this.updateOnlineCount(data.count); break; case 'winner_announcement': this.showWinnerAnnouncement(data.data); break; } } addDanmaku(danmaku) { // 创建弹幕元素 const element = document.createElement('div'); element.className = `danmaku-item ${danmaku.position}`; element.textContent = danmaku.content; element.style.color = danmaku.color; element.style.fontSize = `${danmaku.size}px`; element.style.opacity = this.settings.opacity; element.dataset.id = danmaku.id; // 添加到舞台 this.stage.appendChild(element); // 添加到活动弹幕列表 this.activeDanmaku.push({ element: element, data: danmaku, position: 0 }); // 更新计数 this.danmakuCount++; this.updateStats(); // 限制弹幕数量 if (this.activeDanmaku.length > this.settings.maxDanmaku) { const oldest = this.activeDanmaku.shift(); oldest.element.remove(); } } startRenderLoop() { const render = () => { const stageWidth = this.stage.offsetWidth; const stageHeight = this.stage.offsetHeight; this.activeDanmaku.forEach((danmaku, index) => { const element = danmaku.element; const speed = this.settings.speed; if (danmaku.data.position === 'fly') { // 滚动弹幕 if (danmaku.position === 0) { // 初始位置在右侧 const elementWidth = element.offsetWidth; element.style.left = `${stageWidth}px`; element.style.top = `${Math.random() * (stageHeight - 30)}px`; danmaku.position = stageWidth; } // 向左移动 danmaku.position -= speed; element.style.transform = `translateX(${danmaku.position}px)`; // 移出屏幕后移除 if (danmaku.position < -element.offsetWidth) { element.remove(); this.activeDanmaku.splice(index, 1); } } else { // 固定位置弹幕 if (!element.style.left) { element.style.left = '50%'; element.style.transform = 'translateX(-50%)'; element.style.top = danmaku.data.position === 'top' ? '10px' : 'auto'; element.style.bottom = danmaku.data.position === 'bottom' ? '10px' : 'auto'; // 3秒后移除 setTimeout(() => { element.remove(); this.activeDanmaku.splice(index, 1); }, 3000); } } }); requestAnimationFrame(render); }; render(); } sendDanmaku(content, style = {}) { if (!content.trim() || !this.ws || this.ws.readyState !== WebSocket.OPEN) { return; } const message = { type: 'danmaku', activity_id: this.activityId, content: content, color: style.color || '#FFFFFF', size: style.size || 24, position: style.position || 'fly', user_id: ld_settings.current_user_id || 0 }; this.ws.send(JSON.stringify(message)); } initControls() { const sendBtn = this.container.querySelector('#send-danmaku'); const input = this.container.querySelector('#danmaku-input'); sendBtn.addEventListener('click', () => { const content = input.value.trim(); if (content) { const style = { color: this.container.querySelector('#danmaku-color').value, size: this.container.querySelector('#danmaku-size').value, position: this.container.querySelector('#danmaku-position').value }; this.sendDanmaku(content, style); input.value = ''; } }); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') { sendBtn.click(); } }); // 设置控件 this.container.querySelector('#danmaku-speed').addEventListener('input', (e) => { this.settings.speed = parseInt(e.target.value); }); this.container.querySelector('#danmaku-opacity').addEventListener('input', (e) => { this.settings.opacity = parseFloat(e.target.value); this.activeDanmaku.forEach(d => { d.element.style.opacity = this.settings.opacity; }); }); this.container.querySelector('#clear-danmaku').addEventListener('click', () => { this.activeDanmaku.forEach(d => d.element.remove()); this.activeDanmaku = []; }); } loadHistory() { // 加载历史弹幕 fetch(`${ld_ajax.ajax_url}?action=ld_get_danmaku_history&activity_id=${this.activityId}`) .then(response => response.json()) .then(data => { if (data.success) { data.data.forEach(danmaku => { this.addDanmaku(danmaku); }); } }); } startHeartbeat() { setInterval(() => { if (this.ws.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify({ type: 'heartbeat' })); } }, 30000); } updateStats() { this.container.querySelector('#danmaku-count').textContent = this.danmakuCount; } updateOnlineCount(count) { this.onlineCount = count; this.container.querySelector('#online-count').textContent = count; } showWinnerAnnouncement(data) { // 显示中奖公告弹幕 this.addDanmaku({ id: 'winner_' + Date.now(),

发表评论

一步步实现,为WordPress打造内嵌的在线代码片段管理与共享平台

一步步实现:为WordPress打造内嵌的在线代码片段管理与共享平台 引言:为什么WordPress需要代码片段管理平台? 在当今数字化时代,网站功能日益复杂,WordPress作为全球最流行的内容管理系统,承载着超过40%的网站。对于开发者、技术博主和在线教育者而言,经常需要在文章中嵌入代码片段,并与读者或团队成员共享。然而,WordPress默认的代码展示功能有限,缺乏统一的代码管理、版本控制和协作功能。 传统的代码展示方法存在诸多问题:使用简单的<pre>标签缺乏语法高亮;依赖第三方服务如GitHub Gist可能导致加载缓慢和隐私问题;手动管理代码片段效率低下且难以复用。因此,为WordPress打造一个内嵌的在线代码片段管理与共享平台,不仅能提升内容质量,还能增强用户互动和知识共享。 本文将详细介绍如何通过WordPress程序的二次开发,实现一个功能完整的代码片段管理与共享平台,同时集成常用互联网小工具功能,打造一站式的开发者工具集。 第一部分:项目规划与架构设计 1.1 需求分析与功能规划 在开始开发之前,我们需要明确平台的核心功能: 代码片段管理:创建、编辑、删除和分类代码片段 语法高亮支持:支持多种编程语言的语法高亮 版本控制:记录代码修改历史,支持版本回滚 共享与协作:公开/私有代码片段设置,协作编辑功能 嵌入展示:短代码或区块支持,方便在文章中嵌入代码 用户权限管理:不同用户角色的访问和编辑权限 常用工具集成:JSON格式化、代码压缩、加密解密等小工具 搜索与过滤:按语言、标签、日期等条件搜索代码片段 导入导出:支持从GitHub Gist等平台导入,导出为多种格式 API接口:提供REST API供外部应用调用 1.2 技术栈选择与架构设计 基于WordPress的生态系统,我们选择以下技术方案: 核心框架:WordPress插件架构 前端技术:React.js(用于管理界面) + WordPress区块编辑器 语法高亮:Prism.js或Highlight.js 数据库设计:自定义数据库表 + WordPress元数据 API设计:WordPress REST API扩展 缓存机制:Transients API + 对象缓存 安全措施:Nonce验证、能力检查、输入过滤 系统架构分为四个主要层次: 表示层:用户界面和区块编辑器集成 应用层:业务逻辑和API端点 数据层:数据库操作和缓存管理 集成层:第三方服务集成和工具功能 第二部分:数据库设计与插件初始化 2.1 创建自定义数据库表 我们需要创建专门的数据库表来存储代码片段及其元数据: // 在插件激活时创建数据库表 function code_snippets_platform_activate() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'code_snippets'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(200) NOT NULL, description text, code longtext NOT NULL, language varchar(50) NOT NULL, author_id bigint(20) NOT NULL, status varchar(20) DEFAULT 'publish', visibility varchar(20) DEFAULT 'public', created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, view_count int(11) DEFAULT 0, fork_count int(11) DEFAULT 0, parent_id mediumint(9) DEFAULT 0, version int(11) DEFAULT 1, PRIMARY KEY (id), KEY author_id (author_id), KEY language (language), KEY status (status) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建代码片段元数据表 $meta_table_name = $wpdb->prefix . 'code_snippet_meta'; $sql_meta = "CREATE TABLE IF NOT EXISTS $meta_table_name ( meta_id bigint(20) NOT NULL AUTO_INCREMENT, snippet_id bigint(20) NOT NULL, meta_key varchar(255), meta_value longtext, PRIMARY KEY (meta_id), KEY snippet_id (snippet_id), KEY meta_key (meta_key) ) $charset_collate;"; dbDelta($sql_meta); // 创建标签关系表 $tags_table_name = $wpdb->prefix . 'code_snippet_tags'; $sql_tags = "CREATE TABLE IF NOT EXISTS $tags_table_name ( id bigint(20) NOT NULL AUTO_INCREMENT, snippet_id bigint(20) NOT NULL, tag varchar(100) NOT NULL, PRIMARY KEY (id), KEY snippet_id (snippet_id), KEY tag (tag) ) $charset_collate;"; dbDelta($sql_tags); } register_activation_hook(__FILE__, 'code_snippets_platform_activate'); 2.2 插件基础结构 创建插件主文件,初始化插件的基本结构: <?php /** * Plugin Name: WordPress代码片段管理与共享平台 * Plugin URI: https://yourwebsite.com/code-snippets-platform * Description: 为WordPress打造的内嵌在线代码片段管理与共享平台,集成常用开发工具 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later * Text Domain: code-snippets-platform */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('CSP_VERSION', '1.0.0'); define('CSP_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('CSP_PLUGIN_URL', plugin_dir_url(__FILE__)); define('CSP_PLUGIN_BASENAME', plugin_basename(__FILE__)); // 自动加载类文件 spl_autoload_register(function ($class) { $prefix = 'CSP_'; $base_dir = CSP_PLUGIN_DIR . 'includes/'; $len = strlen($prefix); if (strncmp($prefix, $class, $len) !== 0) { return; } $relative_class = substr($class, $len); $file = $base_dir . str_replace('_', '/', $relative_class) . '.php'; if (file_exists($file)) { require $file; } }); // 初始化插件 class Code_Snippets_Platform { private static $instance = null; public static function get_instance() { if (null === self::$instance) { self::$instance = new self(); } return self::$instance; } private function __construct() { $this->init_hooks(); } private function init_hooks() { // 激活/停用钩子 register_activation_hook(__FILE__, array($this, 'activate')); register_deactivation_hook(__FILE__, array($this, 'deactivate')); // 初始化 add_action('plugins_loaded', array($this, 'init')); // 加载文本域 add_action('init', array($this, 'load_textdomain')); } public function activate() { require_once CSP_PLUGIN_DIR . 'includes/class-activator.php'; CSP_Activator::activate(); } public function deactivate() { require_once CSP_PLUGIN_DIR . 'includes/class-deactivator.php'; CSP_Deactivator::deactivate(); } public function load_textdomain() { load_plugin_textdomain( 'code-snippets-platform', false, dirname(CSP_PLUGIN_BASENAME) . '/languages' ); } public function init() { // 加载管理器 $this->load_managers(); // 注册短代码 $this->register_shortcodes(); // 注册区块 if (function_exists('register_block_type')) { add_action('init', array($this, 'register_blocks')); } // 注册REST API add_action('rest_api_init', array($this, 'register_rest_routes')); } private function load_managers() { require_once CSP_PLUGIN_DIR . 'includes/managers/class-snippet-manager.php'; require_once CSP_PLUGIN_DIR . 'includes/managers/class-tool-manager.php'; require_once CSP_PLUGIN_DIR . 'includes/managers/class-user-manager.php'; new CSP_Snippet_Manager(); new CSP_Tool_Manager(); new CSP_User_Manager(); } public function register_shortcodes() { require_once CSP_PLUGIN_DIR . 'includes/shortcodes/class-snippet-shortcode.php'; new CSP_Snippet_Shortcode(); } public function register_blocks() { require_once CSP_PLUGIN_DIR . 'includes/blocks/class-snippet-block.php'; new CSP_Snippet_Block(); } public function register_rest_routes() { require_once CSP_PLUGIN_DIR . 'includes/api/class-snippet-api.php'; new CSP_Snippet_API(); } } // 启动插件 Code_Snippets_Platform::get_instance(); 第三部分:代码片段管理核心功能实现 3.1 代码片段管理器类 创建代码片段管理器,处理所有业务逻辑: // includes/managers/class-snippet-manager.php class CSP_Snippet_Manager { private $db; private $table_name; public function __construct() { global $wpdb; $this->db = $wpdb; $this->table_name = $wpdb->prefix . 'code_snippets'; $this->init_hooks(); } private function init_hooks() { // 管理页面 add_action('admin_menu', array($this, 'add_admin_menu')); // 前端资源 add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); // AJAX处理 add_action('wp_ajax_csp_save_snippet', array($this, 'ajax_save_snippet')); add_action('wp_ajax_nopriv_csp_get_snippet', array($this, 'ajax_get_snippet')); // 短代码支持 add_shortcode('code_snippet', array($this, 'render_shortcode')); } public function add_admin_menu() { add_menu_page( __('代码片段平台', 'code-snippets-platform'), __('代码片段', 'code-snippets-platform'), 'edit_posts', 'code-snippets', array($this, 'render_admin_page'), 'dashicons-editor-code', 30 ); // 添加子菜单 add_submenu_page( 'code-snippets', __('所有代码片段', 'code-snippets-platform'), __('所有片段', 'code-snippets-platform'), 'edit_posts', 'code-snippets', array($this, 'render_admin_page') ); add_submenu_page( 'code-snippets', __('添加新代码片段', 'code-snippets-platform'), __('添加新片段', 'code-snippets-platform'), 'edit_posts', 'code-snippets-new', array($this, 'render_editor_page') ); add_submenu_page( 'code-snippets', __('开发工具', 'code-snippets-platform'), __('开发工具', 'code-snippets-platform'), 'edit_posts', 'code-snippets-tools', array($this, 'render_tools_page') ); } public function render_admin_page() { // 加载React应用容器 echo '<div id="csp-admin-app"></div>'; } public function render_editor_page() { // 代码编辑器页面 echo '<div id="csp-editor-app"></div>'; } public function render_tools_page() { // 工具页面 echo '<div id="csp-tools-app"></div>'; } public function enqueue_admin_assets($hook) { if (strpos($hook, 'code-snippets') === false) { return; } // 加载React应用 wp_enqueue_script( 'csp-admin-app', CSP_PLUGIN_URL . 'assets/js/admin-app.js', array('wp-element', 'wp-api-fetch', 'wp-components'), CSP_VERSION, true ); wp_enqueue_style( 'csp-admin-style', CSP_PLUGIN_URL . 'assets/css/admin-style.css', array('wp-components'), CSP_VERSION ); // 本地化脚本 wp_localize_script('csp-admin-app', 'csp_admin_data', array( 'api_url' => rest_url('csp/v1'), 'nonce' => wp_create_nonce('wp_rest'), 'current_user' => get_current_user_id(), 'strings' => array( 'save' => __('保存', 'code-snippets-platform'), 'delete' => __('删除', 'code-snippets-platform'), 'edit' => __('编辑', 'code-snippets-platform'), // 更多本地化字符串 ) )); } public function enqueue_frontend_assets() { // 语法高亮库 wp_enqueue_script( 'prism-js', 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/prism.min.js', array(), '1.25.0', true ); wp_enqueue_style( 'prism-css', 'https://cdnjs.cloudflare.com/ajax/libs/prism/1.25.0/themes/prism-tomorrow.min.css', array(), '1.25.0' ); // 添加更多语言支持 wp_enqueue_script( 'prism-language-extras', CSP_PLUGIN_URL . 'assets/js/prism-languages.js', array('prism-js'), CSP_VERSION, true ); // 前端样式 wp_enqueue_style( 'csp-frontend-style', CSP_PLUGIN_URL . 'assets/css/frontend-style.css', array(), CSP_VERSION ); } // 保存代码片段 public function save_snippet($data) { $user_id = get_current_user_id(); // 验证数据 $title = sanitize_text_field($data['title']); $description = sanitize_textarea_field($data['description']); $code = wp_kses_post($data['code']); // 允许HTML但过滤危险内容 $language = sanitize_text_field($data['language']); $tags = isset($data['tags']) ? array_map('sanitize_text_field', $data['tags']) : array(); $visibility = in_array($data['visibility'], array('public', 'private', 'unlisted')) ? $data['visibility'] : 'public'; // 准备插入数据 $snippet_data = array( 'title' => $title, 'description' => $description, 'code' => $code, 'language' => $language, 'author_id' => $user_id, 'visibility' => $visibility, 'updated_at' => current_time('mysql') ); // 如果是更新 if (!empty($data['id'])) { $snippet_id = intval($data['id']); // 检查权限 if (!$this->can_edit_snippet($snippet_id, $user_id)) { return new WP_Error('forbidden', __('您没有权限编辑此代码片段', 'code-snippets-platform'), array('status' => 403)); } // 创建新版本 $old_snippet = $this->get_snippet($snippet_id); $this->create_version($old_snippet); // 更新主记录 $this->db->update( $this->table_name, $snippet_data, array('id' => $snippet_id) ); // 更新标签 $this->update_tags($snippet_id, $tags); return $snippet_id; } else { // 新代码片段 $snippet_data['created_at'] = current_time('mysql'); $this->db->insert( $this->table_name, $snippet_data ); $snippet_id = $this->db->insert_id; // 保存标签 $this->update_tags($snippet_id, $tags); return $snippet_id; } } // 获取代码片段 public function get_snippet($id, $check_permissions = true) { $id = intval($id); $user_id = get_current_user_id(); $snippet = $this->db->get_row( $this->db->prepare("SELECT * FROM {$this->table_name} WHERE id = %d", $id) ); if (!$snippet) { return null; } // 检查权限 if ($check_permissions && !$this->can_view_snippet($snippet, $user_id)) { return new WP_Error('forbidden', __('您没有权限查看此代码片段', 'code-snippets-platform'), array('status' => 403)); } // 获取标签 $snippet->tags = $this->get_snippet_tags($id); 3.2 代码片段展示与短代码实现 // includes/shortcodes/class-snippet-shortcode.php class CSP_Snippet_Shortcode { public function __construct() { add_shortcode('code_snippet', array($this, 'render_shortcode')); add_shortcode('code_tool', array($this, 'render_tool_shortcode')); } public function render_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'title' => '', 'language' => '', 'line_numbers' => true, 'highlight' => '', 'download' => false, 'theme' => 'default' ), $atts, 'code_snippet'); $snippet_id = intval($atts['id']); if ($snippet_id <= 0) { return '<div class="csp-error">' . __('无效的代码片段ID', 'code-snippets-platform') . '</div>'; } // 获取代码片段 $snippet_manager = new CSP_Snippet_Manager(); $snippet = $snippet_manager->get_snippet($snippet_id); if (is_wp_error($snippet) || !$snippet) { return '<div class="csp-error">' . __('代码片段不存在或无权访问', 'code-snippets-platform') . '</div>'; } // 构建输出 $output = '<div class="csp-snippet-container" data-snippet-id="' . esc_attr($snippet_id) . '">'; // 标题栏 $output .= '<div class="csp-snippet-header">'; $output .= '<div class="csp-snippet-meta">'; $output .= '<span class="csp-language-badge">' . esc_html($snippet->language) . '</span>'; $output .= '<span class="csp-author">' . sprintf(__('作者: %s', 'code-snippets-platform'), get_the_author_meta('display_name', $snippet->author_id)) . '</span>'; $output .= '</div>'; // 操作按钮 $output .= '<div class="csp-snippet-actions">'; if ($atts['download']) { $output .= '<button class="csp-btn csp-btn-download" data-snippet-id="' . esc_attr($snippet_id) . '">' . __('下载', 'code-snippets-platform') . '</button>'; } $output .= '<button class="csp-btn csp-btn-copy" data-clipboard-target="#snippet-' . esc_attr($snippet_id) . '">' . __('复制', 'code-snippets-platform') . '</button>'; $output .= '</div>'; $output .= '</div>'; // 代码区域 $output .= '<div class="csp-snippet-code">'; $output .= '<pre class="language-' . esc_attr($snippet->language) . ($atts['line_numbers'] ? ' line-numbers' : '') . '">'; $output .= '<code id="snippet-' . esc_attr($snippet_id) . '">' . esc_html($snippet->code) . '</code>'; $output .= '</pre>'; $output .= '</div>'; // 描述区域 if (!empty($snippet->description)) { $output .= '<div class="csp-snippet-description">'; $output .= wp_kses_post($snippet->description); $output .= '</div>'; } $output .= '</div>'; // 增加查看计数 $this->increment_view_count($snippet_id); return $output; } private function increment_view_count($snippet_id) { global $wpdb; $table_name = $wpdb->prefix . 'code_snippets'; $wpdb->query( $wpdb->prepare( "UPDATE {$table_name} SET view_count = view_count + 1 WHERE id = %d", $snippet_id ) ); } public function render_tool_shortcode($atts) { $atts = shortcode_atts(array( 'tool' => 'json_formatter', 'title' => '', 'height' => '400px', 'width' => '100%' ), $atts, 'code_tool'); $tool = sanitize_text_field($atts['tool']); $title = sanitize_text_field($atts['title']); $available_tools = array( 'json_formatter' => __('JSON格式化', 'code-snippets-platform'), 'code_minifier' => __('代码压缩', 'code-snippets-platform'), 'encryption' => __('加密解密', 'code-snippets-platform'), 'base64' => __('Base64编码', 'code-snippets-platform'), 'regex_tester' => __('正则表达式测试', 'code-snippets-platform') ); if (!array_key_exists($tool, $available_tools)) { return '<div class="csp-error">' . __('无效的工具类型', 'code-snippets-platform') . '</div>'; } $output = '<div class="csp-tool-container" data-tool="' . esc_attr($tool) . '">'; if (!empty($title)) { $output .= '<h3 class="csp-tool-title">' . esc_html($title) . '</h3>'; } else { $output .= '<h3 class="csp-tool-title">' . esc_html($available_tools[$tool]) . '</h3>'; } $output .= '<div class="csp-tool-content" style="height: ' . esc_attr($atts['height']) . '; width: ' . esc_attr($atts['width']) . ';">'; // 根据工具类型加载不同的界面 switch ($tool) { case 'json_formatter': $output .= $this->render_json_formatter(); break; case 'code_minifier': $output .= $this->render_code_minifier(); break; case 'encryption': $output .= $this->render_encryption_tool(); break; // 其他工具... } $output .= '</div>'; $output .= '</div>'; return $output; } private function render_json_formatter() { $output = '<div class="csp-json-formatter">'; $output .= '<div class="csp-tool-row">'; $output .= '<div class="csp-input-section">'; $output .= '<label for="json-input">' . __('输入JSON:', 'code-snippets-platform') . '</label>'; $output .= '<textarea id="json-input" class="csp-textarea" placeholder="{"key": "value"}"></textarea>'; $output .= '</div>'; $output .= '<div class="csp-output-section">'; $output .= '<label for="json-output">' . __('格式化结果:', 'code-snippets-platform') . '</label>'; $output .= '<pre id="json-output" class="csp-code-output"></pre>'; $output .= '</div>'; $output .= '</div>'; $output .= '<div class="csp-tool-actions">'; $output .= '<button class="csp-btn csp-btn-format">' . __('格式化', 'code-snippets-platform') . '</button>'; $output .= '<button class="csp-btn csp-btn-minify">' . __('压缩', 'code-snippets-platform') . '</button>'; $output .= '<button class="csp-btn csp-btn-validate">' . __('验证', 'code-snippets-platform') . '</button>'; $output .= '<button class="csp-btn csp-btn-clear">' . __('清空', 'code-snippets-platform') . '</button>'; $output .= '</div>'; $output .= '</div>'; return $output; } } 第四部分:REST API设计与实现 4.1 代码片段API端点 // includes/api/class-snippet-api.php class CSP_Snippet_API { public function __construct() { $this->register_routes(); } private function register_routes() { register_rest_route('csp/v1', '/snippets', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_snippets'), 'permission_callback' => array($this, 'get_snippets_permissions_check'), 'args' => $this->get_collection_params() ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array($this, 'create_snippet'), 'permission_callback' => array($this, 'create_snippet_permissions_check') ) )); register_rest_route('csp/v1', '/snippets/(?P<id>d+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_snippet'), 'permission_callback' => array($this, 'get_snippet_permissions_check') ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array($this, 'update_snippet'), 'permission_callback' => array($this, 'update_snippet_permissions_check') ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array($this, 'delete_snippet'), 'permission_callback' => array($this, 'delete_snippet_permissions_check') ) )); register_rest_route('csp/v1', '/snippets/(?P<id>d+)/fork', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array($this, 'fork_snippet'), 'permission_callback' => array($this, 'fork_snippet_permissions_check') ) )); register_rest_route('csp/v1', '/tools/(?P<tool>w+)', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array($this, 'process_tool'), 'permission_callback' => array($this, 'process_tool_permissions_check') ) )); } public function get_snippets($request) { global $wpdb; $params = $request->get_params(); $page = isset($params['page']) ? intval($params['page']) : 1; $per_page = isset($params['per_page']) ? intval($params['per_page']) : 20; $offset = ($page - 1) * $per_page; $user_id = get_current_user_id(); $table_name = $wpdb->prefix . 'code_snippets'; // 构建查询条件 $where_conditions = array('1=1'); $where_values = array(); // 权限过滤 if (!current_user_can('manage_options')) { $where_conditions[] = "(visibility = 'public' OR author_id = %d)"; $where_values[] = $user_id; } // 语言过滤 if (!empty($params['language'])) { $where_conditions[] = "language = %s"; $where_values[] = sanitize_text_field($params['language']); } // 搜索 if (!empty($params['search'])) { $search_term = '%' . $wpdb->esc_like(sanitize_text_field($params['search'])) . '%'; $where_conditions[] = "(title LIKE %s OR description LIKE %s)"; $where_values[] = $search_term; $where_values[] = $search_term; } // 作者过滤 if (!empty($params['author'])) { $where_conditions[] = "author_id = %d"; $where_values[] = intval($params['author']); } $where_clause = implode(' AND ', $where_conditions); // 获取总数 $count_query = "SELECT COUNT(*) FROM {$table_name} WHERE {$where_clause}"; if (!empty($where_values)) { $count_query = $wpdb->prepare($count_query, $where_values); } $total_items = $wpdb->get_var($count_query); // 获取数据 $query = "SELECT * FROM {$table_name} WHERE {$where_clause} ORDER BY created_at DESC LIMIT %d OFFSET %d"; $where_values[] = $per_page; $where_values[] = $offset; $query = $wpdb->prepare($query, $where_values); $snippets = $wpdb->get_results($query); // 获取标签 foreach ($snippets as $snippet) { $snippet->tags = $this->get_snippet_tags($snippet->id); } $response = new WP_REST_Response($snippets); $response->header('X-WP-Total', (int) $total_items); $response->header('X-WP-TotalPages', ceil($total_items / $per_page)); return $response; } public function get_snippet($request) { $snippet_id = intval($request['id']); $snippet_manager = new CSP_Snippet_Manager(); $snippet = $snippet_manager->get_snippet($snippet_id); if (is_wp_error($snippet)) { return $snippet; } if (!$snippet) { return new WP_Error( 'rest_snippet_not_found', __('代码片段不存在', 'code-snippets-platform'), array('status' => 404) ); } return rest_ensure_response($snippet); } public function create_snippet($request) { $data = $request->get_json_params(); if (empty($data['title']) || empty($data['code']) || empty($data['language'])) { return new WP_Error( 'rest_invalid_data', __('标题、代码和语言是必填项', 'code-snippets-platform'), array('status' => 400) ); } $snippet_manager = new CSP_Snippet_Manager(); $snippet_id = $snippet_manager->save_snippet($data); if (is_wp_error($snippet_id)) { return $snippet_id; } $response = $this->get_snippet(new WP_REST_Request('GET', array('id' => $snippet_id))); if (is_wp_error($response)) { return $response; } $response->set_status(201); $response->header('Location', rest_url(sprintf('csp/v1/snippets/%d', $snippet_id))); return $response; } public function process_tool($request) { $tool = $request['tool']; $data = $request->get_json_params(); switch ($tool) { case 'json_formatter': return $this->process_json_formatter($data); case 'code_minifier': return $this->process_code_minifier($data); case 'encryption': return $this->process_encryption($data); default: return new WP_Error( 'rest_tool_not_found', __('工具不存在', 'code-snippets-platform'), array('status' => 404) ); } } private function process_json_formatter($data) { if (!isset($data['input']) || empty($data['input'])) { return new WP_Error( 'rest_invalid_data', __('请输入JSON数据', 'code-snippets-platform'), array('status' => 400) ); } $input = stripslashes($data['input']); $action = isset($data['action']) ? $data['action'] : 'format'; try { $json = json_decode($input); if (json_last_error() !== JSON_ERROR_NONE) { throw new Exception(__('无效的JSON格式: ', 'code-snippets-platform') . json_last_error_msg()); } $result = array(); switch ($action) { case 'format': $result['output'] = json_encode($json, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); $result['valid'] = true; break; case 'minify': $result['output'] = json_encode($json, JSON_UNESCAPED_UNICODE); $result['valid'] = true; break; case 'validate': $result['valid'] = true; $result['message'] = __('JSON格式正确', 'code-snippets-platform'); break; } return rest_ensure_response($result); } catch (Exception $e) { return new WP_Error( 'rest_json_error', $e->getMessage(), array('status' => 400) ); } } // 权限检查方法 public function get_snippets_permissions_check($request) { return true; // 公开可读 } public function create_snippet_permissions_check($request) { return is_user_logged_in(); } public function get_snippet_permissions_check($request) { $snippet_id = intval($request['id']); $snippet_manager = new CSP_Snippet_Manager(); $snippet = $snippet_manager->get_snippet($snippet_id, false); if (!$snippet) { return true; } return $snippet_manager->can_view_snippet($snippet, get_current_user_id()); } } 第五部分:前端React应用开发 5.1 代码片段编辑器组件

发表评论

WordPress开发教程,集成网站用户积分兑换虚拟礼物与打赏系统

WordPress开发教程:集成网站用户积分兑换虚拟礼物与打赏系统 引言:为什么WordPress网站需要用户互动系统 在当今互联网环境中,用户参与度已成为衡量网站成功与否的关键指标。无论是内容型网站、社区论坛还是电商平台,如何增强用户粘性、促进用户互动都是运营者面临的重要课题。用户积分系统和虚拟礼物打赏功能正是解决这一问题的有效工具。 WordPress作为全球最流行的内容管理系统,其强大的扩展性和灵活性使其成为实现这些功能的理想平台。通过代码二次开发,我们可以在WordPress网站上集成完整的用户积分、虚拟礼物兑换和打赏系统,从而提升用户体验,增加网站活跃度,甚至创造新的盈利模式。 本教程将详细讲解如何通过WordPress代码二次开发,实现一个完整的用户积分兑换虚拟礼物与打赏系统,涵盖从数据库设计、功能实现到前端展示的全过程。 系统架构设计与技术选型 1.1 系统功能模块划分 在开始开发之前,我们需要明确系统的功能模块: 用户积分管理模块:包括积分获取、消费、查询和统计功能 虚拟礼物商城模块:礼物的展示、分类、购买和赠送功能 打赏系统模块:支持内容打赏、用户间打赏和打赏记录查询 后台管理模块:积分规则设置、礼物管理、数据统计等功能 前端交互模块:用户界面、AJAX交互和实时通知 1.2 技术栈选择 核心框架:WordPress 5.0+ 数据库:MySQL 5.6+ 前端技术:HTML5, CSS3, JavaScript (jQuery/AJAX) 安全机制:WordPress Nonce验证、数据过滤与转义 缓存优化:Transients API、对象缓存 1.3 数据库表设计 我们需要创建以下自定义数据库表来支持系统功能: -- 用户积分表 CREATE TABLE wp_user_points ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) NOT NULL, points INT DEFAULT 0, total_earned INT DEFAULT 0, total_spent INT DEFAULT 0, last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE ); -- 积分记录表 CREATE TABLE wp_points_log ( id INT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT(20) NOT NULL, points_change INT NOT NULL, action_type VARCHAR(50) NOT NULL, related_id BIGINT(20), description TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ON DELETE CASCADE ); -- 虚拟礼物表 CREATE TABLE wp_virtual_gifts ( id INT AUTO_INCREMENT PRIMARY KEY, gift_name VARCHAR(100) NOT NULL, gift_description TEXT, gift_price INT NOT NULL, gift_image VARCHAR(255), gift_category VARCHAR(50), is_active BOOLEAN DEFAULT TRUE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 礼物交易记录表 CREATE TABLE wp_gift_transactions ( id INT AUTO_INCREMENT PRIMARY KEY, sender_id BIGINT(20) NOT NULL, receiver_id BIGINT(20) NOT NULL, gift_id INT NOT NULL, quantity INT DEFAULT 1, message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (sender_id) REFERENCES wp_users(ID) ON DELETE CASCADE, FOREIGN KEY (receiver_id) REFERENCES wp_users(ID) ON DELETE CASCADE, FOREIGN KEY (gift_id) REFERENCES wp_virtual_gifts(id) ON DELETE CASCADE ); -- 打赏记录表 CREATE TABLE wp_tip_records ( id INT AUTO_INCREMENT PRIMARY KEY, tipper_id BIGINT(20) NOT NULL, receiver_id BIGINT(20) NOT NULL, post_id BIGINT(20), points_amount INT NOT NULL, message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (tipper_id) REFERENCES wp_users(ID) ON DELETE CASCADE, FOREIGN KEY (receiver_id) REFERENCES wp_users(ID) ON DELETE CASCADE, FOREIGN KEY (post_id) REFERENCES wp_posts(ID) ON DELETE SET NULL ); 用户积分系统的实现 2.1 积分系统核心类设计 首先,我们创建一个积分系统的核心类,用于处理所有积分相关操作: <?php /** * WordPress用户积分系统核心类 */ class WP_Points_System { 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('user_register', array($this, 'init_user_points_account')); // 删除用户时清理积分数据 add_action('delete_user', array($this, 'delete_user_points_data')); // 添加积分获取规则 add_action('publish_post', array($this, 'award_points_for_publishing')); add_action('wp_insert_comment', array($this, 'award_points_for_comment')); add_action('daily_points_check', array($this, 'award_daily_login_points')); } /** * 初始化用户积分账户 */ public function init_user_points_account($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'user_points'; // 检查是否已存在记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE user_id = %d", $user_id )); if (!$existing) { // 新用户赠送初始积分 $initial_points = get_option('points_initial', 100); $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'points' => $initial_points, 'total_earned' => $initial_points ), array('%d', '%d', '%d') ); // 记录积分日志 $this->add_points_log( $user_id, $initial_points, 'registration', null, '新用户注册赠送积分' ); } } /** * 获取用户当前积分 */ public function get_user_points($user_id) { global $wpdb; $table_name = $wpdb->prefix . 'user_points'; $points = $wpdb->get_var($wpdb->prepare( "SELECT points FROM $table_name WHERE user_id = %d", $user_id )); return $points ? intval($points) : 0; } /** * 增加用户积分 */ public function add_points($user_id, $points, $action_type, $related_id = null, $description = '') { global $wpdb; if ($points <= 0) { return false; } $points_table = $wpdb->prefix . 'user_points'; // 更新用户积分总额 $wpdb->query($wpdb->prepare( "UPDATE $points_table SET points = points + %d, total_earned = total_earned + %d WHERE user_id = %d", $points, $points, $user_id )); // 记录积分日志 $this->add_points_log($user_id, $points, $action_type, $related_id, $description); // 触发积分增加动作 do_action('points_added', $user_id, $points, $action_type); return true; } /** * 扣除用户积分 */ public function deduct_points($user_id, $points, $action_type, $related_id = null, $description = '') { global $wpdb; if ($points <= 0) { return false; } // 检查用户是否有足够积分 $current_points = $this->get_user_points($user_id); if ($current_points < $points) { return false; } $points_table = $wpdb->prefix . 'user_points'; // 更新用户积分总额 $wpdb->query($wpdb->prepare( "UPDATE $points_table SET points = points - %d, total_spent = total_spent + %d WHERE user_id = %d", $points, $points, $user_id )); // 记录积分日志 $this->add_points_log($user_id, -$points, $action_type, $related_id, $description); // 触发积分扣除动作 do_action('points_deducted', $user_id, $points, $action_type); return true; } /** * 添加积分记录 */ private function add_points_log($user_id, $points_change, $action_type, $related_id, $description) { global $wpdb; $log_table = $wpdb->prefix . 'points_log'; $wpdb->insert( $log_table, array( 'user_id' => $user_id, 'points_change' => $points_change, 'action_type' => $action_type, 'related_id' => $related_id, 'description' => $description ), array('%d', '%d', '%s', '%d', '%s') ); return $wpdb->insert_id; } /** * 获取用户积分记录 */ public function get_user_points_log($user_id, $limit = 20, $offset = 0) { global $wpdb; $log_table = $wpdb->prefix . 'points_log'; $logs = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $log_table WHERE user_id = %d ORDER BY created_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset )); return $logs; } /** * 发布文章奖励积分 */ public function award_points_for_publishing($post_id) { $post = get_post($post_id); $user_id = $post->post_author; // 避免重复奖励 if (get_post_meta($post_id, '_points_awarded', true)) { return; } $points_for_post = get_option('points_for_post', 50); if ($points_for_post > 0) { $this->add_points( $user_id, $points_for_post, 'publish_post', $post_id, sprintf('发布文章《%s》', $post->post_title) ); update_post_meta($post_id, '_points_awarded', true); } } /** * 评论奖励积分 */ public function award_points_for_comment($comment_id) { $comment = get_comment($comment_id); $user_id = $comment->user_id; // 只奖励已批准的评论 if ($comment->comment_approved != 1) { return; } // 避免重复奖励 if (get_comment_meta($comment_id, '_points_awarded', true)) { return; } $points_for_comment = get_option('points_for_comment', 10); if ($points_for_comment > 0 && $user_id > 0) { $this->add_points( $user_id, $points_for_comment, 'add_comment', $comment_id, '发表评论' ); update_comment_meta($comment_id, '_points_awarded', true); } } /** * 每日登录奖励积分 */ public function award_daily_login_points() { // 获取所有活跃用户 $users = get_users(array( 'meta_key' => 'last_login_date', 'meta_value' => date('Y-m-d', strtotime('-1 day')), 'meta_compare' => '>=' )); $points_for_daily_login = get_option('points_for_daily_login', 5); foreach ($users as $user) { $this->add_points( $user->ID, $points_for_daily_login, 'daily_login', null, '每日登录奖励' ); } } /** * 删除用户积分数据 */ public function delete_user_points_data($user_id) { global $wpdb; $points_table = $wpdb->prefix . 'user_points'; $log_table = $wpdb->prefix . 'points_log'; $wpdb->delete($points_table, array('user_id' => $user_id), array('%d')); $wpdb->delete($log_table, array('user_id' => $user_id), array('%d')); } } 2.2 积分系统短代码与模板标签 为了让前端可以方便地显示积分信息,我们需要创建一些短代码和模板函数: /** * 积分系统短代码和模板函数 */ class WP_Points_Template_Functions { /** * 显示当前用户积分的短代码 */ public static function points_balance_shortcode($atts) { if (!is_user_logged_in()) { return '<div class="points-balance">请登录查看积分</div>'; } $user_id = get_current_user_id(); $points_system = WP_Points_System::get_instance(); $points = $points_system->get_user_points($user_id); return sprintf( '<div class="points-balance">当前积分: <strong>%d</strong></div>', $points ); } /** * 显示用户积分排行榜的短代码 */ public static function points_leaderboard_shortcode($atts) { $atts = shortcode_atts(array( 'limit' => 10, 'show_avatar' => true, 'show_points' => true ), $atts); global $wpdb; $points_table = $wpdb->prefix . 'user_points'; $users_table = $wpdb->prefix . 'users'; $leaderboard = $wpdb->get_results($wpdb->prepare( "SELECT u.ID, u.user_login, u.display_name, p.points FROM $points_table p INNER JOIN $users_table u ON p.user_id = u.ID ORDER BY p.points DESC LIMIT %d", $atts['limit'] )); if (empty($leaderboard)) { return '<p>暂无积分数据</p>'; } $output = '<div class="points-leaderboard"><h3>积分排行榜</h3><ol>'; foreach ($leaderboard as $index => $user) { $output .= '<li>'; if ($atts['show_avatar']) { $output .= get_avatar($user->ID, 40) . ' '; } $output .= esc_html($user->display_name ?: $user->user_login); if ($atts['show_points']) { $output .= sprintf(' <span class="points-count">(%d 积分)</span>', $user->points); } $output .= '</li>'; } $output .= '</ol></div>'; return $output; } /** * 模板函数:获取用户积分 */ public static function get_user_points($user_id = null) { if (!$user_id) { $user_id = get_current_user_id(); } $points_system = WP_Points_System::get_instance(); return $points_system->get_user_points($user_id); } /** * 模板函数:显示积分日志 */ public static function display_points_log($user_id = null, $limit = 20) { if (!$user_id) { $user_id = get_current_user_id(); } $points_system = WP_Points_System::get_instance(); $logs = $points_system->get_user_points_log($user_id, $limit); if (empty($logs)) { return '<p>暂无积分记录</p>'; } $output = '<table class="points-log-table"><thead> <tr><th>时间</th><th>操作</th><th>积分变化</th><th>描述</th></tr> </thead><tbody>'; foreach ($logs as $log) { $points_change = $log->points_change > 0 ? sprintf('+%d', $log->points_change) : sprintf('%d', $log->points_change); $points_class = $log->points_change > 0 ? 'points-increase' : 'points-decrease'; $output .= sprintf( '<tr> <td>%s</td> <td>%s</td> <td class="%s">%s</td> <td>%s</td> </tr>', $log->created_at, esc_html($log->action_type), $points_class, $points_change, esc_html($log->description) ); } $output .= '</tbody></table>'; return $output; } } // 注册短代码 add_shortcode('points_balance', array('WP_Points_Template_Functions', 'points_balance_shortcode')); add_shortcode('points_leaderboard', array('WP_Points_Template_Functions', 'points_leaderboard_shortcode')); 虚拟礼物系统的实现 3.1 虚拟礼物管理类 <?php /** * WordPress虚拟礼物系统 */ class WP_Virtual_Gifts_System { private static $instance = null; 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('init', array($this, 'check_gifts_table')); // 添加AJAX处理 add_action('wp_ajax_purchase_gift', array($this, 'ajax_purchase_gift')); add_action('wp_ajax_nopriv_purchase_gift', array($this, 'ajax_no_permission')); add_action('wp_ajax_send_gift', array($this, 'ajax_send_gift')); add_action('wp_ajax_nopriv_send_gift', array($this, 'ajax_no_permission')); // 添加管理菜单 add_action('admin_menu', array($this, 'add_admin_menu')); } /** * 检查并创建礼物数据表 */ public function check_gifts_table() { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $transactions_table = $wpdb->prefix . 'gift_transactions'; // 检查表是否存在,如果不存在则创建 if ($wpdb->get_var("SHOW TABLES LIKE '$gifts_table'") != $gifts_table) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $gifts_table ( id INT AUTO_INCREMENT PRIMARY KEY, gift_name VARCHAR(100) NOT NULL, gift_description TEXT, gift_price INT NOT NULL, gift_image VARCHAR(255), gift_category VARCHAR(50), is_active BOOLEAN DEFAULT TRUE, sort_order INT DEFAULT 0, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 插入示例礼物数据 $this->insert_sample_gifts(); } if ($wpdb->get_var("SHOW TABLES LIKE '$transactions_table'") != $transactions_table) { $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $transactions_table ( id INT AUTO_INCREMENT PRIMARY KEY, sender_id BIGINT(20) NOT NULL, receiver_id BIGINT(20) NOT NULL, gift_id INT NOT NULL, quantity INT DEFAULT 1, message TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (sender_id) REFERENCES {$wpdb->prefix}users(ID) ON DELETE CASCADE, FOREIGN KEY (receiver_id) REFERENCES {$wpdb->prefix}users(ID) ON DELETE CASCADE, FOREIGN KEY (gift_id) REFERENCES $gifts_table(id) ON DELETE CASCADE ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } } /** * 插入示例礼物数据 */ private function insert_sample_gifts() { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $sample_gifts = array( array( 'gift_name' => '玫瑰花', 'gift_description' => '表达喜爱之情', 'gift_price' => 10, 'gift_category' => '情感', 'sort_order' => 1 ), array( 'gift_name' => '咖啡', 'gift_description' => '请喝一杯咖啡', 'gift_price' => 20, 'gift_category' => '日常', 'sort_order' => 2 ), array( 'gift_name' => '奖杯', 'gift_description' => '表彰优秀贡献', 'gift_price' => 50, 'gift_category' => '荣誉', 'sort_order' => 3 ), array( 'gift_name' => '钻石', 'gift_description' => '珍贵的心意', 'gift_price' => 100, 'gift_category' => '豪华', 'sort_order' => 4 ), array( 'gift_name' => '蛋糕', 'gift_description' => '庆祝特别时刻', 'gift_price' => 30, 'gift_category' => '庆祝', 'sort_order' => 5 ) ); foreach ($sample_gifts as $gift) { $wpdb->insert($gifts_table, $gift); } } /** * 获取所有可用礼物 */ public function get_available_gifts($category = null) { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $where = "WHERE is_active = 1"; if ($category) { $where .= $wpdb->prepare(" AND gift_category = %s", $category); } $gifts = $wpdb->get_results( "SELECT * FROM $gifts_table $where ORDER BY sort_order ASC, gift_price ASC" ); return $gifts; } /** * 获取单个礼物信息 */ public function get_gift($gift_id) { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $gift = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $gifts_table WHERE id = %d", $gift_id )); return $gift; } /** * 购买礼物 */ public function purchase_gift($user_id, $gift_id, $quantity = 1) { $gift = $this->get_gift($gift_id); if (!$gift || !$gift->is_active) { return array('success' => false, 'message' => '礼物不存在或已下架'); } $total_cost = $gift->gift_price * $quantity; // 检查用户积分是否足够 $points_system = WP_Points_System::get_instance(); $user_points = $points_system->get_user_points($user_id); if ($user_points < $total_cost) { return array('success' => false, 'message' => '积分不足'); } // 扣除积分 $deduct_result = $points_system->deduct_points( $user_id, $total_cost, 'purchase_gift', $gift_id, sprintf('购买礼物「%s」x%d', $gift->gift_name, $quantity) ); if (!$deduct_result) { return array('success' => false, 'message' => '积分扣除失败'); } // 这里可以添加礼物库存逻辑,如果需要的话 return array( 'success' => true, 'message' => '购买成功', 'gift_id' => $gift_id, 'quantity' => $quantity, 'total_cost' => $total_cost ); } /** * 赠送礼物给其他用户 */ public function send_gift($sender_id, $receiver_id, $gift_id, $quantity = 1, $message = '') { global $wpdb; // 检查接收者是否存在 $receiver = get_user_by('ID', $receiver_id); if (!$receiver) { return array('success' => false, 'message' => '接收者不存在'); } // 不能送给自己 if ($sender_id == $receiver_id) { return array('success' => false, 'message' => '不能给自己赠送礼物'); } // 先购买礼物 $purchase_result = $this->purchase_gift($sender_id, $gift_id, $quantity); if (!$purchase_result['success']) { return $purchase_result; } // 记录礼物交易 $transactions_table = $wpdb->prefix . 'gift_transactions'; $wpdb->insert( $transactions_table, array( 'sender_id' => $sender_id, 'receiver_id' => $receiver_id, 'gift_id' => $gift_id, 'quantity' => $quantity, 'message' => $message ), array('%d', '%d', '%d', '%d', '%s') ); $transaction_id = $wpdb->insert_id; // 发送通知给接收者 $this->send_gift_notification($sender_id, $receiver_id, $gift_id, $quantity, $message); return array( 'success' => true, 'message' => '礼物赠送成功', 'transaction_id' => $transaction_id ); } /** * 发送礼物通知 */ private function send_gift_notification($sender_id, $receiver_id, $gift_id, $quantity, $message) { $sender = get_user_by('ID', $sender_id); $gift = $this->get_gift($gift_id); $notification = sprintf( '%s 向您赠送了 %d 个「%s」', $sender->display_name, $quantity, $gift->gift_name ); if (!empty($message)) { $notification .= sprintf(',并留言:%s', $message); } // 使用WordPress的通知系统或自定义通知 if (function_exists('bp_notifications_add_notification')) { // BuddyPress通知 bp_notifications_add_notification(array( 'user_id' => $receiver_id, 'item_id' => $gift_id, 'component_name' => 'gifts', 'component_action' => 'new_gift', 'date_notified' => bp_core_current_time(), 'is_new' => 1, )); } // 发送站内信 $this->send_private_message($sender_id, $receiver_id, $notification); // 发送邮件通知 $receiver_email = get_user_by('ID', $receiver_id)->user_email; $subject = '您收到了一份新礼物'; wp_mail($receiver_email, $subject, $notification); } /** * 发送站内私信 */ private function send_private_message($sender_id, $receiver_id, $message) { // 这里可以集成站内信系统 // 如果使用BuddyPress,可以使用bp_messages_new_message函数 // 或者使用自定义的站内信系统 } /** * 获取用户收到的礼物 */ public function get_received_gifts($user_id, $limit = 20, $offset = 0) { global $wpdb; $transactions_table = $wpdb->prefix . 'gift_transactions'; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $users_table = $wpdb->prefix . 'users'; $gifts = $wpdb->get_results($wpdb->prepare( "SELECT t.*, g.gift_name, g.gift_image, u.display_name as sender_name FROM $transactions_table t INNER JOIN $gifts_table g ON t.gift_id = g.id INNER JOIN $users_table u ON t.sender_id = u.ID WHERE t.receiver_id = %d ORDER BY t.created_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset )); return $gifts; } /** * 获取用户送出的礼物 */ public function get_sent_gifts($user_id, $limit = 20, $offset = 0) { global $wpdb; $transactions_table = $wpdb->prefix . 'gift_transactions'; $gifts_table = $wpdb->prefix . 'virtual_gifts'; $users_table = $wpdb->prefix . 'users'; $gifts = $wpdb->get_results($wpdb->prepare( "SELECT t.*, g.gift_name, g.gift_image, u.display_name as receiver_name FROM $transactions_table t INNER JOIN $gifts_table g ON t.gift_id = g.id INNER JOIN $users_table u ON t.receiver_id = u.ID WHERE t.sender_id = %d ORDER BY t.created_at DESC LIMIT %d OFFSET %d", $user_id, $limit, $offset )); return $gifts; } /** * AJAX处理:购买礼物 */ public function ajax_purchase_gift() { // 安全检查 check_ajax_referer('gift_nonce', 'nonce'); if (!is_user_logged_in()) { wp_die(json_encode(array( 'success' => false, 'message' => '请先登录' ))); } $user_id = get_current_user_id(); $gift_id = intval($_POST['gift_id']); $quantity = isset($_POST['quantity']) ? intval($_POST['quantity']) : 1; if ($quantity < 1) { $quantity = 1; } $result = $this->purchase_gift($user_id, $gift_id, $quantity); wp_die(json_encode($result)); } /** * AJAX处理:赠送礼物 */ public function ajax_send_gift() { // 安全检查 check_ajax_referer('gift_nonce', 'nonce'); if (!is_user_logged_in()) { wp_die(json_encode(array( 'success' => false, 'message' => '请先登录' ))); } $sender_id = get_current_user_id(); $receiver_id = intval($_POST['receiver_id']); $gift_id = intval($_POST['gift_id']); $quantity = isset($_POST['quantity']) ? intval($_POST['quantity']) : 1; $message = isset($_POST['message']) ? sanitize_text_field($_POST['message']) : ''; $result = $this->send_gift($sender_id, $receiver_id, $gift_id, $quantity, $message); wp_die(json_encode($result)); } /** * 无权限的AJAX请求处理 */ public function ajax_no_permission() { wp_die(json_encode(array( 'success' => false, 'message' => '无权限操作' ))); } /** * 添加管理菜单 */ public function add_admin_menu() { add_menu_page( '虚拟礼物管理', '虚拟礼物', 'manage_options', 'virtual-gifts', array($this, 'gifts_admin_page'), 'dashicons-games', 30 ); add_submenu_page( 'virtual-gifts', '礼物管理', '礼物列表', 'manage_options', 'virtual-gifts', array($this, 'gifts_admin_page') ); add_submenu_page( 'virtual-gifts', '交易记录', '交易记录', 'manage_options', 'gift-transactions', array($this, 'transactions_admin_page') ); } /** * 礼物管理页面 */ public function gifts_admin_page() { global $wpdb; $gifts_table = $wpdb->prefix . 'virtual_gifts'; // 处理表单提交 if (isset($_POST['add_gift'])) { $this->handle_add_gift(); } elseif (isset($_POST['update_gift'])) { $this->handle_update_gift(); } elseif (isset($_GET['delete_gift'])) { $this->handle_delete_gift(); } // 获取所有礼物 $gifts = $wpdb->get_results("SELECT * FROM $gifts_table ORDER BY sort_order ASC"); ?> <div class="wrap"> <h1>虚拟礼物管理</h1> <h2>添加新礼物</h2> <form method="post" action=""> <?php wp_nonce_field('gift_management', 'gift_nonce'); ?> <table class="form-table"> <tr> <th><label for="gift_name">礼物名称</label></th> <td><input type="text" id="gift_name" name="gift_name" required class="regular-text"></td> </tr> <tr> <th><label for="gift_description">描述</label></th> <td><textarea id="gift_description" name="gift_description" rows="3" class="large-text"></textarea></td> </tr> <tr> <th><label for="gift_price">价格(积分)</label></th> <td><input type="number" id="gift_price" name="gift_price" required min="1" class="small-text"></td> </tr> <tr> <th><label for="gift_category">分类</label></th> <td><input type="text" id="gift_category" name="gift_category" class="regular-text"></td> </tr> <tr> <th><label for="sort_order">排序</label></th> <td><input type="number" id="sort_order" name="sort_order" value="0" class="small-text"></td> </tr> </table> <p class="submit"> <input type="submit" name="add_gift"

发表评论

实战教学,为你的网站添加在线团队任务投票与优先级排序工具

实战教学:为你的网站添加在线团队任务投票与优先级排序工具 引言:为什么你的团队需要在线任务投票与优先级排序工具 在现代团队协作中,任务优先级管理是决定工作效率的关键因素。根据项目管理协会(PMI)的调查,超过37%的项目失败是由于需求优先级不明确造成的。对于使用WordPress搭建的企业网站、团队协作平台或项目管理门户而言,集成一个在线任务投票与优先级排序工具可以显著提升团队决策效率。 传统的任务优先级确定方式往往依赖于少数管理者的主观判断,而在线投票工具能够让每个团队成员参与决策过程,通过集体智慧确定任务的真实优先级。这不仅提高了决策的透明度,也增强了团队成员的责任感和参与感。 本文将带领你通过WordPress代码二次开发,实现一个功能完整的在线团队任务投票与优先级排序工具。我们将从需求分析开始,逐步完成数据库设计、前端界面开发、后端逻辑实现以及安全性优化,最终打造一个适合团队协作的实用工具。 第一部分:环境准备与需求分析 1.1 开发环境配置 在开始开发之前,我们需要确保WordPress环境已正确配置: 本地开发环境:建议使用Local by Flywheel、XAMPP或MAMP搭建本地WordPress环境 WordPress版本:确保使用最新版本的WordPress(5.8+) 代码编辑器:推荐使用VS Code、PHPStorm或Sublime Text 浏览器开发者工具:用于调试前端代码 1.2 功能需求分析 我们的在线团队任务投票与优先级排序工具需要包含以下核心功能: 任务管理功能: 创建、编辑、删除任务 任务描述、截止日期、负责人设置 任务分类与标签 投票系统功能: 团队成员对任务进行投票 支持多种投票方式(优先级评分、简单投票等) 实时显示投票结果 优先级计算功能: 根据投票结果自动计算任务优先级 支持多种优先级算法(加权平均、多数决等) 可视化优先级展示 用户权限管理: 不同用户角色权限控制 投票权限管理 任务管理权限控制 数据可视化功能: 任务优先级看板 投票结果图表 团队参与度统计 1.3 技术选型 为了实现上述功能,我们将采用以下技术方案: 后端:WordPress原生PHP函数 + 自定义数据库表 前端:HTML5、CSS3、JavaScript(Vanilla JS + 少量jQuery) 数据可视化:Chart.js库 AJAX通信:WordPress REST API + admin-ajax.php 安全性:WordPress非ces、能力检查、数据验证 第二部分:数据库设计与数据模型 2.1 自定义数据库表设计 我们需要创建三个自定义数据库表来存储任务、投票和用户数据: -- 任务表 CREATE TABLE wp_team_tasks ( task_id INT AUTO_INCREMENT PRIMARY KEY, task_title VARCHAR(255) NOT NULL, task_description TEXT, task_status ENUM('pending', 'active', 'completed', 'archived') DEFAULT 'pending', created_by INT NOT NULL, assigned_to INT, due_date DATE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES wp_users(ID), FOREIGN KEY (assigned_to) REFERENCES wp_users(ID) ); -- 投票表 CREATE TABLE wp_task_votes ( vote_id INT AUTO_INCREMENT PRIMARY KEY, task_id INT NOT NULL, user_id INT NOT NULL, vote_value INT NOT NULL CHECK (vote_value BETWEEN 1 AND 10), vote_comment TEXT, voted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_user_task (user_id, task_id), FOREIGN KEY (task_id) REFERENCES wp_team_tasks(task_id) ON DELETE CASCADE, FOREIGN KEY (user_id) REFERENCES wp_users(ID) ); -- 任务分类表 CREATE TABLE wp_task_categories ( category_id INT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(100) NOT NULL, category_color VARCHAR(7) DEFAULT '#3498db', created_by INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (created_by) REFERENCES wp_users(ID) ); 2.2 数据库表关系与索引优化 为了提高查询效率,我们需要为常用查询字段添加索引: -- 为任务表添加索引 ALTER TABLE wp_team_tasks ADD INDEX idx_task_status (task_status); ALTER TABLE wp_team_tasks ADD INDEX idx_due_date (due_date); ALTER TABLE wp_team_tasks ADD INDEX idx_assigned_to (assigned_to); -- 为投票表添加索引 ALTER TABLE wp_task_votes ADD INDEX idx_task_id (task_id); ALTER TABLE wp_task_votes ADD INDEX idx_user_id (user_id); -- 为分类表添加索引 ALTER TABLE wp_task_categories ADD INDEX idx_created_by (created_by); 2.3 创建数据库表的WordPress实现 在WordPress中,我们通常在插件激活时创建自定义数据库表: <?php /** * 创建自定义数据库表 */ function team_task_voting_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_prefix = $wpdb->prefix; // 任务表 $tasks_table = $table_prefix . 'team_tasks'; $tasks_sql = "CREATE TABLE IF NOT EXISTS $tasks_table ( task_id INT AUTO_INCREMENT PRIMARY KEY, task_title VARCHAR(255) NOT NULL, task_description TEXT, task_status ENUM('pending', 'active', 'completed', 'archived') DEFAULT 'pending', created_by INT NOT NULL, assigned_to INT, due_date DATE, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) $charset_collate;"; // 投票表 $votes_table = $table_prefix . 'task_votes'; $votes_sql = "CREATE TABLE IF NOT EXISTS $votes_table ( vote_id INT AUTO_INCREMENT PRIMARY KEY, task_id INT NOT NULL, user_id INT NOT NULL, vote_value INT NOT NULL, vote_comment TEXT, voted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY unique_user_task (user_id, task_id) ) $charset_collate;"; // 分类表 $categories_table = $table_prefix . 'task_categories'; $categories_sql = "CREATE TABLE IF NOT EXISTS $categories_table ( category_id INT AUTO_INCREMENT PRIMARY KEY, category_name VARCHAR(100) NOT NULL, category_color VARCHAR(7) DEFAULT '#3498db', created_by INT NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($tasks_sql); dbDelta($votes_sql); dbDelta($categories_sql); // 添加外键约束(如果支持) if (strpos($wpdb->dbname, 'mysql') !== false) { $wpdb->query("ALTER TABLE $tasks_table ADD FOREIGN KEY (created_by) REFERENCES {$table_prefix}users(ID) ON DELETE CASCADE"); $wpdb->query("ALTER TABLE $tasks_table ADD FOREIGN KEY (assigned_to) REFERENCES {$table_prefix}users(ID) ON DELETE SET NULL"); $wpdb->query("ALTER TABLE $votes_table ADD FOREIGN KEY (task_id) REFERENCES $tasks_table(task_id) ON DELETE CASCADE"); $wpdb->query("ALTER TABLE $votes_table ADD FOREIGN KEY (user_id) REFERENCES {$table_prefix}users(ID) ON DELETE CASCADE"); $wpdb->query("ALTER TABLE $categories_table ADD FOREIGN KEY (created_by) REFERENCES {$table_prefix}users(ID) ON DELETE CASCADE"); } } register_activation_hook(__FILE__, 'team_task_voting_create_tables'); ?> 第三部分:后端功能开发 3.1 创建自定义Post Type与Taxonomy 虽然我们使用了自定义数据库表,但为了利用WordPress的权限系统和部分功能,我们也可以创建自定义文章类型: <?php /** * 注册任务自定义文章类型 */ function register_task_post_type() { $labels = array( 'name' => '团队任务', 'singular_name' => '团队任务', 'menu_name' => '团队任务', 'add_new' => '添加新任务', 'add_new_item' => '添加新任务', 'edit_item' => '编辑任务', 'new_item' => '新任务', 'view_item' => '查看任务', 'search_items' => '搜索任务', 'not_found' => '未找到任务', 'not_found_in_trash' => '回收站中无任务' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'team-task'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-clipboard', 'supports' => array('title', 'editor', 'author', 'custom-fields'), 'show_in_rest' => true, // 启用Gutenberg编辑器支持 ); register_post_type('team_task', $args); // 注册任务分类 register_taxonomy( 'task_category', 'team_task', array( 'label' => '任务分类', 'rewrite' => array('slug' => 'task-category'), 'hierarchical' => true, 'show_in_rest' => true, ) ); } add_action('init', 'register_task_post_type'); ?> 3.2 创建任务管理类 我们将创建一个任务管理类来处理所有与任务相关的操作: <?php /** * 任务管理类 */ class Team_Task_Manager { private $db; private $tasks_table; private $votes_table; private $categories_table; public function __construct() { global $wpdb; $this->db = $wpdb; $this->tasks_table = $wpdb->prefix . 'team_tasks'; $this->votes_table = $wpdb->prefix . 'task_votes'; $this->categories_table = $wpdb->prefix . 'task_categories'; } /** * 创建新任务 */ public function create_task($data) { $defaults = array( 'task_title' => '', 'task_description' => '', 'task_status' => 'pending', 'created_by' => get_current_user_id(), 'assigned_to' => null, 'due_date' => null, 'category_id' => null ); $data = wp_parse_args($data, $defaults); // 验证数据 if (empty($data['task_title'])) { return new WP_Error('empty_title', '任务标题不能为空'); } // 插入数据 $result = $this->db->insert( $this->tasks_table, array( 'task_title' => sanitize_text_field($data['task_title']), 'task_description' => wp_kses_post($data['task_description']), 'task_status' => sanitize_text_field($data['task_status']), 'created_by' => intval($data['created_by']), 'assigned_to' => !empty($data['assigned_to']) ? intval($data['assigned_to']) : null, 'due_date' => !empty($data['due_date']) ? sanitize_text_field($data['due_date']) : null ), array('%s', '%s', '%s', '%d', '%d', '%s') ); if ($result === false) { return new WP_Error('db_error', '数据库插入失败'); } $task_id = $this->db->insert_id; // 如果有分类,添加到分类关系表 if (!empty($data['category_id'])) { $this->add_task_to_category($task_id, intval($data['category_id'])); } return $task_id; } /** * 获取任务列表 */ public function get_tasks($args = array()) { $defaults = array( 'status' => null, 'assigned_to' => null, 'created_by' => null, 'category_id' => null, 'page' => 1, 'per_page' => 10, 'orderby' => 'created_at', 'order' => 'DESC' ); $args = wp_parse_args($args, $defaults); $where = array('1=1'); $values = array(); if (!empty($args['status'])) { $where[] = 'task_status = %s'; $values[] = $args['status']; } if (!empty($args['assigned_to'])) { $where[] = 'assigned_to = %d'; $values[] = $args['assigned_to']; } if (!empty($args['created_by'])) { $where[] = 'created_by = %d'; $values[] = $args['created_by']; } $where_clause = implode(' AND ', $where); // 分页计算 $offset = ($args['page'] - 1) * $args['per_page']; // 如果有分类筛选,需要联表查询 if (!empty($args['category_id'])) { $query = $this->db->prepare( "SELECT t.* FROM {$this->tasks_table} t INNER JOIN {$this->db->prefix}task_category_relationships r ON t.task_id = r.task_id WHERE $where_clause AND r.category_id = %d ORDER BY {$args['orderby']} {$args['order']} LIMIT %d OFFSET %d", array_merge($values, array($args['category_id'], $args['per_page'], $offset)) ); } else { $query = $this->db->prepare( "SELECT * FROM {$this->tasks_table} WHERE $where_clause ORDER BY {$args['orderby']} {$args['order']} LIMIT %d OFFSET %d", array_merge($values, array($args['per_page'], $offset)) ); } $tasks = $this->db->get_results($query, ARRAY_A); // 获取总数量用于分页 if (!empty($args['category_id'])) { $count_query = $this->db->prepare( "SELECT COUNT(*) FROM {$this->tasks_table} t INNER JOIN {$this->db->prefix}task_category_relationships r ON t.task_id = r.task_id WHERE $where_clause AND r.category_id = %d", array_merge($values, array($args['category_id'])) ); } else { $count_query = $this->db->prepare( "SELECT COUNT(*) FROM {$this->tasks_table} WHERE $where_clause", $values ); } $total = $this->db->get_var($count_query); return array( 'tasks' => $tasks, 'total' => $total, 'total_pages' => ceil($total / $args['per_page']) ); } /** * 提交投票 */ public function submit_vote($task_id, $user_id, $vote_value, $comment = '') { // 验证投票值 $vote_value = intval($vote_value); if ($vote_value < 1 || $vote_value > 10) { return new WP_Error('invalid_vote', '投票值必须在1-10之间'); } // 检查用户是否已投票 $existing_vote = $this->db->get_var($this->db->prepare( "SELECT vote_id FROM {$this->votes_table} WHERE task_id = %d AND user_id = %d", $task_id, $user_id )); if ($existing_vote) { // 更新现有投票 $result = $this->db->update( $this->votes_table, array( 'vote_value' => $vote_value, 'vote_comment' => sanitize_textarea_field($comment), 'voted_at' => current_time('mysql') ), array('vote_id' => $existing_vote), array('%d', '%s', '%s'), array('%d') ); } else { // 插入新投票 $result = $this->db->insert( $this->votes_table, array( 'task_id' => $task_id, 'user_id' => $user_id, 'vote_value' => $vote_value, 'vote_comment' => sanitize_textarea_field($comment) ), array('%d', '%d', '%d', '%s') ); } if ($result === false) { return new WP_Error('db_error', '投票提交失败'); } return true; } /** * 计算任务优先级 */ public function calculate_task_priority($task_id) { // 获取所有投票 $votes = $this->db->get_results($this->db->prepare( "SELECT vote_value FROM {$this->votes_table} WHERE task_id = %d", $task_id ), ARRAY_A); if (empty($votes)) { return 0; // 没有投票,优先级为0 3.3 优先级计算算法实现 /** * 计算任务优先级 */ public function calculate_task_priority($task_id) { // 获取所有投票 $votes = $this->db->get_results($this->db->prepare( "SELECT vote_value FROM {$this->votes_table} WHERE task_id = %d", $task_id ), ARRAY_A); if (empty($votes)) { return 0; // 没有投票,优先级为0 } // 提取投票值 $vote_values = array_column($votes, 'vote_value'); // 方法1:简单平均值 $simple_average = array_sum($vote_values) / count($vote_values); // 方法2:加权平均值(考虑投票者权重) $weighted_average = $this->calculate_weighted_average($task_id, $vote_values); // 方法3:去除极端值后的平均值 $trimmed_average = $this->calculate_trimmed_average($vote_values); // 综合优先级计算(可根据需求调整权重) $final_priority = ( $simple_average * 0.4 + $weighted_average * 0.4 + $trimmed_average * 0.2 ); // 获取投票数量作为置信度因子 $vote_count = count($votes); $confidence_factor = min(1, $vote_count / 10); // 最多10票达到完全置信 // 最终优先级(0-10分制) $final_score = $final_priority * $confidence_factor; return round($final_score, 2); } /** * 计算加权平均值 */ private function calculate_weighted_average($task_id, $vote_values) { global $wpdb; // 获取投票者信息 $voters = $wpdb->get_results($wpdb->prepare( "SELECT v.user_id, v.vote_value, u.user_login FROM {$this->votes_table} v LEFT JOIN {$wpdb->users} u ON v.user_id = u.ID WHERE v.task_id = %d", $task_id ), ARRAY_A); if (empty($voters)) { return 0; } $total_weight = 0; $weighted_sum = 0; foreach ($voters as $voter) { // 计算用户权重(基于用户角色、历史投票准确度等) $user_weight = $this->calculate_user_weight($voter['user_id']); $weighted_sum += $voter['vote_value'] * $user_weight; $total_weight += $user_weight; } return $total_weight > 0 ? $weighted_sum / $total_weight : 0; } /** * 计算用户权重 */ private function calculate_user_weight($user_id) { // 基础权重 $weight = 1.0; // 基于用户角色调整权重 $user = get_userdata($user_id); if ($user) { if (in_array('administrator', $user->roles)) { $weight *= 1.5; // 管理员权重更高 } elseif (in_array('editor', $user->roles)) { $weight *= 1.3; } elseif (in_array('author', $user->roles)) { $weight *= 1.2; } } // 基于历史投票参与度调整权重 $participation_rate = $this->get_user_participation_rate($user_id); $weight *= (0.5 + $participation_rate * 0.5); // 参与度影响权重 return $weight; } /** * 计算去除极端值后的平均值 */ private function calculate_trimmed_average($values, $trim_percent = 20) { if (count($values) < 3) { return array_sum($values) / count($values); } sort($values); $trim_count = floor(count($values) * $trim_percent / 100); $trimmed_values = array_slice($values, $trim_count, count($values) - 2 * $trim_count); return array_sum($trimmed_values) / count($trimmed_values); } /** * 获取用户投票参与度 */ private function get_user_participation_rate($user_id) { $total_tasks = $this->db->get_var( "SELECT COUNT(*) FROM {$this->tasks_table} WHERE task_status IN ('pending', 'active')" ); $user_votes = $this->db->get_var($this->db->prepare( "SELECT COUNT(DISTINCT task_id) FROM {$this->votes_table} WHERE user_id = %d", $user_id )); if ($total_tasks == 0) { return 1.0; } return min(1.0, $user_votes / $total_tasks); } /** * 获取任务投票详情 */ public function get_task_voting_details($task_id) { $votes = $this->db->get_results($this->db->prepare( "SELECT v.*, u.display_name, u.user_email FROM {$this->votes_table} v LEFT JOIN {$this->db->users} u ON v.user_id = u.ID WHERE v.task_id = %d ORDER BY v.voted_at DESC", $task_id ), ARRAY_A); $priority_score = $this->calculate_task_priority($task_id); // 统计投票分布 $vote_distribution = array_fill(1, 10, 0); foreach ($votes as $vote) { if ($vote['vote_value'] >= 1 && $vote['vote_value'] <= 10) { $vote_distribution[$vote['vote_value']]++; } } return array( 'votes' => $votes, 'vote_count' => count($votes), 'priority_score' => $priority_score, 'vote_distribution' => $vote_distribution, 'average_vote' => count($votes) > 0 ? array_sum(array_column($votes, 'vote_value')) / count($votes) : 0 ); } } ?> 3.4 REST API端点创建 为了支持前后端分离和AJAX调用,我们需要创建REST API端点: <?php /** * 注册REST API端点 */ class Task_Voting_REST_Controller extends WP_REST_Controller { private $task_manager; public function __construct() { $this->namespace = 'team-tasks/v1'; $this->task_manager = new Team_Task_Manager(); add_action('rest_api_init', array($this, 'register_routes')); } public function register_routes() { // 获取任务列表 register_rest_route($this->namespace, '/tasks', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_tasks'), 'permission_callback' => array($this, 'get_tasks_permissions_check'), 'args' => $this->get_collection_params(), ), array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array($this, 'create_task'), 'permission_callback' => array($this, 'create_task_permissions_check'), ), )); // 单个任务操作 register_rest_route($this->namespace, '/tasks/(?P<id>d+)', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_task'), 'permission_callback' => array($this, 'get_task_permissions_check'), ), array( 'methods' => WP_REST_Server::EDITABLE, 'callback' => array($this, 'update_task'), 'permission_callback' => array($this, 'update_task_permissions_check'), ), array( 'methods' => WP_REST_Server::DELETABLE, 'callback' => array($this, 'delete_task'), 'permission_callback' => array($this, 'delete_task_permissions_check'), ), )); // 投票相关端点 register_rest_route($this->namespace, '/tasks/(?P<task_id>d+)/vote', array( array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array($this, 'submit_vote'), 'permission_callback' => array($this, 'vote_permissions_check'), ), )); // 获取投票结果 register_rest_route($this->namespace, '/tasks/(?P<task_id>d+)/voting-results', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_voting_results'), 'permission_callback' => array($this, 'get_voting_results_permissions_check'), ), )); // 优先级看板数据 register_rest_route($this->namespace, '/priority-board', array( array( 'methods' => WP_REST_Server::READABLE, 'callback' => array($this, 'get_priority_board'), 'permission_callback' => array($this, 'get_priority_board_permissions_check'), ), )); } /** * 获取任务列表 */ public function get_tasks($request) { $params = $request->get_params(); $args = array( 'status' => isset($params['status']) ? sanitize_text_field($params['status']) : null, 'assigned_to' => isset($params['assigned_to']) ? intval($params['assigned_to']) : null, 'category_id' => isset($params['category_id']) ? intval($params['category_id']) : null, 'page' => isset($params['page']) ? intval($params['page']) : 1, 'per_page' => isset($params['per_page']) ? intval($params['per_page']) : 10, 'orderby' => isset($params['orderby']) ? sanitize_text_field($params['orderby']) : 'created_at', 'order' => isset($params['order']) ? sanitize_text_field($params['order']) : 'DESC', ); $result = $this->task_manager->get_tasks($args); // 为每个任务添加优先级分数 foreach ($result['tasks'] as &$task) { $task['priority_score'] = $this->task_manager->calculate_task_priority($task['task_id']); $task['vote_count'] = $this->task_manager->get_vote_count($task['task_id']); } $response = new WP_REST_Response($result); return $response; } /** * 创建新任务 */ public function create_task($request) { $params = $request->get_params(); $task_data = array( 'task_title' => sanitize_text_field($params['title']), 'task_description' => wp_kses_post($params['description']), 'assigned_to' => isset($params['assigned_to']) ? intval($params['assigned_to']) : null, 'due_date' => isset($params['due_date']) ? sanitize_text_field($params['due_date']) : null, 'category_id' => isset($params['category_id']) ? intval($params['category_id']) : null, ); $task_id = $this->task_manager->create_task($task_data); if (is_wp_error($task_id)) { return $task_id; } $response = new WP_REST_Response(array( 'success' => true, 'task_id' => $task_id, 'message' => '任务创建成功' )); return $response; } /** * 提交投票 */ public function submit_vote($request) { $task_id = intval($request['task_id']); $user_id = get_current_user_id(); $params = $request->get_params(); $vote_value = isset($params['vote_value']) ? intval($params['vote_value']) : 0; $comment = isset($params['comment']) ? sanitize_textarea_field($params['comment']) : ''; if ($vote_value < 1 || $vote_value > 10) { return new WP_Error('invalid_vote', '投票值必须在1-10之间', array('status' => 400)); } $result = $this->task_manager->submit_vote($task_id, $user_id, $vote_value, $comment); if (is_wp_error($result)) { return $result; } // 重新计算优先级 $priority_score = $this->task_manager->calculate_task_priority($task_id); $response = new WP_REST_Response(array( 'success' => true, 'priority_score' => $priority_score, 'message' => '投票成功' )); return $response; } /** * 获取优先级看板数据 */ public function get_priority_board($request) { global $wpdb; // 获取所有活跃任务 $tasks = $wpdb->get_results( "SELECT * FROM {$wpdb->prefix}team_tasks WHERE task_status IN ('pending', 'active') ORDER BY created_at DESC", ARRAY_A ); $board_data = array(); foreach ($tasks as $task) { $voting_details = $this->task_manager->get_task_voting_details($task['task_id']); // 获取负责人信息 $assigned_to_name = ''; if ($task['assigned_to']) { $user = get_userdata($task['assigned_to']); $assigned_to_name = $user ? $user->display_name : '未知用户'; } // 获取创建者信息 $creator = get_userdata($task['created_by']); $created_by_name = $creator ? $creator->display_name : '未知用户'; $board_data[] = array( 'id' => $task['task_id'], 'title' => $task['task_title'], 'description' => wp_trim_words($task['task_description'], 20), 'status' => $task['task_status'], 'priority_score' => $voting_details['priority_score'], 'vote_count' => $voting_details['vote_count'], 'average_vote' => $voting_details['average_vote'], 'assigned_to' => $assigned_to_name, 'created_by' => $created_by_name, 'due_date' => $task['due_date'], 'created_at' => $task['created_at'], 'vote_distribution' => $voting_details['vote_distribution'] ); } // 按优先级排序 usort($board_data, function($a, $b) { return $b['priority_score'] <=> $a['priority_score']; }); // 计算统计信息 $stats = array( 'total_tasks' => count($board_data), 'total_votes' => array_sum(array_column($board_data, 'vote_count')), 'average_priority' => count($board_data) > 0 ? array_sum(array_column($board_data, 'priority_score')) / count($board_data) : 0, 'participation_rate' => $this->calculate_overall_participation_rate(), ); $response = new WP_REST_Response(array( 'tasks' => $board_data, 'stats' => $stats )); return $response; } /** * 计算整体参与率 */ private function calculate_overall_participation_rate() { global $wpdb; $total_users = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->users}"); $active_tasks = $wpdb->get_var( "SELECT COUNT(*) FROM {$wpdb->prefix}team_tasks WHERE task_status IN ('pending', 'active')" ); if ($active_tasks == 0 || $total_users == 0) { return 0; } $users_voted = $wpdb->get_var( "SELECT COUNT(DISTINCT user_id) FROM {$wpdb->prefix}task_votes" ); // 参与率 = (已投票用户数 / 总用户数) * (平均投票任务数 / 总活跃任务数) $user_participation = $users_voted / $total_users; $total_votes = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}task_votes"); $average_votes_per_user = $users_voted > 0 ? $total_votes / $users_voted : 0; $task_participation = min(1, $average_votes_per_user / $active_tasks); return round(($user_participation + $task_participation) / 2 * 100, 2); } // 权限检查方法 public function get_tasks_permissions_check($request) { return current_user_can('read'); } public function create_task_permissions_check($request) { return current_user_can('publish_posts'); } public function vote_permissions_check($request) { return is_user_logged_in(); } public function get_priority_board_permissions_check($request) { return current_user_can('read'); } } // 初始化REST控制器 new Task_Voting_REST_Controller(); ?> 第四部分:前端界面开发 4.1 任务列表与投票界面

发表评论

手把手教程,在WordPress中集成网站第三方API状态监控与异常报警面板

手把手教程:在WordPress中集成网站第三方API状态监控与异常报警面板 引言:为什么WordPress需要API状态监控功能 在当今数字化时代,网站已经不再是简单的信息展示平台,而是集成了各种第三方服务和API的复杂生态系统。无论是支付网关、社交媒体集成、地图服务、数据分析工具还是云存储服务,现代网站通常依赖多个外部API来提供完整功能。然而,这些第三方服务偶尔会出现故障或响应缓慢,直接影响网站性能和用户体验。 传统的解决方案是使用独立的监控服务,但这些服务往往需要额外订阅费用,且与WordPress后台分离,增加了管理复杂度。本教程将向您展示如何通过WordPress代码二次开发,创建一个集成的API状态监控与异常报警面板,让您在一个熟悉的界面中监控所有关键服务的健康状况。 通过本教程,您将学习到如何利用WordPress的灵活性,扩展其功能,实现一个专业级的监控系统,而无需依赖外部高价服务。这个解决方案不仅成本效益高,而且完全可控,可以根据您的具体需求进行定制。 第一部分:准备工作与环境配置 1.1 确定监控目标与需求 在开始编码之前,我们需要明确监控系统的目标。您应该列出所有需要监控的第三方API,例如: 支付网关(PayPal、Stripe等) 社交媒体API(Facebook、Twitter、Instagram等) 地图服务(Google Maps、Mapbox等) 邮件服务(SMTP、SendGrid、MailChimp等) 云存储(AWS S3、Google Cloud Storage等) CDN服务(Cloudflare、Akamai等) 对于每个API,确定以下监控参数: 检查频率(每5分钟、每15分钟等) 超时阈值 响应状态码要求 响应内容验证规则 性能基准(响应时间) 1.2 开发环境设置 为了安全地进行开发,建议在本地或测试服务器上设置WordPress开发环境: 安装本地服务器环境:使用XAMPP、MAMP或Local by Flywheel 设置WordPress测试站点:安装最新版本的WordPress 启用调试模式:在wp-config.php中添加以下代码: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 安装代码编辑器:推荐VS Code、PHPStorm或Sublime Text 创建子主题或插件目录:我们将创建一个独立插件来实现功能 1.3 创建基础插件结构 在wp-content/plugins目录下创建新文件夹api-status-monitor,并创建以下文件结构: api-status-monitor/ ├── api-status-monitor.php # 主插件文件 ├── includes/ │ ├── class-api-monitor.php # 核心监控类 │ ├── class-alert-system.php # 报警系统类 │ └── class-dashboard-widget.php # 仪表板小工具 ├── admin/ │ ├── css/ │ │ └── admin-style.css # 管理界面样式 │ ├── js/ │ │ └── admin-script.js # 管理界面脚本 │ └── partials/ │ └── settings-page.php # 设置页面模板 ├── assets/ │ ├── css/ │ │ └── frontend-style.css # 前端样式(可选) │ └── js/ │ └── frontend-script.js # 前端脚本(可选) └── uninstall.php # 插件卸载清理脚本 第二部分:构建核心监控系统 2.1 创建主插件文件 编辑api-status-monitor.php,添加插件基本信息: <?php /** * Plugin Name: API状态监控与异常报警面板 * Plugin URI: https://yourwebsite.com/ * Description: 在WordPress中集成第三方API状态监控与异常报警功能 * Version: 1.0.0 * Author: 您的名称 * License: GPL v2 or later * Text Domain: api-status-monitor */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('ASM_VERSION', '1.0.0'); define('ASM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('ASM_PLUGIN_URL', plugin_dir_url(__FILE__)); // 包含必要文件 require_once ASM_PLUGIN_DIR . 'includes/class-api-monitor.php'; require_once ASM_PLUGIN_DIR . 'includes/class-alert-system.php'; require_once ASM_PLUGIN_DIR . 'includes/class-dashboard-widget.php'; // 初始化插件 function asm_init() { // 检查WordPress版本 if (version_compare(get_bloginfo('version'), '5.0', '<')) { add_action('admin_notices', function() { echo '<div class="notice notice-error"><p>'; echo __('API状态监控插件需要WordPress 5.0或更高版本。', 'api-status-monitor'); echo '</p></div>'; }); return; } // 初始化核心类 $api_monitor = new API_Monitor(); $alert_system = new Alert_System(); $dashboard_widget = new Dashboard_Widget(); // 注册激活/停用钩子 register_activation_hook(__FILE__, array($api_monitor, 'activate')); register_deactivation_hook(__FILE__, array($api_monitor, 'deactivate')); // 初始化 add_action('init', array($api_monitor, 'init')); add_action('admin_menu', array($api_monitor, 'add_admin_menu')); } add_action('plugins_loaded', 'asm_init'); 2.2 实现API监控核心类 创建includes/class-api-monitor.php文件: <?php class API_Monitor { private $monitored_apis; private $check_interval; private $options_name = 'asm_settings'; public function __construct() { $this->check_interval = 300; // 默认5分钟 $this->monitored_apis = array(); } public function init() { // 加载文本域 load_plugin_textdomain('api-status-monitor', false, dirname(plugin_basename(__FILE__)) . '/languages'); // 注册设置 add_action('admin_init', array($this, 'register_settings')); // 安排定时任务 $this->schedule_cron_jobs(); // 加载监控的API列表 $this->load_monitored_apis(); // 添加AJAX处理 add_action('wp_ajax_asm_test_api', array($this, 'ajax_test_api')); add_action('wp_ajax_asm_get_status', array($this, 'ajax_get_status')); } public function activate() { // 创建数据库表 $this->create_tables(); // 设置默认选项 $default_options = array( 'check_interval' => 300, 'enable_email_alerts' => true, 'enable_sms_alerts' => false, 'alert_email' => get_option('admin_email'), 'history_days' => 30 ); if (!get_option($this->options_name)) { add_option($this->options_name, $default_options); } // 安排初始监控任务 wp_schedule_event(time(), 'asm_five_minutes', 'asm_check_all_apis'); } public function deactivate() { // 清除定时任务 wp_clear_scheduled_hook('asm_check_all_apis'); // 清理选项(可选) // delete_option($this->options_name); } private function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'asm_api_status'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, api_name varchar(100) NOT NULL, endpoint varchar(255) NOT NULL, status_code smallint(3) NOT NULL, response_time float NOT NULL, status varchar(20) NOT NULL, checked_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, response_message text, PRIMARY KEY (id), KEY api_name (api_name), KEY checked_at (checked_at) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建报警日志表 $alert_table = $wpdb->prefix . 'asm_alerts'; $alert_sql = "CREATE TABLE IF NOT EXISTS $alert_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, api_name varchar(100) NOT NULL, alert_type varchar(50) NOT NULL, alert_message text NOT NULL, alert_level varchar(20) NOT NULL, sent_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, resolved_at datetime, PRIMARY KEY (id), KEY api_name (api_name), KEY alert_level (alert_level), KEY sent_at (sent_at) ) $charset_collate;"; dbDelta($alert_sql); } private function schedule_cron_jobs() { // 添加自定义cron间隔 add_filter('cron_schedules', array($this, 'add_cron_intervals')); // 添加监控任务 add_action('asm_check_all_apis', array($this, 'check_all_apis')); } public function add_cron_intervals($schedules) { $schedules['asm_five_minutes'] = array( 'interval' => 300, 'display' => __('每5分钟', 'api-status-monitor') ); $schedules['asm_fifteen_minutes'] = array( 'interval' => 900, 'display' => __('每15分钟', 'api-status-monitor') ); $schedules['asm_thirty_minutes'] = array( 'interval' => 1800, 'display' => __('每30分钟', 'api-status-monitor') ); return $schedules; } private function load_monitored_apis() { // 从数据库或选项加载API列表 $default_apis = array( array( 'name' => 'Stripe支付网关', 'endpoint' => 'https://api.stripe.com/v1/', 'method' => 'GET', 'expected_status' => 200, 'check_interval' => 300, 'timeout' => 10, 'headers' => array(), 'body' => '', 'validation' => 'json', 'active' => true ), array( 'name' => 'Google Maps API', 'endpoint' => 'https://maps.googleapis.com/maps/api/geocode/json?address=1600+Amphitheatre+Parkway,+Mountain+View,+CA', 'method' => 'GET', 'expected_status' => 200, 'check_interval' => 900, 'timeout' => 15, 'headers' => array(), 'body' => '', 'validation' => 'json_contains', 'validation_value' => 'OK', 'active' => true ), // 添加更多API配置 ); $saved_apis = get_option('asm_monitored_apis', array()); if (empty($saved_apis)) { $this->monitored_apis = $default_apis; update_option('asm_monitored_apis', $default_apis); } else { $this->monitored_apis = $saved_apis; } } public function check_all_apis() { foreach ($this->monitored_apis as $api) { if ($api['active']) { $this->check_single_api($api); } } } private function check_single_api($api_config) { $start_time = microtime(true); $args = array( 'method' => $api_config['method'], 'timeout' => $api_config['timeout'], 'headers' => $api_config['headers'] ); if (!empty($api_config['body'])) { $args['body'] = $api_config['body']; } $response = wp_remote_request($api_config['endpoint'], $args); $response_time = round((microtime(true) - $start_time) * 1000, 2); // 毫秒 $status_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); // 验证响应 $status = 'unknown'; $message = ''; if (is_wp_error($response)) { $status = 'error'; $message = $response->get_error_message(); } elseif ($status_code == $api_config['expected_status']) { // 进一步验证响应内容 if ($this->validate_response($api_config, $response_body)) { $status = 'healthy'; $message = __('API响应正常', 'api-status-monitor'); } else { $status = 'warning'; $message = __('API响应异常', 'api-status-monitor'); } } else { $status = 'error'; $message = sprintf(__('HTTP状态码异常: %d', 'api-status-monitor'), $status_code); } // 保存结果到数据库 $this->save_api_status($api_config['name'], $api_config['endpoint'], $status_code, $response_time, $status, $message); // 触发报警检查 if ($status != 'healthy') { do_action('asm_api_status_changed', $api_config['name'], $status, $message); } return array( 'status' => $status, 'response_time' => $response_time, 'status_code' => $status_code, 'message' => $message ); } private function validate_response($api_config, $response_body) { if (empty($api_config['validation'])) { return true; } switch ($api_config['validation']) { case 'json': json_decode($response_body); return json_last_error() === JSON_ERROR_NONE; case 'json_contains': $data = json_decode($response_body, true); if (json_last_error() !== JSON_ERROR_NONE) { return false; } // 检查响应中是否包含特定值 $needle = $api_config['validation_value']; $haystack = json_encode($data); return strpos($haystack, $needle) !== false; case 'regex': return preg_match($api_config['validation_pattern'], $response_body) === 1; default: return true; } } private function save_api_status($api_name, $endpoint, $status_code, $response_time, $status, $message) { global $wpdb; $table_name = $wpdb->prefix . 'asm_api_status'; $wpdb->insert( $table_name, array( 'api_name' => $api_name, 'endpoint' => $endpoint, 'status_code' => $status_code, 'response_time' => $response_time, 'status' => $status, 'response_message' => $message ), array('%s', '%s', '%d', '%f', '%s', '%s') ); // 清理旧记录(保留最近30天) $history_days = get_option($this->options_name)['history_days'] ?? 30; $cutoff_date = date('Y-m-d H:i:s', strtotime("-{$history_days} days")); $wpdb->query( $wpdb->prepare( "DELETE FROM $table_name WHERE checked_at < %s", $cutoff_date ) ); } // 更多方法将在后续部分添加... } 第三部分:实现报警系统 3.1 创建报警系统类 创建includes/class-alert-system.php文件: <?php class Alert_System { private $alert_methods; public function __construct() { $this->alert_methods = array(); // 初始化报警方法 $this->init_alert_methods(); } public function init() { // 监听API状态变化事件 add_action('asm_api_status_changed', array($this, 'handle_status_change'), 10, 3); // 注册报警方法 add_action('asm_send_email_alert', array($this, 'send_email_alert'), 10, 3); add_action('asm_send_sms_alert', array($this, 'send_sms_alert'), 10, 3); add_action('asm_send_slack_alert', array($this, 'send_slack_alert'), 10, 3); // 添加报警设置页面 add_action('admin_menu', array($this, 'add_alert_settings_page')); } private function init_alert_methods() { $settings = get_option('asm_settings', array()); $this->alert_methods = array( 'email' => array( 'enabled' => $settings['enable_email_alerts'] ?? true, 'recipients' => isset($settings['alert_email']) ? explode(',', $settings['alert_email']) : array(get_option('admin_email')) ), 'sms' => array( 'enabled' => $settings['enable_sms_alerts'] ?? false, 'provider' => $settings['sms_provider'] ?? '', 'api_key' => $settings['sms_api_key'] ?? '', 'phone_numbers' => isset($settings['sms_numbers']) ? explode(',', $settings['sms_numbers']) : ), 'slack' => array( 'enabled' => $settings['enable_slack_alerts'] ?? false, 'webhook_url' => $settings['slack_webhook'] ?? '', 'channel' => $settings['slack_channel'] ?? '#alerts' ) ); } public function handle_status_change($api_name, $status, $message) { // 检查是否需要发送报警(避免重复报警) if ($this->should_send_alert($api_name, $status)) { $alert_data = array( 'api_name' => $api_name, 'status' => $status, 'message' => $message, 'timestamp' => current_time('mysql'), 'site_url' => get_site_url() ); // 根据状态确定报警级别 $alert_level = $this->determine_alert_level($status); // 记录报警 $this->log_alert($api_name, 'status_change', $message, $alert_level); // 发送报警 $this->dispatch_alerts($alert_data, $alert_level); } } private function should_send_alert($api_name, $status) { global $wpdb; $table_name = $wpdb->prefix . 'asm_alerts'; // 检查最近是否已发送相同报警 $recent_alerts = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE api_name = %s AND alert_type = 'status_change' AND alert_level = %s AND sent_at > DATE_SUB(NOW(), INTERVAL 1 HOUR) AND resolved_at IS NULL", $api_name, $this->determine_alert_level($status) )); return $recent_alerts == 0; } private function determine_alert_level($status) { switch ($status) { case 'error': return 'critical'; case 'warning': return 'warning'; case 'unknown': return 'info'; default: return 'info'; } } private function log_alert($api_name, $alert_type, $message, $alert_level) { global $wpdb; $table_name = $wpdb->prefix . 'asm_alerts'; $wpdb->insert( $table_name, array( 'api_name' => $api_name, 'alert_type' => $alert_type, 'alert_message' => $message, 'alert_level' => $alert_level ), array('%s', '%s', '%s', '%s') ); } private function dispatch_alerts($alert_data, $alert_level) { $settings = get_option('asm_settings', array()); // 发送邮件报警 if ($this->alert_methods['email']['enabled']) { do_action('asm_send_email_alert', $alert_data, $alert_level, $this->alert_methods['email']); } // 发送短信报警 if ($this->alert_methods['sms']['enabled'] && $alert_level == 'critical') { do_action('asm_send_sms_alert', $alert_data, $alert_level, $this->alert_methods['sms']); } // 发送Slack报警 if ($this->alert_methods['slack']['enabled']) { do_action('asm_send_slack_alert', $alert_data, $alert_level, $this->alert_methods['slack']); } } public function send_email_alert($alert_data, $alert_level, $email_config) { $subject = sprintf( '[%s] API监控报警: %s - %s', get_bloginfo('name'), $alert_data['api_name'], strtoupper($alert_level) ); $message = $this->build_alert_message($alert_data, $alert_level); $headers = array('Content-Type: text/html; charset=UTF-8'); foreach ($email_config['recipients'] as $recipient) { wp_mail(trim($recipient), $subject, $message, $headers); } } public function send_sms_alert($alert_data, $alert_level, $sms_config) { // 这里实现短信发送逻辑 // 可以使用Twilio、阿里云、腾讯云等短信服务 $message = sprintf( "API报警: %s状态异常(%s)。详情请查看%s", $alert_data['api_name'], $alert_level, get_site_url() ); // 示例:使用Twilio发送短信 if ($sms_config['provider'] === 'twilio') { $this->send_twilio_sms($sms_config, $message); } // 可以添加更多短信服务商 } public function send_slack_alert($alert_data, $alert_level, $slack_config) { $message = array( 'text' => sprintf("*API监控报警* :warning:n*服务*: %sn*状态*: %sn*详情*: %sn*时间*: %s", $alert_data['api_name'], $alert_level, $alert_data['message'], $alert_data['timestamp'] ), 'channel' => $slack_config['channel'], 'username' => 'API监控机器人', 'icon_emoji' => ':robot_face:' ); if ($alert_level === 'critical') { $message['attachments'] = array(array( 'color' => 'danger', 'fields' => array( array( 'title' => '紧急程度', 'value' => '需要立即处理', 'short' => true ) ) )); } wp_remote_post($slack_config['webhook_url'], array( 'body' => json_encode($message), 'headers' => array('Content-Type' => 'application/json') )); } private function build_alert_message($alert_data, $alert_level) { $color = $alert_level === 'critical' ? '#dc3545' : ($alert_level === 'warning' ? '#ffc107' : '#17a2b8'); ob_start(); ?> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> body { font-family: Arial, sans-serif; line-height: 1.6; } .container { max-width: 600px; margin: 0 auto; padding: 20px; } .header { background-color: <?php echo $color; ?>; color: white; padding: 15px; border-radius: 5px 5px 0 0; } .content { background-color: #f8f9fa; padding: 20px; border-radius: 0 0 5px 5px; } .alert-level { display: inline-block; padding: 3px 10px; border-radius: 3px; background-color: <?php echo $color; ?>; color: white; font-weight: bold; } .details { margin-top: 20px; } .button { display: inline-block; padding: 10px 20px; background-color: <?php echo $color; ?>; color: white; text-decoration: none; border-radius: 4px; } .footer { margin-top: 30px; padding-top: 20px; border-top: 1px solid #dee2e6; color: #6c757d; font-size: 12px; } </style> </head> <body> <div class="container"> <div class="header"> <h2>API监控系统报警通知</h2> </div> <div class="content"> <p><strong>报警级别:</strong> <span class="alert-level"><?php echo strtoupper($alert_level); ?></span></p> <p><strong>API服务:</strong> <?php echo esc_html($alert_data['api_name']); ?></p> <p><strong>状态:</strong> <?php echo esc_html($alert_data['status']); ?></p> <p><strong>详细信息:</strong> <?php echo esc_html($alert_data['message']); ?></p> <p><strong>发生时间:</strong> <?php echo $alert_data['timestamp']; ?></p> <div class="details"> <p>请及时处理此问题,避免影响网站正常功能。</p> <a href="<?php echo admin_url('admin.php?page=api-status-monitor'); ?>" class="button">查看详细报告</a> </div> <div class="footer"> <p>此邮件由 <?php echo get_bloginfo('name'); ?> API监控系统自动发送</p> <p>如需修改报警设置,请访问: <?php echo admin_url('admin.php?page=api-status-monitor-settings'); ?></p> </div> </div> </div> </body> </html> <?php return ob_get_clean(); } public function add_alert_settings_page() { add_submenu_page( 'api-status-monitor', '报警设置', '报警设置', 'manage_options', 'api-status-monitor-alerts', array($this, 'render_alert_settings_page') ); } public function render_alert_settings_page() { include ASM_PLUGIN_DIR . 'admin/partials/alert-settings-page.php'; } } 第四部分:创建仪表板小工具 4.1 创建仪表板小工具类 创建includes/class-dashboard-widget.php文件: <?php class Dashboard_Widget { public function __construct() { // 初始化方法 } public function init() { // 添加仪表板小工具 add_action('wp_dashboard_setup', array($this, 'add_dashboard_widget')); // 添加AJAX处理 add_action('wp_ajax_asm_refresh_status', array($this, 'ajax_refresh_status')); // 添加前端样式和脚本 add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); } public function add_dashboard_widget() { wp_add_dashboard_widget( 'asm_api_status_widget', 'API状态监控', array($this, 'render_dashboard_widget'), null, null, 'normal', 'high' ); } public function render_dashboard_widget() { global $wpdb; $table_name = $wpdb->prefix . 'asm_api_status'; // 获取最近一次检查结果 $recent_status = $wpdb->get_results( "SELECT t1.* FROM $table_name t1 INNER JOIN ( SELECT api_name, MAX(checked_at) as latest FROM $table_name GROUP BY api_name ) t2 ON t1.api_name = t2.api_name AND t1.checked_at = t2.latest ORDER BY CASE WHEN t1.status = 'error' THEN 1 WHEN t1.status = 'warning' THEN 2 WHEN t1.status = 'unknown' THEN 3 ELSE 4 END, t1.api_name", ARRAY_A ); // 获取24小时内的统计数据 $stats = $wpdb->get_row( $wpdb->prepare( "SELECT COUNT(DISTINCT api_name) as total_apis, SUM(CASE WHEN status = 'healthy' THEN 1 ELSE 0 END) as healthy_count, SUM(CASE WHEN status = 'error' THEN 1 ELSE 0 END) as error_count, SUM(CASE WHEN status = 'warning' THEN 1 ELSE 0 END) as warning_count, AVG(response_time) as avg_response_time FROM $table_name WHERE checked_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)", array() ), ARRAY_A ); include ASM_PLUGIN_DIR . 'admin/partials/dashboard-widget.php'; } public function ajax_refresh_status() { // 验证nonce check_ajax_referer('asm_refresh_nonce', 'nonce'); // 验证权限 if (!current_user_can('manage_options')) { wp_die('权限不足'); } // 执行一次API检查 $api_monitor = new API_Monitor(); $api_monitor->check_all_apis(); // 返回更新后的状态 $this->render_dashboard_widget(); wp_die(); } public function enqueue_admin_assets($hook) { if ('index.php' !== $hook) { return; } wp_enqueue_style( 'asm-dashboard-style', ASM_PLUGIN_URL . 'admin/css/admin-style.css', array(), ASM_VERSION ); wp_enqueue_script( 'asm-dashboard-script', ASM_PLUGIN_URL . 'admin/js/admin-script.js', array('jquery'), ASM_VERSION, true ); wp_localize_script('asm-dashboard-script', 'asm_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('asm_refresh_nonce') )); } } 4.2 创建仪表板小工具模板 创建admin/partials/dashboard-widget.php: <div class="asm-dashboard-widget"> <div class="asm-stats-summary"> <div class="asm-stat-card"> <span class="asm-stat-label">监控API总数</span> <span class="asm-stat-value"><?php echo $stats['total_apis'] ?? 0; ?></span> </div> <div class="asm-stat-card asm-stat-healthy"> <span class="asm-stat-label">正常</span> <span class="asm-stat-value"><?php echo $stats['healthy_count'] ?? 0; ?></span> </div> <div class="asm-stat-card asm-stat-warning"> <span class="asm-stat-label">警告</span> <span class="asm-stat-value"><?php echo $stats['warning_count'] ?? 0; ?></span> </div> <div class="asm-stat-card asm-stat-error"> <span class="asm-stat-label">异常</span> <span class="asm-stat-value"><?php echo $stats['error_count'] ?? 0; ?></span> </div> <div class="asm-stat-card"> <span class="asm-stat-label">平均响应时间</span> <span class="asm-stat-value"><?php echo round($stats['avg_response_time'] ?? 0, 2); ?>ms</span> </div> </div> <div class="asm-controls"> <button id="asm-refresh-status" class="button button-primary"> <span class="dashicons dashicons-update"></span> 立即检查 </button> <a href="<?php echo admin_url('admin.php?page=api-status-monitor'); ?>" class="button"> <span class="dashicons dashicons-dashboard"></span> 详细报告 </a> <a href="<?php echo admin_url('admin.php?page=api-status-monitor-settings'); ?>" class="button"> <span class="dashicons dashicons-admin-settings"></span> 设置 </a> </div> <div class="asm-status-list"> <h3>API状态概览</h3> <table class="widefat fixed" cellspacing="0"> <thead> <tr> <th>API名称</th> <th>状态</th> <th>响应时间</th> <th>最后检查</th> <th>操作</th> </tr> </thead> <tbody> <?php if (empty($recent_status)): ?> <tr> <td colspan="5" style="text-align: center;">暂无监控数据</td> </tr> <?php else: ?> <?php foreach ($recent_status as $status): ?> <tr class="asm-status-row asm-status-<?php echo esc_attr($status['status']); ?>"> <td><?php echo esc_html($status['api_name']); ?></td> <td> <span class="asm-status-badge asm-badge-<?php echo esc_attr($status['status']); ?>"> <?php $status_labels = array( 'healthy' => '正常', 'warning' => '警告', 'error' => '异常', 'unknown' => '未知' ); echo $status_labels[$status['status']] ?? $status['status']; ?> </span> </td> <td><?php echo esc_html($status['response_time']); ?>ms</td> <td><?php echo human_time_diff(strtotime($status['checked_at']), current_time('timestamp')) . '前'; ?></td> <td> <button class="button button-small asm-test-api" data-api="<?php echo esc_attr($status['api_name']); ?>"> 测试 </button> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <div class="asm-recent-alerts"> <h3>最近报警</h3> <?php global $wpdb; $alert_table = $wpdb->prefix . 'asm_alerts'; $recent_alerts = $wpdb->get_results( "SELECT * FROM $alert_table WHERE resolved_at IS NULL ORDER BY sent_at DESC LIMIT 5", ARRAY_A );

发表评论

详细教程,为网站打造内嵌的在线简易动画制作与GIF生成工具

详细教程:为WordPress网站打造内嵌在线简易动画制作与GIF生成工具 引言:为什么网站需要内置动画制作工具? 在当今数字时代,视觉内容已成为网站吸引用户、提升参与度的关键因素。动画和GIF图像因其生动、直观的表达方式,在社交媒体、产品演示、教程说明等场景中发挥着不可替代的作用。然而,对于大多数网站运营者来说,创建这些视觉元素通常需要依赖外部工具或专业设计人员,这不仅增加了成本,也降低了内容创作的灵活性。 通过为WordPress网站开发一个内置的简易动画制作与GIF生成工具,您可以: 大幅降低视觉内容创作门槛 提高内容生产效率 保持品牌视觉一致性 增强用户互动体验 减少对外部服务的依赖 本教程将详细指导您如何通过WordPress代码二次开发,实现这一实用功能,让您的网站具备专业级的简易动画制作能力。 第一部分:准备工作与环境配置 1.1 开发环境搭建 在开始开发之前,确保您已具备以下条件: 本地开发环境:推荐使用XAMPP、MAMP或Local by Flywheel搭建本地WordPress环境 代码编辑器:Visual Studio Code、Sublime Text或PHPStorm WordPress版本:5.0或更高版本 基础技能:HTML、CSS、JavaScript、PHP基础知识和WordPress主题开发经验 1.2 创建开发专用子主题 为避免影响主主题功能,建议创建专用子主题: /* Theme Name: Animation Tools Child Theme Template: your-parent-theme-folder-name Version: 1.0 */ // 引入父主题样式表 add_action('wp_enqueue_scripts', 'animation_tools_enqueue_styles'); function animation_tools_enqueue_styles() { wp_enqueue_style('parent-style', get_template_directory_uri() . '/style.css'); wp_enqueue_style('child-style', get_stylesheet_directory_uri() . '/style.css', array('parent-style')); } 1.3 创建必要的目录结构 在子主题目录中创建以下结构: your-child-theme/ ├── animation-tools/ │ ├── css/ │ │ └── animation-editor.css │ ├── js/ │ │ ├── animation-editor.js │ │ └── gif-generator.js │ └── lib/ │ └── gif.js ├── templates/ │ └── animation-tool-page.php └── functions.php 第二部分:构建动画编辑器界面 2.1 创建动画工具管理页面 首先,在WordPress后台添加一个管理页面: // 在functions.php中添加 add_action('admin_menu', 'animation_tools_admin_menu'); function animation_tools_admin_menu() { add_menu_page( '动画制作工具', '动画工具', 'manage_options', 'animation-tools', 'animation_tools_admin_page', 'dashicons-video-alt3', 30 ); } function animation_tools_admin_page() { ?> <div class="wrap"> <h1>网站动画制作工具</h1> <div id="animation-tools-admin"> <p>从这里可以访问网站内置的动画编辑器。</p> <a href="<?php echo home_url('/animation-editor/'); ?>" class="button button-primary" target="_blank">打开动画编辑器</a> </div> </div> <?php } 2.2 创建前端动画编辑器页面模板 创建自定义页面模板,用于前端动画编辑: <?php /* Template Name: 动画编辑器 */ get_header(); ?> <div class="animation-editor-container"> <div class="editor-header"> <h1>简易动画制作工具</h1> <div class="editor-tabs"> <button class="tab-btn active" data-tab="canvas-editor">画布编辑</button> <button class="tab-btn" data-tab="timeline">时间轴</button> <button class="tab-btn" data-tab="export">导出选项</button> </div> </div> <div class="editor-main"> <div class="tool-panel"> <div class="tool-section"> <h3>绘图工具</h3> <div class="tool-buttons"> <button class="tool-btn active" data-tool="select">选择</button> <button class="tool-btn" data-tool="brush">画笔</button> <button class="tool-btn" data-tool="shape">形状</button> <button class="tool-btn" data-tool="text">文字</button> <button class="tool-btn" data-tool="eraser">橡皮擦</button> </div> </div> <div class="tool-section"> <h3>属性设置</h3> <div class="property-controls"> <div class="control-group"> <label>画笔大小:</label> <input type="range" id="brush-size" min="1" max="50" value="5"> <span id="brush-size-value">5px</span> </div> <div class="control-group"> <label>颜色:</label> <input type="color" id="brush-color" value="#ff0000"> </div> <div class="control-group"> <label>不透明度:</label> <input type="range" id="brush-opacity" min="0" max="100" value="100"> <span id="opacity-value">100%</span> </div> </div> </div> </div> <div class="canvas-area"> <div class="canvas-container"> <canvas id="animation-canvas" width="800" height="600"></canvas> <div class="canvas-overlay"> <div class="canvas-grid"></div> </div> </div> <div class="canvas-controls"> <button id="clear-canvas">清空画布</button> <button id="undo-action">撤销</button> <button id="redo-action">重做</button> <button id="add-frame">添加帧</button> </div> </div> <div class="timeline-panel"> <div class="frames-container"> <div class="frames-list" id="frames-list"> <!-- 帧缩略图将通过JS动态生成 --> </div> <div class="timeline-controls"> <button id="play-animation">播放</button> <input type="range" id="frame-speed" min="1" max="30" value="12"> <span>帧速率: <span id="fps-value">12</span> fps</span> </div> </div> </div> </div> <div class="export-panel"> <h3>导出选项</h3> <div class="export-options"> <div class="export-format"> <label><input type="radio" name="export-format" value="gif" checked> GIF动画</label> <label><input type="radio" name="export-format" value="apng"> APNG</label> <label><input type="radio" name="export-format" value="spritesheet"> 精灵图</label> </div> <div class="export-settings"> <div class="setting-group"> <label>循环次数:</label> <select id="loop-count"> <option value="0">无限循环</option> <option value="1">1次</option> <option value="3">3次</option> <option value="5">5次</option> </select> </div> <div class="setting-group"> <label>质量:</label> <input type="range" id="export-quality" min="1" max="100" value="80"> <span id="quality-value">80%</span> </div> </div> <div class="export-actions"> <button id="preview-export">预览</button> <button id="generate-export" class="button-primary">生成并下载</button> <button id="save-to-media">保存到媒体库</button> </div> </div> </div> </div> <?php get_footer(); ?> 第三部分:实现动画编辑器核心功能 3.1 动画编辑器JavaScript核心 创建动画编辑器的主要JavaScript功能: // animation-editor.js document.addEventListener('DOMContentLoaded', function() { // 初始化变量 const canvas = document.getElementById('animation-canvas'); const ctx = canvas.getContext('2d'); const framesList = document.getElementById('frames-list'); let currentTool = 'brush'; let brushSize = 5; let brushColor = '#ff0000'; let brushOpacity = 1.0; let frames = []; let currentFrameIndex = 0; let isDrawing = false; let lastX = 0; let lastY = 0; let undoStack = []; let redoStack = []; // 初始化第一帧 function initializeFirstFrame() { const frameData = { id: Date.now(), imageData: null, thumbnail: null, delay: 100 // 默认每帧延迟100ms }; frames.push(frameData); updateFrameDisplay(); saveFrameState(); } // 保存当前帧状态 function saveFrameState() { const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); frames[currentFrameIndex].imageData = imageData; // 创建缩略图 const thumbnailCanvas = document.createElement('canvas'); thumbnailCanvas.width = 80; thumbnailCanvas.height = 60; const thumbnailCtx = thumbnailCanvas.getContext('2d'); thumbnailCtx.drawImage(canvas, 0, 0, 80, 60); frames[currentFrameIndex].thumbnail = thumbnailCanvas.toDataURL(); updateFrameDisplay(); } // 更新帧显示 function updateFrameDisplay() { framesList.innerHTML = ''; frames.forEach((frame, index) => { const frameElement = document.createElement('div'); frameElement.className = `frame-thumb ${index === currentFrameIndex ? 'active' : ''}`; frameElement.innerHTML = ` <div class="frame-number">${index + 1}</div> <img src="${frame.thumbnail || ''}" alt="帧 ${index + 1}"> <div class="frame-actions"> <button class="frame-delete" data-index="${index}">删除</button> </div> `; frameElement.addEventListener('click', () => { switchToFrame(index); }); framesList.appendChild(frameElement); }); } // 切换到指定帧 function switchToFrame(index) { saveFrameState(); // 保存当前帧 currentFrameIndex = index; // 恢复帧内容 const frame = frames[index]; if (frame.imageData) { ctx.putImageData(frame.imageData, 0, 0); } else { ctx.clearRect(0, 0, canvas.width, canvas.height); } updateFrameDisplay(); } // 添加新帧 document.getElementById('add-frame').addEventListener('click', function() { saveFrameState(); const newFrame = { id: Date.now(), imageData: null, thumbnail: null, delay: 100 }; frames.splice(currentFrameIndex + 1, 0, newFrame); currentFrameIndex++; // 清空画布 ctx.clearRect(0, 0, canvas.width, canvas.height); updateFrameDisplay(); saveFrameState(); }); // 绘图功能 function startDrawing(e) { if (currentTool === 'select') return; isDrawing = true; [lastX, lastY] = getMousePos(canvas, e); // 开始新路径 if (currentTool === 'brush' || currentTool === 'eraser') { ctx.beginPath(); ctx.moveTo(lastX, lastY); } } function draw(e) { if (!isDrawing) return; const [x, y] = getMousePos(canvas, e); switch(currentTool) { case 'brush': ctx.lineTo(x, y); ctx.strokeStyle = brushColor; ctx.lineWidth = brushSize; ctx.lineCap = 'round'; ctx.lineJoin = 'round'; ctx.globalAlpha = brushOpacity; ctx.stroke(); break; case 'eraser': ctx.lineTo(x, y); ctx.strokeStyle = '#ffffff'; ctx.lineWidth = brushSize; ctx.lineCap = 'round'; ctx.stroke(); break; case 'shape': // 绘制形状逻辑 break; case 'text': // 文本输入逻辑 break; } [lastX, lastY] = [x, y]; } function stopDrawing() { if (!isDrawing) return; isDrawing = false; ctx.closePath(); saveFrameState(); } // 获取鼠标位置 function getMousePos(canvas, evt) { const rect = canvas.getBoundingClientRect(); return [ evt.clientX - rect.left, evt.clientY - rect.top ]; } // 工具选择 document.querySelectorAll('.tool-btn').forEach(btn => { btn.addEventListener('click', function() { document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active')); this.classList.add('active'); currentTool = this.dataset.tool; }); }); // 属性控制 document.getElementById('brush-size').addEventListener('input', function() { brushSize = this.value; document.getElementById('brush-size-value').textContent = brushSize + 'px'; }); document.getElementById('brush-color').addEventListener('input', function() { brushColor = this.value; }); document.getElementById('brush-opacity').addEventListener('input', function() { brushOpacity = this.value / 100; document.getElementById('opacity-value').textContent = this.value + '%'; }); // 画布事件监听 canvas.addEventListener('mousedown', startDrawing); canvas.addEventListener('mousemove', draw); canvas.addEventListener('mouseup', stopDrawing); canvas.addEventListener('mouseout', stopDrawing); // 清空画布 document.getElementById('clear-canvas').addEventListener('click', function() { if (confirm('确定要清空当前帧吗?')) { ctx.clearRect(0, 0, canvas.width, canvas.height); saveFrameState(); } }); // 初始化 initializeFirstFrame(); }); 3.2 添加动画播放功能 // 在animation-editor.js中添加动画播放功能 let animationInterval = null; let isPlaying = false; function playAnimation() { if (frames.length < 2) { alert('至少需要两帧才能播放动画'); return; } if (isPlaying) { stopAnimation(); return; } isPlaying = true; document.getElementById('play-animation').textContent = '停止'; let frameIndex = 0; const fps = parseInt(document.getElementById('frame-speed').value); const delay = 1000 / fps; animationInterval = setInterval(() => { switchToFrame(frameIndex); frameIndex = (frameIndex + 1) % frames.length; }, delay); } function stopAnimation() { isPlaying = false; document.getElementById('play-animation').textContent = '播放'; clearInterval(animationInterval); } // 播放按钮事件 document.getElementById('play-animation').addEventListener('click', playAnimation); // 帧速率控制 document.getElementById('frame-speed').addEventListener('input', function() { document.getElementById('fps-value').textContent = this.value; // 如果正在播放,重新开始以应用新速度 if (isPlaying) { stopAnimation(); playAnimation(); } }); 第四部分:实现GIF生成与导出功能 4.1 集成GIF.js库 首先,下载并引入GIF.js库: // 在functions.php中注册GIF.js add_action('wp_enqueue_scripts', 'enqueue_animation_tools_scripts'); function enqueue_animation_tools_scripts() { if (is_page_template('animation-editor.php')) { wp_enqueue_script('gif-js', get_stylesheet_directory_uri() . '/animation-tools/lib/gif.js', array(), '1.0.0', true); wp_enqueue_script('animation-editor', get_stylesheet_directory_uri() . '/animation-tools/js/animation-editor.js', array('jquery', 'gif-js'), '1.0.0', true); wp_enqueue_script('gif-generator', get_stylesheet_directory_uri() . '/animation-tools/js/gif-generator.js', array('animation-editor'), '1.0.0', true); wp_enqueue_style('animation-editor-style', get_stylesheet_directory_uri() . '/animation-tools/css/animation-editor.css', array(), '1.0.0'); } } 4.2 创建GIF生成器 // gif-generator.js class GIFGenerator { constructor(options = {}) { this.options = { quality: options.quality || 10, width: options.width || 800, height: options.height || 600, workerScript: options.workerScript || '/wp-content/themes/your-child-theme/animation-tools/lib/gif.worker.js' }; this.gif = new GIF({ workers: 2, quality: this.options.quality, width: this.options.width, height: this.options.height, workerScript: this.options.workerScript }); this.frames = []; this.onProgress = null; this.onFinished = null; } addFrame(canvas, delay) { this.gif.addFrame(canvas, { delay: delay || 100, copy: true }); } setOptions(options) { if (options.quality) this.gif.setOption('quality', options.quality); if (options.repeat !== undefined) this.gif.setOption('repeat', options.repeat); } generate() { return new Promise((resolve, reject) => { this.gif.on('progress', (progress) => { if (this.onProgress) { this.onProgress(progress); } }); this.gif.on('finished', (blob) => { if (this.onFinished) { this.onFinished(blob); } resolve(blob); }); this.gif.render(); }); } abort() { this.gif.abort(); } } // 导出功能实现document.addEventListener('DOMContentLoaded', function() { const generateExportBtn = document.getElementById('generate-export'); const previewExportBtn = document.getElementById('preview-export'); const saveToMediaBtn = document.getElementById('save-to-media'); const exportQuality = document.getElementById('export-quality'); const loopCount = document.getElementById('loop-count'); let currentGIFBlob = null; // 更新质量显示 exportQuality.addEventListener('input', function() { document.getElementById('quality-value').textContent = this.value + '%'; }); // 生成GIF async function generateGIF() { if (frames.length === 0) { alert('请先创建至少一帧动画'); return; } generateExportBtn.disabled = true; generateExportBtn.textContent = '生成中...'; try { // 创建临时画布用于生成GIF const tempCanvas = document.createElement('canvas'); tempCanvas.width = canvas.width; tempCanvas.height = canvas.height; const tempCtx = tempCanvas.getContext('2d'); // 初始化GIF生成器 const gifGenerator = new GIFGenerator({ quality: parseInt(exportQuality.value), width: canvas.width, height: canvas.height }); // 设置循环次数 const repeat = parseInt(loopCount.value); gifGenerator.setOptions({ repeat: repeat === 0 ? 0 : repeat - 1 }); // 添加进度监听 gifGenerator.onProgress = (progress) => { console.log(`生成进度: ${Math.round(progress * 100)}%`); }; // 添加所有帧 for (let i = 0; i < frames.length; i++) { const frame = frames[i]; if (frame.imageData) { tempCtx.putImageData(frame.imageData, 0, 0); gifGenerator.addFrame(tempCanvas, frame.delay); } } // 生成GIF currentGIFBlob = await gifGenerator.generate(); // 创建下载链接 const url = URL.createObjectURL(currentGIFBlob); const a = document.createElement('a'); a.href = url; a.download = `animation-${Date.now()}.gif`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); alert('GIF生成完成并已开始下载!'); } catch (error) { console.error('GIF生成失败:', error); alert('GIF生成失败,请重试'); } finally { generateExportBtn.disabled = false; generateExportBtn.textContent = '生成并下载'; } } // 预览GIF async function previewGIF() { if (frames.length === 0) { alert('请先创建至少一帧动画'); return; } previewExportBtn.disabled = true; previewExportBtn.textContent = '预览生成中...'; try { // 创建临时画布 const tempCanvas = document.createElement('canvas'); tempCanvas.width = 400; // 预览尺寸较小 tempCanvas.height = 300; const tempCtx = tempCanvas.getContext('2d'); // 创建预览GIF生成器 const previewGenerator = new GIFGenerator({ quality: 20, // 预览质量较低 width: 400, height: 300 }); // 添加所有帧(缩放) for (let i = 0; i < frames.length; i++) { const frame = frames[i]; if (frame.imageData) { // 创建临时画布绘制并缩放 const frameCanvas = document.createElement('canvas'); frameCanvas.width = canvas.width; frameCanvas.height = canvas.height; const frameCtx = frameCanvas.getContext('2d'); frameCtx.putImageData(frame.imageData, 0, 0); // 缩放到预览尺寸 tempCtx.clearRect(0, 0, 400, 300); tempCtx.drawImage(frameCanvas, 0, 0, 400, 300); previewGenerator.addFrame(tempCanvas, frame.delay); } } // 生成预览GIF const previewBlob = await previewGenerator.generate(); const previewUrl = URL.createObjectURL(previewBlob); // 显示预览 showPreviewModal(previewUrl); } catch (error) { console.error('预览生成失败:', error); alert('预览生成失败'); } finally { previewExportBtn.disabled = false; previewExportBtn.textContent = '预览'; } } // 显示预览模态框 function showPreviewModal(gifUrl) { // 移除现有模态框 const existingModal = document.querySelector('.preview-modal'); if (existingModal) { existingModal.remove(); } // 创建模态框 const modal = document.createElement('div'); modal.className = 'preview-modal'; modal.innerHTML = ` <div class="preview-modal-content"> <div class="preview-modal-header"> <h3>GIF预览</h3> <button class="close-preview">&times;</button> </div> <div class="preview-modal-body"> <img src="${gifUrl}" alt="GIF预览" class="preview-gif"> <div class="preview-info"> <p>尺寸: 400x300 (预览)</p> <p>帧数: ${frames.length}</p> <p>文件大小: ${Math.round(currentGIFBlob?.size / 1024) || '未知'} KB</p> </div> </div> <div class="preview-modal-footer"> <button id="download-preview">下载预览</button> <button id="use-full-quality">使用高质量生成</button> </div> </div> `; document.body.appendChild(modal); // 关闭按钮事件 modal.querySelector('.close-preview').addEventListener('click', function() { modal.remove(); URL.revokeObjectURL(gifUrl); }); // 点击外部关闭 modal.addEventListener('click', function(e) { if (e.target === modal) { modal.remove(); URL.revokeObjectURL(gifUrl); } }); // 下载预览 modal.querySelector('#download-preview').addEventListener('click', function() { const a = document.createElement('a'); a.href = gifUrl; a.download = `preview-${Date.now()}.gif`; a.click(); }); // 使用高质量生成 modal.querySelector('#use-full-quality').addEventListener('click', function() { modal.remove(); URL.revokeObjectURL(gifUrl); generateGIF(); }); } // 保存到媒体库 async function saveToMediaLibrary() { if (!currentGIFBlob) { alert('请先生成GIF'); return; } saveToMediaBtn.disabled = true; saveToMediaBtn.textContent = '上传中...'; try { // 创建FormData const formData = new FormData(); formData.append('action', 'save_animation_to_media'); formData.append('security', animationToolsAjax.nonce); formData.append('gif_data', currentGIFBlob, `animation-${Date.now()}.gif`); formData.append('title', `动画作品 ${new Date().toLocaleDateString()}`); // 发送AJAX请求 const response = await fetch(animationToolsAjax.ajax_url, { method: 'POST', body: formData }); const result = await response.json(); if (result.success) { alert(`动画已保存到媒体库!n文件ID: ${result.data.attachment_id}n查看: ${result.data.edit_link}`); } else { throw new Error(result.data || '上传失败'); } } catch (error) { console.error('保存到媒体库失败:', error); alert('保存失败: ' + error.message); } finally { saveToMediaBtn.disabled = false; saveToMediaBtn.textContent = '保存到媒体库'; } } // 绑定事件 generateExportBtn.addEventListener('click', generateGIF); previewExportBtn.addEventListener('click', previewGIF); saveToMediaBtn.addEventListener('click', saveToMediaLibrary); }); ## 第五部分:后端处理与媒体库集成 ### 5.1 创建AJAX处理函数 // 在functions.php中添加AJAX处理add_action('wp_ajax_save_animation_to_media', 'handle_save_animation_to_media');add_action('wp_ajax_nopriv_save_animation_to_media', 'handle_save_animation_to_media'); function handle_save_animation_to_media() { // 安全检查 check_ajax_referer('animation_tools_nonce', 'security'); // 验证用户权限 if (!current_user_can('upload_files')) { wp_die('权限不足'); } // 检查文件上传 if (!isset($_FILES['gif_data']) || $_FILES['gif_data']['error'] !== UPLOAD_ERR_OK) { wp_send_json_error('文件上传失败'); } $file = $_FILES['gif_data']; // 验证文件类型 $allowed_types = array('image/gif'); $filetype = wp_check_filetype(basename($file['name'])); if (!in_array($filetype['type'], $allowed_types)) { wp_send_json_error('仅支持GIF格式'); } // 准备上传文件 $upload_overrides = array( 'test_form' => false, 'mimes' => array('gif' => 'image/gif') ); // 上传文件 $upload = wp_handle_upload($file, $upload_overrides); if (isset($upload['error'])) { wp_send_json_error($upload['error']); } // 准备附件数据 $attachment = array( 'post_mime_type' => $filetype['type'], 'post_title' => sanitize_text_field($_POST['title'] ?? '动画作品'), 'post_content' => '', 'post_status' => 'inherit', 'post_author' => get_current_user_id() ); // 插入附件到媒体库 $attachment_id = wp_insert_attachment($attachment, $upload['file']); if (is_wp_error($attachment_id)) { wp_send_json_error('插入媒体库失败'); } // 生成附件元数据 require_once(ABSPATH . 'wp-admin/includes/image.php'); $attachment_data = wp_generate_attachment_metadata($attachment_id, $upload['file']); wp_update_attachment_metadata($attachment_id, $attachment_data); // 返回成功响应 wp_send_json_success(array( 'attachment_id' => $attachment_id, 'url' => wp_get_attachment_url($attachment_id), 'edit_link' => admin_url('post.php?post=' . $attachment_id . '&action=edit') )); } // 注册AJAX脚本add_action('wp_enqueue_scripts', 'register_animation_tools_ajax');function register_animation_tools_ajax() { if (is_page_template('animation-editor.php')) { wp_localize_script('gif-generator', 'animationToolsAjax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('animation_tools_nonce') )); } } ### 5.2 创建短代码功能 // 添加短代码,允许在文章/页面中插入动画工具add_shortcode('animation_tool', 'animation_tool_shortcode');function animation_tool_shortcode($atts) { $atts = shortcode_atts(array( 'width' => '100%', 'height' => '600px', 'mode' => 'editor' // editor, preview, simple ), $atts); ob_start(); if ($atts['mode'] === 'simple') { // 简化版动画工具 ?> <div class="simple-animation-tool" style="width: <?php echo esc_attr($atts['width']); ?>; height: <?php echo esc_attr($atts['height']); ?>;"> <div class="simple-tool-header"> <h4>简易动画制作</h4> </div> <div class="simple-canvas-container"> <canvas class="simple-animation-canvas"></canvas> </div> <div class="simple-controls"> <button class="simple-draw-btn">绘制</button> <button class="simple-clear-btn">清空</button> <button class="simple-save-btn">保存为GIF</button> </div> </div> <?php } else { // 完整版工具链接 $editor_url = home_url('/animation-editor/'); ?> <div class="animation-tool-link"> <h3>动画制作工具</h3> <p>使用我们的内置工具创建自定义动画和GIF</p> <a href="<?php echo esc_url($editor_url); ?>" class="button button-primary"> 打开动画编辑器 </a> </div> <?php } return ob_get_clean(); } ## 第六部分:样式设计与优化 ### 6.1 基础样式设计 / animation-editor.css /.animation-editor-container { max-width: 1400px; margin: 20px auto; background: #fff; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; } .editor-header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px; } .editor-header h1 { margin: 0 0 20px 0; font-size: 28px; } .editor-tabs { display: flex; gap: 10px; } .tab-btn { background: rgba(255,255,255,0.2); border: none; color: white; padding: 10px 20px; border-radius: 4px; cursor: pointer; transition: background 0.3s; } .tab-btn.active { background: rgba(255,255,255,0.4); } .tab-btn:hover { background: rgba(255,255,255,0.3); } .editor-main { display: grid; grid-template-columns: 250px 1fr 300px; min-height: 600px; } .tool-panel { background: #f8f9fa; border-right: 1px solid #dee2e6; padding: 20px; } .tool-section { margin-bottom: 30px; } .tool-section h3 { margin-top: 0; color: #495057; font-size: 16px; border-bottom: 2px solid #667eea; padding-bottom: 5px; } .tool-buttons { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; } .tool-btn { background: white; border: 2px solid #dee2e6; padding: 10px; border-radius: 4px; cursor: pointer; transition: all 0.3s; } .tool-btn.active { border-color: #667eea; background: #667eea; color: white; } .tool-btn:hover { border-color: #495057; } .property-controls { display: flex; flex-direction: column; gap: 15px; } .control-group { display: flex; flex-direction: column; gap: 5px; } .control-group label { font-size: 14px; color: #6c757d; } .control-group input[type="range"] { width: 100%; } .canvas-area { padding: 20px; display: flex; flex-direction: column; align-items: center; justify-content: center; } .canvas-container { position: relative; border: 2px dashed #dee2e6; border-radius: 4px; margin-bottom: 20px; } animation-canvas { display: block; background: white; cursor: crosshair; } .canvas-overlay { position: absolute; top: 0; left: 0; right:

发表评论

WordPress高级教程,开发集成在线考试测评与自动组卷评分系统

WordPress高级教程:开发集成在线考试测评与自动组卷评分系统 引言:WordPress作为企业级应用开发平台的潜力 WordPress作为全球最流行的内容管理系统,早已超越了简单的博客平台范畴。凭借其强大的插件架构、丰富的API接口和庞大的开发者社区,WordPress已经演变为一个功能完善的企业级应用开发平台。在许多人的印象中,WordPress可能仍是一个"博客工具",但实际上,全球超过43%的网站都运行在WordPress上,其中不乏复杂的企业级应用。 本教程将深入探讨如何通过WordPress的代码二次开发,实现一个功能完整的在线考试测评与自动组卷评分系统。这个系统不仅能够满足教育机构、企业培训部门的在线测评需求,还能展示WordPress作为应用开发平台的强大扩展能力。我们将从系统设计、数据库结构、核心功能实现到前端展示,全方位解析开发过程。 第一章:系统架构设计与技术选型 1.1 需求分析与功能规划 在线考试测评系统需要满足以下核心功能: 试题库管理(支持多种题型:单选、多选、判断、填空、简答等) 智能组卷策略(随机组卷、按知识点组卷、难度系数组卷) 在线考试与计时功能 自动评分与人工阅卷结合 成绩统计与分析报表 用户权限与考试管理 1.2 WordPress作为开发平台的优势 选择WordPress作为开发基础有以下几个显著优势: 用户管理系统完善:WordPress自带的用户角色和权限管理系统可以快速扩展为考试系统的用户体系 主题模板机制:利用WordPress主题系统可以快速构建一致的前端界面 插件架构:通过自定义插件的方式实现功能模块,便于维护和升级 丰富的API:REST API、AJAX、短代码等机制便于前后端交互 安全机制成熟:WordPress经过多年发展,拥有完善的安全防护机制 SEO友好:天生具备良好的搜索引擎优化基础 1.3 技术栈选择 后端:PHP 7.4+,WordPress 5.6+,MySQL 5.6+ 前端:HTML5,CSS3,JavaScript (ES6+),jQuery(兼容性考虑) 关键WordPress特性:自定义文章类型(CPT)、自定义分类法、元数据、短代码、REST API 辅助工具:Chart.js(数据可视化),Select2(下拉框增强),Bootstrap 5(响应式布局) 第二章:数据库设计与数据模型 2.1 核心数据表设计 虽然WordPress本身使用wp_posts和wp_postmeta作为主要数据存储结构,但对于复杂的考试系统,我们需要创建专门的数据表来保证性能和数据一致性。 -- 试题表扩展(基于wp_posts的自定义文章类型) -- 使用post_type = 'exam_question'标识试题 -- 自定义试题元数据表 CREATE TABLE wp_exam_question_meta ( meta_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, question_id bigint(20) unsigned NOT NULL, meta_key varchar(255) DEFAULT NULL, meta_value longtext, PRIMARY KEY (meta_id), KEY question_id (question_id), KEY meta_key (meta_key(191)) ); -- 试卷表 CREATE TABLE wp_exam_papers ( paper_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, paper_title varchar(255) NOT NULL, paper_description text, paper_type varchar(50) DEFAULT 'random', -- random/fixed/manual total_score int(11) DEFAULT 100, time_limit int(11) DEFAULT 60, -- 分钟 created_by bigint(20) unsigned NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (paper_id) ); -- 试卷-试题关联表 CREATE TABLE wp_exam_paper_questions ( relation_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, paper_id bigint(20) unsigned NOT NULL, question_id bigint(20) unsigned NOT NULL, question_order int(11) DEFAULT 0, question_score decimal(5,2) DEFAULT 0, PRIMARY KEY (relation_id), KEY paper_id (paper_id), KEY question_id (question_id) ); -- 考试记录表 CREATE TABLE wp_exam_records ( record_id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NOT NULL, paper_id bigint(20) unsigned NOT NULL, start_time datetime DEFAULT NULL, end_time datetime DEFAULT NULL, submit_time datetime DEFAULT NULL, total_score decimal(5,2) DEFAULT 0, auto_score decimal(5,2) DEFAULT 0, manual_score decimal(5,2) DEFAULT 0, status varchar(20) DEFAULT 'in_progress', -- in_progress/completed/reviewing ip_address varchar(45) DEFAULT NULL, user_answers longtext, -- 存储用户答案的JSON PRIMARY KEY (record_id), KEY user_id (user_id), KEY paper_id (paper_id), KEY status (status) ); 2.2 利用WordPress自定义文章类型管理试题 试题作为系统的核心数据,我们可以使用WordPress的自定义文章类型(CPT)来管理: // 注册试题自定义文章类型 function register_exam_question_post_type() { $labels = array( 'name' => '试题', 'singular_name' => '试题', 'menu_name' => '试题库', 'add_new' => '添加试题', 'add_new_item' => '添加新试题', 'edit_item' => '编辑试题', 'new_item' => '新试题', 'view_item' => '查看试题', 'search_items' => '搜索试题', 'not_found' => '未找到试题', 'not_found_in_trash' => '回收站中无试题' ); $args = array( 'labels' => $labels, 'public' => false, 'publicly_queryable' => false, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => false, 'capability_type' => 'post', 'has_archive' => false, 'hierarchical' => false, 'menu_position' => 30, 'menu_icon' => 'dashicons-welcome-learn-more', 'supports' => array('title', 'editor', 'author'), 'show_in_rest' => true, // 启用REST API支持 ); register_post_type('exam_question', $args); } add_action('init', 'register_exam_question_post_type'); 2.3 试题分类与标签系统 利用WordPress的自定义分类法为试题添加分类和标签: // 注册试题分类法 function register_exam_taxonomies() { // 试题分类(如:数学、语文、英语) register_taxonomy( 'question_category', 'exam_question', array( 'labels' => array( 'name' => '试题分类', 'singular_name' => '试题分类', 'search_items' => '搜索分类', 'all_items' => '全部分类', 'edit_item' => '编辑分类', 'update_item' => '更新分类', 'add_new_item' => '添加新分类', 'new_item_name' => '新分类名称', 'menu_name' => '试题分类' ), 'hierarchical' => true, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => false, 'show_in_rest' => true, ) ); // 试题难度标签 register_taxonomy( 'question_difficulty', 'exam_question', array( 'labels' => array( 'name' => '难度级别', 'singular_name' => '难度级别', 'search_items' => '搜索难度', 'all_items' => '全部难度', 'edit_item' => '编辑难度', 'update_item' => '更新难度', 'add_new_item' => '添加新难度', 'new_item_name' => '新难度名称', 'menu_name' => '难度级别' ), 'hierarchical' => false, 'show_ui' => true, 'show_admin_column' => true, 'query_var' => true, 'rewrite' => false, 'show_in_rest' => true, ) ); } add_action('init', 'register_exam_taxonomies'); 第三章:试题管理与组卷策略实现 3.1 试题录入与管理界面 在WordPress后台创建试题管理界面,支持多种题型: // 为试题添加元数据框 function add_exam_question_meta_boxes() { add_meta_box( 'question-type-meta', '试题类型与选项', 'render_question_type_meta_box', 'exam_question', 'normal', 'high' ); add_meta_box( 'question-answer-meta', '正确答案与解析', 'render_question_answer_meta_box', 'exam_question', 'normal', 'high' ); } add_action('add_meta_boxes', 'add_exam_question_meta_boxes'); // 渲染试题类型元数据框 function render_question_type_meta_box($post) { wp_nonce_field('save_question_meta', 'question_meta_nonce'); $question_type = get_post_meta($post->ID, '_question_type', true); $question_options = get_post_meta($post->ID, '_question_options', true); // 解析选项(如果是选择题) $options = !empty($question_options) ? json_decode($question_options, true) : array(); ?> <div class="question-meta-container"> <div class="form-group"> <label for="question_type">试题类型:</label> <select name="question_type" id="question_type" class="widefat"> <option value="single_choice" <?php selected($question_type, 'single_choice'); ?>>单选题</option> <option value="multiple_choice" <?php selected($question_type, 'multiple_choice'); ?>>多选题</option> <option value="true_false" <?php selected($question_type, 'true_false'); ?>>判断题</option> <option value="fill_blank" <?php selected($question_type, 'fill_blank'); ?>>填空题</option> <option value="short_answer" <?php selected($question_type, 'short_answer'); ?>>简答题</option> <option value="essay" <?php selected($question_type, 'essay'); ?>>论述题</option> </select> </div> <div id="choice-options-container" style="display: <?php echo in_array($question_type, array('single_choice', 'multiple_choice')) ? 'block' : 'none'; ?>;"> <h4>选项设置</h4> <div id="choice-options-list"> <?php if (!empty($options)): ?> <?php foreach ($options as $key => $option): ?> <div class="option-item"> <input type="text" name="question_options[]" value="<?php echo esc_attr($option); ?>" class="widefat" placeholder="选项内容"> <button type="button" class="button remove-option">删除</button> </div> <?php endforeach; ?> <?php else: ?> <div class="option-item"> <input type="text" name="question_options[]" value="" class="widefat" placeholder="选项内容"> <button type="button" class="button remove-option">删除</button> </div> <div class="option-item"> <input type="text" name="question_options[]" value="" class="widefat" placeholder="选项内容"> <button type="button" class="button remove-option">删除</button> </div> <?php endif; ?> </div> <button type="button" id="add-option" class="button">添加选项</button> </div> <div class="form-group"> <label for="question_score">默认分值:</label> <input type="number" name="question_score" id="question_score" value="<?php echo esc_attr(get_post_meta($post->ID, '_question_score', true) ?: 5); ?>" min="0" step="0.5" class="small-text"> 分 </div> </div> <script> jQuery(document).ready(function($) { // 切换试题类型时显示/隐藏选项设置 $('#question_type').change(function() { var type = $(this).val(); if (type === 'single_choice' || type === 'multiple_choice') { $('#choice-options-container').show(); } else { $('#choice-options-container').hide(); } }); // 添加选项 $('#add-option').click(function() { var newOption = '<div class="option-item">' + '<input type="text" name="question_options[]" value="" class="widefat" placeholder="选项内容">' + '<button type="button" class="button remove-option">删除</button>' + '</div>'; $('#choice-options-list').append(newOption); }); // 删除选项 $(document).on('click', '.remove-option', function() { if ($('#choice-options-list .option-item').length > 1) { $(this).closest('.option-item').remove(); } else { alert('至少需要保留一个选项'); } }); }); </script> <?php } 3.2 智能组卷算法实现 智能组卷是考试系统的核心功能,这里实现一个基于分类和难度的随机组卷算法: class ExamPaperGenerator { /** * 生成随机试卷 * * @param array $params 组卷参数 * @return int|false 试卷ID或false */ public static function generateRandomPaper($params) { global $wpdb; $defaults = array( 'title' => '随机试卷', 'description' => '', 'total_questions' => 50, 'categories' => array(), 'difficulties' => array('easy', 'medium', 'hard'), 'difficulty_distribution' => array('easy' => 0.3, 'medium' => 0.5, 'hard' => 0.2), 'question_types' => array('single_choice', 'multiple_choice', 'true_false'), 'time_limit' => 60, 'created_by' => get_current_user_id(), ); $params = wp_parse_args($params, $defaults); // 开始事务 $wpdb->query('START TRANSACTION'); try { // 创建试卷记录 $paper_data = array( 'paper_title' => sanitize_text_field($params['title']), 'paper_description' => sanitize_textarea_field($params['description']), 'paper_type' => 'random', 'total_score' => $params['total_questions'] * 2, // 假设每题2分 'time_limit' => intval($params['time_limit']), 'created_by' => intval($params['created_by']), 'created_at' => current_time('mysql'), ); $wpdb->insert($wpdb->prefix . 'exam_papers', $paper_data); $paper_id = $wpdb->insert_id; if (!$paper_id) { throw new Exception('创建试卷失败'); } // 获取符合条件的试题 $questions = self::getQuestionsByCriteria($params); if (count($questions) < $params['total_questions']) { throw new Exception('试题数量不足,无法生成试卷'); } // 随机选择试题 shuffle($questions); $selected_questions = array_slice($questions, 0, $params['total_questions']); // 关联试题到试卷 $order = 1; foreach ($selected_questions as $question) { $relation_data = array( 'paper_id' => $paper_id, 'question_id' => $question->ID, 'question_order' => $order, 'question_score' => get_post_meta($question->ID, '_question_score', true) ?: 2, ); $wpdb->insert($wpdb->prefix . 'exam_paper_questions', $relation_data); $order++; } // 提交事务 $wpdb->query('COMMIT'); return $paper_id; } catch (Exception $e) { // 回滚事务 $wpdb->query('ROLLBACK'); error_log('生成试卷失败: ' . $e->getMessage()); return false; } } /** * 根据条件获取试题 */ private static function getQuestionsByCriteria($params) { $args = array( 'post_type' => 'exam_question', 'post_status' => 'publish', 'posts_per_page' => -1, 'fields' => 'ids', ); // 添加分类筛选 if (!empty($params['categories'])) { $args['tax_query'][] = array( 'taxonomy' => 'question_category', 'field' => 'term_id', 'terms' => $params['categories'], 'operator' => 'IN', ); } // 添加难度筛选 if (!empty($params['difficulties'])) { $args['tax_query'][] = array( 'taxonomy' => 'question_difficulty', 'field' => 'slug', 'terms' => $params['difficulties'], 'operator' => 'IN', ); } // 添加题型筛选 if (!empty($params['question_types'])) { $args['meta_query'][] = array( 'key' => '_question_type', 'value' => $params['question_types'], 'compare' => 'IN', ); } // 确保tax_query关系正确 if (isset($args['tax_query']) && count($args['tax_query']) > 1) { $args['tax_query']['relation'] = 'AND'; } $query = new WP_Query($args); return $query->posts; } /** * 按知识点比例组卷 */ public static function generatePaperByKnowledgePoints($params) { // 实现按知识点比例分配试题的逻辑 // 这里可以扩展为更复杂的组卷策略 } } ### 3.3 试卷管理后台界面 创建试卷管理后台界面,支持手动组卷和自动组卷: // 添加试卷管理菜单function add_exam_paper_admin_menu() { add_menu_page( '试卷管理', '考试系统', 'manage_options', 'exam-system', 'render_exam_dashboard', 'dashicons-clipboard', 30 ); add_submenu_page( 'exam-system', '试卷管理', '试卷管理', 'manage_options', 'exam-papers', 'render_exam_papers_page' ); add_submenu_page( 'exam-system', '组卷工具', '智能组卷', 'manage_options', 'exam-generator', 'render_paper_generator_page' ); add_submenu_page( 'exam-system', '考试记录', '考试记录', 'manage_options', 'exam-records', 'render_exam_records_page' ); add_submenu_page( 'exam-system', '成绩统计', '成绩统计', 'manage_options', 'exam-statistics', 'render_exam_statistics_page' ); }add_action('admin_menu', 'add_exam_paper_admin_menu'); // 渲染智能组卷页面function render_paper_generator_page() { // 获取所有试题分类 $categories = get_terms(array( 'taxonomy' => 'question_category', 'hide_empty' => false, )); // 获取所有难度级别 $difficulties = get_terms(array( 'taxonomy' => 'question_difficulty', 'hide_empty' => false, )); ?> <div class="wrap"> <h1>智能组卷工具</h1> <div class="card"> <h2>随机组卷</h2> <form id="random-paper-form" method="post"> <?php wp_nonce_field('generate_random_paper', 'paper_nonce'); ?> <table class="form-table"> <tr> <th scope="row"><label for="paper_title">试卷标题</label></th> <td> <input type="text" name="paper_title" id="paper_title" class="regular-text" value="随机试卷 <?php echo date('Y-m-d'); ?>" required> </td> </tr> <tr> <th scope="row"><label for="paper_description">试卷描述</label></th> <td> <textarea name="paper_description" id="paper_description" class="large-text" rows="3"></textarea> </td> </tr> <tr> <th scope="row"><label for="total_questions">试题数量</label></th> <td> <input type="number" name="total_questions" id="total_questions" min="10" max="200" value="50" required> </td> </tr> <tr> <th scope="row"><label>试题分类</label></th> <td> <div style="max-height: 200px; overflow-y: auto; border: 1px solid #ddd; padding: 10px;"> <?php foreach ($categories as $category): ?> <label style="display: block; margin-bottom: 5px;"> <input type="checkbox" name="categories[]" value="<?php echo $category->term_id; ?>"> <?php echo $category->name; ?> (<?php echo $category->count; ?>) </label> <?php endforeach; ?> </div> <p class="description">不选择任何分类表示从所有分类中选题</p> </td> </tr> <tr> <th scope="row"><label>难度分布</label></th> <td> <div id="difficulty-distribution"> <?php foreach ($difficulties as $difficulty): ?> <div style="margin-bottom: 10px;"> <label style="display: inline-block; width: 100px;"> <?php echo $difficulty->name; ?>: </label> <input type="number" name="difficulty_<?php echo $difficulty->slug; ?>" min="0" max="100" value="30" style="width: 60px;"> % </div> <?php endforeach; ?> </div> </td> </tr> <tr> <th scope="row"><label>试题类型</label></th> <td> <label style="margin-right: 15px;"> <input type="checkbox" name="question_types[]" value="single_choice" checked> 单选题 </label> <label style="margin-right: 15px;"> <input type="checkbox" name="question_types[]" value="multiple_choice" checked> 多选题 </label> <label style="margin-right: 15px;"> <input type="checkbox" name="question_types[]" value="true_false"> 判断题 </label> <label style="margin-right: 15px;"> <input type="checkbox" name="question_types[]" value="fill_blank"> 填空题 </label> </td> </tr> <tr> <th scope="row"><label for="time_limit">考试时间(分钟)</label></th> <td> <input type="number" name="time_limit" id="time_limit" min="10" max="300" value="60" required> </td> </tr> </table> <p class="submit"> <button type="submit" name="generate_paper" class="button button-primary">生成试卷</button> </p> </form> </div> <?php // 处理表单提交 if (isset($_POST['generate_paper']) && check_admin_referer('generate_random_paper', 'paper_nonce')) { $params = array( 'title' => sanitize_text_field($_POST['paper_title']), 'description' => sanitize_textarea_field($_POST['paper_description']), 'total_questions' => intval($_POST['total_questions']), 'categories' => isset($_POST['categories']) ? array_map('intval', $_POST['categories']) : array(), 'time_limit' => intval($_POST['time_limit']), 'question_types' => isset($_POST['question_types']) ? array_map('sanitize_text_field', $_POST['question_types']) : array(), ); $paper_id = ExamPaperGenerator::generateRandomPaper($params); if ($paper_id) { echo '<div class="notice notice-success"><p>试卷生成成功!<a href="' . admin_url('admin.php?page=exam-papers&action=edit&paper_id=' . $paper_id) . '">查看试卷</a></p></div>'; } else { echo '<div class="notice notice-error"><p>试卷生成失败,请检查试题库是否足够</p></div>'; } } ?> </div> <script> jQuery(document).ready(function($) { // 验证难度分布总和为100% $('#random-paper-form').submit(function(e) { var total = 0; $('#difficulty-distribution input[type="number"]').each(function() { total += parseInt($(this).val()) || 0; }); if (total !== 100) { alert('难度分布总和必须为100%,当前总和为:' + total + '%'); e.preventDefault(); return false; } }); }); </script> <?php } ## 第四章:在线考试功能实现 ### 4.1 考试前端界面与交互 创建考试前端界面,使用AJAX实现无刷新答题: // 创建考试短代码function exam_paper_shortcode($atts) { $atts = shortcode_atts(array( 'id' => 0, 'preview' => false, ), $atts, 'exam_paper'); $paper_id = intval($atts['id']); if (!$paper_id) { return '<div class="exam-error">请指定试卷ID</div>'; } // 检查用户权限 if (!is_user_logged_in()) { return '<div class="exam-error">请先登录后再参加考试</div>'; } // 获取试卷信息 global $wpdb; $paper = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_papers WHERE paper_id = %d", $paper_id )); if (!$paper) { return '<div class="exam-error">试卷不存在</div>'; } // 检查是否已有进行中的考试 $user_id = get_current_user_id(); $existing_record = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}exam_records WHERE user_id = %d AND paper_id = %d AND status = 'in_progress'", $user_id, $paper_id )); // 如果是预览模式或管理员 $is_preview = $atts['preview'] || current_user_can('manage_options'); if (!$existing_record && !$is_preview) { // 创建新的考试记录 $record_data = array( 'user_id' => $user_id, 'paper_id' => $paper_id, 'start_time' => current_time('mysql'), 'status' => 'in_progress', 'ip_address' => $_SERVER['REMOTE_ADDR'], ); $wpdb->insert($wpdb->prefix . 'exam_records', $record_data); $record_id = $wpdb->insert_id; } else { $record_id = $existing_record ? $existing_record->record_id : 0; } // 获取试卷试题 $questions = $wpdb->get_results($wpdb->prepare( "SELECT q.*, pq.question_order, pq.question_score FROM {$wpdb->prefix}exam_paper_questions pq JOIN {$wpdb->prefix}posts q ON pq.question_id = q.ID WHERE pq.paper_id = %d AND q.post_status = 'publish' ORDER BY pq.question_order ASC", $paper_id )); if (empty($questions)) { return '<div class="exam-error">试卷中没有试题</div>'; } // 获取用户已有的答案(如果是继续考试) $user_answers = array(); if ($record_id && $existing_record && !empty($existing_record->user_answers)) { $user_answers = json_decode($existing_record->user_answers, true); } // 生成考试界面HTML ob_start(); ?> <div class="exam-container" data-paper-id="<?php echo $paper_id; ?>" data-record-id="<?php echo $record_id; ?>" data-time-limit="<?php echo $paper->time_limit; ?>" data-is-preview="<?php echo $is_preview ? '1' : '0'; ?>"> <div class="exam-header"> <h1 class="exam-title"><?php echo esc_html($paper->paper_title); ?></h1> <div class="exam-info"> <div class="exam-timer"> <span class="timer-label">剩余时间:</span> <span class="timer-display"><?php echo $paper->time_limit; ?>:00</span> </div> <div class="exam-progress"> <span class="progress-label">答题进度:</span> <span class="progress-display">0/<?php echo count($questions); ?></span> </div> </div> </div> <div class="exam-navigation"> <div class="question-nav"> <?php foreach ($questions as $index => $question): ?> <button type="button" class="nav-btn" data-question-index="<?php echo $index; ?>"> <?php echo $index + 1; ?> </button> <?php endforeach; ?> </div> <div class="nav-controls"> <button type="button" id="prev-question" class="button">上一题</button> <button type="button" id="next-question" class="button">下一题</button> <button type="button" id="submit-exam" class="button button-primary">提交试卷</button> </div> </div> <div class="exam-questions"> <?php foreach ($questions as $index => $question): $question_type = get_post_meta($question->ID, '_question_type', true); $question_options = get_post_meta($question->ID, '_question_options', true); $options = !empty($question_options) ? json_decode($question_options, true) : array(); $user_answer = isset($user_answers[$question->ID]) ? $user_answers[$question->ID] : ''; ?> <div class="question-item" data-question-id="<?php echo $question->ID; ?>" data-question-index="<?php echo $index; ?>" style="<?php echo $index > 0 ? 'display:none;' : ''; ?>"> <div class="question-header"> <h3 class="question-title"> 第<?php echo $index + 1; ?>题 <span class="question-score">(<?php echo $question->question_score; ?>分)</span> </h3> <div class="question-type"><?php echo get_question_type_name($question_type); ?></div> </div> <div class="question-content"> <?php echo apply_filters('the_content', $question->post_content); ?> </div> <div class="question-answer"> <?php switch ($question_type): case 'single_choice': ?> <div class="choice-options"> <?php foreach ($options as $key => $option): ?> <label class="choice-option"> <input type="radio" name="answer_<?php echo $question->ID; ?>" value="<?php echo chr(65 + $key); ?>" <?php checked($user_answer, chr(65 + $key)); ?>> <span class="option-label"><?php echo chr(65 + $key); ?>.</span> <span class="option-text"><?php echo esc_html($option); ?></span> </label> <?php endforeach; ?> </div> <?php break; case 'multiple_choice': ?> <div class="choice-options"> <?php foreach ($options as $key => $option): $user_answers_array = !empty($user_answer) ? explode(',', $user_answer) : array(); ?> <label class="choice-option"> <input type="checkbox" name="answer_<?php echo $question->ID; ?>[]" value="<?php echo chr(65 + $key); ?>" <?php checked(in_array(chr(65 + $key), $user_answers_array)); ?>> <span class="option-label"><?php echo chr(65 + $key); ?>.</span> <span class="option-text"><?php echo esc_html($option); ?></span> </label> <?php endforeach; ?> </div> <?php break; case 'true_false': ?> <div class="true-false-options"> <label class="true-false-option"> <input type="radio" name="answer_<?php echo $question->ID; ?>" value="true" <?php checked($user_answer, 'true'); ?>> <span>正确</span> </label> <label class="true-false-option"> <input type="radio" name="answer_<?php echo $question->ID; ?>" value="false" <?php checked($user_answer, 'false'); ?>> <span>错误</span> </label> </div> <?php break; case 'fill_blank': ?> <div class="fill-blank-answer"> <input type="text" name="answer_<?php echo $question->ID; ?>" value="<?php echo esc_attr($user_answer); ?>" class="widefat" placeholder="请输入答案"> </div> <?php break; case 'short_answer': case 'essay': ?> <div class="essay-answer"> <textarea name="answer_<?php echo $question->ID; ?>" class="widefat" rows="6" placeholder="请输入您的答案"><?php echo esc_textarea($user_answer); ?></textarea> </div> <?php break; endswitch; ?> </div> <div class="question-actions"> <button type="button" class="button mark-question" data-marked="0"> <span class="mark-text">标记此题</

发表评论