Files
baiyue-firstleg/docs/统一响应约定改造方案-HTTP200.md

12 KiB
Raw Permalink Blame History

统一响应约定改造方案: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 调用统一按新契约处理:

  1. 提供方返回 HTTP 200:检查 response.successful(),失败时读取 body code/message。
  2. 提供方返回 HTTP 非 200:视为鉴权、协议、网关或基础设施异常,统一映射为 SERVICE_UNAVAILABLE,不得按 HTTP 404/409 等状态推断业务结果。
  3. 空响应、解码失败、契约漂移或传输异常同样统一映射为 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. 发布顺序

  1. 同批发布 common-core、common-web 和所有包含它们的服务构建产物。
  2. 同批发布已改造的 Feign 调用方,避免新提供方与旧调用方形成契约窗口。
  3. 发布 admin-web 与 client-web;前后端不可长时间跨版本运行,因为旧前端不识别成功码 "200",新前端也不再接受 "0"。
  4. 验证成功、业务失败、真实 401、readiness 503、audit 202。
  5. 确认业务失败指标和告警正常后再完成发布。

不建议在后端成功码切换和前端切换之间做长时间灰度。如必须灰度,应由网关或独立兼容层按客户端版本转换成功码,不能让客户端同时猜测 "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 及其测试是已有的存储目录写入探针改动,不属于响应约定。本次不覆盖、不回退,提交时建议与响应改造拆分。