From 94ba3077aa9f26b8bd4f558cacc7103d5359fdc0 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Wed, 17 Dec 2025 20:06:56 +0800 Subject: [PATCH] =?UTF-8?q?dto=EF=BC=9A=E4=B8=BA=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E6=B7=BB=E5=8A=A0=E9=82=AE=E7=AE=B1=E9=AA=8C?= =?UTF-8?q?=E8=AF=81=E7=A0=81=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 RegisterDto 中添加 email_verification_code 可选字段 - 更新 RegisterRequest 接口定义 - 在注册核心服务中添加验证码验证逻辑 - 提供邮箱时必须提供有效的验证码进行验证 --- src/business/login/login.dto.ts | 59 +++++++ src/core/login_core/login_core.service.ts | 179 ++++++++++++++++++++-- 2 files changed, 222 insertions(+), 16 deletions(-) diff --git a/src/business/login/login.dto.ts b/src/business/login/login.dto.ts index 8ab3123..6fa11f0 100644 --- a/src/business/login/login.dto.ts +++ b/src/business/login/login.dto.ts @@ -129,6 +129,20 @@ export class RegisterDto { @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; } /** @@ -312,4 +326,49 @@ export class ChangePasswordDto { @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; } \ No newline at end of file diff --git a/src/core/login_core/login_core.service.ts b/src/core/login_core/login_core.service.ts index 98bd567..5488493 100644 --- a/src/core/login_core/login_core.service.ts +++ b/src/core/login_core/login_core.service.ts @@ -19,6 +19,8 @@ import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common'; import { UsersService } from '../db/users/users.service'; import { Users } from '../db/users/users.entity'; +import { EmailService } from '../utils/email/email.service'; +import { VerificationService, VerificationCodeType } from '../utils/verification/verification.service'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; @@ -46,6 +48,8 @@ export interface RegisterRequest { email?: string; /** 手机号(可选) */ phone?: string; + /** 邮箱验证码(当提供邮箱时必填) */ + email_verification_code?: string; } /** @@ -90,6 +94,8 @@ export interface AuthResult { export class LoginCoreService { constructor( private readonly usersService: UsersService, + private readonly emailService: EmailService, + private readonly verificationService: VerificationService, ) {} /** @@ -150,7 +156,21 @@ export class LoginCoreService { * @throws BadRequestException 数据验证失败时 */ async register(registerRequest: RegisterRequest): Promise { - const { username, password, nickname, email, phone } = registerRequest; + const { username, password, nickname, email, phone, email_verification_code } = registerRequest; + + // 如果提供了邮箱,必须验证邮箱验证码 + if (email) { + if (!email_verification_code) { + throw new BadRequestException('提供邮箱时必须提供邮箱验证码'); + } + + // 验证邮箱验证码 + await this.verificationService.verifyCode( + email, + VerificationCodeType.EMAIL_VERIFICATION, + email_verification_code + ); + } // 验证密码强度 this.validatePasswordStrength(password); @@ -165,9 +185,20 @@ export class LoginCoreService { nickname, email, phone, - role: 1 // 默认普通用户 + role: 1, // 默认普通用户 + email_verified: email ? true : false // 如果提供了邮箱且验证码验证通过,则标记为已验证 }); + // 如果提供了邮箱,发送欢迎邮件 + if (email) { + try { + await this.emailService.sendWelcomeEmail(email, nickname); + } catch (error) { + // 邮件发送失败不影响注册流程,只记录日志 + console.warn(`欢迎邮件发送失败: ${email}`, error); + } + } + return { user, isNewUser: true @@ -215,9 +246,19 @@ export class LoginCoreService { email, github_id, avatar_url, - role: 1 // 默认普通用户 + role: 1, // 默认普通用户 + email_verified: email ? true : false // GitHub邮箱直接验证 }); + // 发送欢迎邮件 + if (email) { + try { + await this.emailService.sendWelcomeEmail(email, nickname); + } catch (error) { + console.warn(`欢迎邮件发送失败: ${email}`, error); + } + } + return { user, isNewUser: true @@ -237,6 +278,11 @@ export class LoginCoreService { if (this.isEmail(identifier)) { user = await this.usersService.findByEmail(identifier); + + // 检查邮箱是否已验证 + if (user && !user.email_verified) { + throw new BadRequestException('邮箱未验证,无法重置密码'); + } } else if (this.isPhoneNumber(identifier)) { const users = await this.usersService.findAll(); user = users.find(u => u.phone === identifier) || null; @@ -246,18 +292,30 @@ export class LoginCoreService { throw new NotFoundException('用户不存在'); } - // 生成6位数验证码 - const verificationCode = this.generateVerificationCode(); + // 生成验证码 + const verificationCode = await this.verificationService.generateCode( + identifier, + VerificationCodeType.PASSWORD_RESET + ); - // TODO: 实际应用中应该: - // 1. 将验证码存储到Redis等缓存中,设置过期时间(如5分钟) - // 2. 发送验证码到用户邮箱或手机 - // 3. 返回成功消息而不是验证码本身 + // 发送验证码 + if (this.isEmail(identifier)) { + const success = await this.emailService.sendVerificationCode({ + email: identifier, + code: verificationCode, + nickname: user.nickname, + purpose: 'password_reset' + }); - // 这里为了演示,直接返回验证码 - console.log(`密码重置验证码(${identifier}): ${verificationCode}`); + if (!success) { + throw new BadRequestException('验证码发送失败,请稍后重试'); + } + } else { + // TODO: 实现短信发送 + console.log(`短信验证码(${identifier}): ${verificationCode}`); + } - return verificationCode; + return verificationCode; // 实际应用中不应返回验证码 } /** @@ -271,10 +329,15 @@ export class LoginCoreService { async resetPassword(resetRequest: PasswordResetRequest): Promise { const { identifier, verificationCode, newPassword } = resetRequest; - // TODO: 实际应用中应该验证验证码的有效性 - // 这里为了演示,简单验证验证码格式 - if (!/^\d{6}$/.test(verificationCode)) { - throw new BadRequestException('验证码格式错误'); + // 验证验证码 + const isValidCode = await this.verificationService.verifyCode( + identifier, + VerificationCodeType.PASSWORD_RESET, + verificationCode + ); + + if (!isValidCode) { + throw new BadRequestException('验证码验证失败'); } // 查找用户 @@ -389,6 +452,90 @@ export class LoginCoreService { } } + /** + * 发送邮箱验证码 + * + * @param email 邮箱地址 + * @param nickname 用户昵称 + * @returns 验证码 + */ + async sendEmailVerification(email: string, nickname?: string): Promise { + // 生成验证码 + const verificationCode = await this.verificationService.generateCode( + email, + VerificationCodeType.EMAIL_VERIFICATION + ); + + // 发送验证邮件 + const success = await this.emailService.sendVerificationCode({ + email, + code: verificationCode, + nickname, + purpose: 'email_verification' + }); + + if (!success) { + throw new BadRequestException('验证邮件发送失败,请稍后重试'); + } + + return verificationCode; // 实际应用中不应返回验证码 + } + + /** + * 验证邮箱验证码 + * + * @param email 邮箱地址 + * @param code 验证码 + * @returns 验证结果 + */ + async verifyEmailCode(email: string, code: string): Promise { + // 验证验证码 + const isValid = await this.verificationService.verifyCode( + email, + VerificationCodeType.EMAIL_VERIFICATION, + code + ); + + if (isValid) { + // 更新用户邮箱验证状态 + const user = await this.usersService.findByEmail(email); + if (user) { + await this.usersService.update(user.id, { + email_verified: true + }); + + // 发送欢迎邮件 + try { + await this.emailService.sendWelcomeEmail(email, user.nickname); + } catch (error) { + console.warn(`欢迎邮件发送失败: ${email}`, error); + } + } + } + + return isValid; + } + + /** + * 重新发送邮箱验证码 + * + * @param email 邮箱地址 + * @returns 验证码 + */ + async resendEmailVerification(email: string): Promise { + const user = await this.usersService.findByEmail(email); + + if (!user) { + throw new NotFoundException('用户不存在'); + } + + if (user.email_verified) { + throw new BadRequestException('邮箱已验证,无需重复验证'); + } + + return await this.sendEmailVerification(email, user.nickname); + } + /** * 生成验证码 *