Compare commits
5 Commits
feature/ch
...
fc1566e616
| Author | SHA1 | Date | |
|---|---|---|---|
| fc1566e616 | |||
|
|
7cac8ad8a5 | ||
|
|
cf1b37af78 | ||
|
|
1849415b11 | ||
|
|
963e6ca90f |
@@ -1,9 +1,17 @@
|
||||
# AI Code Inspection Guide - Whale Town Game Server
|
||||
|
||||
## 🎯 Pre-execution Setup
|
||||
## ⚠️ 🚨 CRITICAL: MANDATORY PRE-EXECUTION REQUIREMENTS 🚨 ⚠️
|
||||
|
||||
### 🚀 User Information Setup
|
||||
**Before starting any inspection steps, run the user information script:**
|
||||
**<EFBFBD> AI MUST READ THIS SECTION FIRST - EXECUTION WITHOUT COMPLETING THESE STEPS IS STRICTLY FORBIDDEN 🔴**
|
||||
|
||||
**⛔ STOP! Before executing ANY step (including Step 1), you MUST complete ALL items in this section! ⛔**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Pre-execution Setup (MANDATORY - CANNOT BE SKIPPED)
|
||||
|
||||
### 🚀 User Information Setup (STEP 0 - MUST EXECUTE FIRST)
|
||||
**⚠️ CRITICAL: Before starting any inspection steps (including Step 1), MUST run the user information script:**
|
||||
|
||||
```bash
|
||||
# Enter AI-reading directory
|
||||
@@ -13,6 +21,13 @@ cd docs/ai-reading
|
||||
node tools/setup-user-info.js
|
||||
```
|
||||
|
||||
**🚨 VERIFICATION CHECKPOINT:**
|
||||
- [ ] Have you executed `node tools/setup-user-info.js`?
|
||||
- [ ] Does `docs/ai-reading/me.config.json` file exist?
|
||||
- [ ] Have you read and confirmed the user's date and name from the config file?
|
||||
|
||||
**⛔ IF ANY CHECKBOX ABOVE IS UNCHECKED, YOU CANNOT PROCEED TO STEP 1! ⛔**
|
||||
|
||||
#### Script Functions
|
||||
- Automatically get current date (YYYY-MM-DD format)
|
||||
- Check if config file exists or date matches
|
||||
@@ -43,13 +58,49 @@ const userName = config.name; // e.g.: "John"
|
||||
const modifyRecord = `- ${userDate}: Code standard optimization - Clean unused imports (Modified by: ${userName})`;
|
||||
```
|
||||
|
||||
### 🏗️ Project Characteristics
|
||||
### 🏗️ Project Characteristics (MUST UNDERSTAND BEFORE STEP 1)
|
||||
**⚠️ AI MUST read and understand these characteristics BEFORE executing Step 1:**
|
||||
|
||||
This project is a **NestJS Game Server** with the following features:
|
||||
- **Dual-mode Architecture**: Supports both database and memory modes
|
||||
- **Real-time Communication**: WebSocket-based real-time bidirectional communication
|
||||
- **Property Testing**: Admin modules use fast-check for randomized testing
|
||||
- **Layered Architecture**: Core layer (technical implementation) + Business layer (business logic)
|
||||
|
||||
**🚨 VERIFICATION CHECKPOINT:**
|
||||
- [ ] Have you read and understood the project is a NestJS Game Server?
|
||||
- [ ] Have you understood the dual-mode architecture?
|
||||
- [ ] Have you understood the WebSocket real-time communication feature?
|
||||
- [ ] Have you understood the property testing requirements?
|
||||
- [ ] Have you understood the layered architecture?
|
||||
|
||||
**⛔ IF ANY CHECKBOX ABOVE IS UNCHECKED, YOU CANNOT PROCEED TO STEP 1! ⛔**
|
||||
|
||||
---
|
||||
|
||||
## 🚨 FINAL PRE-EXECUTION CHECKLIST (MUST COMPLETE BEFORE STEP 1) 🚨
|
||||
|
||||
**Before proceeding to Step 1, AI MUST confirm ALL of the following:**
|
||||
|
||||
### ✅ Mandatory Completion Checklist:
|
||||
- [ ] ✅ Executed `node tools/setup-user-info.js` script
|
||||
- [ ] ✅ Confirmed `me.config.json` file exists and contains valid date and name
|
||||
- [ ] ✅ Read and stored user's date from config file
|
||||
- [ ] ✅ Read and stored user's name from config file
|
||||
- [ ] ✅ Understood this is a NestJS Game Server project
|
||||
- [ ] ✅ Understood the dual-mode architecture (database + memory)
|
||||
- [ ] ✅ Understood the WebSocket real-time communication feature
|
||||
- [ ] ✅ Understood the property testing requirements
|
||||
- [ ] ✅ Understood the layered architecture (Core + Business)
|
||||
- [ ] ✅ Read and understood the execution principles below
|
||||
|
||||
### 🔴 CRITICAL RULE:
|
||||
**IF ANY ITEM IN THE CHECKLIST ABOVE IS NOT COMPLETED, AI IS ABSOLUTELY FORBIDDEN FROM EXECUTING STEP 1 OR ANY OTHER STEPS!**
|
||||
|
||||
**AI MUST explicitly confirm completion of ALL checklist items before proceeding to Step 1!**
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Execution Principles
|
||||
|
||||
### 🚨 Mid-step Start Requirements (Important)
|
||||
@@ -95,9 +146,19 @@ Start Executing Specified Step
|
||||
```
|
||||
User Requests Code Inspection
|
||||
↓
|
||||
Collect User Info (date, name)
|
||||
🚨 MANDATORY: Execute node tools/setup-user-info.js
|
||||
↓
|
||||
Identify Project Characteristics
|
||||
🚨 MANDATORY: Verify me.config.json exists
|
||||
↓
|
||||
🚨 MANDATORY: Read and Store User Info (date, name)
|
||||
↓
|
||||
🚨 MANDATORY: Understand Project Characteristics
|
||||
↓
|
||||
🚨 MANDATORY: Complete Pre-execution Checklist
|
||||
↓
|
||||
🚨 MANDATORY: Explicitly Confirm ALL Checklist Items Completed
|
||||
↓
|
||||
⛔ CHECKPOINT: Cannot proceed without completing above steps ⛔
|
||||
↓
|
||||
Execute Step 1 → Provide Report → Wait for Confirmation
|
||||
↓
|
||||
@@ -132,7 +193,18 @@ Execute Step 7 → Provide Report → Wait for Confirmation
|
||||
|
||||
## 📚 Step Execution Guide
|
||||
|
||||
**🚨 REMINDER: Before executing Step 1, ensure you have completed ALL items in the "FINAL PRE-EXECUTION CHECKLIST" above! 🚨**
|
||||
|
||||
### Step 1: Naming Convention Check
|
||||
**⚠️ BEFORE STARTING STEP 1, AI MUST:**
|
||||
1. ✅ Confirm `node tools/setup-user-info.js` has been executed
|
||||
2. ✅ Confirm user date and name have been read from `me.config.json`
|
||||
3. ✅ Confirm project characteristics have been understood
|
||||
4. ✅ Explicitly state: "Pre-execution checklist completed, now starting Step 1"
|
||||
|
||||
**ONLY AFTER EXPLICIT CONFIRMATION CAN STEP 1 BEGIN!**
|
||||
|
||||
---
|
||||
**Read when executing:** `step1-naming-convention.md`
|
||||
**Focus on:** Folder structure flattening, game server special file types
|
||||
**After completion:** Provide inspection report, wait for user confirmation
|
||||
@@ -203,8 +275,9 @@ Execute Step 7 → Provide Report → Wait for Confirmation
|
||||
|
||||
### Step 7: Code Commit
|
||||
**Read when executing:** `step7-code-commit.md`
|
||||
**Focus on:** Git change verification, modification record consistency check, standardized commit process
|
||||
**After completion:** Provide inspection report, wait for user confirmation
|
||||
**Focus on:** Git change detection, branch management, commit message standards, merge document generation
|
||||
**🚨 Important Principle:** Regardless of when or what type of changes were made, if Git detects changes, provide commit service
|
||||
**After completion:** Provide commit report, generate merge document
|
||||
|
||||
## 📋 Unified Report Template
|
||||
|
||||
@@ -394,4 +467,56 @@ When issues are discovered and modifications made in any step, must follow this
|
||||
|
||||
---
|
||||
|
||||
**Before starting execution, please first run `node tools/setup-user-info.js` to set user information!**
|
||||
## 🔴 FINAL WARNING 🔴
|
||||
|
||||
**⚠️ AI EXECUTION REQUIREMENTS SUMMARY:**
|
||||
|
||||
1. **STEP 0 (MANDATORY)**: Execute `node tools/setup-user-info.js`
|
||||
2. **STEP 0.1 (MANDATORY)**: Verify `me.config.json` exists and read user info
|
||||
3. **STEP 0.2 (MANDATORY)**: Understand project characteristics
|
||||
4. **STEP 0.3 (MANDATORY)**: Complete and explicitly confirm pre-execution checklist
|
||||
5. **ONLY THEN**: Begin Step 1
|
||||
|
||||
**🚨 VIOLATION CONSEQUENCES:**
|
||||
- If AI skips Step 0 and directly executes Step 1, user MUST stop AI and require restart
|
||||
- If AI does not explicitly confirm checklist completion, user MUST stop AI and require confirmation
|
||||
- If AI does not read user info from config file, all subsequent steps are INVALID
|
||||
|
||||
**✅ CORRECT EXECUTION START:**
|
||||
```
|
||||
AI: "I will now begin the code inspection process.
|
||||
|
||||
Step 0 - Pre-execution Setup:
|
||||
1. ✅ Executing user information setup script...
|
||||
Command: cd docs/ai-reading && node tools/setup-user-info.js
|
||||
|
||||
2. ✅ Verifying me.config.json exists...
|
||||
File found: docs/ai-reading/me.config.json
|
||||
|
||||
3. ✅ Reading user information...
|
||||
User Date: 2026-01-19
|
||||
User Name: [Name from config]
|
||||
|
||||
4. ✅ Understanding project characteristics...
|
||||
- NestJS Game Server ✓
|
||||
- Dual-mode Architecture ✓
|
||||
- WebSocket Communication ✓
|
||||
- Property Testing ✓
|
||||
- Layered Architecture ✓
|
||||
|
||||
5. ✅ Pre-execution checklist completed!
|
||||
|
||||
All mandatory pre-execution requirements have been satisfied.
|
||||
Now proceeding to Step 1: Naming Convention Check..."
|
||||
```
|
||||
|
||||
**⛔ INCORRECT EXECUTION START (FORBIDDEN):**
|
||||
```
|
||||
AI: "I will start with Step 1: Naming Convention Check..." ❌ WRONG!
|
||||
AI: "Let me check the naming conventions..." ❌ WRONG!
|
||||
AI: "Starting code inspection..." ❌ WRONG!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**🎯 Remember: Step 0 is NOT optional - it is MANDATORY before ANY other step!**
|
||||
@@ -19,10 +19,36 @@
|
||||
## 🎯 检查目标
|
||||
完成代码修改后的规范化提交流程,确保代码变更记录清晰、分支管理规范、提交信息符合项目标准。
|
||||
|
||||
## 🚨 重要原则:提交所有变更
|
||||
|
||||
### 核心原则
|
||||
**无论变更是何时产生的、是什么类型的,只要 Git 检测到有变更,就应该帮助用户提交!**
|
||||
|
||||
### 常见误区
|
||||
❌ **错误想法**:"这些变更不是本次代码检查产生的,所以不需要提交"
|
||||
✅ **正确做法**:检查所有 Git 变更,分析变更类型,询问用户要提交哪些文件,然后用合适的方式提交
|
||||
|
||||
### 执行流程
|
||||
1. **检查 Git 状态**:`git status` 查看所有变更文件
|
||||
2. **分析变更内容**:`git diff` 查看每个文件的具体变更
|
||||
3. **分类变更类型**:判断是功能新增、Bug修复、代码优化等
|
||||
4. **询问用户意图**:确认要提交哪些文件、提交到哪个仓库
|
||||
5. **选择提交策略**:根据变更类型选择合适的分支命名和提交信息
|
||||
6. **执行提交操作**:创建分支、暂存文件、提交、推送
|
||||
|
||||
### 变更来源不重要
|
||||
变更可能来自:
|
||||
- 本次代码检查的修改 ✓
|
||||
- 之前的功能开发 ✓
|
||||
- 其他时间的代码调整 ✓
|
||||
- 任何其他修改 ✓
|
||||
|
||||
**关键是:只要有变更,就应该提供提交服务!**
|
||||
|
||||
## 📋 执行前置条件
|
||||
- 已完成前6个步骤的代码检查和修改
|
||||
- 所有修改的文件已更新修改记录和版本信息
|
||||
- 代码能够正常运行且通过测试
|
||||
- Git 工作区有变更文件(通过 `git status` 检测)
|
||||
- 代码能够正常运行且通过测试(如适用)
|
||||
- 用户明确要提交这些变更
|
||||
|
||||
## 🚨 协作规范和范围控制
|
||||
|
||||
@@ -69,8 +95,30 @@ git diff
|
||||
git diff --cached
|
||||
```
|
||||
|
||||
### 🚨 重要:不要预判变更来源
|
||||
**AI 必须检查所有 Git 变更,不要因为变更不是"本次检查产生的"就忽略!**
|
||||
|
||||
#### 错误示例
|
||||
```
|
||||
❌ AI: "检测到 chat.gateway.ts 有变更,但这是功能新增,不是代码规范检查产生的,所以不需要提交。"
|
||||
```
|
||||
|
||||
#### 正确示例
|
||||
```
|
||||
✅ AI: "检测到以下文件有变更:
|
||||
1. chat.gateway.ts - 功能新增(添加地图切换功能)
|
||||
2. auth/login.service.ts - 代码优化
|
||||
3. chat/chat.service.ts - Bug修复
|
||||
|
||||
请问您要提交哪些文件?我可以帮您:
|
||||
- 全部提交(可以分类提交不同类型的变更)
|
||||
- 只提交部分文件
|
||||
- 按模块分别提交"
|
||||
```
|
||||
|
||||
### 2. 文件修改记录校验
|
||||
**重要**:检查每个修改文件的头部信息是否与实际修改内容一致
|
||||
**注意**:如果变更不是本次代码检查产生的,文件头部可能没有更新修改记录,这是正常的。
|
||||
只需要检查变更内容,生成准确的提交信息即可。
|
||||
|
||||
#### 校验内容包括:
|
||||
- **修改记录**:最新的修改记录是否准确描述了本次变更
|
||||
|
||||
@@ -714,13 +714,7 @@ export class LoginService {
|
||||
apiKeyResult.apiKey!
|
||||
);
|
||||
|
||||
// 4. 更新内存关联
|
||||
await this.zulipAccountService.linkGameAccount(
|
||||
user.id.toString(),
|
||||
zulipAccount.zulipUserId,
|
||||
zulipAccount.zulipEmail,
|
||||
apiKeyResult.apiKey!
|
||||
);
|
||||
// 注意:不在登录时建立内存关联,Zulip客户端将在WebSocket连接时创建
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
@@ -533,15 +533,7 @@ export class RegisterService {
|
||||
status: 'active',
|
||||
});
|
||||
|
||||
// 6. 建立游戏账号与Zulip账号的内存关联(用于当前会话)
|
||||
if (finalApiKey) {
|
||||
await this.zulipAccountService.linkGameAccount(
|
||||
gameUser.id.toString(),
|
||||
createResult.userId, // 已在上面验证不为 undefined
|
||||
createResult.email!,
|
||||
finalApiKey
|
||||
);
|
||||
}
|
||||
// 注意:不在注册时建立内存关联,Zulip客户端将在WebSocket连接时创建
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
|
||||
@@ -7,9 +7,12 @@
|
||||
* - 接口导出验证
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @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';
|
||||
@@ -33,6 +36,7 @@ describe('ChatModule', () => {
|
||||
createUserClient: jest.fn(),
|
||||
destroyUserClient: jest.fn(),
|
||||
sendMessage: jest.fn(),
|
||||
getUserClient: jest.fn(),
|
||||
};
|
||||
|
||||
const mockZulipConfigService = {
|
||||
@@ -61,6 +65,10 @@ describe('ChatModule', () => {
|
||||
verifyToken: jest.fn(),
|
||||
};
|
||||
|
||||
const mockZulipAccountsService = {
|
||||
findByGameUserId: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
// 禁用日志输出
|
||||
jest.spyOn(Logger.prototype, 'log').mockImplementation();
|
||||
@@ -97,6 +105,10 @@ describe('ChatModule', () => {
|
||||
provide: LoginCoreService,
|
||||
useValue: mockLoginCoreService,
|
||||
},
|
||||
{
|
||||
provide: 'ZulipAccountsService',
|
||||
useValue: mockZulipAccountsService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
|
||||
@@ -12,17 +12,19 @@
|
||||
* - 依赖 ZulipCoreModule(核心层)提供Zulip技术服务
|
||||
* - 依赖 RedisModule(核心层)提供缓存服务
|
||||
* - 依赖 LoginCoreModule(核心层)提供Token验证
|
||||
* - 依赖 ZulipAccountsModule(核心层)提供Zulip账号数据访问
|
||||
*
|
||||
* 导出接口:
|
||||
* - SESSION_QUERY_SERVICE: 会话查询接口(供其他 Business 模块使用)
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-15: 功能完善 - 添加ZulipAccountsModule依赖,支持登录时初始化Zulip客户端 (修改者: AI)
|
||||
* - 2026-01-14: 代码规范优化 - 完善文件头注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.1.1
|
||||
* @version 1.2.0
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
* @lastModified 2026-01-15
|
||||
*/
|
||||
|
||||
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 { RedisModule } from '../../core/redis/redis.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';
|
||||
|
||||
@Module({
|
||||
@@ -43,6 +46,8 @@ import { SESSION_QUERY_SERVICE } from '../../core/session_core/session_core.inte
|
||||
RedisModule,
|
||||
// 登录核心模块
|
||||
LoginCoreModule,
|
||||
// Zulip账号数据库模块
|
||||
ZulipAccountsModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
// 主聊天服务
|
||||
|
||||
@@ -8,9 +8,12 @@
|
||||
* - Token验证和错误处理
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.0
|
||||
* @version 1.0.1
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
* @lastModified 2026-01-19
|
||||
*
|
||||
* 修改记录:
|
||||
* - 2026-01-19 moyin: 修复handlePlayerLogout测试,删除不再调用的deleteApiKey断言和过时测试用例
|
||||
*/
|
||||
|
||||
import { Test, TestingModule } from '@nestjs/testing';
|
||||
@@ -51,6 +54,7 @@ describe('ChatService', () => {
|
||||
createUserClient: jest.fn(),
|
||||
destroyUserClient: jest.fn(),
|
||||
sendMessage: jest.fn(),
|
||||
getUserClient: jest.fn(),
|
||||
};
|
||||
|
||||
const mockApiKeySecurityService = {
|
||||
@@ -62,6 +66,10 @@ describe('ChatService', () => {
|
||||
verifyToken: jest.fn(),
|
||||
};
|
||||
|
||||
const mockZulipAccountsService = {
|
||||
findByGameUserId: jest.fn(),
|
||||
};
|
||||
|
||||
mockWebSocketGateway = {
|
||||
broadcastToMap: jest.fn(),
|
||||
sendToPlayer: jest.fn(),
|
||||
@@ -90,6 +98,10 @@ describe('ChatService', () => {
|
||||
provide: LoginCoreService,
|
||||
useValue: mockLoginCoreService,
|
||||
},
|
||||
{
|
||||
provide: 'ZulipAccountsService',
|
||||
useValue: mockZulipAccountsService,
|
||||
},
|
||||
],
|
||||
}).compile();
|
||||
|
||||
@@ -100,6 +112,14 @@ describe('ChatService', () => {
|
||||
apiKeySecurityService = module.get('API_KEY_SECURITY_SERVICE');
|
||||
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网关
|
||||
service.setWebSocketGateway(mockWebSocketGateway);
|
||||
|
||||
@@ -220,14 +240,12 @@ describe('ChatService', () => {
|
||||
createdAt: new Date(),
|
||||
});
|
||||
zulipClientPool.destroyUserClient.mockResolvedValue(undefined);
|
||||
apiKeySecurityService.deleteApiKey.mockResolvedValue(undefined);
|
||||
sessionService.destroySession.mockResolvedValue(true);
|
||||
|
||||
await service.handlePlayerLogout(socketId, 'manual');
|
||||
|
||||
expect(sessionService.getSession).toHaveBeenCalledWith(socketId);
|
||||
expect(zulipClientPool.destroyUserClient).toHaveBeenCalledWith(userId);
|
||||
expect(apiKeySecurityService.deleteApiKey).toHaveBeenCalledWith(userId);
|
||||
expect(sessionService.destroySession).toHaveBeenCalledWith(socketId);
|
||||
});
|
||||
|
||||
@@ -257,25 +275,6 @@ describe('ChatService', () => {
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -14,15 +14,16 @@
|
||||
* - ⚡ 低延迟聊天体验
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-15: 功能完善 - WebSocket登录时自动初始化用户Zulip客户端 (修改者: AI)
|
||||
* - 2026-01-14: 代码规范优化 - 提取魔法数字为常量 (修改者: moyin)
|
||||
* - 2026-01-14: 代码规范优化 - 补充类级别JSDoc注释 (修改者: moyin)
|
||||
* - 2026-01-14: 代码规范优化 - 补充接口定义的JSDoc注释 (修改者: moyin)
|
||||
* - 2026-01-14: 代码规范优化 - 完善文件头注释和方法注释规范 (修改者: moyin)
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.4
|
||||
* @version 1.1.0
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
* @lastModified 2026-01-15
|
||||
*/
|
||||
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
@@ -34,6 +35,8 @@ import {
|
||||
IApiKeySecurityService,
|
||||
} from '../../core/zulip_core/zulip_core.interfaces';
|
||||
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;
|
||||
/** 消息范围:local(本地)、global(全局) */
|
||||
scope: string;
|
||||
/** 目标地图ID(可选,不传则使用会话当前地图) */
|
||||
mapId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,6 +184,8 @@ export class ChatService {
|
||||
@Inject('API_KEY_SECURITY_SERVICE')
|
||||
private readonly apiKeySecurityService: IApiKeySecurityService,
|
||||
private readonly loginCoreService: LoginCoreService,
|
||||
@Inject('ZulipAccountsService')
|
||||
private readonly zulipAccountsService: ZulipAccountsService | ZulipAccountsMemoryService,
|
||||
) {
|
||||
this.logger.log('ChatService初始化完成');
|
||||
}
|
||||
@@ -217,7 +224,10 @@ export class ChatService {
|
||||
return { success: false, error: 'Token验证失败' };
|
||||
}
|
||||
|
||||
// 3. 创建会话
|
||||
// 3. 初始化用户的Zulip客户端(从数据库获取Zulip账号信息)
|
||||
await this.initializeZulipClientForUser(userInfo.userId);
|
||||
|
||||
// 4. 创建会话
|
||||
const sessionResult = await this.createUserSession(request.socketId, userInfo);
|
||||
|
||||
this.logger.log('玩家登录成功', {
|
||||
@@ -256,20 +266,13 @@ export class ChatService {
|
||||
|
||||
const userId = session.userId;
|
||||
|
||||
// 清理Zulip客户端
|
||||
// 清理Zulip客户端(注意:不删除Redis中的API Key,保持持久化)
|
||||
if (userId) {
|
||||
try {
|
||||
await this.zulipClientPool.destroyUserClient(userId);
|
||||
} catch (e) {
|
||||
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: '会话不存在,请重新登录' };
|
||||
}
|
||||
|
||||
// 2. 获取上下文
|
||||
const context = await this.sessionService.injectContext(request.socketId);
|
||||
// 2. 确定目标地图(优先使用请求中的mapId,否则使用会话当前地图)
|
||||
const targetMapId = request.mapId || session.currentMap;
|
||||
|
||||
// 3. 获取上下文
|
||||
const context = await this.sessionService.injectContext(request.socketId, targetMapId);
|
||||
const targetStream = context.stream;
|
||||
const targetTopic = context.topic || 'General';
|
||||
|
||||
// 3. 消息验证
|
||||
// 4. 消息验证
|
||||
const validationResult = await this.filterService.validateMessage(
|
||||
session.userId,
|
||||
request.content,
|
||||
targetStream,
|
||||
session.currentMap,
|
||||
targetMapId,
|
||||
);
|
||||
|
||||
if (!validationResult.allowed) {
|
||||
@@ -323,7 +329,7 @@ export class ChatService {
|
||||
const messageContent = validationResult.filteredContent || request.content;
|
||||
const messageId = `game_${Date.now()}_${session.userId}`;
|
||||
|
||||
// 4. 🚀 立即广播给游戏内玩家
|
||||
// 5. 🚀 立即广播给游戏内玩家(根据scope决定广播范围)
|
||||
const gameMessage: GameChatMessage = {
|
||||
t: 'chat_render',
|
||||
from: session.username,
|
||||
@@ -331,14 +337,15 @@ export class ChatService {
|
||||
bubble: true,
|
||||
timestamp: new Date().toISOString(),
|
||||
messageId,
|
||||
mapId: session.currentMap,
|
||||
mapId: targetMapId,
|
||||
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 }));
|
||||
|
||||
// 5. 🔄 异步同步到Zulip
|
||||
// 6. 🔄 异步同步到Zulip
|
||||
this.syncToZulipAsync(session.userId, targetStream, targetTopic, messageContent, messageId)
|
||||
.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) {
|
||||
try {
|
||||
const payload = await this.loginCoreService.verifyToken(token, 'access');
|
||||
@@ -441,20 +563,18 @@ export class ChatService {
|
||||
|
||||
private async createUserSession(socketId: string, userInfo: any) {
|
||||
const sessionId = randomUUID();
|
||||
let zulipQueueId = `queue_${sessionId}`;
|
||||
|
||||
// 尝试创建Zulip客户端
|
||||
if (userInfo.zulipApiKey) {
|
||||
try {
|
||||
const clientInstance = await this.zulipClientPool.createUserClient(userInfo.userId, {
|
||||
username: userInfo.zulipEmail || userInfo.email,
|
||||
apiKey: userInfo.zulipApiKey,
|
||||
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 });
|
||||
// 尝试获取已创建的Zulip客户端的队列ID
|
||||
let zulipQueueId = `queue_${sessionId}`;
|
||||
try {
|
||||
const existingClient = await this.zulipClientPool.getUserClient(userInfo.userId);
|
||||
if (existingClient?.queueId) {
|
||||
zulipQueueId = existingClient.queueId;
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.debug('获取Zulip客户端队列ID失败,使用默认值', {
|
||||
error: (e as Error).message
|
||||
});
|
||||
}
|
||||
|
||||
const session = await this.sessionService.createSession(
|
||||
|
||||
Reference in New Issue
Block a user