Просмотр исходного кода

📝 docs(architecture): add route aggregation and chaining standards

- document single file aggregation pattern for simple modules
- document mixed route aggregation pattern for complex modules
- define route registration order and priority rules
- add service instantiation and testing best practices
- specify error handling and file organization standards

♻️ refactor(api): adjust passenger service instantiation

- move passengerService from module level to route handler level
- instantiate service within each route handler to improve testability
- remove singleton service instance to follow new architecture standards
yourname 3 месяцев назад
Родитель
Сommit
5cd4b21002

+ 225 - 0
docs/architecture/non-generic-crud-standards.md

@@ -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. 自定义中间件

+ 6 - 1
packages/server/src/api/passengers/index.ts

@@ -246,11 +246,11 @@ const setDefaultRoute = createRoute({
 });
 
 // 注册路由处理函数
-const passengerService = new PassengerService();
 
 const app = new OpenAPIHono<AuthContext>()
   .openapi(listRoute, async (c) => {
     try {
+      const passengerService = new PassengerService();
       const user = c.get('user');
       const query = c.req.valid('query');
 
@@ -272,6 +272,7 @@ const app = new OpenAPIHono<AuthContext>()
   })
   .openapi(createRouteDef, async (c) => {
     try {
+      const passengerService = new PassengerService();
       const user = c.get('user');
       const data = c.req.valid('json');
 
@@ -293,6 +294,7 @@ const app = new OpenAPIHono<AuthContext>()
   })
   .openapi(getRouteDef, async (c) => {
     try {
+      const passengerService = new PassengerService();
       const user = c.get('user');
       const { id } = c.req.valid('param');
 
@@ -318,6 +320,7 @@ const app = new OpenAPIHono<AuthContext>()
   })
   .openapi(updateRouteDef, async (c) => {
     try {
+      const passengerService = new PassengerService();
       const user = c.get('user');
       const { id } = c.req.valid('param');
       const data = c.req.valid('json');
@@ -347,6 +350,7 @@ const app = new OpenAPIHono<AuthContext>()
   })
   .openapi(deleteRouteDef, async (c) => {
     try {
+      const passengerService = new PassengerService();
       const user = c.get('user');
       const { id } = c.req.valid('param');
 
@@ -372,6 +376,7 @@ const app = new OpenAPIHono<AuthContext>()
   })
   .openapi(setDefaultRoute, async (c) => {
     try {
+      const passengerService = new PassengerService();
       const user = c.get('user');
       const { id } = c.req.valid('param');