Compare commits
21 Commits
97ea698f38
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fc1566e616 | |||
|
|
7cac8ad8a5 | ||
|
|
cf1b37af78 | ||
|
|
1849415b11 | ||
|
|
963e6ca90f | ||
|
|
cd2a197288 | ||
| 01787d701c | |||
| 6e7de1a11a | |||
|
|
d92a078fc7 | ||
| 9785908ca9 | |||
| 592a745b8f | |||
|
|
cde20c6fd7 | ||
|
|
a8de2564b6 | ||
|
|
9f4d291619 | ||
|
|
4f18f0fec6 | ||
|
|
519394645a | ||
|
|
223ba2abb8 | ||
|
|
e54d5e3939 | ||
| 299627dac7 | |||
| ae3a256c52 | |||
|
|
434766beb5 |
@@ -1,9 +1,17 @@
|
|||||||
# AI Code Inspection Guide - Whale Town Game Server
|
# AI Code Inspection Guide - Whale Town Game Server
|
||||||
|
|
||||||
## 🎯 Pre-execution Setup
|
## ⚠️ 🚨 CRITICAL: MANDATORY PRE-EXECUTION REQUIREMENTS 🚨 ⚠️
|
||||||
|
|
||||||
### 🚀 User Information Setup
|
**<EFBFBD> AI MUST READ THIS SECTION FIRST - EXECUTION WITHOUT COMPLETING THESE STEPS IS STRICTLY FORBIDDEN 🔴**
|
||||||
**Before starting any inspection steps, run the user information script:**
|
|
||||||
|
**⛔ 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
|
```bash
|
||||||
# Enter AI-reading directory
|
# Enter AI-reading directory
|
||||||
@@ -13,6 +21,13 @@ cd docs/ai-reading
|
|||||||
node tools/setup-user-info.js
|
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
|
#### Script Functions
|
||||||
- Automatically get current date (YYYY-MM-DD format)
|
- Automatically get current date (YYYY-MM-DD format)
|
||||||
- Check if config file exists or date matches
|
- 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})`;
|
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:
|
This project is a **NestJS Game Server** with the following features:
|
||||||
- **Dual-mode Architecture**: Supports both database and memory modes
|
- **Dual-mode Architecture**: Supports both database and memory modes
|
||||||
- **Real-time Communication**: WebSocket-based real-time bidirectional communication
|
- **Real-time Communication**: WebSocket-based real-time bidirectional communication
|
||||||
- **Property Testing**: Admin modules use fast-check for randomized testing
|
- **Property Testing**: Admin modules use fast-check for randomized testing
|
||||||
- **Layered Architecture**: Core layer (technical implementation) + Business layer (business logic)
|
- **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
|
## 🔄 Execution Principles
|
||||||
|
|
||||||
### 🚨 Mid-step Start Requirements (Important)
|
### 🚨 Mid-step Start Requirements (Important)
|
||||||
@@ -95,9 +146,19 @@ Start Executing Specified Step
|
|||||||
```
|
```
|
||||||
User Requests Code Inspection
|
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
|
Execute Step 1 → Provide Report → Wait for Confirmation
|
||||||
↓
|
↓
|
||||||
@@ -132,7 +193,18 @@ Execute Step 7 → Provide Report → Wait for Confirmation
|
|||||||
|
|
||||||
## 📚 Step Execution Guide
|
## 📚 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
|
### 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`
|
**Read when executing:** `step1-naming-convention.md`
|
||||||
**Focus on:** Folder structure flattening, game server special file types
|
**Focus on:** Folder structure flattening, game server special file types
|
||||||
**After completion:** Provide inspection report, wait for user confirmation
|
**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
|
### Step 7: Code Commit
|
||||||
**Read when executing:** `step7-code-commit.md`
|
**Read when executing:** `step7-code-commit.md`
|
||||||
**Focus on:** Git change verification, modification record consistency check, standardized commit process
|
**Focus on:** Git change detection, branch management, commit message standards, merge document generation
|
||||||
**After completion:** Provide inspection report, wait for user confirmation
|
**🚨 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
|
## 📋 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!**
|
||||||
@@ -177,6 +177,227 @@ private validateUserData(userData: CreateUserDto | UpdateUserDto): void {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🚨 异常处理完整性检查(关键规范)
|
||||||
|
|
||||||
|
### 问题定义
|
||||||
|
**异常吞没(Exception Swallowing)** 是指在 catch 块中捕获异常后,只记录日志但不重新抛出,导致:
|
||||||
|
- 调用方无法感知错误
|
||||||
|
- 方法返回 undefined 而非声明的类型
|
||||||
|
- 数据不一致或静默失败
|
||||||
|
- 难以调试和定位问题
|
||||||
|
|
||||||
|
### 检查规则
|
||||||
|
|
||||||
|
#### 规则1:catch 块必须有明确的异常处理策略
|
||||||
|
```typescript
|
||||||
|
// ❌ 严重错误:catch 块吞没异常
|
||||||
|
async create(createDto: CreateDto): Promise<ResponseDto> {
|
||||||
|
try {
|
||||||
|
const result = await this.repository.create(createDto);
|
||||||
|
return this.toResponseDto(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('创建失败', error);
|
||||||
|
// 错误:没有 throw,方法返回 undefined
|
||||||
|
// 但声明返回 Promise<ResponseDto>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 错误:只记录日志不处理
|
||||||
|
async findById(id: string): Promise<Entity> {
|
||||||
|
try {
|
||||||
|
return await this.repository.findById(id);
|
||||||
|
} catch (error) {
|
||||||
|
monitor.error(error);
|
||||||
|
// 错误:异常被吞没,调用方无法感知
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确:重新抛出异常
|
||||||
|
async create(createDto: CreateDto): Promise<ResponseDto> {
|
||||||
|
try {
|
||||||
|
const result = await this.repository.create(createDto);
|
||||||
|
return this.toResponseDto(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('创建失败', error);
|
||||||
|
throw error; // 必须重新抛出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确:转换为特定异常类型
|
||||||
|
async create(createDto: CreateDto): Promise<ResponseDto> {
|
||||||
|
try {
|
||||||
|
const result = await this.repository.create(createDto);
|
||||||
|
return this.toResponseDto(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('创建失败', error);
|
||||||
|
if (error.message.includes('duplicate')) {
|
||||||
|
throw new ConflictException('记录已存在');
|
||||||
|
}
|
||||||
|
throw error; // 其他错误继续抛出
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确:返回错误响应(仅限顶层API)
|
||||||
|
async create(createDto: CreateDto): Promise<ApiResponse<ResponseDto>> {
|
||||||
|
try {
|
||||||
|
const result = await this.repository.create(createDto);
|
||||||
|
return { success: true, data: this.toResponseDto(result) };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('创建失败', error);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: error.message,
|
||||||
|
errorCode: 'CREATE_FAILED'
|
||||||
|
}; // 顶层API可以返回错误响应
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 规则2:Service 层方法必须传播异常
|
||||||
|
```typescript
|
||||||
|
// ❌ 错误:Service 层吞没异常
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
async update(id: string, dto: UpdateDto): Promise<ResponseDto> {
|
||||||
|
try {
|
||||||
|
const result = await this.repository.update(id, dto);
|
||||||
|
return this.toResponseDto(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('更新失败', { id, error });
|
||||||
|
// 错误:Service 层不应吞没异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确:Service 层传播异常
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
async update(id: string, dto: UpdateDto): Promise<ResponseDto> {
|
||||||
|
try {
|
||||||
|
const result = await this.repository.update(id, dto);
|
||||||
|
return this.toResponseDto(result);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('更新失败', { id, error });
|
||||||
|
throw error; // 传播给调用方处理
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 规则3:Repository 层必须传播数据库异常
|
||||||
|
```typescript
|
||||||
|
// ❌ 错误:Repository 层吞没数据库异常
|
||||||
|
@Injectable()
|
||||||
|
export class UserRepository {
|
||||||
|
async findById(id: bigint): Promise<User | null> {
|
||||||
|
try {
|
||||||
|
return await this.repository.findOne({ where: { id } });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('查询失败', { id, error });
|
||||||
|
// 错误:数据库异常被吞没,调用方以为查询成功但返回 null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确:Repository 层传播异常
|
||||||
|
@Injectable()
|
||||||
|
export class UserRepository {
|
||||||
|
async findById(id: bigint): Promise<User | null> {
|
||||||
|
try {
|
||||||
|
return await this.repository.findOne({ where: { id } });
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('查询失败', { id, error });
|
||||||
|
throw error; // 数据库异常必须传播
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 异常处理层级规范
|
||||||
|
|
||||||
|
| 层级 | 异常处理策略 | 说明 |
|
||||||
|
|------|-------------|------|
|
||||||
|
| **Repository 层** | 必须 throw | 数据访问异常必须传播 |
|
||||||
|
| **Service 层** | 必须 throw | 业务异常必须传播给调用方 |
|
||||||
|
| **Business 层** | 必须 throw | 业务逻辑异常必须传播 |
|
||||||
|
| **Gateway/Controller 层** | 可以转换为 HTTP 响应 | 顶层可以将异常转换为错误响应 |
|
||||||
|
|
||||||
|
### 检查清单
|
||||||
|
|
||||||
|
- [ ] **所有 catch 块是否有 throw 语句?**
|
||||||
|
- [ ] **方法返回类型与实际返回是否一致?**(避免返回 undefined)
|
||||||
|
- [ ] **Service/Repository 层是否传播异常?**
|
||||||
|
- [ ] **只有顶层 API 才能将异常转换为错误响应?**
|
||||||
|
- [ ] **异常日志是否包含足够的上下文信息?**
|
||||||
|
|
||||||
|
### 快速检查命令
|
||||||
|
```bash
|
||||||
|
# 搜索可能吞没异常的 catch 块(没有 throw 的 catch)
|
||||||
|
# 在代码审查时重点关注这些位置
|
||||||
|
grep -rn "catch.*error" --include="*.ts" | grep -v "throw"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见错误模式
|
||||||
|
|
||||||
|
#### 模式1:性能监控后忘记抛出
|
||||||
|
```typescript
|
||||||
|
// ❌ 常见错误
|
||||||
|
} catch (error) {
|
||||||
|
monitor.error(error); // 只记录监控
|
||||||
|
// 忘记 throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
} catch (error) {
|
||||||
|
monitor.error(error);
|
||||||
|
throw error; // 必须抛出
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 模式2:条件分支遗漏 throw
|
||||||
|
```typescript
|
||||||
|
// ❌ 常见错误
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'DUPLICATE') {
|
||||||
|
throw new ConflictException('已存在');
|
||||||
|
}
|
||||||
|
// else 分支忘记 throw
|
||||||
|
this.logger.error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
} catch (error) {
|
||||||
|
if (error.code === 'DUPLICATE') {
|
||||||
|
throw new ConflictException('已存在');
|
||||||
|
}
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error; // else 分支也要抛出
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 模式3:返回类型不匹配
|
||||||
|
```typescript
|
||||||
|
// ❌ 错误:声明返回 Promise<Entity> 但可能返回 undefined
|
||||||
|
async findById(id: string): Promise<Entity> {
|
||||||
|
try {
|
||||||
|
return await this.repo.findById(id);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
// 没有 throw,TypeScript 不会报错但运行时返回 undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ✅ 正确
|
||||||
|
async findById(id: string): Promise<Entity> {
|
||||||
|
try {
|
||||||
|
return await this.repo.findById(id);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## 🚫 TODO项处理(强制要求)
|
## 🚫 TODO项处理(强制要求)
|
||||||
|
|
||||||
### 处理原则
|
### 处理原则
|
||||||
@@ -323,12 +544,19 @@ describe('AdminService Properties', () => {
|
|||||||
- 抽象为可复用的工具方法
|
- 抽象为可复用的工具方法
|
||||||
- 消除代码重复
|
- 消除代码重复
|
||||||
|
|
||||||
6. **处理所有TODO项**
|
6. **🚨 检查异常处理完整性(关键步骤)**
|
||||||
|
- 扫描所有 catch 块
|
||||||
|
- 检查是否有 throw 语句
|
||||||
|
- 验证 Service/Repository 层是否传播异常
|
||||||
|
- 确认方法返回类型与实际返回一致
|
||||||
|
- 识别异常吞没模式并修复
|
||||||
|
|
||||||
|
7. **处理所有TODO项**
|
||||||
- 搜索所有TODO注释
|
- 搜索所有TODO注释
|
||||||
- 要求真正实现功能或删除代码
|
- 要求真正实现功能或删除代码
|
||||||
- 确保最终文件无TODO项
|
- 确保最终文件无TODO项
|
||||||
|
|
||||||
7. **游戏服务器特殊检查**
|
8. **游戏服务器特殊检查**
|
||||||
- WebSocket连接管理完整性
|
- WebSocket连接管理完整性
|
||||||
- 双模式服务行为一致性
|
- 双模式服务行为一致性
|
||||||
- 属性测试实现质量
|
- 属性测试实现质量
|
||||||
|
|||||||
@@ -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
|
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. 文件修改记录校验
|
### 2. 文件修改记录校验
|
||||||
**重要**:检查每个修改文件的头部信息是否与实际修改内容一致
|
**注意**:如果变更不是本次代码检查产生的,文件头部可能没有更新修改记录,这是正常的。
|
||||||
|
只需要检查变更内容,生成准确的提交信息即可。
|
||||||
|
|
||||||
#### 校验内容包括:
|
#### 校验内容包括:
|
||||||
- **修改记录**:最新的修改记录是否准确描述了本次变更
|
- **修改记录**:最新的修改记录是否准确描述了本次变更
|
||||||
@@ -505,6 +553,37 @@ mkdir -p docs/merge-requests
|
|||||||
- **监控要点**:关注 [具体的监控指标]
|
- **监控要点**:关注 [具体的监控指标]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 🚨 合并文档不纳入Git提交
|
||||||
|
**重要:合并文档仅用于本地记录和合并操作参考,不应加入到Git提交中!**
|
||||||
|
|
||||||
|
#### 原因说明
|
||||||
|
- 合并文档是临时性的操作记录,不属于项目代码的一部分
|
||||||
|
- 避免在代码仓库中产生大量临时文档
|
||||||
|
- 合并完成后,相关信息已体现在Git提交历史和PR记录中
|
||||||
|
|
||||||
|
#### 操作规范
|
||||||
|
```bash
|
||||||
|
# ❌ 禁止将合并文档加入Git提交
|
||||||
|
git add docs/merge-requests/ # 禁止!
|
||||||
|
|
||||||
|
# ✅ 正确做法:确保合并文档不被提交
|
||||||
|
# 方法1:在.gitignore中已配置忽略(推荐)
|
||||||
|
# 方法2:提交时明确排除
|
||||||
|
git add . -- ':!docs/merge-requests/'
|
||||||
|
|
||||||
|
# ✅ 检查暂存区,确认没有合并文档
|
||||||
|
git diff --cached --name-only | grep "merge-requests"
|
||||||
|
# 如果有输出,需要取消暂存
|
||||||
|
git reset HEAD docs/merge-requests/
|
||||||
|
```
|
||||||
|
|
||||||
|
#### .gitignore 配置建议
|
||||||
|
确保项目的 `.gitignore` 文件中包含:
|
||||||
|
```
|
||||||
|
# 合并文档目录(不纳入版本控制)
|
||||||
|
docs/merge-requests/
|
||||||
|
```
|
||||||
|
|
||||||
### 📝 独立合并文档创建示例
|
### 📝 独立合并文档创建示例
|
||||||
|
|
||||||
#### 1. 创建合并文档目录(如果不存在)
|
#### 1. 创建合并文档目录(如果不存在)
|
||||||
@@ -689,6 +768,7 @@ git remote show [远程仓库名]
|
|||||||
- **完整性**:每次提交的代码都应该能正常运行
|
- **完整性**:每次提交的代码都应该能正常运行
|
||||||
- **描述性**:提交信息要清晰描述改动内容、范围和原因
|
- **描述性**:提交信息要清晰描述改动内容、范围和原因
|
||||||
- **一致性**:文件修改记录必须与实际修改内容一致
|
- **一致性**:文件修改记录必须与实际修改内容一致
|
||||||
|
- **合并文档排除**:`docs/merge-requests/` 目录下的合并文档不纳入Git提交
|
||||||
|
|
||||||
### 质量保证
|
### 质量保证
|
||||||
- 提交前必须验证代码能正常运行
|
- 提交前必须验证代码能正常运行
|
||||||
|
|||||||
@@ -1,151 +0,0 @@
|
|||||||
# 网关模块集成与文档优化合并请求
|
|
||||||
|
|
||||||
## 📋 变更概述
|
|
||||||
本次合并请求包含网关模块的集成工作和命名规范文档的优化,主要涉及应用主模块的架构完善和开发规范文档的更新。
|
|
||||||
|
|
||||||
## 🔍 主要变更内容
|
|
||||||
|
|
||||||
### 功能集成
|
|
||||||
- **网关模块集成**:完善 `src/app.module.ts` 应用主模块
|
|
||||||
- 集成 ChatGatewayModule(聊天网关模块)
|
|
||||||
- 集成 ZulipGatewayModule(Zulip网关模块,提供HTTP API接口)
|
|
||||||
- 优化模块注释,明确各模块职责
|
|
||||||
- 完善应用架构,区分网关层和业务层职责
|
|
||||||
|
|
||||||
### 文档优化
|
|
||||||
- **命名规范文档**:优化 `docs/ai-reading/step1-naming-convention.md`
|
|
||||||
- 调整扁平化标准:从"≤3个文件"改为"1-2个文件"
|
|
||||||
- 明确单文件必须扁平化,双文件建议扁平化
|
|
||||||
- ≥3个文件保持独立文件夹结构
|
|
||||||
- 更新相关检查步骤和常见错误说明
|
|
||||||
|
|
||||||
## 📊 影响范围
|
|
||||||
- **修改文件数量**:2个文件
|
|
||||||
- src/app.module.ts(应用主模块)
|
|
||||||
- docs/ai-reading/step1-naming-convention.md(命名规范文档)
|
|
||||||
- **涉及模块**:应用主模块、网关层
|
|
||||||
- **新增代码行数**:+11行
|
|
||||||
- **删除代码行数**:-6行
|
|
||||||
|
|
||||||
## 🧪 测试验证
|
|
||||||
- [x] 应用启动测试通过
|
|
||||||
- [x] 模块导入无循环依赖
|
|
||||||
- [x] 网关模块功能正常
|
|
||||||
- [x] 文档内容准确性检查通过
|
|
||||||
|
|
||||||
## 📝 提交记录
|
|
||||||
|
|
||||||
### 提交1:文档优化
|
|
||||||
```
|
|
||||||
docs:优化命名规范中的扁平化标准说明
|
|
||||||
|
|
||||||
- 将扁平化标准从≤3个文件调整为1-2个文件
|
|
||||||
- 明确单文件必须扁平化,双文件建议扁平化
|
|
||||||
- ≥3个文件保持独立文件夹结构
|
|
||||||
- 更新相关检查步骤和常见错误说明
|
|
||||||
```
|
|
||||||
|
|
||||||
### 提交2:功能集成
|
|
||||||
```
|
|
||||||
feat:集成聊天和Zulip网关模块到应用主模块
|
|
||||||
|
|
||||||
- 添加ChatGatewayModule到应用模块导入列表
|
|
||||||
- 添加ZulipGatewayModule到应用模块导入列表
|
|
||||||
- 优化模块注释说明,明确各网关模块职责
|
|
||||||
- 完善模块架构,区分网关层和业务层职责
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔗 相关信息
|
|
||||||
- **分支名称**:feature/gateway-module-integration-20260115
|
|
||||||
- **基于分支**:feature/code-standard-zulip-20260114
|
|
||||||
- **创建日期**:2026-01-15
|
|
||||||
- **提交人员**:moyin
|
|
||||||
- **提交数量**:2个提交
|
|
||||||
|
|
||||||
## 📝 文件变更详情
|
|
||||||
|
|
||||||
### 1. src/app.module.ts
|
|
||||||
**变更类型**:功能增强
|
|
||||||
|
|
||||||
**主要变更**:
|
|
||||||
- 新增导入:`ChatGatewayModule`
|
|
||||||
- 新增导入:`ZulipGatewayModule`
|
|
||||||
- 优化模块注释:
|
|
||||||
- `AuthGatewayModule` → `认证网关模块`
|
|
||||||
- 新增 `ChatGatewayModule` → `聊天网关模块`
|
|
||||||
- 新增 `ZulipGatewayModule` → `Zulip网关模块(HTTP API接口)`
|
|
||||||
- `ZulipModule` → `Zulip业务模块(业务逻辑)`
|
|
||||||
|
|
||||||
**架构改进**:
|
|
||||||
- 明确网关层职责:处理HTTP请求和WebSocket连接
|
|
||||||
- 明确业务层职责:处理业务逻辑和数据处理
|
|
||||||
- 完善模块分层架构
|
|
||||||
|
|
||||||
### 2. docs/ai-reading/step1-naming-convention.md
|
|
||||||
**变更类型**:文档优化
|
|
||||||
|
|
||||||
**主要变更**:
|
|
||||||
- 扁平化标准调整:
|
|
||||||
- 旧标准:`≤3个文件:必须扁平化处理`
|
|
||||||
- 新标准:
|
|
||||||
- `1个文件:必须扁平化处理`
|
|
||||||
- `2个文件:建议扁平化处理(除非是完整功能模块)`
|
|
||||||
- `≥3个文件:保持独立文件夹`
|
|
||||||
- 更新常见错误说明:`遗漏≤3个文件文件夹的识别` → `遗漏单文件或双文件文件夹的识别`
|
|
||||||
- 更新检查步骤:`识别需要扁平化的文件夹(≤3个文件)` → `识别需要扁平化的文件夹(1-2个文件)`
|
|
||||||
|
|
||||||
**优化理由**:
|
|
||||||
- 更精确的扁平化标准,避免过度扁平化
|
|
||||||
- 3个文件的文件夹通常代表完整功能模块,应保持独立
|
|
||||||
- 提高代码组织的合理性和可维护性
|
|
||||||
|
|
||||||
## 📋 审查要点
|
|
||||||
请重点关注以下方面:
|
|
||||||
1. **模块集成正确性**:网关模块是否正确导入到应用主模块
|
|
||||||
2. **模块注释准确性**:模块注释是否准确反映模块职责
|
|
||||||
3. **架构合理性**:网关层和业务层职责是否清晰分离
|
|
||||||
4. **文档准确性**:扁平化标准调整是否合理
|
|
||||||
5. **向后兼容性**:变更是否影响现有功能
|
|
||||||
|
|
||||||
## ⚠️ 注意事项
|
|
||||||
- 本次变更为功能增强和文档优化,不涉及破坏性变更
|
|
||||||
- 新增的网关模块已在之前的开发中完成测试
|
|
||||||
- 扁平化标准调整不影响现有代码结构
|
|
||||||
- 建议在合并后验证应用启动和网关功能
|
|
||||||
|
|
||||||
## 🚀 合并后操作
|
|
||||||
1. 验证应用启动正常
|
|
||||||
2. 测试聊天网关功能
|
|
||||||
3. 测试Zulip网关功能
|
|
||||||
4. 确认模块导入无循环依赖
|
|
||||||
5. 通知团队成员架构变更和文档更新
|
|
||||||
|
|
||||||
## 🎯 架构说明
|
|
||||||
|
|
||||||
### 模块分层架构
|
|
||||||
```
|
|
||||||
应用层 (app.module.ts)
|
|
||||||
├── 网关层 (Gateway Layer)
|
|
||||||
│ ├── AuthGatewayModule - 认证网关(HTTP + WebSocket)
|
|
||||||
│ ├── ChatGatewayModule - 聊天网关(WebSocket)
|
|
||||||
│ └── ZulipGatewayModule - Zulip网关(HTTP API)
|
|
||||||
├── 业务层 (Business Layer)
|
|
||||||
│ ├── ZulipModule - Zulip业务逻辑
|
|
||||||
│ ├── UserMgmtModule - 用户管理业务
|
|
||||||
│ └── AdminModule - 管理员业务
|
|
||||||
└── 核心层 (Core Layer)
|
|
||||||
├── LoginCoreModule - 登录核心
|
|
||||||
├── SecurityCoreModule - 安全核心
|
|
||||||
└── RedisModule - Redis核心
|
|
||||||
```
|
|
||||||
|
|
||||||
### 职责划分
|
|
||||||
- **网关层**:处理外部请求,协议转换,请求路由
|
|
||||||
- **业务层**:业务逻辑处理,数据验证,业务规则
|
|
||||||
- **核心层**:基础设施,通用功能,底层服务
|
|
||||||
|
|
||||||
---
|
|
||||||
**文档生成时间**:2026-01-15
|
|
||||||
**对应分支**:feature/gateway-module-integration-20260115
|
|
||||||
**合并状态**:待合并
|
|
||||||
**提交数量**:2个提交(1个文档优化 + 1个功能集成)
|
|
||||||
@@ -6,6 +6,7 @@ import { AppController } from './app.controller';
|
|||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
import { LoggerModule } from './core/utils/logger/logger.module';
|
import { LoggerModule } from './core/utils/logger/logger.module';
|
||||||
import { UsersModule } from './core/db/users/users.module';
|
import { UsersModule } from './core/db/users/users.module';
|
||||||
|
import { ZulipAccountsModule } from './core/db/zulip_accounts/zulip_accounts.module';
|
||||||
import { LoginCoreModule } from './core/login_core/login_core.module';
|
import { LoginCoreModule } from './core/login_core/login_core.module';
|
||||||
import { AuthGatewayModule } from './gateway/auth/auth.gateway.module';
|
import { AuthGatewayModule } from './gateway/auth/auth.gateway.module';
|
||||||
import { ChatGatewayModule } from './gateway/chat/chat.gateway.module';
|
import { ChatGatewayModule } from './gateway/chat/chat.gateway.module';
|
||||||
@@ -62,6 +63,8 @@ function isDatabaseConfigured(): boolean {
|
|||||||
database: process.env.DB_NAME,
|
database: process.env.DB_NAME,
|
||||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
|
// 字符集配置 - 支持中文和emoji
|
||||||
|
charset: 'utf8mb4',
|
||||||
// 添加连接超时和重试配置
|
// 添加连接超时和重试配置
|
||||||
connectTimeout: 10000,
|
connectTimeout: 10000,
|
||||||
retryAttempts: 3,
|
retryAttempts: 3,
|
||||||
@@ -70,6 +73,8 @@ function isDatabaseConfigured(): boolean {
|
|||||||
] : []),
|
] : []),
|
||||||
// 根据数据库配置选择用户模块模式
|
// 根据数据库配置选择用户模块模式
|
||||||
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
|
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
|
||||||
|
// Zulip账号关联模块 - 全局单例,其他模块无需重复导入
|
||||||
|
ZulipAccountsModule.forRoot(),
|
||||||
LoginCoreModule,
|
LoginCoreModule,
|
||||||
AuthGatewayModule, // 认证网关模块
|
AuthGatewayModule, // 认证网关模块
|
||||||
ChatGatewayModule, // 聊天网关模块
|
ChatGatewayModule, // 聊天网关模块
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import { AdminCoreModule } from '../../core/admin_core/admin_core.module';
|
|||||||
import { LoggerModule } from '../../core/utils/logger/logger.module';
|
import { LoggerModule } from '../../core/utils/logger/logger.module';
|
||||||
import { UsersModule } from '../../core/db/users/users.module';
|
import { UsersModule } from '../../core/db/users/users.module';
|
||||||
import { UserProfilesModule } from '../../core/db/user_profiles/user_profiles.module';
|
import { UserProfilesModule } from '../../core/db/user_profiles/user_profiles.module';
|
||||||
import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
|
|
||||||
import { AdminController } from './admin.controller';
|
import { AdminController } from './admin.controller';
|
||||||
import { AdminService } from './admin.service';
|
import { AdminService } from './admin.service';
|
||||||
import { AdminDatabaseController } from './admin_database.controller';
|
import { AdminDatabaseController } from './admin_database.controller';
|
||||||
@@ -55,8 +54,7 @@ function isDatabaseConfigured(): boolean {
|
|||||||
UsersModule,
|
UsersModule,
|
||||||
// 根据数据库配置选择UserProfiles模块模式
|
// 根据数据库配置选择UserProfiles模块模式
|
||||||
isDatabaseConfigured() ? UserProfilesModule.forDatabase() : UserProfilesModule.forMemory(),
|
isDatabaseConfigured() ? UserProfilesModule.forDatabase() : UserProfilesModule.forMemory(),
|
||||||
// 根据数据库配置选择ZulipAccounts模块模式
|
// 注意:ZulipAccountsModule 是全局模块,已在 AppModule 中导入,无需重复导入
|
||||||
isDatabaseConfigured() ? ZulipAccountsModule.forDatabase() : ZulipAccountsModule.forMemory(),
|
|
||||||
// 注册AdminOperationLog实体
|
// 注册AdminOperationLog实体
|
||||||
TypeOrmModule.forFeature([AdminOperationLog])
|
TypeOrmModule.forFeature([AdminOperationLog])
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import { LoginService } from './login.service';
|
|||||||
import { RegisterService } from './register.service';
|
import { RegisterService } from './register.service';
|
||||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||||
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
||||||
import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
|
|
||||||
import { UsersModule } from '../../core/db/users/users.module';
|
import { UsersModule } from '../../core/db/users/users.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@@ -44,7 +43,7 @@ import { UsersModule } from '../../core/db/users/users.module';
|
|||||||
// 导入核心层模块
|
// 导入核心层模块
|
||||||
LoginCoreModule,
|
LoginCoreModule,
|
||||||
ZulipCoreModule,
|
ZulipCoreModule,
|
||||||
ZulipAccountsModule.forRoot(),
|
// 注意:ZulipAccountsModule 是全局模块,已在 AppModule 中导入,无需重复导入
|
||||||
UsersModule,
|
UsersModule,
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
|||||||
@@ -714,13 +714,7 @@ export class LoginService {
|
|||||||
apiKeyResult.apiKey!
|
apiKeyResult.apiKey!
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. 更新内存关联
|
// 注意:不在登录时建立内存关联,Zulip客户端将在WebSocket连接时创建
|
||||||
await this.zulipAccountService.linkGameAccount(
|
|
||||||
user.id.toString(),
|
|
||||||
zulipAccount.zulipUserId,
|
|
||||||
zulipAccount.zulipEmail,
|
|
||||||
apiKeyResult.apiKey!
|
|
||||||
);
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,13 @@
|
|||||||
* - 测试Zulip账号集成
|
* - 测试Zulip账号集成
|
||||||
*
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 代码规范优化 - 清理未使用的变量apiKeySecurityService (修改者: moyin)
|
||||||
* - 2026-01-12: 代码分离 - 从login.service.spec.ts中分离注册相关测试
|
* - 2026-01-12: 代码分离 - 从login.service.spec.ts中分离注册相关测试
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.0.0
|
* @version 1.0.1
|
||||||
* @since 2026-01-12
|
* @since 2026-01-12
|
||||||
* @lastModified 2026-01-12
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
@@ -25,7 +26,6 @@ describe('RegisterService', () => {
|
|||||||
let service: RegisterService;
|
let service: RegisterService;
|
||||||
let loginCoreService: jest.Mocked<LoginCoreService>;
|
let loginCoreService: jest.Mocked<LoginCoreService>;
|
||||||
let zulipAccountService: jest.Mocked<ZulipAccountService>;
|
let zulipAccountService: jest.Mocked<ZulipAccountService>;
|
||||||
let apiKeySecurityService: jest.Mocked<ApiKeySecurityService>;
|
|
||||||
|
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
id: BigInt(1),
|
id: BigInt(1),
|
||||||
@@ -96,7 +96,6 @@ describe('RegisterService', () => {
|
|||||||
service = module.get<RegisterService>(RegisterService);
|
service = module.get<RegisterService>(RegisterService);
|
||||||
loginCoreService = module.get(LoginCoreService);
|
loginCoreService = module.get(LoginCoreService);
|
||||||
zulipAccountService = module.get(ZulipAccountService);
|
zulipAccountService = module.get(ZulipAccountService);
|
||||||
apiKeySecurityService = module.get(ApiKeySecurityService);
|
|
||||||
|
|
||||||
// 设置默认的mock返回值
|
// 设置默认的mock返回值
|
||||||
const mockTokenPair = {
|
const mockTokenPair = {
|
||||||
|
|||||||
@@ -14,16 +14,17 @@
|
|||||||
* - 处理注册相关的邮箱验证和Zulip集成
|
* - 处理注册相关的邮箱验证和Zulip集成
|
||||||
*
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 代码规范优化 - 清理未使用的导入TokenPair,增强userId非空验证 (修改者: moyin)
|
||||||
* - 2026-01-12: 代码分离 - 从login.service.ts中分离注册相关业务逻辑
|
* - 2026-01-12: 代码分离 - 从login.service.ts中分离注册相关业务逻辑
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.0.0
|
* @version 1.0.1
|
||||||
* @since 2026-01-12
|
* @since 2026-01-12
|
||||||
* @lastModified 2026-01-12
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||||
import { LoginCoreService, RegisterRequest, TokenPair } from '../../core/login_core/login_core.service';
|
import { LoginCoreService, RegisterRequest } from '../../core/login_core/login_core.service';
|
||||||
import { Users } from '../../core/db/users/users.entity';
|
import { Users } from '../../core/db/users/users.entity';
|
||||||
import { ZulipAccountService } from '../../core/zulip_core/services/zulip_account.service';
|
import { ZulipAccountService } from '../../core/zulip_core/services/zulip_account.service';
|
||||||
import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service';
|
import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service';
|
||||||
@@ -487,6 +488,11 @@ export class RegisterService {
|
|||||||
throw new Error(createResult.error || 'Zulip账号创建/绑定失败');
|
throw new Error(createResult.error || 'Zulip账号创建/绑定失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证必须获取到 userId(数据库字段 NOT NULL)
|
||||||
|
if (createResult.userId === undefined || createResult.userId === null) {
|
||||||
|
throw new Error('Zulip账号创建成功但未能获取用户ID,无法建立关联');
|
||||||
|
}
|
||||||
|
|
||||||
// 3. 处理API Key
|
// 3. 处理API Key
|
||||||
let finalApiKey = createResult.apiKey;
|
let finalApiKey = createResult.apiKey;
|
||||||
|
|
||||||
@@ -520,22 +526,14 @@ export class RegisterService {
|
|||||||
// 5. 在数据库中创建关联记录
|
// 5. 在数据库中创建关联记录
|
||||||
await this.zulipAccountsService.create({
|
await this.zulipAccountsService.create({
|
||||||
gameUserId: gameUser.id.toString(),
|
gameUserId: gameUser.id.toString(),
|
||||||
zulipUserId: createResult.userId!,
|
zulipUserId: createResult.userId, // 已在上面验证不为 undefined
|
||||||
zulipEmail: createResult.email!,
|
zulipEmail: createResult.email!,
|
||||||
zulipFullName: gameUser.nickname,
|
zulipFullName: gameUser.nickname,
|
||||||
zulipApiKeyEncrypted: finalApiKey ? 'stored_in_redis' : '',
|
zulipApiKeyEncrypted: finalApiKey ? 'stored_in_redis' : '',
|
||||||
status: 'active',
|
status: 'active',
|
||||||
});
|
});
|
||||||
|
|
||||||
// 6. 建立游戏账号与Zulip账号的内存关联(用于当前会话)
|
// 注意:不在注册时建立内存关联,Zulip客户端将在WebSocket连接时创建
|
||||||
if (finalApiKey) {
|
|
||||||
await this.zulipAccountService.linkGameAccount(
|
|
||||||
gameUser.id.toString(),
|
|
||||||
createResult.userId!,
|
|
||||||
createResult.email!,
|
|
||||||
finalApiKey
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
// 尝试获取已创建的Zulip客户端的队列ID
|
||||||
let zulipQueueId = `queue_${sessionId}`;
|
let zulipQueueId = `queue_${sessionId}`;
|
||||||
|
try {
|
||||||
// 尝试创建Zulip客户端
|
const existingClient = await this.zulipClientPool.getUserClient(userInfo.userId);
|
||||||
if (userInfo.zulipApiKey) {
|
if (existingClient?.queueId) {
|
||||||
try {
|
zulipQueueId = existingClient.queueId;
|
||||||
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 });
|
|
||||||
}
|
}
|
||||||
|
} catch (e) {
|
||||||
|
this.logger.debug('获取Zulip客户端队列ID失败,使用默认值', {
|
||||||
|
error: (e as Error).message
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await this.sessionService.createSession(
|
const session = await this.sessionService.createSession(
|
||||||
|
|||||||
@@ -36,7 +36,6 @@ import { ZulipEventProcessorService } from './services/zulip_event_processor.ser
|
|||||||
import { ZulipAccountsBusinessService } from './services/zulip_accounts_business.service';
|
import { ZulipAccountsBusinessService } from './services/zulip_accounts_business.service';
|
||||||
// 依赖模块
|
// 依赖模块
|
||||||
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
import { ZulipCoreModule } from '../../core/zulip_core/zulip_core.module';
|
||||||
import { ZulipAccountsModule } from '../../core/db/zulip_accounts/zulip_accounts.module';
|
|
||||||
import { RedisModule } from '../../core/redis/redis.module';
|
import { RedisModule } from '../../core/redis/redis.module';
|
||||||
import { LoggerModule } from '../../core/utils/logger/logger.module';
|
import { LoggerModule } from '../../core/utils/logger/logger.module';
|
||||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||||
@@ -50,8 +49,7 @@ import { ChatModule } from '../chat/chat.module';
|
|||||||
CacheModule.register(),
|
CacheModule.register(),
|
||||||
// Zulip核心服务模块
|
// Zulip核心服务模块
|
||||||
ZulipCoreModule,
|
ZulipCoreModule,
|
||||||
// Zulip账号关联模块
|
// 注意:ZulipAccountsModule 是全局模块,已在 AppModule 中导入,无需重复导入
|
||||||
ZulipAccountsModule.forRoot(),
|
|
||||||
// Redis模块
|
// Redis模块
|
||||||
RedisModule,
|
RedisModule,
|
||||||
// 日志模块
|
// 日志模块
|
||||||
|
|||||||
@@ -14,13 +14,14 @@
|
|||||||
* - 搜索优化:搜索异常的特殊处理机制
|
* - 搜索优化:搜索异常的特殊处理机制
|
||||||
*
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 代码规范优化 - 为保护方法补充@example示例 (修改者: moyin)
|
||||||
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
|
* - 2026-01-07: 代码规范优化 - 完善注释规范,添加完整的文件头和方法注释
|
||||||
* - 2026-01-07: 功能新增 - 添加敏感信息脱敏处理和结构化日志记录
|
* - 2026-01-07: 功能新增 - 添加敏感信息脱敏处理和结构化日志记录
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.0.1
|
* @version 1.0.2
|
||||||
* @since 2025-01-07
|
* @since 2025-01-07
|
||||||
* @lastModified 2026-01-07
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Logger, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
import { Logger, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
@@ -33,6 +34,12 @@ export abstract class BaseUsersService {
|
|||||||
*
|
*
|
||||||
* @param error 原始错误对象
|
* @param error 原始错误对象
|
||||||
* @returns 格式化后的错误信息字符串
|
* @returns 格式化后的错误信息字符串
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const errorMsg = this.formatError(new Error('数据库连接失败'));
|
||||||
|
* // 返回: "数据库连接失败"
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
protected formatError(error: unknown): string {
|
protected formatError(error: unknown): string {
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
@@ -48,6 +55,15 @@ export abstract class BaseUsersService {
|
|||||||
* @param operation 操作名称
|
* @param operation 操作名称
|
||||||
* @param context 上下文信息
|
* @param context 上下文信息
|
||||||
* @throws 处理后的标准异常
|
* @throws 处理后的标准异常
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* try {
|
||||||
|
* // 业务操作
|
||||||
|
* } catch (error) {
|
||||||
|
* this.handleServiceError(error, '创建用户', { username: 'test' });
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
protected handleServiceError(error: unknown, operation: string, context?: Record<string, any>): never {
|
protected handleServiceError(error: unknown, operation: string, context?: Record<string, any>): never {
|
||||||
const errorMessage = this.formatError(error);
|
const errorMessage = this.formatError(error);
|
||||||
@@ -78,6 +94,15 @@ export abstract class BaseUsersService {
|
|||||||
* @param operation 操作名称
|
* @param operation 操作名称
|
||||||
* @param context 上下文信息
|
* @param context 上下文信息
|
||||||
* @returns 空数组
|
* @returns 空数组
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* try {
|
||||||
|
* // 搜索操作
|
||||||
|
* } catch (error) {
|
||||||
|
* return this.handleSearchError(error, '搜索用户', { keyword: 'test' });
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
protected handleSearchError(error: unknown, operation: string, context?: Record<string, any>): any[] {
|
protected handleSearchError(error: unknown, operation: string, context?: Record<string, any>): any[] {
|
||||||
const errorMessage = this.formatError(error);
|
const errorMessage = this.formatError(error);
|
||||||
@@ -98,6 +123,11 @@ export abstract class BaseUsersService {
|
|||||||
* @param operation 操作名称
|
* @param operation 操作名称
|
||||||
* @param context 上下文信息
|
* @param context 上下文信息
|
||||||
* @param duration 操作耗时
|
* @param duration 操作耗时
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* this.logSuccess('创建用户', { userId: '123', username: 'test' }, 50);
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
protected logSuccess(operation: string, context?: Record<string, any>, duration?: number): void {
|
protected logSuccess(operation: string, context?: Record<string, any>, duration?: number): void {
|
||||||
this.logger.log(`${operation}成功`, {
|
this.logger.log(`${operation}成功`, {
|
||||||
@@ -113,6 +143,11 @@ export abstract class BaseUsersService {
|
|||||||
*
|
*
|
||||||
* @param operation 操作名称
|
* @param operation 操作名称
|
||||||
* @param context 上下文信息
|
* @param context 上下文信息
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* this.logStart('创建用户', { username: 'test' });
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
protected logStart(operation: string, context?: Record<string, any>): void {
|
protected logStart(operation: string, context?: Record<string, any>): void {
|
||||||
this.logger.log(`开始${operation}`, {
|
this.logger.log(`开始${operation}`, {
|
||||||
@@ -127,6 +162,16 @@ export abstract class BaseUsersService {
|
|||||||
*
|
*
|
||||||
* @param data 原始数据
|
* @param data 原始数据
|
||||||
* @returns 脱敏后的数据
|
* @returns 脱敏后的数据
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* const sanitized = this.sanitizeLogData({
|
||||||
|
* email: 'test@example.com',
|
||||||
|
* phone: '13800138000',
|
||||||
|
* password_hash: 'secret'
|
||||||
|
* });
|
||||||
|
* // 返回: { email: 'te***@example.com', phone: '138****00', password_hash: '[REDACTED]' }
|
||||||
|
* ```
|
||||||
*/
|
*/
|
||||||
protected sanitizeLogData(data: Record<string, any>): Record<string, any> {
|
protected sanitizeLogData(data: Record<string, any>): Record<string, any> {
|
||||||
const sanitized = { ...data };
|
const sanitized = { ...data };
|
||||||
|
|||||||
@@ -6,13 +6,19 @@
|
|||||||
* - 避免魔法数字,提高代码可维护性
|
* - 避免魔法数字,提高代码可维护性
|
||||||
* - 集中管理配置参数
|
* - 集中管理配置参数
|
||||||
*
|
*
|
||||||
|
* 职责分离:
|
||||||
|
* - 常量定义:用户角色、字段限制、查询限制等常量值
|
||||||
|
* - 错误消息:统一的错误消息定义和管理
|
||||||
|
* - 工具类:性能监控和验证工具的封装
|
||||||
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 代码规范优化 - 补充职责分离描述 (修改者: moyin)
|
||||||
* - 2026-01-09: 代码质量优化 - 提取魔法数字为常量定义 (修改者: moyin)
|
* - 2026-01-09: 代码质量优化 - 提取魔法数字为常量定义 (修改者: moyin)
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.0.0
|
* @version 1.0.1
|
||||||
* @since 2026-01-09
|
* @since 2026-01-09
|
||||||
* @lastModified 2026-01-09
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { ValidationError } from 'class-validator';
|
import { ValidationError } from 'class-validator';
|
||||||
|
|||||||
@@ -17,22 +17,21 @@
|
|||||||
* - 并发控制:使用悲观锁防止竞态条件
|
* - 并发控制:使用悲观锁防止竞态条件
|
||||||
*
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 代码规范优化 - 清理未使用的导入FindOptionsWhere (修改者: moyin)
|
||||||
* - 2026-01-12: 性能优化 - 集成AppLoggerService,优化查询和批量操作
|
* - 2026-01-12: 性能优化 - 集成AppLoggerService,优化查询和批量操作
|
||||||
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
|
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
|
||||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
|
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
|
||||||
* - 2026-01-07: 功能新增 - 添加事务支持防止并发竞态条件
|
* - 2026-01-07: 功能新增 - 添加事务支持防止并发竞态条件
|
||||||
* - 2026-01-07: 性能优化 - 优化查询语句添加LIMIT限制
|
|
||||||
* - 2026-01-07: 功能新增 - 新增existsByGameUserId方法
|
|
||||||
*
|
*
|
||||||
* @author angjustinl
|
* @author angjustinl
|
||||||
* @version 1.2.0
|
* @version 1.2.1
|
||||||
* @since 2025-01-05
|
* @since 2025-01-05
|
||||||
* @lastModified 2026-01-12
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository, FindOptionsWhere, DataSource, SelectQueryBuilder } from 'typeorm';
|
import { Repository, DataSource, SelectQueryBuilder } from 'typeorm';
|
||||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||||
import { AppLoggerService } from '../../utils/logger/logger.service';
|
import { AppLoggerService } from '../../utils/logger/logger.service';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -16,28 +16,19 @@
|
|||||||
* 注意:业务逻辑已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
|
* 注意:业务逻辑已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
|
||||||
*
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 代码规范优化 - 清理未使用的导入NotFoundException (修改者: moyin)
|
||||||
* - 2026-01-12: 代码规范优化 - 修复依赖注入配置,添加@Inject装饰器确保正确的参数注入 (修改者: moyin)
|
* - 2026-01-12: 代码规范优化 - 修复依赖注入配置,添加@Inject装饰器确保正确的参数注入 (修改者: moyin)
|
||||||
* - 2026-01-12: 功能修改 - 优化create方法错误处理,正确转换重复创建错误为ConflictException (修改者: moyin)
|
* - 2026-01-12: 功能修改 - 优化create方法错误处理,正确转换重复创建错误为ConflictException (修改者: moyin)
|
||||||
* - 2026-01-12: 架构优化 - 移除业务逻辑,转移到zulip_core业务服务 (修改者: moyin)
|
* - 2026-01-12: 架构优化 - 移除业务逻辑,转移到zulip_core业务服务 (修改者: moyin)
|
||||||
* - 2026-01-12: 代码质量优化 - 清理重复导入,统一使用@Inject装饰器 (修改者: moyin)
|
* - 2026-01-12: 代码质量优化 - 清理重复导入,统一使用@Inject装饰器 (修改者: moyin)
|
||||||
* - 2026-01-12: 代码质量优化 - 完成所有性能监控代码优化,统一使用createPerformanceMonitor方法 (修改者: moyin)
|
|
||||||
* - 2026-01-12: 代码质量优化 - 修复所有遗漏的BigInt转换,使用列表响应构建工具方法 (修改者: moyin)
|
|
||||||
* - 2026-01-12: 代码质量优化 - 完善所有BigInt转换和数组映射的优化,彻底消除重复代码 (修改者: moyin)
|
|
||||||
* - 2026-01-12: 代码质量优化 - 使用基类工具方法,优化性能监控和BigInt转换,减少重复代码 (修改者: moyin)
|
|
||||||
* - 2026-01-12: 性能优化 - 集成AppLoggerService和缓存机制,添加性能监控
|
|
||||||
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
|
|
||||||
* - 2026-01-07: 代码规范优化 - 修复导入路径,完善方法三级注释
|
|
||||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
|
|
||||||
* - 2026-01-07: 功能修改 - 优化异常处理逻辑,规范Repository和Service职责边界
|
|
||||||
* - 2026-01-07: 性能优化 - 移除Service层的重复唯一性检查,依赖Repository事务
|
|
||||||
*
|
*
|
||||||
* @author angjustinl
|
* @author angjustinl
|
||||||
* @version 2.1.0
|
* @version 2.1.1
|
||||||
* @since 2025-01-07
|
* @since 2025-01-07
|
||||||
* @lastModified 2026-01-12
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Inject, ConflictException, NotFoundException } from '@nestjs/common';
|
import { Injectable, Inject, ConflictException } from '@nestjs/common';
|
||||||
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
||||||
import { BaseZulipAccountsService } from './base_zulip_accounts.service';
|
import { BaseZulipAccountsService } from './base_zulip_accounts.service';
|
||||||
import { ZulipAccountsRepository } from './zulip_accounts.repository';
|
import { ZulipAccountsRepository } from './zulip_accounts.repository';
|
||||||
@@ -126,8 +117,10 @@ export class ZulipAccountsService extends BaseZulipAccountsService {
|
|||||||
errorMessage.includes('unique constraint')) {
|
errorMessage.includes('unique constraint')) {
|
||||||
const conflictError = new ConflictException(`游戏用户 ${createDto.gameUserId} 已存在Zulip账号关联`);
|
const conflictError = new ConflictException(`游戏用户 ${createDto.gameUserId} 已存在Zulip账号关联`);
|
||||||
monitor.error(conflictError);
|
monitor.error(conflictError);
|
||||||
|
throw conflictError;
|
||||||
} else {
|
} else {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,6 +315,7 @@ export class ZulipAccountsService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -363,6 +357,7 @@ export class ZulipAccountsService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,6 +399,7 @@ export class ZulipAccountsService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,6 +420,7 @@ export class ZulipAccountsService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -444,6 +441,7 @@ export class ZulipAccountsService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,26 +15,19 @@
|
|||||||
* 注意:业务逻辑已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
|
* 注意:业务逻辑已转移到 src/core/zulip_core/services/zulip_accounts_business.service.ts
|
||||||
*
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 代码规范优化 - 清理未使用的导入ConflictException和NotFoundException (修改者: moyin)
|
||||||
* - 2026-01-12: 架构优化 - 移除业务逻辑,转移到zulip_core业务服务 (修改者: moyin)
|
* - 2026-01-12: 架构优化 - 移除业务逻辑,转移到zulip_core业务服务 (修改者: moyin)
|
||||||
* - 2026-01-12: 代码质量优化 - 修复导入语句,添加缺失的AppLoggerService导入 (修改者: moyin)
|
* - 2026-01-12: 代码质量优化 - 修复导入语句,添加缺失的AppLoggerService导入 (修改者: moyin)
|
||||||
* - 2026-01-12: 代码质量优化 - 修复logger初始化问题,统一使用AppLoggerService (修改者: moyin)
|
* - 2026-01-12: 代码质量优化 - 修复logger初始化问题,统一使用AppLoggerService (修改者: moyin)
|
||||||
* - 2026-01-12: 代码质量优化 - 完成所有性能监控代码优化,统一使用createPerformanceMonitor方法 (修改者: moyin)
|
* - 2026-01-12: 代码质量优化 - 完成所有性能监控代码优化,统一使用createPerformanceMonitor方法 (修改者: moyin)
|
||||||
* - 2026-01-12: 代码质量优化 - 修复所有遗漏的BigInt转换,使用列表响应构建工具方法 (修改者: moyin)
|
|
||||||
* - 2026-01-12: 代码质量优化 - 完善所有BigInt转换和数组映射的优化,彻底消除重复代码 (修改者: moyin)
|
|
||||||
* - 2026-01-12: 代码质量优化 - 使用基类工具方法,优化性能监控和BigInt转换,减少重复代码 (修改者: moyin)
|
|
||||||
* - 2026-01-07: 代码规范优化 - 使用统一的常量文件,提高代码质量
|
|
||||||
* - 2026-01-07: 代码规范优化 - 修复导入路径,完善方法三级注释
|
|
||||||
* - 2026-01-07: 代码规范优化 - 完善文件头注释和方法三级注释
|
|
||||||
* - 2026-01-07: 功能完善 - 优化异常处理逻辑和日志记录
|
|
||||||
* - 2025-01-07: 架构优化 - 统一Service层的职责边界和接口设计
|
|
||||||
*
|
*
|
||||||
* @author angjustinl
|
* @author angjustinl
|
||||||
* @version 2.0.0
|
* @version 2.0.1
|
||||||
* @since 2025-01-07
|
* @since 2025-01-07
|
||||||
* @lastModified 2026-01-12
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, Inject, ConflictException, NotFoundException } from '@nestjs/common';
|
import { Injectable, Inject } from '@nestjs/common';
|
||||||
import { BaseZulipAccountsService } from './base_zulip_accounts.service';
|
import { BaseZulipAccountsService } from './base_zulip_accounts.service';
|
||||||
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
|
import { ZulipAccountsMemoryRepository } from './zulip_accounts_memory.repository';
|
||||||
import { ZulipAccounts } from './zulip_accounts.entity';
|
import { ZulipAccounts } from './zulip_accounts.entity';
|
||||||
@@ -101,6 +94,7 @@ export class ZulipAccountsMemoryService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +140,7 @@ export class ZulipAccountsMemoryService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,6 +254,7 @@ export class ZulipAccountsMemoryService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -281,6 +277,7 @@ export class ZulipAccountsMemoryService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,6 +298,7 @@ export class ZulipAccountsMemoryService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +319,7 @@ export class ZulipAccountsMemoryService extends BaseZulipAccountsService {
|
|||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
monitor.error(error);
|
monitor.error(error);
|
||||||
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,42 @@
|
|||||||
|
/**
|
||||||
|
* 登录核心模块测试套件
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 测试LoginCoreModule的模块配置和依赖注入
|
||||||
|
* - 验证服务提供者的正确注册和实例化
|
||||||
|
* - 确保JWT配置的正确加载和访问
|
||||||
|
* - 测试模块导出和依赖关系
|
||||||
|
*
|
||||||
|
* 测试覆盖范围:
|
||||||
|
* - 模块定义:模块实例化和配置验证
|
||||||
|
* - 服务提供者:LoginCoreService和ConfigService的注入
|
||||||
|
* - JWT配置:JWT密钥和过期时间的配置访问
|
||||||
|
* - 模块依赖:依赖模块的正确导入
|
||||||
|
* - 模块导出:服务的正确导出和可用性
|
||||||
|
*
|
||||||
|
* 测试策略:
|
||||||
|
* - 单元测试:独立测试模块配置
|
||||||
|
* - Mock测试:模拟所有外部依赖服务
|
||||||
|
* - 配置测试:验证配置项的正确读取
|
||||||
|
*
|
||||||
|
* 依赖模块:
|
||||||
|
* - Jest: 测试框架和Mock功能
|
||||||
|
* - NestJS Testing: 提供测试模块和依赖注入
|
||||||
|
*
|
||||||
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 代码规范优化 - 清理未使用的导入(UsersService) (修改者: moyin)
|
||||||
|
* - 2026-01-15: 代码规范优化 - 添加文件头注释 (修改者: moyin)
|
||||||
|
*
|
||||||
|
* @author moyin
|
||||||
|
* @version 1.0.2
|
||||||
|
* @since 2025-12-17
|
||||||
|
* @lastModified 2026-01-15
|
||||||
|
*/
|
||||||
|
|
||||||
import { Test, TestingModule } from '@nestjs/testing';
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import { LoginCoreService } from './login_core.service';
|
import { LoginCoreService } from './login_core.service';
|
||||||
import { UsersService } from '../db/users/users.service';
|
|
||||||
import { EmailService } from '../utils/email/email.service';
|
import { EmailService } from '../utils/email/email.service';
|
||||||
import { VerificationService } from '../utils/verification/verification.service';
|
import { VerificationService } from '../utils/verification/verification.service';
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,16 @@
|
|||||||
* - 为business层提供可复用的服务
|
* - 为business层提供可复用的服务
|
||||||
*
|
*
|
||||||
* 最近修改:
|
* 最近修改:
|
||||||
|
* - 2026-01-15: 代码规范优化 - 提取手机号查找为私有方法消除重复代码 (修改者: moyin)
|
||||||
* - 2026-01-12: 代码规范优化 - 提取魔法数字为常量,拆分过长方法,消除代码重复 (修改者: moyin)
|
* - 2026-01-12: 代码规范优化 - 提取魔法数字为常量,拆分过长方法,消除代码重复 (修改者: moyin)
|
||||||
* - 2026-01-12: 代码规范优化 - 添加LoginCoreService类注释,完善类职责和方法说明 (修改者: moyin)
|
* - 2026-01-12: 代码规范优化 - 添加LoginCoreService类注释,完善类职责和方法说明 (修改者: moyin)
|
||||||
* - 2026-01-12: 代码规范优化 - 处理TODO项,移除短信发送相关的TODO注释 (修改者: moyin)
|
* - 2026-01-12: 代码规范优化 - 处理TODO项,移除短信发送相关的TODO注释 (修改者: moyin)
|
||||||
* - 2025-01-07: 代码规范优化 - 清理未使用的导入(EmailSendResult, crypto)
|
* - 2025-01-07: 代码规范优化 - 清理未使用的导入(EmailSendResult, crypto)
|
||||||
* - 2025-01-07: 代码规范优化 - 修复常量命名(saltRounds -> SALT_ROUNDS)
|
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin
|
||||||
* @version 1.1.0
|
* @version 1.1.1
|
||||||
* @since 2025-12-17
|
* @since 2025-12-17
|
||||||
* @lastModified 2026-01-12
|
* @lastModified 2026-01-15
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException, ForbiddenException, Inject } from '@nestjs/common';
|
import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException, ForbiddenException, Inject } from '@nestjs/common';
|
||||||
@@ -243,8 +243,7 @@ export class LoginCoreService {
|
|||||||
|
|
||||||
// 如果邮箱未找到,尝试手机号查找(简单验证)
|
// 如果邮箱未找到,尝试手机号查找(简单验证)
|
||||||
if (!user && this.isPhoneNumber(identifier)) {
|
if (!user && this.isPhoneNumber(identifier)) {
|
||||||
const users = await this.usersService.findAll();
|
user = await this.findUserByPhone(identifier);
|
||||||
user = users.find((u: Users) => u.phone === identifier) || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户不存在
|
// 用户不存在
|
||||||
@@ -340,9 +339,8 @@ export class LoginCoreService {
|
|||||||
|
|
||||||
// 检查手机号是否已存在
|
// 检查手机号是否已存在
|
||||||
if (phone) {
|
if (phone) {
|
||||||
const users = await this.usersService.findAll();
|
const phoneExists = await this.isPhoneExists(phone);
|
||||||
const existingPhone = users.find((u: Users) => u.phone === phone);
|
if (phoneExists) {
|
||||||
if (existingPhone) {
|
|
||||||
throw new ConflictException('手机号已存在');
|
throw new ConflictException('手机号已存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -555,8 +553,7 @@ export class LoginCoreService {
|
|||||||
throw new BadRequestException('邮箱未验证,无法重置密码');
|
throw new BadRequestException('邮箱未验证,无法重置密码');
|
||||||
}
|
}
|
||||||
} else if (this.isPhoneNumber(identifier)) {
|
} else if (this.isPhoneNumber(identifier)) {
|
||||||
const users = await this.usersService.findAll();
|
user = await this.findUserByPhone(identifier);
|
||||||
user = users.find((u: Users) => u.phone === identifier) || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -616,8 +613,7 @@ export class LoginCoreService {
|
|||||||
if (this.isEmail(identifier)) {
|
if (this.isEmail(identifier)) {
|
||||||
user = await this.usersService.findByEmail(identifier);
|
user = await this.usersService.findByEmail(identifier);
|
||||||
} else if (this.isPhoneNumber(identifier)) {
|
} else if (this.isPhoneNumber(identifier)) {
|
||||||
const users = await this.usersService.findAll();
|
user = await this.findUserByPhone(identifier);
|
||||||
user = users.find((u: Users) => u.phone === identifier) || null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -847,6 +843,30 @@ export class LoginCoreService {
|
|||||||
return phoneRegex.test(str.replace(/\s/g, ''));
|
return phoneRegex.test(str.replace(/\s/g, ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过手机号查找用户
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @returns 用户信息或null
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async findUserByPhone(phone: string): Promise<Users | null> {
|
||||||
|
const users = await this.usersService.findAll();
|
||||||
|
return users.find((u: Users) => u.phone === phone) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查手机号是否已存在
|
||||||
|
*
|
||||||
|
* @param phone 手机号
|
||||||
|
* @returns 是否存在
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private async isPhoneExists(phone: string): Promise<boolean> {
|
||||||
|
const user = await this.findUserByPhone(phone);
|
||||||
|
return user !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 验证码登录
|
* 验证码登录
|
||||||
*
|
*
|
||||||
@@ -888,8 +908,7 @@ export class LoginCoreService {
|
|||||||
}
|
}
|
||||||
} else if (this.isPhoneNumber(identifier)) {
|
} else if (this.isPhoneNumber(identifier)) {
|
||||||
// 手机号登录
|
// 手机号登录
|
||||||
const users = await this.usersService.findAll();
|
user = await this.findUserByPhone(identifier);
|
||||||
user = users.find((u: Users) => u.phone === identifier) || null;
|
|
||||||
verificationType = VerificationCodeType.SMS_VERIFICATION;
|
verificationType = VerificationCodeType.SMS_VERIFICATION;
|
||||||
} else {
|
} else {
|
||||||
throw new BadRequestException('请提供有效的邮箱或手机号');
|
throw new BadRequestException('请提供有效的邮箱或手机号');
|
||||||
@@ -964,8 +983,7 @@ export class LoginCoreService {
|
|||||||
throw new BadRequestException('邮箱未验证,无法使用验证码登录');
|
throw new BadRequestException('邮箱未验证,无法使用验证码登录');
|
||||||
}
|
}
|
||||||
} else if (this.isPhoneNumber(identifier)) {
|
} else if (this.isPhoneNumber(identifier)) {
|
||||||
const users = await this.usersService.findAll();
|
user = await this.findUserByPhone(identifier);
|
||||||
user = users.find((u: Users) => u.phone === identifier) || null;
|
|
||||||
verificationType = VerificationCodeType.SMS_VERIFICATION;
|
verificationType = VerificationCodeType.SMS_VERIFICATION;
|
||||||
} else {
|
} else {
|
||||||
throw new BadRequestException('请提供有效的邮箱或手机号');
|
throw new BadRequestException('请提供有效的邮箱或手机号');
|
||||||
|
|||||||
@@ -87,6 +87,25 @@ Zulip Core 是应用的核心聊天集成模块,提供完整的Zulip聊天服
|
|||||||
### getAllAccountLinks()
|
### getAllAccountLinks()
|
||||||
获取所有活跃的账号关联信息,用于系统管理和监控。
|
获取所有活跃的账号关联信息,用于系统管理和监控。
|
||||||
|
|
||||||
|
## 用户注册功能
|
||||||
|
|
||||||
|
### registerUser()
|
||||||
|
在Zulip服务器上注册新用户账号,包含邮箱验证、密码生成和API Key获取。
|
||||||
|
|
||||||
|
## 用户管理功能
|
||||||
|
|
||||||
|
### checkUserExists()
|
||||||
|
检查指定邮箱的用户是否存在于Zulip服务器。
|
||||||
|
|
||||||
|
### getUserInfo()
|
||||||
|
根据邮箱获取用户的详细信息,包含用户ID、状态和权限信息。
|
||||||
|
|
||||||
|
### validateUserCredentials()
|
||||||
|
验证用户的API Key是否有效,用于登录认证。
|
||||||
|
|
||||||
|
### getAllUsers()
|
||||||
|
从Zulip服务器获取所有用户列表,用于管理和统计。
|
||||||
|
|
||||||
## 配置管理功能
|
## 配置管理功能
|
||||||
|
|
||||||
### getAllMapConfigs()
|
### getAllMapConfigs()
|
||||||
@@ -101,6 +120,67 @@ Zulip Core 是应用的核心聊天集成模块,提供完整的Zulip聊天服
|
|||||||
### validateConfig()
|
### validateConfig()
|
||||||
验证配置文件的完整性和正确性,确保系统正常运行。
|
验证配置文件的完整性和正确性,确保系统正常运行。
|
||||||
|
|
||||||
|
## 动态配置管理功能
|
||||||
|
|
||||||
|
### testZulipConnection()
|
||||||
|
测试与Zulip服务器的连接状态,用于健康检查和故障诊断。
|
||||||
|
|
||||||
|
### getZulipStreams()
|
||||||
|
从Zulip服务器获取所有Stream列表,用于配置同步。
|
||||||
|
|
||||||
|
### getZulipTopics()
|
||||||
|
获取指定Stream的所有Topic列表,用于交互对象配置。
|
||||||
|
|
||||||
|
### getConfig()
|
||||||
|
获取当前缓存的配置信息,优先使用内存缓存。
|
||||||
|
|
||||||
|
### syncConfig()
|
||||||
|
手动触发配置同步,从Zulip服务器获取最新配置并更新本地文件。
|
||||||
|
|
||||||
|
### getConfigStatus()
|
||||||
|
获取配置管理器的状态信息,包含同步时间、配置来源等。
|
||||||
|
|
||||||
|
### getBackupFiles()
|
||||||
|
获取配置备份文件列表,用于配置恢复和版本管理。
|
||||||
|
|
||||||
|
### restoreFromBackup()
|
||||||
|
从指定的备份文件恢复配置,支持配置回滚。
|
||||||
|
|
||||||
|
## 错误处理功能
|
||||||
|
|
||||||
|
### handleZulipError()
|
||||||
|
处理Zulip API错误,分析错误类型并决定处理策略。
|
||||||
|
|
||||||
|
### enableDegradedMode()
|
||||||
|
启用服务降级模式,在Zulip服务不可用时提供基础功能。
|
||||||
|
|
||||||
|
### enableNormalMode()
|
||||||
|
恢复正常服务模式,从降级状态切换回正常状态。
|
||||||
|
|
||||||
|
### retryWithBackoff()
|
||||||
|
使用指数退避算法进行重试,避免对服务造成过大压力。
|
||||||
|
|
||||||
|
### handleConnectionError()
|
||||||
|
处理网络连接错误,决定是否重试和启用降级模式。
|
||||||
|
|
||||||
|
### executeWithTimeout()
|
||||||
|
带超时控制的操作执行,超时时自动取消并返回错误。
|
||||||
|
|
||||||
|
### scheduleReconnect()
|
||||||
|
调度自动重连,在连接断开时使用指数退避策略重连。
|
||||||
|
|
||||||
|
### cancelReconnect()
|
||||||
|
取消正在进行的重连尝试,清理重连状态。
|
||||||
|
|
||||||
|
### checkServiceHealth()
|
||||||
|
检查服务健康状态,返回综合健康报告。
|
||||||
|
|
||||||
|
### getServiceStatus()
|
||||||
|
获取当前服务状态(正常/降级/不可用)。
|
||||||
|
|
||||||
|
### getLoadStatus()
|
||||||
|
获取系统负载状态,用于连接限流决策。
|
||||||
|
|
||||||
## 安全管理功能
|
## 安全管理功能
|
||||||
|
|
||||||
### encryptApiKey()
|
### encryptApiKey()
|
||||||
@@ -129,6 +209,32 @@ Zulip Core 是应用的核心聊天集成模块,提供完整的Zulip聊天服
|
|||||||
### getPerformanceMetrics()
|
### getPerformanceMetrics()
|
||||||
获取系统性能指标,包含响应时间和吞吐量统计。
|
获取系统性能指标,包含响应时间和吞吐量统计。
|
||||||
|
|
||||||
|
## 监控日志功能
|
||||||
|
|
||||||
|
### logConnection()
|
||||||
|
记录WebSocket连接事件日志,包含连接、断开和错误事件。
|
||||||
|
|
||||||
|
### logApiCall()
|
||||||
|
记录Zulip API调用日志,包含响应时间和结果状态。
|
||||||
|
|
||||||
|
### logMessageForward()
|
||||||
|
记录消息转发日志,包含成功率和延迟统计。
|
||||||
|
|
||||||
|
### confirmOperation()
|
||||||
|
记录操作确认信息,用于审计和追踪。
|
||||||
|
|
||||||
|
### sendAlert()
|
||||||
|
发送系统告警通知,支持不同严重级别。
|
||||||
|
|
||||||
|
### getStats()
|
||||||
|
获取监控统计信息,包含连接、API调用和消息统计。
|
||||||
|
|
||||||
|
### getRecentAlerts()
|
||||||
|
获取最近的告警列表,用于问题排查。
|
||||||
|
|
||||||
|
### resetStats()
|
||||||
|
重置监控统计数据,用于周期性统计。
|
||||||
|
|
||||||
## 使用的项目内部依赖
|
## 使用的项目内部依赖
|
||||||
|
|
||||||
### RedisModule (来自 ../redis/redis.module)
|
### RedisModule (来自 ../redis/redis.module)
|
||||||
@@ -354,7 +460,7 @@ const newKey = await apiKeySecurityService.rotateApiKey('user123');
|
|||||||
```
|
```
|
||||||
|
|
||||||
## 版本信息
|
## 版本信息
|
||||||
- **版本**: 1.1.1
|
- **版本**: 1.2.0
|
||||||
- **作者**: moyin
|
- **作者**: moyin
|
||||||
- **创建时间**: 2025-12-25
|
- **创建时间**: 2025-12-25
|
||||||
- **最后修改**: 2026-01-07
|
- **最后修改**: 2026-01-15
|
||||||
@@ -69,6 +69,7 @@ export interface CreateZulipAccountResult {
|
|||||||
export interface GenerateApiKeyResult {
|
export interface GenerateApiKeyResult {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
apiKey?: string;
|
apiKey?: string;
|
||||||
|
userId?: number; // 添加 userId,从 profile 中获取
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -301,40 +302,44 @@ export class ZulipAccountService {
|
|||||||
|
|
||||||
// 尝试获取已有用户的信息
|
// 尝试获取已有用户的信息
|
||||||
const userInfo = await this.getExistingUserInfo(request.email);
|
const userInfo = await this.getExistingUserInfo(request.email);
|
||||||
if (userInfo.success) {
|
|
||||||
// 尝试为已有用户生成API Key
|
// 尝试为已有用户生成API Key(同时可以获取 userId)
|
||||||
const apiKeyResult = await this.generateApiKeyForUser(request.email, request.password || '');
|
const apiKeyResult = await this.generateApiKeyForUser(request.email, request.password || '');
|
||||||
|
|
||||||
this.logger.log('Zulip账号绑定成功(已存在)', {
|
// 优先使用 userInfo 中的 userId,其次使用 apiKeyResult 中的 userId
|
||||||
operation: 'handleExistingUser',
|
const finalUserId = userInfo.userId ?? apiKeyResult.userId;
|
||||||
email: request.email,
|
|
||||||
userId: userInfo.userId,
|
if (finalUserId === undefined) {
|
||||||
hasApiKey: apiKeyResult.success,
|
this.logger.error('用户已存在但无法获取用户ID,绑定失败', {
|
||||||
apiKeyError: apiKeyResult.success ? undefined : apiKeyResult.error,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
userId: userInfo.userId,
|
|
||||||
email: request.email,
|
|
||||||
apiKey: apiKeyResult.success ? apiKeyResult.apiKey : undefined,
|
|
||||||
isExistingUser: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.logger.warn('用户已存在但无法获取详细信息,仍返回绑定成功', {
|
|
||||||
operation: 'handleExistingUser',
|
operation: 'handleExistingUser',
|
||||||
email: request.email,
|
email: request.email,
|
||||||
getUserInfoError: userInfo.error,
|
getUserInfoError: userInfo.error,
|
||||||
|
apiKeyError: apiKeyResult.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
userId: undefined,
|
error: `用户已存在但无法获取用户ID`,
|
||||||
email: request.email,
|
errorCode: 'USER_ID_NOT_FOUND',
|
||||||
apiKey: undefined,
|
|
||||||
isExistingUser: true,
|
isExistingUser: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.log('Zulip账号绑定成功(已存在)', {
|
||||||
|
operation: 'handleExistingUser',
|
||||||
|
email: request.email,
|
||||||
|
userId: finalUserId,
|
||||||
|
hasApiKey: apiKeyResult.success,
|
||||||
|
apiKeyError: apiKeyResult.success ? undefined : apiKeyResult.error,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
userId: finalUserId,
|
||||||
|
email: request.email,
|
||||||
|
apiKey: apiKeyResult.success ? apiKeyResult.apiKey : undefined,
|
||||||
|
isExistingUser: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -421,39 +426,43 @@ export class ZulipAccountService {
|
|||||||
|
|
||||||
// 尝试获取已有用户信息
|
// 尝试获取已有用户信息
|
||||||
const userInfo = await this.getExistingUserInfo(request.email);
|
const userInfo = await this.getExistingUserInfo(request.email);
|
||||||
if (userInfo.success) {
|
|
||||||
// 尝试为已有用户生成API Key
|
// 尝试为已有用户生成API Key(同时可以获取 userId)
|
||||||
const apiKeyResult = await this.generateApiKeyForUser(request.email, password);
|
const apiKeyResult = await this.generateApiKeyForUser(request.email, password);
|
||||||
|
|
||||||
this.logger.log('Zulip账号绑定成功(API创建时发现已存在)', {
|
// 优先使用 userInfo 中的 userId,其次使用 apiKeyResult 中的 userId
|
||||||
operation: 'handleCreateUserError',
|
const finalUserId = userInfo.userId ?? apiKeyResult.userId;
|
||||||
email: request.email,
|
|
||||||
userId: userInfo.userId,
|
if (finalUserId === undefined) {
|
||||||
hasApiKey: apiKeyResult.success,
|
this.logger.error('用户已存在但无法获取用户ID,绑定失败', {
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
userId: userInfo.userId,
|
|
||||||
email: request.email,
|
|
||||||
apiKey: apiKeyResult.success ? apiKeyResult.apiKey : undefined,
|
|
||||||
isExistingUser: true,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
this.logger.warn('用户已存在但无法获取详细信息', {
|
|
||||||
operation: 'handleCreateUserError',
|
operation: 'handleCreateUserError',
|
||||||
email: request.email,
|
email: request.email,
|
||||||
getUserInfoError: userInfo.error,
|
getUserInfoError: userInfo.error,
|
||||||
|
apiKeyError: apiKeyResult.error,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: false,
|
||||||
userId: undefined,
|
error: `用户已存在但无法获取用户ID`,
|
||||||
email: request.email,
|
errorCode: 'USER_ID_NOT_FOUND',
|
||||||
apiKey: undefined,
|
|
||||||
isExistingUser: true,
|
isExistingUser: true,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.logger.log('Zulip账号绑定成功(API创建时发现已存在)', {
|
||||||
|
operation: 'handleCreateUserError',
|
||||||
|
email: request.email,
|
||||||
|
userId: finalUserId,
|
||||||
|
hasApiKey: apiKeyResult.success,
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
userId: finalUserId,
|
||||||
|
email: request.email,
|
||||||
|
apiKey: apiKeyResult.success ? apiKeyResult.apiKey : undefined,
|
||||||
|
isExistingUser: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其他类型的错误
|
// 其他类型的错误
|
||||||
@@ -515,12 +524,14 @@ export class ZulipAccountService {
|
|||||||
this.logger.log('API Key生成成功', {
|
this.logger.log('API Key生成成功', {
|
||||||
operation: 'generateApiKeyForUser',
|
operation: 'generateApiKeyForUser',
|
||||||
email,
|
email,
|
||||||
|
userId: profile.user_id,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
apiKey: apiKey,
|
apiKey: apiKey,
|
||||||
|
userId: profile.user_id, // 返回从 profile 获取的 user_id
|
||||||
};
|
};
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -169,6 +169,9 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
|||||||
case 'position':
|
case 'position':
|
||||||
await this.handlePosition(ws, message);
|
await this.handlePosition(ws, message);
|
||||||
break;
|
break;
|
||||||
|
case 'change_map':
|
||||||
|
await this.handleChangeMap(ws, message);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
this.logger.warn(`未知消息类型: ${messageType}`);
|
this.logger.warn(`未知消息类型: ${messageType}`);
|
||||||
this.sendError(ws, `未知消息类型: ${messageType}`);
|
this.sendError(ws, `未知消息类型: ${messageType}`);
|
||||||
@@ -254,7 +257,7 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
|||||||
* 处理聊天消息
|
* 处理聊天消息
|
||||||
*
|
*
|
||||||
* @param ws WebSocket 连接实例
|
* @param ws WebSocket 连接实例
|
||||||
* @param message 聊天消息(包含 content, scope)
|
* @param message 聊天消息(包含 content, scope, mapId)
|
||||||
*/
|
*/
|
||||||
private async handleChat(ws: ExtendedWebSocket, message: any) {
|
private async handleChat(ws: ExtendedWebSocket, message: any) {
|
||||||
if (!ws.authenticated) {
|
if (!ws.authenticated) {
|
||||||
@@ -271,7 +274,8 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
|||||||
const result = await this.chatService.sendChatMessage({
|
const result = await this.chatService.sendChatMessage({
|
||||||
socketId: ws.id,
|
socketId: ws.id,
|
||||||
content: message.content,
|
content: message.content,
|
||||||
scope: message.scope || 'local'
|
scope: message.scope || 'local',
|
||||||
|
mapId: message.mapId || ws.currentMap, // 支持指定目标地图
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
@@ -335,6 +339,82 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理切换地图
|
||||||
|
*
|
||||||
|
* @param ws WebSocket 连接实例
|
||||||
|
* @param message 切换地图消息(包含 mapId)
|
||||||
|
*/
|
||||||
|
private async handleChangeMap(ws: ExtendedWebSocket, message: any) {
|
||||||
|
if (!ws.authenticated) {
|
||||||
|
this.sendError(ws, '请先登录');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.mapId) {
|
||||||
|
this.sendError(ws, '地图ID不能为空');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oldMapId = ws.currentMap;
|
||||||
|
const newMapId = message.mapId;
|
||||||
|
|
||||||
|
// 如果地图相同,直接返回成功
|
||||||
|
if (oldMapId === newMapId) {
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
t: 'map_changed',
|
||||||
|
mapId: newMapId,
|
||||||
|
message: '已在当前地图'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新房间
|
||||||
|
this.leaveMapRoom(ws.id, oldMapId);
|
||||||
|
this.joinMapRoom(ws.id, newMapId);
|
||||||
|
ws.currentMap = newMapId;
|
||||||
|
|
||||||
|
// 更新会话中的地图信息(使用默认位置)
|
||||||
|
await this.chatService.updatePlayerPosition({
|
||||||
|
socketId: ws.id,
|
||||||
|
x: message.x || 400,
|
||||||
|
y: message.y || 300,
|
||||||
|
mapId: newMapId
|
||||||
|
});
|
||||||
|
|
||||||
|
// 通知客户端切换成功
|
||||||
|
this.sendMessage(ws, {
|
||||||
|
t: 'map_changed',
|
||||||
|
mapId: newMapId,
|
||||||
|
oldMapId: oldMapId,
|
||||||
|
message: '地图切换成功'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 向旧地图广播玩家离开
|
||||||
|
this.broadcastToMap(oldMapId, {
|
||||||
|
t: 'player_left',
|
||||||
|
userId: ws.userId,
|
||||||
|
username: ws.username,
|
||||||
|
mapId: oldMapId
|
||||||
|
});
|
||||||
|
|
||||||
|
// 向新地图广播玩家加入
|
||||||
|
this.broadcastToMap(newMapId, {
|
||||||
|
t: 'player_joined',
|
||||||
|
userId: ws.userId,
|
||||||
|
username: ws.username,
|
||||||
|
mapId: newMapId
|
||||||
|
}, ws.id);
|
||||||
|
|
||||||
|
this.logger.log(`用户切换地图: ${ws.username} (${oldMapId} -> ${newMapId})`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('切换地图处理失败', error);
|
||||||
|
this.sendError(ws, '切换地图处理失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 处理连接关闭
|
* 处理连接关闭
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user