文章目录
-
- 在当今数字化工作环境中,协同办公已成为企业和团队提高效率的关键。根据Statista的数据,2023年全球协同软件市场规模已达到138亿美元,预计到2027年将增长至287亿美元。对于拥有WordPress网站的企业、教育机构或内容创作团队而言,集成在线协同文档编辑功能可以显著提升团队协作效率,减少邮件往来,实现实时内容共创。 传统的WordPress内容编辑方式存在明显局限:单用户编辑锁定机制导致多人协作困难;版本管理功能有限,难以追踪每次修改;缺乏实时协同编辑体验,团队成员无法同时处理同一文档。本教程将指导您通过代码二次开发,为WordPress网站添加类似Google Docs的协同编辑与版本管理功能,而无需依赖昂贵的第三方服务。
-
- 在开始编码前,我们需要明确要实现的协同编辑系统应包含以下核心功能: 实时协同编辑:支持多用户同时编辑同一文档,实时显示他人光标位置和编辑内容 版本控制系统:完整记录文档修改历史,支持版本对比与回滚 用户权限管理:基于WordPress用户角色设置文档访问和编辑权限 冲突解决机制:智能处理编辑冲突,确保数据一致性 评论与批注系统:支持文档内评论和批注功能 导出与分享:支持多种格式导出和链接分享功能
- 我们将采用以下技术栈实现协同编辑功能: 前端编辑器:使用开源协同编辑器框架如Y.js或ShareDB 实时通信:WebSocket协议实现实时数据同步 后端框架:基于WordPress REST API扩展 数据存储:MySQL数据库存储文档内容和版本历史 版本控制:自定义版本管理算法或集成Git原理
- 确保您的开发环境满足以下要求: WordPress 5.8或更高版本 PHP 7.4+(建议使用PHP 8.0+以获得更好性能) MySQL 5.7+或MariaDB 10.3+ Node.js环境(用于构建前端资源) WebSocket服务器(如Socket.io服务器)
-
- 首先,我们需要创建一个新的自定义文章类型来存储协同文档: // 在主题的functions.php或自定义插件中添加 function register_collaborative_doc_type() { $labels = array( 'name' => '协同文档', 'singular_name' => '协同文档', 'menu_name' => '协同文档', 'add_new' => '新建文档', 'add_new_item' => '新建协同文档', 'edit_item' => '编辑文档', 'new_item' => '新文档', 'view_item' => '查看文档', 'search_items' => '搜索文档', 'not_found' => '未找到文档', 'not_found_in_trash' => '回收站中无文档' ); $args = array( 'labels' => $labels, 'public' => true, 'publicly_queryable' => true, 'show_ui' => true, 'show_in_menu' => true, 'query_var' => true, 'rewrite' => array('slug' => 'collab-doc'), 'capability_type' => 'post', 'has_archive' => true, 'hierarchical' => false, 'menu_position' => 5, 'supports' => array('title', 'author'), 'show_in_rest' => true, // 启用REST API支持 ); register_post_type('collab_doc', $args); } add_action('init', 'register_collaborative_doc_type');
- 为了实现实时协同编辑,我们需要设置WebSocket服务器处理实时通信: // websocket-server.js - Node.js WebSocket服务器 const WebSocket = require('ws'); const http = require('http'); const server = http.createServer(); const wss = new WebSocket.Server({ server }); // 存储文档状态和连接 const documents = new Map(); wss.on('connection', (ws, request) => { const params = new URLSearchParams(request.url.split('?')[1]); const docId = params.get('docId'); const userId = params.get('userId'); if (!docId || !userId) { ws.close(); return; } // 初始化文档状态 if (!documents.has(docId)) { documents.set(docId, { content: '', users: new Map(), version: 0 }); } const doc = documents.get(docId); // 存储用户连接 doc.users.set(userId, { ws, cursorPosition: 0, lastActive: Date.now() }); // 发送当前文档状态给新用户 ws.send(JSON.stringify({ type: 'init', content: doc.content, version: doc.version, activeUsers: Array.from(doc.users.keys()).filter(id => id !== userId) })); // 广播新用户加入 broadcastToOthers(docId, userId, { type: 'user_joined', userId }); // 处理客户端消息 ws.on('message', (message) => { try { const data = JSON.parse(message); handleClientMessage(docId, userId, data); } catch (error) { console.error('消息解析错误:', error); } }); // 处理连接关闭 ws.on('close', () => { if (doc.users.has(userId)) { doc.users.delete(userId); broadcastToOthers(docId, userId, { type: 'user_left', userId }); } }); }); function handleClientMessage(docId, userId, data) { const doc = documents.get(docId); if (!doc) return; switch (data.type) { case 'edit': // 应用编辑操作 applyEdit(doc, data.operation); doc.version++; // 广播编辑给其他用户 broadcastToOthers(docId, userId, { type: 'update', operation: data.operation, version: doc.version, userId }); break; case 'cursor_move': // 广播光标移动 broadcastToOthers(docId, userId, { type: 'cursor_move', userId, position: data.position }); break; case 'selection_change': // 广播选择变化 broadcastToOthers(docId, userId, { type: 'selection_change', userId, selection: data.selection }); break; } } function applyEdit(doc, operation) { // 根据操作类型更新文档内容 // 这里需要实现操作转换(OT)或冲突无关数据类型(CRDT)逻辑 // 简化示例:直接替换内容 if (operation.type === 'insert') { doc.content = doc.content.slice(0, operation.position) + operation.text + doc.content.slice(operation.position); } else if (operation.type === 'delete') { doc.content = doc.content.slice(0, operation.position) + doc.content.slice(operation.position + operation.length); } } function broadcastToOthers(docId, excludeUserId, message) { const doc = documents.get(docId); if (!doc) return; doc.users.forEach((user, userId) => { if (userId !== excludeUserId && user.ws.readyState === WebSocket.OPEN) { user.ws.send(JSON.stringify(message)); } }); } // 启动服务器 const PORT = process.env.PORT || 8080; server.listen(PORT, () => { console.log(`WebSocket服务器运行在端口 ${PORT}`); });
- 创建前端编辑器界面,使用Y.js库实现协同编辑: <!-- collaborative-editor.php - 编辑器模板文件 --> <div id="collaborative-editor-container"> <div class="editor-header"> <h1 id="doc-title" contenteditable="true"><?php echo get_the_title(); ?></h1> <div class="editor-toolbar"> <button class="format-btn" data-format="bold">粗体</button> <button class="format-btn" data-format="italic">斜体</button> <button class="format-btn" data-format="underline">下划线</button> <select id="font-size"> <option value="12px">12px</option> <option value="14px">14px</option> <option value="16px" selected>16px</option> <option value="18px">18px</option> <option value="24px">24px</option> </select> <button id="save-version">保存版本</button> <button id="export-pdf">导出PDF</button> </div> <div class="user-presence"> <span>在线用户: </span> <div id="active-users"></div> </div> </div> <div class="editor-content"> <div id="editor" contenteditable="true"></div> </div> <div class="editor-sidebar"> <div class="version-history"> <h3>版本历史</h3> <ul id="version-list"></ul> </div> <div class="comments-section"> <h3>评论</h3> <div id="comments-container"></div> <textarea id="new-comment" placeholder="添加评论..."></textarea> <button id="add-comment">提交评论</button> </div> </div> </div> <script> // 协同编辑器前端JavaScript document.addEventListener('DOMContentLoaded', function() { const docId = <?php echo get_the_ID(); ?>; const userId = <?php echo get_current_user_id(); ?>; // 初始化Y.js协同编辑 const ydoc = new Y.Doc(); const ytext = ydoc.getText('content'); const editor = document.getElementById('editor'); // 连接WebSocket服务器 const ws = new WebSocket(`ws://localhost:8080?docId=${docId}&userId=${userId}`); // 绑定Y.js到编辑器 const binding = new Y.QuillBinding(ytext, editor); // 监听WebSocket消息 ws.onmessage = function(event) { const data = JSON.parse(event.data); switch(data.type) { case 'init': // 初始化文档内容 ytext.delete(0, ytext.length); ytext.insert(0, data.content); updateActiveUsers(data.activeUsers); break; case 'update': // 应用远程更新 applyRemoteUpdate(data.operation); break; case 'user_joined': addActiveUser(data.userId); break; case 'user_left': removeActiveUser(data.userId); break; case 'cursor_move': showRemoteCursor(data.userId, data.position); break; } }; // 发送本地编辑到服务器 ydoc.on('update', function(update) { ws.send(JSON.stringify({ type: 'edit', operation: extractOperationFromUpdate(update) })); }); // 光标移动跟踪 editor.addEventListener('keyup', function() { const selection = window.getSelection(); const position = getCursorPosition(editor, selection); ws.send(JSON.stringify({ type: 'cursor_move', position: position })); }); // 初始化版本历史 loadVersionHistory(docId); // 保存版本功能 document.getElementById('save-version').addEventListener('click', function() { saveDocumentVersion(docId, ytext.toString()); }); }); // 辅助函数 function updateActiveUsers(userIds) { const container = document.getElementById('active-users'); container.innerHTML = ''; userIds.forEach(userId => { const userElement = document.createElement('span'); userElement.className = 'active-user'; userElement.textContent = `用户${userId}`; userElement.style.backgroundColor = getUserColor(userId); container.appendChild(userElement); }); } function getUserColor(userId) { // 根据用户ID生成一致的颜色 const colors = ['#FF6B6B', '#4ECDC4', '#FFD166', '#06D6A0', '#118AB2']; return colors[userId % colors.length]; } function loadVersionHistory(docId) { // 通过AJAX加载版本历史 fetch(`/wp-json/collab/v1/document/${docId}/versions`) .then(response => response.json()) .then(versions => { const list = document.getElementById('version-list'); list.innerHTML = ''; versions.forEach(version => { const li = document.createElement('li'); li.innerHTML = ` <span>${version.date}</span> <span>${version.author}</span> <button onclick="restoreVersion(${version.id})">恢复</button> <button onclick="compareVersion(${version.id})">对比</button> `; list.appendChild(li); }); }); } function saveDocumentVersion(docId, content) { fetch(`/wp-json/collab/v1/document/${docId}/version`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': collabVars.nonce }, body: JSON.stringify({ content: content, comment: document.getElementById('version-comment')?.value || '手动保存' }) }) .then(response => response.json()) .then(data => { if(data.success) { alert('版本保存成功'); loadVersionHistory(docId); } }); } </script>
-
- 我们需要创建自定义数据库表来存储文档版本历史: // 创建版本管理数据库表 function create_version_tables() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); $table_name = $wpdb->prefix . 'collab_doc_versions'; $sql = "CREATE TABLE IF NOT EXISTS $table_name ( id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT, doc_id bigint(20) UNSIGNED NOT NULL, version_number int(11) NOT NULL, content longtext NOT NULL, author_id bigint(20) UNSIGNED NOT NULL, created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL, change_summary text, parent_version_id bigint(20) UNSIGNED DEFAULT NULL, PRIMARY KEY (id), KEY doc_id (doc_id), KEY author_id (author_id) ) $charset_collate;"; require_once(ABSPATH . 'wp-admin/includes/upgrade.php'); dbDelta($sql); } register_activation_hook(__FILE__, 'create_version_tables');
- 创建REST API端点处理版本管理操作: // 版本管理REST API add_action('rest_api_init', function() { // 获取文档版本列表 register_rest_route('collab/v1', '/document/(?P<id>d+)/versions', array( 'methods' => 'GET', 'callback' => 'get_document_versions', 'permission_callback' => 'check_document_permission' )); // 创建新版本 register_rest_route('collab/v1', '/document/(?P<id>d+)/version', array( 'methods' => 'POST', 'callback' => 'create_document_version', 'permission_callback' => 'check_document_permission' )); // 恢复特定版本 register_rest_route('collab/v1', '/version/(?P<id>d+)/restore', array( 'methods' => 'POST', 'callback' => 'restore_document_version', 'permission_callback' => 'check_document_permission' )); // 比较两个版本 register_rest_route('collab/v1', '/compare-versions', array( 'methods' => 'GET', 'callback' => 'compare_versions', 'permission_callback' => 'check_document_permission' )); }); function get_document_versions($request) { global $wpdb; $doc_id = $request['id']; $table_name = $wpdb->prefix . 'collab_doc_versions'; $versions = $wpdb->get_results($wpdb->prepare( "SELECT v.*, u.user_login as author_name FROM $table_name v LEFT JOIN {$wpdb->users} u ON v.author_id = u.ID WHERE v.doc_id = %d ORDER BY v.created_at DESC LIMIT 50", $doc_id )); // 格式化返回数据 $formatted_versions = array(); foreach ($versions as $version) { $formatted_versions[] = array( 'id' => $version->id, 'version_number' => $version->version_number, 'created_at' => $version->created_at, 'author' => $version->author_name, 'change_summary' => $version->change_summary, 'content_preview' => wp_trim_words($version->content, 20) ); } return rest_ensure_response($formatted_versions); } function create_document_version($request) { global $wpdb; $doc_id = $request['id']; current_user_id(); $content = sanitize_text_field($request['content']); $comment = sanitize_text_field($request['comment']); // 获取当前最高版本号 $table_name = $wpdb->prefix . 'collab_doc_versions'; $latest_version = $wpdb->get_var($wpdb->prepare( "SELECT MAX(version_number) FROM $table_name WHERE doc_id = %d", $doc_id )); $new_version = $latest_version ? $latest_version + 1 : 1; // 插入新版本 $result = $wpdb->insert( $table_name, array( 'doc_id' => $doc_id, 'version_number' => $new_version, 'content' => $content, 'author_id' => $user_id, 'change_summary' => $comment, 'parent_version_id' => $latest_version ? $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE doc_id = %d AND version_number = %d", $doc_id, $latest_version )) : null ), array('%d', '%d', '%s', '%d', '%s', '%d') ); if ($result) { return rest_ensure_response(array( 'success' => true, 'version_id' => $wpdb->insert_id, 'version_number' => $new_version )); } return new WP_Error('version_creation_failed', '版本创建失败', array('status' => 500)); } function restore_document_version($request) { global $wpdb; $version_id = $request['id']; // 获取版本内容 $table_name = $wpdb->prefix . 'collab_doc_versions'; $version = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $version_id )); if (!$version) { return new WP_Error('version_not_found', '版本不存在', array('status' => 404)); } // 更新文档当前内容 update_post_meta($version->doc_id, '_current_content', $version->content); // 创建恢复记录 $user_id = get_current_user_id(); $latest_version = $wpdb->get_var($wpdb->prepare( "SELECT MAX(version_number) FROM $table_name WHERE doc_id = %d", $version->doc_id )); $wpdb->insert( $table_name, array( 'doc_id' => $version->doc_id, 'version_number' => $latest_version + 1, 'content' => $version->content, 'author_id' => $user_id, 'change_summary' => sprintf('恢复到版本 %d', $version->version_number), 'parent_version_id' => $version->id ), array('%d', '%d', '%s', '%d', '%s', '%d') ); return rest_ensure_response(array( 'success' => true, 'message' => '版本恢复成功' )); } function compare_versions($request) { $version1_id = $request->get_param('v1'); $version2_id = $request->get_param('v2'); global $wpdb; $table_name = $wpdb->prefix . 'collab_doc_versions'; $version1 = $wpdb->get_row($wpdb->prepare( "SELECT content FROM $table_name WHERE id = %d", $version1_id )); $version2 = $wpdb->get_row($wpdb->prepare( "SELECT content FROM $table_name WHERE id = %d", $version2_id )); if (!$version1 || !$version2) { return new WP_Error('versions_not_found', '版本不存在', array('status' => 404)); } // 使用文本差异算法比较版本 $diff = compute_text_diff($version1->content, $version2->content); return rest_ensure_response(array( 'diff' => $diff, 'version1' => array('id' => $version1_id), 'version2' => array('id' => $version2_id) )); } function compute_text_diff($text1, $text2) { // 使用PHP内置的差异计算函数 require_once(ABSPATH . 'wp-admin/includes/diff.php'); $text1_lines = explode("n", $text1); $text2_lines = explode("n", $text2); $diff = new Text_Diff($text1_lines, $text2_lines); $renderer = new Text_Diff_Renderer_inline(); return $renderer->render($diff); } ### 3.3 自动版本保存机制 实现智能的自动版本保存功能: // 自动版本保存机制class AutoVersionSaver { private $save_threshold = 30; // 每30秒检查一次 private $change_threshold = 10; // 至少10个字符变化才保存 public function __construct() { add_action('wp_ajax_save_auto_version', array($this, 'handle_auto_save')); add_action('wp_ajax_nopriv_save_auto_version', array($this, 'handle_auto_save')); } public function handle_auto_save() { $doc_id = intval($_POST['doc_id']); $content = wp_unslash($_POST['content']); $last_saved_version = isset($_POST['last_saved']) ? $_POST['last_saved'] : null; // 验证权限 if (!current_user_can('edit_post', $doc_id)) { wp_die('权限不足'); } // 获取上次保存的内容 $last_content = $this->get_last_saved_content($doc_id, $last_saved_version); // 计算变化量 $changes = $this->calculate_changes($last_content, $content); // 如果变化足够大,则保存新版本 if ($changes['change_count'] >= $this->change_threshold) { $this->save_minor_version($doc_id, $content, $changes['summary']); wp_send_json_success(array( 'saved' => true, 'change_summary' => $changes['summary'], 'timestamp' => current_time('mysql') )); } else { wp_send_json_success(array( 'saved' => false, 'message' => '变化太小,未保存版本' )); } } private function get_last_saved_content($doc_id, $last_saved_version) { global $wpdb; $table_name = $wpdb->prefix . 'collab_doc_versions'; if ($last_saved_version) { $content = $wpdb->get_var($wpdb->prepare( "SELECT content FROM $table_name WHERE id = %d", $last_saved_version )); } else { $content = $wpdb->get_var($wpdb->prepare( "SELECT content FROM $table_name WHERE doc_id = %d ORDER BY created_at DESC LIMIT 1", $doc_id )); } return $content ?: ''; } private function calculate_changes($old_content, $new_content) { // 简单计算变化:字符差异 $old_length = strlen($old_content); $new_length = strlen($new_content); $length_change = $new_length - $old_length; // 使用更高级的差异检测(可选) similar_text($old_content, $new_content, $similarity); $change_percentage = 100 - $similarity; // 生成变化摘要 $summary = sprintf( '长度变化: %+d 字符, 相似度: %.1f%%', $length_change, $similarity ); return array( 'change_count' => abs($length_change), 'summary' => $summary, 'similarity' => $similarity ); } private function save_minor_version($doc_id, $content, $summary) { global $wpdb; $table_name = $wpdb->prefix . 'collab_doc_versions'; $user_id = get_current_user_id(); // 获取当前版本号 $latest_version = $wpdb->get_var($wpdb->prepare( "SELECT MAX(version_number) FROM $table_name WHERE doc_id = %d", $doc_id )); $new_version = $latest_version ? $latest_version + 1 : 1; $wpdb->insert( $table_name, array( 'doc_id' => $doc_id, 'version_number' => $new_version, 'content' => $content, 'author_id' => $user_id, 'change_summary' => '自动保存: ' . $summary, 'parent_version_id' => $latest_version ? $wpdb->get_var($wpdb->prepare( "SELECT id FROM $table_name WHERE doc_id = %d AND version_number = %d", $doc_id, $latest_version )) : null ), array('%d', '%d', '%s', '%d', '%s', '%d') ); } } new AutoVersionSaver(); ## 第四部分:用户权限与访问控制 ### 4.1 扩展WordPress权限系统 // 协同文档权限管理class CollaborativeDocPermissions { public function __construct() { add_filter('user_has_cap', array($this, 'add_collab_capabilities'), 10, 4); add_action('admin_init', array($this, 'setup_roles_and_capabilities')); } public function setup_roles_and_capabilities() { $roles = array('administrator', 'editor', 'author', 'contributor', 'subscriber'); foreach ($roles as $role_name) { $role = get_role($role_name); if ($role) { // 基础权限 $role->add_cap('read_collab_doc'); // 根据角色分配不同权限 switch ($role_name) { case 'administrator': case 'editor': $role->add_cap('edit_collab_docs'); $role->add_cap('edit_others_collab_docs'); $role->add_cap('publish_collab_docs'); $role->add_cap('delete_collab_docs'); $role->add_cap('manage_collab_doc_settings'); break; case 'author': $role->add_cap('edit_collab_docs'); $role->add_cap('publish_collab_docs'); $role->add_cap('delete_collab_docs'); break; case 'contributor': $role->add_cap('edit_collab_docs'); break; } } } } public function add_collab_capabilities($allcaps, $caps, $args, $user) { $requested_capability = $args[0]; $post_id = isset($args[2]) ? $args[2] : 0; // 处理协同文档特定权限 if (strpos($requested_capability, 'collab_doc') !== false && $post_id) { $post_type = get_post_type($post_id); if ($post_type === 'collab_doc') { // 检查文档特定权限设置 $doc_permissions = get_post_meta($post_id, '_collab_permissions', true); if (!empty($doc_permissions)) { $user_id = $user->ID; // 文档所有者有完全权限 $post = get_post($post_id); if ($post && $post->post_author == $user_id) { $allcaps['edit_collab_docs'] = true; $allcaps['edit_others_collab_docs'] = true; $allcaps['delete_collab_docs'] = true; return $allcaps; } // 检查用户是否在允许列表中 if (isset($doc_permissions['allowed_users'])) { $allowed_users = $doc_permissions['allowed_users']; if (in_array($user_id, $allowed_users)) { $permission_level = $doc_permissions['user_levels'][$user_id] ?? 'viewer'; switch ($permission_level) { case 'editor': $allcaps['edit_collab_docs'] = true; break; case 'commenter': $allcaps['comment_collab_docs'] = true; break; case 'viewer': $allcaps['read_collab_doc'] = true; break; } } } // 检查用户组权限 if (isset($doc_permissions['allowed_roles'])) { $user_roles = $user->roles; foreach ($user_roles as $role) { if (in_array($role, $doc_permissions['allowed_roles'])) { $allcaps['read_collab_doc'] = true; if (in_array($role, $doc_permissions['editor_roles'])) { $allcaps['edit_collab_docs'] = true; } break; } } } } } } return $allcaps; } // 文档共享功能 public static function share_document($doc_id, $user_emails, $permission_level = 'viewer') { $user_ids = array(); foreach ($user_emails as $email) { $user = get_user_by('email', $email); if ($user) { $user_ids[] = $user->ID; // 发送通知邮件 self::send_sharing_notification($user, $doc_id, $permission_level); } } // 更新文档权限设置 $permissions = get_post_meta($doc_id, '_collab_permissions', true) ?: array(); if (!isset($permissions['allowed_users'])) { $permissions['allowed_users'] = array(); } foreach ($user_ids as $user_id) { if (!in_array($user_id, $permissions['allowed_users'])) { $permissions['allowed_users'][] = $user_id; } $permissions['user_levels'][$user_id] = $permission_level; } update_post_meta($doc_id, '_collab_permissions', $permissions); return count($user_ids); } private static function send_sharing_notification($user, $doc_id, $permission_level) { $doc_title = get_the_title($doc_id); $doc_link = get_permalink($doc_id); $admin_email = get_option('admin_email'); $subject = sprintf('您被邀请协作编辑文档: %s', $doc_title); $message = sprintf( "您好 %s,nn您被邀请%s文档《%s》。nn文档链接: %snn权限级别: %snn请点击链接开始协作。nn此邮件由系统自动发送,请勿回复。", $user->display_name, $permission_level === 'viewer' ? '查看' : ($permission_level === 'commenter' ? '评论' : '编辑'), $doc_title, $doc_link, $permission_level ); wp_mail($user->user_email, $subject, $message, array( 'From: ' . get_bloginfo('name') . ' <' . $admin_email . '>' )); } } new CollaborativeDocPermissions(); ### 4.2 实时用户状态显示 // 实时用户状态管理class UserPresenceManager { constructor(docId) { this.docId = docId; this.activeUsers = new Map(); this.userColors = new Map(); this.cursorPositions = new Map(); this.initWebSocket(); this.setupHeartbeat(); } initWebSocket() { this.ws = new WebSocket(`ws://localhost:8080/presence?docId=${this.docId}&userId=${this.userId}`); this.ws.onmessage = (event) => { const data = JSON.parse(event.data); this.handlePresenceUpdate(data); }; this.ws.onopen = () => { this.sendHeartbeat(); }; } handlePresenceUpdate(data) { switch(data.type) { case 'user_joined': this.addUser(data.userId, data.userInfo); break; case 'user_left': this.removeUser(data.userId); break; case 'user_activity': this.updateUserActivity(data.userId, data.activity); break; case 'cursor_update': this.updateCursor(data.userId, data.position, data.selection); break; } } addUser(userId, userInfo) { if (!this.activeUsers.has(userId)) { this.activeUsers.set(userId, { ...userInfo, lastActive: Date.now(), color: this.getUserColor(userId) }); this.updateUI(); // 显示通知 this.showNotification(`${userInfo.name} 加入了文档`); } } removeUser(userId) { if (this.activeUsers.has(userId)) { const user = this.activeUsers.get(userId); this.activeUsers.delete(userId); this.updateUI(); // 显示通知 this.showNotification(`${user.name} 离开了文档`); // 移除光标显示 this.removeCursor(userId); } } updateCursor(userId,
在当今数字化工作环境中,协同办公已成为企业和团队提高效率的关键。根据Statista的数据,2023年全球协同软件市场规模已达到138亿美元,预计到2027年将增长至287亿美元。对于拥有WordPress网站的企业、教育机构或内容创作团队而言,集成在线协同文档编辑功能可以显著提升团队协作效率,减少邮件往来,实现实时内容共创。
传统的WordPress内容编辑方式存在明显局限:单用户编辑锁定机制导致多人协作困难;版本管理功能有限,难以追踪每次修改;缺乏实时协同编辑体验,团队成员无法同时处理同一文档。本教程将指导您通过代码二次开发,为WordPress网站添加类似Google Docs的协同编辑与版本管理功能,而无需依赖昂贵的第三方服务。
在开始编码前,我们需要明确要实现的协同编辑系统应包含以下核心功能:
- 实时协同编辑:支持多用户同时编辑同一文档,实时显示他人光标位置和编辑内容
- 版本控制系统:完整记录文档修改历史,支持版本对比与回滚
- 用户权限管理:基于WordPress用户角色设置文档访问和编辑权限
- 冲突解决机制:智能处理编辑冲突,确保数据一致性
- 评论与批注系统:支持文档内评论和批注功能
- 导出与分享:支持多种格式导出和链接分享功能
我们将采用以下技术栈实现协同编辑功能:
- 前端编辑器:使用开源协同编辑器框架如Y.js或ShareDB
- 实时通信:WebSocket协议实现实时数据同步
- 后端框架:基于WordPress REST API扩展
- 数据存储:MySQL数据库存储文档内容和版本历史
- 版本控制:自定义版本管理算法或集成Git原理
确保您的开发环境满足以下要求:
- WordPress 5.8或更高版本
- PHP 7.4+(建议使用PHP 8.0+以获得更好性能)
- MySQL 5.7+或MariaDB 10.3+
- Node.js环境(用于构建前端资源)
- WebSocket服务器(如Socket.io服务器)
首先,我们需要创建一个新的自定义文章类型来存储协同文档:
// 在主题的functions.php或自定义插件中添加
function register_collaborative_doc_type() {
$labels = array(
'name' => '协同文档',
'singular_name' => '协同文档',
'menu_name' => '协同文档',
'add_new' => '新建文档',
'add_new_item' => '新建协同文档',
'edit_item' => '编辑文档',
'new_item' => '新文档',
'view_item' => '查看文档',
'search_items' => '搜索文档',
'not_found' => '未找到文档',
'not_found_in_trash' => '回收站中无文档'
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array('slug' => 'collab-doc'),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'menu_position' => 5,
'supports' => array('title', 'author'),
'show_in_rest' => true, // 启用REST API支持
);
register_post_type('collab_doc', $args);
}
add_action('init', 'register_collaborative_doc_type');
为了实现实时协同编辑,我们需要设置WebSocket服务器处理实时通信:
// websocket-server.js - Node.js WebSocket服务器
const WebSocket = require('ws');
const http = require('http');
const server = http.createServer();
const wss = new WebSocket.Server({ server });
// 存储文档状态和连接
const documents = new Map();
wss.on('connection', (ws, request) => {
const params = new URLSearchParams(request.url.split('?')[1]);
const docId = params.get('docId');
const userId = params.get('userId');
if (!docId || !userId) {
ws.close();
return;
}
// 初始化文档状态
if (!documents.has(docId)) {
documents.set(docId, {
content: '',
users: new Map(),
version: 0
});
}
const doc = documents.get(docId);
// 存储用户连接
doc.users.set(userId, {
ws,
cursorPosition: 0,
lastActive: Date.now()
});
// 发送当前文档状态给新用户
ws.send(JSON.stringify({
type: 'init',
content: doc.content,
version: doc.version,
activeUsers: Array.from(doc.users.keys()).filter(id => id !== userId)
}));
// 广播新用户加入
broadcastToOthers(docId, userId, {
type: 'user_joined',
userId
});
// 处理客户端消息
ws.on('message', (message) => {
try {
const data = JSON.parse(message);
handleClientMessage(docId, userId, data);
} catch (error) {
console.error('消息解析错误:', error);
}
});
// 处理连接关闭
ws.on('close', () => {
if (doc.users.has(userId)) {
doc.users.delete(userId);
broadcastToOthers(docId, userId, {
type: 'user_left',
userId
});
}
});
});
function handleClientMessage(docId, userId, data) {
const doc = documents.get(docId);
if (!doc) return;
switch (data.type) {
case 'edit':
// 应用编辑操作
applyEdit(doc, data.operation);
doc.version++;
// 广播编辑给其他用户
broadcastToOthers(docId, userId, {
type: 'update',
operation: data.operation,
version: doc.version,
userId
});
break;
case 'cursor_move':
// 广播光标移动
broadcastToOthers(docId, userId, {
type: 'cursor_move',
userId,
position: data.position
});
break;
case 'selection_change':
// 广播选择变化
broadcastToOthers(docId, userId, {
type: 'selection_change',
userId,
selection: data.selection
});
break;
}
}
function applyEdit(doc, operation) {
// 根据操作类型更新文档内容
// 这里需要实现操作转换(OT)或冲突无关数据类型(CRDT)逻辑
// 简化示例:直接替换内容
if (operation.type === 'insert') {
doc.content = doc.content.slice(0, operation.position) +
operation.text +
doc.content.slice(operation.position);
} else if (operation.type === 'delete') {
doc.content = doc.content.slice(0, operation.position) +
doc.content.slice(operation.position + operation.length);
}
}
function broadcastToOthers(docId, excludeUserId, message) {
const doc = documents.get(docId);
if (!doc) return;
doc.users.forEach((user, userId) => {
if (userId !== excludeUserId && user.ws.readyState === WebSocket.OPEN) {
user.ws.send(JSON.stringify(message));
}
});
}
// 启动服务器
const PORT = process.env.PORT || 8080;
server.listen(PORT, () => {
console.log(`WebSocket服务器运行在端口 ${PORT}`);
});
创建前端编辑器界面,使用Y.js库实现协同编辑:
<!-- collaborative-editor.php - 编辑器模板文件 -->
<div id="collaborative-editor-container">
<div class="editor-header">
<h1 id="doc-title" contenteditable="true"><?php echo get_the_title(); ?></h1>
<div class="editor-toolbar">
<button class="format-btn" data-format="bold">粗体</button>
<button class="format-btn" data-format="italic">斜体</button>
<button class="format-btn" data-format="underline">下划线</button>
<select id="font-size">
<option value="12px">12px</option>
<option value="14px">14px</option>
<option value="16px" selected>16px</option>
<option value="18px">18px</option>
<option value="24px">24px</option>
</select>
<button id="save-version">保存版本</button>
<button id="export-pdf">导出PDF</button>
</div>
<div class="user-presence">
<span>在线用户: </span>
<div id="active-users"></div>
</div>
</div>
<div class="editor-content">
<div id="editor" contenteditable="true"></div>
</div>
<div class="editor-sidebar">
<div class="version-history">
<h3>版本历史</h3>
<ul id="version-list"></ul>
</div>
<div class="comments-section">
<h3>评论</h3>
<div id="comments-container"></div>
<textarea id="new-comment" placeholder="添加评论..."></textarea>
<button id="add-comment">提交评论</button>
</div>
</div>
</div>
<script>
// 协同编辑器前端JavaScript
document.addEventListener('DOMContentLoaded', function() {
const docId = <?php echo get_the_ID(); ?>;
const userId = <?php echo get_current_user_id(); ?>;
// 初始化Y.js协同编辑
const ydoc = new Y.Doc();
const ytext = ydoc.getText('content');
const editor = document.getElementById('editor');
// 连接WebSocket服务器
const ws = new WebSocket(`ws://localhost:8080?docId=${docId}&userId=${userId}`);
// 绑定Y.js到编辑器
const binding = new Y.QuillBinding(ytext, editor);
// 监听WebSocket消息
ws.onmessage = function(event) {
const data = JSON.parse(event.data);
switch(data.type) {
case 'init':
// 初始化文档内容
ytext.delete(0, ytext.length);
ytext.insert(0, data.content);
updateActiveUsers(data.activeUsers);
break;
case 'update':
// 应用远程更新
applyRemoteUpdate(data.operation);
break;
case 'user_joined':
addActiveUser(data.userId);
break;
case 'user_left':
removeActiveUser(data.userId);
break;
case 'cursor_move':
showRemoteCursor(data.userId, data.position);
break;
}
};
// 发送本地编辑到服务器
ydoc.on('update', function(update) {
ws.send(JSON.stringify({
type: 'edit',
operation: extractOperationFromUpdate(update)
}));
});
// 光标移动跟踪
editor.addEventListener('keyup', function() {
const selection = window.getSelection();
const position = getCursorPosition(editor, selection);
ws.send(JSON.stringify({
type: 'cursor_move',
position: position
}));
});
// 初始化版本历史
loadVersionHistory(docId);
// 保存版本功能
document.getElementById('save-version').addEventListener('click', function() {
saveDocumentVersion(docId, ytext.toString());
});
});
// 辅助函数
function updateActiveUsers(userIds) {
const container = document.getElementById('active-users');
container.innerHTML = '';
userIds.forEach(userId => {
const userElement = document.createElement('span');
userElement.className = 'active-user';
userElement.textContent = `用户${userId}`;
userElement.style.backgroundColor = getUserColor(userId);
container.appendChild(userElement);
});
}
function getUserColor(userId) {
// 根据用户ID生成一致的颜色
const colors = ['#FF6B6B', '#4ECDC4', '#FFD166', '#06D6A0', '#118AB2'];
return colors[userId % colors.length];
}
function loadVersionHistory(docId) {
// 通过AJAX加载版本历史
fetch(`/wp-json/collab/v1/document/${docId}/versions`)
.then(response => response.json())
.then(versions => {
const list = document.getElementById('version-list');
list.innerHTML = '';
versions.forEach(version => {
const li = document.createElement('li');
li.innerHTML = `
<span>${version.date}</span>
<span>${version.author}</span>
<button onclick="restoreVersion(${version.id})">恢复</button>
<button onclick="compareVersion(${version.id})">对比</button>
`;
list.appendChild(li);
});
});
}
function saveDocumentVersion(docId, content) {
fetch(`/wp-json/collab/v1/document/${docId}/version`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': collabVars.nonce
},
body: JSON.stringify({
content: content,
comment: document.getElementById('version-comment')?.value || '手动保存'
})
})
.then(response => response.json())
.then(data => {
if(data.success) {
alert('版本保存成功');
loadVersionHistory(docId);
}
});
}
</script>
我们需要创建自定义数据库表来存储文档版本历史:
// 创建版本管理数据库表
function create_version_tables() {
global $wpdb;
$charset_collate = $wpdb->get_charset_collate();
$table_name = $wpdb->prefix . 'collab_doc_versions';
$sql = "CREATE TABLE IF NOT EXISTS $table_name (
id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT,
doc_id bigint(20) UNSIGNED NOT NULL,
version_number int(11) NOT NULL,
content longtext NOT NULL,
author_id bigint(20) UNSIGNED NOT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
change_summary text,
parent_version_id bigint(20) UNSIGNED DEFAULT NULL,
PRIMARY KEY (id),
KEY doc_id (doc_id),
KEY author_id (author_id)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
}
register_activation_hook(__FILE__, 'create_version_tables');
创建REST API端点处理版本管理操作:
// 版本管理REST API
add_action('rest_api_init', function() {
// 获取文档版本列表
register_rest_route('collab/v1', '/document/(?P<id>d+)/versions', array(
'methods' => 'GET',
'callback' => 'get_document_versions',
'permission_callback' => 'check_document_permission'
));
// 创建新版本
register_rest_route('collab/v1', '/document/(?P<id>d+)/version', array(
'methods' => 'POST',
'callback' => 'create_document_version',
'permission_callback' => 'check_document_permission'
));
// 恢复特定版本
register_rest_route('collab/v1', '/version/(?P<id>d+)/restore', array(
'methods' => 'POST',
'callback' => 'restore_document_version',
'permission_callback' => 'check_document_permission'
));
// 比较两个版本
register_rest_route('collab/v1', '/compare-versions', array(
'methods' => 'GET',
'callback' => 'compare_versions',
'permission_callback' => 'check_document_permission'
));
});
function get_document_versions($request) {
global $wpdb;
$doc_id = $request['id'];
$table_name = $wpdb->prefix . 'collab_doc_versions';
$versions = $wpdb->get_results($wpdb->prepare(
"SELECT v.*, u.user_login as author_name
FROM $table_name v
LEFT JOIN {$wpdb->users} u ON v.author_id = u.ID
WHERE v.doc_id = %d
ORDER BY v.created_at DESC
LIMIT 50",
$doc_id
));
// 格式化返回数据
$formatted_versions = array();
foreach ($versions as $version) {
$formatted_versions[] = array(
'id' => $version->id,
'version_number' => $version->version_number,
'created_at' => $version->created_at,
'author' => $version->author_name,
'change_summary' => $version->change_summary,
'content_preview' => wp_trim_words($version->content, 20)
);
}
return rest_ensure_response($formatted_versions);
}
function create_document_version($request) {
global $wpdb;
$doc_id = $request['id'];
current_user_id();
$content = sanitize_text_field($request['content']);
$comment = sanitize_text_field($request['comment']);
// 获取当前最高版本号
$table_name = $wpdb->prefix . 'collab_doc_versions';
$latest_version = $wpdb->get_var($wpdb->prepare(
"SELECT MAX(version_number) FROM $table_name WHERE doc_id = %d",
$doc_id
));
$new_version = $latest_version ? $latest_version + 1 : 1;
// 插入新版本
$result = $wpdb->insert(
$table_name,
array(
'doc_id' => $doc_id,
'version_number' => $new_version,
'content' => $content,
'author_id' => $user_id,
'change_summary' => $comment,
'parent_version_id' => $latest_version ? $wpdb->get_var($wpdb->prepare(
"SELECT id FROM $table_name WHERE doc_id = %d AND version_number = %d",
$doc_id, $latest_version
)) : null
),
array('%d', '%d', '%s', '%d', '%s', '%d')
);
if ($result) {
return rest_ensure_response(array(
'success' => true,
'version_id' => $wpdb->insert_id,
'version_number' => $new_version
));
}
return new WP_Error('version_creation_failed', '版本创建失败', array('status' => 500));
}
function restore_document_version($request) {
global $wpdb;
$version_id = $request['id'];
// 获取版本内容
$table_name = $wpdb->prefix . 'collab_doc_versions';
$version = $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$version_id
));
if (!$version) {
return new WP_Error('version_not_found', '版本不存在', array('status' => 404));
}
// 更新文档当前内容
update_post_meta($version->doc_id, '_current_content', $version->content);
// 创建恢复记录
$user_id = get_current_user_id();
$latest_version = $wpdb->get_var($wpdb->prepare(
"SELECT MAX(version_number) FROM $table_name WHERE doc_id = %d",
$version->doc_id
));
$wpdb->insert(
$table_name,
array(
'doc_id' => $version->doc_id,
'version_number' => $latest_version + 1,
'content' => $version->content,
'author_id' => $user_id,
'change_summary' => sprintf('恢复到版本 %d', $version->version_number),
'parent_version_id' => $version->id
),
array('%d', '%d', '%s', '%d', '%s', '%d')
);
return rest_ensure_response(array(
'success' => true,
'message' => '版本恢复成功'
));
}
function compare_versions($request) {
$version1_id = $request->get_param('v1');
$version2_id = $request->get_param('v2');
global $wpdb;
$table_name = $wpdb->prefix . 'collab_doc_versions';
$version1 = $wpdb->get_row($wpdb->prepare(
"SELECT content FROM $table_name WHERE id = %d",
$version1_id
));
$version2 = $wpdb->get_row($wpdb->prepare(
"SELECT content FROM $table_name WHERE id = %d",
$version2_id
));
if (!$version1 || !$version2) {
return new WP_Error('versions_not_found', '版本不存在', array('status' => 404));
}
// 使用文本差异算法比较版本
$diff = compute_text_diff($version1->content, $version2->content);
return rest_ensure_response(array(
'diff' => $diff,
'version1' => array('id' => $version1_id),
'version2' => array('id' => $version2_id)
));
}
function compute_text_diff($text1, $text2) {
// 使用PHP内置的差异计算函数
require_once(ABSPATH . 'wp-admin/includes/diff.php');
$text1_lines = explode("n", $text1);
$text2_lines = explode("n", $text2);
$diff = new Text_Diff($text1_lines, $text2_lines);
$renderer = new Text_Diff_Renderer_inline();
return $renderer->render($diff);
}
### 3.3 自动版本保存机制
实现智能的自动版本保存功能:
// 自动版本保存机制
class AutoVersionSaver {
private $save_threshold = 30; // 每30秒检查一次
private $change_threshold = 10; // 至少10个字符变化才保存
public function __construct() {
add_action('wp_ajax_save_auto_version', array($this, 'handle_auto_save'));
add_action('wp_ajax_nopriv_save_auto_version', array($this, 'handle_auto_save'));
}
public function handle_auto_save() {
$doc_id = intval($_POST['doc_id']);
$content = wp_unslash($_POST['content']);
$last_saved_version = isset($_POST['last_saved']) ? $_POST['last_saved'] : null;
// 验证权限
if (!current_user_can('edit_post', $doc_id)) {
wp_die('权限不足');
}
// 获取上次保存的内容
$last_content = $this->get_last_saved_content($doc_id, $last_saved_version);
// 计算变化量
$changes = $this->calculate_changes($last_content, $content);
// 如果变化足够大,则保存新版本
if ($changes['change_count'] >= $this->change_threshold) {
$this->save_minor_version($doc_id, $content, $changes['summary']);
wp_send_json_success(array(
'saved' => true,
'change_summary' => $changes['summary'],
'timestamp' => current_time('mysql')
));
} else {
wp_send_json_success(array(
'saved' => false,
'message' => '变化太小,未保存版本'
));
}
}
private function get_last_saved_content($doc_id, $last_saved_version) {
global $wpdb;
$table_name = $wpdb->prefix . 'collab_doc_versions';
if ($last_saved_version) {
$content = $wpdb->get_var($wpdb->prepare(
"SELECT content FROM $table_name WHERE id = %d",
$last_saved_version
));
} else {
$content = $wpdb->get_var($wpdb->prepare(
"SELECT content FROM $table_name
WHERE doc_id = %d
ORDER BY created_at DESC LIMIT 1",
$doc_id
));
}
return $content ?: '';
}
private function calculate_changes($old_content, $new_content) {
// 简单计算变化:字符差异
$old_length = strlen($old_content);
$new_length = strlen($new_content);
$length_change = $new_length - $old_length;
// 使用更高级的差异检测(可选)
similar_text($old_content, $new_content, $similarity);
$change_percentage = 100 - $similarity;
// 生成变化摘要
$summary = sprintf(
'长度变化: %+d 字符, 相似度: %.1f%%',
$length_change,
$similarity
);
return array(
'change_count' => abs($length_change),
'summary' => $summary,
'similarity' => $similarity
);
}
private function save_minor_version($doc_id, $content, $summary) {
global $wpdb;
$table_name = $wpdb->prefix . 'collab_doc_versions';
$user_id = get_current_user_id();
// 获取当前版本号
$latest_version = $wpdb->get_var($wpdb->prepare(
"SELECT MAX(version_number) FROM $table_name WHERE doc_id = %d",
$doc_id
));
$new_version = $latest_version ? $latest_version + 1 : 1;
$wpdb->insert(
$table_name,
array(
'doc_id' => $doc_id,
'version_number' => $new_version,
'content' => $content,
'author_id' => $user_id,
'change_summary' => '自动保存: ' . $summary,
'parent_version_id' => $latest_version ? $wpdb->get_var($wpdb->prepare(
"SELECT id FROM $table_name
WHERE doc_id = %d AND version_number = %d",
$doc_id, $latest_version
)) : null
),
array('%d', '%d', '%s', '%d', '%s', '%d')
);
}
}
new AutoVersionSaver();
## 第四部分:用户权限与访问控制
### 4.1 扩展WordPress权限系统
// 协同文档权限管理
class CollaborativeDocPermissions {
public function __construct() {
add_filter('user_has_cap', array($this, 'add_collab_capabilities'), 10, 4);
add_action('admin_init', array($this, 'setup_roles_and_capabilities'));
}
public function setup_roles_and_capabilities() {
$roles = array('administrator', 'editor', 'author', 'contributor', 'subscriber');
foreach ($roles as $role_name) {
$role = get_role($role_name);
if ($role) {
// 基础权限
$role->add_cap('read_collab_doc');
// 根据角色分配不同权限
switch ($role_name) {
case 'administrator':
case 'editor':
$role->add_cap('edit_collab_docs');
$role->add_cap('edit_others_collab_docs');
$role->add_cap('publish_collab_docs');
$role->add_cap('delete_collab_docs');
$role->add_cap('manage_collab_doc_settings');
break;
case 'author':
$role->add_cap('edit_collab_docs');
$role->add_cap('publish_collab_docs');
$role->add_cap('delete_collab_docs');
break;
case 'contributor':
$role->add_cap('edit_collab_docs');
break;
}
}
}
}
public function add_collab_capabilities($allcaps, $caps, $args, $user) {
$requested_capability = $args[0];
$post_id = isset($args[2]) ? $args[2] : 0;
// 处理协同文档特定权限
if (strpos($requested_capability, 'collab_doc') !== false && $post_id) {
$post_type = get_post_type($post_id);
if ($post_type === 'collab_doc') {
// 检查文档特定权限设置
$doc_permissions = get_post_meta($post_id, '_collab_permissions', true);
if (!empty($doc_permissions)) {
$user_id = $user->ID;
// 文档所有者有完全权限
$post = get_post($post_id);
if ($post && $post->post_author == $user_id) {
$allcaps['edit_collab_docs'] = true;
$allcaps['edit_others_collab_docs'] = true;
$allcaps['delete_collab_docs'] = true;
return $allcaps;
}
// 检查用户是否在允许列表中
if (isset($doc_permissions['allowed_users'])) {
$allowed_users = $doc_permissions['allowed_users'];
if (in_array($user_id, $allowed_users)) {
$permission_level = $doc_permissions['user_levels'][$user_id] ?? 'viewer';
switch ($permission_level) {
case 'editor':
$allcaps['edit_collab_docs'] = true;
break;
case 'commenter':
$allcaps['comment_collab_docs'] = true;
break;
case 'viewer':
$allcaps['read_collab_doc'] = true;
break;
}
}
}
// 检查用户组权限
if (isset($doc_permissions['allowed_roles'])) {
$user_roles = $user->roles;
foreach ($user_roles as $role) {
if (in_array($role, $doc_permissions['allowed_roles'])) {
$allcaps['read_collab_doc'] = true;
if (in_array($role, $doc_permissions['editor_roles'])) {
$allcaps['edit_collab_docs'] = true;
}
break;
}
}
}
}
}
}
return $allcaps;
}
// 文档共享功能
public static function share_document($doc_id, $user_emails, $permission_level = 'viewer') {
$user_ids = array();
foreach ($user_emails as $email) {
$user = get_user_by('email', $email);
if ($user) {
$user_ids[] = $user->ID;
// 发送通知邮件
self::send_sharing_notification($user, $doc_id, $permission_level);
}
}
// 更新文档权限设置
$permissions = get_post_meta($doc_id, '_collab_permissions', true) ?: array();
if (!isset($permissions['allowed_users'])) {
$permissions['allowed_users'] = array();
}
foreach ($user_ids as $user_id) {
if (!in_array($user_id, $permissions['allowed_users'])) {
$permissions['allowed_users'][] = $user_id;
}
$permissions['user_levels'][$user_id] = $permission_level;
}
update_post_meta($doc_id, '_collab_permissions', $permissions);
return count($user_ids);
}
private static function send_sharing_notification($user, $doc_id, $permission_level) {
$doc_title = get_the_title($doc_id);
$doc_link = get_permalink($doc_id);
$admin_email = get_option('admin_email');
$subject = sprintf('您被邀请协作编辑文档: %s', $doc_title);
$message = sprintf(
"您好 %s,nn您被邀请%s文档《%s》。nn文档链接: %snn权限级别: %snn请点击链接开始协作。nn此邮件由系统自动发送,请勿回复。",
$user->display_name,
$permission_level === 'viewer' ? '查看' : ($permission_level === 'commenter' ? '评论' : '编辑'),
$doc_title,
$doc_link,
$permission_level
);
wp_mail($user->user_email, $subject, $message, array(
'From: ' . get_bloginfo('name') . ' <' . $admin_email . '>'
));
}
}
new CollaborativeDocPermissions();
### 4.2 实时用户状态显示
// 实时用户状态管理
class UserPresenceManager {
constructor(docId) {
this.docId = docId;
this.activeUsers = new Map();
this.userColors = new Map();
this.cursorPositions = new Map();
this.initWebSocket();
this.setupHeartbeat();
}
initWebSocket() {
this.ws = new WebSocket(`ws://localhost:8080/presence?docId=${this.docId}&userId=${this.userId}`);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.handlePresenceUpdate(data);
};
this.ws.onopen = () => {
this.sendHeartbeat();
};
}
handlePresenceUpdate(data) {
switch(data.type) {
case 'user_joined':
this.addUser(data.userId, data.userInfo);
break;
case 'user_left':
this.removeUser(data.userId);
break;
case 'user_activity':
this.updateUserActivity(data.userId, data.activity);
break;
case 'cursor_update':
this.updateCursor(data.userId, data.position, data.selection);
break;
}
}
addUser(userId, userInfo) {
if (!this.activeUsers.has(userId)) {
this.activeUsers.set(userId, {
...userInfo,
lastActive: Date.now(),
color: this.getUserColor(userId)
});
this.updateUI();
// 显示通知
this.showNotification(`${userInfo.name} 加入了文档`);
}
}
removeUser(userId) {
if (this.activeUsers.has(userId)) {
const user = this.activeUsers.get(userId);
this.activeUsers.delete(userId);
this.updateUI();
// 显示通知
this.showNotification(`${user.name} 离开了文档`);
// 移除光标显示
this.removeCursor(userId);
}
}
updateCursor(userId,


