feat: 切换 PostgreSQL 并实现软删除功能
- 数据库从 SQLite 切换到 PostgreSQL,添加 Docker Compose 配置 - 使用 dotenv-cli 支持 .env 和 .env.local 环境变量加载 - 使用 Prisma $extends 实现底层自动软删除机制 - 新增用户恢复和查询已删除用户的 API 接口 - 更新文档和类型定义
This commit is contained in:
36
CLAUDE.md
36
CLAUDE.md
@@ -9,6 +9,9 @@ Seclusion 是一个基于 Next.js + NestJS 的 Monorepo 项目模板,使用 pn
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# 启动数据库 (PostgreSQL)
|
||||
docker compose -f deploy/docker-compose.yml up -d
|
||||
|
||||
# 开发(同时启动前后端)
|
||||
pnpm dev
|
||||
|
||||
@@ -25,7 +28,7 @@ pnpm test
|
||||
cd apps/api && pnpm test:watch # 单个测试文件监听
|
||||
cd apps/api && pnpm test:cov # 测试覆盖率报告
|
||||
|
||||
# 数据库
|
||||
# 数据库(使用 dotenv-cli 加载 .env 和 .env.local)
|
||||
pnpm db:generate # 生成 Prisma Client
|
||||
pnpm db:push # 推送 schema 到数据库
|
||||
pnpm db:migrate # 运行迁移
|
||||
@@ -46,12 +49,32 @@ cd apps/api && pnpm db:studio # 打开 Prisma Studio
|
||||
|
||||
NestJS 采用模块化架构:
|
||||
|
||||
- **PrismaModule** - 全局数据库服务,其他模块通过依赖注入使用
|
||||
- **PrismaModule** - 全局数据库服务,内置软删除扩展
|
||||
- **AuthModule** - JWT 认证(注册、登录、token 验证)
|
||||
- **UserModule** - 用户 CRUD
|
||||
|
||||
认证流程:使用 `@Public()` 装饰器标记公开接口,其他接口需要 JWT Bearer Token。使用 `@CurrentUser()` 装饰器获取当前登录用户信息。
|
||||
|
||||
### 软删除机制
|
||||
|
||||
PrismaService 使用 `$extends` 实现底层自动软删除:
|
||||
|
||||
- **查询自动过滤**: `findMany`、`findFirst`、`findUnique`、`count` 自动添加 `deletedAt: null` 条件
|
||||
- **删除自动转换**: `delete`、`deleteMany` 自动转换为设置 `deletedAt` 时间戳
|
||||
- **绕过过滤**: 显式指定 `deletedAt` 条件可查询已删除数据,如 `where: { deletedAt: { not: null } }`
|
||||
|
||||
启用软删除的模型在 `apps/api/src/prisma/prisma.service.ts` 的 `SOFT_DELETE_MODELS` 数组中配置。
|
||||
|
||||
### API 接口
|
||||
|
||||
- `POST /auth/register` - 用户注册
|
||||
- `POST /auth/login` - 用户登录
|
||||
- `GET /auth/me` - 获取当前用户(需认证)
|
||||
- `GET /users` / `GET /users/:id` - 获取用户(需认证)
|
||||
- `GET /users/deleted` - 获取已删除用户列表(需认证)
|
||||
- `PATCH /users/:id` / `DELETE /users/:id` - 更新/删除用户(需认证)
|
||||
- `PATCH /users/:id/restore` - 恢复已删除用户(需认证)
|
||||
|
||||
### 共享包使用
|
||||
|
||||
```typescript
|
||||
@@ -61,9 +84,14 @@ import { formatDate, generateId } from '@seclusion/shared';
|
||||
|
||||
**注意**: `packages/shared` 中的工具函数应优先使用 lodash-es 实现。
|
||||
|
||||
## Environment Variables
|
||||
|
||||
后端环境变量文件优先级:`.env.local` > `.env`(NestJS ConfigModule 和 Prisma 脚本均支持)
|
||||
|
||||
## Key Files
|
||||
|
||||
- `apps/api/prisma/schema.prisma` - 数据库模型定义(使用 SQLite,ID 使用 cuid2)
|
||||
- `apps/api/.env` - 后端环境变量 (DATABASE_URL, JWT_SECRET)
|
||||
- `deploy/docker-compose.yml` - PostgreSQL 数据库容器配置
|
||||
- `apps/api/prisma/schema.prisma` - 数据库模型定义(使用 PostgreSQL,ID 使用 cuid2)
|
||||
- `apps/api/.env.example` - 后端环境变量模板
|
||||
- `apps/web/.env.local` - 前端环境变量 (NEXT_PUBLIC_API_URL)
|
||||
- `turbo.json` - Turborepo 任务依赖配置
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# 数据库配置
|
||||
DATABASE_URL="file:./dev.db"
|
||||
# 数据库配置 (PostgreSQL)
|
||||
DATABASE_URL="postgresql://dev:dev@localhost:5432/seclusion"
|
||||
|
||||
# JWT 配置
|
||||
JWT_SECRET="your-super-secret-key-change-in-production"
|
||||
|
||||
@@ -12,10 +12,11 @@
|
||||
"test:watch": "jest --watch",
|
||||
"test:cov": "jest --coverage",
|
||||
"clean": "rm -rf dist .turbo node_modules",
|
||||
"db:generate": "prisma generate",
|
||||
"db:push": "prisma db push",
|
||||
"db:migrate": "prisma migrate dev",
|
||||
"db:studio": "prisma studio"
|
||||
"db:generate": "dotenv -e .env -e .env.local -- prisma generate",
|
||||
"db:push": "dotenv -e .env -e .env.local -- prisma db push",
|
||||
"db:migrate": "dotenv -e .env -e .env.local -- prisma migrate dev",
|
||||
"db:migrate:reset": "dotenv -e .env -e .env.local -- prisma migrate reset",
|
||||
"db:studio": "dotenv -e .env -e .env.local -- prisma studio"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^10.4.15",
|
||||
@@ -46,6 +47,7 @@
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/passport-jwt": "^4.0.1",
|
||||
"dotenv-cli": "^11.0.0",
|
||||
"eslint": "^9.39.0",
|
||||
"jest": "^29.7.0",
|
||||
"prisma": "^6.1.0",
|
||||
|
||||
@@ -3,17 +3,20 @@ generator client {
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "sqlite"
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid(2))
|
||||
email String @unique
|
||||
id String @id @default(cuid(2))
|
||||
email String
|
||||
password String
|
||||
name String?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
deletedAt DateTime?
|
||||
|
||||
// 复合唯一约束:未删除用户邮箱唯一,已删除用户邮箱可重复
|
||||
@@unique([email, deletedAt])
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import { UserModule } from './user/user.module';
|
||||
imports: [
|
||||
ConfigModule.forRoot({
|
||||
isGlobal: true,
|
||||
envFilePath: '.env',
|
||||
envFilePath: ['.env.local', '.env'],
|
||||
}),
|
||||
PrismaModule,
|
||||
AuthModule,
|
||||
|
||||
@@ -14,8 +14,8 @@ export class AuthService {
|
||||
) {}
|
||||
|
||||
async register(dto: RegisterDto) {
|
||||
// 检查邮箱是否已存在
|
||||
const existingUser = await this.prisma.user.findUnique({
|
||||
// 底层自动过滤已删除用户
|
||||
const existingUser = await this.prisma.user.findFirst({
|
||||
where: { email: dto.email },
|
||||
});
|
||||
|
||||
@@ -49,8 +49,8 @@ export class AuthService {
|
||||
}
|
||||
|
||||
async login(dto: LoginDto) {
|
||||
// 查找用户
|
||||
const user = await this.prisma.user.findUnique({
|
||||
// 底层自动过滤已删除用户
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { email: dto.email },
|
||||
});
|
||||
|
||||
|
||||
@@ -1,13 +1,109 @@
|
||||
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { Prisma, PrismaClient } from '@prisma/client';
|
||||
|
||||
// 启用软删除的模型列表
|
||||
const SOFT_DELETE_MODELS: Prisma.ModelName[] = ['User'];
|
||||
|
||||
function isSoftDeleteModel(model: string | undefined): boolean {
|
||||
return !!model && SOFT_DELETE_MODELS.includes(model as Prisma.ModelName);
|
||||
}
|
||||
|
||||
// 创建软删除扩展(需要传入客户端实例)
|
||||
function createSoftDeleteExtension(client: PrismaClient) {
|
||||
return Prisma.defineExtension({
|
||||
name: 'softDelete',
|
||||
query: {
|
||||
$allModels: {
|
||||
async findMany({ model, args, query }) {
|
||||
if (isSoftDeleteModel(model) && args.where?.deletedAt === undefined) {
|
||||
args.where = { ...args.where, deletedAt: null };
|
||||
}
|
||||
return query(args);
|
||||
},
|
||||
async findFirst({ model, args, query }) {
|
||||
if (isSoftDeleteModel(model) && args.where?.deletedAt === undefined) {
|
||||
args.where = { ...args.where, deletedAt: null };
|
||||
}
|
||||
return query(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);
|
||||
},
|
||||
async count({ model, args, query }) {
|
||||
if (isSoftDeleteModel(model) && args.where?.deletedAt === undefined) {
|
||||
args.where = { ...args.where, deletedAt: null };
|
||||
}
|
||||
return query(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 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() },
|
||||
});
|
||||
}
|
||||
// 非软删除模型,执行真正删除
|
||||
const modelName = model!.charAt(0).toLowerCase() + model!.slice(1);
|
||||
return (client as unknown as Record<string, { deleteMany: (args: unknown) => unknown }>)[modelName].deleteMany(
|
||||
args
|
||||
);
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 创建扩展后的客户端
|
||||
function createPrismaClient() {
|
||||
const client = new PrismaClient();
|
||||
return client.$extends(createSoftDeleteExtension(client));
|
||||
}
|
||||
|
||||
// 扩展后的客户端类型
|
||||
export type ExtendedPrismaClient = ReturnType<typeof createPrismaClient>;
|
||||
|
||||
@Injectable()
|
||||
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
|
||||
export class PrismaService implements OnModuleInit, OnModuleDestroy {
|
||||
private readonly _client: ExtendedPrismaClient;
|
||||
|
||||
constructor() {
|
||||
this._client = createPrismaClient();
|
||||
}
|
||||
|
||||
get client(): ExtendedPrismaClient {
|
||||
return this._client;
|
||||
}
|
||||
|
||||
// 代理到扩展客户端
|
||||
get user() {
|
||||
return this._client.user;
|
||||
}
|
||||
|
||||
async onModuleInit() {
|
||||
await this.$connect();
|
||||
await this._client.$connect();
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.$disconnect();
|
||||
await this._client.$disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,12 @@ export class UserController {
|
||||
return this.userService.findAll();
|
||||
}
|
||||
|
||||
@Get('deleted')
|
||||
@ApiOperation({ summary: '获取已删除的用户列表' })
|
||||
findDeleted() {
|
||||
return this.userService.findDeleted();
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@ApiOperation({ summary: '根据 ID 获取用户' })
|
||||
findById(@Param('id') id: string) {
|
||||
@@ -36,4 +42,10 @@ export class UserController {
|
||||
delete(@Param('id') id: string) {
|
||||
return this.userService.delete(id);
|
||||
}
|
||||
|
||||
@Patch(':id/restore')
|
||||
@ApiOperation({ summary: '恢复已删除的用户' })
|
||||
restore(@Param('id') id: string) {
|
||||
return this.userService.restore(id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export class UserService {
|
||||
constructor(private prisma: PrismaService) {}
|
||||
|
||||
async findAll() {
|
||||
// 底层自动过滤已删除记录
|
||||
return this.prisma.user.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
@@ -21,6 +22,7 @@ export class UserService {
|
||||
}
|
||||
|
||||
async findById(id: string) {
|
||||
// 底层自动过滤已删除记录
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
@@ -66,8 +68,47 @@ export class UserService {
|
||||
throw new NotFoundException('用户不存在');
|
||||
}
|
||||
|
||||
// 底层自动转换为软删除
|
||||
await this.prisma.user.delete({ where: { id } });
|
||||
|
||||
return { message: '用户已删除' };
|
||||
}
|
||||
|
||||
async findDeleted() {
|
||||
// 显式指定 deletedAt 条件,绕过自动过滤
|
||||
return this.prisma.user.findMany({
|
||||
where: { deletedAt: { not: null } },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
deletedAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async restore(id: string) {
|
||||
// 显式指定 deletedAt 条件,查询已删除的用户
|
||||
const user = await this.prisma.user.findFirst({
|
||||
where: { id, deletedAt: { not: null } },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('已删除的用户不存在');
|
||||
}
|
||||
|
||||
return this.prisma.user.update({
|
||||
where: { id },
|
||||
data: { deletedAt: null },
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
20
deploy/docker-compose.yml
Normal file
20
deploy/docker-compose.yml
Normal file
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: seclusion-postgres
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_USER: dev
|
||||
POSTGRES_PASSWORD: dev
|
||||
POSTGRES_DB: seclusion
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U dev -d seclusion"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
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) | 软删除设计文档 | 了解软删除机制、扩展新模型 |
|
||||
|
||||
## 快速链接
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ export interface User {
|
||||
name: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt?: Date | null;
|
||||
}
|
||||
|
||||
// 用户创建请求
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -102,6 +102,9 @@ importers:
|
||||
'@types/passport-jwt':
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
dotenv-cli:
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
eslint:
|
||||
specifier: ^9.39.0
|
||||
version: 9.39.2(jiti@2.6.1)
|
||||
@@ -2177,10 +2180,18 @@ packages:
|
||||
resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
dotenv-cli@11.0.0:
|
||||
resolution: {integrity: sha512-r5pA8idbk7GFWuHEU7trSTflWcdBpQEK+Aw17UrSHjS6CReuhrrPcyC3zcQBPQvhArRHnBo/h6eLH1fkCvNlww==}
|
||||
hasBin: true
|
||||
|
||||
dotenv-expand@10.0.0:
|
||||
resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv-expand@12.0.3:
|
||||
resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv@16.4.5:
|
||||
resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -2189,6 +2200,10 @@ packages:
|
||||
resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dotenv@17.2.3:
|
||||
resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -6629,12 +6644,25 @@ snapshots:
|
||||
dependencies:
|
||||
esutils: 2.0.3
|
||||
|
||||
dotenv-cli@11.0.0:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
dotenv: 17.2.3
|
||||
dotenv-expand: 12.0.3
|
||||
minimist: 1.2.8
|
||||
|
||||
dotenv-expand@10.0.0: {}
|
||||
|
||||
dotenv-expand@12.0.3:
|
||||
dependencies:
|
||||
dotenv: 16.6.1
|
||||
|
||||
dotenv@16.4.5: {}
|
||||
|
||||
dotenv@16.6.1: {}
|
||||
|
||||
dotenv@17.2.3: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
|
||||
Reference in New Issue
Block a user