refactor:项目架构重构和命名规范化
- 统一文件命名为snake_case格式(kebab-case snake_case) - 重构zulip模块为zulip_core,明确Core层职责 - 重构user-mgmt模块为user_mgmt,统一命名规范 - 调整模块依赖关系,优化架构分层 - 删除过时的文件和目录结构 - 更新相关文档和配置文件 本次重构涉及大量文件重命名和模块重组, 旨在建立更清晰的项目架构和统一的命名规范。
This commit is contained in:
@@ -1,172 +1,276 @@
|
||||
# Zulip集成业务模块
|
||||
# Zulip 游戏集成业务模块
|
||||
|
||||
## 架构重构说明
|
||||
Zulip 是游戏与Zulip社群平台的集成业务模块,提供完整的实时聊天、会话管理、消息过滤和WebSocket通信功能,实现游戏内聊天与Zulip社群的双向同步,支持基于位置的聊天上下文管理和业务规则驱动的消息过滤控制。
|
||||
|
||||
本模块已按照项目的分层架构要求进行重构,将技术实现细节移动到核心服务层,业务逻辑保留在业务层。
|
||||
## 玩家登录和会话管理
|
||||
|
||||
### 重构前后对比
|
||||
### handlePlayerLogin()
|
||||
验证游戏Token,创建Zulip客户端,建立会话映射关系,支持JWT认证和API Key获取。
|
||||
|
||||
#### 重构前(❌ 违反架构原则)
|
||||
```
|
||||
src/business/zulip/services/
|
||||
├── zulip_client.service.ts # 技术实现:API调用
|
||||
├── zulip_client_pool.service.ts # 技术实现:连接池管理
|
||||
├── config_manager.service.ts # 技术实现:配置管理
|
||||
├── zulip_event_processor.service.ts # 技术实现:事件处理
|
||||
├── session_manager.service.ts # ✅ 业务逻辑:会话管理
|
||||
└── message_filter.service.ts # ✅ 业务逻辑:消息过滤
|
||||
```
|
||||
### handlePlayerLogout()
|
||||
清理玩家会话,注销Zulip事件队列,释放相关资源,确保连接正常断开。
|
||||
|
||||
#### 重构后(✅ 符合架构原则)
|
||||
```
|
||||
# 业务逻辑层
|
||||
src/business/zulip/
|
||||
├── zulip.service.ts # 业务协调服务
|
||||
├── zulip_websocket.gateway.ts # WebSocket业务网关
|
||||
└── services/
|
||||
├── session_manager.service.ts # 会话业务逻辑
|
||||
└── message_filter.service.ts # 消息过滤业务规则
|
||||
### getSession()
|
||||
根据socketId获取会话信息,并更新最后活动时间,支持会话状态查询。
|
||||
|
||||
# 核心服务层
|
||||
src/core/zulip/
|
||||
├── interfaces/
|
||||
│ └── zulip-core.interfaces.ts # 核心服务接口定义
|
||||
├── services/
|
||||
│ ├── zulip_client.service.ts # Zulip API封装
|
||||
│ ├── zulip_client_pool.service.ts # 客户端池管理
|
||||
│ ├── config_manager.service.ts # 配置管理
|
||||
│ ├── zulip_event_processor.service.ts # 事件处理
|
||||
│ └── ... # 其他技术服务
|
||||
└── zulip-core.module.ts # 核心服务模块
|
||||
```
|
||||
### getSocketsInMap()
|
||||
获取指定地图中所有在线玩家的Socket ID列表,用于消息分发和空间过滤。
|
||||
|
||||
### 架构优势
|
||||
## 消息发送和处理
|
||||
|
||||
#### 1. 单一职责原则
|
||||
- **业务层**:只关注游戏相关的业务逻辑和规则
|
||||
- **核心层**:只处理技术实现和第三方API调用
|
||||
### sendChatMessage()
|
||||
处理游戏客户端发送的聊天消息,转发到对应的Zulip Stream/Topic,包含内容过滤和权限验证。
|
||||
|
||||
#### 2. 依赖注入和接口抽象
|
||||
### processZulipMessage()
|
||||
处理Zulip事件队列推送的消息,转换格式后发送给相关的游戏客户端,实现双向通信。
|
||||
|
||||
### updatePlayerPosition()
|
||||
更新玩家在游戏世界中的位置信息,用于消息路由和上下文注入,支持地图切换。
|
||||
|
||||
## WebSocket网关功能
|
||||
|
||||
### handleConnection()
|
||||
处理游戏客户端WebSocket连接建立,记录连接信息并初始化连接状态。
|
||||
|
||||
### handleDisconnect()
|
||||
处理游戏客户端连接断开,清理相关资源并执行登出逻辑。
|
||||
|
||||
### handleLogin()
|
||||
处理登录消息,验证Token并建立会话,返回登录结果和用户信息。
|
||||
|
||||
### handleChat()
|
||||
处理聊天消息,验证用户认证状态和消息格式,调用业务服务发送消息。
|
||||
|
||||
### sendChatRender()
|
||||
向指定客户端发送聊天渲染消息,用于显示气泡或聊天框。
|
||||
|
||||
### broadcastToMap()
|
||||
向指定地图的所有客户端广播消息,支持区域性消息分发。
|
||||
|
||||
## 会话管理功能
|
||||
|
||||
### createSession()
|
||||
创建会话并绑定Socket_ID与Zulip_Queue_ID,建立WebSocket连接与Zulip队列的映射关系。
|
||||
|
||||
### injectContext()
|
||||
上下文注入,根据玩家位置确定消息应该发送到的Zulip Stream和Topic。
|
||||
|
||||
### destroySession()
|
||||
清理玩家会话数据,从地图玩家列表中移除,释放相关资源。
|
||||
|
||||
### cleanupExpiredSessions()
|
||||
定时清理超时的会话数据和相关资源,返回需要注销的Zulip队列ID列表。
|
||||
|
||||
## 消息过滤和安全
|
||||
|
||||
### validateMessage()
|
||||
对消息进行综合验证,包括内容过滤、频率限制和权限验证。
|
||||
|
||||
### filterContent()
|
||||
检查消息内容是否包含敏感词,进行内容过滤和替换。
|
||||
|
||||
### checkRateLimit()
|
||||
检查用户是否超过消息发送频率限制,防止刷屏。
|
||||
|
||||
### validatePermission()
|
||||
验证用户是否有权限向目标Stream发送消息,防止位置欺诈。
|
||||
|
||||
### logViolation()
|
||||
记录用户的违规行为,用于监控和分析。
|
||||
|
||||
## REST API接口
|
||||
|
||||
### sendMessage()
|
||||
通过REST API发送聊天消息到Zulip(推荐使用WebSocket接口)。
|
||||
|
||||
### getChatHistory()
|
||||
获取指定地图或全局的聊天历史记录,支持分页查询。
|
||||
|
||||
### getSystemStatus()
|
||||
获取WebSocket连接状态、Zulip集成状态等系统信息。
|
||||
|
||||
### getWebSocketInfo()
|
||||
获取WebSocket连接的详细信息,包括连接地址、协议等。
|
||||
|
||||
## 使用的项目内部依赖
|
||||
|
||||
### ZulipCoreModule (来自 core/zulip_core)
|
||||
提供Zulip核心技术服务,包括客户端池管理、配置管理和事件处理等底层技术实现。
|
||||
|
||||
### LoginCoreModule (来自 core/login_core)
|
||||
提供用户认证和Token验证服务,支持JWT令牌验证和用户信息获取。
|
||||
|
||||
### RedisModule (来自 core/redis)
|
||||
提供会话状态缓存和数据存储服务,支持会话持久化和快速查询。
|
||||
|
||||
### LoggerModule (来自 core/utils/logger)
|
||||
提供统一的日志记录服务,支持结构化日志和性能监控。
|
||||
|
||||
### ZulipAccountsModule (来自 core/db/zulip_accounts)
|
||||
提供Zulip账号关联管理功能,支持用户与Zulip账号的绑定关系。
|
||||
|
||||
### AuthModule (来自 business/auth)
|
||||
提供JWT验证和用户认证服务,支持用户身份验证和权限控制。
|
||||
|
||||
### IZulipClientPoolService (来自 core/zulip_core/interfaces)
|
||||
Zulip客户端池服务接口,用于管理用户专用的Zulip客户端实例。
|
||||
|
||||
### IZulipConfigService (来自 core/zulip_core/interfaces)
|
||||
Zulip配置服务接口,用于获取地图到Stream的映射关系和配置信息。
|
||||
|
||||
### ApiKeySecurityService (来自 core/zulip_core/services)
|
||||
API密钥安全服务,用于获取和管理用户的Zulip API Key。
|
||||
|
||||
### IRedisService (来自 core/redis)
|
||||
Redis服务接口,用于会话数据存储、频率限制和违规记录管理。
|
||||
|
||||
### SendChatMessageDto (本模块)
|
||||
发送聊天消息的数据传输对象,定义消息内容、范围和地图ID等字段。
|
||||
|
||||
### ChatMessageResponseDto (本模块)
|
||||
聊天消息响应的数据传输对象,包含成功状态、消息ID和错误信息。
|
||||
|
||||
### SystemStatusResponseDto (本模块)
|
||||
系统状态响应的数据传输对象,包含WebSocket状态、Zulip集成状态和系统信息。
|
||||
|
||||
## 核心特性
|
||||
|
||||
### 双向通信支持
|
||||
- WebSocket实时通信:支持游戏客户端与服务器的实时双向通信
|
||||
- Zulip集成同步:实现游戏内聊天与Zulip社群的双向消息同步
|
||||
- 事件驱动架构:基于事件队列处理Zulip消息推送和游戏事件
|
||||
|
||||
### 会话状态管理
|
||||
- Redis持久化存储:会话数据存储在Redis中,支持服务重启后状态恢复
|
||||
- 自动过期清理:定时清理超时会话,释放系统资源
|
||||
- 多地图支持:支持玩家在不同地图间切换,自动更新地图玩家列表
|
||||
|
||||
### 消息过滤和安全
|
||||
- 敏感词过滤:支持block和replace两种级别的敏感词处理
|
||||
- 频率限制控制:防止用户发送消息过于频繁导致刷屏
|
||||
- 位置权限验证:防止用户向不匹配位置的Stream发送消息
|
||||
- 违规行为记录:记录和统计用户违规行为,支持监控和分析
|
||||
|
||||
### 业务规则引擎
|
||||
- 上下文注入机制:根据玩家位置自动确定消息的目标Stream和Topic
|
||||
- 动态配置管理:支持地图到Stream映射关系的动态配置和热重载
|
||||
- 权限分级控制:支持不同用户角色的权限控制和消息发送限制
|
||||
|
||||
## 潜在风险
|
||||
|
||||
### 会话数据丢失
|
||||
- Redis服务故障可能导致会话数据丢失,影响用户体验
|
||||
- 建议配置Redis主从复制和持久化策略
|
||||
- 实现会话数据的定期备份和恢复机制
|
||||
|
||||
### 消息同步延迟
|
||||
- Zulip服务器网络延迟可能影响消息同步实时性
|
||||
- 大量并发消息可能导致事件队列处理延迟
|
||||
- 建议监控消息处理延迟并设置合理的超时机制
|
||||
|
||||
### 频率限制绕过
|
||||
- 恶意用户可能通过多个账号绕过频率限制
|
||||
- IP级别的频率限制可能影响正常用户
|
||||
- 建议结合用户行为分析和动态调整限制策略
|
||||
|
||||
### 敏感词过滤失效
|
||||
- 新型敏感词和变体可能绕过现有过滤规则
|
||||
- 过度严格的过滤可能影响正常交流
|
||||
- 建议定期更新敏感词库并优化过滤算法
|
||||
|
||||
### WebSocket连接稳定性
|
||||
- 网络不稳定可能导致WebSocket连接频繁断开重连
|
||||
- 大量连接可能消耗过多服务器资源
|
||||
- 建议实现连接池管理和自动重连机制
|
||||
|
||||
### 位置验证绕过
|
||||
- 客户端修改可能绕过位置验证机制
|
||||
- 服务端位置验证逻辑需要持续完善
|
||||
- 建议结合多种验证手段和异常行为检测
|
||||
|
||||
## 使用示例
|
||||
|
||||
### WebSocket 客户端连接
|
||||
```typescript
|
||||
// 业务层通过接口依赖核心服务
|
||||
constructor(
|
||||
@Inject('ZULIP_CLIENT_POOL_SERVICE')
|
||||
private readonly zulipClientPool: IZulipClientPoolService,
|
||||
@Inject('ZULIP_CONFIG_SERVICE')
|
||||
private readonly configManager: IZulipConfigService,
|
||||
) {}
|
||||
// 建立WebSocket连接
|
||||
const socket = io('ws://localhost:3000/zulip');
|
||||
|
||||
// 监听连接事件
|
||||
socket.on('connect', () => {
|
||||
console.log('Connected to Zulip WebSocket');
|
||||
});
|
||||
|
||||
// 发送登录消息
|
||||
socket.emit('login', {
|
||||
token: 'your-jwt-token'
|
||||
});
|
||||
|
||||
// 发送聊天消息
|
||||
socket.emit('chat', {
|
||||
content: '大家好!',
|
||||
scope: 'local',
|
||||
mapId: 'whale_port'
|
||||
});
|
||||
|
||||
// 监听聊天消息
|
||||
socket.on('chat_render', (data) => {
|
||||
console.log('收到消息:', data);
|
||||
});
|
||||
```
|
||||
|
||||
#### 3. 易于测试和维护
|
||||
- 业务逻辑可以独立测试,不依赖具体的技术实现
|
||||
- 核心服务可以独立替换,不影响业务逻辑
|
||||
- 接口定义清晰,便于理解和维护
|
||||
### REST API 调用
|
||||
```typescript
|
||||
// 发送聊天消息
|
||||
const response = await fetch('/api/zulip/send-message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer your-jwt-token'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
content: '测试消息',
|
||||
scope: 'global',
|
||||
mapId: 'whale_port'
|
||||
})
|
||||
});
|
||||
|
||||
### 服务职责划分
|
||||
// 获取聊天历史
|
||||
const history = await fetch('/api/zulip/chat-history?mapId=whale_port&limit=50');
|
||||
const messages = await history.json();
|
||||
|
||||
#### 业务逻辑层服务
|
||||
// 获取系统状态
|
||||
const status = await fetch('/api/zulip/system-status');
|
||||
const systemInfo = await status.json();
|
||||
```
|
||||
|
||||
| 服务 | 职责 | 业务价值 |
|
||||
|------|------|----------|
|
||||
| `ZulipService` | 游戏登录/登出业务流程协调 | 处理玩家生命周期管理 |
|
||||
| `SessionManagerService` | 游戏会话状态和上下文管理 | 维护玩家位置和聊天上下文 |
|
||||
| `MessageFilterService` | 消息过滤和业务规则控制 | 实现内容审核和权限验证 |
|
||||
| `ZulipWebSocketGateway` | WebSocket业务协议处理 | 游戏协议转换和路由 |
|
||||
|
||||
#### 核心服务层服务
|
||||
|
||||
| 服务 | 职责 | 技术价值 |
|
||||
|------|------|----------|
|
||||
| `ZulipClientService` | Zulip REST API封装 | 第三方API调用抽象 |
|
||||
| `ZulipClientPoolService` | 客户端连接池管理 | 资源管理和性能优化 |
|
||||
| `ConfigManagerService` | 配置文件管理和热重载 | 系统配置和运维支持 |
|
||||
| `ZulipEventProcessorService` | 事件队列处理和消息转换 | 异步消息处理机制 |
|
||||
|
||||
### 使用示例
|
||||
|
||||
#### 业务层调用核心服务
|
||||
### 服务集成示例
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class ZulipService {
|
||||
export class GameChatService {
|
||||
constructor(
|
||||
@Inject('ZULIP_CLIENT_POOL_SERVICE')
|
||||
private readonly zulipClientPool: IZulipClientPoolService,
|
||||
private readonly zulipService: ZulipService,
|
||||
private readonly sessionManager: SessionManagerService
|
||||
) {}
|
||||
|
||||
async sendChatMessage(request: ChatMessageRequest): Promise<ChatMessageResponse> {
|
||||
// 业务逻辑:验证和处理
|
||||
const session = await this.sessionManager.getSession(request.socketId);
|
||||
const context = await this.sessionManager.injectContext(request.socketId);
|
||||
async handlePlayerMessage(playerId: string, message: string) {
|
||||
// 获取玩家会话
|
||||
const session = await this.sessionManager.getSession(playerId);
|
||||
|
||||
// 调用核心服务:技术实现
|
||||
const result = await this.zulipClientPool.sendMessage(
|
||||
session.userId,
|
||||
context.stream,
|
||||
context.topic,
|
||||
request.content,
|
||||
);
|
||||
// 发送消息到Zulip
|
||||
const result = await this.zulipService.sendChatMessage({
|
||||
gameUserId: playerId,
|
||||
content: message,
|
||||
scope: 'local',
|
||||
mapId: session.mapId
|
||||
});
|
||||
|
||||
return { success: result.success, messageId: result.messageId };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移指南
|
||||
|
||||
如果你的代码中直接导入了已移动的服务,请按以下方式更新:
|
||||
|
||||
#### 更新导入路径
|
||||
```typescript
|
||||
// ❌ 旧的导入方式
|
||||
import { ZulipClientPoolService } from './services/zulip_client_pool.service';
|
||||
|
||||
// ✅ 新的导入方式(通过依赖注入)
|
||||
import { IZulipClientPoolService } from '../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
|
||||
constructor(
|
||||
@Inject('ZULIP_CLIENT_POOL_SERVICE')
|
||||
private readonly zulipClientPool: IZulipClientPoolService,
|
||||
) {}
|
||||
```
|
||||
|
||||
#### 更新模块导入
|
||||
```typescript
|
||||
// ✅ 业务模块自动导入核心模块
|
||||
@Module({
|
||||
imports: [
|
||||
ZulipCoreModule, // 自动提供所有核心服务
|
||||
// ...
|
||||
],
|
||||
})
|
||||
export class ZulipModule {}
|
||||
```
|
||||
|
||||
### 测试策略
|
||||
|
||||
#### 业务逻辑测试
|
||||
```typescript
|
||||
// 使用Mock核心服务测试业务逻辑
|
||||
const mockZulipClientPool: IZulipClientPoolService = {
|
||||
sendMessage: jest.fn().mockResolvedValue({ success: true }),
|
||||
// ...
|
||||
};
|
||||
|
||||
const module = await Test.createTestingModule({
|
||||
providers: [
|
||||
ZulipService,
|
||||
{ provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockZulipClientPool },
|
||||
],
|
||||
}).compile();
|
||||
```
|
||||
|
||||
#### 核心服务测试
|
||||
```typescript
|
||||
// 独立测试技术实现
|
||||
describe('ZulipClientService', () => {
|
||||
it('should call Zulip API correctly', async () => {
|
||||
// 测试API调用逻辑
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
这种架构设计确保了业务逻辑与技术实现的清晰分离,提高了代码的可维护性和可测试性。
|
||||
## 版本信息
|
||||
- **版本**: 1.2.1
|
||||
- **作者**: angjustinl
|
||||
- **创建时间**: 2025-12-20
|
||||
- **最后修改**: 2026-01-07
|
||||
@@ -7,9 +7,19 @@
|
||||
* - 查看系统状态和统计信息
|
||||
* - 管理 WebSocket 连接状态
|
||||
*
|
||||
* 职责分离:
|
||||
* - REST接口:提供HTTP方式的聊天功能访问
|
||||
* - 状态查询:提供系统运行状态和统计信息
|
||||
* - 文档支持:提供WebSocket API的使用文档
|
||||
* - 监控支持:提供连接数和性能监控接口
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
@@ -30,7 +40,7 @@ import {
|
||||
ApiBearerAuth,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../auth/guards/jwt-auth.guard';
|
||||
import { JwtAuthGuard } from '../../auth/jwt_auth.guard';
|
||||
import { ZulipService } from '../zulip.service';
|
||||
import { ZulipWebSocketGateway } from '../zulip_websocket.gateway';
|
||||
import {
|
||||
@@ -39,7 +49,7 @@ import {
|
||||
GetChatHistoryDto,
|
||||
ChatHistoryResponseDto,
|
||||
SystemStatusResponseDto,
|
||||
} from '../dto/chat.dto';
|
||||
} from '../chat.dto';
|
||||
|
||||
@ApiTags('chat')
|
||||
@Controller('chat')
|
||||
|
||||
@@ -6,9 +6,19 @@
|
||||
* - 展示消息格式和事件类型
|
||||
* - 提供连接示例和测试工具
|
||||
*
|
||||
* 职责分离:
|
||||
* - API文档:提供完整的WebSocket API使用说明
|
||||
* - 示例代码:提供各种编程语言的连接示例
|
||||
* - 调试支持:提供消息格式验证和测试工具
|
||||
* - 开发指导:提供最佳实践和故障排除指南
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2025-01-07
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
581
src/business/zulip/controllers/zulip_accounts.controller.ts
Normal file
581
src/business/zulip/controllers/zulip_accounts.controller.ts
Normal file
@@ -0,0 +1,581 @@
|
||||
/**
|
||||
* Zulip账号关联管理控制器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 提供Zulip账号关联管理的REST API接口
|
||||
* - 支持CRUD操作和批量管理
|
||||
* - 提供账号验证和统计功能
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @since 2025-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Put,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
HttpStatus,
|
||||
HttpCode,
|
||||
Inject,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse,
|
||||
ApiBearerAuth,
|
||||
ApiParam,
|
||||
ApiQuery,
|
||||
} from '@nestjs/swagger';
|
||||
import { JwtAuthGuard } from '../../auth/jwt_auth.guard';
|
||||
import { ZulipAccountsService } from '../../../core/db/zulip_accounts/zulip_accounts.service';
|
||||
import { ZulipAccountsMemoryService } from '../../../core/db/zulip_accounts/zulip_accounts_memory.service';
|
||||
import {
|
||||
CreateZulipAccountDto,
|
||||
UpdateZulipAccountDto,
|
||||
QueryZulipAccountDto,
|
||||
ZulipAccountResponseDto,
|
||||
ZulipAccountListResponseDto,
|
||||
ZulipAccountStatsResponseDto,
|
||||
BatchUpdateStatusDto,
|
||||
BatchUpdateResponseDto,
|
||||
VerifyAccountDto,
|
||||
VerifyAccountResponseDto,
|
||||
} from '../../../core/db/zulip_accounts/zulip_accounts.dto';
|
||||
|
||||
@ApiTags('zulip-accounts')
|
||||
@Controller('zulip-accounts')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiBearerAuth('JWT-auth')
|
||||
export class ZulipAccountsController {
|
||||
constructor(
|
||||
@Inject('ZulipAccountsService')
|
||||
private readonly zulipAccountsService: ZulipAccountsService | ZulipAccountsMemoryService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 创建Zulip账号关联
|
||||
*/
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
summary: '创建Zulip账号关联',
|
||||
description: '为游戏用户创建与Zulip账号的关联关系'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 201,
|
||||
description: '创建成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 400,
|
||||
description: '请求参数错误',
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 409,
|
||||
description: '关联已存在',
|
||||
})
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
async create(@Body() createDto: CreateZulipAccountDto): Promise<ZulipAccountResponseDto> {
|
||||
return this.zulipAccountsService.create(createDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有Zulip账号关联
|
||||
*/
|
||||
@Get()
|
||||
@ApiOperation({
|
||||
summary: '查询Zulip账号关联列表',
|
||||
description: '根据条件查询Zulip账号关联列表'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'gameUserId',
|
||||
required: false,
|
||||
description: '游戏用户ID',
|
||||
example: '12345'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'zulipUserId',
|
||||
required: false,
|
||||
description: 'Zulip用户ID',
|
||||
example: 67890
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'zulipEmail',
|
||||
required: false,
|
||||
description: 'Zulip邮箱地址',
|
||||
example: 'user@example.com'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'status',
|
||||
required: false,
|
||||
description: '账号状态',
|
||||
enum: ['active', 'inactive', 'suspended', 'error']
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '查询成功',
|
||||
type: ZulipAccountListResponseDto,
|
||||
})
|
||||
async findMany(@Query() queryDto: QueryZulipAccountDto): Promise<ZulipAccountListResponseDto> {
|
||||
return this.zulipAccountsService.findMany(queryDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取Zulip账号关联
|
||||
*/
|
||||
@Get(':id')
|
||||
@ApiOperation({
|
||||
summary: '根据ID获取Zulip账号关联',
|
||||
description: '根据关联记录ID获取详细信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: '关联记录ID',
|
||||
example: '1'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '记录不存在',
|
||||
})
|
||||
async findById(
|
||||
@Param('id') id: string,
|
||||
@Query('includeGameUser') includeGameUser?: boolean,
|
||||
): Promise<ZulipAccountResponseDto> {
|
||||
return this.zulipAccountsService.findById(id, includeGameUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID获取Zulip账号关联
|
||||
*/
|
||||
@Get('game-user/:gameUserId')
|
||||
@ApiOperation({
|
||||
summary: '根据游戏用户ID获取Zulip账号关联',
|
||||
description: '根据游戏用户ID获取关联的Zulip账号信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'gameUserId',
|
||||
description: '游戏用户ID',
|
||||
example: '12345'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async findByGameUserId(
|
||||
@Param('gameUserId') gameUserId: string,
|
||||
@Query('includeGameUser') includeGameUser?: boolean,
|
||||
): Promise<ZulipAccountResponseDto | null> {
|
||||
return this.zulipAccountsService.findByGameUserId(gameUserId, includeGameUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip用户ID获取账号关联
|
||||
*/
|
||||
@Get('zulip-user/:zulipUserId')
|
||||
@ApiOperation({
|
||||
summary: '根据Zulip用户ID获取账号关联',
|
||||
description: '根据Zulip用户ID获取关联的游戏账号信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'zulipUserId',
|
||||
description: 'Zulip用户ID',
|
||||
example: '67890'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async findByZulipUserId(
|
||||
@Param('zulipUserId') zulipUserId: string,
|
||||
@Query('includeGameUser') includeGameUser?: boolean,
|
||||
): Promise<ZulipAccountResponseDto | null> {
|
||||
return this.zulipAccountsService.findByZulipUserId(parseInt(zulipUserId), includeGameUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据Zulip邮箱获取账号关联
|
||||
*/
|
||||
@Get('zulip-email/:zulipEmail')
|
||||
@ApiOperation({
|
||||
summary: '根据Zulip邮箱获取账号关联',
|
||||
description: '根据Zulip邮箱地址获取关联的游戏账号信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'zulipEmail',
|
||||
description: 'Zulip邮箱地址',
|
||||
example: 'user@example.com'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'includeGameUser',
|
||||
required: false,
|
||||
description: '是否包含游戏用户信息',
|
||||
type: Boolean,
|
||||
example: false
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async findByZulipEmail(
|
||||
@Param('zulipEmail') zulipEmail: string,
|
||||
@Query('includeGameUser') includeGameUser?: boolean,
|
||||
): Promise<ZulipAccountResponseDto | null> {
|
||||
return this.zulipAccountsService.findByZulipEmail(zulipEmail, includeGameUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新Zulip账号关联
|
||||
*/
|
||||
@Put(':id')
|
||||
@ApiOperation({
|
||||
summary: '更新Zulip账号关联',
|
||||
description: '根据ID更新Zulip账号关联信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: '关联记录ID',
|
||||
example: '1'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '更新成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '记录不存在',
|
||||
})
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() updateDto: UpdateZulipAccountDto,
|
||||
): Promise<ZulipAccountResponseDto> {
|
||||
return this.zulipAccountsService.update(id, updateDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID更新关联
|
||||
*/
|
||||
@Put('game-user/:gameUserId')
|
||||
@ApiOperation({
|
||||
summary: '根据游戏用户ID更新关联',
|
||||
description: '根据游戏用户ID更新Zulip账号关联信息'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'gameUserId',
|
||||
description: '游戏用户ID',
|
||||
example: '12345'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '更新成功',
|
||||
type: ZulipAccountResponseDto,
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async updateByGameUserId(
|
||||
@Param('gameUserId') gameUserId: string,
|
||||
@Body() updateDto: UpdateZulipAccountDto,
|
||||
): Promise<ZulipAccountResponseDto> {
|
||||
return this.zulipAccountsService.updateByGameUserId(gameUserId, updateDto);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除Zulip账号关联
|
||||
*/
|
||||
@Delete(':id')
|
||||
@ApiOperation({
|
||||
summary: '删除Zulip账号关联',
|
||||
description: '根据ID删除Zulip账号关联记录'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'id',
|
||||
description: '关联记录ID',
|
||||
example: '1'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '删除成功',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true },
|
||||
message: { type: 'string', example: '删除成功' }
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '记录不存在',
|
||||
})
|
||||
async delete(@Param('id') id: string): Promise<{ success: boolean; message: string }> {
|
||||
await this.zulipAccountsService.delete(id);
|
||||
return { success: true, message: '删除成功' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据游戏用户ID删除关联
|
||||
*/
|
||||
@Delete('game-user/:gameUserId')
|
||||
@ApiOperation({
|
||||
summary: '根据游戏用户ID删除关联',
|
||||
description: '根据游戏用户ID删除Zulip账号关联记录'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'gameUserId',
|
||||
description: '游戏用户ID',
|
||||
example: '12345'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '删除成功',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
success: { type: 'boolean', example: true },
|
||||
message: { type: 'string', example: '删除成功' }
|
||||
}
|
||||
}
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 404,
|
||||
description: '关联不存在',
|
||||
})
|
||||
async deleteByGameUserId(@Param('gameUserId') gameUserId: string): Promise<{ success: boolean; message: string }> {
|
||||
await this.zulipAccountsService.deleteByGameUserId(gameUserId);
|
||||
return { success: true, message: '删除成功' };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要验证的账号列表
|
||||
*/
|
||||
@Get('management/verification-needed')
|
||||
@ApiOperation({
|
||||
summary: '获取需要验证的账号列表',
|
||||
description: '获取超过指定时间未验证的账号列表'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'maxAge',
|
||||
required: false,
|
||||
description: '最大验证间隔(毫秒),默认24小时',
|
||||
example: 86400000
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountListResponseDto,
|
||||
})
|
||||
async findAccountsNeedingVerification(
|
||||
@Query('maxAge') maxAge?: number,
|
||||
): Promise<ZulipAccountListResponseDto> {
|
||||
return this.zulipAccountsService.findAccountsNeedingVerification(maxAge);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取错误状态的账号列表
|
||||
*/
|
||||
@Get('management/error-accounts')
|
||||
@ApiOperation({
|
||||
summary: '获取错误状态的账号列表',
|
||||
description: '获取处于错误状态的账号列表'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'maxRetryCount',
|
||||
required: false,
|
||||
description: '最大重试次数,默认3次',
|
||||
example: 3
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountListResponseDto,
|
||||
})
|
||||
async findErrorAccounts(
|
||||
@Query('maxRetryCount') maxRetryCount?: number,
|
||||
): Promise<ZulipAccountListResponseDto> {
|
||||
return this.zulipAccountsService.findErrorAccounts(maxRetryCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新账号状态
|
||||
*/
|
||||
@Put('management/batch-status')
|
||||
@ApiOperation({
|
||||
summary: '批量更新账号状态',
|
||||
description: '批量更新多个账号的状态'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '更新成功',
|
||||
type: BatchUpdateResponseDto,
|
||||
})
|
||||
async batchUpdateStatus(@Body() batchDto: BatchUpdateStatusDto): Promise<BatchUpdateResponseDto> {
|
||||
return this.zulipAccountsService.batchUpdateStatus(batchDto.ids, batchDto.status);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取账号状态统计
|
||||
*/
|
||||
@Get('management/statistics')
|
||||
@ApiOperation({
|
||||
summary: '获取账号状态统计',
|
||||
description: '获取各种状态的账号数量统计'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '获取成功',
|
||||
type: ZulipAccountStatsResponseDto,
|
||||
})
|
||||
async getStatusStatistics(): Promise<ZulipAccountStatsResponseDto> {
|
||||
return this.zulipAccountsService.getStatusStatistics();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证账号有效性
|
||||
*/
|
||||
@Post('management/verify')
|
||||
@ApiOperation({
|
||||
summary: '验证账号有效性',
|
||||
description: '验证指定游戏用户的Zulip账号关联是否有效'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '验证完成',
|
||||
type: VerifyAccountResponseDto,
|
||||
})
|
||||
async verifyAccount(@Body() verifyDto: VerifyAccountDto): Promise<VerifyAccountResponseDto> {
|
||||
return this.zulipAccountsService.verifyAccount(verifyDto.gameUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否已存在
|
||||
*/
|
||||
@Get('validation/email-exists/:email')
|
||||
@ApiOperation({
|
||||
summary: '检查邮箱是否已存在',
|
||||
description: '检查指定的Zulip邮箱是否已被其他账号使用'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'email',
|
||||
description: 'Zulip邮箱地址',
|
||||
example: 'user@example.com'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'excludeId',
|
||||
required: false,
|
||||
description: '排除的记录ID(用于更新时检查)',
|
||||
example: '1'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '检查完成',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
exists: { type: 'boolean', example: false },
|
||||
email: { type: 'string', example: 'user@example.com' }
|
||||
}
|
||||
}
|
||||
})
|
||||
async checkEmailExists(
|
||||
@Param('email') email: string,
|
||||
@Query('excludeId') excludeId?: string,
|
||||
): Promise<{ exists: boolean; email: string }> {
|
||||
const exists = await this.zulipAccountsService.existsByEmail(email, excludeId);
|
||||
return { exists, email };
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查Zulip用户ID是否已存在
|
||||
*/
|
||||
@Get('validation/zulip-user-exists/:zulipUserId')
|
||||
@ApiOperation({
|
||||
summary: '检查Zulip用户ID是否已存在',
|
||||
description: '检查指定的Zulip用户ID是否已被其他账号使用'
|
||||
})
|
||||
@ApiParam({
|
||||
name: 'zulipUserId',
|
||||
description: 'Zulip用户ID',
|
||||
example: '67890'
|
||||
})
|
||||
@ApiQuery({
|
||||
name: 'excludeId',
|
||||
required: false,
|
||||
description: '排除的记录ID(用于更新时检查)',
|
||||
example: '1'
|
||||
})
|
||||
@ApiResponse({
|
||||
status: 200,
|
||||
description: '检查完成',
|
||||
schema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
exists: { type: 'boolean', example: false },
|
||||
zulipUserId: { type: 'number', example: 67890 }
|
||||
}
|
||||
}
|
||||
})
|
||||
async checkZulipUserIdExists(
|
||||
@Param('zulipUserId') zulipUserId: string,
|
||||
@Query('excludeId') excludeId?: string,
|
||||
): Promise<{ exists: boolean; zulipUserId: number }> {
|
||||
const zulipUserIdNum = parseInt(zulipUserId);
|
||||
const exists = await this.zulipAccountsService.existsByZulipUserId(zulipUserIdNum, excludeId);
|
||||
return { exists, zulipUserId: zulipUserIdNum };
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as fc from 'fast-check';
|
||||
import { MessageFilterService, ViolationType } from './message_filter.service';
|
||||
import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { IZulipConfigService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
|
||||
|
||||
@@ -7,6 +7,13 @@
|
||||
* - 防止恶意操作和滥用
|
||||
* - 与ConfigManager集成实现位置权限验证
|
||||
*
|
||||
* 职责分离:
|
||||
* - 内容审核:检查消息内容是否包含敏感词和恶意链接
|
||||
* - 频率控制:防止用户发送消息过于频繁导致刷屏
|
||||
* - 权限验证:验证用户是否有权限向目标Stream发送消息
|
||||
* - 违规记录:记录和统计用户的违规行为
|
||||
* - 规则管理:动态管理敏感词列表和过滤规则
|
||||
*
|
||||
* 主要方法:
|
||||
* - filterContent(): 内容过滤,敏感词检查
|
||||
* - checkRateLimit(): 频率限制检查
|
||||
@@ -23,14 +30,19 @@
|
||||
* - IRedisService: Redis缓存服务
|
||||
* - ConfigManagerService: 配置管理服务
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 清理未使用的导入(forwardRef) (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.1.0
|
||||
* @version 1.1.2
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common';
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { IZulipConfigService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
|
||||
/**
|
||||
* 内容过滤结果接口
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
CleanupResult
|
||||
} from './session_cleanup.service';
|
||||
import { SessionManagerService } from './session_manager.service';
|
||||
import { IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { IZulipClientPoolService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
|
||||
describe('SessionCleanupService', () => {
|
||||
let service: SessionCleanupService;
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common';
|
||||
import { SessionManagerService } from './session_manager.service';
|
||||
import { IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { IZulipClientPoolService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
|
||||
/**
|
||||
* 清理任务配置接口
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
import * as fc from 'fast-check';
|
||||
import { SessionManagerService, GameSession, Position } from './session_manager.service';
|
||||
import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { IZulipConfigService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
* - 支持会话状态的序列化和反序列化
|
||||
* - 支持服务重启后的状态恢复
|
||||
*
|
||||
* 职责分离:
|
||||
* - 会话存储:管理会话数据在Redis中的存储和检索
|
||||
* - 位置跟踪:维护玩家在游戏世界中的位置信息
|
||||
* - 上下文注入:根据玩家位置确定消息的目标Stream和Topic
|
||||
* - 空间过滤:根据地图ID筛选相关的玩家会话
|
||||
* - 资源清理:定期清理过期会话和释放相关资源
|
||||
*
|
||||
* 主要方法:
|
||||
* - createSession(): 创建会话并绑定Socket_ID与Zulip_Queue_ID
|
||||
* - getSession(): 获取会话信息
|
||||
@@ -28,15 +35,19 @@
|
||||
* - 消息分发时进行空间过滤
|
||||
* - 玩家登出时清理会话数据
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
import { IRedisService } from '../../../core/redis/redis.interface';
|
||||
import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { Internal, Constants } from '../../../core/zulip/interfaces/zulip.interfaces';
|
||||
import { IZulipConfigService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
import { Internal, Constants } from '../../../core/zulip_core/interfaces/zulip.interfaces';
|
||||
|
||||
/**
|
||||
* 游戏会话接口 - 重新导出以保持向后兼容
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
MessageDistributor,
|
||||
} from './zulip_event_processor.service';
|
||||
import { SessionManagerService, GameSession } from './session_manager.service';
|
||||
import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
import { AppLoggerService } from '../../../core/utils/logger/logger.service';
|
||||
|
||||
describe('ZulipEventProcessorService', () => {
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
|
||||
import { Injectable, OnModuleDestroy, Inject, forwardRef, Logger } from '@nestjs/common';
|
||||
import { SessionManagerService } from './session_manager.service';
|
||||
import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
|
||||
/**
|
||||
* Zulip消息接口
|
||||
|
||||
@@ -50,8 +50,10 @@ import { MessageFilterService } from './services/message_filter.service';
|
||||
import { ZulipEventProcessorService } from './services/zulip_event_processor.service';
|
||||
import { SessionCleanupService } from './services/session_cleanup.service';
|
||||
import { ChatController } from './controllers/chat.controller';
|
||||
import { WebSocketDocsController } from './controllers/websocket-docs.controller';
|
||||
import { ZulipCoreModule } from '../../core/zulip/zulip-core.module';
|
||||
import { WebSocketDocsController } from './controllers/websocket_docs.controller';
|
||||
import { ZulipAccountsController } from './controllers/zulip_accounts.controller';
|
||||
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
||||
import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
|
||||
import { RedisModule } from '../../core/redis/redis.module';
|
||||
import { LoggerModule } from '../../core/utils/logger/logger.module';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
@@ -61,6 +63,8 @@ import { AuthModule } from '../auth/auth.module';
|
||||
imports: [
|
||||
// Zulip核心服务模块 - 提供技术实现相关的核心服务
|
||||
ZulipCoreModule,
|
||||
// Zulip账号关联模块 - 提供账号关联管理功能
|
||||
ZulipAccountsModule.forRoot(),
|
||||
// Redis模块 - 提供会话状态缓存和数据存储
|
||||
RedisModule,
|
||||
// 日志模块 - 提供统一的日志记录服务
|
||||
@@ -89,6 +93,8 @@ import { AuthModule } from '../auth/auth.module';
|
||||
ChatController,
|
||||
// WebSocket API文档控制器
|
||||
WebSocketDocsController,
|
||||
// Zulip账号关联管理控制器
|
||||
ZulipAccountsController,
|
||||
],
|
||||
exports: [
|
||||
// 导出主服务供其他模块使用
|
||||
|
||||
@@ -39,8 +39,9 @@ import {
|
||||
IZulipConfigService,
|
||||
ZulipClientInstance,
|
||||
SendMessageResult,
|
||||
} from '../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { ApiKeySecurityService } from '../../core/zulip/services/api_key_security.service';
|
||||
} from '../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service';
|
||||
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
||||
|
||||
describe('ZulipService', () => {
|
||||
let service: ZulipService;
|
||||
@@ -49,6 +50,7 @@ describe('ZulipService', () => {
|
||||
let mockMessageFilter: jest.Mocked<MessageFilterService>;
|
||||
let mockEventProcessor: jest.Mocked<ZulipEventProcessorService>;
|
||||
let mockConfigManager: jest.Mocked<IZulipConfigService>;
|
||||
let mockLoginCoreService: jest.Mocked<LoginCoreService>;
|
||||
|
||||
// 创建模拟的Zulip客户端实例
|
||||
const createMockClientInstance = (overrides: Partial<ZulipClientInstance> = {}): ZulipClientInstance => ({
|
||||
@@ -136,6 +138,14 @@ describe('ZulipService', () => {
|
||||
validateConfig: jest.fn(),
|
||||
} as any;
|
||||
|
||||
mockLoginCoreService = {
|
||||
verifyToken: jest.fn(),
|
||||
generateTokens: jest.fn(),
|
||||
refreshTokens: jest.fn(),
|
||||
revokeToken: jest.fn(),
|
||||
validateTokenPayload: jest.fn(),
|
||||
} as any;
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
providers: [
|
||||
ZulipService,
|
||||
@@ -160,7 +170,7 @@ describe('ZulipService', () => {
|
||||
useValue: mockConfigManager,
|
||||
},
|
||||
{
|
||||
provide: ApiKeySecurityService,
|
||||
provide: 'API_KEY_SECURITY_SERVICE',
|
||||
useValue: {
|
||||
extractApiKey: jest.fn(),
|
||||
validateApiKey: jest.fn(),
|
||||
@@ -172,10 +182,39 @@ describe('ZulipService', () => {
|
||||
}),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: LoginCoreService,
|
||||
useValue: mockLoginCoreService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
service = module.get<ZulipService>(ZulipService);
|
||||
|
||||
// 配置LoginCoreService的默认mock行为
|
||||
mockLoginCoreService.verifyToken.mockImplementation(async (token: string) => {
|
||||
// 模拟token验证逻辑
|
||||
if (token.startsWith('invalid')) {
|
||||
throw new Error('Invalid token');
|
||||
}
|
||||
|
||||
// 从token中提取用户信息(模拟JWT解析)
|
||||
const userId = `user_${token.substring(0, 8)}`;
|
||||
const username = `Player_${userId.substring(5, 10)}`;
|
||||
const email = `${userId}@example.com`;
|
||||
|
||||
return {
|
||||
sub: userId,
|
||||
username,
|
||||
email,
|
||||
role: 1, // 数字类型的角色
|
||||
type: 'access' as const,
|
||||
iat: Math.floor(Date.now() / 1000),
|
||||
exp: Math.floor(Date.now() / 1000) + 3600,
|
||||
iss: 'whale-town',
|
||||
aud: 'whale-town-users',
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
it('should be defined', () => {
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
* - 整合各个子服务,提供统一的业务接口
|
||||
* - 处理游戏客户端与Zulip之间的核心业务逻辑
|
||||
*
|
||||
* 职责分离:
|
||||
* - 业务协调:整合会话管理、消息过滤、事件处理等子服务
|
||||
* - 流程控制:管理玩家登录登出的完整业务流程
|
||||
* - 接口适配:在游戏协议和Zulip协议之间进行转换
|
||||
* - 错误处理:统一处理业务异常和降级策略
|
||||
*
|
||||
* 主要方法:
|
||||
* - handlePlayerLogin(): 处理玩家登录和Zulip客户端初始化
|
||||
* - handlePlayerLogout(): 处理玩家登出和资源清理
|
||||
@@ -17,9 +23,15 @@
|
||||
* - 会话管理和状态维护
|
||||
* - 消息格式转换和过滤
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 注释规范检查和修正 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 拆分过长方法,提取validateLoginParams和createUserSession私有方法 (修改者: moyin)
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.1.0
|
||||
* @version 1.2.0
|
||||
* @since 2026-01-06
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
@@ -30,9 +42,9 @@ import { ZulipEventProcessorService } from './services/zulip_event_processor.ser
|
||||
import {
|
||||
IZulipClientPoolService,
|
||||
IZulipConfigService,
|
||||
} from '../../core/zulip/interfaces/zulip-core.interfaces';
|
||||
import { ApiKeySecurityService } from '../../core/zulip/services/api_key_security.service';
|
||||
import { LoginService } from '../auth/services/login.service';
|
||||
IApiKeySecurityService,
|
||||
} from '../../core/zulip_core/interfaces/zulip_core.interfaces';
|
||||
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
||||
|
||||
/**
|
||||
* 玩家登录请求接口
|
||||
@@ -116,8 +128,9 @@ export class ZulipService {
|
||||
private readonly eventProcessor: ZulipEventProcessorService,
|
||||
@Inject('ZULIP_CONFIG_SERVICE')
|
||||
private readonly configManager: IZulipConfigService,
|
||||
private readonly apiKeySecurityService: ApiKeySecurityService,
|
||||
private readonly loginService: LoginService,
|
||||
@Inject('API_KEY_SECURITY_SERVICE')
|
||||
private readonly apiKeySecurityService: IApiKeySecurityService,
|
||||
private readonly loginCoreService: LoginCoreService,
|
||||
) {
|
||||
this.logger.log('ZulipService初始化完成');
|
||||
|
||||
@@ -144,6 +157,18 @@ export class ZulipService {
|
||||
*
|
||||
* @throws UnauthorizedException 当Token验证失败时
|
||||
* @throws InternalServerErrorException 当系统操作失败时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const loginRequest: PlayerLoginRequest = {
|
||||
* token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
* socketId: 'socket_12345'
|
||||
* };
|
||||
* const result = await zulipService.handlePlayerLogin(loginRequest);
|
||||
* if (result.success) {
|
||||
* console.log(`用户 ${result.username} 登录成功`);
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async handlePlayerLogin(request: PlayerLoginRequest): Promise<LoginResponse> {
|
||||
const startTime = Date.now();
|
||||
@@ -156,28 +181,15 @@ export class ZulipService {
|
||||
|
||||
try {
|
||||
// 1. 验证请求参数
|
||||
if (!request.token || !request.token.trim()) {
|
||||
this.logger.warn('登录失败:Token为空', {
|
||||
operation: 'handlePlayerLogin',
|
||||
socketId: request.socketId,
|
||||
});
|
||||
const paramValidation = this.validateLoginParams(request);
|
||||
if (!paramValidation.isValid) {
|
||||
return {
|
||||
success: false,
|
||||
error: 'Token不能为空',
|
||||
error: paramValidation.error,
|
||||
};
|
||||
}
|
||||
|
||||
if (!request.socketId || !request.socketId.trim()) {
|
||||
this.logger.warn('登录失败:socketId为空', {
|
||||
operation: 'handlePlayerLogin',
|
||||
});
|
||||
return {
|
||||
success: false,
|
||||
error: 'socketId不能为空',
|
||||
};
|
||||
}
|
||||
|
||||
// 2. 验证游戏Token并获取用户信息 调用认证服务验证Token
|
||||
// 2. 验证游戏Token并获取用户信息
|
||||
const userInfo = await this.validateGameToken(request.token);
|
||||
if (!userInfo) {
|
||||
this.logger.warn('登录失败:Token验证失败', {
|
||||
@@ -190,80 +202,28 @@ export class ZulipService {
|
||||
};
|
||||
}
|
||||
|
||||
// 3. 生成会话ID
|
||||
const sessionId = randomUUID();
|
||||
|
||||
// 调试日志:检查用户信息
|
||||
this.logger.log('用户信息检查', {
|
||||
operation: 'handlePlayerLogin',
|
||||
userId: userInfo.userId,
|
||||
hasZulipApiKey: !!userInfo.zulipApiKey,
|
||||
zulipApiKeyLength: userInfo.zulipApiKey?.length || 0,
|
||||
zulipEmail: userInfo.zulipEmail,
|
||||
email: userInfo.email,
|
||||
});
|
||||
|
||||
// 4. 创建Zulip客户端(如果有API Key)
|
||||
let zulipQueueId = `queue_${sessionId}`;
|
||||
// 3. 创建Zulip客户端和会话
|
||||
const sessionResult = await this.createUserSession(request.socketId, userInfo);
|
||||
|
||||
if (userInfo.zulipApiKey) {
|
||||
try {
|
||||
const zulipConfig = this.configManager.getZulipConfig();
|
||||
const clientInstance = await this.zulipClientPool.createUserClient(userInfo.userId, {
|
||||
username: userInfo.zulipEmail || userInfo.email,
|
||||
apiKey: userInfo.zulipApiKey,
|
||||
realm: zulipConfig.zulipServerUrl,
|
||||
});
|
||||
|
||||
if (clientInstance.queueId) {
|
||||
zulipQueueId = clientInstance.queueId;
|
||||
}
|
||||
|
||||
this.logger.log('Zulip客户端创建成功', {
|
||||
operation: 'handlePlayerLogin',
|
||||
userId: userInfo.userId,
|
||||
queueId: zulipQueueId,
|
||||
});
|
||||
} catch (zulipError) {
|
||||
const err = zulipError as Error;
|
||||
this.logger.warn('Zulip客户端创建失败,使用本地模式', {
|
||||
operation: 'handlePlayerLogin',
|
||||
userId: userInfo.userId,
|
||||
error: err.message,
|
||||
});
|
||||
// Zulip客户端创建失败不影响登录,使用本地模式
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 创建游戏会话
|
||||
const session = await this.sessionManager.createSession(
|
||||
request.socketId,
|
||||
userInfo.userId,
|
||||
zulipQueueId,
|
||||
userInfo.username,
|
||||
this.DEFAULT_MAP,
|
||||
{ x: 400, y: 300 },
|
||||
);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
this.logger.log('玩家登录处理完成', {
|
||||
operation: 'handlePlayerLogin',
|
||||
socketId: request.socketId,
|
||||
sessionId,
|
||||
sessionId: sessionResult.sessionId,
|
||||
userId: userInfo.userId,
|
||||
username: userInfo.username,
|
||||
currentMap: session.currentMap,
|
||||
currentMap: sessionResult.currentMap,
|
||||
duration,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
sessionId,
|
||||
sessionId: sessionResult.sessionId,
|
||||
userId: userInfo.userId,
|
||||
username: userInfo.username,
|
||||
currentMap: session.currentMap,
|
||||
currentMap: sessionResult.currentMap,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
@@ -285,6 +245,108 @@ export class ZulipService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证登录请求参数
|
||||
*
|
||||
* @param request 登录请求
|
||||
* @returns 验证结果
|
||||
* @private
|
||||
*/
|
||||
private validateLoginParams(request: PlayerLoginRequest): { isValid: boolean; error?: string } {
|
||||
if (!request.token || !request.token.trim()) {
|
||||
this.logger.warn('登录失败:Token为空', {
|
||||
operation: 'validateLoginParams',
|
||||
socketId: request.socketId,
|
||||
});
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'Token不能为空',
|
||||
};
|
||||
}
|
||||
|
||||
if (!request.socketId || !request.socketId.trim()) {
|
||||
this.logger.warn('登录失败:socketId为空', {
|
||||
operation: 'validateLoginParams',
|
||||
});
|
||||
return {
|
||||
isValid: false,
|
||||
error: 'socketId不能为空',
|
||||
};
|
||||
}
|
||||
|
||||
return { isValid: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户会话和Zulip客户端
|
||||
*
|
||||
* @param socketId Socket连接ID
|
||||
* @param userInfo 用户信息
|
||||
* @returns 会话创建结果
|
||||
* @private
|
||||
*/
|
||||
private async createUserSession(socketId: string, userInfo: any): Promise<{ sessionId: string; currentMap: string }> {
|
||||
// 生成会话ID
|
||||
const sessionId = randomUUID();
|
||||
|
||||
// 调试日志:检查用户信息
|
||||
this.logger.log('用户信息检查', {
|
||||
operation: 'createUserSession',
|
||||
userId: userInfo.userId,
|
||||
hasZulipApiKey: !!userInfo.zulipApiKey,
|
||||
zulipApiKeyLength: userInfo.zulipApiKey?.length || 0,
|
||||
zulipEmail: userInfo.zulipEmail,
|
||||
email: userInfo.email,
|
||||
});
|
||||
|
||||
// 创建Zulip客户端(如果有API Key)
|
||||
let zulipQueueId = `queue_${sessionId}`;
|
||||
|
||||
if (userInfo.zulipApiKey) {
|
||||
try {
|
||||
const zulipConfig = this.configManager.getZulipConfig();
|
||||
const clientInstance = await this.zulipClientPool.createUserClient(userInfo.userId, {
|
||||
username: userInfo.zulipEmail || userInfo.email,
|
||||
apiKey: userInfo.zulipApiKey,
|
||||
realm: zulipConfig.zulipServerUrl,
|
||||
});
|
||||
|
||||
if (clientInstance.queueId) {
|
||||
zulipQueueId = clientInstance.queueId;
|
||||
}
|
||||
|
||||
this.logger.log('Zulip客户端创建成功', {
|
||||
operation: 'createUserSession',
|
||||
userId: userInfo.userId,
|
||||
queueId: zulipQueueId,
|
||||
});
|
||||
} catch (zulipError) {
|
||||
const err = zulipError as Error;
|
||||
this.logger.warn('Zulip客户端创建失败,使用本地模式', {
|
||||
operation: 'createUserSession',
|
||||
userId: userInfo.userId,
|
||||
error: err.message,
|
||||
});
|
||||
// Zulip客户端创建失败不影响登录,使用本地模式
|
||||
}
|
||||
}
|
||||
|
||||
// 创建游戏会话
|
||||
const session = await this.sessionManager.createSession(
|
||||
socketId,
|
||||
userInfo.userId,
|
||||
zulipQueueId,
|
||||
userInfo.username,
|
||||
this.DEFAULT_MAP,
|
||||
{ x: 400, y: 300 },
|
||||
);
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
currentMap: session.currentMap,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证游戏Token
|
||||
*
|
||||
@@ -308,8 +370,8 @@ export class ZulipService {
|
||||
});
|
||||
|
||||
try {
|
||||
// 1. 使用LoginService验证JWT token
|
||||
const payload = await this.loginService.verifyToken(token, 'access');
|
||||
// 1. 使用LoginCoreService验证JWT token
|
||||
const payload = await this.loginCoreService.verifyToken(token, 'access');
|
||||
|
||||
if (!payload || !payload.sub) {
|
||||
this.logger.warn('Token载荷无效', {
|
||||
|
||||
@@ -6,6 +6,12 @@
|
||||
* - 实现游戏协议到Zulip协议的转换
|
||||
* - 提供统一的消息路由和权限控制
|
||||
*
|
||||
* 职责分离:
|
||||
* - 连接管理:处理WebSocket连接的建立、维护和断开
|
||||
* - 协议转换:在游戏客户端协议和内部业务协议之间转换
|
||||
* - 权限控制:验证用户身份和消息发送权限
|
||||
* - 消息路由:将消息分发到正确的业务处理服务
|
||||
*
|
||||
* 主要方法:
|
||||
* - handleConnection(): 处理客户端连接建立
|
||||
* - handleDisconnect(): 处理客户端连接断开
|
||||
@@ -18,9 +24,13 @@
|
||||
* - 消息协议转换和路由分发
|
||||
* - 连接状态管理和权限验证
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和修改记录 (修改者: moyin)
|
||||
*
|
||||
* @author angjustinl
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2025-12-25
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
|
||||
Reference in New Issue
Block a user