为了支持多人协作,系统实现了完整的团队协作功能:

10.1 团队管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
graph TD
A[用户] -->|创建| B[团队]
A -->|管理| C[团队成员]
A -->|分配| D[成员角色]
B -->|拥有| E[团队文档]
C -->|访问| E

subgraph "角色权限"
D1[owner-所有者]
D2[admin-管理员]
D3[member-成员]
end

D1 -->|完全控制| B
D2 -->|管理权限| C
D3 -->|使用权限| E

10.2 文档归属与权限

每个云文档都归属于一个团队或个人,系统中的文档权限设计如下:

  • 文档类型

    • 个人文档:归属于个人,可自行决定是否共享
    • 团队文档:归属于团队,团队成员默认具有读写权限
  • 权限级别

    • read:只读权限
    • write:编辑权限
    • admin:管理权限(可进行权限分配)
  • 权限分配

    • 文档创建者自动获得管理权限
    • 团队文档对团队成员默认具有write权限
    • 团队管理员可调整成员对特定文档的权限

10.3 团队文档工作流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
sequenceDiagram
participant User1 as 团队成员1
participant User2 as 团队成员2
participant API as REST API
participant Team as 团队服务
participant Doc as 文档服务
participant WS as WebSocket服务

User1->>API: 创建团队文档
API->>Team: 验证团队成员身份
Team-->>API: 验证通过
API->>Doc: 创建团队文档
Doc->>Doc: 设置默认权限
Doc-->>API: 返回文档ID
API-->>User1: 创建成功

User2->>API: 查询团队文档列表
API->>Team: 验证团队成员身份
Team-->>API: 验证通过
API->>Doc: 获取团队文档列表
Doc-->>API: 返回文档列表
API-->>User2: 文档列表

User2->>WS: 建立WebSocket连接
WS->>Doc: 验证文档访问权限
Doc->>Team: 检查团队权限
Team-->>Doc: 返回权限信息
Doc-->>WS: 验证通过
WS-->>User2: 连接成功

User1->>WS: 发送文档更新
WS->>WS: 广播到所有连接的成员
WS-->>User2: 接收实时更新

10.4 团队功能API

团队功能提供以下API接口:

  • 团队管理:

    • POST /teams - 创建团队
    • PUT /teams/{teamId} - 更新团队信息
    • DELETE /teams/{teamId} - 删除团队
    • GET /teams/{teamId} - 获取团队信息
    • GET /teams - 获取用户所在团队列表
  • 团队成员管理:

    • POST /teams/{teamId}/members - 添加团队成员
    • PUT /teams/{teamId}/members/{userId}/role - 更新成员角色
    • DELETE /teams/{teamId}/members/{userId} - 移除团队成员
    • GET /teams/{teamId}/members - 获取团队成员列表
  • 团队文档:

    • POST /documents/team/{teamId} - 创建团队文档
    • GET /documents/team/{teamId} - 获取团队文档列表# Yjs+Monaco 协同编辑平台架构实现文档

1. 系统架构概述

本文档详细描述了支持Yjs+Monaco前端协同编辑的后端系统架构实现。该系统采用MySQL存储元数据和MongoDB存储文档内容的混合数据存储策略,主要负责实时数据同步、持久化存储、版本管理和用户权限控制。另外,系统支持团队协作功能,每个文档都可以归属于一个团队,团队成员可以共同编辑文档。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
graph TD
Client1["前端客户端1 (Yjs+Monaco)"] <-->|WebSocket| WS[WebSocket服务]
Client2["前端客户端2 (Yjs+Monaco)"] <-->|WebSocket| WS
Client3["前端客户端3 (Yjs+Monaco)"] <-->|WebSocket| WS

Client1 <-->|HTTP| RC[REST控制器]
Client2 <-->|HTTP| RC
Client3 <-->|HTTP| RC

WS <--> YDoc[文档同步服务]
RC <--> DocService[文档服务]
RC <--> TeamService[团队服务]

YDoc <--> DocService
DocService <--> YDoc
DocService <--> TeamService

DocService <--> MetaDB[(MySQL元数据)]
YDoc <--> ContentDB[(MongoDB内容)]
TeamService <--> MetaDB

