diff --git a/src/core/session_core/README.md b/src/core/session_core/README.md new file mode 100644 index 0000000..0aba2a9 --- /dev/null +++ b/src/core/session_core/README.md @@ -0,0 +1,100 @@ +# SessionCore 会话核心模块 + +SessionCore 是为会话管理业务提供技术支撑的核心模块,定义了会话管理的抽象接口,实现 Business 层模块间的解耦。 + +## 对外提供的接口 + +### ISessionQueryService(会话查询服务接口) + +提供只读的会话查询能力,用于跨模块查询会话信息。 + +#### getSession(socketId: string): Promise +获取指定 WebSocket 连接的会话信息,不存在时返回 null。 + +#### getSocketsInMap(mapId: string): Promise +获取指定地图中所有活跃的 Socket ID 列表。 + +### ISessionManagerService(会话管理服务接口) + +提供完整的会话管理能力,包括创建、更新、删除操作,继承自 ISessionQueryService。 + +#### createSession(socketId, userId, zulipQueueId, username?, initialMap?, initialPosition?): Promise +创建新的游戏会话,关联 WebSocket 连接、用户信息和 Zulip 事件队列。 + +#### injectContext(socketId: string, mapId?: string): Promise +根据玩家位置确定 Zulip Stream 和 Topic,实现上下文注入。 + +#### updatePlayerPosition(socketId: string, mapId: string, x: number, y: number): Promise +更新玩家在地图中的位置坐标。 + +#### destroySession(socketId: string): Promise +销毁指定的会话,清理相关资源。 + +#### cleanupExpiredSessions(timeoutMinutes?: number): Promise<{ cleanedCount: number; zulipQueueIds: string[] }> +清理超时的过期会话,返回清理数量和对应的 Zulip 队列 ID 列表。 + +### SessionCoreModule(会话核心模块) + +#### forFeature(options: SessionCoreModuleOptions): DynamicModule +注册会话服务提供者,支持动态配置会话查询和管理服务的实现。 + +## 使用的项目内部依赖 + +### IPosition (本模块) +位置信息接口,定义 x 和 y 坐标。 + +### IGameSession (本模块) +游戏会话接口,包含 socketId、userId、username、zulipQueueId、currentMap、position、lastActivity、createdAt 等字段。 + +### IContextInfo (本模块) +上下文信息接口,包含 stream 和可选的 topic 字段。 + +### SESSION_QUERY_SERVICE (本模块) +会话查询服务的依赖注入 Token。 + +### SESSION_MANAGER_SERVICE (本模块) +会话管理服务的依赖注入 Token。 + +## 核心特性 + +### 接口抽象设计 +- 定义抽象接口,不包含具体实现 +- 实现由 Business 层的 ChatModule 提供 +- 支持依赖注入和模块解耦 + +### 动态模块配置 +- 使用 forFeature 方法支持灵活配置 +- 可单独注册查询服务或管理服务 +- 支持同时注册多个服务提供者 + +### 分离查询和管理职责 +- ISessionQueryService 提供只读查询能力 +- ISessionManagerService 提供完整管理能力 +- 清晰的职责分离,便于权限控制 + +### 跨模块会话查询 +- 其他模块可通过 SESSION_QUERY_SERVICE 查询会话信息 +- 不需要直接依赖 Business 层实现 +- 实现模块间的松耦合 + +## 潜在风险 + +### 接口实现缺失风险 +- Core 层只定义接口,不提供实现 +- 如果 Business 层未正确注册实现,会导致运行时错误 +- 缓解措施:在模块导入时验证服务提供者配置 + +### 依赖注入配置错误风险 +- forFeature 配置不当可能导致服务无法注入 +- Token 名称错误会导致依赖解析失败 +- 缓解措施:使用 TypeScript 类型检查和单元测试验证 + +### 接口变更影响范围风险 +- 接口定义变更会影响所有实现和使用方 +- 可能导致多个模块需要同步修改 +- 缓解措施:保持接口稳定,使用版本化管理 + +### 循环依赖风险 +- 如果 Business 层实现反向依赖 Core 层其他模块 +- 可能形成循环依赖导致模块加载失败 +- 缓解措施:严格遵守分层架构,Core 层不依赖 Business 层 diff --git a/src/core/session_core/index.ts b/src/core/session_core/index.ts new file mode 100644 index 0000000..57a2b06 --- /dev/null +++ b/src/core/session_core/index.ts @@ -0,0 +1,25 @@ +/** + * 会话核心模块导出 + * + * 功能描述: + * - 统一导出会话核心模块的接口和模块定义 + * - 提供会话管理相关的类型定义和依赖注入Token + * - 简化外部模块的导入路径 + * + * 导出内容: + * - IPosition, IGameSession, IContextInfo - 数据接口 + * - ISessionQueryService, ISessionManagerService - 服务接口 + * - SESSION_QUERY_SERVICE, SESSION_MANAGER_SERVICE - 依赖注入Token + * - SessionCoreModule - 核心模块 + * + * 最近修改: + * - 2026-01-14: 代码规范优化 - 完善文件头注释 (修改者: moyin) + * + * @author moyin + * @version 1.0.1 + * @since 2026-01-14 + * @lastModified 2026-01-14 + */ + +export * from './session_core.interfaces'; +export * from './session_core.module'; diff --git a/src/core/session_core/session_core.interfaces.ts b/src/core/session_core/session_core.interfaces.ts new file mode 100644 index 0000000..d7ca0b7 --- /dev/null +++ b/src/core/session_core/session_core.interfaces.ts @@ -0,0 +1,140 @@ +/** + * 会话管理核心接口定义 + * + * 功能描述: + * - 定义会话管理的抽象接口 + * - 供 Business 层实现,Core 层依赖 + * - 实现 Business 层模块间的解耦 + * + * 架构层级:Core Layer(核心层) + * + * 使用场景: + * - ZulipEventProcessorService 需要查询玩家会话信息 + * - 其他需要会话信息的服务 + * + * 最近修改: + * - 2026-01-14: 代码规范优化 - 完善方法注释,添加@param和@returns (修改者: moyin) + * - 2026-01-14: 代码规范优化 - 完善文件头注释,替换AI标识 (修改者: moyin) + * + * @author moyin + * @version 1.0.2 + * @since 2026-01-14 + * @lastModified 2026-01-14 + */ + +/** + * 位置信息接口 + */ +export interface IPosition { + x: number; + y: number; +} + +/** + * 游戏会话接口 + */ +export interface IGameSession { + socketId: string; + userId: string; + username: string; + zulipQueueId: string; + currentMap: string; + position: IPosition; + lastActivity: Date; + createdAt: Date; +} + +/** + * 上下文信息接口 + */ +export interface IContextInfo { + stream: string; + topic?: string; +} + +/** + * 会话查询服务接口 + * + * 提供只读的会话查询能力,用于跨模块查询会话信息 + * 不包含会话的创建、更新、删除操作 + */ +export interface ISessionQueryService { + /** + * 获取会话信息 + * @param socketId WebSocket连接ID + * @returns 会话信息,不存在返回null + */ + getSession(socketId: string): Promise; + + /** + * 获取指定地图的所有Socket ID + * @param mapId 地图ID + * @returns Socket ID列表 + */ + getSocketsInMap(mapId: string): Promise; +} + +/** + * 会话管理服务接口 + * + * 提供完整的会话管理能力,包括创建、更新、删除 + * 继承自 ISessionQueryService + */ +export interface ISessionManagerService extends ISessionQueryService { + /** + * 创建会话 + * @param socketId WebSocket连接ID + * @param userId 用户ID + * @param zulipQueueId Zulip事件队列ID + * @param username 用户名(可选) + * @param initialMap 初始地图ID(可选) + * @param initialPosition 初始位置(可选) + * @returns 创建的会话信息 + */ + createSession( + socketId: string, + userId: string, + zulipQueueId: string, + username?: string, + initialMap?: string, + initialPosition?: IPosition, + ): Promise; + + /** + * 上下文注入:根据位置确定Stream/Topic + * @param socketId WebSocket连接ID + * @param mapId 地图ID(可选) + * @returns 上下文信息,包含stream和topic + */ + injectContext(socketId: string, mapId?: string): Promise; + + /** + * 更新玩家位置 + * @param socketId WebSocket连接ID + * @param mapId 地图ID + * @param x X坐标 + * @param y Y坐标 + * @returns 更新是否成功 + */ + updatePlayerPosition(socketId: string, mapId: string, x: number, y: number): Promise; + + /** + * 销毁会话 + * @param socketId WebSocket连接ID + * @returns 销毁是否成功 + */ + destroySession(socketId: string): Promise; + + /** + * 清理过期会话 + * @param timeoutMinutes 超时时间(分钟),可选 + * @returns 清理结果,包含清理数量和对应的Zulip队列ID列表 + */ + cleanupExpiredSessions(timeoutMinutes?: number): Promise<{ cleanedCount: number; zulipQueueIds: string[] }>; +} + +/** + * 依赖注入 Token + */ +export const SESSION_QUERY_SERVICE = 'SESSION_QUERY_SERVICE'; +export const SESSION_MANAGER_SERVICE = 'SESSION_MANAGER_SERVICE'; diff --git a/src/core/session_core/session_core.module.spec.ts b/src/core/session_core/session_core.module.spec.ts new file mode 100644 index 0000000..c955a64 --- /dev/null +++ b/src/core/session_core/session_core.module.spec.ts @@ -0,0 +1,164 @@ +/** + * SessionCoreModule 单元测试 + * + * 功能描述: + * - 测试 SessionCoreModule 的动态模块配置功能 + * - 验证 forFeature 方法的正确行为 + * - 确保依赖注入配置正确 + * + * @author moyin + * @version 1.0.0 + * @since 2026-01-14 + * @lastModified 2026-01-14 + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import { SessionCoreModule } from './session_core.module'; +import { + SESSION_QUERY_SERVICE, + SESSION_MANAGER_SERVICE, + ISessionQueryService, + ISessionManagerService, + IGameSession, + IContextInfo, +} from './session_core.interfaces'; + +class MockSessionQueryService implements ISessionQueryService { + async getSession(): Promise { + return null; + } + async getSocketsInMap(): Promise { + return []; + } +} + +class MockSessionManagerService implements ISessionManagerService { + async getSession(): Promise { + return null; + } + async getSocketsInMap(): Promise { + return []; + } + async createSession( + socketId: string, + userId: string, + zulipQueueId: string, + ): Promise { + return { + socketId, + userId, + username: 'test', + zulipQueueId, + currentMap: 'default', + position: { x: 0, y: 0 }, + lastActivity: new Date(), + createdAt: new Date(), + }; + } + async injectContext(): Promise { + return { stream: 'test' }; + } + async updatePlayerPosition(): Promise { + return true; + } + async destroySession(): Promise { + return true; + } + async cleanupExpiredSessions(): Promise<{ + cleanedCount: number; + zulipQueueIds: string[]; + }> { + return { cleanedCount: 0, zulipQueueIds: [] }; + } +} + +describe('SessionCoreModule', () => { + describe('forFeature', () => { + it('should return dynamic module with empty providers when no options', () => { + const result = SessionCoreModule.forFeature({}); + expect(result.module).toBe(SessionCoreModule); + expect(result.providers).toEqual([]); + expect(result.exports).toEqual([]); + }); + + it('should register sessionQueryProvider when provided', () => { + const result = SessionCoreModule.forFeature({ + sessionQueryProvider: { + provide: SESSION_QUERY_SERVICE, + useClass: MockSessionQueryService, + }, + }); + expect(result.providers).toHaveLength(1); + expect(result.exports).toContain(SESSION_QUERY_SERVICE); + }); + + it('should register sessionManagerProvider when provided', () => { + const result = SessionCoreModule.forFeature({ + sessionManagerProvider: { + provide: SESSION_MANAGER_SERVICE, + useClass: MockSessionManagerService, + }, + }); + expect(result.providers).toHaveLength(1); + expect(result.exports).toContain(SESSION_MANAGER_SERVICE); + }); + + it('should register both providers when both provided', () => { + const result = SessionCoreModule.forFeature({ + sessionQueryProvider: { + provide: SESSION_QUERY_SERVICE, + useClass: MockSessionQueryService, + }, + sessionManagerProvider: { + provide: SESSION_MANAGER_SERVICE, + useClass: MockSessionManagerService, + }, + }); + expect(result.providers).toHaveLength(2); + expect(result.exports).toContain(SESSION_QUERY_SERVICE); + expect(result.exports).toContain(SESSION_MANAGER_SERVICE); + }); + }); + + describe('Module Integration', () => { + let module: TestingModule; + + afterEach(async () => { + if (module) { + await module.close(); + } + }); + + it('should inject sessionQueryService correctly', async () => { + module = await Test.createTestingModule({ + imports: [ + SessionCoreModule.forFeature({ + sessionQueryProvider: { + provide: SESSION_QUERY_SERVICE, + useClass: MockSessionQueryService, + }, + }), + ], + }).compile(); + + const service = module.get(SESSION_QUERY_SERVICE); + expect(service).toBeInstanceOf(MockSessionQueryService); + }); + + it('should inject sessionManagerService correctly', async () => { + module = await Test.createTestingModule({ + imports: [ + SessionCoreModule.forFeature({ + sessionManagerProvider: { + provide: SESSION_MANAGER_SERVICE, + useClass: MockSessionManagerService, + }, + }), + ], + }).compile(); + + const service = module.get(SESSION_MANAGER_SERVICE); + expect(service).toBeInstanceOf(MockSessionManagerService); + }); + }); +}); diff --git a/src/core/session_core/session_core.module.ts b/src/core/session_core/session_core.module.ts new file mode 100644 index 0000000..2b34659 --- /dev/null +++ b/src/core/session_core/session_core.module.ts @@ -0,0 +1,99 @@ +/** + * 会话核心模块 + * + * 功能描述: + * - 提供会话管理接口的依赖注入配置 + * - 作为 Core 层模块,不包含具体实现 + * - 实现由 Business 层的 ChatModule 提供 + * + * 架构层级:Core Layer(核心层) + * + * 最近修改: + * - 2026-01-14: 代码规范优化 - 清理未使用的导入 (修改者: moyin) + * - 2026-01-14: 代码规范优化 - 添加SessionCoreModule类注释 (修改者: moyin) + * - 2026-01-14: 代码规范优化 - 完善文件头注释,替换AI标识 (修改者: moyin) + * + * @author moyin + * @version 1.0.3 + * @since 2026-01-14 + * @lastModified 2026-01-14 + */ + +import { Module, DynamicModule, Provider } from '@nestjs/common'; +import { + SESSION_QUERY_SERVICE, + SESSION_MANAGER_SERVICE, +} from './session_core.interfaces'; + +/** + * 会话核心模块配置选项 + */ +export interface SessionCoreModuleOptions { + /** + * 会话查询服务提供者 + */ + sessionQueryProvider?: Provider; + + /** + * 会话管理服务提供者 + */ + sessionManagerProvider?: Provider; +} + +/** + * 会话核心模块类 + * + * 职责: + * - 提供会话服务的依赖注入配置 + * - 支持动态注册会话查询和管理服务 + * - 作为Core层与Business层的桥梁 + * + * 主要方法: + * - forFeature() - 注册会话服务提供者 + * + * 使用场景: + * - Business层模块注册会话服务实现 + * - 其他模块导入以获取会话查询能力 + */ +@Module({}) +export class SessionCoreModule { + /** + * 注册会话服务提供者 + * + * @param options 模块配置选项 + * @returns 动态模块配置 + * + * @example + * // 在 ChatModule 中注册实现 + * SessionCoreModule.forFeature({ + * sessionQueryProvider: { + * provide: SESSION_QUERY_SERVICE, + * useExisting: ChatSessionService, + * }, + * sessionManagerProvider: { + * provide: SESSION_MANAGER_SERVICE, + * useExisting: ChatSessionService, + * }, + * }) + */ + static forFeature(options: SessionCoreModuleOptions): DynamicModule { + const providers: Provider[] = []; + const exports: string[] = []; + + if (options.sessionQueryProvider) { + providers.push(options.sessionQueryProvider); + exports.push(SESSION_QUERY_SERVICE); + } + + if (options.sessionManagerProvider) { + providers.push(options.sessionManagerProvider); + exports.push(SESSION_MANAGER_SERVICE); + } + + return { + module: SessionCoreModule, + providers, + exports, + }; + } +}