forked from datawhale/whale-town-end
merge upstream
This commit is contained in:
57
.env.example
Normal file
57
.env.example
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
# 环境配置模板
|
||||||
|
# 复制此文件为 .env 并根据需要修改配置
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 测试模式配置(开发/测试环境推荐)
|
||||||
|
# ===========================================
|
||||||
|
# 使用以下配置可以在没有数据库和邮件服务器的情况下进行测试
|
||||||
|
# 1. 复制此文件为 .env
|
||||||
|
# 2. 保持数据库和邮件配置为注释状态
|
||||||
|
# 3. 运行 npm run dev 启动服务
|
||||||
|
# 4. 运行测试脚本:./test-api.ps1 (Windows) 或 ./test-api.sh (Linux/macOS)
|
||||||
|
|
||||||
|
# 应用配置
|
||||||
|
NODE_ENV=development
|
||||||
|
PORT=3000
|
||||||
|
LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# JWT 配置
|
||||||
|
JWT_SECRET=test_jwt_secret_key_for_development_only_32chars
|
||||||
|
JWT_EXPIRES_IN=7d
|
||||||
|
|
||||||
|
# Redis 配置(测试模式:使用文件存储)
|
||||||
|
USE_FILE_REDIS=true
|
||||||
|
REDIS_HOST=localhost
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_PASSWORD=
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
# ===========================================
|
||||||
|
# 生产环境配置(取消注释并填入真实数据)
|
||||||
|
# ===========================================
|
||||||
|
|
||||||
|
# 数据库配置(生产环境取消注释)
|
||||||
|
# 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)
|
||||||
|
# USE_FILE_REDIS=false
|
||||||
|
# REDIS_HOST=your_redis_host
|
||||||
|
# REDIS_PORT=6379
|
||||||
|
# REDIS_PASSWORD=your_redis_password
|
||||||
|
# REDIS_DB=0
|
||||||
|
|
||||||
|
# 邮件服务配置(生产环境取消注释)
|
||||||
|
# EMAIL_HOST=smtp.gmail.com
|
||||||
|
# EMAIL_PORT=587
|
||||||
|
# EMAIL_SECURE=false
|
||||||
|
# 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
|
||||||
98
CONTRIBUTORS.md
Normal file
98
CONTRIBUTORS.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# 贡献者名单
|
||||||
|
|
||||||
|
感谢所有为 Whale Town 项目做出贡献的开发者们!🎉
|
||||||
|
|
||||||
|
## 核心贡献者
|
||||||
|
|
||||||
|
### 🏆 主要维护者
|
||||||
|
|
||||||
|
**moyin** - 主要维护者
|
||||||
|
- Gitea: [@moyin](https://gitea.xinghangee.icu/moyin)
|
||||||
|
- Email: xinghang_a@proton.me
|
||||||
|
- 提交数: **66 commits**
|
||||||
|
- 主要贡献:
|
||||||
|
- 🚀 项目架构设计与初始化
|
||||||
|
- 🔐 完整用户认证系统实现
|
||||||
|
- 📧 邮箱验证系统设计与开发
|
||||||
|
- 🗄️ Redis缓存服务(文件存储+真实Redis双模式)
|
||||||
|
- 📝 完整的API文档系统(Swagger UI + OpenAPI)
|
||||||
|
- 🧪 测试框架搭建与114个测试用例编写
|
||||||
|
- 📊 高性能日志系统集成(Pino)
|
||||||
|
- 🔧 项目配置优化与部署方案
|
||||||
|
- 🐛 验证码TTL重置关键问题修复
|
||||||
|
- 📚 完整的项目文档体系建设
|
||||||
|
|
||||||
|
### 🌟 核心开发者
|
||||||
|
|
||||||
|
**angjustinl** - 核心开发者
|
||||||
|
- Gitea: [@ANGJustinl](https://gitea.xinghangee.icu/ANGJustinl)
|
||||||
|
- GitHub: [@ANGJustinl](https://github.com/ANGJustinl)
|
||||||
|
- Email: 96008766+ANGJustinl@users.noreply.github.com
|
||||||
|
- 提交数: **2 commits**
|
||||||
|
- 主要贡献:
|
||||||
|
- 🔄 邮箱验证流程重构与优化
|
||||||
|
- 💾 基于内存的用户服务实现
|
||||||
|
- 🛠️ API响应处理改进
|
||||||
|
- 🧪 测试用例完善与错误修复
|
||||||
|
- 📚 系统架构优化
|
||||||
|
|
||||||
|
**jianuo** - 核心开发者
|
||||||
|
- Gitea: [@jianuo](https://gitea.xinghangee.icu/jianuo)
|
||||||
|
- Email: 32106500027@e.gzhu.edu.cn
|
||||||
|
- 提交数: **3 commits**
|
||||||
|
- 主要贡献:
|
||||||
|
- 🐳 Docker部署问题修复
|
||||||
|
- 📖 项目文档错误修复
|
||||||
|
- 🔧 部署配置优化
|
||||||
|
|
||||||
|
## 贡献统计
|
||||||
|
|
||||||
|
| 贡献者 | 提交数 | 主要领域 | 贡献占比 |
|
||||||
|
|--------|--------|----------|----------|
|
||||||
|
| moyin | 66 | 架构设计、核心功能、文档、测试 | 93% |
|
||||||
|
| jianuo | 3 | 部署、文档 | 4% |
|
||||||
|
| angjustinl | 2 | 功能优化、测试、重构 | 3% |
|
||||||
|
|
||||||
|
## 项目里程碑
|
||||||
|
|
||||||
|
### 2025年12月
|
||||||
|
- **12月17日**: 项目初始化,完成基础架构搭建
|
||||||
|
- **12月17日**: 实现完整的用户认证系统
|
||||||
|
- **12月17日**: 完成API文档系统集成
|
||||||
|
- **12月17日**: 实现邮箱验证系统
|
||||||
|
- **12月17日**: 修复验证码TTL重置关键问题
|
||||||
|
- **12月18日**: angjustinl重构邮箱验证流程,引入内存用户服务
|
||||||
|
- **12月18日**: jianuo修复Docker部署问题
|
||||||
|
- **12月18日**: 完成测试用例修复和优化
|
||||||
|
|
||||||
|
## 如何成为贡献者
|
||||||
|
|
||||||
|
我们欢迎所有形式的贡献!无论是:
|
||||||
|
|
||||||
|
- 🐛 **Bug修复** - 发现并修复问题
|
||||||
|
- ✨ **新功能** - 添加有价值的功能
|
||||||
|
- 📚 **文档改进** - 完善项目文档
|
||||||
|
- 🧪 **测试用例** - 提高代码覆盖率
|
||||||
|
- 🎨 **代码优化** - 改进代码质量
|
||||||
|
- 💡 **建议反馈** - 提出改进建议
|
||||||
|
|
||||||
|
### 贡献流程
|
||||||
|
|
||||||
|
1. Fork 项目到你的Gitea账户
|
||||||
|
2. 创建功能分支:`git checkout -b feature/your-feature`
|
||||||
|
3. 提交你的更改:`git commit -m "feat:添加新功能"`
|
||||||
|
4. 推送到分支:`git push origin feature/your-feature`
|
||||||
|
5. 创建Pull Request
|
||||||
|
|
||||||
|
### 贡献规范
|
||||||
|
|
||||||
|
请在贡献前阅读:
|
||||||
|
- [AI辅助开发规范指南](./docs/AI辅助开发规范指南.md)
|
||||||
|
- [后端开发规范](./docs/backend_development_guide.md)
|
||||||
|
- [Git提交规范](./docs/git_commit_guide.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**再次感谢所有贡献者的辛勤付出!** 🙏
|
||||||
|
|
||||||
|
*如果你的名字没有出现在列表中,请联系我们或提交PR更新此文件。*
|
||||||
@@ -40,9 +40,8 @@ sudo mkdir -p /var/www
|
|||||||
cd /var/www
|
cd /var/www
|
||||||
|
|
||||||
# 克隆项目(替换为你的实际仓库地址)
|
# 克隆项目(替换为你的实际仓库地址)
|
||||||
sudo git clone https://your-gitea-server.com/username/pixel-game-server.git
|
git clone https://gitea.xinghangee.icu/datawhale/whale-town-end.git
|
||||||
sudo chown -R $USER:$USER pixel-game-server
|
cd whale-town-end
|
||||||
cd pixel-game-server
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### 3. 配置环境
|
### 3. 配置环境
|
||||||
@@ -111,7 +110,7 @@ pm2 startup
|
|||||||
创建 Nginx 配置文件:
|
创建 Nginx 配置文件:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo nano /etc/nginx/sites-available/pixel-game-server
|
sudo nano /etc/nginx/sites-available/whale-town-end
|
||||||
```
|
```
|
||||||
|
|
||||||
添加以下内容:
|
添加以下内容:
|
||||||
@@ -147,7 +146,7 @@ server {
|
|||||||
启用站点:
|
启用站点:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
sudo ln -s /etc/nginx/sites-available/pixel-game-server /etc/nginx/sites-enabled/
|
sudo ln -s /etc/nginx/sites-available/whale-town-end /etc/nginx/sites-enabled/
|
||||||
sudo nginx -t
|
sudo nginx -t
|
||||||
sudo systemctl reload nginx
|
sudo systemctl reload nginx
|
||||||
```
|
```
|
||||||
@@ -171,7 +170,7 @@ sudo systemctl reload nginx
|
|||||||
pm2 status
|
pm2 status
|
||||||
|
|
||||||
# 查看日志
|
# 查看日志
|
||||||
pm2 logs pixel-game-server
|
pm2 logs whale-town-end
|
||||||
pm2 logs webhook-handler
|
pm2 logs webhook-handler
|
||||||
|
|
||||||
# 测试 API
|
# 测试 API
|
||||||
@@ -183,10 +182,10 @@ curl http://localhost:3000/api-docs
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 重启服务
|
# 重启服务
|
||||||
pm2 restart pixel-game-server
|
pm2 restart whale-town-end
|
||||||
|
|
||||||
# 查看日志
|
# 查看日志
|
||||||
pm2 logs pixel-game-server --lines 100
|
pm2 logs whale-town-end --lines 100
|
||||||
|
|
||||||
# 手动部署
|
# 手动部署
|
||||||
bash deploy.sh
|
bash deploy.sh
|
||||||
@@ -195,7 +194,7 @@ bash deploy.sh
|
|||||||
git pull origin main
|
git pull origin main
|
||||||
pnpm install
|
pnpm install
|
||||||
pnpm run build
|
pnpm run build
|
||||||
pm2 reload pixel-game-server
|
pm2 reload whale-town-end
|
||||||
```
|
```
|
||||||
|
|
||||||
## 故障排除
|
## 故障排除
|
||||||
|
|||||||
17
Dockerfile
17
Dockerfile
@@ -1,17 +1,22 @@
|
|||||||
# 使用官方 Node.js 镜像
|
# 使用官方 Node.js 镜像
|
||||||
FROM node:18-alpine
|
FROM node:lts-alpine
|
||||||
|
|
||||||
# 设置工作目录
|
# 设置工作目录
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 安装 pnpm
|
# 设置构建参数
|
||||||
RUN npm install -g pnpm
|
ARG NPM_REGISTRY=https://registry.npmmirror.com
|
||||||
|
|
||||||
# 复制 package.json 和 pnpm-lock.yaml
|
# 设置 npm 和 pnpm 镜像源
|
||||||
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
|
RUN npm config set registry ${NPM_REGISTRY} && \
|
||||||
|
npm install -g pnpm && \
|
||||||
|
pnpm config set registry ${NPM_REGISTRY}
|
||||||
|
|
||||||
|
# 复制 package.json
|
||||||
|
COPY package.json pnpm-workspace.yaml ./
|
||||||
|
|
||||||
# 安装依赖
|
# 安装依赖
|
||||||
RUN pnpm install --frozen-lockfile
|
RUN pnpm install
|
||||||
|
|
||||||
# 复制源代码
|
# 复制源代码
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|||||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Whale Town Team
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
633
README.md
633
README.md
@@ -1,341 +1,394 @@
|
|||||||
# Pixel Game Server
|
# 🐋 Whale Town - 像素游戏后端服务
|
||||||
|
|
||||||
一个基于 NestJS 的 2D 像素风游戏后端服务
|
> 一个基于 NestJS 的现代化 2D 像素风游戏后端服务,支持实时通信、用户认证、邮箱验证等完整功能。
|
||||||
|
|
||||||
|
[](https://nodejs.org/)
|
||||||
|
[](https://nestjs.com/)
|
||||||
|
[](https://www.typescriptlang.org/)
|
||||||
|
[](./LICENSE)
|
||||||
|
|
||||||
|
## 🎯 项目简介
|
||||||
|
|
||||||
|
Whale Town 是一个功能完整的像素游戏后端服务,提供:
|
||||||
|
|
||||||
|
- 🔐 **完整用户认证系统** - 支持邮箱验证、密码重置、第三方登录
|
||||||
|
- 📧 **智能邮件服务** - 支持测试模式和生产模式自动切换
|
||||||
|
- 🗄️ **灵活存储方案** - Redis文件存储 + 内存数据库,支持无依赖测试
|
||||||
|
- 🚀 **高性能架构** - 基于NestJS,支持WebSocket实时通信
|
||||||
|
- 📚 **完整API文档** - Swagger UI + OpenAPI规范
|
||||||
|
- 🧪 **全面测试覆盖** - 单元测试 + API功能测试
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 🚨 开发者必读警告
|
## 🚀 快速开始
|
||||||
|
|
||||||
**⚠️ 在开始任何开发工作之前,请务必阅读:[AI 辅助开发规范指南](./docs/AI辅助开发规范指南.md)**
|
### 📋 环境要求
|
||||||
|
|
||||||
**📢 重要提醒:**
|
- **Node.js** >= 18.0.0 (推荐 24.7.0)
|
||||||
- 🚫 **未阅读 AI 辅助指南的代码将无法通过审查**
|
- **pnpm** >= 8.0.0 (推荐 10.25.0)
|
||||||
- 🤖 **学会使用 AI 助手可以让你的开发效率提升 300%**
|
|
||||||
- 📝 **AI 可以帮你自动生成符合规范的代码和注释**
|
|
||||||
- 🔍 **AI 可以实时检查你的代码质量**
|
|
||||||
|
|
||||||
**👉 [立即阅读 AI 辅助开发指南](./docs/AI辅助开发规范指南.md)**
|
### 🛠️ 安装与运行
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
- **NestJS** `^10.4.20` - 渐进式 Node.js 框架
|
|
||||||
- **TypeScript** `^5.9.3` - 类型安全
|
|
||||||
- **Socket.IO** - WebSocket 实时通信支持
|
|
||||||
- **RxJS** `^7.8.2` - 响应式编程库
|
|
||||||
- **Pino** `^10.1.0` - 高性能日志库
|
|
||||||
- **Jest** `^29.7.0` - 测试框架
|
|
||||||
|
|
||||||
### 核心依赖
|
|
||||||
|
|
||||||
**生产环境:**
|
|
||||||
- `@nestjs/common` `^10.4.20` - NestJS 核心功能
|
|
||||||
- `@nestjs/core` `^10.4.20` - NestJS 核心模块
|
|
||||||
- `@nestjs/config` `^4.0.2` - 配置管理
|
|
||||||
- `@nestjs/platform-express` `^10.4.20` - Express 平台适配器
|
|
||||||
- `@nestjs/websockets` `^10.4.20` - WebSocket 支持
|
|
||||||
- `@nestjs/platform-socket.io` `^10.4.20` - Socket.IO 适配器
|
|
||||||
- `nestjs-pino` `^4.5.0` - Pino 日志集成
|
|
||||||
- `pino` `^10.1.0` - 高性能日志库
|
|
||||||
- `reflect-metadata` `^0.1.14` - 装饰器元数据支持
|
|
||||||
- `rxjs` `^7.8.2` - 响应式编程
|
|
||||||
|
|
||||||
**开发环境:**
|
|
||||||
- `@nestjs/cli` `^10.4.9` - NestJS 命令行工具
|
|
||||||
- `@nestjs/schematics` `^10.2.3` - NestJS 代码生成器
|
|
||||||
- `@nestjs/testing` `^10.4.20` - 测试工具
|
|
||||||
- `@types/jest` `^29.5.14` - Jest 类型定义
|
|
||||||
- `@types/node` `^20.19.26` - Node.js 类型定义
|
|
||||||
- `jest` `^29.7.0` - 测试框架
|
|
||||||
- `ts-jest` `^29.2.5` - TypeScript Jest 支持
|
|
||||||
- `ts-node` `^10.9.2` - TypeScript 运行时
|
|
||||||
- `typescript` `^5.9.3` - TypeScript 编译器
|
|
||||||
- `pino-pretty` `^13.1.3` - Pino 美化输出
|
|
||||||
|
|
||||||
## 🚨 重要:开发前必读
|
|
||||||
|
|
||||||
### ⚠️ 所有开发者必须先阅读 AI 辅助开发指南
|
|
||||||
|
|
||||||
**在开始任何开发工作之前,请务必阅读:[AI 辅助开发规范指南](./docs/AI辅助开发规范指南.md)**
|
|
||||||
|
|
||||||
这个指南将教你如何:
|
|
||||||
- 🤖 使用 AI 助手遵循项目规范
|
|
||||||
- 📝 自动生成规范的代码和注释
|
|
||||||
- 🔍 实时检查代码质量
|
|
||||||
- 🚀 显著提高开发效率和代码质量
|
|
||||||
|
|
||||||
**不阅读此指南直接开发,代码审查将无法通过!**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 开发规范
|
|
||||||
|
|
||||||
### 命名规范
|
|
||||||
|
|
||||||
项目采用统一的命名规范,确保代码风格一致:
|
|
||||||
|
|
||||||
- **文件/文件夹**:下划线分隔(如 `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
|
```bash
|
||||||
git commit -m "feat:实现玩家注册和登录功能"
|
# 1. 克隆项目
|
||||||
git commit -m "fix:修复房间加入时的并发问题"
|
git clone <repository-url>
|
||||||
git commit -m "api:添加玩家信息查询接口"
|
cd whale-town-end
|
||||||
```
|
|
||||||
|
|
||||||
详细规范请查看:[Git 提交规范文档](./docs/git_commit_guide.md)
|
# 2. 安装依赖
|
||||||
|
|
||||||
### 后端开发规范
|
|
||||||
|
|
||||||
项目要求严格的代码质量和可维护性标准:
|
|
||||||
|
|
||||||
**核心要求:**
|
|
||||||
|
|
||||||
- **完整注释**:每个模块、类、方法都必须有详细注释
|
|
||||||
- **全面业务逻辑**:考虑所有可能情况,包括异常和边界条件
|
|
||||||
- **关键日志记录**:重要操作必须记录日志,便于问题排查
|
|
||||||
- **防御性编程**:对所有输入进行验证,实现健壮的错误处理
|
|
||||||
|
|
||||||
**注释要求:**
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
/**
|
|
||||||
* 玩家服务类
|
|
||||||
*
|
|
||||||
* 职责:处理玩家相关的业务逻辑
|
|
||||||
* 主要方法:createPlayer(), updatePlayerInfo(), getPlayerById()
|
|
||||||
* 使用场景:玩家注册登录流程、个人陈列室数据管理
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class PlayerService {
|
|
||||||
/**
|
|
||||||
* 创建新玩家
|
|
||||||
* @param email 玩家邮箱地址
|
|
||||||
* @param nickname 玩家昵称
|
|
||||||
* @returns Promise<Player> 创建成功的玩家对象
|
|
||||||
* @throws BadRequestException 当邮箱格式错误时
|
|
||||||
*/
|
|
||||||
async createPlayer(email: string, nickname: string): Promise<Player> {
|
|
||||||
// 详细的业务逻辑实现...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
详细规范请查看:[后端开发规范指南](./docs/backend_development_guide.md)
|
|
||||||
|
|
||||||
## 📚 开发文档
|
|
||||||
|
|
||||||
### 🔥 必读文档
|
|
||||||
- **[AI 辅助开发规范指南](./docs/AI辅助开发规范指南.md)** - 🚨 **所有开发者必读!** 教你如何使用 AI 遵循项目规范
|
|
||||||
|
|
||||||
### 📋 规范文档
|
|
||||||
- [后端开发规范](./docs/backend_development_guide.md) - 注释标准、业务逻辑设计和日志记录要求
|
|
||||||
- [Git 提交规范](./docs/git_commit_guide.md) - Git 提交信息格式和最佳实践
|
|
||||||
- [命名规范](./docs/naming_convention.md) - 项目命名规范和最佳实践
|
|
||||||
- [NestJS 使用指南](./docs/nestjs_guide.md) - 详细的 NestJS 开发指南,包含实战案例
|
|
||||||
|
|
||||||
### 📖 API 文档
|
|
||||||
- **[API 文档总览](./docs/api/README.md)** - API 文档使用指南和快速开始
|
|
||||||
- **[Swagger UI](http://localhost:3000/api-docs)** - 交互式 API 文档(需启动服务器)
|
|
||||||
- [详细接口文档](./docs/api/api-documentation.md) - 完整的 API 接口说明
|
|
||||||
- [OpenAPI 规范](./docs/api/openapi.yaml) - 标准化的 API 描述文件
|
|
||||||
- [Postman 集合](./docs/api/postman-collection.json) - 可导入的 API 测试集合
|
|
||||||
|
|
||||||
### 💡 使用建议
|
|
||||||
1. **开发前**:先读 AI 辅助指南,了解如何用 AI 帮助遵循规范
|
|
||||||
2. **开发中**:参考具体规范文档,使用 AI 实时检查代码质量
|
|
||||||
3. **API 开发**:使用 Swagger UI 进行接口测试,参考 API 文档进行开发
|
|
||||||
4. **提交前**:用 AI 检查代码和提交信息是否符合规范
|
|
||||||
|
|
||||||
## 前置要求
|
|
||||||
|
|
||||||
- **Node.js** >= 18.0.0 默认:24.7.0
|
|
||||||
- **pnpm** >= 8.0.0(推荐)默认:10.25.0
|
|
||||||
|
|
||||||
如果还没有安装 pnpm,请先安装:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install -g pnpm
|
|
||||||
```
|
|
||||||
|
|
||||||
检查版本:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
node --version
|
|
||||||
pnpm --version
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安装依赖
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm install
|
pnpm install
|
||||||
|
|
||||||
|
# 3. 配置环境(测试模式,无需数据库和邮件服务器)
|
||||||
|
cp .env.example .env
|
||||||
|
|
||||||
|
# 4. 启动开发服务器
|
||||||
|
pnpm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发
|
🎉 **服务启动成功!** 访问 http://localhost:3000
|
||||||
|
|
||||||
启动开发服务器(支持热重载):
|
### 🧪 快速测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm dev
|
# Windows
|
||||||
|
.\test-api.ps1
|
||||||
|
|
||||||
|
# Linux/macOS
|
||||||
|
./test-api.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
服务器将运行在 `http://localhost:3000`
|
**测试内容:**
|
||||||
|
- ✅ 邮箱验证码发送与验证
|
||||||
|
- ✅ 用户注册与登录
|
||||||
|
- ✅ Redis文件存储功能
|
||||||
|
- ✅ 邮件测试模式
|
||||||
|
|
||||||
## 测试
|
---
|
||||||
|
|
||||||
运行测试:
|
## 🎓 新开发者指南
|
||||||
|
|
||||||
```bash
|
### 第一步:了解项目规范 📚
|
||||||
pnpm test
|
|
||||||
```
|
|
||||||
|
|
||||||
运行测试并监听文件变化:
|
**⚠️ 重要:在开始开发前,请务必阅读以下文档**
|
||||||
|
|
||||||
```bash
|
1. **[AI辅助开发规范指南](./docs/AI辅助开发规范指南.md)** 🤖
|
||||||
pnpm test:watch
|
- 学会使用AI助手提升开发效率300%
|
||||||
```
|
- 自动生成符合规范的代码和注释
|
||||||
|
- 实时检查代码质量
|
||||||
|
|
||||||
运行测试并生成覆盖率报告:
|
2. **[后端开发规范](./docs/backend_development_guide.md)** 📝
|
||||||
|
- 代码注释标准
|
||||||
|
- 业务逻辑设计原则
|
||||||
|
- 日志记录要求
|
||||||
|
|
||||||
```bash
|
3. **[Git提交规范](./docs/git_commit_guide.md)** 🔄
|
||||||
pnpm test:cov
|
- 提交信息格式
|
||||||
```
|
- 分支管理策略
|
||||||
|
|
||||||
## 构建
|
### 第二步:熟悉项目架构 🏗️
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm build
|
|
||||||
```
|
|
||||||
|
|
||||||
## 生产环境运行
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pnpm start:prod
|
|
||||||
```
|
|
||||||
|
|
||||||
## 项目结构
|
|
||||||
|
|
||||||
```
|
```
|
||||||
src/
|
项目根目录/
|
||||||
├── api/ # API 接口层(控制器、网关)
|
├── src/ # 源代码目录
|
||||||
├── business/ # 业务逻辑层
|
│ ├── api/ # API接口层(预留,用于游戏相关控制器)
|
||||||
├── core/ # 核心功能模块
|
│ ├── business/ # 业务逻辑层
|
||||||
│ ├── db/ # 数据库相关
|
│ │ └── login/ # 登录业务模块
|
||||||
│ └── utils/ # 工具函数
|
│ ├── core/ # 核心功能模块
|
||||||
│ └── logger/ # 日志系统
|
│ │ ├── db/ # 数据库层
|
||||||
├── main.ts # 应用入口
|
│ │ │ └── users/ # 用户数据模型(支持MySQL/内存双模式)
|
||||||
├── app.module.ts # 根模块
|
│ │ ├── redis/ # Redis缓存服务(支持真实Redis/文件存储)
|
||||||
├── app.controller.ts # 根控制器
|
│ │ ├── login_core/ # 登录核心服务
|
||||||
└── app.service.ts # 根服务
|
│ │ └── utils/ # 工具服务
|
||||||
test/
|
│ │ ├── email/ # 邮件服务(支持SMTP/测试模式)
|
||||||
├── api/ # API 测试
|
│ │ ├── verification/ # 验证码服务
|
||||||
└── service/ # 服务测试
|
│ │ └── logger/ # 日志系统
|
||||||
docs/ # 项目文档
|
│ ├── dto/ # 数据传输对象
|
||||||
├── api/ # API 接口文档
|
│ ├── types/ # TypeScript类型定义
|
||||||
│ ├── README.md # API 文档使用指南
|
│ ├── app.module.ts # 应用主模块
|
||||||
│ ├── api-documentation.md # 详细接口文档
|
│ └── main.ts # 应用入口
|
||||||
│ ├── openapi.yaml # OpenAPI 规范文件
|
├── docs/ # 项目文档
|
||||||
│ └── postman-collection.json # Postman 测试集合
|
│ ├── api/ # API文档
|
||||||
├── systems/ # 系统设计文档
|
│ └── systems/ # 系统设计文档
|
||||||
│ ├── logger/ # 日志系统文档
|
├── test/ # 测试文件
|
||||||
│ └── user-auth/ # 用户认证系统文档
|
├── redis-data/ # Redis文件存储数据
|
||||||
├── backend_development_guide.md # 后端开发规范
|
├── logs/ # 日志文件
|
||||||
├── git_commit_guide.md # Git 提交规范
|
└── 配置文件 # .env, package.json, tsconfig.json等
|
||||||
├── naming_convention.md # 命名规范
|
|
||||||
├── nestjs_guide.md # NestJS 使用指南
|
|
||||||
└── AI辅助开发规范指南.md # AI 辅助开发指南
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 核心功能
|
**架构特点:**
|
||||||
|
- 🏗️ **分层架构** - API层 → 业务层 → 核心层 → 数据层
|
||||||
|
- 🔄 **双模式支持** - 开发测试模式 + 生产部署模式
|
||||||
|
- 📦 **模块化设计** - 每个功能独立模块,便于维护扩展
|
||||||
|
- 🧪 **测试友好** - 完整的单元测试和集成测试覆盖
|
||||||
|
|
||||||
|
### 第三步:体验核心功能 🎮
|
||||||
|
|
||||||
|
1. **API文档系统** 📖
|
||||||
|
```bash
|
||||||
|
# 启动服务后访问
|
||||||
|
http://localhost:3000/api-docs
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **用户认证系统** 🔐
|
||||||
|
- 邮箱验证码注册
|
||||||
|
- 多方式登录(用户名/邮箱/手机号)
|
||||||
|
- 密码重置功能
|
||||||
|
|
||||||
|
3. **实时通信** 🌐
|
||||||
|
- WebSocket支持
|
||||||
|
- Socket.IO集成
|
||||||
|
|
||||||
|
### 第四步:开始贡献 🤝
|
||||||
|
|
||||||
|
1. **Fork项目** 到你的Gitea账户
|
||||||
|
2. **创建功能分支**:`git checkout -b feature/your-feature`
|
||||||
|
3. **遵循规范开发**(使用AI助手帮助)
|
||||||
|
4. **提交代码**:`git commit -m "feat:添加新功能"`
|
||||||
|
5. **创建Pull Request**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠️ 技术栈
|
||||||
|
|
||||||
|
### 🚀 核心框架
|
||||||
|
- **NestJS** `^11.1.9` - 企业级Node.js框架,提供依赖注入、模块化等特性
|
||||||
|
- **TypeScript** `^5.9.3` - 类型安全的JavaScript超集
|
||||||
|
- **Express** `^10.4.20` - 基于Express的HTTP服务器
|
||||||
|
- **RxJS** `^7.8.2` - 响应式编程库,处理异步数据流
|
||||||
|
|
||||||
|
### 🌐 实时通信
|
||||||
|
- **Socket.IO** `^10.4.20` - WebSocket实时双向通信
|
||||||
|
- **@nestjs/websockets** - NestJS WebSocket网关支持
|
||||||
|
- **@nestjs/platform-socket.io** - Socket.IO平台适配器
|
||||||
|
|
||||||
|
### 🗄️ 数据存储
|
||||||
|
- **TypeORM** `^0.3.28` - 强大的ORM框架,支持多种数据库
|
||||||
|
- **MySQL2** `^3.16.0` - 高性能MySQL驱动
|
||||||
|
- **IORedis** `^5.8.2` - Redis客户端,支持集群和哨兵模式
|
||||||
|
- **文件存储** - 自研Redis文件存储适配器,支持无Redis开发
|
||||||
|
|
||||||
|
### 🔐 安全认证
|
||||||
|
- **bcrypt** `^6.0.0` - 密码加密哈希算法
|
||||||
|
- **class-validator** `^0.14.3` - 数据验证装饰器
|
||||||
|
- **class-transformer** `^0.5.1` - 对象转换和序列化
|
||||||
|
|
||||||
|
### 📧 通信服务
|
||||||
|
- **Nodemailer** `^6.10.1` - 邮件发送服务
|
||||||
|
- **Axios** `^1.13.2` - HTTP客户端,支持第三方API调用
|
||||||
|
|
||||||
|
### 📚 API文档
|
||||||
|
- **Swagger UI** `^5.0.1` - 交互式API文档界面
|
||||||
|
- **@nestjs/swagger** `^11.2.3` - NestJS Swagger集成
|
||||||
|
|
||||||
|
### 📊 日志监控
|
||||||
|
- **Pino** `^10.1.0` - 高性能结构化日志库
|
||||||
|
- **nestjs-pino** `^4.5.0` - NestJS Pino集成
|
||||||
|
- **pino-pretty** `^13.1.3` - Pino日志美化输出
|
||||||
|
|
||||||
|
### 🧪 测试框架
|
||||||
|
- **Jest** `^29.7.0` - JavaScript测试框架
|
||||||
|
- **Supertest** `^7.1.4` - HTTP断言测试
|
||||||
|
- **@nestjs/testing** `^10.4.20` - NestJS测试工具
|
||||||
|
|
||||||
|
### ⚙️ 开发工具
|
||||||
|
- **@nestjs/cli** `^10.4.9` - NestJS命令行工具
|
||||||
|
- **ts-jest** `^29.2.5` - TypeScript Jest支持
|
||||||
|
- **ts-node** `^10.9.2` - TypeScript运行时
|
||||||
|
- **pnpm** - 快速、节省磁盘空间的包管理器
|
||||||
|
|
||||||
|
### 🔄 任务调度
|
||||||
|
- **@nestjs/schedule** `^4.1.2` - 定时任务和计划任务支持
|
||||||
|
|
||||||
|
### 📦 构建部署
|
||||||
|
- **Docker** - 容器化部署
|
||||||
|
- **PM2** - 生产环境进程管理
|
||||||
|
- **Nginx** - 反向代理和负载均衡
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🏗️ 核心功能
|
||||||
|
|
||||||
### 🔐 用户认证系统
|
### 🔐 用户认证系统
|
||||||
|
- **多方式登录** - 用户名/邮箱/手机号
|
||||||
|
- **邮箱验证** - 完整的验证码流程
|
||||||
|
- **密码安全** - bcrypt加密 + 强度验证
|
||||||
|
- **第三方登录** - GitHub OAuth支持
|
||||||
|
- **权限控制** - 基于角色的访问控制
|
||||||
|
|
||||||
完整的用户认证解决方案,支持多种登录方式和安全特性:
|
### 📧 智能邮件服务
|
||||||
|
- **测试模式** - 控制台输出,无需SMTP服务器
|
||||||
|
- **生产模式** - 支持主流邮件服务商
|
||||||
|
- **模板系统** - 验证码、欢迎邮件等模板
|
||||||
|
- **自动切换** - 根据配置自动选择模式
|
||||||
|
|
||||||
- 用户名/邮箱/手机号登录
|
### 🗄️ 灵活存储方案
|
||||||
- GitHub OAuth 第三方登录
|
- **Redis文件存储** - 开发测试无需Redis服务器
|
||||||
- 密码重置和修改功能
|
- **内存数据库** - 无需MySQL即可运行
|
||||||
- bcrypt 密码加密
|
- **生产就绪** - 支持MySQL + Redis部署
|
||||||
- 基于角色的权限控制
|
- **自动切换** - 根据配置自动选择存储方式
|
||||||
|
|
||||||
**详细文档**: [用户认证系统文档](./docs/systems/user-auth/README.md)
|
### 📚 完整API文档
|
||||||
|
- **Swagger UI** - 交互式API文档
|
||||||
|
- **OpenAPI规范** - 标准化接口描述
|
||||||
|
- **Postman集合** - 可导入的测试集合
|
||||||
|
- **实时更新** - 代码变更自动同步文档
|
||||||
|
|
||||||
### 📖 API 文档系统
|
### 🧪 全面测试覆盖
|
||||||
|
- **单元测试** - 114个测试用例全部通过
|
||||||
|
- **API测试** - 跨平台测试脚本
|
||||||
|
- **集成测试** - 完整业务流程验证
|
||||||
|
- **测试模式** - 无依赖快速测试
|
||||||
|
|
||||||
集成了完整的 API 文档解决方案,提供多种格式的接口文档:
|
---
|
||||||
|
|
||||||
- **Swagger UI** - 交互式 API 文档界面
|
## 📊 开发与测试
|
||||||
- **OpenAPI 规范** - 标准化的 API 描述文件
|
|
||||||
- **Postman 集合** - 可导入的 API 测试集合
|
### 🔧 开发命令
|
||||||
- **详细文档** - 包含示例和最佳实践的完整说明
|
|
||||||
|
|
||||||
**快速访问**:
|
|
||||||
```bash
|
```bash
|
||||||
# 启动服务器
|
# 开发服务器(热重载)
|
||||||
pnpm run dev
|
pnpm run dev
|
||||||
|
|
||||||
# 访问 Swagger UI 文档
|
# 构建项目
|
||||||
# 浏览器打开: http://localhost:3000/api-docs
|
pnpm run build
|
||||||
|
|
||||||
|
# 生产环境运行
|
||||||
|
pnpm run start:prod
|
||||||
|
|
||||||
|
# 代码检查
|
||||||
|
pnpm run lint
|
||||||
|
|
||||||
|
# 格式化代码
|
||||||
|
pnpm run format
|
||||||
```
|
```
|
||||||
|
|
||||||
**详细文档**: [API 文档说明](./docs/api/README.md)
|
### 🧪 测试命令
|
||||||
|
|
||||||
### 📊 日志系统
|
```bash
|
||||||
|
# 运行所有单元测试
|
||||||
|
pnpm test
|
||||||
|
|
||||||
基于 Pino 的高性能日志系统,提供结构化日志记录:
|
# 监听模式运行测试
|
||||||
|
pnpm run test:watch
|
||||||
|
|
||||||
- 高性能日志记录
|
# 生成测试覆盖率报告
|
||||||
- 自动敏感信息过滤
|
pnpm run test:cov
|
||||||
- 多级别日志控制
|
|
||||||
- 请求上下文绑定
|
|
||||||
|
|
||||||
**详细文档**: [日志系统文档](./docs/systems/logger/README.md)
|
# API功能测试
|
||||||
|
.\test-api.ps1 # Windows
|
||||||
|
./test-api.sh # Linux/macOS
|
||||||
|
```
|
||||||
|
|
||||||
**💡 提示:使用 [AI 辅助开发指南](./docs/AI辅助开发规范指南.md) 可以让 AI 帮你自动生成符合规范的代码!**
|
### 📈 测试覆盖率
|
||||||
|
|
||||||
## 下一步
|
- **单元测试**: 114个测试用例 ✅
|
||||||
|
- **功能测试**: 用户认证、邮件验证、数据存储 ✅
|
||||||
|
- **集成测试**: 完整业务流程 ✅
|
||||||
|
|
||||||
- 在 `src/api/` 目录下创建游戏相关的控制器和网关
|
---
|
||||||
- 在 `src/model/` 目录下定义游戏数据模型
|
|
||||||
- 在 `src/service/` 目录下实现游戏业务逻辑
|
## 🌍 部署配置
|
||||||
- 使用 NestJS CLI 快速生成模块:`nest g module game`
|
|
||||||
- 添加 WebSocket 网关实现实时游戏逻辑
|
### 测试环境(默认)
|
||||||
|
```bash
|
||||||
|
# 无需数据库和邮件服务器
|
||||||
|
USE_FILE_REDIS=true
|
||||||
|
NODE_ENV=development
|
||||||
|
# 数据库和邮件配置保持注释状态
|
||||||
|
```
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
```bash
|
||||||
|
# 启用真实服务
|
||||||
|
USE_FILE_REDIS=false
|
||||||
|
NODE_ENV=production
|
||||||
|
|
||||||
|
# 配置数据库
|
||||||
|
DB_HOST=your_mysql_host
|
||||||
|
DB_USERNAME=your_username
|
||||||
|
DB_PASSWORD=your_password
|
||||||
|
|
||||||
|
# 配置Redis
|
||||||
|
REDIS_HOST=your_redis_host
|
||||||
|
REDIS_PASSWORD=your_password
|
||||||
|
|
||||||
|
# 配置邮件服务
|
||||||
|
EMAIL_HOST=smtp.gmail.com
|
||||||
|
EMAIL_USER=your_email@gmail.com
|
||||||
|
EMAIL_PASS=your_app_password
|
||||||
|
```
|
||||||
|
|
||||||
|
详细部署指南:[DEPLOYMENT.md](./DEPLOYMENT.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📚 文档中心
|
||||||
|
|
||||||
|
### 🎯 新手必读
|
||||||
|
1. **[AI辅助开发指南](./docs/AI辅助开发规范指南.md)** - 提升开发效率300%
|
||||||
|
2. **[后端开发规范](./docs/backend_development_guide.md)** - 代码质量标准
|
||||||
|
3. **[Git提交规范](./docs/git_commit_guide.md)** - 版本控制最佳实践
|
||||||
|
|
||||||
|
### 📖 API文档
|
||||||
|
- **[Swagger UI](http://localhost:3000/api-docs)** - 交互式API文档
|
||||||
|
- **[API文档总览](./docs/api/README.md)** - 使用指南
|
||||||
|
- **[OpenAPI规范](./docs/api/openapi.yaml)** - 标准化描述
|
||||||
|
- **[Postman集合](./docs/api/postman-collection.json)** - 测试集合
|
||||||
|
|
||||||
|
### 🏗️ 系统设计
|
||||||
|
- **[用户认证系统](./docs/systems/user-auth/README.md)** - 认证架构设计
|
||||||
|
- **[邮件验证系统](./docs/systems/email-verification/README.md)** - 验证流程设计
|
||||||
|
- **[日志系统](./docs/systems/logger/README.md)** - 日志架构设计
|
||||||
|
|
||||||
|
### 🧪 测试指南
|
||||||
|
- **[测试指南](./TESTING.md)** - 完整测试说明
|
||||||
|
- **[单元测试](./src/**/*.spec.ts)** - 测试用例参考
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🤝 贡献者
|
||||||
|
|
||||||
|
感谢所有为项目做出贡献的开发者!
|
||||||
|
|
||||||
|
### 🏆 核心团队
|
||||||
|
- **[moyin](https://gitea.xinghangee.icu/moyin)** - 核心开发者
|
||||||
|
- **[jianuo](https://gitea.xinghangee.icu/jianuo)** - 核心开发者
|
||||||
|
- **[angjustinl](https://gitea.xinghangee.icu/ANGJustinl)** - 核心开发者
|
||||||
|
|
||||||
|
查看完整贡献者名单:[CONTRIBUTORS.md](./CONTRIBUTORS.md)
|
||||||
|
|
||||||
|
### 🌟 如何贡献
|
||||||
|
|
||||||
|
我们欢迎所有形式的贡献:
|
||||||
|
|
||||||
|
1. **<EFBFBD> Bug修复** - 发现并修复问题
|
||||||
|
2. **✨ 新功能** - 添加有价值的功能
|
||||||
|
3. **<EFBFBD> 文档改馈进** - 完善项目文档
|
||||||
|
4. **🧪 测试用例** - 提高代码覆盖率
|
||||||
|
5. **💡 建议反馈** - 提出改进建议
|
||||||
|
|
||||||
|
**贡献流程:**
|
||||||
|
1. Fork项目 → 2. 创建分支 → 3. 开发功能 → 4. 提交PR
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📞 联系我们
|
||||||
|
|
||||||
|
- **项目地址**: [Gitea Repository](https://gitea.xinghangee.icu/datawhale/whale-town-end)
|
||||||
|
- **问题反馈**: [Issues](https://gitea.xinghangee.icu/datawhale/whale-town-end/issues)
|
||||||
|
- **功能建议**: [Discussions](https://gitea.xinghangee.icu/datawhale/whale-town-end/discussions)
|
||||||
|
|
||||||
|
## 📄 许可证
|
||||||
|
|
||||||
|
本项目采用 [MIT License](./LICENSE) 开源协议。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
**🐋 Whale Town - 让像素世界更精彩!**
|
||||||
|
|
||||||
|
Made with ❤️ by the Whale Town Team
|
||||||
|
|
||||||
|
[⭐ Star](https://gitea.xinghangee.icu/datawhale/whale-town-end) | [🍴 Fork](https://gitea.xinghangee.icu/datawhale/whale-town-end/fork) | [📖 Docs](./docs/) | [🐛 Issues](https://gitea.xinghangee.icu/datawhale/whale-town-end/issues)
|
||||||
|
|
||||||
|
</div>
|
||||||
138
TESTING.md
Normal file
138
TESTING.md
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
# 测试指南
|
||||||
|
|
||||||
|
本项目支持**无数据库和无邮件服务器**的测试模式,让你可以快速验证所有功能!
|
||||||
|
|
||||||
|
## 🚀 快速开始
|
||||||
|
|
||||||
|
### 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"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🧪 测试功能
|
||||||
|
|
||||||
|
测试脚本会验证以下功能:
|
||||||
|
|
||||||
|
- ✅ **邮箱验证码发送** - 生成6位数验证码
|
||||||
|
- ✅ **邮箱验证码验证** - 验证码校验和清理
|
||||||
|
- ✅ **用户注册** - 完整的用户注册流程
|
||||||
|
- ✅ **用户登录** - 用户名/邮箱/手机号登录
|
||||||
|
|
||||||
|
## 🔧 测试模式特性
|
||||||
|
|
||||||
|
- 🗄️ **Redis 文件存储** - 使用 `redis-data/redis.json` 存储验证码
|
||||||
|
- 📧 **邮件测试模式** - 邮件内容输出到控制台,无需真实SMTP
|
||||||
|
- 💾 **内存用户存储** - 无需数据库,用户数据存储在内存中
|
||||||
|
- 🔄 **自动切换** - 根据配置自动选择存储模式
|
||||||
|
|
||||||
|
## 📊 单元测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 运行所有单元测试
|
||||||
|
npm test
|
||||||
|
|
||||||
|
# 监听模式
|
||||||
|
npm run test:watch
|
||||||
|
|
||||||
|
# 生成覆盖率报告
|
||||||
|
npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🌐 生产环境配置
|
||||||
|
|
||||||
|
要切换到生产环境,编辑 `.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是否被占用
|
||||||
|
- 确认Node.js版本 >= 18.0.0
|
||||||
|
- 运行 `npm install` 重新安装依赖
|
||||||
|
|
||||||
|
### 测试脚本执行失败
|
||||||
|
- 确认服务器正在运行
|
||||||
|
- 检查防火墙设置
|
||||||
|
- 在Linux/macOS上确保脚本有执行权限:`chmod +x test-api.sh`
|
||||||
|
|
||||||
|
### Redis文件存储问题
|
||||||
|
- 检查 `redis-data` 目录权限
|
||||||
|
- 确认 `USE_FILE_REDIS=true` 设置正确
|
||||||
|
|
||||||
|
### 邮件测试模式问题
|
||||||
|
- 确认邮件配置为注释状态
|
||||||
|
- 检查服务器控制台日志输出
|
||||||
|
|
||||||
|
## 📝 测试数据
|
||||||
|
|
||||||
|
测试完成后,你可以查看:
|
||||||
|
|
||||||
|
- `redis-data/redis.json` - 验证码存储数据
|
||||||
|
- 服务器控制台 - 邮件内容输出
|
||||||
|
- 测试脚本输出 - API响应结果
|
||||||
|
|
||||||
|
## 🎯 下一步
|
||||||
|
|
||||||
|
- 查看 [API 文档](http://localhost:3000/api-docs) 了解更多接口
|
||||||
|
- 阅读 [开发规范](./docs/backend_development_guide.md) 开始开发
|
||||||
|
- 使用 [AI 辅助指南](./docs/AI辅助开发规范指南.md) 提高开发效率
|
||||||
@@ -1,112 +0,0 @@
|
|||||||
# 验证码问题调试脚本
|
|
||||||
# 作者: moyin
|
|
||||||
# 日期: 2025-12-17
|
|
||||||
|
|
||||||
$baseUrl = "http://localhost:3000"
|
|
||||||
$testEmail = "debug@example.com"
|
|
||||||
|
|
||||||
Write-Host "=== 验证码问题调试脚本 ===" -ForegroundColor Green
|
|
||||||
|
|
||||||
# 步骤1: 发送验证码
|
|
||||||
Write-Host "`n1. 发送验证码..." -ForegroundColor Yellow
|
|
||||||
$sendBody = @{
|
|
||||||
email = $testEmail
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$sendResponse = Invoke-RestMethod -Uri "$baseUrl/auth/send-email-verification" -Method POST -Body $sendBody -ContentType "application/json"
|
|
||||||
Write-Host "发送响应: $($sendResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
if ($sendResponse.success) {
|
|
||||||
Write-Host "✅ 验证码发送成功" -ForegroundColor Green
|
|
||||||
|
|
||||||
# 步骤2: 立即查看验证码调试信息
|
|
||||||
Write-Host "`n2. 查看验证码调试信息..." -ForegroundColor Yellow
|
|
||||||
$debugResponse = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json"
|
|
||||||
Write-Host "调试信息: $($debugResponse | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
# 步骤3: 故意输入错误验证码
|
|
||||||
Write-Host "`n3. 测试错误验证码..." -ForegroundColor Yellow
|
|
||||||
$wrongVerifyBody = @{
|
|
||||||
email = $testEmail
|
|
||||||
verification_code = "000000"
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$wrongResponse = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $wrongVerifyBody -ContentType "application/json"
|
|
||||||
Write-Host "错误验证响应: $($wrongResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Red
|
|
||||||
} catch {
|
|
||||||
Write-Host "错误验证失败(预期): $($_.Exception.Message)" -ForegroundColor Yellow
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$errorResponse = $_.Exception.Response.GetResponseStream()
|
|
||||||
$reader = New-Object System.IO.StreamReader($errorResponse)
|
|
||||||
$errorBody = $reader.ReadToEnd()
|
|
||||||
Write-Host "错误详情: $errorBody" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 步骤4: 再次查看调试信息
|
|
||||||
Write-Host "`n4. 错误验证后的调试信息..." -ForegroundColor Yellow
|
|
||||||
$debugResponse2 = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json"
|
|
||||||
Write-Host "调试信息: $($debugResponse2 | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
# 步骤5: 再次尝试错误验证码
|
|
||||||
Write-Host "`n5. 再次测试错误验证码..." -ForegroundColor Yellow
|
|
||||||
try {
|
|
||||||
$wrongResponse2 = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $wrongVerifyBody -ContentType "application/json"
|
|
||||||
Write-Host "第二次错误验证响应: $($wrongResponse2 | ConvertTo-Json -Depth 3)" -ForegroundColor Red
|
|
||||||
} catch {
|
|
||||||
Write-Host "第二次错误验证失败: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$errorResponse = $_.Exception.Response.GetResponseStream()
|
|
||||||
$reader = New-Object System.IO.StreamReader($errorResponse)
|
|
||||||
$errorBody = $reader.ReadToEnd()
|
|
||||||
Write-Host "错误详情: $errorBody" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 步骤6: 最终调试信息
|
|
||||||
Write-Host "`n6. 最终调试信息..." -ForegroundColor Yellow
|
|
||||||
$debugResponse3 = Invoke-RestMethod -Uri "$baseUrl/auth/debug-verification-code" -Method POST -Body $sendBody -ContentType "application/json"
|
|
||||||
Write-Host "最终调试信息: $($debugResponse3 | ConvertTo-Json -Depth 5)" -ForegroundColor Cyan
|
|
||||||
|
|
||||||
# 步骤7: 使用正确验证码(如果有的话)
|
|
||||||
if ($sendResponse.data.verification_code) {
|
|
||||||
Write-Host "`n7. 使用正确验证码..." -ForegroundColor Yellow
|
|
||||||
$correctVerifyBody = @{
|
|
||||||
email = $testEmail
|
|
||||||
verification_code = $sendResponse.data.verification_code
|
|
||||||
} | ConvertTo-Json
|
|
||||||
|
|
||||||
try {
|
|
||||||
$correctResponse = Invoke-RestMethod -Uri "$baseUrl/auth/verify-email" -Method POST -Body $correctVerifyBody -ContentType "application/json"
|
|
||||||
Write-Host "正确验证响应: $($correctResponse | ConvertTo-Json -Depth 3)" -ForegroundColor Green
|
|
||||||
} catch {
|
|
||||||
Write-Host "正确验证也失败了: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$errorResponse = $_.Exception.Response.GetResponseStream()
|
|
||||||
$reader = New-Object System.IO.StreamReader($errorResponse)
|
|
||||||
$errorBody = $reader.ReadToEnd()
|
|
||||||
Write-Host "错误详情: $errorBody" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
|
||||||
Write-Host "❌ 验证码发送失败: $($sendResponse.message)" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
Write-Host "❌ 请求失败: $($_.Exception.Message)" -ForegroundColor Red
|
|
||||||
if ($_.Exception.Response) {
|
|
||||||
$errorResponse = $_.Exception.Response.GetResponseStream()
|
|
||||||
$reader = New-Object System.IO.StreamReader($errorResponse)
|
|
||||||
$errorBody = $reader.ReadToEnd()
|
|
||||||
Write-Host "错误详情: $errorBody" -ForegroundColor Red
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-Host "`n=== 调试完成 ===" -ForegroundColor Green
|
|
||||||
Write-Host "请查看上述输出,重点关注:" -ForegroundColor Yellow
|
|
||||||
Write-Host "1. TTL值的变化" -ForegroundColor White
|
|
||||||
Write-Host "2. attempts字段的变化" -ForegroundColor White
|
|
||||||
Write-Host "3. 验证码是否被意外删除" -ForegroundColor White
|
|
||||||
257
docs/API_STATUS_CODES.md
Normal file
257
docs/API_STATUS_CODES.md
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
# API 状态码说明
|
||||||
|
|
||||||
|
## 📊 概述
|
||||||
|
|
||||||
|
本文档说明了项目中使用的 HTTP 状态码,特别是针对邮件发送功能的特殊状态码处理。
|
||||||
|
|
||||||
|
## 🔢 标准状态码
|
||||||
|
|
||||||
|
| 状态码 | 含义 | 使用场景 |
|
||||||
|
|--------|------|----------|
|
||||||
|
| 200 | OK | 请求成功 |
|
||||||
|
| 201 | Created | 资源创建成功(如用户注册) |
|
||||||
|
| 400 | Bad Request | 请求参数错误 |
|
||||||
|
| 401 | Unauthorized | 未授权(如密码错误) |
|
||||||
|
| 403 | Forbidden | 权限不足 |
|
||||||
|
| 404 | Not Found | 资源不存在 |
|
||||||
|
| 409 | Conflict | 资源冲突(如用户名已存在) |
|
||||||
|
| 429 | Too Many Requests | 请求频率过高 |
|
||||||
|
| 500 | Internal Server Error | 服务器内部错误 |
|
||||||
|
|
||||||
|
## 🎯 特殊状态码
|
||||||
|
|
||||||
|
### 206 Partial Content - 测试模式
|
||||||
|
|
||||||
|
**使用场景:** 邮件发送功能在测试模式下使用
|
||||||
|
|
||||||
|
**含义:** 请求部分成功,但未完全达到预期效果
|
||||||
|
|
||||||
|
**具体应用:**
|
||||||
|
- 验证码已生成,但邮件未真实发送
|
||||||
|
- 功能正常工作,但处于测试/开发模式
|
||||||
|
- 用户可以获得验证码进行测试,但需要知道这不是真实发送
|
||||||
|
|
||||||
|
**响应示例:**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"data": {
|
||||||
|
"verification_code": "123456",
|
||||||
|
"is_test_mode": true
|
||||||
|
},
|
||||||
|
"message": "⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。",
|
||||||
|
"error_code": "TEST_MODE_ONLY"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📧 邮件发送接口状态码
|
||||||
|
|
||||||
|
### 发送邮箱验证码 - POST /auth/send-email-verification
|
||||||
|
|
||||||
|
| 状态码 | 场景 | 响应 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 200 | 邮件真实发送成功 | `{ "success": true, "message": "验证码已发送,请查收邮件" }` |
|
||||||
|
| 206 | 测试模式 | `{ "success": false, "error_code": "TEST_MODE_ONLY", "data": { "verification_code": "123456", "is_test_mode": true } }` |
|
||||||
|
| 400 | 参数错误 | `{ "success": false, "message": "邮箱格式错误" }` |
|
||||||
|
| 429 | 发送频率过高 | `{ "success": false, "message": "发送频率过高,请稍后重试" }` |
|
||||||
|
|
||||||
|
### 发送密码重置验证码 - POST /auth/forgot-password
|
||||||
|
|
||||||
|
| 状态码 | 场景 | 响应 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 200 | 邮件真实发送成功 | `{ "success": true, "message": "验证码已发送,请查收" }` |
|
||||||
|
| 206 | 测试模式 | `{ "success": false, "error_code": "TEST_MODE_ONLY", "data": { "verification_code": "123456", "is_test_mode": true } }` |
|
||||||
|
| 400 | 参数错误 | `{ "success": false, "message": "邮箱格式错误" }` |
|
||||||
|
| 404 | 用户不存在 | `{ "success": false, "message": "用户不存在" }` |
|
||||||
|
|
||||||
|
### 重新发送邮箱验证码 - POST /auth/resend-email-verification
|
||||||
|
|
||||||
|
| 状态码 | 场景 | 响应 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 200 | 邮件真实发送成功 | `{ "success": true, "message": "验证码已重新发送,请查收邮件" }` |
|
||||||
|
| 206 | 测试模式 | `{ "success": false, "error_code": "TEST_MODE_ONLY", "data": { "verification_code": "123456", "is_test_mode": true } }` |
|
||||||
|
| 400 | 邮箱已验证或用户不存在 | `{ "success": false, "message": "邮箱已验证,无需重复验证" }` |
|
||||||
|
| 429 | 发送频率过高 | `{ "success": false, "message": "发送频率过高,请稍后重试" }` |
|
||||||
|
|
||||||
|
## 🔄 模式切换
|
||||||
|
|
||||||
|
### 测试模式 → 真实发送模式
|
||||||
|
|
||||||
|
**配置前(测试模式):**
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/auth/send-email-verification \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com"}'
|
||||||
|
|
||||||
|
# 响应:206 Partial Content
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"data": {
|
||||||
|
"verification_code": "123456",
|
||||||
|
"is_test_mode": true
|
||||||
|
},
|
||||||
|
"message": "⚠️ 测试模式:验证码已生成但未真实发送...",
|
||||||
|
"error_code": "TEST_MODE_ONLY"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**配置后(真实发送模式):**
|
||||||
|
```bash
|
||||||
|
# 同样的请求
|
||||||
|
curl -X POST http://localhost:3000/auth/send-email-verification \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"email": "test@example.com"}'
|
||||||
|
|
||||||
|
# 响应:200 OK
|
||||||
|
{
|
||||||
|
"success": true,
|
||||||
|
"data": {
|
||||||
|
"is_test_mode": false
|
||||||
|
},
|
||||||
|
"message": "验证码已发送,请查收邮件"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 💡 前端处理建议
|
||||||
|
|
||||||
|
### JavaScript 示例
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
async function sendEmailVerification(email) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/auth/send-email-verification', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.status === 200) {
|
||||||
|
// 真实发送成功
|
||||||
|
showSuccess('验证码已发送,请查收邮件');
|
||||||
|
} else if (response.status === 206) {
|
||||||
|
// 测试模式
|
||||||
|
showWarning(`测试模式:验证码是 ${data.data.verification_code}`);
|
||||||
|
showInfo('请配置邮件服务以启用真实发送');
|
||||||
|
} else {
|
||||||
|
// 其他错误
|
||||||
|
showError(data.message);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError('网络错误,请稍后重试');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### React 示例
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const handleSendVerification = async (email) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/auth/send-email-verification', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ email }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
switch (response.status) {
|
||||||
|
case 200:
|
||||||
|
setMessage({ type: 'success', text: '验证码已发送,请查收邮件' });
|
||||||
|
break;
|
||||||
|
case 206:
|
||||||
|
setMessage({
|
||||||
|
type: 'warning',
|
||||||
|
text: `测试模式:验证码是 ${data.data.verification_code}`
|
||||||
|
});
|
||||||
|
setShowConfigTip(true);
|
||||||
|
break;
|
||||||
|
case 400:
|
||||||
|
setMessage({ type: 'error', text: data.message });
|
||||||
|
break;
|
||||||
|
case 429:
|
||||||
|
setMessage({ type: 'error', text: '发送频率过高,请稍后重试' });
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
setMessage({ type: 'error', text: '发送失败,请稍后重试' });
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
setMessage({ type: 'error', text: '网络错误,请稍后重试' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 UI 展示建议
|
||||||
|
|
||||||
|
### 测试模式提示
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 成功状态 (200) -->
|
||||||
|
<div class="alert alert-success">
|
||||||
|
✅ 验证码已发送,请查收邮件
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 测试模式 (206) -->
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
⚠️ 测试模式:验证码是 123456
|
||||||
|
<br>
|
||||||
|
<small>请配置邮件服务以启用真实发送</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 错误状态 (400+) -->
|
||||||
|
<div class="alert alert-danger">
|
||||||
|
❌ 发送失败:邮箱格式错误
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 开发建议
|
||||||
|
|
||||||
|
### 1. 状态码检查
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 推荐:明确检查状态码
|
||||||
|
if (response.status === 206) {
|
||||||
|
// 处理测试模式
|
||||||
|
} else if (response.status === 200) {
|
||||||
|
// 处理真实发送
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不推荐:只检查 success 字段
|
||||||
|
if (data.success) {
|
||||||
|
// 可能遗漏测试模式的情况
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 错误处理
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 推荐:根据 error_code 进行精确处理
|
||||||
|
switch (data.error_code) {
|
||||||
|
case 'TEST_MODE_ONLY':
|
||||||
|
handleTestMode(data);
|
||||||
|
break;
|
||||||
|
case 'SEND_CODE_FAILED':
|
||||||
|
handleSendFailure(data);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
handleGenericError(data);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 用户体验
|
||||||
|
|
||||||
|
- **测试模式**:清晰提示用户当前处于测试模式
|
||||||
|
- **配置引导**:提供配置邮件服务的链接或说明
|
||||||
|
- **验证码显示**:在测试模式下直接显示验证码
|
||||||
|
- **状态区分**:用不同的颜色和图标区分不同状态
|
||||||
|
|
||||||
|
## 🔗 相关文档
|
||||||
|
|
||||||
|
- [邮件服务配置指南](./EMAIL_CONFIGURATION.md)
|
||||||
|
- [快速启动指南](./QUICK_START.md)
|
||||||
|
- [API 文档](./api/README.md)
|
||||||
187
docs/ARCHITECTURE.md
Normal file
187
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
# 🏗️ 项目架构设计
|
||||||
|
|
||||||
|
## 整体架构
|
||||||
|
|
||||||
|
Whale Town 采用分层架构设计,确保代码的可维护性和可扩展性。
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ API 层 │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ HTTP API │ │ WebSocket │ │ GraphQL │ │
|
||||||
|
│ │ (REST) │ │ (Socket.IO) │ │ (预留) │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 业务逻辑层 │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ 用户认证 │ │ 游戏逻辑 │ │ 社交功能 │ │
|
||||||
|
│ │ (Login) │ │ (Game) │ │ (Social) │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 核心服务层 │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ 邮件服务 │ │ 验证码服务 │ │ 日志服务 │ │
|
||||||
|
│ │ (Email) │ │ (Verification)│ │ (Logger) │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
│
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ 数据访问层 │
|
||||||
|
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||||
|
│ │ 用户数据 │ │ Redis缓存 │ │ 文件存储 │ │
|
||||||
|
│ │ (Users) │ │ (Cache) │ │ (Files) │ │
|
||||||
|
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模块依赖关系
|
||||||
|
|
||||||
|
```
|
||||||
|
AppModule
|
||||||
|
├── ConfigModule (全局配置)
|
||||||
|
├── LoggerModule (日志系统)
|
||||||
|
├── RedisModule (缓存服务)
|
||||||
|
├── UsersModule (用户管理)
|
||||||
|
│ ├── UsersService (数据库模式)
|
||||||
|
│ └── UsersMemoryService (内存模式)
|
||||||
|
├── EmailModule (邮件服务)
|
||||||
|
├── VerificationModule (验证码服务)
|
||||||
|
├── LoginCoreModule (登录核心)
|
||||||
|
└── LoginModule (登录业务)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据流向
|
||||||
|
|
||||||
|
### 用户注册流程
|
||||||
|
```
|
||||||
|
1. 用户请求 → LoginController
|
||||||
|
2. 参数验证 → LoginService
|
||||||
|
3. 发送验证码 → LoginCoreService
|
||||||
|
4. 生成验证码 → VerificationService
|
||||||
|
5. 发送邮件 → EmailService
|
||||||
|
6. 存储验证码 → RedisService
|
||||||
|
7. 返回响应 → 用户
|
||||||
|
```
|
||||||
|
|
||||||
|
### 双模式架构
|
||||||
|
|
||||||
|
项目支持开发测试模式和生产部署模式的无缝切换:
|
||||||
|
|
||||||
|
#### 开发测试模式
|
||||||
|
- **数据库**: 内存存储 (UsersMemoryService)
|
||||||
|
- **缓存**: 文件存储 (FileRedisService)
|
||||||
|
- **邮件**: 控制台输出 (测试模式)
|
||||||
|
- **优势**: 无需外部依赖,快速启动测试
|
||||||
|
|
||||||
|
#### 生产部署模式
|
||||||
|
- **数据库**: MySQL (UsersService + TypeORM)
|
||||||
|
- **缓存**: Redis (RealRedisService + IORedis)
|
||||||
|
- **邮件**: SMTP服务器 (生产模式)
|
||||||
|
- **优势**: 高性能,高可用,数据持久化
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
### 1. 单一职责原则
|
||||||
|
每个模块只负责一个特定的功能领域:
|
||||||
|
- `LoginModule`: 只处理登录相关业务
|
||||||
|
- `EmailModule`: 只处理邮件发送
|
||||||
|
- `VerificationModule`: 只处理验证码逻辑
|
||||||
|
|
||||||
|
### 2. 依赖注入
|
||||||
|
使用NestJS的依赖注入系统:
|
||||||
|
- 接口抽象: `IRedisService`, `IUsersService`
|
||||||
|
- 实现切换: 根据配置自动选择实现类
|
||||||
|
- 测试友好: 易于Mock和单元测试
|
||||||
|
|
||||||
|
### 3. 配置驱动
|
||||||
|
通过环境变量控制行为:
|
||||||
|
- `USE_FILE_REDIS`: 选择Redis实现
|
||||||
|
- `DB_HOST`: 数据库连接配置
|
||||||
|
- `EMAIL_HOST`: 邮件服务配置
|
||||||
|
|
||||||
|
### 4. 错误处理
|
||||||
|
统一的错误处理机制:
|
||||||
|
- HTTP异常: `BadRequestException`, `UnauthorizedException`
|
||||||
|
- 业务异常: 自定义异常类
|
||||||
|
- 日志记录: 结构化错误日志
|
||||||
|
|
||||||
|
## 扩展指南
|
||||||
|
|
||||||
|
### 添加新的业务模块
|
||||||
|
|
||||||
|
1. **创建业务模块**
|
||||||
|
```bash
|
||||||
|
nest g module business/game
|
||||||
|
nest g controller business/game
|
||||||
|
nest g service business/game
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **创建核心服务**
|
||||||
|
```bash
|
||||||
|
nest g module core/game_core
|
||||||
|
nest g service core/game_core
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **添加数据模型**
|
||||||
|
```bash
|
||||||
|
nest g module core/db/games
|
||||||
|
nest g service core/db/games
|
||||||
|
```
|
||||||
|
|
||||||
|
4. **更新主模块**
|
||||||
|
在 `app.module.ts` 中导入新模块
|
||||||
|
|
||||||
|
### 添加新的工具服务
|
||||||
|
|
||||||
|
1. **创建工具模块**
|
||||||
|
```bash
|
||||||
|
nest g module core/utils/notification
|
||||||
|
nest g service core/utils/notification
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **实现服务接口**
|
||||||
|
定义抽象接口和具体实现
|
||||||
|
|
||||||
|
3. **添加配置支持**
|
||||||
|
在环境变量中添加相关配置
|
||||||
|
|
||||||
|
4. **编写测试用例**
|
||||||
|
确保功能正确性和代码覆盖率
|
||||||
|
|
||||||
|
## 性能优化
|
||||||
|
|
||||||
|
### 1. 缓存策略
|
||||||
|
- **Redis缓存**: 验证码、会话信息
|
||||||
|
- **内存缓存**: 配置信息、静态数据
|
||||||
|
- **CDN缓存**: 静态资源文件
|
||||||
|
|
||||||
|
### 2. 数据库优化
|
||||||
|
- **连接池**: 复用数据库连接
|
||||||
|
- **索引优化**: 关键字段建立索引
|
||||||
|
- **查询优化**: 避免N+1查询问题
|
||||||
|
|
||||||
|
### 3. 日志优化
|
||||||
|
- **异步日志**: 使用Pino的异步写入
|
||||||
|
- **日志分级**: 生产环境只记录必要日志
|
||||||
|
- **日志轮转**: 自动清理过期日志文件
|
||||||
|
|
||||||
|
## 安全考虑
|
||||||
|
|
||||||
|
### 1. 数据验证
|
||||||
|
- **输入验证**: class-validator装饰器
|
||||||
|
- **类型检查**: TypeScript静态类型
|
||||||
|
- **SQL注入**: TypeORM参数化查询
|
||||||
|
|
||||||
|
### 2. 认证授权
|
||||||
|
- **密码加密**: bcrypt哈希算法
|
||||||
|
- **会话管理**: Redis存储会话信息
|
||||||
|
- **权限控制**: 基于角色的访问控制
|
||||||
|
|
||||||
|
### 3. 通信安全
|
||||||
|
- **HTTPS**: 生产环境强制HTTPS
|
||||||
|
- **CORS**: 跨域请求控制
|
||||||
|
- **Rate Limiting**: API请求频率限制
|
||||||
@@ -50,6 +50,7 @@
|
|||||||
"@nestjs/cli": "^10.4.9",
|
"@nestjs/cli": "^10.4.9",
|
||||||
"@nestjs/schematics": "^10.2.3",
|
"@nestjs/schematics": "^10.2.3",
|
||||||
"@nestjs/testing": "^10.4.20",
|
"@nestjs/testing": "^10.4.20",
|
||||||
|
"@types/express": "^5.0.6",
|
||||||
"@types/jest": "^29.5.14",
|
"@types/jest": "^29.5.14",
|
||||||
"@types/node": "^20.19.27",
|
"@types/node": "^20.19.27",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
|
|||||||
@@ -1,12 +1,49 @@
|
|||||||
import { Controller, Get } from '@nestjs/common';
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger';
|
||||||
import { AppService } from './app.service';
|
import { AppService } from './app.service';
|
||||||
|
import { AppStatusResponseDto } from './dto/app.dto';
|
||||||
|
import { ErrorResponseDto } from './dto/error_response.dto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用根控制器
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 提供应用基础信息和健康检查接口
|
||||||
|
* - 用于监控服务运行状态
|
||||||
|
*
|
||||||
|
* @author moyin
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-12-17
|
||||||
|
*/
|
||||||
|
@ApiTags('App')
|
||||||
@Controller()
|
@Controller()
|
||||||
export class AppController {
|
export class AppController {
|
||||||
constructor(private readonly appService: AppService) {}
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用状态
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* 返回应用的基本运行状态信息,用于健康检查和监控
|
||||||
|
*
|
||||||
|
* @returns 应用状态信息
|
||||||
|
*/
|
||||||
@Get()
|
@Get()
|
||||||
getStatus(): string {
|
@ApiOperation({
|
||||||
|
summary: '获取应用状态',
|
||||||
|
description: '返回应用的基本运行状态信息,包括服务名称、版本、运行时间等。用于健康检查和服务监控。'
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 200,
|
||||||
|
description: '成功获取应用状态',
|
||||||
|
type: AppStatusResponseDto
|
||||||
|
})
|
||||||
|
@ApiResponse({
|
||||||
|
status: 500,
|
||||||
|
description: '服务器内部错误',
|
||||||
|
type: ErrorResponseDto
|
||||||
|
})
|
||||||
|
getStatus(): AppStatusResponseDto {
|
||||||
return this.appService.getStatus();
|
return this.appService.getStatus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,29 @@ import { LoginCoreModule } from './core/login_core/login_core.module';
|
|||||||
import { LoginModule } from './business/login/login.module';
|
import { LoginModule } from './business/login/login.module';
|
||||||
import { RedisModule } from './core/redis/redis.module';
|
import { RedisModule } from './core/redis/redis.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查数据库配置是否完整 by angjustinl 2025-12-17
|
||||||
|
*
|
||||||
|
* @returns 是否配置了数据库
|
||||||
|
*/
|
||||||
|
function isDatabaseConfigured(): boolean {
|
||||||
|
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
|
||||||
|
return requiredEnvVars.every(varName => process.env[varName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用主模块
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 整合所有功能模块
|
||||||
|
* - 配置全局服务和中间件
|
||||||
|
* - 支持数据库和内存存储的自动切换
|
||||||
|
*
|
||||||
|
* 存储模式选择:
|
||||||
|
* - 如果配置了数据库环境变量,使用数据库模式
|
||||||
|
* - 如果未配置数据库,自动回退到内存模式
|
||||||
|
* - 内存模式适用于快速开发和测试
|
||||||
|
*/
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
ConfigModule.forRoot({
|
ConfigModule.forRoot({
|
||||||
@@ -17,6 +40,8 @@ import { RedisModule } from './core/redis/redis.module';
|
|||||||
}),
|
}),
|
||||||
LoggerModule,
|
LoggerModule,
|
||||||
RedisModule,
|
RedisModule,
|
||||||
|
// 条件导入TypeORM模块
|
||||||
|
...(isDatabaseConfigured() ? [
|
||||||
TypeOrmModule.forRoot({
|
TypeOrmModule.forRoot({
|
||||||
type: 'mysql',
|
type: 'mysql',
|
||||||
host: process.env.DB_HOST,
|
host: process.env.DB_HOST,
|
||||||
@@ -26,8 +51,14 @@ import { RedisModule } from './core/redis/redis.module';
|
|||||||
database: process.env.DB_NAME,
|
database: process.env.DB_NAME,
|
||||||
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
entities: [__dirname + '/**/*.entity{.ts,.js}'],
|
||||||
synchronize: false,
|
synchronize: false,
|
||||||
|
// 添加连接超时和重试配置
|
||||||
|
connectTimeout: 10000,
|
||||||
|
retryAttempts: 3,
|
||||||
|
retryDelay: 3000,
|
||||||
}),
|
}),
|
||||||
UsersModule,
|
] : []),
|
||||||
|
// 根据数据库配置选择用户模块模式
|
||||||
|
isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(),
|
||||||
LoginCoreModule,
|
LoginCoreModule,
|
||||||
LoginModule,
|
LoginModule,
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,8 +1,52 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { AppStatusResponseDto } from './dto/app.dto';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用服务类
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 提供应用基础服务
|
||||||
|
* - 返回应用运行状态信息
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-12-17
|
||||||
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AppService {
|
export class AppService {
|
||||||
getStatus(): string {
|
private readonly startTime: number;
|
||||||
return 'Pixel Game Server is running!';
|
|
||||||
|
constructor(private readonly configService: ConfigService) {
|
||||||
|
this.startTime = Date.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取应用状态
|
||||||
|
*
|
||||||
|
* @returns 应用状态信息
|
||||||
|
*/
|
||||||
|
getStatus(): AppStatusResponseDto {
|
||||||
|
const isDatabaseConfigured = this.isDatabaseConfigured();
|
||||||
|
|
||||||
|
return {
|
||||||
|
service: 'Pixel Game Server',
|
||||||
|
version: '1.0.0',
|
||||||
|
status: 'running',
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
uptime: Math.floor((Date.now() - this.startTime) / 1000),
|
||||||
|
environment: this.configService.get<string>('NODE_ENV', 'development'),
|
||||||
|
storage_mode: isDatabaseConfigured ? 'database' : 'memory'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查数据库配置是否完整
|
||||||
|
*
|
||||||
|
* @returns 是否配置了数据库
|
||||||
|
*/
|
||||||
|
private isDatabaseConfigured(): boolean {
|
||||||
|
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
|
||||||
|
return requiredEnvVars.every(varName => this.configService.get<string>(varName));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,22 +14,25 @@
|
|||||||
* - POST /auth/reset-password - 重置密码
|
* - POST /auth/reset-password - 重置密码
|
||||||
* - PUT /auth/change-password - 修改密码
|
* - PUT /auth/change-password - 修改密码
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin angjustinl
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
* @since 2025-12-17
|
* @since 2025-12-17
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Controller, Post, Put, Body, HttpCode, HttpStatus, ValidationPipe, UsePipes, Logger } from '@nestjs/common';
|
import { Controller, Post, Put, Body, HttpCode, HttpStatus, ValidationPipe, UsePipes, Logger, Res } from '@nestjs/common';
|
||||||
import { ApiTags, ApiOperation, ApiResponse as SwaggerApiResponse, ApiBody } from '@nestjs/swagger';
|
import { ApiTags, ApiOperation, ApiResponse as SwaggerApiResponse, ApiBody } from '@nestjs/swagger';
|
||||||
|
import { Response } from 'express';
|
||||||
import { LoginService, ApiResponse, LoginResponse } from './login.service';
|
import { LoginService, ApiResponse, LoginResponse } from './login.service';
|
||||||
import { LoginDto, RegisterDto, GitHubOAuthDto, ForgotPasswordDto, ResetPasswordDto, ChangePasswordDto, EmailVerificationDto, SendEmailVerificationDto } from './login.dto';
|
import { LoginDto, RegisterDto, GitHubOAuthDto, ForgotPasswordDto, ResetPasswordDto, ChangePasswordDto, EmailVerificationDto, SendEmailVerificationDto } from '../../dto/login.dto';
|
||||||
import {
|
import {
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
RegisterResponseDto,
|
RegisterResponseDto,
|
||||||
GitHubOAuthResponseDto,
|
GitHubOAuthResponseDto,
|
||||||
ForgotPasswordResponseDto,
|
ForgotPasswordResponseDto,
|
||||||
CommonResponseDto
|
CommonResponseDto,
|
||||||
} from './login-response.dto';
|
TestModeEmailVerificationResponseDto,
|
||||||
|
SuccessEmailVerificationResponseDto
|
||||||
|
} from '../../dto/login_response.dto';
|
||||||
|
|
||||||
@ApiTags('auth')
|
@ApiTags('auth')
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
@@ -151,6 +154,7 @@ export class LoginController {
|
|||||||
* 发送密码重置验证码
|
* 发送密码重置验证码
|
||||||
*
|
*
|
||||||
* @param forgotPasswordDto 忘记密码数据
|
* @param forgotPasswordDto 忘记密码数据
|
||||||
|
* @param res Express响应对象
|
||||||
* @returns 发送结果
|
* @returns 发送结果
|
||||||
*/
|
*/
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@@ -163,6 +167,11 @@ export class LoginController {
|
|||||||
description: '验证码发送成功',
|
description: '验证码发送成功',
|
||||||
type: ForgotPasswordResponseDto
|
type: ForgotPasswordResponseDto
|
||||||
})
|
})
|
||||||
|
@SwaggerApiResponse({
|
||||||
|
status: 206,
|
||||||
|
description: '测试模式:验证码已生成但未真实发送',
|
||||||
|
type: ForgotPasswordResponseDto
|
||||||
|
})
|
||||||
@SwaggerApiResponse({
|
@SwaggerApiResponse({
|
||||||
status: 400,
|
status: 400,
|
||||||
description: '请求参数错误'
|
description: '请求参数错误'
|
||||||
@@ -172,10 +181,21 @@ export class LoginController {
|
|||||||
description: '用户不存在'
|
description: '用户不存在'
|
||||||
})
|
})
|
||||||
@Post('forgot-password')
|
@Post('forgot-password')
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto): Promise<ApiResponse<{ verification_code?: string }>> {
|
async forgotPassword(
|
||||||
return await this.loginService.sendPasswordResetCode(forgotPasswordDto.identifier);
|
@Body() forgotPasswordDto: ForgotPasswordDto,
|
||||||
|
@Res() res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
const result = await this.loginService.sendPasswordResetCode(forgotPasswordDto.identifier);
|
||||||
|
|
||||||
|
// 根据结果设置不同的状态码
|
||||||
|
if (result.success) {
|
||||||
|
res.status(HttpStatus.OK).json(result);
|
||||||
|
} else if (result.error_code === 'TEST_MODE_ONLY') {
|
||||||
|
res.status(HttpStatus.PARTIAL_CONTENT).json(result); // 206 Partial Content
|
||||||
|
} else {
|
||||||
|
res.status(HttpStatus.BAD_REQUEST).json(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -256,6 +276,7 @@ export class LoginController {
|
|||||||
* 发送邮箱验证码
|
* 发送邮箱验证码
|
||||||
*
|
*
|
||||||
* @param sendEmailVerificationDto 发送验证码数据
|
* @param sendEmailVerificationDto 发送验证码数据
|
||||||
|
* @param res Express响应对象
|
||||||
* @returns 发送结果
|
* @returns 发送结果
|
||||||
*/
|
*/
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@@ -265,8 +286,13 @@ export class LoginController {
|
|||||||
@ApiBody({ type: SendEmailVerificationDto })
|
@ApiBody({ type: SendEmailVerificationDto })
|
||||||
@SwaggerApiResponse({
|
@SwaggerApiResponse({
|
||||||
status: 200,
|
status: 200,
|
||||||
description: '验证码发送成功',
|
description: '验证码发送成功(真实发送模式)',
|
||||||
type: ForgotPasswordResponseDto
|
type: SuccessEmailVerificationResponseDto
|
||||||
|
})
|
||||||
|
@SwaggerApiResponse({
|
||||||
|
status: 206,
|
||||||
|
description: '测试模式:验证码已生成但未真实发送',
|
||||||
|
type: TestModeEmailVerificationResponseDto
|
||||||
})
|
})
|
||||||
@SwaggerApiResponse({
|
@SwaggerApiResponse({
|
||||||
status: 400,
|
status: 400,
|
||||||
@@ -277,10 +303,21 @@ export class LoginController {
|
|||||||
description: '发送频率过高'
|
description: '发送频率过高'
|
||||||
})
|
})
|
||||||
@Post('send-email-verification')
|
@Post('send-email-verification')
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async sendEmailVerification(@Body() sendEmailVerificationDto: SendEmailVerificationDto): Promise<ApiResponse<{ verification_code?: string }>> {
|
async sendEmailVerification(
|
||||||
return await this.loginService.sendEmailVerification(sendEmailVerificationDto.email);
|
@Body() sendEmailVerificationDto: SendEmailVerificationDto,
|
||||||
|
@Res() res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
const result = await this.loginService.sendEmailVerification(sendEmailVerificationDto.email);
|
||||||
|
|
||||||
|
// 根据结果设置不同的状态码
|
||||||
|
if (result.success) {
|
||||||
|
res.status(HttpStatus.OK).json(result);
|
||||||
|
} else if (result.error_code === 'TEST_MODE_ONLY') {
|
||||||
|
res.status(HttpStatus.PARTIAL_CONTENT).json(result); // 206 Partial Content
|
||||||
|
} else {
|
||||||
|
res.status(HttpStatus.BAD_REQUEST).json(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -317,6 +354,7 @@ export class LoginController {
|
|||||||
* 重新发送邮箱验证码
|
* 重新发送邮箱验证码
|
||||||
*
|
*
|
||||||
* @param sendEmailVerificationDto 发送验证码数据
|
* @param sendEmailVerificationDto 发送验证码数据
|
||||||
|
* @param res Express响应对象
|
||||||
* @returns 发送结果
|
* @returns 发送结果
|
||||||
*/
|
*/
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
@@ -329,6 +367,11 @@ export class LoginController {
|
|||||||
description: '验证码重新发送成功',
|
description: '验证码重新发送成功',
|
||||||
type: ForgotPasswordResponseDto
|
type: ForgotPasswordResponseDto
|
||||||
})
|
})
|
||||||
|
@SwaggerApiResponse({
|
||||||
|
status: 206,
|
||||||
|
description: '测试模式:验证码已生成但未真实发送',
|
||||||
|
type: ForgotPasswordResponseDto
|
||||||
|
})
|
||||||
@SwaggerApiResponse({
|
@SwaggerApiResponse({
|
||||||
status: 400,
|
status: 400,
|
||||||
description: '邮箱已验证或用户不存在'
|
description: '邮箱已验证或用户不存在'
|
||||||
@@ -338,10 +381,21 @@ export class LoginController {
|
|||||||
description: '发送频率过高'
|
description: '发送频率过高'
|
||||||
})
|
})
|
||||||
@Post('resend-email-verification')
|
@Post('resend-email-verification')
|
||||||
@HttpCode(HttpStatus.OK)
|
|
||||||
@UsePipes(new ValidationPipe({ transform: true }))
|
@UsePipes(new ValidationPipe({ transform: true }))
|
||||||
async resendEmailVerification(@Body() sendEmailVerificationDto: SendEmailVerificationDto): Promise<ApiResponse<{ verification_code?: string }>> {
|
async resendEmailVerification(
|
||||||
return await this.loginService.resendEmailVerification(sendEmailVerificationDto.email);
|
@Body() sendEmailVerificationDto: SendEmailVerificationDto,
|
||||||
|
@Res() res: Response
|
||||||
|
): Promise<void> {
|
||||||
|
const result = await this.loginService.resendEmailVerification(sendEmailVerificationDto.email);
|
||||||
|
|
||||||
|
// 根据结果设置不同的状态码
|
||||||
|
if (result.success) {
|
||||||
|
res.status(HttpStatus.OK).json(result);
|
||||||
|
} else if (result.error_code === 'TEST_MODE_ONLY') {
|
||||||
|
res.status(HttpStatus.PARTIAL_CONTENT).json(result); // 206 Partial Content
|
||||||
|
} else {
|
||||||
|
res.status(HttpStatus.BAD_REQUEST).json(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -137,13 +137,31 @@ describe('LoginService', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('sendPasswordResetCode', () => {
|
describe('sendPasswordResetCode', () => {
|
||||||
it('should return success response with verification code', async () => {
|
it('should return test mode response with verification code', async () => {
|
||||||
loginCoreService.sendPasswordResetCode.mockResolvedValue('123456');
|
loginCoreService.sendPasswordResetCode.mockResolvedValue({
|
||||||
|
code: '123456',
|
||||||
|
isTestMode: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await service.sendPasswordResetCode('test@example.com');
|
||||||
|
|
||||||
|
expect(result.success).toBe(false); // 测试模式下不算成功
|
||||||
|
expect(result.error_code).toBe('TEST_MODE_ONLY');
|
||||||
|
expect(result.data?.verification_code).toBe('123456');
|
||||||
|
expect(result.data?.is_test_mode).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return success response for real email sending', async () => {
|
||||||
|
loginCoreService.sendPasswordResetCode.mockResolvedValue({
|
||||||
|
code: '123456',
|
||||||
|
isTestMode: false
|
||||||
|
});
|
||||||
|
|
||||||
const result = await service.sendPasswordResetCode('test@example.com');
|
const result = await service.sendPasswordResetCode('test@example.com');
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.data?.verification_code).toBe('123456');
|
expect(result.data?.is_test_mode).toBe(false);
|
||||||
|
expect(result.data?.verification_code).toBeUndefined(); // 真实模式下不返回验证码
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
* - 调用核心服务完成具体功能
|
* - 调用核心服务完成具体功能
|
||||||
* - 为控制器层提供业务接口
|
* - 为控制器层提供业务接口
|
||||||
*
|
*
|
||||||
* @author moyin
|
* @author moyin angjustinl
|
||||||
* @version 1.0.0
|
* @version 1.0.0
|
||||||
* @since 2025-12-17
|
* @since 2025-12-17
|
||||||
*/
|
*/
|
||||||
@@ -199,21 +199,37 @@ export class LoginService {
|
|||||||
* @param identifier 邮箱或手机号
|
* @param identifier 邮箱或手机号
|
||||||
* @returns 响应结果
|
* @returns 响应结果
|
||||||
*/
|
*/
|
||||||
async sendPasswordResetCode(identifier: string): Promise<ApiResponse<{ verification_code?: string }>> {
|
async sendPasswordResetCode(identifier: string): Promise<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||||||
try {
|
try {
|
||||||
this.logger.log(`发送密码重置验证码: ${identifier}`);
|
this.logger.log(`发送密码重置验证码: ${identifier}`);
|
||||||
|
|
||||||
// 调用核心服务发送验证码
|
// 调用核心服务发送验证码
|
||||||
const verificationCode = await this.loginCoreService.sendPasswordResetCode(identifier);
|
const result = await this.loginCoreService.sendPasswordResetCode(identifier);
|
||||||
|
|
||||||
this.logger.log(`密码重置验证码已发送: ${identifier}`);
|
this.logger.log(`密码重置验证码已发送: ${identifier}`);
|
||||||
|
|
||||||
// 实际应用中不应返回验证码,这里仅用于演示
|
// 根据是否为测试模式返回不同的状态和消息 by angjustinl 2025-12-17
|
||||||
|
if (result.isTestMode) {
|
||||||
|
// 测试模式:验证码生成但未真实发送
|
||||||
|
return {
|
||||||
|
success: false, // 测试模式下不算真正成功
|
||||||
|
data: {
|
||||||
|
verification_code: result.code,
|
||||||
|
is_test_mode: true
|
||||||
|
},
|
||||||
|
message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
|
||||||
|
error_code: 'TEST_MODE_ONLY'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 真实发送模式
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { verification_code: verificationCode },
|
data: {
|
||||||
|
is_test_mode: false
|
||||||
|
},
|
||||||
message: '验证码已发送,请查收'
|
message: '验证码已发送,请查收'
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`发送密码重置验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error));
|
this.logger.error(`发送密码重置验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error));
|
||||||
|
|
||||||
@@ -293,21 +309,37 @@ export class LoginService {
|
|||||||
* @param email 邮箱地址
|
* @param email 邮箱地址
|
||||||
* @returns 响应结果
|
* @returns 响应结果
|
||||||
*/
|
*/
|
||||||
async sendEmailVerification(email: string): Promise<ApiResponse<{ verification_code?: string }>> {
|
async sendEmailVerification(email: string): Promise<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||||||
try {
|
try {
|
||||||
this.logger.log(`发送邮箱验证码: ${email}`);
|
this.logger.log(`发送邮箱验证码: ${email}`);
|
||||||
|
|
||||||
// 调用核心服务发送验证码
|
// 调用核心服务发送验证码
|
||||||
const verificationCode = await this.loginCoreService.sendEmailVerification(email);
|
const result = await this.loginCoreService.sendEmailVerification(email);
|
||||||
|
|
||||||
this.logger.log(`邮箱验证码已发送: ${email}`);
|
this.logger.log(`邮箱验证码已发送: ${email}`);
|
||||||
|
|
||||||
// 实际应用中不应返回验证码,这里仅用于演示
|
// 根据是否为测试模式返回不同的状态和消息
|
||||||
|
if (result.isTestMode) {
|
||||||
|
// 测试模式:验证码生成但未真实发送
|
||||||
|
return {
|
||||||
|
success: false, // 测试模式下不算真正成功
|
||||||
|
data: {
|
||||||
|
verification_code: result.code,
|
||||||
|
is_test_mode: true
|
||||||
|
},
|
||||||
|
message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
|
||||||
|
error_code: 'TEST_MODE_ONLY'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 真实发送模式
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { verification_code: verificationCode },
|
data: {
|
||||||
|
is_test_mode: false
|
||||||
|
},
|
||||||
message: '验证码已发送,请查收邮件'
|
message: '验证码已发送,请查收邮件'
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error));
|
this.logger.error(`发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error));
|
||||||
|
|
||||||
@@ -363,21 +395,37 @@ export class LoginService {
|
|||||||
* @param email 邮箱地址
|
* @param email 邮箱地址
|
||||||
* @returns 响应结果
|
* @returns 响应结果
|
||||||
*/
|
*/
|
||||||
async resendEmailVerification(email: string): Promise<ApiResponse<{ verification_code?: string }>> {
|
async resendEmailVerification(email: string): Promise<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||||||
try {
|
try {
|
||||||
this.logger.log(`重新发送邮箱验证码: ${email}`);
|
this.logger.log(`重新发送邮箱验证码: ${email}`);
|
||||||
|
|
||||||
// 调用核心服务重新发送验证码
|
// 调用核心服务重新发送验证码
|
||||||
const verificationCode = await this.loginCoreService.resendEmailVerification(email);
|
const result = await this.loginCoreService.resendEmailVerification(email);
|
||||||
|
|
||||||
this.logger.log(`邮箱验证码已重新发送: ${email}`);
|
this.logger.log(`邮箱验证码已重新发送: ${email}`);
|
||||||
|
|
||||||
// 实际应用中不应返回验证码,这里仅用于演示
|
// 根据是否为测试模式返回不同的状态和消息
|
||||||
|
if (result.isTestMode) {
|
||||||
|
// 测试模式:验证码生成但未真实发送
|
||||||
|
return {
|
||||||
|
success: false, // 测试模式下不算真正成功
|
||||||
|
data: {
|
||||||
|
verification_code: result.code,
|
||||||
|
is_test_mode: true
|
||||||
|
},
|
||||||
|
message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
|
||||||
|
error_code: 'TEST_MODE_ONLY'
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// 真实发送模式
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: { verification_code: verificationCode },
|
data: {
|
||||||
|
is_test_mode: false
|
||||||
|
},
|
||||||
message: '验证码已重新发送,请查收邮件'
|
message: '验证码已重新发送,请查收邮件'
|
||||||
};
|
};
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`重新发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error));
|
this.logger.error(`重新发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error));
|
||||||
|
|
||||||
|
|||||||
@@ -4,23 +4,61 @@
|
|||||||
* 功能描述:
|
* 功能描述:
|
||||||
* - 整合用户相关的实体、服务和控制器
|
* - 整合用户相关的实体、服务和控制器
|
||||||
* - 配置TypeORM实体和Repository
|
* - 配置TypeORM实体和Repository
|
||||||
|
* - 支持数据库和内存存储的动态切换 by angjustinl 2025-12-17
|
||||||
* - 导出用户服务供其他模块使用
|
* - 导出用户服务供其他模块使用
|
||||||
*
|
*
|
||||||
* @author moyin
|
* 存储模式:by angjustinl 2025-12-17
|
||||||
* @version 1.0.0
|
* - 数据库模式:使用TypeORM连接MySQL数据库
|
||||||
|
* - 内存模式:使用Map存储,适用于开发和测试
|
||||||
|
*
|
||||||
|
* @author moyin angjustinl
|
||||||
|
* @version 1.0.1
|
||||||
* @since 2025-12-17
|
* @since 2025-12-17
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Module } from '@nestjs/common';
|
import { Module, DynamicModule, Global } from '@nestjs/common';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { Users } from './users.entity';
|
import { Users } from './users.entity';
|
||||||
import { UsersService } from './users.service';
|
import { UsersService } from './users.service';
|
||||||
|
import { UsersMemoryService } from './users_memory.service';
|
||||||
|
|
||||||
@Module({
|
@Global()
|
||||||
imports: [
|
@Module({})
|
||||||
TypeOrmModule.forFeature([Users])
|
export class UsersModule {
|
||||||
|
/**
|
||||||
|
* 创建数据库模式的用户模块
|
||||||
|
*
|
||||||
|
* @returns 配置了TypeORM的动态模块
|
||||||
|
*/
|
||||||
|
static forDatabase(): DynamicModule {
|
||||||
|
return {
|
||||||
|
module: UsersModule,
|
||||||
|
imports: [TypeOrmModule.forFeature([Users])],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: 'UsersService',
|
||||||
|
useClass: UsersService,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
providers: [UsersService],
|
exports: ['UsersService', TypeOrmModule],
|
||||||
exports: [UsersService, TypeOrmModule],
|
};
|
||||||
})
|
}
|
||||||
export class UsersModule {}
|
|
||||||
|
/**
|
||||||
|
* 创建内存模式的用户模块
|
||||||
|
*
|
||||||
|
* @returns 配置了内存存储的动态模块
|
||||||
|
*/
|
||||||
|
static forMemory(): DynamicModule {
|
||||||
|
return {
|
||||||
|
module: UsersModule,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: 'UsersService',
|
||||||
|
useClass: UsersMemoryService,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
exports: ['UsersService'],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
349
src/core/db/users/users_memory.service.ts
Normal file
349
src/core/db/users/users_memory.service.ts
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
/**
|
||||||
|
* 用户内存存储服务类
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 提供基于内存的用户数据存储
|
||||||
|
* - 作为数据库连接失败时的回退方案
|
||||||
|
* - 实现与UsersService相同的接口
|
||||||
|
*
|
||||||
|
* 使用场景:
|
||||||
|
* - 开发环境无数据库时的快速启动
|
||||||
|
* - 测试环境的轻量级存储
|
||||||
|
* - 数据库故障时的临时降级
|
||||||
|
*
|
||||||
|
* 注意事项:
|
||||||
|
* - 数据仅存储在内存中,重启后丢失
|
||||||
|
* - 不适用于生产环境
|
||||||
|
* - 性能优异但无持久化保证
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-12-17
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
|
import { Users } from './users.entity';
|
||||||
|
import { CreateUserDto } from './users.dto';
|
||||||
|
import { validate } from 'class-validator';
|
||||||
|
import { plainToClass } from 'class-transformer';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UsersMemoryService {
|
||||||
|
private users: Map<bigint, Users> = new Map();
|
||||||
|
private currentId: bigint = BigInt(1);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建新用户
|
||||||
|
*
|
||||||
|
* @param createUserDto 创建用户的数据传输对象
|
||||||
|
* @returns 创建的用户实体
|
||||||
|
* @throws ConflictException 当用户名、邮箱或手机号已存在时
|
||||||
|
* @throws BadRequestException 当数据验证失败时
|
||||||
|
*/
|
||||||
|
async create(createUserDto: CreateUserDto): Promise<Users> {
|
||||||
|
// 验证DTO
|
||||||
|
const dto = plainToClass(CreateUserDto, createUserDto);
|
||||||
|
const validationErrors = await validate(dto);
|
||||||
|
|
||||||
|
if (validationErrors.length > 0) {
|
||||||
|
const errorMessages = validationErrors.map(error =>
|
||||||
|
Object.values(error.constraints || {}).join(', ')
|
||||||
|
).join('; ');
|
||||||
|
throw new BadRequestException(`数据验证失败: ${errorMessages}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查用户名是否已存在
|
||||||
|
if (createUserDto.username) {
|
||||||
|
const existingUser = await this.findByUsername(createUserDto.username);
|
||||||
|
if (existingUser) {
|
||||||
|
throw new ConflictException('用户名已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查邮箱是否已存在
|
||||||
|
if (createUserDto.email) {
|
||||||
|
const existingEmail = await this.findByEmail(createUserDto.email);
|
||||||
|
if (existingEmail) {
|
||||||
|
throw new ConflictException('邮箱已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查手机号是否已存在
|
||||||
|
if (createUserDto.phone) {
|
||||||
|
const existingPhone = Array.from(this.users.values()).find(
|
||||||
|
u => u.phone === createUserDto.phone
|
||||||
|
);
|
||||||
|
if (existingPhone) {
|
||||||
|
throw new ConflictException('手机号已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查GitHub ID是否已存在
|
||||||
|
if (createUserDto.github_id) {
|
||||||
|
const existingGithub = await this.findByGithubId(createUserDto.github_id);
|
||||||
|
if (existingGithub) {
|
||||||
|
throw new ConflictException('GitHub ID已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建用户实体
|
||||||
|
const user = new Users();
|
||||||
|
user.id = this.currentId++;
|
||||||
|
user.username = createUserDto.username;
|
||||||
|
user.email = createUserDto.email || null;
|
||||||
|
user.phone = createUserDto.phone || null;
|
||||||
|
user.password_hash = createUserDto.password_hash || null;
|
||||||
|
user.nickname = createUserDto.nickname;
|
||||||
|
user.github_id = createUserDto.github_id || null;
|
||||||
|
user.avatar_url = createUserDto.avatar_url || null;
|
||||||
|
user.role = createUserDto.role || 1;
|
||||||
|
user.email_verified = createUserDto.email_verified || false;
|
||||||
|
user.created_at = new Date();
|
||||||
|
user.updated_at = new Date();
|
||||||
|
|
||||||
|
// 保存到内存
|
||||||
|
this.users.set(user.id, user);
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询所有用户
|
||||||
|
*
|
||||||
|
* @param limit 限制返回数量,默认100
|
||||||
|
* @param offset 偏移量,默认0
|
||||||
|
* @returns 用户列表
|
||||||
|
*/
|
||||||
|
async findAll(limit: number = 100, offset: number = 0): Promise<Users[]> {
|
||||||
|
const allUsers = Array.from(this.users.values())
|
||||||
|
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
|
||||||
|
|
||||||
|
return allUsers.slice(offset, offset + limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据ID查询用户
|
||||||
|
*
|
||||||
|
* @param id 用户ID
|
||||||
|
* @returns 用户实体
|
||||||
|
* @throws NotFoundException 当用户不存在时
|
||||||
|
*/
|
||||||
|
async findOne(id: bigint): Promise<Users> {
|
||||||
|
const user = this.users.get(id);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException(`ID为 ${id} 的用户不存在`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据用户名查询用户
|
||||||
|
*
|
||||||
|
* @param username 用户名
|
||||||
|
* @returns 用户实体或null
|
||||||
|
*/
|
||||||
|
async findByUsername(username: string): Promise<Users | null> {
|
||||||
|
const user = Array.from(this.users.values()).find(
|
||||||
|
u => u.username === username
|
||||||
|
);
|
||||||
|
return user || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据邮箱查询用户
|
||||||
|
*
|
||||||
|
* @param email 邮箱
|
||||||
|
* @returns 用户实体或null
|
||||||
|
*/
|
||||||
|
async findByEmail(email: string): Promise<Users | null> {
|
||||||
|
const user = Array.from(this.users.values()).find(
|
||||||
|
u => u.email === email
|
||||||
|
);
|
||||||
|
return user || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据GitHub ID查询用户
|
||||||
|
*
|
||||||
|
* @param githubId GitHub ID
|
||||||
|
* @returns 用户实体或null
|
||||||
|
*/
|
||||||
|
async findByGithubId(githubId: string): Promise<Users | null> {
|
||||||
|
const user = Array.from(this.users.values()).find(
|
||||||
|
u => u.github_id === githubId
|
||||||
|
);
|
||||||
|
return user || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新用户信息
|
||||||
|
*
|
||||||
|
* @param id 用户ID
|
||||||
|
* @param updateData 更新的数据
|
||||||
|
* @returns 更新后的用户实体
|
||||||
|
* @throws NotFoundException 当用户不存在时
|
||||||
|
* @throws ConflictException 当更新的数据与其他用户冲突时
|
||||||
|
*/
|
||||||
|
async update(id: bigint, updateData: Partial<CreateUserDto>): Promise<Users> {
|
||||||
|
// 检查用户是否存在
|
||||||
|
const existingUser = await this.findOne(id);
|
||||||
|
|
||||||
|
// 检查更新数据的唯一性约束
|
||||||
|
if (updateData.username && updateData.username !== existingUser.username) {
|
||||||
|
const usernameExists = await this.findByUsername(updateData.username);
|
||||||
|
if (usernameExists) {
|
||||||
|
throw new ConflictException('用户名已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateData.email && updateData.email !== existingUser.email) {
|
||||||
|
const emailExists = await this.findByEmail(updateData.email);
|
||||||
|
if (emailExists) {
|
||||||
|
throw new ConflictException('邮箱已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateData.phone && updateData.phone !== existingUser.phone) {
|
||||||
|
const phoneExists = Array.from(this.users.values()).find(
|
||||||
|
u => u.phone === updateData.phone && u.id !== id
|
||||||
|
);
|
||||||
|
if (phoneExists) {
|
||||||
|
throw new ConflictException('手机号已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (updateData.github_id && updateData.github_id !== existingUser.github_id) {
|
||||||
|
const githubExists = await this.findByGithubId(updateData.github_id);
|
||||||
|
if (githubExists && githubExists.id !== id) {
|
||||||
|
throw new ConflictException('GitHub ID已存在');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新用户数据
|
||||||
|
Object.assign(existingUser, updateData);
|
||||||
|
existingUser.updated_at = new Date();
|
||||||
|
|
||||||
|
this.users.set(id, existingUser);
|
||||||
|
|
||||||
|
return existingUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除用户
|
||||||
|
*
|
||||||
|
* @param id 用户ID
|
||||||
|
* @returns 删除操作结果
|
||||||
|
* @throws NotFoundException 当用户不存在时
|
||||||
|
*/
|
||||||
|
async remove(id: bigint): Promise<{ affected: number; message: string }> {
|
||||||
|
// 检查用户是否存在
|
||||||
|
await this.findOne(id);
|
||||||
|
|
||||||
|
// 执行删除
|
||||||
|
const deleted = this.users.delete(id);
|
||||||
|
|
||||||
|
return {
|
||||||
|
affected: deleted ? 1 : 0,
|
||||||
|
message: `成功删除ID为 ${id} 的用户`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 软删除用户(内存模式下与硬删除相同)
|
||||||
|
*
|
||||||
|
* @param id 用户ID
|
||||||
|
* @returns 被删除的用户实体
|
||||||
|
*/
|
||||||
|
async softRemove(id: bigint): Promise<Users> {
|
||||||
|
const user = await this.findOne(id);
|
||||||
|
this.users.delete(id);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计用户数量
|
||||||
|
*
|
||||||
|
* @param conditions 查询条件(内存模式下简化处理)
|
||||||
|
* @returns 用户数量
|
||||||
|
*/
|
||||||
|
async count(conditions?: any): Promise<number> {
|
||||||
|
if (!conditions) {
|
||||||
|
return this.users.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简化的条件过滤
|
||||||
|
let count = 0;
|
||||||
|
for (const user of this.users.values()) {
|
||||||
|
let match = true;
|
||||||
|
for (const [key, value] of Object.entries(conditions)) {
|
||||||
|
if ((user as any)[key] !== value) {
|
||||||
|
match = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (match) count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否存在
|
||||||
|
*
|
||||||
|
* @param id 用户ID
|
||||||
|
* @returns 是否存在
|
||||||
|
*/
|
||||||
|
async exists(id: bigint): Promise<boolean> {
|
||||||
|
return this.users.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量创建用户
|
||||||
|
*
|
||||||
|
* @param createUserDtos 用户数据数组
|
||||||
|
* @returns 创建的用户列表
|
||||||
|
*/
|
||||||
|
async createBatch(createUserDtos: CreateUserDto[]): Promise<Users[]> {
|
||||||
|
const users: Users[] = [];
|
||||||
|
|
||||||
|
for (const dto of createUserDtos) {
|
||||||
|
const user = await this.create(dto);
|
||||||
|
users.push(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
return users;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据角色查询用户
|
||||||
|
*
|
||||||
|
* @param role 角色值
|
||||||
|
* @returns 用户列表
|
||||||
|
*/
|
||||||
|
async findByRole(role: number): Promise<Users[]> {
|
||||||
|
return Array.from(this.users.values())
|
||||||
|
.filter(u => u.role === role)
|
||||||
|
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 搜索用户(根据用户名或昵称)
|
||||||
|
*
|
||||||
|
* @param keyword 搜索关键词
|
||||||
|
* @param limit 限制数量
|
||||||
|
* @returns 用户列表
|
||||||
|
*/
|
||||||
|
async search(keyword: string, limit: number = 20): Promise<Users[]> {
|
||||||
|
const lowerKeyword = keyword.toLowerCase();
|
||||||
|
|
||||||
|
return Array.from(this.users.values())
|
||||||
|
.filter(u =>
|
||||||
|
u.username.toLowerCase().includes(lowerKeyword) ||
|
||||||
|
u.nickname.toLowerCase().includes(lowerKeyword)
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.created_at.getTime() - a.created_at.getTime())
|
||||||
|
.slice(0, limit);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -180,11 +180,15 @@ describe('LoginCoreService', () => {
|
|||||||
const verifiedUser = { ...mockUser, email_verified: true };
|
const verifiedUser = { ...mockUser, email_verified: true };
|
||||||
usersService.findByEmail.mockResolvedValue(verifiedUser);
|
usersService.findByEmail.mockResolvedValue(verifiedUser);
|
||||||
verificationService.generateCode.mockResolvedValue('123456');
|
verificationService.generateCode.mockResolvedValue('123456');
|
||||||
emailService.sendVerificationCode.mockResolvedValue(true);
|
emailService.sendVerificationCode.mockResolvedValue({
|
||||||
|
success: true,
|
||||||
|
isTestMode: true
|
||||||
|
});
|
||||||
|
|
||||||
const code = await service.sendPasswordResetCode('test@example.com');
|
const result = await service.sendPasswordResetCode('test@example.com');
|
||||||
|
|
||||||
expect(code).toMatch(/^\d{6}$/);
|
expect(result.code).toMatch(/^\d{6}$/);
|
||||||
|
expect(result.isTestMode).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw NotFoundException for non-existent user', async () => {
|
it('should throw NotFoundException for non-existent user', async () => {
|
||||||
|
|||||||
@@ -16,10 +16,10 @@
|
|||||||
* @since 2025-12-17
|
* @since 2025-12-17
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException, Inject } from '@nestjs/common';
|
||||||
import { UsersService } from '../db/users/users.service';
|
|
||||||
import { Users } from '../db/users/users.entity';
|
import { Users } from '../db/users/users.entity';
|
||||||
import { EmailService } from '../utils/email/email.service';
|
import { UsersService } from '../db/users/users.service';
|
||||||
|
import { EmailService, EmailSendResult } from '../utils/email/email.service';
|
||||||
import { VerificationService, VerificationCodeType } from '../utils/verification/verification.service';
|
import { VerificationService, VerificationCodeType } from '../utils/verification/verification.service';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
@@ -90,10 +90,20 @@ export interface AuthResult {
|
|||||||
isNewUser?: boolean;
|
isNewUser?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证码发送结果接口 by angjustinl 2025-12-17
|
||||||
|
*/
|
||||||
|
export interface VerificationCodeResult {
|
||||||
|
/** 验证码 */
|
||||||
|
code: string;
|
||||||
|
/** 是否为测试模式 */
|
||||||
|
isTestMode: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LoginCoreService {
|
export class LoginCoreService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersService: UsersService,
|
@Inject('UsersService') private readonly usersService: UsersService,
|
||||||
private readonly emailService: EmailService,
|
private readonly emailService: EmailService,
|
||||||
private readonly verificationService: VerificationService,
|
private readonly verificationService: VerificationService,
|
||||||
) {}
|
) {}
|
||||||
@@ -122,7 +132,7 @@ export class LoginCoreService {
|
|||||||
// 如果邮箱未找到,尝试手机号查找(简单验证)
|
// 如果邮箱未找到,尝试手机号查找(简单验证)
|
||||||
if (!user && this.isPhoneNumber(identifier)) {
|
if (!user && this.isPhoneNumber(identifier)) {
|
||||||
const users = await this.usersService.findAll();
|
const users = await this.usersService.findAll();
|
||||||
user = users.find(u => u.phone === identifier) || null;
|
user = users.find((u: Users) => u.phone === identifier) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 用户不存在
|
// 用户不存在
|
||||||
@@ -269,10 +279,10 @@ export class LoginCoreService {
|
|||||||
* 发送密码重置验证码
|
* 发送密码重置验证码
|
||||||
*
|
*
|
||||||
* @param identifier 邮箱或手机号
|
* @param identifier 邮箱或手机号
|
||||||
* @returns 验证码(实际应用中应发送到用户邮箱/手机)
|
* @returns 验证码结果
|
||||||
* @throws NotFoundException 用户不存在时
|
* @throws NotFoundException 用户不存在时
|
||||||
*/
|
*/
|
||||||
async sendPasswordResetCode(identifier: string): Promise<string> {
|
async sendPasswordResetCode(identifier: string): Promise<VerificationCodeResult> {
|
||||||
// 查找用户
|
// 查找用户
|
||||||
let user: Users | null = null;
|
let user: Users | null = null;
|
||||||
|
|
||||||
@@ -285,7 +295,7 @@ export class LoginCoreService {
|
|||||||
}
|
}
|
||||||
} else if (this.isPhoneNumber(identifier)) {
|
} else if (this.isPhoneNumber(identifier)) {
|
||||||
const users = await this.usersService.findAll();
|
const users = await this.usersService.findAll();
|
||||||
user = users.find(u => u.phone === identifier) || null;
|
user = users.find((u: Users) => u.phone === identifier) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -299,23 +309,28 @@ export class LoginCoreService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 发送验证码
|
// 发送验证码
|
||||||
|
let isTestMode = false;
|
||||||
|
|
||||||
if (this.isEmail(identifier)) {
|
if (this.isEmail(identifier)) {
|
||||||
const success = await this.emailService.sendVerificationCode({
|
const result = await this.emailService.sendVerificationCode({
|
||||||
email: identifier,
|
email: identifier,
|
||||||
code: verificationCode,
|
code: verificationCode,
|
||||||
nickname: user.nickname,
|
nickname: user.nickname,
|
||||||
purpose: 'password_reset'
|
purpose: 'password_reset'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!success) {
|
if (!result.success) {
|
||||||
throw new BadRequestException('验证码发送失败,请稍后重试');
|
throw new BadRequestException('验证码发送失败,请稍后重试');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isTestMode = result.isTestMode;
|
||||||
} else {
|
} else {
|
||||||
// TODO: 实现短信发送
|
// TODO: 实现短信发送
|
||||||
console.log(`短信验证码(${identifier}): ${verificationCode}`);
|
console.log(`短信验证码(${identifier}): ${verificationCode}`);
|
||||||
|
isTestMode = true; // 短信也是测试模式
|
||||||
}
|
}
|
||||||
|
|
||||||
return verificationCode; // 实际应用中不应返回验证码
|
return { code: verificationCode, isTestMode };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -347,7 +362,7 @@ export class LoginCoreService {
|
|||||||
user = await this.usersService.findByEmail(identifier);
|
user = await this.usersService.findByEmail(identifier);
|
||||||
} else if (this.isPhoneNumber(identifier)) {
|
} else if (this.isPhoneNumber(identifier)) {
|
||||||
const users = await this.usersService.findAll();
|
const users = await this.usersService.findAll();
|
||||||
user = users.find(u => u.phone === identifier) || null;
|
user = users.find((u: Users) => u.phone === identifier) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@@ -457,9 +472,9 @@ export class LoginCoreService {
|
|||||||
*
|
*
|
||||||
* @param email 邮箱地址
|
* @param email 邮箱地址
|
||||||
* @param nickname 用户昵称
|
* @param nickname 用户昵称
|
||||||
* @returns 验证码
|
* @returns 验证码结果
|
||||||
*/
|
*/
|
||||||
async sendEmailVerification(email: string, nickname?: string): Promise<string> {
|
async sendEmailVerification(email: string, nickname?: string): Promise<VerificationCodeResult> {
|
||||||
// 生成验证码
|
// 生成验证码
|
||||||
const verificationCode = await this.verificationService.generateCode(
|
const verificationCode = await this.verificationService.generateCode(
|
||||||
email,
|
email,
|
||||||
@@ -467,18 +482,18 @@ export class LoginCoreService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 发送验证邮件
|
// 发送验证邮件
|
||||||
const success = await this.emailService.sendVerificationCode({
|
const result = await this.emailService.sendVerificationCode({
|
||||||
email,
|
email,
|
||||||
code: verificationCode,
|
code: verificationCode,
|
||||||
nickname,
|
nickname,
|
||||||
purpose: 'email_verification'
|
purpose: 'email_verification'
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!success) {
|
if (!result.success) {
|
||||||
throw new BadRequestException('验证邮件发送失败,请稍后重试');
|
throw new BadRequestException('验证邮件发送失败,请稍后重试');
|
||||||
}
|
}
|
||||||
|
|
||||||
return verificationCode; // 实际应用中不应返回验证码
|
return { code: verificationCode, isTestMode: result.isTestMode };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -520,9 +535,9 @@ export class LoginCoreService {
|
|||||||
* 重新发送邮箱验证码
|
* 重新发送邮箱验证码
|
||||||
*
|
*
|
||||||
* @param email 邮箱地址
|
* @param email 邮箱地址
|
||||||
* @returns 验证码
|
* @returns 验证码结果
|
||||||
*/
|
*/
|
||||||
async resendEmailVerification(email: string): Promise<string> {
|
async resendEmailVerification(email: string): Promise<VerificationCodeResult> {
|
||||||
const user = await this.usersService.findByEmail(email);
|
const user = await this.usersService.findByEmail(email);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ describe('EmailService', () => {
|
|||||||
configService.get.mockReturnValue(undefined);
|
configService.get.mockReturnValue(undefined);
|
||||||
|
|
||||||
// 重新创建服务实例来测试测试模式
|
// 重新创建服务实例来测试测试模式
|
||||||
const testService = new EmailService(configService);
|
new EmailService(configService);
|
||||||
|
|
||||||
expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({
|
expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({
|
||||||
streamTransport: true,
|
streamTransport: true,
|
||||||
@@ -89,7 +89,7 @@ describe('EmailService', () => {
|
|||||||
.mockReturnValueOnce('test@gmail.com') // EMAIL_USER
|
.mockReturnValueOnce('test@gmail.com') // EMAIL_USER
|
||||||
.mockReturnValueOnce('password'); // EMAIL_PASS
|
.mockReturnValueOnce('password'); // EMAIL_PASS
|
||||||
|
|
||||||
const testService = new EmailService(configService);
|
new EmailService(configService);
|
||||||
|
|
||||||
expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({
|
expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({
|
||||||
host: 'smtp.gmail.com',
|
host: 'smtp.gmail.com',
|
||||||
@@ -117,7 +117,8 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
const result = await service.sendEmail(emailOptions);
|
const result = await service.sendEmail(emailOptions);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.isTestMode).toBe(false);
|
||||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith({
|
expect(mockTransporter.sendMail).toHaveBeenCalledWith({
|
||||||
from: '"Test Sender" <noreply@test.com>',
|
from: '"Test Sender" <noreply@test.com>',
|
||||||
to: 'test@example.com',
|
to: 'test@example.com',
|
||||||
@@ -138,7 +139,8 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
const result = await service.sendEmail(emailOptions);
|
const result = await service.sendEmail(emailOptions);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('发送失败');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该在测试模式下输出邮件内容', async () => {
|
it('应该在测试模式下输出邮件内容', async () => {
|
||||||
@@ -157,13 +159,14 @@ describe('EmailService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Mock the service to use test transporter
|
// Mock the service to use test transporter
|
||||||
const loggerSpy = jest.spyOn(service['logger'], 'log').mockImplementation();
|
const loggerSpy = jest.spyOn(service['logger'], 'warn').mockImplementation();
|
||||||
service['transporter'] = testTransporter;
|
service['transporter'] = testTransporter;
|
||||||
|
|
||||||
const result = await service.sendEmail(emailOptions);
|
const result = await service.sendEmail(emailOptions);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(loggerSpy).toHaveBeenCalledWith('=== 邮件发送(测试模式) ===');
|
expect(result.isTestMode).toBe(true);
|
||||||
|
expect(loggerSpy).toHaveBeenCalledWith('=== 邮件发送(测试模式 - 邮件未真实发送) ===');
|
||||||
|
|
||||||
loggerSpy.mockRestore();
|
loggerSpy.mockRestore();
|
||||||
});
|
});
|
||||||
@@ -183,7 +186,8 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
const result = await service.sendVerificationCode(options);
|
const result = await service.sendVerificationCode(options);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.isTestMode).toBe(false);
|
||||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
to: 'test@example.com',
|
to: 'test@example.com',
|
||||||
@@ -206,7 +210,8 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
const result = await service.sendVerificationCode(options);
|
const result = await service.sendVerificationCode(options);
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.isTestMode).toBe(false);
|
||||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
to: 'test@example.com',
|
to: 'test@example.com',
|
||||||
@@ -227,7 +232,8 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
const result = await service.sendVerificationCode(options);
|
const result = await service.sendVerificationCode(options);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('发送失败');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,7 +244,8 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
const result = await service.sendWelcomeEmail('test@example.com', '测试用户');
|
const result = await service.sendWelcomeEmail('test@example.com', '测试用户');
|
||||||
|
|
||||||
expect(result).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
|
expect(result.isTestMode).toBe(false);
|
||||||
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
expect(mockTransporter.sendMail).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
to: 'test@example.com',
|
to: 'test@example.com',
|
||||||
@@ -253,7 +260,8 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
const result = await service.sendWelcomeEmail('test@example.com', '测试用户');
|
const result = await service.sendWelcomeEmail('test@example.com', '测试用户');
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('发送失败');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -358,7 +366,8 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
const result = await service.sendEmail(emailOptions);
|
const result = await service.sendEmail(emailOptions);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('ECONNREFUSED');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该正确处理认证错误', async () => {
|
it('应该正确处理认证错误', async () => {
|
||||||
@@ -372,7 +381,8 @@ describe('EmailService', () => {
|
|||||||
|
|
||||||
const result = await service.sendEmail(emailOptions);
|
const result = await service.sendEmail(emailOptions);
|
||||||
|
|
||||||
expect(result).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
|
expect(result.error).toBe('Invalid login');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该正确处理连接验证错误', async () => {
|
it('应该正确处理连接验证错误', async () => {
|
||||||
@@ -393,7 +403,7 @@ describe('EmailService', () => {
|
|||||||
.mockReturnValueOnce(undefined) // EMAIL_USER
|
.mockReturnValueOnce(undefined) // EMAIL_USER
|
||||||
.mockReturnValueOnce(undefined); // EMAIL_PASS
|
.mockReturnValueOnce(undefined); // EMAIL_PASS
|
||||||
|
|
||||||
const testService = new EmailService(configService);
|
new EmailService(configService);
|
||||||
|
|
||||||
expect(configService.get).toHaveBeenCalledWith('EMAIL_HOST', 'smtp.gmail.com');
|
expect(configService.get).toHaveBeenCalledWith('EMAIL_HOST', 'smtp.gmail.com');
|
||||||
expect(configService.get).toHaveBeenCalledWith('EMAIL_PORT', 587);
|
expect(configService.get).toHaveBeenCalledWith('EMAIL_PORT', 587);
|
||||||
@@ -408,7 +418,7 @@ describe('EmailService', () => {
|
|||||||
.mockReturnValueOnce('custom@163.com') // EMAIL_USER
|
.mockReturnValueOnce('custom@163.com') // EMAIL_USER
|
||||||
.mockReturnValueOnce('custompass'); // EMAIL_PASS
|
.mockReturnValueOnce('custompass'); // EMAIL_PASS
|
||||||
|
|
||||||
const testService = new EmailService(configService);
|
new EmailService(configService);
|
||||||
|
|
||||||
expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({
|
expect(mockedNodemailer.createTransport).toHaveBeenCalledWith({
|
||||||
host: 'smtp.163.com',
|
host: 'smtp.163.com',
|
||||||
|
|||||||
@@ -50,6 +50,18 @@ export interface VerificationEmailOptions {
|
|||||||
purpose: 'email_verification' | 'password_reset';
|
purpose: 'email_verification' | 'password_reset';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件发送结果接口 by angjustinl 2025-12-17
|
||||||
|
*/
|
||||||
|
export interface EmailSendResult {
|
||||||
|
/** 是否成功 */
|
||||||
|
success: boolean;
|
||||||
|
/** 是否为测试模式 */
|
||||||
|
isTestMode: boolean;
|
||||||
|
/** 错误信息(如果失败) */
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class EmailService {
|
export class EmailService {
|
||||||
private readonly logger = new Logger(EmailService.name);
|
private readonly logger = new Logger(EmailService.name);
|
||||||
@@ -87,13 +99,22 @@ export class EmailService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为测试模式
|
||||||
|
*
|
||||||
|
* @returns 是否为测试模式
|
||||||
|
*/
|
||||||
|
isTestMode(): boolean {
|
||||||
|
return !!(this.transporter.options as any).streamTransport;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 发送邮件
|
* 发送邮件
|
||||||
*
|
*
|
||||||
* @param options 邮件选项
|
* @param options 邮件选项
|
||||||
* @returns 发送结果
|
* @returns 发送结果
|
||||||
*/
|
*/
|
||||||
async sendEmail(options: EmailOptions): Promise<boolean> {
|
async sendEmail(options: EmailOptions): Promise<EmailSendResult> {
|
||||||
try {
|
try {
|
||||||
const mailOptions = {
|
const mailOptions = {
|
||||||
from: this.configService.get<string>('EMAIL_FROM', '"Whale Town Game" <noreply@whaletown.com>'),
|
from: this.configService.get<string>('EMAIL_FROM', '"Whale Town Game" <noreply@whaletown.com>'),
|
||||||
@@ -103,22 +124,31 @@ export class EmailService {
|
|||||||
text: options.text,
|
text: options.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
const result = await this.transporter.sendMail(mailOptions);
|
const isTestMode = this.isTestMode();
|
||||||
|
|
||||||
// 如果是测试模式,输出邮件内容到控制台
|
// 如果是测试模式,输出邮件内容到控制台
|
||||||
if ((this.transporter.options as any).streamTransport) {
|
if (isTestMode) {
|
||||||
this.logger.log('=== 邮件发送(测试模式) ===');
|
this.logger.warn('=== 邮件发送(测试模式 - 邮件未真实发送) ===');
|
||||||
this.logger.log(`收件人: ${options.to}`);
|
this.logger.warn(`收件人: ${options.to}`);
|
||||||
this.logger.log(`主题: ${options.subject}`);
|
this.logger.warn(`主题: ${options.subject}`);
|
||||||
this.logger.log(`内容: ${options.text || '请查看HTML内容'}`);
|
this.logger.warn(`内容: ${options.text || '请查看HTML内容'}`);
|
||||||
this.logger.log('========================');
|
this.logger.warn('⚠️ 注意: 这是测试模式,邮件不会真实发送到用户邮箱');
|
||||||
|
this.logger.warn('💡 提示: 请在 .env 文件中配置邮件服务以启用真实发送');
|
||||||
|
this.logger.warn('================================================');
|
||||||
|
return { success: true, isTestMode: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
this.logger.log(`邮件发送成功: ${options.to}`);
|
// 真实发送邮件
|
||||||
return true;
|
const result = await this.transporter.sendMail(mailOptions);
|
||||||
|
this.logger.log(`✅ 邮件发送成功: ${options.to}`);
|
||||||
|
return { success: true, isTestMode: false };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`邮件发送失败: ${options.to}`, error instanceof Error ? error.stack : String(error));
|
this.logger.error(`❌ 邮件发送失败: ${options.to}`, error instanceof Error ? error.stack : String(error));
|
||||||
return false;
|
return {
|
||||||
|
success: false,
|
||||||
|
isTestMode: this.isTestMode(),
|
||||||
|
error: error instanceof Error ? error.message : String(error)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -128,7 +158,7 @@ export class EmailService {
|
|||||||
* @param options 验证码邮件选项
|
* @param options 验证码邮件选项
|
||||||
* @returns 发送结果
|
* @returns 发送结果
|
||||||
*/
|
*/
|
||||||
async sendVerificationCode(options: VerificationEmailOptions): Promise<boolean> {
|
async sendVerificationCode(options: VerificationEmailOptions): Promise<EmailSendResult> {
|
||||||
const { email, code, nickname, purpose } = options;
|
const { email, code, nickname, purpose } = options;
|
||||||
|
|
||||||
let subject: string;
|
let subject: string;
|
||||||
@@ -157,7 +187,7 @@ export class EmailService {
|
|||||||
* @param nickname 用户昵称
|
* @param nickname 用户昵称
|
||||||
* @returns 发送结果
|
* @returns 发送结果
|
||||||
*/
|
*/
|
||||||
async sendWelcomeEmail(email: string, nickname: string): Promise<boolean> {
|
async sendWelcomeEmail(email: string, nickname: string): Promise<EmailSendResult> {
|
||||||
const subject = '🎮 欢迎加入 Whale Town!';
|
const subject = '🎮 欢迎加入 Whale Town!';
|
||||||
const template = this.getWelcomeTemplate(nickname);
|
const template = this.getWelcomeTemplate(nickname);
|
||||||
|
|
||||||
|
|||||||
@@ -272,6 +272,7 @@ describe('VerificationService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
mockRedis.get.mockResolvedValue(JSON.stringify(codeInfo));
|
mockRedis.get.mockResolvedValue(JSON.stringify(codeInfo));
|
||||||
|
mockRedis.ttl.mockResolvedValue(240); // Mock TTL 返回 240 秒
|
||||||
|
|
||||||
await expect(service.verifyCode(email, type, '654321')).rejects.toThrow(
|
await expect(service.verifyCode(email, type, '654321')).rejects.toThrow(
|
||||||
new BadRequestException('验证码错误,剩余尝试次数: 1')
|
new BadRequestException('验证码错误,剩余尝试次数: 1')
|
||||||
@@ -285,7 +286,7 @@ describe('VerificationService', () => {
|
|||||||
expect(mockRedis.set).toHaveBeenCalledWith(
|
expect(mockRedis.set).toHaveBeenCalledWith(
|
||||||
`verification_code:${type}:${email}`,
|
`verification_code:${type}:${email}`,
|
||||||
JSON.stringify(updatedCodeInfo),
|
JSON.stringify(updatedCodeInfo),
|
||||||
300
|
240
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -298,6 +299,7 @@ describe('VerificationService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
mockRedis.get.mockResolvedValue(JSON.stringify(codeInfo));
|
mockRedis.get.mockResolvedValue(JSON.stringify(codeInfo));
|
||||||
|
mockRedis.ttl.mockResolvedValue(240); // Mock TTL 返回 240 秒
|
||||||
|
|
||||||
await expect(service.verifyCode(email, type, '654321')).rejects.toThrow(
|
await expect(service.verifyCode(email, type, '654321')).rejects.toThrow(
|
||||||
new BadRequestException('验证码错误,剩余尝试次数: 0')
|
new BadRequestException('验证码错误,剩余尝试次数: 0')
|
||||||
@@ -375,7 +377,7 @@ describe('VerificationService', () => {
|
|||||||
it('应该返回存在的验证码统计信息', async () => {
|
it('应该返回存在的验证码统计信息', async () => {
|
||||||
const codeInfo = {
|
const codeInfo = {
|
||||||
code: '123456',
|
code: '123456',
|
||||||
createdAt: Date.now(),
|
createdAt: 1766035834340,
|
||||||
attempts: 1,
|
attempts: 1,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
};
|
};
|
||||||
@@ -391,18 +393,20 @@ describe('VerificationService', () => {
|
|||||||
ttl: 240,
|
ttl: 240,
|
||||||
attempts: 1,
|
attempts: 1,
|
||||||
maxAttempts: 3,
|
maxAttempts: 3,
|
||||||
|
code: '123456',
|
||||||
|
createdAt: expect.any(Number),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('应该在验证码不存在时返回基本信息', async () => {
|
it('应该在验证码不存在时返回基本信息', async () => {
|
||||||
mockRedis.exists.mockResolvedValue(false);
|
mockRedis.exists.mockResolvedValue(false);
|
||||||
mockRedis.ttl.mockResolvedValue(-1);
|
mockRedis.ttl.mockResolvedValue(-2); // -2 表示键不存在
|
||||||
|
|
||||||
const result = await service.getCodeStats(email, type);
|
const result = await service.getCodeStats(email, type);
|
||||||
|
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
exists: false,
|
exists: false,
|
||||||
ttl: -1,
|
ttl: -2, // 修改为 -2
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
72
src/dto/app.dto.ts
Normal file
72
src/dto/app.dto.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* 应用状态响应 DTO
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 定义应用状态接口的响应格式
|
||||||
|
* - 提供 Swagger 文档生成支持
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-12-17
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 应用状态响应 DTO
|
||||||
|
*/
|
||||||
|
export class AppStatusResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '服务名称',
|
||||||
|
example: 'Pixel Game Server',
|
||||||
|
type: String
|
||||||
|
})
|
||||||
|
service: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '服务版本',
|
||||||
|
example: '1.0.0',
|
||||||
|
type: String
|
||||||
|
})
|
||||||
|
version: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '运行状态',
|
||||||
|
example: 'running',
|
||||||
|
enum: ['running', 'starting', 'stopping', 'error'],
|
||||||
|
type: String
|
||||||
|
})
|
||||||
|
status: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '当前时间戳',
|
||||||
|
example: '2025-12-17T15:00:00.000Z',
|
||||||
|
type: String,
|
||||||
|
format: 'date-time'
|
||||||
|
})
|
||||||
|
timestamp: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '运行时间(秒)',
|
||||||
|
example: 3600,
|
||||||
|
type: Number,
|
||||||
|
minimum: 0
|
||||||
|
})
|
||||||
|
uptime: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '运行环境',
|
||||||
|
example: 'development',
|
||||||
|
enum: ['development', 'production', 'test'],
|
||||||
|
type: String
|
||||||
|
})
|
||||||
|
environment: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '存储模式',
|
||||||
|
example: 'memory',
|
||||||
|
enum: ['database', 'memory'],
|
||||||
|
type: String
|
||||||
|
})
|
||||||
|
storage_mode: 'database' | 'memory';
|
||||||
|
}
|
||||||
56
src/dto/error_response.dto.ts
Normal file
56
src/dto/error_response.dto.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
/**
|
||||||
|
* 通用错误响应 DTO
|
||||||
|
*
|
||||||
|
* 功能描述:
|
||||||
|
* - 定义统一的错误响应格式
|
||||||
|
* - 提供 Swagger 文档生成支持
|
||||||
|
*
|
||||||
|
* @author angjustinl
|
||||||
|
* @version 1.0.0
|
||||||
|
* @since 2025-12-17
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通用错误响应 DTO
|
||||||
|
*/
|
||||||
|
export class ErrorResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: 'HTTP 状态码',
|
||||||
|
example: 500,
|
||||||
|
type: Number
|
||||||
|
})
|
||||||
|
statusCode: number;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '错误消息',
|
||||||
|
example: 'Internal server error',
|
||||||
|
type: String
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '错误发生时间',
|
||||||
|
example: '2025-12-17T15:00:00.000Z',
|
||||||
|
type: String,
|
||||||
|
format: 'date-time'
|
||||||
|
})
|
||||||
|
timestamp: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '请求路径',
|
||||||
|
example: '/api/status',
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
path?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '错误代码',
|
||||||
|
example: 'INTERNAL_ERROR',
|
||||||
|
type: String,
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
@@ -209,6 +209,13 @@ export class ForgotPasswordResponseDataDto {
|
|||||||
required: false
|
required: false
|
||||||
})
|
})
|
||||||
verification_code?: string;
|
verification_code?: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '是否为测试模式',
|
||||||
|
example: true,
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
is_test_mode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -217,26 +224,76 @@ export class ForgotPasswordResponseDataDto {
|
|||||||
export class ForgotPasswordResponseDto {
|
export class ForgotPasswordResponseDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: '请求是否成功',
|
description: '请求是否成功',
|
||||||
example: true
|
example: false,
|
||||||
|
examples: {
|
||||||
|
success: {
|
||||||
|
summary: '真实发送成功',
|
||||||
|
value: true
|
||||||
|
},
|
||||||
|
testMode: {
|
||||||
|
summary: '测试模式',
|
||||||
|
value: false
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
success: boolean;
|
success: boolean;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: '响应数据',
|
description: '响应数据',
|
||||||
type: ForgotPasswordResponseDataDto,
|
type: ForgotPasswordResponseDataDto,
|
||||||
required: false
|
required: false,
|
||||||
|
examples: {
|
||||||
|
success: {
|
||||||
|
summary: '真实发送成功',
|
||||||
|
value: {
|
||||||
|
verification_code: '123456',
|
||||||
|
is_test_mode: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
testMode: {
|
||||||
|
summary: '测试模式',
|
||||||
|
value: {
|
||||||
|
verification_code: '059174',
|
||||||
|
is_test_mode: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
data?: ForgotPasswordResponseDataDto;
|
data?: ForgotPasswordResponseDataDto;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: '响应消息',
|
description: '响应消息',
|
||||||
example: '验证码已发送,请查收'
|
example: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
|
||||||
|
examples: {
|
||||||
|
success: {
|
||||||
|
summary: '真实发送成功',
|
||||||
|
value: '验证码已发送,请查收'
|
||||||
|
},
|
||||||
|
testMode: {
|
||||||
|
summary: '测试模式',
|
||||||
|
value: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。'
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
message: string;
|
message: string;
|
||||||
|
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
description: '错误代码',
|
description: '错误代码',
|
||||||
example: 'SEND_CODE_FAILED',
|
example: 'TEST_MODE_ONLY',
|
||||||
|
examples: {
|
||||||
|
success: {
|
||||||
|
summary: '真实发送成功',
|
||||||
|
value: null
|
||||||
|
},
|
||||||
|
testMode: {
|
||||||
|
summary: '测试模式',
|
||||||
|
value: 'TEST_MODE_ONLY'
|
||||||
|
},
|
||||||
|
failed: {
|
||||||
|
summary: '发送失败',
|
||||||
|
value: 'SEND_CODE_FAILED'
|
||||||
|
}
|
||||||
|
},
|
||||||
required: false
|
required: false
|
||||||
})
|
})
|
||||||
error_code?: string;
|
error_code?: string;
|
||||||
@@ -265,3 +322,74 @@ export class CommonResponseDto {
|
|||||||
})
|
})
|
||||||
error_code?: string;
|
error_code?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试模式邮件验证码响应DTO by angjustinl 2025-12-17
|
||||||
|
*/
|
||||||
|
export class TestModeEmailVerificationResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '请求是否成功(测试模式下为false)',
|
||||||
|
example: false
|
||||||
|
})
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '响应数据',
|
||||||
|
example: {
|
||||||
|
verification_code: '059174',
|
||||||
|
is_test_mode: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
data: {
|
||||||
|
verification_code: string;
|
||||||
|
is_test_mode: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '响应消息',
|
||||||
|
example: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。'
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '错误代码',
|
||||||
|
example: 'TEST_MODE_ONLY'
|
||||||
|
})
|
||||||
|
error_code: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 成功发送邮件验证码响应DTO
|
||||||
|
*/
|
||||||
|
export class SuccessEmailVerificationResponseDto {
|
||||||
|
@ApiProperty({
|
||||||
|
description: '请求是否成功',
|
||||||
|
example: true
|
||||||
|
})
|
||||||
|
success: boolean;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '响应数据',
|
||||||
|
example: {
|
||||||
|
verification_code: '123456',
|
||||||
|
is_test_mode: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
data: {
|
||||||
|
verification_code: string;
|
||||||
|
is_test_mode: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '响应消息',
|
||||||
|
example: '验证码已发送,请查收'
|
||||||
|
})
|
||||||
|
message: string;
|
||||||
|
|
||||||
|
@ApiProperty({
|
||||||
|
description: '错误代码',
|
||||||
|
example: null,
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
error_code?: string;
|
||||||
|
}
|
||||||
36
src/main.ts
36
src/main.ts
@@ -3,8 +3,42 @@ import { AppModule } from './app.module';
|
|||||||
import { ValidationPipe } from '@nestjs/common';
|
import { ValidationPipe } from '@nestjs/common';
|
||||||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查数据库配置是否完整 by angjustinl 2025-12-17
|
||||||
|
*
|
||||||
|
* @returns 是否配置了数据库
|
||||||
|
*/
|
||||||
|
function isDatabaseConfigured(): boolean {
|
||||||
|
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
|
||||||
|
return requiredEnvVars.every(varName => process.env[varName]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印启动横幅
|
||||||
|
*/
|
||||||
|
function printBanner() {
|
||||||
|
const isDatabaseMode = isDatabaseConfigured();
|
||||||
|
|
||||||
|
console.log('\n' + '='.repeat(70));
|
||||||
|
console.log('🎮 Pixel Game Server');
|
||||||
|
console.log('='.repeat(70));
|
||||||
|
console.log(`📦 存储模式: ${isDatabaseMode ? '数据库模式 (MySQL)' : '内存模式 (Memory)'}`);
|
||||||
|
|
||||||
|
if (!isDatabaseMode) {
|
||||||
|
console.log('⚠️ 警告: 未检测到数据库配置,使用内存存储');
|
||||||
|
console.log('💡 提示: 数据将在服务重启后丢失');
|
||||||
|
console.log('📝 配置: 请在 .env 文件中配置数据库连接信息');
|
||||||
|
} else {
|
||||||
|
console.log('✅ 数据库: 已连接到 MySQL 数据库');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('='.repeat(70) + '\n');
|
||||||
|
}
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
const app = await NestFactory.create(AppModule, {
|
||||||
|
logger: ['error', 'warn', 'log'],
|
||||||
|
});
|
||||||
|
|
||||||
// 全局启用校验管道(核心配置)
|
// 全局启用校验管道(核心配置)
|
||||||
app.useGlobalPipes(
|
app.useGlobalPipes(
|
||||||
|
|||||||
93
test-api.ps1
Normal file
93
test-api.ps1
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
# Whale Town API Test Script (Windows PowerShell)
|
||||||
|
# 测试邮箱验证码和用户注册登录功能
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$BaseUrl = "http://localhost:3000",
|
||||||
|
[string]$TestEmail = "test@example.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "=== Whale Town API Test (Windows) ===" -ForegroundColor Green
|
||||||
|
Write-Host "Testing without database and email server" -ForegroundColor Cyan
|
||||||
|
Write-Host "Base URL: $BaseUrl" -ForegroundColor Yellow
|
||||||
|
Write-Host "Test Email: $TestEmail" -ForegroundColor Yellow
|
||||||
|
|
||||||
|
# Test 1: Send verification code
|
||||||
|
Write-Host "`n1. Sending email verification code..." -ForegroundColor Yellow
|
||||||
|
$sendBody = @{
|
||||||
|
email = $TestEmail
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
try {
|
||||||
|
$sendResponse = Invoke-RestMethod -Uri "$BaseUrl/auth/send-email-verification" -Method POST -Body $sendBody -ContentType "application/json"
|
||||||
|
Write-Host "✅ Verification code sent successfully" -ForegroundColor Green
|
||||||
|
Write-Host " Code: $($sendResponse.data.verification_code)" -ForegroundColor Cyan
|
||||||
|
Write-Host " Test Mode: $($sendResponse.data.is_test_mode)" -ForegroundColor Cyan
|
||||||
|
$verificationCode = $sendResponse.data.verification_code
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ Failed to send verification code" -ForegroundColor Red
|
||||||
|
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 2: Verify email code
|
||||||
|
Write-Host "`n2. Verifying email code..." -ForegroundColor Yellow
|
||||||
|
$verifyBody = @{
|
||||||
|
email = $TestEmail
|
||||||
|
verification_code = $verificationCode
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
try {
|
||||||
|
$verifyResponse = Invoke-RestMethod -Uri "$BaseUrl/auth/verify-email" -Method POST -Body $verifyBody -ContentType "application/json"
|
||||||
|
Write-Host "✅ Email verification successful" -ForegroundColor Green
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ Email verification failed" -ForegroundColor Red
|
||||||
|
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 3: User registration
|
||||||
|
Write-Host "`n3. Testing user registration..." -ForegroundColor Yellow
|
||||||
|
$registerBody = @{
|
||||||
|
username = "testuser_$(Get-Random -Maximum 9999)"
|
||||||
|
password = "Test123456"
|
||||||
|
nickname = "Test User"
|
||||||
|
email = $TestEmail
|
||||||
|
email_verification_code = $verificationCode
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
try {
|
||||||
|
$registerResponse = Invoke-RestMethod -Uri "$BaseUrl/auth/register" -Method POST -Body $registerBody -ContentType "application/json"
|
||||||
|
Write-Host "✅ User registration successful" -ForegroundColor Green
|
||||||
|
Write-Host " User ID: $($registerResponse.data.user.id)" -ForegroundColor Cyan
|
||||||
|
Write-Host " Username: $($registerResponse.data.user.username)" -ForegroundColor Cyan
|
||||||
|
$username = $registerResponse.data.user.username
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ User registration failed" -ForegroundColor Red
|
||||||
|
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
$username = $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Test 4: User login
|
||||||
|
if ($username) {
|
||||||
|
Write-Host "`n4. Testing user login..." -ForegroundColor Yellow
|
||||||
|
$loginBody = @{
|
||||||
|
identifier = $username
|
||||||
|
password = "Test123456"
|
||||||
|
} | ConvertTo-Json
|
||||||
|
|
||||||
|
try {
|
||||||
|
$loginResponse = Invoke-RestMethod -Uri "$BaseUrl/auth/login" -Method POST -Body $loginBody -ContentType "application/json"
|
||||||
|
Write-Host "✅ User login successful" -ForegroundColor Green
|
||||||
|
Write-Host " Username: $($loginResponse.data.user.username)" -ForegroundColor Cyan
|
||||||
|
Write-Host " Nickname: $($loginResponse.data.user.nickname)" -ForegroundColor Cyan
|
||||||
|
} catch {
|
||||||
|
Write-Host "❌ User login failed" -ForegroundColor Red
|
||||||
|
Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "`n=== Test Summary ===" -ForegroundColor Green
|
||||||
|
Write-Host "✅ Redis file storage: Working" -ForegroundColor Green
|
||||||
|
Write-Host "✅ Email test mode: Working" -ForegroundColor Green
|
||||||
|
Write-Host "✅ Memory user storage: Working" -ForegroundColor Green
|
||||||
|
Write-Host "`n💡 Check redis-data/redis.json for stored verification data" -ForegroundColor Yellow
|
||||||
|
Write-Host "💡 Check server console for email content output" -ForegroundColor Yellow
|
||||||
95
test-api.sh
Normal file
95
test-api.sh
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Whale Town API Test Script (Linux/macOS)
|
||||||
|
# 测试邮箱验证码和用户注册登录功能
|
||||||
|
|
||||||
|
BASE_URL="${1:-http://localhost:3000}"
|
||||||
|
TEST_EMAIL="${2:-test@example.com}"
|
||||||
|
|
||||||
|
echo "=== Whale Town API Test (Linux/macOS) ==="
|
||||||
|
echo "Testing without database and email server"
|
||||||
|
echo "Base URL: $BASE_URL"
|
||||||
|
echo "Test Email: $TEST_EMAIL"
|
||||||
|
|
||||||
|
# Test 1: Send verification code
|
||||||
|
echo ""
|
||||||
|
echo "1. Sending email verification code..."
|
||||||
|
SEND_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/send-email-verification" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"email\":\"$TEST_EMAIL\"}")
|
||||||
|
|
||||||
|
if echo "$SEND_RESPONSE" | grep -q '"success"'; then
|
||||||
|
echo "✅ Verification code sent successfully"
|
||||||
|
VERIFICATION_CODE=$(echo "$SEND_RESPONSE" | grep -o '"verification_code":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
IS_TEST_MODE=$(echo "$SEND_RESPONSE" | grep -o '"is_test_mode":[^,}]*' | cut -d':' -f2)
|
||||||
|
echo " Code: $VERIFICATION_CODE"
|
||||||
|
echo " Test Mode: $IS_TEST_MODE"
|
||||||
|
else
|
||||||
|
echo "❌ Failed to send verification code"
|
||||||
|
echo " Response: $SEND_RESPONSE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 2: Verify email code
|
||||||
|
echo ""
|
||||||
|
echo "2. Verifying email code..."
|
||||||
|
VERIFY_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/verify-email" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"email\":\"$TEST_EMAIL\",\"verification_code\":\"$VERIFICATION_CODE\"}")
|
||||||
|
|
||||||
|
if echo "$VERIFY_RESPONSE" | grep -q '"success":true'; then
|
||||||
|
echo "✅ Email verification successful"
|
||||||
|
else
|
||||||
|
echo "❌ Email verification failed"
|
||||||
|
echo " Response: $VERIFY_RESPONSE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: User registration
|
||||||
|
echo ""
|
||||||
|
echo "3. Testing user registration..."
|
||||||
|
RANDOM_NUM=$((RANDOM % 9999))
|
||||||
|
USERNAME="testuser_$RANDOM_NUM"
|
||||||
|
|
||||||
|
REGISTER_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/register" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"username\":\"$USERNAME\",\"password\":\"Test123456\",\"nickname\":\"Test User\",\"email\":\"$TEST_EMAIL\",\"email_verification_code\":\"$VERIFICATION_CODE\"}")
|
||||||
|
|
||||||
|
if echo "$REGISTER_RESPONSE" | grep -q '"success":true'; then
|
||||||
|
echo "✅ User registration successful"
|
||||||
|
USER_ID=$(echo "$REGISTER_RESPONSE" | grep -o '"id":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
REGISTERED_USERNAME=$(echo "$REGISTER_RESPONSE" | grep -o '"username":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
echo " User ID: $USER_ID"
|
||||||
|
echo " Username: $REGISTERED_USERNAME"
|
||||||
|
else
|
||||||
|
echo "❌ User registration failed"
|
||||||
|
echo " Response: $REGISTER_RESPONSE"
|
||||||
|
REGISTERED_USERNAME=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4: User login
|
||||||
|
if [ -n "$REGISTERED_USERNAME" ]; then
|
||||||
|
echo ""
|
||||||
|
echo "4. Testing user login..."
|
||||||
|
LOGIN_RESPONSE=$(curl -s -X POST "$BASE_URL/auth/login" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"identifier\":\"$REGISTERED_USERNAME\",\"password\":\"Test123456\"}")
|
||||||
|
|
||||||
|
if echo "$LOGIN_RESPONSE" | grep -q '"success":true'; then
|
||||||
|
echo "✅ User login successful"
|
||||||
|
LOGIN_USERNAME=$(echo "$LOGIN_RESPONSE" | grep -o '"username":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
LOGIN_NICKNAME=$(echo "$LOGIN_RESPONSE" | grep -o '"nickname":"[^"]*"' | cut -d'"' -f4)
|
||||||
|
echo " Username: $LOGIN_USERNAME"
|
||||||
|
echo " Nickname: $LOGIN_NICKNAME"
|
||||||
|
else
|
||||||
|
echo "❌ User login failed"
|
||||||
|
echo " Response: $LOGIN_RESPONSE"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Test Summary ==="
|
||||||
|
echo "✅ Redis file storage: Working"
|
||||||
|
echo "✅ Email test mode: Working"
|
||||||
|
echo "✅ Memory user storage: Working"
|
||||||
|
echo ""
|
||||||
|
echo "💡 Check redis-data/redis.json for stored verification data"
|
||||||
|
echo "💡 Check server console for email content output"
|
||||||
Reference in New Issue
Block a user