# API契约-公共约定 > 两个世界(管理面 / 成员视图)共用的 HTTP 层约定:路由隔离、鉴权两档、错误信封、时间 / 数值 / 分页 / DTO 命名。本篇是《API契约-管理面》《API契约-成员视图》的引用锚点——跨面共性在此定死,端点逐个定义归后两篇。 | 项目 | 内容 | |------|------| | 状态 | 定稿(2026-10-02) | | 关联 | 《交互设计-总览与共性》(§1 访问拓扑 · §3 呈现约定)·《交互设计-登录与会话》(鉴权档位与 401 分流)· 七篇表字段设计(DTO 字段权威)·《技术选型》§4(DTO 类型属 `packages/core`) | | 产出 | 路由前缀与隔离机制 · 鉴权两档 · 错误信封 · 时间 / 数值 / 分页 / 命名约定 | --- ## 1. 总则 - JSON over HTTP,UTF-8;请求与响应体均为 `application/json` - 一套路由代码,两个世界靠**路径前缀与凭证中间件物理隔离**(共性 §1:令牌进不了管理面、会话进不了成员视图) - DTO 类型定义于 `packages/core`,web 以纯类型依赖引用——前后端一份契约类型(技术选型 §4 依赖规则) - **端点面向交互设计,不与数据表对齐**——每个端点对应一个功能面(页面 / 流程需要什么);表只是存储模型,按其事实性质获得恰好够用的操作面(引擎专写表纯只读、人工抄录表全 CRUD、账本实体软删除) ## 2. 路由前缀与隔离机制 | 面 | 路由前缀 | 凭证 | 校验中间件 | |----|---------|------|-----------| | 管理面 | `/api/admin/*` | 会话 cookie | sessions 校验 | | 成员视图 | `/api/member/*` | Bearer 令牌 | members 哈希比对 + `status` 检查 | - **会话 cookie**:`HttpOnly + Secure + SameSite=Lax`,`Path=/api/admin`(「路径限管理面」裁决的落地);随请求自动携带、登出即删行 - **成员令牌**:`Authorization: Bearer `;明文令牌只存在于登录壳输入与 sessionStorage(关标签即失效——《交互设计-登录与会话》§4) - 401 分流为前端职责:管理面任意 401 → 登录页「会话已过期」;成员视图任意 401 → 登录壳「令牌无效或已停用」——后端 401 响应不带面特化信息,前端按所在面分流(共性 §1) - **命名空间约定:`/api/*` 专属接口**——前端页面路由永不进入 `/api` 前缀;未知 `/api/*` 路径 = 接口 `404` 错误信封,非 `/api` 请求归前端托管(SPA history 回退「非 `/api` → index.html」,细则归骨架与部署设计) ## 3. 错误信封 统一错误体(成功响应无信封包装,见 §6): ```json { "error": { "code": "invalid_credentials", "message": "用户名或密码错误" } } ``` - `message`:面向使用者的中文、说人话(共性 §3.6 文案基调);不携带堆栈与内部细节 - `code`:机器可读枚举(snake_case);表单校验失败可加 `fields`(字段名 → 中文说明);判别已足时允许端点附机器可读附注字段(如重复登记的 `409` 附 `key_id` 定位既有行) | HTTP 状态 | 语义 | 本项目对应 | |-----------|------|-----------| | 200 / 201 | 成功 / 创建成功 | 创建类写操作返 201 | | 400 | 请求格式错误 | 参数缺漏、JSON 解析失败 | | 401 | 凭证无效或过期 | 会话过期、令牌停用 / 重发 | | 404 | 资源不存在 | 非法 id | | 409 | 冲突 | 重复登记(sk-key 已存在等) | | 422 | 校验失败 | 表单字段校验(带 `fields`) | | 429 | 请求过频 | 登录节流(管理面,最小实现) | | 5xx | 服务端故障 | `message` 统一「服务暂时不可用,请稍后重试」,不泄内部细节 | - **HTTP 状态码是成功/失败的唯一判别**——客户端永远先看状态码、再解析响应体,不从响应体形状推断成败;错误信封的解析集中在前端唯一的统一请求封装内,各端点代码只碰成功类型;中间人返回的非 JSON 错误体(代理 502 页等)回退为通用文案 - **业务错误即请求失败**——状态码表即业务错误分类表(409 重复登记、422 规则校验失败、404 资源不存在等),一律非 2xx + 错误信封;**不存在「请求成功但携带业务错误」的响应形态**。系统状态类数据例外:响应携带的「坏事」是事实数据而非请求错误——如总览健康带的「最近轮次失败 2」走 200,失败的轮次是数据、不是本次请求的错误 ## 4. 时间口径 - **响应时刻字段:Unix 秒整数**(UTC 时刻,如 `1780000000`)——无时区歧义;本地/北京时间由展示层转换 - **请求时间参数**(`from` / `to` 等):ISO 8601 UTC 字符串(便于试调);范围 **左闭右开 `[from, to)`** - 存储层维持 timestamptz / 上游 Unix 秒;DTO 输出统一 `toUnixSec` - 快捷项(最近 7 / 30 天、本订阅期间、自定义起止)是前端预置逻辑:前端算出 `from/to` 再请求,后端无快捷参数 ## 5. 数值口径(对齐共性 §3.2) - **quota 双单位**:用量一律成对返回 `quota`(整数原值,口径内精确显示)+ `quota_usd`(**字符串,两位小数**,如 `"25.00"`) - **换算责任在后端**:按日志时刻取 QPU(`qpu_history` 时间线)逐行换算——单一事实源,前端只呈现不换算;占比 / 分段条比例由前端现算(纯呈现职责) - **取整规则**:换算内部以微分(1/100 分)整数运算,仅输出时四舍五入到分;聚合桶先微分求和再一次取整——小行贡献不丢失、单一舍入点 - **支付金额**:`paid_amount` 原样数字(币种无关,R9 裁决),无符号、无换算、无派生金额 - **次数**:`count`(日志行数) ## 6. 分页与响应形态 - **列表类端点**(日志明细、审计日志等):`page`(1 起)+ `page_size`(默认 50,上限 200)→ `{ "items": [...], "total": 123 }` - **聚合类端点**(按成员 / key / 来源分组):不分页——分组基数有限(成员个位数、key 10–25),返回全量数组 - **成功响应无信封**:对象直返、列表 `items/total`、创建返创建后实体——信封只用于错误(§3);空响应体的动作类成功(如登出)返 `204 No Content` 无响应体 ## 7. DTO 命名 - **snake_case、与表字段同名同构**——七篇表字段设计即 DTO 字段权威,无映射层 - 前端不做 camelCase 转换:全栈 snake_case(组件 props 是 antd 的 API、与数据 DTO 互不干扰) ## 8. 本稿拟定约定(交互设计未覆盖,请作者重点审) 1. 时间传输格式 ISO 8601 UTC 字符串(§4); 2. 时间范围左闭右开 `[from, to)`(§4); 3. `quota_usd` 字符串两位小数 + 后端换算责任 + 微分取整规则(§5); 4. DTO 与表字段 snake_case 同构、无映射层(§7); 5. 分页 `page/page_size` + `items/total`,聚合类不分页(§6); 6. 错误信封单层 `{error: {code, message, fields?}}`,成功无信封(§3/§6); 7. 空响应体动作类成功返 `204`(§6);HTTP 状态码为成功/失败的唯一判别(§3); 8. 业务错误一律非 2xx——不存在 200 携业务错误的形态;系统状态类数据(健康带轮次失败数等)例外,是响应数据、不是请求错误(§3)。