跳至内容

分类: 应用软件

手把手教程,为WordPress实现基于用户分层的差异化内容访问控制工具

手把手教程:为WordPress实现基于用户分层的差异化内容访问控制工具 引言:为什么WordPress需要用户分层内容控制? 在当今互联网环境中,个性化体验已成为网站成功的关键因素之一。根据Monetate的研究,93%的企业表示个性化策略显著提高了收入。对于WordPress网站而言,实现基于用户分层的差异化内容访问控制不仅能提升用户体验,还能有效提高转化率、用户粘性和内容价值。 传统的WordPress用户角色系统虽然提供了基本的权限管理,但在面对复杂的内容访问策略时显得力不从心。许多网站管理员需要根据用户等级、订阅状态、积分体系或购买历史等因素,为不同用户群体展示不同的内容。本教程将手把手指导您通过代码二次开发,实现一个功能完善的用户分层内容访问控制工具。 第一部分:理解WordPress用户系统与权限架构 1.1 WordPress默认用户角色与能力 WordPress内置了五种基本用户角色:订阅者、投稿者、作者、编辑和管理员。每个角色都有一组特定的"能力"(capabilities),这些能力决定了用户可以执行哪些操作。 // 查看WordPress默认能力 global $wp_roles; print_r($wp_roles->roles); 然而,这些默认角色和能力主要针对内容管理,而非内容访问控制。要实现基于用户分层的差异化访问,我们需要扩展这一系统。 1.2 用户分层策略设计 在开始编码之前,我们需要设计合理的用户分层策略。常见的分层维度包括: 会员等级:免费用户、基础会员、高级会员、VIP 订阅状态:试用期用户、活跃订阅用户、过期用户 行为积分:根据互动行为积累的积分等级 购买历史:基于消费金额或频率的分层 用户标签:基于兴趣、职业等特征的手动标签 在本教程中,我们将以会员等级为核心,创建一个四级分层系统: 等级0:未注册游客 等级1:免费注册用户 等级2:基础会员 等级3:高级会员 第二部分:构建用户分层管理系统 2.1 创建用户等级数据库字段 首先,我们需要在用户数据表中添加等级字段。虽然WordPress提供了wp_usermeta表存储用户元数据,但为了性能考虑,我们将使用专门的数据表来管理用户等级信息。 // 创建用户等级数据表 function create_user_level_table() { global $wpdb; $table_name = $wpdb->prefix . 'user_levels'; $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, user_id bigint(20) NOT NULL, user_level int(2) NOT NULL DEFAULT 1, level_expiry datetime DEFAULT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY user_id (user_id), KEY user_level (user_level) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } register_activation_hook(__FILE__, 'create_user_level_table'); 2.2 用户等级管理后台界面 接下来,我们创建一个用户等级管理后台页面,让管理员可以轻松调整用户等级。 // 添加用户等级管理菜单 function add_user_level_admin_menu() { add_users_page( '用户等级管理', '用户等级', 'manage_options', 'user-level-manager', 'render_user_level_admin_page' ); } add_action('admin_menu', 'add_user_level_admin_menu'); // 渲染用户等级管理页面 function render_user_level_admin_page() { global $wpdb; $table_name = $wpdb->prefix . 'user_levels'; // 处理表单提交 if (isset($_POST['update_user_level'])) { $user_id = intval($_POST['user_id']); $new_level = intval($_POST['user_level']); $expiry_date = sanitize_text_field($_POST['level_expiry']); update_user_level($user_id, $new_level, $expiry_date); echo '<div class="notice notice-success"><p>用户等级已更新</p></div>'; } // 分页逻辑 $per_page = 20; $current_page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $offset = ($current_page - 1) * $per_page; // 获取用户数据 $users = $wpdb->get_results( $wpdb->prepare( "SELECT u.ID, u.user_login, u.user_email, ul.user_level, ul.level_expiry FROM {$wpdb->users} u LEFT JOIN $table_name ul ON u.ID = ul.user_id ORDER BY u.ID DESC LIMIT %d OFFSET %d", $per_page, $offset ) ); // 计算总页数 $total_users = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->users}"); $total_pages = ceil($total_users / $per_page); // 渲染页面HTML ?> <div class="wrap"> <h1 class="wp-heading-inline">用户等级管理</h1> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>用户ID</th> <th>用户名</th> <th>邮箱</th> <th>当前等级</th> <th>等级有效期</th> <th>操作</th> </tr> </thead> <tbody> <?php foreach ($users as $user): ?> <tr> <td><?php echo $user->ID; ?></td> <td><?php echo esc_html($user->user_login); ?></td> <td><?php echo esc_html($user->user_email); ?></td> <td> <select id="level_<?php echo $user->ID; ?>"> <option value="0" <?php selected($user->user_level, 0); ?>>游客</option> <option value="1" <?php selected($user->user_level, 1); ?>>免费用户</option> <option value="2" <?php selected($user->user_level, 2); ?>>基础会员</option> <option value="3" <?php selected($user->user_level, 3); ?>>高级会员</option> </select> </td> <td> <input type="date" id="expiry_<?php echo $user->ID; ?>" value="<?php echo $user->level_expiry ? date('Y-m-d', strtotime($user->level_expiry)) : ''; ?>"> </td> <td> <button class="button button-primary update-level-btn" data-user-id="<?php echo $user->ID; ?>">更新</button> </td> </tr> <?php endforeach; ?> </tbody> </table> <!-- 分页导航 --> <div class="tablenav bottom"> <div class="tablenav-pages"> <?php echo paginate_links(array( 'base' => add_query_arg('paged', '%#%'), 'format' => '', 'prev_text' => '&laquo;', 'next_text' => '&raquo;', 'total' => $total_pages, 'current' => $current_page )); ?> </div> </div> </div> <script> jQuery(document).ready(function($) { $('.update-level-btn').click(function() { var userId = $(this).data('user-id'); var newLevel = $('#level_' + userId).val(); var expiryDate = $('#expiry_' + userId).val(); $.post(ajaxurl, { action: 'update_user_level', user_id: userId, user_level: newLevel, level_expiry: expiryDate, nonce: '<?php echo wp_create_nonce("update_user_level_nonce"); ?>' }, function(response) { if (response.success) { alert('用户等级已更新'); } else { alert('更新失败: ' + response.data); } }); }); }); </script> <?php } 2.3 用户等级API函数 创建核心的用户等级管理函数,这些函数将在整个插件中使用。 // 获取用户等级 function get_user_level($user_id = null) { if (!$user_id) { $user_id = get_current_user_id(); } if (!$user_id) { return 0; // 游客 } global $wpdb; $table_name = $wpdb->prefix . 'user_levels'; $result = $wpdb->get_row($wpdb->prepare( "SELECT user_level, level_expiry FROM $table_name WHERE user_id = %d", $user_id )); // 检查等级是否过期 if ($result && $result->level_expiry) { $expiry_time = strtotime($result->level_expiry); if (time() > $expiry_time) { // 等级已过期,降级为免费用户 update_user_level($user_id, 1, null); return 1; } } return $result ? $result->user_level : 1; // 默认等级为1(免费用户) } // 更新用户等级 function update_user_level($user_id, $level, $expiry_date = null) { global $wpdb; $table_name = $wpdb->prefix . 'user_levels'; // 验证用户是否存在 $user = get_user_by('ID', $user_id); if (!$user) { return new WP_Error('invalid_user', '用户不存在'); } // 验证等级值 if (!in_array($level, [0, 1, 2, 3])) { return new WP_Error('invalid_level', '无效的用户等级'); } // 检查是否已有记录 $existing = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $table_name WHERE user_id = %d", $user_id )); if ($existing) { // 更新现有记录 $wpdb->update( $table_name, array( 'user_level' => $level, 'level_expiry' => $expiry_date, 'updated_at' => current_time('mysql') ), array('user_id' => $user_id) ); } else { // 插入新记录 $wpdb->insert( $table_name, array( 'user_id' => $user_id, 'user_level' => $level, 'level_expiry' => $expiry_date, 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql') ) ); } // 清除用户缓存 clean_user_cache($user_id); // 触发等级变更钩子 do_action('user_level_updated', $user_id, $level, $expiry_date); return true; } // AJAX处理用户等级更新 add_action('wp_ajax_update_user_level', 'handle_ajax_update_user_level'); function handle_ajax_update_user_level() { // 验证nonce if (!check_ajax_referer('update_user_level_nonce', 'nonce', false)) { wp_die('安全验证失败', 403); } // 验证权限 if (!current_user_can('manage_options')) { wp_die('权限不足', 403); } $user_id = intval($_POST['user_id']); $user_level = intval($_POST['user_level']); $level_expiry = sanitize_text_field($_POST['level_expiry']); $result = update_user_level($user_id, $user_level, $level_expiry); if (is_wp_error($result)) { wp_send_json_error($result->get_error_message()); } else { wp_send_json_success(); } } 第三部分:实现内容访问控制功能 3.1 短代码实现内容保护 短代码是WordPress中最灵活的内容控制方式之一。我们将创建几个短代码,用于根据用户等级显示或隐藏内容。 // 等级保护短代码 function user_level_protected_shortcode($atts, $content = null) { // 解析短代码属性 $atts = shortcode_atts(array( 'min_level' => 1, 'max_level' => 3, 'message' => '您没有权限查看此内容', 'show_to' => '' // 特定用户ID,用逗号分隔 ), $atts, 'user_level_content'); $current_user_id = get_current_user_id(); $current_user_level = get_user_level($current_user_id); // 检查特定用户权限 if (!empty($atts['show_to'])) { $allowed_users = array_map('intval', explode(',', $atts['show_to'])); if (in_array($current_user_id, $allowed_users)) { return do_shortcode($content); } } // 检查等级权限 if ($current_user_level >= $atts['min_level'] && $current_user_level <= $atts['max_level']) { return do_shortcode($content); } // 没有权限,显示提示信息 $message = '<div class="user-level-restricted-message">'; $message .= '<p>' . esc_html($atts['message']) . '</p>'; // 如果用户未登录,显示登录链接 if (!$current_user_id) { $message .= '<p>请<a href="' . wp_login_url(get_permalink()) . '">登录</a>或'; $message .= '<a href="' . wp_registration_url() . '">注册</a></p>'; } // 如果用户等级不够,显示升级提示 elseif ($current_user_level < $atts['min_level']) { $message .= '<p>您需要升级到等级' . $atts['min_level'] . '才能查看此内容</p>'; $message .= '<a href="/membership" class="button">立即升级</a>'; } $message .= '</div>'; return $message; } add_shortcode('user_level_content', 'user_level_protected_shortcode'); // 使用示例: // [user_level_content min_level="2"]仅限基础会员及以上查看的内容[/user_level_content] // [user_level_content min_level="3" message="这是VIP专属内容"]VIP内容[/user_level_content] 3.2 文章/页面级别的内容保护 除了短代码,我们还需要在文章/页面级别实现内容保护。这可以通过自定义元框和内容过滤器来实现。 // 添加文章/页面等级保护元框 function add_user_level_metabox() { $post_types = get_post_types(array('public' => true)); foreach ($post_types as $post_type) { add_meta_box( 'user_level_protection', '用户等级访问控制', 'render_user_level_metabox', $post_type, 'side', 'high' ); } } add_action('add_meta_boxes', 'add_user_level_metabox'); // 渲染元框内容 function render_user_level_metabox($post) { wp_nonce_field('save_user_level_protection', 'user_level_protection_nonce'); $min_level = get_post_meta($post->ID, '_user_level_min', true); $max_level = get_post_meta($post->ID, '_user_level_max', true); $custom_message = get_post_meta($post->ID, '_user_level_message', true); ?> <p> <label for="user_level_min">最低访问等级:</label> <select name="user_level_min" id="user_level_min"> <option value="0" <?php selected($min_level, '0'); ?>>所有人(包括游客)</option> <option value="1" <?php selected($min_level, '1'); ?>>免费用户及以上</option> <option value="2" <?php selected($min_level, '2'); ?>>基础会员及以上</option> <option value="3" <?php selected($min_level, '3'); ?>>高级会员及以上</option> </select> </p> <p> <label for="user_level_max">最高访问等级:</label> <select name="user_level_max" id="user_level_max"> <option value="0" <?php selected($max_level, '0'); ?>>不限</option> <option value="1" <?php selected($max_level, '1'); ?>>仅免费用户</option> <option value="2" <?php selected($max_level, '2'); ?>>仅基础会员</option> <option value="3" <?php selected($max_level, '3'); ?>>仅高级会员</option> </select> </p> <p> <label for="user_level_message">无权限提示信息:</label> <textarea name="user_level_message" id="user_level_message" rows="3" style="width:100%;"><?php echo esc_textarea($custom_message); ?></textarea> </p> <p class="description"> 设置此文章/页面的访问等级限制。如果用户等级不符合要求,将看到提示信息。 </p> <?php } // 保存元框数据 function save_user_level_protection($post_id) { // 检查nonce if (!isset($_POST['user_level_protection_nonce']) || nono'], 'save_user_level_protection')) { return; } // 检查自动保存 if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } // 检查用户权限 if (!current_user_can('edit_post', $post_id)) { return; } // 保存数据 if (isset($_POST['user_level_min'])) { update_post_meta($post_id, '_user_level_min', intval($_POST['user_level_min'])); } if (isset($_POST['user_level_max'])) { update_post_meta($post_id, '_user_level_max', intval($_POST['user_level_max'])); } if (isset($_POST['user_level_message'])) { update_post_meta($post_id, '_user_level_message', sanitize_textarea_field($_POST['user_level_message'])); } }add_action('save_post', 'save_user_level_protection'); // 内容访问控制过滤器function filter_protected_content($content) { global $post; // 只在单篇文章/页面应用 if (!is_singular() || !isset($post->ID)) { return $content; } // 获取当前用户等级 $current_user_id = get_current_user_id(); $current_user_level = get_user_level($current_user_id); // 获取文章保护设置 $min_level = get_post_meta($post->ID, '_user_level_min', true); $max_level = get_post_meta($post->ID, '_user_level_max', true); $custom_message = get_post_meta($post->ID, '_user_level_message', true); // 如果没有设置保护,直接返回内容 if (empty($min_level) && empty($max_level)) { return $content; } // 设置默认值 $min_level = empty($min_level) ? 0 : intval($min_level); $max_level = empty($max_level) ? 3 : intval($max_level); // 检查用户权限 $has_access = true; if ($current_user_level < $min_level) { $has_access = false; $reason = 'level_too_low'; } if ($max_level > 0 && $current_user_level > $max_level) { $has_access = false; $reason = 'level_too_high'; } // 如果有权限,返回完整内容 if ($has_access) { return $content; } // 没有权限,返回提示信息 $message = '<div class="user-level-restricted-content">'; if (!empty($custom_message)) { $message .= '<p>' . esc_html($custom_message) . '</p>'; } else { $message .= '<h3>内容访问受限</h3>'; if ($reason === 'level_too_low') { $level_names = ['游客', '免费用户', '基础会员', '高级会员']; $message .= '<p>此内容需要' . $level_names[$min_level] . '及以上等级才能访问</p>'; $message .= '<p>您的当前等级:' . $level_names[$current_user_level] . '</p>'; } else { $message .= '<p>此内容仅限特定等级用户访问</p>'; } } // 添加操作按钮 if (!$current_user_id) { $message .= '<div class="restricted-actions">'; $message .= '<a href="' . wp_login_url(get_permalink()) . '" class="button button-primary">登录</a>'; $message .= '<a href="' . wp_registration_url() . '" class="button">注册</a>'; $message .= '</div>'; } elseif ($current_user_level < $min_level) { $message .= '<div class="restricted-actions">'; $message .= '<a href="/membership" class="button button-primary">升级会员</a>'; $message .= '</div>'; } $message .= '</div>'; return $message; }add_filter('the_content', 'filter_protected_content', 10); ### 3.3 分类和标签级别的访问控制 除了文章级别的控制,我们还可以实现分类和标签级别的访问控制。 // 为分类添加等级保护字段function add_user_level_to_category($term) { ?> <div class="form-field"> <label for="user_level_min">最低访问等级</label> <select name="user_level_min" id="user_level_min"> <option value="0">所有人(包括游客)</option> <option value="1">免费用户及以上</option> <option value="2">基础会员及以上</option> <option value="3">高级会员及以上</option> </select> <p>设置访问此分类的最低用户等级</p> </div> <?php }add_action('category_add_form_fields', 'add_user_level_to_category'); // 编辑分类时的字段function edit_user_level_for_category($term) { $min_level = get_term_meta($term->term_id, 'user_level_min', true); ?> <tr class="form-field"> <th scope="row"><label for="user_level_min">最低访问等级</label></th> <td> <select name="user_level_min" id="user_level_min"> <option value="0" <?php selected($min_level, '0'); ?>>所有人(包括游客)</option> <option value="1" <?php selected($min_level, '1'); ?>>免费用户及以上</option> <option value="2" <?php selected($min_level, '2'); ?>>基础会员及以上</option> <option value="3" <?php selected($min_level, '3'); ?>>高级会员及以上</option> </select> <p class="description">设置访问此分类的最低用户等级</p> </td> </tr> <?php }add_action('category_edit_form_fields', 'edit_user_level_for_category'); // 保存分类等级设置function save_category_user_level($term_id) { if (isset($_POST['user_level_min'])) { update_term_meta($term_id, 'user_level_min', intval($_POST['user_level_min'])); } }add_action('created_category', 'save_category_user_level');add_action('edited_category', 'save_category_user_level'); // 分类页面访问控制function filter_category_archive($query) { if (!is_admin() && $query->is_main_query() && $query->is_category()) { $current_user_level = get_user_level(); $category_id = $query->get_queried_object_id(); $category_min_level = get_term_meta($category_id, 'user_level_min', true); if (!empty($category_min_level) && $current_user_level < intval($category_min_level)) { // 用户等级不够,重定向或显示错误 if (!is_user_logged_in()) { wp_redirect(wp_login_url(get_category_link($category_id))); exit; } else { wp_die('您没有权限访问此分类', '访问受限', 403); } } } }add_action('pre_get_posts', 'filter_category_archive'); ## 第四部分:用户界面与体验优化 ### 4.1 前端用户等级显示 让用户清楚了解自己的等级状态非常重要。我们将在用户资料页和前端显示用户等级信息。 // 在用户资料页显示等级信息function show_user_level_in_profile($user) { $user_level = get_user_level($user->ID); $level_names = ['游客', '免费用户', '基础会员', '高级会员']; $level_colors = ['#95a5a6', '#3498db', '#2ecc71', '#e74c3c']; // 获取等级有效期 global $wpdb; $table_name = $wpdb->prefix . 'user_levels'; $expiry = $wpdb->get_var($wpdb->prepare( "SELECT level_expiry FROM $table_name WHERE user_id = %d", $user->ID )); ?> <h3>用户等级信息</h3> <table class="form-table"> <tr> <th><label>当前等级</label></th> <td> <div style="display: flex; align-items: center; gap: 10px;"> <span style="display: inline-block; width: 20px; height: 20px; border-radius: 50%; background-color: <?php echo $level_colors[$user_level]; ?>;"></span> <strong style="color: <?php echo $level_colors[$user_level]; ?>;"> <?php echo $level_names[$user_level]; ?> (等级 <?php echo $user_level; ?>) </strong> </div> </td> </tr> <?php if ($expiry): ?> <tr> <th><label>等级有效期</label></th> <td><?php echo date('Y年m月d日', strtotime($expiry)); ?></td> </tr> <?php endif; ?> <tr> <th><label>等级权限</label></th> <td> <ul style="margin: 0;"> <?php $permissions = [ 0 => '只能浏览公开内容', 1 => '可以浏览基础内容,参与评论', 2 => '可以访问会员专属内容,下载资源', 3 => '可以访问所有内容,享受优先支持' ]; foreach ($permissions as $level => $permission) { $checked = $user_level >= $level ? '✓' : '✗'; $color = $user_level >= $level ? '#2ecc71' : '#e74c3c'; echo '<li style="color: ' . $color . ';">' . $checked . ' ' . $permission . '</li>'; } ?> </ul> </td> </tr> </table> <?php }add_action('show_user_profile', 'show_user_level_in_profile');add_action('edit_user_profile', 'show_user_level_in_profile'); // 在前端显示用户等级徽章function display_user_level_badge() { if (!is_user_logged_in()) { return; } $user_id = get_current_user_id(); $user_level = get_user_level($user_id); $level_names = ['游客', '免费用户', '基础会员', '高级会员']; $level_colors = ['#95a5a6', '#3498db', '#2ecc71', '#e74c3c']; // 获取用户信息 $user = wp_get_current_user(); $display_name = $user->display_name; ob_start(); ?> <div class="user-level-badge" style=" display: inline-flex; align-items: center; gap: 8px; padding: 6px 12px; background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%); border-radius: 20px; border: 2px solid <?php echo $level_colors[$user_level]; ?>; font-size: 14px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); "> <span style=" display: inline-block; width: 12px; height: 12px; border-radius: 50%; background-color: <?php echo $level_colors[$user_level]; ?>; "></span> <span style="font-weight: bold; color: #2c3e50;"><?php echo esc_html($display_name); ?></span> <span style=" padding: 2px 8px; background-color: <?php echo $level_colors[$user_level]; ?>; color: white; border-radius: 10px; font-size: 12px; font-weight: bold; "> <?php echo $level_names[$user_level]; ?> </span> </div> <?php return ob_get_clean(); }add_shortcode('user_level_badge', 'display_user_level_badge'); // 自动在导航栏添加用户等级信息function add_user_level_to_nav($items, $args) { if (!is_user_logged_in() || $args->theme_location != 'primary') { return $items; } $badge = display_user_level_badge(); $items .= '<li class="menu-item user-level-menu-item">' . $badge . '</li>'; return $items; }add_filter('wp_nav_menu_items', 'add_user_level_to_nav', 10, 2); ### 4.2 等级升级提示和引导 当用户尝试访问受限内容时,提供清晰的升级路径非常重要。 // 创建升级提示小工具class User_Level_Upgrade_Widget extends WP_Widget { public function __construct() { parent::__construct( 'user_level_upgrade_widget', '用户等级升级提示', array('description' => '根据用户当前等级显示升级提示') ); } public function widget($args, $instance) { if (!is_user_logged_in()) { return; } $current_user_level = get_user_level(); // 如果已经是最高等级,不显示 if ($current_user_level >= 3) { return; } $next_level = $current_user_level + 1; $level_names = ['免费用户', '基础会员', '高级会员', 'VIP']; $level_prices = ['免费', '99元/年', '299元/年', '999元/年']; $level_features = [ 1 => ['基础内容访问', '参与评论', '社区互动'], 2 => ['会员专属内容', '资源下载', '优先支持'], 3 => ['所有内容访问', '专属客服', '活动邀请', '定制服务'] ]; echo $args['before_widget']; echo $args['before_title'] . '升级到' . $level_names[$next_level] . $args['after_title']; ?> <div class="upgrade-widget-content"> <div class="current-level" style=" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 15px; border-radius: 8px; margin-bottom: 15px; "> <h4 style="margin: 0 0 5px 0;">当前等级</h4> <div style="font-size: 24px; font-weight: bold;"><?php echo $level_names[$current_user_level]; ?></div> </div> <div class="next-level" style=" background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); color: white; padding: 15px; border-radius: 8px; margin-bottom: 15px; "> <h4 style="margin: 0 0 5px 0;">下一等级</h4> <div style="font-size: 24px; font-weight: bold;"><?php echo $level_names[$next_level]; ?></div> <div style="font-size: 18px; margin-top: 5px;"><?php echo $level_prices[$next_level]; ?></div> </div> <div class="features" style="margin-bottom: 15px;"> <h4 style="margin-bottom: 10px;">升级后将获得:</h4> <ul style="margin: 0; padding-left: 20px;"> <?php foreach ($level_features[$next_level] as $feature): ?> <li style="margin-bottom: 5px;">✓ <?php echo $feature; ?></li> <?php endforeach; ?> </ul> </div> <a href="/upgrade?level=<?php echo $next_level; ?>" class="button button-primary" style=" display: block; text-align: center; padding: 12px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; text-decoration: none; border-radius: 6px; font-weight: bold; transition: transform 0.3s; " onmouseover="this.style.transform='translateY(-2px)'" onmouseout="this.style.transform='translateY(0)'"> 立即升级 </a> </div> <?php echo $args['after_widget']; } } // 注册小工具function register_user_level_widget() { register_widget('User_Level_Upgrade_Widget'); }add_action('widgets_init', 'register_user_level_widget'); ## 第五部分:高级功能与集成 ### 5.1 与WooCommerce集成 如果网站使用WooCommerce,我们可以将用户等级与购买行为关联起来。 // WooCommerce购买后自动升级用户等级function upgrade_user_level_on_purchase($order_id) { $order = wc_get_order($order_id); $user_id = $order->get_user_id(); if (!$user_id) { return; } // 获取订单总金额 $order_total = $order->get_total(); // 根据订单金额确定等级 $new_level = 1; // 默认免费用户 if ($order_total >= 999) { $new_level = 3; // 高级会员 $expiry_date = date('Y-m-d H:i:s', strtotime('+1 year')); } elseif ($order_total >= 99) { $new_level = 2; // 基础会员 $expiry_date = date('Y-m-d H:i:s', strtotime('+1 year')); } // 更新用户等级 if ($new_level >

