Files
whale-town-end/docs/backend_development_guide.md
moyin c14a49a88e chore:更新项目配置
- 更新pnpm工作区配置
- 完善后端开发规范文档
2025-12-17 14:40:00 +08:00

856 lines
25 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 后端开发规范指南
## 一、文档概述
### 1.1 文档目的
本文档定义了 Datawhale Town 后端开发的编码规范、注释标准、业务逻辑设计原则和日志记录要求,确保代码质量、可维护性和系统稳定性。
### 1.2 适用范围
- 所有后端开发人员
- 代码审查人员
- 系统维护人员
---
## 二、注释规范
### 2.1 模块注释
每个功能模块文件必须包含模块级注释,说明模块用途、主要功能和依赖关系。
**格式要求:**
```typescript
/**
* 玩家管理模块
*
* 功能描述:
* - 处理玩家注册、登录、信息更新等核心功能
* - 管理玩家角色皮肤和个人资料
* - 提供玩家数据的 CRUD 操作
*
* 依赖模块:
* - AuthService: 身份验证服务
* - DatabaseService: 数据库操作服务
* - LoggerService: 日志记录服务
*
* @author 开发者姓名
* @version 1.0.0
* @since 2025-12-13
*/
```
### 2.2 类注释
每个类必须包含类级注释,说明类的职责、主要方法和使用场景。
**格式要求:**
```typescript
/**
* 玩家服务类
*
* 职责:
* - 处理玩家相关的业务逻辑
* - 管理玩家状态和数据
* - 提供玩家操作的统一接口
*
* 主要方法:
* - createPlayer(): 创建新玩家
* - updatePlayerInfo(): 更新玩家信息
* - getPlayerById(): 根据ID获取玩家信息
*
* 使用场景:
* - 玩家注册登录流程
* - 个人陈列室数据管理
* - 广场玩家状态同步
*/
@Injectable()
export class PlayerService {
// 类实现
}
```
### 2.3 方法注释
每个方法必须包含详细的方法注释,说明功能、参数、返回值、异常和业务逻辑。
**格式要求:**
```typescript
/**
* 创建新玩家
*
* 功能描述:
* 根据邮箱创建新的玩家账户,包含基础信息初始化和默认配置设置
*
* 业务逻辑:
* 1. 验证邮箱格式和白名单
* 2. 检查邮箱是否已存在
* 3. 生成唯一玩家ID
* 4. 初始化默认角色皮肤和个人信息
* 5. 创建对应的个人陈列室
* 6. 记录创建日志
*
* @param email 玩家邮箱地址,必须符合邮箱格式且在白名单中
* @param nickname 玩家昵称长度3-20字符不能包含特殊字符
* @param avatarSkin 角色皮肤ID必须是1-8之间的有效值
* @returns Promise<Player> 创建成功的玩家对象
*
* @throws BadRequestException 当邮箱格式错误或不在白名单中
* @throws ConflictException 当邮箱已存在时
* @throws InternalServerErrorException 当数据库操作失败时
*
* @example
* ```typescript
* const player = await playerService.createPlayer(
* 'user@datawhale.club',
* '数据鲸鱼',
* '1'
* );
* ```
*/
async createPlayer(
email: string,
nickname: string,
avatarSkin: string
): Promise<Player> {
// 方法实现
}
```
### 2.4 复杂业务逻辑注释
对于复杂的业务逻辑,必须添加行内注释说明每个步骤的目的和处理逻辑。
**示例:**
```typescript
async joinRoom(roomId: string, playerId: string): Promise<Room> {
// 1. 参数验证 - 确保房间ID和玩家ID格式正确
if (!roomId || !playerId) {
this.logger.warn(`房间加入失败:参数无效`, { roomId, playerId });
throw new BadRequestException('房间ID和玩家ID不能为空');
}
// 2. 获取房间信息 - 检查房间是否存在
const room = await this.roomRepository.findById(roomId);
if (!room) {
this.logger.warn(`房间加入失败:房间不存在`, { roomId, playerId });
throw new NotFoundException('房间不存在');
}
// 3. 检查房间状态 - 只有等待中的房间才能加入
if (room.status !== RoomStatus.WAITING) {
this.logger.warn(`房间加入失败:房间状态不允许加入`, {
roomId,
playerId,
currentStatus: room.status
});
throw new BadRequestException('游戏已开始,无法加入房间');
}
// 4. 检查房间容量 - 防止超过最大人数限制
if (room.players.length >= room.maxPlayers) {
this.logger.warn(`房间加入失败:房间已满`, {
roomId,
playerId,
currentPlayers: room.players.length,
maxPlayers: room.maxPlayers
});
throw new BadRequestException('房间已满');
}
// 5. 检查玩家是否已在房间中 - 防止重复加入
if (room.players.includes(playerId)) {
this.logger.info(`玩家已在房间中,跳过加入操作`, { roomId, playerId });
return room;
}
// 6. 执行加入操作 - 更新房间玩家列表
try {
room.players.push(playerId);
const updatedRoom = await this.roomRepository.save(room);
// 7. 记录成功日志
this.logger.info(`玩家成功加入房间`, {
roomId,
playerId,
currentPlayers: updatedRoom.players.length,
maxPlayers: updatedRoom.maxPlayers
});
return updatedRoom;
} catch (error) {
// 8. 异常处理 - 记录错误并抛出
this.logger.error(`房间加入操作数据库错误`, {
roomId,
playerId,
error: error.message,
stack: error.stack
});
throw new InternalServerErrorException('房间加入失败,请稍后重试');
}
}
```
---
## 三、业务逻辑设计原则
### 3.1 全面性原则
每个业务方法必须考虑所有可能的情况,包括正常流程、异常情况和边界条件。
**必须考虑的情况:**
| 类别 | 具体情况 | 处理方式 |
|------|---------|---------|
| **输入验证** | 参数为空、格式错误、超出范围 | 参数校验 + 异常抛出 |
| **权限检查** | 未登录、权限不足、令牌过期 | 身份验证 + 权限验证 |
| **资源状态** | 资源不存在、状态不正确、已被占用 | 状态检查 + 业务规则验证 |
| **并发控制** | 同时操作、数据竞争、锁冲突 | 事务处理 + 乐观锁/悲观锁 |
| **系统异常** | 数据库连接失败、网络超时、内存不足 | 异常捕获 + 降级处理 |
| **业务规则** | 违反业务约束、超出限制、状态冲突 | 业务规则验证 + 友好提示 |
### 3.2 防御性编程
采用防御性编程思想,对所有外部输入和依赖进行验证和保护。
**实现要求:**
```typescript
/**
* 更新玩家信息 - 防御性编程示例
*/
async updatePlayerInfo(
playerId: string,
updateData: UpdatePlayerDto
): Promise<Player> {
// 1. 输入参数防御性检查
if (!playerId) {
this.logger.warn('更新玩家信息失败玩家ID为空');
throw new BadRequestException('玩家ID不能为空');
}
if (!updateData || Object.keys(updateData).length === 0) {
this.logger.warn('更新玩家信息失败:更新数据为空', { playerId });
throw new BadRequestException('更新数据不能为空');
}
// 2. 数据格式验证
if (updateData.nickname) {
if (updateData.nickname.length < 3 || updateData.nickname.length > 20) {
this.logger.warn('更新玩家信息失败:昵称长度不符合要求', {
playerId,
nicknameLength: updateData.nickname.length
});
throw new BadRequestException('昵称长度必须在3-20字符之间');
}
}
if (updateData.avatarSkin) {
const validSkins = ['1', '2', '3', '4', '5', '6', '7', '8'];
if (!validSkins.includes(updateData.avatarSkin)) {
this.logger.warn('更新玩家信息失败角色皮肤ID无效', {
playerId,
avatarSkin: updateData.avatarSkin
});
throw new BadRequestException('角色皮肤ID必须在1-8之间');
}
}
// 3. 玩家存在性检查
const existingPlayer = await this.playerRepository.findById(playerId);
if (!existingPlayer) {
this.logger.warn('更新玩家信息失败:玩家不存在', { playerId });
throw new NotFoundException('玩家不存在');
}
// 4. 昵称唯一性检查(如果更新昵称)
if (updateData.nickname && updateData.nickname !== existingPlayer.nickname) {
const nicknameExists = await this.playerRepository.findByNickname(updateData.nickname);
if (nicknameExists) {
this.logger.warn('更新玩家信息失败:昵称已存在', {
playerId,
nickname: updateData.nickname
});
throw new ConflictException('昵称已被使用');
}
}
// 5. 执行更新操作(使用事务保证数据一致性)
try {
const updatedPlayer = await this.playerRepository.update(playerId, updateData);
this.logger.info('玩家信息更新成功', {
playerId,
updatedFields: Object.keys(updateData),
timestamp: new Date().toISOString()
});
return updatedPlayer;
} catch (error) {
this.logger.error('更新玩家信息数据库操作失败', {
playerId,
updateData,
error: error.message,
stack: error.stack
});
throw new InternalServerErrorException('更新失败,请稍后重试');
}
}
```
### 3.3 异常处理策略
建立统一的异常处理策略,确保所有异常都能被正确捕获和处理。
**异常分类和处理:**
| 异常类型 | HTTP状态码 | 处理策略 | 日志级别 |
|---------|-----------|---------|---------|
| **BadRequestException** | 400 | 参数验证失败,返回具体错误信息 | WARN |
| **UnauthorizedException** | 401 | 身份验证失败,要求重新登录 | WARN |
| **ForbiddenException** | 403 | 权限不足,拒绝访问 | WARN |
| **NotFoundException** | 404 | 资源不存在,返回友好提示 | WARN |
| **ConflictException** | 409 | 资源冲突,如重复创建 | WARN |
| **InternalServerErrorException** | 500 | 系统内部错误,记录详细日志 | ERROR |
---
## 四、日志系统使用指南
### 4.1 日志服务简介
项目使用统一的 `AppLoggerService` 提供日志记录功能,集成了 Pino 高性能日志库,支持自动敏感信息过滤和请求上下文绑定。
### 4.2 在服务中使用日志
**依赖注入:**
```typescript
import { Injectable } from '@nestjs/common';
import { AppLoggerService } from '../core/utils/logger/logger.service';
@Injectable()
export class UserService {
constructor(
private readonly logger: AppLoggerService
) {}
}
```
### 4.3 日志级别和使用场景
| 级别 | 使用场景 | 示例 |
|------|---------|------|
| **ERROR** | 系统错误、异常情况、数据库连接失败 | 数据库连接超时、第三方服务调用失败 |
| **WARN** | 业务警告、参数错误、权限不足 | 用户输入无效参数、尝试访问不存在的资源 |
| **INFO** | 重要业务操作、状态变更、关键流程 | 用户登录成功、房间创建、玩家加入广场 |
| **DEBUG** | 调试信息、详细执行流程 | 方法调用参数、中间计算结果 |
| **FATAL** | 致命错误、系统不可用 | 数据库完全不可用、关键服务宕机 |
| **TRACE** | 极细粒度调试信息 | 循环内的变量状态、算法执行步骤 |
### 4.4 标准日志格式
**推荐的日志上下文格式:**
```typescript
// 成功操作日志
this.logger.info('操作描述', {
operation: '操作类型',
userId: '用户ID',
resourceId: '资源ID',
params: '关键参数',
result: '操作结果',
duration: '执行时间(ms)',
timestamp: new Date().toISOString()
});
// 警告日志
this.logger.warn('警告描述', {
operation: '操作类型',
userId: '用户ID',
reason: '警告原因',
params: '相关参数',
timestamp: new Date().toISOString()
});
// 错误日志
this.logger.error('错误描述', {
operation: '操作类型',
userId: '用户ID',
error: error.message,
params: '相关参数',
timestamp: new Date().toISOString()
}, error.stack);
```
### 4.5 请求上下文绑定
**在 Controller 中使用:**
```typescript
@Controller('users')
export class UserController {
constructor(private readonly logger: AppLoggerService) {}
@Get(':id')
async getUser(@Param('id') id: string, @Req() req: Request) {
// 绑定请求上下文
const requestLogger = this.logger.bindRequest(req, 'UserController');
requestLogger.info('开始获取用户信息', { userId: id });
try {
const user = await this.userService.findById(id);
requestLogger.info('用户信息获取成功', { userId: id });
return user;
} catch (error) {
requestLogger.error('用户信息获取失败', error.stack, {
userId: id,
reason: error.message
});
throw error;
}
}
}
```
### 4.6 业务方法日志记录最佳实践
**完整的业务方法日志记录示例:**
```typescript
async createPlayer(email: string, nickname: string): Promise<Player> {
const startTime = Date.now();
this.logger.info('开始创建玩家', {
operation: 'createPlayer',
email,
nickname,
timestamp: new Date().toISOString()
});
try {
// 1. 参数验证
if (!email || !nickname) {
this.logger.warn('创建玩家失败:参数无效', {
operation: 'createPlayer',
email,
nickname,
reason: 'invalid_parameters'
});
throw new BadRequestException('邮箱和昵称不能为空');
}
// 2. 邮箱格式验证
if (!this.isValidEmail(email)) {
this.logger.warn('创建玩家失败:邮箱格式无效', {
operation: 'createPlayer',
email,
nickname
});
throw new BadRequestException('邮箱格式不正确');
}
// 3. 检查邮箱是否已存在
const existingPlayer = await this.playerRepository.findByEmail(email);
if (existingPlayer) {
this.logger.warn('创建玩家失败:邮箱已存在', {
operation: 'createPlayer',
email,
nickname,
existingPlayerId: existingPlayer.id
});
throw new ConflictException('邮箱已被使用');
}
// 4. 创建玩家
const player = await this.playerRepository.create({
email,
nickname,
avatarSkin: '1', // 默认皮肤
createTime: new Date()
});
const duration = Date.now() - startTime;
this.logger.info('玩家创建成功', {
operation: 'createPlayer',
playerId: player.id,
email,
nickname,
duration,
timestamp: new Date().toISOString()
});
return player;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof BadRequestException ||
error instanceof ConflictException) {
// 业务异常,重新抛出
throw error;
}
// 系统异常,记录详细日志
this.logger.error('创建玩家系统异常', {
operation: 'createPlayer',
email,
nickname,
error: error.message,
duration,
timestamp: new Date().toISOString()
}, error.stack);
throw new InternalServerErrorException('创建玩家失败,请稍后重试');
}
}
```
### 4.7 必须记录日志的操作
| 操作类型 | 日志级别 | 记录内容 |
|---------|---------|---------|
| **用户认证** | INFO/WARN | 登录成功/失败、令牌生成/验证 |
| **数据变更** | INFO | 创建、更新、删除操作 |
| **权限检查** | WARN | 权限验证失败、非法访问尝试 |
| **系统异常** | ERROR | 异常堆栈、错误上下文、影响范围 |
| **性能监控** | INFO | 慢查询、高并发操作、资源使用 |
| **安全事件** | WARN/ERROR | 恶意请求、频繁操作、异常行为 |
### 4.8 敏感信息保护
日志系统会自动过滤以下敏感字段:
- `password` - 密码
- `token` - 令牌
- `secret` - 密钥
- `authorization` - 授权信息
- `cardNo` - 卡号
**注意:** 包含这些关键词的字段会被自动替换为 `[REDACTED]`
---
## 五、代码审查检查清单
### 5.1 注释检查
- [ ] 模块文件包含完整的模块级注释
- [ ] 每个类都有详细的类级注释
- [ ] 每个公共方法都有完整的方法注释
- [ ] 复杂业务逻辑有行内注释说明
- [ ] 注释内容准确,与代码实现一致
### 5.2 业务逻辑检查
- [ ] 考虑了所有可能的输入情况
- [ ] 包含完整的参数验证
- [ ] 处理了所有可能的异常情况
- [ ] 实现了适当的权限检查
- [ ] 考虑了并发和竞态条件
### 5.3 日志记录检查
- [ ] 关键业务操作都有日志记录
- [ ] 日志级别使用正确
- [ ] 日志格式符合规范
- [ ] 包含足够的上下文信息
- [ ] 敏感信息已脱敏处理
### 5.4 异常处理检查
- [ ] 所有异常都被正确捕获
- [ ] 异常类型选择合适
- [ ] 异常信息对用户友好
- [ ] 系统异常有详细的错误日志
- [ ] 不会泄露敏感的系统信息
---
## 六、最佳实践示例
### 6.1 完整的服务类示例
```typescript
/**
* 广场管理服务
*
* 功能描述:
* - 管理中央广场的玩家状态和位置同步
* - 处理玩家进入和离开广场的逻辑
* - 维护广场在线玩家列表最多50人
*
* 依赖模块:
* - PlayerService: 玩家信息服务
* - WebSocketGateway: WebSocket通信网关
* - RedisService: 缓存服务
* - LoggerService: 日志记录服务
*
* @author 开发团队
* @version 1.0.0
* @since 2025-12-13
*/
@Injectable()
export class PlazaService {
private readonly logger = new Logger(PlazaService.name);
private readonly MAX_PLAYERS = 50;
constructor(
private readonly playerService: PlayerService,
private readonly redisService: RedisService,
private readonly webSocketGateway: WebSocketGateway
) {}
/**
* 玩家进入广场
*
* 功能描述:
* 处理玩家进入中央广场的逻辑,包括人数限制检查、位置分配和状态同步
*
* 业务逻辑:
* 1. 验证玩家身份和权限
* 2. 检查广场当前人数是否超限
* 3. 为玩家分配初始位置
* 4. 更新Redis中的在线玩家列表
* 5. 向其他玩家广播新玩家进入消息
* 6. 向新玩家发送当前广场状态
*
* @param playerId 玩家ID必须是有效的已注册玩家
* @param socketId WebSocket连接ID用于消息推送
* @returns Promise<PlazaPlayerInfo> 玩家在广场的信息
*
* @throws UnauthorizedException 当玩家身份验证失败时
* @throws BadRequestException 当广场人数已满时
* @throws InternalServerErrorException 当系统操作失败时
*/
async enterPlaza(playerId: string, socketId: string): Promise<PlazaPlayerInfo> {
const startTime = Date.now();
this.logger.info('玩家尝试进入广场', {
operation: 'enterPlaza',
playerId,
socketId,
timestamp: new Date().toISOString()
});
try {
// 1. 验证玩家身份
const player = await this.playerService.getPlayerById(playerId);
if (!player) {
this.logger.warn('进入广场失败:玩家不存在', {
operation: 'enterPlaza',
playerId,
socketId
});
throw new UnauthorizedException('玩家身份验证失败');
}
// 2. 检查广场人数限制
const currentPlayers = await this.redisService.scard('plaza:online_players');
if (currentPlayers >= this.MAX_PLAYERS) {
this.logger.warn('进入广场失败:人数已满', {
operation: 'enterPlaza',
playerId,
currentPlayers,
maxPlayers: this.MAX_PLAYERS
});
throw new BadRequestException('广场人数已满,请稍后再试');
}
// 3. 检查玩家是否已在广场中
const isAlreadyInPlaza = await this.redisService.sismember('plaza:online_players', playerId);
if (isAlreadyInPlaza) {
this.logger.info('玩家已在广场中,更新连接信息', {
operation: 'enterPlaza',
playerId,
socketId
});
// 更新Socket连接映射
await this.redisService.hset('plaza:player_sockets', playerId, socketId);
// 获取当前位置信息
const existingInfo = await this.redisService.hget('plaza:player_positions', playerId);
return JSON.parse(existingInfo);
}
// 4. 为玩家分配初始位置(广场中心附近随机位置)
const initialPosition = this.generateInitialPosition();
const playerInfo: PlazaPlayerInfo = {
playerId: player.id,
nickname: player.nickname,
avatarSkin: player.avatarSkin,
position: initialPosition,
lastUpdate: new Date(),
socketId
};
// 5. 更新Redis中的玩家状态
await Promise.all([
this.redisService.sadd('plaza:online_players', playerId),
this.redisService.hset('plaza:player_positions', playerId, JSON.stringify(playerInfo)),
this.redisService.hset('plaza:player_sockets', playerId, socketId),
this.redisService.expire('plaza:player_positions', 3600), // 1小时过期
this.redisService.expire('plaza:player_sockets', 3600)
]);
// 6. 向其他玩家广播新玩家进入消息
this.webSocketGateway.broadcastToPlaza('player_entered', {
playerId: player.id,
nickname: player.nickname,
avatarSkin: player.avatarSkin,
position: initialPosition
}, socketId); // 排除新进入的玩家
// 7. 向新玩家发送当前广场状态
const allPlayers = await this.getAllPlazaPlayers();
this.webSocketGateway.sendToPlayer(socketId, 'plaza_state', {
players: allPlayers.filter(p => p.playerId !== playerId),
totalPlayers: allPlayers.length
});
const duration = Date.now() - startTime;
this.logger.info('玩家成功进入广场', {
operation: 'enterPlaza',
playerId,
socketId,
position: initialPosition,
totalPlayers: currentPlayers + 1,
duration,
timestamp: new Date().toISOString()
});
return playerInfo;
} catch (error) {
const duration = Date.now() - startTime;
if (error instanceof UnauthorizedException ||
error instanceof BadRequestException) {
throw error;
}
this.logger.error('玩家进入广场系统异常', {
operation: 'enterPlaza',
playerId,
socketId,
error: error.message,
stack: error.stack,
duration,
timestamp: new Date().toISOString()
});
throw new InternalServerErrorException('进入广场失败,请稍后重试');
}
}
/**
* 生成初始位置
*
* 功能描述:
* 在广场中心附近生成随机的初始位置,避免玩家重叠
*
* @returns Position 包含x、y坐标的位置对象
* @private
*/
private generateInitialPosition(): Position {
// 广场中心坐标 (400, 300)在半径100像素范围内随机分配
const centerX = 400;
const centerY = 300;
const radius = 100;
const angle = Math.random() * 2 * Math.PI;
const distance = Math.random() * radius;
const x = Math.round(centerX + distance * Math.cos(angle));
const y = Math.round(centerY + distance * Math.sin(angle));
return { x, y };
}
/**
* 获取所有广场玩家信息
*
* @returns Promise<PlazaPlayerInfo[]> 广场中所有玩家的信息列表
* @private
*/
private async getAllPlazaPlayers(): Promise<PlazaPlayerInfo[]> {
try {
const playerIds = await this.redisService.smembers('plaza:online_players');
const playerInfos = await Promise.all(
playerIds.map(async (playerId) => {
const info = await this.redisService.hget('plaza:player_positions', playerId);
return info ? JSON.parse(info) : null;
})
);
return playerInfos.filter(info => info !== null);
} catch (error) {
this.logger.error('获取广场玩家列表失败', {
operation: 'getAllPlazaPlayers',
error: error.message
});
return [];
}
}
}
```
---
## 七、工具和配置
### 7.1 推荐的开发工具
| 工具 | 用途 | 配置说明 |
|------|------|---------|
| **ESLint** | 代码规范检查 | 配置注释规范检查规则 |
| **Prettier** | 代码格式化 | 统一代码格式 |
| **TSDoc** | 文档生成 | 从注释生成API文档 |
| **SonarQube** | 代码质量分析 | 检查代码覆盖率和复杂度 |
### 7.2 日志配置示例
```typescript
// logger.config.ts
export const loggerConfig = {
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
})
]
};
```
---
## 八、总结
本规范文档定义了后端开发的核心要求:
1. **完整的注释体系**:模块、类、方法三级注释,确保代码可读性
2. **全面的业务逻辑**:考虑所有可能情况,实现防御性编程
3. **规范的日志记录**:关键操作必须记录,便于问题排查和系统监控
4. **统一的异常处理**:分类处理不同类型异常,提供友好的用户体验
遵循本规范将显著提高代码质量、系统稳定性和团队协作效率。所有开发人员必须严格按照本规范进行开发,代码审查时将重点检查这些方面的实现。