- 添加图形验证码模块(登录/注册需验证码) - 添加 refresh token 机制和 API 接口 - 认证响应返回 token 有效期 - 添加 Redis 模块支持验证码存储 - 添加前端验证码组件和用户管理 Demo 页面 - 添加 CRUD 基类和分页响应 DTO mixin - 添加请求/响应加密模块(AES-256-GCM) - 完善共享类型定义和前后端类型一致性 - 更新 CLAUDE.md 文档 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
5.8 KiB
5.8 KiB
软删除(Soft Delete)设计文档
概述
软删除是一种数据删除策略,不物理删除数据库记录,而是通过标记字段(deletedAt)来标识记录已被删除。这样可以保留数据历史,支持数据恢复,同时在业务层面表现为"已删除"。
设计目标
- 通用方案:底层自动处理,业务代码无需关心软删除逻辑
- 可扩展:新增模型只需加入配置数组即可启用软删除
- 支持恢复:已删除的数据可以恢复
- 邮箱可复用:用户软删除后,其邮箱可被新用户注册
技术实现
1. 数据库层 (Prisma Schema)
apps/api/prisma/schema.prisma:
model User {
id String @id @default(cuid(2))
email String
password String
name String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // 软删除标记,null 表示未删除
// 复合唯一约束:允许已删除用户的邮箱被重新注册
// email + null 唯一(未删除用户)
// email + deletedAt 唯一(已删除用户,同一邮箱可多次删除)
@@unique([email, deletedAt])
@@map("users")
}
2. PrismaService 扩展
apps/api/src/prisma/prisma.service.ts:
使用 Prisma Client Extensions ($extends) 实现底层自动软删除:
// 启用软删除的模型列表
const SOFT_DELETE_MODELS: Prisma.ModelName[] = ['User'];
// 扩展拦截以下操作:
const softDeleteExtension = Prisma.defineExtension({
query: {
$allModels: {
// 查询操作:自动添加 deletedAt: null 条件
findMany({ model, args, query }) { ... },
findFirst({ model, args, query }) { ... },
findUnique({ model, args, query }) { ... },
count({ model, args, query }) { ... },
// 删除操作:转换为 update 设置 deletedAt
delete({ model, args }) { ... },
deleteMany({ model, args }) { ... },
},
},
});
3. 自动处理逻辑
| 操作 | 自动处理 |
|---|---|
findMany |
自动添加 where: { deletedAt: null } |
findFirst |
自动添加 where: { deletedAt: null } |
findUnique |
自动添加 where: { deletedAt: null } |
count |
自动添加 where: { deletedAt: null } |
delete |
转换为 update({ data: { deletedAt: new Date() } }) |
deleteMany |
转换为 updateMany({ data: { deletedAt: new Date() } }) |
使用方式
业务代码(无需关心软删除)
// 查询 - 自动过滤已删除记录
const users = await prisma.user.findMany();
// 删除 - 自动转换为软删除
await prisma.user.delete({ where: { id } });
// 按 ID 查找 - 自动过滤已删除记录
const user = await prisma.user.findUnique({ where: { id } });
查询已删除数据
显式指定 deletedAt 条件可绕过自动过滤:
// 查询已删除的用户
const deletedUsers = await prisma.user.findMany({
where: { deletedAt: { not: null } },
});
// 查询指定 ID 的已删除用户
const deletedUser = await prisma.user.findFirst({
where: { id, deletedAt: { not: null } },
});
恢复已删除数据
// 恢复用户
await prisma.user.update({
where: { id },
data: { deletedAt: null },
});
API 接口
| 接口 | 方法 | 说明 |
|---|---|---|
/users |
GET | 获取用户列表(自动排除已删除) |
/users/:id |
GET | 获取单个用户(自动排除已删除) |
/users/:id |
DELETE | 软删除用户 |
/users/deleted |
GET | 获取已删除用户列表 |
/users/:id/restore |
PATCH | 恢复已删除用户 |
扩展新模型
为新模型启用软删除只需三步:
1. 修改 Schema
在模型中添加 deletedAt 字段:
model Post {
id String @id @default(cuid(2))
title String
content String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
deletedAt DateTime? // 添加此字段
@@map("posts")
}
2. 配置启用
在 apps/api/src/prisma/prisma.service.ts 中添加模型名:
const SOFT_DELETE_MODELS: Prisma.ModelName[] = ['User', 'Post'];
3. 添加恢复接口(可选)
如需恢复功能,在 Service 中添加:
async findDeleted() {
return this.prisma.post.findMany({
where: { deletedAt: { not: null } },
});
}
async restore(id: string) {
const post = await this.prisma.post.findFirst({
where: { id, deletedAt: { not: null } },
});
if (!post) throw new NotFoundException('已删除的文章不存在');
return this.prisma.post.update({
where: { id },
data: { deletedAt: null },
});
}
注意事项
-
复合唯一约束:
@@unique([email, deletedAt])使得:- 未删除用户:email + null 唯一
- 已删除用户:email + deletedAt 唯一(同一邮箱可多次删除)
-
update 操作不受影响:可以更新已删除的记录,这对于恢复功能是必要的
-
物理删除:如确需物理删除数据,需要绕过扩展直接使用原始 PrismaClient
-
性能考虑:软删除会增加查询条件,建议为
deletedAt字段添加索引
相关文件
apps/api/prisma/schema.prisma- 数据库模型定义apps/api/src/prisma/prisma.service.ts- 软删除扩展实现apps/api/src/user/user.service.ts- 用户服务(含恢复逻辑)apps/api/src/user/user.controller.ts- 用户接口(含恢复接口)