发表评论

开发指南,打造网站内嵌的在线视频速度控制与章节标记工具

开发指南:打造网站内嵌的在线视频速度控制与章节标记工具 摘要 随着在线教育、知识分享和内容创作行业的蓬勃发展,视频内容已成为网站不可或缺的组成部分。然而,大多数网站的视频播放功能相对基础,缺乏个性化控制选项。本文将详细介绍如何通过WordPress代码二次开发,为网站视频播放器添加速度控制与章节标记功能,从而提升用户体验和内容互动性。我们将从需求分析、技术选型、代码实现到测试部署,全面解析这一实用工具的构建过程。 一、需求分析与功能规划 1.1 当前视频播放的局限性 大多数WordPress网站使用默认的视频播放器或第三方插件,这些方案通常存在以下局限性: 播放速度固定,无法根据用户需求调整 缺乏章节标记功能,长视频导航困难 用户无法自定义播放体验 互动性差,用户参与度低 1.2 目标功能定义 我们的开发目标是为WordPress网站视频播放器添加以下核心功能: 多级播放速度控制:提供0.5x到3.0x之间的多档速度选择 智能章节标记系统:允许内容创作者为视频添加章节标记 用户友好的交互界面:直观的控制面板,不影响观看体验 数据持久化:记住用户的播放偏好设置 响应式设计:适配各种设备和屏幕尺寸 1.3 技术可行性分析 WordPress作为开源CMS系统,提供了丰富的API和钩子机制,使我们能够: 通过JavaScript操作HTML5视频元素 使用PHP扩展视频元数据处理 利用WordPress数据库存储章节信息 通过AJAX实现前后端数据交互 二、技术架构与开发环境搭建 2.1 开发环境配置 在开始开发前,需要准备以下环境: 本地开发环境:XAMPP/MAMP或Local by Flywheel WordPress安装:最新版本的WordPress(5.8+) 代码编辑器:VS Code、Sublime Text或PHPStorm 浏览器开发者工具:用于调试JavaScript和CSS 版本控制系统:Git(可选但推荐) 2.2 项目结构设计 我们将创建一个独立的WordPress插件来管理所有功能代码: wp-content/plugins/video-enhancer-tool/ ├── video-enhancer.php # 主插件文件 ├── includes/ │ ├── class-video-processor.php # 视频处理类 │ ├── class-chapter-manager.php # 章节管理类 │ └── class-settings-handler.php # 设置处理类 ├── assets/ │ ├── css/ │ │ ├── video-controls.css # 控制界面样式 │ │ └── admin-styles.css # 后台管理样式 │ ├── js/ │ │ ├── video-controls.js # 前端控制逻辑 │ │ ├── chapter-editor.js # 章节编辑器 │ │ └── admin-scripts.js # 后台脚本 │ └── images/ # 图标和图片资源 ├── templates/ # 前端模板文件 │ └── video-player-enhanced.php └── languages/ # 国际化文件 2.3 核心技术与API 我们将使用以下关键技术: HTML5 Video API:控制视频播放的核心JavaScript接口 WordPress REST API:处理章节数据的存储和检索 jQuery:简化DOM操作和事件处理(WordPress已内置) LocalStorage:存储用户偏好设置 CSS3 Flexbox/Grid:构建响应式控制界面 三、播放速度控制功能实现 3.1 HTML5视频播放器基础集成 首先,我们需要确保网站的视频使用HTML5播放器,这是实现高级控制的基础: // 在video-enhancer.php中 function vet_replace_video_shortcode($output, $tag) { if ('video' !== $tag) { return $output; } // 为视频元素添加ID和类名以便JavaScript操作 $output = preg_replace('/<video/', '<video data-vet-enhanced="true"', $output); return $output; } add_filter('do_shortcode_tag', 'vet_replace_video_shortcode', 10, 2); 3.2 速度控制界面设计 创建直观的速度控制界面,包含按钮和下拉菜单: /* assets/css/video-controls.css */ .vet-controls-container { position: relative; display: flex; align-items: center; justify-content: space-between; background: rgba(0, 0, 0, 0.7); padding: 10px 15px; border-radius: 5px; margin-top: 10px; color: white; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } .vet-speed-control { display: flex; align-items: center; } .vet-speed-label { margin-right: 10px; font-size: 14px; opacity: 0.9; } .vet-speed-buttons { display: flex; gap: 5px; } .vet-speed-btn { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.3); color: white; padding: 5px 10px; border-radius: 3px; cursor: pointer; font-size: 13px; transition: all 0.2s ease; } .vet-speed-btn:hover { background: rgba(255, 255, 255, 0.2); } .vet-speed-btn.active { background: #0073aa; border-color: #0073aa; } .vet-speed-dropdown { position: relative; display: inline-block; } .vet-speed-dropdown-btn { background: rgba(255, 255, 255, 0.1); border: 1px solid rgba(255, 255, 255, 0.3); color: white; padding: 5px 15px 5px 10px; border-radius: 3px; cursor: pointer; font-size: 13px; position: relative; } .vet-speed-dropdown-btn:after { content: "▼"; font-size: 10px; margin-left: 5px; } .vet-speed-dropdown-content { display: none; position: absolute; bottom: 100%; left: 0; background: rgba(0, 0, 0, 0.9); min-width: 80px; border-radius: 3px; z-index: 1000; margin-bottom: 5px; } .vet-speed-dropdown-content.show { display: block; } .vet-speed-option { color: white; padding: 8px 12px; text-decoration: none; display: block; cursor: pointer; font-size: 13px; } .vet-speed-option:hover { background: rgba(255, 255, 255, 0.1); } 3.3 JavaScript速度控制逻辑 实现播放速度控制的核心JavaScript代码: // assets/js/video-controls.js (function($) { 'use strict'; // 视频增强工具主对象 var VideoEnhancer = { // 初始化 init: function() { this.setupVideoPlayers(); this.bindEvents(); this.loadUserPreferences(); }, // 查找页面上的视频元素并添加控制 setupVideoPlayers: function() { $('video[data-vet-enhanced="true"]').each(function() { var $video = $(this); // 确保视频有ID if (!$video.attr('id')) { $video.attr('id', 'vet-video-' + Math.random().toString(36).substr(2, 9)); } // 添加控制容器 VideoEnhancer.addControls($video); }); }, // 为视频添加控制界面 addControls: function($video) { var videoId = $video.attr('id'); var controlsHtml = ` <div class="vet-controls-container" data-video-id="${videoId}"> <div class="vet-speed-control"> <span class="vet-speed-label">播放速度:</span> <div class="vet-speed-buttons"> <button class="vet-speed-btn" data-speed="0.5">0.5x</button> <button class="vet-speed-btn" data-speed="0.75">0.75x</button> <button class="vet-speed-btn active" data-speed="1">1x</button> <button class="vet-speed-btn" data-speed="1.25">1.25x</button> <button class="vet-speed-btn" data-speed="1.5">1.5x</button> <button class="vet-speed-btn" data-speed="2">2x</button> </div> <div class="vet-speed-dropdown"> <button class="vet-speed-dropdown-btn">更多速度</button> <div class="vet-speed-dropdown-content"> <div class="vet-speed-option" data-speed="0.25">0.25x</div> <div class="vet-speed-option" data-speed="0.5">0.5x</div> <div class="vet-speed-option" data-speed="0.75">0.75x</div> <div class="vet-speed-option" data-speed="1">1x</div> <div class="vet-speed-option" data-speed="1.25">1.25x</div> <div class="vet-speed-option" data-speed="1.5">1.5x</div> <div class="vet-speed-option" data-speed="1.75">1.75x</div> <div class="vet-speed-option" data-speed="2">2x</div> <div class="vet-speed-option" data-speed="2.5">2.5x</div> <div class="vet-speed-option" data-speed="3">3x</div> </div> </div> </div> <div class="vet-chapter-control"> <button class="vet-chapters-toggle-btn">章节</button> <div class="vet-chapters-panel"> <div class="vet-chapters-list"></div> </div> </div> </div> `; // 将控制容器插入到视频后面 $video.after(controlsHtml); // 初始化章节功能 VideoEnhancer.loadChaptersForVideo(videoId); }, // 绑定事件处理 bindEvents: function() { // 速度按钮点击事件 $(document).on('click', '.vet-speed-btn', function() { var speed = $(this).data('speed'); VideoEnhancer.setPlaybackSpeed(speed, $(this)); }); // 下拉速度选项点击事件 $(document).on('click', '.vet-speed-option', function() { var speed = $(this).data('speed'); VideoEnhancer.setPlaybackSpeed(speed, $(this)); }); // 下拉菜单显示/隐藏 $(document).on('click', '.vet-speed-dropdown-btn', function(e) { e.stopPropagation(); $(this).siblings('.vet-speed-dropdown-content').toggleClass('show'); }); // 点击其他地方关闭下拉菜单 $(document).on('click', function() { $('.vet-speed-dropdown-content').removeClass('show'); }); // 章节切换按钮 $(document).on('click', '.vet-chapters-toggle-btn', function() { $(this).siblings('.vet-chapters-panel').toggleClass('show'); }); }, // 设置播放速度 setPlaybackSpeed: function(speed, $element) { // 获取对应的视频元素 var videoId = $element.closest('.vet-controls-container').data('video-id'); var video = document.getElementById(videoId); if (video) { // 设置播放速度 video.playbackRate = speed; // 更新按钮状态 $('.vet-speed-btn').removeClass('active'); $('.vet-speed-btn[data-speed="' + speed + '"]').addClass('active'); // 更新下拉按钮文本 var $dropdownBtn = $element.closest('.vet-controls-container').find('.vet-speed-dropdown-btn'); $dropdownBtn.text(speed + 'x'); // 保存用户偏好 VideoEnhancer.saveUserPreference('playbackSpeed', speed); // 显示速度变化提示 VideoEnhancer.showSpeedNotification(speed); } }, // 显示速度变化提示 showSpeedNotification: function(speed) { // 创建或获取通知元素 var $notification = $('.vet-speed-notification'); if ($notification.length === 0) { $notification = $('<div class="vet-speed-notification"></div>'); $('body').append($notification); } // 设置内容和显示 $notification.text('播放速度: ' + speed + 'x').addClass('show'); // 2秒后隐藏 setTimeout(function() { $notification.removeClass('show'); }, 2000); }, // 加载用户偏好设置 loadUserPreferences: function() { var savedSpeed = localStorage.getItem('vet_playback_speed'); if (savedSpeed) { // 页面加载后应用保存的速度 $(document).ready(function() { setTimeout(function() { $('.vet-speed-btn[data-speed="' + savedSpeed + '"]').click(); }, 500); }); } }, // 保存用户偏好 saveUserPreference: function(key, value) { if (key === 'playbackSpeed') { localStorage.setItem('vet_playback_speed', value); } }, // 加载视频章节 loadChaptersForVideo: function(videoId) { // 通过AJAX获取章节数据 $.ajax({ url: vet_ajax.ajax_url, type: 'POST', data: { action: 'vet_get_chapters', video_id: videoId, nonce: vet_ajax.nonce }, success: function(response) { if (response.success && response.data.chapters) { VideoEnhancer.renderChapters(videoId, response.data.chapters); } } }); }, // 渲染章节列表 renderChapters: function(videoId, chapters) { var $chaptersList = $('.vet-controls-container[data-video-id="' + videoId + '"] .vet-chapters-list'); var html = ''; if (chapters.length > 0) { html += '<div class="vet-chapters-header">视频章节</div>'; chapters.forEach(function(chapter) { html += ` <div class="vet-chapter-item" data-timestamp="${chapter.timestamp}"> <div class="vet-chapter-time">${VideoEnhancer.formatTime(chapter.timestamp)}</div> <div class="vet-chapter-title">${chapter.title}</div> </div> `; }); // 绑定章节点击事件 $chaptersList.html(html); $chaptersList.find('.vet-chapter-item').on('click', function() { var timestamp = $(this).data('timestamp'); var video = document.getElementById(videoId); if (video) { video.currentTime = timestamp; video.play(); } }); } else { $chaptersList.html('<div class="vet-no-chapters">暂无章节标记</div>'); } }, // 格式化时间显示 formatTime: function(seconds) { var hrs = Math.floor(seconds / 3600); var mins = Math.floor((seconds % 3600) / 60); var secs = Math.floor(seconds % 60); if (hrs > 0) { return hrs + ':' + (mins < 10 ? '0' : '') + mins + ':' + (secs < 10 ? '0' : '') + secs; } else { return mins + ':' + (secs < 10 ? '0' : '') + secs; } } }; // 初始化 $(document).ready(function() { VideoEnhancer.init(); }); })(jQuery); 3.4 后端支持代码 创建PHP类来处理速度控制相关的后端逻辑: // includes/class-video-processor.php class Video_Enhancer_Processor { // 初始化 public function __construct() { add_action('wp_enqueue_scripts', array($this, 'enqueue_frontend_assets')); add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); add_action('wp_ajax_vet_get_chapters', array($this, 'ajax_get_chapters')); add_action('wp_ajax_nopriv_vet_get_chapters', array($this, 'ajax_get_chapters')); add_action('wp_ajax_vet_save_chapters', array($this, 'ajax_save_chapters')); } // 加载前端资源 public function enqueue_frontend_assets() { // 加载CSS wp_enqueue_style( 'video-enhancer-controls', plugin_dir_url(__FILE__) . '../assets/css/video-controls.css', array(), '1.0.0' ); // 加载JavaScript wp_enqueue_script( 'video-enhancer-controls', plugin_dir_url(__FILE__) . '../assets/js/video-controls.js', array('jquery'), '1.0.0', true ); // 传递AJAX参数到前端 wp_localize_script('video-enhancer-controls', 'vet_ajax', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('vet_ajax_nonce') )); } // 加载后台资源 public function enqueue_admin_assets($hook) { // 只在文章编辑页面加载 if ('post.php' !== $hook && 'post-new.php' !== $hook) { return; } wp_enqueue_style( 'video-enhancer-admin', plugin_dir_url(__FILE__) . '../assets/css/admin-styles.css', array(), '1.0.0' ); wp_enqueue_script( 'video-enhancer-admin', plugin_dir_url(__FILE__) . '../assets/js/admin-scripts.js', array('jquery', 'jquery-ui-sortable'), '1.0.0', true ); } // AJAX获取章节数据 public function ajax_get_chapters() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'vet_ajax_nonce')) { wp_die('安全验证失败'); } $video_id = sanitize_text_field($_POST['video_id']); $post_id = get_the_ID(); // 获取章节数据 $chapters = $this->get_chapters_for_video($post_id, $video_id); wp_send_json_success(array( 'chapters' => $chapters )); } // 获取视频章节 private function get_chapters_for_video($post_id, $video_id) { $chapters = get_post_meta($post_id, '_vet_video_chapters_' . $video_id, true); if (empty($chapters) || !is_array($chapters)) { return array(); } // 按时间戳排序 usort($chapters, function($a, $b) { return $a['timestamp'] - $b['timestamp']; }); return $chapters; } } 四、章节标记系统实现 4.1 章节数据结构设计 章节数据需要包含以下信息: 时间戳(秒) 章节标题 章节描述(可选) 缩略图(可选) // includes/class-chapter-manager.php class Video_Enhancer_Chapter_Manager { // 初始化 public function __construct() { add_action('add_meta_boxes', array($this, 'add_chapter_meta_box')); add_action('save_post', array($this, 'save_chapter_data')); } // 添加章节管理元框 public function add_chapter_meta_box() { $post_types = apply_filters('vet_supported_post_types', array('post', 'page')); foreach ($post_types as $post_type) { add_meta_box( 'vet-chapter-manager', '视频章节管理', array($this, 'render_chapter_meta_box'), $post_type, 'normal', 'high' ); } } // 渲染章节管理界面 public function render_chapter_meta_box($post) { // 获取文章中的视频 $videos = $this->extract_videos_from_content($post->post_content); if (empty($videos)) { echo '<p>本文中没有找到视频。请先添加视频到内容中。</p>'; return; } // 添加非ce字段 wp_nonce_field('vet_save_chapters', 'vet_chapter_nonce'); echo '<div class="vet-chapter-manager">'; foreach ($videos as $index => $video) { $video_id = $video['id']; $chapters = $this->get_chapters_for_video($post->ID, $video_id); echo '<div class="vet-video-section">'; echo '<h3>视频 #' . ($index + 1) . '</h3>'; echo '<div class="vet-video-preview">' . $video['html'] . '</div>'; // 章节管理界面 $this->render_chapter_editor($video_id, $chapters); echo '</div>'; } echo '</div>'; } // 渲染章节编辑器 private function render_chapter_editor($video_id, $chapters) { ?> <div class="vet-chapter-editor" data-video-id="<?php echo esc_attr($video_id); ?>"> <div class="vet-chapter-header"> <h4>章节管理</h4> <button type="button" class="button vet-add-chapter">添加章节</button> </div> <div class="vet-chapters-list"> <?php if (!empty($chapters)): ?> <?php foreach ($chapters as $chapter): ?> <div class="vet-chapter-item" data-timestamp="<?php echo esc_attr($chapter['timestamp']); ?>"> <input type="hidden" name="vet_chapters[<?php echo esc_attr($video_id); ?>][timestamp][]" value="<?php echo esc_attr($chapter['timestamp']); ?>"> <input type="hidden" name="vet_chapters[<?php echo esc_attr($video_id); ?>][title][]" value="<?php echo esc_attr($chapter['title']); ?>"> <div class="vet-chapter-preview"> <span class="vet-chapter-time"><?php echo $this->format_time($chapter['timestamp']); ?></span> <span class="vet-chapter-title"><?php echo esc_html($chapter['title']); ?></span> </div> <div class="vet-chapter-actions"> <button type="button" class="button vet-edit-chapter">编辑</button> <button type="button" class="button button-link vet-remove-chapter">删除</button> </div> </div> <?php endforeach; ?> <?php else: ?> <p class="vet-no-chapters">暂无章节,点击"添加章节"按钮创建</p> <?php endif; ?> </div> <!-- 章节编辑模板 --> <div class="vet-chapter-template" style="display: none;"> <div class="vet-chapter-item"> <input type="hidden" name="vet_chapters[<?php echo esc_attr($video_id); ?>][timestamp][]" value=""> <input type="hidden" name="vet_chapters[<?php echo esc_attr($video_id); ?>][title][]" value=""> <div class="vet-chapter-preview"> <span class="vet-chapter-time">00:00</span> <span class="vet-chapter-title">新章节</span> </div> <div class="vet-chapter-actions"> <button type="button" class="button vet-edit-chapter">编辑</button> <button type="button" class="button button-link vet-remove-chapter">删除</button> </div> </div> </div> </div> <?php } // 从内容中提取视频 private function extract_videos_from_content($content) { $videos = array(); // 匹配视频短代码 preg_match_all('/]*]/', $content, $shortcode_matches); foreach ($shortcode_matches[0] as $shortcode) { $video_id = 'video-' . md5($shortcode); $videos[] = array( 'id' => $video_id, 'html' => do_shortcode($shortcode) ); } // 匹配HTML5视频标签 preg_match_all('/<video[^>]*>/', $content, $html_matches); foreach ($html_matches[0] as $index => $video_tag) { $video_id = 'html5-video-' . $index; $videos[] = array( 'id' => $video_id, 'html' => $video_tag . '</video>' ); } return $videos; } // 保存章节数据 public function save_chapter_data($post_id) { // 检查nonce if (!isset($_POST['vet_chapter_nonce']) || !wp_verify_nonce($_POST['vet_chapter_nonce'], 'vet_save_chapters')) { return; } // 检查权限 if (!current_user_can('edit_post', $post_id)) { return; } // 保存章节数据 if (isset($_POST['vet_chapters']) && is_array($_POST['vet_chapters'])) { foreach ($_POST['vet_chapters'] as $video_id => $chapters_data) { $chapters = array(); if (isset($chapters_data['timestamp']) && isset($chapters_data['title'])) { $timestamps = $chapters_data['timestamp']; $titles = $chapters_data['title']; for ($i = 0; $i < count($timestamps); $i++) { if (!empty($timestamps[$i]) && !empty($titles[$i])) { $chapters[] = array( 'timestamp' => floatval($timestamps[$i]), 'title' => sanitize_text_field($titles[$i]) ); } } } // 按时间戳排序 usort($chapters, function($a, $b) { return $a['timestamp'] - $b['timestamp']; }); // 保存到post meta update_post_meta($post_id, '_vet_video_chapters_' . sanitize_key($video_id), $chapters); } } } // 格式化时间显示 private function format_time($seconds) { $mins = floor($seconds / 60); $secs = floor($seconds % 60); return sprintf('%02d:%02d', $mins, $secs); } } 4.2 章节编辑器前端交互 // assets/js/admin-scripts.js (function($) { 'use strict'; // 章节编辑器对象 var ChapterEditor = { init: function() { this.bindEvents(); this.initSortable(); }, bindEvents: function() { // 添加章节按钮 $(document).on('click', '.vet-add-chapter', function() { ChapterEditor.addNewChapter($(this).closest('.vet-chapter-editor')); }); // 编辑章节按钮 $(document).on('click', '.vet-edit-chapter', function() { ChapterEditor.editChapter($(this).closest('.vet-chapter-item')); }); // 删除章节按钮 $(document).on('click', '.vet-remove-chapter', function() { ChapterEditor.removeChapter($(this).closest('.vet-chapter-item')); }); // 视频时间点击事件 $(document).on('click', '.vet-video-preview video', function() { var $video = $(this); var currentTime = $video[0].currentTime; var $editor = $video.closest('.vet-video-section').find('.vet-chapter-editor'); // 在章节编辑器中显示当前时间 ChapterEditor.showCurrentTime($editor, currentTime); }); }, // 初始化可排序 initSortable: function() { $('.vet-chapters-list').sortable({ handle: '.vet-chapter-preview', update: function() { ChapterEditor.updateChapterOrder($(this)); } }); }, // 添加新章节 addNewChapter: function($editor) { var $template = $editor.find('.vet-chapter-template .vet-chapter-item').clone(); var $list = $editor.find('.vet-chapters-list'); // 移除"暂无章节"提示 $list.find('.vet-no-chapters').remove(); // 添加到列表 $list.append($template); // 编辑新章节 ChapterEditor.editChapter($template); }, // 编辑章节 editChapter: function($chapterItem) { var $preview = $chapterItem.find('.vet-chapter-preview'); var $timeSpan = $preview.find('.vet-chapter-time'); var $titleSpan = $preview.find('.vet-chapter-title'); var currentTime = $timeSpan.text(); var currentTitle = $titleSpan.text(); // 创建编辑表单 var $form = $('<div class="vet-chapter-edit-form"></div>'); $form.html(` <div class="vet-edit-fields"> <div class="vet-field"> <label>时间戳 (秒):</label> <input type="number" class="vet-time-input" step="0.1" min="0" value="${ChapterEditor.timeToSeconds(currentTime)}"> </div> <div class="vet-field"> <label>章节标题:</label> <input type="text" class="vet-title-input" value="${currentTitle}"> </div> <div class="vet-edit-actions"> <button type="button" class="button button-primary vet-save-chapter">保存</button> <button type="button" class="button vet-cancel-edit">取消</button> </div> </div> `); // 替换预览为编辑表单 $preview.hide(); $chapterItem.find('.vet-chapter-actions').hide(); $chapterItem.append($form); // 绑定保存事件 $form.find('.vet-save-chapter').on('click', function() { var timeInput = $form.find('.vet-time-input').val(); var titleInput = $form.find('.vet-title-input').val(); if (timeInput && titleInput) { // 更新时间戳和标题 $chapterItem.find('input[name*="timestamp"]').val(timeInput); $chapterItem.find('input[name*="title"]').val(titleInput); // 更新预览显示 $timeSpan.text(ChapterEditor.secondsToTime(timeInput)); $titleSpan.text(titleInput); } // 恢复显示 $form.remove(); $preview.show(); $chapterItem.find('.vet-chapter-actions').show(); }); // 绑定取消事件 $form.find('.vet-cancel-edit').on('click', function() { $form.remove(); $preview.show(); $chapterItem.find('.vet-chapter-actions').show(); }); }, // 删除章节 removeChapter: function($chapterItem) { if (confirm('确定要删除这个章节吗?')) { $chapterItem.remove(); // 如果没有章节了,显示提示 var $list = $chapterItem.closest('.vet-chapters-list'); if ($list.children('.vet-chapter-item').length === 0) { $list.html('<p class="vet-no-chapters">暂无章节,点击"添加章节"按钮创建</p>'); } } }, // 显示当前时间 showCurrentTime: function($editor, currentTime) { // 创建或更新时间提示 var $timeHint = $editor.find('.vet-current-time-hint'); if ($timeHint.length === 0) { $timeHint = $('<div class="vet-current-time-hint"></div>'); $editor.find('.vet-chapter-header').after($timeHint); } var timeStr = ChapterEditor.secondsToTime(currentTime); $timeHint.html('当前视频时间: <strong>' + timeStr + '</strong> (点击视频可获取当前时间)'); // 3秒后淡出 $timeHint.show(); setTimeout(function() { $timeHint.fadeOut(); }, 3000); }, // 更新章节顺序 updateChapterOrder: function($list) { // 重新排序后,可以在这里添加额外的处理逻辑 console.log('章节顺序已更新'); }, // 时间字符串转秒数 timeToSeconds: function(timeStr) { var parts = timeStr.split(':'); if (parts.length === 2) { return parseInt(parts[0]) * 60 + parseInt(parts[1]); } return 0; }, // 秒数转时间字符串 secondsToTime: function(seconds) { var mins = Math.floor(seconds / 60); var secs = Math.floor(seconds % 60); return (mins < 10 ? '0' : '') + mins + ':' + (secs < 10 ? '0' : '') + secs; } }; // 初始化 $(document).ready(function() { ChapterEditor.init(); }); })(jQuery); 4.3 章节标记样式设计 /* assets/css/admin-styles.css */ /* 章节管理器样式 */ .vet-chapter-manager { padding: 15px; background: #f8f9fa; border-radius: 5px; } .vet-video-section { margin-bottom: 30px; padding: 20px; background: white; border: 1px solid #ddd; border-radius: 5px; } .vet-video-section h3 { margin-top: 0; color: #23282d; border-bottom: 2px solid #0073aa; padding-bottom: 10px; } .vet-video-preview { margin: 15px 0; text-align: center; } .vet-video-preview video {

