feat(sql, auth, email, dto):重构邮箱验证流程,引入基于内存的用户服务,并改进 API 响应处理

* 新增完整的 API 状态码文档,并对测试模式进行特殊处理(`206 Partial Content`)
* 重组 DTO 结构,引入 `app.dto.ts` 与 `error_response.dto.ts`,以实现统一、规范的响应格式
* 重构登录相关 DTO,优化命名与结构,提升可维护性
* 实现基于内存的用户服务(`users_memory.service.ts`),用于开发与测试环境
* 更新邮件服务,增强验证码生成逻辑,并支持测试模式自动识别
* 增强登录控制器与服务层的错误处理能力,统一响应行为
* 优化核心登录服务,强化参数校验并集成邮箱验证流程
* 新增 `@types/express` 依赖,提升 TypeScript 类型支持与开发体验
* 改进 `main.ts`,优化应用初始化流程与配置管理
* 在所有服务中统一错误处理机制,采用标准化的错误响应格式
* 实现测试模式(`206`)与生产环境邮件发送(`200`)之间的无缝切换
This commit is contained in:
angjustinl
2025-12-18 00:17:43 +08:00
parent 2a3698b26a
commit 26ea5ac815
19 changed files with 1362 additions and 111 deletions

View File

@@ -1,267 +0,0 @@
/**
* 登录业务响应数据传输对象
*
* 功能描述:
* - 定义登录相关API的响应数据结构
* - 提供Swagger文档生成支持
* - 确保API响应的数据格式一致性
*
* @author moyin
* @version 1.0.0
* @since 2025-12-17
*/
import { ApiProperty } from '@nestjs/swagger';
/**
* 用户信息响应DTO
*/
export class UserInfoDto {
@ApiProperty({
description: '用户ID',
example: '1'
})
id: string;
@ApiProperty({
description: '用户名',
example: 'testuser'
})
username: string;
@ApiProperty({
description: '用户昵称',
example: '测试用户'
})
nickname: string;
@ApiProperty({
description: '邮箱地址',
example: 'test@example.com',
required: false
})
email?: string;
@ApiProperty({
description: '手机号码',
example: '+8613800138000',
required: false
})
phone?: string;
@ApiProperty({
description: '头像URL',
example: 'https://example.com/avatar.jpg',
required: false
})
avatar_url?: string;
@ApiProperty({
description: '用户角色',
example: 1
})
role: number;
@ApiProperty({
description: '创建时间',
example: '2025-12-17T10:00:00.000Z'
})
created_at: Date;
}
/**
* 登录响应数据DTO
*/
export class LoginResponseDataDto {
@ApiProperty({
description: '用户信息',
type: UserInfoDto
})
user: UserInfoDto;
@ApiProperty({
description: '访问令牌',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
})
access_token: string;
@ApiProperty({
description: '刷新令牌',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
required: false
})
refresh_token?: string;
@ApiProperty({
description: '是否为新用户',
example: false,
required: false
})
is_new_user?: boolean;
@ApiProperty({
description: '响应消息',
example: '登录成功'
})
message: string;
}
/**
* 登录响应DTO
*/
export class LoginResponseDto {
@ApiProperty({
description: '请求是否成功',
example: true
})
success: boolean;
@ApiProperty({
description: '响应数据',
type: LoginResponseDataDto,
required: false
})
data?: LoginResponseDataDto;
@ApiProperty({
description: '响应消息',
example: '登录成功'
})
message: string;
@ApiProperty({
description: '错误代码',
example: 'LOGIN_FAILED',
required: false
})
error_code?: string;
}
/**
* 注册响应DTO
*/
export class RegisterResponseDto {
@ApiProperty({
description: '请求是否成功',
example: true
})
success: boolean;
@ApiProperty({
description: '响应数据',
type: LoginResponseDataDto,
required: false
})
data?: LoginResponseDataDto;
@ApiProperty({
description: '响应消息',
example: '注册成功'
})
message: string;
@ApiProperty({
description: '错误代码',
example: 'REGISTER_FAILED',
required: false
})
error_code?: string;
}
/**
* GitHub OAuth响应DTO
*/
export class GitHubOAuthResponseDto {
@ApiProperty({
description: '请求是否成功',
example: true
})
success: boolean;
@ApiProperty({
description: '响应数据',
type: LoginResponseDataDto,
required: false
})
data?: LoginResponseDataDto;
@ApiProperty({
description: '响应消息',
example: 'GitHub登录成功'
})
message: string;
@ApiProperty({
description: '错误代码',
example: 'GITHUB_OAUTH_FAILED',
required: false
})
error_code?: string;
}
/**
* 忘记密码响应数据DTO
*/
export class ForgotPasswordResponseDataDto {
@ApiProperty({
description: '验证码(仅用于演示,实际应用中不应返回)',
example: '123456',
required: false
})
verification_code?: string;
}
/**
* 忘记密码响应DTO
*/
export class ForgotPasswordResponseDto {
@ApiProperty({
description: '请求是否成功',
example: true
})
success: boolean;
@ApiProperty({
description: '响应数据',
type: ForgotPasswordResponseDataDto,
required: false
})
data?: ForgotPasswordResponseDataDto;
@ApiProperty({
description: '响应消息',
example: '验证码已发送,请查收'
})
message: string;
@ApiProperty({
description: '错误代码',
example: 'SEND_CODE_FAILED',
required: false
})
error_code?: string;
}
/**
* 通用响应DTO用于重置密码、修改密码等
*/
export class CommonResponseDto {
@ApiProperty({
description: '请求是否成功',
example: true
})
success: boolean;
@ApiProperty({
description: '响应消息',
example: '操作成功'
})
message: string;
@ApiProperty({
description: '错误代码',
example: 'OPERATION_FAILED',
required: false
})
error_code?: string;
}

