refactor(api): 优化 PrismaService 类型设计,修复依赖注入问题

- 重构 PrismaService 使其类型包含所有模型访问器
- 使用 PrismaServiceImpl 内部类 + 类型断言导出 PrismaService
- 所有 Service 现在可以直接使用 PrismaService 类型注入
- 修复 NestJS 依赖注入无法识别类型别名的问题
- 统一各 Service 的 PrismaService 导入方式

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
charilezhou
2026-01-19 12:34:11 +08:00
parent 57456be013
commit d747f98d08
6 changed files with 159 additions and 101 deletions

View File

@@ -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<SendResetPasswordEmailResponse> {
// 验证图形验证码
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<ResetPasswordResponse> {
// 验证邮箱验证码并获取邮箱
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: '密码重置成功' };
}
}

View File

@@ -17,7 +17,6 @@ import { PrismaService } from '@/prisma/prisma.service';
@Injectable()
@CrudOptions({
softDelete: true,
defaultPageSize: 100,
maxPageSize: 500,
defaultSortBy: 'sort',

View File

@@ -9,7 +9,6 @@ import { PrismaService } from '@/prisma/prisma.service';
@Injectable()
@CrudOptions({
softDelete: true,
defaultPageSize: 20,
maxPageSize: 100,
defaultSortBy: 'resource',

View File

@@ -10,7 +10,6 @@ import { PrismaService } from '@/prisma/prisma.service';
@Injectable()
@CrudOptions({
softDelete: true,
defaultPageSize: 20,
maxPageSize: 100,
defaultSortBy: 'sort',

View File

@@ -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<T extends { where?: Record<string, unknown> }>(
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<unknown>;
updateMany: (args: unknown) => Promise<unknown>;
};
}
// 创建软删除扩展(需要传入客户端实例)
@@ -14,75 +38,60 @@ function createSoftDeleteExtension(client: PrismaClient) {
name: 'softDelete',
query: {
$allModels: {
// 查询类方法:自动过滤已删除记录
async findMany({ model, args, query }) {
if (
isSoftDeleteModel(model) &&
(args.where as Record<string, unknown> | 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<string, unknown> | 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<string, unknown>)?.deletedAt === undefined
) {
(args.where as Record<string, unknown>).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<string, unknown> | 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<string, { update: (args: unknown) => 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<string, { delete: (args: unknown) => 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<string, { updateMany: (args: unknown) => 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<string, { deleteMany: (args: unknown) => 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<typeof createPrismaClient>;
@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;
// 导出 PrismaServiceNestJS 使用此类作为注入 token类型为 PrismaServiceType
@Injectable()
export class PrismaService extends (PrismaServiceImpl as unknown as new () => PrismaServiceType) {}

View File

@@ -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(),