# 非通用 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<number>().int().positive('省份ID必须为正整数').openapi({
    example: 1,
    description: '省份ID'
  }),
  page: z.coerce.number<number>().int().min(1).default(1).openapi({
    example: 1,
    description: '页码'
  }),
  pageSize: z.coerce.number<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<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<AuthContext>()
  .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<number>().int().positive().default(1).openapi({
    example: 1,
    description: '页码，从1开始'
  }),
  pageSize: z.coerce.number<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<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<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. 自定义中间件
对于需要特殊权限验证的路由，可以创建自定义中间件：

```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