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>
This commit is contained in:
317
docs/backend/crud-service.md
Normal file
317
docs/backend/crud-service.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# CRUD Service 模板
|
||||
|
||||
本文档介绍 `CrudService` 泛型基类的使用方法,用于快速创建标准 CRUD 服务。
|
||||
|
||||
## 概述
|
||||
|
||||
`CrudService` 是一个泛型抽象基类,提供:
|
||||
|
||||
- 标准 CRUD 操作(创建、查询、更新、删除)
|
||||
- 强制分页查询
|
||||
- 可选软删除支持(查询已删除、恢复)
|
||||
- 通过装饰器配置行为
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 创建 Service
|
||||
|
||||
```typescript
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { Prisma, User } from '@prisma/client';
|
||||
|
||||
import { CrudOptions, CrudService } from '@/common/crud';
|
||||
import { PrismaService } from '@/prisma/prisma.service';
|
||||
|
||||
import { UpdateUserDto } from './dto/user.dto';
|
||||
|
||||
@Injectable()
|
||||
@CrudOptions({
|
||||
softDelete: true,
|
||||
defaultSelect: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
})
|
||||
export class UserService extends CrudService<
|
||||
User, // 实体类型
|
||||
Prisma.UserCreateInput, // 创建 DTO
|
||||
UpdateUserDto, // 更新 DTO
|
||||
Prisma.UserWhereInput, // Where 条件类型
|
||||
Prisma.UserWhereUniqueInput // WhereUnique 条件类型
|
||||
> {
|
||||
constructor(prisma: PrismaService) {
|
||||
super(prisma, 'user'); // 'user' 对应 prisma.user
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 创建 Controller
|
||||
|
||||
```typescript
|
||||
import { Controller, Get, Post, Patch, Delete, Param, Body, Query } from '@nestjs/common';
|
||||
|
||||
import { PaginationQueryDto } from '@/common/crud';
|
||||
|
||||
import { CreateUserDto, UpdateUserDto } from './dto/user.dto';
|
||||
import { UserService } from './user.service';
|
||||
|
||||
@Controller('users')
|
||||
export class UserController {
|
||||
constructor(private readonly userService: UserService) {}
|
||||
|
||||
@Get()
|
||||
findAll(@Query() query: PaginationQueryDto) {
|
||||
return this.userService.findAll(query);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findById(@Param('id') id: string) {
|
||||
return this.userService.findById(id);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(@Body() dto: CreateUserDto) {
|
||||
return this.userService.create(dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(@Param('id') id: string, @Body() dto: UpdateUserDto) {
|
||||
return this.userService.update(id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
delete(@Param('id') id: string) {
|
||||
return this.userService.delete(id);
|
||||
}
|
||||
|
||||
// 软删除相关(需要 softDelete: true)
|
||||
@Get('deleted')
|
||||
findDeleted(@Query() query: PaginationQueryDto) {
|
||||
return this.userService.findDeleted(query);
|
||||
}
|
||||
|
||||
@Patch(':id/restore')
|
||||
restore(@Param('id') id: string) {
|
||||
return this.userService.restore(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 配置选项
|
||||
|
||||
通过 `@CrudOptions()` 装饰器配置:
|
||||
|
||||
| 选项 | 类型 | 默认值 | 说明 |
|
||||
| ------------------ | ------------------------- | ------------- | ---------------------------- |
|
||||
| `softDelete` | `boolean` | `false` | 是否启用软删除功能 |
|
||||
| `defaultPageSize` | `number` | `20` | 默认分页大小 |
|
||||
| `maxPageSize` | `number` | `100` | 最大分页大小 |
|
||||
| `defaultSortBy` | `string` | `'createdAt'` | 默认排序字段 |
|
||||
| `defaultSortOrder` | `'asc' \| 'desc'` | `'desc'` | 默认排序方向 |
|
||||
| `defaultSelect` | `Record<string, boolean>` | `{}` | 默认返回字段(排除敏感字段) |
|
||||
|
||||
### 配置示例
|
||||
|
||||
```typescript
|
||||
@CrudOptions({
|
||||
softDelete: true,
|
||||
defaultPageSize: 10,
|
||||
maxPageSize: 50,
|
||||
defaultSortBy: 'name',
|
||||
defaultSortOrder: 'asc',
|
||||
defaultSelect: {
|
||||
id: true,
|
||||
name: true,
|
||||
email: true,
|
||||
createdAt: true,
|
||||
// password: false - 不返回密码
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
## 基类方法
|
||||
|
||||
### 标准 CRUD
|
||||
|
||||
| 方法 | 说明 | 返回类型 |
|
||||
| ------------------ | ------------ | --------------------------- |
|
||||
| `findAll(params?)` | 分页查询 | `PaginatedResponse<Entity>` |
|
||||
| `findById(id)` | 根据 ID 查询 | `Entity` |
|
||||
| `create(dto)` | 创建记录 | `Entity` |
|
||||
| `update(id, dto)` | 更新记录 | `Entity` |
|
||||
| `delete(id)` | 删除记录 | `{ message: string }` |
|
||||
|
||||
### 软删除(需要 `softDelete: true`)
|
||||
|
||||
| 方法 | 说明 | 返回类型 |
|
||||
| ---------------------- | -------------- | --------------------------- |
|
||||
| `findDeleted(params?)` | 查询已删除记录 | `PaginatedResponse<Entity>` |
|
||||
| `restore(id)` | 恢复已删除记录 | `Entity` |
|
||||
|
||||
### 分页参数
|
||||
|
||||
`findAll` 和 `findDeleted` 支持以下参数:
|
||||
|
||||
```typescript
|
||||
interface FindAllParams<WhereInput> {
|
||||
page?: number; // 页码,默认 1
|
||||
pageSize?: number; // 每页数量,默认 20(-1 或 0 表示不分页)
|
||||
sortBy?: string; // 排序字段(逗号分隔支持多字段)
|
||||
sortOrder?: string; // 排序方向(逗号分隔,与 sortBy 对应)
|
||||
where?: WhereInput; // 过滤条件
|
||||
}
|
||||
```
|
||||
|
||||
### 非分页查询
|
||||
|
||||
当 `pageSize <= 0`(如 `-1` 或 `0`)时,返回所有匹配数据:
|
||||
|
||||
```bash
|
||||
# 返回所有用户(不分页)
|
||||
GET /users?pageSize=-1
|
||||
|
||||
# 或
|
||||
GET /users?pageSize=0
|
||||
```
|
||||
|
||||
非分页响应格式保持一致,`page=1`,`totalPages=1`,`pageSize` 和 `total` 等于实际返回数量。
|
||||
|
||||
### 多字段排序
|
||||
|
||||
支持通过逗号分隔实现多字段排序:
|
||||
|
||||
```bash
|
||||
# 单字段排序
|
||||
GET /users?sortBy=createdAt&sortOrder=desc
|
||||
|
||||
# 多字段排序:先按 status 升序,再按 createdAt 降序
|
||||
GET /users?sortBy=status,createdAt&sortOrder=asc,desc
|
||||
```
|
||||
|
||||
排序规则:
|
||||
|
||||
- `sortBy` 和 `sortOrder` 按位置一一对应
|
||||
- 如果 `sortOrder` 数量少于 `sortBy`,后续字段使用第一个排序方向
|
||||
- 示例:`sortBy=a,b,c&sortOrder=asc` 等同于 `sortBy=a,b,c&sortOrder=asc,asc,asc`
|
||||
|
||||
### 分页响应
|
||||
|
||||
```typescript
|
||||
interface PaginatedResponse<T> {
|
||||
items: T[]; // 数据列表
|
||||
total: number; // 总记录数
|
||||
page: number; // 当前页码
|
||||
pageSize: number; // 每页数量
|
||||
totalPages: number; // 总页数
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义扩展
|
||||
|
||||
### 覆盖错误消息
|
||||
|
||||
```typescript
|
||||
export class UserService extends CrudService<...> {
|
||||
protected getNotFoundMessage(id: string): string {
|
||||
return '用户不存在';
|
||||
}
|
||||
|
||||
protected getDeletedMessage(id: string): string {
|
||||
return '用户已删除';
|
||||
}
|
||||
|
||||
protected getDeletedNotFoundMessage(id: string): string {
|
||||
return '已删除的用户不存在';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 覆盖默认 Select
|
||||
|
||||
```typescript
|
||||
export class UserService extends CrudService<...> {
|
||||
protected getDefaultSelect(): Record<string, boolean> {
|
||||
return {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
profile: true, // 包含关联
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 添加业务方法
|
||||
|
||||
```typescript
|
||||
export class UserService extends CrudService<...> {
|
||||
async findByEmail(email: string): Promise<User | null> {
|
||||
return this.model.findFirst({
|
||||
where: { email },
|
||||
});
|
||||
}
|
||||
|
||||
async findWithPosts(id: string): Promise<User> {
|
||||
const user = await this.prisma.user.findUnique({
|
||||
where: { id },
|
||||
include: { posts: true },
|
||||
});
|
||||
if (!user) throw new NotFoundException('用户不存在');
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 带过滤条件的查询
|
||||
|
||||
```typescript
|
||||
// Controller
|
||||
@Get()
|
||||
findAll(
|
||||
@Query() query: PaginationQueryDto,
|
||||
@Query('status') status?: string,
|
||||
) {
|
||||
return this.userService.findAll({
|
||||
...query,
|
||||
where: status ? { status } : undefined,
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 泛型参数说明
|
||||
|
||||
```typescript
|
||||
CrudService<Entity, CreateDto, UpdateDto, WhereInput, WhereUniqueInput>;
|
||||
```
|
||||
|
||||
| 参数 | 说明 | 来源 |
|
||||
| ------------------ | ------------ | ------------------------------------ |
|
||||
| `Entity` | 实体类型 | `@prisma/client` 导出的模型类型 |
|
||||
| `CreateDto` | 创建数据类型 | `Prisma.XxxCreateInput` 或自定义 DTO |
|
||||
| `UpdateDto` | 更新数据类型 | `Prisma.XxxUpdateInput` 或自定义 DTO |
|
||||
| `WhereInput` | 查询条件类型 | `Prisma.XxxWhereInput`(可选) |
|
||||
| `WhereUniqueInput` | 唯一查询条件 | `Prisma.XxxWhereUniqueInput`(可选) |
|
||||
|
||||
## 与软删除的配合
|
||||
|
||||
`CrudService` 的 `softDelete` 配置需要与 `PrismaService` 的软删除扩展配合使用:
|
||||
|
||||
1. 在 `prisma.service.ts` 的 `SOFT_DELETE_MODELS` 数组中添加模型名
|
||||
2. 在 Service 的 `@CrudOptions` 中设置 `softDelete: true`
|
||||
|
||||
详见 [软删除设计文档](./soft-delete.md)。
|
||||
|
||||
## 文件结构
|
||||
|
||||
```
|
||||
apps/api/src/common/crud/
|
||||
├── index.ts # 统一导出
|
||||
├── crud.types.ts # 类型定义
|
||||
├── crud.decorator.ts # @CrudOptions 装饰器
|
||||
├── crud.service.ts # CrudService 基类
|
||||
└── dto/
|
||||
└── pagination.dto.ts # PaginationQueryDto
|
||||
```
|
||||
@@ -65,13 +65,13 @@ const softDeleteExtension = Prisma.defineExtension({
|
||||
|
||||
### 3. 自动处理逻辑
|
||||
|
||||
| 操作 | 自动处理 |
|
||||
|------|---------|
|
||||
| `findMany` | 自动添加 `where: { deletedAt: null }` |
|
||||
| `findFirst` | 自动添加 `where: { deletedAt: null }` |
|
||||
| `findUnique` | 自动添加 `where: { deletedAt: null }` |
|
||||
| `count` | 自动添加 `where: { deletedAt: null }` |
|
||||
| `delete` | 转换为 `update({ data: { deletedAt: new Date() } })` |
|
||||
| 操作 | 自动处理 |
|
||||
| ------------ | -------------------------------------------------------- |
|
||||
| `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() } })` |
|
||||
|
||||
## 使用方式
|
||||
@@ -117,13 +117,13 @@ await prisma.user.update({
|
||||
|
||||
## API 接口
|
||||
|
||||
| 接口 | 方法 | 说明 |
|
||||
|------|------|------|
|
||||
| `/users` | GET | 获取用户列表(自动排除已删除) |
|
||||
| `/users/:id` | GET | 获取单个用户(自动排除已删除) |
|
||||
| `/users/:id` | DELETE | 软删除用户 |
|
||||
| `/users/deleted` | GET | 获取已删除用户列表 |
|
||||
| `/users/:id/restore` | PATCH | 恢复已删除用户 |
|
||||
| 接口 | 方法 | 说明 |
|
||||
| -------------------- | ------ | ------------------------------ |
|
||||
| `/users` | GET | 获取用户列表(自动排除已删除) |
|
||||
| `/users/:id` | GET | 获取单个用户(自动排除已删除) |
|
||||
| `/users/:id` | DELETE | 软删除用户 |
|
||||
| `/users/deleted` | GET | 获取已删除用户列表 |
|
||||
| `/users/:id/restore` | PATCH | 恢复已删除用户 |
|
||||
|
||||
## 扩展新模型
|
||||
|
||||
|
||||
@@ -4,10 +4,11 @@
|
||||
|
||||
## 文档列表
|
||||
|
||||
| 文档 | 说明 | 适用场景 |
|
||||
| ------------------------------------------ | ---------------- | -------------------------------- |
|
||||
| [design.md](./design.md) | 项目设计文档 | 了解整体架构、技术选型、模块设计 |
|
||||
| [backend/soft-delete.md](./backend/soft-delete.md) | 软删除设计文档 | 了解软删除机制、扩展新模型 |
|
||||
| 文档 | 说明 | 适用场景 |
|
||||
| ---------------------------------------------------- | ----------------- | -------------------------------- |
|
||||
| [design.md](./design.md) | 项目设计文档 | 了解整体架构、技术选型、模块设计 |
|
||||
| [backend/soft-delete.md](./backend/soft-delete.md) | 软删除设计文档 | 了解软删除机制、扩展新模型 |
|
||||
| [backend/crud-service.md](./backend/crud-service.md) | CRUD Service 模板 | 快速创建标准 CRUD 服务 |
|
||||
|
||||
## 快速链接
|
||||
|
||||
|
||||
Reference in New Issue
Block a user