Files
seclusion/docs/backend/soft-delete.md
charilezhou c958271027 feat: 完善认证系统和前端 Demo 页面
- 添加图形验证码模块(登录/注册需验证码)
- 添加 refresh token 机制和 API 接口
- 认证响应返回 token 有效期
- 添加 Redis 模块支持验证码存储
- 添加前端验证码组件和用户管理 Demo 页面
- 添加 CRUD 基类和分页响应 DTO mixin
- 添加请求/响应加密模块(AES-256-GCM)
- 完善共享类型定义和前后端类型一致性
- 更新 CLAUDE.md 文档

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-16 19:17:11 +08:00

5.8 KiB
Raw Permalink Blame History

软删除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 },
  });
}

注意事项

  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 - 用户接口(含恢复接口)