View File

@@ -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<ApiResponse<{ verification_code?: string }>> {
return await this.loginService.sendPasswordResetCode(forgotPasswordDto.identifier);
async forgotPassword(
@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 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<ApiResponse<{ verification_code?: string }>> {
return await this.loginService.sendEmailVerification(sendEmailVerificationDto.email);
async sendEmailVerification(
@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 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<ApiResponse<{ verification_code?: string }>> {
return await this.loginService.resendEmailVerification(sendEmailVerificationDto.email);
async resendEmailVerification(
@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);
}
}
/**

View File

@@ -1,374 +0,0 @@
/**
* 登录业务数据传输对象
*
* 功能描述:
* - 定义登录相关API的请求数据结构
* - 提供数据验证规则和错误提示
* - 确保API接口的数据格式一致性
*
* @author moyin
* @version 1.0.0
* @since 2025-12-17
*/
import {
IsString,
IsEmail,
IsPhoneNumber,
IsNotEmpty,
Length,
IsOptional,
Matches,
IsNumberString
} from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
/**
* 登录请求DTO
*/
export class LoginDto {
/**
* 登录标识符
* 支持用户名、邮箱或手机号登录
*/
@ApiProperty({
description: '登录标识符,支持用户名、邮箱或手机号',
example: 'testuser',
minLength: 1,
maxLength: 100
})
@IsString({ message: '登录标识符必须是字符串' })
@IsNotEmpty({ message: '登录标识符不能为空' })
@Length(1, 100, { message: '登录标识符长度需在1-100字符之间' })
identifier: string;
/**
* 密码
*/
@ApiProperty({
description: '用户密码',
example: 'password123',
minLength: 1,
maxLength: 128
})
@IsString({ message: '密码必须是字符串' })
@IsNotEmpty({ message: '密码不能为空' })
@Length(1, 128, { message: '密码长度需在1-128字符之间' })
password: string;
}
/**
* 注册请求DTO
*/
export class RegisterDto {
/**
* 用户名
*/
@ApiProperty({
description: '用户名,只能包含字母、数字和下划线',
example: 'testuser',
minLength: 1,
maxLength: 50,
pattern: '^[a-zA-Z0-9_]+$'
})
@IsString({ message: '用户名必须是字符串' })
@IsNotEmpty({ message: '用户名不能为空' })
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
@Matches(/^[a-zA-Z0-9_]+$/, { message: '用户名只能包含字母、数字和下划线' })
username: string;
/**
* 密码
*/
@ApiProperty({
description: '密码必须包含字母和数字长度8-128字符',
example: 'password123',
minLength: 8,
maxLength: 128
})
@IsString({ message: '密码必须是字符串' })
@IsNotEmpty({ message: '密码不能为空' })
@Length(8, 128, { message: '密码长度需在8-128字符之间' })
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '密码必须包含字母和数字' })
password: string;
/**
* 昵称
*/
@ApiProperty({
description: '用户昵称',
example: '测试用户',
minLength: 1,
maxLength: 50
})
@IsString({ message: '昵称必须是字符串' })
@IsNotEmpty({ message: '昵称不能为空' })
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
nickname: string;
/**
* 邮箱(可选)
*/
@ApiProperty({
description: '邮箱地址(可选)',
example: 'test@example.com',
required: false
})
@IsOptional()
@IsEmail({}, { message: '邮箱格式不正确' })
email?: string;
/**
* 手机号(可选)
*/
@ApiProperty({
description: '手机号码(可选)',
example: '+8613800138000',
required: false
})
@IsOptional()
@IsPhoneNumber(null, { message: '手机号格式不正确' })
phone?: string;
/**
* 邮箱验证码(当提供邮箱时必填)
*/
@ApiProperty({
description: '邮箱验证码,当提供邮箱时必填',
example: '123456',
pattern: '^\\d{6}$',
required: false
})
@IsOptional()
@IsString({ message: '验证码必须是字符串' })
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
email_verification_code?: string;
}
/**
* GitHub OAuth登录请求DTO
*/
export class GitHubOAuthDto {
/**
* GitHub用户ID
*/
@ApiProperty({
description: 'GitHub用户ID',
example: '12345678',
minLength: 1,
maxLength: 100
})
@IsString({ message: 'GitHub ID必须是字符串' })
@IsNotEmpty({ message: 'GitHub ID不能为空' })
@Length(1, 100, { message: 'GitHub ID长度需在1-100字符之间' })
github_id: string;
/**
* 用户名
*/
@ApiProperty({
description: 'GitHub用户名',
example: 'octocat',
minLength: 1,
maxLength: 50
})
@IsString({ message: '用户名必须是字符串' })
@IsNotEmpty({ message: '用户名不能为空' })
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
username: string;
/**
* 昵称
*/
@ApiProperty({
description: 'GitHub显示名称',
example: 'The Octocat',
minLength: 1,
maxLength: 50
})
@IsString({ message: '昵称必须是字符串' })
@IsNotEmpty({ message: '昵称不能为空' })
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
nickname: string;
/**
* 邮箱(可选)
*/
@ApiProperty({
description: 'GitHub邮箱地址可选',
example: 'octocat@github.com',
required: false
})
@IsOptional()
@IsEmail({}, { message: '邮箱格式不正确' })
email?: string;
/**
* 头像URL可选
*/
@ApiProperty({
description: 'GitHub头像URL可选',
example: 'https://github.com/images/error/octocat_happy.gif',
required: false
})
@IsOptional()
@IsString({ message: '头像URL必须是字符串' })
avatar_url?: string;
}
/**
* 忘记密码请求DTO
*/
export class ForgotPasswordDto {
/**
* 邮箱或手机号
*/
@ApiProperty({
description: '邮箱或手机号',
example: 'test@example.com',
minLength: 1,
maxLength: 100
})
@IsString({ message: '标识符必须是字符串' })
@IsNotEmpty({ message: '邮箱或手机号不能为空' })
@Length(1, 100, { message: '标识符长度需在1-100字符之间' })
identifier: string;
}
/**
* 重置密码请求DTO
*/
export class ResetPasswordDto {
/**
* 邮箱或手机号
*/
@ApiProperty({
description: '邮箱或手机号',
example: 'test@example.com',
minLength: 1,
maxLength: 100
})
@IsString({ message: '标识符必须是字符串' })
@IsNotEmpty({ message: '邮箱或手机号不能为空' })
@Length(1, 100, { message: '标识符长度需在1-100字符之间' })
identifier: string;
/**
* 验证码
*/
@ApiProperty({
description: '6位数字验证码',
example: '123456',
pattern: '^\\d{6}$'
})
@IsString({ message: '验证码必须是字符串' })
@IsNotEmpty({ message: '验证码不能为空' })
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
verification_code: string;
/**
* 新密码
*/
@ApiProperty({
description: '新密码必须包含字母和数字长度8-128字符',
example: 'newpassword123',
minLength: 8,
maxLength: 128
})
@IsString({ message: '新密码必须是字符串' })
@IsNotEmpty({ message: '新密码不能为空' })
@Length(8, 128, { message: '新密码长度需在8-128字符之间' })
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
new_password: string;
}
/**
* 修改密码请求DTO
*/
export class ChangePasswordDto {
/**
* 用户ID
* 实际应用中应从JWT令牌中获取这里为了演示放在请求体中
*/
@ApiProperty({
description: '用户ID实际应用中应从JWT令牌中获取',
example: '1'
})
@IsNumberString({}, { message: '用户ID必须是数字字符串' })
@IsNotEmpty({ message: '用户ID不能为空' })
user_id: string;
/**
* 旧密码
*/
@ApiProperty({
description: '当前密码',
example: 'oldpassword123',
minLength: 1,
maxLength: 128
})
@IsString({ message: '旧密码必须是字符串' })
@IsNotEmpty({ message: '旧密码不能为空' })
@Length(1, 128, { message: '旧密码长度需在1-128字符之间' })
old_password: string;
/**
* 新密码
*/
@ApiProperty({
description: '新密码必须包含字母和数字长度8-128字符',
example: 'newpassword123',
minLength: 8,
maxLength: 128
})
@IsString({ message: '新密码必须是字符串' })
@IsNotEmpty({ message: '新密码不能为空' })
@Length(8, 128, { message: '新密码长度需在8-128字符之间' })
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
new_password: string;
}
/**
* 邮箱验证请求DTO
*/
export class EmailVerificationDto {
/**
* 邮箱地址
*/
@ApiProperty({
description: '邮箱地址',
example: 'test@example.com'
})
@IsEmail({}, { message: '邮箱格式不正确' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;
/**
* 验证码
*/
@ApiProperty({
description: '6位数字验证码',
example: '123456',
pattern: '^\\d{6}$'
})
@IsString({ message: '验证码必须是字符串' })
@IsNotEmpty({ message: '验证码不能为空' })
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
verification_code: string;
}
/**
* 发送邮箱验证码请求DTO
*/
export class SendEmailVerificationDto {
/**
* 邮箱地址
*/
@ApiProperty({
description: '邮箱地址',
example: 'test@example.com'
})
@IsEmail({}, { message: '邮箱格式不正确' })
@IsNotEmpty({ message: '邮箱不能为空' })
email: string;
}

View File

@@ -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(); // 真实模式下不返回验证码
});
});

View File

@@ -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<ApiResponse<{ verification_code?: string }>> {
async sendPasswordResetCode(identifier: string): Promise<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
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<ApiResponse<{ verification_code?: string }>> {
async sendEmailVerification(email: string): Promise<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
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<ApiResponse<{ verification_code?: string }>> {
async resendEmailVerification(email: string): Promise<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
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));