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:
charilezhou
2026-01-16 19:17:11 +08:00
parent 1c7e2c3a7c
commit c958271027
61 changed files with 3828 additions and 205 deletions

View 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
```

View File

@@ -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 | 恢复已删除用户 |
## 扩展新模型

View File

@@ -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 服务 |
## 快速链接