feat: 切换 PostgreSQL 并实现软删除功能
- 数据库从 SQLite 切换到 PostgreSQL,添加 Docker Compose 配置 - 使用 dotenv-cli 支持 .env 和 .env.local 环境变量加载 - 使用 Prisma $extends 实现底层自动软删除机制 - 新增用户恢复和查询已删除用户的 API 接口 - 更新文档和类型定义
This commit is contained in:
197
docs/backend/soft-delete.md
Normal file
197
docs/backend/soft-delete.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# 软删除(Soft Delete)设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
软删除是一种数据删除策略,不物理删除数据库记录,而是通过标记字段(`deletedAt`)来标识记录已被删除。这样可以保留数据历史,支持数据恢复,同时在业务层面表现为"已删除"。
|
||||
|
||||
## 设计目标
|
||||
|
||||
- **通用方案**:底层自动处理,业务代码无需关心软删除逻辑
|
||||
- **可扩展**:新增模型只需加入配置数组即可启用软删除
|
||||
- **支持恢复**:已删除的数据可以恢复
|
||||
- **邮箱可复用**:用户软删除后,其邮箱可被新用户注册
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 1. 数据库层 (Prisma Schema)
|
||||
|
||||
`apps/api/prisma/schema.prisma`:
|
||||
|
||||
```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`) 实现底层自动软删除:
|
||||
|
||||
```typescript
|
||||
// 启用软删除的模型列表
|
||||
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() } })` |
|
||||
|
||||
## 使用方式
|
||||
|
||||
### 业务代码(无需关心软删除)
|
||||
|
||||
```typescript
|
||||
// 查询 - 自动过滤已删除记录
|
||||
const users = await prisma.user.findMany();
|
||||
|
||||
// 删除 - 自动转换为软删除
|
||||
await prisma.user.delete({ where: { id } });
|
||||
|
||||
// 按 ID 查找 - 自动过滤已删除记录
|
||||
const user = await prisma.user.findUnique({ where: { id } });
|
||||
```
|
||||
|
||||
### 查询已删除数据
|
||||
|
||||
显式指定 `deletedAt` 条件可绕过自动过滤:
|
||||
|
||||
```typescript
|
||||
// 查询已删除的用户
|
||||
const deletedUsers = await prisma.user.findMany({
|
||||
where: { deletedAt: { not: null } },
|
||||
});
|
||||
|
||||
// 查询指定 ID 的已删除用户
|
||||
const deletedUser = await prisma.user.findFirst({
|
||||
where: { id, deletedAt: { not: null } },
|
||||
});
|
||||
```
|
||||
|
||||
### 恢复已删除数据
|
||||
|
||||
```typescript
|
||||
// 恢复用户
|
||||
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` 字段:
|
||||
|
||||
```prisma
|
||||
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` 中添加模型名:
|
||||
|
||||
```typescript
|
||||
const SOFT_DELETE_MODELS: Prisma.ModelName[] = ['User', 'Post'];
|
||||
```
|
||||
|
||||
### 3. 添加恢复接口(可选)
|
||||
|
||||
如需恢复功能,在 Service 中添加:
|
||||
|
||||
```typescript
|
||||
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 },
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **复合唯一约束**:`@@unique([email, deletedAt])` 使得:
|
||||
- 未删除用户:email + null 唯一
|
||||
- 已删除用户:email + deletedAt 唯一(同一邮箱可多次删除)
|
||||
|
||||
2. **update 操作不受影响**:可以更新已删除的记录,这对于恢复功能是必要的
|
||||
|
||||
3. **物理删除**:如确需物理删除数据,需要绕过扩展直接使用原始 PrismaClient
|
||||
|
||||
4. **性能考虑**:软删除会增加查询条件,建议为 `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` - 用户接口(含恢复接口)
|
||||
@@ -4,9 +4,10 @@
|
||||
|
||||
## 文档列表
|
||||
|
||||
| 文档 | 说明 | 适用场景 |
|
||||
| ------------------------ | ------------ | -------------------------------- |
|
||||
| [design.md](./design.md) | 项目设计文档 | 了解整体架构、技术选型、模块设计 |
|
||||
| 文档 | 说明 | 适用场景 |
|
||||
| ------------------------------------------ | ---------------- | -------------------------------- |
|
||||
| [design.md](./design.md) | 项目设计文档 | 了解整体架构、技术选型、模块设计 |
|
||||
| [backend/soft-delete.md](./backend/soft-delete.md) | 软删除设计文档 | 了解软删除机制、扩展新模型 |
|
||||
|
||||
## 快速链接
|
||||
|
||||
|
||||
Reference in New Issue
Block a user