发表评论

WordPress集成教程,连接酒店预订API实现房源查询与在线预订功能

WordPress集成教程:连接酒店预订API实现房源查询与在线预订功能 引言:WordPress作为多功能开发平台的潜力 在当今数字化时代,酒店和住宿行业正经历着前所未有的转型。随着在线预订成为主流,拥有一个功能齐全、用户友好的网站对于酒店经营者至关重要。WordPress作为全球最受欢迎的内容管理系统,早已超越了简单的博客平台定位,通过其强大的可扩展性,可以转变为功能丰富的业务管理工具。 本教程将深入探讨如何通过WordPress代码二次开发,连接酒店预订API,实现专业的房源查询与在线预订功能。我们将从基础概念讲起,逐步深入到实际代码实现,最终打造一个完整的酒店预订系统。无论您是WordPress开发者、酒店经营者还是对网站集成感兴趣的技术爱好者,本教程都将为您提供实用的指导和解决方案。 第一部分:准备工作与环境搭建 1.1 理解酒店预订API的基本原理 酒店预订API(应用程序编程接口)是连接您的网站与酒店库存管理系统之间的桥梁。它允许您的WordPress网站实时访问房型信息、价格、可用性,并处理预订请求。常见的酒店API提供商包括Booking.com、Expedia、Hotelbeds等,也有许多专门为中小型酒店设计的API服务。 API通常通过RESTful架构或SOAP协议工作,使用JSON或XML格式传输数据。在开始集成前,您需要: 注册并获取API密钥和访问令牌 了解API的端点(Endpoints)、请求方法和参数 熟悉API的响应数据结构和错误处理机制 1.2 WordPress开发环境配置 为了顺利进行二次开发,您需要搭建合适的开发环境: 本地开发环境:使用XAMPP、MAMP或Local by Flywheel搭建本地WordPress环境 代码编辑器:推荐使用VS Code、PHPStorm或Sublime Text 必备插件: Advanced Custom Fields(用于自定义字段管理) Custom Post Type UI(创建自定义文章类型) Query Monitor(调试数据库查询和API请求) 启用WordPress调试模式:在wp-config.php中添加以下代码: define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); define('WP_DEBUG_DISPLAY', false); 1.3 创建自定义文章类型管理房源 在WordPress中,我们可以使用自定义文章类型(Custom Post Type)来管理酒店房源: // 在主题的functions.php文件中添加以下代码 function register_hotel_room_post_type() { $labels = array( 'name' => '酒店房源', 'singular_name' => '房源', 'menu_name' => '酒店管理', 'name_admin_bar' => '房源', 'add_new' => '添加新房源', 'add_new_item' => '添加新房源', 'new_item' => '新房源', 'edit_item' => '编辑房源', 'view_item' => '查看房源', 'all_items' => '所有房源', 'search_items' => '搜索房源', 'parent_item_colon' => '父房源:', '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' => 'hotel-room'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'menu_icon' => 'dashicons-building', 'supports' => array('title', 'editor', 'thumbnail', 'excerpt') ); register_post_type('hotel_room', $args); } add_action('init', 'register_hotel_room_post_type'); 第二部分:酒店预订API集成核心实现 2.1 API连接类设计与实现 创建一个专门的类来处理与酒店预订API的通信: class Hotel_API_Connector { private $api_key; private $api_secret; private $base_url; private $access_token; private $token_expiry; public function __construct($api_key, $api_secret, $base_url) { $this->api_key = $api_key; $this->api_secret = $api_secret; $this->base_url = $base_url; $this->access_token = $this->get_stored_token(); } // 获取访问令牌 private function authenticate() { $response = wp_remote_post($this->base_url . '/auth', array( 'body' => json_encode(array( 'api_key' => $this->api_key, 'api_secret' => $this->api_secret )), 'headers' => array('Content-Type' => 'application/json'), 'timeout' => 30 )); if (is_wp_error($response)) { error_log('酒店API认证失败: ' . $response->get_error_message()); return false; } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (isset($data['access_token'])) { $this->access_token = $data['access_token']; $this->token_expiry = time() + $data['expires_in']; // 存储令牌到数据库 update_option('hotel_api_token', array( 'token' => $this->access_token, 'expiry' => $this->token_expiry )); return true; } return false; } // 获取存储的令牌 private function get_stored_token() { $token_data = get_option('hotel_api_token', array()); if (!empty($token_data) && isset($token_data['expiry']) && $token_data['expiry'] > time()) { return $token_data['token']; } return null; } // 发送API请求 public function make_request($endpoint, $method = 'GET', $params = array()) { // 检查令牌有效性 if (!$this->access_token || $this->token_expiry <= time()) { if (!$this->authenticate()) { return new WP_Error('auth_failed', 'API认证失败'); } } $url = $this->base_url . $endpoint; $args = array( 'method' => $method, 'headers' => array( 'Authorization' => 'Bearer ' . $this->access_token, 'Content-Type' => 'application/json' ), 'timeout' => 30 ); if (!empty($params)) { if ($method === 'GET') { $url = add_query_arg($params, $url); } else { $args['body'] = json_encode($params); } } $response = wp_remote_request($url, $args); if (is_wp_error($response)) { error_log('酒店API请求失败: ' . $response->get_error_message()); return $response; } $response_code = wp_remote_retrieve_response_code($response); $response_body = wp_remote_retrieve_body($response); if ($response_code >= 400) { error_log('酒店API错误响应: ' . $response_body); return new WP_Error('api_error', 'API返回错误: ' . $response_code, $response_body); } return json_decode($response_body, true); } // 搜索可用房源 public function search_rooms($check_in, $check_out, $guests = 2, $rooms = 1) { $endpoint = '/api/v1/rooms/search'; $params = array( 'check_in' => $check_in, 'check_out' => $check_out, 'guests' => $guests, 'rooms' => $rooms ); return $this->make_request($endpoint, 'GET', $params); } // 获取房源详情 public function get_room_details($room_id) { $endpoint = '/api/v1/rooms/' . $room_id; return $this->make_request($endpoint); } // 创建预订 public function create_booking($room_id, $check_in, $check_out, $guest_info, $payment_info) { $endpoint = '/api/v1/bookings'; $params = array( 'room_id' => $room_id, 'check_in' => $check_in, 'check_out' => $check_out, 'guest_info' => $guest_info, 'payment_info' => $payment_info ); return $this->make_request($endpoint, 'POST', $params); } // 取消预订 public function cancel_booking($booking_id) { $endpoint = '/api/v1/bookings/' . $booking_id . '/cancel'; return $this->make_request($endpoint, 'POST'); } } 2.2 房源数据同步机制 为了确保WordPress中的房源信息与API数据同步,我们需要建立定期同步机制: class Hotel_Room_Sync { private $api_connector; public function __construct($api_connector) { $this->api_connector = $api_connector; $this->init_hooks(); } private function init_hooks() { // 每天凌晨同步房源数据 if (!wp_next_scheduled('daily_hotel_room_sync')) { wp_schedule_event(strtotime('02:00:00'), 'daily', 'daily_hotel_room_sync'); } add_action('daily_hotel_room_sync', array($this, 'sync_all_rooms')); // 手动同步触发器 add_action('admin_post_sync_hotel_rooms', array($this, 'handle_manual_sync')); } // 同步所有房源 public function sync_all_rooms() { // 获取API中的所有房源 $api_rooms = $this->api_connector->make_request('/api/v1/rooms'); if (is_wp_error($api_rooms)) { error_log('同步房源失败: ' . $api_rooms->get_error_message()); return false; } $synced_count = 0; $updated_count = 0; foreach ($api_rooms as $api_room) { $room_id = $this->sync_single_room($api_room); if ($room_id) { if (get_post_meta($room_id, '_last_api_sync', true) == $api_room['updated_at']) { $synced_count++; } else { $updated_count++; } } } // 记录同步结果 update_option('last_room_sync', array( 'time' => current_time('mysql'), 'synced' => $synced_count, 'updated' => $updated_count, 'total' => count($api_rooms) )); return true; } // 同步单个房源 private function sync_single_room($api_room) { // 检查是否已存在 $existing_posts = get_posts(array( 'post_type' => 'hotel_room', 'meta_key' => '_api_room_id', 'meta_value' => $api_room['id'], 'posts_per_page' => 1 )); $post_data = array( 'post_title' => $api_room['name'], 'post_content' => $api_room['description'], 'post_type' => 'hotel_room', 'post_status' => 'publish' ); if (!empty($existing_posts)) { $post_data['ID'] = $existing_posts[0]->ID; $post_id = wp_update_post($post_data); } else { $post_id = wp_insert_post($post_data); } if (is_wp_error($post_id) || $post_id == 0) { error_log('创建/更新房源失败: ' . print_r($post_id, true)); return false; } // 更新自定义字段 update_post_meta($post_id, '_api_room_id', $api_room['id']); update_post_meta($post_id, '_room_type', $api_room['type']); update_post_meta($post_id, '_max_guests', $api_room['max_guests']); update_post_meta($post_id, '_base_price', $api_room['base_price']); update_post_meta($post_id, '_amenities', $api_room['amenities']); update_post_meta($post_id, '_last_api_sync', $api_room['updated_at']); // 处理房源图片 if (!empty($api_room['images'])) { $this->sync_room_images($post_id, $api_room['images']); } return $post_id; } // 同步房源图片 private function sync_room_images($post_id, $images) { $attachment_ids = array(); foreach ($images as $index => $image_url) { // 下载并设置特色图片 $attachment_id = $this->upload_image_from_url($image_url, $post_id); if ($attachment_id && $index === 0) { set_post_thumbnail($post_id, $attachment_id); } if ($attachment_id) { $attachment_ids[] = $attachment_id; } } // 保存到图库 if (!empty($attachment_ids)) { update_post_meta($post_id, '_room_gallery', $attachment_ids); } } // 从URL上传图片 private function upload_image_from_url($image_url, $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'); // 检查是否已存在相同图片 $existing_attachment = $this->get_attachment_by_url($image_url); if ($existing_attachment) { return $existing_attachment; } // 下载图片 $tmp = download_url($image_url); if (is_wp_error($tmp)) { error_log('下载图片失败: ' . $tmp->get_error_message()); return false; } // 准备文件数据 $file_array = array( 'name' => basename($image_url), 'tmp_name' => $tmp ); // 上传到媒体库 $attachment_id = media_handle_sideload($file_array, $post_id); if (is_wp_error($attachment_id)) { @unlink($file_array['tmp_name']); error_log('上传图片失败: ' . $attachment_id->get_error_message()); return false; } // 保存原始URL以便后续识别 update_post_meta($attachment_id, '_original_image_url', $image_url); return $attachment_id; } // 根据URL获取已存在的附件 private function get_attachment_by_url($url) { global $wpdb; $query = $wpdb->prepare( "SELECT post_id FROM $wpdb->postmeta WHERE meta_key = '_original_image_url' AND meta_value = %s", $url ); $attachment_id = $wpdb->get_var($query); return $attachment_id ? intval($attachment_id) : false; } } 第三部分:前端功能实现与用户界面 3.1 房源搜索与筛选功能 创建一个短代码来显示房源搜索表单: // 搜索表单短代码 function hotel_search_form_shortcode($atts) { ob_start(); ?> <div class="hotel-search-form"> <form id="hotel-search-form" method="GET" action="<?php echo esc_url(get_permalink(get_option('hotel_search_results_page'))); ?>"> <div class="search-row"> <div class="form-group"> <label for="check-in">入住日期</label> <input type="date" id="check-in" name="check_in" value="<?php echo isset($_GET['check_in']) ? esc_attr($_GET['check_in']) : date('Y-m-d'); ?>" min="<?php echo date('Y-m-d'); ?>" required> </div> <div class="form-group"> <label for="check-out">退房日期</label> <input type="date" id="check-out" name="check_out" value="<?php echo isset($_GET['check_out']) ? esc_attr($_GET['check_out']) : date('Y-m-d', strtotime('+1 day')); ?>" min="<?php echo date('Y-m-d', strtotime('+1 day')); ?>" required> </div> <div class="form-group"> <label for="guests">入住人数</label> <select id="guests" name="guests"> <?php for ($i = 1; $i <= 10; $i++): ?> <option value="<?php echo $i; ?>" <?php selected(isset($_GET['guests']) ? $_GET['guests'] : 2, $i); ?>> <?php echo $i; ?> 人 </option> <?php endfor; ?> </select> </div> <div class="form-group"> <label for="rooms">房间数</label> <select id="rooms" name="rooms"> <?php for ($i = 1; $i <= 5; $i++): ?> <option value="<?php echo $i; ?>" <?php selected(isset($_GET['rooms']) ? $_GET['rooms'] : 1, $i); ?>> <?php echo $i; ?> 间 </option> <?php endfor; ?> </select> </div> </div> <div class="search-row"> <div class="form-group"> <label for="room-type">房型</label> <select id="room-type" name="room_type"> <option value="">所有房型</option> <option value="single" <?php selected(isset($_GET['room_type']) ? $_GET['room_type'] : '', 'single'); ?>>单人间</option> <option value="double" <?php selected(isset($_GET['room_type']) ? $_GET['room_type'] : '', 'double'); ?>>双人间</option> <option value="suite" <?php selected(isset($_GET['room_type']) ? $_GET['room_type'] : '', 'suite'); ?>>套房</option> <option value="family" <?php selected(isset($_GET['room_type']) ? $_GET['room_type'] : '', 'family'); ?>>家庭房</option> </select> </div> <div class="form-group"> <label for="price-min">价格范围</label> <div class="price-range"> <input type="number" id="price-min" name="price_min" placeholder="最低价" value="<?php echo isset($_GET['price_min']) ? esc_attr($_GET['price_min']) : ''; ?>"> <span>至</span> <input type="number" id="price-max" name="price_max" placeholder="最高价" value="<?php echo isset($_GET['price_max']) ? esc_attr($_GET['price_max']) : ''; ?>"> </div> </div> <div class="form-group"> <label for="amenities">设施</label> <select id="amenities" name="amenities[]" multiple class="multi-select"> <option value="wifi" <?php selected(in_array('wifi', isset($_GET['amenities']) ? (array)$_GET['amenities'] : array()), true); ?>>WiFi</option> <option value="breakfast" <?php selected(in_array('breakfast', isset($_GET['amenities']) ? (array)$_GET['amenities'] : array()), true); ?>>早餐</option> <option value="parking" <?php selected(in_array('parking', isset($_GET['amenities']) ? (array)$_GET['amenities'] : array()), true); ?>>停车</option> <option value="pool" <?php selected(in_array('pool', isset($_GET['amenities']) ? (array)$_GET['amenities'] : array()), true); ?>>泳池</option> <option value="gym" <?php selected(in_array('gym', isset($_GET['amenities']) ? (array)$_GET['amenities'] : array()), true); ?>>健身房</option> </select> </div> </div> <div class="form-submit"> <button type="submit" class="search-button">搜索房源</button> <button type="button" class="reset-button" onclick="resetSearchForm()">重置条件</button> </div> </form> </div> <script> function resetSearchForm() { document.getElementById('hotel-search-form').reset(); // 重置日期为默认值 document.getElementById('check-in').value = '<?php echo date("Y-m-d"); ?>'; document.getElementById('check-out').value = '<?php echo date("Y-m-d", strtotime("+1 day")); ?>'; } // 日期验证 document.getElementById('check-in').addEventListener('change', function() { var checkIn = new Date(this.value); var checkOut = new Date(document.getElementById('check-out').value); if (checkIn >= checkOut) { var nextDay = new Date(checkIn); nextDay.setDate(nextDay.getDate() + 1); document.getElementById('check-out').value = nextDay.toISOString().split('T')[0]; } // 设置退房日期的最小值 document.getElementById('check-out').min = this.value; }); </script> <style> .hotel-search-form { background: #f8f9fa; padding: 25px; border-radius: 10px; box-shadow: 0 2px 15px rgba(0,0,0,0.1); margin: 20px 0; } .search-row { display: flex; flex-wrap: wrap; gap: 20px; margin-bottom: 20px; } .form-group { flex: 1; min-width: 200px; } .form-group label { display: block; margin-bottom: 8px; font-weight: 600; color: #333; } .form-group input, .form-group select { width: 100%; padding: 10px; border: 1px solid #ddd; border-radius: 5px; font-size: 14px; } .price-range { display: flex; align-items: center; gap: 10px; } .price-range input { flex: 1; } .multi-select { height: 100px; } .form-submit { text-align: center; margin-top: 20px; } .search-button, .reset-button { padding: 12px 30px; border: none; border-radius: 5px; font-size: 16px; cursor: pointer; transition: all 0.3s ease; } .search-button { background: #007cba; color: white; margin-right: 10px; } .search-button:hover { background: #005a87; } .reset-button { background: #6c757d; color: white; } .reset-button:hover { background: #545b62; } @media (max-width: 768px) { .search-row { flex-direction: column; } .form-group { min-width: 100%; } } </style> <?php return ob_get_clean(); } add_shortcode('hotel_search', 'hotel_search_form_shortcode'); 3.2 房源搜索结果展示 创建搜索结果页面模板: // 搜索结果处理函数 function display_hotel_search_results() { if (!isset($_GET['check_in']) || !isset($_GET['check_out'])) { return '<div class="hotel-search-message">请先选择入住和退房日期</div>'; } $check_in = sanitize_text_field($_GET['check_in']); $check_out = sanitize_text_field($_GET['check_out']); $guests = isset($_GET['guests']) ? intval($_GET['guests']) : 2; $rooms = isset($_GET['rooms']) ? intval($_GET['rooms']) : 1; // 获取API连接实例 $api_connector = new Hotel_API_Connector( get_option('hotel_api_key'), get_option('hotel_api_secret'), get_option('hotel_api_base_url') ); // 搜索可用房源 $search_params = array( 'check_in' => $check_in, 'check_out' => $check_out, 'guests' => $guests, 'rooms' => $rooms ); // 添加筛选条件 if (!empty($_GET['room_type'])) { $search_params['room_type'] = sanitize_text_field($_GET['room_type']); } if (!empty($_GET['price_min'])) { $search_params['price_min'] = floatval($_GET['price_min']); } if (!empty($_GET['price_max'])) { $search_params['price_max'] = floatval($_GET['price_max']); } if (!empty($_GET['amenities'])) { $search_params['amenities'] = array_map('sanitize_text_field', (array)$_GET['amenities']); } $available_rooms = $api_connector->search_rooms($check_in, $check_out, $guests, $rooms); if (is_wp_error($available_rooms)) { return '<div class="hotel-search-error">搜索失败:' . esc_html($available_rooms->get_error_message()) . '</div>'; } if (empty($available_rooms)) { return '<div class="hotel-search-empty">未找到符合条件的房源,请尝试调整搜索条件。</div>'; } ob_start(); ?> <div class="hotel-search-results"> <div class="search-summary"> <h3>搜索结果</h3> <p>入住:<?php echo date('Y年m月d日', strtotime($check_in)); ?> - 退房:<?php echo date('Y年m月d日', strtotime($check_out)); ?> | 共 <?php echo count($available_rooms); ?> 个房源可选</p> </div> <div class="results-controls"> <div class="sort-options"> <label>排序:</label> <select id="sort-results" onchange="sortRooms(this.value)"> <option value="price_asc">价格从低到高</option> <option value="price_desc">价格从高到低</option> <option value="rating_desc">评分从高到低</option> <option value="name_asc">名称A-Z</option> </select> </div> <div class="view-options"> <button class="view-btn active" onclick="changeView('grid')" title="网格视图"> <span class="dashicons dashicons-grid-view"></span> </button> <button class="view-btn" onclick="changeView('list')" title="列表视图"> <span class="dashicons dashicons-list-view"></span> </button> </div> </div> <div class="rooms-container grid-view"> <?php foreach ($available_rooms as $room): ?> <div class="room-card" data-price="<?php echo $room['price_per_night']; ?>" data-rating="<?php echo $room['rating'] ?? 0; ?>" data-name="<?php echo esc_attr($room['name']); ?>"> <div class="room-image"> <?php if (!empty($room['image'])): ?> <img src="<?php echo esc_url($room['image']); ?>" alt="<?php echo esc_attr($room['name']); ?>"> <?php else: ?> <div class="no-image">暂无图片</div> <?php endif; ?> <?php if (isset($room['discount']) && $room['discount'] > 0): ?> <span class="discount-badge">-<?php echo $room['discount']; ?>%</span> <?php endif; ?> </div> <div class="room-info"> <h3 class="room-title"> <a href="<?php echo get_permalink(); ?>?room_id=<?php echo $room['id']; ?>&check_in=<?php echo $check_in; ?>&check_out=<?php echo $check_out; ?>"> <?php echo esc_html($room['name']); ?> </a> </h3> <div class="room-meta"> <span class="room-type"><?php echo esc_html($room['type']); ?></span> <span class="room-guests">最多 <?php echo $room['max_guests']; ?> 人</span> <span class="room-size"><?php echo $room['size'] ?? '--'; ?> ㎡</span> </div> <div class="room-amenities"> <?php if (!empty($room['amenities'])): ?> <?php $amenities_display = array_slice($room['amenities'], 0, 3); foreach ($amenities_display as $amenity): ?> <span class="amenity-tag"><?php echo esc_html($amenity); ?></span> <?php endforeach; ?> <?php if (count($room['amenities']) > 3): ?> <span class="amenity-more">+<?php echo count($room['amenities']) - 3; ?> 更多</span> <?php endif; ?> <?php endif; ?> </div> <div class="room-rating"> <?php if (isset($room['rating'])): ?> <div class="stars"> <?php $full_stars = floor($room['rating']); $half_star = ($room['rating'] - $full_stars) >= 0.5; for ($i = 1; $i <= 5; $i++): if ($i <= $full_stars): ?> <span class="dashicons dashicons-star-filled"></span> <?php elseif ($half_star && $i == $full_stars + 1): ?> <span class="dashicons dashicons-star-half"></span> <?php else: ?> <span class="dashicons dashicons-star-empty"></span> <?php endif; endfor; ?> </div> <span class="rating-score"><?php echo number_format($room['rating'], 1); ?></span> <?php endif; ?> </div> <div class="room-price"> <div class="price-main"> <span class="currency">¥</span> <span class="amount"><?php echo number_format($room['price_per_night'], 0); ?></span> <span class="period">/晚</span> </div> <?php if (isset($room['original_price']) && $room['original_price'] > $room['price_per_night']): ?> <div class="price-original"> ¥<?php echo number_format($room['original_price'], 0); ?> </div> <?php endif; ?> <div class="price-total"> 总计:¥<?php echo number_format($room['total_price'], 0); ?> </div> </div> <div class="room-actions"> <a href="<?php echo get_permalink(); ?>?room_id=<?php echo $room['id']; ?>&check_in=<?php echo $check_in; ?>&check_out=<?php echo $check_out; ?>&action=book" class="book-button"> 立即预订 </a> <button class="view-details" onclick="showRoomDetails('<?php echo $room['id']; ?>')"> 查看详情 </button> </div> </div> </div> <?php endforeach; ?> </div> <!-- 分页 --> <?php if (count($available_rooms) > 12): ?> <div class="pagination"> <button class="page-btn prev" disabled>上一页</button> <span class="page-numbers">1 / <?php echo ceil(count($available_rooms) / 12); ?></span> <button class="page-btn next">下一页</button> </div> <?php endif; ?> </div> <!-- 房源详情模态框 --> <div id="room-details-modal" class="modal"> <div class="modal-content"> <span class="close-modal">&times;</span> <div id="room-details-content"></div> </div> </div> <script> // 排序功能 function sortRooms(sortBy) { const container = document.querySelector('.rooms-container'); const rooms = Array.from(container.querySelectorAll('.room-card')); rooms.sort((a, b) => { let aValue, bValue; switch(sortBy) { case 'price_asc': aValue = parseFloat(a.dataset.price); bValue = parseFloat(b.dataset.price); return aValue - bValue; case 'price_desc': aValue = parseFloat(a.dataset.price); bValue = parseFloat(b.dataset.price); return bValue - aValue; case 'rating_desc': aValue = parseFloat(a.dataset.rating); bValue = parseFloat(b.dataset.rating); return bValue - aValue; case 'name_asc': aValue = a.dataset.name.toLowerCase(); bValue = b.dataset.name.toLowerCase(); return aValue.localeCompare(bValue); default: return 0; } }); // 重新排列DOM元素 rooms.forEach(room => container.appendChild(room)); } // 切换视图 function changeView(viewType) { const container = document.querySelector('.rooms-container'); const buttons = document.querySelectorAll('.view-btn'); buttons.forEach(btn => btn.classList.remove('active')); event.target.classList.add('active'); container.classList.remove('grid-view', 'list-view'); container.classList.add(viewType + '-view'); } // 显示房源详情 function showRoomDetails(roomId) { // 使用AJAX获取房源详情 fetch('<?php echo admin_url('admin-ajax.php'); ?>', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, body: 'action=get_room_details&room_id=' + roomId + '&check_in=<?php echo $check_in; ?>' +

