Compare commits
7 Commits
5dc97532c6
...
7ebc75e678
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7ebc75e678 | ||
|
|
310ee46910 | ||
|
|
ae58596473 | ||
|
|
1478a2dbf5 | ||
|
|
6aa34ea379 | ||
|
|
dff0ac8325 | ||
|
|
a3ea69d99a |
185
README.md
185
README.md
@@ -4,28 +4,38 @@
|
|||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- **NestJS** `^10.0.0` - 渐进式 Node.js 框架
|
- **NestJS** `^10.4.20` - 渐进式 Node.js 框架
|
||||||
- **TypeScript** `^5.3.0` - 类型安全
|
- **TypeScript** `^5.9.3` - 类型安全
|
||||||
- **Socket.IO** - WebSocket 实时通信支持
|
- **Socket.IO** - WebSocket 实时通信支持
|
||||||
- **RxJS** `^7.8.1` - 响应式编程库
|
- **RxJS** `^7.8.2` - 响应式编程库
|
||||||
|
- **Pino** `^10.1.0` - 高性能日志库
|
||||||
|
- **Jest** `^29.7.0` - 测试框架
|
||||||
|
|
||||||
### 核心依赖
|
### 核心依赖
|
||||||
|
|
||||||
**生产环境:**
|
**生产环境:**
|
||||||
- `@nestjs/common` `^10.0.0` - NestJS 核心功能
|
- `@nestjs/common` `^10.4.20` - NestJS 核心功能
|
||||||
- `@nestjs/core` `^10.0.0` - NestJS 核心模块
|
- `@nestjs/core` `^10.4.20` - NestJS 核心模块
|
||||||
- `@nestjs/platform-express` `^10.0.0` - Express 平台适配器
|
- `@nestjs/config` `^4.0.2` - 配置管理
|
||||||
- `@nestjs/websockets` `^10.0.0` - WebSocket 支持
|
- `@nestjs/platform-express` `^10.4.20` - Express 平台适配器
|
||||||
- `@nestjs/platform-socket.io` `^10.0.0` - Socket.IO 适配器
|
- `@nestjs/websockets` `^10.4.20` - WebSocket 支持
|
||||||
- `reflect-metadata` `^0.1.13` - 装饰器元数据支持
|
- `@nestjs/platform-socket.io` `^10.4.20` - Socket.IO 适配器
|
||||||
- `rxjs` `^7.8.1` - 响应式编程
|
- `nestjs-pino` `^4.5.0` - Pino 日志集成
|
||||||
|
- `pino` `^10.1.0` - 高性能日志库
|
||||||
|
- `reflect-metadata` `^0.1.14` - 装饰器元数据支持
|
||||||
|
- `rxjs` `^7.8.2` - 响应式编程
|
||||||
|
|
||||||
**开发环境:**
|
**开发环境:**
|
||||||
- `@nestjs/cli` `^10.0.0` - NestJS 命令行工具
|
- `@nestjs/cli` `^10.4.9` - NestJS 命令行工具
|
||||||
- `@nestjs/schematics` `^10.0.0` - NestJS 代码生成器
|
- `@nestjs/schematics` `^10.2.3` - NestJS 代码生成器
|
||||||
- `@types/node` `^20.0.0` - Node.js 类型定义
|
- `@nestjs/testing` `^10.4.20` - 测试工具
|
||||||
- `ts-node` `^10.9.0` - TypeScript 运行时
|
- `@types/jest` `^29.5.14` - Jest 类型定义
|
||||||
- `typescript` `^5.3.0` - TypeScript 编译器
|
- `@types/node` `^20.19.26` - Node.js 类型定义
|
||||||
|
- `jest` `^29.7.0` - 测试框架
|
||||||
|
- `ts-jest` `^29.2.5` - TypeScript Jest 支持
|
||||||
|
- `ts-node` `^10.9.2` - TypeScript 运行时
|
||||||
|
- `typescript` `^5.9.3` - TypeScript 编译器
|
||||||
|
- `pino-pretty` `^13.1.3` - Pino 美化输出
|
||||||
|
|
||||||
## 开发规范
|
## 开发规范
|
||||||
|
|
||||||
@@ -82,11 +92,50 @@ git commit -m "api:添加玩家信息查询接口"
|
|||||||
|
|
||||||
详细规范请查看:[Git 提交规范文档](./docs/git_commit_guide.md)
|
详细规范请查看:[Git 提交规范文档](./docs/git_commit_guide.md)
|
||||||
|
|
||||||
|
### 后端开发规范
|
||||||
|
|
||||||
|
项目要求严格的代码质量和可维护性标准:
|
||||||
|
|
||||||
|
**核心要求:**
|
||||||
|
|
||||||
|
- **完整注释**:每个模块、类、方法都必须有详细注释
|
||||||
|
- **全面业务逻辑**:考虑所有可能情况,包括异常和边界条件
|
||||||
|
- **关键日志记录**:重要操作必须记录日志,便于问题排查
|
||||||
|
- **防御性编程**:对所有输入进行验证,实现健壮的错误处理
|
||||||
|
|
||||||
|
**注释要求:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 玩家服务类
|
||||||
|
*
|
||||||
|
* 职责:处理玩家相关的业务逻辑
|
||||||
|
* 主要方法:createPlayer(), updatePlayerInfo(), getPlayerById()
|
||||||
|
* 使用场景:玩家注册登录流程、个人陈列室数据管理
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class PlayerService {
|
||||||
|
/**
|
||||||
|
* 创建新玩家
|
||||||
|
* @param email 玩家邮箱地址
|
||||||
|
* @param nickname 玩家昵称
|
||||||
|
* @returns Promise<Player> 创建成功的玩家对象
|
||||||
|
* @throws BadRequestException 当邮箱格式错误时
|
||||||
|
*/
|
||||||
|
async createPlayer(email: string, nickname: string): Promise<Player> {
|
||||||
|
// 详细的业务逻辑实现...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
详细规范请查看:[后端开发规范指南](./docs/backend_development_guide.md)
|
||||||
|
|
||||||
## 文档
|
## 文档
|
||||||
|
|
||||||
- [NestJS 使用指南](./docs/nestjs_guide.md) - 详细的 NestJS 开发指南,包含实战案例
|
- [NestJS 使用指南](./docs/nestjs_guide.md) - 详细的 NestJS 开发指南,包含实战案例
|
||||||
- [命名规范](./docs/naming_convention.md) - 项目命名规范和最佳实践
|
- [命名规范](./docs/naming_convention.md) - 项目命名规范和最佳实践
|
||||||
- [Git 提交规范](./docs/git_commit_guide.md) - Git 提交信息格式和最佳实践
|
- [Git 提交规范](./docs/git_commit_guide.md) - Git 提交信息格式和最佳实践
|
||||||
|
- [后端开发规范](./docs/backend_development_guide.md) - 注释标准、业务逻辑设计和日志记录要求
|
||||||
|
|
||||||
## 前置要求
|
## 前置要求
|
||||||
|
|
||||||
@@ -112,23 +161,6 @@ pnpm --version
|
|||||||
pnpm install
|
pnpm install
|
||||||
```
|
```
|
||||||
|
|
||||||
**注意**:首次安装时,pnpm 可能会提示需要批准构建脚本。这是 pnpm 的安全特性,用于防止恶意脚本执行。
|
|
||||||
|
|
||||||
如果看到以下警告:
|
|
||||||
|
|
||||||
```
|
|
||||||
Warning: Ignored build scripts: @nestjs/core.
|
|
||||||
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
|
|
||||||
```
|
|
||||||
|
|
||||||
请运行以下命令批准 NestJS 的构建脚本:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm approve-builds
|
|
||||||
```
|
|
||||||
|
|
||||||
然后选择批准 `@nestjs/core` 和其他 NestJS 相关包的构建脚本。
|
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
启动开发服务器(支持热重载):
|
启动开发服务器(支持热重载):
|
||||||
@@ -139,6 +171,26 @@ pnpm dev
|
|||||||
|
|
||||||
服务器将运行在 `http://localhost:3000`
|
服务器将运行在 `http://localhost:3000`
|
||||||
|
|
||||||
|
## 测试
|
||||||
|
|
||||||
|
运行测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test
|
||||||
|
```
|
||||||
|
|
||||||
|
运行测试并监听文件变化:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test:watch
|
||||||
|
```
|
||||||
|
|
||||||
|
运行测试并生成覆盖率报告:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm test:cov
|
||||||
|
```
|
||||||
|
|
||||||
## 构建
|
## 构建
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -156,11 +208,11 @@ pnpm start:prod
|
|||||||
```
|
```
|
||||||
src/
|
src/
|
||||||
├── api/ # API 接口层(控制器、网关)
|
├── api/ # API 接口层(控制器、网关)
|
||||||
├── config/ # 配置文件
|
├── business/ # 业务逻辑层
|
||||||
├── data/ # 数据访问层(数据库、缓存)
|
├── core/ # 核心功能模块
|
||||||
├── model/ # 数据模型、实体、DTO
|
│ ├── db/ # 数据库相关
|
||||||
├── service/ # 业务逻辑层
|
│ └── utils/ # 工具函数
|
||||||
├── utils/ # 工具函数
|
│ └── logger/ # 日志系统
|
||||||
├── main.ts # 应用入口
|
├── main.ts # 应用入口
|
||||||
├── app.module.ts # 根模块
|
├── app.module.ts # 根模块
|
||||||
├── app.controller.ts # 根控制器
|
├── app.controller.ts # 根控制器
|
||||||
@@ -168,8 +220,67 @@ src/
|
|||||||
test/
|
test/
|
||||||
├── api/ # API 测试
|
├── api/ # API 测试
|
||||||
└── service/ # 服务测试
|
└── service/ # 服务测试
|
||||||
|
docs/ # 项目文档
|
||||||
|
├── backend_development_guide.md # 后端开发规范
|
||||||
|
├── git_commit_guide.md # Git 提交规范
|
||||||
|
├── naming_convention.md # 命名规范
|
||||||
|
└── nestjs_guide.md # NestJS 使用指南
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 核心功能
|
||||||
|
|
||||||
|
### 日志系统
|
||||||
|
|
||||||
|
项目集成了完整的日志系统,基于 Pino 高性能日志库:
|
||||||
|
|
||||||
|
**特性:**
|
||||||
|
- 🚀 高性能日志记录
|
||||||
|
- 🔒 自动敏感信息过滤
|
||||||
|
- 🎯 多级别日志控制
|
||||||
|
- 🔍 请求上下文绑定
|
||||||
|
- 📊 结构化日志输出
|
||||||
|
|
||||||
|
**使用示例:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AppLoggerService } from './core/utils/logger/logger.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserService {
|
||||||
|
constructor(private readonly logger: AppLoggerService) {}
|
||||||
|
|
||||||
|
async createUser(userData: CreateUserDto) {
|
||||||
|
this.logger.info('开始创建用户', {
|
||||||
|
operation: 'createUser',
|
||||||
|
email: userData.email,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = await this.userRepository.save(userData);
|
||||||
|
|
||||||
|
this.logger.info('用户创建成功', {
|
||||||
|
operation: 'createUser',
|
||||||
|
userId: user.id,
|
||||||
|
email: userData.email
|
||||||
|
});
|
||||||
|
|
||||||
|
return user;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('用户创建失败', {
|
||||||
|
operation: 'createUser',
|
||||||
|
email: userData.email,
|
||||||
|
error: error.message
|
||||||
|
}, error.stack);
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
详细使用方法请参考:[后端开发规范指南 - 日志系统使用指南](./docs/backend_development_guide.md#四日志系统使用指南)
|
||||||
|
|
||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
- 在 `src/api/` 目录下创建游戏相关的控制器和网关
|
- 在 `src/api/` 目录下创建游戏相关的控制器和网关
|
||||||
|
|||||||
856
docs/backend_development_guide.md
Normal file
856
docs/backend_development_guide.md
Normal file
@@ -0,0 +1,856 @@
|
|||||||
|
# 后端开发规范指南
|
||||||
|
|
||||||
|
## 一、文档概述
|
||||||
|
|
||||||
|
### 1.1 文档目的
|
||||||
|
|
||||||
|
本文档定义了 Datawhale Town 后端开发的编码规范、注释标准、业务逻辑设计原则和日志记录要求,确保代码质量、可维护性和系统稳定性。
|
||||||
|
|
||||||
|
### 1.2 适用范围
|
||||||
|
|
||||||
|
- 所有后端开发人员
|
||||||
|
- 代码审查人员
|
||||||
|
- 系统维护人员
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、注释规范
|
||||||
|
|
||||||
|
### 2.1 模块注释
|
||||||
|
|
||||||
|
每个功能模块文件必须包含模块级注释,说明模块用途、主要功能和依赖关系。
|
||||||
|
|
||||||
|
**格式要求:**
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
/**
|
||||||
|
* 玩家管理模块
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 处理玩家注册、登录、信息更新等核心功能
|
||||||
|
* - 管理玩家角色皮肤和个人资料
|
||||||
|
* - 提供玩家数据的 CRUD 操作
|
||||||
|
*
|
||||||
|
* 依赖模块:
|
||||||
|
* - AuthService: 身份验证服务
|
||||||
|
* - DatabaseService: 数据库操作服务
|
||||||
|
* - LoggerService: 日志记录服务
|
||||||
|
*
|
||||||
|
* @author 开发者姓名
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2024-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 2024-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. **统一的异常处理**:分类处理不同类型异常,提供友好的用户体验
|
||||||
|
|
||||||
|
遵循本规范将显著提高代码质量、系统稳定性和团队协作效率。所有开发人员必须严格按照本规范进行开发,代码审查时将重点检查这些方面的实现。
|
||||||
16
jest.config.js
Normal file
16
jest.config.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
module.exports = {
|
||||||
|
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||||
|
rootDir: 'src',
|
||||||
|
testRegex: '.*\\.spec\\.ts$',
|
||||||
|
transform: {
|
||||||
|
'^.+\\.(t|j)s$': 'ts-jest',
|
||||||
|
},
|
||||||
|
collectCoverageFrom: [
|
||||||
|
'**/*.(t|j)s',
|
||||||
|
],
|
||||||
|
coverageDirectory: '../coverage',
|
||||||
|
testEnvironment: 'node',
|
||||||
|
moduleNameMapper: {
|
||||||
|
'^src/(.*)$': '<rootDir>/$1',
|
||||||
|
},
|
||||||
|
};
|
||||||
45
package.json
45
package.json
@@ -7,25 +7,42 @@
|
|||||||
"dev": "nest start --watch",
|
"dev": "nest start --watch",
|
||||||
"build": "nest build",
|
"build": "nest build",
|
||||||
"start": "node dist/main.js",
|
"start": "node dist/main.js",
|
||||||
"start:prod": "node dist/main.js"
|
"start:prod": "node dist/main.js",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage"
|
||||||
},
|
},
|
||||||
"keywords": ["game", "pixel", "2d", "server", "nestjs"],
|
"keywords": [
|
||||||
|
"game",
|
||||||
|
"pixel",
|
||||||
|
"2d",
|
||||||
|
"server",
|
||||||
|
"nestjs"
|
||||||
|
],
|
||||||
"author": "",
|
"author": "",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^10.0.0",
|
"@nestjs/common": "^10.4.20",
|
||||||
"@nestjs/core": "^10.0.0",
|
"@nestjs/config": "^4.0.2",
|
||||||
"@nestjs/platform-express": "^10.0.0",
|
"@nestjs/core": "^10.4.20",
|
||||||
"@nestjs/websockets": "^10.0.0",
|
"@nestjs/platform-express": "^10.4.20",
|
||||||
"@nestjs/platform-socket.io": "^10.0.0",
|
"@nestjs/platform-socket.io": "^10.4.20",
|
||||||
"reflect-metadata": "^0.1.13",
|
"@nestjs/websockets": "^10.4.20",
|
||||||
"rxjs": "^7.8.1"
|
"nestjs-pino": "^4.5.0",
|
||||||
|
"pino": "^10.1.0",
|
||||||
|
"reflect-metadata": "^0.1.14",
|
||||||
|
"rxjs": "^7.8.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@nestjs/cli": "^10.0.0",
|
"@nestjs/cli": "^10.4.9",
|
||||||
"@nestjs/schematics": "^10.0.0",
|
"@nestjs/schematics": "^10.2.3",
|
||||||
"@types/node": "^20.0.0",
|
"@nestjs/testing": "^10.4.20",
|
||||||
"ts-node": "^10.9.0",
|
"@types/jest": "^29.5.14",
|
||||||
"typescript": "^5.3.0"
|
"@types/node": "^20.19.26",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
import { Module } from '@nestjs/common';
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { LoggerModule } from './core/utils/logger/logger.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [],
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
envFilePath: '.env',
|
||||||
|
}),
|
||||||
|
LoggerModule,
|
||||||
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService],
|
providers: [AppService],
|
||||||
})
|
})
|
||||||
|
|||||||
81
src/core/utils/logger/logger.module.ts
Normal file
81
src/core/utils/logger/logger.module.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
/**
|
||||||
|
* 日志模块
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 配置和提供全局日志服务
|
||||||
|
* - 集成 Pino 高性能日志库
|
||||||
|
* - 支持不同环境的日志配置
|
||||||
|
* - 提供统一的日志记录接口
|
||||||
|
*
|
||||||
|
* 依赖模块:
|
||||||
|
* - ConfigModule: 环境配置模块
|
||||||
|
* - PinoLoggerModule: Pino 日志模块
|
||||||
|
* - AppLoggerService: 应用日志服务
|
||||||
|
*
|
||||||
|
* @author 开发团队
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2024-12-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { LoggerModule as PinoLoggerModule } from 'nestjs-pino';
|
||||||
|
import { AppLoggerService } from './logger.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志模块类
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 配置 Pino 日志库的各种选项
|
||||||
|
* - 根据环境变量调整日志输出格式和级别
|
||||||
|
* - 提供全局可用的日志服务
|
||||||
|
* - 管理日志相关的依赖注入
|
||||||
|
*
|
||||||
|
* 配置说明:
|
||||||
|
* - 开发环境:使用 pino-pretty 美化输出,日志级别为 debug
|
||||||
|
* - 生产环境:使用 JSON 格式输出,日志级别为 info
|
||||||
|
* - 自动过滤请求和响应中的敏感信息
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 在 AppModule 中导入,提供全局日志服务
|
||||||
|
* - 在其他模块中注入 AppLoggerService 使用
|
||||||
|
*/
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule,
|
||||||
|
PinoLoggerModule.forRootAsync({
|
||||||
|
imports: [ConfigModule],
|
||||||
|
useFactory: () => ({
|
||||||
|
pinoHttp: {
|
||||||
|
// 根据环境设置日志级别
|
||||||
|
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
|
||||||
|
|
||||||
|
// 开发环境使用美化输出
|
||||||
|
transport: process.env.NODE_ENV !== 'production' ? {
|
||||||
|
target: 'pino-pretty',
|
||||||
|
options: {
|
||||||
|
colorize: true,
|
||||||
|
translateTime: 'SYS:standard',
|
||||||
|
ignore: 'pid,hostname',
|
||||||
|
},
|
||||||
|
} : undefined,
|
||||||
|
|
||||||
|
// 自定义序列化器,过滤敏感信息
|
||||||
|
serializers: {
|
||||||
|
req: (req) => ({
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
headers: req.headers,
|
||||||
|
}),
|
||||||
|
res: (res) => ({
|
||||||
|
statusCode: res.statusCode,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
providers: [AppLoggerService],
|
||||||
|
exports: [AppLoggerService],
|
||||||
|
})
|
||||||
|
export class LoggerModule {}
|
||||||
132
src/core/utils/logger/logger.service.spec.ts
Normal file
132
src/core/utils/logger/logger.service.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
/**
|
||||||
|
* 应用日志服务测试
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 测试日志服务的核心功能
|
||||||
|
* - 验证不同日志级别的正确性
|
||||||
|
* - 测试敏感信息过滤功能
|
||||||
|
* - 验证请求上下文绑定功能
|
||||||
|
*
|
||||||
|
* 测试覆盖:
|
||||||
|
* - 服务实例化
|
||||||
|
* - 日志方法调用
|
||||||
|
* - 敏感数据过滤
|
||||||
|
* - 请求上下文绑定
|
||||||
|
*
|
||||||
|
* @author moyin
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2024-12-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AppLoggerService } from './logger.service';
|
||||||
|
|
||||||
|
describe('AppLoggerService', () => {
|
||||||
|
let service: AppLoggerService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
AppLoggerService,
|
||||||
|
{
|
||||||
|
provide: ConfigService,
|
||||||
|
useValue: {
|
||||||
|
get: jest.fn((key: string, defaultValue?: any) => {
|
||||||
|
const config: Record<string, any> = {
|
||||||
|
NODE_ENV: 'test',
|
||||||
|
APP_NAME: 'test-app',
|
||||||
|
};
|
||||||
|
return config[key] || defaultValue;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<AppLoggerService>(AppLoggerService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试信息日志记录功能
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - info 方法能够正确调用内部 log 方法
|
||||||
|
* - 传递的参数格式正确
|
||||||
|
* - 日志级别设置正确
|
||||||
|
*/
|
||||||
|
it('should log info messages', () => {
|
||||||
|
// 监听内部 log 方法调用
|
||||||
|
const logSpy = jest.spyOn(service as any, 'log').mockImplementation();
|
||||||
|
|
||||||
|
// 调用 info 方法
|
||||||
|
service.info('Test message', { module: 'TestModule' });
|
||||||
|
|
||||||
|
// 验证调用参数
|
||||||
|
expect(logSpy).toHaveBeenCalledWith('info', {
|
||||||
|
message: 'Test message',
|
||||||
|
context: { module: 'TestModule' }
|
||||||
|
});
|
||||||
|
|
||||||
|
logSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试敏感信息过滤功能
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - 敏感信息过滤方法被正确调用
|
||||||
|
* - 包含敏感字段的日志会触发过滤逻辑
|
||||||
|
* - 过滤功能不影响正常的日志记录流程
|
||||||
|
*/
|
||||||
|
it('should filter sensitive data', () => {
|
||||||
|
// 监听敏感信息过滤方法
|
||||||
|
const redactSpy = jest.spyOn(service as any, 'redactSensitiveData');
|
||||||
|
|
||||||
|
// 记录包含敏感信息的日志
|
||||||
|
service.info('Login attempt', {
|
||||||
|
module: 'AuthModule',
|
||||||
|
password: 'secret123',
|
||||||
|
token: 'jwt-token'
|
||||||
|
});
|
||||||
|
|
||||||
|
// 验证过滤方法被调用
|
||||||
|
expect(redactSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
redactSpy.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试请求上下文绑定功能
|
||||||
|
*
|
||||||
|
* 验证点:
|
||||||
|
* - bindRequest 方法返回正确的日志方法对象
|
||||||
|
* - 返回的对象包含所有必要的日志方法
|
||||||
|
* - 绑定的上下文信息能够正确传递
|
||||||
|
*/
|
||||||
|
it('should bind request context', () => {
|
||||||
|
// 模拟 HTTP 请求对象
|
||||||
|
const mockReq = {
|
||||||
|
id: 'req-123',
|
||||||
|
headers: {
|
||||||
|
'x-user-id': 'user-456'
|
||||||
|
},
|
||||||
|
ip: '127.0.0.1'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 绑定请求上下文
|
||||||
|
const boundLogger = service.bindRequest(mockReq, 'TestController');
|
||||||
|
|
||||||
|
// 验证返回的日志方法对象
|
||||||
|
expect(boundLogger).toHaveProperty('info');
|
||||||
|
expect(boundLogger).toHaveProperty('error');
|
||||||
|
expect(boundLogger).toHaveProperty('warn');
|
||||||
|
expect(boundLogger).toHaveProperty('debug');
|
||||||
|
expect(boundLogger).toHaveProperty('fatal');
|
||||||
|
expect(boundLogger).toHaveProperty('trace');
|
||||||
|
});
|
||||||
|
});
|
||||||
487
src/core/utils/logger/logger.service.ts
Normal file
487
src/core/utils/logger/logger.service.ts
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
/**
|
||||||
|
* 日志系统模块
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 提供统一的日志记录服务,支持多种日志级别
|
||||||
|
* - 集成 Pino 高性能日志库,支持降级到 NestJS 内置 Logger
|
||||||
|
* - 自动过滤敏感信息,保护系统安全
|
||||||
|
* - 支持请求上下文绑定,便于链路追踪
|
||||||
|
*
|
||||||
|
* 依赖模块:
|
||||||
|
* - ConfigService: 环境配置服务
|
||||||
|
* - PinoLogger: 高性能日志库(可选)
|
||||||
|
* - Logger: NestJS 内置日志服务(降级使用)
|
||||||
|
*
|
||||||
|
* @author moyin
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2024-12-13
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, Logger, Inject, Optional } from '@nestjs/common';
|
||||||
|
import { PinoLogger } from 'nestjs-pino';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志级别枚举
|
||||||
|
*
|
||||||
|
* 级别说明:
|
||||||
|
* - trace: 极细粒度调试信息
|
||||||
|
* - debug: 调试信息,开发环境使用
|
||||||
|
* - info: 重要业务操作记录
|
||||||
|
* - warn: 警告信息,需要关注但不影响正常流程
|
||||||
|
* - error: 错误信息,影响功能正常使用
|
||||||
|
* - fatal: 致命错误,可能导致系统不可用
|
||||||
|
*/
|
||||||
|
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'trace';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志上下文接口
|
||||||
|
*
|
||||||
|
* 用于补充日志的上下文信息,便于问题排查和链路追踪
|
||||||
|
*/
|
||||||
|
export interface LogContext {
|
||||||
|
/** 请求 ID,用于链路追踪 */
|
||||||
|
reqId?: string;
|
||||||
|
/** 模块名称,标识日志来源 */
|
||||||
|
module?: string;
|
||||||
|
/** 用户 ID,关联用户行为 */
|
||||||
|
userId?: string;
|
||||||
|
/** 操作类型,描述具体操作 */
|
||||||
|
operation?: string;
|
||||||
|
/** 时间戳,记录操作时间 */
|
||||||
|
timestamp?: string;
|
||||||
|
/** 执行时长,性能监控 */
|
||||||
|
duration?: number;
|
||||||
|
/** 自定义扩展字段 */
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日志选项接口
|
||||||
|
*
|
||||||
|
* 定义日志记录时的参数结构
|
||||||
|
*/
|
||||||
|
export interface LogOptions {
|
||||||
|
/** 日志消息内容 */
|
||||||
|
message: string;
|
||||||
|
/** 日志上下文信息 */
|
||||||
|
context?: LogContext;
|
||||||
|
/** 错误堆栈信息(仅用于 error/fatal 级别) */
|
||||||
|
stack?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用日志服务类
|
||||||
|
*
|
||||||
|
* 职责:
|
||||||
|
* - 提供统一的日志记录接口
|
||||||
|
* - 管理不同环境下的日志级别控制
|
||||||
|
* - 自动过滤敏感信息,防止数据泄露
|
||||||
|
* - 支持请求上下文绑定,便于问题追踪
|
||||||
|
*
|
||||||
|
* 主要方法:
|
||||||
|
* - debug(): 记录调试信息
|
||||||
|
* - info(): 记录重要业务操作
|
||||||
|
* - warn(): 记录警告信息
|
||||||
|
* - error(): 记录错误信息
|
||||||
|
* - fatal(): 记录致命错误
|
||||||
|
* - trace(): 记录追踪信息
|
||||||
|
* - bindRequest(): 绑定请求上下文
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 业务操作日志记录
|
||||||
|
* - 系统异常监控
|
||||||
|
* - 性能监控和问题排查
|
||||||
|
* - 安全审计和行为追踪
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class AppLoggerService {
|
||||||
|
// 底层日志实例(优先 Pino,无则用内置 Logger)
|
||||||
|
private readonly logger: PinoLogger | Logger;
|
||||||
|
// 日志级别开关(生产环境可动态调整)
|
||||||
|
private readonly enableLevels: Record<LogLevel, boolean>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
// 注入 Pino Logger(可选,无则降级到内置 Logger)
|
||||||
|
@Optional() @Inject(PinoLogger) private readonly pinoLogger: PinoLogger,
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
) {
|
||||||
|
// 初始化底层日志实例
|
||||||
|
this.logger = this.pinoLogger || new Logger('AppLogger');
|
||||||
|
|
||||||
|
// 从环境变量读取启用的日志级别(默认开发环境全开启,生产环境仅开启 warn/error/fatal)
|
||||||
|
const env = this.configService.get('NODE_ENV', 'development');
|
||||||
|
this.enableLevels = {
|
||||||
|
trace: env === 'development',
|
||||||
|
debug: env === 'development',
|
||||||
|
info: env !== 'production',
|
||||||
|
warn: true,
|
||||||
|
error: true,
|
||||||
|
fatal: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用日志记录方法
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 封装所有日志级别的核心记录逻辑,统一处理日志格式化、上下文补充和敏感信息过滤
|
||||||
|
*
|
||||||
|
* 业务逻辑:
|
||||||
|
* 1. 检查日志级别是否启用
|
||||||
|
* 2. 补充默认上下文信息
|
||||||
|
* 3. 合并自定义上下文
|
||||||
|
* 4. 过滤敏感信息
|
||||||
|
* 5. 构造标准日志数据
|
||||||
|
* 6. 根据底层日志实例类型选择合适的输出方式
|
||||||
|
*
|
||||||
|
* @param level 日志级别,决定日志的重要程度和输出策略
|
||||||
|
* @param options 日志选项,包含消息内容、上下文信息等
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private log(level: LogLevel, options: LogOptions): void {
|
||||||
|
// 过滤禁用的日志级别(生产环境不输出 debug/trace)
|
||||||
|
if (!this.enableLevels[level]) return;
|
||||||
|
|
||||||
|
// 1. 补充默认上下文
|
||||||
|
const defaultContext: LogContext = {
|
||||||
|
module: options.context?.module || 'Unknown',
|
||||||
|
reqId: options.context?.reqId || 'no-req-id',
|
||||||
|
userId: options.context?.userId || 'anonymous',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
app: this.configService.get('APP_NAME', 'nest-app'),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. 合并上下文(自定义上下文覆盖默认)
|
||||||
|
const context = { ...defaultContext, ...options.context };
|
||||||
|
|
||||||
|
// 3. 敏感信息过滤(避免日志泄露密码/Token)
|
||||||
|
this.redactSensitiveData(context);
|
||||||
|
|
||||||
|
// 4. 构造日志数据
|
||||||
|
const logData = {
|
||||||
|
message: options.message,
|
||||||
|
context,
|
||||||
|
...(options.stack ? { stack: options.stack } : {}), // 仅错误级别携带栈信息
|
||||||
|
};
|
||||||
|
|
||||||
|
// 5. 适配 Pino/内置 Logger 的调用方式
|
||||||
|
if (this.pinoLogger) {
|
||||||
|
// Pino 调用方式:直接使用 pinoLogger 实例
|
||||||
|
switch (level) {
|
||||||
|
case 'debug':
|
||||||
|
this.pinoLogger.debug(logData.message, logData);
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
this.pinoLogger.info(logData.message, logData);
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
this.pinoLogger.warn(logData.message, logData);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
this.pinoLogger.error(logData.message, logData);
|
||||||
|
break;
|
||||||
|
case 'fatal':
|
||||||
|
this.pinoLogger.fatal(logData.message, logData);
|
||||||
|
break;
|
||||||
|
case 'trace':
|
||||||
|
this.pinoLogger.trace(logData.message, logData);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
this.pinoLogger.info(logData.message, logData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 内置 Logger 降级调用:根据级别调用对应方法
|
||||||
|
const builtInLogger = this.logger as Logger;
|
||||||
|
const contextString = JSON.stringify(logData.context);
|
||||||
|
|
||||||
|
switch (level) {
|
||||||
|
case 'debug':
|
||||||
|
builtInLogger.debug(logData.message, contextString);
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
builtInLogger.log(logData.message, contextString); // 内置 Logger 使用 log 方法代替 info
|
||||||
|
break;
|
||||||
|
case 'warn':
|
||||||
|
builtInLogger.warn(logData.message, contextString);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
case 'fatal': // fatal 级别降级为 error
|
||||||
|
builtInLogger.error(logData.message, options.stack || '', contextString);
|
||||||
|
break;
|
||||||
|
case 'trace':
|
||||||
|
builtInLogger.verbose(logData.message, contextString); // trace 级别降级为 verbose
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
builtInLogger.log(logData.message, contextString);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 敏感信息过滤方法
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 递归扫描日志数据中的敏感字段,将其替换为占位符,防止敏感信息泄露
|
||||||
|
*
|
||||||
|
* 业务逻辑:
|
||||||
|
* 1. 定义敏感字段关键词列表
|
||||||
|
* 2. 遍历数据对象的所有键
|
||||||
|
* 3. 检查键名是否包含敏感关键词
|
||||||
|
* 4. 将敏感字段值替换为 [REDACTED]
|
||||||
|
* 5. 递归处理嵌套对象
|
||||||
|
*
|
||||||
|
* @param data 需要过滤的日志数据对象
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private redactSensitiveData(data: Record<string, any>): void {
|
||||||
|
const sensitiveKeys = ['password', 'token', 'secret', 'authorization', 'cardNo'];
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (sensitiveKeys.some((sk) => key.toLowerCase().includes(sk))) {
|
||||||
|
data[key] = '[REDACTED]'; // 替换为占位符,或直接删除
|
||||||
|
}
|
||||||
|
// 递归过滤嵌套对象
|
||||||
|
if (typeof data[key] === 'object' && data[key] !== null) {
|
||||||
|
this.redactSensitiveData(data[key]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 公共日志记录方法 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录调试日志
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 记录详细的调试信息,主要用于开发环境的问题排查
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 方法调用参数记录
|
||||||
|
* - 中间计算结果输出
|
||||||
|
* - 详细的执行流程追踪
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* - 仅在开发环境启用
|
||||||
|
* - 生产环境自动禁用以提高性能
|
||||||
|
*
|
||||||
|
* @param message 调试消息内容
|
||||||
|
* @param context 调试上下文信息
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* this.logger.debug('开始处理用户请求', {
|
||||||
|
* module: 'UserService',
|
||||||
|
* operation: 'getUserInfo',
|
||||||
|
* userId: 'user123',
|
||||||
|
* params: { includeProfile: true }
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
debug(message: string, context?: LogContext): void {
|
||||||
|
this.log('debug', { message, context });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录信息日志
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 记录重要的业务操作和系统状态变更,用于业务监控和审计
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 用户登录成功
|
||||||
|
* - 重要业务操作完成
|
||||||
|
* - 系统状态变更
|
||||||
|
* - 关键流程节点记录
|
||||||
|
*
|
||||||
|
* @param message 信息消息内容
|
||||||
|
* @param context 操作上下文信息
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* this.logger.info('用户登录成功', {
|
||||||
|
* module: 'AuthService',
|
||||||
|
* operation: 'userLogin',
|
||||||
|
* userId: 'user123',
|
||||||
|
* timestamp: new Date().toISOString()
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
info(message: string, context?: LogContext): void {
|
||||||
|
this.log('info', { message, context });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录警告日志
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 记录需要关注但不影响正常业务流程的警告信息
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 参数验证失败
|
||||||
|
* - 权限检查失败
|
||||||
|
* - 资源不存在
|
||||||
|
* - 业务规则违反
|
||||||
|
* - 性能指标异常
|
||||||
|
*
|
||||||
|
* @param message 警告消息内容
|
||||||
|
* @param context 警告上下文信息
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* this.logger.warn('用户尝试访问不存在的资源', {
|
||||||
|
* module: 'ResourceService',
|
||||||
|
* operation: 'getResource',
|
||||||
|
* userId: 'user123',
|
||||||
|
* resourceId: 'res456',
|
||||||
|
* reason: 'resource_not_found'
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
warn(message: string, context?: LogContext): void {
|
||||||
|
this.log('warn', { message, context });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录错误日志
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 记录影响业务功能正常使用的错误信息,包含详细的错误上下文和堆栈信息
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 业务逻辑异常
|
||||||
|
* - 数据库操作失败
|
||||||
|
* - 第三方服务调用失败
|
||||||
|
* - 系统内部错误
|
||||||
|
*
|
||||||
|
* @param message 错误消息内容
|
||||||
|
* @param context 错误上下文信息
|
||||||
|
* @param stack 错误堆栈信息,用于问题定位
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* this.logger.error('数据库连接失败', {
|
||||||
|
* module: 'DatabaseService',
|
||||||
|
* operation: 'connect',
|
||||||
|
* error: error.message,
|
||||||
|
* timestamp: new Date().toISOString()
|
||||||
|
* }, error.stack);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
error(message: string, context?: LogContext, stack?: string): void {
|
||||||
|
this.log('error', { message, context, stack });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录致命错误日志
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 记录可能导致系统不可用的严重错误,需要立即处理
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 数据库完全不可用
|
||||||
|
* - 关键服务宕机
|
||||||
|
* - 系统资源耗尽
|
||||||
|
* - 安全漏洞被利用
|
||||||
|
*
|
||||||
|
* @param message 致命错误消息内容
|
||||||
|
* @param context 错误上下文信息
|
||||||
|
* @param stack 错误堆栈信息
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* this.logger.fatal('数据库连接池耗尽', {
|
||||||
|
* module: 'DatabaseService',
|
||||||
|
* operation: 'getConnection',
|
||||||
|
* activeConnections: 100,
|
||||||
|
* maxConnections: 100,
|
||||||
|
* timestamp: new Date().toISOString()
|
||||||
|
* }, error.stack);
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
fatal(message: string, context?: LogContext, stack?: string): void {
|
||||||
|
this.log('fatal', { message, context, stack });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录追踪日志
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 记录极细粒度的执行追踪信息,用于深度调试和性能分析
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 循环内的变量状态
|
||||||
|
* - 算法执行步骤
|
||||||
|
* - 性能关键路径追踪
|
||||||
|
* - 复杂业务逻辑的详细执行流程
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* - 仅在开发环境启用
|
||||||
|
* - 会产生大量日志,谨慎使用
|
||||||
|
*
|
||||||
|
* @param message 追踪消息内容
|
||||||
|
* @param context 追踪上下文信息
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* this.logger.trace('处理数组元素', {
|
||||||
|
* module: 'DataProcessor',
|
||||||
|
* operation: 'processArray',
|
||||||
|
* currentIndex: i,
|
||||||
|
* elementValue: array[i],
|
||||||
|
* totalElements: array.length
|
||||||
|
* });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
trace(message: string, context?: LogContext): void {
|
||||||
|
this.log('trace', { message, context });
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 便捷方法 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定请求上下文的日志记录器
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 创建一个绑定了特定请求上下文的日志记录器,自动携带请求相关信息
|
||||||
|
*
|
||||||
|
* 业务逻辑:
|
||||||
|
* 1. 从请求对象中提取关键信息
|
||||||
|
* 2. 构建基础上下文对象
|
||||||
|
* 3. 返回包装后的日志方法集合
|
||||||
|
* 4. 每次调用时自动合并基础上下文和额外上下文
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - HTTP 请求处理过程中的日志记录
|
||||||
|
* - WebSocket 连接的日志追踪
|
||||||
|
* - 需要关联用户行为的业务操作
|
||||||
|
*
|
||||||
|
* @param req HTTP 请求对象或类似的上下文对象
|
||||||
|
* @param module 模块名称,标识日志来源
|
||||||
|
* @returns 绑定了请求上下文的日志方法对象
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* ```typescript
|
||||||
|
* // 在 Controller 中使用
|
||||||
|
* const requestLogger = this.logger.bindRequest(req, 'UserController');
|
||||||
|
* requestLogger.info('开始处理用户请求', { action: 'getUserProfile' });
|
||||||
|
* requestLogger.error('处理失败', error.stack, { reason: 'database_error' });
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
bindRequest(req: any, module: string) {
|
||||||
|
const baseContext: LogContext = {
|
||||||
|
reqId: req.id || req.headers['x-request-id'],
|
||||||
|
userId: req.headers['x-user-id'] || 'anonymous',
|
||||||
|
ip: req.ip,
|
||||||
|
module,
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
debug: (message: string, extra?: LogContext) => this.debug(message, { ...baseContext, ...extra }),
|
||||||
|
info: (message: string, extra?: LogContext) => this.info(message, { ...baseContext, ...extra }),
|
||||||
|
warn: (message: string, extra?: LogContext) => this.warn(message, { ...baseContext, ...extra }),
|
||||||
|
error: (message: string, stack?: string, extra?: LogContext) => this.error(message, { ...baseContext, ...extra }, stack),
|
||||||
|
fatal: (message: string, stack?: string, extra?: LogContext) => this.fatal(message, { ...baseContext, ...extra }, stack),
|
||||||
|
trace: (message: string, extra?: LogContext) => this.trace(message, { ...baseContext, ...extra }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,7 +17,8 @@
|
|||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"baseUrl": "./",
|
"baseUrl": "./",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"strictNullChecks": false
|
"strictNullChecks": false,
|
||||||
|
"types": ["jest", "node"]
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
|
|||||||
Reference in New Issue
Block a user