refactor:重构Zulip模块按业务功能模块化架构

- 将技术实现服务从business层迁移到core层
- 创建src/core/zulip/核心服务模块,包含API客户端、连接池等技术服务
- 保留src/business/zulip/业务逻辑,专注游戏相关的业务规则
- 通过依赖注入实现业务层与核心层的解耦
- 更新模块导入关系,确保架构分层清晰

重构后的架构符合单一职责原则,提高了代码的可维护性和可测试性
This commit is contained in:
moyin
2025-12-31 15:44:36 +08:00
parent 5140bd1a54
commit 2d10131838
36 changed files with 2773 additions and 125 deletions

View File

@@ -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,

View File

@@ -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<ChatMessageResponse> {
// 业务逻辑:验证和处理
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调用逻辑
});
});
```
这种架构设计确保了业务逻辑与技术实现的清晰分离,提高了代码的可维护性和可测试性。

View File

@@ -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<AppLoggerService>;
let mockRedisService: jest.Mocked<IRedisService>;
let mockConfigManager: jest.Mocked<ConfigManagerService>;
let mockConfigManager: jest.Mocked<IZulipConfigService>;
// 内存存储模拟Redis
let memoryStore: Map<string, { value: string; expireAt?: number }>;
@@ -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,
},
],

View File

@@ -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初始化完成');
}

View File

@@ -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<SessionManagerService>;
let mockZulipClientPool: jest.Mocked<IZulipClientPoolService>;
// 模拟清理结果
const createMockCleanupResult = (overrides: Partial<any> = {}): 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>(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<any>(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<CleanupConfig> = {
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<CleanupConfig> = {
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<CleanupConfig> = {
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<any>(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>(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);
});
});
});

View File

@@ -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(),

View File

@@ -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<AppLoggerService>;
let mockRedisService: jest.Mocked<IRedisService>;
let mockConfigManager: jest.Mocked<ConfigManagerService>;
let mockConfigManager: jest.Mocked<IZulipConfigService>;
// 内存存储模拟Redis
let memoryStore: Map<string, { value: string; expireAt?: number }>;
@@ -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,
},
],

View File

@@ -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<GameSession>
*
* @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<ContextInfo>
*
* @throws Error
*/
async injectContext(socketId: string, mapId?: string): Promise<ContextInfo> {
this.logger.debug('开始上下文注入', {

View File

@@ -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<AppLoggerService>;
let mockSessionManager: jest.Mocked<SessionManagerService>;
let mockConfigManager: jest.Mocked<ConfigManagerService>;
let mockClientPool: jest.Mocked<ZulipClientPoolService>;
let mockConfigManager: jest.Mocked<IZulipConfigService>;
let mockClientPool: jest.Mocked<IZulipClientPoolService>;
let mockDistributor: jest.Mocked<MessageDistributor>;
// 创建模拟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,
},
],

View File

@@ -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初始化完成');
}

View File

@@ -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,
],

File diff suppressed because it is too large Load Diff

View File

@@ -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初始化完成');
}

View File

@@ -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);
});

View File

@@ -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', () => {

View File

@@ -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: '*' },

26
src/core/zulip/index.ts Normal file
View File

@@ -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';

View File

@@ -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<ZulipClientInstance>;
/**
* 验证API Key有效性
*/
validateApiKey(clientInstance: ZulipClientInstance): Promise<boolean>;
/**
* 发送消息到指定Stream/Topic
*/
sendMessage(
clientInstance: ZulipClientInstance,
stream: string,
topic: string,
content: string,
): Promise<SendMessageResult>;
/**
* 注册事件队列
*/
registerQueue(
clientInstance: ZulipClientInstance,
eventTypes?: string[],
): Promise<RegisterQueueResult>;
/**
* 注销事件队列
*/
deregisterQueue(clientInstance: ZulipClientInstance): Promise<boolean>;
/**
* 获取事件队列中的事件
*/
getEvents(
clientInstance: ZulipClientInstance,
dontBlock?: boolean,
): Promise<GetEventsResult>;
/**
* 销毁客户端实例
*/
destroyClient(clientInstance: ZulipClientInstance): Promise<void>;
}
/**
* Zulip客户端池服务接口
*
* 职责:
* - 管理用户专用的Zulip客户端实例
* - 维护客户端连接池和生命周期
* - 处理客户端的创建、销毁和状态管理
*/
export interface IZulipClientPoolService {
/**
* 为用户创建专用Zulip客户端
*/
createUserClient(userId: string, config: ZulipClientConfig): Promise<ZulipClientInstance>;
/**
* 获取用户的Zulip客户端
*/
getUserClient(userId: string): Promise<ZulipClientInstance | null>;
/**
* 检查用户客户端是否存在
*/
hasUserClient(userId: string): boolean;
/**
* 发送消息到指定Stream/Topic
*/
sendMessage(
userId: string,
stream: string,
topic: string,
content: string,
): Promise<SendMessageResult>;
/**
* 注册事件队列
*/
registerEventQueue(userId: string): Promise<RegisterQueueResult>;
/**
* 注销事件队列
*/
deregisterEventQueue(userId: string): Promise<boolean>;
/**
* 销毁用户客户端
*/
destroyUserClient(userId: string): Promise<void>;
/**
* 获取客户端池统计信息
*/
getPoolStats(): PoolStats;
/**
* 清理过期客户端
*/
cleanupIdleClients(maxIdleMinutes?: number): Promise<number>;
}
/**
* 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<void>;
/**
* 验证配置有效性
*/
validateConfig(): Promise<{ valid: boolean; errors: string[] }>;
}
/**
* Zulip事件处理服务接口
*
* 职责:
* - 处理从Zulip接收的事件队列消息
* - 将Zulip消息转换为游戏协议格式
* - 管理事件队列的生命周期
*/
export interface IZulipEventProcessorService {
/**
* 启动事件处理循环
*/
startEventProcessing(): Promise<void>;
/**
* 停止事件处理循环
*/
stopEventProcessing(): Promise<void>;
/**
* 注册事件队列
*/
registerEventQueue(queueId: string, userId: string, lastEventId?: number): Promise<void>;
/**
* 注销事件队列
*/
unregisterEventQueue(queueId: string): Promise<void>;
/**
* 处理Zulip消息事件
*/
processMessageEvent(event: any, senderUserId: string): Promise<void>;
/**
* 设置消息分发器
*/
setMessageDistributor(distributor: any): void;
/**
* 获取事件处理统计信息
*/
getProcessingStats(): any;
}

View File

@@ -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', () => {

View File

@@ -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);

View File

@@ -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';

View File

@@ -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<string, MapConfig> = new Map();
@@ -216,6 +238,9 @@ export class ConfigManagerService implements OnModuleDestroy {
* 4.
*
* @returns Promise<void>
*
* @throws Error
* @throws Error
*/
async loadMapConfig(): Promise<void> {
this.logger.log('开始加载地图配置', {

View File

@@ -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', () => {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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', () => {

View File

@@ -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);

View File

@@ -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;

View File

@@ -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<string, UserClientInfo>();

View File

@@ -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 {}