Auth[认证授权服务] <--> Client1
Auth <--> Client2
Auth <--> Client3
Auth <--> WS
Auth <--> RC

Cache[Redis缓存] <--> DocService
Cache <--> YDoc
Cache <--> TeamService

Task[定时任务] --> YDoc

subgraph "Java SpringBoot后端"
WS
RC
YDoc
DocService
TeamService
Auth
Task
end

2. 数据模型设计

2.1 MySQL元数据模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
classDiagram
class User {
+Long id
+String username
+String password
+String email
+String avatar
+Integer status
+LocalDateTime createTime
+LocalDateTime updateTime
+Boolean isDeleted
}

class Team {
+Long id
+String name
+String description
+Long ownerId
+LocalDateTime createTime
+LocalDateTime updateTime
+Boolean isDeleted
}

class TeamMember {
+Long id
+Long teamId
+Long userId
+String role
+LocalDateTime createTime
+LocalDateTime updateTime
+Boolean isDeleted
}

class DocumentMeta {
+Long id
+String docId
+String title
+String description
+String type
+String language
+Long ownerId
+Long teamId
+String sharedType
+Integer status
+LocalDateTime lastEditTime
+Long lastEditUserId
+LocalDateTime createTime
+LocalDateTime updateTime
+Boolean isDeleted
}

class DocumentPermission {
+Long id
+String docId
+Long userId
+Long teamId
+String permissionType
+LocalDateTime createTime
+LocalDateTime updateTime
+Boolean isDeleted
}

class DocumentVersion {
+Long id
+String docId
+Integer version
+String title
+Long userId
+LocalDateTime createTime
+Boolean isDeleted
}

class DocumentHistory {
+Long id
+String docId
+Long userId
+String operationType
+String operationDesc
+LocalDateTime createTime
}

User "1" --> "*" TeamMember : 创建/加入
Team "1" --> "*" TeamMember : 包含
Team "1" -- "1" User : 所有者
User "1" --> "*" DocumentMeta : 创建
Team "1" --> "*" DocumentMeta : 拥有
DocumentMeta "1" --> "*" DocumentPermission : 授权
DocumentMeta "1" --> "*" DocumentVersion : 版本历史
DocumentMeta "1" --> "*" DocumentHistory : 操作历史
User "1" --> "*" DocumentVersion : 创建版本
User "1" --> "*" DocumentHistory : 操作记录
TeamMember --o Team : 成员关系
TeamMember --o User : 成员关系
DocumentPermission --o User : 个人权限
DocumentPermission --o Team : 团队权限

2.2 MongoDB文档内容模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
classDiagram
class YjsDocument {
+String id
+String docId
+Binary stateVector
+List~UpdateEntry~ updates
+LocalDateTime createdAt
+LocalDateTime lastModifiedAt
+Map~String,Object~ metadata
+addUpdate(byte[] update, Long userId, String username)
}

class UpdateEntry {
+LocalDateTime timestamp
+Long userId
+String username
+Binary update
}

class DocumentVersionContent {
+String id
+String docId
+Integer version
+Binary stateVector
+Long userId
+String username
+LocalDateTime createdAt
+String remark
}

YjsDocument "1" --> "*" UpdateEntry
YjsDocument "1" -- "*" DocumentVersionContent

3. 系统组件详细实现

3.1 数据访问层

实现了基于MyBatis Plus的MySQL数据访问层和基于Spring Data MongoDB的MongoDB数据访问层:

3.1.1 MySQL Mapper

MyBatis Plus Mapper接口用于操作MySQL中的元数据,包括:

  • DocumentMetaMapper - 文档元数据管理
  • DocumentPermissionMapper - 文档权限管理
  • DocumentVersionMapper - 文档版本管理
  • DocumentHistoryMapper - 文档历史记录管理
  • TeamMapper - 团队管理
  • TeamMemberMapper - 团队成员管理
  • UserMapper - 用户管理

3.1.2 MongoDB Repository

MongoDB Repository接口用于操作MongoDB中的文档内容数据,包括:

  • YjsDocumentRepository - Yjs文档内容管理
  • DocumentVersionContentRepository - 文档版本内容管理

