diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsFeignClientConfig.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsFeignClientConfig.java
index 9c893d56..122c083c 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsFeignClientConfig.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsFeignClientConfig.java
@@ -27,6 +27,7 @@ package org.springblade.hcs.config;
import feign.Request;
import feign.RequestInterceptor;
+import org.springblade.hcs.feign.HcsClientFallbackFactory;
import org.springblade.hcs.interceptor.HcsAuthInterceptor;
import org.springframework.context.annotation.Bean;
@@ -49,6 +50,14 @@ public class HcsFeignClientConfig {
};
}
+ /**
+ * HCS 降级工厂:非 2xx 时还原远程错误体 msg/traceId,避免全局兜底吞成 null(配合 IHcsClient 的 fallbackFactory 声明)
+ */
+ @Bean
+ public HcsClientFallbackFactory hcsClientFallbackFactory() {
+ return new HcsClientFallbackFactory();
+ }
+
/**
* HCS 认证拦截器:所有请求注入 Authorization + Key-Auth 两个静态认证头(v4 接口说明)
*/
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/HcsClientFallbackFactory.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/HcsClientFallbackFactory.java
new file mode 100644
index 00000000..a4da6201
--- /dev/null
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/HcsClientFallbackFactory.java
@@ -0,0 +1,116 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement
+ * obtained after purchasing a license from BladeX.
+ *
+ * 1. This software is for development use only under a valid license
+ * from BladeX.
+ *
+ * 2. Redistribution of this software's source code to any third party
+ * without a commercial license is strictly prohibited.
+ *
+ * 3. Licensees may copyright their own code but cannot use segments
+ * from this software for such purposes. Copyright of this software
+ * remains with BladeX.
+ *
+ * Using this software signifies agreement to this License, and the software
+ * must not be used for illegal purposes.
+ *
+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
+ * not liable for any claims arising from secondary or illegal development.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.hcs.feign;
+
+import feign.FeignException;
+import org.springblade.core.tool.jackson.JsonUtil;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.hcs.dto.HcsResult;
+import org.springframework.cloud.openfeign.FallbackFactory;
+
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+
+/**
+ * HCS Feign 客户端降级工厂
+ *
+ * HCS 返回非 2xx(如 500)时 Feign 抛 {@link FeignException},全局 BladeFeignFallback
+ * 对 HcsResult 返回值只会返回 null,导致远程错误信息丢失(调用侧只见"无响应/响应为空")。
+ * 本工厂在降级时将错误体还原为失败的 {@link HcsResult}:HCS 错误体与 HcsResult 同构
+ * (success/code/msg/traceId),直接反序列化取回远程 msg 与 traceId,
+ * 供调用侧日志与 hcs_sync_msg 等失败原因落库使用。
+ *
+ * 通过 {@link IHcsClient} 的 fallbackFactory 声明并注册于 {@link org.springblade.hcs.config.HcsFeignClientConfig},
+ * 所有 HCS 接口(含双层嵌套 HcsResult 外层)统一生效,调用侧零改动。
+ *
+ * @author Chill
+ */
+public class HcsClientFallbackFactory implements FallbackFactory {
+
+ @Override
+ public IHcsClient create(Throwable cause) {
+ return (IHcsClient) Proxy.newProxyInstance(IHcsClient.class.getClassLoader(), new Class>[]{IHcsClient.class},
+ (Object proxy, Method method, Object[] args) -> {
+ // Object 基础方法(toString/hashCode/equals)直接执行
+ if (method.getDeclaringClass() != IHcsClient.class) {
+ return method.invoke(this, args);
+ }
+ // 附件下载等非 HcsResult 返回值无业务结果可还原,保持 null(与原全局兜底行为一致)
+ if (!HcsResult.class.isAssignableFrom(method.getReturnType())) {
+ return null;
+ }
+ return buildFailResult(cause);
+ });
+ }
+
+ /**
+ * 按异常构造失败的 HcsResult,优先还原远程错误体中的 msg/traceId
+ */
+ private HcsResult> buildFailResult(Throwable cause) {
+ HcsResult> result = new HcsResult<>();
+ result.setSuccess(false);
+ // HCS 非 2xx 时错误体随 FeignException 下发,结构与 HcsResult 一致
+ if (cause instanceof FeignException feignException) {
+ result.setCode("errors.http." + feignException.status());
+ HcsResult> remote = parseRemoteResult(feignException.contentUTF8());
+ if (remote != null && StringUtil.isNotBlank(remote.getMsg())) {
+ result.setMsg(remote.getMsg());
+ result.setTraceId(remote.getTraceId());
+ } else {
+ result.setMsg(resolveFallbackMessage(cause));
+ }
+ return result;
+ }
+ // 非 HTTP 错误(熔断打开、连接超时等)
+ result.setCode("errors.feignFallback");
+ result.setMsg(resolveFallbackMessage(cause));
+ return result;
+ }
+
+ /**
+ * 错误体反序列化为 HcsResult(非 JSON 或解析失败时返回 null)
+ */
+ private HcsResult> parseRemoteResult(String body) {
+ if (StringUtil.isBlank(body)) {
+ return null;
+ }
+ try {
+ return JsonUtil.parse(body, HcsResult.class);
+ } catch (Exception exception) {
+ return null;
+ }
+ }
+
+ /**
+ * 兜底消息:异常摘要非空用之,否则给可读默认值
+ */
+ private String resolveFallbackMessage(Throwable cause) {
+ if (StringUtil.isNotBlank(cause.getMessage())) {
+ return cause.getMessage();
+ }
+ return "HCS 接口调用异常(" + cause.getClass().getSimpleName() + ")";
+ }
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java
index 503bc9df..e545d6ec 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java
@@ -78,7 +78,7 @@ import org.springframework.web.multipart.MultipartFile;
*
* @author Chill
*/
-@FeignClient(name = "HCS", url = "${thirdParty.hcs.baseUrl}", configuration = HcsFeignClientConfig.class)
+@FeignClient(name = "HCS", url = "${thirdParty.hcs.baseUrl}", configuration = HcsFeignClientConfig.class, fallbackFactory = HcsClientFallbackFactory.class)
public interface IHcsClient {
String PROJECT_ADD_URL = "${thirdParty.hcs.projectAddUrl:/openapi/sys-modeling/apis/data/km-project1/V1/projectApproval/add}";
diff --git a/doc/docs/hcs合同/2026-10-10-HCS接口500错误信息还原设计(Feign降级工厂).md b/doc/docs/hcs合同/2026-10-10-HCS接口500错误信息还原设计(Feign降级工厂).md
new file mode 100644
index 00000000..a11b92af
--- /dev/null
+++ b/doc/docs/hcs合同/2026-10-10-HCS接口500错误信息还原设计(Feign降级工厂).md
@@ -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`:`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` 配置或由合规方在地址本新建该组织后重推。