# 04 · API 与路由规范 | 项 | 内容 | | --- | --- | | 文档版本 | V1.0(2026-11-02) | | 依据 | 技术架构 §6(接口设计)、§3.2(模块划分) | | 适用范围 | `apps/api` 全部 HTTP / SSE 接口、`apps/web` 全部前端路由 | | 强制级别 | 强制 | --- ## 1 URL 与版本 - `API-01` 服务端统一前缀 `/api/v1`;版本在 **URL** 中,不使用 header 版本。 - `API-02` 资源名用**复数 kebab-case**:`/brief-cards`、`/media-tasks`、`/industry-packs`。 - `API-03` 嵌套 ≤ 2 层:`/clients/:clientId/scripts`;更深的归属用 query 过滤。 - `API-04` 动作优先用子资源表达(`POST /scripts/:id/approve` → 可接受),**禁止**在 URL 用动词拼接业务流(`/doGenerate`)。 - `API-05` 破坏性变更(删字段 / 改语义 / 改状态码)必须升 `/api/v2`;新增可选字段不升版本。 - `API-06` 对外契约由 OpenAPI 生成,`pnpm openapi:diff` 检出破坏性变更即阻断(见 [06](06-测试与CI规范.md))。 --- ## 2 请求约定 | 项 | 约定 | | --- | --- | | 认证 | `Authorization: Bearer `;refresh 走独立端点 | | 幂等 | 写接口支持 `Idempotency-Key`(24h 内同 key 返回首次结果);**任务提交必带** | | 内容类型 | `application/json; charset=utf-8`;上传走预签名直传,不经 API 中转 | | 时间 | ISO 8601 带时区(`2026-11-02T14:30:00+08:00`),**存储统一 UTC** | | ID | ULID 字符串,26 字符;请求体里的 ID 一律字符串 | | 字段命名 | JSON body 用 **camelCase**;query 用 **snake_case**(`cursor` / `limit` / `sort` / `next_cursor`,与架构文档示例一致) | | 未知字段 | 严格模式拒绝(Zod `.strict()`),避免静默丢弃 | - `API-07` 所有入参用 Zod schema 校验,Controller 不写手写校验;动态 schema(要素卡)用 Ajv + 白名单。 - `API-08` 分页:`?cursor=&limit=50`,返回 `next_cursor`;`limit` 默认 50、上限 200。 - `API-09` 排序:`?sort=-created_at`(`-` 为降序);仅允许白名单字段,非法字段 400。 - `API-10` 过滤:显式白名单参数;禁止把任意 query 透传给 Prisma。 - `API-11` 列表**禁止返回大字段**(JSONB 富文本、base64、完整分镜);产物只返回签名 URL。 --- ## 3 响应约定 **成功** - 单个资源:直接返回对象(不套 `data` 信封)。 - 列表:`{ "items": [...], "next_cursor": "..." }`;无下一页时 `next_cursor` 为 `null`。 - 创建:`201` + 资源;异步任务提交:`202` + `{ taskId, estimatedItems, estimatedCost, estimatedSeconds }`。 - 删除:`204` 无 body。 **失败**(统一结构,禁止其他形状) ```json { "code": 40302, "message": "无权访问该客户数据", "details": {}, "trace_id": "01JA..." } ``` - `API-12` `message` 面向用户、中文、不含内部实现细节;`details` 放字段级错误;`trace_id` 必填。 - `API-13` 禁止把堆栈、SQL、密钥、供应商原始报文写进响应。 --- ## 4 错误码表 | 段 | 含义 | 常用码 | | --- | --- | --- | | `400xx` | 参数校验 | `40001` 参数缺失 / 格式错误 | | `401xx` | 认证 | `40101` 未登录、`40102` token 过期、`40103` 签名失败 | | `403xx` | 授权 | `40301` 无权限点、`40302` 数据范围外、`40303` 缺特殊权限、`40304` 违反职责互斥 | | `404xx` | 不存在 | `40401` 资源不存在 / 已删除 | | `409xx` | 冲突 | `40901` 版本冲突、`40902` 重复提交、`40903` 状态不允许 | | `422xx` | 业务规则 | `42201` 业务校验失败、`42202` 合规拦截(禁忌词 / 极限词)、`42203` 成本超预算 | | `429xx` | 限流 | `42901` 请求过频、`42902` 并发任务超限 | | `500xx` | 系统 | `50001` 内部错误、`50002` 依赖不可用、`50003` 超时 | - `API-14` 新增错误码必须集中登记在 `packages/shared` 的错误码常量,禁止在业务代码里写裸数字。 - `API-15` 授权类错误**一律 403 且不区分「不存在」与「无权限」的原因**(避免探测他人数据;日志里保留真实原因)。 --- ## 5 鉴权与权限校验链 顺序固定,不得跳步: ``` JwtGuard → PermissionGuard(权限点)→ SpecialPermissionGuard(特殊权限) → ScopeGuard(数据范围)→ 业务校验 → Repository(ScopeContext) → 审计埋点 ``` - `API-16` 权限点由代码注册(常量),角色是权限点组合(数据化),见技术架构 D-07。 - `API-17` 特殊权限(导出 / AI 配置 / 审计查看 / 行业包发布 / 跨客户复用授权)必须单独授予,不得打包进普通角色。 - `API-18` 数据范围解析失败**一律 403**,禁止降级为「全量可见」。 - `API-19` `/public/review/*`、`/public/share/*` 是**唯一免 JWT** 路由组,必须:HMAC 签名 + 有效期 + 范围(只读 / 可改稿)+ 限流 + 访问留痕;不得在其中暴露任何其他资源。 - `API-20` 每个端点必须有 3 类权限测试用例(有权限 / 无权限 / 范围外),见 [06](06-测试与CI规范.md)。 --- ## 6 异步任务协议(所有耗时操作统一) | 步骤 | 约定 | | --- | --- | | 提交 | `POST /xxx-tasks`,带 `Idempotency-Key`,返回 `202` + `{ taskId, estimatedItems, estimatedCost, estimatedSeconds }` | | 订阅 | `GET /xxx-tasks/{id}/events`(SSE),事件 `item` / `progress` / `done` / `error`,支持 `Last-Event-ID` 续推 | | 兜底 | `GET /xxx-tasks/{id}` 轮询(前端 SSE 失败 2 次后自动降级) | | 取消 | `POST /xxx-tasks/{id}/cancel`;仅未开始项取消,已开始项等待结束 | | 重试 | `POST /xxx-tasks/{id}/retry`;**只重跑失败项**,成功项不得重算成本 | | 产物 | 一律签名 URL(5–15 分钟),随 `item` / `done` 事件下发 | - `API-21` 长耗时操作**禁止**放在同步请求里(> 2s 一律异步)。 - `API-22` `estimatedCost` 必填;为 0 也要返回(前端展示"预估成本"是 PRD R-06 硬要求)。 - `API-23` 任务终态写入后事件必须可重放(存事件表或 Redis Stream),断线可恢复。 --- ## 7 上传与下载 | 场景 | 约定 | | --- | --- | | 上传 | 预签名 PUT 直传对象存储;前端先算 `sha256` 走秒传;> 100 MB 用分片(`listMultipart`) | | 校验 | 上传完成后服务端 `headObject` 校验大小 / 类型;不一致则任务失败并清理 | | 下载 | `302` 到签名 URL(5–15 分钟),支持 `Range`(视频预览断点续播) | | 导出包 | 服务端打包 → `exports` 桶 → 签名 URL,产物保留 3 天 | | 限制 | 单文件上限读环境变量 `MAX_UPLOAD_MB`,**禁止**代码里写死 | - `API-24` 禁止 API 进程中转文件流(内存与带宽不可控)。 - `API-25` 签名 URL 不得记录到日志(含 query 签名参数)。 --- ## 8 前端路由 > 前端路由的模块表、路径语法与目录映射以 [11](11-前端路由与目录规范.md) 为准;本节保留通用约束。 - `API-26` 路由常量集中 `apps/web/src/routes.ts`,禁止散落字符串路径。 - `API-27` 前端路由统一为 `/<模块>/<功能>`:`/system/users`、`/scripts/library`;模块表、目录映射与旧路径重定向见 [11 · 前端路由与目录规范](11-前端路由与目录规范.md)。 - `API-28` 统一布局路由:登录后进入 `AppLayout`(侧边导航 + 面包屑),未登录访问自动跳 `/login` 并带回跳地址。 - `API-29` 权限守卫组件 ``;无权限渲染 403 页,**不隐藏路由**(避免"菜单有、点进去白屏")。 - `API-30` 页面级懒加载(`React.lazy` + `Suspense`),首屏包体积纳入性能预算。 - `API-31` URL 是唯一状态源:筛选 / 分页 / 选中态可分享的必须进 query(`useSearchParams`),禁止只存 Zustand。 - `API-32` 404 / 403 / 500 三个页面必须存在,且 500 页展示 `trace_id` 供排查。 --- ## 9 契约与兼容 - `API-33` OpenAPI 由 NestJS 装饰器生成,提交到仓库,CI 用 `oasdiff` 检测破坏性变更。 - `API-34` 新增字段必须可选且前端有默认值;删除字段先标记 `deprecated` 一个版本。 - `API-35` 前端不得依赖未在 OpenAPI 声明的字段。 --- ## 10 检查清单 - [ ] URL 复数 kebab-case,版本 `/api/v1`,嵌套 ≤ 2 层 - [ ] 入参 Zod 校验,未知字段拒绝 - [ ] 列表走游标分页,不返回大字段 - [ ] 错误体 `{ code, message, details, trace_id }`,错误码取自常量 - [ ] 守卫链完整,特殊权限单独校验 - [ ] 任务提交带 `Idempotency-Key` 与预估成本 - [ ] 产物只给签名 URL,不打日志 - [ ] 前端路由常量集中,权限守卫 + 三张错误页齐全 - [ ] OpenAPI 已更新,`openapi:diff` 通过