发表评论

详细教程,为WordPress网站开发线上研讨会预约与虚拟会议室管理

详细教程:为WordPress网站开发线上研讨会预约与虚拟会议室管理系统 引言:线上活动管理的数字化需求 在数字化时代,线上研讨会已成为企业、教育机构和组织进行知识分享、产品发布和团队协作的重要方式。然而,许多WordPress网站所有者面临一个共同挑战:如何高效管理线上活动的预约流程和虚拟会议室?虽然市场上有一些插件可用,但它们往往功能有限、价格昂贵或不符合特定需求。 本教程将指导您通过WordPress代码二次开发,创建一个完整的线上研讨会预约与虚拟会议室管理系统。这个解决方案不仅成本效益高,而且完全可定制,能够满足您的特定需求。我们将从系统设计开始,逐步实现预约管理、虚拟会议室集成、自动通知和数据分析等功能。 第一部分:系统架构设计与环境准备 1.1 需求分析与功能规划 在开始编码之前,我们需要明确系统需求: 预约管理功能:用户可查看 upcoming 研讨会,选择并注册参加 虚拟会议室集成:自动生成或连接Zoom、Google Meet等平台会议室 自动化工作流:注册确认、提醒邮件、会议链接发送 管理后台:活动创建、参与者管理、数据分析 用户界面:直观的前端展示和响应式设计 1.2 开发环境配置 首先确保您的开发环境满足以下要求: WordPress 5.0以上版本 PHP 7.4以上版本 MySQL 5.6以上或MariaDB 10.0以上 代码编辑器(如VS Code、Sublime Text) 本地开发环境(如XAMPP、MAMP或Local by Flywheel) 1.3 创建自定义插件 为了避免主题更新导致代码丢失,我们将创建一个独立插件: 在wp-content/plugins/目录下创建新文件夹virtual-seminar-manager 创建主插件文件virtual-seminar-manager.php: <?php /** * Plugin Name: Virtual Seminar Manager * Plugin URI: https://yourwebsite.com/ * Description: 完整的线上研讨会预约与虚拟会议室管理系统 * Version: 1.0.0 * Author: Your Name * License: GPL v2 or later * Text Domain: virtual-seminar */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('VSM_VERSION', '1.0.0'); define('VSM_PLUGIN_DIR', plugin_dir_path(__FILE__)); define('VSM_PLUGIN_URL', plugin_dir_url(__FILE__)); // 初始化插件 require_once VSM_PLUGIN_DIR . 'includes/class-init.php'; VSM_Init::register_services(); 第二部分:数据库设计与自定义帖子类型 2.1 创建数据库表 我们需要两个自定义表来存储研讨会和预约数据。在插件初始化类中添加以下代码: // 在includes/class-init.php中 class VSM_Init { public static function register_services() { // 注册激活钩子 register_activation_hook(__FILE__, [self::class, 'activate']); // 注册其他服务 add_action('init', [self::class, 'register_post_types']); add_action('init', [self::class, 'register_taxonomies']); } public static function activate() { self::create_tables(); flush_rewrite_rules(); } private static function create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); // 研讨会表 $seminars_table = $wpdb->prefix . 'vsm_seminars'; $sql1 = "CREATE TABLE IF NOT EXISTS $seminars_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, post_id bigint(20) NOT NULL, start_time datetime NOT NULL, end_time datetime NOT NULL, max_participants int(11) DEFAULT 100, current_participants int(11) DEFAULT 0, meeting_platform varchar(50) DEFAULT 'zoom', meeting_id varchar(255), meeting_password varchar(100), meeting_url text, status varchar(20) DEFAULT 'scheduled', created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY post_id (post_id), KEY start_time (start_time) ) $charset_collate;"; // 预约表 $bookings_table = $wpdb->prefix . 'vsm_bookings'; $sql2 = "CREATE TABLE IF NOT EXISTS $bookings_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, seminar_id mediumint(9) NOT NULL, user_id bigint(20), user_email varchar(100) NOT NULL, user_name varchar(100) NOT NULL, booking_time datetime DEFAULT CURRENT_TIMESTAMP, confirmation_code varchar(32), attendance_status varchar(20) DEFAULT 'registered', join_time datetime, leave_time datetime, notes text, PRIMARY KEY (id), KEY seminar_id (seminar_id), KEY user_email (user_email), KEY confirmation_code (confirmation_code) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql1); dbDelta($sql2); } } 2.2 注册自定义帖子类型 创建研讨会自定义帖子类型,使其在WordPress后台可管理: // 在includes/class-post-types.php中 class VSM_Post_Types { public static function register() { // 注册研讨会帖子类型 $labels = array( 'name' => __('研讨会', 'virtual-seminar'), 'singular_name' => __('研讨会', 'virtual-seminar'), 'menu_name' => __('研讨会管理', 'virtual-seminar'), 'add_new' => __('添加新研讨会', 'virtual-seminar'), 'add_new_item' => __('添加新研讨会', 'virtual-seminar'), 'edit_item' => __('编辑研讨会', 'virtual-seminar'), 'new_item' => __('新研讨会', 'virtual-seminar'), 'view_item' => __('查看研讨会', 'virtual-seminar'), 'search_items' => __('搜索研讨会', 'virtual-seminar'), 'not_found' => __('未找到研讨会', 'virtual-seminar'), 'not_found_in_trash' => __('回收站中未找到研讨会', 'virtual-seminar') ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'seminar'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 25, 'menu_icon' => 'dashicons-video-alt3', 'supports' => array('title', 'editor', 'thumbnail', 'excerpt'), 'show_in_rest' => true ); register_post_type('seminar', $args); } } 第三部分:前端预约界面开发 3.1 创建预约短代码 为了让用户可以在任何页面添加预约界面,我们创建一个短代码: // 在includes/class-shortcodes.php中 class VSM_Shortcodes { public static function init() { add_shortcode('seminar_booking', [self::class, 'seminar_booking_shortcode']); add_shortcode('seminar_list', [self::class, 'seminar_list_shortcode']); } public static function seminar_booking_shortcode($atts) { // 解析短代码属性 $atts = shortcode_atts(array( 'seminar_id' => 0, 'show_title' => 'yes' ), $atts, 'seminar_booking'); // 如果没有指定研讨会ID,尝试从当前页面获取 $seminar_id = $atts['seminar_id']; if (!$seminar_id && get_post_type() === 'seminar') { $seminar_id = get_the_ID(); } if (!$seminar_id) { return '<p class="vsm-error">' . __('未指定研讨会', 'virtual-seminar') . '</p>'; } // 检查研讨会是否存在且可预约 $seminar_data = self::get_seminar_data($seminar_id); if (!$seminar_data) { return '<p class="vsm-error">' . __('研讨会不存在或已结束', 'virtual-seminar') . '</p>'; } // 输出预约表单 ob_start(); include VSM_PLUGIN_DIR . 'templates/booking-form.php'; return ob_get_clean(); } private static function get_seminar_data($post_id) { global $wpdb; $table_name = $wpdb->prefix . 'vsm_seminars'; $seminar = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE post_id = %d AND status = 'scheduled'", $post_id )); if (!$seminar) { return false; } // 检查是否还有空位 if ($seminar->current_participants >= $seminar->max_participants) { return false; } return $seminar; } } 3.2 设计预约表单模板 创建templates/booking-form.php模板文件: <?php /** * 研讨会预约表单模板 */ // 确保从短代码中调用 if (!isset($seminar_data)) { return; } // 获取帖子信息 $post = get_post($seminar_data->post_id); $title = $post->post_title; $excerpt = $post->post_excerpt; $thumbnail = get_the_post_thumbnail($post->ID, 'medium'); // 格式化时间 $start_time = date_i18n('Y年m月d日 H:i', strtotime($seminar_data->start_time)); $end_time = date_i18n('H:i', strtotime($seminar_data->end_time)); $remaining = $seminar_data->max_participants - $seminar_data->current_participants; // 处理表单提交 $submitted = false; $error = ''; $success = ''; if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['vsm_booking_nonce'])) { if (wp_verify_nonce($_POST['vsm_booking_nonce'], 'vsm_booking_action')) { $result = VSM_Booking_Handler::process_booking($_POST, $seminar_data); if ($result['success']) { $success = $result['message']; $submitted = true; } else { $error = $result['message']; } } } ?> <div class="vsm-booking-container"> <?php if ($atts['show_title'] === 'yes') : ?> <div class="vsm-seminar-header"> <?php if ($thumbnail) : ?> <div class="vsm-thumbnail"> <?php echo $thumbnail; ?> </div> <?php endif; ?> <h2 class="vsm-title"><?php echo esc_html($title); ?></h2> <?php if ($excerpt) : ?> <div class="vsm-excerpt"> <?php echo wpautop($excerpt); ?> </div> <?php endif; ?> <div class="vsm-meta"> <div class="vsm-meta-item"> <span class="dashicons dashicons-calendar-alt"></span> <span><?php echo $start_time . ' - ' . $end_time; ?></span> </div> <div class="vsm-meta-item"> <span class="dashicons dashicons-groups"></span> <span><?php printf(__('剩余席位: %d/%d', 'virtual-seminar'), $remaining, $seminar_data->max_participants); ?></span> </div> </div> </div> <?php endif; ?> <?php if ($success) : ?> <div class="vsm-alert vsm-success"> <?php echo $success; ?> </div> <?php elseif (!$submitted) : ?> <?php if ($error) : ?> <div class="vsm-alert vsm-error"> <?php echo $error; ?> </div> <?php endif; ?> <form id="vsm-booking-form" method="post" class="vsm-booking-form"> <?php wp_nonce_field('vsm_booking_action', 'vsm_booking_nonce'); ?> <input type="hidden" name="seminar_id" value="<?php echo $seminar_data->id; ?>"> <input type="hidden" name="post_id" value="<?php echo $seminar_data->post_id; ?>"> <div class="vsm-form-group"> <label for="vsm_user_name"><?php _e('姓名', 'virtual-seminar'); ?> *</label> <input type="text" id="vsm_user_name" name="user_name" required placeholder="<?php _e('请输入您的姓名', 'virtual-seminar'); ?>"> </div> <div class="vsm-form-group"> <label for="vsm_user_email"><?php _e('电子邮箱', 'virtual-seminar'); ?> *</label> <input type="email" id="vsm_user_email" name="user_email" required placeholder="<?php _e('请输入您的邮箱地址', 'virtual-seminar'); ?>"> <small class="vsm-help-text"><?php _e('会议链接将发送到此邮箱', 'virtual-seminar'); ?></small> </div> <div class="vsm-form-group"> <label for="vsm_user_phone"><?php _e('手机号码', 'virtual-seminar'); ?></label> <input type="tel" id="vsm_user_phone" name="user_phone" placeholder="<?php _e('请输入您的手机号码', 'virtual-seminar'); ?>"> </div> <div class="vsm-form-group"> <label for="vsm_user_company"><?php _e('公司/机构', 'virtual-seminar'); ?></label> <input type="text" id="vsm_user_company" name="user_company" placeholder="<?php _e('请输入您的公司或机构名称', 'virtual-seminar'); ?>"> </div> <div class="vsm-form-group"> <label for="vsm_notes"><?php _e('备注或问题', 'virtual-seminar'); ?></label> <textarea id="vsm_notes" name="notes" rows="3" placeholder="<?php _e('如有特殊需求或问题,请在此说明', 'virtual-seminar'); ?>"></textarea> </div> <div class="vsm-form-group vsm-consent"> <input type="checkbox" id="vsm_consent" name="consent" required> <label for="vsm_consent"> <?php _e('我同意接收关于此研讨会的通知和后续信息', 'virtual-seminar'); ?> </label> </div> <div class="vsm-form-submit"> <button type="submit" class="vsm-submit-btn"> <?php _e('立即预约', 'virtual-seminar'); ?> </button> </div> </form> <?php endif; ?> </div> 第四部分:虚拟会议室集成 4.1 Zoom API集成 我们将集成Zoom API来自动创建会议室。首先创建Zoom API处理类: // 在includes/class-zoom-integration.php中 class VSM_Zoom_Integration { private $api_key; private $api_secret; private $account_id; // JWT应用需要 private $client_id; // OAuth应用需要 private $client_secret; // OAuth应用需要 private $access_token; public function __construct() { $options = get_option('vsm_zoom_settings'); $this->api_key = $options['api_key'] ?? ''; $this->api_secret = $options['api_secret'] ?? ''; $this->account_id = $options['account_id'] ?? ''; $this->client_id = $options['client_id'] ?? ''; $this->client_secret = $options['client_secret'] ?? ''; // 获取或刷新访问令牌 $this->access_token = $this->get_access_token(); } /** * 获取OAuth访问令牌 */ private function get_access_token() { $token_data = get_transient('vsm_zoom_access_token'); if (!$token_data) { // 使用OAuth 2.0获取新令牌 $response = wp_remote_post('https://zoom.us/oauth/token', [ 'headers' => [ 'Authorization' => 'Basic ' . base64_encode($this->client_id . ':' . $this->client_secret), 'Content-Type' => 'application/x-www-form-urlencoded' ], 'body' => [ 'grant_type' => 'account_credentials', 'account_id' => $this->account_id ] ]); if (!is_wp_error($response)) { $body = json_decode(wp_remote_retrieve_body($response)); if (isset($body->access_token)) { $token_data = $body->access_token; // 令牌通常有效1小时,我们设置55分钟过期 set_transient('vsm_zoom_access_token', $token_data, 55 * MINUTE_IN_SECONDS); } } } return $token_data; } /** * 创建Zoom会议 */ public function create_meeting($seminar_data) { /v2/users/me/meetings', [ 'headers' => [ 'Authorization' => 'Bearer ' . $this->access_token, 'Content-Type' => 'application/json' ], 'body' => json_encode([ 'topic' => $seminar_data['title'], 'type' => 2, // 预定会议 'start_time' => $seminar_data['start_time'], 'duration' => $seminar_data['duration'], 'timezone' => wp_timezone_string(), 'password' => wp_generate_password(6, false), // 生成随机密码 'settings' => [ 'host_video' => true, 'participant_video' => true, 'join_before_host' => false, 'mute_upon_entry' => true, 'waiting_room' => true, 'auto_recording' => 'cloud' // 可选:none, local, cloud ] ]) ]); if (is_wp_error($response)) { return [ 'success' => false, 'error' => $response->get_error_message() ]; } $body = json_decode(wp_remote_retrieve_body($response), true); if (isset($body['code'])) { return [ 'success' => false, 'error' => $body['message'] ]; } return [ 'success' => true, 'meeting_id' => $body['id'], 'meeting_password' => $body['password'], 'join_url' => $body['join_url'], 'start_url' => $body['start_url'] // 主持人链接 ]; } /** * 更新Zoom会议 */ public function update_meeting($meeting_id, $data) { $response = wp_remote_request("https://api.zoom.us/v2/meetings/{$meeting_id}", [ 'method' => 'PATCH', 'headers' => [ 'Authorization' => 'Bearer ' . $this->access_token, 'Content-Type' => 'application/json' ], 'body' => json_encode($data) ]); return !is_wp_error($response); } /** * 删除Zoom会议 */ public function delete_meeting($meeting_id) { $response = wp_remote_request("https://api.zoom.us/v2/meetings/{$meeting_id}", [ 'method' => 'DELETE', 'headers' => [ 'Authorization' => 'Bearer ' . $this->access_token ] ]); return !is_wp_error($response); } } ### 4.2 Google Meet集成(替代方案) // 在includes/class-google-meet-integration.php中class VSM_Google_Meet_Integration { private $client; private $service; public function __construct() { $this->init_client(); } private function init_client() { require_once VSM_PLUGIN_DIR . 'vendor/autoload.php'; // 需要Google API客户端库 $options = get_option('vsm_google_settings'); $credentials = $options['credentials'] ?? ''; if (!$credentials) { return false; } try { $this->client = new Google_Client(); $this->client->setApplicationName('Virtual Seminar Manager'); $this->client->setAuthConfig(json_decode($credentials, true)); $this->client->addScope(Google_Service_Calendar::CALENDAR); // 检查是否有存储的令牌 $token_path = VSM_PLUGIN_DIR . 'tokens/google-token.json'; if (file_exists($token_path)) { $access_token = json_decode(file_get_contents($token_path), true); $this->client->setAccessToken($access_token); } // 如果令牌过期,刷新它 if ($this->client->isAccessTokenExpired()) { if ($this->client->getRefreshToken()) { $this->client->fetchAccessTokenWithRefreshToken(); file_put_contents($token_path, json_encode($this->client->getAccessToken())); } } $this->service = new Google_Service_Calendar($this->client); return true; } catch (Exception $e) { error_log('Google Meet初始化失败: ' . $e->getMessage()); return false; } } /** * 创建Google Calendar事件并生成Meet链接 */ public function create_meeting($seminar_data) { if (!$this->service) { return [ 'success' => false, 'error' => 'Google服务未初始化' ]; } try { // 创建日历事件 $event = new Google_Service_Calendar_Event([ 'summary' => $seminar_data['title'], 'description' => $seminar_data['description'], 'start' => [ 'dateTime' => $seminar_data['start_time'], 'timeZone' => wp_timezone_string(), ], 'end' => [ 'dateTime' => $seminar_data['end_time'], 'timeZone' => wp_timezone_string(), ], 'conferenceData' => [ 'createRequest' => [ 'requestId' => uniqid(), 'conferenceSolutionKey' => [ 'type' => 'hangoutsMeet' ] ] ], 'attendees' => [ ['email' => $seminar_data['organizer_email']] ], 'reminders' => [ 'useDefault' => false, 'overrides' => [ ['method' => 'email', 'minutes' => 24 * 60], // 提前1天 ['method' => 'popup', 'minutes' => 30] // 提前30分钟 ] ] ]); $created_event = $this->service->events->insert('primary', $event, [ 'conferenceDataVersion' => 1 ]); return [ 'success' => true, 'meeting_id' => $created_event->getId(), 'join_url' => $created_event->getHangoutLink(), 'html_link' => $created_event->getHtmlLink() ]; } catch (Exception $e) { return [ 'success' => false, 'error' => $e->getMessage() ]; } } } ## 第五部分:预约处理与数据库操作 ### 5.1 预约处理类 // 在includes/class-booking-handler.php中class VSM_Booking_Handler { public static function process_booking($data, $seminar_data) { global $wpdb; // 验证数据 $validation = self::validate_booking_data($data); if (!$validation['valid']) { return [ 'success' => false, 'message' => $validation['message'] ]; } // 检查是否已预约 $bookings_table = $wpdb->prefix . 'vsm_bookings'; $existing_booking = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $bookings_table WHERE seminar_id = %d AND user_email = %s", $seminar_data->id, sanitize_email($data['user_email']) )); if ($existing_booking > 0) { return [ 'success' => false, 'message' => __('您已经预约了此研讨会', 'virtual-seminar') ]; } // 检查是否还有空位 if ($seminar_data->current_participants >= $seminar_data->max_participants) { return [ 'success' => false, 'message' => __('研讨会名额已满', 'virtual-seminar') ]; } // 生成确认码 $confirmation_code = wp_generate_password(12, false); // 插入预约记录 $insert_result = $wpdb->insert( $bookings_table, [ 'seminar_id' => $seminar_data->id, 'user_id' => is_user_logged_in() ? get_current_user_id() : 0, 'user_email' => sanitize_email($data['user_email']), 'user_name' => sanitize_text_field($data['user_name']), 'confirmation_code' => $confirmation_code, 'notes' => sanitize_textarea_field($data['notes'] ?? '') ], ['%d', '%d', '%s', '%s', '%s', '%s'] ); if (!$insert_result) { return [ 'success' => false, 'message' => __('预约失败,请稍后重试', 'virtual-seminar') ]; } $booking_id = $wpdb->insert_id; // 更新研讨会参与人数 $seminars_table = $wpdb->prefix . 'vsm_seminars'; $wpdb->update( $seminars_table, ['current_participants' => $seminar_data->current_participants + 1], ['id' => $seminar_data->id], ['%d'], ['%d'] ); // 发送确认邮件 self::send_confirmation_email($booking_id, $confirmation_code, $seminar_data, $data); // 发送给管理员的通知 self::send_admin_notification($booking_id, $seminar_data, $data); return [ 'success' => true, 'message' => __('预约成功!确认邮件已发送到您的邮箱。', 'virtual-seminar'), 'booking_id' => $booking_id, 'confirmation_code' => $confirmation_code ]; } private static function validate_booking_data($data) { // 验证必填字段 $required_fields = ['user_name', 'user_email', 'seminar_id']; foreach ($required_fields as $field) { if (empty($data[$field])) { return [ 'valid' => false, 'message' => __('请填写所有必填字段', 'virtual-seminar') ]; } } // 验证邮箱格式 if (!is_email($data['user_email'])) { return [ 'valid' => false, 'message' => __('请输入有效的邮箱地址', 'virtual-seminar') ]; } // 验证同意条款 if (!isset($data['consent']) || $data['consent'] !== 'on') { return [ 'valid' => false, 'message' => __('请同意接收研讨会相关信息', 'virtual-seminar') ]; } return ['valid' => true]; } private static function send_confirmation_email($booking_id, $confirmation_code, $seminar_data, $user_data) { $post = get_post($seminar_data->post_id); $start_time = date_i18n('Y年m月d日 H:i', strtotime($seminar_data->start_time)); $to = $user_data['user_email']; $subject = sprintf(__('研讨会预约确认: %s', 'virtual-seminar'), $post->post_title); // 构建邮件内容 $message = sprintf(__('尊敬的 %s,', 'virtual-seminar'), $user_data['user_name']) . "nn"; $message .= sprintf(__('您已成功预约研讨会: %s', 'virtual-seminar'), $post->post_title) . "n"; $message .= sprintf(__('时间: %s', 'virtual-seminar'), $start_time) . "nn"; // 如果有会议链接,添加到邮件中 if (!empty($seminar_data->meeting_url)) { $message .= __('会议链接: ', 'virtual-seminar') . $seminar_data->meeting_url . "n"; if (!empty($seminar_data->meeting_password)) { $message .= __('会议密码: ', 'virtual-seminar') . $seminar_data->meeting_password . "n"; } } else { $message .= __('会议链接将在研讨会开始前发送给您。', 'virtual-seminar') . "n"; } $message .= "n" . __('您的预约确认码: ', 'virtual-seminar') . $confirmation_code . "n"; $message .= __('如需取消预约,请访问: ', 'virtual-seminar') . home_url('/cancel-booking/?code=' . $confirmation_code) . "nn"; $message .= __('感谢您的参与!', 'virtual-seminar') . "n"; $message .= get_bloginfo('name'); // 设置邮件头 $headers = ['Content-Type: text/plain; charset=UTF-8']; // 发送邮件 wp_mail($to, $subject, $message, $headers); // 记录邮件发送日志 self::log_email_sent($booking_id, 'confirmation', $to); } private static function send_admin_notification($booking_id, $seminar_data, $user_data) { $admin_email = get_option('admin_email'); $post = get_post($seminar_data->post_id); $subject = sprintf(__('新研讨会预约: %s', 'virtual-seminar'), $post->post_title); $message = sprintf(__('有新的研讨会预约:', 'virtual-seminar')) . "nn"; $message .= sprintf(__('研讨会: %s', 'virtual-seminar'), $post->post_title) . "n"; $message .= sprintf(__('预约人: %s', 'virtual-seminar'), $user_data['user_name']) . "n"; $message .= sprintf(__('邮箱: %s', 'virtual-seminar'), $user_data['user_email']) . "n"; $message .= sprintf(__('预约时间: %s', 'virtual-seminar'), current_time('mysql')) . "n"; $message .= sprintf(__('预约ID: %d', 'virtual-seminar'), $booking_id) . "nn"; if (!empty($user_data['user_phone'])) { $message .= sprintf(__('电话: %s', 'virtual-seminar'), $user_data['user_phone']) . "n"; } if (!empty($user_data['user_company'])) { $message .= sprintf(__('公司: %s', 'virtual-seminar'), $user_data['user_company']) . "n"; } if (!empty($user_data['notes'])) { $message .= sprintf(__('备注: %s', 'virtual-seminar'), $user_data['notes']) . "n"; } $message .= "n" . __('查看所有预约: ', 'virtual-seminar') . admin_url('admin.php?page=vsm-bookings&seminar_id=' . $seminar_data->id); wp_mail($admin_email, $subject, $message); } private static function log_email_sent($booking_id, $type, $recipient) { global $wpdb; $table_name = $wpdb->prefix . 'vsm_email_logs'; $wpdb->insert( $table_name, [ 'booking_id' => $booking_id, 'email_type' => $type, 'recipient' => $recipient, 'sent_at' => current_time('mysql') ], ['%d', '%s', '%s', '%s'] ); } } ## 第六部分:后台管理界面 ### 6.1 创建管理菜单 // 在includes/class-admin.php中class VSM_Admin { public static function init() { add_action('admin_menu', [self::class, 'add_admin_menu']); add_action('admin_enqueue_scripts', [self::class, 'enqueue_admin_scripts']); add_action('add_meta_boxes', [self::class, 'add_seminar_meta_boxes']); add_action('save_post_seminar', [self::class, 'save_seminar_meta'], 10, 2); } public static function add_admin_menu() { // 主菜单 add_menu_page( __('研讨会管理', 'virtual-seminar'), __('研讨会管理', 'virtual-seminar'), 'manage_options', 'vsm-dashboard', [self::class, 'render_dashboard'], 'dashicons-video-alt3', 30 ); // 子菜单 add_submenu_page( 'vsm-dashboard', __('所有预约', 'virtual-seminar'), __('所有预约', 'virtual-seminar'), 'manage_options', 'vsm-bookings', [self::class, 'render_bookings_page'] ); add_submenu_page( 'vsm-dashboard', __('设置', 'virtual-seminar'), __('设置', 'virtual-seminar'), 'manage_options', 'vsm-settings', [self::class, 'render_settings_page'] ); add_submenu_page( 'vsm-dashboard', __('报告', 'virtual-seminar'), __('报告', 'virtual-seminar'), 'manage_options', 'vsm-reports', [self::class, 'render_reports_page'] ); } public static function render_dashboard() { global $wpdb; // 获取统计数据 $seminars_table = $wpdb->prefix . 'vsm_seminars'; $bookings_table = $wpdb->prefix . 'vsm_bookings'; $total_seminars = $wpdb->get_var("SELECT COUNT(*) FROM $seminars_table"); $upcoming_seminars = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $seminars_table WHERE start_time > %s", current_time('mysql') )); $total_bookings = $wpdb->get_var("SELECT COUNT(*) FROM $bookings_table"); $today_bookings = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $bookings_table WHERE DATE(booking_time) = %s", current_time('Y-m-d') ));

