12 KiB
统一响应约定改造方案:HTTP 200 + body code
状态:代码改造与后端全量测试已完成,待测试环境发布验证 日期:2026-08-01
1. 最终约定
业务接口统一返回 HTTP 200,调用方只按响应体判断业务结果:
| 判断条件 | 含义 |
|---|---|
HTTP 200 且 body code == "200" |
成功 |
HTTP 200 且 body code != "200" |
业务失败,展示或记录 message |
HTTP 非 200 |
鉴权、协议、网关或基础设施异常,不属于普通业务失败 |
code 是字符串。前端不得再把旧成功码 "0" 视为成功。
{
"code": "200",
"message": "success",
"data": {},
"requestId": "...",
"traceId": "...",
"timestamp": "2026-08-01T00:00:00Z"
}
业务失败示例:
HTTP/1.1 200 OK
Content-Type: application/json
{"code":"A0409","message":"Review already completed / 审核已完成","data":null}
2. 保留真实 HTTP 状态的边界
以下响应不强制改成 HTTP 200:
- 网关鉴权和 Servlet Security 入口的
401/403,前端 token 刷新依赖真实401。 - Event Bridge 内部令牌保护的
401/503。 - Warehouse readiness 探针的
503。 - Audit 异步接收成功的
202。 - 框架、协议和基础设施错误,例如路由不存在
404、405、413、415、代理或网关502/504。
控制器或业务服务抛出的 BizException、参数校验失败和统一异常处理器捕获的系统异常,均返回 HTTP 200 + 非 200 body code。
3. 后端实现
3.1 统一响应
ResultCode.SUCCESS已从"0"改为"200"。ApiResponse.successful()只认ResultCode.SUCCESS.code()。GlobalExceptionHandler的业务、校验和兜底异常统一使用ResponseEntity.ok(...)。- Warehouse 数据访问异常处理器同步使用 HTTP 200。
common-web增加自动配置,确保只扫描自身包的 audit-service、event-bridge-service 也能注册统一异常处理器和关联 ID 过滤器。
3.2 服务间调用
Feign 调用统一按新契约处理:
- 提供方返回 HTTP 200:检查
response.successful(),失败时读取 bodycode/message。 - 提供方返回 HTTP 非 200:视为鉴权、协议、网关或基础设施异常,统一映射为
SERVICE_UNAVAILABLE,不得按 HTTP 404/409 等状态推断业务结果。 - 空响应、解码失败、契约漂移或传输异常同样统一映射为
SERVICE_UNAVAILABLE。
ResultCode.of(rawCode) 只还原本地已知枚举;未知或空 code 会降级为 B0001,不会原样透传未知 code,也不会误判成功。
服务间调用统一收敛为 OrderEligibilityGuard 的三段式写法:
} catch (FeignException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "...服务暂时不可用");
} catch (BizException exception) {
throw exception;
} catch (RuntimeException exception) {
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "...响应无效");
}
catch (BizException)必须显式置于RuntimeException之前,否则主路径抛出的业务异常会被兜底分支吞掉、降级成S0503。Spring MVC 对未匹配路由返回的 404 不经过
GlobalExceptionHandler,仍是真 404;服务间调用方不得把这个协议错误伪装成业务资源不存在。
4. 企业审核代码与修复
当前已有企业审核流程:
- 接口:
PlatformTenantController的/reviews/{id}/approve与/reviews/{id}/reject。 - 服务:
TenantService.review()。 - 表:
iam_tenant_review,审核意见字段为VARCHAR(500)。
本次补齐:
ReviewRequest.comment增加@Size(max = 500)(TenantDtos.java:52),审批和驳回请求增加@Valid(PlatformTenantController.java:122、:137)。- 审核状态使用
WHERE id = ? AND status = 'PENDING'条件更新(TenantRepository.completeReview())。 - 只有影响行数为 1 的请求获得审核权;重复或并发请求返回 body code
A0409。 - 获得审核权后才更新租户和待审资质,整个流程仍在同一事务内;后续步骤失败时审核更新一并回滚。
这样可避免两个操作员同时通过/驳回时相互覆盖审核结果。
实现说明:
TenantService.review()中if (!"PENDING".equals(review.getStatus()))(:98)只是读后快速失败,用于给出即时提示;真正的并发防护在completeReview()的条件更新 —— 两个并发请求都通过前置检查时,只有一个能拿到affected == 1,另一个落到:105抛A0409。两层都保留是合理的,不要因为「看起来重复」而删掉任一层。
5. 前端约定
admin-web 与 client-web 都只在 code === "200" 时解包数据;其他 body code 转为业务错误。真实 HTTP 401 的刷新重试逻辑保持不变,真实 5xx 的防御性错误解析也继续保留。
5.1 改造落点(2026-08-01 实测确认已完成)
| 仓库 | 位置 | 现状 |
|---|---|---|
| admin-web | src/api/http.ts:101 |
if (response?.code !== '200') ✅ |
| client-web | src/api/http.ts:17 |
if (envelope.code !== '200') ✅ |
| admin-web | src/stores/auth.test.ts:13-16 |
测试内自建 unwrap 同步改为 !== '200' ✅ |
| admin-web | 4 个 api/utils 测试文件 | mock 数据已全部改为 code: '200',共 24 处 ✅ |
code 为字符串,前端使用严格比较 !==,与后端 ApiResponse.code(String 类型,Jackson 不做数字推断)对齐。若后端误改为数字类型,'200' !== 200 恒为真会导致全站失败 —— 见 3.1。
5.2 防回归断言
admin-web/src/api/http.test.ts:21-26 新增用例 treats only body code 200 as success,显式断言旧成功码 '0' 现在必须抛 ApiResponseError:
expect(unwrapApiResponse({ code: '200', message: 'success', data: { id: '1' } })).toEqual({ id: '1' })
expect(() => unwrapApiResponse({ code: '0', message: 'legacy success code', data: null }))
.toThrow(ApiResponseError)
这是文件里唯一保留的 code: '0',属于有意为之,核查脚本扫到时不应误判为漏改。
client-web 通过 src/views/AuthViews.test.mjs 的源码契约测试覆盖 unwrap():明确要求 code !== '200',并拒绝旧的 code !== '0' 判断。
5.3 不受影响的应用
apps/ 下另外 4 个应用经核查无成功码判断逻辑:
| 应用 | 情况 |
|---|---|
mobile-app、website、wechat-miniprogram |
仅有 AGENTS.md / README.md,尚无实现代码 |
pda-uniapp |
有页面代码但无 ApiResponse 判定(.code 均为任务编号或构建产物) |
另注:admin-web/shared/src/index.ts:30 的 '0' 是数字字符范围判断(>= '0' && <= '9'),与成功码无关,勿改。
6. OpenAPI 与可观测性
- 控制器注解以及 IAM、Order、Firstleg 静态 OpenAPI 已移除普通业务
400/404/409/422/503响应声明。 实测确认:三份静态 OpenAPI 中此类声明为 0 处;控制器注解仅剩EventBridgeController.java:41-42的401/503,属第 2 节允许的内部令牌保护边界,不是漏改。 - HTTP 200 的描述明确要求继续检查 body code;真实鉴权状态继续保留。
- 统一异常处理器增加
baiyue.api.business.failures计数器,仅使用有限的结果码作为code标签,避免高基数。 - Prometheus 中对应的计数指标通常导出为
baiyue_api_business_failures_total{code="A0409"}。
发布后,业务错误率告警应基于该指标,不再依赖 HTTP 4xx/5xx 曲线。
7. 发布顺序
- 同批发布
common-core、common-web和所有包含它们的服务构建产物。 - 同批发布已改造的 Feign 调用方,避免新提供方与旧调用方形成契约窗口。
- 发布 admin-web 与 client-web;前后端不可长时间跨版本运行,因为旧前端不识别成功码
"200",新前端也不再接受"0"。 - 验证成功、业务失败、真实 401、readiness 503、audit 202。
- 确认业务失败指标和告警正常后再完成发布。
不建议在后端成功码切换和前端切换之间做长时间灰度。如必须灰度,应由网关或独立兼容层按客户端版本转换成功码,不能让客户端同时猜测 "0" 和 "200"。
8. 核查与验收
8.1 静态核查脚本
# —— 后端(在 baiyue-firstleg 下执行)——
# 成功码应为字符串 "200"
grep -n 'SUCCESS(' common-core/src/main/java/com/baiyue/common/core/api/ResultCode.java
# 期望:SUCCESS("200", "success", HttpStatus.OK)
# code 字段必须是 String,不能是 int/Integer
grep -n 'code' common-core/src/main/java/com/baiyue/common/core/api/ApiResponse.java | head -3
# 期望:String code,
# 除枚举定义外不应再有 "0" 成功码硬编码
grep -rn '"0"' --include="*.java" */src/main | grep -v 'ResultCode.java' | grep -v 'logic-delete'
# 生产代码不应使用 Feign HTTP 状态码推断业务结果;此命令应无输出
grep -rn 'exception\.status()\|feign\.status()\|FeignException\.NotFound' --include="*.java" */src/main
# —— 前端(在 apps 下执行)——
# 两个前端的成功码判断
grep -rn "code !== '200'" admin-web/src client-web/src --include="*.ts" --exclude-dir=node_modules
# 期望:admin-web/src/api/http.ts、client-web/src/api/http.ts 各一处
# 残留旧成功码:期望仅 http.test.ts 那一条反向断言(见 5.2)
grep -rn "code: '0'\|code !== '0'\|code === '0'" admin-web/src client-web/src \
--include="*.ts" --include="*.tsx" --exclude-dir=node_modules
8.2 构建与测试
# 后端:common-web 是共享依赖,必须带 -am
cd baiyue-firstleg
mvn -pl common-core,common-web,auth,iam,file,order,warehouse,audit-service,event-bridge-service -am test
# 前端
cd ../apps/admin-web && pnpm test && pnpm typecheck && pnpm build
cd ../client-web && pnpm test && pnpm typecheck && pnpm build
8.3 部署后冒烟
# ① 业务成功:HTTP 200 + "code":"200"(不再是 "0")
curl -isS "http://8.163.30.128:9000/api/merchant/v1/iam/tenant/qualifications" \
-H "Authorization: Bearer <MERCHANT_TOKEN>" | sed -n '1p;/^{/p'
# ② 业务失败:HTTP 200 + "code":"A0409"
curl -isS -X POST "http://8.163.30.128:9000/api/merchant/v1/files/qualifications" \
-H "Authorization: Bearer <MERCHANT_TOKEN>" \
-F "file=@/path/to/license.png" -F "certificateNo=91310000PHASE10001" \
| sed -n '1p;/^{/p'
# ③ 真实 401 保持(前端 token 刷新依赖)
curl -isS -X POST "http://8.163.30.128:9000/api/merchant/v1/files/qualifications" | sed -n '1p'
# ④ readiness 真实 503
curl -isS "http://8.163.30.128:9205/actuator/health/readiness" | sed -n '1p'
# ⑤ 业务失败指标
curl -sS "http://8.163.30.128:9201/actuator/prometheus" | grep baiyue_api_business_failures_total
① 是本次成功码改造的关键验证点:若仍返回
"code":"0"说明该服务未重新构建(common-core打包在各服务 jar 内)。
8.4 回滚
无数据库变更、无 Nacos 变更,回滚即回退代码重新构建。注意前后端必须一起回滚 —— 只回滚一侧会立即造成全站失败,原因见第 7 节。
9. 验收清单
静态核查(2026-08-01 已实测)
- 后端成功码为字符串
"200"—ResultCode.java:11,字段类型String ApiResponse.code为String,Jackson 不做数字推断,序列化为"200"- admin-web
src/api/http.ts:101判!== '200' - client-web
src/api/http.ts:17判!== '200' - 前端 mock 与自建 unwrap 全部改为
'200'(24 处) - 唯一残留的
code: '0'是http.test.ts:24的防回归反向断言,属有意保留
运行时验收(部署后按 8.3 执行)
- 成功响应:HTTP 200 +
code: "200"。 - 参数错误、资源不存在、冲突、下游业务失败:HTTP 200 + 非 200 code。
- 重复或并发审核:仅一个请求成功,其余返回
A0409。 - 超过 500 字的审核意见:HTTP 200 +
A0422。 - 未登录请求:真实 HTTP 401,前端刷新逻辑正常。
- Warehouse readiness 不可用:真实 HTTP 503。
- Audit 接收成功:真实 HTTP 202,body
code: "200"。 - Prometheus 能查询
baiyue_api_business_failures_total。 - 三个仓库测试、类型检查和构建全部通过(8.2)。
遗留项
- 已删除 Feign 旧 HTTP 状态码业务映射;服务间调用统一使用 HTTP 200 body code
10. 与本改造无关的现有工作区改动
file 模块的 LocalQualificationStorage 及其测试是已有的存储目录写入探针改动,不属于响应约定。本次不覆盖、不回退,提交时建议与响应改造拆分。