|
@@ -0,0 +1,323 @@
|
|
|
|
|
+# 通用 CRUD 规范
|
|
|
|
|
+
|
|
|
|
|
+## 版本信息
|
|
|
|
|
+| 版本 | 日期 | 描述 | 作者 |
|
|
|
|
|
+|------|------|------|------|
|
|
|
|
|
+| 1.0 | 2025-10-15 | 创建通用 CRUD 规范文档 | Winston |
|
|
|
|
|
+
|
|
|
|
|
+## 概述
|
|
|
|
|
+
|
|
|
|
|
+通用 CRUD(Create, Read, Update, Delete)系统是本项目的核心基础设施,提供类型安全、可扩展的数据操作服务。基于 TypeORM 和 Hono 框架构建,支持自动生成 OpenAPI 文档、关联查询、用户跟踪等高级功能。
|
|
|
|
|
+
|
|
|
|
|
+## 设计原则
|
|
|
|
|
+
|
|
|
|
|
+### 核心原则
|
|
|
|
|
+- **类型安全**: 基于 TypeScript 的完整类型支持
|
|
|
|
|
+- **可扩展性**: 支持自定义扩展和业务逻辑注入
|
|
|
|
|
+- **一致性**: 统一的 API 设计和错误处理
|
|
|
|
|
+- **性能优化**: 支持分页、缓存、关联查询优化
|
|
|
|
|
+- **安全性**: 内置用户跟踪和权限控制
|
|
|
|
|
+
|
|
|
|
|
+### 架构模式
|
|
|
|
|
+- **服务层**: `GenericCrudService` 提供核心 CRUD 操作
|
|
|
|
|
+- **路由层**: `createCrudRoutes` 自动生成 RESTful API 路由
|
|
|
|
|
+- **实体层**: TypeORM 实体定义数据模型
|
|
|
|
|
+- **验证层**: Zod schema 提供输入验证
|
|
|
|
|
+
|
|
|
|
|
+## 核心组件
|
|
|
|
|
+
|
|
|
|
|
+### 1. GenericCrudService
|
|
|
|
|
+
|
|
|
|
|
+**位置**: `src/server/utils/generic-crud.service.ts`
|
|
|
|
|
+
|
|
|
|
|
+**核心功能**:
|
|
|
|
|
+- 分页列表查询(支持搜索、排序、复杂筛选)
|
|
|
|
|
+- 单个实体查询(支持关联关系)
|
|
|
|
|
+- 创建、更新、删除操作
|
|
|
|
|
+- 用户跟踪(创建人、更新人)
|
|
|
|
|
+- 关联字段处理
|
|
|
|
|
+
|
|
|
|
|
+**主要方法**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 分页查询
|
|
|
|
|
+async getList(
|
|
|
|
|
+ page: number = 1,
|
|
|
|
|
+ pageSize: number = 10,
|
|
|
|
|
+ keyword?: string,
|
|
|
|
|
+ searchFields?: string[],
|
|
|
|
|
+ where?: Partial<T>,
|
|
|
|
|
+ relations: string[] = [],
|
|
|
|
|
+ order: { [P in keyof T]?: 'ASC' | 'DESC' } = {},
|
|
|
|
|
+ filters?: { [key: string]: any }
|
|
|
|
|
+): Promise<[T[], number]>
|
|
|
|
|
+
|
|
|
|
|
+// 根据ID查询
|
|
|
|
|
+async getById(id: number, relations: string[] = []): Promise<T | null>
|
|
|
|
|
+
|
|
|
|
|
+// 创建实体
|
|
|
|
|
+async create(data: DeepPartial<T>, userId?: string | number): Promise<T>
|
|
|
|
|
+
|
|
|
|
|
+// 更新实体
|
|
|
|
|
+async update(id: number, data: Partial<T>, userId?: string | number): Promise<T | null>
|
|
|
|
|
+
|
|
|
|
|
+// 删除实体
|
|
|
|
|
+async delete(id: number): Promise<boolean>
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2. ConcreteCrudService
|
|
|
|
|
+
|
|
|
|
|
+**位置**: `src/server/utils/concrete-crud.service.ts`
|
|
|
|
|
+
|
|
|
|
|
+**用途**: 简化 `GenericCrudService` 的实例化,自动注入数据源。
|
|
|
|
|
+
|
|
|
|
|
+**使用示例**:
|
|
|
|
|
+```typescript
|
|
|
|
|
+const userService = new ConcreteCrudService(User, {
|
|
|
|
|
+ userTracking: { createdByField: 'createdBy', updatedByField: 'updatedBy' }
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3. createCrudRoutes
|
|
|
|
|
+
|
|
|
|
|
+**位置**: `src/server/utils/generic-crud.routes.ts`
|
|
|
|
|
+
|
|
|
|
|
+**功能**: 自动生成完整的 CRUD 路由,包括:
|
|
|
|
|
+- `GET /` - 分页列表
|
|
|
|
|
+- `GET /{id}` - 单个实体详情
|
|
|
|
|
+- `POST /` - 创建实体
|
|
|
|
|
+- `PUT /{id}` - 更新实体
|
|
|
|
|
+- `DELETE /{id}` - 删除实体
|
|
|
|
|
+
|
|
|
|
|
+**支持特性**:
|
|
|
|
|
+- 自动 OpenAPI 文档生成
|
|
|
|
|
+- 输入验证(Zod schema)
|
|
|
|
|
+- 错误处理统一
|
|
|
|
|
+- 中间件支持
|
|
|
|
|
+- 只读模式
|
|
|
|
|
+
|
|
|
|
|
+## 使用指南
|
|
|
|
|
+
|
|
|
|
|
+### 1. 创建实体类
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// user.entity.ts
|
|
|
|
|
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
|
|
|
|
|
+
|
|
|
|
|
+@Entity()
|
|
|
|
|
+export class User {
|
|
|
|
|
+ @PrimaryGeneratedColumn()
|
|
|
|
|
+ id: number;
|
|
|
|
|
+
|
|
|
|
|
+ @Column({ unique: true })
|
|
|
|
|
+ username: string;
|
|
|
|
|
+
|
|
|
|
|
+ @Column({ nullable: true })
|
|
|
|
|
+ email: string | null;
|
|
|
|
|
+
|
|
|
|
|
+ @CreateDateColumn()
|
|
|
|
|
+ createdAt: Date;
|
|
|
|
|
+
|
|
|
|
|
+ @UpdateDateColumn()
|
|
|
|
|
+ updatedAt: Date;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 2. 创建 Zod Schema
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// user.schema.ts
|
|
|
|
|
+import { z } from '@hono/zod-openapi';
|
|
|
|
|
+
|
|
|
|
|
+export const UserCreateSchema = z.object({
|
|
|
|
|
+ username: z.string().min(3).max(50),
|
|
|
|
|
+ email: z.string().email().optional().nullable(),
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+export const UserUpdateSchema = UserCreateSchema.partial();
|
|
|
|
|
+
|
|
|
|
|
+export const UserGetSchema = z.object({
|
|
|
|
|
+ id: z.number(),
|
|
|
|
|
+ username: z.string(),
|
|
|
|
|
+ email: z.string().email().nullable(),
|
|
|
|
|
+ createdAt: z.string().datetime(),
|
|
|
|
|
+ updatedAt: z.string().datetime(),
|
|
|
|
|
+});
|
|
|
|
|
+
|
|
|
|
|
+export const UserListSchema = UserGetSchema;
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 3. 注册 CRUD 路由
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// api/users/index.ts
|
|
|
|
|
+import { createCrudRoutes } from '../../../utils/generic-crud.routes';
|
|
|
|
|
+import { User } from '../../../modules/users/user.entity';
|
|
|
|
|
+import {
|
|
|
|
|
+ UserCreateSchema,
|
|
|
|
|
+ UserUpdateSchema,
|
|
|
|
|
+ UserGetSchema,
|
|
|
|
|
+ UserListSchema
|
|
|
|
|
+} from '../../../modules/users/user.schema';
|
|
|
|
|
+
|
|
|
|
|
+export const userRoutes = createCrudRoutes({
|
|
|
|
|
+ entity: User,
|
|
|
|
|
+ createSchema: UserCreateSchema,
|
|
|
|
|
+ updateSchema: UserUpdateSchema,
|
|
|
|
|
+ getSchema: UserGetSchema,
|
|
|
|
|
+ listSchema: UserListSchema,
|
|
|
|
|
+ searchFields: ['username', 'email'],
|
|
|
|
|
+ relations: ['roles'],
|
|
|
|
|
+ middleware: [authMiddleware],
|
|
|
|
|
+ userTracking: {
|
|
|
|
|
+ createdByField: 'createdBy',
|
|
|
|
|
+ updatedByField: 'updatedBy'
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 4. 在 API 中注册路由
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// api/index.ts
|
|
|
|
|
+import { userRoutes } from './users';
|
|
|
|
|
+
|
|
|
|
|
+const app = new OpenAPIHono();
|
|
|
|
|
+app.route('/users', userRoutes);
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 高级功能
|
|
|
|
|
+
|
|
|
|
|
+### 关联字段处理
|
|
|
|
|
+
|
|
|
|
|
+支持多对多、一对多等关联关系的自动处理:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+const roleRoutes = createCrudRoutes({
|
|
|
|
|
+ entity: Role,
|
|
|
|
|
+ // ... schemas
|
|
|
|
|
+ relationFields: {
|
|
|
|
|
+ permissions: {
|
|
|
|
|
+ relationName: 'permissions',
|
|
|
|
|
+ targetEntity: Permission
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 复杂筛选
|
|
|
|
|
+
|
|
|
|
|
+支持多种筛选方式:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 精确匹配
|
|
|
|
|
+filters: '{"status": 1}'
|
|
|
|
|
+
|
|
|
|
|
+// 范围查询
|
|
|
|
|
+filters: '{"createdAt": {"gte": "2024-01-01", "lte": "2024-12-31"}}'
|
|
|
|
|
+
|
|
|
|
|
+// IN 查询
|
|
|
|
|
+filters: '{"status": [1, 2, 3]}'
|
|
|
|
|
+
|
|
|
|
|
+// 模糊匹配
|
|
|
|
|
+filters: '{"name": "%keyword%"}'
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 关联字段搜索
|
|
|
|
|
+
|
|
|
|
|
+支持嵌套关联字段的搜索:
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+// 搜索用户关联的角色名称
|
|
|
|
|
+searchFields: ['username', 'roles.name']
|
|
|
|
|
+
|
|
|
|
|
+// 搜索嵌套关联
|
|
|
|
|
+searchFields: ['contract.client.name']
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 最佳实践
|
|
|
|
|
+
|
|
|
|
|
+### 1. 实体设计
|
|
|
|
|
+- 所有实体必须继承 `ObjectLiteral`
|
|
|
|
|
+- 使用 TypeORM 装饰器定义字段
|
|
|
|
|
+- 包含 `createdAt` 和 `updatedAt` 时间戳
|
|
|
|
|
+- 考虑软删除需求
|
|
|
|
|
+
|
|
|
|
|
+### 2. Schema 设计
|
|
|
|
|
+- 创建和更新使用不同的 schema
|
|
|
|
|
+- 响应 schema 应包含完整字段
|
|
|
|
|
+- 使用 `.optional()` 和 `.nullable()` 明确字段可选性
|
|
|
|
|
+
|
|
|
|
|
+### 3. 性能优化
|
|
|
|
|
+- 合理设置分页大小(默认10条)
|
|
|
|
|
+- 为常用查询字段添加索引
|
|
|
|
|
+- 使用关联查询时注意 N+1 问题
|
|
|
|
|
+- 考虑缓存策略
|
|
|
|
|
+
|
|
|
|
|
+### 4. 安全性
|
|
|
|
|
+- 使用用户跟踪记录操作人
|
|
|
|
|
+- 实现适当的权限控制
|
|
|
|
|
+- 验证所有输入数据
|
|
|
|
|
+- 避免敏感信息暴露
|
|
|
|
|
+
|
|
|
|
|
+## 扩展和自定义
|
|
|
|
|
+
|
|
|
|
|
+### 自定义业务逻辑
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+class CustomUserService extends ConcreteCrudService<User> {
|
|
|
|
|
+ async createWithValidation(data: UserCreateDto): Promise<User> {
|
|
|
|
|
+ // 自定义验证逻辑
|
|
|
|
|
+ await this.validateUniqueUsername(data.username);
|
|
|
|
|
+
|
|
|
|
|
+ // 调用父类方法
|
|
|
|
|
+ return super.create(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async validateUniqueUsername(username: string): Promise<void> {
|
|
|
|
|
+ const existing = await this.repository.findOne({ where: { username } });
|
|
|
|
|
+ if (existing) {
|
|
|
|
|
+ throw new Error('用户名已存在');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+### 自定义路由
|
|
|
|
|
+
|
|
|
|
|
+```typescript
|
|
|
|
|
+const customRoutes = new OpenAPIHono();
|
|
|
|
|
+
|
|
|
|
|
+// 使用通用 CRUD 路由
|
|
|
|
|
+customRoutes.route('/', createCrudRoutes(crudOptions));
|
|
|
|
|
+
|
|
|
|
|
+// 添加自定义路由
|
|
|
|
|
+customRoutes.post('/bulk-create', async (c) => {
|
|
|
|
|
+ // 自定义批量创建逻辑
|
|
|
|
|
+});
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+## 常见问题解答
|
|
|
|
|
+
|
|
|
|
|
+### Q: 如何处理复杂的业务逻辑?
|
|
|
|
|
+A: 继承 `ConcreteCrudService` 并添加自定义方法,或创建独立的业务服务类。
|
|
|
|
|
+
|
|
|
|
|
+### Q: 如何实现软删除?
|
|
|
|
|
+A: 在实体中添加 `deletedAt` 字段,重写 `delete` 方法实现软删除逻辑。
|
|
|
|
|
+
|
|
|
|
|
+### Q: 如何优化关联查询性能?
|
|
|
|
|
+A: 使用 `relations` 参数指定需要的关联关系,避免不必要的关联加载。
|
|
|
|
|
+
|
|
|
|
|
+### Q: 如何处理文件上传等非标准操作?
|
|
|
|
|
+A: 创建独立的文件管理服务,或扩展 CRUD 服务添加文件处理逻辑。
|
|
|
|
|
+
|
|
|
|
|
+## 相关文档
|
|
|
|
|
+
|
|
|
|
|
+- [主架构文档](../architecture.md)
|
|
|
|
|
+- [API 设计规范](./api-design-integration.md)
|
|
|
|
|
+- [数据模型规范](./data-model-schema-changes.md)
|
|
|
|
|
+- [测试策略](./testing-strategy.md)
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**文档状态**: 正式版
|
|
|
|
|
+**下次评审**: 2025-11-15
|