diff --git a/README.md b/README.md index 1412666..36a44e4 100644 --- a/README.md +++ b/README.md @@ -4,18 +4,108 @@ ## 技术栈 -- **NestJS** - 渐进式 Node.js 框架 -- **TypeScript** - 类型安全 -- **WebSocket** - 实时通信支持 +- **NestJS** `^10.0.0` - 渐进式 Node.js 框架 +- **TypeScript** `^5.3.0` - 类型安全 +- **Socket.IO** - WebSocket 实时通信支持 +- **RxJS** `^7.8.1` - 响应式编程库 + +### 核心依赖 + +**生产环境:** +- `@nestjs/common` `^10.0.0` - NestJS 核心功能 +- `@nestjs/core` `^10.0.0` - NestJS 核心模块 +- `@nestjs/platform-express` `^10.0.0` - Express 平台适配器 +- `@nestjs/websockets` `^10.0.0` - WebSocket 支持 +- `@nestjs/platform-socket.io` `^10.0.0` - Socket.IO 适配器 +- `reflect-metadata` `^0.1.13` - 装饰器元数据支持 +- `rxjs` `^7.8.1` - 响应式编程 + +**开发环境:** +- `@nestjs/cli` `^10.0.0` - NestJS 命令行工具 +- `@nestjs/schematics` `^10.0.0` - NestJS 代码生成器 +- `@types/node` `^20.0.0` - Node.js 类型定义 +- `ts-node` `^10.9.0` - TypeScript 运行时 +- `typescript` `^5.3.0` - TypeScript 编译器 + +## 开发规范 + +### 命名规范 + +项目采用统一的命名规范,确保代码风格一致: + +- **文件/文件夹**:下划线分隔(如 `order_controller.ts`) +- **变量/函数**:小驼峰命名(如 `userName`、`async queryUserInfo()`) +- **类/构造函数**:大驼峰命名(如 `UserModel`、`OrderService`) +- **常量**:全大写 + 下划线(如 `PORT`、`DB_HOST`) +- **接口路由**:全小写 + 短横线(如 `/user/get-info`、`/order/create-order`) + +详细规范请查看:[命名规范文档](./docs/naming_convention.md) + +### Git 提交规范 + +项目采用约定式提交规范,提交信息格式:`<类型>:<简短描述>` + +**常用提交类型:** + +- `feat` - 新增功能 +- `fix` - 修复 Bug +- `docs` - 文档更新 +- `style` - 代码格式调整 +- `refactor` - 代码重构 +- `perf` - 性能优化 +- `test` - 测试相关 +- `chore` - 构建/工具变动 + +**后端特定类型:** + +- `api` - API 接口 +- `db` - 数据库 +- `websocket` - WebSocket +- `auth` - 认证授权 +- `dto` - 数据传输对象 +- `service` - 服务层 + +**核心原则:** + +- ⭐ 一次提交只做一件事 +- 使用中文冒号 `:` +- 简短明确(不超过 50 字符) +- 能拆分就拆分,保持提交历史清晰 + +**示例:** + +```bash +git commit -m "feat:实现玩家注册和登录功能" +git commit -m "fix:修复房间加入时的并发问题" +git commit -m "api:添加玩家信息查询接口" +``` + +详细规范请查看:[Git 提交规范文档](./docs/git_commit_guide.md) + +## 文档 + +- [NestJS 使用指南](./docs/nestjs_guide.md) - 详细的 NestJS 开发指南,包含实战案例 +- [命名规范](./docs/naming_convention.md) - 项目命名规范和最佳实践 +- [Git 提交规范](./docs/git_commit_guide.md) - Git 提交信息格式和最佳实践 ## 前置要求 +- **Node.js** >= 18.0.0 +- **Yarn** >= 1.22.0(推荐)或 npm >= 9.0.0 + 如果还没有安装 Yarn,请先安装: ```bash npm install -g yarn ``` +检查版本: + +```bash +node --version +yarn --version +``` + ## 安装依赖 ```bash diff --git a/docs/.gitkeep b/docs/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/git_commit_guide.md b/docs/git_commit_guide.md new file mode 100644 index 0000000..484726a --- /dev/null +++ b/docs/git_commit_guide.md @@ -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 混在一起 + +**好的提交习惯 = 清晰的项目历史 = 高效的团队协作** diff --git a/docs/naming_convention.md b/docs/naming_convention.md new file mode 100644 index 0000000..c46c068 --- /dev/null +++ b/docs/naming_convention.md @@ -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(id: string): T { } +class Repository { } +interface Response { } + +// 使用有意义的名称 +function mapArray(arr: TInput[]): TOutput[] { } + +❌ 错误示例: +function findById(id: string): t { } +class Repository { } +``` + +### 装饰器参数 + +```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 = 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"] + } + ] + } +} +``` + +## 总结 + +遵循统一的命名规范能够: + +- 提高代码可读性 +- 减少团队沟通成本 +- 降低代码维护难度 +- 避免命名冲突 +- 提升项目专业度 + +记住:**好的命名是自解释的,不需要额外的注释。** diff --git a/docs/nestjs_guide.md b/docs/nestjs_guide.md new file mode 100644 index 0000000..6de406d --- /dev/null +++ b/docs/nestjs_guide.md @@ -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 +yarn 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 +yarn 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(); + 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 = 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/)