From 2d10131838db35f266a6fb2a770dccc90af6f36e Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 31 Dec 2025 15:44:36 +0800 Subject: [PATCH] =?UTF-8?q?refactor=EF=BC=9A=E9=87=8D=E6=9E=84Zulip?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=8C=89=E4=B8=9A=E5=8A=A1=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8C=96=E6=9E=B6=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将技术实现服务从business层迁移到core层 - 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务 - 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则 - 通过依赖注入实现业务层与核心层的解耦 - 更新模块导入关系,确保架构分层清晰 重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性 --- src/app.module.ts | 2 + src/business/zulip/README.md | 172 +++ ...spec.ts => message_filter.service.spec.ts} | 16 +- ...r.service.ts => message_filter.service.ts} | 28 +- .../services/session_cleanup.service.spec.ts | 650 ++++++++++ ....service.ts => session_cleanup.service.ts} | 36 +- ...pec.ts => session_manager.service.spec.ts} | 18 +- ....service.ts => session_manager.service.ts} | 35 +- ... => zulip_event_processor.service.spec.ts} | 29 +- ...ce.ts => zulip_event_processor.service.ts} | 34 +- src/business/zulip/zulip.module.ts | 104 +- src/business/zulip/zulip.service.spec.ts | 1134 +++++++++++++++++ src/business/zulip/zulip.service.ts | 43 +- ....spec.ts => zulip_integration.e2e.spec.ts} | 2 +- ...pec.ts => zulip_websocket.gateway.spec.ts} | 4 +- ....gateway.ts => zulip_websocket.gateway.ts} | 25 +- src/{business => core}/zulip/config/index.ts | 0 .../zulip/config/zulip.config.ts | 0 src/core/zulip/index.ts | 26 + .../zulip/interfaces/zulip-core.interfaces.ts | 294 +++++ .../zulip/interfaces/zulip.interfaces.ts | 0 .../api_key_security.service.spec.ts} | 2 +- .../services/api_key_security.service.ts} | 22 + .../services/config_manager.service.spec.ts} | 4 +- .../zulip/services/config_manager.service.ts} | 25 + .../services/error_handler.service.spec.ts} | 2 +- .../zulip/services/error_handler.service.ts} | 22 + .../zulip/services/monitoring.service.spec.ts | 0 .../zulip/services/monitoring.service.ts | 23 + .../services/stream_initializer.service.ts} | 24 +- .../services/zulip_client.service.spec.ts} | 2 +- .../zulip/services/zulip_client.service.ts} | 22 + .../zulip_client_pool.service.spec.ts} | 6 +- .../services/zulip_client_pool.service.ts} | 24 +- .../zulip/types/zulip-js.d.ts | 0 src/core/zulip/zulip-core.module.ts | 68 + 36 files changed, 2773 insertions(+), 125 deletions(-) create mode 100644 src/business/zulip/README.md rename src/business/zulip/services/{message-filter.service.spec.ts => message_filter.service.spec.ts} (97%) rename src/business/zulip/services/{message-filter.service.ts => message_filter.service.ts} (96%) create mode 100644 src/business/zulip/services/session_cleanup.service.spec.ts rename src/business/zulip/services/{session-cleanup.service.ts => session_cleanup.service.ts} (86%) rename src/business/zulip/services/{session-manager.service.spec.ts => session_manager.service.spec.ts} (97%) rename src/business/zulip/services/{session-manager.service.ts => session_manager.service.ts} (95%) rename src/business/zulip/services/{zulip-event-processor.service.spec.ts => zulip_event_processor.service.spec.ts} (97%) rename src/business/zulip/services/{zulip-event-processor.service.ts => zulip_event_processor.service.ts} (96%) create mode 100644 src/business/zulip/zulip.service.spec.ts rename src/business/zulip/{zulip-integration.e2e.spec.ts => zulip_integration.e2e.spec.ts} (99%) rename src/business/zulip/{zulip-websocket.gateway.spec.ts => zulip_websocket.gateway.spec.ts} (99%) rename src/business/zulip/{zulip-websocket.gateway.ts => zulip_websocket.gateway.ts} (95%) rename src/{business => core}/zulip/config/index.ts (100%) rename src/{business => core}/zulip/config/zulip.config.ts (100%) create mode 100644 src/core/zulip/index.ts create mode 100644 src/core/zulip/interfaces/zulip-core.interfaces.ts rename src/{business => core}/zulip/interfaces/zulip.interfaces.ts (100%) rename src/{business/zulip/services/api-key-security.service.spec.ts => core/zulip/services/api_key_security.service.spec.ts} (99%) rename src/{business/zulip/services/api-key-security.service.ts => core/zulip/services/api_key_security.service.ts} (97%) rename src/{business/zulip/services/config-manager.service.spec.ts => core/zulip/services/config_manager.service.spec.ts} (99%) rename src/{business/zulip/services/config-manager.service.ts => core/zulip/services/config_manager.service.ts} (98%) rename src/{business/zulip/services/error-handler.service.spec.ts => core/zulip/services/error_handler.service.spec.ts} (99%) rename src/{business/zulip/services/error-handler.service.ts => core/zulip/services/error_handler.service.ts} (97%) rename src/{business => core}/zulip/services/monitoring.service.spec.ts (100%) rename src/{business => core}/zulip/services/monitoring.service.ts (96%) rename src/{business/zulip/services/stream-initializer.service.ts => core/zulip/services/stream_initializer.service.ts} (92%) rename src/{business/zulip/services/zulip-client.service.spec.ts => core/zulip/services/zulip_client.service.spec.ts} (99%) rename src/{business/zulip/services/zulip-client.service.ts => core/zulip/services/zulip_client.service.ts} (96%) rename src/{business/zulip/services/zulip-client-pool.service.spec.ts => core/zulip/services/zulip_client_pool.service.spec.ts} (98%) rename src/{business/zulip/services/zulip-client-pool.service.ts => core/zulip/services/zulip_client_pool.service.ts} (95%) rename src/{business => core}/zulip/types/zulip-js.d.ts (100%) create mode 100644 src/core/zulip/zulip-core.module.ts diff --git a/src/app.module.ts b/src/app.module.ts index 858b3cd..2355bfc 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -8,6 +8,7 @@ import { LoggerModule } from './core/utils/logger/logger.module'; import { UsersModule } from './core/db/users/users.module'; import { LoginCoreModule } from './core/login_core/login_core.module'; import { AuthModule } from './business/auth/auth.module'; +import { ZulipModule } from './business/zulip/zulip.module'; import { RedisModule } from './core/redis/redis.module'; import { AdminModule } from './business/admin/admin.module'; import { UserMgmtModule } from './business/user-mgmt/user-mgmt.module'; @@ -67,6 +68,7 @@ function isDatabaseConfigured(): boolean { isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(), LoginCoreModule, AuthModule, + ZulipModule, UserMgmtModule, AdminModule, SecurityModule, diff --git a/src/business/zulip/README.md b/src/business/zulip/README.md new file mode 100644 index 0000000..3cba68c --- /dev/null +++ b/src/business/zulip/README.md @@ -0,0 +1,172 @@ +# Zulip集成业务模块 + +## 架构重构说明 + +本模块已按照项目的分层架构要求进行重构,将技术实现细节移动到核心服务层,业务逻辑保留在业务层。 + +### 重构前后对比 + +#### 重构前(❌ 违反架构原则) +``` +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 # ✅ 业务逻辑:消息过滤 +``` + +#### 重构后(✅ 符合架构原则) +``` +# 业务逻辑层 +src/business/zulip/ +├── zulip.service.ts # 业务协调服务 +├── zulip_websocket.gateway.ts # WebSocket业务网关 +└── services/ + ├── session_manager.service.ts # 会话业务逻辑 + └── message_filter.service.ts # 消息过滤业务规则 + +# 核心服务层 +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 # 核心服务模块 +``` + +### 架构优势 + +#### 1. 单一职责原则 +- **业务层**:只关注游戏相关的业务逻辑和规则 +- **核心层**:只处理技术实现和第三方API调用 + +#### 2. 依赖注入和接口抽象 +```typescript +// 业务层通过接口依赖核心服务 +constructor( + @Inject('ZULIP_CLIENT_POOL_SERVICE') + private readonly zulipClientPool: IZulipClientPoolService, + @Inject('ZULIP_CONFIG_SERVICE') + private readonly configManager: IZulipConfigService, +) {} +``` + +#### 3. 易于测试和维护 +- 业务逻辑可以独立测试,不依赖具体的技术实现 +- 核心服务可以独立替换,不影响业务逻辑 +- 接口定义清晰,便于理解和维护 + +### 服务职责划分 + +#### 业务逻辑层服务 + +| 服务 | 职责 | 业务价值 | +|------|------|----------| +| `ZulipService` | 游戏登录/登出业务流程协调 | 处理玩家生命周期管理 | +| `SessionManagerService` | 游戏会话状态和上下文管理 | 维护玩家位置和聊天上下文 | +| `MessageFilterService` | 消息过滤和业务规则控制 | 实现内容审核和权限验证 | +| `ZulipWebSocketGateway` | WebSocket业务协议处理 | 游戏协议转换和路由 | + +#### 核心服务层服务 + +| 服务 | 职责 | 技术价值 | +|------|------|----------| +| `ZulipClientService` | Zulip REST API封装 | 第三方API调用抽象 | +| `ZulipClientPoolService` | 客户端连接池管理 | 资源管理和性能优化 | +| `ConfigManagerService` | 配置文件管理和热重载 | 系统配置和运维支持 | +| `ZulipEventProcessorService` | 事件队列处理和消息转换 | 异步消息处理机制 | + +### 使用示例 + +#### 业务层调用核心服务 +```typescript +@Injectable() +export class ZulipService { + constructor( + @Inject('ZULIP_CLIENT_POOL_SERVICE') + private readonly zulipClientPool: IZulipClientPoolService, + ) {} + + async sendChatMessage(request: ChatMessageRequest): Promise { + // 业务逻辑:验证和处理 + const session = await this.sessionManager.getSession(request.socketId); + const context = await this.sessionManager.injectContext(request.socketId); + + // 调用核心服务:技术实现 + const result = await this.zulipClientPool.sendMessage( + session.userId, + context.stream, + context.topic, + request.content, + ); + + return { success: result.success, messageId: result.messageId }; + } +} +``` + +### 迁移指南 + +如果你的代码中直接导入了已移动的服务,请按以下方式更新: + +#### 更新导入路径 +```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调用逻辑 + }); +}); +``` + +这种架构设计确保了业务逻辑与技术实现的清晰分离,提高了代码的可维护性和可测试性。 \ No newline at end of file diff --git a/src/business/zulip/services/message-filter.service.spec.ts b/src/business/zulip/services/message_filter.service.spec.ts similarity index 97% rename from src/business/zulip/services/message-filter.service.spec.ts rename to src/business/zulip/services/message_filter.service.spec.ts index 904356c..a2bf502 100644 --- a/src/business/zulip/services/message-filter.service.spec.ts +++ b/src/business/zulip/services/message_filter.service.spec.ts @@ -12,8 +12,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import * as fc from 'fast-check'; -import { MessageFilterService, ViolationType, ContentFilterResult } from './message-filter.service'; -import { ConfigManagerService } from './config-manager.service'; +import { MessageFilterService, ViolationType } from './message_filter.service'; +import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces'; import { AppLoggerService } from '../../../core/utils/logger/logger.service'; import { IRedisService } from '../../../core/redis/redis.interface'; @@ -21,7 +21,7 @@ describe('MessageFilterService', () => { let service: MessageFilterService; let mockLogger: jest.Mocked; let mockRedisService: jest.Mocked; - let mockConfigManager: jest.Mocked; + let mockConfigManager: jest.Mocked; // 内存存储模拟Redis let memoryStore: Map; @@ -100,6 +100,14 @@ describe('MessageFilterService', () => { hasMap: jest.fn().mockImplementation((mapId: string) => { return ['novice_village', 'tavern', 'market'].includes(mapId); }), + getMapIdByStream: jest.fn(), + getTopicByObject: jest.fn(), + getZulipConfig: jest.fn(), + hasStream: jest.fn(), + getAllMapIds: jest.fn(), + getAllStreams: jest.fn(), + reloadConfig: jest.fn(), + validateConfig: jest.fn(), } as any; const module: TestingModule = await Test.createTestingModule({ @@ -114,7 +122,7 @@ describe('MessageFilterService', () => { useValue: mockRedisService, }, { - provide: ConfigManagerService, + provide: 'ZULIP_CONFIG_SERVICE', useValue: mockConfigManager, }, ], diff --git a/src/business/zulip/services/message-filter.service.ts b/src/business/zulip/services/message_filter.service.ts similarity index 96% rename from src/business/zulip/services/message-filter.service.ts rename to src/business/zulip/services/message_filter.service.ts index aad005b..54ff578 100644 --- a/src/business/zulip/services/message-filter.service.ts +++ b/src/business/zulip/services/message_filter.service.ts @@ -30,7 +30,7 @@ import { Injectable, Logger, Inject, forwardRef } from '@nestjs/common'; import { IRedisService } from '../../../core/redis/redis.interface'; -import { ConfigManagerService } from './config-manager.service'; +import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces'; /** * 内容过滤结果接口 @@ -90,6 +90,28 @@ export interface SensitiveWordConfig { category?: string; } +/** + * 消息过滤服务类 + * + * 职责: + * - 实施内容审核和频率控制 + * - 敏感词过滤和权限验证 + * - 防止恶意操作和滥用 + * - 与ConfigManager集成实现位置权限验证 + * + * 主要方法: + * - filterContent(): 内容过滤,敏感词检查 + * - checkRateLimit(): 频率限制检查 + * - validatePermission(): 权限验证,防止位置欺诈 + * - validateMessage(): 综合消息验证 + * - logViolation(): 记录违规行为 + * + * 使用场景: + * - 消息发送前的内容审核 + * - 频率限制和防刷屏 + * - 权限验证和安全控制 + * - 违规行为监控和记录 + */ @Injectable() export class MessageFilterService { private readonly RATE_LIMIT_PREFIX = 'zulip:rate_limit:'; @@ -127,8 +149,8 @@ export class MessageFilterService { constructor( @Inject('REDIS_SERVICE') private readonly redisService: IRedisService, - @Inject(forwardRef(() => ConfigManagerService)) - private readonly configManager: ConfigManagerService, + @Inject('ZULIP_CONFIG_SERVICE') + private readonly configManager: IZulipConfigService, ) { this.logger.log('MessageFilterService初始化完成'); } diff --git a/src/business/zulip/services/session_cleanup.service.spec.ts b/src/business/zulip/services/session_cleanup.service.spec.ts new file mode 100644 index 0000000..3e0469d --- /dev/null +++ b/src/business/zulip/services/session_cleanup.service.spec.ts @@ -0,0 +1,650 @@ +/** + * 会话清理定时任务服务测试 + * + * 功能描述: + * - 测试SessionCleanupService的核心功能 + * - 包含属性测试验证定时清理机制 + * - 包含属性测试验证资源释放完整性 + * + * **Feature: zulip-integration, Property 13: 定时清理机制** + * **Validates: Requirements 6.1, 6.2, 6.3** + * + * **Feature: zulip-integration, Property 14: 资源释放完整性** + * **Validates: Requirements 6.4, 6.5** + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-12-31 + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import * as fc from 'fast-check'; +import { + SessionCleanupService, + CleanupConfig, + CleanupResult +} from './session_cleanup.service'; +import { SessionManagerService } from './session_manager.service'; +import { IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces'; + +describe('SessionCleanupService', () => { + let service: SessionCleanupService; + let mockSessionManager: jest.Mocked; + let mockZulipClientPool: jest.Mocked; + + // 模拟清理结果 + const createMockCleanupResult = (overrides: Partial = {}): any => ({ + cleanedCount: 3, + zulipQueueIds: ['queue-1', 'queue-2', 'queue-3'], + duration: 150, + timestamp: new Date(), + ...overrides, + }); + + beforeEach(async () => { + jest.clearAllMocks(); + // Only use fake timers for tests that need them + // The concurrent test will use real timers for proper Promise handling + + mockSessionManager = { + cleanupExpiredSessions: jest.fn(), + getSession: jest.fn(), + destroySession: jest.fn(), + createSession: jest.fn(), + updatePlayerPosition: jest.fn(), + getSocketsInMap: jest.fn(), + injectContext: jest.fn(), + } as any; + + mockZulipClientPool = { + createUserClient: jest.fn(), + getUserClient: jest.fn(), + hasUserClient: jest.fn(), + sendMessage: jest.fn(), + registerEventQueue: jest.fn(), + deregisterEventQueue: jest.fn(), + destroyUserClient: jest.fn(), + getPoolStats: jest.fn(), + cleanupIdleClients: jest.fn(), + } as any; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SessionCleanupService, + { + provide: SessionManagerService, + useValue: mockSessionManager, + }, + { + provide: 'ZULIP_CLIENT_POOL_SERVICE', + useValue: mockZulipClientPool, + }, + ], + }).compile(); + + service = module.get(SessionCleanupService); + }); + + afterEach(() => { + service.stopCleanupTask(); + // Only restore timers if they were faked + if (jest.isMockFunction(setTimeout)) { + jest.useRealTimers(); + } + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('startCleanupTask - 启动清理任务', () => { + it('应该启动定时清理任务', () => { + service.startCleanupTask(); + + const status = service.getStatus(); + expect(status.isEnabled).toBe(true); + }); + + it('应该在已启动时不重复启动', () => { + service.startCleanupTask(); + service.startCleanupTask(); // 第二次调用 + + const status = service.getStatus(); + expect(status.isEnabled).toBe(true); + }); + + it('应该立即执行一次清理', async () => { + jest.useFakeTimers(); + + mockSessionManager.cleanupExpiredSessions.mockResolvedValue( + createMockCleanupResult({ cleanedCount: 2 }) + ); + + service.startCleanupTask(); + + // 等待立即执行的清理完成 + await jest.runOnlyPendingTimersAsync(); + + expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30); + + jest.useRealTimers(); + }); + }); + + describe('stopCleanupTask - 停止清理任务', () => { + it('应该停止定时清理任务', () => { + service.startCleanupTask(); + service.stopCleanupTask(); + + const status = service.getStatus(); + expect(status.isEnabled).toBe(false); + }); + + it('应该在未启动时安全停止', () => { + service.stopCleanupTask(); + + const status = service.getStatus(); + expect(status.isEnabled).toBe(false); + }); + }); + + describe('runCleanup - 执行清理', () => { + it('应该成功执行清理并返回结果', async () => { + const mockResult = createMockCleanupResult({ + cleanedCount: 5, + zulipQueueIds: ['queue-1', 'queue-2', 'queue-3', 'queue-4', 'queue-5'], + }); + + mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); + + const result = await service.runCleanup(); + + expect(result.success).toBe(true); + expect(result.cleanedSessions).toBe(5); + expect(result.deregisteredQueues).toBe(5); + expect(result.duration).toBeGreaterThanOrEqual(0); // 修改为 >= 0,因为测试环境可能很快 + expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30); + }); + + it('应该处理清理过程中的错误', async () => { + const error = new Error('清理失败'); + mockSessionManager.cleanupExpiredSessions.mockRejectedValue(error); + + const result = await service.runCleanup(); + + expect(result.success).toBe(false); + expect(result.error).toBe('清理失败'); + expect(result.cleanedSessions).toBe(0); + expect(result.deregisteredQueues).toBe(0); + }); + + it('应该防止并发执行', async () => { + let resolveFirst: () => void; + const firstPromise = new Promise(resolve => { + resolveFirst = () => resolve(createMockCleanupResult()); + }); + + mockSessionManager.cleanupExpiredSessions.mockReturnValueOnce(firstPromise); + + // 同时启动两个清理任务 + const promise1 = service.runCleanup(); + const promise2 = service.runCleanup(); + + // 第二个应该立即返回失败 + const result2 = await promise2; + expect(result2.success).toBe(false); + expect(result2.error).toContain('正在执行中'); + + // 完成第一个任务 + resolveFirst!(); + const result1 = await promise1; + expect(result1.success).toBe(true); + }, 15000); + + it('应该记录最后一次清理结果', async () => { + const mockResult = createMockCleanupResult({ cleanedCount: 3 }); + mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); + + await service.runCleanup(); + + const lastResult = service.getLastCleanupResult(); + expect(lastResult).not.toBeNull(); + expect(lastResult!.cleanedSessions).toBe(3); + expect(lastResult!.success).toBe(true); + }); + }); + + describe('getStatus - 获取状态', () => { + it('应该返回正确的状态信息', () => { + const status = service.getStatus(); + + expect(status).toHaveProperty('isRunning'); + expect(status).toHaveProperty('isEnabled'); + expect(status).toHaveProperty('config'); + expect(status).toHaveProperty('lastResult'); + expect(typeof status.isRunning).toBe('boolean'); + expect(typeof status.isEnabled).toBe('boolean'); + }); + + it('应该反映任务启动状态', () => { + let status = service.getStatus(); + expect(status.isEnabled).toBe(false); + + service.startCleanupTask(); + status = service.getStatus(); + expect(status.isEnabled).toBe(true); + + service.stopCleanupTask(); + status = service.getStatus(); + expect(status.isEnabled).toBe(false); + }); + }); + + describe('updateConfig - 更新配置', () => { + it('应该更新清理配置', () => { + const newConfig: Partial = { + intervalMs: 10 * 60 * 1000, // 10分钟 + sessionTimeoutMinutes: 60, // 60分钟 + }; + + service.updateConfig(newConfig); + + const status = service.getStatus(); + expect(status.config.intervalMs).toBe(10 * 60 * 1000); + expect(status.config.sessionTimeoutMinutes).toBe(60); + }); + + it('应该在配置更改后重启任务', () => { + service.startCleanupTask(); + + const newConfig: Partial = { + intervalMs: 2 * 60 * 1000, // 2分钟 + }; + + service.updateConfig(newConfig); + + const status = service.getStatus(); + expect(status.isEnabled).toBe(true); + expect(status.config.intervalMs).toBe(2 * 60 * 1000); + }); + + it('应该支持禁用清理任务', () => { + service.startCleanupTask(); + + service.updateConfig({ enabled: false }); + + const status = service.getStatus(); + expect(status.isEnabled).toBe(false); + }); + }); + /** + * 属性测试: 定时清理机制 + * + * **Feature: zulip-integration, Property 13: 定时清理机制** + * **Validates: Requirements 6.1, 6.2, 6.3** + * + * 系统应该定期清理过期的游戏会话,释放相关资源, + * 并确保清理过程不影响正常的游戏服务 + */ + describe('Property 13: 定时清理机制', () => { + /** + * 属性: 对于任何有效的清理配置,系统应该按配置间隔执行清理 + * 验证需求 6.1: 系统应定期检查并清理过期的游戏会话 + */ + it('对于任何有效的清理配置,系统应该按配置间隔执行清理', async () => { + await fc.assert( + fc.asyncProperty( + // 生成有效的清理间隔(1-10分钟) + fc.integer({ min: 1, max: 10 }).map(minutes => minutes * 60 * 1000), + // 生成有效的会话超时时间(10-120分钟) + fc.integer({ min: 10, max: 120 }), + async (intervalMs, sessionTimeoutMinutes) => { + // 重置mock以确保每次测试都是干净的状态 + jest.clearAllMocks(); + jest.useFakeTimers(); + + const config: Partial = { + intervalMs, + sessionTimeoutMinutes, + enabled: true, + }; + + // 模拟清理结果 + mockSessionManager.cleanupExpiredSessions.mockResolvedValue( + createMockCleanupResult({ cleanedCount: 2 }) + ); + + service.updateConfig(config); + service.startCleanupTask(); + + // 验证配置被正确设置 + const status = service.getStatus(); + expect(status.config.intervalMs).toBe(intervalMs); + expect(status.config.sessionTimeoutMinutes).toBe(sessionTimeoutMinutes); + expect(status.isEnabled).toBe(true); + + // 验证立即执行了一次清理 + await jest.runOnlyPendingTimersAsync(); + expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(sessionTimeoutMinutes); + + service.stopCleanupTask(); + jest.useRealTimers(); + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 对于任何清理操作,都应该记录清理结果和统计信息 + * 验证需求 6.2: 清理过程中系统应记录清理的会话数量和释放的资源 + */ + it('对于任何清理操作,都应该记录清理结果和统计信息', async () => { + await fc.assert( + fc.asyncProperty( + // 生成清理的会话数量 + fc.integer({ min: 0, max: 20 }), + // 生成Zulip队列ID列表 + fc.array( + fc.string({ minLength: 5, maxLength: 20 }).filter(s => s.trim().length > 0), + { minLength: 0, maxLength: 20 } + ), + async (cleanedCount, queueIds) => { + // 重置mock以确保每次测试都是干净的状态 + jest.clearAllMocks(); + + const mockResult = createMockCleanupResult({ + cleanedCount, + zulipQueueIds: queueIds.slice(0, cleanedCount), // 确保队列数量不超过清理数量 + }); + + mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); + + const result = await service.runCleanup(); + + // 验证清理结果被正确记录 + expect(result.success).toBe(true); + expect(result.cleanedSessions).toBe(cleanedCount); + expect(result.deregisteredQueues).toBe(Math.min(queueIds.length, cleanedCount)); + expect(result.duration).toBeGreaterThanOrEqual(0); + expect(result.timestamp).toBeInstanceOf(Date); + + // 验证最后一次清理结果被保存 + const lastResult = service.getLastCleanupResult(); + expect(lastResult).not.toBeNull(); + expect(lastResult!.cleanedSessions).toBe(cleanedCount); + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 清理过程中发生错误时,系统应该正确处理并记录错误信息 + * 验证需求 6.3: 清理过程中出现错误时系统应记录错误信息并继续正常服务 + */ + it('清理过程中发生错误时,系统应该正确处理并记录错误信息', async () => { + await fc.assert( + fc.asyncProperty( + // 生成各种错误消息 + fc.string({ minLength: 5, maxLength: 100 }).filter(s => s.trim().length > 0), + async (errorMessage) => { + // 重置mock以确保每次测试都是干净的状态 + jest.clearAllMocks(); + + const error = new Error(errorMessage.trim()); + mockSessionManager.cleanupExpiredSessions.mockRejectedValue(error); + + const result = await service.runCleanup(); + + // 验证错误被正确处理 + expect(result.success).toBe(false); + expect(result.error).toBe(errorMessage.trim()); + expect(result.cleanedSessions).toBe(0); + expect(result.deregisteredQueues).toBe(0); + expect(result.duration).toBeGreaterThanOrEqual(0); + + // 验证错误结果被保存 + const lastResult = service.getLastCleanupResult(); + expect(lastResult).not.toBeNull(); + expect(lastResult!.success).toBe(false); + expect(lastResult!.error).toBe(errorMessage.trim()); + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 并发清理请求应该被正确处理,避免重复执行 + * 验证需求 6.1: 系统应避免同时执行多个清理任务 + */ + it('并发清理请求应该被正确处理,避免重复执行', async () => { + // 重置mock + jest.clearAllMocks(); + + // 创建一个可控的Promise,使用实际的异步行为 + let resolveCleanup: (value: any) => void; + const cleanupPromise = new Promise(resolve => { + resolveCleanup = resolve; + }); + + mockSessionManager.cleanupExpiredSessions.mockReturnValue(cleanupPromise); + + // 启动第一个清理请求(应该成功) + const promise1 = service.runCleanup(); + + // 等待一个微任务周期,确保第一个请求开始执行 + await Promise.resolve(); + + // 启动第二个和第三个清理请求(应该被拒绝) + const promise2 = service.runCleanup(); + const promise3 = service.runCleanup(); + + // 第二个和第三个请求应该立即返回失败 + const result2 = await promise2; + const result3 = await promise3; + + expect(result2.success).toBe(false); + expect(result2.error).toContain('正在执行中'); + expect(result3.success).toBe(false); + expect(result3.error).toContain('正在执行中'); + + // 完成第一个清理操作 + resolveCleanup!(createMockCleanupResult({ cleanedCount: 1 })); + const result1 = await promise1; + + expect(result1.success).toBe(true); + }, 10000); + }); + /** + * 属性测试: 资源释放完整性 + * + * **Feature: zulip-integration, Property 14: 资源释放完整性** + * **Validates: Requirements 6.4, 6.5** + * + * 清理过期会话时,系统应该完整释放所有相关资源, + * 包括Zulip事件队列、内存缓存等,确保不会造成资源泄漏 + */ + describe('Property 14: 资源释放完整性', () => { + /** + * 属性: 对于任何过期会话,清理时应该释放所有相关的Zulip资源 + * 验证需求 6.4: 清理会话时系统应注销对应的Zulip事件队列 + */ + it('对于任何过期会话,清理时应该释放所有相关的Zulip资源', async () => { + await fc.assert( + fc.asyncProperty( + // 生成过期会话数量 + fc.integer({ min: 1, max: 10 }), + // 生成每个会话对应的Zulip队列ID + fc.array( + fc.string({ minLength: 8, maxLength: 20 }).filter(s => s.trim().length > 0), + { minLength: 1, maxLength: 10 } + ), + async (sessionCount, queueIds) => { + // 重置mock以确保每次测试都是干净的状态 + jest.clearAllMocks(); + + const actualQueueIds = queueIds.slice(0, sessionCount); + const mockResult = createMockCleanupResult({ + cleanedCount: sessionCount, + zulipQueueIds: actualQueueIds, + }); + + mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); + + const result = await service.runCleanup(); + + // 验证清理成功 + expect(result.success).toBe(true); + expect(result.cleanedSessions).toBe(sessionCount); + + // 验证Zulip队列被处理(这里简化为计数验证) + expect(result.deregisteredQueues).toBe(actualQueueIds.length); + + // 验证SessionManager被调用清理过期会话 + expect(mockSessionManager.cleanupExpiredSessions).toHaveBeenCalledWith(30); + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 清理操作应该是原子性的,要么全部成功要么全部回滚 + * 验证需求 6.5: 清理过程应确保数据一致性,避免部分清理导致的不一致状态 + */ + it('清理操作应该是原子性的,要么全部成功要么全部回滚', async () => { + await fc.assert( + fc.asyncProperty( + // 生成是否模拟清理失败 + fc.boolean(), + // 生成会话数量 + fc.integer({ min: 1, max: 5 }), + async (shouldFail, sessionCount) => { + // 重置mock以确保每次测试都是干净的状态 + jest.clearAllMocks(); + + if (shouldFail) { + // 模拟清理失败 + const error = new Error('清理操作失败'); + mockSessionManager.cleanupExpiredSessions.mockRejectedValue(error); + } else { + // 模拟清理成功 + const mockResult = createMockCleanupResult({ + cleanedCount: sessionCount, + zulipQueueIds: Array.from({ length: sessionCount }, (_, i) => `queue-${i}`), + }); + mockSessionManager.cleanupExpiredSessions.mockResolvedValue(mockResult); + } + + const result = await service.runCleanup(); + + if (shouldFail) { + // 失败时应该没有任何资源被释放 + expect(result.success).toBe(false); + expect(result.cleanedSessions).toBe(0); + expect(result.deregisteredQueues).toBe(0); + expect(result.error).toBeDefined(); + } else { + // 成功时所有资源都应该被正确处理 + expect(result.success).toBe(true); + expect(result.cleanedSessions).toBe(sessionCount); + expect(result.deregisteredQueues).toBe(sessionCount); + expect(result.error).toBeUndefined(); + } + + // 验证结果的一致性 + expect(result.timestamp).toBeInstanceOf(Date); + expect(result.duration).toBeGreaterThanOrEqual(0); + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 清理配置更新应该正确重启清理任务而不丢失状态 + * 验证需求 6.5: 配置更新时系统应保持服务连续性 + */ + it('清理配置更新应该正确重启清理任务而不丢失状态', async () => { + await fc.assert( + fc.asyncProperty( + // 生成初始配置 + fc.record({ + intervalMs: fc.integer({ min: 1, max: 5 }).map(m => m * 60 * 1000), + sessionTimeoutMinutes: fc.integer({ min: 10, max: 60 }), + }), + // 生成新配置 + fc.record({ + intervalMs: fc.integer({ min: 1, max: 5 }).map(m => m * 60 * 1000), + sessionTimeoutMinutes: fc.integer({ min: 10, max: 60 }), + }), + async (initialConfig, newConfig) => { + // 重置mock以确保每次测试都是干净的状态 + jest.clearAllMocks(); + + // 设置初始配置并启动任务 + service.updateConfig(initialConfig); + service.startCleanupTask(); + + let status = service.getStatus(); + expect(status.isEnabled).toBe(true); + expect(status.config.intervalMs).toBe(initialConfig.intervalMs); + + // 更新配置 + service.updateConfig(newConfig); + + // 验证配置更新后任务仍在运行 + status = service.getStatus(); + expect(status.isEnabled).toBe(true); + expect(status.config.intervalMs).toBe(newConfig.intervalMs); + expect(status.config.sessionTimeoutMinutes).toBe(newConfig.sessionTimeoutMinutes); + + service.stopCleanupTask(); + } + ), + { numRuns: 30 } + ); + }, 30000); + }); + + describe('模块生命周期', () => { + it('应该在模块初始化时启动清理任务', async () => { + // 重新创建服务实例来测试模块初始化 + const module: TestingModule = await Test.createTestingModule({ + providers: [ + SessionCleanupService, + { + provide: SessionManagerService, + useValue: mockSessionManager, + }, + { + provide: 'ZULIP_CLIENT_POOL_SERVICE', + useValue: mockZulipClientPool, + }, + ], + }).compile(); + + const newService = module.get(SessionCleanupService); + + // 模拟模块初始化 + await newService.onModuleInit(); + + const status = newService.getStatus(); + expect(status.isEnabled).toBe(true); + + // 清理 + await newService.onModuleDestroy(); + }); + + it('应该在模块销毁时停止清理任务', async () => { + service.startCleanupTask(); + + await service.onModuleDestroy(); + + const status = service.getStatus(); + expect(status.isEnabled).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/src/business/zulip/services/session-cleanup.service.ts b/src/business/zulip/services/session_cleanup.service.ts similarity index 86% rename from src/business/zulip/services/session-cleanup.service.ts rename to src/business/zulip/services/session_cleanup.service.ts index 3f5fc0c..66f1639 100644 --- a/src/business/zulip/services/session-cleanup.service.ts +++ b/src/business/zulip/services/session_cleanup.service.ts @@ -21,9 +21,9 @@ * @since 2025-12-25 */ -import { Injectable, Logger, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; -import { SessionManagerService } from './session-manager.service'; -import { ZulipClientPoolService } from './zulip-client-pool.service'; +import { Injectable, Logger, OnModuleInit, OnModuleDestroy, Inject } from '@nestjs/common'; +import { SessionManagerService } from './session_manager.service'; +import { IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces'; /** * 清理任务配置接口 @@ -55,6 +55,28 @@ export interface CleanupResult { error?: string; } +/** + * 会话清理服务类 + * + * 职责: + * - 定时清理过期的游戏会话 + * - 释放无效的Zulip客户端资源 + * - 维护会话数据的一致性 + * - 提供会话清理统计和监控 + * + * 主要方法: + * - startCleanup(): 启动定时清理任务 + * - stopCleanup(): 停止清理任务 + * - performCleanup(): 执行一次清理操作 + * - getCleanupStats(): 获取清理统计信息 + * - updateConfig(): 更新清理配置 + * + * 使用场景: + * - 系统启动时自动开始清理任务 + * - 定期清理过期会话和资源 + * - 系统关闭时停止清理任务 + * - 监控清理效果和系统健康 + */ @Injectable() export class SessionCleanupService implements OnModuleInit, OnModuleDestroy { private cleanupInterval: NodeJS.Timeout | null = null; @@ -70,7 +92,8 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy { constructor( private readonly sessionManager: SessionManagerService, - private readonly zulipClientPool: ZulipClientPoolService, + @Inject('ZULIP_CLIENT_POOL_SERVICE') + private readonly zulipClientPool: IZulipClientPoolService, ) { this.logger.log('SessionCleanupService初始化完成'); } @@ -176,7 +199,8 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy { // 2. 注销对应的Zulip事件队列 let deregisteredQueues = 0; - for (const queueId of cleanupResult.zulipQueueIds) { + const queueIds = cleanupResult?.zulipQueueIds || []; + for (const queueId of queueIds) { try { // 根据queueId找到对应的用户并注销队列 // 注意:这里需要通过某种方式找到queueId对应的userId @@ -200,7 +224,7 @@ export class SessionCleanupService implements OnModuleInit, OnModuleDestroy { const duration = Date.now() - startTime; const result: CleanupResult = { - cleanedSessions: cleanupResult.cleanedCount, + cleanedSessions: cleanupResult?.cleanedCount || 0, deregisteredQueues, duration, timestamp: new Date(), diff --git a/src/business/zulip/services/session-manager.service.spec.ts b/src/business/zulip/services/session_manager.service.spec.ts similarity index 97% rename from src/business/zulip/services/session-manager.service.spec.ts rename to src/business/zulip/services/session_manager.service.spec.ts index 9cd3db2..fef1cce 100644 --- a/src/business/zulip/services/session-manager.service.spec.ts +++ b/src/business/zulip/services/session_manager.service.spec.ts @@ -12,8 +12,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import * as fc from 'fast-check'; -import { SessionManagerService, GameSession, Position } from './session-manager.service'; -import { ConfigManagerService } from './config-manager.service'; +import { SessionManagerService, GameSession, Position } from './session_manager.service'; +import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces'; import { AppLoggerService } from '../../../core/utils/logger/logger.service'; import { IRedisService } from '../../../core/redis/redis.interface'; @@ -21,7 +21,7 @@ describe('SessionManagerService', () => { let service: SessionManagerService; let mockLogger: jest.Mocked; let mockRedisService: jest.Mocked; - let mockConfigManager: jest.Mocked; + let mockConfigManager: jest.Mocked; // 内存存储模拟Redis let memoryStore: Map; @@ -57,9 +57,15 @@ describe('SessionManagerService', () => { }; return streamMap[mapId] || 'General'; }), + getMapIdByStream: jest.fn(), getTopicByObject: jest.fn().mockReturnValue('General'), - getMapConfig: jest.fn(), - getAllMaps: jest.fn(), + getZulipConfig: jest.fn(), + hasMap: jest.fn(), + hasStream: jest.fn(), + getAllMapIds: jest.fn(), + getAllStreams: jest.fn(), + reloadConfig: jest.fn(), + validateConfig: jest.fn(), } as any; // 创建模拟Redis服务,使用内存存储 @@ -135,7 +141,7 @@ describe('SessionManagerService', () => { useValue: mockRedisService, }, { - provide: ConfigManagerService, + provide: 'ZULIP_CONFIG_SERVICE', useValue: mockConfigManager, }, ], diff --git a/src/business/zulip/services/session-manager.service.ts b/src/business/zulip/services/session_manager.service.ts similarity index 95% rename from src/business/zulip/services/session-manager.service.ts rename to src/business/zulip/services/session_manager.service.ts index 3db5580..5490201 100644 --- a/src/business/zulip/services/session-manager.service.ts +++ b/src/business/zulip/services/session_manager.service.ts @@ -35,8 +35,8 @@ import { Injectable, Logger, Inject } from '@nestjs/common'; import { IRedisService } from '../../../core/redis/redis.interface'; -import { ConfigManagerService } from './config-manager.service'; -import { Internal, Constants } from '../interfaces/zulip.interfaces'; +import { IZulipConfigService } from '../../../core/zulip/interfaces/zulip-core.interfaces'; +import { Internal, Constants } from '../../../core/zulip/interfaces/zulip.interfaces'; /** * 游戏会话接口 - 重新导出以保持向后兼容 @@ -78,6 +78,29 @@ export interface SessionStats { newestSession?: Date; } +/** + * 会话管理服务类 + * + * 职责: + * - 维护WebSocket连接ID与Zulip队列ID的映射关系 + * - 管理玩家位置跟踪和上下文注入 + * - 提供空间过滤和会话查询功能 + * - 支持会话状态的序列化和反序列化 + * + * 主要方法: + * - createSession(): 创建会话并绑定Socket_ID与Zulip_Queue_ID + * - getSession(): 获取会话信息 + * - injectContext(): 上下文注入,根据位置确定Stream/Topic + * - getSocketsInMap(): 空间过滤,获取指定地图的所有Socket + * - updatePlayerPosition(): 更新玩家位置 + * - destroySession(): 销毁会话 + * + * 使用场景: + * - 玩家登录时创建会话映射 + * - 消息路由时进行上下文注入 + * - 消息分发时进行空间过滤 + * - 玩家登出时清理会话数据 + */ @Injectable() export class SessionManagerService { private readonly SESSION_PREFIX = 'zulip:session:'; @@ -91,7 +114,8 @@ export class SessionManagerService { constructor( @Inject('REDIS_SERVICE') private readonly redisService: IRedisService, - private readonly configManager: ConfigManagerService, + @Inject('ZULIP_CONFIG_SERVICE') + private readonly configManager: IZulipConfigService, ) { this.logger.log('SessionManagerService初始化完成'); } @@ -170,6 +194,9 @@ export class SessionManagerService { * @param initialMap 初始地图(可选) * @param initialPosition 初始位置(可选) * @returns Promise 创建的会话对象 + * + * @throws Error 当参数验证失败时 + * @throws Error 当Redis操作失败时 */ async createSession( socketId: string, @@ -378,6 +405,8 @@ export class SessionManagerService { * @param socketId WebSocket连接ID * @param mapId 地图ID(可选,用于覆盖当前地图) * @returns Promise 上下文信息 + * + * @throws Error 当会话不存在时 */ async injectContext(socketId: string, mapId?: string): Promise { this.logger.debug('开始上下文注入', { diff --git a/src/business/zulip/services/zulip-event-processor.service.spec.ts b/src/business/zulip/services/zulip_event_processor.service.spec.ts similarity index 97% rename from src/business/zulip/services/zulip-event-processor.service.spec.ts rename to src/business/zulip/services/zulip_event_processor.service.spec.ts index bfd3b5b..ef0cf63 100644 --- a/src/business/zulip/services/zulip-event-processor.service.spec.ts +++ b/src/business/zulip/services/zulip_event_processor.service.spec.ts @@ -24,18 +24,17 @@ import { ZulipMessage, GameMessage, MessageDistributor, -} from './zulip-event-processor.service'; -import { SessionManagerService, GameSession } from './session-manager.service'; -import { ConfigManagerService } from './config-manager.service'; -import { ZulipClientPoolService } from './zulip-client-pool.service'; +} from './zulip_event_processor.service'; +import { SessionManagerService, GameSession } from './session_manager.service'; +import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces'; import { AppLoggerService } from '../../../core/utils/logger/logger.service'; describe('ZulipEventProcessorService', () => { let service: ZulipEventProcessorService; let mockLogger: jest.Mocked; let mockSessionManager: jest.Mocked; - let mockConfigManager: jest.Mocked; - let mockClientPool: jest.Mocked; + let mockConfigManager: jest.Mocked; + let mockClientPool: jest.Mocked; let mockDistributor: jest.Mocked; // 创建模拟Zulip消息 @@ -87,14 +86,26 @@ describe('ZulipEventProcessorService', () => { mockConfigManager = { getMapIdByStream: jest.fn(), getStreamByMap: jest.fn(), - getMapConfig: jest.fn(), + getTopicByObject: jest.fn(), + getZulipConfig: jest.fn(), + hasMap: jest.fn(), + hasStream: jest.fn(), getAllMapIds: jest.fn(), + getAllStreams: jest.fn(), + reloadConfig: jest.fn(), + validateConfig: jest.fn(), } as any; mockClientPool = { getUserClient: jest.fn(), createUserClient: jest.fn(), destroyUserClient: jest.fn(), + hasUserClient: jest.fn(), + sendMessage: jest.fn(), + registerEventQueue: jest.fn(), + deregisterEventQueue: jest.fn(), + getPoolStats: jest.fn(), + cleanupIdleClients: jest.fn(), } as any; mockDistributor = { @@ -114,11 +125,11 @@ describe('ZulipEventProcessorService', () => { useValue: mockSessionManager, }, { - provide: ConfigManagerService, + provide: 'ZULIP_CONFIG_SERVICE', useValue: mockConfigManager, }, { - provide: ZulipClientPoolService, + provide: 'ZULIP_CLIENT_POOL_SERVICE', useValue: mockClientPool, }, ], diff --git a/src/business/zulip/services/zulip-event-processor.service.ts b/src/business/zulip/services/zulip_event_processor.service.ts similarity index 96% rename from src/business/zulip/services/zulip-event-processor.service.ts rename to src/business/zulip/services/zulip_event_processor.service.ts index d3f69b7..b034c33 100644 --- a/src/business/zulip/services/zulip-event-processor.service.ts +++ b/src/business/zulip/services/zulip_event_processor.service.ts @@ -31,9 +31,8 @@ */ import { Injectable, OnModuleDestroy, Inject, forwardRef, Logger } from '@nestjs/common'; -import { SessionManagerService } from './session-manager.service'; -import { ConfigManagerService } from './config-manager.service'; -import { ZulipClientPoolService } from './zulip-client-pool.service'; +import { SessionManagerService } from './session_manager.service'; +import { IZulipConfigService, IZulipClientPoolService } from '../../../core/zulip/interfaces/zulip-core.interfaces'; /** * Zulip消息接口 @@ -94,6 +93,28 @@ export interface EventProcessingStats { lastEventTime?: Date; } +/** + * Zulip事件处理服务类 + * + * 职责: + * - 处理从Zulip接收的事件队列消息 + * - 将Zulip消息转换为游戏协议格式 + * - 管理事件队列的生命周期 + * - 提供消息分发和路由功能 + * + * 主要方法: + * - processEvents(): 处理Zulip事件队列 + * - processMessage(): 处理单个消息事件 + * - startProcessing(): 启动事件处理 + * - stopProcessing(): 停止事件处理 + * - registerQueue(): 注册新的事件队列 + * + * 使用场景: + * - 接收Zulip服务器推送的消息 + * - 将Zulip消息转发给游戏客户端 + * - 管理多用户的事件队列 + * - 消息格式转换和过滤 + */ @Injectable() export class ZulipEventProcessorService implements OnModuleDestroy { private readonly logger = new Logger(ZulipEventProcessorService.name); @@ -109,9 +130,10 @@ export class ZulipEventProcessorService implements OnModuleDestroy { constructor( private readonly sessionManager: SessionManagerService, - private readonly configManager: ConfigManagerService, - @Inject(forwardRef(() => ZulipClientPoolService)) - private readonly clientPool: ZulipClientPoolService, + @Inject('ZULIP_CONFIG_SERVICE') + private readonly configManager: IZulipConfigService, + @Inject('ZULIP_CLIENT_POOL_SERVICE') + private readonly clientPool: IZulipClientPoolService, ) { this.logger.log('ZulipEventProcessorService初始化完成'); } diff --git a/src/business/zulip/zulip.module.ts b/src/business/zulip/zulip.module.ts index e587d58..13590aa 100644 --- a/src/business/zulip/zulip.module.ts +++ b/src/business/zulip/zulip.module.ts @@ -2,26 +2,32 @@ * Zulip集成业务模块 * * 功能描述: - * - 整合Zulip集成相关的控制器、服务和依赖 - * - 提供完整的Zulip集成功能模块 - * - 实现游戏与Zulip的无缝通信桥梁 - * - 支持WebSocket网关、会话管理、消息过滤等核心功能 - * - 启动时自动检查并创建所有地图对应的Zulip Streams + * - 整合Zulip集成相关的业务逻辑和控制器 + * - 提供完整的Zulip集成业务功能模块 + * - 实现游戏与Zulip的业务逻辑协调 + * - 支持WebSocket网关、会话管理、消息过滤等业务功能 * - * 核心服务: - * - ZulipService: 主协调服务,处理登录、消息发送等核心业务 + * 架构设计: + * - 业务逻辑层:处理游戏相关的业务规则和流程 + * - 核心服务层:封装技术实现细节和第三方API调用 + * - 通过依赖注入实现业务层与技术层的解耦 + * + * 业务服务: + * - ZulipService: 主协调服务,处理登录、消息发送等核心业务流程 * - ZulipWebSocketGateway: WebSocket统一网关,处理客户端连接 - * - ZulipClientPoolService: Zulip客户端池管理 - * - SessionManagerService: 会话状态管理 - * - MessageFilterService: 消息过滤和安全控制 + * - SessionManagerService: 会话状态管理和业务逻辑 + * - MessageFilterService: 消息过滤和业务规则控制 + * + * 核心服务(通过ZulipCoreModule提供): + * - ZulipClientService: Zulip REST API封装 + * - ZulipClientPoolService: 客户端池管理 * - ConfigManagerService: 配置管理和热重载 - * - StreamInitializerService: Stream初始化和自动创建 - * - ErrorHandlerService: 错误处理和服务降级 - * - MonitoringService: 系统监控和告警 - * - ApiKeySecurityService: API Key安全存储 + * - ZulipEventProcessorService: 事件处理和消息转换 + * - 其他技术支持服务 * * 依赖模块: - * - LoginModule: 用户认证和会话管理 + * - ZulipCoreModule: Zulip核心技术服务 + * - LoginCoreModule: 用户认证和会话管理 * - RedisModule: 会话状态缓存 * - LoggerModule: 日志记录服务 * @@ -29,65 +35,47 @@ * - 游戏客户端通过WebSocket连接进行实时聊天 * - 游戏内消息与Zulip社群的双向同步 * - 基于位置的聊天上下文管理 - * - 系统启动时自动初始化所有地图对应的Streams + * - 业务规则驱动的消息过滤和权限控制 * * @author angjustinl - * @version 1.0.0 - * @since 2025-12-25 + * @version 2.0.0 + * @since 2025-12-31 */ import { Module } from '@nestjs/common'; -import { ZulipWebSocketGateway } from './zulip-websocket.gateway'; +import { ZulipWebSocketGateway } from './zulip_websocket.gateway'; import { ZulipService } from './zulip.service'; -import { ZulipClientService } from './services/zulip-client.service'; -import { ZulipClientPoolService } from './services/zulip-client-pool.service'; -import { SessionManagerService } from './services/session-manager.service'; -import { SessionCleanupService } from './services/session-cleanup.service'; -import { MessageFilterService } from './services/message-filter.service'; -import { ZulipEventProcessorService } from './services/zulip-event-processor.service'; -import { ConfigManagerService } from './services/config-manager.service'; -import { ErrorHandlerService } from './services/error-handler.service'; -import { MonitoringService } from './services/monitoring.service'; -import { ApiKeySecurityService } from './services/api-key-security.service'; -import { StreamInitializerService } from './services/stream-initializer.service'; +import { SessionManagerService } from './services/session_manager.service'; +import { MessageFilterService } from './services/message_filter.service'; +import { ZulipEventProcessorService } from './services/zulip_event_processor.service'; +import { SessionCleanupService } from './services/session_cleanup.service'; +import { ZulipCoreModule } from '../../core/zulip/zulip-core.module'; import { RedisModule } from '../../core/redis/redis.module'; import { LoggerModule } from '../../core/utils/logger/logger.module'; -import { LoginModule } from '../login/login.module'; +import { LoginCoreModule } from '../../core/login_core/login_core.module'; @Module({ imports: [ + // Zulip核心服务模块 - 提供技术实现相关的核心服务 + ZulipCoreModule, // Redis模块 - 提供会话状态缓存和数据存储 RedisModule, // 日志模块 - 提供统一的日志记录服务 LoggerModule, // 登录模块 - 提供用户认证和Token验证 - LoginModule, + LoginCoreModule, ], providers: [ // 主协调服务 - 整合各子服务,提供统一业务接口 ZulipService, - // Zulip客户端服务 - 封装Zulip REST API调用 - ZulipClientService, - // Zulip客户端池服务 - 管理用户专用Zulip客户端实例 - ZulipClientPoolService, // 会话管理服务 - 维护Socket_ID与Zulip_Queue_ID的映射关系 SessionManagerService, - // 会话清理服务 - 定时清理过期会话 - SessionCleanupService, // 消息过滤服务 - 敏感词过滤、频率限制、权限验证 MessageFilterService, // Zulip事件处理服务 - 处理Zulip事件队列消息 ZulipEventProcessorService, - // 配置管理服务 - 地图映射配置和系统配置管理 - ConfigManagerService, - // Stream初始化服务 - 启动时检查并创建所有地图对应的Streams - StreamInitializerService, - // 错误处理服务 - 错误处理、重试机制、服务降级 - ErrorHandlerService, - // 监控服务 - 系统监控、健康检查、告警 - MonitoringService, - // API Key安全服务 - API Key加密存储和安全日志 - ApiKeySecurityService, + // 会话清理服务 - 定时清理过期会话 + SessionCleanupService, // WebSocket网关 - 处理游戏客户端WebSocket连接 ZulipWebSocketGateway, ], @@ -95,26 +83,14 @@ import { LoginModule } from '../login/login.module'; exports: [ // 导出主服务供其他模块使用 ZulipService, - // 导出Zulip客户端服务 - ZulipClientService, - // 导出客户端池服务 - ZulipClientPoolService, // 导出会话管理服务 SessionManagerService, - // 导出会话清理服务 - SessionCleanupService, // 导出消息过滤服务 MessageFilterService, - // 导出配置管理服务 - ConfigManagerService, - // 导出Stream初始化服务 - StreamInitializerService, - // 导出错误处理服务 - ErrorHandlerService, - // 导出监控服务 - MonitoringService, - // 导出API Key安全服务 - ApiKeySecurityService, + // 导出事件处理服务 + ZulipEventProcessorService, + // 导出会话清理服务 + SessionCleanupService, // 导出WebSocket网关 ZulipWebSocketGateway, ], diff --git a/src/business/zulip/zulip.service.spec.ts b/src/business/zulip/zulip.service.spec.ts new file mode 100644 index 0000000..aa021ce --- /dev/null +++ b/src/business/zulip/zulip.service.spec.ts @@ -0,0 +1,1134 @@ +/** + * Zulip集成主服务测试 + * + * 功能描述: + * - 测试ZulipService的核心功能 + * - 包含属性测试验证玩家登录流程完整性 + * - 包含属性测试验证消息发送流程完整性 + * - 包含属性测试验证位置更新和上下文注入 + * + * **Feature: zulip-integration, Property 1: 玩家登录流程完整性** + * **Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5** + * + * **Feature: zulip-integration, Property 3: 消息发送流程完整性** + * **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5** + * + * **Feature: zulip-integration, Property 6: 位置更新和上下文注入** + * **Validates: Requirements 4.1, 4.2, 4.3, 4.4** + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-12-31 + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import * as fc from 'fast-check'; +import { + ZulipService, + PlayerLoginRequest, + ChatMessageRequest, + PositionUpdateRequest, + LoginResponse, + ChatMessageResponse, +} from './zulip.service'; +import { SessionManagerService, GameSession } from './services/session_manager.service'; +import { MessageFilterService } from './services/message_filter.service'; +import { ZulipEventProcessorService } from './services/zulip_event_processor.service'; +import { + IZulipClientPoolService, + IZulipConfigService, + ZulipClientInstance, + SendMessageResult, +} from '../../core/zulip/interfaces/zulip-core.interfaces'; + +describe('ZulipService', () => { + let service: ZulipService; + let mockZulipClientPool: jest.Mocked; + let mockSessionManager: jest.Mocked; + let mockMessageFilter: jest.Mocked; + let mockEventProcessor: jest.Mocked; + let mockConfigManager: jest.Mocked; + + // 创建模拟的Zulip客户端实例 + const createMockClientInstance = (overrides: Partial = {}): ZulipClientInstance => ({ + userId: 'test-user-123', + config: { + username: 'test@example.com', + apiKey: 'test-api-key', + realm: 'https://zulip.example.com', + }, + client: {}, + queueId: 'queue-123', + lastEventId: 0, + createdAt: new Date(), + lastActivity: new Date(), + isValid: true, + ...overrides, + }); + + // 创建模拟的游戏会话 + const createMockSession = (overrides: Partial = {}): GameSession => ({ + socketId: 'socket-123', + userId: 'user-123', + username: 'TestPlayer', + zulipQueueId: 'queue-123', + currentMap: 'whale_port', + position: { x: 400, y: 300 }, + lastActivity: new Date(), + createdAt: new Date(), + ...overrides, + }); + + beforeEach(async () => { + jest.clearAllMocks(); + + mockZulipClientPool = { + createUserClient: jest.fn(), + getUserClient: jest.fn(), + hasUserClient: jest.fn(), + sendMessage: jest.fn(), + registerEventQueue: jest.fn(), + deregisterEventQueue: jest.fn(), + destroyUserClient: jest.fn(), + getPoolStats: jest.fn(), + cleanupIdleClients: jest.fn(), + } as any; + + mockSessionManager = { + createSession: jest.fn(), + getSession: jest.fn(), + destroySession: jest.fn(), + updatePlayerPosition: jest.fn(), + getSocketsInMap: jest.fn(), + injectContext: jest.fn(), + cleanupExpiredSessions: jest.fn(), + } as any; + + mockMessageFilter = { + validateMessage: jest.fn(), + filterContent: jest.fn(), + checkRateLimit: jest.fn(), + validatePermission: jest.fn(), + logViolation: jest.fn(), + } as any; + + mockEventProcessor = { + startEventProcessing: jest.fn(), + stopEventProcessing: jest.fn(), + registerEventQueue: jest.fn(), + unregisterEventQueue: jest.fn(), + processMessageEvent: jest.fn(), + setMessageDistributor: jest.fn(), + getProcessingStats: jest.fn(), + } as any; + + mockConfigManager = { + getStreamByMap: jest.fn(), + getMapIdByStream: jest.fn(), + getTopicByObject: jest.fn(), + getZulipConfig: jest.fn(), + hasMap: jest.fn(), + hasStream: jest.fn(), + getAllMapIds: jest.fn(), + getAllStreams: jest.fn(), + reloadConfig: jest.fn(), + validateConfig: jest.fn(), + } as any; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + ZulipService, + { + provide: 'ZULIP_CLIENT_POOL_SERVICE', + useValue: mockZulipClientPool, + }, + { + provide: SessionManagerService, + useValue: mockSessionManager, + }, + { + provide: MessageFilterService, + useValue: mockMessageFilter, + }, + { + provide: ZulipEventProcessorService, + useValue: mockEventProcessor, + }, + { + provide: 'ZULIP_CONFIG_SERVICE', + useValue: mockConfigManager, + }, + ], + }).compile(); + + service = module.get(ZulipService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('handlePlayerLogin - 处理玩家登录', () => { + it('应该成功处理有效Token的登录请求', async () => { + const loginRequest: PlayerLoginRequest = { + token: 'valid_token_123', + socketId: 'socket-456', + }; + + const mockSession = createMockSession({ + socketId: 'socket-456', + userId: 'user_valid_to', + username: 'Player_lid_to', + }); + + mockConfigManager.getZulipConfig.mockReturnValue({ + zulipServerUrl: 'https://zulip.example.com', + }); + + mockZulipClientPool.createUserClient.mockResolvedValue( + createMockClientInstance({ + userId: 'user_valid_to', + queueId: 'queue-789', + }) + ); + + mockSessionManager.createSession.mockResolvedValue(mockSession); + + const result = await service.handlePlayerLogin(loginRequest); + + expect(result.success).toBe(true); + expect(result.userId).toBe('user_valid_to'); + expect(result.username).toBe('Player_valid'); + expect(result.currentMap).toBe('whale_port'); + expect(mockSessionManager.createSession).toHaveBeenCalled(); + }); + + it('应该拒绝无效Token的登录请求', async () => { + const loginRequest: PlayerLoginRequest = { + token: 'invalid_token', + socketId: 'socket-456', + }; + + const result = await service.handlePlayerLogin(loginRequest); + + expect(result.success).toBe(false); + expect(result.error).toBe('Token验证失败'); + expect(mockSessionManager.createSession).not.toHaveBeenCalled(); + }); + + it('应该处理空Token的情况', async () => { + const loginRequest: PlayerLoginRequest = { + token: '', + socketId: 'socket-456', + }; + + const result = await service.handlePlayerLogin(loginRequest); + + expect(result.success).toBe(false); + expect(result.error).toBe('Token不能为空'); + }); + + it('应该处理空socketId的情况', async () => { + const loginRequest: PlayerLoginRequest = { + token: 'valid_token', + socketId: '', + }; + + const result = await service.handlePlayerLogin(loginRequest); + + expect(result.success).toBe(false); + expect(result.error).toBe('socketId不能为空'); + }); + it('应该在Zulip客户端创建失败时使用本地模式', async () => { + const loginRequest: PlayerLoginRequest = { + token: 'real_user_token_with_zulip_key_123', // 有API Key的Token + socketId: 'socket-456', + }; + + const mockSession = createMockSession({ + socketId: 'socket-456', + userId: 'user_real_user_', + }); + + mockConfigManager.getZulipConfig.mockReturnValue({ + zulipServerUrl: 'https://zulip.example.com', + }); + + // 模拟Zulip客户端创建失败 + mockZulipClientPool.createUserClient.mockRejectedValue(new Error('Zulip连接失败')); + mockSessionManager.createSession.mockResolvedValue(mockSession); + + const result = await service.handlePlayerLogin(loginRequest); + + // 应该成功登录(本地模式) + expect(result.success).toBe(true); + expect(mockSessionManager.createSession).toHaveBeenCalled(); + }); + }); + + describe('handlePlayerLogout - 处理玩家登出', () => { + it('应该成功处理玩家登出', async () => { + const socketId = 'socket-123'; + const mockSession = createMockSession({ socketId, userId: 'user-123' }); + + mockSessionManager.getSession.mockResolvedValue(mockSession); + mockZulipClientPool.destroyUserClient.mockResolvedValue(); + mockSessionManager.destroySession.mockResolvedValue(undefined); + + await service.handlePlayerLogout(socketId); + + expect(mockSessionManager.getSession).toHaveBeenCalledWith(socketId); + expect(mockZulipClientPool.destroyUserClient).toHaveBeenCalledWith('user-123'); + expect(mockSessionManager.destroySession).toHaveBeenCalledWith(socketId); + }); + + it('应该处理会话不存在的情况', async () => { + const socketId = 'non-existent-socket'; + + mockSessionManager.getSession.mockResolvedValue(null); + + await service.handlePlayerLogout(socketId); + + expect(mockSessionManager.getSession).toHaveBeenCalledWith(socketId); + expect(mockZulipClientPool.destroyUserClient).not.toHaveBeenCalled(); + expect(mockSessionManager.destroySession).not.toHaveBeenCalled(); + }); + + it('应该在Zulip客户端清理失败时继续执行会话清理', async () => { + const socketId = 'socket-123'; + const mockSession = createMockSession({ socketId, userId: 'user-123' }); + + mockSessionManager.getSession.mockResolvedValue(mockSession); + mockZulipClientPool.destroyUserClient.mockRejectedValue(new Error('清理失败')); + mockSessionManager.destroySession.mockResolvedValue(undefined); + + await service.handlePlayerLogout(socketId); + + expect(mockZulipClientPool.destroyUserClient).toHaveBeenCalledWith('user-123'); + expect(mockSessionManager.destroySession).toHaveBeenCalledWith(socketId); + }); + }); + + describe('sendChatMessage - 发送聊天消息', () => { + it('应该成功发送聊天消息', async () => { + const chatRequest: ChatMessageRequest = { + socketId: 'socket-123', + content: 'Hello, world!', + scope: 'local', + }; + + const mockSession = createMockSession({ + socketId: 'socket-123', + userId: 'user-123', + currentMap: 'tavern', + }); + + mockSessionManager.getSession.mockResolvedValue(mockSession); + mockSessionManager.injectContext.mockResolvedValue({ + stream: 'Tavern', + topic: 'General', + }); + + mockMessageFilter.validateMessage.mockResolvedValue({ + allowed: true, + filteredContent: 'Hello, world!', + }); + + mockZulipClientPool.sendMessage.mockResolvedValue({ + success: true, + messageId: 12345, + }); + + const result = await service.sendChatMessage(chatRequest); + + expect(result.success).toBe(true); + expect(result.messageId).toBe(12345); + expect(mockZulipClientPool.sendMessage).toHaveBeenCalledWith( + 'user-123', + 'Tavern', + 'General', + 'Hello, world!' + ); + }); + + it('应该拒绝会话不存在的消息发送', async () => { + const chatRequest: ChatMessageRequest = { + socketId: 'non-existent-socket', + content: 'Hello, world!', + scope: 'local', + }; + + mockSessionManager.getSession.mockResolvedValue(null); + + const result = await service.sendChatMessage(chatRequest); + + expect(result.success).toBe(false); + expect(result.error).toBe('会话不存在,请重新登录'); + expect(mockZulipClientPool.sendMessage).not.toHaveBeenCalled(); + }); + + it('应该拒绝未通过验证的消息', async () => { + const chatRequest: ChatMessageRequest = { + socketId: 'socket-123', + content: '敏感词内容', + scope: 'local', + }; + + const mockSession = createMockSession({ socketId: 'socket-123' }); + + mockSessionManager.getSession.mockResolvedValue(mockSession); + mockSessionManager.injectContext.mockResolvedValue({ + stream: 'Tavern', + topic: 'General', + }); + + mockMessageFilter.validateMessage.mockResolvedValue({ + allowed: false, + reason: '消息包含敏感词', + }); + + const result = await service.sendChatMessage(chatRequest); + + expect(result.success).toBe(false); + expect(result.error).toBe('消息包含敏感词'); + expect(mockZulipClientPool.sendMessage).not.toHaveBeenCalled(); + }); + + it('应该在Zulip发送失败时仍返回成功(本地模式)', async () => { + const chatRequest: ChatMessageRequest = { + socketId: 'socket-123', + content: 'Hello, world!', + scope: 'local', + }; + + const mockSession = createMockSession({ socketId: 'socket-123' }); + + mockSessionManager.getSession.mockResolvedValue(mockSession); + mockSessionManager.injectContext.mockResolvedValue({ + stream: 'Tavern', + topic: 'General', + }); + + mockMessageFilter.validateMessage.mockResolvedValue({ + allowed: true, + filteredContent: 'Hello, world!', + }); + + mockZulipClientPool.sendMessage.mockResolvedValue({ + success: false, + error: 'Zulip服务不可用', + }); + + const result = await service.sendChatMessage(chatRequest); + + expect(result.success).toBe(true); // 本地模式下仍返回成功 + }); + }); + + describe('updatePlayerPosition - 更新玩家位置', () => { + it('应该成功更新玩家位置', async () => { + const positionRequest: PositionUpdateRequest = { + socketId: 'socket-123', + x: 500, + y: 400, + mapId: 'tavern', + }; + + mockSessionManager.updatePlayerPosition.mockResolvedValue(true); + + const result = await service.updatePlayerPosition(positionRequest); + + expect(result).toBe(true); + expect(mockSessionManager.updatePlayerPosition).toHaveBeenCalledWith( + 'socket-123', + 'tavern', + 500, + 400 + ); + }); + + it('应该拒绝空socketId的位置更新', async () => { + const positionRequest: PositionUpdateRequest = { + socketId: '', + x: 500, + y: 400, + mapId: 'tavern', + }; + + const result = await service.updatePlayerPosition(positionRequest); + + expect(result).toBe(false); + expect(mockSessionManager.updatePlayerPosition).not.toHaveBeenCalled(); + }); + + it('应该拒绝空mapId的位置更新', async () => { + const positionRequest: PositionUpdateRequest = { + socketId: 'socket-123', + x: 500, + y: 400, + mapId: '', + }; + + const result = await service.updatePlayerPosition(positionRequest); + + expect(result).toBe(false); + expect(mockSessionManager.updatePlayerPosition).not.toHaveBeenCalled(); + }); + }); + /** + * 属性测试: 玩家登录流程完整性 + * + * **Feature: zulip-integration, Property 1: 玩家登录流程完整性** + * **Validates: Requirements 1.1, 1.2, 1.3, 1.4, 1.5** + * + * 对于任何有效的游戏Token,系统应该能够验证Token,创建Zulip客户端, + * 建立会话映射,并返回成功的登录响应 + */ + describe('Property 1: 玩家登录流程完整性', () => { + /** + * 属性: 对于任何有效的Token和socketId,登录应该成功并创建会话 + * 验证需求 1.1: 游戏客户端连接时系统应验证游戏Token的有效性 + * 验证需求 1.2: Token验证成功后系统应获取用户的Zulip API Key + * 验证需求 1.3: 获取API Key后系统应创建用户专用的Zulip客户端实例 + */ + it('对于任何有效的Token和socketId,登录应该成功并创建会话', async () => { + await fc.assert( + fc.asyncProperty( + // 生成有效的Token(不以'invalid'开头) + fc.string({ minLength: 8, maxLength: 50 }) + .filter(s => !s.startsWith('invalid') && s.trim().length > 0), + // 生成有效的socketId + fc.string({ minLength: 5, maxLength: 30 }) + .filter(s => s.trim().length > 0), + async (token, socketId) => { + const trimmedToken = token.trim(); + const trimmedSocketId = socketId.trim(); + + const loginRequest: PlayerLoginRequest = { + token: trimmedToken, + socketId: trimmedSocketId, + }; + + const expectedUserId = `user_${trimmedToken.substring(0, 8)}`; + const expectedUsername = `Player_${expectedUserId.substring(5, 10)}`; + + const mockSession = createMockSession({ + socketId: trimmedSocketId, + userId: expectedUserId, + username: expectedUsername, + }); + + mockConfigManager.getZulipConfig.mockReturnValue({ + zulipServerUrl: 'https://zulip.example.com', + }); + + mockSessionManager.createSession.mockResolvedValue(mockSession); + + const result = await service.handlePlayerLogin(loginRequest); + + // 验证登录成功 + expect(result.success).toBe(true); + expect(result.userId).toBe(expectedUserId); + expect(result.username).toBe(expectedUsername); + expect(result.currentMap).toBe('whale_port'); + expect(result.sessionId).toBeDefined(); + + // 验证会话创建被调用 + expect(mockSessionManager.createSession).toHaveBeenCalledWith( + trimmedSocketId, + expectedUserId, + expect.any(String), // zulipQueueId + expectedUsername, + 'whale_port', + { x: 400, y: 300 } + ); + } + ), + { numRuns: 100 } + ); + }, 60000); + + /** + * 属性: 对于任何无效的Token,登录应该失败 + * 验证需求 1.1: 游戏客户端连接时系统应验证游戏Token的有效性 + */ + it('对于任何无效的Token,登录应该失败', async () => { + await fc.assert( + fc.asyncProperty( + // 生成无效的Token(以'invalid'开头) + fc.string({ minLength: 1, maxLength: 30 }) + .map(s => `invalid${s}`), + // 生成有效的socketId + fc.string({ minLength: 5, maxLength: 30 }) + .filter(s => s.trim().length > 0), + async (invalidToken, socketId) => { + const loginRequest: PlayerLoginRequest = { + token: invalidToken, + socketId: socketId.trim(), + }; + + const result = await service.handlePlayerLogin(loginRequest); + + // 验证登录失败 + expect(result.success).toBe(false); + expect(result.error).toBe('Token验证失败'); + expect(result.userId).toBeUndefined(); + expect(result.sessionId).toBeUndefined(); + + // 验证没有创建会话 + expect(mockSessionManager.createSession).not.toHaveBeenCalled(); + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 对于空或无效的参数,登录应该返回相应的错误信息 + * 验证需求 1.1: 系统应正确处理无效的登录请求 + */ + it('对于空或无效的参数,登录应该返回相应的错误信息', async () => { + await fc.assert( + fc.asyncProperty( + // 生成可能为空或以'invalid'开头的Token + fc.oneof( + fc.constant(''), // 空字符串 + fc.constant(' '), // 只有空格 + fc.string({ minLength: 1, maxLength: 50 }).map(s => 'invalid' + s), // 以invalid开头 + ), + // 生成可能为空的socketId + fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: '' }), + async (token, socketId) => { + // 重置mock调用历史 + jest.clearAllMocks(); + + const loginRequest: PlayerLoginRequest = { + token: token || '', + socketId: socketId || '', + }; + + const result = await service.handlePlayerLogin(loginRequest); + + // 验证登录失败 + expect(result.success).toBe(false); + expect(result.error).toBeDefined(); + + if (!token || token.trim().length === 0) { + expect(result.error).toBe('Token不能为空'); + } else if (!socketId || socketId.trim().length === 0) { + expect(result.error).toBe('socketId不能为空'); + } else if (token.startsWith('invalid')) { + expect(result.error).toBe('Token验证失败'); + } + + // 验证没有创建会话 + expect(mockSessionManager.createSession).not.toHaveBeenCalled(); + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 对于有Zulip API Key的用户,应该尝试创建Zulip客户端 + * 验证需求 1.2: Token验证成功后系统应获取用户的Zulip API Key + * 验证需求 1.3: 获取API Key后系统应创建用户专用的Zulip客户端实例 + */ + it('对于有Zulip API Key的用户,应该尝试创建Zulip客户端', async () => { + await fc.assert( + fc.asyncProperty( + // 生成包含特定标识的Token(表示有API Key) + fc.constantFrom( + 'real_user_token_with_zulip_key_123', + 'token_with_lCPWCPf_key', + 'token_with_W2KhXaQx_key' + ), + // 生成有效的socketId + fc.string({ minLength: 5, maxLength: 30 }) + .filter(s => s.trim().length > 0), + async (tokenWithApiKey, socketId) => { + const loginRequest: PlayerLoginRequest = { + token: tokenWithApiKey, + socketId: socketId.trim(), + }; + + const mockClientInstance = createMockClientInstance({ + userId: `user_${tokenWithApiKey.substring(0, 8)}`, + queueId: 'test-queue-123', + }); + + const mockSession = createMockSession({ + socketId: socketId.trim(), + zulipQueueId: 'test-queue-123', + }); + + mockConfigManager.getZulipConfig.mockReturnValue({ + zulipServerUrl: 'https://zulip.example.com', + }); + + mockZulipClientPool.createUserClient.mockResolvedValue(mockClientInstance); + mockSessionManager.createSession.mockResolvedValue(mockSession); + + const result = await service.handlePlayerLogin(loginRequest); + + // 验证登录成功 + expect(result.success).toBe(true); + + // 验证尝试创建了Zulip客户端 + expect(mockZulipClientPool.createUserClient).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + username: expect.any(String), + apiKey: 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8', + realm: 'https://zulip.example.com', + }) + ); + } + ), + { numRuns: 30 } + ); + }, 30000); + }); + /** + * 属性测试: 消息发送流程完整性 + * + * **Feature: zulip-integration, Property 3: 消息发送流程完整性** + * **Validates: Requirements 3.1, 3.2, 3.3, 3.4, 3.5** + * + * 对于任何有效的聊天消息请求,系统应该进行内容过滤、权限验证、 + * 上下文注入,并成功发送到对应的Zulip Stream/Topic + */ + describe('Property 3: 消息发送流程完整性', () => { + /** + * 属性: 对于任何有效会话的消息发送请求,应该成功处理并发送 + * 验证需求 3.1: 游戏客户端发送聊天消息时系统应获取玩家当前位置 + * 验证需求 3.2: 获取位置后系统应根据位置确定目标Stream和Topic + * 验证需求 3.3: 确定目标后系统应进行消息内容过滤和频率检查 + */ + it('对于任何有效会话的消息发送请求,应该成功处理并发送', async () => { + await fc.assert( + fc.asyncProperty( + // 生成有效的socketId + fc.string({ minLength: 5, maxLength: 30 }) + .filter(s => s.trim().length > 0), + // 生成有效的消息内容 + fc.string({ minLength: 1, maxLength: 200 }) + .filter(s => s.trim().length > 0 && !/[敏感词|违禁词]/.test(s)), + // 生成地图和Stream映射 + fc.record({ + mapId: fc.constantFrom('tavern', 'novice_village', 'market'), + streamName: fc.constantFrom('Tavern', 'Novice Village', 'Market'), + }), + async (socketId, content, mapping) => { + const chatRequest: ChatMessageRequest = { + socketId: socketId.trim(), + content: content.trim(), + scope: 'local', + }; + + const mockSession = createMockSession({ + socketId: socketId.trim(), + userId: `user_${socketId.substring(0, 8)}`, + currentMap: mapping.mapId, + }); + + mockSessionManager.getSession.mockResolvedValue(mockSession); + mockSessionManager.injectContext.mockResolvedValue({ + stream: mapping.streamName, + topic: 'General', + }); + + mockMessageFilter.validateMessage.mockResolvedValue({ + allowed: true, + filteredContent: content.trim(), + }); + + mockZulipClientPool.sendMessage.mockResolvedValue({ + success: true, + messageId: Math.floor(Math.random() * 1000000), + }); + + const result = await service.sendChatMessage(chatRequest); + + // 验证消息发送成功 + expect(result.success).toBe(true); + expect(result.messageId).toBeDefined(); + + // 验证调用了正确的方法 + expect(mockSessionManager.getSession).toHaveBeenCalledWith(socketId.trim()); + expect(mockSessionManager.injectContext).toHaveBeenCalledWith(socketId.trim()); + expect(mockMessageFilter.validateMessage).toHaveBeenCalledWith( + mockSession.userId, + content.trim(), + mapping.streamName, + mapping.mapId + ); + expect(mockZulipClientPool.sendMessage).toHaveBeenCalledWith( + mockSession.userId, + mapping.streamName, + 'General', + content.trim() + ); + } + ), + { numRuns: 100 } + ); + }, 60000); + + /** + * 属性: 对于任何不存在的会话,消息发送应该失败 + * 验证需求 3.1: 系统应验证会话的有效性 + */ + it('对于任何不存在的会话,消息发送应该失败', async () => { + await fc.assert( + fc.asyncProperty( + // 生成不存在的socketId + fc.string({ minLength: 5, maxLength: 30 }) + .filter(s => s.trim().length > 0) + .map(s => `nonexistent_${s}`), + // 生成任意消息内容 + fc.string({ minLength: 1, maxLength: 200 }) + .filter(s => s.trim().length > 0), + async (nonExistentSocketId, content) => { + const chatRequest: ChatMessageRequest = { + socketId: nonExistentSocketId, + content: content.trim(), + scope: 'local', + }; + + mockSessionManager.getSession.mockResolvedValue(null); + + const result = await service.sendChatMessage(chatRequest); + + // 验证消息发送失败 + expect(result.success).toBe(false); + expect(result.error).toBe('会话不存在,请重新登录'); + + // 验证没有进行后续处理 + expect(mockMessageFilter.validateMessage).not.toHaveBeenCalled(); + expect(mockZulipClientPool.sendMessage).not.toHaveBeenCalled(); + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 对于任何未通过验证的消息,发送应该失败 + * 验证需求 3.3: 确定目标后系统应进行消息内容过滤和频率检查 + */ + it('对于任何未通过验证的消息,发送应该失败', async () => { + await fc.assert( + fc.asyncProperty( + // 生成有效的socketId + fc.string({ minLength: 5, maxLength: 30 }) + .filter(s => s.trim().length > 0), + // 生成可能包含敏感词的消息内容 + fc.string({ minLength: 1, maxLength: 200 }) + .filter(s => s.trim().length > 0), + // 生成验证失败的原因 + fc.constantFrom( + '消息包含敏感词', + '发送频率过快', + '权限不足', + '消息长度超限' + ), + async (socketId, content, failureReason) => { + const chatRequest: ChatMessageRequest = { + socketId: socketId.trim(), + content: content.trim(), + scope: 'local', + }; + + const mockSession = createMockSession({ + socketId: socketId.trim(), + }); + + mockSessionManager.getSession.mockResolvedValue(mockSession); + mockSessionManager.injectContext.mockResolvedValue({ + stream: 'Tavern', + topic: 'General', + }); + + mockMessageFilter.validateMessage.mockResolvedValue({ + allowed: false, + reason: failureReason, + }); + + const result = await service.sendChatMessage(chatRequest); + + // 验证消息发送失败 + expect(result.success).toBe(false); + expect(result.error).toBe(failureReason); + + // 验证没有发送到Zulip + expect(mockZulipClientPool.sendMessage).not.toHaveBeenCalled(); + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 即使Zulip发送失败,系统也应该返回成功(本地模式) + * 验证需求 3.5: 发送消息到Zulip时系统应处理发送失败的情况 + */ + it('即使Zulip发送失败,系统也应该返回成功(本地模式)', async () => { + await fc.assert( + fc.asyncProperty( + // 生成有效的socketId + fc.string({ minLength: 5, maxLength: 30 }) + .filter(s => s.trim().length > 0), + // 生成有效的消息内容 + fc.string({ minLength: 1, maxLength: 200 }) + .filter(s => s.trim().length > 0), + // 生成Zulip错误信息 + fc.constantFrom( + 'Zulip服务不可用', + '网络连接超时', + 'API Key无效', + 'Stream不存在' + ), + async (socketId, content, zulipError) => { + const chatRequest: ChatMessageRequest = { + socketId: socketId.trim(), + content: content.trim(), + scope: 'local', + }; + + const mockSession = createMockSession({ + socketId: socketId.trim(), + }); + + mockSessionManager.getSession.mockResolvedValue(mockSession); + mockSessionManager.injectContext.mockResolvedValue({ + stream: 'Tavern', + topic: 'General', + }); + + mockMessageFilter.validateMessage.mockResolvedValue({ + allowed: true, + filteredContent: content.trim(), + }); + + mockZulipClientPool.sendMessage.mockResolvedValue({ + success: false, + error: zulipError, + }); + + const result = await service.sendChatMessage(chatRequest); + + // 验证本地模式下仍返回成功 + expect(result.success).toBe(true); + expect(result.messageId).toBeUndefined(); + } + ), + { numRuns: 50 } + ); + }, 30000); + }); + /** + * 属性测试: 位置更新和上下文注入 + * + * **Feature: zulip-integration, Property 6: 位置更新和上下文注入** + * **Validates: Requirements 4.1, 4.2, 4.3, 4.4** + * + * 对于任何位置更新请求,系统应该正确更新玩家位置信息, + * 并在消息发送时根据位置进行上下文注入 + */ + describe('Property 6: 位置更新和上下文注入', () => { + /** + * 属性: 对于任何有效的位置更新请求,应该成功更新位置 + * 验证需求 4.1: 玩家移动时系统应更新玩家在游戏世界中的位置信息 + * 验证需求 4.2: 更新位置时系统应验证位置的有效性 + */ + it('对于任何有效的位置更新请求,应该成功更新位置', async () => { + await fc.assert( + fc.asyncProperty( + // 生成有效的socketId + fc.string({ minLength: 5, maxLength: 30 }) + .filter(s => s.trim().length > 0), + // 生成有效的坐标 + fc.record({ + x: fc.integer({ min: 0, max: 2000 }), + y: fc.integer({ min: 0, max: 2000 }), + }), + // 生成有效的地图ID + fc.constantFrom('tavern', 'novice_village', 'market', 'whale_port'), + async (socketId, position, mapId) => { + const positionRequest: PositionUpdateRequest = { + socketId: socketId.trim(), + x: position.x, + y: position.y, + mapId, + }; + + mockSessionManager.updatePlayerPosition.mockResolvedValue(true); + + const result = await service.updatePlayerPosition(positionRequest); + + // 验证位置更新成功 + expect(result).toBe(true); + + // 验证调用了正确的方法 + expect(mockSessionManager.updatePlayerPosition).toHaveBeenCalledWith( + socketId.trim(), + mapId, + position.x, + position.y + ); + } + ), + { numRuns: 100 } + ); + }, 60000); + + /** + * 属性: 对于任何无效的参数,位置更新应该失败 + * 验证需求 4.2: 更新位置时系统应验证位置的有效性 + */ + it('对于任何无效的参数,位置更新应该失败', async () => { + await fc.assert( + fc.asyncProperty( + // 生成可能为空的socketId + fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: '' }), + // 生成可能为空的mapId + fc.option(fc.constantFrom('tavern', 'market'), { nil: '' }), + // 生成坐标 + fc.record({ + x: fc.integer({ min: 0, max: 2000 }), + y: fc.integer({ min: 0, max: 2000 }), + }), + async (socketId, mapId, position) => { + // 重置mock调用历史 + jest.clearAllMocks(); + + const positionRequest: PositionUpdateRequest = { + socketId: socketId || '', + x: position.x, + y: position.y, + mapId: mapId || '', + }; + + const result = await service.updatePlayerPosition(positionRequest); + + if (!socketId || socketId.trim().length === 0 || + !mapId || mapId.trim().length === 0) { + // 验证位置更新失败 + expect(result).toBe(false); + + // 验证没有调用SessionManager + expect(mockSessionManager.updatePlayerPosition).not.toHaveBeenCalled(); + } + } + ), + { numRuns: 50 } + ); + }, 30000); + + /** + * 属性: 位置更新失败时应该正确处理错误 + * 验证需求 4.1: 系统应正确处理位置更新过程中的错误 + */ + it('位置更新失败时应该正确处理错误', async () => { + await fc.assert( + fc.asyncProperty( + // 生成有效的socketId + fc.string({ minLength: 5, maxLength: 30 }) + .filter(s => s.trim().length > 0), + // 生成有效的坐标 + fc.record({ + x: fc.integer({ min: 0, max: 2000 }), + y: fc.integer({ min: 0, max: 2000 }), + }), + // 生成有效的地图ID + fc.constantFrom('tavern', 'novice_village', 'market'), + async (socketId, position, mapId) => { + const positionRequest: PositionUpdateRequest = { + socketId: socketId.trim(), + x: position.x, + y: position.y, + mapId, + }; + + // 模拟SessionManager抛出错误 + mockSessionManager.updatePlayerPosition.mockRejectedValue( + new Error('位置更新失败') + ); + + const result = await service.updatePlayerPosition(positionRequest); + + // 验证位置更新失败 + expect(result).toBe(false); + } + ), + { numRuns: 50 } + ); + }, 30000); + }); + + describe('processZulipMessage - 处理Zulip消息', () => { + it('应该正确处理Zulip消息并确定目标玩家', async () => { + const zulipMessage = { + id: 12345, + sender_full_name: 'Alice', + sender_email: 'alice@example.com', + content: 'Hello everyone!', + display_recipient: 'Tavern', + stream_name: 'Tavern', + }; + + mockConfigManager.getMapIdByStream.mockReturnValue('tavern'); + mockSessionManager.getSocketsInMap.mockResolvedValue(['socket-1', 'socket-2']); + + const result = await service.processZulipMessage(zulipMessage); + + expect(result.targetSockets).toEqual(['socket-1', 'socket-2']); + expect(result.message.t).toBe('chat_render'); + expect(result.message.from).toBe('Alice'); + expect(result.message.txt).toBe('Hello everyone!'); + expect(result.message.bubble).toBe(true); + }); + + it('应该在未知Stream时返回空的目标列表', async () => { + const zulipMessage = { + id: 12345, + sender_full_name: 'Alice', + content: 'Hello!', + display_recipient: 'UnknownStream', + }; + + mockConfigManager.getMapIdByStream.mockReturnValue(null); + + const result = await service.processZulipMessage(zulipMessage); + + expect(result.targetSockets).toEqual([]); + }); + }); + + describe('辅助方法', () => { + it('getSession - 应该返回会话信息', async () => { + const socketId = 'socket-123'; + const mockSession = createMockSession({ socketId }); + + mockSessionManager.getSession.mockResolvedValue(mockSession); + + const result = await service.getSession(socketId); + + expect(result).toBe(mockSession); + expect(mockSessionManager.getSession).toHaveBeenCalledWith(socketId); + }); + + it('getSocketsInMap - 应该返回地图中的Socket列表', async () => { + const mapId = 'tavern'; + const socketIds = ['socket-1', 'socket-2', 'socket-3']; + + mockSessionManager.getSocketsInMap.mockResolvedValue(socketIds); + + const result = await service.getSocketsInMap(mapId); + + expect(result).toBe(socketIds); + expect(mockSessionManager.getSocketsInMap).toHaveBeenCalledWith(mapId); + }); + }); +}); \ No newline at end of file diff --git a/src/business/zulip/zulip.service.ts b/src/business/zulip/zulip.service.ts index 0390857..32faf11 100644 --- a/src/business/zulip/zulip.service.ts +++ b/src/business/zulip/zulip.service.ts @@ -22,14 +22,15 @@ * @since 2025-12-25 */ -import { Injectable, Logger } from '@nestjs/common'; +import { Injectable, Logger, Inject } from '@nestjs/common'; import { randomUUID } from 'crypto'; -import { ZulipClientPoolService } from './services/zulip-client-pool.service'; -import { SessionManagerService } from './services/session-manager.service'; -import { MessageFilterService } from './services/message-filter.service'; -import { ZulipEventProcessorService } from './services/zulip-event-processor.service'; -import { ConfigManagerService } from './services/config-manager.service'; -import { ErrorHandlerService } from './services/error-handler.service'; +import { SessionManagerService } from './services/session_manager.service'; +import { MessageFilterService } from './services/message_filter.service'; +import { ZulipEventProcessorService } from './services/zulip_event_processor.service'; +import { + IZulipClientPoolService, + IZulipConfigService, +} from '../../core/zulip/interfaces/zulip-core.interfaces'; /** * 玩家登录请求接口 @@ -79,18 +80,40 @@ export interface ChatMessageResponse { error?: string; } +/** + * Zulip集成主服务类 + * + * 职责: + * - 作为Zulip集成系统的主要协调服务 + * - 整合各个子服务,提供统一的业务接口 + * - 处理游戏客户端与Zulip之间的核心业务逻辑 + * - 管理玩家会话和消息路由 + * + * 主要方法: + * - handlePlayerLogin(): 处理玩家登录和Zulip客户端初始化 + * - handlePlayerLogout(): 处理玩家登出和资源清理 + * - sendChatMessage(): 处理游戏聊天消息发送到Zulip + * - updatePlayerPosition(): 更新玩家位置信息 + * + * 使用场景: + * - WebSocket网关调用处理消息路由 + * - 会话管理和状态维护 + * - 消息格式转换和过滤 + * - 游戏与Zulip的双向通信桥梁 + */ @Injectable() export class ZulipService { private readonly logger = new Logger(ZulipService.name); private readonly DEFAULT_MAP = 'whale_port'; constructor( - private readonly zulipClientPool: ZulipClientPoolService, + @Inject('ZULIP_CLIENT_POOL_SERVICE') + private readonly zulipClientPool: IZulipClientPoolService, private readonly sessionManager: SessionManagerService, private readonly messageFilter: MessageFilterService, private readonly eventProcessor: ZulipEventProcessorService, - private readonly configManager: ConfigManagerService, - private readonly errorHandler: ErrorHandlerService, + @Inject('ZULIP_CONFIG_SERVICE') + private readonly configManager: IZulipConfigService, ) { this.logger.log('ZulipService初始化完成'); } diff --git a/src/business/zulip/zulip-integration.e2e.spec.ts b/src/business/zulip/zulip_integration.e2e.spec.ts similarity index 99% rename from src/business/zulip/zulip-integration.e2e.spec.ts rename to src/business/zulip/zulip_integration.e2e.spec.ts index 084b348..b71da8f 100644 --- a/src/business/zulip/zulip-integration.e2e.spec.ts +++ b/src/business/zulip/zulip_integration.e2e.spec.ts @@ -56,7 +56,7 @@ describeE2E('Zulip Integration E2E Tests', () => { }); client.on('connect', () => resolve(client)); - client.on('connect_error', (err) => reject(err)); + client.on('connect_error', (err: any) => reject(err)); setTimeout(() => reject(new Error('Connection timeout')), 5000); }); diff --git a/src/business/zulip/zulip-websocket.gateway.spec.ts b/src/business/zulip/zulip_websocket.gateway.spec.ts similarity index 99% rename from src/business/zulip/zulip-websocket.gateway.spec.ts rename to src/business/zulip/zulip_websocket.gateway.spec.ts index 4f6a6e5..c1fc883 100644 --- a/src/business/zulip/zulip-websocket.gateway.spec.ts +++ b/src/business/zulip/zulip_websocket.gateway.spec.ts @@ -16,9 +16,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Logger } from '@nestjs/common'; import * as fc from 'fast-check'; -import { ZulipWebSocketGateway } from './zulip-websocket.gateway'; +import { ZulipWebSocketGateway } from './zulip_websocket.gateway'; import { ZulipService, LoginResponse, ChatMessageResponse } from './zulip.service'; -import { SessionManagerService, GameSession } from './services/session-manager.service'; +import { SessionManagerService, GameSession } from './services/session_manager.service'; import { Server, Socket } from 'socket.io'; describe('ZulipWebSocketGateway', () => { diff --git a/src/business/zulip/zulip-websocket.gateway.ts b/src/business/zulip/zulip_websocket.gateway.ts similarity index 95% rename from src/business/zulip/zulip-websocket.gateway.ts rename to src/business/zulip/zulip_websocket.gateway.ts index d90dba1..50e0598 100644 --- a/src/business/zulip/zulip-websocket.gateway.ts +++ b/src/business/zulip/zulip_websocket.gateway.ts @@ -35,7 +35,7 @@ import { import { Server, Socket } from 'socket.io'; import { Injectable, Logger } from '@nestjs/common'; import { ZulipService } from './zulip.service'; -import { SessionManagerService } from './services/session-manager.service'; +import { SessionManagerService } from './services/session_manager.service'; /** * 登录消息接口 - 按guide.md格式 @@ -96,6 +96,29 @@ interface ClientData { connectedAt: Date; } +/** + * Zulip WebSocket网关类 + * + * 职责: + * - 处理所有Godot游戏客户端的WebSocket连接 + * - 实现游戏协议到Zulip协议的转换 + * - 提供统一的消息路由和权限控制 + * - 管理客户端连接状态和会话 + * + * 主要方法: + * - handleConnection(): 处理客户端连接建立 + * - handleDisconnect(): 处理客户端连接断开 + * - handleLogin(): 处理登录消息 + * - handleChat(): 处理聊天消息 + * - handlePositionUpdate(): 处理位置更新 + * - sendChatRender(): 向客户端发送聊天渲染消息 + * + * 使用场景: + * - 游戏客户端WebSocket通信的统一入口 + * - 消息协议转换和路由分发 + * - 连接状态管理和权限验证 + * - 实时消息推送和广播 + */ @Injectable() @WebSocketGateway({ cors: { origin: '*' }, diff --git a/src/business/zulip/config/index.ts b/src/core/zulip/config/index.ts similarity index 100% rename from src/business/zulip/config/index.ts rename to src/core/zulip/config/index.ts diff --git a/src/business/zulip/config/zulip.config.ts b/src/core/zulip/config/zulip.config.ts similarity index 100% rename from src/business/zulip/config/zulip.config.ts rename to src/core/zulip/config/zulip.config.ts diff --git a/src/core/zulip/index.ts b/src/core/zulip/index.ts new file mode 100644 index 0000000..5583b5d --- /dev/null +++ b/src/core/zulip/index.ts @@ -0,0 +1,26 @@ +/** + * Zulip核心服务模块导出 + * + * 功能描述: + * - 统一导出Zulip核心服务的接口和类型 + * - 为业务层提供清晰的导入路径 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-12-31 + */ + +// 导出核心服务接口 +export * from './interfaces/zulip-core.interfaces'; + +// 导出核心服务模块 +export { ZulipCoreModule } from './zulip-core.module'; + +// 导出具体实现类(供内部使用) +export { ZulipClientService } from './services/zulip_client.service'; +export { ZulipClientPoolService } from './services/zulip_client_pool.service'; +export { ConfigManagerService } from './services/config_manager.service'; +export { ApiKeySecurityService } from './services/api_key_security.service'; +export { ErrorHandlerService } from './services/error_handler.service'; +export { MonitoringService } from './services/monitoring.service'; +export { StreamInitializerService } from './services/stream_initializer.service'; \ No newline at end of file diff --git a/src/core/zulip/interfaces/zulip-core.interfaces.ts b/src/core/zulip/interfaces/zulip-core.interfaces.ts new file mode 100644 index 0000000..db8d38a --- /dev/null +++ b/src/core/zulip/interfaces/zulip-core.interfaces.ts @@ -0,0 +1,294 @@ +/** + * Zulip核心服务接口定义 + * + * 功能描述: + * - 定义Zulip核心服务的抽象接口 + * - 分离业务逻辑与技术实现 + * - 支持依赖注入和接口切换 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-12-31 + */ + +/** + * Zulip客户端配置接口 + */ +export interface ZulipClientConfig { + username: string; + apiKey: string; + realm: string; +} + +/** + * Zulip客户端实例接口 + */ +export interface ZulipClientInstance { + userId: string; + config: ZulipClientConfig; + client: any; + queueId?: string; + lastEventId: number; + createdAt: Date; + lastActivity: Date; + isValid: boolean; +} + +/** + * 发送消息结果接口 + */ +export interface SendMessageResult { + success: boolean; + messageId?: number; + error?: string; +} + +/** + * 事件队列注册结果接口 + */ +export interface RegisterQueueResult { + success: boolean; + queueId?: string; + lastEventId?: number; + error?: string; +} + +/** + * 获取事件结果接口 + */ +export interface GetEventsResult { + success: boolean; + events?: any[]; + error?: string; +} + +/** + * 客户端池统计信息接口 + */ +export interface PoolStats { + totalClients: number; + activeClients: number; + clientsWithQueues: number; + clientIds: string[]; +} + +/** + * Zulip客户端核心服务接口 + * + * 职责: + * - 封装Zulip REST API调用 + * - 处理API Key验证和错误处理 + * - 提供消息发送、事件队列管理等核心功能 + */ +export interface IZulipClientService { + /** + * 创建并初始化Zulip客户端 + */ + createClient(userId: string, config: ZulipClientConfig): Promise; + + /** + * 验证API Key有效性 + */ + validateApiKey(clientInstance: ZulipClientInstance): Promise; + + /** + * 发送消息到指定Stream/Topic + */ + sendMessage( + clientInstance: ZulipClientInstance, + stream: string, + topic: string, + content: string, + ): Promise; + + /** + * 注册事件队列 + */ + registerQueue( + clientInstance: ZulipClientInstance, + eventTypes?: string[], + ): Promise; + + /** + * 注销事件队列 + */ + deregisterQueue(clientInstance: ZulipClientInstance): Promise; + + /** + * 获取事件队列中的事件 + */ + getEvents( + clientInstance: ZulipClientInstance, + dontBlock?: boolean, + ): Promise; + + /** + * 销毁客户端实例 + */ + destroyClient(clientInstance: ZulipClientInstance): Promise; +} + +/** + * Zulip客户端池服务接口 + * + * 职责: + * - 管理用户专用的Zulip客户端实例 + * - 维护客户端连接池和生命周期 + * - 处理客户端的创建、销毁和状态管理 + */ +export interface IZulipClientPoolService { + /** + * 为用户创建专用Zulip客户端 + */ + createUserClient(userId: string, config: ZulipClientConfig): Promise; + + /** + * 获取用户的Zulip客户端 + */ + getUserClient(userId: string): Promise; + + /** + * 检查用户客户端是否存在 + */ + hasUserClient(userId: string): boolean; + + /** + * 发送消息到指定Stream/Topic + */ + sendMessage( + userId: string, + stream: string, + topic: string, + content: string, + ): Promise; + + /** + * 注册事件队列 + */ + registerEventQueue(userId: string): Promise; + + /** + * 注销事件队列 + */ + deregisterEventQueue(userId: string): Promise; + + /** + * 销毁用户客户端 + */ + destroyUserClient(userId: string): Promise; + + /** + * 获取客户端池统计信息 + */ + getPoolStats(): PoolStats; + + /** + * 清理过期客户端 + */ + cleanupIdleClients(maxIdleMinutes?: number): Promise; +} + +/** + * Zulip配置管理服务接口 + * + * 职责: + * - 管理地图到Zulip Stream的映射配置 + * - 提供Zulip服务器连接配置 + * - 支持配置文件的热重载 + */ +export interface IZulipConfigService { + /** + * 根据地图获取对应的Stream + */ + getStreamByMap(mapId: string): string | null; + + /** + * 根据Stream名称获取地图ID + */ + getMapIdByStream(streamName: string): string | null; + + /** + * 根据交互对象获取Topic + */ + getTopicByObject(mapId: string, objectId: string): string | null; + + /** + * 获取Zulip配置 + */ + getZulipConfig(): any; + + /** + * 检查地图是否存在 + */ + hasMap(mapId: string): boolean; + + /** + * 检查Stream是否存在 + */ + hasStream(streamName: string): boolean; + + /** + * 获取所有地图ID列表 + */ + getAllMapIds(): string[]; + + /** + * 获取所有Stream名称列表 + */ + getAllStreams(): string[]; + + /** + * 热重载配置 + */ + reloadConfig(): Promise; + + /** + * 验证配置有效性 + */ + validateConfig(): Promise<{ valid: boolean; errors: string[] }>; +} + +/** + * Zulip事件处理服务接口 + * + * 职责: + * - 处理从Zulip接收的事件队列消息 + * - 将Zulip消息转换为游戏协议格式 + * - 管理事件队列的生命周期 + */ +export interface IZulipEventProcessorService { + /** + * 启动事件处理循环 + */ + startEventProcessing(): Promise; + + /** + * 停止事件处理循环 + */ + stopEventProcessing(): Promise; + + /** + * 注册事件队列 + */ + registerEventQueue(queueId: string, userId: string, lastEventId?: number): Promise; + + /** + * 注销事件队列 + */ + unregisterEventQueue(queueId: string): Promise; + + /** + * 处理Zulip消息事件 + */ + processMessageEvent(event: any, senderUserId: string): Promise; + + /** + * 设置消息分发器 + */ + setMessageDistributor(distributor: any): void; + + /** + * 获取事件处理统计信息 + */ + getProcessingStats(): any; +} \ No newline at end of file diff --git a/src/business/zulip/interfaces/zulip.interfaces.ts b/src/core/zulip/interfaces/zulip.interfaces.ts similarity index 100% rename from src/business/zulip/interfaces/zulip.interfaces.ts rename to src/core/zulip/interfaces/zulip.interfaces.ts diff --git a/src/business/zulip/services/api-key-security.service.spec.ts b/src/core/zulip/services/api_key_security.service.spec.ts similarity index 99% rename from src/business/zulip/services/api-key-security.service.spec.ts rename to src/core/zulip/services/api_key_security.service.spec.ts index 995e283..a5698b2 100644 --- a/src/business/zulip/services/api-key-security.service.spec.ts +++ b/src/core/zulip/services/api_key_security.service.spec.ts @@ -17,7 +17,7 @@ import { ApiKeySecurityService, SecurityEventType, SecuritySeverity, -} from './api-key-security.service'; +} from './api_key_security.service'; import { IRedisService } from '../../../core/redis/redis.interface'; describe('ApiKeySecurityService', () => { diff --git a/src/business/zulip/services/api-key-security.service.ts b/src/core/zulip/services/api_key_security.service.ts similarity index 97% rename from src/business/zulip/services/api-key-security.service.ts rename to src/core/zulip/services/api_key_security.service.ts index bdcfd2c..a27ae3a 100644 --- a/src/business/zulip/services/api-key-security.service.ts +++ b/src/core/zulip/services/api_key_security.service.ts @@ -100,6 +100,28 @@ export interface GetApiKeyResult { message?: string; } +/** + * API密钥安全服务类 + * + * 职责: + * - 管理Zulip API密钥的安全存储 + * - 提供API密钥的加密和解密功能 + * - 记录API密钥的访问日志 + * - 监控API密钥的使用情况和安全事件 + * + * 主要方法: + * - storeApiKey(): 安全存储加密的API密钥 + * - retrieveApiKey(): 检索并解密API密钥 + * - validateApiKey(): 验证API密钥的有效性 + * - logSecurityEvent(): 记录安全相关事件 + * - getAccessStats(): 获取API密钥访问统计 + * + * 使用场景: + * - 用户API密钥的安全存储 + * - API密钥访问时的解密操作 + * - 安全事件的监控和记录 + * - API密钥使用情况的统计分析 + */ @Injectable() export class ApiKeySecurityService { private readonly logger = new Logger(ApiKeySecurityService.name); diff --git a/src/business/zulip/services/config-manager.service.spec.ts b/src/core/zulip/services/config_manager.service.spec.ts similarity index 99% rename from src/business/zulip/services/config-manager.service.spec.ts rename to src/core/zulip/services/config_manager.service.spec.ts index 49b7f94..39a91bb 100644 --- a/src/business/zulip/services/config-manager.service.spec.ts +++ b/src/core/zulip/services/config_manager.service.spec.ts @@ -12,8 +12,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import * as fc from 'fast-check'; -import { ConfigManagerService, MapConfig, ZulipConfig } from './config-manager.service'; -import { AppLoggerService } from '../../../core/utils/logger/logger.service'; +import { ConfigManagerService, MapConfig, ZulipConfig } from './config_manager.service'; +import { AppLoggerService } from '../../utils/logger/logger.service'; import * as fs from 'fs'; import * as path from 'path'; diff --git a/src/business/zulip/services/config-manager.service.ts b/src/core/zulip/services/config_manager.service.ts similarity index 98% rename from src/business/zulip/services/config-manager.service.ts rename to src/core/zulip/services/config_manager.service.ts index a569824..a340599 100644 --- a/src/business/zulip/services/config-manager.service.ts +++ b/src/core/zulip/services/config_manager.service.ts @@ -108,6 +108,28 @@ export interface InteractionObject extends InteractionObjectConfig { mapId: string; // 所属地图ID } +/** + * 配置管理服务类 + * + * 职责: + * - 管理地图到Zulip Stream的映射配置 + * - 提供Zulip服务器连接配置 + * - 支持配置文件的热重载 + * - 验证配置的完整性和有效性 + * + * 主要方法: + * - loadMapConfig(): 加载地图配置文件 + * - getStreamByMap(): 根据地图ID获取对应的Stream + * - getZulipConfig(): 获取Zulip服务器配置 + * - validateConfig(): 验证配置文件格式 + * - enableConfigWatcher(): 启用配置文件监控 + * + * 使用场景: + * - 系统启动时加载配置 + * - 消息路由时查找Stream映射 + * - 配置文件变更时自动重载 + * - 配置验证和错误处理 + */ @Injectable() export class ConfigManagerService implements OnModuleDestroy { private mapConfigs: Map = new Map(); @@ -216,6 +238,9 @@ export class ConfigManagerService implements OnModuleDestroy { * 4. 存储到内存映射 * * @returns Promise + * + * @throws Error 当配置格式无效时 + * @throws Error 当文件读取失败时 */ async loadMapConfig(): Promise { this.logger.log('开始加载地图配置', { diff --git a/src/business/zulip/services/error-handler.service.spec.ts b/src/core/zulip/services/error_handler.service.spec.ts similarity index 99% rename from src/business/zulip/services/error-handler.service.spec.ts rename to src/core/zulip/services/error_handler.service.spec.ts index 42c426c..0baf916 100644 --- a/src/business/zulip/services/error-handler.service.spec.ts +++ b/src/core/zulip/services/error_handler.service.spec.ts @@ -23,7 +23,7 @@ import { LoadStatus, ErrorHandlingResult, RetryConfig, -} from './error-handler.service'; +} from './error_handler.service'; import { AppLoggerService } from '../../../core/utils/logger/logger.service'; describe('ErrorHandlerService', () => { diff --git a/src/business/zulip/services/error-handler.service.ts b/src/core/zulip/services/error_handler.service.ts similarity index 97% rename from src/business/zulip/services/error-handler.service.ts rename to src/core/zulip/services/error_handler.service.ts index 0041fe3..f1f12c2 100644 --- a/src/business/zulip/services/error-handler.service.ts +++ b/src/core/zulip/services/error_handler.service.ts @@ -115,6 +115,28 @@ export enum LoadStatus { CRITICAL = 'critical', } +/** + * 错误处理服务类 + * + * 职责: + * - 统一处理系统错误和异常 + * - 实现重试机制和服务降级 + * - 监控系统健康状态和负载 + * - 提供错误恢复和告警功能 + * + * 主要方法: + * - handleError(): 处理各类错误和异常 + * - retryWithBackoff(): 带退避策略的重试机制 + * - enableDegradedMode(): 启用服务降级模式 + * - getServiceStatus(): 获取服务状态 + * - recordError(): 记录错误统计 + * + * 使用场景: + * - Zulip API调用失败时的错误处理 + * - 网络连接异常的重试机制 + * - 系统负载过高时的服务降级 + * - 错误监控和告警通知 + */ @Injectable() export class ErrorHandlerService extends EventEmitter implements OnModuleDestroy { private readonly logger = new Logger(ErrorHandlerService.name); diff --git a/src/business/zulip/services/monitoring.service.spec.ts b/src/core/zulip/services/monitoring.service.spec.ts similarity index 100% rename from src/business/zulip/services/monitoring.service.spec.ts rename to src/core/zulip/services/monitoring.service.spec.ts diff --git a/src/business/zulip/services/monitoring.service.ts b/src/core/zulip/services/monitoring.service.ts similarity index 96% rename from src/business/zulip/services/monitoring.service.ts rename to src/core/zulip/services/monitoring.service.ts index 34ef9d3..8a6f65a 100644 --- a/src/business/zulip/services/monitoring.service.ts +++ b/src/core/zulip/services/monitoring.service.ts @@ -182,6 +182,29 @@ export interface MonitoringStats { }; } +/** + * 监控服务类 + * + * 职责: + * - 监控Zulip集成系统的运行状态 + * - 收集和统计系统性能指标 + * - 提供健康检查和告警功能 + * - 生成系统监控报告 + * + * 主要方法: + * - recordConnection(): 记录连接统计 + * - recordApiCall(): 记录API调用统计 + * - recordMessage(): 记录消息统计 + * - triggerAlert(): 触发告警 + * - getSystemStats(): 获取系统统计信息 + * - performHealthCheck(): 执行健康检查 + * + * 使用场景: + * - 系统性能监控和统计 + * - 异常情况的告警通知 + * - 系统健康状态检查 + * - 运维数据的收集和分析 + */ @Injectable() export class MonitoringService extends EventEmitter implements OnModuleInit, OnModuleDestroy { private readonly logger = new Logger(MonitoringService.name); diff --git a/src/business/zulip/services/stream-initializer.service.ts b/src/core/zulip/services/stream_initializer.service.ts similarity index 92% rename from src/business/zulip/services/stream-initializer.service.ts rename to src/core/zulip/services/stream_initializer.service.ts index cbf2339..06b644b 100644 --- a/src/business/zulip/services/stream-initializer.service.ts +++ b/src/core/zulip/services/stream_initializer.service.ts @@ -21,8 +21,30 @@ */ import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; -import { ConfigManagerService } from './config-manager.service'; +import { ConfigManagerService } from './config_manager.service'; +/** + * Stream初始化服务类 + * + * 职责: + * - 系统启动时自动检查并创建Zulip Streams + * - 确保所有地图对应的Stream都存在 + * - 验证Stream配置的完整性 + * - 提供Stream初始化状态监控 + * + * 主要方法: + * - onModuleInit(): 模块初始化时自动执行 + * - initializeStreams(): 初始化所有必需的Streams + * - createStreamIfNotExists(): 检查并创建单个Stream + * - validateStreamConfig(): 验证Stream配置 + * - getInitializationStatus(): 获取初始化状态 + * + * 使用场景: + * - 系统启动时自动初始化Streams + * - 确保消息路由的目标Stream存在 + * - 新增地图时自动创建对应Stream + * - 系统部署和配置验证 + */ @Injectable() export class StreamInitializerService implements OnModuleInit { private readonly logger = new Logger(StreamInitializerService.name); diff --git a/src/business/zulip/services/zulip-client.service.spec.ts b/src/core/zulip/services/zulip_client.service.spec.ts similarity index 99% rename from src/business/zulip/services/zulip-client.service.spec.ts rename to src/core/zulip/services/zulip_client.service.spec.ts index 4316892..f2d0ddf 100644 --- a/src/business/zulip/services/zulip-client.service.spec.ts +++ b/src/core/zulip/services/zulip_client.service.spec.ts @@ -12,7 +12,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import * as fc from 'fast-check'; -import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip-client.service'; +import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip_client.service'; import { AppLoggerService } from '../../../core/utils/logger/logger.service'; describe('ZulipClientService', () => { diff --git a/src/business/zulip/services/zulip-client.service.ts b/src/core/zulip/services/zulip_client.service.ts similarity index 96% rename from src/business/zulip/services/zulip-client.service.ts rename to src/core/zulip/services/zulip_client.service.ts index 07cc647..5355612 100644 --- a/src/business/zulip/services/zulip-client.service.ts +++ b/src/core/zulip/services/zulip_client.service.ts @@ -77,6 +77,28 @@ export interface GetEventsResult { error?: string; } +/** + * Zulip客户端服务类 + * + * 职责: + * - 封装Zulip REST API调用 + * - 处理Zulip客户端的创建和配置 + * - 管理事件队列的注册和轮询 + * - 提供消息发送和接收功能 + * + * 主要方法: + * - createClient(): 创建并初始化Zulip客户端 + * - registerQueue(): 注册Zulip事件队列 + * - sendMessage(): 发送消息到Zulip Stream + * - getEvents(): 获取Zulip事件 + * - validateConfig(): 验证客户端配置 + * + * 使用场景: + * - 为每个用户创建独立的Zulip客户端 + * - 处理与Zulip服务器的所有通信 + * - 消息的发送和事件的接收 + * - API调用的错误处理和重试 + */ @Injectable() export class ZulipClientService { private readonly logger = new Logger(ZulipClientService.name); diff --git a/src/business/zulip/services/zulip-client-pool.service.spec.ts b/src/core/zulip/services/zulip_client_pool.service.spec.ts similarity index 98% rename from src/business/zulip/services/zulip-client-pool.service.spec.ts rename to src/core/zulip/services/zulip_client_pool.service.spec.ts index 2cdd754..4a5bb64 100644 --- a/src/business/zulip/services/zulip-client-pool.service.spec.ts +++ b/src/core/zulip/services/zulip_client_pool.service.spec.ts @@ -12,9 +12,9 @@ */ import { Test, TestingModule } from '@nestjs/testing'; -import { ZulipClientPoolService, PoolStats } from './zulip-client-pool.service'; -import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip-client.service'; -import { AppLoggerService } from '../../../core/utils/logger/logger.service'; +import { ZulipClientPoolService, PoolStats } from './zulip_client_pool.service'; +import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from './zulip_client.service'; +import { AppLoggerService } from '../../utils/logger/logger.service'; describe('ZulipClientPoolService', () => { let service: ZulipClientPoolService; diff --git a/src/business/zulip/services/zulip-client-pool.service.ts b/src/core/zulip/services/zulip_client_pool.service.ts similarity index 95% rename from src/business/zulip/services/zulip-client-pool.service.ts rename to src/core/zulip/services/zulip_client_pool.service.ts index 5e43e14..743d539 100644 --- a/src/business/zulip/services/zulip-client-pool.service.ts +++ b/src/core/zulip/services/zulip_client_pool.service.ts @@ -35,7 +35,7 @@ import { SendMessageResult, RegisterQueueResult, GetEventsResult, -} from './zulip-client.service'; +} from './zulip_client.service'; /** * 用户客户端信息接口 @@ -57,6 +57,28 @@ export interface PoolStats { clientIds: string[]; } +/** + * Zulip客户端池服务类 + * + * 职责: + * - 管理用户专用的Zulip客户端实例 + * - 维护客户端连接池和生命周期 + * - 处理客户端的创建、销毁和状态管理 + * - 提供客户端池统计和监控功能 + * + * 主要方法: + * - createUserClient(): 为用户创建专用Zulip客户端 + * - getUserClient(): 获取用户的Zulip客户端 + * - destroyUserClient(): 销毁用户的Zulip客户端 + * - getPoolStats(): 获取客户端池统计信息 + * - startEventPolling(): 启动事件轮询 + * + * 使用场景: + * - 玩家登录时创建专用客户端 + * - 消息发送时获取客户端实例 + * - 玩家登出时清理客户端资源 + * - 系统监控和性能统计 + */ @Injectable() export class ZulipClientPoolService implements OnModuleDestroy { private readonly clientPool = new Map(); diff --git a/src/business/zulip/types/zulip-js.d.ts b/src/core/zulip/types/zulip-js.d.ts similarity index 100% rename from src/business/zulip/types/zulip-js.d.ts rename to src/core/zulip/types/zulip-js.d.ts diff --git a/src/core/zulip/zulip-core.module.ts b/src/core/zulip/zulip-core.module.ts new file mode 100644 index 0000000..e30d2c1 --- /dev/null +++ b/src/core/zulip/zulip-core.module.ts @@ -0,0 +1,68 @@ +/** + * Zulip核心服务模块 + * + * 功能描述: + * - 提供Zulip技术实现相关的核心服务 + * - 封装第三方API调用和技术细节 + * - 为业务层提供抽象接口 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-12-31 + */ + +import { Module } from '@nestjs/common'; +import { ZulipClientService } from './services/zulip_client.service'; +import { ZulipClientPoolService } from './services/zulip_client_pool.service'; +import { ConfigManagerService } from './services/config_manager.service'; +import { ApiKeySecurityService } from './services/api_key_security.service'; +import { ErrorHandlerService } from './services/error_handler.service'; +import { MonitoringService } from './services/monitoring.service'; +import { StreamInitializerService } from './services/stream_initializer.service'; +import { RedisModule } from '../redis/redis.module'; + +@Module({ + imports: [ + // Redis模块 - ApiKeySecurityService需要REDIS_SERVICE + RedisModule, + ], + providers: [ + // 核心客户端服务 + { + provide: 'ZULIP_CLIENT_SERVICE', + useClass: ZulipClientService, + }, + { + provide: 'ZULIP_CLIENT_POOL_SERVICE', + useClass: ZulipClientPoolService, + }, + { + provide: 'ZULIP_CONFIG_SERVICE', + useClass: ConfigManagerService, + }, + + // 辅助服务 + ApiKeySecurityService, + ErrorHandlerService, + MonitoringService, + StreamInitializerService, + + // 直接提供类(用于内部依赖) + ZulipClientService, + ZulipClientPoolService, + ConfigManagerService, + ], + exports: [ + // 导出接口标识符供业务层使用 + 'ZULIP_CLIENT_SERVICE', + 'ZULIP_CLIENT_POOL_SERVICE', + 'ZULIP_CONFIG_SERVICE', + + // 导出辅助服务 + ApiKeySecurityService, + ErrorHandlerService, + MonitoringService, + StreamInitializerService, + ], +}) +export class ZulipCoreModule {} \ No newline at end of file