3.2 服务层

服务层实现了业务逻辑处理,主要包括以下服务:

3.2.1 YjsDocumentService

Yjs文档服务,负责文档内容的存储、加载和版本管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface YjsDocumentService {
// 存储文档更新
void storeUpdate(String docId, byte[] update, Long userId, String username);

// 获取文档当前状态
byte[] getDocumentState(String docId);

// 创建文档版本
Integer createVersion(String docId, String title, String remark, Long userId, String username);

// 获取文档版本列表
List<DocumentVersionVO> getVersions(String docId);

// 回滚到指定版本
byte[] rollbackToVersion(String docId, Integer version);

// 其他文档操作方法
// ...
}

3.2.2 DocumentService

文档元数据服务,负责文档元数据和权限管理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public interface DocumentService {
// 创建文档
String createDocument(DocumentMeta documentMeta, Long userId);

// 创建团队文档
String createTeamDocument(DocumentMeta documentMeta, Long teamId, Long userId);

// 更新文档元数据
boolean updateDocument(DocumentMeta documentMeta, Long userId);

// 删除文档
boolean deleteDocument(String docId, Long userId);

// 获取文档元数据
DocumentMetaVO getDocumentMeta(String docId, Long userId);

// 查询文档列表
IPage<DocumentMetaVO> getUserDocuments(Page<DocumentMetaVO> page, Long userId, String keyword);

// 检查文档权限
boolean checkPermission(String docId, Long userId, String permissionType);

// 其他文档元数据和权限操作方法
// ...
}

3.2.3 TeamService

团队服务,负责团队管理和成员权限控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface TeamService {
// 创建团队
Long createTeam(Team team, Long userId);

// 更新团队
boolean updateTeam(Team team, Long userId);

// 删除团队
boolean deleteTeam(Long teamId, Long userId);

// 添加团队成员
boolean addTeamMember(Long teamId, Long userId, String role, Long adminId);

// 更新成员角色
boolean updateTeamMemberRole(Long teamId, Long userId, String role, Long adminId);

// 移除团队成员
boolean removeTeamMember(Long teamId, Long userId, Long adminId);

// 检查用户权限
boolean isTeamMember(Long teamId, Long userId);
boolean isTeamAdmin(Long teamId, Long userId);

// 其他团队管理方法
// ...
}

3.3 WebSocket通信

WebSocket是协同编辑的核心,用于实时传输Yjs更新数据:

3.3.1 WebSocket配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {

@Autowired
private YjsWebSocketHandler yjsWebSocketHandler;

@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(yjsWebSocketHandler, "/ws/yjs/{docId}")
.addInterceptors(new HttpSessionHandshakeInterceptor())
.setAllowedOrigins("*");
}
}

3.3.2 WebSocket处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
@Slf4j
public class YjsWebSocketHandler extends TextWebSocketHandler {

@Autowired
private YjsDocumentService yjsDocumentService;

@Autowired
private DocumentService documentService;

private final Map<String, Set<WebSocketSession>> roomSessions = new ConcurrentHashMap<>();
private final Map<String, Long> sessionUserMap = new ConcurrentHashMap<>();

// WebSocket连接建立
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
// 实现连接建立逻辑
}

// 处理二进制消息(Yjs更新)
@Override
protected void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
// 实现Yjs更新处理逻辑
}

// 其他WebSocket处理方法
// ...
}

3.4 REST API控制器

REST API提供了文档管理的HTTP接口:

3.4.1 文档控制器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@RestController
@RequestMapping("/documents")
@Slf4j
public class DocumentController {

@Autowired
private DocumentService documentService;

@Autowired
private YjsDocumentService yjsDocumentService;

// 创建文档
@PostMapping
public ApiResult<String> createDocument(@RequestBody DocumentMeta documentMeta) {
// 实现创建文档逻辑
}

// 更新文档
@PutMapping("/{docId}")
public ApiResult<Boolean> updateDocument(@PathVariable String docId, @RequestBody DocumentMeta documentMeta) {
// 实现更新文档逻辑
}

// 其他文档管理接口
// ...
}

3.5 缓存配置

