双重表达:HTTP 状态码 + 业务码
每个响应的结果由两层共同表达,客户端应以业务码 code 分支、以 HTTP 状态码做传输层判断:
- HTTP 状态码:表达请求在协议层的处理结果(200/401/404/422/429/500...);
- 业务码
code:表达业务语义(OK、UNAUTHORIZED、NOT_FOUND...),成功恒为OK。
失败响应结构(与成功同信封):
{
"code": "VALIDATION_FAILED",
"message": "手机号不能为空",
"data": {},
"errors": { "mobile": "手机号不能为空" },
"request_id": "9f2a5c8e1b3d7f40"
}
errors 与 data 的分工:
| 字段 | 用途 |
|---|---|
errors |
字段级校验错误集合,键=字段名、值=错误文案。适合做表单逐项红字标注 |
data |
失败时仍需回传的辅助业务数据(如 405 的 allow 允许方法列表) |
业务码(code)全集
| 业务码 | 典型 HTTP | 含义与客户端建议动作 |
|---|---|---|
OK |
200 | 成功 |
BUSINESS_RULE_VIOLATION |
422 | 通用业务规则违反(如库存不足、状态不允许操作)。展示 message 即可 |
INVALID_PARAMS |
422 / 400 | 参数非法 / 无效 ID。检查请求参数 |
VALIDATION_FAILED |
422 | 字段级校验失败。按 errors 逐字段提示 |
UNAUTHORIZED |
401 | 未携带或携带了无效令牌。清本地令牌 → 跳登录 |
AUTH_REQUIRED |
401 | 需要登录后访问(部分场景带 data.jump_url 引导地址) |
FORBIDDEN |
403 | 已登录但无权限(如工作端接口无员工身份、模块未装) |
NOT_FOUND |
404 | 资源不存在(数据 ID 无效、路由不存在) |
QUOTA_EXCEEDED |
422 / 429 | 配额 / 额度超限(AI 对话次数、短信额度等) |
RATE_LIMITED |
429 | 请求过频被限流。按 Retry-After 退避重试 |
SERVER_ERROR |
500 | 服务端兜底错误。展示通用错误提示,可记录 request_id 反馈 |
AI_CONFIG_UNAVAILABLE |
503 | AI 模型 / 服务不可用(见《AI 助手接口》) |
CHAT_NOT_FOUND |
404 | AI 会话不存在或无权限(见《AI 助手接口》) |
CHAT_CREATE_FAILED |
500 | AI 会话创建失败 |
CHAT_SAVE_FAILED |
500 | AI 会话保存失败 |
业务码字符串是对外契约:客户端可硬编码这些值做分支,服务端只会在末尾追加新码,不会改名。新增码会通过文档与发版说明同步。
HTTP 状态码行为说明
| 状态码 | 触发场景 | 备注 |
|---|---|---|
| 200 | 成功;以及「检查登录态」这类"查询结果负面但不是错误"的接口 | 未登录调 check_login_state 也返回 200 + login_state:false |
| 204 | CORS 预检(OPTIONS)通过 | 空 body + 全套 Access-Control-Allow-* 头 |
| 401 | 必须鉴权的接口未带 / 带错令牌 | code=UNAUTHORIZED |
| 403 | 无工作端身份等权限不足 | code=FORBIDDEN |
| 404 | 数据不存在;路由不存在(含方法/路径拼写错误) | 路由不存在时也是 JSON 信封,不是 HTML 404 页 |
| 405 | OPTIONS 预检未命中白名单;路由存在但 HTTP 方法不允许 | 方法不允许时 code 仍为 NOT_FOUND、message 为 Method Not Allowed,data.allow 给出允许的方法列表 |
| 422 | 字段校验失败、业务规则违反(默认失败状态码) | 默认的"业务失败"载体 |
| 429 | 触发定向限流 | 带 Retry-After 头 |
| 500 | 未预期异常 / 邮件发送失败等 | code=SERVER_ERROR |
| 503 | AI Provider 不可用 | code=AI_CONFIG_UNAVAILABLE |
405 示例(用 GET 调了一个只支持 POST 的接口):
{
"code": "NOT_FOUND",
"message": "Method Not Allowed",
"data": { "allow": ["POST"] },
"errors": {},
"request_id": "9f2a5c8e1b3d7f40"
}
限流(429)
以下接口按客户端 IP 定向限流,超限返回 429 RATE_LIMITED 并带 Retry-After 响应头(单位:秒,表示最早可重试时间):
HTTP/1.1 429 Too Many Requests
Retry-After: 37
Content-Type: application/json; charset=utf-8
{
"code": "RATE_LIMITED",
"message": "请求过于频繁,请稍后再试",
"data": {},
"errors": {},
"request_id": "9f2a5c8e1b3d7f40"
}
完整配额表:
| 接口 | 配额 | 窗口 |
|---|---|---|
POST user/login_post |
5 次 | 60 秒 |
POST user/login_phone_post |
5 次 | 60 秒 |
POST user/register_post |
5 次 | 60 秒 |
POST user/password_reset_post |
5 次 | 300 秒 |
POST captcha/verification |
5 次 | 300 秒 |
POST guestbook/store |
5 次 | 300 秒 |
POST consultation/store |
5 次 | 300 秒 |
POST email/store |
5 次 | 300 秒 |
GET sn/search |
20 次 | 60 秒 |
POST chat/stream |
20 次 | 60 秒 |
POST chat/new_session |
10 次 | 60 秒 |
重试约定:收到 429 后应读取 Retry-After 秒数做退避;不要立即重试。前端对表单类接口(登录/注册)建议在按钮上做倒计时提示。
除上表外,公共匿名写接口在各控制器业务层还有一层按 IP 的"灌水"防护(
isWaterByIp),两者互补:中间件拦高频,业务层拦内容灌水。
排查建议
- 先看
code与errors:表单类问题基本都由errors定位到具体字段; - 记录
request_id:只要把该 ID 提供给服务端,即可在日志中定位完整请求链路; - 区分 401 的两种子态:
UNAUTHORIZED是令牌无效(清令牌重登);若未来收到AUTH_REQUIRED且有data.jump_url,可直接跳转该地址登录; - 405 时看
data.allow:确认自己用的 HTTP 方法是否与文档一致(注意方法伪装_method/X-HTTP-Method-Override是否拼写正确)。