文章目录[隐藏]
手把手教学:为WordPress网站添加在线协同文档编辑与版本管理功能
引言:为什么网站需要协同编辑功能?
在当今数字化工作环境中,协同办公已成为企业和团队提高效率的关键。根据Statista的数据,2023年全球协同软件市场规模已达到138亿美元,预计到2027年将增长至287亿美元。对于拥有WordPress网站的企业、教育机构或内容创作团队而言,集成在线协同文档编辑功能可以显著提升团队协作效率,减少邮件往来,实现实时内容共创。
传统的WordPress内容编辑方式存在明显局限:单用户编辑锁定机制导致多人协作困难;版本管理功能有限,难以追踪每次修改;缺乏实时协同编辑体验,团队成员无法同时处理同一文档。本教程将指导您通过代码二次开发,为WordPress网站添加类似Google Docs的协同编辑与版本管理功能,而无需依赖昂贵的第三方服务。
第一部分:项目规划与技术选型
1.1 功能需求分析
在开始编码前,我们需要明确要实现的协同编辑系统应包含以下核心功能:
- 实时协同编辑:支持多用户同时编辑同一文档,实时显示他人光标位置和编辑内容
- 版本控制系统:完整记录文档修改历史,支持版本对比与回滚
- 用户权限管理:基于WordPress用户角色设置文档访问和编辑权限
- 冲突解决机制:智能处理编辑冲突,确保数据一致性
- 评论与批注系统:支持文档内评论和批注功能
- 导出与分享:支持多种格式导出和链接分享功能
1.2 技术架构设计
我们将采用以下技术栈实现协同编辑功能:
- 前端编辑器:使用开源协同编辑器框架如Y.js或ShareDB
- 实时通信:WebSocket协议实现实时数据同步
- 后端框架:基于WordPress REST API扩展
- 数据存储:MySQL数据库存储文档内容和版本历史
- 版本控制:自定义版本管理算法或集成Git原理
1.3 开发环境准备
确保您的开发环境满足以下要求:
- WordPress 5.8或更高版本
- PHP 7.4+(建议使用PHP 8.0+以获得更好性能)
- MySQL 5.7+或MariaDB 10.3+
- Node.js环境(用于构建前端资源)
- WebSocket服务器(如Socket.io服务器)
第二部分:构建协同编辑核心系统
2.1 创建自定义文章类型
首先,我们需要创建一个新的自定义文章类型来存储协同文档:
// 在主题的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');
2.2 设置WebSocket服务器
为了实现实时协同编辑,我们需要设置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}`);
});
2.3 集成前端协同编辑器
创建前端编辑器界面,使用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>
第三部分:实现版本管理系统
3.1 设计版本数据库结构
我们需要创建自定义数据库表来存储文档版本历史:
// 创建版本管理数据库表
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');
3.2 实现版本控制API
创建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,
