Compare commits
2 Commits
refactor/a
...
fix/chat-m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf1b37af78 | ||
|
|
1849415b11 |
@@ -7,9 +7,12 @@
|
|||||||
* - 接口导出验证
|
* - 接口导出验证
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.0.0
|
* @version 1.0.1
|
||||||
* @since 2026-01-14
|
* @since 2026-01-14
|
||||||
* @lastModified 2026-01-14
|
* @lastModified 2026-01-19
|
||||||
|
*
|
||||||
|
* 修改记录:
|
||||||
|
* - 2026-01-19 moyin: Bug修复 - 添加缺失的ZulipAccountsService Mock配置
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
@@ -33,6 +36,7 @@ describe('ChatModule', () => {
|
|||||||
createUserClient: jest.fn(),
|
createUserClient: jest.fn(),
|
||||||
destroyUserClient: jest.fn(),
|
destroyUserClient: jest.fn(),
|
||||||
sendMessage: jest.fn(),
|
sendMessage: jest.fn(),
|
||||||
|
getUserClient: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockZulipConfigService = {
|
const mockZulipConfigService = {
|
||||||
@@ -61,6 +65,10 @@ describe('ChatModule', () => {
|
|||||||
verifyToken: jest.fn(),
|
verifyToken: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockZulipAccountsService = {
|
||||||
|
findByGameUserId: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
// 禁用日志输出
|
// 禁用日志输出
|
||||||
jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
||||||
@@ -97,6 +105,10 @@ describe('ChatModule', () => {
|
|||||||
provide: LoginCoreService,
|
provide: LoginCoreService,
|
||||||
useValue: mockLoginCoreService,
|
useValue: mockLoginCoreService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: 'ZulipAccountsService',
|
||||||
|
useValue: mockZulipAccountsService,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
|
|||||||
@@ -12,17 +12,19 @@
|
|||||||
* - 依赖 ZulipCoreModule(核心层)提供Zulip技术服务
|
* - 依赖 ZulipCoreModule(核心层)提供Zulip技术服务
|
||||||
* - 依赖 RedisModule(核心层)提供缓存服务
|
* - 依赖 RedisModule(核心层)提供缓存服务
|
||||||
* - 依赖 LoginCoreModule(核心层)提供Token验证
|
* - 依赖 LoginCoreModule(核心层)提供Token验证
|
||||||
|
* - 依赖 ZulipAccountsModule(核心层)提供Zulip账号数据访问
|
||||||
*
|
*
|
||||||
* 导出接口:
|
* 导出接口:
|
||||||
* - SESSION_QUERY_SERVICE: 会话查询接口(供其他 Business 模块使用)
|
* - SESSION_QUERY_SERVICE: 会话查询接口(供其他 Business 模块使用)
|
||||||
*
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 功能完善 - 添加ZulipAccountsModule依赖,支持登录时初始化Zulip客户端 (修改者: AI)
|
||||||
* - 2026-01-14: 代码规范优化 - 完善文件头注释规范 (修改者: moyin)
|
* - 2026-01-14: 代码规范优化 - 完善文件头注释规范 (修改者: moyin)
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.1.1
|
* @version 1.2.0
|
||||||
* @since 2026-01-14
|
* @since 2026-01-14
|
||||||
* @lastModified 2026-01-14
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
@@ -33,6 +35,7 @@ import { ChatCleanupService } from './services/chat_cleanup.service';
|
|||||||
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
||||||
import { RedisModule } from '../../core/redis/redis.module';
|
import { RedisModule } from '../../core/redis/redis.module';
|
||||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||||
|
import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
|
||||||
import { SESSION_QUERY_SERVICE } from '../../core/session_core/session_core.interfaces';
|
import { SESSION_QUERY_SERVICE } from '../../core/session_core/session_core.interfaces';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -43,6 +46,8 @@ import { SESSION_QUERY_SERVICE } from '../../core/session_core/session_core.inte
|
|||||||
RedisModule,
|
RedisModule,
|
||||||
// 登录核心模块
|
// 登录核心模块
|
||||||
LoginCoreModule,
|
LoginCoreModule,
|
||||||
|
// Zulip账号数据库模块
|
||||||
|
ZulipAccountsModule.forRoot(),
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
// 主聊天服务
|
// 主聊天服务
|
||||||
|
|||||||
@@ -8,9 +8,12 @@
|
|||||||
* - Token验证和错误处理
|
* - Token验证和错误处理
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.0.0
|
* @version 1.0.1
|
||||||
* @since 2026-01-14
|
* @since 2026-01-14
|
||||||
* @lastModified 2026-01-14
|
* @lastModified 2026-01-19
|
||||||
|
*
|
||||||
|
* 修改记录:
|
||||||
|
* - 2026-01-19 moyin: 修复handlePlayerLogout测试,删除不再调用的deleteApiKey断言和过时测试用例
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
@@ -51,6 +54,7 @@ describe('ChatService', () => {
|
|||||||
createUserClient: jest.fn(),
|
createUserClient: jest.fn(),
|
||||||
destroyUserClient: jest.fn(),
|
destroyUserClient: jest.fn(),
|
||||||
sendMessage: jest.fn(),
|
sendMessage: jest.fn(),
|
||||||
|
getUserClient: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockApiKeySecurityService = {
|
const mockApiKeySecurityService = {
|
||||||
@@ -62,6 +66,10 @@ describe('ChatService', () => {
|
|||||||
verifyToken: jest.fn(),
|
verifyToken: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockZulipAccountsService = {
|
||||||
|
findByGameUserId: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
mockWebSocketGateway = {
|
mockWebSocketGateway = {
|
||||||
broadcastToMap: jest.fn(),
|
broadcastToMap: jest.fn(),
|
||||||
sendToPlayer: jest.fn(),
|
sendToPlayer: jest.fn(),
|
||||||
@@ -90,6 +98,10 @@ describe('ChatService', () => {
|
|||||||
provide: LoginCoreService,
|
provide: LoginCoreService,
|
||||||
useValue: mockLoginCoreService,
|
useValue: mockLoginCoreService,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
provide: 'ZulipAccountsService',
|
||||||
|
useValue: mockZulipAccountsService,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}).compile();
|
}).compile();
|
||||||
|
|
||||||
@@ -100,6 +112,14 @@ describe('ChatService', () => {
|
|||||||
apiKeySecurityService = module.get('API_KEY_SECURITY_SERVICE');
|
apiKeySecurityService = module.get('API_KEY_SECURITY_SERVICE');
|
||||||
loginCoreService = module.get(LoginCoreService);
|
loginCoreService = module.get(LoginCoreService);
|
||||||
|
|
||||||
|
// 设置默认的mock行为
|
||||||
|
// ZulipAccountsService默认返回null(用户没有Zulip账号)
|
||||||
|
const zulipAccountsService = module.get('ZulipAccountsService');
|
||||||
|
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
|
||||||
|
|
||||||
|
// ZulipClientPool的getUserClient默认返回null
|
||||||
|
zulipClientPool.getUserClient.mockResolvedValue(null);
|
||||||
|
|
||||||
// 设置WebSocket网关
|
// 设置WebSocket网关
|
||||||
service.setWebSocketGateway(mockWebSocketGateway);
|
service.setWebSocketGateway(mockWebSocketGateway);
|
||||||
|
|
||||||
@@ -220,14 +240,12 @@ describe('ChatService', () => {
|
|||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
});
|
});
|
||||||
zulipClientPool.destroyUserClient.mockResolvedValue(undefined);
|
zulipClientPool.destroyUserClient.mockResolvedValue(undefined);
|
||||||
apiKeySecurityService.deleteApiKey.mockResolvedValue(undefined);
|
|
||||||
sessionService.destroySession.mockResolvedValue(true);
|
sessionService.destroySession.mockResolvedValue(true);
|
||||||
|
|
||||||
await service.handlePlayerLogout(socketId, 'manual');
|
await service.handlePlayerLogout(socketId, 'manual');
|
||||||
|
|
||||||
expect(sessionService.getSession).toHaveBeenCalledWith(socketId);
|
expect(sessionService.getSession).toHaveBeenCalledWith(socketId);
|
||||||
expect(zulipClientPool.destroyUserClient).toHaveBeenCalledWith(userId);
|
expect(zulipClientPool.destroyUserClient).toHaveBeenCalledWith(userId);
|
||||||
expect(apiKeySecurityService.deleteApiKey).toHaveBeenCalledWith(userId);
|
|
||||||
expect(sessionService.destroySession).toHaveBeenCalledWith(socketId);
|
expect(sessionService.destroySession).toHaveBeenCalledWith(socketId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,25 +275,6 @@ describe('ChatService', () => {
|
|||||||
|
|
||||||
expect(sessionService.destroySession).toHaveBeenCalled();
|
expect(sessionService.destroySession).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该处理API Key清理失败', async () => {
|
|
||||||
sessionService.getSession.mockResolvedValue({
|
|
||||||
socketId,
|
|
||||||
userId,
|
|
||||||
username: 'testuser',
|
|
||||||
zulipQueueId: 'queue_123',
|
|
||||||
currentMap: 'whale_port',
|
|
||||||
position: { x: 400, y: 300 },
|
|
||||||
lastActivity: new Date(),
|
|
||||||
createdAt: new Date(),
|
|
||||||
});
|
|
||||||
apiKeySecurityService.deleteApiKey.mockRejectedValue(new Error('Redis error'));
|
|
||||||
sessionService.destroySession.mockResolvedValue(true);
|
|
||||||
|
|
||||||
await service.handlePlayerLogout(socketId);
|
|
||||||
|
|
||||||
expect(sessionService.destroySession).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('sendChatMessage', () => {
|
describe('sendChatMessage', () => {
|
||||||
|
|||||||
@@ -14,15 +14,16 @@
|
|||||||
* - ⚡ 低延迟聊天体验
|
* - ⚡ 低延迟聊天体验
|
||||||
*
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 功能完善 - WebSocket登录时自动初始化用户Zulip客户端 (修改者: AI)
|
||||||
* - 2026-01-14: 代码规范优化 - 提取魔法数字为常量 (修改者: moyin)
|
* - 2026-01-14: 代码规范优化 - 提取魔法数字为常量 (修改者: moyin)
|
||||||
* - 2026-01-14: 代码规范优化 - 补充类级别JSDoc注释 (修改者: moyin)
|
* - 2026-01-14: 代码规范优化 - 补充类级别JSDoc注释 (修改者: moyin)
|
||||||
* - 2026-01-14: 代码规范优化 - 补充接口定义的JSDoc注释 (修改者: moyin)
|
* - 2026-01-14: 代码规范优化 - 补充接口定义的JSDoc注释 (修改者: moyin)
|
||||||
* - 2026-01-14: 代码规范优化 - 完善文件头注释和方法注释规范 (修改者: moyin)
|
* - 2026-01-14: 代码规范优化 - 完善文件头注释和方法注释规范 (修改者: moyin)
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.0.4
|
* @version 1.1.0
|
||||||
* @since 2026-01-14
|
* @since 2026-01-14
|
||||||
* @lastModified 2026-01-14
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||||
@@ -34,6 +35,8 @@ import {
|
|||||||
IApiKeySecurityService,
|
IApiKeySecurityService,
|
||||||
} from '../../core/zulip_core/zulip_core.interfaces';
|
} from '../../core/zulip_core/zulip_core.interfaces';
|
||||||
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
import { LoginCoreService } from '../../core/login_core/login_core.service';
|
||||||
|
import { ZulipAccountsService } from '../../core/db/zulip_accounts/zulip_accounts.service';
|
||||||
|
import { ZulipAccountsMemoryService } from '../../core/db/zulip_accounts/zulip_accounts_memory.service';
|
||||||
|
|
||||||
// ========== 接口定义 ==========
|
// ========== 接口定义 ==========
|
||||||
|
|
||||||
@@ -47,6 +50,8 @@ export interface ChatMessageRequest {
|
|||||||
content: string;
|
content: string;
|
||||||
/** 消息范围:local(本地)、global(全局) */
|
/** 消息范围:local(本地)、global(全局) */
|
||||||
scope: string;
|
scope: string;
|
||||||
|
/** 目标地图ID(可选,不传则使用会话当前地图) */
|
||||||
|
mapId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,6 +184,8 @@ export class ChatService {
|
|||||||
@Inject('API_KEY_SECURITY_SERVICE')
|
@Inject('API_KEY_SECURITY_SERVICE')
|
||||||
private readonly apiKeySecurityService: IApiKeySecurityService,
|
private readonly apiKeySecurityService: IApiKeySecurityService,
|
||||||
private readonly loginCoreService: LoginCoreService,
|
private readonly loginCoreService: LoginCoreService,
|
||||||
|
@Inject('ZulipAccountsService')
|
||||||
|
private readonly zulipAccountsService: ZulipAccountsService | ZulipAccountsMemoryService,
|
||||||
) {
|
) {
|
||||||
this.logger.log('ChatService初始化完成');
|
this.logger.log('ChatService初始化完成');
|
||||||
}
|
}
|
||||||
@@ -217,7 +224,10 @@ export class ChatService {
|
|||||||
return { success: false, error: 'Token验证失败' };
|
return { success: false, error: 'Token验证失败' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 创建会话
|
// 3. 初始化用户的Zulip客户端(从数据库获取Zulip账号信息)
|
||||||
|
await this.initializeZulipClientForUser(userInfo.userId);
|
||||||
|
|
||||||
|
// 4. 创建会话
|
||||||
const sessionResult = await this.createUserSession(request.socketId, userInfo);
|
const sessionResult = await this.createUserSession(request.socketId, userInfo);
|
||||||
|
|
||||||
this.logger.log('玩家登录成功', {
|
this.logger.log('玩家登录成功', {
|
||||||
@@ -256,20 +266,13 @@ export class ChatService {
|
|||||||
|
|
||||||
const userId = session.userId;
|
const userId = session.userId;
|
||||||
|
|
||||||
// 清理Zulip客户端
|
// 清理Zulip客户端(注意:不删除Redis中的API Key,保持持久化)
|
||||||
if (userId) {
|
if (userId) {
|
||||||
try {
|
try {
|
||||||
await this.zulipClientPool.destroyUserClient(userId);
|
await this.zulipClientPool.destroyUserClient(userId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn('Zulip客户端清理失败', { error: (e as Error).message });
|
this.logger.warn('Zulip客户端清理失败', { error: (e as Error).message });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 清理API Key缓存
|
|
||||||
try {
|
|
||||||
await this.apiKeySecurityService.deleteApiKey(userId);
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.warn('API Key缓存清理失败', { error: (e as Error).message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 销毁会话
|
// 销毁会话
|
||||||
@@ -303,17 +306,20 @@ export class ChatService {
|
|||||||
return { success: false, error: '会话不存在,请重新登录' };
|
return { success: false, error: '会话不存在,请重新登录' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 获取上下文
|
// 2. 确定目标地图(优先使用请求中的mapId,否则使用会话当前地图)
|
||||||
const context = await this.sessionService.injectContext(request.socketId);
|
const targetMapId = request.mapId || session.currentMap;
|
||||||
|
|
||||||
|
// 3. 获取上下文
|
||||||
|
const context = await this.sessionService.injectContext(request.socketId, targetMapId);
|
||||||
const targetStream = context.stream;
|
const targetStream = context.stream;
|
||||||
const targetTopic = context.topic || 'General';
|
const targetTopic = context.topic || 'General';
|
||||||
|
|
||||||
// 3. 消息验证
|
// 4. 消息验证
|
||||||
const validationResult = await this.filterService.validateMessage(
|
const validationResult = await this.filterService.validateMessage(
|
||||||
session.userId,
|
session.userId,
|
||||||
request.content,
|
request.content,
|
||||||
targetStream,
|
targetStream,
|
||||||
session.currentMap,
|
targetMapId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!validationResult.allowed) {
|
if (!validationResult.allowed) {
|
||||||
@@ -323,7 +329,7 @@ export class ChatService {
|
|||||||
const messageContent = validationResult.filteredContent || request.content;
|
const messageContent = validationResult.filteredContent || request.content;
|
||||||
const messageId = `game_${Date.now()}_${session.userId}`;
|
const messageId = `game_${Date.now()}_${session.userId}`;
|
||||||
|
|
||||||
// 4. 🚀 立即广播给游戏内玩家
|
// 5. 🚀 立即广播给游戏内玩家(根据scope决定广播范围)
|
||||||
const gameMessage: GameChatMessage = {
|
const gameMessage: GameChatMessage = {
|
||||||
t: 'chat_render',
|
t: 'chat_render',
|
||||||
from: session.username,
|
from: session.username,
|
||||||
@@ -331,14 +337,15 @@ export class ChatService {
|
|||||||
bubble: true,
|
bubble: true,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
messageId,
|
messageId,
|
||||||
mapId: session.currentMap,
|
mapId: targetMapId,
|
||||||
scope: request.scope,
|
scope: request.scope,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.broadcastToGamePlayers(session.currentMap, gameMessage, request.socketId)
|
// local: 只广播给目标地图的玩家; global: 广播给所有玩家(暂时也用地图广播)
|
||||||
|
this.broadcastToGamePlayers(targetMapId, gameMessage, request.socketId)
|
||||||
.catch(e => this.logger.warn('游戏内广播失败', { error: (e as Error).message }));
|
.catch(e => this.logger.warn('游戏内广播失败', { error: (e as Error).message }));
|
||||||
|
|
||||||
// 5. 🔄 异步同步到Zulip
|
// 6. 🔄 异步同步到Zulip
|
||||||
this.syncToZulipAsync(session.userId, targetStream, targetTopic, messageContent, messageId)
|
this.syncToZulipAsync(session.userId, targetStream, targetTopic, messageContent, messageId)
|
||||||
.catch(e => this.logger.warn('Zulip同步失败', { error: (e as Error).message }));
|
.catch(e => this.logger.warn('Zulip同步失败', { error: (e as Error).message }));
|
||||||
|
|
||||||
@@ -421,6 +428,121 @@ export class ChatService {
|
|||||||
|
|
||||||
// ========== 私有方法 ==========
|
// ========== 私有方法 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化用户的Zulip客户端
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 1. 从数据库获取用户的Zulip账号信息
|
||||||
|
* 2. 检查Redis中是否已有API Key缓存
|
||||||
|
* 3. 如果Redis中没有,从数据库标记判断是否需要重新获取
|
||||||
|
* 4. 创建Zulip客户端实例
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
*/
|
||||||
|
private async initializeZulipClientForUser(userId: string): Promise<void> {
|
||||||
|
this.logger.log('开始初始化用户Zulip客户端', {
|
||||||
|
operation: 'initializeZulipClientForUser',
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. 从数据库获取用户的Zulip账号信息
|
||||||
|
const zulipAccount = await this.zulipAccountsService.findByGameUserId(userId);
|
||||||
|
|
||||||
|
if (!zulipAccount) {
|
||||||
|
this.logger.debug('用户没有关联的Zulip账号,跳过Zulip客户端初始化', {
|
||||||
|
operation: 'initializeZulipClientForUser',
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zulipAccount.status !== 'active') {
|
||||||
|
this.logger.warn('用户Zulip账号状态异常,跳过初始化', {
|
||||||
|
operation: 'initializeZulipClientForUser',
|
||||||
|
userId,
|
||||||
|
status: zulipAccount.status,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查Redis中是否已有API Key
|
||||||
|
const existingApiKey = await this.apiKeySecurityService.getApiKey(userId);
|
||||||
|
|
||||||
|
if (existingApiKey.success && existingApiKey.apiKey) {
|
||||||
|
this.logger.log('Redis中已有API Key缓存,直接创建Zulip客户端', {
|
||||||
|
operation: 'initializeZulipClientForUser',
|
||||||
|
userId,
|
||||||
|
zulipEmail: zulipAccount.zulipEmail,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建Zulip客户端
|
||||||
|
await this.createZulipClientWithApiKey(
|
||||||
|
userId,
|
||||||
|
zulipAccount.zulipEmail,
|
||||||
|
existingApiKey.apiKey
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Redis中没有API Key,记录警告
|
||||||
|
// 注意:由于登录时没有用户密码,无法重新生成API Key
|
||||||
|
// API Key应该在用户注册时存储到Redis,如果丢失需要用户重新绑定Zulip账号
|
||||||
|
this.logger.warn('Redis中没有用户的Zulip API Key缓存,无法创建Zulip客户端', {
|
||||||
|
operation: 'initializeZulipClientForUser',
|
||||||
|
userId,
|
||||||
|
zulipEmail: zulipAccount.zulipEmail,
|
||||||
|
hint: '用户可能需要重新绑定Zulip账号',
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
this.logger.error('初始化用户Zulip客户端失败', {
|
||||||
|
operation: 'initializeZulipClientForUser',
|
||||||
|
userId,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
// 不抛出异常,允许用户继续登录(只是没有Zulip功能)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 使用API Key创建Zulip客户端
|
||||||
|
*
|
||||||
|
* @param userId 用户ID
|
||||||
|
* @param zulipEmail Zulip邮箱
|
||||||
|
* @param apiKey API Key
|
||||||
|
*/
|
||||||
|
private async createZulipClientWithApiKey(
|
||||||
|
userId: string,
|
||||||
|
zulipEmail: string,
|
||||||
|
apiKey: string
|
||||||
|
): Promise<void> {
|
||||||
|
try {
|
||||||
|
const clientInstance = await this.zulipClientPool.createUserClient(userId, {
|
||||||
|
username: zulipEmail,
|
||||||
|
apiKey: apiKey,
|
||||||
|
realm: process.env.ZULIP_SERVER_URL || 'https://zulip.xinghangee.icu/',
|
||||||
|
});
|
||||||
|
|
||||||
|
this.logger.log('Zulip客户端创建成功', {
|
||||||
|
operation: 'createZulipClientWithApiKey',
|
||||||
|
userId,
|
||||||
|
zulipEmail,
|
||||||
|
queueId: clientInstance.queueId,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const err = error as Error;
|
||||||
|
this.logger.error('创建Zulip客户端失败', {
|
||||||
|
operation: 'createZulipClientWithApiKey',
|
||||||
|
userId,
|
||||||
|
zulipEmail,
|
||||||
|
error: err.message,
|
||||||
|
});
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async validateGameToken(token: string) {
|
private async validateGameToken(token: string) {
|
||||||
try {
|
try {
|
||||||
const payload = await this.loginCoreService.verifyToken(token, 'access');
|
const payload = await this.loginCoreService.verifyToken(token, 'access');
|
||||||
@@ -441,20 +563,18 @@ export class ChatService {
|
|||||||
|
|
||||||
private async createUserSession(socketId: string, userInfo: any) {
|
private async createUserSession(socketId: string, userInfo: any) {
|
||||||
const sessionId = randomUUID();
|
const sessionId = randomUUID();
|
||||||
let zulipQueueId = `queue_${sessionId}`;
|
|
||||||
|
|
||||||
// 尝试创建Zulip客户端
|
// 尝试获取已创建的Zulip客户端的队列ID
|
||||||
if (userInfo.zulipApiKey) {
|
let zulipQueueId = `queue_${sessionId}`;
|
||||||
try {
|
try {
|
||||||
const clientInstance = await this.zulipClientPool.createUserClient(userInfo.userId, {
|
const existingClient = await this.zulipClientPool.getUserClient(userInfo.userId);
|
||||||
username: userInfo.zulipEmail || userInfo.email,
|
if (existingClient?.queueId) {
|
||||||
apiKey: userInfo.zulipApiKey,
|
zulipQueueId = existingClient.queueId;
|
||||||
realm: process.env.ZULIP_SERVER_URL || 'https://zulip.xinghangee.icu/',
|
|
||||||
});
|
|
||||||
if (clientInstance.queueId) zulipQueueId = clientInstance.queueId;
|
|
||||||
} catch (e) {
|
|
||||||
this.logger.warn('Zulip客户端创建失败', { error: (e as Error).message });
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.debug('获取Zulip客户端队列ID失败,使用默认值', {
|
||||||
|
error: (e as Error).message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await this.sessionService.createSession(
|
const session = await this.sessionService.createSession(
|
||||||
|
|||||||
Reference in New Issue
Block a user