Compare commits
11 Commits
9f4d291619
...
feature/ch
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd2a197288 | ||
| 01787d701c | |||
| 6e7de1a11a | |||
|
|
d92a078fc7 | ||
| 9785908ca9 | |||
| 592a745b8f | |||
|
|
cde20c6fd7 | ||
|
|
a8de2564b6 | ||
| 299627dac7 | |||
| ae3a256c52 | |||
| 97ea698f38 |
@@ -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项处理(强制要求)
|
||||
|
||||
### 处理原则
|
||||
@@ -323,12 +544,19 @@ describe('AdminService Properties', () => {
|
||||
- 抽象为可复用的工具方法
|
||||
- 消除代码重复
|
||||
|
||||
6. **处理所有TODO项**
|
||||
6. **🚨 检查异常处理完整性(关键步骤)**
|
||||
- 扫描所有 catch 块
|
||||
- 检查是否有 throw 语句
|
||||
- 验证 Service/Repository 层是否传播异常
|
||||
- 确认方法返回类型与实际返回一致
|
||||
- 识别异常吞没模式并修复
|
||||
|
||||
7. **处理所有TODO项**
|
||||
- 搜索所有TODO注释
|
||||
- 要求真正实现功能或删除代码
|
||||
- 确保最终文件无TODO项
|
||||
|
||||
7. **游戏服务器特殊检查**
|
||||
8. **游戏服务器特殊检查**
|
||||
- WebSocket连接管理完整性
|
||||
- 双模式服务行为一致性
|
||||
- 属性测试实现质量
|
||||
|
||||
@@ -505,6 +505,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. 创建合并文档目录(如果不存在)
|
||||
@@ -689,6 +720,7 @@ git remote show [远程仓库名]
|
||||
- **完整性**:每次提交的代码都应该能正常运行
|
||||
- **描述性**:提交信息要清晰描述改动内容、范围和原因
|
||||
- **一致性**:文件修改记录必须与实际修改内容一致
|
||||
- **合并文档排除**:`docs/merge-requests/` 目录下的合并文档不纳入Git提交
|
||||
|
||||
### 质量保证
|
||||
- 提交前必须验证代码能正常运行
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AppController } from './app.controller';
|
||||
import { AppService } from './app.service';
|
||||
import { LoggerModule } from './core/utils/logger/logger.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 { AuthGatewayModule } from './gateway/auth/auth.gateway.module';
|
||||
import { ChatGatewayModule } from './gateway/chat/chat.gateway.module';
|
||||
@@ -62,6 +63,8 @@ function isDatabaseConfigured(): boolean {
|
||||
database: process.env.DB_NAME,
|
||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||
synchronize: false,
|
||||
// 字符集配置 - 支持中文和emoji
|
||||
charset: 'utf8mb4',
|
||||
// 添加连接超时和重试配置
|
||||
connectTimeout: 10000,
|
||||
retryAttempts: 3,
|
||||
@@ -70,6 +73,8 @@ function isDatabaseConfigured(): boolean {
|
||||
] : []),
|
||||
// 根据数据库配置选择用户模块模式
|
||||
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
|
||||
// Zulip账号关联模块 - 全局单例,其他模块无需重复导入
|
||||
ZulipAccountsModule.forRoot(),
|
||||
LoginCoreModule,
|
||||
AuthGatewayModule, // 认证网关模块
|
||||
ChatGatewayModule, // 聊天网关模块
|
||||
|
||||
@@ -27,7 +27,6 @@ import { AdminCoreModule } from '../../core/admin_core/admin_core.module';
|
||||
import { LoggerModule } from '../../core/utils/logger/logger.module';
|
||||
import { UsersModule } from '../../core/db/users/users.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 { AdminService } from './admin.service';
|
||||
import { AdminDatabaseController } from './admin_database.controller';
|
||||
@@ -55,8 +54,7 @@ function isDatabaseConfigured(): boolean {
|
||||
UsersModule,
|
||||
// 根据数据库配置选择UserProfiles模块模式
|
||||
isDatabaseConfigured() ? UserProfilesModule.forDatabase() : UserProfilesModule.forMemory(),
|
||||
// 根据数据库配置选择ZulipAccounts模块模式
|
||||
isDatabaseConfigured() ? ZulipAccountsModule.forDatabase() : ZulipAccountsModule.forMemory(),
|
||||
// 注意:ZulipAccountsModule 是全局模块,已在 AppModule 中导入,无需重复导入
|
||||
// 注册AdminOperationLog实体
|
||||
TypeOrmModule.forFeature([AdminOperationLog])
|
||||
],
|
||||
|
||||
@@ -36,7 +36,6 @@ import { LoginService } from './login.service';
|
||||
import { RegisterService } from './register.service';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_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';
|
||||
|
||||
@Module({
|
||||
@@ -44,7 +43,7 @@ import { UsersModule } from '../../core/db/users/users.module';
|
||||
// 导入核心层模块
|
||||
LoginCoreModule,
|
||||
ZulipCoreModule,
|
||||
ZulipAccountsModule.forRoot(),
|
||||
// 注意:ZulipAccountsModule 是全局模块,已在 AppModule 中导入,无需重复导入
|
||||
UsersModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@@ -36,7 +36,6 @@ import { ZulipEventProcessorService } from './services/zulip_event_processor.ser
|
||||
import { ZulipAccountsBusinessService } from './services/zulip_accounts_business.service';
|
||||
// 依赖模块
|
||||
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 { LoggerModule } from '../../core/utils/logger/logger.module';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
@@ -50,8 +49,7 @@ import { ChatModule } from '../chat/chat.module';
|
||||
CacheModule.register(),
|
||||
// Zulip核心服务模块
|
||||
ZulipCoreModule,
|
||||
// Zulip账号关联模块
|
||||
ZulipAccountsModule.forRoot(),
|
||||
// 注意:ZulipAccountsModule 是全局模块,已在 AppModule 中导入,无需重复导入
|
||||
// Redis模块
|
||||
RedisModule,
|
||||
// 日志模块
|
||||
|
||||
@@ -169,6 +169,9 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
case 'position':
|
||||
await this.handlePosition(ws, message);
|
||||
break;
|
||||
case 'change_map':
|
||||
await this.handleChangeMap(ws, message);
|
||||
break;
|
||||
default:
|
||||
this.logger.warn(`未知消息类型: ${messageType}`);
|
||||
this.sendError(ws, `未知消息类型: ${messageType}`);
|
||||
@@ -254,7 +257,7 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
* 处理聊天消息
|
||||
*
|
||||
* @param ws WebSocket 连接实例
|
||||
* @param message 聊天消息(包含 content, scope)
|
||||
* @param message 聊天消息(包含 content, scope, mapId)
|
||||
*/
|
||||
private async handleChat(ws: ExtendedWebSocket, message: any) {
|
||||
if (!ws.authenticated) {
|
||||
@@ -271,7 +274,8 @@ export class ChatWebSocketGateway implements OnModuleInit, OnModuleDestroy, ICha
|
||||
const result = await this.chatService.sendChatMessage({
|
||||
socketId: ws.id,
|
||||
content: message.content,
|
||||
scope: message.scope || 'local'
|
||||
scope: message.scope || 'local',
|
||||
mapId: message.mapId || ws.currentMap, // 支持指定目标地图
|
||||
});
|
||||
|
||||
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