- 重新组织docs目录结构,按功能模块分类 - 新增deployment和development目录 - 更新API文档结构 - 添加客户端README文档 - 移除过时的文档文件
856 lines
25 KiB
Markdown
856 lines
25 KiB
Markdown
# 后端开发规范指南
|
||
|
||
## 一、文档概述
|
||
|
||
### 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. **统一的异常处理**:分类处理不同类型异常,提供友好的用户体验
|
||
|
||
遵循本规范将显著提高代码质量、系统稳定性和团队协作效率。所有开发人员必须严格按照本规范进行开发,代码审查时将重点检查这些方面的实现。 |