使用Redis缓存提高系统性能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableCaching
public class RedisConfig {

@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// Redis模板配置
}

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
// 缓存管理器配置
}
}

3.6 定时任务

实现文档优化定时任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
@Slf4j
public class DocumentMaintenanceTask {

@Autowired
private YjsDocumentService yjsDocumentService;

@Autowired
private DocumentMetaMapper documentMetaMapper;

// 每天凌晨2点执行文档优化任务
@Scheduled(cron = "0 0 2 * * ?")
public void optimizeDocuments() {
// 实现文档优化逻辑
}
}

4. 关键流程实现

4.1 文档加载流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
sequenceDiagram
participant Client as 前端客户端
participant WS as WebSocket处理器
participant Doc as 文档服务
participant YDoc as Yjs文档服务
participant Cache as Redis缓存
participant MongoDB as MongoDB

Note over Client,MongoDB: WebSocket建立连接
Client->>WS: 建立WebSocket连接 (docId, token)
WS->>Doc: 检查文档访问权限
Doc-->>WS: 权限验证结果

alt 有权限访问
WS->>YDoc: 获取文档状态 (docId)
YDoc->>Cache: 查询缓存

alt 缓存命中
Cache-->>YDoc: 返回文档状态
else 缓存未命中
YDoc->>MongoDB: 查询文档
MongoDB-->>YDoc: 返回文档数据
YDoc->>Cache: 缓存文档状态
end

YDoc-->>WS: 返回文档状态
WS-->>Client: 发送初始文档状态
Note over Client: 应用文档状态到本地Yjs文档
else 无权限访问
WS-->>Client: 关闭连接(无权限)
end

4.2 文档更新流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
sequenceDiagram
participant Client1 as 客户端1
participant Client2 as 客户端2
participant WS as WebSocket处理器
participant Doc as 文档服务
participant YDoc as Yjs文档服务
participant Cache as Redis缓存
participant MongoDB as MongoDB
participant MySQL as MySQL

Note over Client1,MySQL: 编辑文档并同步
Client1->>WS: 发送文档更新
WS->>Doc: 检查编辑权限
Doc-->>WS: 权限验证结果

alt 有编辑权限
WS->>YDoc: 存储更新
YDoc->>MongoDB: 保存更新数据
YDoc->>Cache: 更新缓存

WS->>Doc: 更新最后编辑信息
Doc->>MySQL: 更新元数据

WS->>Client2: 广播更新给其他客户端
Note over Client2: 应用更新到本地Yjs文档
else 无编辑权限
WS-->>Client1: 忽略更新(无权限)
end

4.3 版本管理流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
sequenceDiagram
participant Client as 客户端
participant API as REST API
participant Doc as 文档服务
participant YDoc as Yjs文档服务
participant MySQL as MySQL
participant MongoDB as MongoDB

Note over Client,MongoDB: 创建文档版本
Client->>API: 请求创建版本
API->>Doc: 检查编辑权限
Doc-->>API: 权限验证结果

alt 有编辑权限
API->>YDoc: 创建版本
YDoc->>MongoDB: 获取当前文档状态
MongoDB-->>YDoc: 返回文档数据

YDoc->>MongoDB: 保存版本内容
YDoc->>MySQL: 保存版本元数据

MySQL-->>YDoc: 返回版本号
YDoc-->>API: 返回版本号
API-->>Client: 返回版本创建结果
else 无编辑权限
API-->>Client: 返回错误(无权限)
end

5. 数据存储优化策略

5.1 MongoDB数据模型优化

MongoDB中的Yjs文档存储采用以下优化策略:

  1. 增量更新存储

    • 将每次更新作为单独条目存储在updates数组中
    • 包含时间戳、用户ID和用户名等元数据
  2. 状态向量存储

    • 保存最新的文档状态向量,用于新客户端快速加载
  3. 更新合并策略

    • 当更新数量超过阈值时,进行更新合并
    • 仅保留最近的N个更新,减少存储空间占用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void optimizeUpdates(YjsDocument document) {
List<YjsDocument.UpdateEntry> updates = document.getUpdates();

if (updates.size() <= KEEP_RECENT_UPDATES) {
return;
}

log.info("Optimizing document: {}, updates count: {}", document.getDocId(), updates.size());

// 仅保留最近的N个更新
document.setUpdates(
updates.subList(updates.size() - KEEP_RECENT_UPDATES, updates.size())
);

log.info("After optimization, updates count: {}", document.getUpdates().size());
}

