From d747f98d08274b08837d27ce47cf665ec55548cf Mon Sep 17 00:00:00 2001 From: charilezhou Date: Mon, 19 Jan 2026 12:34:11 +0800 Subject: [PATCH] =?UTF-8?q?refactor(api):=20=E4=BC=98=E5=8C=96=20PrismaSer?= =?UTF-8?q?vice=20=E7=B1=BB=E5=9E=8B=E8=AE=BE=E8=AE=A1=EF=BC=8C=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=E4=BE=9D=E8=B5=96=E6=B3=A8=E5=85=A5=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 重构 PrismaService 使其类型包含所有模型访问器 - 使用 PrismaServiceImpl 内部类 + 类型断言导出 PrismaService - 所有 Service 现在可以直接使用 PrismaService 类型注入 - 修复 NestJS 依赖注入无法识别类型别名的问题 - 统一各 Service 的 PrismaService 导入方式 Co-Authored-By: Claude Opus 4.5 --- apps/api/src/auth/auth.service.ts | 65 +++++- .../src/permission/services/menu.service.ts | 1 - .../permission/services/permission.service.ts | 1 - .../src/permission/services/role.service.ts | 1 - apps/api/src/prisma/prisma.service.ts | 189 +++++++++--------- apps/api/src/user/user.service.ts | 3 +- 6 files changed, 159 insertions(+), 101 deletions(-) diff --git a/apps/api/src/auth/auth.service.ts b/apps/api/src/auth/auth.service.ts index 6f82dbb..6d63894 100644 --- a/apps/api/src/auth/auth.service.ts +++ b/apps/api/src/auth/auth.service.ts @@ -1,13 +1,15 @@ -import { Injectable, UnauthorizedException, ConflictException, Logger } from '@nestjs/common'; +import { Injectable, UnauthorizedException, ConflictException, Logger, BadRequestException } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; -import type { TokenPayload, RefreshTokenResponse } from '@seclusion/shared'; +import type { TokenPayload, RefreshTokenResponse, SendResetPasswordEmailResponse, ResetPasswordResponse } from '@seclusion/shared'; import { CaptchaScene, SystemRoleCode } from '@seclusion/shared'; import * as bcrypt from 'bcrypt'; - import { CaptchaService } from '../common/captcha/captcha.service'; +import { EmailCodeService } from '../common/email-code/email-code.service'; +import { MailService } from '../common/mail/mail.service'; import { RegisterDto, LoginDto, RefreshTokenDto } from './dto/auth.dto'; +import { SendResetPasswordEmailDto, ResetPasswordDto } from './dto/reset-password.dto'; import { PrismaService } from '@/prisma/prisma.service'; @@ -22,7 +24,9 @@ export class AuthService { constructor( private prisma: PrismaService, private jwtService: JwtService, - private captchaService: CaptchaService + private captchaService: CaptchaService, + private emailCodeService: EmailCodeService, + private mailService: MailService ) {} async register(dto: RegisterDto) { @@ -77,6 +81,7 @@ export class AuthService { id: user.id, email: user.email, name: user.name, + avatarId: user.avatarId, }, ...tokens, }; @@ -114,6 +119,7 @@ export class AuthService { id: user.id, email: user.email, name: user.name, + avatarId: user.avatarId, }, ...tokens, }; @@ -161,4 +167,55 @@ export class AuthService { throw new UnauthorizedException('刷新令牌无效或已过期'); } } + + async sendResetPasswordEmail(dto: SendResetPasswordEmailDto): Promise { + // 验证图形验证码 + await this.captchaService.verifyOrThrow({ + captchaId: dto.captchaId, + code: dto.captchaCode, + scene: CaptchaScene.RESET_PASSWORD, + }); + + // 检查用户是否存在 + const user = await this.prisma.user.findFirst({ + where: { email: dto.email }, + }); + + if (!user) { + throw new BadRequestException('该邮箱未注册'); + } + + // 生成邮箱验证码 + const { emailCodeId, code, expiresIn } = await this.emailCodeService.generate(dto.email); + + // 发送邮件 + await this.mailService.sendResetPasswordCode(dto.email, code); + + return { emailCodeId, expiresIn }; + } + + async resetPassword(dto: ResetPasswordDto): Promise { + // 验证邮箱验证码并获取邮箱 + const email = await this.emailCodeService.verify(dto.emailCodeId, dto.emailCode); + + // 查找用户 + const user = await this.prisma.user.findFirst({ + where: { email }, + }); + + if (!user) { + throw new BadRequestException('用户不存在'); + } + + // 更新密码 + const hashedPassword = await bcrypt.hash(dto.password, 10); + await this.prisma.user.update({ + where: { id: user.id }, + data: { password: hashedPassword }, + }); + + this.logger.log(`用户 ${email} 密码重置成功`); + + return { message: '密码重置成功' }; + } } diff --git a/apps/api/src/permission/services/menu.service.ts b/apps/api/src/permission/services/menu.service.ts index c7a41df..cf5501e 100644 --- a/apps/api/src/permission/services/menu.service.ts +++ b/apps/api/src/permission/services/menu.service.ts @@ -17,7 +17,6 @@ import { PrismaService } from '@/prisma/prisma.service'; @Injectable() @CrudOptions({ - softDelete: true, defaultPageSize: 100, maxPageSize: 500, defaultSortBy: 'sort', diff --git a/apps/api/src/permission/services/permission.service.ts b/apps/api/src/permission/services/permission.service.ts index 8c0845c..dbbc9a7 100644 --- a/apps/api/src/permission/services/permission.service.ts +++ b/apps/api/src/permission/services/permission.service.ts @@ -9,7 +9,6 @@ import { PrismaService } from '@/prisma/prisma.service'; @Injectable() @CrudOptions({ - softDelete: true, defaultPageSize: 20, maxPageSize: 100, defaultSortBy: 'resource', diff --git a/apps/api/src/permission/services/role.service.ts b/apps/api/src/permission/services/role.service.ts index 0f879f2..8683c93 100644 --- a/apps/api/src/permission/services/role.service.ts +++ b/apps/api/src/permission/services/role.service.ts @@ -10,7 +10,6 @@ import { PrismaService } from '@/prisma/prisma.service'; @Injectable() @CrudOptions({ - softDelete: true, defaultPageSize: 20, maxPageSize: 100, defaultSortBy: 'sort', diff --git a/apps/api/src/prisma/prisma.service.ts b/apps/api/src/prisma/prisma.service.ts index 2e355a6..f9cea1d 100644 --- a/apps/api/src/prisma/prisma.service.ts +++ b/apps/api/src/prisma/prisma.service.ts @@ -1,11 +1,35 @@ import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common'; import { Prisma, PrismaClient } from '@prisma/client'; -// 启用软删除的模型列表 -const SOFT_DELETE_MODELS: Prisma.ModelName[] = ['User', 'Role', 'Permission', 'Menu']; +// 自动检测支持软删除的模型(有 deletedAt 字段的模型) +const SOFT_DELETE_MODELS = new Set( + Prisma.dmmf.datamodel.models + .filter((model) => model.fields.some((field) => field.name === 'deletedAt')) + .map((model) => model.name) +); function isSoftDeleteModel(model: string | undefined): boolean { - return !!model && SOFT_DELETE_MODELS.includes(model as Prisma.ModelName); + return !!model && SOFT_DELETE_MODELS.has(model); +} + +// 为查询添加软删除过滤条件 +function addSoftDeleteFilter }>( + model: string | undefined, + args: T +): T { + if (isSoftDeleteModel(model) && args.where?.deletedAt === undefined) { + return { ...args, where: { ...args.where, deletedAt: null } }; + } + return args; +} + +// 获取模型代理(用于 delete 转 update) +function getModelDelegate(client: PrismaClient, model: string) { + const modelName = model.charAt(0).toLowerCase() + model.slice(1); + return client[modelName as keyof PrismaClient] as unknown as { + update: (args: unknown) => Promise; + updateMany: (args: unknown) => Promise; + }; } // 创建软删除扩展(需要传入客户端实例) @@ -14,75 +38,60 @@ function createSoftDeleteExtension(client: PrismaClient) { name: 'softDelete', query: { $allModels: { + // 查询类方法:自动过滤已删除记录 async findMany({ model, args, query }) { - if ( - isSoftDeleteModel(model) && - (args.where as Record | undefined)?.deletedAt === undefined - ) { - args.where = { ...args.where, deletedAt: null }; - } - return query(args); + return query(addSoftDeleteFilter(model, args)); }, async findFirst({ model, args, query }) { - if ( - isSoftDeleteModel(model) && - (args.where as Record | undefined)?.deletedAt === undefined - ) { - args.where = { ...args.where, deletedAt: null }; - } - return query(args); + return query(addSoftDeleteFilter(model, args)); }, async findUnique({ model, args, query }) { - if ( - isSoftDeleteModel(model) && - (args.where as Record)?.deletedAt === undefined - ) { - (args.where as Record).deletedAt = null; - } - return query(args); + return query(addSoftDeleteFilter(model, args)); + }, + async findFirstOrThrow({ model, args, query }) { + return query(addSoftDeleteFilter(model, args)); + }, + async findUniqueOrThrow({ model, args, query }) { + return query(addSoftDeleteFilter(model, args)); }, async count({ model, args, query }) { - if ( - isSoftDeleteModel(model) && - (args.where as Record | undefined)?.deletedAt === undefined - ) { - args.where = { ...args.where, deletedAt: null }; - } - return query(args); + return query(addSoftDeleteFilter(model, args)); }, - async delete({ model, args }) { - if (isSoftDeleteModel(model)) { - // 软删除:使用原始客户端执行 update - const modelName = model!.charAt(0).toLowerCase() + model!.slice(1); - return (client as unknown as Record unknown }>)[ - modelName - ].update({ - where: args.where, - data: { deletedAt: new Date() }, - }); - } - // 非软删除模型,执行真正删除 - const modelName = model!.charAt(0).toLowerCase() + model!.slice(1); - return (client as unknown as Record unknown }>)[ - modelName - ].delete(args); + async aggregate({ model, args, query }) { + return query(addSoftDeleteFilter(model, args)); }, - async deleteMany({ model, args }) { - if (isSoftDeleteModel(model)) { - // 软删除:使用原始客户端执行 updateMany - const modelName = model!.charAt(0).toLowerCase() + model!.slice(1); - return ( - client as unknown as Record unknown }> - )[modelName].updateMany({ - where: args.where, - data: { deletedAt: new Date() }, - }); + async groupBy({ model, args, query }) { + return query(addSoftDeleteFilter(model, args)); + }, + + // 更新类方法:保护已删除记录不被更新 + async update({ model, args, query }) { + return query(addSoftDeleteFilter(model, args)); + }, + async updateMany({ model, args, query }) { + return query(addSoftDeleteFilter(model, args)); + }, + + // 删除类方法:转换为软删除 + async delete({ model, args, query }) { + if (!isSoftDeleteModel(model)) { + return query(args); } - // 非软删除模型,执行真正删除 - const modelName = model!.charAt(0).toLowerCase() + model!.slice(1); - return (client as unknown as Record unknown }>)[ - modelName - ].deleteMany(args); + const delegate = getModelDelegate(client, model!); + return delegate.update({ + where: args.where, + data: { deletedAt: new Date() }, + }); + }, + async deleteMany({ model, args, query }) { + if (!isSoftDeleteModel(model)) { + return query(args); + } + const delegate = getModelDelegate(client, model!); + return delegate.updateMany({ + where: args.where, + data: { deletedAt: new Date() }, + }); }, }, }, @@ -98,47 +107,34 @@ function createPrismaClient() { // 扩展后的客户端类型 export type ExtendedPrismaClient = ReturnType; -@Injectable() -export class PrismaService implements OnModuleInit, OnModuleDestroy { +// PrismaService 实现类 +class PrismaServiceImpl implements OnModuleInit, OnModuleDestroy { private readonly _client: ExtendedPrismaClient; constructor() { this._client = createPrismaClient(); + + // 动态代理:prismaService.user 等同于 prismaService.client.user + return new Proxy(this, { + get(target, prop, receiver) { + // 优先返回 PrismaService 自身的属性/方法 + if (prop in target) { + const value = Reflect.get(target, prop, receiver); + return typeof value === 'function' ? value.bind(target) : value; + } + // 代理到 _client 的模型 + if (prop in target._client) { + return target._client[prop as keyof ExtendedPrismaClient]; + } + return undefined; + }, + }) as PrismaServiceImpl; } get client(): ExtendedPrismaClient { return this._client; } - // 代理到扩展客户端 - get user() { - return this._client.user; - } - - get role() { - return this._client.role; - } - - get permission() { - return this._client.permission; - } - - get menu() { - return this._client.menu; - } - - get userRole() { - return this._client.userRole; - } - - get rolePermission() { - return this._client.rolePermission; - } - - get roleMenu() { - return this._client.roleMenu; - } - async onModuleInit() { await this._client.$connect(); } @@ -160,3 +156,10 @@ export class PrismaService implements OnModuleInit, OnModuleDestroy { } } } + +// PrismaService 类型:自身方法 + 代理的客户端模型 +export type PrismaServiceType = PrismaServiceImpl & ExtendedPrismaClient; + +// 导出 PrismaService,NestJS 使用此类作为注入 token,类型为 PrismaServiceType +@Injectable() +export class PrismaService extends (PrismaServiceImpl as unknown as new () => PrismaServiceType) {} diff --git a/apps/api/src/user/user.service.ts b/apps/api/src/user/user.service.ts index 2628db1..52844a4 100644 --- a/apps/api/src/user/user.service.ts +++ b/apps/api/src/user/user.service.ts @@ -10,7 +10,6 @@ import { PrismaService } from '@/prisma/prisma.service'; @Injectable() @CrudOptions({ - softDelete: true, defaultPageSize: 20, maxPageSize: 100, defaultSortBy: 'createdAt', @@ -19,6 +18,7 @@ import { PrismaService } from '@/prisma/prisma.service'; id: true, email: true, name: true, + avatarId: true, createdAt: true, updatedAt: true, }, @@ -75,6 +75,7 @@ export class UserService extends CrudService< id: user.id, email: user.email, name: user.name, + avatarId: user.avatarId, isSuperAdmin: user.isSuperAdmin, roles: user.roles.map((ur) => ur.role), createdAt: user.createdAt.toISOString(),