From 26ea5ac81543f242c60317b741455d0047d6a6e1 Mon Sep 17 00:00:00 2001 From: angjustinl <96008766+ANGJustinl@users.noreply.github.com> Date: Thu, 18 Dec 2025 00:17:43 +0800 Subject: [PATCH] =?UTF-8?q?feat(sql,=20auth,=20email,=20dto)=EF=BC=9A?= =?UTF-8?q?=E9=87=8D=E6=9E=84=E9=82=AE=E7=AE=B1=E9=AA=8C=E8=AF=81=E6=B5=81?= =?UTF-8?q?=E7=A8=8B=EF=BC=8C=E5=BC=95=E5=85=A5=E5=9F=BA=E4=BA=8E=E5=86=85?= =?UTF-8?q?=E5=AD=98=E7=9A=84=E7=94=A8=E6=88=B7=E6=9C=8D=E5=8A=A1=EF=BC=8C?= =?UTF-8?q?=E5=B9=B6=E6=94=B9=E8=BF=9B=20API=20=E5=93=8D=E5=BA=94=E5=A4=84?= =?UTF-8?q?=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 新增完整的 API 状态码文档,并对测试模式进行特殊处理(`206 Partial Content`) * 重组 DTO 结构,引入 `app.dto.ts` 与 `error_response.dto.ts`,以实现统一、规范的响应格式 * 重构登录相关 DTO,优化命名与结构,提升可维护性 * 实现基于内存的用户服务(`users_memory.service.ts`),用于开发与测试环境 * 更新邮件服务,增强验证码生成逻辑,并支持测试模式自动识别 * 增强登录控制器与服务层的错误处理能力,统一响应行为 * 优化核心登录服务,强化参数校验并集成邮箱验证流程 * 新增 `@types/express` 依赖,提升 TypeScript 类型支持与开发体验 * 改进 `main.ts`,优化应用初始化流程与配置管理 * 在所有服务中统一错误处理机制,采用标准化的错误响应格式 * 实现测试模式(`206`)与生产环境邮件发送(`200`)之间的无缝切换 --- .env.example | 36 ++ docs/API_STATUS_CODES.md | 257 +++++++++++++ package.json | 1 + src/app.controller.ts | 39 +- src/app.module.ts | 53 ++- src/app.service.ts | 48 ++- src/business/login/login.controller.ts | 86 ++++- src/business/login/login.service.spec.ts | 24 +- src/business/login/login.service.ts | 98 +++-- src/core/db/users/users.module.ts | 60 ++- src/core/db/users/users_memory.service.ts | 349 ++++++++++++++++++ .../login_core/login_core.service.spec.ts | 10 +- src/core/login_core/login_core.service.ts | 52 ++- src/core/utils/email/email.service.ts | 60 ++- src/dto/app.dto.ts | 72 ++++ src/dto/error_response.dto.ts | 56 +++ src/{business/login => dto}/login.dto.ts | 0 .../login_response.dto.ts} | 136 ++++++- src/main.ts | 36 +- 19 files changed, 1362 insertions(+), 111 deletions(-) create mode 100644 .env.example create mode 100644 docs/API_STATUS_CODES.md create mode 100644 src/core/db/users/users_memory.service.ts create mode 100644 src/dto/app.dto.ts create mode 100644 src/dto/error_response.dto.ts rename src/{business/login => dto}/login.dto.ts (100%) rename src/{business/login/login-response.dto.ts => dto/login_response.dto.ts} (62%) diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..71f2e67 --- /dev/null +++ b/.env.example @@ -0,0 +1,36 @@ +# 环境配置模板 + +# 数据库配置 +# DB_HOST=localhost +# DB_PORT=3306 +# DB_USERNAME=your_db_username +# DB_PASSWORD=your_db_password +# DB_NAME=your_db_name + +# 应用配置 +NODE_ENV=production +PORT=3000 +LOG_LEVEL=info + +# JWT 配置(如果有的话) +JWT_SECRET=your_jwt_secret_key_here_at_least_32_characters +JWT_EXPIRES_IN=7d + +# Redis 配置(用于验证码存储) +# 生产环境使用真实Redis服务 +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" + +# 其他配置 +# 根据项目需要添加其他环境变量 \ No newline at end of file diff --git a/docs/API_STATUS_CODES.md b/docs/API_STATUS_CODES.md new file mode 100644 index 0000000..1042acb --- /dev/null +++ b/docs/API_STATUS_CODES.md @@ -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 + +
+ ✅ 验证码已发送,请查收邮件 +
+ + +
+ ⚠️ 测试模式:验证码是 123456 +
+ 请配置邮件服务以启用真实发送 +
+ + +
+ ❌ 发送失败:邮箱格式错误 +
+``` + +## 📝 开发建议 + +### 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) \ No newline at end of file diff --git a/package.json b/package.json index 129c528..ac9307f 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@nestjs/cli": "^10.4.9", "@nestjs/schematics": "^10.2.3", "@nestjs/testing": "^10.4.20", + "@types/express": "^5.0.6", "@types/jest": "^29.5.14", "@types/node": "^20.19.27", "@types/nodemailer": "^6.4.14", diff --git a/src/app.controller.ts b/src/app.controller.ts index 1d102bb..5b66006 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -1,12 +1,49 @@ import { Controller, Get } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse } from '@nestjs/swagger'; 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() export class AppController { constructor(private readonly appService: AppService) {} + /** + * 获取应用状态 + * + * 功能描述: + * 返回应用的基本运行状态信息,用于健康检查和监控 + * + * @returns 应用状态信息 + */ @Get() - getStatus(): string { + @ApiOperation({ + summary: '获取应用状态', + description: '返回应用的基本运行状态信息,包括服务名称、版本、运行时间等。用于健康检查和服务监控。' + }) + @ApiResponse({ + status: 200, + description: '成功获取应用状态', + type: AppStatusResponseDto + }) + @ApiResponse({ + status: 500, + description: '服务器内部错误', + type: ErrorResponseDto + }) + getStatus(): AppStatusResponseDto { return this.appService.getStatus(); } } diff --git a/src/app.module.ts b/src/app.module.ts index ec8112e..12d727f 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -9,6 +9,29 @@ import { LoginCoreModule } from './core/login_core/login_core.module'; import { LoginModule } from './business/login/login.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({ imports: [ ConfigModule.forRoot({ @@ -17,17 +40,25 @@ import { RedisModule } from './core/redis/redis.module'; }), LoggerModule, RedisModule, - TypeOrmModule.forRoot({ - type: 'mysql', - host: process.env.DB_HOST, - port: parseInt(process.env.DB_PORT), - username: process.env.DB_USERNAME, - password: process.env.DB_PASSWORD, - database: process.env.DB_NAME, - entities: [__dirname + '/**/*.entity{.ts,.js}'], - synchronize: false, - }), - UsersModule, + // 条件导入TypeORM模块 + ...(isDatabaseConfigured() ? [ + TypeOrmModule.forRoot({ + type: 'mysql', + host: process.env.DB_HOST, + port: parseInt(process.env.DB_PORT), + username: process.env.DB_USERNAME, + password: process.env.DB_PASSWORD, + database: process.env.DB_NAME, + entities: [__dirname + '/**/*.entity{.ts,.js}'], + synchronize: false, + // 添加连接超时和重试配置 + connectTimeout: 10000, + retryAttempts: 3, + retryDelay: 3000, + }), + ] : []), + // 根据数据库配置选择用户模块模式 + isDatabaseConfigured() ? UsersModule.forDatabase() : UsersModule.forMemory(), LoginCoreModule, LoginModule, ], diff --git a/src/app.service.ts b/src/app.service.ts index c93fae9..59dea0d 100644 --- a/src/app.service.ts +++ b/src/app.service.ts @@ -1,8 +1,52 @@ 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() export class AppService { - getStatus(): string { - return 'Pixel Game Server is running!'; + private readonly startTime: number; + + 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('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(varName)); } } diff --git a/src/business/login/login.controller.ts b/src/business/login/login.controller.ts index ba16f86..dc117c5 100644 --- a/src/business/login/login.controller.ts +++ b/src/business/login/login.controller.ts @@ -14,22 +14,25 @@ * - POST /auth/reset-password - 重置密码 * - PUT /auth/change-password - 修改密码 * - * @author moyin + * @author moyin angjustinl * @version 1.0.0 * @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 { Response } from 'express'; 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 { LoginResponseDto, RegisterResponseDto, GitHubOAuthResponseDto, ForgotPasswordResponseDto, - CommonResponseDto -} from './login-response.dto'; + CommonResponseDto, + TestModeEmailVerificationResponseDto, + SuccessEmailVerificationResponseDto +} from '../../dto/login_response.dto'; @ApiTags('auth') @Controller('auth') @@ -151,6 +154,7 @@ export class LoginController { * 发送密码重置验证码 * * @param forgotPasswordDto 忘记密码数据 + * @param res Express响应对象 * @returns 发送结果 */ @ApiOperation({ @@ -163,6 +167,11 @@ export class LoginController { description: '验证码发送成功', type: ForgotPasswordResponseDto }) + @SwaggerApiResponse({ + status: 206, + description: '测试模式:验证码已生成但未真实发送', + type: ForgotPasswordResponseDto + }) @SwaggerApiResponse({ status: 400, description: '请求参数错误' @@ -172,10 +181,21 @@ export class LoginController { description: '用户不存在' }) @Post('forgot-password') - @HttpCode(HttpStatus.OK) @UsePipes(new ValidationPipe({ transform: true })) - async forgotPassword(@Body() forgotPasswordDto: ForgotPasswordDto): Promise> { - return await this.loginService.sendPasswordResetCode(forgotPasswordDto.identifier); + async forgotPassword( + @Body() forgotPasswordDto: ForgotPasswordDto, + @Res() res: Response + ): Promise { + 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 res Express响应对象 * @returns 发送结果 */ @ApiOperation({ @@ -265,8 +286,13 @@ export class LoginController { @ApiBody({ type: SendEmailVerificationDto }) @SwaggerApiResponse({ status: 200, - description: '验证码发送成功', - type: ForgotPasswordResponseDto + description: '验证码发送成功(真实发送模式)', + type: SuccessEmailVerificationResponseDto + }) + @SwaggerApiResponse({ + status: 206, + description: '测试模式:验证码已生成但未真实发送', + type: TestModeEmailVerificationResponseDto }) @SwaggerApiResponse({ status: 400, @@ -277,10 +303,21 @@ export class LoginController { description: '发送频率过高' }) @Post('send-email-verification') - @HttpCode(HttpStatus.OK) @UsePipes(new ValidationPipe({ transform: true })) - async sendEmailVerification(@Body() sendEmailVerificationDto: SendEmailVerificationDto): Promise> { - return await this.loginService.sendEmailVerification(sendEmailVerificationDto.email); + async sendEmailVerification( + @Body() sendEmailVerificationDto: SendEmailVerificationDto, + @Res() res: Response + ): Promise { + 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 res Express响应对象 * @returns 发送结果 */ @ApiOperation({ @@ -329,6 +367,11 @@ export class LoginController { description: '验证码重新发送成功', type: ForgotPasswordResponseDto }) + @SwaggerApiResponse({ + status: 206, + description: '测试模式:验证码已生成但未真实发送', + type: ForgotPasswordResponseDto + }) @SwaggerApiResponse({ status: 400, description: '邮箱已验证或用户不存在' @@ -338,10 +381,21 @@ export class LoginController { description: '发送频率过高' }) @Post('resend-email-verification') - @HttpCode(HttpStatus.OK) @UsePipes(new ValidationPipe({ transform: true })) - async resendEmailVerification(@Body() sendEmailVerificationDto: SendEmailVerificationDto): Promise> { - return await this.loginService.resendEmailVerification(sendEmailVerificationDto.email); + async resendEmailVerification( + @Body() sendEmailVerificationDto: SendEmailVerificationDto, + @Res() res: Response + ): Promise { + 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); + } } /** diff --git a/src/business/login/login.service.spec.ts b/src/business/login/login.service.spec.ts index f3ac346..e1495e3 100644 --- a/src/business/login/login.service.spec.ts +++ b/src/business/login/login.service.spec.ts @@ -137,13 +137,31 @@ describe('LoginService', () => { }); describe('sendPasswordResetCode', () => { - it('should return success response with verification code', async () => { - loginCoreService.sendPasswordResetCode.mockResolvedValue('123456'); + it('should return test mode response with verification code', async () => { + 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'); 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(); // 真实模式下不返回验证码 }); }); diff --git a/src/business/login/login.service.ts b/src/business/login/login.service.ts index f17b838..6c23fe4 100644 --- a/src/business/login/login.service.ts +++ b/src/business/login/login.service.ts @@ -11,7 +11,7 @@ * - 调用核心服务完成具体功能 * - 为控制器层提供业务接口 * - * @author moyin + * @author moyin angjustinl * @version 1.0.0 * @since 2025-12-17 */ @@ -199,21 +199,37 @@ export class LoginService { * @param identifier 邮箱或手机号 * @returns 响应结果 */ - async sendPasswordResetCode(identifier: string): Promise> { + async sendPasswordResetCode(identifier: string): Promise> { try { this.logger.log(`发送密码重置验证码: ${identifier}`); // 调用核心服务发送验证码 - const verificationCode = await this.loginCoreService.sendPasswordResetCode(identifier); + const result = await this.loginCoreService.sendPasswordResetCode(identifier); this.logger.log(`密码重置验证码已发送: ${identifier}`); - // 实际应用中不应返回验证码,这里仅用于演示 - return { - success: true, - data: { verification_code: verificationCode }, - message: '验证码已发送,请查收' - }; + // 根据是否为测试模式返回不同的状态和消息 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 { + success: true, + data: { + is_test_mode: false + }, + message: '验证码已发送,请查收' + }; + } } catch (error) { this.logger.error(`发送密码重置验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error)); @@ -293,21 +309,37 @@ export class LoginService { * @param email 邮箱地址 * @returns 响应结果 */ - async sendEmailVerification(email: string): Promise> { + async sendEmailVerification(email: string): Promise> { try { this.logger.log(`发送邮箱验证码: ${email}`); // 调用核心服务发送验证码 - const verificationCode = await this.loginCoreService.sendEmailVerification(email); + const result = await this.loginCoreService.sendEmailVerification(email); this.logger.log(`邮箱验证码已发送: ${email}`); - // 实际应用中不应返回验证码,这里仅用于演示 - return { - success: true, - data: { verification_code: verificationCode }, - message: '验证码已发送,请查收邮件' - }; + // 根据是否为测试模式返回不同的状态和消息 + if (result.isTestMode) { + // 测试模式:验证码生成但未真实发送 + return { + success: false, // 测试模式下不算真正成功 + data: { + verification_code: result.code, + is_test_mode: true + }, + message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。', + error_code: 'TEST_MODE_ONLY' + }; + } else { + // 真实发送模式 + return { + success: true, + data: { + is_test_mode: false + }, + message: '验证码已发送,请查收邮件' + }; + } } catch (error) { this.logger.error(`发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error)); @@ -363,21 +395,37 @@ export class LoginService { * @param email 邮箱地址 * @returns 响应结果 */ - async resendEmailVerification(email: string): Promise> { + async resendEmailVerification(email: string): Promise> { try { this.logger.log(`重新发送邮箱验证码: ${email}`); // 调用核心服务重新发送验证码 - const verificationCode = await this.loginCoreService.resendEmailVerification(email); + const result = await this.loginCoreService.resendEmailVerification(email); this.logger.log(`邮箱验证码已重新发送: ${email}`); - // 实际应用中不应返回验证码,这里仅用于演示 - return { - success: true, - data: { verification_code: verificationCode }, - message: '验证码已重新发送,请查收邮件' - }; + // 根据是否为测试模式返回不同的状态和消息 + if (result.isTestMode) { + // 测试模式:验证码生成但未真实发送 + return { + success: false, // 测试模式下不算真正成功 + data: { + verification_code: result.code, + is_test_mode: true + }, + message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。', + error_code: 'TEST_MODE_ONLY' + }; + } else { + // 真实发送模式 + return { + success: true, + data: { + is_test_mode: false + }, + message: '验证码已重新发送,请查收邮件' + }; + } } catch (error) { this.logger.error(`重新发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error)); diff --git a/src/core/db/users/users.module.ts b/src/core/db/users/users.module.ts index 2987a3c..530446a 100644 --- a/src/core/db/users/users.module.ts +++ b/src/core/db/users/users.module.ts @@ -4,23 +4,61 @@ * 功能描述: * - 整合用户相关的实体、服务和控制器 * - 配置TypeORM实体和Repository + * - 支持数据库和内存存储的动态切换 by angjustinl 2025-12-17 * - 导出用户服务供其他模块使用 * - * @author moyin - * @version 1.0.0 + * 存储模式:by angjustinl 2025-12-17 + * - 数据库模式:使用TypeORM连接MySQL数据库 + * - 内存模式:使用Map存储,适用于开发和测试 + * + * @author moyin angjustinl + * @version 1.0.1 * @since 2025-12-17 */ -import { Module } from '@nestjs/common'; +import { Module, DynamicModule, Global } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Users } from './users.entity'; import { UsersService } from './users.service'; +import { UsersMemoryService } from './users_memory.service'; -@Module({ - imports: [ - TypeOrmModule.forFeature([Users]) - ], - providers: [UsersService], - exports: [UsersService, TypeOrmModule], -}) -export class UsersModule {} \ No newline at end of file +@Global() +@Module({}) +export class UsersModule { + /** + * 创建数据库模式的用户模块 + * + * @returns 配置了TypeORM的动态模块 + */ + static forDatabase(): DynamicModule { + return { + module: UsersModule, + imports: [TypeOrmModule.forFeature([Users])], + providers: [ + { + provide: 'UsersService', + useClass: UsersService, + }, + ], + exports: ['UsersService', TypeOrmModule], + }; + } + + /** + * 创建内存模式的用户模块 + * + * @returns 配置了内存存储的动态模块 + */ + static forMemory(): DynamicModule { + return { + module: UsersModule, + providers: [ + { + provide: 'UsersService', + useClass: UsersMemoryService, + }, + ], + exports: ['UsersService'], + }; + } +} \ No newline at end of file diff --git a/src/core/db/users/users_memory.service.ts b/src/core/db/users/users_memory.service.ts new file mode 100644 index 0000000..cb515b0 --- /dev/null +++ b/src/core/db/users/users_memory.service.ts @@ -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 = new Map(); + private currentId: bigint = BigInt(1); + + /** + * 创建新用户 + * + * @param createUserDto 创建用户的数据传输对象 + * @returns 创建的用户实体 + * @throws ConflictException 当用户名、邮箱或手机号已存在时 + * @throws BadRequestException 当数据验证失败时 + */ + async create(createUserDto: CreateUserDto): Promise { + // 验证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 { + 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 { + const user = this.users.get(id); + + if (!user) { + throw new NotFoundException(`ID为 ${id} 的用户不存在`); + } + + return user; + } + + /** + * 根据用户名查询用户 + * + * @param username 用户名 + * @returns 用户实体或null + */ + async findByUsername(username: string): Promise { + const user = Array.from(this.users.values()).find( + u => u.username === username + ); + return user || null; + } + + /** + * 根据邮箱查询用户 + * + * @param email 邮箱 + * @returns 用户实体或null + */ + async findByEmail(email: string): Promise { + 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 { + 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): Promise { + // 检查用户是否存在 + 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 { + const user = await this.findOne(id); + this.users.delete(id); + return user; + } + + /** + * 统计用户数量 + * + * @param conditions 查询条件(内存模式下简化处理) + * @returns 用户数量 + */ + async count(conditions?: any): Promise { + 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 { + return this.users.has(id); + } + + /** + * 批量创建用户 + * + * @param createUserDtos 用户数据数组 + * @returns 创建的用户列表 + */ + async createBatch(createUserDtos: CreateUserDto[]): Promise { + 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 { + 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 { + 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); + } +} diff --git a/src/core/login_core/login_core.service.spec.ts b/src/core/login_core/login_core.service.spec.ts index 2fb5514..6d7c47a 100644 --- a/src/core/login_core/login_core.service.spec.ts +++ b/src/core/login_core/login_core.service.spec.ts @@ -180,11 +180,15 @@ describe('LoginCoreService', () => { const verifiedUser = { ...mockUser, email_verified: true }; usersService.findByEmail.mockResolvedValue(verifiedUser); 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 () => { diff --git a/src/core/login_core/login_core.service.ts b/src/core/login_core/login_core.service.ts index d288a49..7781599 100644 --- a/src/core/login_core/login_core.service.ts +++ b/src/core/login_core/login_core.service.ts @@ -16,10 +16,9 @@ * @since 2025-12-17 */ -import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; -import { UsersService } from '../db/users/users.service'; +import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException, Inject } from '@nestjs/common'; import { Users } from '../db/users/users.entity'; -import { EmailService } from '../utils/email/email.service'; +import { EmailService, EmailSendResult } from '../utils/email/email.service'; import { VerificationService, VerificationCodeType } from '../utils/verification/verification.service'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; @@ -90,10 +89,20 @@ export interface AuthResult { isNewUser?: boolean; } +/** + * 验证码发送结果接口 by angjustinl 2025-12-17 + */ +export interface VerificationCodeResult { + /** 验证码 */ + code: string; + /** 是否为测试模式 */ + isTestMode: boolean; +} + @Injectable() export class LoginCoreService { constructor( - private readonly usersService: UsersService, + @Inject('UsersService') private readonly usersService: any, private readonly emailService: EmailService, private readonly verificationService: VerificationService, ) {} @@ -122,7 +131,7 @@ export class LoginCoreService { // 如果邮箱未找到,尝试手机号查找(简单验证) if (!user && this.isPhoneNumber(identifier)) { 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 +278,10 @@ export class LoginCoreService { * 发送密码重置验证码 * * @param identifier 邮箱或手机号 - * @returns 验证码(实际应用中应发送到用户邮箱/手机) + * @returns 验证码结果 * @throws NotFoundException 用户不存在时 */ - async sendPasswordResetCode(identifier: string): Promise { + async sendPasswordResetCode(identifier: string): Promise { // 查找用户 let user: Users | null = null; @@ -285,7 +294,7 @@ export class LoginCoreService { } } else if (this.isPhoneNumber(identifier)) { 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) { @@ -299,23 +308,28 @@ export class LoginCoreService { ); // 发送验证码 + let isTestMode = false; + if (this.isEmail(identifier)) { - const success = await this.emailService.sendVerificationCode({ + const result = await this.emailService.sendVerificationCode({ email: identifier, code: verificationCode, nickname: user.nickname, purpose: 'password_reset' }); - if (!success) { + if (!result.success) { throw new BadRequestException('验证码发送失败,请稍后重试'); } + + isTestMode = result.isTestMode; } else { // TODO: 实现短信发送 console.log(`短信验证码(${identifier}): ${verificationCode}`); + isTestMode = true; // 短信也是测试模式 } - return verificationCode; // 实际应用中不应返回验证码 + return { code: verificationCode, isTestMode }; } /** @@ -347,7 +361,7 @@ export class LoginCoreService { user = await this.usersService.findByEmail(identifier); } else if (this.isPhoneNumber(identifier)) { 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) { @@ -457,9 +471,9 @@ export class LoginCoreService { * * @param email 邮箱地址 * @param nickname 用户昵称 - * @returns 验证码 + * @returns 验证码结果 */ - async sendEmailVerification(email: string, nickname?: string): Promise { + async sendEmailVerification(email: string, nickname?: string): Promise { // 生成验证码 const verificationCode = await this.verificationService.generateCode( email, @@ -467,18 +481,18 @@ export class LoginCoreService { ); // 发送验证邮件 - const success = await this.emailService.sendVerificationCode({ + const result = await this.emailService.sendVerificationCode({ email, code: verificationCode, nickname, purpose: 'email_verification' }); - if (!success) { + if (!result.success) { throw new BadRequestException('验证邮件发送失败,请稍后重试'); } - return verificationCode; // 实际应用中不应返回验证码 + return { code: verificationCode, isTestMode: result.isTestMode }; } /** @@ -520,9 +534,9 @@ export class LoginCoreService { * 重新发送邮箱验证码 * * @param email 邮箱地址 - * @returns 验证码 + * @returns 验证码结果 */ - async resendEmailVerification(email: string): Promise { + async resendEmailVerification(email: string): Promise { const user = await this.usersService.findByEmail(email); if (!user) { diff --git a/src/core/utils/email/email.service.ts b/src/core/utils/email/email.service.ts index 2fe85c1..f6f2a4f 100644 --- a/src/core/utils/email/email.service.ts +++ b/src/core/utils/email/email.service.ts @@ -50,6 +50,18 @@ export interface VerificationEmailOptions { purpose: 'email_verification' | 'password_reset'; } +/** + * 邮件发送结果接口 by angjustinl 2025-12-17 + */ +export interface EmailSendResult { + /** 是否成功 */ + success: boolean; + /** 是否为测试模式 */ + isTestMode: boolean; + /** 错误信息(如果失败) */ + error?: string; +} + @Injectable() export class EmailService { 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 邮件选项 * @returns 发送结果 */ - async sendEmail(options: EmailOptions): Promise { + async sendEmail(options: EmailOptions): Promise { try { const mailOptions = { from: this.configService.get('EMAIL_FROM', '"Whale Town Game" '), @@ -103,22 +124,31 @@ export class EmailService { text: options.text, }; - const result = await this.transporter.sendMail(mailOptions); - + const isTestMode = this.isTestMode(); + // 如果是测试模式,输出邮件内容到控制台 - if ((this.transporter.options as any).streamTransport) { - this.logger.log('=== 邮件发送(测试模式) ==='); - this.logger.log(`收件人: ${options.to}`); - this.logger.log(`主题: ${options.subject}`); - this.logger.log(`内容: ${options.text || '请查看HTML内容'}`); - this.logger.log('========================'); + if (isTestMode) { + this.logger.warn('=== 邮件发送(测试模式 - 邮件未真实发送) ==='); + this.logger.warn(`收件人: ${options.to}`); + this.logger.warn(`主题: ${options.subject}`); + this.logger.warn(`内容: ${options.text || '请查看HTML内容'}`); + 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) { - this.logger.error(`邮件发送失败: ${options.to}`, error instanceof Error ? error.stack : String(error)); - return false; + this.logger.error(`❌ 邮件发送失败: ${options.to}`, error instanceof Error ? error.stack : String(error)); + return { + success: false, + isTestMode: this.isTestMode(), + error: error instanceof Error ? error.message : String(error) + }; } } @@ -128,7 +158,7 @@ export class EmailService { * @param options 验证码邮件选项 * @returns 发送结果 */ - async sendVerificationCode(options: VerificationEmailOptions): Promise { + async sendVerificationCode(options: VerificationEmailOptions): Promise { const { email, code, nickname, purpose } = options; let subject: string; @@ -157,7 +187,7 @@ export class EmailService { * @param nickname 用户昵称 * @returns 发送结果 */ - async sendWelcomeEmail(email: string, nickname: string): Promise { + async sendWelcomeEmail(email: string, nickname: string): Promise { const subject = '🎮 欢迎加入 Whale Town!'; const template = this.getWelcomeTemplate(nickname); diff --git a/src/dto/app.dto.ts b/src/dto/app.dto.ts new file mode 100644 index 0000000..498f5c5 --- /dev/null +++ b/src/dto/app.dto.ts @@ -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'; +} \ No newline at end of file diff --git a/src/dto/error_response.dto.ts b/src/dto/error_response.dto.ts new file mode 100644 index 0000000..595fc42 --- /dev/null +++ b/src/dto/error_response.dto.ts @@ -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; +} \ No newline at end of file diff --git a/src/business/login/login.dto.ts b/src/dto/login.dto.ts similarity index 100% rename from src/business/login/login.dto.ts rename to src/dto/login.dto.ts diff --git a/src/business/login/login-response.dto.ts b/src/dto/login_response.dto.ts similarity index 62% rename from src/business/login/login-response.dto.ts rename to src/dto/login_response.dto.ts index d9f2a18..ef853f2 100644 --- a/src/business/login/login-response.dto.ts +++ b/src/dto/login_response.dto.ts @@ -209,6 +209,13 @@ export class ForgotPasswordResponseDataDto { required: false }) verification_code?: string; + + @ApiProperty({ + description: '是否为测试模式', + example: true, + required: false + }) + is_test_mode?: boolean; } /** @@ -217,26 +224,76 @@ export class ForgotPasswordResponseDataDto { export class ForgotPasswordResponseDto { @ApiProperty({ description: '请求是否成功', - example: true + example: false, + examples: { + success: { + summary: '真实发送成功', + value: true + }, + testMode: { + summary: '测试模式', + value: false + } + } }) success: boolean; @ApiProperty({ description: '响应数据', 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; @ApiProperty({ description: '响应消息', - example: '验证码已发送,请查收' + example: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。', + examples: { + success: { + summary: '真实发送成功', + value: '验证码已发送,请查收' + }, + testMode: { + summary: '测试模式', + value: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。' + } + } }) message: string; @ApiProperty({ 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 }) error_code?: string; @@ -264,4 +321,75 @@ export class CommonResponseDto { required: false }) 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; } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 4f1028d..279f830 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,8 +3,42 @@ import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; 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() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule, { + logger: ['error', 'warn', 'log'], + }); // 全局启用校验管道(核心配置) app.useGlobalPipes(