# 通用CRUD实现规范 ## 概述 通用CRUD模块提供标准化的增删改查功能实现,通过泛型机制支持快速创建实体的RESTful API接口。本规范定义了使用通用CRUD服务和路由的标准流程和最佳实践。 ## 1. 通用CRUD服务 (GenericCrudService) ### 1.1 基础用法 通用CRUD服务提供基础的数据操作方法,所有实体服务类应继承此类: ```typescript import { GenericCrudService } from '@/server/utils/generic-crud.service'; import { DataSource } from 'typeorm'; import { YourEntity } from '@/server/modules/your-module/your-entity.entity'; export class YourEntityService extends GenericCrudService { constructor(dataSource: DataSource) { super(dataSource, YourEntity); } // 可以重写或扩展基础方法 async customMethod() { // 自定义业务逻辑 } } ``` ### 1.2 核心方法 | 方法 | 描述 | 参数 | 返回值 | |------|------|------|--------| | `getList` | 获取分页列表 | `page`, `pageSize`, `keyword`, `searchFields`, `where`, `relations`, `order` | `[T[], number]` | | `getById` | 根据ID获取单个实体 | `id: number`, `relations?: string[]` | `T \| null` | | `create` | 创建实体 | `data: DeepPartial` | `T` | | `update` | 更新实体 | `id: number`, `data: Partial` | `T \| null` | | `delete` | 删除实体 | `id: number` | `boolean` | | `createQueryBuilder` | 创建查询构建器 | `alias?: string` | `QueryBuilder` | ### 1.3 构造函数注入 必须通过构造函数注入`DataSource`,禁止直接使用全局实例: ```typescript // 正确示例 constructor(private dataSource: DataSource) { super(dataSource, YourEntity); } // 错误示例 constructor() { super(AppDataSource, YourEntity); // 禁止使用全局AppDataSource } ``` ## 2. 通用CRUD路由 (createCrudRoutes) ### 2.1 基础用法 通过`createCrudRoutes`函数快速创建标准CRUD路由: ```typescript import { createCrudRoutes } from '@/server/utils/generic-crud.routes'; import { YourEntity } from '@/server/modules/your-module/your-entity.entity'; import { YourEntitySchema, CreateYourEntityDto, UpdateYourEntityDto } from '@/server/modules/your-module/your-entity.schema'; import { authMiddleware } from '@/server/middleware/auth.middleware'; const yourEntityRoutes = createCrudRoutes({ entity: YourEntity, createSchema: CreateYourEntityDto, updateSchema: UpdateYourEntityDto, getSchema: YourEntitySchema, listSchema: YourEntitySchema, searchFields: ['name', 'description'], // 可选,指定搜索字段 relations: ['relatedEntity'], // 可选,指定关联查询关系(支持嵌套关联,如 ['relatedEntity.client']) middleware: [authMiddleware] // 可选,添加中间件 }); export default yourEntityRoutes; ``` ### 2.2 配置选项 (CrudOptions) | 参数 | 类型 | 描述 | 是否必需 | |------|------|------|----------| | `entity` | `new () => T` | 实体类构造函数 | 是 | | `createSchema` | `z.ZodSchema` | 创建实体的Zod验证 schema | 是 | | `updateSchema` | `z.ZodSchema` | 更新实体的Zod验证 schema | 是 | | `getSchema` | `z.ZodSchema` | 获取单个实体的响应 schema | 是 | | `listSchema` | `z.ZodSchema` | 获取列表的响应 schema | 是 | | `searchFields` | `string[]` | 搜索字段数组,用于关键词搜索 | 否 | | `relations` | `string[]` | 关联查询配置,指定需要关联查询的关系 | 否 | | `middleware` | `any[]` | 应用于所有CRUD路由的中间件数组 | 否 | | `relationFields` | `RelationFieldOptions` | 多对多关联字段配置,支持通过ID数组操作关联关系 | 否 | | `readOnly` | `boolean` | 只读模式,只生成GET路由,默认false | 否 | ### 2.3 生成的路由 调用`createCrudRoutes`会自动生成以下标准RESTful路由: | 方法 | 路径 | 描述 | |------|------|------| | GET | `/` | 获取实体列表(支持分页、搜索、排序、关联查询) | | POST | `/` | 创建新实体 | | GET | `/{id}` | 获取单个实体详情(支持关联查询) | | PUT | `/{id}` | 更新实体 | | DELETE | `/{id}` | 删除实体 | #### 2.3.1 只读模式 (readOnly) 通过设置 `readOnly: true` 可以创建只读路由,仅包含读取操作: | 方法 | 路径 | 描述 | |------|------|------| | GET | `/` | 获取实体列表(支持分页、搜索、排序、关联查询) | | GET | `/{id}` | 获取单个实体详情(支持关联查询) | **使用示例:** ```typescript const readOnlyRoutes = createCrudRoutes({ entity: Advertisement, createSchema: CreateAdvertisementDto, updateSchema: UpdateAdvertisementDto, getSchema: AdvertisementSchema, listSchema: AdvertisementSchema, readOnly: true, // 启用只读模式 searchFields: ['title', 'description'], relations: ['imageFile'], }); ``` **适用场景:** - 公开API接口(如游客访问的广告列表) - 只读数据展示 - 需要限制修改操作的接口 ### 2.4 路由注册 生成的路由需要在API入口文件中注册: ```typescript // src/server/api.ts import yourEntityRoutes from '@/server/api/your-entity/index'; // 注册路由 api.route('/api/v1/your-entities', yourEntityRoutes); ``` ## 3. 实体类要求 使用通用CRUD模块的实体类必须满足以下要求: 1. 必须包含主键`id`字段,类型为数字 2. 必须定义配套的Zod schema: - 实体完整schema(用于响应) - 创建DTO schema(用于创建请求验证) - 更新DTO schema(用于更新请求验证) 示例: ```typescript // your-entity.entity.ts import { Entity, PrimaryGeneratedColumn, Column, ManyToOne, OneToMany } from 'typeorm'; import { RelatedEntity } from './related-entity.entity'; @Entity('your_entity') export class YourEntity { @PrimaryGeneratedColumn({ unsigned: true }) id!: number; @Column({ name: 'name', type: 'varchar', length: 255 }) name!: string; @ManyToOne(() => RelatedEntity, related => related.yourEntities) relatedEntity!: RelatedEntity; // 其他字段... } // your-entity.schema.ts import { z } from '@hono/zod-openapi'; // Zod schemas export const YourEntitySchema = z.object({ id: z.number().int().positive().openapi({ description: '实体ID' }), name: z.string().max(255).openapi({ description: '名称', example: '示例名称' }), relatedEntity: RelatedEntitySchema, // 关联实体schema // 其他字段schema... }); export const CreateYourEntityDto = z.object({ name: z.string().max(255).openapi({ description: '名称', example: '示例名称' }), relatedEntityId: z.number().int().positive().openapi({ description: '关联实体ID', example: 1 }), // 其他创建字段schema... }); export const UpdateYourEntityDto = z.object({ name: z.string().max(255).optional().openapi({ description: '名称', example: '示例名称' }), relatedEntityId: z.number().int().positive().optional().openapi({ description: '关联实体ID', example: 1 }), // 其他更新字段schema... }); ``` ### 2.5 多对多关联字段配置 #### 2.5.1 关联字段配置 (RelationFieldOptions) 新增 `relationFields` 配置选项,用于处理多对多关联字段: ```typescript import { createCrudRoutes } from '@/server/utils/generic-crud.routes'; import { PolicyNews } from '@/server/modules/silver-users/policy-news.entity'; import { PolicyNewsSchema, CreatePolicyNewsDto, UpdatePolicyNewsDto } from '@/server/modules/silver-users/policy-news.schema'; import { File } from '@/server/modules/files/file.entity'; import { authMiddleware } from '@/server/middleware/auth.middleware'; const policyNewsRoutes = createCrudRoutes({ entity: PolicyNews, createSchema: CreatePolicyNewsDto, updateSchema: UpdatePolicyNewsDto, getSchema: PolicyNewsSchema, listSchema: PolicyNewsSchema, relations: ['files'], middleware: [authMiddleware], relationFields: { fileIds: { relationName: 'files', // 实体中的关联属性名 targetEntity: File, // 关联的目标实体类 joinTableName: 'policy_news_files' // 可选:中间表名 } } }); ``` #### 2.5.2 RelationFieldOptions 类型定义 ```typescript interface RelationFieldOptions { [fieldName: string]: { relationName: string; // 实体中的关联属性名 targetEntity: new () => any; // 关联的目标实体类 joinTableName?: string; // 中间表名(可选) }; } ``` #### 2.5.3 使用示例 **实体定义:** ```typescript @Entity('policy_news') export class PolicyNews { @PrimaryGeneratedColumn({ unsigned: true }) id!: number; @Column({ name: 'news_title', type: 'varchar', length: 255 }) newsTitle!: string; // 关联文件的多对多关系 @ManyToMany(() => File) @JoinTable({ name: 'policy_news_files', joinColumn: { name: 'policy_news_id', referencedColumnName: 'id' }, inverseJoinColumn: { name: 'file_id', referencedColumnName: 'id' } }) files?: File[]; } ``` **请求格式:** ```typescript // 创建政策资讯 POST /api/v1/policy-news { "newsTitle": "新政策解读", "fileIds": [1, 2, 3] // 关联文件ID数组 } // 更新政策资讯 PUT /api/v1/policy-news/1 { "newsTitle": "更新后的政策解读", "fileIds": [2, 4, 5] // 更新后的文件ID数组 } ``` #### 2.5.4 特性说明 - **自动处理**:系统会自动处理多对多关联的中间表操作 - **事务安全**:所有关联操作都在事务中执行 - **空数组支持**:传空数组 `[]` 表示清除所有关联 - **向后兼容**:不影响现有实体的使用方式 - **类型安全**:支持完整的TypeScript类型检查 ## 4. 高级用法 ### 4.1 自定义中间件 可以为CRUD路由添加自定义中间件,如认证和权限控制: ```typescript import { authMiddleware } from '@/server/middleware/auth.middleware'; import { permissionMiddleware } from '@/server/middleware/permission.middleware'; const yourEntityRoutes = createCrudRoutes({ // ...其他配置 middleware: [ authMiddleware, permissionMiddleware(['your_entity:read', 'your_entity:write']) ] }); ``` ### 4.2 扩展路由 生成基础CRUD路由后,可以添加自定义路由: ```typescript import { OpenAPIHono } from '@hono/zod-openapi'; const app = new OpenAPIHono() .route('/', yourEntityRoutes) .get('/custom-action', (c) => { // 自定义路由处理逻辑 return c.json({ message: '自定义操作' }); }); export default app; ``` ### 4.3 重写服务方法 当通用CRUD服务的默认实现不满足需求时,可以重写相应方法: ```typescript export class YourEntityService extends GenericCrudService { // ...构造函数 async getList( page: number = 1, pageSize: number = 10, keyword?: string, searchFields?: string[], where: Partial = {}, relations: string[] = [], order: { [P in keyof YourEntity]?: 'ASC' | 'DESC' } = {} ): Promise<[YourEntity[], number]> { // 添加自定义过滤条件 where.isDeleted = 0; // 例如:默认过滤已删除数据 return super.getList(page, pageSize, keyword, searchFields, where, relations, order); } } ``` ## 5. 错误处理 通用CRUD模块已内置标准错误处理机制: 1. 参数验证错误:返回400状态码和验证错误详情 2. 资源不存在:返回404状态码 3. 服务器错误:返回500状态码和错误消息 所有错误响应格式统一为: ```json { "code": 错误代码, "message": "错误描述", "errors": 可选的详细错误信息 } ``` ## 6. 最佳实践 1. **单一职责**:通用CRUD仅处理标准数据操作,复杂业务逻辑应在具体服务类中实现 2. **命名规范**: - 服务类:`[EntityName]Service` - 路由文件:遵循RESTful资源命名规范,使用复数形式 3. **权限控制**:始终为CRUD路由添加适当的认证和授权中间件 4. **数据验证**:确保Zod schema包含完整的验证规则和OpenAPI元数据 5. **搜索优化**:合理设置`searchFields`,避免在大表的文本字段上进行模糊搜索 6. **分页处理**:所有列表接口必须支持分页,避免返回大量数据 7. **关联查询**:使用`relations`配置时,避免过度关联导致性能问题 8. **事务管理**:复杂操作应使用事务确保数据一致性