# 非通用 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. 自定义中间件 对于需要特殊权限验证的路由,可以创建自定义中间件: ```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