# 非通用 CRUD 路由规范 ## 版本信息 | 版本 | 日期 | 描述 | 作者 | |------|------|------|------| | 1.0 | 2025-10-23 | 创建非通用 CRUD 路由规范文档 | Winston | ## 概述 非通用 CRUD 路由是指那些不遵循标准 CRUD 操作模式的 API 端点,包括统计查询、业务操作、复杂查询等场景。这些路由需要特定的业务逻辑和自定义处理,无法通过通用 CRUD 服务自动生成。 ## 设计原则 ### 核心原则 - **业务导向**: 路由设计应反映业务需求而非技术实现 - **一致性**: 保持与通用 CRUD 相同的错误处理、认证和文档标准 - **类型安全**: 使用 Zod schema 确保输入输出类型安全 - **文档完整**: 自动生成 OpenAPI 文档,包含完整的请求响应示例 ### 架构模式 - **单一职责**: 每个路由文件专注于特定业务功能 - **服务分离**: 业务逻辑封装在服务层,路由层只负责 HTTP 处理 - **中间件复用**: 复用现有的认证、授权中间件 ## 非通用 CRUD 路由类型 ### 1. 统计查询路由 (Stats Routes) **适用场景**: 获取聚合数据、统计信息、报表数据 **示例文件**: `packages/server/src/api/admin/orders/stats.ts` **规范要求**: - 使用 `GET` 方法 - 路径以 `/stats` 或 `/analytics` 结尾 - 返回聚合数据而非分页列表 - 包含完整的错误处理 **代码模板**: ```typescript import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { authMiddleware } from '../../../middleware/auth.middleware'; import { adminMiddleware } from '../../../middleware/admin.middleware'; import { OrderService } from '../../../modules/orders/order.service'; import { OrderStatsSchema } from '../../../modules/orders/order.schema'; import { ErrorSchema } from '../../../utils/errorHandler'; // 统计路由定义 const statsRoute = createRoute({ method: 'get', path: '/', middleware: [authMiddleware, adminMiddleware], responses: { 200: { description: '成功获取统计信息', content: { 'application/json': { schema: OrderStatsSchema } } }, 401: { description: '未授权', content: { 'application/json': { schema: ErrorSchema } } }, 403: { description: '权限不足', content: { 'application/json': { schema: ErrorSchema } } }, 500: { description: '服务器错误', content: { 'application/json': { schema: ErrorSchema } } } } }); const app = new OpenAPIHono().openapi(statsRoute, async (c) => { try { const orderService = new OrderService(); const stats = await orderService.getOrderStats(); return c.json(stats, 200); } catch (error) { console.error('获取统计信息失败:', error); return c.json({ code: 500, message: error instanceof Error ? error.message : '获取统计信息失败' }, 500); } }); export default app; ``` ### 2. 层级数据查询路由 (Hierarchical Data Routes) **适用场景**: 树形结构数据、省市区数据、分类数据 **示例文件**: `packages/server/src/api/areas/index.ts` **规范要求**: - 使用 `GET` 方法 - 路径反映数据层级关系(如 `/provinces`, `/cities`, `/districts`) - 支持分页和父级ID筛选 - 返回标准化的分页响应格式 **代码模板**: ```typescript import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { z } from '@hono/zod-openapi'; import { AreaService } from '../../modules/areas/area.service'; // 查询参数Schema const getCitiesSchema = z.object({ provinceId: z.coerce.number().int().positive('省份ID必须为正整数').openapi({ example: 1, description: '省份ID' }), page: z.coerce.number().int().min(1).default(1).openapi({ example: 1, description: '页码' }), pageSize: z.coerce.number().int().min(1).max(100).default(50).openapi({ example: 50, description: '每页数量' }) }); // 响应Schema const citiesResponseSchema = z.object({ success: z.boolean(), data: z.object({ cities: z.array(areaResponseSchema), pagination: z.object({ page: z.number(), pageSize: z.number(), total: z.number(), totalPages: z.number() }) }), message: z.string() }); // 创建城市查询路由 const getCitiesRoute = createRoute({ method: 'get', path: '/cities', request: { query: getCitiesSchema }, responses: { 200: { description: '获取城市列表成功', content: { 'application/json': { schema: citiesResponseSchema } } }, 400: { description: '参数错误', content: { 'application/json': { schema: errorSchema } } }, 500: { description: '获取城市列表失败', content: { 'application/json': { schema: errorSchema } } } } }); const app = new OpenAPIHono() .openapi(getCitiesRoute, async (c) => { try { const { provinceId, page, pageSize } = c.req.valid('query'); const areaService = new AreaService(); // 业务逻辑处理 const allCities = await areaService.getAreaTreeByLevel(AreaLevel.CITY); const cities = allCities.filter(city => city.parentId === provinceId); // 分页处理 const startIndex = (page - 1) * pageSize; const endIndex = startIndex + pageSize; const paginatedCities = cities.slice(startIndex, endIndex); return c.json({ success: true, data: { cities: paginatedCities, pagination: { page, pageSize, total: cities.length, totalPages: Math.ceil(cities.length / pageSize) } }, message: '获取城市列表成功' }, 200); } catch (error) { console.error('获取城市列表失败:', error); return c.json({ code: 500, message: error instanceof Error ? error.message : '获取城市列表失败' }, 500); } }); export default app; ``` ### 3. 业务操作路由 (Business Action Routes) **适用场景**: 设置默认项、状态变更、批量操作等业务动作 **示例文件**: `packages/server/src/api/passengers/index.ts` **规范要求**: - 使用 `POST` 方法表示动作执行 - 路径格式为 `/{id}/{action-name}` - 包含完整的权限验证 - 返回操作结果 **代码模板**: ```typescript import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { z } from '@hono/zod-openapi'; import { authMiddleware } from '../../middleware/auth.middleware'; import { PassengerService } from '../../modules/passengers/passenger.service'; // 设置默认乘客路由 const setDefaultRoute = createRoute({ method: 'post', path: '/{id}/set-default', middleware: [authMiddleware], request: { params: z.object({ id: z.coerce.number().openapi({ param: { name: 'id', in: 'path' }, example: 1, description: '乘客ID' }) }) }, responses: { 200: { description: '设置默认乘客成功', content: { 'application/json': { schema: PassengerResponseSchema } } }, 401: { description: '未授权', content: { 'application/json': { schema: ErrorSchema } } }, 403: { description: '无权设置', content: { 'application/json': { schema: ErrorSchema } } }, 404: { description: '乘客不存在', content: { 'application/json': { schema: ErrorSchema } } }, 500: { description: '服务器错误', content: { 'application/json': { schema: ErrorSchema } } } } }); const app = new OpenAPIHono() .openapi(setDefaultRoute, async (c) => { try { const user = c.get('user'); const { id } = c.req.valid('param'); // 权限验证 const existingPassenger = await passengerService.getPassengerById(id); if (!existingPassenger) { return c.json({ code: 404, message: '乘客不存在' }, 404); } if (existingPassenger.userId !== user.id) { return c.json({ code: 403, message: '无权设置该乘客为默认' }, 403); } // 执行业务操作 const result = await passengerService.setDefaultPassenger(user.id, id); if (!result) { return c.json({ code: 500, message: '设置默认乘客失败' }, 500); } return c.json(result, 200); } catch (error) { console.error('设置默认乘客失败:', error); return c.json({ code: 500, message: error instanceof Error ? error.message : '设置默认乘客失败' }, 500); } }); export default app; ``` ## Schema 设计规范 ### 1. 查询参数 Schema - 使用 `z.coerce` 处理类型转换 - 为所有参数添加 `.openapi()` 配置,包含示例和描述 - 设置合理的默认值和验证规则 ```typescript const querySchema = z.object({ page: z.coerce.number().int().positive().default(1).openapi({ example: 1, description: '页码,从1开始' }), pageSize: z.coerce.number().int().positive().default(20).openapi({ example: 20, description: '每页数量' }), keyword: z.string().optional().openapi({ example: '搜索关键词', description: '搜索关键词' }) }); ``` ### 2. 路径参数 Schema - 使用 `z.coerce` 处理类型转换 - 包含 `param` 配置指定参数位置 ```typescript const paramsSchema = z.object({ id: z.coerce.number().openapi({ param: { name: 'id', in: 'path' }, example: 1, description: '实体ID' }) }); ``` ### 3. 响应 Schema - 使用标准化的响应格式 - 包含 `success`、`data`、`message` 字段 - 为分页数据包含 `pagination` 信息 ```typescript const responseSchema = z.object({ success: z.boolean(), data: z.object({ // 业务数据字段 items: z.array(itemSchema), pagination: z.object({ page: z.number(), pageSize: z.number(), total: z.number(), totalPages: z.number() }).optional() }), message: z.string() }); ``` ## 错误处理规范 ### 1. 标准错误响应 所有非通用 CRUD 路由必须使用统一的错误处理: ```typescript return c.json({ code: 500, message: error instanceof Error ? error.message : '操作失败' }, 500); ``` ### 2. 常见错误状态码 - `400`: 参数错误 - `401`: 未授权 - `403`: 权限不足 - `404`: 资源不存在 - `500`: 服务器错误 ### 3. 错误日志记录 - 使用 `console.error` 记录详细错误信息 - 包含操作上下文信息 - 避免在生产环境暴露敏感信息 ## 权限控制规范 ### 1. 中间件使用 - 复用现有的认证中间件 `authMiddleware` - 管理员权限使用 `adminMiddleware` - 自定义权限中间件应遵循相同模式 ### 2. 资源所有权验证 - 验证用户对资源的访问权限 - 在服务层或路由层实现权限检查 - 返回明确的权限错误信息 ```typescript // 检查资源所有权 if (resource.userId !== user.id) { return c.json({ code: 403, message: '无权访问该资源' }, 403); } ``` ## 最佳实践 ### 1. 文件组织 - 每个非通用路由功能创建独立文件 - 文件名反映功能用途(如 `stats.ts`, `actions.ts`) - 在模块的 `index.ts` 中统一导出 ### 2. 代码结构 - 路由定义在前,处理函数在后 - 使用 try-catch 包装所有业务逻辑 - 保持错误处理的一致性 ### 3. 性能考虑 - 为统计查询添加适当的缓存策略 - 复杂查询使用数据库索引优化 - 避免 N+1 查询问题 ### 4. 测试策略 - 为每个非通用路由编写集成测试 - 测试所有可能的错误场景 - 验证权限控制和业务逻辑 ## 路由聚合链式规范 ### 1. 路由聚合模式 #### 1.1 单一文件聚合模式 适用于功能相对简单的模块,所有路由定义在单个文件中: **示例文件**: `packages/server/src/api/passengers/index.ts` **规范要求**: - 所有路由定义在同一文件中 - 使用 `.openapi()` 方法链式注册路由 - 保持路由定义的顺序性(CRUD 操作在前,业务操作在后) - 统一的服务实例管理 **代码模板**: ```typescript import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { authMiddleware } from '../../middleware/auth.middleware'; import { PassengerService } from '../../modules/passengers/passenger.service'; // 服务实例化(单例模式) const passengerService = new PassengerService(); // 路由定义 const listRoute = createRoute({ /* ... */ }); const createRouteDef = createRoute({ /* ... */ }); const getRouteDef = createRoute({ /* ... */ }); const updateRouteDef = createRoute({ /* ... */ }); const deleteRouteDef = createRoute({ /* ... */ }); const setDefaultRoute = createRoute({ /* ... */ }); // 链式路由注册 const app = new OpenAPIHono() .openapi(listRoute, async (c) => { /* ... */ }) .openapi(createRouteDef, async (c) => { /* ... */ }) .openapi(getRouteDef, async (c) => { /* ... */ }) .openapi(updateRouteDef, async (c) => { /* ... */ }) .openapi(deleteRouteDef, async (c) => { /* ... */ }) .openapi(setDefaultRoute, async (c) => { /* ... */ }); export default app; ``` #### 1.2 混合路由聚合模式 适用于需要结合通用 CRUD 和自定义业务路由的模块: **示例文件**: `packages/server/src/api/users/index.ts` **规范要求**: - 使用 `.route()` 方法聚合多个路由应用 - 通用 CRUD 路由与自定义业务路由分离 - 自定义路由优先于通用路由 - 明确的中间件配置 **代码模板**: ```typescript import { OpenAPIHono } from '@hono/zod-openapi'; import { createCrudRoutes } from '../../utils/generic-crud.routes'; import { UserEntity } from '../../modules/users/user.entity'; import { authMiddleware } from '../../middleware/auth.middleware'; import customRoutes from './custom'; // 创建通用CRUD路由配置 const userCrudRoutes = createCrudRoutes({ entity: UserEntity, createSchema: CreateUserDto, updateSchema: UpdateUserDto, getSchema: UserSchema, listSchema: UserSchema.omit({ password: true }), searchFields: ['username', 'nickname', 'phone', 'email'], relations: ['roles'], middleware: [authMiddleware], readOnly: true // 创建/更新/删除使用自定义路由 }); // 创建混合路由应用 const app = new OpenAPIHono() .route('/', customRoutes) // 自定义业务路由(创建/更新/删除) .route('/', userCrudRoutes); // 通用CRUD路由(列表查询和获取详情) export default app; ``` ### 2. 路由注册顺序规范 #### 2.1 路由优先级规则 - **自定义路由优先**: 自定义业务路由应注册在通用路由之前 - **具体路径优先**: 具体路径的路由应注册在通用路径之前 - **中间件顺序**: 全局中间件在前,路由特定中间件在后 #### 2.2 推荐注册顺序 ```typescript const app = new OpenAPIHono() // 1. 业务操作路由(POST/PUT/DELETE) .openapi(createRoute, createHandler) .openapi(updateRoute, updateHandler) .openapi(deleteRoute, deleteHandler) // 2. 自定义业务动作路由 .openapi(setDefaultRoute, setDefaultHandler) .openapi(bulkUpdateRoute, bulkUpdateHandler) // 3. 查询路由(GET) .openapi(listRoute, listHandler) .openapi(getRoute, getHandler) // 4. 统计和聚合路由 .openapi(statsRoute, statsHandler); ``` ### 3. 服务实例管理规范 #### 3.1 路由级别服务实例化 - 在路由处理函数中实例化服务类 - 避免模块级别的单例,便于测试时 mock - 保持代码的可测试性和灵活性 ```typescript // ✅ 推荐:路由级别实例化(便于测试) const app = new OpenAPIHono() .openapi(listRoute, async (c) => { const passengerService = new PassengerService(); // 路由级别实例化 const result = await passengerService.getPassengers(params); return c.json(result, 200); }); // ❌ 不推荐:模块级别单例(测试困难) const passengerService = new PassengerService(); // 模块级别单例 const app = new OpenAPIHono() .openapi(listRoute, async (c) => { const result = await passengerService.getPassengers(params); return c.json(result, 200); }); ``` #### 3.2 测试时的 Mock 方法 在测试时,可以直接使用 `vi.mocked()` 来 mock 服务类: ```typescript // 测试示例 import { PassengerService } from '../../modules/passengers/passenger.service'; // Mock 服务类 vi.mock('../../modules/passengers/passenger.service'); // 在测试中使用 const mockGetPassengers = vi.mocked(PassengerService.prototype.getPassengers); mockGetPassengers.mockResolvedValue({ /* mock data */ }); ``` ### 4. 错误处理统一规范 #### 4.1 统一错误响应格式 所有路由应使用统一的错误处理模式: ```typescript .openapi(route, async (c) => { try { // 业务逻辑 const result = await service.method(params); return c.json(result, 200); } catch (error) { console.error('操作失败:', error); return c.json({ code: 500, message: error instanceof Error ? error.message : '操作失败' }, 500); } }) ``` #### 4.2 权限验证模式 对于需要资源所有权验证的路由: ```typescript .openapi(route, async (c) => { try { const user = c.get('user'); const { id } = c.req.valid('param'); // 资源存在性检查 const resource = await service.getById(id); if (!resource) { return c.json({ code: 404, message: '资源不存在' }, 404); } // 所有权验证 if (resource.userId !== user.id) { return c.json({ code: 403, message: '无权访问该资源' }, 403); } // 执行业务逻辑 const result = await service.method(id, data); return c.json(result, 200); } catch (error) { // 统一错误处理 } }) ``` ### 5. 文件组织规范 #### 5.1 路由文件结构 ``` packages/server/src/api/ ├── users/ │ ├── index.ts # 路由聚合入口 │ ├── custom.ts # 自定义业务路由 │ └── stats.ts # 统计路由(可选) ├── passengers/ │ └── index.ts # 单一文件聚合 └── admin/ └── orders/ ├── index.ts # 路由聚合 ├── stats.ts # 统计路由 └── actions.ts # 业务操作路由 ``` #### 5.2 导出规范 - 每个路由文件应导出默认的 OpenAPIHono 实例 - 使用 `export default app` 语法 - 确保类型安全,正确配置泛型类型 ## 扩展和自定义 ### 1. 自定义中间件 对于需要特殊权限验证的路由,可以创建自定义中间件: ```typescript const ownerMiddleware = createMiddleware(async (c, next) => { const user = c.get('user'); const { id } = c.req.param(); const resource = await resourceService.getById(id); if (!resource || resource.userId !== user.id) { return c.json({ code: 403, message: '无权访问' }, 403); } c.set('resource', resource); await next(); }); ``` ### 2. 批量操作路由 对于批量操作,创建专门的批量处理路由: ```typescript const bulkUpdateRoute = createRoute({ method: 'post', path: '/bulk-update', middleware: [authMiddleware], request: { body: { content: { 'application/json': { schema: BulkUpdateSchema } } } }, responses: { 200: { description: '批量更新成功', content: { 'application/json': { schema: BulkUpdateResponseSchema } } } } }); ``` ## 相关文档 - [通用 CRUD 规范](./generic-crud-standards.md) - [API 设计规范](./api-design-integration.md) - [数据模型规范](./data-model-schema-changes.md) - [测试策略](./testing-strategy.md) --- **文档状态**: 正式版 **下次评审**: 2025-11-23