7 Commits

Author SHA1 Message Date
89a222348a Merge pull request 'refactor(auth): 优化注册流程中Zulip账号创建为异步处理' (#53) from refactor/register-zulip-async-20260309 into main
Reviewed-on: #53
2026-03-09 11:06:17 +08:00
moyin
8ef45f53f1 refactor(auth): 优化注册流程中Zulip账号创建为异步处理
- 注册时Zulip账号创建改为异步,不阻塞注册流程
- 新增带3次重试和递增延迟的createZulipAccountWithRetry方法
- Zulip失败只记录日志,不影响用户注册成功
- 更新API文档,补充80+接口覆盖聊天/通知/位置等模块
- 添加.claude/和test-*.ps1到.gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-09 11:04:57 +08:00
fc1566e616 Merge pull request 'docs/ai-reading-improvements-20260119' (#52) from docs/ai-reading-improvements-20260119 into main
Reviewed-on: #52
2026-01-19 18:38:00 +08:00
moyin
7cac8ad8a5 docs(ai-reading): 完善AI代码检查规范文档
范围: docs/ai-reading/
涉及文件:
- README.md
- step7-code-commit.md

主要改进:
- 增强执行前强制性检查要求,添加明确的检查点
- 完善Step 0的执行流程和验证机制
- 强化代码提交原则,明确提交所有Git变更的规范
- 优化文档结构,提升可读性和执行指导性
- 添加更清晰的警告信息和错误示例
2026-01-19 18:35:45 +08:00
moyin
cf1b37af78 feat(chat): 实现登录时自动初始化Zulip客户端
范围: src/business/chat/
涉及文件:
- chat.module.ts
- chat.service.ts

主要功能:
- 添加ZulipAccountsModule依赖,支持查询用户Zulip账号
- 实现initializeZulipClientForUser方法,登录时自动初始化Zulip客户端
- 从数据库获取用户Zulip账号信息和API Key
- 优化会话创建流程,使用已创建的Zulip客户端队列ID
- 移除登出时的API Key删除逻辑,保持持久化
- 支持基于目标地图的消息发送(mapId参数)

技术改进:
- 分离Zulip客户端初始化逻辑,提高代码可维护性
- 添加完整的错误处理和日志记录
- 支持用户没有Zulip账号的场景(优雅降级)
2026-01-19 18:29:53 +08:00
moyin
1849415b11 test(chat): 修复测试文件Mock配置
范围: src/business/chat/
涉及文件:
- chat.module.spec.ts
- chat.service.spec.ts

主要改进:
- 添加缺失的ZulipAccountsService Mock配置
- 修复handlePlayerLogout测试,删除过时的deleteApiKey断言
- 删除不再需要的API Key清理失败测试用例
- 添加getUserClient Mock方法
- 设置默认Mock行为,提高测试稳定性
2026-01-19 18:29:27 +08:00
moyin
963e6ca90f refactor(auth): 移除登录注册时的Zulip内存关联逻辑
范围: src/business/auth/
涉及文件:
- src/business/auth/login.service.ts
- src/business/auth/register.service.ts

主要改进:
- 移除登录时建立Zulip内存关联的代码
- 移除注册时建立Zulip内存关联的代码
- 改为在WebSocket连接时由Zulip客户端创建内存关联
- 优化了内存关联的时机,避免不必要的提前创建

技术说明:
- 原逻辑在登录/注册时就建立内存关联,但用户可能不会立即使用Zulip
- 新逻辑延迟到WebSocket连接时创建,更加合理和高效
- 减少了登录/注册流程的复杂度和耦合度
2026-01-19 17:59:58 +08:00
12 changed files with 2880 additions and 674 deletions

4
.gitignore vendored
View File

@@ -46,6 +46,10 @@ coverage/
redis-data/
.kiro/
.claude/
# 本地测试脚本
test-*.ps1
config/
docs/merge-requests

220
REGISTER_ZULIP_CHANGES.md Normal file
View File

@@ -0,0 +1,220 @@
# 注册流程 Zulip 错误处理优化
## 修改概述
优化了用户注册流程中的 Zulip 账号创建逻辑,确保 Zulip 服务的问题不会影响用户注册体验。
## 修改内容
### 1. 异步处理 Zulip 账号创建
**修改前:**
- Zulip 账号创建是同步的,阻塞注册流程
- 如果 Zulip 创建失败,会回滚游戏账号
- 错误信息直接返回给前端用户
**修改后:**
- Zulip 账号创建改为异步处理,不阻塞注册流程
- 游戏账号创建成功后立即返回Zulip 在后台处理
- 用户无感知,注册体验流畅
### 2. 自动重试机制
新增 `createZulipAccountWithRetry()` 方法:
- 最多重试 3 次
- 递增延迟1秒、2秒、3秒
- 每次尝试都记录详细日志
- 所有重试失败后记录最终错误
### 3. 错误信息隔离
**修改前:**
```json
{
"success": false,
"message": "注册失败Zulip账号创建失败 - [具体错误]",
"error_code": "REGISTER_FAILED"
}
```
**修改后:**
```json
{
"success": true,
"data": {
"user": {...},
"access_token": "...",
"message": "注册成功"
}
}
```
用户永远不会看到 Zulip 相关的错误信息。
### 4. 日志记录增强
所有 Zulip 相关操作都有详细日志:
```typescript
// 开始尝试
LOG: 尝试创建Zulip账号 (1/3)
- operationId: 用于追踪
- gameUserId: 游戏用户ID
- email: 用户邮箱
// 失败重试
WARN: Zulip账号创建失败 (1/3)
- error: 具体错误信息
- 1000ms后重试
// 最终失败
ERROR: Zulip账号创建最终失败3
- finalError: 最后一次错误
- note: 用户注册已成功Zulip账号创建失败
```
## 代码变更
### 文件:`src/business/auth/register.service.ts`
#### 1. 移除同步 Zulip 创建和回滚逻辑
```typescript
// 删除的代码
try {
await this.createZulipAccountForUser(...);
} catch (zulipError) {
// 回滚游戏用户注册
await this.loginCoreService.deleteUser(...);
throw new Error(`注册失败Zulip账号创建失败 - ${err.message}`);
}
```
#### 2. 新增异步处理
```typescript
// 新增的代码
if (registerRequest.email && registerRequest.password) {
this.createZulipAccountWithRetry(
authResult.user,
registerRequest.password,
operationId
).then(success => {
// 成功日志
}).catch(err => {
// 失败日志(不影响用户)
});
}
```
#### 3. 新增重试方法
```typescript
private async createZulipAccountWithRetry(
gameUser: Users,
password: string,
operationId: string,
maxRetries: number = 3
): Promise<boolean> {
// 重试逻辑
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
await this.createZulipAccountForUser(gameUser, password);
return true;
} catch (error) {
// 记录日志并重试
if (attempt < maxRetries) {
await this.delay(attempt * 1000);
}
}
}
return false;
}
```
## 测试验证
### 测试场景 1正常注册无邮箱
```bash
POST /auth/register
{
"username": "testuser",
"password": "Test123456",
"nickname": "测试用户"
}
响应:
{
"success": true,
"data": {
"user": {...},
"access_token": "...",
"message": "注册成功" // ✓ 不包含 Zulip 信息
}
}
```
### 测试场景 2Zulip 服务不可用
**行为:**
1. 用户注册成功
2. 后台尝试创建 Zulip 账号
3. 失败后自动重试 3 次
4. 所有失败都记录在服务器日志
5. 用户收到成功响应
**日志示例:**
```
LOG: 尝试创建Zulip账号 (第1/3次)
WARN: Zulip账号创建失败 (第1/3次尝试)
LOG: 等待1000ms后重试
LOG: 尝试创建Zulip账号 (第2/3次)
WARN: Zulip账号创建失败 (第2/3次尝试)
LOG: 等待2000ms后重试
LOG: 尝试创建Zulip账号 (第3/3次)
WARN: Zulip账号创建失败 (第3/3次尝试)
ERROR: Zulip账号创建最终失败已尝试3次
```
## 优势
### 1. 用户体验
- ✅ 注册流程不受 Zulip 服务影响
- ✅ 响应速度快(不等待 Zulip
- ✅ 错误信息友好(不暴露技术细节)
### 2. 系统稳定性
- ✅ Zulip 故障不影响核心注册功能
- ✅ 自动重试提高成功率
- ✅ 降级处理保证服务可用性
### 3. 可维护性
- ✅ 详细的日志便于问题排查
- ✅ 清晰的错误追踪operationId
- ✅ 独立的重试逻辑易于调整
## 注意事项
1. **Zulip 账号创建失败的用户**
- 可以正常使用游戏
- 无法使用聊天功能
- 需要开发者手动处理或实现补偿机制
2. **监控建议**
- 定期检查 Zulip 创建失败的日志
- 统计失败率,及时发现 Zulip 服务问题
- 考虑实现告警机制
3. **后续优化方向**
- 实现定时任务补偿创建失败的 Zulip 账号
- 添加管理员界面查看 Zulip 创建状态
- 提供手动重试接口
## 修改日期
2026-02-08
## 修改者
AI Assistant (Kiro)

View File

@@ -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!**

View File

@@ -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. 文件修改记录校验
**重要**检查每个修改文件头部信息是否与实际修改内容一致
**注意**如果变更不是本次代码检查产生的,文件头部可能没有更新修改记录,这是正常的。
只需要检查变更内容,生成准确的提交信息即可。
#### 校验内容包括:
- **修改记录**:最新的修改记录是否准确描述了本次变更

View File

@@ -1,12 +1,12 @@
# API接口文档
本目录包含了 Whale Town 像素游戏服务器的完整API文档采用业务功能模块化设计提供17个接口覆盖所有核心功能。
本目录包含了 Whale Town 像素游戏服务器的完整API文档采用业务功能模块化设计提供80+个接口覆盖所有核心功能。
## 📋 文档文件说明
### 1. api-documentation.md
详细的API接口文档包含
- **17个API接口** - 用户认证、用户管理、管理员功能、安全防护
- **80+个API接口** - 用户认证、用户管理、管理员功能、聊天系统、位置广播、通知系统、Zulip集成、数据库管理、操作日志
- 接口概述和通用响应格式
- 每个接口的详细说明、参数、响应示例
- 错误代码说明和状态码映射
@@ -65,18 +65,21 @@ openapi-generator generate -i docs/api/openapi.yaml -g typescript-axios -o ./cli
## 📊 API接口概览
### 🔐 用户认证模块 (9个接口)
### 🔐 用户认证模块 (12个接口)
| 接口 | 方法 | 路径 | 描述 |
|------|------|------|------|
| 用户登录 | POST | /auth/login | 支持用户名、邮箱或手机号登录 |
| 用户注册 | POST | /auth/register | 创建新用户账户 |
| GitHub OAuth | POST | /auth/github | 使用GitHub账户登录或注册 |
| 发送重置验证码 | POST | /auth/forgot-password | 发送密码重置验证码 |
| 重置密码 | POST | /auth/reset-password | 使用验证码重置密码 |
| 修改密码 | PUT | /auth/change-password | 修改用户密码 |
| 验证码登录 | POST | /auth/verification-code-login | 使用验证码登录 |
| 发送登录验证码 | POST | /auth/send-login-verification-code | 发送登录验证码 |
| 发送邮箱验证码 | POST | /auth/send-email-verification | 发送邮箱验证码 |
| 验证邮箱 | POST | /auth/verify-email | 验证邮箱验证码 |
| 重发邮箱验证码 | POST | /auth/resend-email-verification | 重新发送邮箱验证码 |
| 发送重置验证码 | POST | /auth/forgot-password | 发送密码重置验证码 |
| 重置密码 | POST | /auth/reset-password | 使用验证码重置密码 |
| 修改密码 | PUT | /auth/change-password | 修改用户密码 |
| 刷新令牌 | POST | /auth/refresh-token | 刷新访问令牌 |
### 👥 用户管理模块 (3个接口)
| 接口 | 方法 | 路径 | 描述 |
@@ -85,13 +88,83 @@ openapi-generator generate -i docs/api/openapi.yaml -g typescript-axios -o ./cli
| 批量修改状态 | POST | /admin/users/batch-status | 批量修改用户状态 |
| 用户状态统计 | GET | /admin/users/status-stats | 获取各状态用户统计 |
### 🛡️ 管理员模块 (4个接口)
### 🛡️ 管理员模块 (6个接口)
| 接口 | 方法 | 路径 | 描述 |
|------|------|------|------|
| 管理员登录 | POST | /admin/auth/login | 管理员身份认证 |
| 获取用户列表 | GET | /admin/users | 分页获取用户列表 |
| 获取用户详情 | GET | /admin/users/:id | 获取指定用户信息 |
| 重置用户密码 | POST | /admin/users/:id/reset-password | 管理员重置用户密码 |
| 获取运行时日志 | GET | /admin/logs/runtime | 获取运行日志 |
| 下载日志归档 | GET | /admin/logs/archive | 下载全部日志 |
### 💬 聊天系统 (3个接口)
| 接口 | 方法 | 路径 | 描述 |
|------|------|------|------|
| 获取聊天历史 | GET | /chat/history | 获取聊天历史记录 |
| 获取系统状态 | GET | /chat/status | 获取聊天系统状态 |
| WebSocket信息 | GET | /chat/websocket/info | 获取WebSocket连接信息 |
### 📍 位置广播 (9个接口)
| 接口 | 方法 | 路径 | 描述 |
|------|------|------|------|
| 创建会话 | POST | /location-broadcast/sessions | 创建新的游戏会话 |
| 查询会话列表 | GET | /location-broadcast/sessions | 查询游戏会话列表 |
| 获取会话详情 | GET | /location-broadcast/sessions/:sessionId | 获取会话详细信息 |
| 更新会话配置 | PUT | /location-broadcast/sessions/:sessionId/config | 更新会话配置 |
| 结束会话 | DELETE | /location-broadcast/sessions/:sessionId | 结束游戏会话 |
| 查询位置信息 | GET | /location-broadcast/positions | 查询用户位置 |
| 位置统计 | GET | /location-broadcast/positions/stats | 获取位置统计 |
| 位置历史 | GET | /location-broadcast/users/:userId/position-history | 获取位置历史 |
| 清理用户数据 | DELETE | /location-broadcast/users/:userId/data | 清理用户数据 |
### 🔔 通知系统 (7个接口)
| 接口 | 方法 | 路径 | 描述 |
|------|------|------|------|
| 创建通知 | POST | /api/notices | 创建新通知 |
| 获取通知列表 | GET | /api/notices | 获取通知列表 |
| 未读通知数量 | GET | /api/notices/unread-count | 获取未读数量 |
| 获取通知详情 | GET | /api/notices/:id | 获取通知详情 |
| 标记已读 | PATCH | /api/notices/:id/read | 标记为已读 |
| 发送系统通知 | POST | /api/notices/system | 发送系统通知 |
| 发送广播通知 | POST | /api/notices/broadcast | 发送广播通知 |
### 🔗 Zulip集成 (17个接口)
| 接口 | 方法 | 路径 | 描述 |
|------|------|------|------|
| 创建关联 | POST | /zulip-accounts | 创建Zulip账号关联 |
| 查询关联列表 | GET | /zulip-accounts | 查询关联列表 |
| 获取关联详情 | GET | /zulip-accounts/:id | 根据ID获取关联 |
| 根据游戏用户ID获取 | GET | /zulip-accounts/game-user/:gameUserId | 根据游戏用户ID获取 |
| 根据Zulip用户ID获取 | GET | /zulip-accounts/zulip-user/:zulipUserId | 根据Zulip用户ID获取 |
| 根据邮箱获取 | GET | /zulip-accounts/zulip-email/:zulipEmail | 根据邮箱获取 |
| 更新关联 | PUT | /zulip-accounts/:id | 更新关联信息 |
| 根据游戏用户ID更新 | PUT | /zulip-accounts/game-user/:gameUserId | 根据游戏用户ID更新 |
| 删除关联 | DELETE | /zulip-accounts/:id | 删除关联 |
| 根据游戏用户ID删除 | DELETE | /zulip-accounts/game-user/:gameUserId | 根据游戏用户ID删除 |
| 需要验证的账号 | GET | /zulip-accounts/management/verification-needed | 获取需要验证的账号 |
| 错误状态账号 | GET | /zulip-accounts/management/error-accounts | 获取错误状态账号 |
| 批量更新状态 | PUT | /zulip-accounts/management/batch-status | 批量更新状态 |
| 账号统计 | GET | /zulip-accounts/management/statistics | 获取统计信息 |
| 验证账号 | POST | /zulip-accounts/management/verify | 验证账号有效性 |
| 检查邮箱存在 | GET | /zulip-accounts/validation/email-exists/:email | 检查邮箱是否存在 |
| 检查用户ID存在 | GET | /zulip-accounts/validation/zulip-user-exists/:zulipUserId | 检查用户ID是否存在 |
### 🗄️ 数据库管理 (6个接口)
| 接口 | 方法 | 路径 | 描述 |
|------|------|------|------|
| 获取用户列表 | GET | /admin/database/users | 获取用户列表 |
| 获取用户详情 | GET | /admin/database/users/:id | 获取用户详情 |
| 搜索用户 | GET | /admin/database/users/search | 搜索用户 |
| 创建用户 | POST | /admin/database/users | 创建新用户 |
| 更新用户 | PUT | /admin/database/users/:id | 更新用户信息 |
| 删除用户 | DELETE | /admin/database/users/:id | 删除用户 |
### 📝 操作日志 (2个接口)
| 接口 | 方法 | 路径 | 描述 |
|------|------|------|------|
| 获取日志列表 | GET | /admin/operation-logs | 获取操作日志列表 |
| 获取日志详情 | GET | /admin/operation-logs/:id | 获取日志详情 |
### 📊 系统状态 (1个接口)
| 接口 | 方法 | 路径 | 描述 |
@@ -188,6 +261,9 @@ console.log('登录结果:', loginData);
5. **测试模式**: 邮件服务支持测试模式,验证码会在控制台输出
6. **存储模式**: 支持Redis文件存储和内存数据库便于无依赖测试
7. **安全防护**: 实现了维护模式、内容类型检查、超时控制等安全机制
8. **WebSocket**: 实时功能聊天、位置广播推荐使用WebSocket接口
9. **Zulip集成**: 支持与Zulip聊天系统的完整集成
10. **操作审计**: 管理员操作自动记录审计日志
## 🔄 更新文档

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -48,7 +48,6 @@ const ERROR_CODES = {
const MESSAGES = {
REGISTER_SUCCESS: '注册成功',
REGISTER_SUCCESS_WITH_ZULIP: '注册成功Zulip账号已同步创建',
EMAIL_VERIFICATION_SUCCESS: '邮箱验证成功',
CODE_SENT: '验证码已发送,请查收',
EMAIL_CODE_SENT: '验证码已发送,请查收邮件',
@@ -136,53 +135,38 @@ export class RegisterService {
// 2. 调用核心服务进行注册
const authResult = await this.loginCoreService.register(registerRequest);
// 3. 创建Zulip账号使用相同的邮箱和密码
let zulipAccountCreated = false;
// 3. 创建Zulip账号使用相同的邮箱和密码- 异步处理,不影响注册流程
if (registerRequest.email && registerRequest.password) {
try {
await this.createZulipAccountForUser(authResult.user, registerRequest.password);
zulipAccountCreated = true;
this.logger.log(`Zulip账号创建成功`, {
// 异步创建Zulip账号不阻塞注册流程
this.createZulipAccountWithRetry(
authResult.user,
registerRequest.password,
operationId
).then(success => {
if (success) {
this.logger.log(`Zulip账号异步创建成功`, {
operation: 'register',
operationId,
gameUserId: authResult.user.id.toString(),
email: registerRequest.email,
});
}
}).catch(err => {
// 错误已在重试方法中记录这里只是确保Promise不会未处理
this.logger.warn(`Zulip账号异步创建最终失败`, {
operation: 'register',
operationId,
gameUserId: authResult.user.id.toString(),
email: registerRequest.email,
});
} catch (zulipError) {
const err = zulipError as Error;
this.logger.error(`Zulip账号创建失败,开始回滚`, {
operation: 'register',
operationId,
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
zulipError: err.message,
}, err.stack);
// 回滚游戏用户注册
try {
await this.loginCoreService.deleteUser(authResult.user.id);
this.logger.log(`游戏用户注册回滚成功`, {
operation: 'register',
operationId,
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
});
} catch (rollbackError) {
const rollbackErr = rollbackError as Error;
this.logger.error(`游戏用户注册回滚失败`, {
operation: 'register',
operationId,
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
rollbackError: rollbackErr.message,
}, rollbackErr.stack);
}
// 抛出原始错误
throw new Error(`注册失败Zulip账号创建失败 - ${err.message}`);
}
});
this.logger.log(`Zulip账号创建已提交到后台异步处理`, {
operation: 'register',
operationId,
gameUserId: authResult.user.id.toString(),
email: registerRequest.email,
});
} else {
this.logger.log(`跳过Zulip账号创建缺少邮箱或密码`, {
operation: 'register',
@@ -203,7 +187,7 @@ export class RegisterService {
expires_in: tokenPair.expires_in,
token_type: tokenPair.token_type,
is_new_user: true,
message: zulipAccountCreated ? MESSAGES.REGISTER_SUCCESS_WITH_ZULIP : MESSAGES.REGISTER_SUCCESS
message: MESSAGES.REGISTER_SUCCESS
};
const duration = Date.now() - startTime;
@@ -214,7 +198,6 @@ export class RegisterService {
gameUserId: authResult.user.id.toString(),
username: authResult.user.username,
email: authResult.user.email,
zulipAccountCreated,
duration,
timestamp: new Date().toISOString(),
});
@@ -437,6 +420,103 @@ export class RegisterService {
}
}
/**
* 带重试机制的异步创建Zulip账号
*
* 功能描述:
* 异步创建Zulip账号失败时自动重试最多3次
* 所有错误只记录日志,不影响用户注册流程
*
* @param gameUser 游戏用户信息
* @param password 用户密码
* @param operationId 操作ID用于日志追踪
* @param maxRetries 最大重试次数默认3次
* @returns Promise<boolean> 是否创建成功
* @private
*/
private async createZulipAccountWithRetry(
gameUser: Users,
password: string,
operationId: string,
maxRetries: number = 3
): Promise<boolean> {
let lastError: Error | null = null;
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
this.logger.log(`尝试创建Zulip账号 (第${attempt}/${maxRetries}次)`, {
operation: 'createZulipAccountWithRetry',
operationId,
attempt,
maxRetries,
gameUserId: gameUser.id.toString(),
email: gameUser.email,
});
await this.createZulipAccountForUser(gameUser, password);
this.logger.log(`Zulip账号创建成功 (第${attempt}次尝试)`, {
operation: 'createZulipAccountWithRetry',
operationId,
attempt,
gameUserId: gameUser.id.toString(),
email: gameUser.email,
});
return true;
} catch (error) {
lastError = error as Error;
this.logger.warn(`Zulip账号创建失败 (第${attempt}/${maxRetries}次尝试)`, {
operation: 'createZulipAccountWithRetry',
operationId,
attempt,
maxRetries,
gameUserId: gameUser.id.toString(),
email: gameUser.email,
error: lastError.message,
});
// 如果不是最后一次尝试,等待后重试
if (attempt < maxRetries) {
const delayMs = attempt * 1000; // 递增延迟1秒、2秒、3秒
this.logger.log(`等待${delayMs}ms后重试`, {
operation: 'createZulipAccountWithRetry',
operationId,
attempt,
delayMs,
});
await this.delay(delayMs);
}
}
}
// 所有重试都失败
this.logger.error(`Zulip账号创建最终失败已尝试${maxRetries}`, {
operation: 'createZulipAccountWithRetry',
operationId,
maxRetries,
gameUserId: gameUser.id.toString(),
email: gameUser.email,
finalError: lastError?.message,
note: '用户注册已成功但Zulip账号创建失败。用户可以正常使用游戏但无法使用聊天功能。',
}, lastError?.stack);
return false;
}
/**
* 延迟工具方法
*
* @param ms 延迟毫秒数
* @returns Promise<void>
* @private
*/
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* 为用户创建或绑定Zulip账号
*
@@ -533,15 +613,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;

View File

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

View File

@@ -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: [
// 主聊天服务

View File

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

View File

@@ -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();
// 尝试获取已创建的Zulip客户端的队列ID
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 });
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(