| 版本 | 日期 | 描述 | 作者 |
|---|---|---|---|
| 1.0 | 2025-10-23 | 创建非通用 CRUD 路由规范文档 | Winston |
非通用 CRUD 路由是指那些不遵循标准 CRUD 操作模式的 API 端点,包括统计查询、业务操作、复杂查询等场景。这些路由需要特定的业务逻辑和自定义处理,无法通过通用 CRUD 服务自动生成。
适用场景: 获取聚合数据、统计信息、报表数据
示例文件: packages/server/src/api/admin/orders/stats.ts
规范要求:
GET 方法/stats 或 /analytics 结尾代码模板:
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;
适用场景: 树形结构数据、省市区数据、分类数据
示例文件: packages/server/src/api/areas/index.ts
规范要求:
GET 方法/provinces, /cities, /districts)代码模板:
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;
适用场景: 设置默认项、状态变更、批量操作等业务动作
示例文件: packages/server/src/api/passengers/index.ts
规范要求:
POST 方法表示动作执行/{id}/{action-name}代码模板:
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;
z.coerce 处理类型转换.openapi() 配置,包含示例和描述设置合理的默认值和验证规则
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: '搜索关键词'
})
});
z.coerce 处理类型转换包含 param 配置指定参数位置
const paramsSchema = z.object({
id: z.coerce.number<number>().openapi({
param: { name: 'id', in: 'path' },
example: 1,
description: '实体ID'
})
});
success、data、message 字段为分页数据包含 pagination 信息
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()
});
所有非通用 CRUD 路由必须使用统一的错误处理:
return c.json({
code: 500,
message: error instanceof Error ? error.message : '操作失败'
}, 500);
400: 参数错误401: 未授权403: 权限不足404: 资源不存在500: 服务器错误console.error 记录详细错误信息authMiddlewareadminMiddleware返回明确的权限错误信息
// 检查资源所有权
if (resource.userId !== user.id) {
return c.json({ code: 403, message: '无权访问该资源' }, 403);
}
stats.ts, actions.ts)index.ts 中统一导出适用于功能相对简单的模块,所有路由定义在单个文件中:
示例文件: packages/server/src/api/passengers/index.ts
规范要求:
.openapi() 方法链式注册路由代码模板:
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;
适用于需要结合通用 CRUD 和自定义业务路由的模块:
示例文件: packages/server/src/api/users/index.ts
规范要求:
.route() 方法聚合多个路由应用代码模板:
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;
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);
保持代码的可测试性和灵活性
// ✅ 推荐:路由级别实例化(便于测试)
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);
});
在测试时,可以直接使用 vi.mocked() 来 mock 服务类:
// 测试示例
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 */ });
所有路由应使用统一的错误处理模式:
.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);
}
})
对于需要资源所有权验证的路由:
.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) {
// 统一错误处理
}
})
packages/server/src/api/
├── users/
│ ├── index.ts # 路由聚合入口
│ ├── custom.ts # 自定义业务路由
│ └── stats.ts # 统计路由(可选)
├── passengers/
│ └── index.ts # 单一文件聚合
└── admin/
└── orders/
├── index.ts # 路由聚合
├── stats.ts # 统计路由
└── actions.ts # 业务操作路由
export default app 语法对于需要特殊权限验证的路由,可以创建自定义中间件:
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();
});
对于批量操作,创建专门的批量处理路由:
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 } }
}
}
});
文档状态: 正式版 下次评审: 2025-11-23