forked from datawhale/whale-town-end
main #2
@@ -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,
|
||||
|
||||
172
src/business/zulip/README.md
Normal file
172
src/business/zulip/README.md
Normal 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调用逻辑
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
这种架构设计确保了业务逻辑与技术实现的清晰分离,提高了代码的可维护性和可测试性。
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
@@ -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初始化完成');
|
||||
}
|
||||
650
src/business/zulip/services/session_cleanup.service.spec.ts
Normal file
650
src/business/zulip/services/session_cleanup.service.spec.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(),
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
@@ -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('开始上下文注入', {
|
||||
@@ -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,
|
||||
},
|
||||
],
|
||||
@@ -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初始化完成');
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
1134
src/business/zulip/zulip.service.spec.ts
Normal file
1134
src/business/zulip/zulip.service.spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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初始化完成');
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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', () => {
|
||||
@@ -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
26
src/core/zulip/index.ts
Normal 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';
|
||||
294
src/core/zulip/interfaces/zulip-core.interfaces.ts
Normal file
294
src/core/zulip/interfaces/zulip-core.interfaces.ts
Normal 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;
|
||||
}
|
||||
@@ -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', () => {
|
||||
@@ -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);
|
||||
@@ -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';
|
||||
|
||||
@@ -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('开始加载地图配置', {
|
||||
@@ -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', () => {
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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', () => {
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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>();
|
||||
68
src/core/zulip/zulip-core.module.ts
Normal file
68
src/core/zulip/zulip-core.module.ts
Normal 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 {}
|
||||
Reference in New Issue
Block a user