5.2 缓存策略

使用Redis缓存优化文档加载性能:

  1. 文档状态缓存

    • 缓存文档最新状态,避免频繁查询MongoDB
    • 设置合理的过期时间(如10分钟)
  2. 文档元数据缓存

    • 缓存文档元数据,提高权限检查性能
    • 设置较长的过期时间(如30分钟)
1
2
3
4
5
6
7
8
9
10
11
@Cacheable(value = "documentStates", key = "#docId")
public byte[] getDocumentState(String docId) {
return yjsDocumentRepository.findByDocId(docId)
.map(doc -> doc.getStateVector().getData())
.orElse(null);
}

@CacheEvict(value = "documentStates", key = "#docId")
public void storeUpdate(String docId, byte[] update, Long userId, String username) {
// 存储更新逻辑
}

5.3 定时优化任务

实现定时任务对文档进行优化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Scheduled(cron = "0 0 2 * * ?")
public void optimizeDocuments() {
log.info("Starting document optimization task");

List<DocumentMeta> documents = documentMetaMapper.selectList(
new LambdaQueryWrapper<DocumentMeta>()
.eq(DocumentMeta::getStatus, 0)
);

// 遍历文档进行优化
for (DocumentMeta document : documents) {
try {
yjsDocumentService.optimizeDocument(document.getDocId());
} catch (Exception e) {
log.error("Failed to optimize document: {}", document.getDocId(), e);
}
}
}

6. 部署架构

系统可以采用以下部署架构实现高可用性和可扩展性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
graph TD
Client[客户端浏览器] <-->|HTTPS/WSS| LB[负载均衡器]

subgraph "应用服务器集群"
App1[应用实例1]
App2[应用实例2]
App3[应用实例3]
end

LB <--> App1
LB <--> App2
LB <--> App3

App1 <-->|分布式缓存| Redis[Redis集群]
App2 <-->|分布式缓存| Redis
App3 <-->|分布式缓存| Redis

App1 <--> MySQL[(MySQL主从集群)]
App2 <--> MySQL
App3 <--> MySQL

App1 <--> MongoDB[(MongoDB复制集)]
App2 <--> MongoDB
App3 <--> MongoDB

6.1 水平扩展支持

  1. 无状态应用设计

    • WebSocket连接信息保存在Redis中
    • 用户会话通过Redis共享
    • 所有应用实例可以处理任何请求
  2. Redis分布式锁

    • 用于处理并发操作
    • 保证数据一致性
  3. MongoDB分片

    • 对大型文档集合进行分片
    • 提高数据存储和查询性能

7. 安全机制

系统实现了多层次的安全机制:

  1. 身份验证

    • 基于token的认证机制
    • WebSocket连接需要有效token
  2. 权限控制

    • 文档级权限控制(读、写、管理)
    • 用户和团队权限分离
    • 权限缓存优化
  3. 数据加密

    • 传输加密(HTTPS/WSS)
    • 敏感数据存储加密

8. 总结与展望

本系统实现了一个完整的基于Yjs+Monaco的协同编辑平台,采用MySQL+MongoDB混合存储架构,具有以下特点:

  1. 实时协作:基于WebSocket的低延迟实时数据同步
  2. 高效存储:混合存储架构,元数据与内容分离
  3. 版本管理:完整的文档版本控制功能
  4. 权限系统:细粒度的文档访问权限控制
  5. 性能优化:缓存、增量更新和定时优化机制

未来优化方向

  1. WebSocket集群扩展

    • 实现基于Redis的WebSocket集群,支持更大规模用户同时在线
  2. 文档快照机制

    • 优化大文档的历史版本存储,实现快照与增量更新结合的存储策略
  3. 实时分析

    • 添加用户编辑行为分析功能,提供协作洞察
  4. 离线编辑支持

    • 实现基于IndexedDB的本地存储机制,支持离线编辑后的自动同步