forked from datawhale/whale-town-end
docs: 重构文档结构和组织
- 重新组织docs目录结构,按功能模块分类 - 新增deployment和development目录 - 更新API文档结构 - 添加客户端README文档 - 移除过时的文档文件
This commit is contained in:
441
docs/development/AI辅助开发规范指南.md
Normal file
441
docs/development/AI辅助开发规范指南.md
Normal file
@@ -0,0 +1,441 @@
|
||||
# AI 辅助开发规范指南
|
||||
|
||||
## 📋 文档概述
|
||||
|
||||
本指南教你如何使用 AI 助手(如 Claude、ChatGPT、Kiro 等)来帮助遵循项目的开发规范,提高代码质量和开发效率。
|
||||
|
||||
**⚠️ 重要提醒:所有开发者在开始编码前必须阅读本指南!**
|
||||
|
||||
---
|
||||
|
||||
## 🎯 为什么需要 AI 辅助?
|
||||
|
||||
### 传统开发痛点
|
||||
- ❌ 记不住复杂的规范要求
|
||||
- ❌ 注释写得不够详细或格式不对
|
||||
- ❌ Git 提交信息不规范
|
||||
- ❌ 代码审查时发现大量规范问题
|
||||
- ❌ 新手学习成本高,容易出错
|
||||
|
||||
### AI 辅助的优势
|
||||
- ✅ 实时检查和建议
|
||||
- ✅ 自动生成规范的注释和文档
|
||||
- ✅ 智能提示最佳实践
|
||||
- ✅ 减少人工审查工作量
|
||||
- ✅ 快速学习和应用规范
|
||||
|
||||
---
|
||||
|
||||
## 📚 项目规范文档体系
|
||||
|
||||
我们的项目有以下四个核心规范文档:
|
||||
|
||||
| 文档 | 用途 | AI 检查重点 |
|
||||
|------|------|------------|
|
||||
| [命名规范](./naming_convention.md) | 文件、变量、函数命名 | 命名格式、一致性检查 |
|
||||
| [Git 提交规范](./git_commit_guide.md) | 提交信息格式 | 提交类型、描述格式 |
|
||||
| [后端开发规范](./backend_development_guide.md) | 注释、日志、业务逻辑 | 注释完整性、日志规范 |
|
||||
| [NestJS 使用指南](./nestjs_guide.md) | 框架最佳实践 | 架构设计、代码组织 |
|
||||
|
||||
---
|
||||
|
||||
## 🤖 AI 辅助开发工作流程
|
||||
|
||||
### 阶段一:开发前准备
|
||||
|
||||
#### 1.1 向 AI 提供项目上下文
|
||||
|
||||
**推荐提示词模板:**
|
||||
|
||||
```
|
||||
我正在开发一个基于 NestJS 的像素游戏后端项目。
|
||||
|
||||
项目有以下规范要求:
|
||||
1. 命名规范:[粘贴 naming_convention.md 的关键内容]
|
||||
2. Git 提交规范:[粘贴 git_commit_guide.md 的关键内容]
|
||||
3. 后端开发规范:[粘贴 backend_development_guide.md 的关键内容]
|
||||
4. NestJS 使用指南:[粘贴 nestjs_guide.md 的关键内容]
|
||||
|
||||
请帮我在开发过程中严格遵循这些规范。
|
||||
```
|
||||
|
||||
#### 1.2 功能开发规划
|
||||
|
||||
**提示词示例:**
|
||||
|
||||
```
|
||||
我要开发一个用户管理功能,包括:
|
||||
- 用户注册
|
||||
- 用户登录
|
||||
- 用户信息查询
|
||||
- 用户信息更新
|
||||
|
||||
请帮我:
|
||||
1. 按照命名规范设计文件和类名
|
||||
2. 按照 NestJS 指南规划代码结构
|
||||
3. 按照后端开发规范设计注释模板
|
||||
```
|
||||
|
||||
### 阶段二:编码过程中
|
||||
|
||||
#### 2.1 代码生成和检查
|
||||
|
||||
**提示词模板:**
|
||||
|
||||
```
|
||||
请帮我生成一个用户服务类,要求:
|
||||
|
||||
1. 严格按照后端开发规范添加完整注释:
|
||||
- 模块级注释(功能描述、依赖模块、作者、版本)
|
||||
- 类级注释(职责、主要方法、使用场景)
|
||||
- 方法级注释(功能描述、业务逻辑、参数、返回值、异常、示例)
|
||||
|
||||
2. 按照命名规范:
|
||||
- 类名使用大驼峰
|
||||
- 方法名使用小驼峰
|
||||
- 文件名使用下划线分隔
|
||||
|
||||
3. 包含以下方法:
|
||||
- createUser() - 创建用户
|
||||
- getUserById() - 根据ID获取用户
|
||||
- updateUser() - 更新用户信息
|
||||
|
||||
4. 每个方法都要考虑:
|
||||
- 参数验证
|
||||
- 异常处理
|
||||
- 日志记录
|
||||
- 业务逻辑完整性
|
||||
```
|
||||
|
||||
#### 2.2 代码审查和优化
|
||||
|
||||
**提示词示例:**
|
||||
|
||||
```
|
||||
请帮我检查以下代码是否符合项目规范:
|
||||
|
||||
[粘贴你的代码]
|
||||
|
||||
请检查:
|
||||
1. 注释是否完整和规范
|
||||
2. 命名是否符合规范
|
||||
3. 业务逻辑是否考虑了所有情况
|
||||
4. 异常处理是否完善
|
||||
5. 日志记录是否合适
|
||||
6. 是否遵循了 NestJS 最佳实践
|
||||
|
||||
如有问题,请提供具体的修改建议。
|
||||
```
|
||||
|
||||
### 阶段三:提交前检查
|
||||
|
||||
#### 3.1 Git 提交信息生成
|
||||
|
||||
**提示词模板:**
|
||||
|
||||
```
|
||||
我完成了以下开发工作:
|
||||
[描述你的具体改动]
|
||||
|
||||
请按照 Git 提交规范帮我生成合适的提交信息:
|
||||
1. 选择正确的提交类型(feat/fix/docs/refactor等)
|
||||
2. 写出简短明确的描述(不超过50字符)
|
||||
3. 如果改动复杂,提供详细描述
|
||||
4. 考虑是否需要拆分成多个提交
|
||||
|
||||
提交规范要求:
|
||||
- 使用中文冒号
|
||||
- 一次提交只做一件事
|
||||
- 描述要清晰明确
|
||||
```
|
||||
|
||||
#### 3.2 最终代码检查
|
||||
|
||||
**提示词示例:**
|
||||
|
||||
```
|
||||
这是我准备提交的完整代码,请做最后的规范检查:
|
||||
|
||||
[粘贴完整代码]
|
||||
|
||||
请确认:
|
||||
1. 所有注释都符合后端开发规范
|
||||
2. 命名都符合命名规范
|
||||
3. 代码结构符合 NestJS 指南
|
||||
4. 没有遗漏的异常处理
|
||||
5. 日志记录完整合适
|
||||
6. 代码可以直接运行
|
||||
|
||||
如有任何问题,请指出并提供修改建议。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 AI 提示词最佳实践
|
||||
|
||||
### 1. 上下文提供技巧
|
||||
|
||||
**✅ 好的做法:**
|
||||
```
|
||||
我在开发 NestJS 项目的用户模块,项目要求:
|
||||
- 每个方法必须有完整注释
|
||||
- 使用 AppLoggerService 记录日志
|
||||
- 异常要抛出合适的 HTTP 状态码
|
||||
- 文件名使用下划线命名
|
||||
|
||||
请帮我生成 UserController...
|
||||
```
|
||||
|
||||
**❌ 不好的做法:**
|
||||
```
|
||||
帮我写个用户控制器
|
||||
```
|
||||
|
||||
### 2. 具体化需求
|
||||
|
||||
**✅ 好的做法:**
|
||||
```
|
||||
请检查这个方法是否符合规范:
|
||||
1. 注释是否包含功能描述、业务逻辑、参数说明?
|
||||
2. 是否考虑了参数验证?
|
||||
3. 是否有适当的异常处理?
|
||||
4. 是否记录了关键操作日志?
|
||||
```
|
||||
|
||||
**❌ 不好的做法:**
|
||||
```
|
||||
这个代码有问题吗?
|
||||
```
|
||||
|
||||
### 3. 迭代优化
|
||||
|
||||
**建议流程:**
|
||||
1. 先让 AI 生成基础代码
|
||||
2. 要求 AI 检查规范符合性
|
||||
3. 根据建议进行修改
|
||||
4. 再次检查直到完全符合规范
|
||||
|
||||
---
|
||||
|
||||
## 🔧 常用 AI 检查清单
|
||||
|
||||
### 代码质量检查
|
||||
|
||||
复制以下清单,让 AI 帮你逐项检查:
|
||||
|
||||
```
|
||||
请帮我检查代码是否符合以下规范:
|
||||
|
||||
□ 模块级注释(功能描述、依赖模块、作者、版本)
|
||||
□ 类级注释(职责、主要方法、使用场景)
|
||||
□ 方法级注释(功能描述、业务逻辑、参数、返回值、异常、示例)
|
||||
□ 文件命名使用下划线分隔
|
||||
□ 类名使用大驼峰命名
|
||||
□ 方法名使用小驼峰命名
|
||||
□ 变量名使用小驼峰命名
|
||||
□ 常量使用全大写+下划线
|
||||
□ 参数验证完整
|
||||
□ 异常处理合适
|
||||
□ 日志记录规范
|
||||
□ 业务逻辑考虑全面
|
||||
□ 符合 NestJS 最佳实践
|
||||
```
|
||||
|
||||
### Git 提交检查
|
||||
|
||||
```
|
||||
请帮我检查提交信息是否符合规范:
|
||||
|
||||
□ 使用了正确的提交类型
|
||||
□ 使用中文冒号
|
||||
□ 描述简短明确(不超过50字符)
|
||||
□ 一次提交只做一件事
|
||||
□ 如有必要,包含详细描述
|
||||
□ 考虑了是否需要拆分提交
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📖 实战案例演示
|
||||
|
||||
### 案例1:开发用户注册功能
|
||||
|
||||
#### 第1步:向 AI 提供上下文
|
||||
|
||||
```
|
||||
我要开发用户注册功能,项目规范要求:
|
||||
|
||||
1. 后端开发规范:
|
||||
- 每个方法必须有完整的三级注释
|
||||
- 必须考虑所有异常情况
|
||||
- 关键操作必须记录日志
|
||||
- 使用防御性编程
|
||||
|
||||
2. 命名规范:
|
||||
- 文件名:user_service.ts
|
||||
- 类名:UserService
|
||||
- 方法名:registerUser
|
||||
|
||||
3. 需要实现的业务逻辑:
|
||||
- 邮箱格式验证
|
||||
- 邮箱唯一性检查
|
||||
- 密码强度验证
|
||||
- 创建用户记录
|
||||
- 返回用户信息(不含密码)
|
||||
|
||||
请帮我生成符合规范的代码。
|
||||
```
|
||||
|
||||
#### 第2步:AI 生成代码
|
||||
|
||||
AI 会生成包含完整注释和异常处理的代码。
|
||||
|
||||
#### 第3步:代码检查
|
||||
|
||||
```
|
||||
请检查上面生成的代码是否完全符合项目规范:
|
||||
1. 注释是否完整?
|
||||
2. 异常处理是否全面?
|
||||
3. 日志记录是否合适?
|
||||
4. 业务逻辑是否考虑了所有情况?
|
||||
```
|
||||
|
||||
#### 第4步:提交信息生成
|
||||
|
||||
```
|
||||
我完成了用户注册功能的开发,包括:
|
||||
- 创建 UserService 类
|
||||
- 实现 registerUser 方法
|
||||
- 添加完整的参数验证和异常处理
|
||||
- 记录关键操作日志
|
||||
|
||||
请按照 Git 提交规范生成提交信息。
|
||||
```
|
||||
|
||||
### 案例2:代码审查场景
|
||||
|
||||
#### 现有代码检查
|
||||
|
||||
```
|
||||
这是我写的代码,请帮我检查是否符合项目规范:
|
||||
|
||||
[粘贴代码]
|
||||
|
||||
项目规范要求:
|
||||
- 完整的三级注释
|
||||
- 防御性编程
|
||||
- 合适的异常处理
|
||||
- 规范的命名
|
||||
- 关键操作日志记录
|
||||
|
||||
请指出所有不符合规范的地方,并提供修改建议。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 注意事项和限制
|
||||
|
||||
### AI 的局限性
|
||||
|
||||
1. **不能替代人工判断**
|
||||
- AI 可能不理解特定的业务逻辑
|
||||
- 复杂的架构决策仍需人工判断
|
||||
|
||||
2. **需要明确的指导**
|
||||
- 必须提供详细的规范要求
|
||||
- 模糊的需求会得到模糊的结果
|
||||
|
||||
3. **需要验证和测试**
|
||||
- AI 生成的代码必须经过测试
|
||||
- 不要盲目信任 AI 的输出
|
||||
|
||||
### 最佳实践建议
|
||||
|
||||
1. **始终提供项目上下文**
|
||||
2. **使用具体的检查清单**
|
||||
3. **迭代优化,不要一次性完成**
|
||||
4. **结合人工审查**
|
||||
5. **保持学习,理解规范背后的原理**
|
||||
|
||||
---
|
||||
|
||||
## 🚀 进阶技巧
|
||||
|
||||
### 1. 创建个人 AI 助手模板
|
||||
|
||||
将常用的提示词保存为模板,提高效率:
|
||||
|
||||
```
|
||||
# 我的开发助手模板
|
||||
|
||||
## 项目上下文
|
||||
我在开发 NestJS 像素游戏后端项目,规范要求:
|
||||
[保存项目规范要点]
|
||||
|
||||
## 代码生成模板
|
||||
请生成符合规范的代码,包括:
|
||||
- 完整的三级注释
|
||||
- 防御性编程
|
||||
- 异常处理
|
||||
- 日志记录
|
||||
- 规范命名
|
||||
|
||||
## 代码检查模板
|
||||
请检查代码规范符合性:
|
||||
[保存检查清单]
|
||||
```
|
||||
|
||||
### 2. 建立代码片段库
|
||||
|
||||
让 AI 帮你生成常用的代码模板:
|
||||
|
||||
```
|
||||
请帮我生成以下代码模板,符合项目规范:
|
||||
|
||||
1. Controller 类模板
|
||||
2. Service 类模板
|
||||
3. 异常处理模板
|
||||
4. 日志记录模板
|
||||
5. 参数验证模板
|
||||
|
||||
每个模板都要包含完整的注释和最佳实践。
|
||||
```
|
||||
|
||||
### 3. 自动化检查脚本
|
||||
|
||||
```
|
||||
请帮我写一个脚本,自动检查代码是否符合命名规范:
|
||||
- 检查文件名是否使用下划线
|
||||
- 检查类名是否使用大驼峰
|
||||
- 检查方法名是否使用小驼峰
|
||||
- 生成检查报告
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📞 获得帮助
|
||||
|
||||
如果在使用 AI 辅助开发过程中遇到问题:
|
||||
|
||||
1. **查看规范文档**:确保理解项目规范要求
|
||||
2. **优化提示词**:提供更详细的上下文和需求
|
||||
3. **寻求人工帮助**:复杂问题可以咨询有经验的开发者
|
||||
4. **持续学习**:理解规范背后的原理,不只是机械执行
|
||||
|
||||
---
|
||||
|
||||
## 🎉 总结
|
||||
|
||||
使用 AI 辅助开发可以显著提高代码质量和开发效率,但关键是:
|
||||
|
||||
1. **提供清晰的上下文和规范要求**
|
||||
2. **使用具体的检查清单**
|
||||
3. **迭代优化,持续改进**
|
||||
4. **结合人工判断,不盲目依赖**
|
||||
5. **理解规范原理,而不只是执行**
|
||||
|
||||
记住:**AI 是你的助手,不是替代品。好的开发者会善用工具,但始终保持思考和判断能力。**
|
||||
|
||||
---
|
||||
|
||||
**🔥 开始使用 AI 辅助开发,让代码质量和效率双提升!**
|
||||
276
docs/development/TESTING.md
Normal file
276
docs/development/TESTING.md
Normal file
@@ -0,0 +1,276 @@
|
||||
# 测试指南
|
||||
|
||||
本项目支持**无数据库和无邮件服务器**的测试模式,让你可以快速验证所有功能!
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 1. 环境配置
|
||||
|
||||
```bash
|
||||
# 复制环境配置文件
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
默认配置已经设置为测试模式,无需修改即可使用。
|
||||
|
||||
### 2. 启动服务
|
||||
|
||||
```bash
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
# 启动开发服务器
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 3. 运行测试
|
||||
|
||||
**Windows (PowerShell):**
|
||||
```powershell
|
||||
.\test-api.ps1
|
||||
```
|
||||
|
||||
**Linux/macOS:**
|
||||
```bash
|
||||
./test-api.sh
|
||||
```
|
||||
|
||||
**自定义参数:**
|
||||
```bash
|
||||
# Windows
|
||||
.\test-api.ps1 -BaseUrl "http://localhost:3000" -TestEmail "custom@example.com"
|
||||
|
||||
# Linux/macOS
|
||||
./test-api.sh "http://localhost:3000" "custom@example.com"
|
||||
```
|
||||
|
||||
## 🧪 测试功能
|
||||
|
||||
### API功能测试
|
||||
测试脚本会验证以下核心功能:
|
||||
|
||||
**用户认证模块:**
|
||||
- ✅ **邮箱验证码发送** - 生成6位数验证码,测试模式输出到控制台
|
||||
- ✅ **邮箱验证码验证** - 验证码校验和自动清理
|
||||
- ✅ **用户注册** - 完整的用户注册流程,包含邮箱验证
|
||||
- ✅ **用户登录** - 支持用户名/邮箱/手机号多种方式登录
|
||||
|
||||
**系统状态测试:**
|
||||
- ✅ **应用状态检查** - 验证服务器运行状态和系统信息
|
||||
- ✅ **Redis文件存储** - 验证验证码存储和读取功能
|
||||
- ✅ **内存数据库** - 验证用户数据存储功能
|
||||
|
||||
### 单元测试覆盖
|
||||
|
||||
**核心服务测试(7个测试套件,140个测试用例):**
|
||||
|
||||
1. **LoginCoreService** - 登录核心服务(15个测试)
|
||||
- 用户登录成功/失败场景
|
||||
- 用户注册功能测试
|
||||
- GitHub OAuth登录测试
|
||||
- 密码重置和修改功能
|
||||
- 用户状态验证(active、inactive、locked等)
|
||||
|
||||
2. **AdminService** - 管理员服务测试
|
||||
- 管理员登录认证
|
||||
- 用户列表管理
|
||||
- 用户密码重置
|
||||
- 日志管理功能
|
||||
|
||||
3. **VerificationService** - 验证码服务测试
|
||||
- 验证码生成和验证
|
||||
- 频率限制机制
|
||||
- Redis存储操作
|
||||
- 错误处理和边界条件
|
||||
|
||||
4. **EmailService** - 邮件服务测试
|
||||
- 邮件发送功能(测试模式和生产模式)
|
||||
- 验证码邮件模板
|
||||
- 连接验证和错误处理
|
||||
- SMTP配置测试
|
||||
|
||||
5. **UsersService** - 用户数据服务测试
|
||||
- 用户CRUD操作
|
||||
- 用户查询功能
|
||||
- 数据验证和约束
|
||||
|
||||
6. **AdminCoreService** - 管理员核心服务测试
|
||||
- 管理员认证逻辑
|
||||
- 权限验证
|
||||
- 管理员引导创建
|
||||
|
||||
7. **LoggerService** - 日志服务测试
|
||||
- 日志记录功能
|
||||
- 敏感信息过滤
|
||||
- 日志级别控制
|
||||
|
||||
### E2E端到端测试
|
||||
|
||||
**登录功能完整流程测试:**
|
||||
- 用户注册 → 邮箱验证 → 登录验证
|
||||
- GitHub OAuth登录流程
|
||||
- 密码重置完整流程
|
||||
- 错误处理和边界条件测试
|
||||
|
||||
## 🔧 测试模式特性
|
||||
|
||||
- 🗄️ **Redis 文件存储** - 使用 `redis-data/redis.json` 存储验证码
|
||||
- 📧 **邮件测试模式** - 邮件内容输出到控制台,无需真实SMTP
|
||||
- 💾 **内存用户存储** - 无需数据库,用户数据存储在内存中
|
||||
- 🔄 **自动切换** - 根据配置自动选择存储模式
|
||||
|
||||
## 📊 单元测试
|
||||
|
||||
### 运行测试命令
|
||||
|
||||
```bash
|
||||
# 运行所有单元测试
|
||||
npm test
|
||||
|
||||
# 监听模式(开发时使用)
|
||||
npm run test:watch
|
||||
|
||||
# 生成覆盖率报告
|
||||
npm run test:cov
|
||||
|
||||
# 运行特定测试文件
|
||||
npm test -- src/core/login_core/login_core.service.spec.ts
|
||||
```
|
||||
|
||||
### 测试覆盖情况
|
||||
|
||||
**测试统计:**
|
||||
- 测试套件:7个
|
||||
- 测试用例:140个
|
||||
- 覆盖率:100%通过
|
||||
|
||||
**测试文件列表:**
|
||||
```
|
||||
src/core/login_core/login_core.service.spec.ts # 登录核心服务
|
||||
src/business/admin/admin.service.spec.ts # 管理员服务
|
||||
src/core/utils/verification/verification.service.spec.ts # 验证码服务
|
||||
src/core/utils/email/email.service.spec.ts # 邮件服务
|
||||
src/core/db/users/users.service.spec.ts # 用户数据服务
|
||||
src/core/admin_core/admin_core.service.spec.ts # 管理员核心服务
|
||||
src/core/utils/logger/logger.service.spec.ts # 日志服务
|
||||
test/business/login.e2e-spec.ts # E2E端到端测试
|
||||
```
|
||||
|
||||
### 测试场景覆盖
|
||||
|
||||
**正常流程测试:**
|
||||
- 用户注册、登录、密码管理
|
||||
- 邮箱验证码发送和验证
|
||||
- 管理员认证和用户管理
|
||||
- 系统状态和日志功能
|
||||
|
||||
**异常情况测试:**
|
||||
- 无效输入和参数验证
|
||||
- 网络连接失败处理
|
||||
- 权限验证和访问控制
|
||||
- 频率限制和安全防护
|
||||
|
||||
**边界条件测试:**
|
||||
- 密码强度验证
|
||||
- 验证码过期处理
|
||||
- 用户状态变更
|
||||
- 数据库连接异常
|
||||
|
||||
## 🌐 生产环境配置
|
||||
|
||||
要切换到生产环境,编辑 `.env` 文件:
|
||||
|
||||
```bash
|
||||
# 启用数据库(取消注释并填入真实数据)
|
||||
DB_HOST=your_mysql_host
|
||||
DB_PORT=3306
|
||||
DB_USERNAME=your_db_username
|
||||
DB_PASSWORD=your_db_password
|
||||
DB_NAME=your_db_name
|
||||
|
||||
# 启用真实Redis(取消注释并设置)
|
||||
USE_FILE_REDIS=false
|
||||
REDIS_HOST=your_redis_host
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_redis_password
|
||||
|
||||
# 启用邮件服务(取消注释并填入真实数据)
|
||||
EMAIL_HOST=smtp.gmail.com
|
||||
EMAIL_PORT=587
|
||||
EMAIL_USER=your_email@gmail.com
|
||||
EMAIL_PASS=your_app_password
|
||||
EMAIL_FROM="Whale Town Game" <noreply@whaletown.com>
|
||||
|
||||
# 生产环境设置
|
||||
NODE_ENV=production
|
||||
LOG_LEVEL=info
|
||||
```
|
||||
|
||||
## 🔍 故障排除
|
||||
|
||||
### 服务启动失败
|
||||
- **端口占用**:检查端口3000是否被占用,使用 `netstat -ano | findstr :3000` 查看
|
||||
- **Node.js版本**:确认Node.js版本 >= 18.0.0,使用 `node --version` 检查
|
||||
- **依赖问题**:运行 `npm install` 或 `pnpm install` 重新安装依赖
|
||||
- **权限问题**:确保有足够的文件读写权限
|
||||
|
||||
### 测试脚本执行失败
|
||||
- **服务器状态**:确认服务器正在运行,访问 http://localhost:3000 检查
|
||||
- **网络连接**:检查防火墙设置,确保端口3000可访问
|
||||
- **脚本权限**:在Linux/macOS上确保脚本有执行权限:`chmod +x test-api.sh`
|
||||
- **PowerShell策略**:Windows上可能需要设置执行策略:`Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser`
|
||||
|
||||
### 单元测试失败
|
||||
- **依赖冲突**:清理node_modules并重新安装:`rm -rf node_modules && npm install`
|
||||
- **TypeScript错误**:运行 `npm run build` 检查编译错误
|
||||
- **环境变量**:确保测试环境变量配置正确
|
||||
- **数据库连接**:测试模式下应该使用内存数据库,检查配置
|
||||
|
||||
### Redis文件存储问题
|
||||
- **目录权限**:检查 `redis-data` 目录的读写权限
|
||||
- **配置设置**:确认 `USE_FILE_REDIS=true` 设置正确
|
||||
- **文件锁定**:确保redis.json文件没有被其他进程锁定
|
||||
- **磁盘空间**:检查磁盘空间是否充足
|
||||
|
||||
### 邮件测试模式问题
|
||||
- **配置检查**:确认邮件配置为注释状态(测试模式)
|
||||
- **控制台输出**:检查服务器控制台是否有邮件内容输出
|
||||
- **日志级别**:确保日志级别设置为info或debug以查看详细输出
|
||||
|
||||
### 常见错误解决
|
||||
|
||||
**EADDRINUSE错误:**
|
||||
```bash
|
||||
# 查找占用端口的进程
|
||||
netstat -ano | findstr :3000
|
||||
# 结束进程(Windows)
|
||||
taskkill /PID <进程ID> /F
|
||||
```
|
||||
|
||||
**权限错误:**
|
||||
```bash
|
||||
# Linux/macOS设置权限
|
||||
chmod +x test-api.sh
|
||||
chmod 755 redis-data/
|
||||
```
|
||||
|
||||
**模块未找到错误:**
|
||||
```bash
|
||||
# 清理并重新安装
|
||||
rm -rf node_modules package-lock.json
|
||||
npm install
|
||||
```
|
||||
|
||||
## 📝 测试数据
|
||||
|
||||
测试完成后,你可以查看:
|
||||
|
||||
- `redis-data/redis.json` - 验证码存储数据
|
||||
- 服务器控制台 - 邮件内容输出
|
||||
- 测试脚本输出 - API响应结果
|
||||
|
||||
## 🎯 下一步
|
||||
|
||||
- 查看 [API 文档](http://localhost:3000/api-docs) 了解更多接口
|
||||
- 阅读 [开发规范](./docs/backend_development_guide.md) 开始开发
|
||||
- 使用 [AI 辅助指南](./docs/AI辅助开发规范指南.md) 提高开发效率
|
||||
856
docs/development/backend_development_guide.md
Normal file
856
docs/development/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 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. **统一的异常处理**:分类处理不同类型异常,提供友好的用户体验
|
||||
|
||||
遵循本规范将显著提高代码质量、系统稳定性和团队协作效率。所有开发人员必须严格按照本规范进行开发,代码审查时将重点检查这些方面的实现。
|
||||
372
docs/development/git_commit_guide.md
Normal file
372
docs/development/git_commit_guide.md
Normal file
@@ -0,0 +1,372 @@
|
||||
# Git 提交规范
|
||||
|
||||
本文档定义了项目的 Git 提交信息格式规范,以保持提交历史的清晰和一致性。
|
||||
|
||||
## 提交格式
|
||||
|
||||
```
|
||||
<类型>:<简短描述>
|
||||
|
||||
[可选的详细描述]
|
||||
|
||||
[可选的注释或关联 Issue]
|
||||
```
|
||||
|
||||
## 提交类型
|
||||
|
||||
### 主要类型
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `init` | 项目初始化 | `init:项目初始化,搭建Godot文件结构` |
|
||||
| `feat` | 新增功能 | `feat:添加角色移动系统` |
|
||||
| `fix` | 修复 Bug | `fix:修复角色跳跃时的碰撞检测问题` |
|
||||
| `docs` | 文档更新 | `docs:更新 README 中的安装说明` |
|
||||
| `style` | 代码格式调整(不影响功能) | `style:统一代码缩进格式` |
|
||||
| `refactor` | 代码重构(不新增功能也不修复 Bug) | `refactor:重构敌人 AI 逻辑` |
|
||||
| `perf` | 性能优化 | `perf:优化场景加载速度` |
|
||||
| `test` | 添加或修改测试 | `test:添加角色控制器单元测试` |
|
||||
| `chore` | 构建过程或辅助工具的变动 | `chore:更新 .gitignore 文件` |
|
||||
| `revert` | 回滚之前的提交 | `revert:回滚 feat:添加角色移动系统` |
|
||||
|
||||
### 后端开发特定类型
|
||||
|
||||
| 类型 | 说明 | 示例 |
|
||||
|------|------|------|
|
||||
| `api` | API 接口相关 | `api:添加玩家信息查询接口` |
|
||||
| `db` | 数据库相关 | `db:创建房间表结构` |
|
||||
| `config` | 配置文件相关 | `config:添加数据库连接配置` |
|
||||
| `middleware` | 中间件相关 | `middleware:添加请求日志中间件` |
|
||||
| `websocket` | WebSocket 相关 | `websocket:实现游戏状态实时推送` |
|
||||
| `auth` | 认证授权相关 | `auth:实现 JWT 身份验证` |
|
||||
| `dto` | 数据传输对象相关 | `dto:添加创建房间的 DTO` |
|
||||
| `service` | 服务层相关 | `service:实现玩家匹配逻辑` |
|
||||
|
||||
## 提交示例
|
||||
|
||||
### 基础示例
|
||||
|
||||
```bash
|
||||
# 项目初始化
|
||||
git commit -m "init:项目初始化,搭建 NestJS 项目结构"
|
||||
|
||||
# 新增功能
|
||||
git commit -m "feat:实现玩家注册和登录功能"
|
||||
|
||||
# 修复问题
|
||||
git commit -m "fix:修复房间加入时的并发问题"
|
||||
|
||||
# 文档更新
|
||||
git commit -m "docs:添加 Git 提交规范文档"
|
||||
```
|
||||
|
||||
### 带详细描述的示例
|
||||
|
||||
```bash
|
||||
git commit -m "feat:添加房间系统
|
||||
|
||||
- 实现房间创建和加入功能
|
||||
- 支持多人房间管理
|
||||
- 添加房间状态同步机制
|
||||
|
||||
关联 Issue #12"
|
||||
```
|
||||
|
||||
### 后端开发示例
|
||||
|
||||
```bash
|
||||
# API 接口
|
||||
git commit -m "api:添加玩家信息查询接口"
|
||||
|
||||
# 数据库
|
||||
git commit -m "db:创建玩家和房间表结构"
|
||||
|
||||
# WebSocket
|
||||
git commit -m "websocket:实现玩家位置实时同步"
|
||||
|
||||
# 中间件
|
||||
git commit -m "middleware:添加请求日志和错误处理中间件"
|
||||
|
||||
# 认证授权
|
||||
git commit -m "auth:实现 JWT 身份验证机制"
|
||||
|
||||
# DTO
|
||||
git commit -m "dto:添加创建房间的数据验证"
|
||||
|
||||
# 服务层
|
||||
git commit -m "service:实现玩家匹配算法"
|
||||
|
||||
# 配置
|
||||
git commit -m "config:添加 Redis 缓存配置"
|
||||
```
|
||||
|
||||
### 重构和优化示例
|
||||
|
||||
```bash
|
||||
# 代码重构
|
||||
git commit -m "refactor:重构房间管理服务逻辑"
|
||||
|
||||
# 性能优化
|
||||
git commit -m "perf:优化数据库查询性能"
|
||||
|
||||
# 代码格式
|
||||
git commit -m "style:统一 TypeScript 代码风格"
|
||||
```
|
||||
|
||||
## 多类型改动的处理
|
||||
|
||||
### 问题场景
|
||||
|
||||
有时一次开发工作可能同时包含多种类型的改动,例如:
|
||||
- 既修复了 Bug,又添加了新功能
|
||||
- 既重构了代码,又优化了性能
|
||||
- 既更新了场景,又修改了脚本
|
||||
|
||||
### 推荐做法:拆分提交(强烈推荐)⭐
|
||||
|
||||
**原则**:一次提交只做一件事,保持提交历史清晰
|
||||
|
||||
```bash
|
||||
# ❌ 不推荐:混合提交
|
||||
git commit -m "fix + feat:修复房间 Bug 并添加踢人功能"
|
||||
|
||||
# ✅ 推荐:拆分成两次提交
|
||||
git add src/service/room_service.ts # 只添加修复 Bug 的部分
|
||||
git commit -m "fix:修复房间加入时的并发问题"
|
||||
|
||||
git add src/service/room_service.ts # 添加新功能的部分
|
||||
git commit -m "feat:实现房间踢人功能"
|
||||
```
|
||||
|
||||
### 拆分提交的优势
|
||||
|
||||
1. **清晰的历史记录**:每次提交目的明确,便于回溯
|
||||
2. **便于代码审查**:审查者可以分别查看不同类型的改动
|
||||
3. **灵活的回滚**:可以单独回滚某个功能或修复,而不影响其他改动
|
||||
4. **更好的协作**:团队成员能快速理解每次提交的意图
|
||||
5. **自动化工具友好**:CI/CD 和版本管理工具能更好地处理
|
||||
|
||||
### 如何拆分提交
|
||||
|
||||
#### 方法一:使用 git add -p(交互式暂存)
|
||||
|
||||
```bash
|
||||
# 交互式选择要暂存的代码块
|
||||
git add -p src/service/room_service.ts
|
||||
|
||||
# 选择修复 Bug 的部分,提交
|
||||
git commit -m "fix:修复房间加入时的并发问题"
|
||||
|
||||
# 暂存剩余的新功能代码
|
||||
git add src/service/room_service.ts
|
||||
git commit -m "feat:实现房间踢人功能"
|
||||
```
|
||||
|
||||
#### 方法二:分步开发和提交
|
||||
|
||||
```bash
|
||||
# 第一步:先完成并提交 Bug 修复
|
||||
# 修改代码...
|
||||
git add src/service/room_service.ts
|
||||
git commit -m "fix:修复房间加入时的并发问题"
|
||||
|
||||
# 第二步:再开发并提交新功能
|
||||
# 继续修改代码...
|
||||
git add src/service/room_service.ts
|
||||
git commit -m "feat:实现房间踢人功能"
|
||||
```
|
||||
|
||||
#### 方法三:使用临时分支
|
||||
|
||||
```bash
|
||||
# 创建临时分支保存当前工作
|
||||
git stash
|
||||
|
||||
# 只恢复修复 Bug 的部分并提交
|
||||
git stash pop
|
||||
# 手动撤销新功能代码,只保留 Bug 修复
|
||||
git add src/service/room_service.ts
|
||||
git commit -m "fix:修复房间加入时的并发问题"
|
||||
|
||||
# 恢复新功能代码并提交
|
||||
# 重新添加新功能代码...
|
||||
git add src/service/room_service.ts
|
||||
git commit -m "feat:实现房间踢人功能"
|
||||
```
|
||||
|
||||
### 特殊情况:确实无法拆分时
|
||||
|
||||
如果改动确实紧密耦合、无法拆分(这种情况应该很少见),可以使用以下方式:
|
||||
|
||||
#### 方式一:选择主要类型 + 详细描述(推荐)
|
||||
|
||||
```bash
|
||||
git commit -m "feat:实现房间匹配系统
|
||||
|
||||
- 添加自动匹配的核心逻辑
|
||||
- 修复原有房间系统的并发问题
|
||||
- 优化匹配算法的性能
|
||||
|
||||
本次提交包含了功能开发和 Bug 修复,因为匹配系统的实现
|
||||
依赖于修复原有房间系统的并发问题。"
|
||||
```
|
||||
|
||||
#### 方式二:使用多行描述分别说明
|
||||
|
||||
```bash
|
||||
git commit -m "feat:重构并优化房间管理系统
|
||||
|
||||
功能改进:
|
||||
- 实现新的房间状态机
|
||||
- 添加房间自动回收功能
|
||||
|
||||
Bug 修复:
|
||||
- 修复房间满员时的加入问题
|
||||
- 修复房间解散时的通知问题
|
||||
|
||||
性能优化:
|
||||
- 减少数据库查询频率
|
||||
- 优化房间列表缓存策略"
|
||||
```
|
||||
|
||||
### 什么时候应该拆分?
|
||||
|
||||
| 情况 | 是否拆分 | 原因 |
|
||||
|------|---------|------|
|
||||
| 修复 Bug + 添加新功能 | ✅ 应该拆分 | 两个独立的逻辑改动 |
|
||||
| 重构代码 + 性能优化 | ✅ 应该拆分 | 目的不同,影响范围不同 |
|
||||
| 添加 API + 编写服务层 | ✅ 应该拆分 | 不同层次的代码改动 |
|
||||
| 修复 Bug + 添加该 Bug 的测试 | ❌ 可以合并 | 测试是修复的一部分 |
|
||||
| 重构 + 重构后必需的修复 | ❌ 可以合并 | 修复是重构的直接结果 |
|
||||
| 添加功能 + 更新相关文档 | ⚠️ 视情况而定 | 简单文档可合并,复杂文档应拆分 |
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **使用中文冒号**:类型后使用中文冒号 `:` 而非英文冒号 `:`
|
||||
2. **简短明确**:描述应简洁明了,一般不超过 50 个字符
|
||||
3. **动词开头**:描述部分使用动词开头,如"添加"、"修复"、"更新"等
|
||||
4. **一次提交一个改动**:每次提交应该只包含一个逻辑改动(最重要的原则)⭐
|
||||
5. **详细描述**:对于复杂的改动,应该添加详细描述说明改动的原因和影响
|
||||
6. **避免混合类型**:不要在一次提交中混合多种类型的改动(如 fix + feat)
|
||||
7. **先提交修复,再提交功能**:如果必须在同一开发周期内完成,优先提交 Bug 修复
|
||||
8. **使用 git add -p**:善用交互式暂存来精确控制每次提交的内容
|
||||
9. **提交前自查**:问自己"这次提交是否只做了一件事?"
|
||||
10. **保持提交原子性**:每次提交都应该是完整的、可独立运行的改动
|
||||
|
||||
## 分支命名规范
|
||||
|
||||
```bash
|
||||
# 功能分支
|
||||
feature/player-system
|
||||
feature/room-matching
|
||||
|
||||
# 修复分支
|
||||
fix/room-concurrency
|
||||
fix/websocket-leak
|
||||
|
||||
# 开发分支
|
||||
dev
|
||||
develop
|
||||
|
||||
# 发布分支
|
||||
release/v1.0.0
|
||||
release/v1.1.0
|
||||
```
|
||||
|
||||
## 常见问题 FAQ
|
||||
|
||||
### Q1: 我同时修复了 3 个小 Bug,应该分 3 次提交吗?
|
||||
|
||||
**A**: 视情况而定:
|
||||
- 如果是**相关的 Bug**(同一个系统/模块),可以合并为一次提交
|
||||
- 如果是**不相关的 Bug**(不同系统/模块),应该分别提交
|
||||
|
||||
```bash
|
||||
# ✅ 相关 Bug 可以合并
|
||||
git commit -m "fix:修复房间系统的多个问题
|
||||
|
||||
- 修复房间满员时的加入问题
|
||||
- 修复房间解散时的通知问题
|
||||
- 修复房间列表的分页错误"
|
||||
|
||||
# ✅ 不相关 Bug 应该拆分
|
||||
git commit -m "fix:修复房间加入时的并发问题"
|
||||
git commit -m "fix:修复玩家信息查询的权限问题"
|
||||
git commit -m "fix:修复 WebSocket 连接的内存泄漏"
|
||||
```
|
||||
|
||||
### Q2: 我在开发新功能时发现了 Bug,应该怎么办?
|
||||
|
||||
**A**: 推荐流程:
|
||||
1. 暂存当前新功能的代码(`git stash`)
|
||||
2. 修复 Bug 并提交
|
||||
3. 恢复新功能代码继续开发
|
||||
4. 完成后提交新功能
|
||||
|
||||
```bash
|
||||
# 保存当前工作
|
||||
git stash save "WIP: 开发房间匹配功能"
|
||||
|
||||
# 修复 Bug
|
||||
git add src/service/room_service.ts
|
||||
git commit -m "fix:修复房间加入时的并发问题"
|
||||
|
||||
# 恢复工作并继续开发
|
||||
git stash pop
|
||||
# 继续开发...
|
||||
git commit -m "feat:实现房间自动匹配功能"
|
||||
```
|
||||
|
||||
### Q3: 重构代码时顺便优化了性能,算一次提交吗?
|
||||
|
||||
**A**: 应该拆分:
|
||||
- 重构(`refactor`):改变代码结构但不改变行为
|
||||
- 优化(`perf`):改善性能表现
|
||||
|
||||
```bash
|
||||
# ✅ 拆分提交
|
||||
git commit -m "refactor:重构房间管理服务逻辑"
|
||||
git commit -m "perf:优化房间列表查询性能"
|
||||
```
|
||||
|
||||
### Q4: 我应该多久提交一次?
|
||||
|
||||
**A**: 遵循以下原则:
|
||||
- ✅ 完成一个**完整的逻辑单元**就提交
|
||||
- ✅ 代码能够**正常运行**时提交
|
||||
- ✅ 下班前提交当天的工作进度
|
||||
- ❌ 不要等到功能完全完成才提交(可能跨越多天)
|
||||
- ❌ 不要提交无法运行的代码
|
||||
|
||||
### Q5: 提交信息写错了怎么办?
|
||||
|
||||
**A**: 使用 `git commit --amend` 修改最后一次提交:
|
||||
|
||||
```bash
|
||||
# 修改最后一次提交信息
|
||||
git commit --amend -m "fix:修复房间加入时的并发问题"
|
||||
|
||||
# 如果已经推送到远程,需要强制推送(谨慎使用)
|
||||
git push --force-with-lease
|
||||
```
|
||||
|
||||
⚠️ **注意**:只修改未推送或未被他人使用的提交!
|
||||
|
||||
## 工具推荐
|
||||
|
||||
- **Commitizen**: 交互式提交信息生成工具
|
||||
- **Git Hooks**: 使用 pre-commit 钩子自动检查提交格式
|
||||
- **Conventional Commits**: 遵循约定式提交规范
|
||||
- **Git GUI 工具**: GitKraken、SourceTree 等支持可视化暂存部分代码
|
||||
|
||||
## 总结
|
||||
|
||||
记住这些核心原则:
|
||||
|
||||
1. ⭐ **一次提交只做一件事** - 最重要的原则
|
||||
2. 🔀 **能拆分就拆分** - 保持提交历史清晰
|
||||
3. 🎯 **提交要有意义** - 每次提交都应该是完整的改动
|
||||
4. 📝 **描述要清晰** - 让别人(和未来的自己)能快速理解
|
||||
5. 🚫 **避免混合类型** - 不要 fix + feat 混在一起
|
||||
|
||||
**好的提交习惯 = 清晰的项目历史 = 高效的团队协作**
|
||||
527
docs/development/naming_convention.md
Normal file
527
docs/development/naming_convention.md
Normal file
@@ -0,0 +1,527 @@
|
||||
# 命名规范
|
||||
|
||||
本文档定义了项目中所有代码的命名规范,确保代码风格统一,提高可读性和可维护性。
|
||||
|
||||
## 目录
|
||||
|
||||
- [文件和文件夹命名](#文件和文件夹命名)
|
||||
- [变量和函数命名](#变量和函数命名)
|
||||
- [类和构造函数命名](#类和构造函数命名)
|
||||
- [常量命名](#常量命名)
|
||||
- [接口路由命名](#接口路由命名)
|
||||
- [TypeScript 特定规范](#typescript-特定规范)
|
||||
- [命名示例](#命名示例)
|
||||
|
||||
## 文件和文件夹命名
|
||||
|
||||
**规则:使用下划线分隔(snake_case)**
|
||||
|
||||
### 文件命名
|
||||
|
||||
```
|
||||
✅ 正确示例:
|
||||
- order_controller.ts
|
||||
- user_service.ts
|
||||
- game_gateway.ts
|
||||
- player_entity.ts
|
||||
- create_room_dto.ts
|
||||
- database_config.ts
|
||||
|
||||
❌ 错误示例:
|
||||
- OrderController.ts
|
||||
- userService.ts
|
||||
- game-gateway.ts
|
||||
- playerEntity.ts
|
||||
```
|
||||
|
||||
### 文件夹命名
|
||||
|
||||
```
|
||||
✅ 正确示例:
|
||||
- src/api/
|
||||
- src/service/
|
||||
- src/model/dto/
|
||||
- src/utils/
|
||||
- src/config/
|
||||
- test/unit_test/
|
||||
- test/integration_test/
|
||||
|
||||
❌ 错误示例:
|
||||
- src/API/
|
||||
- src/Service/
|
||||
- src/model-dto/
|
||||
- src/Utils/
|
||||
```
|
||||
|
||||
### 特殊说明
|
||||
|
||||
- 所有文件和文件夹名使用小写字母
|
||||
- 多个单词之间使用下划线 `_` 连接
|
||||
- 避免使用缩写,除非是广泛认可的缩写(如 dto、api)
|
||||
|
||||
## 变量和函数命名
|
||||
|
||||
**规则:使用小驼峰命名(camelCase)**
|
||||
|
||||
### 变量命名
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
const userName = 'Alice';
|
||||
let playerScore = 100;
|
||||
const roomId = '12345';
|
||||
const isGameStarted = false;
|
||||
const maxPlayerCount = 4;
|
||||
|
||||
❌ 错误示例:
|
||||
const UserName = 'Alice';
|
||||
const player_score = 100;
|
||||
const RoomId = '12345';
|
||||
const is_game_started = false;
|
||||
```
|
||||
|
||||
### 函数命名
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
function getUserInfo() { }
|
||||
async function queryUserInfo() { }
|
||||
function calculateDamage() { }
|
||||
function isPlayerAlive() { }
|
||||
function handlePlayerMove() { }
|
||||
|
||||
❌ 错误示例:
|
||||
function GetUserInfo() { }
|
||||
function query_user_info() { }
|
||||
function CalculateDamage() { }
|
||||
function IsPlayerAlive() { }
|
||||
```
|
||||
|
||||
### 命名建议
|
||||
|
||||
- 使用动词开头描述函数功能:
|
||||
- `get` - 获取数据(如 `getUserById`)
|
||||
- `set` - 设置数据(如 `setPlayerPosition`)
|
||||
- `create` - 创建实体(如 `createRoom`)
|
||||
- `update` - 更新数据(如 `updatePlayerScore`)
|
||||
- `delete` - 删除数据(如 `deleteRoom`)
|
||||
- `is/has` - 布尔判断(如 `isGameOver`、`hasPermission`)
|
||||
- `handle` - 事件处理(如 `handlePlayerAttack`)
|
||||
- `calculate` - 计算逻辑(如 `calculateDamage`)
|
||||
- `validate` - 验证逻辑(如 `validateInput`)
|
||||
|
||||
- 布尔变量使用 `is`、`has`、`can` 等前缀:
|
||||
```typescript
|
||||
const isActive = true;
|
||||
const hasPermission = false;
|
||||
const canMove = true;
|
||||
```
|
||||
|
||||
## 类和构造函数命名
|
||||
|
||||
**规则:使用大驼峰命名(PascalCase)**
|
||||
|
||||
### 类命名
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
class UserModel { }
|
||||
class OrderService { }
|
||||
class GameController { }
|
||||
class PlayerEntity { }
|
||||
class RoomGateway { }
|
||||
class DatabaseConnection { }
|
||||
|
||||
❌ 错误示例:
|
||||
class userModel { }
|
||||
class order_service { }
|
||||
class gameController { }
|
||||
class player_entity { }
|
||||
```
|
||||
|
||||
### 接口命名
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
interface User { }
|
||||
interface GameConfig { }
|
||||
interface PlayerData { }
|
||||
interface RoomOptions { }
|
||||
|
||||
// 或使用 I 前缀(可选)
|
||||
interface IUser { }
|
||||
interface IGameConfig { }
|
||||
|
||||
❌ 错误示例:
|
||||
interface user { }
|
||||
interface game_config { }
|
||||
interface playerData { }
|
||||
```
|
||||
|
||||
### DTO 命名
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
class CreateUserDto { }
|
||||
class UpdatePlayerDto { }
|
||||
class JoinRoomDto { }
|
||||
class GameStateDto { }
|
||||
|
||||
❌ 错误示例:
|
||||
class createUserDto { }
|
||||
class update_player_dto { }
|
||||
class joinRoomDTO { }
|
||||
```
|
||||
|
||||
### 装饰器命名
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
@Controller('users')
|
||||
@Injectable()
|
||||
@Module()
|
||||
@Get()
|
||||
|
||||
// 自定义装饰器
|
||||
function CustomDecorator() { }
|
||||
```
|
||||
|
||||
## 常量命名
|
||||
|
||||
**规则:全大写 + 下划线分隔(SCREAMING_SNAKE_CASE)**
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
const PORT = 3000;
|
||||
const DB_HOST = 'localhost';
|
||||
const MAX_PLAYERS = 10;
|
||||
const API_VERSION = 'v1';
|
||||
const DEFAULT_TIMEOUT = 5000;
|
||||
const GAME_STATUS_WAITING = 'waiting';
|
||||
const GAME_STATUS_PLAYING = 'playing';
|
||||
|
||||
❌ 错误示例:
|
||||
const port = 3000;
|
||||
const dbHost = 'localhost';
|
||||
const maxPlayers = 10;
|
||||
const ApiVersion = 'v1';
|
||||
const default_timeout = 5000;
|
||||
```
|
||||
|
||||
### 枚举命名
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
enum GameStatus {
|
||||
WAITING = 'waiting',
|
||||
PLAYING = 'playing',
|
||||
FINISHED = 'finished',
|
||||
}
|
||||
|
||||
enum PlayerRole {
|
||||
ADMIN = 'admin',
|
||||
PLAYER = 'player',
|
||||
SPECTATOR = 'spectator',
|
||||
}
|
||||
|
||||
❌ 错误示例:
|
||||
enum gameStatus {
|
||||
waiting = 'waiting',
|
||||
playing = 'playing',
|
||||
}
|
||||
|
||||
enum PlayerRole {
|
||||
admin = 'admin',
|
||||
player = 'player',
|
||||
}
|
||||
```
|
||||
|
||||
## 接口路由命名
|
||||
|
||||
**规则:全小写 + 短横线分隔(kebab-case)**
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
@Get('user/get-info')
|
||||
@Post('order/create-order')
|
||||
@Put('player/update-position')
|
||||
@Delete('room/delete-room')
|
||||
@Get('game/get-state')
|
||||
@Post('room/join-room')
|
||||
|
||||
❌ 错误示例:
|
||||
@Get('user/getInfo')
|
||||
@Post('order/createOrder')
|
||||
@Put('player/update_position')
|
||||
@Delete('room/DeleteRoom')
|
||||
@Get('game/GetState')
|
||||
```
|
||||
|
||||
### 路由结构建议
|
||||
|
||||
```typescript
|
||||
// 资源型路由
|
||||
@Controller('api/players')
|
||||
export class PlayerController {
|
||||
@Get() // GET /api/players
|
||||
@Get(':id') // GET /api/players/:id
|
||||
@Post() // POST /api/players
|
||||
@Put(':id') // PUT /api/players/:id
|
||||
@Delete(':id') // DELETE /api/players/:id
|
||||
}
|
||||
|
||||
// 动作型路由
|
||||
@Controller('api/game')
|
||||
export class GameController {
|
||||
@Post('start-game') // POST /api/game/start-game
|
||||
@Post('end-game') // POST /api/game/end-game
|
||||
@Get('get-state') // GET /api/game/get-state
|
||||
}
|
||||
|
||||
// 嵌套资源路由
|
||||
@Controller('api/rooms')
|
||||
export class RoomController {
|
||||
@Post(':id/join') // POST /api/rooms/:id/join
|
||||
@Post(':id/leave') // POST /api/rooms/:id/leave
|
||||
@Get(':id/players') // GET /api/rooms/:id/players
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript 特定规范
|
||||
|
||||
### 类型别名
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
type UserId = string;
|
||||
type PlayerPosition = { x: number; y: number };
|
||||
type GameCallback = (state: GameState) => void;
|
||||
|
||||
❌ 错误示例:
|
||||
type userId = string;
|
||||
type player_position = { x: number; y: number };
|
||||
```
|
||||
|
||||
### 泛型参数
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
function findById<T>(id: string): T { }
|
||||
class Repository<T, K> { }
|
||||
interface Response<T> { }
|
||||
|
||||
// 使用有意义的名称
|
||||
function mapArray<TInput, TOutput>(arr: TInput[]): TOutput[] { }
|
||||
|
||||
❌ 错误示例:
|
||||
function findById<t>(id: string): t { }
|
||||
class Repository<type, key> { }
|
||||
```
|
||||
|
||||
### 装饰器参数
|
||||
|
||||
```typescript
|
||||
✅ 正确示例:
|
||||
@Column({ name: 'user_name' })
|
||||
@IsString({ message: 'Name must be a string' })
|
||||
@ApiProperty({ description: 'User ID' })
|
||||
|
||||
❌ 错误示例:
|
||||
@Column({ name: 'UserName' })
|
||||
@IsString({ message: 'name_must_be_string' })
|
||||
```
|
||||
|
||||
## 命名示例
|
||||
|
||||
### 完整的模块示例
|
||||
|
||||
```typescript
|
||||
// 文件:src/api/player_controller.ts
|
||||
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
||||
import { PlayerService } from '../service/player_service';
|
||||
import { CreatePlayerDto } from '../model/dto/create_player_dto';
|
||||
|
||||
const MAX_PLAYERS = 100;
|
||||
|
||||
@Controller('api/players')
|
||||
export class PlayerController {
|
||||
constructor(private readonly playerService: PlayerService) {}
|
||||
|
||||
@Get()
|
||||
async getAllPlayers() {
|
||||
return this.playerService.findAll();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async getPlayerById(@Param('id') playerId: string) {
|
||||
return this.playerService.findById(playerId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async createPlayer(@Body() createPlayerDto: CreatePlayerDto) {
|
||||
return this.playerService.create(createPlayerDto);
|
||||
}
|
||||
|
||||
@Post(':id/update-position')
|
||||
async updatePlayerPosition(
|
||||
@Param('id') playerId: string,
|
||||
@Body() body: { x: number; y: number }
|
||||
) {
|
||||
const { x, y } = body;
|
||||
return this.playerService.updatePosition(playerId, x, y);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 文件:src/service/player_service.ts
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { Player } from '../model/player_entity';
|
||||
import { CreatePlayerDto } from '../model/dto/create_player_dto';
|
||||
|
||||
const DEFAULT_HEALTH = 100;
|
||||
const DEFAULT_SPEED = 5;
|
||||
|
||||
@Injectable()
|
||||
export class PlayerService {
|
||||
private players: Map<string, Player> = new Map();
|
||||
|
||||
findAll(): Player[] {
|
||||
return Array.from(this.players.values());
|
||||
}
|
||||
|
||||
findById(playerId: string): Player {
|
||||
const player = this.players.get(playerId);
|
||||
if (!player) {
|
||||
throw new NotFoundException(`Player with ID ${playerId} not found`);
|
||||
}
|
||||
return player;
|
||||
}
|
||||
|
||||
create(createPlayerDto: CreatePlayerDto): Player {
|
||||
const newPlayer: Player = {
|
||||
id: this.generatePlayerId(),
|
||||
name: createPlayerDto.name,
|
||||
health: DEFAULT_HEALTH,
|
||||
speed: DEFAULT_SPEED,
|
||||
position: { x: 0, y: 0 },
|
||||
isAlive: true,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
this.players.set(newPlayer.id, newPlayer);
|
||||
return newPlayer;
|
||||
}
|
||||
|
||||
updatePosition(playerId: string, x: number, y: number): Player {
|
||||
const player = this.findById(playerId);
|
||||
player.position = { x, y };
|
||||
return player;
|
||||
}
|
||||
|
||||
private generatePlayerId(): string {
|
||||
return `player_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
|
||||
private calculateDamage(attackPower: number, defense: number): number {
|
||||
return Math.max(0, attackPower - defense);
|
||||
}
|
||||
|
||||
isPlayerAlive(playerId: string): boolean {
|
||||
const player = this.findById(playerId);
|
||||
return player.isAlive && player.health > 0;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 文件:src/model/player_entity.ts
|
||||
export interface Player {
|
||||
id: string;
|
||||
name: string;
|
||||
health: number;
|
||||
speed: number;
|
||||
position: Position;
|
||||
isAlive: boolean;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export enum PlayerStatus {
|
||||
IDLE = 'idle',
|
||||
MOVING = 'moving',
|
||||
ATTACKING = 'attacking',
|
||||
DEAD = 'dead',
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// 文件:src/model/dto/create_player_dto.ts
|
||||
import { IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator';
|
||||
|
||||
export class CreatePlayerDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(3)
|
||||
@MaxLength(20)
|
||||
name: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 检查清单
|
||||
|
||||
在提交代码前,请确保:
|
||||
|
||||
- [ ] 所有文件和文件夹使用下划线分隔命名
|
||||
- [ ] 所有变量和函数使用小驼峰命名
|
||||
- [ ] 所有类、接口、DTO 使用大驼峰命名
|
||||
- [ ] 所有常量和枚举值使用全大写 + 下划线命名
|
||||
- [ ] 所有路由使用全小写 + 短横线命名
|
||||
- [ ] 函数名清晰表达其功能
|
||||
- [ ] 布尔变量使用 is/has/can 前缀
|
||||
- [ ] 避免使用无意义的缩写
|
||||
|
||||
## 工具配置
|
||||
|
||||
### ESLint 配置建议
|
||||
|
||||
```json
|
||||
{
|
||||
"rules": {
|
||||
"@typescript-eslint/naming-convention": [
|
||||
"error",
|
||||
{
|
||||
"selector": "variable",
|
||||
"format": ["camelCase", "UPPER_CASE"]
|
||||
},
|
||||
{
|
||||
"selector": "function",
|
||||
"format": ["camelCase"]
|
||||
},
|
||||
{
|
||||
"selector": "class",
|
||||
"format": ["PascalCase"]
|
||||
},
|
||||
{
|
||||
"selector": "interface",
|
||||
"format": ["PascalCase"]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 总结
|
||||
|
||||
遵循统一的命名规范能够:
|
||||
|
||||
- 提高代码可读性
|
||||
- 减少团队沟通成本
|
||||
- 降低代码维护难度
|
||||
- 避免命名冲突
|
||||
- 提升项目专业度
|
||||
|
||||
记住:**好的命名是自解释的,不需要额外的注释。**
|
||||
460
docs/development/nestjs_guide.md
Normal file
460
docs/development/nestjs_guide.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# NestJS 使用指南
|
||||
|
||||
本文档提供 NestJS 在游戏后端开发中的常用模式和最佳实践。
|
||||
|
||||
## 目录
|
||||
|
||||
- [核心概念](#核心概念)
|
||||
- [模块化开发](#模块化开发)
|
||||
- [控制器与路由](#控制器与路由)
|
||||
- [服务与依赖注入](#服务与依赖注入)
|
||||
- [WebSocket 实时通信](#websocket-实时通信)
|
||||
- [数据验证](#数据验证)
|
||||
- [异常处理](#异常处理)
|
||||
|
||||
## 核心概念
|
||||
|
||||
NestJS 采用模块化架构,主要由以下几个部分组成:
|
||||
|
||||
- **Module(模块)**:组织代码的基本单元
|
||||
- **Controller(控制器)**:处理 HTTP 请求
|
||||
- **Provider(提供者)**:包括 Service、Repository 等,处理业务逻辑
|
||||
- **Gateway(网关)**:处理 WebSocket 连接
|
||||
|
||||
## 模块化开发
|
||||
|
||||
### 创建游戏模块
|
||||
|
||||
使用 NestJS CLI 快速生成模块:
|
||||
|
||||
```bash
|
||||
nest g module game
|
||||
nest g controller game
|
||||
nest g service game
|
||||
```
|
||||
|
||||
### 模块示例
|
||||
|
||||
```typescript
|
||||
// src/game/game_module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GameController } from './game_controller';
|
||||
import { GameService } from './game_service';
|
||||
|
||||
@Module({
|
||||
controllers: [GameController],
|
||||
providers: [GameService],
|
||||
exports: [GameService], // 导出供其他模块使用
|
||||
})
|
||||
export class GameModule {}
|
||||
```
|
||||
|
||||
在根模块中导入:
|
||||
|
||||
```typescript
|
||||
// src/app_module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GameModule } from './game/game_module';
|
||||
|
||||
@Module({
|
||||
imports: [GameModule],
|
||||
})
|
||||
export class AppModule {}
|
||||
```
|
||||
|
||||
## 控制器与路由
|
||||
|
||||
控制器负责处理 HTTP 请求,定义 RESTful API。
|
||||
|
||||
### 基础控制器示例
|
||||
|
||||
```typescript
|
||||
// src/api/player_controller.ts
|
||||
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
||||
import { PlayerService } from '../service/player_service';
|
||||
import { CreatePlayerDto } from '../model/dto/create_player_dto';
|
||||
|
||||
@Controller('api/players')
|
||||
export class PlayerController {
|
||||
constructor(private readonly playerService: PlayerService) {}
|
||||
|
||||
// GET /api/players
|
||||
@Get()
|
||||
findAll() {
|
||||
return this.playerService.findAll();
|
||||
}
|
||||
|
||||
// GET /api/players/:id
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.playerService.findOne(id);
|
||||
}
|
||||
|
||||
// POST /api/players
|
||||
@Post()
|
||||
create(@Body() createPlayerDto: CreatePlayerDto) {
|
||||
return this.playerService.create(createPlayerDto);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 常用装饰器
|
||||
|
||||
- `@Get()`, `@Post()`, `@Put()`, `@Delete()` - HTTP 方法
|
||||
- `@Param()` - 路由参数
|
||||
- `@Body()` - 请求体
|
||||
- `@Query()` - 查询参数
|
||||
- `@Headers()` - 请求头
|
||||
|
||||
## 服务与依赖注入
|
||||
|
||||
服务层包含业务逻辑,通过依赖注入使用。
|
||||
|
||||
### 服务示例
|
||||
|
||||
```typescript
|
||||
// src/service/player_service.ts
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CreatePlayerDto } from '../model/dto/create_player_dto';
|
||||
import { Player } from '../model/player_entity';
|
||||
|
||||
@Injectable()
|
||||
export class PlayerService {
|
||||
private players: Player[] = [];
|
||||
|
||||
findAll(): Player[] {
|
||||
return this.players;
|
||||
}
|
||||
|
||||
findOne(id: string): Player {
|
||||
return this.players.find(player => player.id === id);
|
||||
}
|
||||
|
||||
create(createPlayerDto: CreatePlayerDto): Player {
|
||||
const player: Player = {
|
||||
id: Date.now().toString(),
|
||||
...createPlayerDto,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
this.players.push(player);
|
||||
return player;
|
||||
}
|
||||
|
||||
updatePosition(id: string, x: number, y: number): Player {
|
||||
const player = this.findOne(id);
|
||||
if (player) {
|
||||
player.x = x;
|
||||
player.y = y;
|
||||
}
|
||||
return player;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket 实时通信
|
||||
|
||||
游戏需要实时通信,使用 WebSocket Gateway。
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm add @nestjs/websockets @nestjs/platform-socket.io socket.io
|
||||
```
|
||||
|
||||
### Gateway 示例
|
||||
|
||||
```typescript
|
||||
// src/api/game_gateway.ts
|
||||
import {
|
||||
WebSocketGateway,
|
||||
WebSocketServer,
|
||||
SubscribeMessage,
|
||||
OnGatewayConnection,
|
||||
OnGatewayDisconnect,
|
||||
} from '@nestjs/websockets';
|
||||
import { Server, Socket } from 'socket.io';
|
||||
|
||||
@WebSocketGateway({
|
||||
cors: {
|
||||
origin: '*',
|
||||
},
|
||||
})
|
||||
export class GameGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
||||
@WebSocketServer()
|
||||
server: Server;
|
||||
|
||||
// 玩家连接
|
||||
handleConnection(client: Socket) {
|
||||
console.log(`Player connected: ${client.id}`);
|
||||
}
|
||||
|
||||
// 玩家断开
|
||||
handleDisconnect(client: Socket) {
|
||||
console.log(`Player disconnected: ${client.id}`);
|
||||
}
|
||||
|
||||
// 监听玩家移动事件
|
||||
@SubscribeMessage('player-move')
|
||||
handlePlayerMove(client: Socket, payload: { x: number; y: number }) {
|
||||
// 广播给所有其他玩家
|
||||
client.broadcast.emit('player-moved', {
|
||||
playerId: client.id,
|
||||
x: payload.x,
|
||||
y: payload.y,
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// 监听玩家攻击事件
|
||||
@SubscribeMessage('player-attack')
|
||||
handlePlayerAttack(client: Socket, payload: { targetId: string }) {
|
||||
// 发送给特定玩家
|
||||
this.server.to(payload.targetId).emit('attacked', {
|
||||
attackerId: client.id,
|
||||
});
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
// 服务器主动推送游戏状态
|
||||
broadcastGameState(gameState: any) {
|
||||
this.server.emit('game-state', gameState);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 在模块中注册
|
||||
|
||||
```typescript
|
||||
// src/game/game_module.ts
|
||||
import { Module } from '@nestjs/common';
|
||||
import { GameGateway } from '../api/game_gateway';
|
||||
import { GameService } from '../service/game_service';
|
||||
|
||||
@Module({
|
||||
providers: [GameGateway, GameService],
|
||||
})
|
||||
export class GameModule {}
|
||||
```
|
||||
|
||||
## 数据验证
|
||||
|
||||
使用 DTO(Data Transfer Object)和 class-validator 进行数据验证。
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm add class-validator class-transformer
|
||||
```
|
||||
|
||||
### DTO 示例
|
||||
|
||||
```typescript
|
||||
// src/model/dto/create_player_dto.ts
|
||||
import { IsString, IsNotEmpty, MinLength, MaxLength } from 'class-validator';
|
||||
|
||||
export class CreatePlayerDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MinLength(3)
|
||||
@MaxLength(20)
|
||||
name: string;
|
||||
|
||||
@IsString()
|
||||
avatar?: string;
|
||||
}
|
||||
```
|
||||
|
||||
### 启用全局验证管道
|
||||
|
||||
```typescript
|
||||
// src/main.ts
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { ValidationPipe } from '@nestjs/common';
|
||||
import { AppModule } from './app.module';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule);
|
||||
|
||||
// 启用全局验证
|
||||
app.useGlobalPipes(new ValidationPipe({
|
||||
whitelist: true, // 自动移除非白名单属性
|
||||
forbidNonWhitelisted: true, // 存在非白名单属性时抛出错误
|
||||
transform: true, // 自动转换类型
|
||||
}));
|
||||
|
||||
await app.listen(3000);
|
||||
}
|
||||
|
||||
bootstrap();
|
||||
```
|
||||
|
||||
## 异常处理
|
||||
|
||||
### 使用内置异常
|
||||
|
||||
```typescript
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
|
||||
@Injectable()
|
||||
export class PlayerService {
|
||||
findOne(id: string): Player {
|
||||
const player = this.players.find(p => p.id === id);
|
||||
if (!player) {
|
||||
throw new NotFoundException(`Player with ID ${id} not found`);
|
||||
}
|
||||
return player;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 常用异常类型
|
||||
|
||||
- `BadRequestException` - 400
|
||||
- `UnauthorizedException` - 401
|
||||
- `NotFoundException` - 404
|
||||
- `ForbiddenException` - 403
|
||||
- `InternalServerErrorException` - 500
|
||||
|
||||
### 自定义异常过滤器
|
||||
|
||||
```typescript
|
||||
// src/utils/http_exception_filter.ts
|
||||
import {
|
||||
ExceptionFilter,
|
||||
Catch,
|
||||
ArgumentsHost,
|
||||
HttpException,
|
||||
} from '@nestjs/common';
|
||||
import { Response } from 'express';
|
||||
|
||||
@Catch(HttpException)
|
||||
export class HttpExceptionFilter implements ExceptionFilter {
|
||||
catch(exception: HttpException, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const status = exception.getStatus();
|
||||
|
||||
response.status(status).json({
|
||||
statusCode: status,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: exception.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 实战案例:房间系统
|
||||
|
||||
### 数据模型
|
||||
|
||||
```typescript
|
||||
// src/model/room_entity.ts
|
||||
export interface Room {
|
||||
id: string;
|
||||
name: string;
|
||||
maxPlayers: number;
|
||||
players: string[];
|
||||
status: 'waiting' | 'playing' | 'finished';
|
||||
createdAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### 服务层
|
||||
|
||||
```typescript
|
||||
// src/service/room_service.ts
|
||||
import { Injectable, BadRequestException } from '@nestjs/common';
|
||||
import { Room } from '../model/room_entity';
|
||||
|
||||
@Injectable()
|
||||
export class RoomService {
|
||||
private rooms: Map<string, Room> = new Map();
|
||||
|
||||
createRoom(name: string, maxPlayers: number): Room {
|
||||
const room: Room = {
|
||||
id: Date.now().toString(),
|
||||
name,
|
||||
maxPlayers,
|
||||
players: [],
|
||||
status: 'waiting',
|
||||
createdAt: new Date(),
|
||||
};
|
||||
this.rooms.set(room.id, room);
|
||||
return room;
|
||||
}
|
||||
|
||||
joinRoom(roomId: string, playerId: string): Room {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (!room) {
|
||||
throw new BadRequestException('Room not found');
|
||||
}
|
||||
if (room.players.length >= room.maxPlayers) {
|
||||
throw new BadRequestException('Room is full');
|
||||
}
|
||||
if (room.status !== 'waiting') {
|
||||
throw new BadRequestException('Game already started');
|
||||
}
|
||||
room.players.push(playerId);
|
||||
return room;
|
||||
}
|
||||
|
||||
leaveRoom(roomId: string, playerId: string): void {
|
||||
const room = this.rooms.get(roomId);
|
||||
if (room) {
|
||||
room.players = room.players.filter(id => id !== playerId);
|
||||
if (room.players.length === 0) {
|
||||
this.rooms.delete(roomId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
listRooms(): Room[] {
|
||||
return Array.from(this.rooms.values());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 控制器
|
||||
|
||||
```typescript
|
||||
// src/api/room_controller.ts
|
||||
import { Controller, Get, Post, Body, Param } from '@nestjs/common';
|
||||
import { RoomService } from '../service/room_service';
|
||||
|
||||
@Controller('api/rooms')
|
||||
export class RoomController {
|
||||
constructor(private readonly roomService: RoomService) {}
|
||||
|
||||
@Get()
|
||||
listRooms() {
|
||||
return this.roomService.listRooms();
|
||||
}
|
||||
|
||||
@Post()
|
||||
createRoom(@Body() body: { name: string; maxPlayers: number }) {
|
||||
return this.roomService.createRoom(body.name, body.maxPlayers);
|
||||
}
|
||||
|
||||
@Post(':id/join')
|
||||
joinRoom(@Param('id') id: string, @Body() body: { playerId: string }) {
|
||||
return this.roomService.joinRoom(id, body.playerId);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **模块化设计**:按功能划分模块(player、room、game 等)
|
||||
2. **分层架构**:Controller → Service → Data,职责清晰
|
||||
3. **使用 DTO**:定义清晰的数据传输对象,启用验证
|
||||
4. **依赖注入**:充分利用 NestJS 的 DI 系统
|
||||
5. **异常处理**:使用内置异常类,提供友好的错误信息
|
||||
6. **配置管理**:使用 @nestjs/config 管理环境变量
|
||||
7. **日志记录**:使用内置 Logger 或集成第三方日志库
|
||||
8. **测试**:编写单元测试和 E2E 测试
|
||||
|
||||
## 更多资源
|
||||
|
||||
- [NestJS 官方文档](https://docs.nestjs.com/)
|
||||
- [NestJS 中文文档](https://docs.nestjs.cn/)
|
||||
- [Socket.IO 文档](https://socket.io/docs/)
|
||||
Reference in New Issue
Block a user