技术方案设计文档 & API 接口开发文档
版本 v2.0 | 更新时间:2026 年 3 月
本文档详细描述了一个支持多人实时协同编辑的文档系统的完整技术方案和 API 接口规范。系统采用先进的 CRDT(Conflict-free Replicated Data Type)算法确保数据一致性,支持数百人同时在线编辑同一文档。
| 指标项 | 目标值 | 说明 |
|---|---|---|
| 并发用户数 | ≥ 500 人/文档 | 单文档最大支持并发编辑人数 |
| 同步延迟 | < 100ms | P99 延迟,局域网环境 |
| 可用性 | 99.9% | 系统月度可用性指标 |
| 数据一致性 | 最终一致性 | 所有客户端最终达到一致状态 |
| 消息吞吐量 | ≥ 10,000 TPS | 单节点每秒处理消息数 |
| 模块名称 | 职责描述 | 技术实现 |
|---|---|---|
| Connection Manager | 管理 WebSocket 连接生命周期 | ws/uWebSockets.js |
| Document Provider | 提供文档 CRDT 数据同步 | Yjs + y-websocket |
| Operation Handler | 处理编辑操作的转换和广播 | 自定义 OT/CRDT 引擎 |
| Presence Service | 维护用户在线状态和光标位置 | Redis Pub/Sub |
| Version Control | 文档版本管理和快照 | MongoDB GridFS |
| Auth Service | 用户认证和权限校验 | JWT + OAuth2 |
| 技术领域 | 选型方案 | 选型理由 |
|---|---|---|
| 框架 | React 18 + TypeScript | 组件化开发、类型安全、生态丰富 |
| 编辑器内核 | ProseMirror / TipTap | 可定制性强、支持协同编辑、性能优秀 |
| CRDT 库 | Yjs v13+ | 高性能、小体积、完善的协同原语 |
| 状态管理 | Zustand / Jotai | 轻量级、响应式、TypeScript 友好 |
| 网络通信 | WebSocket API + Axios | 实时双向通信 + RESTful API |
| 本地存储 | IndexedDB + LocalStorage | 大容量离线存储 + 配置缓存 |
| 技术领域 | 选型方案 | 选型理由 |
|---|---|---|
| 主要语言 | Node.js 20 LTS + Go 1.21 | 高并发 I/O + 高性能计算 |
| WebSocket 服务 | uWebSockets.js | 业界最高性能 WebSocket 库 |
| API 框架 | Express / Fastify + Gin | 轻量快速、中间件丰富 |
| CRDT 服务端 | Yjs + y-websocket | 与前端同构、简化开发 |
| 消息队列 | Apache Kafka | 高吞吐、持久化、流处理 |
| 缓存 | Redis 7 Cluster | 高性能、数据结构丰富、Pub/Sub |
| 数据库 | 用途 | 数据特点 |
|---|---|---|
| MongoDB 6.0 | 文档内容、操作日志 | JSON 文档、灵活 Schema、水平扩展 |
| PostgreSQL 15 | 用户信息、权限、元数据 | 关系型、ACID、复杂查询 |
| Redis 7 | 会话、在线状态、热点数据 | 内存 KV、超高吞吐、过期策略 |
| MinIO | 附件、图片、文件存储 | S3 兼容、分布式、低成本 |
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
│ User │ │ Document │ │ Workspace │
├─────────────┤ ├──────────────┤ ├─────────────┤
│ id │◄────►│ id │◄────►│ id │
│ email │ │ title │ │ name │
│ username │ │ workspace_id │ │ owner_id │
│ avatar │ │ created_by │ │ created_at │
│ created_at │ │ created_at │ └─────────────┘
└─────────────┘ │ updated_at │
│ content │
└──────────────┘
│
▼
┌──────────────┐
│ Operation │
├──────────────┤
│ id │
│ document_id │
│ user_id │
│ type │
│ data │
│ timestamp │
│ version │
└──────────────┘
{
"_id": "ObjectId",
"title": "String",
"workspaceId": "ObjectId",
"createdBy": "ObjectId",
"updatedAt": "Date",
"createdAt": "Date",
"content": {
"type": "Binary", // Yjs 二进制更新数据
"encoding": "UInt8Array"
},
"snapshot": {
"type": "Binary", // 定期快照
"version": "Number",
"createdAt": "Date"
},
"metadata": {
"tags": ["String"],
"color": "String",
"icon": "String"
},
"settings": {
"allowComments": "Boolean",
"allowSuggestions": "Boolean",
"lockEditing": "Boolean"
},
"stats": {
"viewCount": "Number",
"editCount": "Number",
"collaboratorCount": "Number"
},
"version": "Number", // 当前版本号
"isDeleted": "Boolean",
"deletedAt": "Date"
}
{
"_id": "ObjectId",
"documentId": "ObjectId",
"userId": "ObjectId",
"username": "String",
"type": "String", // insert, delete, update, format
"data": {
"position": "Number",
"length": "Number",
"content": "String",
"attributes": "Object"
},
"timestamp": "Date",
"version": "Number",
"clientId": "String", // 客户端唯一标识
"siteId": "Number", // CRDT site ID
"lamport": "Number", // Lamport 时间戳
"vectorClock": "Object"
}
{
"_id": "ObjectId",
"email": "String",
"username": "String",
"passwordHash": "String",
"avatar": "String",
"role": "String", // admin, member, guest
"workspaces": ["ObjectId"],
"preferences": {
"theme": "String",
"language": "String",
"notifications": "Object"
},
"lastActiveAt": "Date",
"createdAt": "Date",
"isVerified": "Boolean",
"isOnline": "Boolean"
}
-- 工作空间表
CREATE TABLE workspaces (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
description TEXT,
owner_id UUID NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 文档权限表
CREATE TABLE document_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL,
user_id UUID NOT NULL,
role VARCHAR(50) NOT NULL, -- owner, editor, commenter, viewer
granted_by UUID,
granted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
expires_at TIMESTAMP,
UNIQUE(document_id, user_id)
);
-- 评论表
CREATE TABLE comments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID NOT NULL,
parent_comment_id UUID,
user_id UUID NOT NULL,
content TEXT NOT NULL,
position_start INTEGER,
position_end INTEGER,
status VARCHAR(20) DEFAULT 'active', -- active, resolved, deleted
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 索引优化
CREATE INDEX idx_document_permissions_doc ON document_permissions(document_id);
CREATE INDEX idx_document_permissions_user ON document_permissions(user_id);
CREATE INDEX idx_comments_document ON comments(document_id);
CREATE INDEX idx_operations_document_version ON operations(document_id, version);
# 在线用户集合
SET workspace:{workspaceId}:users {userId}
EXPIRE workspace:{workspaceId}:users 300
# 文档活跃用户
HSET doc:{docId}:presence {userId} "{jsonUserData}"
EXPIRE doc:{docId}:presence 60
# 用户光标位置
HSET doc:{docId}:cursors {userId} "{cursorData}"
EXPIRE doc:{docId}:cursors 30
# 会话 Token
SETEX session:{sessionId} 86400 "{sessionData}"
# 速率限制计数器
INCR rate_limit:{userId}:{endpoint}
EXPIRE rate_limit:{userId}:{endpoint} 60
# 发布订阅频道
PUBLISH doc:{docId}:updates "{updateMessage}"
本系统采用 CRDT(Conflict-free Replicated Data Type) 作为核心协同算法,相比传统 OT(Operational Transformation)算法,CRDT 具有去中心化、数学可证明一致性、更好的离线支持等优势。
| 特性 | CRDT | OT |
|---|---|---|
| 一致性保证 | 数学可证明 | 依赖转换函数正确性 |
| 架构模式 | 去中心化 P2P | 中心化服务器 |
| 离线支持 | 优秀 | 一般 |
| 实现复杂度 | 中等 | 高 |
| 性能开销 | 低 | 中等 |
| 代表产品 | Figma, Notion | Google Docs |
// 客户端 A 操作
const ydoc = new Y.Doc();
const ytext = ydoc.getText('content');
// 用户输入 "Hello"
ytext.insert(0, 'Hello');
// 生成更新
const update = Y.encodeStateAsUpdate(ydoc);
// 发送到服务端和其他客户端
websocket.send(update);
// 客户端 B 接收更新
const remoteUpdate = websocket.receive();
Y.applyUpdate(ydoc, remoteUpdate);
// 自动合并,保证一致性
场景:两个用户同时在位置 5 插入不同文本
解决方案:使用 Client ID + Lamport 时间戳确定顺序
if (lamportA !== lamportB) {
return lamportA < lamportB ? -1 : 1;
} else {
return clientIdA < clientIdB ? -1 : 1;
}
场景:用户对同一段文本应用不同格式
解决方案:Last-Writer-Wins + 属性合并
// 格式属性合并策略
const mergedAttributes = {
...baseAttributes,
...newAttributes,
timestamp: Math.max(timestampA, timestampB)
};
https://api.collab-doc.com/v2创建新用户账户
{
"email": "user@example.com",
"username": "string (3-20 字符)",
"password": "string (最少 8 位)",
"inviteCode": "string (可选)"
}
{
"success": true,
"data": {
"userId": "uuid",
"email": "user@example.com",
"username": "username",
"accessToken": "jwt_token",
"refreshToken": "refresh_token"
},
"message": "注册成功"
}
{
"email": "user@example.com",
"password": "string"
}
{
"success": true,
"data": {
"userId": "uuid",
"accessToken": "jwt_token",
"refreshToken": "refresh_token",
"expiresIn": 3600
}
}
Authorization: Bearer {refreshToken}
{
"success": true,
"data": {
"accessToken": "new_jwt_token",
"expiresIn": 3600
}
}
{
"workspaceId": "uuid (可选)",
"page": "number (默认 1)",
"limit": "number (默认 20)",
"sortBy": "updatedAt|createdAt|title",
"order": "asc|desc",
"search": "string (可选)"
}
{
"success": true,
"data": {
"documents": [
{
"id": "uuid",
"title": "文档标题",
"workspaceId": "uuid",
"createdBy": {"id": "uuid", "name": "用户名"},
"updatedAt": "ISO8601",
"collaborators": 5,
"preview": "文档预览内容..."
}
],
"pagination": {
"total": 100,
"page": 1,
"limit": 20,
"totalPages": 5
}
}
}
{
"title": "新文档",
"workspaceId": "uuid",
"content": "初始内容 (可选)",
"templateId": "uuid (可选)",
"visibility": "private|workspace|public"
}
{
"success": true,
"data": {
"id": "uuid",
"title": "新文档",
"createdAt": "ISO8601",
"accessUrl": "/docs/uuid/edit"
}
}
documentId: string (required)
{
"includeContent": "boolean (默认 true)",
"version": "number (可选,获取历史版本)"
}
{
"success": true,
"data": {
"id": "uuid",
"title": "文档标题",
"content": "完整内容",
"createdBy": {...},
"collaborators": [...],
"permissions": "owner|editor|viewer",
"version": 42,
"updatedAt": "ISO8601"
}
}
{
"title": "新标题 (可选)",
"workspaceId": "uuid (可选)",
"settings": {
"allowComments": true,
"lockEditing": false
}
}
软删除,可在回收站恢复
{
"success": true,
"message": "文档已移至回收站"
}
{
"success": true,
"data": {
"workspaces": [
{
"id": "uuid",
"name": "工作空间名称",
"role": "owner|member",
"documentCount": 25,
"memberCount": 10
}
]
}
}
{
"name": "工作空间名称",
"description": "描述 (可选)",
"visibility": "private|public"
}
{
"userId": "uuid",
"role": "editor|commenter|viewer",
"expiresIn": 86400 (可选,秒)
}
{
"limit": "number (默认 50)",
"before": "ISO8601 (可选)"
}
{
"success": true,
"data": {
"versions": [
{
"version": 42,
"timestamp": "ISO8601",
"userId": "uuid",
"username": "用户名",
"operationCount": 15,
"summary": "添加了第三章内容"
}
]
}
}
{
"content": "评论内容",
"positionStart": 100,
"positionEnd": 150,
"parentCommentId": "uuid (可选,回复评论)"
}
// 标准错误响应
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "人类可读的错误描述",
"details": {} // 可选的详细错误信息
}
}
// 常见错误码
{
"UNAUTHORIZED": "未授权访问",
"FORBIDDEN": "权限不足",
"NOT_FOUND": "资源不存在",
"VALIDATION_ERROR": "参数验证失败",
"RATE_LIMIT_EXCEEDED": "请求频率超限",
"DOCUMENT_LOCKED": "文档已被锁定",
"VERSION_CONFLICT": "版本冲突"
}
wss://ws.collab-doc.com/v2/wswss://ws.collab-doc.com/v2/ws?token={jwt_token}&documentId={doc_id}&clientId={client_id}
// 服务端发送的欢迎消息
{
"type": "welcome",
"data": {
"clientId": "assigned_client_id",
"documentId": "doc_id",
"serverTime": "ISO8601",
"protocolVersion": "2.0"
}
}
{
"type": "message_type",
"requestId": "uuid (可选,用于请求 - 响应配对)",
"timestamp": "number (毫秒时间戳)",
"data": {}
}
{
"type": "join-document",
"requestId": "uuid",
"data": {
"documentId": "uuid",
"lastSyncedVersion": "number (可选)"
}
}
// 响应
{
"type": "document-state",
"requestId": "uuid",
"data": {
"documentId": "uuid",
"content": "binary_update",
"version": 42,
"collaborators": [...],
"cursors": {...}
}
}
{
"type": "sync-update",
"data": {
"update": "base64_encoded_binary",
"version": 43,
"clientId": "client_id"
}
}
{
"type": "cursor-update",
"data": {
"selection": {
"anchor": {"pos": 100, "offset": 0},
"head": {"pos": 150, "offset": 5}
},
"scrollPosition": {"top": 500},
"userColor": "#FF5733"
}
}
{
"type": "awareness-update",
"data": {
"status": "active|idle|offline",
"typing": true,
"metadata": {
"browser": "Chrome 120",
"platform": "Windows"
}
}
}
{
"type": "request-snapshot",
"requestId": "uuid",
"data": {
"version": 40
}
}
{
"type": "sync-update",
"data": {
"update": "base64_encoded_binary",
"version": 43,
"clientId": "other_client_id",
"userId": "user_id"
}
}
{
"type": "cursor-broadcast",
"data": {
"clientId": "client_id",
"userId": "user_id",
"username": "用户名",
"selection": {...},
"color": "#FF5733"
}
}
{
"type": "user-joined",
"data": {
"userId": "uuid",
"username": "用户名",
"avatar": "url",
"color": "#FF5733",
"role": "editor"
}
}
{
"type": "user-left",
"data": {
"userId": "uuid",
"username": "用户名"
}
}
{
"type": "awareness-change",
"data": {
"clientId": "client_id",
"userId": "user_id",
"status": "idle",
"typing": false
}
}
{
"type": "version-created",
"data": {
"version": 43,
"timestamp": "ISO8601",
"userId": "uuid",
"operationCount": 10
}
}
{
"type": "error",
"requestId": "uuid (如果是对请求的响应)",
"data": {
"code": "SYNC_VERSION_MISMATCH",
"message": "版本不匹配",
"expectedVersion": 42,
"receivedVersion": 40
}
}
// 客户端发送心跳
{
"type": "ping",
"timestamp": 1234567890
}
// 服务端响应
{
"type": "pong",
"timestamp": 1234567890,
"serverTime": 1234567895
}
// 超时断开:90 秒无通信自动断开
join-document 携带 lastSyncedVersion| 角色 | 查看 | 编辑 | 评论 | 分享 | 删除 | 权限管理 |
|---|---|---|---|---|---|---|
| Owner | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Editor | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| Commenter | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ |
| Viewer | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
apiVersion: apps/v1
kind: Deployment
metadata:
name: collab-ws-service
spec:
replicas: 5
selector:
matchLabels:
app: collab-ws
template:
spec:
containers:
- name: ws-server
image: collab-doc/ws-server:v2.0
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
ports:
- containerPort: 8080
env:
- name: REDIS_URL
valueFrom:
secretKeyRef:
name: app-secrets
key: redis-url
---
apiVersion: v1
kind: Service
metadata:
name: ws-service
spec:
selector:
app: collab-ws
ports:
- port: 80
targetPort: 8080
type: LoadBalancer
| 指标类别 | 关键指标 | 告警阈值 |
|---|---|---|
| 性能 | P99 延迟、TPS、错误率 | 延迟>500ms, 错误率>1% |
| 资源 | CPU、内存、磁盘、网络 | CPU>80%, 内存>85% |
| 业务 | 在线用户数、文档数、操作数 | 突增/突降 50% |
| 连接 | WebSocket 连接数、断线率 | 断线率>5% |