|
@@ -419,6 +419,231 @@ if (resource.userId !== user.id) {
|
|
|
- 测试所有可能的错误场景
|
|
- 测试所有可能的错误场景
|
|
|
- 验证权限控制和业务逻辑
|
|
- 验证权限控制和业务逻辑
|
|
|
|
|
|
|
|
|
|
+## 路由聚合链式规范
|
|
|
|
|
+
|
|
|
|
|
+### 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<AuthContext>()
|
|
|
|
|
+ .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. 自定义中间件
|
|
### 1. 自定义中间件
|