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:
@@ -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: '密码重置成功' };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ import { PrismaService } from '@/prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
@CrudOptions({
|
||||
softDelete: true,
|
||||
defaultPageSize: 100,
|
||||
maxPageSize: 500,
|
||||
defaultSortBy: 'sort',
|
||||
|
||||
@@ -9,7 +9,6 @@ import { PrismaService } from '@/prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
@CrudOptions({
|
||||
softDelete: true,
|
||||
defaultPageSize: 20,
|
||||
maxPageSize: 100,
|
||||
defaultSortBy: 'resource',
|
||||
|
||||
@@ -10,7 +10,6 @@ import { PrismaService } from '@/prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
@CrudOptions({
|
||||
softDelete: true,
|
||||
defaultPageSize: 20,
|
||||
maxPageSize: 100,
|
||||
defaultSortBy: 'sort',
|
||||
|
||||
@@ -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;
|
||||
|
||||
// 导出 PrismaService,NestJS 使用此类作为注入 token,类型为 PrismaServiceType
|
||||
@Injectable()
|
||||
export class PrismaService extends (PrismaServiceImpl as unknown as new () => PrismaServiceType) {}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user