feat(gateway/chat): 新增聊天网关模块

范围:src/gateway/chat/
- 新增 ChatWebSocketGateway WebSocket 网关,处理实时聊天通信
- 新增 ChatController HTTP 控制器,提供聊天历史和系统状态接口
- 新增 ChatGatewayModule 模块配置,整合网关层组件
- 新增请求/响应 DTO 定义,提供数据验证和类型约束
- 新增完整的单元测试覆盖
- 新增模块 README 文档,包含接口说明、核心特性和风险评估
This commit is contained in:
moyin
2026-01-14 19:11:25 +08:00
parent 3f3c29354e
commit 5bcf3cb678
8 changed files with 1702 additions and 0 deletions

333
src/gateway/chat/README.md Normal file
View File

@@ -0,0 +1,333 @@
# 聊天网关模块 (Chat Gateway Module)
聊天网关模块是聊天系统的协议入口,负责处理 WebSocket 和 HTTP 请求,提供统一的 API 接口。作为 Gateway Layer 的核心组件,它专注于协议转换和路由管理,将客户端请求转发到 Business Layer 处理,不包含业务逻辑。
## 架构层级
**Gateway Layer网关层**
## 职责定位
网关层负责:
1. **协议处理**:处理 WebSocket 和 HTTP 请求
2. **数据验证**:使用 DTO 进行请求参数验证
3. **路由管理**:定义 API 端点和消息路由
4. **错误转换**:将业务错误转换为协议响应
## 模块组成
```
src/gateway/chat/
├── chat.gateway.ts # WebSocket 网关
├── chat.controller.ts # HTTP 控制器
├── chat.gateway.module.ts # 网关模块配置
├── chat.dto.ts # 请求 DTO
├── chat_response.dto.ts # 响应 DTO
└── README.md # 模块文档
```
## 依赖关系
```
Gateway Layer (chat.gateway.module)
↓ 依赖
Business Layer (chat.module)
↓ 依赖
Core Layer (zulip_core.module, redis.module)
```
## 对外提供的接口
### ChatWebSocketGateway 类
#### sendToPlayer(socketId: string, data: any): void
向指定玩家的 WebSocket 连接发送消息,用于单播通信。
#### broadcastToMap(mapId: string, data: any, excludeId?: string): void
向指定地图内的所有玩家广播消息,支持排除特定玩家。
#### getConnectionCount(): number
获取当前 WebSocket 总连接数,用于监控和统计。
#### getAuthenticatedConnectionCount(): number
获取已认证的 WebSocket 连接数,用于在线玩家统计。
#### getMapPlayerCounts(): Record<string, number>
获取各地图的在线玩家数量统计,用于负载监控。
#### getMapPlayers(mapId: string): string[]
获取指定地图内的所有玩家用户名列表,用于房间成员查询。
### ChatController 类
#### getChatHistory(query: GetChatHistoryDto): Promise<ChatHistoryResponseDto>
获取聊天历史记录,支持按地图筛选和分页查询。
#### getSystemStatus(): Promise<SystemStatusResponseDto>
获取聊天系统状态,包括 WebSocket 连接数、Zulip 状态、内存使用等。
#### getWebSocketInfo(): Promise<object>
获取 WebSocket 连接配置信息,包括连接地址、支持的事件类型等。
#### sendMessage(dto: SendChatMessageDto): Promise<ChatMessageResponseDto>
通过 REST API 发送聊天消息(不推荐),该接口会返回错误提示使用 WebSocket 连接。
## WebSocket 事件接口
### 连接地址
```
wss://whaletownend.xinghangee.icu/game
```
### 'connection'
客户端建立 WebSocket 连接,服务器自动分配连接 ID。
- 输入:无(自动触发)
- 输出:`{ type: 'connected', message: '连接成功', socketId: string }`
### 'login'
用户登录认证,验证 JWT token 并建立会话。
- 输入:`{ type: 'login', token: string }`
- 输出成功:`{ t: 'login_success', sessionId: string, userId: string, username: string, currentMap: string }`
- 输出失败:`{ t: 'login_error', message: string }`
### 'logout'
用户主动登出,清理会话和房间信息。
- 输入:`{ type: 'logout' }`
- 输出:`{ t: 'logout_success', message: '登出成功' }`
### 'chat'
发送聊天消息,支持本地和全局范围。
- 输入:`{ type: 'chat', content: string, scope?: 'local' | 'global' }`
- 输出成功:`{ t: 'chat_sent', messageId: string, message: '消息发送成功' }`
- 输出失败:`{ t: 'chat_error', message: string }`
### 'position'
更新玩家位置,自动处理地图切换和位置广播。
- 输入:`{ type: 'position', x: number, y: number, mapId: string }`
- 输出:广播给地图内其他玩家 `{ t: 'position_update', userId: string, username: string, x: number, y: number, mapId: string }`
### 'chat_render'
接收其他玩家的聊天消息(服务器推送)。
- 输入:无(服务器推送)
- 输出:`{ t: 'chat_render', from: string, txt: string, scope: string, mapId: string }`
### 'disconnect'
客户端断开连接,自动清理资源和通知其他玩家。
- 输入:无(自动触发)
- 输出:无
### 'error'
通用错误消息(服务器推送)。
- 输入:无(服务器推送)
- 输出:`{ type: 'error', message: string }`
## 对外 API 接口
### POST /chat/send
通过 REST API 发送聊天消息(不推荐使用)。
- 认证:需要 JWT Bearer Token
- 请求体:`{ content: string, scope: string, mapId?: string }`
- 响应:返回 400 错误,提示使用 WebSocket 接口
- 说明:该接口仅用于提示,实际聊天消息需通过 WebSocket 发送
### GET /chat/history
获取聊天历史记录,支持按地图筛选和分页查询。
- 认证:需要 JWT Bearer Token
- 查询参数:`mapId?: string, limit?: number, offset?: number`
- 响应:聊天消息列表和总数统计
### GET /chat/status
获取聊天系统状态,包括 WebSocket 连接数、Zulip 集成状态、内存使用等。
- 认证:无需认证
- 响应:系统状态详细信息
### GET /chat/websocket/info
获取 WebSocket 连接配置信息,包括连接地址、支持的事件类型、认证方式等。
- 认证:无需认证
- 响应WebSocket 连接配置
## 使用的项目内部依赖
### ChatService (来自 business/chat/chat.service)
聊天业务服务,处理聊天消息发送、历史查询、玩家登录登出等业务逻辑。
### JwtAuthGuard (来自 gateway/auth/jwt_auth.guard)
JWT 认证守卫,用于保护需要认证的 HTTP API 接口。
### SendChatMessageDto (本模块)
发送聊天消息的请求 DTO提供消息内容和范围的验证规则。
### GetChatHistoryDto (本模块)
获取聊天历史的请求 DTO提供地图筛选和分页参数的验证规则。
### ChatMessageResponseDto (本模块)
聊天消息响应 DTO定义消息发送结果的数据结构。
### ChatHistoryResponseDto (本模块)
聊天历史响应 DTO定义历史消息列表的数据结构。
### SystemStatusResponseDto (本模块)
系统状态响应 DTO定义系统状态信息的数据结构。
### LoginCoreModule (来自 core/login_core/login_core.module)
登录核心模块,提供 JWT 验证和认证功能。
### ChatModule (来自 business/chat/chat.module)
聊天业务模块,提供聊天相关的业务逻辑处理。
## 核心特性
### WebSocket 连接管理
- 原生 WebSocket 支持:基于 ws 库的原生 WebSocket 实现
- 连接生命周期管理:自动处理连接建立、认证、断开和清理
- 连接状态追踪:维护连接 ID、认证状态、用户信息等
- 心跳检测机制:通过 isAlive 标记检测连接活性
### 地图房间系统
- 动态房间管理:根据玩家所在地图自动创建和销毁房间
- 房间成员追踪:维护每个地图的玩家列表
- 自动房间切换:玩家切换地图时自动加入新房间并离开旧房间
- 房间广播优化:仅向房间内的已认证玩家广播消息
### 实时消息广播
- 单播通信:向指定玩家发送消息
- 地图广播:向地图内所有玩家广播消息,支持排除发送者
- 位置同步:实时广播玩家位置更新给房间成员
- 聊天消息推送:接收业务层的聊天消息并推送给客户端
### 协议转换与路由
- 消息类型路由:根据消息类型自动路由到对应处理方法
- 协议格式统一:统一 WebSocket 和 HTTP 的响应格式
- 错误转换:将业务层错误转换为客户端友好的错误消息
- DTO 数据验证:使用 class-validator 进行请求参数验证
### 监控与统计
- 连接数统计:实时统计总连接数和已认证连接数
- 地图人数统计:统计各地图的在线玩家数量
- 系统状态监控:提供内存使用、运行时间等系统指标
- 日志记录:记录连接、消息、错误等关键事件
## 潜在风险
### WebSocket 连接管理风险
- 大量并发连接可能导致内存占用过高
- 连接泄漏风险:异常断开时可能未正确清理资源
- 僵尸连接问题:网络异常时连接可能长时间挂起
- 缓解措施:实现连接数限制、定期清理超时连接、完善错误处理
### 实时通信性能风险
- 高频位置更新可能导致服务器 CPU 压力
- 大房间广播延迟:房间人数过多时广播性能下降
- 消息队列堆积:处理速度慢于接收速度时消息堆积
- 缓解措施:位置更新限流、分片广播、消息优先级队列
### 认证与安全风险
- JWT token 泄露风险WebSocket 连接中 token 可能被截获
- 未认证消息攻击:恶意客户端可能发送大量未认证消息
- 消息内容安全:缺少消息内容的安全过滤
- 缓解措施:使用 WSS 加密传输、限制未认证连接的消息频率、在业务层进行内容过滤
### 资源清理风险
- 断开连接时资源清理不完整可能导致内存泄漏
- 地图房间未及时清理导致空房间占用内存
- 客户端映射未清理导致无效引用
- 缓解措施:完善 cleanupClient 方法、定期清理空房间、使用 WeakMap 避免内存泄漏
### 错误处理风险
- 业务层异常未正确捕获可能导致连接中断
- 消息解析失败可能导致连接关闭
- 错误信息泄露敏感信息
- 缓解措施:完善 try-catch 覆盖、统一错误处理、脱敏错误消息
### 扩展性风险
- 单实例 WebSocket 服务器无法水平扩展
- 内存存储的房间信息无法跨实例共享
- 负载均衡时 WebSocket 连接可能断开
- 缓解措施:引入 Redis 共享房间信息、使用 Sticky Session、实现 WebSocket 集群
## 核心原则
### 1. 只做协议转换,不做业务逻辑
```typescript
// ✅ 正确:只做协议处理
private async handleChat(ws: ExtendedWebSocket, message: any) {
if (!ws.authenticated) {
this.sendError(ws, '请先登录');
return;
}
const result = await this.chatService.sendChatMessage({
socketId: ws.id,
content: message.content,
scope: message.scope || 'local'
});
if (result.success) {
this.sendMessage(ws, { t: 'chat_sent', messageId: result.messageId });
} else {
this.sendMessage(ws, { t: 'chat_error', message: result.error });
}
}
// ❌ 错误:在网关中包含业务逻辑
private async handleChat(ws: ExtendedWebSocket, message: any) {
// 不应该在这里做敏感词过滤、频率限制等业务逻辑
if (message.content.includes('敏感词')) {
this.sendError(ws, '包含敏感词');
return;
}
}
```
### 2. 统一的错误处理
```typescript
private sendError(ws: ExtendedWebSocket, message: string) {
this.sendMessage(ws, { type: 'error', message });
}
```
## 使用示例
### WebSocket 连接示例
```javascript
const ws = new WebSocket('wss://whaletownend.xinghangee.icu/game');
ws.onopen = () => {
// 登录
ws.send(JSON.stringify({
type: 'login',
token: 'your-jwt-token'
}));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
switch (data.t || data.type) {
case 'login_success':
console.log('登录成功', data);
break;
case 'chat_render':
console.log('收到消息', data.from, data.txt);
break;
}
};
// 发送聊天消息
ws.send(JSON.stringify({
type: 'chat',
content: '大家好!',
scope: 'local'
}));
```
## 注意事项
- 网关层不应该直接访问数据库
- 网关层不应该包含复杂的业务逻辑
- 所有业务逻辑都应该在 Business 层实现
- WebSocket 连接需要先登录才能发送聊天消息