--- description: "通用curd扩展路由开发指令" --- # 通用CRUD扩展路由开发指令 本指令基于通用CRUD规范,指导如何为已存在的通用CRUD路由添加自定义扩展路由,采用模块化方式保持代码清晰。 ## 适用场景 当通用CRUD提供的标准路由(GET /, POST /, GET /{id}, PUT /{id}, DELETE /{id})无法满足业务需求时,需要添加自定义业务路由。 ## 开发流程 ### 1. **定位现有通用CRUD路由文件** 找到对应的通用CRUD路由文件,通常位于: - `src/server/api/[实体名]/index.ts` ### 2. **创建扩展路由文件** 为每个扩展功能创建单独的路由文件: ``` src/server/api/your-entity/ ├── index.ts # 聚合路由(已存在) ├── batch/ # 新增 - 批量操作 │ └── delete.ts # 批量删除 ├── [id]/ # 新增 - 单条记录扩展操作 │ ├── status.ts # 状态更新 │ ├── toggle.ts # 状态切换 │ └── audit.ts # 审核操作 ├── export.ts # 新增 - 数据导出 ├── import.ts # 新增 - 数据导入 ├── stats.ts # 新增 - 统计信息 └── upload.ts # 新增 - 文件上传 ``` ### 3. **创建独立扩展路由文件** #### 3.1 批量删除路由 - `batch/delete.ts` ```typescript import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { z } from '@hono/zod-openapi'; import { ErrorSchema } from '@/server/utils/errorHandler'; import { AppDataSource } from '@/server/data-source'; import { YourEntityService } from '@/server/modules/your-module/your-entity.service'; import { AuthContext } from '@/server/types/context'; import { authMiddleware } from '@/server/middleware/auth.middleware'; const routeDef = createRoute({ method: 'delete', path: '/', middleware: [authMiddleware], request: { body: { content: { 'application/json': { schema: z.object({ ids: z.array(z.number().int().positive()).openapi({ description: '要删除的ID列表', example: [1, 2, 3] }) }) } } } }, responses: { 200: { description: '批量删除成功', content: { 'application/json': { schema: z.object({ deletedCount: z.number().openapi({ example: 3, description: '删除的记录数' }) }) } } }, 400: { description: '请求参数错误', content: { 'application/json': { schema: ErrorSchema } } }, 500: { description: '服务器错误', content: { 'application/json': { schema: ErrorSchema } } } } }); const app = new OpenAPIHono().openapi(routeDef, async (c) => { try { const { ids } = await c.req.json(); const service = new YourEntityService(AppDataSource); let deletedCount = 0; for (const id of ids) { const result = await service.delete(id); if (result) deletedCount++; } return c.json({ deletedCount }, 200); } catch (error) { return c.json({ code: 500, message: '批量删除失败' }, 500); } }); export default app; ``` #### 3.2 状态更新路由 - `[id]/status.ts` ```typescript import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { z } from '@hono/zod-openapi'; import { YourEntitySchema } from '@/server/modules/your-module/your-entity.schema'; import { parseWithAwait } from '@/server/utils/parseWithAwait'; import { ErrorSchema } from '@/server/utils/errorHandler'; import { AppDataSource } from '@/server/data-source'; import { YourEntityService } from '@/server/modules/your-module/your-entity.service'; import { AuthContext } from '@/server/types/context'; import { authMiddleware } from '@/server/middleware/auth.middleware'; const routeDef = createRoute({ method: 'patch', path: '/', middleware: [authMiddleware], request: { params: z.object({ id: z.string().openapi({ param: { name: 'id', in: 'path' }, example: '1', description: '记录ID' }) }), body: { content: { 'application/json': { schema: z.object({ status: z.number().openapi({ example: 1, description: '新状态值' }) }) } } } }, responses: { 200: { description: '状态更新成功', content: { 'application/json': { schema: YourEntitySchema } } }, 404: { description: '记录不存在', content: { 'application/json': { schema: ErrorSchema } } }, 500: { description: '服务器错误', content: { 'application/json': { schema: ErrorSchema } } } } }); const app = new OpenAPIHono().openapi(routeDef, async (c) => { try { const { id } = c.req.valid('param'); const { status } = await c.req.json(); const service = new YourEntityService(AppDataSource); const result = await service.update(Number(id), { status }); if (!result) { return c.json({ code: 404, message: '记录不存在' }, 404); } // 使用 parseWithAwait 处理响应数据 const validatedResult = await parseWithAwait(YourEntitySchema, result); return c.json(validatedResult, 200); } catch (error) { return c.json({ code: 500, message: '状态更新失败' }, 500); } }); export default app; ``` #### 3.3 数据导出路由 - `export.ts` ```typescript import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { z } from '@hono/zod-openapi'; import { ErrorSchema } from '@/server/utils/errorHandler'; import { AppDataSource } from '@/server/data-source'; import { YourEntityService } from '@/server/modules/your-module/your-entity.service'; import { YourEntitySchema } from '@/server/modules/your-module/your-entity.schema'; import { parseWithAwait } from '@/server/utils/parseWithAwait'; import { AuthContext } from '@/server/types/context'; import { authMiddleware } from '@/server/middleware/auth.middleware'; const routeDef = createRoute({ method: 'get', path: '/', middleware: [authMiddleware], request: { query: z.object({ format: z.enum(['csv', 'xlsx']).default('csv').openapi({ description: '导出格式', example: 'csv' }), keyword: z.string().optional().openapi({ description: '搜索关键词', example: '测试' }), filters: z.string().optional().openapi({ description: '筛选条件(JSON字符串)', example: '{"status":1}' }) }) }, responses: { 200: { description: '导出文件', content: { 'text/csv': { schema: z.string() }, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': { schema: z.any() } } } } }); const app = new OpenAPIHono().openapi(routeDef, async (c) => { try { const { format, keyword, filters } = c.req.valid('query'); const service = new YourEntityService(AppDataSource); let filterObj = {}; if (filters) { try { filterObj = JSON.parse(filters); } catch (e) { return c.json({ code: 400, message: '筛选条件格式错误' }, 400); } } const [data] = await service.getList(1, 1000, keyword, undefined, filterObj); // 使用 parseWithAwait 处理数据格式 const validatedData = await parseWithAwait(z.array(YourEntitySchema), data); if (format === 'csv') { const csv = convertToCSV(validatedData); return new Response(csv, { headers: { 'Content-Type': 'text/csv', 'Content-Disposition': 'attachment; filename="export.csv"' } }); } return c.json({ code: 400, message: '不支持的导出格式' }, 400); } catch (error) { return c.json({ code: 500, message: '导出失败' }, 500); } }); export default app; function convertToCSV(data: any[]): string { if (!data || data.length === 0) return ''; const headers = Object.keys(data[0]); const csvHeaders = headers.join(','); const csvRows = data.map(row => headers.map(header => { const value = row[header]; return typeof value === 'string' && value.includes(',') ? `"${value}"` : value; }).join(',') ); return [csvHeaders, ...csvRows].join('\n'); } ``` ### 4. **聚合所有路由** 在 `index.ts` 中聚合基础CRUD路由和所有扩展路由: ```typescript // src/server/api/your-entity/index.ts import { OpenAPIHono } from '@hono/zod-openapi'; 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'; // 导入基础路由和各扩展路由 import batchDeleteRoute from './batch/delete'; import statusUpdateRoute from './[id]/status'; import exportRoute from './export'; // 1. 创建基础CRUD路由 const yourEntityRoutes = createCrudRoutes({ entity: YourEntity, createSchema: CreateYourEntityDto, updateSchema: UpdateYourEntityDto, getSchema: YourEntitySchema, listSchema: YourEntitySchema, searchFields: ['name', 'description'], middleware: [authMiddleware] }); // 2. 聚合所有路由(保持链式) const app = new OpenAPIHono() .route('/batch', batchDeleteRoute) // 批量操作路由 .route('/:id/status', statusUpdateRoute) // 状态更新路由 .route('/export', exportRoute) // 导出路由 .route('/', yourEntityRoutes); // 基础CRUD路由必需放最后,不然自定义路由会覆盖掉基础CRUD路由 // 3. 导出聚合后的路由 export default app; ``` ## 常见扩展场景 ### 1. **批量操作** - 批量删除:`DELETE /your-entity/batch` - 批量更新状态:`PATCH /your-entity/batch/status` - 批量导入:`POST /your-entity/import` ### 2. **数据统计** - 获取统计信息:`GET /your-entity/stats` - 获取图表数据:`GET /your-entity/chart-data` ### 3. **文件相关** - 上传文件:`POST /your-entity/upload` - 下载文件:`GET /your-entity/download/{id}` - 导出数据:`GET /your-entity/export` ### 4. **状态管理** - 状态切换:`PATCH /your-entity/{id}/toggle-status` - 审核操作:`POST /your-entity/{id}/audit` ### 5. **关联操作** - 获取关联数据:`GET /your-entity/{id}/related-data` - 更新关联关系:`PUT /your-entity/{id}/relations` ## 命名规范 - **路径命名**:使用RESTful风格,动词用HTTP方法表示 - **批量操作**:使用复数名词,如 `/batch`, `/import`, `/export` - **状态变更**:使用 PATCH 方法,路径中体现操作,如 `/status`, `/toggle` - **自定义方法**:避免在路径中使用动词,用名词+参数表示 ## 扩展路由 Schema 文件规范 ### Schema 文件位置 所有扩展路由的 Zod Schema 定义必须遵循以下文件位置规范: ``` src/server/modules/[模块名]/ ├── [实体名].entity.ts # 实体定义 ├── [实体名].schema.ts # 实体Schema定义(已存在) └── schemas/ # 扩展路由专用Schema目录(新增) ├── batch/ # 批量操作Schema │ └── delete.schema.ts ├── [id]/ # 单条记录操作Schema │ ├── status.schema.ts │ ├── toggle.schema.ts │ └── audit.schema.ts ├── export.schema.ts # 导出操作Schema ├── import.schema.ts # 导入操作Schema └── stats.schema.ts # 统计操作Schema ``` ### Schema 文件命名规范 - **文件名**:`[操作名].schema.ts` - **导出**:必须包含完整的请求/响应Schema定义 - **引用**:在扩展路由文件中直接引用对应的Schema文件 ### Schema 文件示例 #### 批量删除Schema - `schemas/batch/delete.schema.ts` ```typescript import { z } from '@hono/zod-openapi'; // 请求Schema export const BatchDeleteRequestSchema = z.object({ ids: z.array(z.number().int().positive()).openapi({ description: '要删除的ID列表', example: [1, 2, 3] }) }); // 响应Schema export const BatchDeleteResponseSchema = z.object({ deletedCount: z.number().openapi({ example: 3, description: '删除的记录数' }) }); // 类型定义 export type BatchDeleteRequest = z.infer; export type BatchDeleteResponse = z.infer; ``` #### 状态更新Schema - `schemas/[id]/status.schema.ts` ```typescript import { z } from '@hono/zod-openapi'; import { YourEntitySchema } from '../../your-entity.schema'; // 路径参数Schema export const StatusUpdateParamsSchema = z.object({ id: z.string().openapi({ param: { name: 'id', in: 'path' }, example: '1', description: '记录ID' }) }); // 请求体Schema export const StatusUpdateBodySchema = z.object({ status: z.number().openapi({ example: 1, description: '新状态值' }) }); // 响应Schema(复用实体Schema) export const StatusUpdateResponseSchema = YourEntitySchema; // 类型定义 export type StatusUpdateParams = z.infer; export type StatusUpdateBody = z.infer; export type StatusUpdateResponse = z.infer; ``` ### 在扩展路由中的使用方式 #### 引用Schema文件 ```typescript // src/server/api/your-entity/batch/delete.ts import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { BatchDeleteRequestSchema, BatchDeleteResponseSchema } from '@/server/modules/your-module/schemas/batch/delete.schema'; // ...其他导入 ``` ### 最佳实践 1. **Schema复用**:尽量复用实体已有的Schema定义 2. **类型安全**:所有Schema必须包含完整的OpenAPI元数据 3. **模块化**:每个扩展路由对应独立的Schema文件 4. **命名一致**:Schema文件名与路由功能保持一致 5. **导出规范**:同时导出Schema和对应的TypeScript类型 ## 注意事项 1. **模块化设计**:每个扩展功能单独一个文件,保持代码清晰 2. **路径一致性**:扩展路由的路径要与聚合时的路径匹配 3. **类型安全**:为所有自定义路由定义完整的OpenAPI schema 4. **错误处理**:统一使用标准错误响应格式 5. **权限控制**:为敏感操作添加适当的中间件 6. **性能考虑**:批量操作要考虑事务处理和性能优化 7. **Schema管理**:所有Schema必须放在指定的schemas目录下 8. **版本兼容**:Schema变更要保持向后兼容性 9. **数据验证**:所有查询类路由必须使用 `parseWithAwait` 处理响应数据,确保类型安全 ## parseWithAwait 使用规范 ### 概述 `parseWithAwait` 是通用CRUD模块提供的数据验证工具,用于确保返回数据的类型安全,支持异步验证和转换。 ### 使用场景 所有涉及数据查询和返回的扩展路由都应使用 `parseWithAwait` 处理响应数据。 ### 基本用法 ```typescript import { parseWithAwait } from '@/server/utils/parseWithAwait'; // 验证单个实体 const validatedEntity = await parseWithAwait(YourEntitySchema, entityData); // 验证实体数组 const validatedEntities = await parseWithAwait(z.array(YourEntitySchema), entitiesData); ``` ### 集成示例 ```typescript // 在扩展路由中使用 const app = new OpenAPIHono().openapi(routeDef, async (c) => { try { const data = await yourService.getList(); // 使用 parseWithAwait 确保数据格式正确 const validatedData = await parseWithAwait(z.array(YourEntitySchema), data); return c.json({ data: validatedData, pagination: { total, current: page, pageSize } }, 200); } catch (error) { return c.json({ code: 500, message: '获取数据失败' }, 500); } }); ``` ### 优势 - **类型安全**:确保返回数据完全符合Zod schema定义 - **异步支持**:支持异步验证和转换操作 - **错误处理**:提供详细的验证错误信息 - **性能优化**:避免运行时类型错误 - **向后兼容**:与现有代码完全兼容 ### 最佳实践 1. **所有查询路由**:GET请求返回数据前必须使用 `parseWithAwait` 2. **列表查询**:使用 `z.array(EntitySchema)` 格式验证数组 3. **单条查询**:直接使用实体Schema验证单个对象 4. **错误处理**:捕获并适当处理验证错误 ## 验证步骤 1. 创建独立的扩展路由文件 2. 实现各路由的业务逻辑 3. 在 index.ts 中聚合所有路由 4. 测试所有API端点 5. 验证RPC客户端能正确识别所有路由 6. 更新前端API客户端(如需要)