发表评论

一步步教你,集成在线简易信息图表与数据报告生成工具到网站

一步步教你,集成在线简易信息图表与数据报告生成工具到网站,通过WordPress程序的代码二次开发实现常用互联网小工具功能 引言:为什么你的网站需要信息图表与数据报告生成工具? 在当今数据驱动的互联网时代,可视化数据呈现已成为网站吸引用户、提升专业形象的关键手段。信息图表(Infographics)能够将复杂数据转化为直观、易懂的视觉形式,而数据报告生成工具则允许用户根据自身需求创建个性化内容。对于企业网站、博客、教育平台或任何需要展示数据的在线平台而言,集成这些功能可以显著提升用户体验和网站价值。 WordPress作为全球最流行的内容管理系统,拥有强大的扩展性和灵活性。通过代码二次开发,我们可以在WordPress网站上集成在线简易信息图表与数据报告生成工具,实现专业级的数据可视化功能,而无需依赖昂贵的外部服务或复杂的第三方插件。 本文将详细介绍如何通过WordPress代码二次开发,一步步实现这些功能,让你的网站具备创建和展示信息图表、生成数据报告的能力。 第一部分:准备工作与环境搭建 1.1 理解WordPress开发基础 在开始之前,我们需要了解WordPress开发的基本概念: 主题与插件结构:WordPress功能扩展主要通过主题和插件实现 钩子(Hooks)系统:动作钩子(Action Hooks)和过滤器钩子(Filter Hooks)是WordPress扩展的核心机制 短代码(Shortcodes):允许在文章和页面中嵌入动态内容 自定义文章类型(Custom Post Types):用于创建特殊类型的内容 REST API:WordPress提供的API接口,用于前后端数据交互 1.2 开发环境配置 为了安全地进行代码开发,建议搭建本地开发环境: 本地服务器环境:使用XAMPP、MAMP或Local by Flywheel等工具 代码编辑器:推荐VS Code、PHPStorm或Sublime Text 版本控制:安装Git用于代码版本管理 浏览器开发者工具:熟悉Chrome或Firefox的开发者工具 1.3 创建开发用子主题或插件 为了避免直接修改主题文件导致更新时丢失更改,我们创建一个专用插件: <?php /** * Plugin Name: 简易信息图表与数据报告生成工具 * Plugin URI: https://yourwebsite.com/ * Description: 为WordPress网站添加在线信息图表和数据报告生成功能 * Version: 1.0.0 * Author: 你的名字 * License: GPL v2 or later */ // 防止直接访问 if (!defined('ABSPATH')) { exit; } // 定义插件常量 define('INFOGRAPHIC_TOOL_VERSION', '1.0.0'); define('INFOGRAPHIC_TOOL_PATH', plugin_dir_path(__FILE__)); define('INFOGRAPHIC_TOOL_URL', plugin_dir_url(__FILE__)); 第二部分:创建信息图表生成器核心功能 2.1 设计数据库结构 我们需要存储用户创建的信息图表模板和数据。在插件激活时创建必要的数据库表: // 激活插件时创建数据库表 register_activation_hook(__FILE__, 'infographic_tool_create_tables'); function infographic_tool_create_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'infographic_templates'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id mediumint(9) NOT NULL AUTO_INCREMENT, title varchar(255) NOT NULL, template_data longtext NOT NULL, template_type varchar(50) DEFAULT 'chart', created_by bigint(20) NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP, updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); // 创建图表数据表 $data_table = $wpdb->prefix . 'infographic_charts'; $sql2 = "CREATE TABLE IF NOT EXISTS $data_table ( id mediumint(9) NOT NULL AUTO_INCREMENT, template_id mediumint(9) NOT NULL, chart_data longtext NOT NULL, chart_options longtext NOT NULL, user_id bigint(20), created_at datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), FOREIGN KEY (template_id) REFERENCES {$table_name}(id) ON DELETE CASCADE ) $charset_collate;"; dbDelta($sql2); } 2.2 创建信息图表编辑器界面 使用HTML、CSS和JavaScript创建前端编辑器: // 添加信息图表编辑器短代码 add_shortcode('infographic_editor', 'infographic_tool_editor_shortcode'); function infographic_tool_editor_shortcode($atts) { // 只有登录用户可以使用编辑器 if (!is_user_logged_in()) { return '<p>请先登录以使用信息图表编辑器。</p>'; } // 加载必要资源 wp_enqueue_style('infographic-editor-style', INFOGRAPHIC_TOOL_URL . 'assets/css/editor.css'); wp_enqueue_script('chart-js', 'https://cdn.jsdelivr.net/npm/chart.js', array(), '3.7.0', true); wp_enqueue_script('infographic-editor-script', INFOGRAPHIC_TOOL_URL . 'assets/js/editor.js', array('jquery', 'chart-js'), INFOGRAPHIC_TOOL_VERSION, true); // 传递数据到JavaScript wp_localize_script('infographic-editor-script', 'infographicTool', array( 'ajax_url' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('infographic_tool_nonce'), 'user_id' => get_current_user_id() )); ob_start(); ?> <div class="infographic-editor-container"> <div class="editor-header"> <h2>信息图表编辑器</h2> <div class="editor-actions"> <button id="save-template" class="button button-primary">保存模板</button> <button id="load-template" class="button">加载模板</button> <button id="export-chart" class="button">导出图表</button> </div> </div> <div class="editor-main"> <div class="sidebar"> <div class="sidebar-section"> <h3>图表类型</h3> <select id="chart-type" class="chart-type-select"> <option value="bar">柱状图</option> <option value="line">折线图</option> <option value="pie">饼图</option> <option value="doughnut">环形图</option> <option value="radar">雷达图</option> </select> </div> <div class="sidebar-section"> <h3>数据输入</h3> <div class="data-input"> <textarea id="chart-data" rows="10" placeholder="输入数据,格式:标签1,值1 标签2,值2 ...">第一季度,120 第二季度,200 第三季度,150 第四季度,180</textarea> <button id="update-chart" class="button">更新图表</button> </div> </div> <div class="sidebar-section"> <h3>图表选项</h3> <div class="chart-options"> <label>标题: <input type="text" id="chart-title" value="年度销售数据"></label> <label>背景颜色: <input type="color" id="bg-color" value="#ffffff"></label> <label>显示图例: <input type="checkbox" id="show-legend" checked></label> </div> </div> </div> <div class="chart-container"> <div class="chart-wrapper"> <canvas id="infographic-chart" width="800" height="500"></canvas> </div> <div class="chart-actions"> <button id="download-png" class="button">下载PNG</button> <button id="download-pdf" class="button">下载PDF</button> <button id="embed-code" class="button">获取嵌入代码</button> </div> </div> </div> <div class="templates-modal" id="templates-modal"> <div class="modal-content"> <span class="close-modal">&times;</span> <h3>选择模板</h3> <div class="templates-list" id="templates-list"> <!-- 模板将通过AJAX加载 --> </div> </div> </div> </div> <?php return ob_get_clean(); } 2.3 实现图表生成与保存功能 创建JavaScript文件处理图表生成和用户交互: // assets/js/editor.js jQuery(document).ready(function($) { let currentChart = null; let chartData = { labels: ['第一季度', '第二季度', '第三季度', '第四季度'], datasets: [{ label: '销售额', data: [120, 200, 150, 180], backgroundColor: [ 'rgba(255, 99, 132, 0.6)', 'rgba(54, 162, 235, 0.6)', 'rgba(255, 206, 86, 0.6)', 'rgba(75, 192, 192, 0.6)' ], borderColor: [ 'rgba(255, 99, 132, 1)', 'rgba(54, 162, 235, 1)', 'rgba(255, 206, 86, 1)', 'rgba(75, 192, 192, 1)' ], borderWidth: 1 }] }; // 初始化图表 function initChart() { const ctx = document.getElementById('infographic-chart').getContext('2d'); const chartType = $('#chart-type').val(); if (currentChart) { currentChart.destroy(); } currentChart = new Chart(ctx, { type: chartType, data: chartData, options: { responsive: false, plugins: { legend: { display: $('#show-legend').is(':checked'), position: 'top' }, title: { display: true, text: $('#chart-title').val() } } } }); } // 更新图表数据 $('#update-chart').on('click', function() { const dataText = $('#chart-data').val(); const lines = dataText.split('n'); const labels = []; const data = []; lines.forEach(line => { if (line.trim()) { const parts = line.split(','); if (parts.length >= 2) { labels.push(parts[0].trim()); data.push(parseFloat(parts[1].trim()) || 0); } } }); chartData.labels = labels; chartData.datasets[0].data = data; chartData.datasets[0].label = $('#chart-title').val() || '数据'; initChart(); }); // 更改图表类型 $('#chart-type').on('change', initChart); // 保存模板 $('#save-template').on('click', function() { const templateName = prompt('请输入模板名称:'); if (!templateName) return; const templateData = { chartType: $('#chart-type').val(), chartData: chartData, options: { title: $('#chart-title').val(), showLegend: $('#show-legend').is(':checked'), bgColor: $('#bg-color').val() } }; $.ajax({ url: infographicTool.ajax_url, type: 'POST', data: { action: 'save_infographic_template', nonce: infographicTool.nonce, title: templateName, data: JSON.stringify(templateData), user_id: infographicTool.user_id }, success: function(response) { if (response.success) { alert('模板保存成功!'); } else { alert('保存失败: ' + response.data); } } }); }); // 加载模板 $('#load-template').on('click', function() { $.ajax({ url: infographicTool.ajax_url, type: 'POST', data: { action: 'get_infographic_templates', nonce: infographicTool.nonce, user_id: infographicTool.user_id }, success: function(response) { if (response.success) { $('#templates-list').html(''); response.data.forEach(template => { $('#templates-list').append( `<div class="template-item" data-id="${template.id}"> <h4>${template.title}</h4> <button class="load-template-btn button">加载</button> <button class="delete-template-btn button">删除</button> </div>` ); }); $('#templates-modal').show(); } } }); }); // 初始化图表 initChart(); }); 第三部分:实现数据报告生成功能 3.1 设计报告生成系统架构 数据报告生成器需要更复杂的结构,包括: 报告模板系统:预定义报告结构和样式 数据源连接:支持数据库、API或手动输入数据 报告生成引擎:将数据填充到模板中生成报告 导出功能:支持PDF、Word、Excel等格式 3.2 创建报告生成器短代码 // 添加报告生成器短代码 add_shortcode('report_generator', 'report_generator_shortcode'); function report_generator_shortcode($atts) { $atts = shortcode_atts(array( 'type' => 'simple', 'template' => 'default' ), $atts); // 加载资源 wp_enqueue_style('report-generator-style', INFOGRAPHIC_TOOL_URL . 'assets/css/report-generator.css'); wp_enqueue_script('report-generator-script', INFOGRAPHIC_TOOL_URL . 'assets/js/report-generator.js', array('jquery'), INFOGRAPHIC_TOOL_VERSION, true); wp_enqueue_script('html2pdf', 'https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js', array(), '0.10.1', true); ob_start(); ?> <div class="report-generator-container"> <div class="report-header"> <h2>数据报告生成器</h2> <div class="report-actions"> <select id="report-template"> <option value="business">业务报告</option> <option value="financial">财务报告</option> <option value="marketing">营销报告</option> <option value="custom">自定义报告</option> </select> <button id="generate-report" class="button button-primary">生成报告</button> <button id="export-pdf" class="button">导出PDF</button> </div> </div> <div class="report-builder"> <div class="data-sources"> <h3>数据源</h3> <div class="source-options"> <label><input type="radio" name="data-source" value="manual" checked> 手动输入</label> <label><input type="radio" name="data-source" value="csv"> 上传CSV</label> <label><input type="radio" name="data-source" value="api"> API连接</label> </div> <div class="source-input manual-input"> <h4>输入报告数据</h4> <div class="data-table-editor"> <table id="data-table"> <thead> <tr> <th>指标</th> <th>Q1</th> <th>Q2</th> <th>Q3</th> <th>Q4</th> </tr> </thead> <tbody> <tr> <td><input type="text" value="销售额" class="metric-name"></td> <td><input type="number" value="120000"></td> <td><input type="number" value="150000"></td> <td><input type="number" value="180000"></td> <td><input type="number" value="210000"></td> </tr> <!-- 更多行可以通过JavaScript添加 --> </tbody> </table> <button id="add-row" class="button">添加行</button> </div> </div> </div> <div class="report-preview"> <h3>报告预览</h3> <div class="preview-container" id="report-preview"> <!-- 报告预览将通过JavaScript生成 --> </div> </div> </div> </div> <?php return ob_get_clean(); } 3.3 实现报告生成逻辑 创建PHP处理类来处理报告生成: // 创建报告生成类 class Report_Generator { private $data; private $template; private $output_format; public function __construct($data, $template = 'default', $format = 'html') { $this->data = $data; $this->template = $template; $this->output_format = $format; } public function generate() { // 根据模板类型选择生成方法 switch ($this->template) { case 'business': return $this->generate_business_report(); case 'financial': return $this->generate_financial_report(); case 'marketing': return $this->generate_marketing_report(); default: return $this->generate_custom_report(); } } private function generate_business_report() { $html = '<div class="business-report">'; $html .= '<h1 class="report-title">业务分析报告</h1>'; $html .= '<div class="report-meta">'; $html .= '<p>生成日期: ' . date('Y年m月d日') . '</p>'; $html .= '<p>报告周期: ' . ($this->data['period'] ?? '最近季度') . '</p>'; $html .= '</div>'; // 关键指标摘要 if (!empty($this->data['metrics'])) { $html .= '<div class="key-metrics">'; $html .= '<h2>关键绩效指标</h2>'; $html .= '<div class="metrics-grid">'; foreach ($this->data['metrics'] as $metric) { $html .= '<div class="metric-card">'; $html .= '<h3>' . esc_html($metric['name']) . '</h3>'; $html .= '<div class="metric-value">' . esc_html($metric['value']) . '</div>'; if (isset($metric['change'])) { $change_class = $metric['change'] >= 0 ? 'positive' : 'negative'; $html .= '<div class="metric-change ' . $change_class . '">'; $html .= ($metric['change'] >= 0 ? '+' : '') . esc_html($metric['change']) . '%'; $html .= '</div>'; } $html .= '</div>'; } $html .= '</div></div>'; } // 数据表格 if (!empty($this->data['table_data'])) { $html .= '<div class="data-table-section">'; $html .= '<h2>详细数据</h2>'; $html .= '<table class="report-table">'; $html .= '<thead><tr>'; // 表头 foreach ($this->data['table_data']['headers'] as $header) { $html .= '<th>' . esc_html($header) . '</th>'; } $html .= '</tr></thead><tbody>'; // 表格内容 foreach ($this->data['table_data']['rows'] as $row) { $html .= '<tr>'; foreach ($row as $cell) { $html .= '<td>' . esc_html($cell) . '</td>'; } $html .= '</tr>'; } $html .= '</tbody></table></div>'; } // 分析与建议 $html .= '<div class="analysis-section">'; $html .= '<h2>分析与建议</h2>'; $html .= '<div class="analysis-content">'; $html .= '<p>' . ($this->data['analysis'] ?? '基于以上数据,建议关注关键指标的变化趋势,优化资源配置。') . '</p>'; $html .= '</div>'; $html .= '</div>'; $html .= '</div>'; return $this->format_output($html); } private function format_output($html) { if ($this->output_format === 'pdf') { // 这里可以集成PDF生成库,如TCPDF或mPDF return $this->generate_pdf($html); } return $html; } private function generate_pdf($html) { // 简化示例,实际应使用PDF生成库 return array( 'type' => 'pdf', 'content' => 'PDF生成功能需要集成第三方库', 'html_template' => $html ); } } // AJAX处理报告生成add_action('wp_ajax_generate_data_report', 'handle_report_generation');add_action('wp_ajax_nopriv_generate_data_report', 'handle_report_generation'); function handle_report_generation() { // 验证nonce if (!wp_verify_nonce($_POST['nonce'], 'infographic_tool_nonce')) { wp_die('安全验证失败'); } $data = json_decode(stripslashes($_POST['data']), true); $template = sanitize_text_field($_POST['template']); $format = sanitize_text_field($_POST['format'] ?? 'html'); $generator = new Report_Generator($data, $template, $format); $report = $generator->generate(); wp_send_json_success(array( 'report' => $report, 'download_url' => $format === 'pdf' ? generate_pdf_download_link($report) : null )); } ## 第四部分:创建管理界面与高级功能 ### 4.1 构建WordPress管理界面 // 添加管理菜单add_action('admin_menu', 'infographic_tool_admin_menu'); function infographic_tool_admin_menu() { add_menu_page( '信息图表工具', '图表工具', 'manage_options', 'infographic-tool', 'infographic_tool_admin_page', 'dashicons-chart-area', 30 ); add_submenu_page( 'infographic-tool', '图表模板', '模板管理', 'manage_options', 'infographic-templates', 'infographic_templates_page' ); add_submenu_page( 'infographic-tool', '报告设置', '报告设置', 'manage_options', 'infographic-reports', 'infographic_reports_page' ); add_submenu_page( 'infographic-tool', '数据源配置', '数据源', 'manage_options', 'infographic-data-sources', 'infographic_data_sources_page' ); } function infographic_tool_admin_page() { ?> <div class="wrap"> <h1>信息图表与数据报告工具</h1> <div class="dashboard-cards"> <div class="card"> <h3>使用统计</h3> <div class="card-content"> <?php global $wpdb; $templates_count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}infographic_templates"); $charts_count = $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}infographic_charts"); ?> <p>模板数量: <strong><?php echo $templates_count; ?></strong></p> <p>生成图表: <strong><?php echo $charts_count; ?></strong></p> </div> </div> <div class="card"> <h3>快速操作</h3> <div class="card-content"> <p> <a href="<?php echo admin_url('admin.php?page=infographic-templates'); ?>" class="button">管理模板</a> <a href="<?php echo admin_url('admin.php?page=infographic-reports'); ?>" class="button">报告设置</a> </p> <p>短代码示例:</p> <code>[infographic_editor]</code><br> <code>[report_generator]</code> </div> </div> </div> <div class="settings-section"> <h2>全局设置</h2> <form method="post" action="options.php"> <?php settings_fields('infographic_tool_settings'); do_settings_sections('infographic_tool_settings'); ?> <table class="form-table"> <tr> <th>默认图表尺寸</th> <td> <input type="number" name="default_chart_width" value="<?php echo get_option('default_chart_width', 800); ?>" placeholder="宽度"> × <input type="number" name="default_chart_height" value="<?php echo get_option('default_chart_height', 500); ?>" placeholder="高度"> 像素 </td> </tr> <tr> <th>允许的用户角色</th> <td> <?php $roles = get_editable_roles(); $allowed_roles = get_option('infographic_allowed_roles', array('administrator', 'editor')); foreach ($roles as $role_name => $role_info) { $checked = in_array($role_name, $allowed_roles) ? 'checked' : ''; echo '<label style="display: block; margin-bottom: 5px;">'; echo '<input type="checkbox" name="allowed_roles[]" value="' . $role_name . '" ' . $checked . '> '; echo $role_info['name']; echo '</label>'; } ?> </td> </tr> <tr> <th>启用数据导出</th> <td> <label> <input type="checkbox" name="enable_data_export" value="1" <?php checked(get_option('enable_data_export', 1)); ?>> 允许用户导出图表数据 </label> </td> </tr> </table> <?php submit_button(); ?> </form> </div> </div> <?php } // 注册设置add_action('admin_init', 'infographic_tool_register_settings'); function infographic_tool_register_settings() { register_setting('infographic_tool_settings', 'default_chart_width'); register_setting('infographic_tool_settings', 'default_chart_height'); register_setting('infographic_tool_settings', 'infographic_allowed_roles'); register_setting('infographic_tool_settings', 'enable_data_export'); } ### 4.2 实现模板管理系统 function infographic_templates_page() { global $wpdb; // 处理模板操作 if (isset($_GET['action'])) { if ($_GET['action'] === 'delete' && isset($_GET['template_id'])) { $template_id = intval($_GET['template_id']); $wpdb->delete( $wpdb->prefix . 'infographic_templates', array('id' => $template_id) ); echo '<div class="notice notice-success"><p>模板已删除</p></div>'; } } // 获取所有模板 $templates = $wpdb->get_results(" SELECT t.*, u.user_login as author FROM {$wpdb->prefix}infographic_templates t LEFT JOIN {$wpdb->users} u ON t.created_by = u.ID ORDER BY t.created_at DESC "); ?> <div class="wrap"> <h1>图表模板管理</h1> <div class="tablenav top"> <div class="alignleft actions"> <a href="<?php echo admin_url('admin.php?page=infographic-tool&action=add_template'); ?>" class="button button-primary">添加新模板</a> </div> </div> <table class="wp-list-table widefat fixed striped"> <thead> <tr> <th>ID</th> <th>模板名称</th> <th>类型</th> <th>作者</th> <th>创建时间</th> <th>操作</th> </tr> </thead> <tbody> <?php if (empty($templates)): ?> <tr> <td colspan="6" style="text-align: center;">暂无模板</td> </tr> <?php else: ?> <?php foreach ($templates as $template): ?> <tr> <td><?php echo $template->id; ?></td> <td><?php echo esc_html($template->title); ?></td> <td><?php echo esc_html($template->template_type); ?></td> <td><?php echo esc_html($template->author); ?></td> <td><?php echo date('Y-m-d H:i', strtotime($template->created_at)); ?></td> <td> <a href="<?php echo admin_url("admin.php?page=infographic-tool&action=edit_template&id={$template->id}"); ?>" class="button button-small">编辑</a> <a href="<?php echo admin_url("admin.php?page=infographic-templates&action=delete&template_id={$template->id}"); ?>" class="button button-small button-link-delete" onclick="return confirm('确定删除此模板?')">删除</a> <button class="button button-small preview-template" data-id="<?php echo $template->id; ?>">预览</button> </td> </tr> <?php endforeach; ?> <?php endif; ?> </tbody> </table> </div> <!-- 模板预览模态框 --> <div id="template-preview-modal" class="modal" style="display:none;"> <div class="modal-content"> <span class="close-modal">&times;</span> <div id="template-preview-content"></div> </div> </div> <script> jQuery(document).ready(function($) { $('.preview-template').on('click', function() { var templateId = $(this).data('id'); $.ajax({ url: ajaxurl, type: 'POST', data: { action: 'preview_infographic_template', template_id: templateId, nonce: '<?php echo wp_create_nonce('infographic_preview_nonce'); ?>' }, success: function(response) { if (response.success) { $('#template-preview-content').html(response.data.preview); $('#template-preview-modal').show(); } } }); }); $('.close-modal').on('click', function() { $('#template-preview-modal').hide(); }); }); </script> <?php } ### 4.3 添加REST API端点 // 注册REST API端点add_action('rest_api_init', 'register_infographic_rest_routes'); function register_infographic_rest_routes() { // 获取图表数据 register_rest_route('infographic-tool/v1', '/charts/(?P<id>d+)', array( 'methods' => 'GET', 'callback' => 'get_chart_data_api', 'permission_callback' => function() { return current_user_can('read'); } )); // 创建新图表 register_rest_route('infographic-tool/v1', '/charts', array( 'methods' => 'POST', 'callback' => 'create_chart_api', 'permission_callback' => function() { return current_user_can('edit_posts'); } )); // 获取报告模板 register_rest_route('infographic-tool/v1', '/report-templates', array( 'methods' => 'GET', 'callback' => 'get_report_templates_api', 'permission_callback' => function() { return current_user_can('read'); } )); } function get_chart_data_api($request) { $chart_id = $request['id']; global $wpdb; $chart = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->prefix}infographic_charts WHERE id = %d", $chart_id )); if (!$chart) { return new WP_Error('not_found', '图表未找到', array('status' => 404)); } return array( 'id' => $chart->id, 'data' => json_decode($chart->chart_data), 'options' => json_decode($chart->chart_options), 'created_at' => $chart->created_at ); } function create_chart_api($request) { $parameters = $request->get_json_params(); if (empty($parameters['data']) || empty($parameters['template_id'])) { return new WP_Error('invalid_data', '缺少必要数据', array('status' => 400)); } global $wpdb; $result = $wpdb->insert( $wpdb->prefix . 'infographic_charts', array( 'template_id' => intval($parameters['template_id']), 'chart_data' => json_encode($parameters['data']), 'chart_options' => json_encode($parameters['options'] ?? array()), 'user_id' => get_current_user_id() ), array('%d', '%s', '%s', '%d') ); if ($result === false) { return new WP_Error('db_error', '数据库错误', array('status' => 500)); } return array( 'id' => $wpdb->insert_id, 'message' => '图表创建成功', 'embed_code' => generate_chart_embed_code($wpdb->insert_id) ); } function generate_chart_embed_code($chart_id) { $embed_url = home_url("/infographic-embed/{$chart_id}/"); return '<iframe src="' . esc_url($embed_url) . '" width="800" height="500" frameborder="0"></iframe>'; } ## 第五部分:优化与安全增强 ### 5.1 数据安全与验证 // 增强数据验证class Data_Validator { public static function validate_chart_data($data) { $errors = array(); // 验证数据格式 if (!is_array($data)) { $errors[] = '数据格式不正确'; return $errors; } // 验证标签 if (empty($data['labels']) || !is_array($data['labels'])) { $errors[] = '标签数据不能为空'; } // 验证数据集 if (empty($data['datasets']) || !is_array($data['datasets'])) { $errors[] = '数据集不能为空'; } else { foreach ($data['datasets'] as $index => $dataset) { if (empty($dataset['data']) || !is_array($dataset['data'])) { $errors[] = "数据集 {$index} 的数据不能为空"; } // 验证数据值是否为数字

发表评论

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 代码片段编辑器组件

发表评论