non-generic-crud-standards.md 13 KB

非通用 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 结尾
  • 返回聚合数据而非分页列表
  • 包含完整的错误处理

代码模板:

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筛选
  • 返回标准化的分页响应格式

代码模板:

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}
  • 包含完整的权限验证
  • 返回操作结果

代码模板:

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() 配置,包含示例和描述
  • 设置合理的默认值和验证规则

    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 配置指定参数位置

    const paramsSchema = z.object({
    id: z.coerce.number<number>().openapi({
    param: { name: 'id', in: 'path' },
    example: 1,
    description: '实体ID'
    })
    });
    

3. 响应 Schema

  • 使用标准化的响应格式
  • 包含 successdatamessage 字段
  • 为分页数据包含 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()
    });
    

错误处理规范

1. 标准错误响应

所有非通用 CRUD 路由必须使用统一的错误处理:

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. 资源所有权验证

  • 验证用户对资源的访问权限
  • 在服务层或路由层实现权限检查
  • 返回明确的权限错误信息

    // 检查资源所有权
    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. 自定义中间件

对于需要特殊权限验证的路由,可以创建自定义中间件:

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. 批量操作路由

对于批量操作,创建专门的批量处理路由:

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