:feat: HCS接口500错误信息还原(Feign降级工厂还原远程msg/traceId,落库hcs_sync_msg)
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
# HCS 接口 500 错误信息还原设计(Feign 降级工厂)
|
||||
|
||||
> 日期:2026-10-10
|
||||
> 涉及模块:`blade-third-party-api/blade-hcs-api`、`blade-service/blade-transport`
|
||||
> 背景:HCS(合同合规系统)接口返回 HTTP 500 时,TMS 侧只见"无响应/响应为空",远程真实失败原因丢失,`hcs_sync_msg` 无法落库有效信息。
|
||||
|
||||
---
|
||||
|
||||
## 一、问题现象
|
||||
|
||||
真实日志(C102-1 项目立项新建推送,projectCode=LX-2026-000064):
|
||||
|
||||
```text
|
||||
CustomFeignLogger : <==== [IHcsClient#projectAdd(HcsProjectAddDTO)] .../km-project1/V1/projectApproval/add | Status: 500 | Body: {"success":false,"code":"errors.noRecord","msg":"地址本数据标识{\"fdName\":\"桂物物流集团\"}在系统中查找不到对应的组织机构数据!","traceId":"1k4ikrub...","errStacks":[...]}
|
||||
BladeFeignFallback : BladeFeignFallback:[org.springblade.hcs.feign.IHcsClient.projectAdd] serviceId:[HCS] message:[[500 Internal Server Error] during [POST] ...]
|
||||
HcsProjectSyncSupport : HCS项目立项新建失败响应:无响应,projectCode=LX-2026-000064
|
||||
```
|
||||
|
||||
现象:HCS 明确返回了业务错误(地址本组织机构不存在),但业务代码只拿到"无响应",`hcs_sync_msg` 只能落"响应为空",排查必须翻 Feign 日志。
|
||||
|
||||
## 二、根因分析
|
||||
|
||||
结合报文与 `blade-core-cloud`(4.10.0.BASE-SNAPSHOT)反编译,失败链路如下:
|
||||
|
||||
```
|
||||
HCS 返回 HTTP 500(body 含真实业务错误)
|
||||
→ Feign 对非 2xx 抛 FeignException(body 锁在异常 contentUTF8() 中)
|
||||
→ BladeSentinelInvocationHandler 捕获,进入降级
|
||||
→ @FeignClient 未声明 fallback/fallbackFactory
|
||||
→ 落到全局 org.springblade.core.cloud.feign.BladeFallbackFactory
|
||||
→ BladeFeignFallback 代理对非 R 返回值一律返回 null
|
||||
→ 调用侧 HcsProjectSyncSupport#addProject 拿到 result == null
|
||||
→ 打印"无响应"、抛 HcsException("...响应为空")
|
||||
```
|
||||
|
||||
关键依据(javap 反编译 `BladeFeignSentinel$Builder$1` 字节码):
|
||||
|
||||
1. `@FeignClient.fallback()` 有值 → 从 Feign 子上下文取该 bean,构造 `BladeSentinelInvocationHandler`;
|
||||
2. 否则 `@FeignClient.fallbackFactory()` 有值 → 同样取自定义工厂(**本方案接入点**);
|
||||
3. 两者皆无 → `new BladeFallbackFactory(target)` 全局兜底(即吞异常返回 null 的根因)。
|
||||
|
||||
即:**声明专属 fallbackFactory 后,全局兜底不再介入**。
|
||||
|
||||
## 三、备选方案否决记录
|
||||
|
||||
### 3.1 在调用侧捕获 HcsException —— 不可行
|
||||
|
||||
`HcsException` 是 `HcsProjectSyncSupport` 在拿到 null **之后**抛的,此时远程消息早已丢失。真正的 `FeignException` 在到达调用方之前就被 `BladeSentinelInvocationHandler → BladeFeignFallback` 吞掉并转成 null,任何 try/catch 都拦不到它——没有可捕获的异常内容。
|
||||
|
||||
### 3.2 照搬 BFI 的 commonCall + BfiExceptionTranslator 方式 —— 不可行
|
||||
|
||||
BFI 同类报错能正常拿到原因,但其成立前提与 HCS 不同:
|
||||
|
||||
| 对比项 | BFI(可行) | HCS(不可行) |
|
||||
| --- | --- | --- |
|
||||
| 业务失败时 HTTP 状态 | **200** | **500** |
|
||||
| Feign 行为 | body 正常反序列化返回 | 抛 FeignException,body 进异常对象 |
|
||||
| 错误体到达 service 层? | 到达 → `BfiServiceImpl#commonCall` 手动解析 `message` 后抛 `BfiException` | **到不了**,service 层只拿到 null |
|
||||
|
||||
而 `BfiExceptionTranslator` 实际是 `@RestControllerAdvice`,职责是把**我们自己 Controller 往外抛的** `BfiException` 转成 `R.fail` 给前端——它在出站 Feign 链路之外,`@ExceptionHandler` 无从触发。HCS 失败时 service 层连异常都收不到,service 层没有可写解析逻辑的位置,Rest Controller Advice 也拦不到 Feign 出站降级。
|
||||
|
||||
结论:两者错误发生在不同环节。HCS 错误体唯一能拦住的位置是 **Feign 降级层**。
|
||||
|
||||
### 3.3 关闭熔断兜底(feign.circuitbreaker.enabled=false)—— 不采纳
|
||||
|
||||
异常虽可直接抛到调用侧,但该配置全局生效会波及服务内所有 Feign 客户端,且 32 个 HCS 调用点都要补 try/catch 解析,改动面大。
|
||||
|
||||
## 四、采用方案:HcsClientFallbackFactory
|
||||
|
||||
### 4.1 改动清单(共 3 处)
|
||||
|
||||
| # | 文件 | 改动 |
|
||||
| --- | --- | --- |
|
||||
| 1 | `blade-hcs-api/.../hcs/feign/HcsClientFallbackFactory.java` | 新增降级工厂(约 120 行) |
|
||||
| 2 | `blade-hcs-api/.../hcs/feign/IHcsClient.java` | `@FeignClient` 增加 `fallbackFactory = HcsClientFallbackFactory.class` |
|
||||
| 3 | `blade-hcs-api/.../hcs/config/HcsFeignClientConfig.java` | 注册 `HcsClientFallbackFactory` 为 Feign 子上下文 bean(`BladeFeignSentinel` 经 `FeignClientFactory.getInstance` 取用) |
|
||||
|
||||
### 4.2 降级工厂处理规则
|
||||
|
||||
`create(cause)` 返回 JDK 动态代理(24 个接口方法无需逐一实现):
|
||||
|
||||
| 场景 | 处理 |
|
||||
| --- | --- |
|
||||
| 方法返回类型是 `HcsResult`(含双层嵌套 `HcsResult<HcsResult<T>>` 外层) | 构造失败 `HcsResult`:`cause` 为 `FeignException` 时,`contentUTF8()` 取错误体——HCS 错误体与 `HcsResult` 同构(success/code/msg/traceId),`JsonUtil.parse` 直接还原远程 `msg`/`traceId`;非 JSON body 降级为异常摘要;`status` 记入 `code`(errors.http.500 样式) |
|
||||
| 非 `FeignException`(熔断打开、连接超时等) | `msg` 取异常摘要,空则给 `HCS 接口调用异常(异常类名)`,`code=errors.feignFallback` |
|
||||
| 非 `HcsResult` 返回类型(附件下载 `Response` 等) | 返回 null,与原全局兜底行为一致 |
|
||||
| Object 基础方法(toString/hashCode/equals) | 直接反射执行 |
|
||||
|
||||
### 4.3 还原后的完整链路
|
||||
|
||||
```
|
||||
HCS 500 → FeignException(错误体)
|
||||
→ BladeSentinelInvocationHandler → HcsClientFallbackFactory.create(cause)
|
||||
→ 返回 HcsResult{success=false, msg=远程msg, traceId=远程traceId}
|
||||
→ 各 support 现有判断 result.isSuccess()==false
|
||||
→ 日志:HCS项目立项新建失败响应:msg=地址本数据标识{...}在系统中查找不到对应的组织机构数据!, traceId=1k4ikrub...
|
||||
→ 抛 HcsException(远程msg)
|
||||
→ markFailed → retryCount+1 → hcs_sync_msg = 远程错误原因
|
||||
→ 手动重推场景:repushProject 转 ServiceException → BladeX 全局 RestExceptionTranslator → 前端收到远程原因
|
||||
```
|
||||
|
||||
## 五、影响范围
|
||||
|
||||
- 调用点:`IHcsClient` 共 32 处调用,全部位于 `blade-transport` 的 10 个 `support/hcs` 类中,**零改动**——各处已有的 `result == null || !result.isSuccess()` 防御判断继续有效(null 分支保留为极端兜底)。
|
||||
- 成功路径:降级工厂仅在异常时被调用,正常 2xx 响应不经过它,零影响。
|
||||
- 全局兜底:仅 HCS 客户端声明了专属 fallbackFactory,其余 Feign 客户端(BFI 等)行为不变。
|
||||
- 重试策略:不变,仍由扫描任务按 `hcs_retry_count` 补偿(D7 约定,Feign 层无 Retryer)。
|
||||
|
||||
## 六、验证与回归建议
|
||||
|
||||
- 编译验证:`mvn clean compile -pl blade-third-party-api/blade-hcs-api,blade-service/blade-transport -am -DskipTests` 通过。
|
||||
- 建议回归场景:
|
||||
1. **本案例复现**:HCS 地址本不含"桂物物流集团"时推送项目立项,确认日志与 `hcs_sync_msg` 出现远程 msg 及 traceId;
|
||||
2. 修正 `thirdParty.hcs.creator-unit-name` 为 HCS 地址本准确名称后手动重推,确认成功链路不受影响;
|
||||
3. HCS 停机/连接超时场景,确认失败原因为可读的连接类摘要(而非"无响应");
|
||||
4. 其余 HCS 接口(合同 C103、开票登记 C111 等)抽查推送失败场景,确认 msg 还原一致生效。
|
||||
|
||||
## 七、遗留事项(与本设计无关,另案处理)
|
||||
|
||||
- 本案例的业务根因:`fd_creator_unit`/`fd_avail_units` 传值"桂物物流集团"在 HCS 地址本不存在,需修正 `thirdParty.hcs.creator-unit-name` 配置或由合规方在地址本新建该组织后重推。
|
||||
Reference in New Issue
Block a user