diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiMockConfig.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiMockConfig.java
new file mode 100644
index 00000000..fcf24015
--- /dev/null
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiMockConfig.java
@@ -0,0 +1,64 @@
+/**
+ * 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.thirdparty.bfi.config;
+
+import org.springblade.thirdparty.bfi.feign.IBfiClient;
+import org.springblade.thirdparty.bfi.mock.BfiClientMock;
+import org.springframework.beans.factory.ObjectProvider;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+/**
+ * BFI 客户端 Mock 配置
+ *
+ * thirdParty.bfi.mock-enabled=true(Nacos 单入口总开关)时注册 @Primary 的 mock 实现,
+ * 覆盖同类型 Feign 客户端 bean,{@link org.springblade.thirdparty.bfi.service.impl.BfiServiceImpl} 注入的 {@link IBfiClient}
+ * 无感知切换为"有桩用桩、没桩走接口":{mockDir}/{方法名}.json 有桩走本地 JSON 桩,
+ * 无桩透传真实 Feign 接口({@link BfiClientMock},目录经 thirdParty.bfi.mockDir 配置),调用侧零改动。
+ * 仅本地自测/演示环境开启,生产必须保持 false。
+ *
+ * @author Chill
+ */
+@Configuration
+public class BfiMockConfig {
+
+ /**
+ * mock 版 BFI 客户端(@Primary 优先于 Feign 客户端注入)
+ *
+ * @param properties BFI 配置(取桩文件目录 mockDir)
+ * @param realClientProvider 真实客户端候选
+ * @return mock 客户端
+ */
+ @Bean
+ @Primary
+ // @ConditionalOnProperty 不支持宽松绑定,key 必须与 Nacos 实际键完全一致(本仓库 Nacos 为 thirdParty.bfi.* 驼峰根)
+ @ConditionalOnProperty(name = "thirdParty.bfi.mock-enabled", havingValue = "true")
+ public IBfiClient bfiClientMock(BfiProperties properties, ObjectProvider realClientProvider) {
+ return BfiClientMock.create(properties.getMockDir(), realClientProvider);
+ }
+}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiProperties.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiProperties.java
index dd979537..fd0ef5de 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiProperties.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiProperties.java
@@ -65,7 +65,12 @@ public class BfiProperties {
*/
private String language = "zh_CN";
/**
- * 是否启用 mock 模式(开发调试用)
+ * 是否启用 mock 模式(开发调试用):开启后 IBfiClient 走本地桩目录(有桩用桩、无桩透传真实接口,见 BfiClientMock)
*/
private boolean mockEnabled = false;
+ /**
+ * mock 客户端桩文件目录,文件名为接口方法名:{mockDir}/{方法名}.json,如 payApplyQuery.json;
+ * 仅 mock-enabled=true 生效,文件每次调用现读,改文件即生效
+ */
+ private String mockDir = "./mock/bfi";
}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/mock/BfiClientMock.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/mock/BfiClientMock.java
new file mode 100644
index 00000000..795d324c
--- /dev/null
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/mock/BfiClientMock.java
@@ -0,0 +1,176 @@
+/**
+ * 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.thirdparty.bfi.mock;
+
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import feign.Response;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.tool.jackson.JsonUtil;
+import org.springblade.thirdparty.bfi.exception.BfiException;
+import org.springblade.thirdparty.bfi.feign.IBfiClient;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiResultVO;
+import org.springframework.beans.factory.ObjectProvider;
+
+import java.io.InputStream;
+import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+/**
+ * BFI 客户端本地 Mock(thirdParty.bfi.mock-enabled=true 时经 BfiMockConfig 注册为 @Primary,包装真实 Feign 客户端)
+ *
+ * 单入口总开关,方法级混合:每个 IBfiClient 方法对应一个外部目录桩文件 {mockDir}/{方法名}.json(thirdParty.bfi.mockDir 配置),
+ * 有桩即走 mock(内容即金蝶真实响应报文,含 BfiResultVO/BfiOpenApiResultVO 包装,按方法返回泛型反序列化,调用侧无感知),
+ * 无桩自动透传真实 Feign 接口,实现"有桩用桩、没桩走接口"。
+ * 桩文件每次调用现读,改文件即生效无需重启;桩解析失败返回失败的 BfiResultVO,不静默吞错。
+ * 文件下载接口 invoiceDownload 返回 feign.Response,无法按 JSON 桩反序列化,始终透传真实接口。
+ *
+ * @author Chill
+ */
+@Slf4j
+public class BfiClientMock implements InvocationHandler {
+
+ /**
+ * 桩文件后缀(文件名即接口方法名)
+ */
+ private static final String MOCK_FILE_SUFFIX = ".json";
+
+ /**
+ * 桩文件目录
+ */
+ private final Path mockDir;
+
+ /**
+ * 真实客户端候选(排除 mock 自身,运行期首次透传时解析并缓存)
+ */
+ private final ObjectProvider realClientProvider;
+
+ /**
+ * 真实 Feign 客户端缓存(double-checked locking)
+ */
+ private volatile IBfiClient realClient;
+
+ /**
+ * 构建 mock 版 IBfiClient
+ *
+ * @param mockDir 桩文件目录(third-party.bfi.mock-dir)
+ * @param realClientProvider 真实客户端候选(容器内 IBfiClient 全量,含 mock 自身,解析时排除)
+ * @return mock 客户端
+ */
+ public static IBfiClient create(String mockDir, ObjectProvider realClientProvider) {
+ return (IBfiClient) Proxy.newProxyInstance(IBfiClient.class.getClassLoader(),
+ new Class>[]{IBfiClient.class}, new BfiClientMock(mockDir, realClientProvider));
+ }
+
+ private BfiClientMock(String mockDir, ObjectProvider realClientProvider) {
+ this.mockDir = Path.of(mockDir);
+ this.realClientProvider = realClientProvider;
+ }
+
+ @Override
+ public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
+ // Object 基础方法(toString/hashCode/equals)直接执行
+ if (method.getDeclaringClass() != IBfiClient.class) {
+ return method.invoke(this, args);
+ }
+ // 文件下载返回 feign.Response,无法按 JSON 桩反序列化,始终透传
+ if (method.getReturnType() == Response.class) {
+ return this.invokeReal(method, args, proxy);
+ }
+ return this.loadMock(method, args, proxy);
+ }
+
+ /**
+ * 有桩读桩,无桩透传真实接口
+ *
+ * @param method 接口方法
+ * @param args 入参
+ * @param proxy mock 代理自身(解析真实客户端时排除)
+ * @return 桩数据或真实接口响应
+ */
+ private Object loadMock(Method method, Object[] args, Object proxy) throws Throwable {
+ Path mockFile = this.mockDir.resolve(method.getName() + MOCK_FILE_SUFFIX);
+ if (!Files.exists(mockFile)) {
+ log.debug("BFI mock 桩文件缺失,透传真实接口:{}", method.getName());
+ return this.invokeReal(method, args, proxy);
+ }
+ // 外部桩每次调用现读,改文件即生效
+ try (InputStream inputStream = Files.newInputStream(mockFile)) {
+ JavaType javaType = TypeFactory.defaultInstance().constructType(method.getGenericReturnType());
+ return JsonUtil.getInstance().readValue(inputStream, javaType);
+ } catch (Exception exception) {
+ log.error("BFI mock 桩文件解析失败:{}", mockFile.toAbsolutePath(), exception);
+ return this.failResult("BFI mock 桩文件解析失败:" + mockFile.toAbsolutePath());
+ }
+ }
+
+ /**
+ * 透传真实 Feign 客户端(首次调用时懒解析,排除 mock 代理自身,异常原样抛出)
+ *
+ * @param method 接口方法
+ * @param args 入参
+ * @param proxy mock 代理自身
+ * @return 真实接口响应
+ * @throws Throwable 真实调用异常(Feign 降级/业务异常原样透传)
+ */
+ private Object invokeReal(Method method, Object[] args, Object proxy) throws Throwable {
+ IBfiClient real = this.realClient;
+ if (real == null) {
+ synchronized (this) {
+ if (this.realClient == null) {
+ this.realClient = this.realClientProvider.stream()
+ .filter(client -> client != proxy)
+ .findFirst()
+ .orElseThrow(() -> new BfiException("BFI mock 桩缺失且未找到真实 BFI 客户端:" + method.getName()));
+ }
+ real = this.realClient;
+ }
+ }
+ try {
+ return method.invoke(real, args);
+ } catch (InvocationTargetException exception) {
+ throw exception.getCause();
+ }
+ }
+
+ /**
+ * 构造失败的 BfiResultVO(桩解析异常时兜底,避免调用侧拿到 null)
+ *
+ * @param msg 失败原因
+ * @return 失败结果
+ */
+ private BfiResultVO> failResult(String msg) {
+ BfiResultVO> result = new BfiResultVO<>();
+ result.setStatus(false);
+ result.setErrorCode("errors.mock");
+ result.setMessage(msg);
+ return result;
+ }
+}
diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml
index 6e390a3d..41dd3399 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -47,6 +47,11 @@ thirdParty:
accountId: ${BFI_ACCOUNT_ID:}
# 语言字串,默认 zh_CN
language: ${BFI_LANGUAGE:zh_CN}
+ # BFI 客户端本地 Mock 开关(联调自测用,生产必须 false):
+ # true 时 IBfiClient 有桩用桩({mockDir}/{接口方法名}.json)、无桩透传真实 Feign 接口,调用侧零改动
+ mock-enabled: ${BFI_MOCK_ENABLED:false}
+ # Mock 桩文件目录:文件名为接口方法名,如 getToken.json、payApplyQuery.json;文件每次调用现读,改文件即生效
+ mockDir: ${BFI_MOCK_DIR:./mock/bfi}
# 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS)
srcSys: ${BFI_SRC_SYS:WLTMS}
# 应付/应收/收票单分录物料映射(A102-2/A302-2/A303-2 共用,未配置使用代码默认值):