From 80e56d01c9dd39f3f7d498d5a10044fb1efba1b6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 15:12:02 +0800
Subject: [PATCH 01/63] =?UTF-8?q?feat:=20=E4=BB=98=E6=AC=BE=E7=89=B9?=
=?UTF-8?q?=E6=89=B9=E7=BB=93=E6=9E=9C=E9=99=84=E4=BB=B6=E8=90=BD=E5=BA=93?=
=?UTF-8?q?=E5=B9=B6=E7=BB=8FC103-5=E4=B8=8B=E8=BD=BD=E8=81=94=E5=B8=A6?=
=?UTF-8?q?=E4=B8=8A=E4=BC=A0=E9=87=91=E8=9D=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
paymentLegal/result 回调 fileUrl(合规附件ID)落库 hcs_spec_attach_fd_id;金蝶附件联带上传新增合规附件来源(C103-5 按 fdId 下载,幂等键=fdId,20MB 封顶);付款申请详情动态追加特批附件展示行走 C103-5 代理下载;同步提交同链路配套修正:paymentLegal/result 回调改按合规特批流程ID(hcs_spec_fd_id)定位付款申请、C106-3 特批发起报文补充 fd_contract_plan 款项日期(履约计划行合规阶段ID);含 blade_payment_application 增量脚本
---
.../pojo/entity/PaymentApplication.java | 2 +
.../transport/constant/HcsSyncConstant.java | 5 +++
.../controller/HcsSyncController.java | 2 +-
.../impl/PaymentApplicationServiceImpl.java | 42 ++++++++++++++++-
.../support/KingdeeAttachmentSyncSupport.java | 45 ++++++++++++++++---
.../support/hcs/HcsPaymentSyncSupport.java | 19 +++++---
.../support/hcs/HcsSpecPaySyncSupport.java | 17 +++++++
.../hcs/dto/HcsLegalResultDTO.java | 6 ++-
.../hcs/dto/HcsSpecPayPropsDTO.java | 5 +++
...ade_payment_spec_attach_fd_id_20261027.sql | 11 +++++
10 files changed, 136 insertions(+), 18 deletions(-)
create mode 100644 doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
index e4021397..b5b00417 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
@@ -141,6 +141,8 @@ public class PaymentApplication extends TenantEntity {
private String hcsSpecFdId;
/** 特批状态:none/need/pushing/processing/approved/rejected */
private String hcsSpecStatus;
+ /** 合规付款特批结果附件ID(paymentLegal/result 回调 fileUrl,C103-5 下载入参、金蝶附件联带用) */
+ private String hcsSpecAttachFdId;
/** 履约记录ID(C108-1 返回,C108-2 更新必传) */
private String hcsPerfFdId;
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index 42eaaf09..5585ea46 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -190,6 +190,11 @@ public final class HcsSyncConstant {
*/
public static final String PAYMENT_ATTACH_TYPE_SPECIAL = "special_approval";
+ /**
+ * 付款申请附件类型值:合规特批结果附件(paymentLegal/result 回传展示行标记,详情展示用,不参与特批附件提交校验)
+ */
+ public static final String PAYMENT_ATTACH_TYPE_SPEC_RESULT = "spec_result";
+
/**
* 附件类型:用印附件(双章归档文件,取值沿用 ContractManageServiceImpl#FILE_TYPE_SEAL_ARCHIVE)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index e192d772..2526f7dc 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -307,7 +307,7 @@ public class HcsSyncController {
*
* 免登免授权同合同接口(blade.secure 放行);
* iPaaS 网关注册映射:/gwzh/HTHG/paymentLegal/result → /hcs/paymentLegal/result;
- * 报文 {sourceSystem, requestId=TMS付款申请ID, status=APPROVED/REJECTED, reason},
+ * 报文 {sourceSystem, requestId=合规特批流程ID(hcs_spec_fd_id), status=APPROVED/REJECTED, reason},
* approved 恢复"审批通过"放行,rejected 联动付款审批驳回走重新提交闭环。
*/
@PostMapping("/paymentLegal/result")
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index 98777d55..4f957049 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -116,6 +116,8 @@ import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@@ -261,6 +263,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery()
.eq(PaymentApplicationInvoice::getPaymentApplicationId, id).orderByAsc(PaymentApplicationInvoice::getLineNo)));
@@ -585,6 +589,34 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl rows = Func.isBlank(vo.getAttachmentsJson())
+ ? new ArrayList<>() : new ArrayList<>(JsonUtil.parse(vo.getAttachmentsJson(), List.class));
+ rows.removeIf(row -> row instanceof Map, ?> map
+ && HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT.equals(map.get("attachmentType")));
+ Map specRow = new LinkedHashMap<>();
+ specRow.put("name", "付款特批附件");
+ specRow.put("attachmentType", HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT);
+ specRow.put("url", "/hcs/attachment/download?fdId=" + vo.getHcsSpecAttachFdId()
+ + "&name=" + URLEncoder.encode("付款特批附件", StandardCharsets.UTF_8));
+ rows.add(specRow);
+ vo.setAttachmentsJson(JsonUtil.toJson(rows));
+ } catch (Exception exception) {
+ log.warn("付款申请[{}]特批结果附件展示行追加失败", vo.getId(), exception);
+ }
+ }
+
/**
* 附件联带上传(A902):主单推送成功(含幂等预检命中)后调用,
* 以本地 kingdeeBillId(星瀚单据内码)定位金蝶单据;本地缺失(存量数据)跳过待重推补传;
@@ -599,8 +631,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl items = new ArrayList<>(
+ kingdeeAttachmentSyncSupport.parseUserAttachments(entity.getAttachmentsJson()));
+ // 合规付款特批结果附件(paymentLegal/result 回传):C103-5 按合规附件ID 下载后联带上传金蝶
+ if (Func.isNotBlank(entity.getHcsSpecAttachFdId())) {
+ items.add(KingdeeAttachmentSyncSupport.KingdeeAttachmentItem.hcsAttachment("付款特批附件", entity.getHcsSpecAttachFdId()));
+ }
+ kingdeeAttachmentSyncSupport.syncAttachments(KingdeeAttachmentConstant.BIZ_PAY_APPLICATION,
+ id, entity.getKingdeeBillId(), items);
} catch (Exception exception) {
log.error("付款申请[{}]金蝶附件联带上传异常(不影响主单同步状态,重推自动补传)", id, exception);
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java
index dd20ddde..ed9a38f4 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java
@@ -37,6 +37,7 @@ import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.transport.constant.KingdeeAttachmentConstant;
import org.springblade.transport.mapper.KingdeeAttachmentSyncMapper;
import org.springblade.transport.pojo.entity.KingdeeAttachmentSync;
+import org.springblade.transport.support.hcs.HcsAttachmentSupport;
import org.springframework.stereotype.Component;
import java.io.ByteArrayOutputStream;
@@ -64,9 +65,10 @@ import java.util.stream.Collectors;
* 2. 附件失败永不回滚、不污染主单同步状态(逐附件独立 try/catch);
* 3. 同 (biz_type, biz_id, attachment_key) 最多一条成功记录(唯一键 + 成功集合幂等跳过);
* 4. 不自动重试,重推主单时自动补传;5. 单文件软限制 20MB,超限/下载失败记原因跳过。
- * 附件来源三类:用户附件(attachmentsJson 数组项,按 url 下载,幂等键=url,仅接受 http(s) 绝对地址)、
* 票池镜像发票文件(attachmentsJson 对象 {source,path,name},经 A801-2 下载回传,幂等键=path,
- * path 变化(红冲重开)视为新文件自动触发重传)。
+ * path 变化(红冲重开)视为新文件自动触发重传)与
+ * HCS 合规附件(付款特批结果附件等,按合规附件ID 经 C103-5 下载,幂等键=fdId)。
*
* @author Chill
*/
@@ -80,6 +82,7 @@ public class KingdeeAttachmentSyncSupport {
private final IBfiService bfiService;
private final KingdeeAttachmentSyncMapper attachmentSyncMapper;
+ private final HcsAttachmentSupport hcsAttachmentSupport;
/** 附件下载客户端(无状态,进程内复用) */
private final HttpClient downloadClient = HttpClient.newBuilder()
@@ -91,11 +94,30 @@ public class KingdeeAttachmentSyncSupport {
* 待同步附件项
*
* @param name 附件名称(含扩展名,展示/金蝶侧文件名)
- * @param key 幂等键:用户附件=附件 url;发票文件=A801-2 的 path
+ * @param key 幂等键:用户附件=附件 url;发票文件=A801-2 的 path;合规附件=合规附件ID fdId
* @param size 附件大小(字节,可空:空时下载后按实际字节校验)
* @param kingdeePathSource true 表示 key 为星瀚侧文件 path(经 A801-2 下载);false 表示 http(s) 附件 url
+ * @param hcsFdIdSource true 表示 key 为合规附件ID fdId(经 C103-5 下载)
*/
- public record KingdeeAttachmentItem(String name, String key, Long size, boolean kingdeePathSource) {
+ public record KingdeeAttachmentItem(String name, String key, Long size, boolean kingdeePathSource, boolean hcsFdIdSource) {
+
+ /**
+ * 兼容既有四参构造(默认非合规附件来源)
+ */
+ public KingdeeAttachmentItem(String name, String key, Long size, boolean kingdeePathSource) {
+ this(name, key, size, kingdeePathSource, false);
+ }
+
+ /**
+ * HCS 合规附件项(C103-5 按合规附件ID 下载)
+ *
+ * @param name 附件名称
+ * @param fdId 合规附件ID
+ * @return 附件项
+ */
+ public static KingdeeAttachmentItem hcsAttachment(String name, String fdId) {
+ return new KingdeeAttachmentItem(name, fdId, null, false, true);
+ }
}
/**
@@ -225,7 +247,8 @@ public class KingdeeAttachmentSyncSupport {
continue;
}
String url = firstText(map, "url", "link", "fileUrl", "downloadUrl", "domain", "src");
- if (Func.isBlank(url)) {
+ // 仅接受 http(s) 绝对地址:相对/代理路径(如合规特批附件展示行)无法直下,跳过防下载必败
+ if (Func.isBlank(url) || !(url.startsWith("http://") || url.startsWith("https://"))) {
continue;
}
String name = firstText(map, "originalName", "name", "fileName");
@@ -362,10 +385,18 @@ public class KingdeeAttachmentSyncSupport {
}
/**
- * 拉取附件内容:星瀚侧文件经 A801-2 下载(流式读取,防大文件全量进内存失控);
- * 用户附件按 http(s) url 下载(流式读取)。调用方负责按 20MB 上限校验
+ * 拉取附件内容:合规附件经 C103-5 按合规附件ID 下载(流式读取,防大文件全量进内存失控);
+ * 星瀚侧文件经 A801-2 下载(流式读取);用户附件按 http(s) url 下载(流式读取)。调用方负责按 20MB 上限校验
*/
private byte[] fetchContent(KingdeeAttachmentItem item) throws Exception {
+ if (item.hcsFdIdSource()) {
+ Response response = hcsAttachmentSupport.downloadRaw(item.key());
+ try (InputStream inputStream = response.body().asInputStream()) {
+ return readCapped(inputStream);
+ } finally {
+ response.close();
+ }
+ }
if (item.kingdeePathSource()) {
Response response = bfiService.downloadInvoice(item.key());
try (InputStream inputStream = response.body().asInputStream()) {
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
index 46059777..3bb56b68 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
@@ -26,6 +26,7 @@
package org.springblade.transport.support.hcs;
import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
@@ -219,7 +220,7 @@ public class HcsPaymentSyncSupport {
/**
* 付款特批结果接收处理(合规 paymentLegal/result 回调通知特批审批结果):
* sourceSystem 须与 third-party.hcs.src-system 配置一致(不符抛出"来源系统标识错误"异常),
- * requestId=TMS 付款申请ID、status=APPROVED/REJECTED;
+ * requestId=合规特批流程ID(hcs_spec_fd_id,C106-3 回存值)、status=APPROVED/REJECTED;
* 特批结果联动付款审批状态:approved 恢复"审批通过"放行(后续金蝶推送放行),
* rejected 联动付款审批驳回(returned),走"重新编辑→提交→审批→重新特批"闭环
*
@@ -250,6 +251,11 @@ public class HcsPaymentSyncSupport {
}
syncPaymentOnSpecResult(payment, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status));
payment.setHcsSpecStatus(status);
+ // 特批通过回传特批结果附件(fileUrl 值=合规附件ID):落库供 C103-5 下载与金蝶附件联带;
+ // 回调无附件时保留旧值(shortcut: 驳回重批期间旧附件仍展示,重批回调会覆盖,金蝶侧特批中门禁不会误传)
+ if (HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status) && StringUtil.isNotBlank(param.getFileUrl())) {
+ payment.setHcsSpecAttachFdId(param.getFileUrl().trim());
+ }
paymentApplicationMapper.updateById(payment);
// 特批通过后补推履约计划与履约记录(驳回不产生履约数据),失败不阻断后续金蝶补推
if (HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status)) {
@@ -288,17 +294,16 @@ public class HcsPaymentSyncSupport {
}
/**
- * 按 TMS 付款申请ID 定位付款申请(合规推送 requestId)
+ * 按合规特批流程ID定位付款申请(合规推送 requestId=hcs_spec_fd_id,即 C106-3 返回并回存的流程 fdId;
+ * 与合同链路 locateContractByFdId 同口径,C106-3 发起报文未传 TMS 付款申请ID,合规无法回传数字ID)
*/
private PaymentApplication locatePayment(String requestId) {
if (StringUtil.isBlank(requestId)) {
return null;
}
- try {
- return paymentApplicationMapper.selectById(Long.valueOf(requestId.trim()));
- } catch (NumberFormatException exception) {
- return null;
- }
+ return paymentApplicationMapper.selectOne(Wrappers.lambdaQuery()
+ .eq(PaymentApplication::getHcsSpecFdId, requestId.trim())
+ .last("limit 1"));
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
index 6f4de144..eb258a17 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
@@ -42,7 +42,9 @@ import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.entity.PaymentApplication;
+import org.springblade.transport.service.IContractExeStageService;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@@ -74,6 +76,7 @@ public class HcsSpecPaySyncSupport {
private final HcsProperties hcsProperties;
private final ContractManageMapper contractManageMapper;
private final PaymentApplicationMapper paymentApplicationMapper;
+ private final IContractExeStageService contractExeStageService;
private final HcsAttachmentSupport hcsAttachmentSupport;
/**
@@ -156,6 +159,8 @@ public class HcsSpecPaySyncSupport {
props.setFdDocStatus(hcsProperties.getSpecPay().getDocStatus());
// 特批附件字段标识:固定 fd_spc_approval_attach
props.setFdSpcApprovalAttach(SPEC_ATTACH_ENTITY_KEY);
+ // 【必填】款项日期:传履约计划id(匹配付款计划行合规阶段ID,无关联不传,口径同 C108 履约记录)
+ props.setFdContractPlan(this.resolveStageFdId(payment));
formData.setDynamicProps(props);
// 【必填】表单层单据主题
formData.setFdDocSubject("结算付款特批-" + payment.getPaymentNo());
@@ -168,6 +173,18 @@ public class HcsSpecPaySyncSupport {
return param;
}
+ /**
+ * 匹配合同的付款履约计划行合规阶段ID(无匹配返回 null,DTO NON_NULL 序列化时不传,与 C108 履约记录口径一致)
+ */
+ private String resolveStageFdId(PaymentApplication payment) {
+ if (payment.getContractId() == null) {
+ return null;
+ }
+ HcsContractExeStage stageRow = this.contractExeStageService.findPaymentStageRow(payment.getContractId(),
+ HcsSyncConstant.fundNatureOfPayment(payment.getPaymentType()));
+ return stageRow == null ? null : stageRow.getHcsStageFdId();
+ }
+
/**
* 特批附件挂载(附件转传开关关闭或无附件时返回 null)
*/
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.java
index b74880d9..1cb8bb4f 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsLegalResultDTO.java
@@ -51,7 +51,7 @@ public class HcsLegalResultDTO implements Serializable {
*/
private String sourceSystem;
/**
- * 定位单据ID(付款特批=TMS 付款申请ID;合同特批=合规合同ID,与 id 同值,按 blade_contract_manage.hcs_fd_id 匹配)
+ * 定位单据ID(付款特批=合规特批流程ID(hcs_spec_fd_id,C106-3 回存值);合同特批=合规合同ID,与 id 同值,按 blade_contract_manage.hcs_fd_id 匹配)
*/
private String requestId;
/**
@@ -66,4 +66,8 @@ public class HcsLegalResultDTO implements Serializable {
* 结果说明(驳回原因等,非必传)
*/
private String reason;
+ /**
+ * 特批结果附件(付款特批回传携带,值为合规附件ID,C103-5 下载入参,非必传)
+ */
+ private String fileUrl;
}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java
index d2ddbce8..eb9d3fc1 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java
@@ -145,4 +145,9 @@ public class HcsSpecPayPropsDTO implements Serializable {
*/
@JsonProperty("fd_spc_approval_attach")
private String fdSpcApprovalAttach;
+ /**
+ * 款项日期(传履约计划id,接口必填;无关联计划行不传,与 C108 履约记录口径一致)
+ */
+ @JsonProperty("fd_contract_plan")
+ private String fdContractPlan;
}
diff --git a/doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql b/doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql
new file mode 100644
index 00000000..4cb562eb
--- /dev/null
+++ b/doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql
@@ -0,0 +1,11 @@
+-- 付款特批结果回传附件(paymentLegal/result 回调 fileUrl=合规附件ID):
+-- blade_payment_application 追加合规特批结果附件ID 字段,供 C103-5 下载与金蝶附件联带上传。
+-- 通过 information_schema 判断字段是否存在,可重复执行。
+SET @db_name = DATABASE();
+
+SET @sql = IF(
+ EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = @db_name AND table_name = 'blade_payment_application' AND column_name = 'hcs_spec_attach_fd_id'),
+ 'SELECT 1',
+ 'ALTER TABLE blade_payment_application ADD COLUMN hcs_spec_attach_fd_id varchar(128) DEFAULT NULL COMMENT ''合规付款特批结果附件ID(paymentLegal/result 回调 fileUrl,C103-5 下载入参)'''
+);
+PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
From e5c251b1ef48f5a96ccad23df3cf458f77b1558b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 15:32:15 +0800
Subject: [PATCH 02/63] =?UTF-8?q?fix:=20=E5=B1=A5=E7=BA=A6=E8=AE=A1?=
=?UTF-8?q?=E5=88=92ID=E5=9B=9E=E5=A1=AB=E7=AE=80=E5=8C=96=E4=B8=BA?=
=?UTF-8?q?=E5=93=8D=E5=BA=94=E8=A1=8C=E9=A6=96=E4=B8=AA=E9=9D=9E=E7=A9=BA?=
=?UTF-8?q?fdId=E5=B9=B6=E8=A6=86=E7=9B=96fdAddStages=E9=A6=96=E6=AC=A1?=
=?UTF-8?q?=E5=90=8C=E6=AD=A5=E5=9C=BA=E6=99=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsExeStageSyncSupport.java | 43 +++++++++++--------
1 file changed, 25 insertions(+), 18 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
index 89bed20c..9cc2a52d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
@@ -46,19 +46,19 @@ import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.ArrayList;
-import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.stream.Stream;
/**
* HCS 履约规则同步支撑组件(C103-9 新版行级同步,设计文档 §5.3,ADR-0002)
*
- * 随合同推送链路执行(合同须已同步、hcs_fd_id 存在):聚合 row_op 非 none 的规则行
- * (add 行不带 fdId,update/delete 行必须带合规阶段ID),整体 fdEopExeplanOperateType
- * 首次为 add、后续为 update;返回的 fdExeplanId 回填 blade_contract_manage.hcs_exeplan_id,
- * 变更阶段 fdId 按行回填 hcs_stage_fd_id(合规返回的 fdId 必须持久化,后续行更新/删除全靠它)。
+ * 随合同推送链路执行(合同须已同步、hcs_fd_id 存在):聚合 row_op 非 none 的规则行,
+ * 整体 fdEopExeplanOperateType 首次为 add、后续为 update;请求行必送 fdUniqueId(取 TMS 行ID,重试稳定),响应行回 fdId;
+ * 履约计划ID 取返回 fdChangeStages 行的 fdId 回填 blade_contract_manage.hcs_exeplan_id(实际响应无顶层 fdExeplanId),
+ * 各行 fdId 按行回填 hcs_stage_fd_id(必须持久化,供履约记录等接口引用)。
*
* @author Chill
*/
@@ -123,10 +123,8 @@ public class HcsExeStageSyncSupport {
List stages = new ArrayList<>();
for (HcsContractExeStage row : pendingRows) {
HcsExeplanStageDTO stage = new HcsExeplanStageDTO();
- // 阶段项ID:行级 update/delete 必传(取 hcs_stage_fd_id),add 行不带
- if (!"add".equals(row.getRowOp())) {
- stage.setFdId(row.getHcsStageFdId());
- }
+ // 阶段唯一标识:所有行必传(取 TMS 行ID,重试重发保持不变,合规按此匹配行;add 行 hcs_stage_fd_id 为空不能作值)
+ stage.setFdUniqueId(String.valueOf(row.getId()));
// 【必填】行级操作:add / update / delete
stage.setFdEopExeplanOperateType(row.getRowOp());
// 【必填】履约类型:payment=付款 / collection=收款
@@ -154,19 +152,25 @@ public class HcsExeStageSyncSupport {
}
/**
- * 回填:fdExeplanId → 合同 hcs_exeplan_id;变更阶段 fdId 按行回填 hcs_stage_fd_id 并清除行级操作标记
+ * 回填:响应行 fdId 即履约计划ID → 合同 hcs_exeplan_id(fdAddStages/fdChangeStages 取首个非空);各行 fdId 按行回填 hcs_stage_fd_id 并清除行级操作标记
*
- * TODO 待联调确认:fdChangeStages 与请求行的对应关系 v4 未明确,优先按请求行序对应(首次全 add 场景),
- * 行数不一致时按业务属性组合键匹配,仍匹配不到的 add 行保持待同步(下轮重试)。
+ * 新增行阶段在 fdAddStages 返回(实测不含业务属性,按 add 行出现顺序对应),变更行在 fdChangeStages 返回;
+ * fdChangeStages 按请求行序对应,行数不一致时按业务属性组合键匹配,仍匹配不到的 add 行保持待同步(下轮重试)。
*/
private void backfill(ContractManage contractManage, List pendingRows,
List requestStages, HcsExeplanVO exeplan) {
- // 履约计划ID 回填(后续行更新/删除依赖,必须持久化)
- if (StringUtil.isNotBlank(exeplan.getFdExeplanId())
- && !exeplan.getFdExeplanId().equals(contractManage.getHcsExeplanId())) {
- contractManage.setHcsExeplanId(exeplan.getFdExeplanId());
+ // 履约计划ID 回填:响应行 fdId 即履约计划ID(首次同步行在 fdAddStages、变更行在 fdChangeStages,取首个非空回填)
+ String fdExeplanId = Stream.of(exeplan.getFdChangeStages(), exeplan.getFdAddStages())
+ .filter(Objects::nonNull)
+ .flatMap(List::stream)
+ .map(HcsExeplanStageVO::getFdId)
+ .filter(StringUtil::isNotBlank)
+ .findFirst().orElse(null);
+ if (StringUtil.isNotBlank(fdExeplanId)) {
+ contractManage.setHcsExeplanId(fdExeplanId);
}
backfillExeplanId(contractManage);
+ List addStages = exeplan.getFdAddStages() == null ? List.of() : exeplan.getFdAddStages();
List changeStages = exeplan.getFdChangeStages() == null ? List.of() : exeplan.getFdChangeStages();
// 行数一致时按请求行序一一对应;不一致时按业务属性组合键匹配
boolean positional = changeStages.size() == requestStages.size();
@@ -177,9 +181,12 @@ public class HcsExeStageSyncSupport {
HcsContractExeStage row = pendingRows.get(index);
String stageFdId = row.getHcsStageFdId();
if ("add".equals(row.getRowOp())) {
- addRowCount++;
- HcsExeplanStageVO matched = positional ? changeStages.get(index) : byAttribute.get(attributeKey(requestStages.get(index)));
+ HcsExeplanStageVO matched = addRowCount < addStages.size() ? addStages.get(addRowCount) : null;
+ if (matched == null) {
+ matched = positional ? changeStages.get(index) : byAttribute.get(attributeKey(requestStages.get(index)));
+ }
stageFdId = matched == null ? null : matched.getFdId();
+ addRowCount++;
if (StringUtil.isBlank(stageFdId)) {
unmatchedCount++;
}
From 5756cd9b2df83b0ccd24fec4bfa63cd30ce64042 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 18:20:05 +0800
Subject: [PATCH 03/63] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E5=8F=AF?=
=?UTF-8?q?=E9=80=89=E5=90=88=E5=90=8C=E6=8E=A5=E5=8F=A3=E4=B8=BA=E6=89=B9?=
=?UTF-8?q?=E9=87=8F=E6=9F=A5=E8=AF=A2=E8=A7=A3=E5=86=B3N+1=E8=B6=85?=
=?UTF-8?q?=E6=97=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../impl/PreSettlementServiceImpl.java | 45 +++++++++++--------
1 file changed, 27 insertions(+), 18 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java
index f9bdc3a9..6b0247ca 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java
@@ -94,6 +94,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* 预结算单服务实现类
@@ -232,13 +233,34 @@ public class PreSettlementServiceImpl extends BaseServiceImpl contracts = contractManageService.list(wrapper).stream()
- .filter(this::isAvailableContract).toList();
+ .filter(contract -> contract.getProjectId() != null
+ && List.of("客户合同", "承运商合同").contains(contract.getContractCategory())
+ && contractManageService.isUsableForSettlement(contract))
+ .toList();
+ if (contracts.isEmpty()) return List.of();
+ // 项目与客户均批量查询,避免逐条 N+1 查询导致接口超时
Set projectIds = contracts.stream().map(ContractManage::getProjectId)
- .filter(Objects::nonNull).collect(Collectors.toSet());
- Map projectById = Func.isEmpty(projectIds) ? Map.of()
- : projectApplyService.listByIds(projectIds).stream()
+ .collect(Collectors.toSet());
+ Map projectById = projectApplyService.listByIds(projectIds).stream()
.collect(Collectors.toMap(ProjectApply::getId, Function.identity()));
- return contracts.stream().map(contract -> {
+ Set partyNames = contracts.stream()
+ .flatMap(contract -> Stream.of(contract.getPartyA(), contract.getPartyB()))
+ .filter(Func::isNotEmpty).collect(Collectors.toSet());
+ Set approvedCustomers = customerArchiveService.list(Wrappers.lambdaQuery()
+ .eq(CustomerArchive::getIsDeleted, 0)
+ .eq(CustomerArchive::getStatus, 1)
+ .eq(CustomerArchive::getApprovalStatus, "approved")
+ .in(CustomerArchive::getFullName, partyNames))
+ .stream().map(CustomerArchive::getFullName).collect(Collectors.toSet());
+ return contracts.stream()
+ .filter(contract -> {
+ ProjectApply project = projectById.get(contract.getProjectId());
+ return project != null && !Objects.equals(project.getIsDeleted(), 1)
+ && List.of("approved", "change_approved").contains(project.getApprovalStatus())
+ && approvedCustomers.contains(contract.getPartyA())
+ && approvedCustomers.contains(contract.getPartyB());
+ })
+ .map(contract -> {
ProjectApply project = projectById.get(contract.getProjectId());
Map result = new LinkedHashMap<>();
result.put("id", contract.getId());
@@ -1325,19 +1347,6 @@ public class PreSettlementServiceImpl extends BaseServiceImpl contractFeeTypeMap(Long contractId) {
ContractManage contract = contractId == null ? null : contractManageService.getById(contractId);
Map result = new LinkedHashMap<>();
From 79bdb8fc424a29e111a05a1bf8f06432fe7d1f4e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 18:39:28 +0800
Subject: [PATCH 04/63] =?UTF-8?q?fix:=20=E9=87=91=E8=9D=B6=E9=99=84?=
=?UTF-8?q?=E4=BB=B6=E4=B8=8A=E4=BC=A0=E8=A1=A5=E9=BD=90=E5=90=88=E8=A7=84?=
=?UTF-8?q?=E9=99=84=E4=BB=B6=E6=96=87=E4=BB=B6=E5=90=8E=E7=BC=80=E5=90=8D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
合规报文无文件名字段,固定名付款特批附件无后缀被BFI拒收(无后缀名不支持上传);改为解析C103-5响应头Content-Disposition文件名,其次按Content-Type映射扩展名,兜底.pdf;主单已同步时重推同步金蝶自动重试附件上传
---
.../support/KingdeeAttachmentSyncSupport.java | 103 +++++++++++++++++-
1 file changed, 97 insertions(+), 6 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java
index ed9a38f4..4b5b609d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeAttachmentSyncSupport.java
@@ -44,9 +44,11 @@ import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
+import java.net.URLDecoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
@@ -177,12 +179,14 @@ public class KingdeeAttachmentSyncSupport {
throw new IllegalArgumentException("附件超过20MB大小限制("
+ formatSize(item.size()) + "),请压缩后重试");
}
- byte[] content = fetchContent(item);
+ FetchedFile fetched = fetchContent(item);
+ byte[] content = fetched.content();
if (content.length > KingdeeAttachmentConstant.MAX_FILE_SIZE_BYTES) {
throw new IllegalArgumentException("附件超过20MB大小限制("
+ formatSize((long) content.length) + "),请压缩后重试");
}
- String fileName = Func.isBlank(item.name()) ? "attachment" : item.name();
+ String fileName = Func.isNotBlank(fetched.fileName()) ? fetched.fileName()
+ : (Func.isBlank(item.name()) ? "attachment" : item.name());
BfiAttachmentUploadVO uploadVO = bfiService.uploadAttachment(entityNumber, billPkId,
KingdeeAttachmentConstant.CONTROL_KEY, fileName, content);
log.info("金蝶附件上传成功:业务单据[{}:{}],附件[{}],kingdeePath={}", bizType, bizId, item.name(), uploadVO.getPath());
@@ -384,15 +388,24 @@ public class KingdeeAttachmentSyncSupport {
return record;
}
+ /**
+ * 附件内容拉取结果(合规附件按 C103-5 响应头解析真实文件名,其余来源 fileName 为 null 沿用原名称)
+ *
+ * @param content 附件内容
+ * @param fileName 解析出的文件名(可空)
+ */
+ private record FetchedFile(byte[] content, String fileName) {
+ }
+
/**
* 拉取附件内容:合规附件经 C103-5 按合规附件ID 下载(流式读取,防大文件全量进内存失控);
* 星瀚侧文件经 A801-2 下载(流式读取);用户附件按 http(s) url 下载(流式读取)。调用方负责按 20MB 上限校验
*/
- private byte[] fetchContent(KingdeeAttachmentItem item) throws Exception {
+ private FetchedFile fetchContent(KingdeeAttachmentItem item) throws Exception {
if (item.hcsFdIdSource()) {
Response response = hcsAttachmentSupport.downloadRaw(item.key());
try (InputStream inputStream = response.body().asInputStream()) {
- return readCapped(inputStream);
+ return new FetchedFile(readCapped(inputStream), resolveHcsFileName(response, item.name()));
} finally {
response.close();
}
@@ -400,7 +413,7 @@ public class KingdeeAttachmentSyncSupport {
if (item.kingdeePathSource()) {
Response response = bfiService.downloadInvoice(item.key());
try (InputStream inputStream = response.body().asInputStream()) {
- return readCapped(inputStream);
+ return new FetchedFile(readCapped(inputStream), null);
} finally {
response.close();
}
@@ -414,10 +427,88 @@ public class KingdeeAttachmentSyncSupport {
throw new IllegalStateException("附件下载失败,HTTP状态码 " + response.statusCode());
}
try (InputStream inputStream = response.body()) {
- return readCapped(inputStream);
+ return new FetchedFile(readCapped(inputStream), null);
}
}
+ /**
+ * 解析合规附件文件名(BFI 附件上传要求带后缀名):名称已带后缀原样返回;
+ * 否则优先取 C103-5 响应头 Content-Disposition 文件名,其次按 Content-Type 映射扩展名追加,兜底 .pdf
+ *
+ * @param response C103-5 文件流响应
+ * @param fallbackName 兜底名称(无扩展名)
+ * @return 带后缀名的文件名
+ */
+ private String resolveHcsFileName(Response response, String fallbackName) {
+ String name = Func.isBlank(fallbackName) ? "付款特批附件" : fallbackName.trim();
+ if (name.lastIndexOf('.') > 0) {
+ return name;
+ }
+ String fromDisposition = fileNameFromDisposition(firstHeader(response, "Content-Disposition"));
+ if (Func.isNotBlank(fromDisposition) && fromDisposition.lastIndexOf('.') > 0) {
+ return fromDisposition;
+ }
+ return name + extensionOfContentType(firstHeader(response, "Content-Type"));
+ }
+
+ /**
+ * 响应头 Content-Disposition 文件名提取(容忍 filename*=UTF-8''xxx 与 filename="xxx" 两种形式,异常返回 null)
+ */
+ private String fileNameFromDisposition(String disposition) {
+ if (Func.isBlank(disposition)) {
+ return null;
+ }
+ try {
+ String lower = disposition.toLowerCase();
+ int index = lower.indexOf("filename*=");
+ if (index >= 0) {
+ String value = disposition.substring(index + "filename*=".length());
+ int quote = value.indexOf("''");
+ value = quote >= 0 ? value.substring(quote + 2) : value;
+ return URLDecoder.decode(value.replaceAll("\"", "").trim(), StandardCharsets.UTF_8);
+ }
+ index = lower.indexOf("filename=");
+ if (index >= 0) {
+ return URLDecoder.decode(disposition.substring(index + "filename=".length())
+ .replaceAll("\"", "").trim(), StandardCharsets.UTF_8);
+ }
+ } catch (Exception ignored) {
+ // 响应头格式异常按无文件名处理,走 Content-Type 扩展名兜底
+ }
+ return null;
+ }
+
+ /** 响应头取值(key 忽略大小写,取首个非空值) */
+ private String firstHeader(Response response, String name) {
+ if (response.headers() == null) {
+ return null;
+ }
+ for (Map.Entry> header : response.headers().entrySet()) {
+ if (header.getKey() != null && header.getKey().equalsIgnoreCase(name) && Func.isNotEmpty(header.getValue())) {
+ for (String value : header.getValue()) {
+ if (Func.isNotBlank(value)) {
+ return value;
+ }
+ }
+ }
+ }
+ return null;
+ }
+
+ /** Content-Type → 扩展名(特批结果附件常规为 pdf,未识别类型兜底 .pdf) */
+ private String extensionOfContentType(String contentType) {
+ String type = contentType == null ? "" : contentType.split(";")[0].trim().toLowerCase();
+ return switch (type) {
+ case "application/pdf" -> ".pdf";
+ case "image/jpeg" -> ".jpg";
+ case "image/png" -> ".png";
+ case "image/bmp" -> ".bmp";
+ case "image/gif" -> ".gif";
+ case "application/ofd" -> ".ofd";
+ default -> ".pdf";
+ };
+ }
+
/** 流式读取并封顶(超出软限制即中断读取,避免大文件占满内存) */
private byte[] readCapped(InputStream inputStream) throws IOException {
int bufferSize = 64 * 1024;
From a4fa6e45717f1ca295f7e81fbfffacace832f124 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 19:07:46 +0800
Subject: [PATCH 05/63] =?UTF-8?q?fix:=20=E4=BB=98=E6=AC=BE=E7=89=B9?=
=?UTF-8?q?=E6=89=B9=E9=99=84=E4=BB=B6=E5=89=8D=E7=AB=AF=E6=9F=A5=E7=9C=8B?=
=?UTF-8?q?=E4=B8=8B=E8=BD=BD=E6=94=BE=E8=A1=8C=E5=B9=B6=E6=94=AF=E6=8C=81?=
=?UTF-8?q?pdf=E5=86=85=E5=B5=8C=E9=A2=84=E8=A7=88?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
附件下载接口加入 nacos skip-url 与 auth permitAll(前端新窗口打开无令牌,fdId 32位随机不可枚举);下载接口支持 inline=true 内嵌预览(pdf 在线打开,Content-Type application/pdf),默认仍为 attachment 下载;特批附件展示行 url 改网关服务名前缀 /blade-transport 全路径并携带 name=付款特批附件.pdf 与 inline=true
---
.../transport/controller/HcsSyncController.java | 10 +++++++---
.../service/impl/PaymentApplicationServiceImpl.java | 7 ++++---
doc/nacos/blade.yaml | 10 ++++++++++
3 files changed, 21 insertions(+), 6 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index 2526f7dc..2b099116 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -261,13 +261,17 @@ public class HcsSyncController {
*/
@GetMapping("/attachment/download")
@ApiOperationSupport(order = 12)
- @Operation(summary = "合规附件下载", description = "C103-5 代理:按合规附件ID 下载文件流,name 参数可选用于指定下载文件名")
+ @Operation(summary = "合规附件下载", description = "C103-5 代理:按合规附件ID 下载文件流,name 参数可选用于指定下载文件名,inline=true 时内嵌预览(pdf 浏览器直接打开)")
public void downloadAttachment(@Parameter(description = "合规附件ID", required = true) @RequestParam String fdId,
@Parameter(description = "下载文件名(可选)") @RequestParam(required = false) String name,
+ @Parameter(description = "内嵌预览(可选,pdf 在线查看)") @RequestParam(required = false) Boolean inline,
HttpServletResponse response) throws Exception {
String fileName = StringUtil.isBlank(name) ? "hcs-attachment-" + fdId : name;
- response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
- response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''" + URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20"));
+ // 内嵌预览:inline + 真实类型浏览器直接打开(pdf 在线查看),默认 attachment 走下载
+ boolean preview = Boolean.TRUE.equals(inline) && fileName.toLowerCase().endsWith(".pdf");
+ response.setContentType(preview ? "application/pdf" : MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ response.setHeader("Content-Disposition", (preview ? "inline" : "attachment") + "; filename*=UTF-8''"
+ + URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20"));
try (Response hcsResponse = hcsAttachmentSupport.downloadRaw(fdId);
InputStream inputStream = hcsResponse.body().asInputStream();
OutputStream outputStream = response.getOutputStream()) {
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index 4f957049..e91c6ba1 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -606,10 +606,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl row instanceof Map, ?> map
&& HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT.equals(map.get("attachmentType")));
Map specRow = new LinkedHashMap<>();
- specRow.put("name", "付款特批附件");
+ specRow.put("name", "付款特批附件.pdf");
specRow.put("attachmentType", HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT);
- specRow.put("url", "/hcs/attachment/download?fdId=" + vo.getHcsSpecAttachFdId()
- + "&name=" + URLEncoder.encode("付款特批附件", StandardCharsets.UTF_8));
+ // 网关按服务名前缀路由(口径同 nacos skip-url);inline 内嵌预览 pdf,浏览器可直接查看/另存
+ specRow.put("url", "/blade-transport/hcs/attachment/download?fdId=" + vo.getHcsSpecAttachFdId()
+ + "&inline=true&name=" + URLEncoder.encode("付款特批附件.pdf", StandardCharsets.UTF_8));
rows.add(specRow);
vo.setAttachmentsJson(JsonUtil.toJson(rows));
} catch (Exception exception) {
diff --git a/doc/nacos/blade.yaml b/doc/nacos/blade.yaml
index 8515a4fb..7f1b43ef 100644
--- a/doc/nacos/blade.yaml
+++ b/doc/nacos/blade.yaml
@@ -246,6 +246,9 @@ blade:
- /hcs/projectLegal/result
- /blade-transport/hcs/contractKingdee/result
- /hcs/contractKingdee/result
+ # HCS 合规附件下载(C103-5 代理):前端附件面板凭合规附件ID 查看/下载,fdId 32位随机不可枚举,免登免授权
+ - /blade-transport/hcs/attachment/download
+ - /hcs/attachment/download
#授权认证配置
auth:
- method: ALL
@@ -298,6 +301,13 @@ blade:
- method: POST
pattern: /hcs/contractKingdee/result
expression: "permitAll()"
+ # HCS 合规附件下载(C103-5 代理):前端附件面板凭合规附件ID 查看/下载,fdId 32位随机不可枚举,免登免授权
+ - method: GET
+ pattern: /blade-transport/hcs/attachment/download
+ expression: "permitAll()"
+ - method: GET
+ pattern: /hcs/attachment/download
+ expression: "permitAll()"
#基础认证配置
basic:
- method: ALL
From bdb1d3e4bb2d44a50f7c9f5cafdb30a8ebb8ee32 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 19:19:07 +0800
Subject: [PATCH 06/63] =?UTF-8?q?fix:=20=E9=87=91=E8=9D=B6=E4=B8=8E?=
=?UTF-8?q?=E5=89=8D=E7=AB=AF=E5=B1=95=E7=A4=BA=E7=BB=9F=E4=B8=80=E7=89=B9?=
=?UTF-8?q?=E6=89=B9=E9=99=84=E4=BB=B6=E5=90=8D=E4=B8=BA=E4=BB=98=E6=AC=BE?=
=?UTF-8?q?=E7=89=B9=E6=89=B9=E6=B5=81=E7=A8=8B=E9=99=84=E4=BB=B6.pdf?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
resolveHcsFileName 改为统一业务名+扩展名(扩展名仍取C103-5响应头Content-Disposition后缀,其次Content-Type映射,兜底.pdf),不再上送合规侧原始名(文档内容.pdf);名称收敛为常量 PAYMENT_SPEC_ATTACH_NAME 供金蝶上送与前端展示行共用
---
.../transport/constant/HcsSyncConstant.java | 5 ++++
.../impl/PaymentApplicationServiceImpl.java | 7 ++---
.../support/KingdeeAttachmentSyncSupport.java | 27 ++++++++++++-------
3 files changed, 27 insertions(+), 12 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index 5585ea46..26c54bb0 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -195,6 +195,11 @@ public final class HcsSyncConstant {
*/
public static final String PAYMENT_ATTACH_TYPE_SPEC_RESULT = "spec_result";
+ /**
+ * 合规特批结果附件统一业务名(金蝶上送与前端展示用,上传时按 C103-5 响应头补扩展名)
+ */
+ public static final String PAYMENT_SPEC_ATTACH_NAME = "付款特批流程附件";
+
/**
* 附件类型:用印附件(双章归档文件,取值沿用 ContractManageServiceImpl#FILE_TYPE_SEAL_ARCHIVE)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index e91c6ba1..63da966d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -606,11 +606,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl row instanceof Map, ?> map
&& HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT.equals(map.get("attachmentType")));
Map specRow = new LinkedHashMap<>();
- specRow.put("name", "付款特批附件.pdf");
+ specRow.put("name", HcsSyncConstant.PAYMENT_SPEC_ATTACH_NAME + ".pdf");
specRow.put("attachmentType", HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT);
// 网关按服务名前缀路由(口径同 nacos skip-url);inline 内嵌预览 pdf,浏览器可直接查看/另存
specRow.put("url", "/blade-transport/hcs/attachment/download?fdId=" + vo.getHcsSpecAttachFdId()
- + "&inline=true&name=" + URLEncoder.encode("付款特批附件.pdf", StandardCharsets.UTF_8));
+ + "&inline=true&name=" + URLEncoder.encode(HcsSyncConstant.PAYMENT_SPEC_ATTACH_NAME + ".pdf", StandardCharsets.UTF_8));
rows.add(specRow);
vo.setAttachmentsJson(JsonUtil.toJson(rows));
} catch (Exception exception) {
@@ -636,7 +636,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0) {
return name;
}
- String fromDisposition = fileNameFromDisposition(firstHeader(response, "Content-Disposition"));
- if (Func.isNotBlank(fromDisposition) && fromDisposition.lastIndexOf('.') > 0) {
- return fromDisposition;
+ String extension = extensionOf(fileNameFromDisposition(firstHeader(response, "Content-Disposition")));
+ if (Func.isBlank(extension)) {
+ extension = extensionOfContentType(firstHeader(response, "Content-Type"));
}
- return name + extensionOfContentType(firstHeader(response, "Content-Type"));
+ return name + extension;
+ }
+
+ /** 取文件名扩展名(含点,无扩展名返回空串) */
+ private String extensionOf(String fileName) {
+ String name = Func.isBlank(fileName) ? "" : fileName.trim();
+ int index = name.lastIndexOf('.');
+ return index > 0 && index < name.length() - 1 ? name.substring(index) : "";
}
/**
From 4918054762073a7b3526dc73dd199469e379c304 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 19:22:30 +0800
Subject: [PATCH 07/63] =?UTF-8?q?fix:=20=E7=89=B9=E6=89=B9=E9=99=84?=
=?UTF-8?q?=E4=BB=B6=E5=B1=95=E7=A4=BA=E8=A1=8C=E9=99=84=E4=BB=B6=E7=B1=BB?=
=?UTF-8?q?=E5=9E=8B=E6=94=B9=E4=B8=BA=E4=BB=98=E6=AC=BE=E7=89=B9=E6=89=B9?=
=?UTF-8?q?=E6=B5=81=E7=A8=8B=E9=99=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
展示行 attachmentType 由 spec_result 改为中文值付款特批流程附件,与附件名称口径一致;详情追加时兼容清理 spec_result 旧值历史行防重复
---
.../org/springblade/transport/constant/HcsSyncConstant.java | 2 +-
.../transport/service/impl/PaymentApplicationServiceImpl.java | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index 26c54bb0..bf786eaf 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -193,7 +193,7 @@ public final class HcsSyncConstant {
/**
* 付款申请附件类型值:合规特批结果附件(paymentLegal/result 回传展示行标记,详情展示用,不参与特批附件提交校验)
*/
- public static final String PAYMENT_ATTACH_TYPE_SPEC_RESULT = "spec_result";
+ public static final String PAYMENT_ATTACH_TYPE_SPEC_RESULT = "付款特批流程附件";
/**
* 合规特批结果附件统一业务名(金蝶上送与前端展示用,上传时按 C103-5 响应头补扩展名)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index 63da966d..4e4b8aee 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -603,8 +603,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl rows = Func.isBlank(vo.getAttachmentsJson())
? new ArrayList<>() : new ArrayList<>(JsonUtil.parse(vo.getAttachmentsJson(), List.class));
+ // 剔除历史展示行(含改版前 spec_result 旧值),避免前端随单保存后重复追加
rows.removeIf(row -> row instanceof Map, ?> map
- && HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT.equals(map.get("attachmentType")));
+ && (HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT.equals(map.get("attachmentType"))
+ || "spec_result".equals(map.get("attachmentType"))));
Map specRow = new LinkedHashMap<>();
specRow.put("name", HcsSyncConstant.PAYMENT_SPEC_ATTACH_NAME + ".pdf");
specRow.put("attachmentType", HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT);
From 85d1576b1afc3a129c5a1b4e0a2b39010bcb1ff6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 19:31:14 +0800
Subject: [PATCH 08/63] =?UTF-8?q?fix:=20=E7=89=B9=E6=89=B9=E9=99=84?=
=?UTF-8?q?=E4=BB=B6=E4=B8=8B=E8=BD=BD=E6=96=B0=E5=A2=9E=E8=B7=AF=E5=BE=84?=
=?UTF-8?q?=E5=BC=8F=E6=96=87=E4=BB=B6=E5=90=8D=E7=AB=AF=E7=82=B9=E6=94=AF?=
=?UTF-8?q?=E6=8C=81=E5=89=8D=E7=AB=AF=E9=A2=84=E8=A7=88=E8=AF=86=E5=88=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
前端附件组件按url路径后缀判断可否预览,query携带的pdf后缀不识别导致提示仅能下载;新增GET /hcs/attachment/download/{name} 端点(路径携带文件名含后缀,行为与原接口一致),特批附件展示行url改路径式;nacos放行改 /** 通配同时覆盖两种形式
---
.../transport/controller/HcsSyncController.java | 14 ++++++++++++++
.../impl/PaymentApplicationServiceImpl.java | 8 +++++---
doc/nacos/blade.yaml | 8 ++++----
3 files changed, 23 insertions(+), 7 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index 2b099116..531d9b94 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -280,6 +280,20 @@ public class HcsSyncController {
}
}
+ /**
+ * 合同附件下载(路径式文件名,url 以 .pdf 等后缀结尾):前端附件组件按 url 路径后缀识别可否预览,
+ * query 传 fdId 与 inline;行为与 /attachment/download 完全一致
+ */
+ @GetMapping("/attachment/download/{name}")
+ @ApiOperationSupport(order = 12)
+ @Operation(summary = "合规附件下载(路径式文件名)", description = "C103-5 代理:路径携带文件名(含后缀,前端预览识别用),fdId 必传,inline=true 时内嵌预览")
+ public void downloadAttachmentWithPath(@Parameter(description = "下载文件名(含后缀)", required = true) @PathVariable String name,
+ @Parameter(description = "合规附件ID", required = true) @RequestParam String fdId,
+ @Parameter(description = "内嵌预览(可选,pdf 在线查看)") @RequestParam(required = false) Boolean inline,
+ HttpServletResponse response) throws Exception {
+ this.downloadAttachment(fdId, name, inline, response);
+ }
+
/**
* 合同特批结果人工确认(§6-U1 过渡方案:与合规推送等效)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index 4e4b8aee..9352a005 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -610,9 +610,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl specRow = new LinkedHashMap<>();
specRow.put("name", HcsSyncConstant.PAYMENT_SPEC_ATTACH_NAME + ".pdf");
specRow.put("attachmentType", HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT);
- // 网关按服务名前缀路由(口径同 nacos skip-url);inline 内嵌预览 pdf,浏览器可直接查看/另存
- specRow.put("url", "/blade-transport/hcs/attachment/download?fdId=" + vo.getHcsSpecAttachFdId()
- + "&inline=true&name=" + URLEncoder.encode(HcsSyncConstant.PAYMENT_SPEC_ATTACH_NAME + ".pdf", StandardCharsets.UTF_8));
+ // 网关按服务名前缀路由(口径同 nacos skip-url);url 路径以 .pdf 结尾供前端预览组件识别后缀,
+ // inline 内嵌预览,浏览器可直接查看/另存
+ specRow.put("url", "/blade-transport/hcs/attachment/download/"
+ + URLEncoder.encode(HcsSyncConstant.PAYMENT_SPEC_ATTACH_NAME + ".pdf", StandardCharsets.UTF_8)
+ + "?fdId=" + vo.getHcsSpecAttachFdId() + "&inline=true");
rows.add(specRow);
vo.setAttachmentsJson(JsonUtil.toJson(rows));
} catch (Exception exception) {
diff --git a/doc/nacos/blade.yaml b/doc/nacos/blade.yaml
index 7f1b43ef..1917bf5f 100644
--- a/doc/nacos/blade.yaml
+++ b/doc/nacos/blade.yaml
@@ -247,8 +247,8 @@ blade:
- /blade-transport/hcs/contractKingdee/result
- /hcs/contractKingdee/result
# HCS 合规附件下载(C103-5 代理):前端附件面板凭合规附件ID 查看/下载,fdId 32位随机不可枚举,免登免授权
- - /blade-transport/hcs/attachment/download
- - /hcs/attachment/download
+ - /blade-transport/hcs/attachment/download/**
+ - /hcs/attachment/download/**
#授权认证配置
auth:
- method: ALL
@@ -303,10 +303,10 @@ blade:
expression: "permitAll()"
# HCS 合规附件下载(C103-5 代理):前端附件面板凭合规附件ID 查看/下载,fdId 32位随机不可枚举,免登免授权
- method: GET
- pattern: /blade-transport/hcs/attachment/download
+ pattern: /blade-transport/hcs/attachment/download/**
expression: "permitAll()"
- method: GET
- pattern: /hcs/attachment/download
+ pattern: /hcs/attachment/download/**
expression: "permitAll()"
#基础认证配置
basic:
From cc465d4cee7d00cfbd2b11eb2d0c7688daf529e0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 19:37:10 +0800
Subject: [PATCH 09/63] =?UTF-8?q?fix:=20=E7=89=B9=E6=89=B9=E9=99=84?=
=?UTF-8?q?=E4=BB=B6=E5=B1=95=E7=A4=BA=E8=A1=8Curl=E8=A1=A5/api=E4=BB=A3?=
=?UTF-8?q?=E7=90=86=E5=89=8D=E7=BC=80=E5=B9=B6=E6=94=B9=E4=B8=BA=E4=BB=85?=
=?UTF-8?q?=E4=B8=8B=E8=BD=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
预览失败根因:展示行相对路径缺前端代理前缀/api,浏览器请求落在前端SPA路由返回index.html;实测 /api/blade-transport/hcs/attachment/download 已返回真实文件流;按需求去掉inline内嵌预览逻辑(删除inline参数与pdf分支),固定attachment下载;展示行url改为 /api 前缀路径式文件名
---
.../transport/controller/HcsSyncController.java | 16 ++++++----------
.../impl/PaymentApplicationServiceImpl.java | 7 +++----
2 files changed, 9 insertions(+), 14 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index 531d9b94..220ce98b 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -264,13 +264,10 @@ public class HcsSyncController {
@Operation(summary = "合规附件下载", description = "C103-5 代理:按合规附件ID 下载文件流,name 参数可选用于指定下载文件名,inline=true 时内嵌预览(pdf 浏览器直接打开)")
public void downloadAttachment(@Parameter(description = "合规附件ID", required = true) @RequestParam String fdId,
@Parameter(description = "下载文件名(可选)") @RequestParam(required = false) String name,
- @Parameter(description = "内嵌预览(可选,pdf 在线查看)") @RequestParam(required = false) Boolean inline,
HttpServletResponse response) throws Exception {
String fileName = StringUtil.isBlank(name) ? "hcs-attachment-" + fdId : name;
- // 内嵌预览:inline + 真实类型浏览器直接打开(pdf 在线查看),默认 attachment 走下载
- boolean preview = Boolean.TRUE.equals(inline) && fileName.toLowerCase().endsWith(".pdf");
- response.setContentType(preview ? "application/pdf" : MediaType.APPLICATION_OCTET_STREAM_VALUE);
- response.setHeader("Content-Disposition", (preview ? "inline" : "attachment") + "; filename*=UTF-8''"
+ response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
+ response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''"
+ URLEncoder.encode(fileName, StandardCharsets.UTF_8).replace("+", "%20"));
try (Response hcsResponse = hcsAttachmentSupport.downloadRaw(fdId);
InputStream inputStream = hcsResponse.body().asInputStream();
@@ -281,17 +278,16 @@ public class HcsSyncController {
}
/**
- * 合同附件下载(路径式文件名,url 以 .pdf 等后缀结尾):前端附件组件按 url 路径后缀识别可否预览,
- * query 传 fdId 与 inline;行为与 /attachment/download 完全一致
+ * 合同附件下载(路径式文件名,url 以 .pdf 等后缀结尾):前端附件组件按 url 路径后缀识别附件类型,
+ * query 传 fdId;行为与 /attachment/download 完全一致,强制下载不做内嵌预览
*/
@GetMapping("/attachment/download/{name}")
@ApiOperationSupport(order = 12)
- @Operation(summary = "合规附件下载(路径式文件名)", description = "C103-5 代理:路径携带文件名(含后缀,前端预览识别用),fdId 必传,inline=true 时内嵌预览")
+ @Operation(summary = "合规附件下载(路径式文件名)", description = "C103-5 代理:路径携带文件名(含后缀,前端附件类型识别用),fdId 必传,文件流 attachment 下载")
public void downloadAttachmentWithPath(@Parameter(description = "下载文件名(含后缀)", required = true) @PathVariable String name,
@Parameter(description = "合规附件ID", required = true) @RequestParam String fdId,
- @Parameter(description = "内嵌预览(可选,pdf 在线查看)") @RequestParam(required = false) Boolean inline,
HttpServletResponse response) throws Exception {
- this.downloadAttachment(fdId, name, inline, response);
+ this.downloadAttachment(fdId, name, response);
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index 9352a005..8d4066e3 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -610,11 +610,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl specRow = new LinkedHashMap<>();
specRow.put("name", HcsSyncConstant.PAYMENT_SPEC_ATTACH_NAME + ".pdf");
specRow.put("attachmentType", HcsSyncConstant.PAYMENT_ATTACH_TYPE_SPEC_RESULT);
- // 网关按服务名前缀路由(口径同 nacos skip-url);url 路径以 .pdf 结尾供前端预览组件识别后缀,
- // inline 内嵌预览,浏览器可直接查看/另存
- specRow.put("url", "/blade-transport/hcs/attachment/download/"
+ // 前端代理前缀 /api(网关服务名路由);url 路径以 .pdf 结尾供前端附件组件识别类型,文件流 attachment 强制下载
+ specRow.put("url", "/api/blade-transport/hcs/attachment/download/"
+ URLEncoder.encode(HcsSyncConstant.PAYMENT_SPEC_ATTACH_NAME + ".pdf", StandardCharsets.UTF_8)
- + "?fdId=" + vo.getHcsSpecAttachFdId() + "&inline=true");
+ + "?fdId=" + vo.getHcsSpecAttachFdId());
rows.add(specRow);
vo.setAttachmentsJson(JsonUtil.toJson(rows));
} catch (Exception exception) {
From f3d598f796137d6f409f66d4b210f8e7a705c052 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 19:43:01 +0800
Subject: [PATCH 10/63] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3HCS=E5=B1=A5?=
=?UTF-8?q?=E7=BA=A6=E9=98=B6=E6=AE=B5=E4=B8=8E=E5=B1=A5=E7=BA=A6=E8=AE=B0?=
=?UTF-8?q?=E5=BD=95=E6=8E=A5=E5=8F=A3=E5=AD=97=E6=AE=B5=E4=BC=A0=E5=80=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- C103-9 履约规则行新增 fdId(update/delete 定位用),fdUniqueId 仅 add 行传值
- 履约计划ID 回填优先取响应顶层 fdExeplanId,兜底响应行 fdId
- C108 履约记录补传 fdPlanId(必填)、fdJdDocNo、fdDocSubject,附件补 fdBindType=ADD
- 履约计划行未同步时抛异常交由付款链置 failed 重试
- 结算付款特批补传 fd_source_sys 外部系统编号
---
.../support/hcs/HcsExeStageSyncSupport.java | 15 ++++++++-----
.../support/hcs/HcsPaymentSyncSupport.java | 2 ++
.../support/hcs/HcsPerfRecordSyncSupport.java | 22 ++++++++++++++-----
.../support/hcs/HcsSpecPaySyncSupport.java | 2 ++
.../hcs/dto/HcsExeplanStageDTO.java | 9 ++++++--
.../hcs/dto/HcsSpecPayPropsDTO.java | 5 +++++
6 files changed, 42 insertions(+), 13 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
index 9cc2a52d..088b44d8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsExeStageSyncSupport.java
@@ -57,7 +57,7 @@ import java.util.stream.Stream;
*
* 随合同推送链路执行(合同须已同步、hcs_fd_id 存在):聚合 row_op 非 none 的规则行,
* 整体 fdEopExeplanOperateType 首次为 add、后续为 update;请求行必送 fdUniqueId(取 TMS 行ID,重试稳定),响应行回 fdId;
- * 履约计划ID 取返回 fdChangeStages 行的 fdId 回填 blade_contract_manage.hcs_exeplan_id(实际响应无顶层 fdExeplanId),
+ * 履约计划ID 取响应顶层 fdExeplanId 回填 blade_contract_manage.hcs_exeplan_id,
* 各行 fdId 按行回填 hcs_stage_fd_id(必须持久化,供履约记录等接口引用)。
*
* @author Chill
@@ -123,8 +123,12 @@ public class HcsExeStageSyncSupport {
List stages = new ArrayList<>();
for (HcsContractExeStage row : pendingRows) {
HcsExeplanStageDTO stage = new HcsExeplanStageDTO();
- // 阶段唯一标识:所有行必传(取 TMS 行ID,重试重发保持不变,合规按此匹配行;add 行 hcs_stage_fd_id 为空不能作值)
- stage.setFdUniqueId(String.valueOf(row.getId()));
+ // 阶段行ID:update/delete 行必传(HCS 凭首次 add 返回的 fdId 定位行,缺失报"履约计划已不存在"),add 行无值自动省略
+ stage.setFdId(row.getHcsStageFdId());
+ // 阶段唯一标识:仅 add 行必传(取 TMS 行ID,HCS 回显存储);update/delete 行实测无需携带,以 fdId 定位
+ if ("add".equals(row.getRowOp())) {
+ stage.setFdUniqueId(String.valueOf(row.getId()));
+ }
// 【必填】行级操作:add / update / delete
stage.setFdEopExeplanOperateType(row.getRowOp());
// 【必填】履约类型:payment=付款 / collection=收款
@@ -159,8 +163,9 @@ public class HcsExeStageSyncSupport {
*/
private void backfill(ContractManage contractManage, List pendingRows,
List requestStages, HcsExeplanVO exeplan) {
- // 履约计划ID 回填:响应行 fdId 即履约计划ID(首次同步行在 fdAddStages、变更行在 fdChangeStages,取首个非空回填)
- String fdExeplanId = Stream.of(exeplan.getFdChangeStages(), exeplan.getFdAddStages())
+ // 履约计划ID 回填:优先取响应顶层 fdExeplanId(实测 add/update 均有返回),兜底取响应行 fdId
+ String fdExeplanId = StringUtil.isNotBlank(exeplan.getFdExeplanId()) ? exeplan.getFdExeplanId()
+ : Stream.of(exeplan.getFdChangeStages(), exeplan.getFdAddStages())
.filter(Objects::nonNull)
.flatMap(List::stream)
.map(HcsExeplanStageVO::getFdId)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
index 3bb56b68..1783fe6f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
@@ -157,6 +157,8 @@ public class HcsPaymentSyncSupport {
String message = StringUtil.isBlank(exception.getMessage()) ? "履约数据推送异常" : exception.getMessage();
markFailed(payment, message);
log.error("付款特批通过后履约数据推送失败:paymentNo={},{}", payment.getPaymentNo(), message, exception);
+ // todo
+ throw new ServiceException("付款特批通过后履约数据推送失败:" + message);
}
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
index 7e7aa5a0..54e14846 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
@@ -58,7 +58,7 @@ import java.util.List;
* 付款申请为主线:审批通过 → C108-1 新增(返回 fdId 回填 hcs_perf_fd_id);
* 实际付款推进(paidAmount 变化置待同步)→ C108-2 更新(formData.fdId=hcs_perf_fd_id 必填,响应无 data)。
* 关联履约阶段:按合同+付款类型匹配已同步的履约规则行(付款/收款执行与计划阶段对应),
- * 无关联则不传 fd_id(§5.6)。
+ * fd_plan_id(履约计划id)接口必填,无已同步计划行时抛出重试(合同链路首推后自愈)。
*
* @author Chill
*/
@@ -149,6 +149,10 @@ public class HcsPerfRecordSyncSupport {
private HcsPerfRecordFormDataDTO buildFormData(PaymentApplication payment) {
ContractManage contractManage = payment.getContractId() == null ? null : contractManageMapper.selectById(payment.getContractId());
HcsContractExeStage stageRow = findPaymentStageRow(payment);
+ // 【必填】fd_plan_id 履约计划id:无已同步计划行时无值可传,抛出由付款链置 failed 重试(合同链路首推后下轮自愈)
+ if (stageRow == null || StringUtil.isBlank(stageRow.getHcsStageFdId())) {
+ throw new HcsException("C108 履约记录未匹配到已同步履约计划行,fd_plan_id 必传:paymentNo=" + payment.getPaymentNo());
+ }
HcsPerfRecordFormDataDTO formData = new HcsPerfRecordFormDataDTO();
HcsPerfRecordPropsDTO props = new HcsPerfRecordPropsDTO();
// 申请金额:appliedAmount(接口按字符串传值)
@@ -161,10 +165,13 @@ public class HcsPerfRecordSyncSupport {
props.setFdPlanDate(toMillis(payment.getApplyDate()));
// 计划类型:paymentType(原文拼写 fd_paln_type 必须沿用)
props.setFdPalnType(payment.getPaymentType());
- // 性质:款项性质取关联履约规则行的款项性质(无关联不传)
- props.setFdProperty(stageRow == null ? null : stageRow.getFundNature());
- // 来源履约计划项ID:关联履约阶段 hcs_stage_fd_id(无关联则不传,§5.6)
- props.setFdId(stageRow == null ? null : stageRow.getHcsStageFdId());
+ // 性质:款项性质取关联履约计划行的款项性质
+ props.setFdProperty(stageRow.getFundNature());
+ // 履约计划id / 来源履约计划项ID:履约计划行合规阶段ID(HCS 只认自己的阶段ID)
+ props.setFdId(stageRow.getHcsStageFdId());
+ props.setFdPlanId(stageRow.getHcsStageFdId());
+ // 金蝶单据号:付款推送金蝶返回的单号(溯源用)
+ props.setFdJdDocNo(payment.getKingdeeBillNo());
// 【必填】合同ID:HCS 合同ID(合同未推送时为空,联调确认兜底口径)
props.setFdContractId(contractManage == null ? null : contractManage.getHcsFdId());
// 业务ID:TMS 付款申请单主键(方便溯源,v4 新增)
@@ -176,6 +183,8 @@ public class HcsPerfRecordSyncSupport {
props.setFdXformId(xformId);
// 【必填】表单模板ID(formData 顶层 fd_xform_id,v4 必填)
formData.setFdXformId(xformId);
+ // 标题:付款履约-付款申请单号(参照结算付款特批标题风格)
+ formData.setFdDocSubject("付款履约-" + payment.getPaymentNo());
formData.setDynamicProps(props);
return formData;
}
@@ -194,8 +203,9 @@ public class HcsPerfRecordSyncSupport {
}
HcsMechanismsDTO mechanisms = new HcsMechanismsDTO();
mechanisms.setAttachment(attachmentRefs.stream().map(ref -> {
- // 附件ID(C103-2 转传返回)
+ // 附件ID(C103-2 转传返回);C108-1 为发起类接口,绑定类型固定 ADD
ref.setFdEntityKey(PERF_ATTACH_ENTITY_KEY);
+ ref.setFdBindType("ADD");
return ref;
}).toList());
return mechanisms;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
index eb258a17..d9543a14 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsSpecPaySyncSupport.java
@@ -135,6 +135,8 @@ public class HcsSpecPaySyncSupport {
props.setFdDocSubject("结算付款特批-" + payment.getPaymentNo());
// 来源付款单ID:付款申请单号(§5.5 映射表)
props.setFdId(payment.getPaymentNo());
+ props.setFdSourceSys(hcsProperties.getSrcSystem());
+
// 【必填】付款金额:appliedAmount
props.setFdPayAmount(payment.getAppliedAmount());
// 【必填】合计金额:settlementAmount
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageDTO.java
index 21dbc7ac..7443fc35 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageDTO.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanStageDTO.java
@@ -36,7 +36,8 @@ import java.math.BigDecimal;
/**
* 合同合规系统(HCS)C103-9(新) 履约规则同步 行级操作项(数据源 blade_contract_exe_stage,ADR-0002)
*
- * 新增行可不带 fdId;更新/删除行必须带 fdId(合规阶段ID,回填自首次同步返回)。
+ * add 行必传 fdUniqueId(TMS 行唯一标识,HCS 回显存储);update/delete 行必传 fdId(首次 add 返回的阶段行ID,
+ * 持久化于 hcs_stage_fd_id),实测无需 fdUniqueId。
*
* @author Chill
*/
@@ -47,9 +48,13 @@ public class HcsExeplanStageDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
- * 阶段项ID(行级 update/delete 必传,取 blade_contract_exe_stage.hcs_stage_fd_id)
+ * 阶段行ID(HCS 返回的 fdId,持久化于 hcs_stage_fd_id;update/delete 行必传,add 行可不传)
*/
private String fdId;
+ /**
+ * 阶段唯一标识(仅 add 行必传,取 TMS 行ID,HCS 回显存储;update/delete 行以 fdId 定位,无需此值)
+ */
+ private String fdUniqueId;
/**
* 行级操作:add / update / delete
*/
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java
index eb9d3fc1..4b292d5e 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsSpecPayPropsDTO.java
@@ -55,6 +55,11 @@ public class HcsSpecPayPropsDTO implements Serializable {
*/
@JsonProperty("fd_contract_no")
private String fdContractNo;
+ /**
+ * 外部系统编号
+ */
+ @JsonProperty("fd_source_sys")
+ private String fdSourceSys;
/**
* 收款账户名
*/
From dcd32af341a8513fa3f704eec2526740b333e219 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 20:23:51 +0800
Subject: [PATCH 11/63] =?UTF-8?q?:bug:=20=E8=A1=A5=E9=BD=90HcsExeplanVO?=
=?UTF-8?q?=E6=96=B0=E5=A2=9E=E9=98=B6=E6=AE=B5=E5=88=97=E8=A1=A8=E5=AD=97?=
=?UTF-8?q?=E6=AE=B5=E4=BF=AE=E5=A4=8D=E5=B1=A5=E7=BA=A6=E9=98=B6=E6=AE=B5?=
=?UTF-8?q?=E5=90=8C=E6=AD=A5=E7=BC=96=E8=AF=91?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../src/main/java/org/springblade/hcs/dto/HcsExeplanVO.java | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanVO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanVO.java
index 973ff964..be3dc269 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanVO.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsExeplanVO.java
@@ -49,7 +49,11 @@ public class HcsExeplanVO implements Serializable {
*/
private String fdExeplanId;
/**
- * 本次变更的阶段列表(按行回填 hcs_stage_fd_id)
+ * 本次新增的阶段列表(add 行返回,不含业务属性,按行回填 hcs_stage_fd_id)
+ */
+ private List fdAddStages;
+ /**
+ * 本次变更的阶段列表(update/delete 行返回,按行回填 hcs_stage_fd_id)
*/
private List fdChangeStages;
}
From deb08d8af5de80eedd92340d93f68d3e0148b20d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 27 Oct 2026 23:29:20 +0800
Subject: [PATCH 12/63] =?UTF-8?q?:sparkles:=20=E8=A1=A5=E5=85=85=E9=87=91?=
=?UTF-8?q?=E8=9D=B6=E9=BB=91=E5=90=8D=E5=8D=95B=E7=B1=BB=E6=8B=A6?=
=?UTF-8?q?=E6=88=AA=E5=BC=80=E5=85=B3Nacos=E9=85=8D=E7=BD=AE=EF=BC=88bloc?=
=?UTF-8?q?kRiskWarning=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
doc/nacos/blade-dev.yaml | 5 +++++
doc/nacos/blade-prod.yaml | 4 ++++
doc/nacos/third-party-api.yaml | 4 ++++
3 files changed, 13 insertions(+)
diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml
index 7607c247..6b47a7ca 100644
--- a/doc/nacos/blade-dev.yaml
+++ b/doc/nacos/blade-dev.yaml
@@ -155,6 +155,11 @@ thirdParty:
# 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS)
srcSys: ${BFI_SRC_SYS:WLTMS}
+ # 金蝶黑名单查询(A801-5 客商提审拦截):禁止交易类(A)始终拦截;
+ # 风险警示类(B)默认放行告警,开启后一并拦截
+ blacklist:
+ blockRiskWarning: ${BFI_BLACKLIST_BLOCK_RISK_WARNING:false}
+
# 应付/应收/收票单分录物料映射(A102-2/A302-2/A303-2 共用,未配置使用代码默认值):
# 运输类按项目业务模式映射——国内运输按运单运输方式映射、国际运输映射国际货代服务;
# 装卸大类及手动添加的非运费杂费映射装卸物料
diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml
index a444dde6..a7ea8a30 100644
--- a/doc/nacos/blade-prod.yaml
+++ b/doc/nacos/blade-prod.yaml
@@ -120,6 +120,10 @@ thirdParty:
language: ${BFI_LANGUAGE:zh_CN}
# 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS)
srcSys: ${BFI_SRC_SYS:WLTMS}
+ # 金蝶黑名单查询(A801-5 客商提审拦截):禁止交易类(A)始终拦截;
+ # 风险警示类(B)默认放行告警,开启后一并拦截
+ blacklist:
+ blockRiskWarning: ${BFI_BLACKLIST_BLOCK_RISK_WARNING:false}
# 付款申请推送配置(A103-3 付款申请保存)
payApply:
# 单据类型编码:ap_payapply_BT_S 采购付款申请、ap_payapply_oth_BT_S 其他付款申请
diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml
index cda8991b..efbaaef0 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -57,6 +57,10 @@ thirdParty:
# 数据来源系统编码(推送金蝶公共参数:客商、付款申请等统一使用),默认 WLTMS(运输TMS)
srcSys: ${BFI_SRC_SYS:WLTMS}
+ # 金蝶黑名单查询(A801-5 客商提审拦截):禁止交易类(A)始终拦截;
+ # 风险警示类(B)默认放行告警,开启后一并拦截
+ blacklist:
+ blockRiskWarning: ${BFI_BLACKLIST_BLOCK_RISK_WARNING:false}
# 应付/应收/收票单分录物料映射(A102-2/A302-2/A303-2 共用,未配置使用代码默认值):
# 运输类按项目业务模式映射——国内运输按运单运输方式映射、国际运输映射国际货代服务;
# 装卸大类及手动添加的非运费杂费映射装卸物料
From 9a55670d42528445e8ff31cf7acfb76b4863cc91 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 00:22:27 +0800
Subject: [PATCH 13/63] =?UTF-8?q?:sparkles:=20=E6=96=B0=E5=A2=9EBFI?=
=?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3?=
=?UTF-8?q?A901-1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../thirdparty/bfi/feign/IBfiClient.java | 29 +++
.../bfi/pojo/dto/BfiSupplierQueryDTO.java | 83 ++++++++
.../bfi/pojo/vo/BfiSupplierBankVO.java | 114 +++++++++++
.../thirdparty/bfi/pojo/vo/BfiSupplierVO.java | 181 ++++++++++++++++++
.../thirdparty/bfi/service/IBfiService.java | 10 +
.../bfi/service/impl/BfiServiceImpl.java | 14 ++
.../bfi/service/BfiServiceImplTest.java | 19 ++
7 files changed, 450 insertions(+)
create mode 100644 blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiSupplierQueryDTO.java
create mode 100644 blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiSupplierBankVO.java
create mode 100644 blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiSupplierVO.java
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java
index 8e321535..d017ce32 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java
@@ -65,6 +65,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiResultVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiSupplierVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiTokenVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiVerifyTokenVO;
import org.springframework.cloud.openfeign.FeignClient;
@@ -99,6 +100,7 @@ public interface IBfiClient {
String PAY_BILL_QUERY_URL = "${thirdParty.bfi.payBillQueryUrl:/ierp/kapi/v2/awrp/cas/cas_paybill/query}";
String CUSTOMER_QUERY_URL = "${thirdParty.bfi.customerQueryUrl:/ierp/kapi/v2/awrp/basedata/bd_customer/query}";
String CUSTOMER_SAVE_URL = "${thirdParty.bfi.customerSaveUrl:/ierp/kapi/v2/awrp/basedata/bd_customer/save}";
+ String SUPPLIER_QUERY_URL = "${thirdParty.bfi.supplierQueryUrl:/ierp/kapi/v2/awrp/basedata/bd_supplier/query}";
String AP_BILL_SAVE_URL = "${thirdParty.bfi.apBillSaveUrl:/ierp/kapi/v2/awrp/ap/ap/ap_finapbill/save}";
String AP_BILL_QUERY_URL = "${thirdParty.bfi.apBillQueryUrl:/ierp/kapi/v2/awrp/ap/ap_finapbill/query}";
String AR_BILL_SAVE_URL = "${thirdParty.bfi.arBillSaveUrl:/ierp/kapi/v2/awrp/ar/ar/ar_finarbill/save}";
@@ -266,6 +268,33 @@ public interface IBfiClient {
@PostMapping(CUSTOMER_SAVE_URL)
BfiResultVO customerSave(@RequestHeader(BfiConstant.HEADER_ACCESS_TOKEN) String accessToken, @RequestBody BfiCustomerSaveBodyDTO param);
+ /**
+ * A901-1 供应商查询
+ *
+ * @param accessToken 访问令牌,通过 getToken 获取
+ * @param pageNo 页码,从1开始
+ * @param pageSize 页数
+ * @param enable 使用状态
+ * @param status 数据状态
+ * @param createorgNumber 创建组织.编码
+ * @param societycreditcode 统一社会信用代码(个人填写身份证号或手机号)
+ * @param idno 身份证号
+ * @param modifytimeS 修改时间-开始
+ * @param modifytimeE 修改时间-结束
+ * @return 供应商分页数据
+ */
+ @GetMapping(SUPPLIER_QUERY_URL)
+ BfiResultVO> supplierQuery(@RequestHeader(BfiConstant.HEADER_ACCESS_TOKEN) String accessToken,
+ @RequestParam("pageNo") Integer pageNo,
+ @RequestParam("pageSize") Integer pageSize,
+ @RequestParam(value = "enable", required = false) String enable,
+ @RequestParam(value = "status", required = false) String status,
+ @RequestParam(value = "createorg_number", required = false) String createorgNumber,
+ @RequestParam(value = "societycreditcode", required = false) List societycreditcode,
+ @RequestParam(value = "idno", required = false) List idno,
+ @RequestParam(value = "modifytimeS", required = false) String modifytimeS,
+ @RequestParam(value = "modifytimeE", required = false) String modifytimeE);
+
/**
* A102-2 财务应付单保存
*
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiSupplierQueryDTO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiSupplierQueryDTO.java
new file mode 100644
index 00000000..99d835db
--- /dev/null
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiSupplierQueryDTO.java
@@ -0,0 +1,83 @@
+/**
+ * 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.pojo.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 业财融合(BFI)A901-1 供应商查询参数
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class BfiSupplierQueryDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 【必填】页码,从1开始
+ */
+ private Integer pageNo;
+ /**
+ * 【必填】页数
+ */
+ private Integer pageSize;
+ /**
+ * 使用状态[0:禁用, 1:可用]
+ */
+ private String enable;
+ /**
+ * 数据状态[A:暂存, B:已提交, C:已审核]
+ */
+ private String status;
+ /**
+ * 创建组织.编码
+ */
+ @JsonProperty("createorg_number")
+ private String createorgNumber;
+ /**
+ * 修改时间-开始,格式:yyyy-MM-dd HH:mm:ss,限定时间范围不可超过6个月
+ */
+ private String modifytimeS;
+ /**
+ * 修改时间-结束,格式:yyyy-MM-dd HH:mm:ss
+ */
+ private String modifytimeE;
+ /**
+ * 统一社会信用代码(个人填写身份证号或手机号)
+ */
+ private List societycreditcode;
+ /**
+ * 身份证号
+ */
+ private List idno;
+}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiSupplierBankVO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiSupplierBankVO.java
new file mode 100644
index 00000000..852bab9a
--- /dev/null
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiSupplierBankVO.java
@@ -0,0 +1,114 @@
+/**
+ * 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.pojo.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 业财融合(BFI)A901-1 供应商查询 银行信息分录
+ *
+ * @author Chill
+ */
+@Data
+public class BfiSupplierBankVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long id;
+ /**
+ * 分录行号
+ */
+ private Integer seq;
+ /**
+ * 银行账号
+ */
+ private String bankaccount;
+ /**
+ * 账户名称
+ */
+ private String accountname;
+ /**
+ * 默认
+ */
+ @JsonProperty("isdefault_bank")
+ private Boolean isdefaultBank;
+ /**
+ * 银行账户类型
+ */
+ private String bankaccounttype;
+ /**
+ * 账户性质[0:公司, 1:个人]
+ */
+ @JsonProperty("account_property")
+ private String accountProperty;
+ /**
+ * 默认手续费承担方[1:付款方, 2:收款方]
+ */
+ private String commissionbearer;
+ /**
+ * 币种(货币代码)
+ */
+ private Currency currency;
+ /**
+ * 开户银行(编码)
+ */
+ private Bank bank;
+
+ /**
+ * 币种引用
+ */
+ @Data
+ public static class Currency implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 货币代码
+ */
+ private String number;
+ }
+
+ /**
+ * 开户银行引用
+ */
+ @Data
+ public static class Bank implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 编码(行号)
+ */
+ private String number;
+ }
+}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiSupplierVO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiSupplierVO.java
new file mode 100644
index 00000000..523b2bbd
--- /dev/null
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiSupplierVO.java
@@ -0,0 +1,181 @@
+/**
+ * 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.pojo.vo;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 业财融合(BFI)A901-1 供应商查询返回结果
+ *
+ * @author Chill
+ */
+@Data
+public class BfiSupplierVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long id;
+ /**
+ * 编码
+ */
+ private String number;
+ /**
+ * 名称
+ */
+ private String name;
+ /**
+ * 数据状态[A:暂存, B:已提交, C:已审核]
+ */
+ private String status;
+ /**
+ * 使用状态[0:禁用, 1:可用]
+ */
+ private String enable;
+ /**
+ * 创建时间
+ */
+ private String createtime;
+ /**
+ * 修改时间
+ */
+ private String modifytime;
+ /**
+ * 控制策略[1:逐级分配, 2:自由分配, 5:全局共享, 6:管控范围内共享, 7:私有]
+ */
+ private String ctrlstrategy;
+ /**
+ * 简称
+ */
+ private String simplename;
+ /**
+ * 伙伴类型[1:法人企业, 2:非法人企业, 3:非企业单位, 4:个人, 5:个体户, 6:大陆以外的企业]
+ */
+ private String type;
+ /**
+ * 统一社会信用代码(个人填写身份证号或手机号)
+ */
+ private String societycreditcode;
+ /**
+ * 电子邮箱
+ */
+ @JsonProperty("postal_code")
+ private String postalCode;
+ /**
+ * 身份证号
+ */
+ private String idno;
+ /**
+ * 供应商分组.编码
+ */
+ @JsonProperty("group_number")
+ private String groupNumber;
+ /**
+ * 详细地址
+ */
+ @JsonProperty("bizpartner_address")
+ private String bizpartnerAddress;
+ /**
+ * 联系人
+ */
+ private String linkman;
+ /**
+ * 联系电话
+ */
+ @JsonProperty("bizpartner_phone")
+ private String bizpartnerPhone;
+ /**
+ * 传真
+ */
+ @JsonProperty("bizpartner_fax")
+ private String bizpartnerFax;
+ /**
+ * 纳税人识别号
+ */
+ @JsonProperty("tx_register_no")
+ private String txRegisterNo;
+ /**
+ * 法人代表
+ */
+ private String artificialperson;
+ /**
+ * 注册资本
+ */
+ private String regcapital;
+ /**
+ * 营业期限
+ */
+ private String businessterm;
+ /**
+ * 成立日期
+ */
+ private String establishdate;
+ /**
+ * 经营范围
+ */
+ private String businessscope;
+ /**
+ * 业务职能[1:采购, 2:结算, 3:收款, 4:供货]
+ */
+ private String bizfunction;
+ /**
+ * 简拼
+ */
+ private String simplepinyin;
+ /**
+ * 税号
+ */
+ private String taxno;
+ /**
+ * 是否为贸易客商[A:是, B:否]
+ */
+ @JsonProperty("awrp_supfortrade")
+ private String awrpSupfortrade;
+ /**
+ * 数据来源系统[XH:星瀚, WCY:物产云, JCZX:检测中心, NBERP:南北ERP, WCCK:城矿ERP, WY:维云(服务), CMS:合同系统, DTP:数据中台]
+ */
+ @JsonProperty("awrp_datasourcesys")
+ private String awrpDatasourcesys;
+ /**
+ * 备注
+ */
+ @JsonProperty("awrp_sppfbeizhu")
+ private String awrpSppfbeizhu;
+ /**
+ * 银行信息分录
+ */
+ @JsonProperty("entry_bank")
+ private List entryBank;
+}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java
index 50a26ba2..a8ff0944 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java
@@ -48,6 +48,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiPayBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiSupplierQueryDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiArBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiAttachmentUploadVO;
@@ -65,6 +66,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiSupplierVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiTokenVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiVerifyTokenVO;
@@ -183,6 +185,14 @@ public interface IBfiService {
*/
BfiBillSaveResultVO saveCustomer(BfiCustomerSaveDTO param);
+ /**
+ * A901-1 供应商查询
+ *
+ * @param param 查询参数,页码从1开始,修改时间范围不可超过6个月
+ * @return 供应商分页数据
+ */
+ BfiPageVO querySupplier(BfiSupplierQueryDTO param);
+
/**
* A102-2 财务应付单保存
*
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java
index de5f8b77..f3301b41 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java
@@ -72,6 +72,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveBodyDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveDTO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiSupplierQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiTokenDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiVerifyTokenDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiWithdrawTokenDTO;
@@ -95,6 +96,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiResultVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiSupplierVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiTokenVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiVerifyTokenVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
@@ -332,6 +334,18 @@ public class BfiServiceImpl implements IBfiService {
return result.getData();
}
+ @Override
+ public BfiPageVO querySupplier(BfiSupplierQueryDTO param) {
+ if (param == null || param.getPageNo() == null || param.getPageSize() == null) {
+ throw new BfiException("查询BFI供应商失败,分页参数不能为空");
+ }
+ String accessToken = getAccessToken();
+ BfiResultVO> result = commonCall("供应商查询", () -> client.supplierQuery(accessToken,
+ param.getPageNo(), param.getPageSize(), param.getEnable(), param.getStatus(), param.getCreateorgNumber(),
+ param.getSocietycreditcode(), param.getIdno(), param.getModifytimeS(), param.getModifytimeE()));
+ return result.getData();
+ }
+
@Override
public BfiBillSaveResultVO saveApBill(BfiApBillSaveDTO param) {
if (param == null || StringUtil.isBlank(param.getAwrpSrcSys()) || StringUtil.isBlank(param.getAwrpSrcCode())) {
diff --git a/blade-third-party-api/blade-bfi-api/src/test/java/org/springblade/thirdparty/bfi/service/BfiServiceImplTest.java b/blade-third-party-api/blade-bfi-api/src/test/java/org/springblade/thirdparty/bfi/service/BfiServiceImplTest.java
index 5794196b..19c8bb0c 100644
--- a/blade-third-party-api/blade-bfi-api/src/test/java/org/springblade/thirdparty/bfi/service/BfiServiceImplTest.java
+++ b/blade-third-party-api/blade-bfi-api/src/test/java/org/springblade/thirdparty/bfi/service/BfiServiceImplTest.java
@@ -65,6 +65,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveEntryDTO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiSupplierQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiTokenDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiArBillVO;
@@ -78,6 +79,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiResultVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiSupplierVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiTokenVO;
import org.springblade.thirdparty.bfi.service.impl.BfiServiceImpl;
import org.springframework.data.redis.core.ValueOperations;
@@ -212,6 +214,23 @@ class BfiServiceImplTest {
assertThrows(BfiException.class, () -> service.queryCustomer(new BfiCustomerQueryDTO()));
}
+ @Test
+ void querySupplierShouldReturnPage() {
+ mockAccessToken();
+ BfiPageVO page = new BfiPageVO<>();
+ when(client.supplierQuery(eq("token-1"), eq(1), eq(10), any(), any(), any(), any(), any(), any(), any()))
+ .thenReturn(success(page));
+ BfiSupplierQueryDTO param = new BfiSupplierQueryDTO();
+ param.setPageNo(1);
+ param.setPageSize(10);
+ assertEquals(page, service.querySupplier(param));
+ }
+
+ @Test
+ void querySupplierShouldThrowWhenPageEmpty() {
+ assertThrows(BfiException.class, () -> service.querySupplier(new BfiSupplierQueryDTO()));
+ }
+
@Test
void saveCustomerShouldValidateRequiredFields() {
BfiCustomerSaveDTO noName = new BfiCustomerSaveDTO();
From b2b37192ec11af3602e08d96655ab373118326d6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 10:36:14 +0800
Subject: [PATCH 14/63] =?UTF-8?q?:feat:=20=E9=87=91=E8=9D=B6=E6=8E=A8?=
=?UTF-8?q?=E9=80=81=E5=AE=A2=E5=95=86=E7=BC=96=E7=A0=81=E6=94=B9=E4=B8=BA?=
=?UTF-8?q?=E7=BB=8FA901-1=E4=BE=9B=E5=BA=94=E5=95=86=E6=9F=A5=E8=AF=A2?=
=?UTF-8?q?=E5=8F=96=E9=87=91=E8=9D=B6=E7=BC=96=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../impl/InvoiceApplicationServiceImpl.java | 17 ++++++----
.../impl/PaymentApplicationServiceImpl.java | 2 +-
.../support/KingdeeApBillSyncSupport.java | 32 ++++++++++++++++---
.../support/KingdeeArBillSyncSupport.java | 5 +--
.../KingdeeReceivedInvoiceSyncSupport.java | 2 +-
5 files changed, 43 insertions(+), 15 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
index 7b74830e..5de8ea1e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
@@ -823,8 +823,13 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl savedBillNos = new ArrayList<>();
@@ -852,7 +857,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl items) {
BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO();
// ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ====================
@@ -965,8 +970,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpllambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, partyB).or().eq(CustomerArchive::getShortName, partyB))
.eq(CustomerArchive::getIsDeleted, 0).last("limit 1"));
- return customer == null ? null : customer.getCustomerCode();
+ if (customer == null || Func.isBlank(customer.getUnifiedCreditCode())) {
+ return null;
+ }
+ return this.queryKingdeeSupplierNumber(customer.getUnifiedCreditCode().trim());
+ }
+
+ /** A901-1 按统一社会信用代码查询金蝶供应商档案,取返回值 number 即金蝶客商编码,无数据返回 null(开票申请受票方复用);
+ * shortcut: 查询未过滤 status=C(已审核)/enable=1,暂存/禁用档案的 number 也会返回,金蝶侧拒收时再补过滤 */
+ public String queryKingdeeSupplierNumber(String unifiedCreditCode) {
+ BfiSupplierQueryDTO param = new BfiSupplierQueryDTO();
+ param.setPageNo(1);
+ param.setPageSize(1);
+ param.setSocietycreditcode(List.of(unifiedCreditCode));
+ BfiPageVO page = bfiService.querySupplier(param);
+ List rows = page == null ? null : page.getRows();
+ return rows == null || rows.isEmpty() ? null : rows.get(0).getNumber();
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java
index f9e214c1..5a000afb 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java
@@ -202,14 +202,15 @@ public class KingdeeArBillSyncSupport {
String currency = kingdeeApBillSyncSupport.bfiCurrency(contract);
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),以同步当日作为应收单据日期
String billDateText = kingdeeApBillSyncSupport.bfiDateTimeText(LocalDate.now());
- // 往来户.编码:应收结算单付款方(客户)关联客商档案客商编码
+ // 往来户.编码:应收结算单付款方(客户)关联客商档案取统一社会信用代码,经 A901-1 供应商查询取金蝶客商编码
String payerName = settlement.getPayerName();
if (Func.isBlank(payerName)) {
throw new ServiceException("推送金蝶财务应收单失败,结算单付款方不能为空,请检查正式结算单[" + Func.toStr(subject) + "]付款方信息");
}
String customerCode = context.customerCodeCache().computeIfAbsent(payerName.trim(), kingdeeApBillSyncSupport::queryCustomerCode);
if (Func.isBlank(customerCode)) {
- throw new ServiceException("推送金蝶财务应收单失败,结算单付款方[" + payerName + "]未在客商档案中维护客商编码,请补充后重试");
+ throw new ServiceException("推送金蝶财务应收单失败,结算单付款方[" + payerName
+ + "]未在客商档案中维护统一社会信用代码或金蝶侧未查到对应供应商档案,请补充后重试");
}
BfiArBillSaveDTO param = new BfiArBillSaveDTO();
// 【必填】单据类型.编码
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java
index 48a74c14..04e0fdd3 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java
@@ -189,7 +189,7 @@ public class KingdeeReceivedInvoiceSyncSupport {
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract, context));
// 【必填】结算币别:合同结算币种
param.setCurrencyNumber(kingdeeApBillSyncSupport.bfiCurrency(contract));
- // 【必填】往来户.编码:合同乙方(开票供应商)客商编码
+ // 【必填】往来户.编码:合同乙方(开票供应商)统一社会信用代码经 A901-1 供应商查询取金蝶客商编码
param.setReceivablessuppNumber(kingdeeApBillSyncSupport.bfiAsstactNumber(contract, context));
// 【必填】汇率表.编码
param.setExratetableNumber(BfiConstant.EXRATE_TABLE_DEFAULT);
From 632e0e59716dd400ca54dcef50baad9e0787679d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 14:16:17 +0800
Subject: [PATCH 15/63] =?UTF-8?q?:feat:=20=E5=90=88=E5=90=8C=E5=8F=98?=
=?UTF-8?q?=E6=9B=B4=E5=90=88=E8=A7=84=E6=8E=A8=E9=80=81=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?=E5=90=8C=E6=AD=A5=E7=8A=B6=E6=80=81=E5=9B=9E=E5=86=99=E9=97=AD?=
=?UTF-8?q?=E7=8E=AF=EF=BC=8C=E4=BB=85=E5=8E=9F=E5=90=88=E5=90=8C=E5=B7=B2?=
=?UTF-8?q?=E6=8E=A8=E9=80=81=E5=90=88=E8=A7=84=E7=9A=84=E5=8F=98=E6=9B=B4?=
=?UTF-8?q?=E5=8D=95=E5=8F=82=E4=B8=8E=E6=8E=A8=E9=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/ContractChangeController.java | 14 +++++--------
.../service/IContractChangeService.java | 9 +++++++++
.../impl/ContractChangeServiceImpl.java | 20 ++++++++++++++++++-
.../support/hcs/HcsImmediatePushSupport.java | 7 +++++++
4 files changed, 40 insertions(+), 10 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractChangeController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractChangeController.java
index 89ddf9e0..219be8a1 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractChangeController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractChangeController.java
@@ -33,6 +33,7 @@ import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
+import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.pojo.entity.ContractChange;
import org.springblade.transport.pojo.vo.ContractChangeVO;
import org.springblade.transport.service.IContractChangeService;
@@ -110,17 +111,12 @@ public class ContractChangeController extends BladeController {
hcsImmediatePushSupport.pushContractAndWait(contractId);
String syncFailure = HcsImmediatePushSupport.pollSyncFailure();
if (syncFailure != null) {
- ContractChange contractChange = contractChangeService.getById(id);
- if (contractChange != null) {
- contractChange.setSyncStatus("failed");
- contractChangeService.updateById(contractChange);
- }
+ contractChangeService.writeBackSyncStatus(contractId, HcsSyncConstant.HCS_STATUS_FAILED);
return R.data(Boolean.FALSE, "审批通过,但合规推送失败:" + syncFailure);
}
- ContractChange contractChange = contractChangeService.getById(id);
- if (contractChange != null) {
- contractChange.setSyncStatus("synced");
- contractChangeService.updateById(contractChange);
+ // 即时推送未开启时(开关关闭不触发推送)不标记已推送,待推送状态由定时扫描任务推送后闭环
+ if (hcsImmediatePushSupport.isImmediateEnabled()) {
+ contractChangeService.writeBackSyncStatus(contractId, HcsSyncConstant.HCS_STATUS_SYNCED);
}
return R.success("审批通过");
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractChangeService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractChangeService.java
index 8dab1de0..f715bcf7 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractChangeService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractChangeService.java
@@ -72,4 +72,13 @@ public interface IContractChangeService extends BaseService {
*/
boolean markSyncStatus(String ids, String syncStatus);
+ /**
+ * 按原合同回写最近一条待同步变更单的同步状态(审批即时推送/定时任务推送后调用)
+ *
+ * @param contractId 原合同ID
+ * @param syncStatus 同步状态(synced/failed)
+ * @return 是否回写成功(无待同步变更单时返回 false)
+ */
+ boolean writeBackSyncStatus(Long contractId, String syncStatus);
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java
index a663c015..ab043070 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java
@@ -190,7 +190,11 @@ public class ContractChangeServiceImpl extends BaseServiceImpllambdaUpdate()
+ .eq(ContractChange::getContractId, contractId)
+ .eq(ContractChange::getApprovalStatus, STATUS_APPROVED)
+ .in(ContractChange::getSyncStatus, List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_FAILED))
+ .set(ContractChange::getSyncStatus, syncStatus)
+ .last("order by approved_time desc limit 1"));
+ }
+
private LambdaQueryWrapper buildQuery(ContractChangeVO contractChange) {
TransportBusinessSupport.validateAllDept(contractChange.getAllDept(), "合同变更");
LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery().eq(ContractChange::getIsDeleted, 0);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java
index cc0e6106..fc0339ce 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java
@@ -153,6 +153,13 @@ public class HcsImmediatePushSupport {
this.pushWithLock(HcsSyncConstant.BIZ_TYPE_CONTRACT, id, () -> this.pushContractNow(id));
}
+ /**
+ * 即时推送开关(thirdParty.hcs.push.immediate-enabled)状态,供 Controller 判断审批响应是否实际等待过推送结果
+ */
+ public boolean isImmediateEnabled() {
+ return hcsProperties.getPush().isImmediateEnabled();
+ }
+
/**
* 项目立项"通过"按钮路径:在审批事务提交返回后由 Controller 同步调用,失败原因留在当前线程供其取走
*
From b3b299edccf1d3068edbf3ad9547c2972921f866 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 14:16:25 +0800
Subject: [PATCH 16/63] =?UTF-8?q?:feat:=20=E9=A1=B9=E7=9B=AE=E5=8F=98?=
=?UTF-8?q?=E6=9B=B4=E5=90=88=E8=A7=84=E6=8E=A8=E9=80=81=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?=E5=90=8C=E6=AD=A5=E7=8A=B6=E6=80=81=E5=9B=9E=E5=86=99=E9=97=AD?=
=?UTF-8?q?=E7=8E=AF=EF=BC=8C=E4=BB=85=E5=8E=9F=E9=A1=B9=E7=9B=AE=E5=B7=B2?=
=?UTF-8?q?=E6=8E=A8=E9=80=81=E5=90=88=E8=A7=84=E7=9A=84=E5=8F=98=E6=9B=B4?=
=?UTF-8?q?=E5=8D=95=E5=8F=82=E4=B8=8E=E6=8E=A8=E9=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/ProjectChangeController.java | 14 +++++--------
.../service/IProjectChangeService.java | 9 +++++++++
.../impl/ProjectChangeServiceImpl.java | 20 ++++++++++++++++++-
3 files changed, 33 insertions(+), 10 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectChangeController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectChangeController.java
index c697286c..7bfdf1e6 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectChangeController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectChangeController.java
@@ -33,6 +33,7 @@ import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
+import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.pojo.entity.ProjectChange;
import org.springblade.transport.pojo.vo.ProjectChangeVO;
import org.springblade.transport.service.IProjectChangeService;
@@ -110,17 +111,12 @@ public class ProjectChangeController extends BladeController {
hcsImmediatePushSupport.pushProjectAndWait(projectId);
String syncFailure = HcsImmediatePushSupport.pollSyncFailure();
if (syncFailure != null) {
- ProjectChange projectChange = projectChangeService.getById(id);
- if (projectChange != null) {
- projectChange.setSyncStatus("failed");
- projectChangeService.updateById(projectChange);
- }
+ projectChangeService.writeBackSyncStatus(projectId, HcsSyncConstant.HCS_STATUS_FAILED);
return R.data(Boolean.FALSE, "审批通过,但合规推送失败:" + syncFailure);
}
- ProjectChange projectChange = projectChangeService.getById(id);
- if (projectChange != null) {
- projectChange.setSyncStatus("synced");
- projectChangeService.updateById(projectChange);
+ // 即时推送未开启时(开关关闭不触发推送)不标记已推送,待推送状态由定时扫描任务推送后闭环
+ if (hcsImmediatePushSupport.isImmediateEnabled()) {
+ projectChangeService.writeBackSyncStatus(projectId, HcsSyncConstant.HCS_STATUS_SYNCED);
}
return R.success("审批通过");
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectChangeService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectChangeService.java
index 1cbafa62..81fd622a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectChangeService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectChangeService.java
@@ -67,4 +67,13 @@ public interface IProjectChangeService extends BaseService {
boolean removeDraft(String ids);
+ /**
+ * 按原项目回写最近一条待同步变更单的同步状态(审批即时推送/定时任务推送后调用)
+ *
+ * @param projectId 原项目ID
+ * @param syncStatus 同步状态(synced/failed)
+ * @return 是否回写成功(无待同步变更单时返回 false)
+ */
+ boolean writeBackSyncStatus(Long projectId, String syncStatus);
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectChangeServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectChangeServiceImpl.java
index 313573b9..ecb31750 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectChangeServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectChangeServiceImpl.java
@@ -188,7 +188,11 @@ public class ProjectChangeServiceImpl extends BaseServiceImpllambdaUpdate()
+ .eq(ProjectChange::getProjectId, projectId)
+ .eq(ProjectChange::getApprovalStatus, STATUS_APPROVED)
+ .in(ProjectChange::getSyncStatus, List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_FAILED))
+ .set(ProjectChange::getSyncStatus, syncStatus)
+ .last("order by approved_time desc limit 1"));
+ }
+
private LambdaQueryWrapper buildQuery(ProjectChangeVO projectChange) {
TransportBusinessSupport.validateAllDept(projectChange.getAllDept(), "项目变更");
LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery().eq(ProjectChange::getIsDeleted, 0);
From dc75a8eacde2b0025bb966997205ac7c618d8299 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 14:16:33 +0800
Subject: [PATCH 17/63] =?UTF-8?q?:feat:=20HCS=E5=AE=9A=E6=97=B6=E6=8E=A8?=
=?UTF-8?q?=E9=80=81=E5=90=8E=E6=8C=89=E5=8E=9F=E5=8D=95=E5=9B=9E=E5=86=99?=
=?UTF-8?q?=E5=90=88=E5=90=8C/=E9=A1=B9=E7=9B=AE=E5=8F=98=E6=9B=B4?=
=?UTF-8?q?=E5=8D=95=E5=90=8C=E6=AD=A5=E7=8A=B6=E6=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../org/springblade/transport/task/HcsPushTask.java | 10 ++++++++++
1 file changed, 10 insertions(+)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
index 874fc41f..2738bab2 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
@@ -42,6 +42,8 @@ import org.springblade.transport.pojo.entity.InvoiceApplication;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.ProjectApply;
+import org.springblade.transport.service.IContractChangeService;
+import org.springblade.transport.service.IProjectChangeService;
import org.springblade.transport.support.hcs.HcsContractSpecSyncSupport;
import org.springblade.transport.support.hcs.HcsContractSyncSupport;
import org.springblade.transport.support.hcs.HcsInvoiceRegSyncSupport;
@@ -108,6 +110,8 @@ public class HcsPushTask {
private final HcsPaymentSyncSupport hcsPaymentSyncSupport;
private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport;
private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport;
+ private final IContractChangeService contractChangeService;
+ private final IProjectChangeService projectChangeService;
private final BladeRedis bladeRedis;
/**
@@ -211,10 +215,13 @@ public class HcsPushTask {
for (ProjectApply projectApply : projectList) {
try {
hcsProjectSyncSupport.pushProject(projectApply);
+ // 推送成功回写原项目最近一条待同步变更单,闭合变更单同步状态(C102-1 首推或 C102-2 更新均覆盖)
+ projectChangeService.writeBackSyncStatus(projectApply.getId(), HcsSyncConstant.HCS_STATUS_SYNCED);
successCount++;
} catch (Exception exception) {
String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
hcsProjectSyncSupport.markFailed(projectApply, message);
+ projectChangeService.writeBackSyncStatus(projectApply.getId(), HcsSyncConstant.HCS_STATUS_FAILED);
failCount++;
}
}
@@ -242,10 +249,13 @@ public class HcsPushTask {
for (ContractManage contractManage : contractList) {
try {
hcsContractSyncSupport.pushContract(contractManage);
+ // 推送成功回写原合同最近一条待同步变更单,闭合变更单同步状态(C103-1 首推或 C103-11 更新均覆盖)
+ contractChangeService.writeBackSyncStatus(contractManage.getId(), HcsSyncConstant.HCS_STATUS_SYNCED);
successCount++;
} catch (Exception exception) {
String message = StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage();
hcsContractSyncSupport.markFailed(contractManage, message);
+ contractChangeService.writeBackSyncStatus(contractManage.getId(), HcsSyncConstant.HCS_STATUS_FAILED);
failCount++;
}
}
From 5c4f9ba6f470a080a99a99bad8e20324b6670d78 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 14:34:23 +0800
Subject: [PATCH 18/63] =?UTF-8?q?feat:=20=E5=B1=A5=E7=BA=A6=E8=AE=B0?=
=?UTF-8?q?=E5=BD=95C108-1/2=E6=8A=A5=E6=96=87=E6=96=B0=E5=A2=9EcheckAuth?=
=?UTF-8?q?=E5=8F=82=E6=95=B0=E5=9B=BA=E5=AE=9A=E4=BC=A0false?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../java/org/springblade/hcs/dto/HcsPerfRecordSaveDTO.java | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordSaveDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordSaveDTO.java
index 362d8539..98abd32b 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordSaveDTO.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsPerfRecordSaveDTO.java
@@ -54,6 +54,10 @@ public class HcsPerfRecordSaveDTO implements Serializable {
* 履约记录表单数据
*/
private HcsPerfRecordFormDataDTO formData;
+ /**
+ * 是否校验权限(C108-1/2 接口要求固定传 false,跳过合规侧权限校验)
+ */
+ private Boolean checkAuth = Boolean.FALSE;
/**
* 附件挂载
*/
From 857a32f29c71a29ffd565ea746ac83f7aacfde4d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 15:01:49 +0800
Subject: [PATCH 19/63] =?UTF-8?q?feat:=20=E5=90=88=E5=90=8C=E5=8F=98?=
=?UTF-8?q?=E6=9B=B4=E8=AE=B0=E5=BD=95=E5=AF=B9=E9=BD=90=E9=A1=B9=E7=9B=AE?=
=?UTF-8?q?=E5=8F=98=E6=9B=B4=EF=BC=8C=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90?=
=?UTF-8?q?=E5=8F=98=E6=9B=B4=E5=86=85=E5=AE=B9=E5=B9=B6=E8=AE=B0=E5=BD=95?=
=?UTF-8?q?=E5=8F=98=E6=9B=B4=E5=AD=97=E6=AE=B5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../impl/ContractManageServiceImpl.java | 52 ++++++++++++++++++-
1 file changed, 51 insertions(+), 1 deletion(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
index 3d2ac007..4a5719ac 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
@@ -67,6 +67,7 @@ import java.util.Objects;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
/**
* 合同管理 服务实现类
@@ -1279,11 +1280,12 @@ public class ContractManageServiceImpl extends BaseServiceImpl(beforeData.keySet()));
record.put("beforeData", beforeData);
record.put("afterData", afterData);
}
@@ -1291,6 +1293,54 @@ public class ContractManageServiceImpl extends BaseServiceImpl beforeData,
+ Map afterData) {
+ if (beforeData == null || afterData == null || beforeData.isEmpty()) {
+ return TransportBusinessSupport.trimToNull(contractManage.getChangeContent());
+ }
+ return beforeData.entrySet().stream()
+ .map(entry -> "将【" + contractChangeFieldLabel(entry.getKey()) + "】由【" + formatChangeRecordValue(entry.getValue())
+ + "】变更为【" + formatChangeRecordValue(afterData.get(entry.getKey())) + "】")
+ .collect(Collectors.joining("、"));
+ }
+
+ /**
+ * 合同变更快照字段中文名(change_record_json 展示用,与前端变更字段标签一致)
+ */
+ private static final Map CONTRACT_CHANGE_FIELD_LABELS = Map.ofEntries(
+ Map.entry("contractNo", "合同编号"), Map.entry("contractName", "合同名称"),
+ Map.entry("partyB", "乙方"), Map.entry("startDate", "开始日期"),
+ Map.entry("endDate", "结束日期"), Map.entry("contractFormat", "合同格式"),
+ Map.entry("settlementMode", "结算方式"), Map.entry("legalSealFlag", "是否需要加盖法人章"),
+ Map.entry("copyCount", "一式份数"), Map.entry("settlementCurrency", "结算币种"),
+ Map.entry("invoiceCycle", "开票周期(天)"), Map.entry("paymentDays", "回款账期(天)"),
+ Map.entry("contractAmount", "合同金额"), Map.entry("templateFlag", "是否范本"),
+ Map.entry("originalContractNo", "原件合同编号"), Map.entry("electronicSealFlag", "是否电子章"),
+ Map.entry("mainContent", "合同主要内容"), Map.entry("remark", "其他事项说明"),
+ Map.entry("feeGenerationMode", "费用生成模式"), Map.entry("billingPlanJson", "计费方案"),
+ Map.entry("settlementRuleJson", "结算生成规则"), Map.entry("preSettlementConfigJson", "预结算配置"),
+ Map.entry("formalSettlementConfigJson", "正式结算配置"), Map.entry("paymentRatioJson", "付款比例设置"),
+ Map.entry("contractFileJson", "合同主文件"), Map.entry("attachmentsJson", "其它附件"),
+ Map.entry("changeAttachmentsJson", "变更附件")
+ );
+
+ private String contractChangeFieldLabel(String field) {
+ return CONTRACT_CHANGE_FIELD_LABELS.getOrDefault(field, field);
+ }
+
+ private String formatChangeRecordValue(Object value) {
+ if (value == null || (value instanceof String text && text.isBlank())) {
+ return "空";
+ }
+ String text = String.valueOf(value);
+ // 计费方案/附件等长 JSON 文本截断,保证变更内容可读
+ return text.length() > 100 ? text.substring(0, 100) + "…" : text;
+ }
+
private Map buildContractChangeSnapshot(ContractManage contractManage) {
Map data = new LinkedHashMap<>();
data.put("contractNo", contractManage.getContractNo());
From b939b69bb945f58401a74fa544469c73bc38a830 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 15:09:55 +0800
Subject: [PATCH 20/63] =?UTF-8?q?feat:=20=E5=90=88=E5=90=8C=E5=8F=98?=
=?UTF-8?q?=E6=9B=B4=E8=AE=B0=E5=BD=95=E6=9A=82=E4=B8=8D=E8=AE=B0=E5=BD=95?=
=?UTF-8?q?=E9=99=84=E4=BB=B6=E5=AD=97=E6=AE=B5=EF=BC=8C=E6=B3=A8=E9=87=8A?=
=?UTF-8?q?=E4=BF=9D=E7=95=99=E5=BE=85=E5=90=AF=E7=94=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/service/impl/ContractManageServiceImpl.java | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
index 4a5719ac..721fa931 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
@@ -1367,9 +1367,10 @@ public class ContractManageServiceImpl extends BaseServiceImpl
Date: Wed, 28 Oct 2026 15:40:53 +0800
Subject: [PATCH 21/63] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DA901-1=E4=BE=9B?=
=?UTF-8?q?=E5=BA=94=E5=95=86=E6=9F=A5=E8=AF=A2=E6=9C=AA=E8=BF=87=E6=BB=A4?=
=?UTF-8?q?=E5=B7=B2=E5=AE=A1=E6=A0=B8=E5=8F=AF=E7=94=A8=E7=8A=B6=E6=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/support/KingdeeApBillSyncSupport.java | 6 ++++--
.../thirdparty/bfi/constant/BfiConstant.java | 10 ++++++++++
2 files changed, 14 insertions(+), 2 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
index 58789084..5330abc6 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
@@ -36,6 +36,7 @@ import org.springblade.system.cache.SysCache;
import org.springblade.system.feign.ISysClient;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.system.pojo.entity.FeeItem;
+import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveEntryDTO;
@@ -1074,12 +1075,13 @@ public class KingdeeApBillSyncSupport {
return this.queryKingdeeSupplierNumber(customer.getUnifiedCreditCode().trim());
}
- /** A901-1 按统一社会信用代码查询金蝶供应商档案,取返回值 number 即金蝶客商编码,无数据返回 null(开票申请受票方复用);
- * shortcut: 查询未过滤 status=C(已审核)/enable=1,暂存/禁用档案的 number 也会返回,金蝶侧拒收时再补过滤 */
+ /** A901-1 按统一社会信用代码查询金蝶供应商档案,仅取已审核且可用的档案,返回值 number 即金蝶客商编码,无数据返回 null(开票申请受票方复用) */
public String queryKingdeeSupplierNumber(String unifiedCreditCode) {
BfiSupplierQueryDTO param = new BfiSupplierQueryDTO();
param.setPageNo(1);
param.setPageSize(1);
+ param.setStatus(BfiConstant.BILL_STATUS_AUDITED);
+ param.setEnable(BfiConstant.ENABLE_ENABLED);
param.setSocietycreditcode(List.of(unifiedCreditCode));
BfiPageVO page = bfiService.querySupplier(param);
List rows = page == null ? null : page.getRows();
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/constant/BfiConstant.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/constant/BfiConstant.java
index 608c39b2..fdecaa0a 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/constant/BfiConstant.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/constant/BfiConstant.java
@@ -250,6 +250,16 @@ public class BfiConstant {
*/
public static final String BILL_STATUS_RETURNED = "G";
+ /**
+ * 使用状态:可用
+ */
+ public static final String ENABLE_ENABLED = "1";
+
+ /**
+ * 使用状态:禁用
+ */
+ public static final String ENABLE_DISABLED = "0";
+
/**
* 结算状态:未结算
*/
From 4f9ae4953844d73bd324417269511a305e6ef666 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 17:00:25 +0800
Subject: [PATCH 22/63] =?UTF-8?q?feat:=20=E4=BB=98=E6=AC=BE=E7=94=B3?=
=?UTF-8?q?=E8=AF=B7=E9=87=91=E8=9D=B6=E6=8E=A8=E9=80=81=E5=A4=B1=E8=B4=A5?=
=?UTF-8?q?=E5=8E=9F=E5=9B=A0=E8=90=BD=E5=BA=93=E5=B9=B6=E9=9A=8F=E5=88=97?=
=?UTF-8?q?=E8=A1=A8=E8=BF=94=E5=9B=9E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/PaymentApplication.java | 2 ++
.../service/IPaymentApplicationService.java | 7 ++++---
.../impl/PaymentApplicationServiceImpl.java | 15 ++++++++++-----
...application_kingdee_fail_reason_20261028.sql | 17 +++++++++++++++++
4 files changed, 33 insertions(+), 8 deletions(-)
create mode 100644 doc/sql/transport/lqj/20261028/01_blade_payment_application_kingdee_fail_reason_20261028.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
index b5b00417..f8f3bb81 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/PaymentApplication.java
@@ -117,6 +117,8 @@ public class PaymentApplication extends TenantEntity {
private String kingdeeBillId;
/** 金蝶同步状态:unsynced 未生成 / synced 已生成 / paying 付款中 / paid 已付款 / closed 已关闭 / failed 生成失败 */
private String kingdeeStatus;
+ /** 金蝶推送失败原因(仅 failed 态展示;重推成功转 synced 后残留原因不再回显) */
+ private String kingdeeFailReason;
/** 附件信息JSON */
private String attachmentsJson;
/** 备注 */
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java
index e68ec0c0..c55c874f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java
@@ -72,12 +72,13 @@ public interface IPaymentApplicationService extends BaseServicelambdaUpdate()
.eq(PaymentApplication::getId, id)
.eq(PaymentApplication::getApprovalStatus, TransportBusiConstant.APPROVED)
- .set(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
+ .set(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
+ .set(PaymentApplication::getKingdeeFailReason,
+ failReason.length() > 500 ? failReason.substring(0, 500) : failReason));
}
@Override
@@ -793,6 +797,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl
Date: Wed, 28 Oct 2026 17:00:26 +0800
Subject: [PATCH 23/63] =?UTF-8?q?feat:=20=E5=BC=80=E7=A5=A8=E7=94=B3?=
=?UTF-8?q?=E8=AF=B7=E4=B8=8E=E7=BB=93=E7=AE=97=E5=8D=95=E9=87=91=E8=9D=B6?=
=?UTF-8?q?=E6=8E=A8=E9=80=81=E5=A4=B1=E8=B4=A5=E5=8E=9F=E5=9B=A0=E8=90=BD?=
=?UTF-8?q?=E5=BA=93=E5=B9=B6=E9=9A=8F=E5=88=97=E8=A1=A8=E8=BF=94=E5=9B=9E?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/FormalSettlement.java | 4 ++
.../pojo/entity/InvoiceApplication.java | 4 ++
.../pojo/entity/SettlementAdjustment.java | 2 +
.../impl/FormalSettlementServiceImpl.java | 9 ++--
.../impl/InvoiceApplicationServiceImpl.java | 8 ++--
.../02_blade_kingdee_fail_reason_20261028.sql | 43 +++++++++++++++++++
6 files changed, 64 insertions(+), 6 deletions(-)
create mode 100644 doc/sql/transport/lqj/20261028/02_blade_kingdee_fail_reason_20261028.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java
index 20c42775..66fbf13d 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/FormalSettlement.java
@@ -160,6 +160,10 @@ public class FormalSettlement extends TenantEntity {
* 金蝶同步状态:unsynced-未同步 synced-已同步 failed-同步失败
*/
private String kingdeeSyncStatus;
+ /**
+ * 金蝶推送失败原因(仅 failed 态展示;重推成功转 synced 后残留原因不再回显)
+ */
+ private String kingdeeFailReason;
/**
* 附件JSON
*/
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java
index 60ede7e1..6b85f353 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplication.java
@@ -167,6 +167,10 @@ public class InvoiceApplication extends TenantEntity {
* 金蝶同步状态:unsynced-未同步 synced-已同步 failed-同步失败 invoiced-开票完成(终态)
*/
private String kingdeeStatus;
+ /**
+ * 金蝶推送失败原因(仅 failed 态展示;重推成功转 synced 后残留原因不再回显)
+ */
+ private String kingdeeFailReason;
/**
* 同步金蝶时间
*/
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java
index 888fcce8..700c7b88 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/SettlementAdjustment.java
@@ -30,6 +30,8 @@ public class SettlementAdjustment extends TenantEntity {
private String currentNode;
private String currentProcessor;
private String kingdeeSyncStatus;
+ /** 金蝶推送失败原因(随结算单推送失败落库,仅 failed 态展示) */
+ private String kingdeeFailReason;
/**
* 金蝶单据号(未推送场景回填原结算单金蝶单号;重推场景回填新单金蝶单号)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
index 34795cdb..9e9f1b1e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
@@ -872,13 +872,16 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl 0) {
- // 结算调整单回填:未推送场景审批通过后自动推送失败,调整单置 failed 供「推送金蝶」重试
+ // 失败原因截断留档,与列长 varchar(500) 对齐
+ String failReason = Func.toStrWithEmpty(reason, "未知异常");
+ String columnReason = failReason.length() > 500 ? failReason.substring(0, 500) : failReason;
+ // 结算调整单回填:未推送场景审批通过后自动推送失败,调整单置 failed + 失败原因供「推送金蝶」重试
settlementAdjustmentMapper.update(null, Wrappers.lambdaUpdate()
.eq(SettlementAdjustment::getFormalSettlementId, id)
.eq(SettlementAdjustment::getApprovalStatus, TransportBusiConstant.APPROVED)
- .set(SettlementAdjustment::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
+ .set(SettlementAdjustment::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
+ .set(SettlementAdjustment::getKingdeeFailReason, columnReason));
// 失败原因写变更记录留档(推送事务已回滚,此处独立事务);原因过长截断留档,不抛错
- String failReason = Func.toStrWithEmpty(reason, "未知异常");
saveChange(id, "结算单基本信息", null, "调整",
"同步金蝶失败:" + (failReason.length() > 200 ? failReason.substring(0, 200) : failReason), null);
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
index 5de8ea1e..46af949b 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
@@ -639,17 +639,19 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpllambdaUpdate()
.eq(InvoiceApplication::getId, id)
.eq(InvoiceApplication::getApprovalStatus, TransportBusiConstant.APPROVED)
.notIn(InvoiceApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED,
KingdeeBfiConstant.KINGDEE_STATUS_INVOICED)
- .set(InvoiceApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
+ .set(InvoiceApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
+ .set(InvoiceApplication::getKingdeeFailReason, failReason));
if (updated > 0) {
// 失败原因写操作流水留档(推送事务已回滚,此处独立事务)
record(id, "sync", "同步金蝶失败", TransportBusiConstant.APPROVED, TransportBusiConstant.APPROVED,
- truncateReason(reason), null);
+ failReason, null);
}
}
diff --git a/doc/sql/transport/lqj/20261028/02_blade_kingdee_fail_reason_20261028.sql b/doc/sql/transport/lqj/20261028/02_blade_kingdee_fail_reason_20261028.sql
new file mode 100644
index 00000000..ccf684c3
--- /dev/null
+++ b/doc/sql/transport/lqj/20261028/02_blade_kingdee_fail_reason_20261028.sql
@@ -0,0 +1,43 @@
+-- 开票申请 / 正式结算单 / 结算调整单:金蝶推送失败原因留档展示
+-- markKingdeeSyncFailed 落"同步失败"时同步落失败原因,列表"金蝶单据状态"列悬浮提示展示;
+-- 重推成功转 synced 后残留原因不再回显。付款管理 blade_payment_application 已单独处理(20261028)
+
+-- 以下结构升级支持重复执行(三张表逐一判断)
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_invoice_application` ADD COLUMN `kingdee_fail_reason` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶推送失败原因'' AFTER `kingdee_status`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_invoice_application'
+ AND COLUMN_NAME = 'kingdee_fail_reason'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_formal_settlement` ADD COLUMN `kingdee_fail_reason` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶推送失败原因'' AFTER `kingdee_sync_status`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_formal_settlement'
+ AND COLUMN_NAME = 'kingdee_fail_reason'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_settlement_adjustment` ADD COLUMN `kingdee_fail_reason` varchar(500) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶推送失败原因'' AFTER `kingdee_sync_status`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_settlement_adjustment'
+ AND COLUMN_NAME = 'kingdee_fail_reason'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
From 47fd0298dba86c451a4449b2bbd3b4c80c2a424d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 19:30:38 +0800
Subject: [PATCH 24/63] =?UTF-8?q?feat:=20=E5=B1=A5=E7=BA=A6=E8=AE=B0?=
=?UTF-8?q?=E5=BD=95=E6=8E=A8=E9=80=81=E6=96=B0=E5=A2=9E=E5=8F=91=E7=A5=A8?=
=?UTF-8?q?=E4=B8=8E=E6=94=B6=E7=A5=A8=E5=88=86=E6=91=8A=E5=AE=9E=E4=BD=93?=
=?UTF-8?q?=E7=8A=B6=E6=80=81=E5=AD=97=E6=AE=B5=E5=8F=8A=E8=A1=A8=E7=BB=93?=
=?UTF-8?q?=E6=9E=84=E8=84=9A=E6=9C=AC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/pojo/entity/InvoiceApplicationInvoice.java | 7 +++++++
.../transport/pojo/entity/InvoiceReceiptSettlement.java | 6 ++++++
.../blade_invoice_application_invoice_perf_20261028.sql | 5 +++++
.../blade_invoice_receipt_settlement_perf_20261028.sql | 5 +++++
4 files changed, 23 insertions(+)
create mode 100644 doc/sql/transport/blade_invoice_application_invoice_perf_20261028.sql
create mode 100644 doc/sql/transport/blade_invoice_receipt_settlement_perf_20261028.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationInvoice.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationInvoice.java
index 8e145070..be6896cc 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationInvoice.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceApplicationInvoice.java
@@ -77,4 +77,11 @@ public class InvoiceApplicationInvoice extends TenantEntity {
@Schema(description = "A303-5 返回原文JSON留档")
private String rawJson;
+ /** 履约开票记录ID(C108-1 返回;非空即已推送,扫描据此幂等跳过) */
+ private String hcsPerfFdId;
+ /** 履约开票记录推送失败重试次数 */
+ private Integer hcsPerfRetryCount;
+ /** 履约开票记录最近推送失败原因(成功后清空) */
+ private String hcsPerfSyncMsg;
+
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptSettlement.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptSettlement.java
index 4f19d2d5..dc4be0bb 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptSettlement.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/InvoiceReceiptSettlement.java
@@ -55,4 +55,10 @@ public class InvoiceReceiptSettlement extends TenantEntity {
private BigDecimal settlementAmount;
private BigDecimal receivedInvoiceAmount;
private BigDecimal allocatedInvoiceAmount;
+ /** 履约收票记录ID(C108-1 返回;非空即已推送,扫描据此幂等跳过) */
+ private String hcsPerfFdId;
+ /** 履约收票记录推送失败重试次数 */
+ private Integer hcsPerfRetryCount;
+ /** 履约收票记录最近推送失败原因(成功后清空) */
+ private String hcsPerfSyncMsg;
}
diff --git a/doc/sql/transport/blade_invoice_application_invoice_perf_20261028.sql b/doc/sql/transport/blade_invoice_application_invoice_perf_20261028.sql
new file mode 100644
index 00000000..153b2b77
--- /dev/null
+++ b/doc/sql/transport/blade_invoice_application_invoice_perf_20261028.sql
@@ -0,0 +1,5 @@
+-- 开票申请金蝶发票结果表新增履约开票记录推送字段(C108-1:金蝶开票完成后每张发票推送一条履约开票记录)
+ALTER TABLE `blade_invoice_application_invoice`
+ ADD COLUMN `hcs_perf_fd_id` varchar(64) DEFAULT NULL COMMENT '履约开票记录ID(C108-1返回,非空即已推送)' AFTER `raw_json`,
+ ADD COLUMN `hcs_perf_retry_count` int DEFAULT NULL COMMENT '履约开票记录推送失败重试次数' AFTER `hcs_perf_fd_id`,
+ ADD COLUMN `hcs_perf_sync_msg` varchar(512) DEFAULT NULL COMMENT '履约开票记录最近推送失败原因' AFTER `hcs_perf_retry_count`;
diff --git a/doc/sql/transport/blade_invoice_receipt_settlement_perf_20261028.sql b/doc/sql/transport/blade_invoice_receipt_settlement_perf_20261028.sql
new file mode 100644
index 00000000..6684c793
--- /dev/null
+++ b/doc/sql/transport/blade_invoice_receipt_settlement_perf_20261028.sql
@@ -0,0 +1,5 @@
+-- 收票登记结算单分摊表新增履约收票记录推送字段(C108-1:收票单每条结算信息推送一条履约收票记录)
+ALTER TABLE `blade_invoice_receipt_settlement`
+ ADD COLUMN `hcs_perf_fd_id` varchar(64) DEFAULT NULL COMMENT '履约收票记录ID(C108-1返回,非空即已推送)' AFTER `allocated_invoice_amount`,
+ ADD COLUMN `hcs_perf_retry_count` int DEFAULT NULL COMMENT '履约收票记录推送失败重试次数' AFTER `hcs_perf_fd_id`,
+ ADD COLUMN `hcs_perf_sync_msg` varchar(512) DEFAULT NULL COMMENT '履约收票记录最近推送失败原因' AFTER `hcs_perf_retry_count`;
From 73be998f66cec2658349f169e3b380ba4c7a1888 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 19:30:43 +0800
Subject: [PATCH 25/63] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=B1=A5?=
=?UTF-8?q?=E7=BA=A6=E8=AE=B0=E5=BD=95=E9=80=9A=E7=94=A8C108-1=E6=8E=A8?=
=?UTF-8?q?=E9=80=81=E6=94=AF=E6=92=91=E7=BB=84=E4=BB=B6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsPerfRecordAddSupport.java | 134 ++++++++++++++++++
1 file changed, 134 insertions(+)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordAddSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordAddSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordAddSupport.java
new file mode 100644
index 00000000..ebd1101b
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordAddSupport.java
@@ -0,0 +1,134 @@
+/**
+ * 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.transport.support.hcs;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.hcs.config.HcsProperties;
+import org.springblade.hcs.dto.HcsFormSaveVO;
+import org.springblade.hcs.dto.HcsMechanismsDTO;
+import org.springblade.hcs.dto.HcsPerfRecordFormDataDTO;
+import org.springblade.hcs.dto.HcsPerfRecordPropsDTO;
+import org.springblade.hcs.dto.HcsPerfRecordSaveDTO;
+import org.springblade.hcs.dto.HcsResult;
+import org.springblade.hcs.dto.HcsUserRefVO;
+import org.springblade.hcs.exception.HcsException;
+import org.springblade.hcs.feign.IHcsClient;
+import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.HcsContractExeStage;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * HCS 履约记录通用新建支撑组件(C108-1,收款/开票/收票场景共用)
+ *
+ * 付款申请场景因含 C108-2 实际金额推进与附件转传,仍走 {@link HcsPerfRecordSyncSupport};
+ * 本组件收敛其余场景的 C108-1 组装与推送:申请金额=实际金额、履约计划id 取已同步履约计划行合规阶段ID(必填)、
+ * 机制数据传空列表(无附件)、履约记录ID 由调用方按各自粒度持久化。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsPerfRecordAddSupport {
+
+ private final IHcsClient hcsClient;
+ private final HcsProperties hcsProperties;
+
+ /**
+ * C108-1 新建履约记录(通用)
+ *
+ * @param contractManage 合同(须已推送合规,hcsFdId 必有)
+ * @param stageRow 已同步履约计划行(fd_plan_id 必填,无已同步计划行时抛出由调用方重试)
+ * @param applyNo 申请编号(收款=通知单号 / 开票=发票号 / 收票=发票号)
+ * @param amount 申请金额(=实际金额)
+ * @param perfDateMillis 履约日期(毫秒时间戳:收款=交易日期 / 开票=发票完成开票日期 / 收票=发票日期)
+ * @param planType 履约类型:payment 付款 / collection 收款
+ * @param jdDocNo 金蝶单据号(溯源)
+ * @param businessId TMS 业务ID(溯源)
+ * @param title 履约记录标题
+ * @return 推送结果描述
+ */
+ public String pushPerfRecordAdd(ContractManage contractManage, HcsContractExeStage stageRow, String applyNo,
+ BigDecimal amount, Long perfDateMillis, String planType,
+ String jdDocNo, Long businessId, String title) {
+ // 【必填】fd_plan_id 履约计划id:无已同步计划行时无值可传,抛出由调用方置重试(合同链路首推后自愈)
+ if (stageRow == null || StringUtil.isBlank(stageRow.getHcsStageFdId())) {
+ throw new HcsException("C108 履约记录未匹配到已同步履约计划行,fd_plan_id 必传:applyNo=" + applyNo);
+ }
+ if (StringUtil.isBlank(contractManage.getHcsFdId())) {
+ throw new HcsException("C108 履约记录推送失败,合同尚未推送合规:contractNo=" + contractManage.getContractNo());
+ }
+ // 表单模板ID(配置 perf-record.xform-id,环境值 §9-U9,未配置不传)
+ String xformId = StringUtil.isBlank(hcsProperties.getPerfRecord().getXformId()) ? null : hcsProperties.getPerfRecord().getXformId();
+ HcsPerfRecordPropsDTO props = new HcsPerfRecordPropsDTO();
+ // 申请金额 / 实际金额:接口按字符串传值
+ props.setFdApplyAmount(amount == null ? null : amount.toPlainString());
+ props.setFdFactAmount(amount == null ? null : amount.toPlainString());
+ // 【必填】申请编号
+ props.setFdApplyNo(applyNo);
+ // 【必填】履约日期(毫秒时间戳)
+ props.setFdPlanDate(perfDateMillis);
+ // 履约类型(原文拼写 fd_paln_type 必须沿用)
+ props.setFdPalnType(planType);
+ // 性质:取关联履约计划行的款项性质
+ props.setFdProperty(stageRow.getFundNature());
+ // 履约计划id / 来源履约计划项ID:履约计划行合规阶段ID(HCS 只认自己的阶段ID)
+ props.setFdId(stageRow.getHcsStageFdId());
+ props.setFdPlanId(stageRow.getHcsStageFdId());
+ // 金蝶单据号(溯源用)
+ props.setFdJdDocNo(jdDocNo);
+ // 【必填】合同ID:HCS 合同ID
+ props.setFdContractId(contractManage.getHcsFdId());
+ // 业务ID / 创建时间(溯源)
+ props.setFdBusinessId(businessId == null ? null : String.valueOf(businessId));
+ props.setFdCreateTime(System.currentTimeMillis());
+ props.setFdXformId(xformId);
+ HcsPerfRecordFormDataDTO formData = new HcsPerfRecordFormDataDTO();
+ formData.setFdXformId(xformId);
+ formData.setFdDocSubject(title);
+ formData.setDynamicProps(props);
+ // 机制数据(C108-1 必填):无附件传空列表
+ HcsMechanismsDTO mechanisms = new HcsMechanismsDTO();
+ mechanisms.setAttachment(List.of());
+ HcsPerfRecordSaveDTO param = new HcsPerfRecordSaveDTO();
+ param.setCurUser(HcsUserRefVO.ofLoginName(hcsProperties.getOperatorLoginName()));
+ param.setFormData(formData);
+ param.setMechanisms(mechanisms);
+ log.info("HCS履约记录新建开始:title={},applyNo={},金额={}", title, applyNo, amount);
+ HcsResult result = hcsClient.perfRecordAdd(param);
+ if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
+ log.error("HCS履约记录新建失败响应:{},title={},applyNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), title, applyNo);
+ throw new HcsException("C108-1 履约记录新建失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ log.info("HCS履约记录新建成功:title={},applyNo={},perfFdId={}", title, applyNo, result.getData().getFdId());
+ return result.getData().getFdId();
+ }
+}
From d928ba6ddfcf9c6d76521a17a1094b48b908542b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 19:30:48 +0800
Subject: [PATCH 26/63] =?UTF-8?q?feat:=20=E6=94=B6=E6=AC=BE=E5=B1=A5?=
=?UTF-8?q?=E7=BA=A6=E6=B5=81=E6=B0=B4=E5=B1=A5=E7=BA=A6=E6=97=A5=E6=9C=9F?=
=?UTF-8?q?=E8=B0=83=E6=95=B4=E4=B8=BA=E6=94=B6=E6=AC=BE=E4=BA=A4=E6=98=93?=
=?UTF-8?q?=E6=97=A5=E6=9C=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsClaimSyncSupport.java | 78 ++++---------------
1 file changed, 13 insertions(+), 65 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java
index a74391fd..ea242d5c 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java
@@ -33,17 +33,11 @@ import org.springblade.core.tool.utils.StringUtil;
import org.springblade.hcs.config.HcsProperties;
import org.springblade.hcs.dto.HcsClaimRecordAddDTO;
import org.springblade.hcs.dto.HcsCollectionFlowVO;
-import org.springblade.hcs.dto.HcsFormSaveVO;
import org.springblade.hcs.dto.HcsIdVO;
-import org.springblade.hcs.dto.HcsMechanismsDTO;
import org.springblade.hcs.dto.HcsPageQueryDTO;
import org.springblade.hcs.dto.HcsPageVO;
-import org.springblade.hcs.dto.HcsPerfRecordFormDataDTO;
-import org.springblade.hcs.dto.HcsPerfRecordPropsDTO;
-import org.springblade.hcs.dto.HcsPerfRecordSaveDTO;
import org.springblade.hcs.dto.HcsRefVO;
import org.springblade.hcs.dto.HcsResult;
-import org.springblade.hcs.dto.HcsUserRefVO;
import org.springblade.hcs.exception.HcsException;
import org.springblade.hcs.feign.IHcsClient;
import org.springblade.transport.constant.HcsSyncConstant;
@@ -102,6 +96,7 @@ public class HcsClaimSyncSupport {
private final ContractManageMapper contractManageMapper;
private final IContractExeStageService contractExeStageService;
private final HcsExeStageSyncSupport hcsExeStageSyncSupport;
+ private final HcsPerfRecordAddSupport hcsPerfRecordAddSupport;
/**
* 金蝶认领推送成功后推送合同合规(C103-9 → C107-1 → C108-1)
@@ -213,7 +208,7 @@ public class HcsClaimSyncSupport {
}
}
// ③ C108-1 履约流水:按合同一条(开关 perf-record.sync-enabled,与付款链履约记录同步同开关),
- // 须已推送合规且存在已同步收款计划行(fd_plan_id 必填)
+ // 须已推送合规且存在已同步收款计划行(fd_plan_id 必填);履约日期为收款流水交易日期
if (!hcsProperties.getPerfRecord().isSyncEnabled()) {
log.info("HCS履约流水推送跳过:履约记录同步开关未开启 contractNo={}", contractManage.getContractNo());
return;
@@ -226,7 +221,7 @@ public class HcsClaimSyncSupport {
log.info("HCS履约流水推送跳过:无已同步收款履约计划行 contractNo={}", contractManage.getContractNo());
return;
}
- pushPerfRecord(flow, contractManage, stageRow, claimedTotal, latestClaimDate(claims));
+ pushPerfRecord(flow, contractManage, stageRow, claimedTotal);
}
/**
@@ -279,54 +274,18 @@ public class HcsClaimSyncSupport {
}
/**
- * C108-1 履约流水推送(单合同一条:申请金额=实际金额=该合同累计认领金额)
+ * C108-1 履约流水推送(单合同一条:申请金额=实际金额=该合同累计认领金额,履约日期=收款流水交易日期)
*/
private void pushPerfRecord(KingdeeReceiptFlow flow, ContractManage contractManage,
- HcsContractExeStage stageRow, BigDecimal claimedTotal, LocalDate planDate) {
- // 表单模板ID(配置 perf-record.xform-id,环境值 §9-U9,未配置不传)
- String xformId = StringUtil.isBlank(hcsProperties.getPerfRecord().getXformId()) ? null : hcsProperties.getPerfRecord().getXformId();
- HcsPerfRecordPropsDTO props = new HcsPerfRecordPropsDTO();
- // 【必填】申请编号:收款通知单号,兜底明细流水号
- props.setFdApplyNo(firstNonBlank(flow.getReceiptNoticeNo(), flow.getDetailSerialNo()));
- // 申请金额 / 实际金额:该合同累计认领金额
- props.setFdApplyAmount(claimedTotal.toPlainString());
- props.setFdFactAmount(claimedTotal.toPlainString());
- // 【必填】履约日期:最近认领日期(毫秒时间戳)
- props.setFdPlanDate(toMillis(planDate));
- // 履约类型:收款
- props.setFdPalnType(HcsSyncConstant.PERFORM_TYPE_COLLECTION);
- // 履约性质:收款计划行款项性质
- props.setFdProperty(stageRow.getFundNature());
- // 履约计划id / 来源履约计划项ID:收款计划行合规阶段ID(HCS 只认自己的阶段ID)
- props.setFdId(stageRow.getHcsStageFdId());
- props.setFdPlanId(stageRow.getHcsStageFdId());
- // 金蝶单据号:A301-3 推送返回的金蝶收款单号
- props.setFdJdDocNo(flow.getKingdeeBillNo());
- // 【必填】合同ID:HCS 合同ID
- props.setFdContractId(contractManage.getHcsFdId());
- // 业务ID:TMS 收款流水ID(溯源)
- props.setFdBusinessId(String.valueOf(flow.getId()));
- props.setFdCreateTime(System.currentTimeMillis());
- props.setFdXformId(xformId);
- HcsPerfRecordFormDataDTO formData = new HcsPerfRecordFormDataDTO();
- formData.setFdXformId(xformId);
- formData.setDynamicProps(props);
- // 机制数据(C108-1 必填):认领附件暂不转传,传空列表
- HcsMechanismsDTO mechanisms = new HcsMechanismsDTO();
- mechanisms.setAttachment(List.of());
- HcsPerfRecordSaveDTO param = new HcsPerfRecordSaveDTO();
- param.setCurUser(HcsUserRefVO.ofLoginName(hcsProperties.getOperatorLoginName()));
- param.setFormData(formData);
- param.setMechanisms(mechanisms);
- log.info("HCS履约流水新建开始 flowId:{},contractNo:{},认领金额 {}", flow.getId(), contractManage.getContractNo(), claimedTotal);
- HcsResult result = hcsClient.perfRecordAdd(param);
- if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) {
- log.error("HCS履约流水新建失败响应:{},contractNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo());
- throw new HcsException("C108-1 履约流水新建失败:" + (result == null ? "响应为空" : result.getMsg()));
- }
- // shortcut: 履约流水 fdId 未持久化(认领链无 C108-2 更新场景);需推进实际金额更新时先落库
- log.info("HCS履约流水新建成功 flowId:{},contractNo:{},perfFdId:{}",
- flow.getId(), contractManage.getContractNo(), result.getData().getFdId());
+ HcsContractExeStage stageRow, BigDecimal claimedTotal) {
+ // 履约日期:交易日期(需求口径),交易时间为空兜底当天
+ LocalDate perfDate = flow.getTransactionTime() != null ? flow.getTransactionTime().toLocalDate() : LocalDate.now();
+ String perfFdId = hcsPerfRecordAddSupport.pushPerfRecordAdd(contractManage, stageRow,
+ firstNonBlank(flow.getReceiptNoticeNo(), flow.getDetailSerialNo()), claimedTotal, toMillis(perfDate),
+ HcsSyncConstant.PERFORM_TYPE_COLLECTION, flow.getKingdeeBillNo(), flow.getId(),
+ "收款履约-" + firstNonBlank(flow.getReceiptNoticeNo(), flow.getDetailSerialNo()));
+ // shortcut: 履约流水 fdId 未持久化(认领链无 C108-2 更新场景,重推会重复);需推进实际金额更新时先落库
+ log.info("HCS履约流水新建完成 flowId:{},contractNo:{},perfFdId:{}", flow.getId(), contractManage.getContractNo(), perfFdId);
}
/**
@@ -353,17 +312,6 @@ public class HcsClaimSyncSupport {
.findFirst().orElse(null);
}
- /**
- * 最近认领日期(C108-1 履约日期)
- */
- private LocalDate latestClaimDate(List claims) {
- return claims.stream()
- .map(ReceiptClaim::getClaimDate)
- .filter(Objects::nonNull)
- .max(LocalDate::compareTo)
- .orElse(LocalDate.now());
- }
-
private String firstNonBlank(String... values) {
for (String value : values) {
if (Func.isNotEmpty(value)) {
From 7c0509392ad0b640d1b51e4f6060337eb5f6c2f8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 19:30:53 +0800
Subject: [PATCH 27/63] =?UTF-8?q?feat:=20=E4=BB=98=E6=AC=BE=E5=B1=A5?=
=?UTF-8?q?=E7=BA=A6=E8=AE=B0=E5=BD=95=E8=B0=83=E6=95=B4=E4=B8=BA=E9=87=91?=
=?UTF-8?q?=E8=9D=B6=E5=AE=9E=E9=99=85=E4=BB=98=E6=AC=BE=E5=90=8E=E6=8E=A8?=
=?UTF-8?q?=E9=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../service/impl/PaymentApplicationServiceImpl.java | 9 +++++++--
.../transport/support/hcs/HcsPaymentSyncSupport.java | 12 +++++++++++-
2 files changed, 18 insertions(+), 3 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index f6ce65c2..def5f9a8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -1012,6 +1012,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0
+ && (previousPaid == null || previousPaid.signum() <= 0 || previousPaid.compareTo(currentPaid) != 0);
+ if (paidAdvanced) {
entity.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
}
updateById(entity);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
index 1783fe6f..2cfb4a75 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
@@ -112,11 +112,21 @@ public class HcsPaymentSyncSupport {
}
result.append(stageResult);
}
- // ③ 履约记录:审批通过新建 C108-1 / 实际付款推进更新 C108-2(一期暂不对接默认关)
+ // ③ 履约记录:金蝶实际付款后推送(需求口径:金蝶实际付款了再推给合同系统)——
+ // 首次回写实际付款走 C108-1 新建,分期付款推进(paidAmount 变化置待同步)走 C108-2 更新;
+ // 未实际付款时跳过(整体状态照常回写,付款回写时重新置待同步触发推送)
if (!hcsProperties.getPerfRecord().isSyncEnabled()) {
markSynced(payment, result.toString());
return result.toString();
}
+ if (payment.getPaidAmount() == null || payment.getPaidAmount().signum() <= 0) {
+ if (result.length() > 0) {
+ result.append(";");
+ }
+ result.append("金蝶未实际付款,待付款后推送履约记录");
+ markSynced(payment, result.toString());
+ return result.toString();
+ }
if (result.length() > 0) {
result.append(";");
}
From bd0c4d098156a99a347111d4cdc5df73edf16538 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 19:30:59 +0800
Subject: [PATCH 28/63] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=B1=A5?=
=?UTF-8?q?=E7=BA=A6=E5=BC=80=E7=A5=A8=E8=AE=B0=E5=BD=95=E4=B8=8E=E5=B1=A5?=
=?UTF-8?q?=E7=BA=A6=E6=94=B6=E7=A5=A8=E8=AE=B0=E5=BD=95=E6=8E=A8=E9=80=81?=
=?UTF-8?q?=E9=93=BE=E8=B7=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/HcsSyncController.java | 5 +-
.../hcs/HcsInvoicePerfSyncSupport.java | 222 ++++++++++++++++++
.../hcs/HcsReceiptPerfSyncSupport.java | 211 +++++++++++++++++
.../transport/task/HcsPushTask.java | 30 ++-
4 files changed, 462 insertions(+), 6 deletions(-)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index 220ce98b..ea8c0527 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -130,8 +130,9 @@ public class HcsSyncController {
case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsPushTask.pushUnsyncedProjects();
case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsPushTask.pushUnsyncedContracts();
case HcsSyncConstant.BIZ_TYPE_PAYMENT -> hcsPushTask.pushUnsyncedPayments();
- case HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION -> hcsPushTask.pushUnsyncedInvoiceApplications();
- case HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT -> hcsPushTask.pushUnsyncedInvoiceReceipts();
+ // 开票/收票走组合动作(C111/C109 + 履约记录 C108-1),与定时任务行为一致
+ case HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION -> hcsPushTask.syncHcsInvoiceApplications();
+ case HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT -> hcsPushTask.syncHcsInvoiceReceipts();
default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType);
};
return R.data(result);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java
new file mode 100644
index 00000000..a831adcc
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java
@@ -0,0 +1,222 @@
+/**
+ * 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.transport.support.hcs;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.hcs.config.HcsProperties;
+import org.springblade.hcs.exception.HcsException;
+import org.springblade.transport.constant.HcsSyncConstant;
+import org.springblade.transport.mapper.ContractManageMapper;
+import org.springblade.transport.mapper.InvoiceApplicationInvoiceMapper;
+import org.springblade.transport.mapper.InvoiceApplicationMapper;
+import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper;
+import org.springblade.transport.mapper.FormalSettlementMapper;
+import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.FormalSettlement;
+import org.springblade.transport.pojo.entity.HcsContractExeStage;
+import org.springblade.transport.pojo.entity.InvoiceApplication;
+import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice;
+import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
+import org.springblade.transport.service.IContractExeStageService;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.time.ZoneId;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * HCS 履约开票记录同步支撑组件(C108-1,需求:金蝶开票完成后推送合同系统)
+ *
+ * 粒度:金蝶回写的每张发票(blade_invoice_application_invoice)对应一条履约开票记录;
+ * 触发:A303-5 开票结果回写落库发票后,随开票登记扫描任务(HcsPushTask invoice-application)推送;
+ * 履约日期为发票完成开票日期(invoice_date),履约类型 collection(收款方向,关联合同收款履约计划行);
+ * 合同经应收结算链路回溯(开票申请 → 应收结算关联 → 正式结算单合同,同 C111 §9-U10 口径);
+ * 幂等凭 hcs_perf_fd_id(非空跳过),失败置重试计数与原因由下轮扫描自动补偿。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsInvoicePerfSyncSupport {
+
+ private final HcsProperties hcsProperties;
+ private final InvoiceApplicationInvoiceMapper invoiceApplicationInvoiceMapper;
+ private final InvoiceApplicationMapper invoiceApplicationMapper;
+ private final InvoiceApplicationSettlementMapper invoiceApplicationSettlementMapper;
+ private final FormalSettlementMapper formalSettlementMapper;
+ private final ContractManageMapper contractManageMapper;
+ private final IContractExeStageService contractExeStageService;
+ private final HcsPerfRecordAddSupport hcsPerfRecordAddSupport;
+
+ /**
+ * 扫描并推送待同步的履约开票记录(每张发票一条 C108-1),返回结果汇总
+ *
+ * @return 汇总描述
+ */
+ public String pushUnsyncedInvoicePerfRecords() {
+ if (!hcsProperties.getPerfRecord().isSyncEnabled()) {
+ return "履约记录同步开关未开启";
+ }
+ int maxRetry = hcsProperties.getPush().getMaxRetry();
+ // 待推送发票:未建过履约记录且重试未超限
+ List invoiceList = invoiceApplicationInvoiceMapper.selectList(
+ Wrappers.lambdaQuery()
+ .isNull(InvoiceApplicationInvoice::getHcsPerfFdId)
+ .and(wrapper -> wrapper.isNull(InvoiceApplicationInvoice::getHcsPerfRetryCount)
+ .or().lt(InvoiceApplicationInvoice::getHcsPerfRetryCount, maxRetry))
+ .last("limit " + hcsProperties.getPush().getBatchSize()));
+ if (invoiceList.isEmpty()) {
+ return "履约开票记录:无待推送发票";
+ }
+ // 批量装配上下文(申请单/合同/计划行,一次 IN 查询避免逐行 N+1)
+ Map applicationById = loadApplications(invoiceList);
+ Map contractByApplicationId = loadContracts(List.copyOf(applicationById.values()));
+ int successCount = 0;
+ int failCount = 0;
+ for (InvoiceApplicationInvoice invoice : invoiceList) {
+ try {
+ pushOneInvoice(invoice, applicationById.get(invoice.getInvoiceApplicationId()),
+ contractByApplicationId.get(invoice.getInvoiceApplicationId()));
+ successCount++;
+ } catch (Exception exception) {
+ markFailed(invoice, StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage());
+ failCount++;
+ }
+ }
+ String summary = String.format("履约开票记录:扫描 %d 张,成功 %d 张,失败 %d 张(重试上限 %d)",
+ invoiceList.size(), successCount, failCount, maxRetry);
+ if (failCount > 0) {
+ log.warn("HCS履约开票记录推送存在失败:{}", summary);
+ }
+ return summary;
+ }
+
+ /**
+ * 推送单张发票的履约开票记录
+ */
+ private void pushOneInvoice(InvoiceApplicationInvoice invoice, InvoiceApplication application, ContractManage contractManage) {
+ if (application == null || !"approved".equals(application.getApprovalStatus())) {
+ throw new HcsException("开票申请不存在或未审批通过:invoiceApplicationId=" + invoice.getInvoiceApplicationId());
+ }
+ if (contractManage == null) {
+ throw new HcsException("应收结算链路回溯不到合同(applicationNo=" + application.getApplicationNo() + ",同 C111 §9-U10 口径)");
+ }
+ // 关联合同的收款履约计划行(fd_plan_id 必填,无已同步计划行时通用组件抛出重试,合同链路首推后自愈)
+ HcsContractExeStage stageRow = contractExeStageService.findCollectionStageRow(
+ contractManage.getId(), HcsSyncConstant.FUND_NATURE_PROGRESS);
+ BigDecimal amount = invoice.getInvoiceAmount() == null ? BigDecimal.ZERO : invoice.getInvoiceAmount();
+ String perfFdId = hcsPerfRecordAddSupport.pushPerfRecordAdd(contractManage, stageRow,
+ invoice.getInvoiceNo(), amount, toMillis(invoice.getInvoiceDate()),
+ HcsSyncConstant.PERFORM_TYPE_COLLECTION, invoice.getSerialNo(), invoice.getId(),
+ "开票履约-" + invoice.getInvoiceNo());
+ markSynced(invoice, perfFdId);
+ }
+
+ /**
+ * 批量装配发票所属开票申请(过滤已作废)
+ */
+ private Map loadApplications(List invoiceList) {
+ List applicationIds = invoiceList.stream()
+ .map(InvoiceApplicationInvoice::getInvoiceApplicationId).filter(Objects::nonNull).distinct().toList();
+ if (applicationIds.isEmpty()) {
+ return Map.of();
+ }
+ return invoiceApplicationMapper.selectBatchIds(applicationIds).stream()
+ .collect(Collectors.toMap(InvoiceApplication::getId, item -> item, (first, second) -> first));
+ }
+
+ /**
+ * 批量回溯合同:申请单 → 应收结算关联 → 正式结算单合同(一次 IN 查询,同 C111 §9-U10 口径)
+ */
+ private Map loadContracts(List applicationList) { List applicationIds = applicationList.stream().map(InvoiceApplication::getId).toList();
+ if (applicationIds.isEmpty()) {
+ return Map.of();
+ }
+ Map relationByApplicationId = invoiceApplicationSettlementMapper.selectList(
+ Wrappers.lambdaQuery()
+ .in(InvoiceApplicationSettlement::getInvoiceApplicationId, applicationIds)).stream()
+ .filter(relation -> relation.getFormalSettlementId() != null)
+ .collect(Collectors.toMap(InvoiceApplicationSettlement::getInvoiceApplicationId, relation -> relation,
+ (first, second) -> first, LinkedHashMap::new));
+ if (relationByApplicationId.isEmpty()) {
+ return Map.of();
+ }
+ Map settlementById = formalSettlementMapper.selectBatchIds(
+ relationByApplicationId.values().stream()
+ .map(InvoiceApplicationSettlement::getFormalSettlementId).distinct().toList()).stream()
+ .filter(settlement -> settlement.getContractId() != null)
+ .collect(Collectors.toMap(FormalSettlement::getId, item -> item, (first, second) -> first));
+ if (settlementById.isEmpty()) {
+ return Map.of();
+ }
+ Map contractById = contractManageMapper.selectBatchIds(
+ settlementById.values().stream().map(FormalSettlement::getContractId).distinct().toList()).stream()
+ .collect(Collectors.toMap(ContractManage::getId, item -> item, (first, second) -> first));
+ Map contractByApplicationId = new LinkedHashMap<>();
+ relationByApplicationId.forEach((applicationId, relation) -> {
+ FormalSettlement settlement = settlementById.get(relation.getFormalSettlementId());
+ if (settlement != null) {
+ contractByApplicationId.put(applicationId, contractById.get(settlement.getContractId()));
+ }
+ });
+ return contractByApplicationId;
+ }
+
+ /**
+ * 推送成功回填:履约记录ID 立即持久化(幂等键),清失败原因
+ */
+ private void markSynced(InvoiceApplicationInvoice invoice, String perfFdId) {
+ invoice.setHcsPerfFdId(perfFdId);
+ invoice.setHcsPerfRetryCount(0);
+ invoice.setHcsPerfSyncMsg(null);
+ invoiceApplicationInvoiceMapper.updateById(invoice);
+ log.info("HCS履约开票记录推送成功:invoiceNo={},perfFdId={}", invoice.getInvoiceNo(), perfFdId);
+ }
+
+ /**
+ * 推送失败回写:重试次数+1 + 失败原因(下轮扫描自动补偿)
+ */
+ private void markFailed(InvoiceApplicationInvoice invoice, String message) {
+ invoice.setHcsPerfRetryCount(invoice.getHcsPerfRetryCount() == null ? 1 : invoice.getHcsPerfRetryCount() + 1);
+ invoice.setHcsPerfSyncMsg(HcsSyncConstant.truncateMsg(message));
+ invoiceApplicationInvoiceMapper.updateById(invoice);
+ log.error("HCS履约开票记录推送失败:invoiceNo={},retryCount={},{}", invoice.getInvoiceNo(), invoice.getHcsPerfRetryCount(), message);
+ }
+
+ private Long toMillis(java.time.LocalDateTime localDateTime) {
+ return localDateTime == null ? null
+ : localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java
new file mode 100644
index 00000000..96e37fde
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java
@@ -0,0 +1,211 @@
+/**
+ * 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.transport.support.hcs;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.tool.utils.StringUtil;
+import org.springblade.hcs.config.HcsProperties;
+import org.springblade.hcs.exception.HcsException;
+import org.springblade.transport.constant.HcsSyncConstant;
+import org.springblade.transport.mapper.ContractManageMapper;
+import org.springblade.transport.mapper.FormalSettlementMapper;
+import org.springblade.transport.mapper.InvoiceReceiptMapper;
+import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper;
+import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.FormalSettlement;
+import org.springblade.transport.pojo.entity.HcsContractExeStage;
+import org.springblade.transport.pojo.entity.InvoiceReceipt;
+import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement;
+import org.springblade.transport.service.IContractExeStageService;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * HCS 履约收票记录同步支撑组件(C108-1,需求:收票单每条结算信息对应一条履约收票记录)
+ *
+ * 粒度:收票登记结算单分摊行(blade_invoice_receipt_settlement)对应一条履约收票记录;
+ * 触发:收票单推送合规(C109)成功后,随收票单扫描任务(HcsPushTask invoice-receipt)推送,
+ * 扫描范围限定所属收票单 hcs_sync_status=synced(C109 已推送),失败下轮自动补偿;
+ * 履约日期为发票日期(收票单 invoice_date),履约类型 payment(付款方向,关联合同付款履约计划行);
+ * 合同经应付结算链路回溯(结算分摊行 → 正式结算单合同);
+ * 幂等凭 hcs_perf_fd_id(非空跳过),失败置重试计数与原因由下轮扫描自动补偿。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class HcsReceiptPerfSyncSupport {
+
+ private final HcsProperties hcsProperties;
+ private final InvoiceReceiptSettlementMapper invoiceReceiptSettlementMapper;
+ private final InvoiceReceiptMapper invoiceReceiptMapper;
+ private final FormalSettlementMapper formalSettlementMapper;
+ private final ContractManageMapper contractManageMapper;
+ private final IContractExeStageService contractExeStageService;
+ private final HcsPerfRecordAddSupport hcsPerfRecordAddSupport;
+
+ /**
+ * 扫描并推送待同步的履约收票记录(每条结算信息一条 C108-1),返回结果汇总
+ *
+ * @return 汇总描述
+ */
+ public String pushUnsyncedReceiptPerfRecords() {
+ if (!hcsProperties.getPerfRecord().isSyncEnabled()) {
+ return "履约记录同步开关未开启";
+ }
+ int maxRetry = hcsProperties.getPush().getMaxRetry();
+ // 待推送结算信息:未建过履约记录且重试未超限
+ List settlementList = invoiceReceiptSettlementMapper.selectList(
+ Wrappers.lambdaQuery()
+ .isNull(InvoiceReceiptSettlement::getHcsPerfFdId)
+ .and(wrapper -> wrapper.isNull(InvoiceReceiptSettlement::getHcsPerfRetryCount)
+ .or().lt(InvoiceReceiptSettlement::getHcsPerfRetryCount, maxRetry))
+ .last("limit " + hcsProperties.getPush().getBatchSize()));
+ if (settlementList.isEmpty()) {
+ return "履约收票记录:无待推送结算信息";
+ }
+ // 批量装配上下文(收票单/结算单/合同/计划行,一次 IN 查询避免逐行 N+1)
+ Map receiptById = loadReceipts(settlementList);
+ Map settlementById = loadSettlements(settlementList);
+ int successCount = 0;
+ int failCount = 0;
+ for (InvoiceReceiptSettlement settlementLine : settlementList) {
+ try {
+ pushOneSettlementLine(settlementLine, receiptById.get(settlementLine.getInvoiceReceiptId()),
+ settlementById.get(settlementLine.getFormalSettlementId()));
+ successCount++;
+ } catch (Exception exception) {
+ markFailed(settlementLine, StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage());
+ failCount++;
+ }
+ }
+ String summary = String.format("履约收票记录:扫描 %d 条,成功 %d 条,失败 %d 条(重试上限 %d)",
+ settlementList.size(), successCount, failCount, maxRetry);
+ if (failCount > 0) {
+ log.warn("HCS履约收票记录推送存在失败:{}", summary);
+ }
+ return summary;
+ }
+
+ /**
+ * 推送单条结算信息的履约收票记录
+ */
+ private void pushOneSettlementLine(InvoiceReceiptSettlement settlementLine, InvoiceReceipt receipt, FormalSettlement settlement) {
+ if (receipt == null || !"approved".equals(receipt.getApprovalStatus())) {
+ throw new HcsException("收票单不存在或未审批通过:invoiceReceiptId=" + settlementLine.getInvoiceReceiptId());
+ }
+ // C109 收票单须已推送合规(履约记录引用合规合同与计划行)
+ if (!HcsSyncConstant.HCS_STATUS_SYNCED.equals(receipt.getHcsSyncStatus())) {
+ throw new HcsException("收票单尚未推送合规(invoiceNo=" + receipt.getInvoiceNo() + "),待 C109 推送成功后重试");
+ }
+ if (settlement == null || settlement.getContractId() == null) {
+ throw new HcsException("结算分摊行回溯不到合同(settlementId=" + settlementLine.getFormalSettlementId() + ")");
+ }
+ ContractManage contractManage = contractManageMapper.selectById(settlement.getContractId());
+ if (contractManage == null) {
+ throw new HcsException("合同不存在:contractId=" + settlement.getContractId());
+ }
+ // 关联合同的付款履约计划行(fd_plan_id 必填,无已同步计划行时通用组件抛出重试,合同链路首推后自愈)
+ HcsContractExeStage stageRow = contractExeStageService.findPaymentStageRow(
+ contractManage.getId(), HcsSyncConstant.FUND_NATURE_PROGRESS);
+ // 金额:分摊到该结算单的收票金额
+ BigDecimal amount = settlementLine.getAllocatedInvoiceAmount() == null
+ ? settlementLine.getSettlementAmount() : settlementLine.getAllocatedInvoiceAmount();
+ if (amount == null) {
+ throw new HcsException("结算分摊行金额为空(id=" + settlementLine.getId() + ")");
+ }
+ // 申请编号:发票号-结算单号(同一发票分摊多结算单时逐条可区分)
+ String settlementNo = StringUtil.isBlank(settlementLine.getFormalSettlementNo()) ? settlement.getFormalSettlementNo() : settlementLine.getFormalSettlementNo();
+ String applyNo = receipt.getInvoiceNo() + "-" + (settlementNo == null ? "" : settlementNo);
+ String perfFdId = hcsPerfRecordAddSupport.pushPerfRecordAdd(contractManage, stageRow,
+ applyNo, amount, toMillis(receipt.getInvoiceDate()),
+ HcsSyncConstant.PERFORM_TYPE_PAYMENT, receipt.getKingdeeReceivedInvoiceNo(), settlementLine.getId(),
+ "收票履约-" + applyNo);
+ markSynced(settlementLine, perfFdId);
+ }
+
+ /**
+ * 批量装配结算信息所属收票单(须审批通过且 C109 已推送)
+ */
+ private Map loadReceipts(List settlementList) {
+ List receiptIds = settlementList.stream()
+ .map(InvoiceReceiptSettlement::getInvoiceReceiptId).filter(Objects::nonNull).distinct().toList();
+ if (receiptIds.isEmpty()) {
+ return Map.of();
+ }
+ return invoiceReceiptMapper.selectBatchIds(receiptIds).stream()
+ .collect(Collectors.toMap(InvoiceReceipt::getId, item -> item, (first, second) -> first));
+ }
+
+ /**
+ * 批量装配结算信息关联的正式结算单
+ */
+ private Map loadSettlements(List settlementList) {
+ List settlementIds = settlementList.stream()
+ .map(InvoiceReceiptSettlement::getFormalSettlementId).filter(Objects::nonNull).distinct().toList();
+ if (settlementIds.isEmpty()) {
+ return Map.of();
+ }
+ return formalSettlementMapper.selectBatchIds(settlementIds).stream()
+ .collect(Collectors.toMap(FormalSettlement::getId, item -> item, (first, second) -> first, LinkedHashMap::new));
+ }
+
+ /**
+ * 推送成功回填:履约记录ID 立即持久化(幂等键),清失败原因
+ */
+ private void markSynced(InvoiceReceiptSettlement settlementLine, String perfFdId) {
+ settlementLine.setHcsPerfFdId(perfFdId);
+ settlementLine.setHcsPerfRetryCount(0);
+ settlementLine.setHcsPerfSyncMsg(null);
+ invoiceReceiptSettlementMapper.updateById(settlementLine);
+ log.info("HCS履约收票记录推送成功:settlementId={},perfFdId={}", settlementLine.getId(), perfFdId);
+ }
+
+ /**
+ * 推送失败回写:重试次数+1 + 失败原因(下轮扫描自动补偿)
+ */
+ private void markFailed(InvoiceReceiptSettlement settlementLine, String message) {
+ settlementLine.setHcsPerfRetryCount(settlementLine.getHcsPerfRetryCount() == null ? 1 : settlementLine.getHcsPerfRetryCount() + 1);
+ settlementLine.setHcsPerfSyncMsg(HcsSyncConstant.truncateMsg(message));
+ invoiceReceiptSettlementMapper.updateById(settlementLine);
+ log.error("HCS履约收票记录推送失败:settlementId={},retryCount={},{}", settlementLine.getId(), settlementLine.getHcsPerfRetryCount(), message);
+ }
+
+ private Long toMillis(LocalDate localDate) {
+ return localDate == null ? null : localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli();
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
index 2738bab2..70bdaadf 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java
@@ -46,10 +46,12 @@ import org.springblade.transport.service.IContractChangeService;
import org.springblade.transport.service.IProjectChangeService;
import org.springblade.transport.support.hcs.HcsContractSpecSyncSupport;
import org.springblade.transport.support.hcs.HcsContractSyncSupport;
+import org.springblade.transport.support.hcs.HcsInvoicePerfSyncSupport;
import org.springblade.transport.support.hcs.HcsInvoiceRegSyncSupport;
import org.springblade.transport.support.hcs.HcsPaymentSyncSupport;
import org.springblade.transport.support.hcs.HcsProjectSyncSupport;
import org.springblade.transport.support.hcs.HcsReceiptBillSyncSupport;
+import org.springblade.transport.support.hcs.HcsReceiptPerfSyncSupport;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@@ -110,6 +112,8 @@ public class HcsPushTask {
private final HcsPaymentSyncSupport hcsPaymentSyncSupport;
private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport;
private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport;
+ private final HcsInvoicePerfSyncSupport hcsInvoicePerfSyncSupport;
+ private final HcsReceiptPerfSyncSupport hcsReceiptPerfSyncSupport;
private final IContractChangeService contractChangeService;
private final IProjectChangeService projectChangeService;
private final BladeRedis bladeRedis;
@@ -148,19 +152,37 @@ public class HcsPushTask {
}
/**
- * 开票登记(C111)定时推送:扫描审批通过且待同步/待补偿的开票申请(含金蝶开票完成后回填),独立开关与 cron 调度
+ * 开票登记(C111)定时推送:扫描审批通过且待同步/待补偿的开票申请(含金蝶开票完成后回填),
+ * 并推送履约开票记录(C108-1,金蝶回写发票逐张推送),独立开关与 cron 调度
*/
@Scheduled(cron = "${thirdParty.hcs.push.invoice-application.cron:0 3/5 * * * ?}")
public void pushHcsInvoiceApplication() {
- this.runOnce("开票登记", HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION, hcsProperties.getPush().getInvoiceApplication().isSyncEnabled(), this::pushUnsyncedInvoiceApplications);
+ this.runOnce("开票登记", HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION, hcsProperties.getPush().getInvoiceApplication().isSyncEnabled(), this::syncHcsInvoiceApplications);
}
/**
- * 收票单(C109)定时推送:扫描审批通过且待同步/待补偿的收票单,独立开关与 cron 调度
+ * 开票链路组合动作:开票登记(C111)推送 + 履约开票记录(C108-1)推送(定时任务与手动批量入口共用)
+ */
+ public String syncHcsInvoiceApplications() {
+ String pushSummary = this.pushUnsyncedInvoiceApplications();
+ return pushSummary + ";" + hcsInvoicePerfSyncSupport.pushUnsyncedInvoicePerfRecords();
+ }
+
+ /**
+ * 收票单(C109)定时推送:扫描审批通过且待同步/待补偿的收票单,并推送履约收票记录
+ * (C108-1,收票单每条结算信息一条,限 C109 已推送),独立开关与 cron 调度
*/
@Scheduled(cron = "${thirdParty.hcs.push.invoice-receipt.cron:0 4/5 * * * ?}")
public void pushHcsInvoiceReceipt() {
- this.runOnce("收票单", HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT, hcsProperties.getPush().getInvoiceReceipt().isSyncEnabled(), this::pushUnsyncedInvoiceReceipts);
+ this.runOnce("收票单", HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT, hcsProperties.getPush().getInvoiceReceipt().isSyncEnabled(), this::syncHcsInvoiceReceipts);
+ }
+
+ /**
+ * 收票链路组合动作:收票单(C109)推送 + 履约收票记录(C108-1)推送(定时任务与手动批量入口共用)
+ */
+ public String syncHcsInvoiceReceipts() {
+ String pushSummary = this.pushUnsyncedInvoiceReceipts();
+ return pushSummary + ";" + hcsReceiptPerfSyncSupport.pushUnsyncedReceiptPerfRecords();
}
/**
From 30ba7c6b3a1e275e99f83787e1773f779e146976 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 19:31:04 +0800
Subject: [PATCH 29/63] =?UTF-8?q?docs:=20=E6=96=B0=E5=A2=9EC108=E5=B1=A5?=
=?UTF-8?q?=E7=BA=A6=E8=AE=B0=E5=BD=95=E5=9B=9B=E5=9C=BA=E6=99=AF=E6=8E=A8?=
=?UTF-8?q?=E9=80=81=E6=94=B9=E9=80=A0=E4=B8=8E=E9=AA=8C=E8=AF=81=E6=8C=87?=
=?UTF-8?q?=E5=BC=95=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...四场景推送改造流程梳理(收款-付款-开票-收票).md | 263 ++++++++++++++++++
1 file changed, 263 insertions(+)
create mode 100644 doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md
diff --git a/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md b/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md
new file mode 100644
index 00000000..f85fd850
--- /dev/null
+++ b/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md
@@ -0,0 +1,263 @@
+# C108 履约记录四场景推送改造流程梳理(收款/付款/开票/收票)
+
+> 日期:2026-10-28
+> 范围:`tms-api` blade-transport HCS 推送链路;合同系统(HCS)接口 C108-1 履约记录新建 / C108-2 履约记录更新
+> 关联文档:《2026-10-27-履约计划与履约记录逻辑与流程梳理》(改造前基线)
+
+---
+
+## 1. 需求口径
+
+C108-1(履约记录新建)、C108-2(履约记录更新)两个接口需在收款、付款、开票、收票四个场景使用:
+
+| # | 场景 | 对应 TMS 数据 | 推送时机 | 履约日期(fd_plan_date) | 履约类型(fd_paln_type) |
+|---|------|--------------|----------|------------------------|------------------------|
+| 1 | 履约收款记录 | 收款认领记录(收款流水 × 认领分摊) | 金蝶认领推送(A301-3)成功后 | **交易日期**(收款流水 `transaction_time`) | collection(收款) |
+| 2 | 履约付款记录 | 付款申请(含实际付款金额) | **金蝶实际付款后**(付款结果回写推进) | 申请日期(计划日期口径,维持原样) | payment(付款) |
+| 3 | 履约开票记录 | 开票申请的金蝶回写发票(每张发票一条) | **金蝶开票完成后**(A303-5 发票落库后) | **发票完成开票日期**(`invoice_date`) | collection(收款) |
+| 4 | 履约收票记录 | 收票单的每条结算信息(分摊行,每条一条) | 收票单推送合规(C109)成功后 | **发票日期**(收票单 `invoice_date`) | payment(付款) |
+
+本次确认的业务口径调整:**履约付款记录由「审批通过即新建」改为「金蝶实际付款后才新建」**。
+
+---
+
+## 2. 总览:改造前后对照
+
+| 场景 | 改造前 | 改造后 |
+|------|--------|--------|
+| 收款 | 已实现(C108-1),履约日期=最近认领日期 | 履约日期改为交易日期;组装逻辑收敛到通用组件 |
+| 付款 | 审批通过即 C108-1 新建,实际付款后 C108-2 更新 | 未实际付款不推送;首次实际付款 C108-1 新建;分期付款推进 C108-2 更新 |
+| 开票 | 无(仅 C111 开票登记) | 新增:金蝶回写发票逐张推 C108-1,发票粒度幂等+重试 |
+| 收票 | 无(仅 C109 收票单) | 新增:每条结算分摊行推 C108-1,分摊行粒度幂等+重试,且限 C109 已推送 |
+
+---
+
+## 3. 通用机制
+
+### 3.1 接口与字段
+
+- C108-1:`IHcsClient.perfRecordAdd`,返回 `fdId`(履约记录ID,**必须持久化**,C108-2 更新必传)
+- C108-2:`IHcsClient.perfRecordUpdate`,`formData.fdId` 必填,响应无 data,`success=true` 即成功
+- `fd_plan_id`(履约计划id)接口必填:取合同**已同步的履约计划行合规阶段ID**(`blade_contract_exe_stage.hcs_stage_fd_id`);无已同步计划行时抛出重试,合同链路首推 C103-9 后下轮自愈
+- `fd_paln_type` 为接口文档原文拼写(plan 笔误),必须沿用;取值 `payment` / `collection`
+- 机制数据(附件)C108-1 必填:新三场景(收款/开票/收票)无附件传空列表;付款场景保留附件转传逻辑
+
+### 3.2 开关
+
+统一复用 `thirdParty.hcs.perf-record.sync-enabled`(默认关,灰度打开),无新增配置:
+- 收款:门禁 C108-1(C103-9/C107-1 仍由 `exe-stage.sync-enabled` 门禁)
+- 付款:门禁 C108-1/C108-2
+- 开票/收票:门禁各自扫描任务,且随宿主任务(invoice-application / invoice-receipt)的 `sync-enabled` 一起生效
+
+### 3.3 幂等与重试模式
+
+- **付款**:单据粒度,`blade_payment_application.hcs_perf_fd_id` + `hcs_sync_status`(unsynced/synced/failed)+ `hcs_retry_count`,随 HcsPushTask 付款任务扫描重试
+- **开票**:发票粒度,`blade_invoice_application_invoice.hcs_perf_fd_id`(非空即已推,扫描跳过)+ `hcs_perf_retry_count` / `hcs_perf_sync_msg`,失败下轮自动补偿,超上限人工
+- **收票**:分摊行粒度,`blade_invoice_receipt_settlement.hcs_perf_fd_id` + 同上两列;扫描范围限定所属收票单 C109 已推送(`hcs_sync_status=synced`)
+- **收款**:认领链事件驱动(金蝶认领推送成功后内联),fdId 暂不落库(shortcut:认领链无 C108-2 场景,重推会重复,需作废联动时先落库)
+
+### 3.4 通用组件
+
+新增 `HcsPerfRecordAddSupport`(transport/support/hcs):收敛收款/开票/收票三场景的 C108-1 组装与推送(申请金额=实际金额、fd_plan_id 校验、标题、溯源字段),入参:合同、履约计划行、申请编号、金额、履约日期、履约类型、金蝶单据号、业务ID、标题。
+付款场景因含 C108-2 推进与附件转传,保留原 `HcsPerfRecordSyncSupport`。
+
+---
+
+## 4. 各场景流程明细
+
+### 4.1 履约收款记录(对应认领记录)
+
+```
+认领操作 → 收款流水推送金蝶(A301-3)成功
+ → HcsClaimSyncSupport.pushClaimToHcs(flowId)
+ ① C103-9 认领金额累加收款履约计划行并重推
+ ② C107-1 认领记录(按「认领主单 × 合同」分组,金额为分摊到该合同的金额)
+ ③ C108-1 履约流水(按合同一条) ← 本次改造点
+```
+
+- 履约日期:**收款流水交易日期** `blade_kingdee_receipt_flow.transaction_time`(原为最近认领日期,空时兜底当天)
+- 申请编号:收款通知单号,兜底明细流水号
+- 申请金额 = 实际金额 = 该合同累计认领金额
+- fd_jd_doc_no:A301-3 返回的金蝶收款单号;fd_business_id:收款流水ID
+- 标题:`收款履约-{通知单号}`
+- 前置门禁:合同已推送合规(hcs_fd_id 非空)、存在已同步收款计划行,不满足仅记日志跳过
+
+### 4.2 履约付款记录(对应付款记录,金蝶实际付款了再推)
+
+```
+付款申请审批通过(特批通过后)
+ → HCS 推送链:C106-3 特批(如需)→ C103-9 履约计划重推
+ → C108 履约记录环节:paidAmount 为空/0 → 跳过(提示"金蝶未实际付款,待付款后推送履约记录")★改
+ 整体状态照常置 synced
+
+金蝶实际付款 → 定时任务 A103-2 付款结果回写(applyPayApplyResult)
+ → paidAmount 首次 >0 或金额变化 → hcs_sync_status 置 unsynced ★改(原仅在有 fdId 时置)
+ → HcsPushTask 付款任务下轮扫描 → pushPayment
+ 无 hcs_perf_fd_id → C108-1 新建(申请金额 + 实际金额)→ fdId 回填 hcs_perf_fd_id
+ 有 hcs_perf_fd_id → C108-2 更新(分期付款推进实际金额)
+```
+
+- 触发数据源:A103-2 分录 `e_paidamt` 汇总(分录缺失且已付款时兜底申请全额)
+- 履约日期:申请日期(fd_plan_date 计划日期口径不变);C108-2 仅在金额推进时触发,`paying` 状态每轮回写金额不变不再重复推送
+- 标题:`付款履约-{付款申请单号}`;fd_jd_doc_no:金蝶付款申请单号
+
+### 4.3 履约开票记录(对应开票申请,金蝶开票完成后推送)
+
+```
+开票申请审批通过 → 推金蝶(A303-1)→ 推合规 C111 开票登记(票据日期空)
+金蝶开票完成 → 定时任务 A303-5 开票结果回写(executeInvoiceResultSync)
+ → 发票逐张落库 blade_invoice_application_invoice
+ → 申请单置 kingdeeStatus=invoiced + hcs_sync_status=unsynced(触发 C111-3 回填票据日期)
+
+HcsPushTask 开票任务(每5分钟):
+ ① pushUnsyncedInvoiceApplications:C111 开票登记推送/回填
+ ② HcsInvoicePerfSyncSupport.pushUnsyncedInvoicePerfRecords ★新增
+ 扫描 hcs_perf_fd_id IS NULL 且重试未超限的发票
+ → 批量装配:申请单(须审批通过)→ 应收结算关联 → 正式结算单合同 → 收款履约计划行
+ → 每张发票一条 C108-1:金额=发票价税合计,履约日期=发票完成开票日期
+ → 成功回填 hcs_perf_fd_id;失败 retry+1 记 hcs_perf_sync_msg 下轮补偿
+```
+
+- 申请编号:发票号;fd_jd_doc_no:金蝶开票申请单编号(serialNo);fd_business_id:发票记录ID
+- 标题:`开票履约-{发票号}`
+- 合同回溯不到(无应收结算关联/合同)→ 记失败原因,下轮重试(同 C111 §9-U10 口径)
+
+### 4.4 履约收票记录(对应收票单每条结算信息)
+
+```
+收票登记审批通过
+ → 推金蝶收票单(A303-2 + A302-6)
+ → HcsPushTask 收票任务(每5分钟):
+ ① pushUnsyncedInvoiceReceipts:C109 收票单推送合规
+ ② HcsReceiptPerfSyncSupport.pushUnsyncedReceiptPerfRecords ★新增
+ 扫描 hcs_perf_fd_id IS NULL 且重试未超限的分摊行,
+ 且所属收票单 hcs_sync_status=synced(C109 已推送)+ 审批通过
+ → 每条结算信息一条 C108-1:金额=分摊收票金额(兜底结算金额),
+ 履约日期=收票单发票日期,履约类型 payment,关联合同付款履约计划行
+ → 成功回填 hcs_perf_fd_id;失败 retry+1 记 hcs_perf_sync_msg 下轮补偿
+```
+
+- 申请编号:`{发票号}-{结算单号}`(同一发票分摊多结算单时逐条可区分);标题:`收票履约-{发票号}-{结算单号}`
+- fd_jd_doc_no:金蝶收票单号(`kingdee_received_invoice_no`);fd_business_id:分摊行ID
+- 合同回溯:分摊行 → 正式结算单 → contractId
+
+---
+
+## 5. 定时任务编排(改造后)
+
+| 任务 | cron(默认) | 内容 |
+|------|-------------|------|
+| payment | `0 2/5 * * * ?` | 付款特批 C106 + 履约计划 C103-9 + **履约付款记录 C108-1/2(金蝶实际付款门禁)** |
+| invoice-application | `0 3/5 * * * ?` | C111 开票登记 + **履约开票记录 C108-1(发票粒度)** |
+| invoice-receipt | `0 4/5 * * * ?` | C109 收票单 + **履约收票记录 C108-1(分摊行粒度)** |
+| (收款无独立任务) | 事件驱动 | 金蝶认领推送(A301-3)成功后内联:C103-9 → C107-1 → C108-1 |
+
+---
+
+## 6. 数据库变更
+
+`doc/sql/transport/blade_invoice_application_invoice_perf_20261028.sql`、`blade_invoice_receipt_settlement_perf_20261028.sql`:
+
+- `blade_invoice_application_invoice` 加 `hcs_perf_fd_id varchar(64)` / `hcs_perf_retry_count int` / `hcs_perf_sync_msg varchar(512)`
+- `blade_invoice_receipt_settlement` 加同三列
+
+---
+
+## 7. 关键代码位置
+
+| 文件 | 说明 |
+|------|------|
+| `support/hcs/HcsPerfRecordAddSupport.java` | ★新增 通用 C108-1 组件(收款/开票/收票共用) |
+| `support/hcs/HcsInvoicePerfSyncSupport.java` | ★新增 履约开票记录扫描推送 |
+| `support/hcs/HcsReceiptPerfSyncSupport.java` | ★新增 履约收票记录扫描推送 |
+| `support/hcs/HcsClaimSyncSupport.java` | 改:履约日期→交易日期;组装改调通用组件 |
+| `support/hcs/HcsPaymentSyncSupport.java` | 改:C108 环节增加「金蝶实际付款」门禁 |
+| `service/impl/PaymentApplicationServiceImpl.java` | 改:实际付款推进置 unsynced(不再要求已有 fdId;金额不变不重复触发) |
+| `task/HcsPushTask.java` | 改:开票/收票任务追加履约记录推送;组合方法改 public 供手动批量入口复用 |
+| `controller/HcsSyncController.java` | 改:`POST /hcs/sync/invoice-application|invoice-receipt` 批量入口改走组合方法(手动触发也推履约记录) |
+| `pojo/entity/InvoiceApplicationInvoice.java`、`InvoiceReceiptSettlement.java` | 改:加 3 个 hcs_perf_* 字段 |
+
+---
+
+## 8. 验证指引
+
+### 8.1 前置准备
+
+1. **执行 SQL**:`doc/sql/transport/blade_invoice_application_invoice_perf_20261028.sql`、`blade_invoice_receipt_settlement_perf_20261028.sql`(两表各加 3 列)。
+2. **编译**:`mvn clean compile -DskipTests -pl blade-service/blade-transport -am`(已验证通过)。
+3. **Nacos 开关**(`third-party-api.yaml` / 环境配置):
+ - `thirdParty.hcs.perf-record.sync-enabled: true`(履约记录总开关,四场景共用,默认关)
+ - 宿主任务开关:`thirdParty.hcs.push.payment.sync-enabled`、`thirdParty.hcs.push.invoice-application.sync-enabled`、`thirdParty.hcs.push.invoice-receipt.sync-enabled`(定时任务与手动批量共用)
+ - 收款链另需 `thirdParty.hcs.exe-stage.sync-enabled: true`(门禁 C103-9/C107-1)
+ - 环境项照旧:`thirdParty.hcs.base-url/authorization/key-auth`、`perf-record.xform-id`
+4. **基础数据**:合同已推送合规(`blade_contract_manage.hcs_fd_id` 非空)且存在已同步履约计划行(`blade_contract_exe_stage.hcs_stage_fd_id` 非空,收款场景需收款行、付款/收票需付款行、开票需收款行)——`fd_plan_id` 必填来源,无则推送记失败等待合同链路首推后自愈。
+
+### 8.2 手动触发入口(联调不等定时任务)
+
+| 动作 | 接口 |
+|------|------|
+| 认领推送金蝶(收款链触发点) | `POST /receipt-flow/push-kingdee` |
+| 付款结果回写(A103-2/A103-4) | `POST /payment-application/sync-kingdee-result` |
+| 开票结果回写(A303-5 发票落库) | `POST /invoice-application/sync-kingdee-invoice-result` |
+| 收票单推送金蝶(A303-2+A302-6) | `POST /invoice-receipt/push-kingdee` |
+| HCS 批量扫描一轮(含履约记录) | `POST /hcs/sync/payment`、`/hcs/sync/invoice-application`、`/hcs/sync/invoice-receipt` |
+| HCS 单条重推(付款) | `POST /hcs/sync/payment/{付款申请ID}` |
+
+> 注意:开票/收票的手动批量与定时任务走同一组合方法(C111/C109 + 履约记录一起推);单条重推仅覆盖 C111/C109 主单据,发票/分摊行粒度的履约重推走批量扫描(fdId 为空的行自动补偿)。
+
+### 8.3 分场景验证步骤
+
+**场景 1|履约收款记录**
+
+1. 页面对收款流水做认领(分摊到关联合同的正式结算单),推送金蝶认领:`POST /receipt-flow/push-kingdee`。
+2. TMS 日志:`HCS认领记录推送成功` → `HCS履约流水新建成功`(含 perfFdId)。
+3. HCS 侧核对履约记录:标题 `收款履约-{通知单号}`、申请/实际金额=该合同累计认领金额、**fd_plan_date=收款流水交易日期**(改造点,不再是最早/最近认领日期)、fd_plan_id=收款计划行阶段ID。
+4. 边界:交易时间为空的流水 → fd_plan_date 兜底当天;认领作废不触发推送。
+
+**场景 2|履约付款记录(金蝶实际付款了再推)**
+
+1. 付款申请审批通过并推金蝶(`POST /payment-application/sync-kingdee`);此时**不应**产生履约付款记录——日志出现 `金蝶未实际付款,待付款后推送履约记录`,HCS 侧无记录。
+2. 在金蝶完成实际付款后,手动回写:`POST /payment-application/sync-kingdee-result`。
+3. 数据库:`SELECT paid_amount, hcs_sync_status FROM blade_payment_application WHERE id=?` → paid_amount>0 且 hcs_sync_status=unsynced。
+4. 手动扫描:`POST /hcs/sync/payment` → 日志 `HCS履约记录新建成功`(C108-1)→ 数据库 `hcs_perf_fd_id` 回填。
+5. 分期付款(金蝶再次付款)→ 再触发回写 → 金额变化置 unsynced → 再扫描 → 日志 `HCS履约记录更新成功`(C108-2);HCS 侧 fd_fact_amount 与最新 paid_amount 一致。
+6. 反向验证:`paying` 状态下连续两轮回写但金额不变 → 不置 unsynced、不重复推送。
+
+**场景 3|履约开票记录**
+
+1. 开票申请审批通过 → 推金蝶 → 金蝶完成开票后手动回写:`POST /invoice-application/sync-kingdee-invoice-result` → `blade_invoice_application_invoice` 落库发票。
+2. 手动扫描:`POST /hcs/sync/invoice-application` → 汇总含 `履约开票记录:扫描 X 张,成功 X 张`。
+3. 数据库:`SELECT invoice_no, invoice_date, hcs_perf_fd_id, hcs_perf_sync_msg FROM blade_invoice_application_invoice WHERE invoice_application_id=?` → 每张发票 fdId 回填、msg 清空。
+4. HCS 侧核对:每张发票一条履约记录、标题 `开票履约-{发票号}`、**fd_plan_date=发票完成开票日期**、金额=发票价税合计、fd_plan_id=收款计划行阶段ID。
+5. 失败重试:构造失败(如临时断开 HCS / 删掉合同 hcs_fd_id)→ fdId 保持空、`hcs_perf_retry_count` 每轮 +1、`hcs_perf_sync_msg` 记录原因;恢复后下轮自动补推成功。
+
+**场景 4|履约收票记录**
+
+1. 收票登记(分摊到结算单)审批通过 → 推金蝶:`POST /invoice-receipt/push-kingdee`。
+2. 手动扫描:`POST /hcs/sync/invoice-receipt` → 先 `C109 收票单推送成功`,再 `履约收票记录:扫描 X 条,成功 X 条`。
+3. 数据库:`SELECT formal_settlement_no, allocated_invoice_amount, hcs_perf_fd_id, hcs_perf_sync_msg FROM blade_invoice_receipt_settlement WHERE invoice_receipt_id=?` → 每条分摊行 fdId 回填。
+4. HCS 侧核对:每条结算信息一条履约记录、申请编号 `-{发票号}-{结算单号}`、**fd_plan_date=收票单发票日期**、金额=分摊收票金额、fd_plan_id=付款计划行阶段ID、履约类型 payment。
+5. 依赖顺序验证:C109 未推送成功时先扫描 → 分摊行 `hcs_perf_sync_msg` 记"收票单尚未推送合规";C109 成功后下轮自动补推,无需人工干预。
+
+### 8.4 通用检查点
+
+| 检查项 | 方法 | 预期 |
+|--------|------|------|
+| 幂等 | 同条件连续触发两轮 `/hcs/sync/{bizType}` | 第二轮履约记录扫描 0 条(fdId 非空跳过),HCS 无重复记录 |
+| 失败重试上限 | 持续失败场景观察 | `hcs_perf_retry_count` 达 `thirdParty.hcs.push.max-retry`(默认 5)后不再扫描,人工处理后重置计数 |
+| 履约日期口径 | HCS 侧逐条核对 fd_plan_date | 收款=交易日期 / 开票=发票开票日期 / 收票=发票日期 / 付款=申请日期 |
+| 溯源字段 fd_business_id | HCS 侧核对 | 付款=付款申请ID、收款=流水ID、开票=发票记录ID、收票=分摊行ID |
+| 开关关闭回归 | `perf-record.sync-enabled=false` 后触发各链路 | 金蝶/合规主流程(A301-3、C111、C109)不受影响,仅履约记录跳过 |
+| 日志 | 检索 `HCS履约` | 推送开始/成功/失败成对出现,失败带响应 msg 与重试计数 |
+
+> 服务端核对路径:HCS 管理端按 `fd_doc_subject`(收款履约-/付款履约-/开票履约-/收票履约-前缀)检索履约记录列表。
+
+---
+
+## 9. 联调注意事项与风险
+
+1. **fd_paln_type / 款项性质口径**:开票=collection+收款进度款计划行、收票=payment+付款进度款计划行,为按履约计划行方向推定的口径,需与合规侧确认。
+2. **C108-1 无服务端判重键**:新三场景极端情况下「推送超时但远端已成功」的重试可能在合同系统产生重复记录(与既有 C107-1 同风险);开票/收票有本地 fdId 幂等只能防"确认成功后"的重复,不能防"未知结果"的重试。
+3. **收票履约依赖 C109 先推**:收票单未推送合规(或推送失败)时分摊行记失败原因等待,C109 成功后下轮自动补偿。
+4. **存量数据**:改造前已按「审批通过即新建」推送过履约付款记录的单据(已有 `hcs_perf_fd_id`),后续金额推进仍走 C108-2,行为兼容;开票/收票新场景上线后只会处理新落库数据(`hcs_perf_fd_id` 为空的历史发票会被补推,如需排除请手工回填标记)。
+5. 测试由业务侧执行;开关 `thirdParty.hcs.perf-record.sync-enabled` 默认关闭,灰度打开后观察 HcsPushTask 日志(前缀 `HCS履约`)。
From 830acee00e604b8f69c4d5e40864a3e4072f754a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 19:32:57 +0800
Subject: [PATCH 30/63] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E6=AD=A3?=
=?UTF-8?q?=E5=BC=8F=E7=BB=93=E7=AE=97=E5=8D=95=E5=90=8C=E6=AD=A5=E9=87=91?=
=?UTF-8?q?=E8=9D=B6=E5=A4=B1=E8=B4=A5=E5=8E=9F=E5=9B=A0=E6=9C=AA=E8=90=BD?=
=?UTF-8?q?=E5=BA=93=E5=AF=BC=E8=87=B4=E5=88=97=E8=A1=A8=E4=BB=85=E6=98=BE?=
=?UTF-8?q?=E7=A4=BA=E5=85=9C=E5=BA=95=E6=8F=90=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../service/impl/FormalSettlementServiceImpl.java | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
index 9e9f1b1e..497919f0 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
@@ -865,16 +865,17 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl 500 ? failReason.substring(0, 500) : failReason;
+ // 条件更新:仅审批通过且未同步的单据落"生成失败"状态与失败原因,未审批/已同步/已删除单据自动跳过保持原状态
int updated = baseMapper.update(null, Wrappers.lambdaUpdate()
.eq(FormalSettlement::getId, id)
.eq(FormalSettlement::getApprovalStatus, TransportBusiConstant.APPROVED)
.ne(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED)
- .set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
+ .set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
+ .set(FormalSettlement::getKingdeeFailReason, columnReason));
if (updated > 0) {
- // 失败原因截断留档,与列长 varchar(500) 对齐
- String failReason = Func.toStrWithEmpty(reason, "未知异常");
- String columnReason = failReason.length() > 500 ? failReason.substring(0, 500) : failReason;
// 结算调整单回填:未推送场景审批通过后自动推送失败,调整单置 failed + 失败原因供「推送金蝶」重试
settlementAdjustmentMapper.update(null, Wrappers.lambdaUpdate()
.eq(SettlementAdjustment::getFormalSettlementId, id)
From adbc87e2e4a50a57be94a0328a8d4f4a7b47c85b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Wed, 28 Oct 2026 19:43:37 +0800
Subject: [PATCH 31/63] =?UTF-8?q?feat:=20=E5=B1=A5=E7=BA=A6=E8=AE=B0?=
=?UTF-8?q?=E5=BD=95=E7=B1=BB=E5=9E=8Bfd=5Fpaln=5Ftype=E6=8C=89=E5=9B=9B?=
=?UTF-8?q?=E5=9C=BA=E6=99=AF=E4=BC=A0=E4=B8=AD=E6=96=87=E5=80=BC=EF=BC=88?=
=?UTF-8?q?=E4=BB=98=E6=AC=BE/=E6=94=B6=E6=AC=BE/=E5=BC=80=E7=A5=A8/?=
=?UTF-8?q?=E6=94=B6=E7=A5=A8=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/constant/HcsSyncConstant.java | 26 +++++++++++++++++--
.../support/hcs/HcsClaimSyncSupport.java | 2 +-
.../hcs/HcsInvoicePerfSyncSupport.java | 4 +--
.../support/hcs/HcsPerfRecordSyncSupport.java | 4 +--
.../hcs/HcsReceiptPerfSyncSupport.java | 4 +--
...四场景推送改造流程梳理(收款-付款-开票-收票).md | 12 +++++----
6 files changed, 38 insertions(+), 14 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index bf786eaf..13069ebd 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -250,15 +250,37 @@ public final class HcsSyncConstant {
public static final String ROW_OP_DELETE = "delete";
/**
- * 履约类型:付款
+ * 履约类型:付款(C103-9 履约计划行)
*/
public static final String PERFORM_TYPE_PAYMENT = "payment";
/**
- * 履约类型:收款
+ * 履约类型:收款(C103-9 履约计划行)
*/
public static final String PERFORM_TYPE_COLLECTION = "collection";
+ // ==================== 履约记录类型(C108 fd_paln_type,按四场景传中文) ====================
+
+ /**
+ * 履约记录类型:付款
+ */
+ public static final String PERF_TYPE_PAYMENT = "付款";
+
+ /**
+ * 履约记录类型:收款
+ */
+ public static final String PERF_TYPE_COLLECTION = "收款";
+
+ /**
+ * 履约记录类型:开票
+ */
+ public static final String PERF_TYPE_INVOICE = "开票";
+
+ /**
+ * 履约记录类型:收票
+ */
+ public static final String PERF_TYPE_INVOICE_RECEIPT = "收票";
+
/**
* 款项性质:进度款
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java
index ea242d5c..8577b62d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java
@@ -282,7 +282,7 @@ public class HcsClaimSyncSupport {
LocalDate perfDate = flow.getTransactionTime() != null ? flow.getTransactionTime().toLocalDate() : LocalDate.now();
String perfFdId = hcsPerfRecordAddSupport.pushPerfRecordAdd(contractManage, stageRow,
firstNonBlank(flow.getReceiptNoticeNo(), flow.getDetailSerialNo()), claimedTotal, toMillis(perfDate),
- HcsSyncConstant.PERFORM_TYPE_COLLECTION, flow.getKingdeeBillNo(), flow.getId(),
+ HcsSyncConstant.PERF_TYPE_COLLECTION, flow.getKingdeeBillNo(), flow.getId(),
"收款履约-" + firstNonBlank(flow.getReceiptNoticeNo(), flow.getDetailSerialNo()));
// shortcut: 履约流水 fdId 未持久化(认领链无 C108-2 更新场景,重推会重复);需推进实际金额更新时先落库
log.info("HCS履约流水新建完成 flowId:{},contractNo:{},perfFdId:{}", flow.getId(), contractManage.getContractNo(), perfFdId);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java
index a831adcc..00ed9b2f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java
@@ -60,7 +60,7 @@ import java.util.stream.Collectors;
*
* 粒度:金蝶回写的每张发票(blade_invoice_application_invoice)对应一条履约开票记录;
* 触发:A303-5 开票结果回写落库发票后,随开票登记扫描任务(HcsPushTask invoice-application)推送;
- * 履约日期为发票完成开票日期(invoice_date),履约类型 collection(收款方向,关联合同收款履约计划行);
+ * 履约日期为发票完成开票日期(invoice_date),履约类型「开票」(关联合同收款履约计划行);
* 合同经应收结算链路回溯(开票申请 → 应收结算关联 → 正式结算单合同,同 C111 §9-U10 口径);
* 幂等凭 hcs_perf_fd_id(非空跳过),失败置重试计数与原因由下轮扫描自动补偿。
*
@@ -139,7 +139,7 @@ public class HcsInvoicePerfSyncSupport {
BigDecimal amount = invoice.getInvoiceAmount() == null ? BigDecimal.ZERO : invoice.getInvoiceAmount();
String perfFdId = hcsPerfRecordAddSupport.pushPerfRecordAdd(contractManage, stageRow,
invoice.getInvoiceNo(), amount, toMillis(invoice.getInvoiceDate()),
- HcsSyncConstant.PERFORM_TYPE_COLLECTION, invoice.getSerialNo(), invoice.getId(),
+ HcsSyncConstant.PERF_TYPE_INVOICE, invoice.getSerialNo(), invoice.getId(),
"开票履约-" + invoice.getInvoiceNo());
markSynced(invoice, perfFdId);
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
index 54e14846..9d2c3a32 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPerfRecordSyncSupport.java
@@ -163,8 +163,8 @@ public class HcsPerfRecordSyncSupport {
props.setFdApplyNo(payment.getPaymentNo());
// 【必填】计划日期:applyDate(毫秒时间戳)
props.setFdPlanDate(toMillis(payment.getApplyDate()));
- // 计划类型:paymentType(原文拼写 fd_paln_type 必须沿用)
- props.setFdPalnType(payment.getPaymentType());
+ // 计划类型:履约记录类型按场景传中文「付款」(原文拼写 fd_paln_type 必须沿用)
+ props.setFdPalnType(HcsSyncConstant.PERF_TYPE_PAYMENT);
// 性质:款项性质取关联履约计划行的款项性质
props.setFdProperty(stageRow.getFundNature());
// 履约计划id / 来源履约计划项ID:履约计划行合规阶段ID(HCS 只认自己的阶段ID)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java
index 96e37fde..13d2bddc 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java
@@ -59,7 +59,7 @@ import java.util.stream.Collectors;
* 粒度:收票登记结算单分摊行(blade_invoice_receipt_settlement)对应一条履约收票记录;
* 触发:收票单推送合规(C109)成功后,随收票单扫描任务(HcsPushTask invoice-receipt)推送,
* 扫描范围限定所属收票单 hcs_sync_status=synced(C109 已推送),失败下轮自动补偿;
- * 履约日期为发票日期(收票单 invoice_date),履约类型 payment(付款方向,关联合同付款履约计划行);
+ * 履约日期为发票日期(收票单 invoice_date),履约类型「收票」(关联合同付款履约计划行);
* 合同经应付结算链路回溯(结算分摊行 → 正式结算单合同);
* 幂等凭 hcs_perf_fd_id(非空跳过),失败置重试计数与原因由下轮扫描自动补偿。
*
@@ -153,7 +153,7 @@ public class HcsReceiptPerfSyncSupport {
String applyNo = receipt.getInvoiceNo() + "-" + (settlementNo == null ? "" : settlementNo);
String perfFdId = hcsPerfRecordAddSupport.pushPerfRecordAdd(contractManage, stageRow,
applyNo, amount, toMillis(receipt.getInvoiceDate()),
- HcsSyncConstant.PERFORM_TYPE_PAYMENT, receipt.getKingdeeReceivedInvoiceNo(), settlementLine.getId(),
+ HcsSyncConstant.PERF_TYPE_INVOICE_RECEIPT, receipt.getKingdeeReceivedInvoiceNo(), settlementLine.getId(),
"收票履约-" + applyNo);
markSynced(settlementLine, perfFdId);
}
diff --git a/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md b/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md
index f85fd850..d60a406c 100644
--- a/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md
+++ b/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md
@@ -12,10 +12,12 @@ C108-1(履约记录新建)、C108-2(履约记录更新)两个接口需
| # | 场景 | 对应 TMS 数据 | 推送时机 | 履约日期(fd_plan_date) | 履约类型(fd_paln_type) |
|---|------|--------------|----------|------------------------|------------------------|
-| 1 | 履约收款记录 | 收款认领记录(收款流水 × 认领分摊) | 金蝶认领推送(A301-3)成功后 | **交易日期**(收款流水 `transaction_time`) | collection(收款) |
-| 2 | 履约付款记录 | 付款申请(含实际付款金额) | **金蝶实际付款后**(付款结果回写推进) | 申请日期(计划日期口径,维持原样) | payment(付款) |
-| 3 | 履约开票记录 | 开票申请的金蝶回写发票(每张发票一条) | **金蝶开票完成后**(A303-5 发票落库后) | **发票完成开票日期**(`invoice_date`) | collection(收款) |
-| 4 | 履约收票记录 | 收票单的每条结算信息(分摊行,每条一条) | 收票单推送合规(C109)成功后 | **发票日期**(收票单 `invoice_date`) | payment(付款) |
+| 1 | 履约收款记录 | 收款认领记录(收款流水 × 认领分摊) | 金蝶认领推送(A301-3)成功后 | **交易日期**(收款流水 `transaction_time`) | 收款 |
+| 2 | 履约付款记录 | 付款申请(含实际付款金额) | **金蝶实际付款后**(付款结果回写推进) | 申请日期(计划日期口径,维持原样) | 付款 |
+| 3 | 履约开票记录 | 开票申请的金蝶回写发票(每张发票一条) | **金蝶开票完成后**(A303-5 发票落库后) | **发票完成开票日期**(`invoice_date`) | 开票 |
+| 4 | 履约收票记录 | 收票单的每条结算信息(分摊行,每条一条) | 收票单推送合规(C109)成功后 | **发票日期**(收票单 `invoice_date`) | 收票 |
+
+> `fd_paln_type` 按四场景分别传中文值:**付款 / 收款 / 开票 / 收票**(常量 `HcsSyncConstant.PERF_TYPE_*`);C103-9 履约计划行的履约类型仍传 `payment`/`collection`,两者互不影响。
本次确认的业务口径调整:**履约付款记录由「审批通过即新建」改为「金蝶实际付款后才新建」**。
@@ -256,7 +258,7 @@ HcsPushTask 开票任务(每5分钟):
## 9. 联调注意事项与风险
-1. **fd_paln_type / 款项性质口径**:开票=collection+收款进度款计划行、收票=payment+付款进度款计划行,为按履约计划行方向推定的口径,需与合规侧确认。
+1. **fd_paln_type 口径(已确认)**:履约记录类型按四场景传中文——付款 / 收款 / 开票 / 收票;款项性质仍取关联履约计划行(progress),如合规侧字典要求编码(如 4141 样例)联调时调整常量即可。
2. **C108-1 无服务端判重键**:新三场景极端情况下「推送超时但远端已成功」的重试可能在合同系统产生重复记录(与既有 C107-1 同风险);开票/收票有本地 fdId 幂等只能防"确认成功后"的重复,不能防"未知结果"的重试。
3. **收票履约依赖 C109 先推**:收票单未推送合规(或推送失败)时分摊行记失败原因等待,C109 成功后下轮自动补偿。
4. **存量数据**:改造前已按「审批通过即新建」推送过履约付款记录的单据(已有 `hcs_perf_fd_id`),后续金额推进仍走 C108-2,行为兼容;开票/收票新场景上线后只会处理新落库数据(`hcs_perf_fd_id` 为空的历史发票会被补推,如需排除请手工回填标记)。
From 82724ceaa03d1b75f5172693dc9c6f5ac890094d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Thu, 29 Oct 2026 02:15:54 +0800
Subject: [PATCH 32/63] =?UTF-8?q?fix:=20=E6=8B=A6=E6=88=AA=E5=B7=B2?=
=?UTF-8?q?=E8=B0=83=E6=95=B4=E7=9A=84=E6=AD=A3=E5=BC=8F=E7=BB=93=E7=AE=97?=
=?UTF-8?q?=E5=8D=95=E5=86=8D=E6=AC=A1=E5=8F=91=E8=B5=B7=E4=BB=98=E6=AC=BE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../constant/TransportBusiConstant.java | 3 +++
.../impl/FormalSettlementServiceImpl.java | 4 +++-
.../impl/PaymentApplicationServiceImpl.java | 16 ++++++++++++++++
3 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java
index 93f278b8..60a36c71 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java
@@ -61,6 +61,9 @@ public final class TransportBusiConstant {
/** 审批状态:已作废(终态)。 */
public static final String VOIDED = "voided";
+ /** 正式结算单调整标识:已调整(结算调整重推后的归档原单,金额已作废,不可再用于下游业务)。 */
+ public static final String SETTLEMENT_ADJUST_STATUS_ADJUSTED = "adjusted";
+
/**
* 「未分配组织」筛选项的哨兵值,不是真实组织名。
*
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
index 497919f0..0e978b4f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
@@ -790,7 +790,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaUpdate()
.eq(FormalSettlement::getId, settlementId)
- .set(FormalSettlement::getAdjustStatus, "adjusted"));
+ .set(FormalSettlement::getAdjustStatus, TransportBusiConstant.SETTLEMENT_ADJUST_STATUS_ADJUSTED));
kingdeeBillLockService.unlockByAdjustRepush(KingdeeLockConstant.SOURCE_SETTLEMENT, settlementId, AuthUtil.getUserName());
saveChange(settlementId, "结算单基本信息", null, "调整",
"结算调整单" + Func.toStr(adjustmentNo) + "重推生成新单" + Func.toStr(newSettlementNo) + ",原单标记已调整", null);
@@ -1251,6 +1251,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery()
.eq(FormalSettlementPayment::getFormalSettlementId, settlement.getId())
.eq(FormalSettlementPayment::getIsDeleted, 0)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index def5f9a8..cc2ff23d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -299,6 +299,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl !Objects.equals(contractId, item.getContractId()))) {
throw new ServiceException("多选正式结算单必须属于同一合同");
@@ -2206,6 +2209,19 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 1;
From 702e6b12a8f72b81cfbdc799c956afc7f80492f6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Thu, 29 Oct 2026 02:19:11 +0800
Subject: [PATCH 33/63] =?UTF-8?q?fix:=20=E9=A2=84=E7=BB=93=E7=AE=97?=
=?UTF-8?q?=E5=88=97=E8=A1=A8=E6=8C=89=E4=BB=98=E6=AC=BE=E7=94=B3=E8=AF=B7?=
=?UTF-8?q?=E5=85=B3=E8=81=94=E8=A1=8C=E5=BD=92=E9=9B=86=E9=A2=84=E4=BB=98?=
=?UTF-8?q?=E7=94=B3=E8=AF=B7=E4=B8=8E=E5=B7=B2=E4=BB=98=E9=87=91=E9=A2=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
多选预结算单时付款申请主表只存第一张单,原汇总逻辑按主表 pre_settlement_id 归集,
导致第二张及之后单据的预付申请金额恒为 0。改为按 blade_payment_application_settlement
关联行逐单归集,已付金额按关联行顺序分摊,与金蝶预付建档口径一致;无关联行的历史单据仍按主表兜底。
---
.../impl/PreSettlementServiceImpl.java | 62 +++++++++++++++----
1 file changed, 49 insertions(+), 13 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java
index 6b0247ca..20e911d6 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PreSettlementServiceImpl.java
@@ -44,6 +44,7 @@ import org.springblade.transport.mapper.PreSettlementDetailMapper;
import org.springblade.transport.mapper.PreSettlementMapper;
import org.springblade.transport.mapper.PreSettlementSummaryFeeMapper;
import org.springblade.transport.mapper.PaymentApplicationMapper;
+import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
import org.springblade.transport.mapper.ReceivablePayableDetailMapper;
import org.springblade.transport.pojo.dto.PreSettlementAdvanceRequest;
@@ -59,6 +60,7 @@ import org.springblade.transport.pojo.entity.PreSettlementDetail;
import org.springblade.transport.pojo.entity.PreSettlementDetailFee;
import org.springblade.transport.pojo.entity.PreSettlementSummaryFee;
import org.springblade.transport.pojo.entity.PaymentApplication;
+import org.springblade.transport.pojo.entity.PaymentApplicationSettlement;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.ReceivablePayableCargoFee;
import org.springblade.transport.pojo.entity.ReceivablePayableDetail;
@@ -118,6 +120,7 @@ public class PreSettlementServiceImpl extends BaseServiceImpl preSettlementIds = records.stream().map(PreSettlementVO::getId)
.filter(Objects::nonNull).collect(Collectors.toSet());
if (preSettlementIds.isEmpty()) return;
- List applications = paymentApplicationMapper.selectList(
- Wrappers.lambdaQuery()
- .select(PaymentApplication::getPreSettlementId, PaymentApplication::getAppliedAmount,
- PaymentApplication::getPaidAmount)
- .eq(PaymentApplication::getPaymentType, "progress_advance")
- .in(PaymentApplication::getPreSettlementId, preSettlementIds)
- .eq(PaymentApplication::getApprovalStatus, STATUS_APPROVED)
- .eq(PaymentApplication::getIsDeleted, 0));
Map appliedAmounts = new HashMap<>();
Map paidAmounts = new HashMap<>();
- applications.forEach(application -> {
- Long preSettlementId = application.getPreSettlementId();
- appliedAmounts.merge(preSettlementId, money(application.getAppliedAmount()), BigDecimal::add);
- paidAmounts.merge(preSettlementId, money(application.getPaidAmount()), BigDecimal::add);
- });
+ // 多选预结算单时付款申请主表只存第一张,按关联行逐单归集;已付金额按关联行顺序分摊实付金额,
+ // 与同步金蝶预付建档(syncPreSettlementAdvance)同一口径,避免第二张及之后的单据金额恒为0
+ List relations = settlementRelationMapper.selectList(
+ Wrappers.lambdaQuery()
+ .in(PaymentApplicationSettlement::getPreSettlementId, preSettlementIds)
+ .eq(PaymentApplicationSettlement::getIsDeleted, 0)
+ .orderByAsc(PaymentApplicationSettlement::getId));
+ Set relationPaymentIds = relations.stream()
+ .map(PaymentApplicationSettlement::getPaymentApplicationId).filter(Objects::nonNull)
+ .collect(Collectors.toSet());
+ Map relationPayments = relationPaymentIds.isEmpty() ? new HashMap<>()
+ : paymentApplicationMapper.selectBatchIds(relationPaymentIds).stream()
+ .filter(application -> !Objects.equals(application.getIsDeleted(), 1))
+ .filter(application -> TransportBusiConstant.APPROVED.equals(application.getApprovalStatus()))
+ .filter(application -> "progress_advance".equals(application.getPaymentType()))
+ .collect(Collectors.toMap(PaymentApplication::getId, Function.identity(), (left, right) -> left));
+ relations.stream()
+ .filter(relation -> relationPayments.containsKey(relation.getPaymentApplicationId()))
+ .collect(Collectors.groupingBy(PaymentApplicationSettlement::getPaymentApplicationId,
+ LinkedHashMap::new, Collectors.toList()))
+ .forEach((paymentId, paymentRelations) -> {
+ BigDecimal remainingPaid = money(relationPayments.get(paymentId).getPaidAmount());
+ for (int index = 0; index < paymentRelations.size(); index++) {
+ PaymentApplicationSettlement relation = paymentRelations.get(index);
+ BigDecimal applied = money(relation.getAppliedAmount());
+ BigDecimal paid = index == paymentRelations.size() - 1 ? remainingPaid
+ : applied.min(remainingPaid);
+ remainingPaid = remainingPaid.subtract(paid);
+ appliedAmounts.merge(relation.getPreSettlementId(), applied, BigDecimal::add);
+ paidAmounts.merge(relation.getPreSettlementId(), paid, BigDecimal::add);
+ }
+ });
+ // 历史仅写主表、无关联行的付款申请按主表兜底
+ paymentApplicationMapper.selectList(
+ Wrappers.lambdaQuery()
+ .select(PaymentApplication::getId, PaymentApplication::getPreSettlementId,
+ PaymentApplication::getAppliedAmount, PaymentApplication::getPaidAmount)
+ .in(PaymentApplication::getPreSettlementId, preSettlementIds)
+ .eq(PaymentApplication::getPaymentType, "progress_advance")
+ .eq(PaymentApplication::getApprovalStatus, STATUS_APPROVED)
+ .eq(PaymentApplication::getIsDeleted, 0)
+ .notIn(!relationPaymentIds.isEmpty(), PaymentApplication::getId, relationPaymentIds))
+ .forEach(application -> {
+ appliedAmounts.merge(application.getPreSettlementId(), money(application.getAppliedAmount()), BigDecimal::add);
+ paidAmounts.merge(application.getPreSettlementId(), money(application.getPaidAmount()), BigDecimal::add);
+ });
records.forEach(record -> {
record.setAdvanceAppliedAmount(money(appliedAmounts.get(record.getId())));
record.setAdvancePaidAmount(money(paidAmounts.get(record.getId())));
From d7a3c3cbdd1e8afc13deaeb24fa689a6faba78ac Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Thu, 29 Oct 2026 02:19:13 +0800
Subject: [PATCH 34/63] =?UTF-8?q?fix:=20=E4=BB=98=E6=AC=BE=E7=94=B3?=
=?UTF-8?q?=E8=AF=B7=E9=A2=84=E7=BB=93=E7=AE=97=E8=A1=8C=E4=BB=98=E6=AC=BE?=
=?UTF-8?q?=E9=87=91=E9=A2=9D=E5=90=88=E8=AE=A1=E9=A1=BB=E7=AD=89=E4=BA=8E?=
=?UTF-8?q?=E7=94=B3=E8=AF=B7=E4=BB=98=E6=AC=BE=E9=87=91=E9=A2=9D?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
推送金蝶按付款基本信息中的申请付款金额单条入账,明细行金额合计不一致时,
预付记录建档会按行分摊出与金蝶金额不符的结果。保存时校验行合计必须等于申请付款金额。
---
.../service/impl/PaymentApplicationServiceImpl.java | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
index cc2ff23d..5d52c861 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java
@@ -2362,10 +2362,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl lines) {
return lines != null && !lines.isEmpty();
}
From 073a4ee07bc1464a0e0c8d2afd16f73debb380f9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Thu, 29 Oct 2026 10:32:01 +0800
Subject: [PATCH 35/63] =?UTF-8?q?fix:=20=E6=9F=A5=E8=AF=A2=E4=BE=9B?=
=?UTF-8?q?=E5=BA=94=E5=95=86=E4=BF=A1=E6=81=AF=E4=B8=8D=E9=9C=80=E8=A6=81?=
=?UTF-8?q?=E8=BF=87=E6=BB=A4=E7=8A=B6=E6=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../springblade/transport/support/KingdeeApBillSyncSupport.java | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
index 5330abc6..3b144dc4 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java
@@ -1080,7 +1080,7 @@ public class KingdeeApBillSyncSupport {
BfiSupplierQueryDTO param = new BfiSupplierQueryDTO();
param.setPageNo(1);
param.setPageSize(1);
- param.setStatus(BfiConstant.BILL_STATUS_AUDITED);
+ // param.setStatus(BfiConstant.BILL_STATUS_AUDITED);
param.setEnable(BfiConstant.ENABLE_ENABLED);
param.setSocietycreditcode(List.of(unifiedCreditCode));
BfiPageVO page = bfiService.querySupplier(param);
From 3f571449d39fde5361166719e7e4716218c2619b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Thu, 29 Oct 2026 10:57:35 +0800
Subject: [PATCH 36/63] =?UTF-8?q?feat:=20=E4=BB=98=E6=AC=BE=E7=89=B9?=
=?UTF-8?q?=E6=89=B9=E9=80=9A=E8=BF=87=E5=90=8E=E4=B8=8D=E5=86=8D=E6=8E=A8?=
=?UTF-8?q?=E9=80=81=E5=B1=A5=E7=BA=A6=E8=AE=A1=E5=88=92=E4=B8=8E=E5=B1=A5?=
=?UTF-8?q?=E7=BA=A6=E8=AE=B0=E5=BD=95=EF=BC=8C=E7=BB=9F=E4=B8=80=E7=94=B1?=
=?UTF-8?q?=E5=90=84=E4=B8=9A=E5=8A=A1=E7=8E=AF=E8=8A=82=E6=8E=A8=E9=80=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsPaymentSyncSupport.java | 49 ++++++-------------
1 file changed, 14 insertions(+), 35 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
index 2cfb4a75..0b486e78 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsPaymentSyncSupport.java
@@ -50,7 +50,8 @@ import java.util.List;
/**
* HCS 付款链推送编排支撑组件(结算付款特批 C106-3/4 + 履约计划重推 C103-9 + 履约记录 C108-1/2 + 特批结果推送接收,设计文档 §5.0/§5.3/§5.5/§5.6)
*
- * 编排顺序:特批(提交时高风险/黑名单须特批附件或额度超额标记 need 时发起)→ 特批通过后再推履约计划(付款审批款项金额累加行)→ 履约记录(审批通过新建 / 实际付款推进更新)。
+ * 编排顺序:特批(提交时高风险/黑名单须特批附件或额度超额标记 need 时发起)→ 履约计划重推(付款审批款项金额累加行)→ 履约记录(金蝶实际付款后新建/分期推进更新)。
+ * 特批通过后不推履约数据:履约计划与履约记录统一在各业务环节推送(付款=金蝶实际付款后,收款/开票/收票各自环节)。
* 特批审核期间(need/pushing/processing)仅发起/等待 C106,不推送履约计划与履约记录;驳回不产生履约数据。
* 特批流程ID 与履约记录ID 在各自 Support 内即时持久化;整体同步状态(hcs_sync_status)由本组件统一回写。
* 特批发起后 TMS 挂起 currentNode="特批审核",合规 paymentLegal/result 回调联动付款审批状态
@@ -77,7 +78,7 @@ public class HcsPaymentSyncSupport {
private final ApplicationEventPublisher eventPublisher;
/**
- * 推送付款申请到 HCS:特批(need 时发起 C106-3 后挂起)→ 特批通过后履约计划重推(C103-9 款项金额累加行)→ 履约记录(新建/更新)→ 回填整体同步状态
+ * 推送付款申请到 HCS:特批(need 时发起 C106-3 后挂起)→ 履约计划重推(C103-9 款项金额累加行)→ 履约记录(金蝶实际付款后新建/更新)→ 回填整体同步状态
*
* @param payment 付款申请单(approvalStatus=approved)
* @return 推送结果描述
@@ -85,7 +86,7 @@ public class HcsPaymentSyncSupport {
public String pushPayment(PaymentApplication payment) {
StringBuilder result = new StringBuilder();
// ① 结算付款特批:提交时高风险/黑名单(须特批附件)或额度超额(hcs_spec_status=need)且未发起时发起 C106-3;
- // 发起后挂起等待特批结果,履约计划与履约记录由特批通过后的 pushAfterSpecApproved 补推(驳回不产生履约数据)
+ // 发起后挂起等待特批结果,特批通过后不推履约数据(各业务环节推送),仅补推金蝶(驳回不产生履约数据)
if (HcsSyncConstant.SPEC_STATUS_NEED.equals(payment.getHcsSpecStatus())) {
String specResult = hcsSpecPaySyncSupport.pushSpecPay(payment);
// C106-4 更新路径不改状态,统一归一为特批审核中,避免下轮扫描重复发起
@@ -99,18 +100,20 @@ public class HcsPaymentSyncSupport {
// 特批审核中:仅等待合规回调/人工确认,不推送履约计划与履约记录
if (HcsSyncConstant.SPEC_STATUS_PUSHING.equals(payment.getHcsSpecStatus())
|| HcsSyncConstant.SPEC_STATUS_PROCESSING.equals(payment.getHcsSpecStatus())) {
- String message = "付款特批审核中,待特批通过后推送履约数据";
+ String message = "付款特批审核中,履约数据待各业务环节推送";
markSynced(payment, message);
return message;
}
// ② 履约计划重推(C103-9 行级更新):付款审批通过累加款项金额置 row_op=update 的计划行,
// 凭已存合规阶段ID 重推;独立于履约记录开关,推送失败抛出由付款链置 failed 重试兜底
- String stageResult = this.pushExeStageUpdate(payment);
- if (StringUtil.isNotBlank(stageResult)) {
- if (result.length() > 0) {
- result.append(";");
+ if (hcsProperties.getExeStage().isSyncEnabled()) {
+ String stageResult = this.pushExeStageUpdate(payment);
+ if (StringUtil.isNotBlank(stageResult)) {
+ if (result.length() > 0) {
+ result.append(";");
+ }
+ result.append(stageResult);
}
- result.append(stageResult);
}
// ③ 履约记录:金蝶实际付款后推送(需求口径:金蝶实际付款了再推给合同系统)——
// 首次回写实际付款走 C108-1 新建,分期付款推进(paidAmount 变化置待同步)走 C108-2 更新;
@@ -154,24 +157,6 @@ public class HcsPaymentSyncSupport {
return "履约规则无待同步行".equals(stageResult) ? "" : stageResult;
}
- /**
- * 特批通过后补推履约数据(C103-9 计划重推 + C108 履约记录):补推失败置 failed 留待人工重推兜底,
- * 不阻断特批结果落库与金蝶补推
- *
- * @param payment 付款申请单(hcs_spec_status=approved)
- */
- private void pushAfterSpecApproved(PaymentApplication payment) {
- try {
- this.pushPayment(payment);
- } catch (Exception exception) {
- String message = StringUtil.isBlank(exception.getMessage()) ? "履约数据推送异常" : exception.getMessage();
- markFailed(payment, message);
- log.error("付款特批通过后履约数据推送失败:paymentNo={},{}", payment.getPaymentNo(), message, exception);
- // todo
- throw new ServiceException("付款特批通过后履约数据推送失败:" + message);
- }
- }
-
/**
* 单条手动重推:重置为待同步并清零重试计数后立即推送(页面"推送合规/重推"按钮入口)
*
@@ -220,10 +205,7 @@ public class HcsPaymentSyncSupport {
syncPaymentOnSpecResult(payment, HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result));
payment.setHcsSpecStatus(result);
paymentApplicationMapper.updateById(payment);
- // 特批通过后补推履约计划与履约记录(驳回不产生履约数据),失败不阻断后续金蝶补推
- if (HcsSyncConstant.SPEC_STATUS_APPROVED.equals(result)) {
- this.pushAfterSpecApproved(payment);
- }
+ // 特批通过后不推履约数据:履约计划与履约记录统一在各业务环节(付款=金蝶实际付款后)推送
publishKingdeeAutoSyncOnSpecApproved(payment, result);
log.info("HCS特批结果人工确认:paymentNo={},specFdId={},结果={}", payment.getPaymentNo(), payment.getHcsSpecFdId(), result);
return "特批结果已确认:" + result;
@@ -269,10 +251,7 @@ public class HcsPaymentSyncSupport {
payment.setHcsSpecAttachFdId(param.getFileUrl().trim());
}
paymentApplicationMapper.updateById(payment);
- // 特批通过后补推履约计划与履约记录(驳回不产生履约数据),失败不阻断后续金蝶补推
- if (HcsSyncConstant.SPEC_STATUS_APPROVED.equals(status)) {
- this.pushAfterSpecApproved(payment);
- }
+ // 特批通过后不推履约数据:履约计划与履约记录统一在各业务环节(付款=金蝶实际付款后)推送
publishKingdeeAutoSyncOnSpecApproved(payment, status);
log.info("付款特批结果推送处理完成:paymentNo={},结果={},reason={}", payment.getPaymentNo(), status, param.getReason());
return "特批结果已处理:" + status;
From 4d160309999a1e08e17c10179c9b189cd23e2cd7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Thu, 29 Oct 2026 11:04:10 +0800
Subject: [PATCH 37/63] =?UTF-8?q?docs:=20=E5=90=8C=E6=AD=A5=E4=BB=98?=
=?UTF-8?q?=E6=AC=BE=E7=89=B9=E6=89=B9=E9=80=9A=E8=BF=87=E5=90=8E=E4=B8=8D?=
=?UTF-8?q?=E6=8E=A8=E5=B1=A5=E7=BA=A6=E6=95=B0=E6=8D=AE=E7=9A=84=E5=8F=A3?=
=?UTF-8?q?=E5=BE=84=E4=B8=8E=E9=AA=8C=E8=AF=81=E6=AD=A5=E9=AA=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...四场景推送改造流程梳理(收款-付款-开票-收票).md | 23 +++++++++++--------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md b/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md
index d60a406c..f47d8675 100644
--- a/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md
+++ b/doc/docs/hcs合同/2026-10-28-C108履约记录四场景推送改造流程梳理(收款-付款-开票-收票).md
@@ -28,7 +28,7 @@ C108-1(履约记录新建)、C108-2(履约记录更新)两个接口需
| 场景 | 改造前 | 改造后 |
|------|--------|--------|
| 收款 | 已实现(C108-1),履约日期=最近认领日期 | 履约日期改为交易日期;组装逻辑收敛到通用组件 |
-| 付款 | 审批通过即 C108-1 新建,实际付款后 C108-2 更新 | 未实际付款不推送;首次实际付款 C108-1 新建;分期付款推进 C108-2 更新 |
+| 付款 | 审批通过即 C108-1 新建,特批通过后补推履约计划与履约记录,实际付款后 C108-2 更新 | **特批通过后不推任何履约数据**;未实际付款不推送;首次实际付款 C108-1 新建;分期付款推进 C108-2 更新 |
| 开票 | 无(仅 C111 开票登记) | 新增:金蝶回写发票逐张推 C108-1,发票粒度幂等+重试 |
| 收票 | 无(仅 C109 收票单) | 新增:每条结算分摊行推 C108-1,分摊行粒度幂等+重试,且限 C109 已推送 |
@@ -87,14 +87,16 @@ C108-1(履约记录新建)、C108-2(履约记录更新)两个接口需
### 4.2 履约付款记录(对应付款记录,金蝶实际付款了再推)
```
-付款申请审批通过(特批通过后)
- → HCS 推送链:C106-3 特批(如需)→ C103-9 履约计划重推
- → C108 履约记录环节:paidAmount 为空/0 → 跳过(提示"金蝶未实际付款,待付款后推送履约记录")★改
- 整体状态照常置 synced
+付款申请审批通过
+ → 如需特批:发起 C106-3 挂起,特批通过后仅联动审批恢复 + 补推金蝶
+ ★改:特批通过后不推履约计划与履约记录(履约数据统一在各业务环节推送)
+ → 审批通过即时推送链不产生履约付款记录(paidAmount 为空/0 跳过,
+ 提示"金蝶未实际付款,待付款后推送履约记录",整体状态照常置 synced)
金蝶实际付款 → 定时任务 A103-2 付款结果回写(applyPayApplyResult)
→ paidAmount 首次 >0 或金额变化 → hcs_sync_status 置 unsynced ★改(原仅在有 fdId 时置)
→ HcsPushTask 付款任务下轮扫描 → pushPayment
+ C103-9 履约计划重推(付款审批累加金额行,exe-stage 开关门禁)
无 hcs_perf_fd_id → C108-1 新建(申请金额 + 实际金额)→ fdId 回填 hcs_perf_fd_id
有 hcs_perf_fd_id → C108-2 更新(分期付款推进实际金额)
```
@@ -219,11 +221,12 @@ HcsPushTask 开票任务(每5分钟):
**场景 2|履约付款记录(金蝶实际付款了再推)**
1. 付款申请审批通过并推金蝶(`POST /payment-application/sync-kingdee`);此时**不应**产生履约付款记录——日志出现 `金蝶未实际付款,待付款后推送履约记录`,HCS 侧无记录。
-2. 在金蝶完成实际付款后,手动回写:`POST /payment-application/sync-kingdee-result`。
-3. 数据库:`SELECT paid_amount, hcs_sync_status FROM blade_payment_application WHERE id=?` → paid_amount>0 且 hcs_sync_status=unsynced。
-4. 手动扫描:`POST /hcs/sync/payment` → 日志 `HCS履约记录新建成功`(C108-1)→ 数据库 `hcs_perf_fd_id` 回填。
-5. 分期付款(金蝶再次付款)→ 再触发回写 → 金额变化置 unsynced → 再扫描 → 日志 `HCS履约记录更新成功`(C108-2);HCS 侧 fd_fact_amount 与最新 paid_amount 一致。
-6. 反向验证:`paying` 状态下连续两轮回写但金额不变 → 不置 unsynced、不重复推送。
+2. 特批场景(高风险/黑名单收款方或额度超额):提交后发起 C106-3 挂起;特批通过(回调 `paymentLegal/result` 或人工确认 `POST /hcs/spec-pay/confirm`)后,核对**无任何履约计划重推与履约记录推送动作**(仅审批恢复 + 金蝶补推),HCS 侧仍无履约付款记录。
+3. 在金蝶完成实际付款后,手动回写:`POST /payment-application/sync-kingdee-result`。
+4. 数据库:`SELECT paid_amount, hcs_sync_status FROM blade_payment_application WHERE id=?` → paid_amount>0 且 hcs_sync_status=unsynced。
+5. 手动扫描:`POST /hcs/sync/payment` → 日志 `HCS履约记录新建成功`(C108-1)→ 数据库 `hcs_perf_fd_id` 回填。
+6. 分期付款(金蝶再次付款)→ 再触发回写 → 金额变化置 unsynced → 再扫描 → 日志 `HCS履约记录更新成功`(C108-2);HCS 侧 fd_fact_amount 与最新 paid_amount 一致。
+7. 反向验证:`paying` 状态下连续两轮回写但金额不变 → 不置 unsynced、不重复推送。
**场景 3|履约开票记录**
From 3f23baccd80fb5737e28beee24ed6f7f1b373753 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Thu, 29 Oct 2026 18:17:13 +0800
Subject: [PATCH 38/63] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=A4=9A?=
=?UTF-8?q?=E5=BC=A0=E5=8F=91=E7=A5=A8=E5=BC=80=E7=A5=A8=E7=BB=93=E6=9E=9C?=
=?UTF-8?q?=E9=83=A8=E5=88=86=E5=9B=9E=E5=86=99=E5=8D=B3=E6=8E=A8=E8=BF=9B?=
=?UTF-8?q?=E5=BC=80=E7=A5=A8=E5=AE=8C=E6=88=90=E5=AF=BC=E8=87=B4=E5=89=A9?=
=?UTF-8?q?=E4=BD=99=E5=BC=A0=E6=AC=A1=E4=B8=8D=E5=86=8D=E6=9F=A5=E8=AF=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../impl/InvoiceApplicationServiceImpl.java | 44 ++++++++++++-------
1 file changed, 28 insertions(+), 16 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
index 46af949b..4865f443 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
@@ -1158,7 +1158,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl applicationBySerialNo = new LinkedHashMap<>();
+ Map> serialNosByApplicationId = new LinkedHashMap<>();
applications.forEach(application -> {
if (Func.isNotBlank(application.getKingdeeBillNo())) {
// 多张发票时申请单金蝶单号逗号拼接存储,拆分为逐张单号分别归集回写
for (String serialNo : application.getKingdeeBillNo().split(",")) {
if (Func.isNotBlank(serialNo)) {
applicationBySerialNo.putIfAbsent(serialNo.trim(), application);
+ serialNosByApplicationId.computeIfAbsent(application.getId(), key -> new ArrayList<>()).add(serialNo.trim());
}
}
}
});
+ // 已有开票结果的单号归集(含历史已落库),全部张次齐备才推进开票完成
+ Set serialNosWithResult = new LinkedHashSet<>();
Map sellerCache = new LinkedHashMap<>();
Map> serialNosBySellerTaxNo = new LinkedHashMap<>();
int unresolved = 0;
@@ -1211,7 +1216,6 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl> group : serialNosBySellerTaxNo.entrySet()) {
List groupSerialNos = group.getValue();
for (int from = 0; from < groupSerialNos.size(); from += INVOICE_RESULT_QUERY_BATCH) {
@@ -1232,10 +1236,11 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl existingInvoiceMap = batchQueryExistingInvoices(batch);
log.debug("金蝶开票结果回写:批次已存在发票记录数={}", existingInvoiceMap.size());
- // 收集待插入的发票记录、待补文件地址的已有发票记录和待更新的开票申请
+ // 已落库记录对应的单号视为已有开票结果(跨批次幂等归集)
+ existingInvoiceMap.values().forEach(record -> serialNosWithResult.add(Func.toStr(record.getSerialNo()).trim()));
+ // 收集待插入的发票记录和待补文件地址的已有发票记录
List toInsertInvoices = new ArrayList<>();
List toPatchInvoices = new ArrayList<>();
- List toUpdateApplications = new ArrayList<>();
for (BfiInvoiceApplyOpenApiBillVO bill : result.getData()) {
String billNo = bill.getBillNo();
InvoiceApplication application = applicationBySerialNo.get(billNo);
@@ -1277,13 +1282,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl 0) {
invoiceWritten += inserted;
- // 查到发票:开票申请单推进开票完成(终态)
- application.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_INVOICED);
- // HCS 开票登记待更新:扫描任务凭 hcs_fd_id 走 C111-3 回填票据日期(D12)
- application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
- toUpdateApplications.add(application);
- invoicedCount++;
- log.info("金蝶开票结果回写:开票申请[{}]状态推进为invoiced,本次写入发票{}张", application.getApplicationNo(), inserted);
+ serialNosWithResult.add(billNo);
+ log.info("金蝶开票结果回写:单据编号[{}]写入发票{}张", billNo, inserted);
}
}
// 批量插入发票记录
@@ -1296,17 +1296,29 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl toCompleteApplications = new ArrayList<>();
+ for (InvoiceApplication application : applications) {
+ List applicationSerialNos = serialNosByApplicationId.get(application.getId());
+ if (Func.isEmpty(applicationSerialNos) || !serialNosWithResult.containsAll(applicationSerialNos)) {
+ continue;
+ }
+ application.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_INVOICED);
+ // HCS 开票登记待更新:扫描任务凭 hcs_fd_id 走 C111-3 回填票据日期(D12)
+ application.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
+ toCompleteApplications.add(application);
+ }
+ int invoicedCount = toCompleteApplications.size();
+ if (!toCompleteApplications.isEmpty()) {
+ updateBatchById(toCompleteApplications);
+ log.info("金蝶开票结果回写:批量推进开票完成{}笔", invoicedCount);
+ }
log.info("金蝶开票结果回写完成:待查开票申请{}笔(销方税号无法解析跳过{}笔),回写发票{}张(开票完成{}笔,失败批次{}个)",
applications.size(), unresolved, invoiceWritten, invoicedCount, failed);
return "金蝶开票结果回写完成:待查开票申请" + applications.size() + "笔(销方税号无法解析跳过" + unresolved
From 6e5b6958eae028d5ba5cbf4a75096b69efe16930 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Thu, 29 Oct 2026 18:17:23 +0800
Subject: [PATCH 39/63] =?UTF-8?q?feat:=20=E5=BC=80=E7=A5=A8=E7=94=B3?=
=?UTF-8?q?=E8=AF=B7=E6=9F=A5=E7=9C=8B=E5=A2=9E=E5=8A=A0=E6=AF=8F=E5=BC=A0?=
=?UTF-8?q?=E5=8F=91=E7=A5=A8=E9=87=91=E8=9D=B6=E5=BC=80=E7=A5=A8=E7=8A=B6?=
=?UTF-8?q?=E6=80=81=E4=B8=8E=E7=BB=93=E6=9E=9C=E5=A4=87=E6=B3=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/vo/InvoiceApplicationSheetVO.java | 5 +++
.../impl/InvoiceApplicationServiceImpl.java | 40 +++++++++++++++++--
2 files changed, 42 insertions(+), 3 deletions(-)
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationSheetVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationSheetVO.java
index 0b0dd2f8..0b35cd16 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationSheetVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationSheetVO.java
@@ -26,6 +26,7 @@
package org.springblade.transport.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
+import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.InvoiceApplicationLine;
@@ -44,4 +45,8 @@ import java.util.List;
public class InvoiceApplicationSheetVO extends InvoiceApplicationSheet {
@Serial private static final long serialVersionUID = 1L;
@TableField(exist = false) private List lines;
+ @Schema(description = "开票状态名称(按金蝶回写的发票结果实时推导:已开票/未开票)")
+ @TableField(exist = false) private String invoiceStatusName;
+ @Schema(description = "金蝶开票结果备注(本张发票经 A303-5 回写的发票号、开票日期、价税合计汇总)")
+ @TableField(exist = false) private String kingdeeResultRemark;
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
index 4865f443..3048c8bf 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
@@ -138,6 +138,9 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl sheets = sheetMapper.selectList(Wrappers.lambdaQuery()
.eq(InvoiceApplicationSheet::getInvoiceApplicationId, id).orderByAsc(InvoiceApplicationSheet::getSheetNo));
+ List invoices = invoiceApplicationInvoiceMapper.selectList(
+ Wrappers.lambdaQuery()
+ .eq(InvoiceApplicationInvoice::getInvoiceApplicationId, id)
+ .orderByAsc(InvoiceApplicationInvoice::getCreateTime));
+ vo.setInvoices(invoices);
+ // 每张发票的金蝶开票结果:按张次金蝶单号归集,查看页展示开票状态与结果备注
+ Map> invoicesBySerialNo = invoices.stream()
+ .filter(invoice -> Func.isNotBlank(invoice.getSerialNo()))
+ .collect(Collectors.groupingBy(invoice -> Func.toStr(invoice.getSerialNo()).trim()));
vo.setSheets(sheets.stream().map(sheet -> {
InvoiceApplicationSheetVO sheetVO = Objects.requireNonNull(BeanUtil.copyProperties(sheet, InvoiceApplicationSheetVO.class));
sheetVO.setLines(lineMapper.selectList(Wrappers.lambdaQuery()
.eq(InvoiceApplicationLine::getInvoiceSheetId, sheet.getId()).orderByAsc(InvoiceApplicationLine::getLineNo)));
+ List sheetInvoices =
+ invoicesBySerialNo.get(Func.toStr(sheet.getKingdeeBillNo()).trim());
+ sheetVO.setInvoiceStatusName(Func.isEmpty(sheetInvoices) ? INVOICE_STATUS_UNINVOICED : INVOICE_STATUS_INVOICED);
+ sheetVO.setKingdeeResultRemark(Func.isEmpty(sheetInvoices) ? "" : kingdeeResultRemark(sheetInvoices));
return sheetVO;
}).toList());
vo.setDetails(applicationDetailMapper.selectList(Wrappers.lambdaQuery()
@@ -262,9 +278,6 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpllambdaQuery()
.eq(InvoiceApplicationRecord::getInvoiceApplicationId, id)
.orderByAsc(InvoiceApplicationRecord::getCreateTime)));
- vo.setInvoices(invoiceApplicationInvoiceMapper.selectList(Wrappers.lambdaQuery()
- .eq(InvoiceApplicationInvoice::getInvoiceApplicationId, id)
- .orderByAsc(InvoiceApplicationInvoice::getCreateTime)));
return vo;
}
@@ -1340,6 +1353,27 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl left));
}
+ /**
+ * 汇总本张发票经 A303-5 回写的金蝶开票结果备注(发票号、开票日期、价税合计)
+ *
+ * @param sheetInvoices 本张发票的金蝶发票结果
+ * @return 结果备注
+ */
+ private String kingdeeResultRemark(List sheetInvoices) {
+ return sheetInvoices.stream()
+ .map(invoice -> {
+ StringBuilder remark = new StringBuilder("发票号").append(Func.toStr(invoice.getInvoiceNo()));
+ if (invoice.getInvoiceDate() != null) {
+ remark.append(",开票日期").append(invoice.getInvoiceDate().toLocalDate());
+ }
+ if (invoice.getInvoiceAmount() != null) {
+ remark.append(",价税合计").append(invoice.getInvoiceAmount());
+ }
+ return remark.toString();
+ })
+ .collect(Collectors.joining(";"));
+ }
+
/**
* 将 A303-5 返回的发票文件地址补齐到已落库记录(仅补空字段,不覆盖已有值)
*
From 91d94f7ac008d83c9cee275e8b8fb8edea9d8f9b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 11:33:04 +0800
Subject: [PATCH 40/63] =?UTF-8?q?feat:=20=E5=AE=A2=E5=95=86=E6=A1=A3?=
=?UTF-8?q?=E6=A1=88=E6=96=B0=E5=A2=9E=E5=90=88=E8=A7=84=E5=AE=A2=E5=95=86?=
=?UTF-8?q?=E9=A3=8E=E9=99=A9=E4=B8=8E=E9=87=91=E8=9D=B6=E9=BB=91=E5=90=8D?=
=?UTF-8?q?=E5=8D=95=E8=90=BD=E5=BA=93=E5=AD=97=E6=AE=B5=E5=8F=8A=E5=88=97?=
=?UTF-8?q?=E8=A1=A8=E7=AD=9B=E9=80=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/CustomerArchive.java | 15 ++++
.../transport/pojo/vo/CustomerArchiveVO.java | 8 ++
.../mapper/CustomerArchiveMapper.xml | 27 ++++++
doc/sql/transport/blade_customer_archive.sql | 9 +-
...ade_customer_archive_hcs_risk_20261030.sql | 73 +++++++++++++++
.../03_blade_customer_risk_dict_20261030.sql | 89 +++++++++++++++++++
6 files changed, 220 insertions(+), 1 deletion(-)
create mode 100644 doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
create mode 100644 doc/sql/transport/lqj/temp/03_blade_customer_risk_dict_20261030.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
index 242d0f65..54987c07 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
@@ -182,4 +182,19 @@ public class CustomerArchive extends TenantEntity {
@Schema(description = "金蝶推送时间")
private LocalDateTime kingdeeSyncTime;
+ @Schema(description = "合规客商风险等级(C101-1,每日定时刷新):0=低风险,1=中风险,2=高风险")
+ private String hcsRiskLevel;
+
+ @Schema(description = "合规客商黑名单状态(C101-1,每日定时刷新):yes=是,no=否,handing=处理中")
+ private String hcsBlackStatus;
+
+ @Schema(description = "合规客商风险最近查询时间(C101-1)")
+ private LocalDateTime hcsRiskTime;
+
+ @Schema(description = "金蝶黑名单失信类别(A801-5 awrp_combofield_title,缺失兜底 awrp_combofield)")
+ private String kingdeeBlackCategory;
+
+ @Schema(description = "金蝶黑名单列入事由(A801-5 awrp_textareafield)")
+ private String kingdeeBlackReason;
+
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java
index 5e924aec..807d5a72 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java
@@ -106,4 +106,12 @@ public class CustomerArchiveVO extends CustomerArchive {
@Schema(description = "已付金额合计(元)")
private BigDecimal usedFundLimit;
+ @TableField(exist = false)
+ @Schema(description = "客商风险筛选值:blacklist黑名单/high高风险/middle中风险/low低风险")
+ private String customerRisk;
+
+ @TableField(exist = false)
+ @Schema(description = "客商风险明细(后台组装:合同系统风险等级+金蝶黑名单失信类别/列入事由)")
+ private String hcsRiskDetail;
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
index 2c04f248..fd345444 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
@@ -54,6 +54,11 @@
+
+
+
+
+
@@ -108,6 +113,11 @@
kingdee_status,
kingdee_fail_reason,
kingdee_sync_time,
+ hcs_risk_level,
+ hcs_black_status,
+ hcs_risk_time,
+ kingdee_black_category,
+ kingdee_black_reason,
(
SELECT s.review_status
FROM blade_customer_credit_score s
@@ -171,6 +181,23 @@
AND create_time <= #{customer.createTimeEnd}
+
+
+
+
+ AND hcs_black_status = 'yes'
+
+
+ AND hcs_risk_level = '2' AND (hcs_black_status IS NULL OR hcs_black_status != 'yes')
+
+
+ AND hcs_risk_level = '1' AND (hcs_black_status IS NULL OR hcs_black_status != 'yes')
+
+
+ AND hcs_risk_level = '0' AND (hcs_black_status IS NULL OR hcs_black_status != 'yes')
+
+
+
ORDER BY create_time DESC
diff --git a/doc/sql/transport/blade_customer_archive.sql b/doc/sql/transport/blade_customer_archive.sql
index 0cec14d9..0350134b 100644
--- a/doc/sql/transport/blade_customer_archive.sql
+++ b/doc/sql/transport/blade_customer_archive.sql
@@ -46,6 +46,11 @@ CREATE TABLE `blade_customer_archive` (
`kingdee_status` varchar(20) NOT NULL DEFAULT 'none' COMMENT '金蝶协同状态:none未推送/exist金蝶已有/synced已推送/failed推送失败',
`kingdee_fail_reason` varchar(500) NULL DEFAULT NULL COMMENT '金蝶推送失败原因',
`kingdee_sync_time` datetime NULL DEFAULT NULL COMMENT '金蝶推送时间',
+ `hcs_risk_level` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合规客商风险等级(C101-1,0=低风险/1=中风险/2=高风险)',
+ `hcs_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合规客商黑名单状态(C101-1,yes=是/no=否/handing=处理中)',
+ `hcs_risk_time` datetime DEFAULT NULL COMMENT '客商风险最近查询时间(C101-1)',
+ `kingdee_black_category` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '金蝶黑名单失信类别(A801-5 awrp_combofield_title,缺失兜底 awrp_combofield)',
+ `kingdee_black_reason` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '金蝶黑名单列入事由(A801-5 awrp_textareafield)',
`create_user` bigint NULL DEFAULT NULL COMMENT '创建人',
`create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
@@ -58,7 +63,9 @@ CREATE TABLE `blade_customer_archive` (
UNIQUE KEY `uk_customer_archive_credit_code` (`tenant_id`, `unified_credit_code`, `is_deleted`) USING BTREE,
KEY `idx_customer_archive_access` (`tenant_id`, `access_type`, `approval_status`) USING BTREE,
KEY `idx_customer_archive_name` (`tenant_id`, `full_name`, `short_name`) USING BTREE,
- KEY `idx_kingdee_status` (`kingdee_status`) USING BTREE
+ KEY `idx_kingdee_status` (`kingdee_status`) USING BTREE,
+ KEY `idx_hcs_risk_level` (`hcs_risk_level`) USING BTREE,
+ KEY `idx_hcs_black_status` (`hcs_black_status`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客商档案';
-- ----------------------------
diff --git a/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql b/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
new file mode 100644
index 00000000..f67ca273
--- /dev/null
+++ b/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
@@ -0,0 +1,73 @@
+-- 客商档案:合规客商风险(C101-1)落库字段
+-- 用途:每日定时刷新合同期限内生效合同关联客商的风险等级/黑名单状态并落库,支持列表按客商风险筛选
+-- 口径:内部组织客商默认无风险不落值;筛选值映射 blacklist=黑名单/high=高风险/middle=中风险/low=低风险
+-- 幂等可重复执行:逐列/逐索引查 information_schema,已存在则跳过
+
+-- ① 列 hcs_risk_level
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND COLUMN_NAME = 'hcs_risk_level') = 0,
+ 'ALTER TABLE `blade_customer_archive` ADD COLUMN `hcs_risk_level` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规客商风险等级(C101-1,0=低风险/1=中风险/2=高风险)'' AFTER `kingdee_sync_time`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ② 列 hcs_black_status
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND COLUMN_NAME = 'hcs_black_status') = 0,
+ 'ALTER TABLE `blade_customer_archive` ADD COLUMN `hcs_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规客商黑名单状态(C101-1,yes=是/no=否/handing=处理中)'' AFTER `hcs_risk_level`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ③ 列 hcs_risk_time
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND COLUMN_NAME = 'hcs_risk_time') = 0,
+ 'ALTER TABLE `blade_customer_archive` ADD COLUMN `hcs_risk_time` datetime DEFAULT NULL COMMENT ''客商风险最近查询时间(C101-1)'' AFTER `hcs_black_status`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ④ 索引 idx_hcs_risk_level
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND INDEX_NAME = 'idx_hcs_risk_level') = 0,
+ 'ALTER TABLE `blade_customer_archive` ADD INDEX idx_hcs_risk_level (hcs_risk_level)',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ⑤ 索引 idx_hcs_black_status
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND INDEX_NAME = 'idx_hcs_black_status') = 0,
+ 'ALTER TABLE `blade_customer_archive` ADD INDEX idx_hcs_black_status (hcs_black_status)',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ⑥ 列 hcs_black_category / hcs_black_reason(A801-5 金蝶黑名单原始字段:失信类别、列入事由,明细文案由后台组装)
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND COLUMN_NAME = 'kingdee_black_category') = 0,
+ 'ALTER TABLE `blade_customer_archive` ADD COLUMN `kingdee_black_category` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶黑名单失信类别(A801-5 awrp_combofield_title,缺失兜底 awrp_combofield)'' AFTER `hcs_risk_time`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND COLUMN_NAME = 'kingdee_black_reason') = 0,
+ 'ALTER TABLE `blade_customer_archive` ADD COLUMN `kingdee_black_reason` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''金蝶黑名单列入事由(A801-5 awrp_textareafield)'' AFTER `kingdee_black_category`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git a/doc/sql/transport/lqj/temp/03_blade_customer_risk_dict_20261030.sql b/doc/sql/transport/lqj/temp/03_blade_customer_risk_dict_20261030.sql
new file mode 100644
index 00000000..1cfeb626
--- /dev/null
+++ b/doc/sql/transport/lqj/temp/03_blade_customer_risk_dict_20261030.sql
@@ -0,0 +1,89 @@
+-- 客商风险筛选字典 customer_risk(客商档案列表搜索栏「客商风险」下拉)。
+-- dict_key=筛选值(与后端列表筛选入参一致:blacklist/high/middle/low),dict_value=展示名称。
+-- 字典缺失时前端保留固定兜底选项,落库后各环境与前端兜底保持一致;id 由 UUID_SHORT() 动态生成,不写死,幂等可重复执行。
+
+-- ① 字典父项
+INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
+SELECT UUID_SHORT(), '000000', 0, 'customer_risk', '-1', '客商风险', 1, NULL, 0, 1, 0
+WHERE NOT EXISTS (
+ SELECT 1 FROM blade_dict_biz
+ WHERE code = 'customer_risk' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0
+);
+
+-- ② 子项:黑名单
+INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
+SELECT UUID_SHORT(), '000000',
+ (SELECT id FROM blade_dict_biz WHERE code = 'customer_risk' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1),
+ 'customer_risk', 'blacklist', '黑名单', 1, NULL, 0, 1, 0
+WHERE NOT EXISTS (
+ SELECT 1 FROM blade_dict_biz
+ WHERE code = 'customer_risk' AND dict_key = 'blacklist' AND tenant_id = '000000' AND is_deleted = 0
+);
+
+-- ③ 子项:高风险
+INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
+SELECT UUID_SHORT(), '000000',
+ (SELECT id FROM blade_dict_biz WHERE code = 'customer_risk' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1),
+ 'customer_risk', 'high', '高风险', 2, NULL, 0, 1, 0
+WHERE NOT EXISTS (
+ SELECT 1 FROM blade_dict_biz
+ WHERE code = 'customer_risk' AND dict_key = 'high' AND tenant_id = '000000' AND is_deleted = 0
+);
+
+-- ④ 子项:中风险
+INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
+SELECT UUID_SHORT(), '000000',
+ (SELECT id FROM blade_dict_biz WHERE code = 'customer_risk' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1),
+ 'customer_risk', 'middle', '中风险', 3, NULL, 0, 1, 0
+WHERE NOT EXISTS (
+ SELECT 1 FROM blade_dict_biz
+ WHERE code = 'customer_risk' AND dict_key = 'middle' AND tenant_id = '000000' AND is_deleted = 0
+);
+
+-- ⑤ 子项:低风险
+INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
+SELECT UUID_SHORT(), '000000',
+ (SELECT id FROM blade_dict_biz WHERE code = 'customer_risk' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1),
+ 'customer_risk', 'low', '低风险', 4, NULL, 0, 1, 0
+WHERE NOT EXISTS (
+ SELECT 1 FROM blade_dict_biz
+ WHERE code = 'customer_risk' AND dict_key = 'low' AND tenant_id = '000000' AND is_deleted = 0
+);
+
+-- ⑥ 客商风险等级字典 risk_level(C101-1 风险等级翻译,dictKey=等级 0/1/2):父项
+INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
+SELECT UUID_SHORT(), '000000', 0, 'risk_level', '-1', '客商风险等级', 1, NULL, 0, 1, 0
+WHERE NOT EXISTS (
+ SELECT 1 FROM blade_dict_biz
+ WHERE code = 'risk_level' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0
+);
+
+-- ⑦ 子项:低风险(0)
+INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
+SELECT UUID_SHORT(), '000000',
+ (SELECT id FROM blade_dict_biz WHERE code = 'risk_level' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1),
+ 'risk_level', '0', '低风险', 1, NULL, 0, 1, 0
+WHERE NOT EXISTS (
+ SELECT 1 FROM blade_dict_biz
+ WHERE code = 'risk_level' AND dict_key = '0' AND tenant_id = '000000' AND is_deleted = 0
+);
+
+-- ⑧ 子项:中风险(1)
+INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
+SELECT UUID_SHORT(), '000000',
+ (SELECT id FROM blade_dict_biz WHERE code = 'risk_level' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1),
+ 'risk_level', '1', '中风险', 2, NULL, 0, 1, 0
+WHERE NOT EXISTS (
+ SELECT 1 FROM blade_dict_biz
+ WHERE code = 'risk_level' AND dict_key = '1' AND tenant_id = '000000' AND is_deleted = 0
+);
+
+-- ⑨ 子项:高风险(2)
+INSERT INTO blade_dict_biz (id, tenant_id, parent_id, code, dict_key, dict_value, sort, remark, is_sealed, status, is_deleted)
+SELECT UUID_SHORT(), '000000',
+ (SELECT id FROM blade_dict_biz WHERE code = 'risk_level' AND dict_key = '-1' AND tenant_id = '000000' AND is_deleted = 0 LIMIT 1),
+ 'risk_level', '2', '高风险', 3, NULL, 0, 1, 0
+WHERE NOT EXISTS (
+ SELECT 1 FROM blade_dict_biz
+ WHERE code = 'risk_level' AND dict_key = '2' AND tenant_id = '000000' AND is_deleted = 0
+);
From 2e650b84ffa6197b6ce6d0385d9292074ab61d7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 11:33:15 +0800
Subject: [PATCH 41/63] =?UTF-8?q?feat:=20=E5=AE=A2=E5=95=86=E9=A3=8E?=
=?UTF-8?q?=E9=99=A9=E6=AF=8F=E6=97=A5=E5=AE=9A=E6=97=B6=E5=88=B7=E6=96=B0?=
=?UTF-8?q?=E4=BB=BB=E5=8A=A1=EF=BC=88C101-1=E9=A3=8E=E9=99=A9=E7=AD=89?=
=?UTF-8?q?=E7=BA=A7=E4=B8=8EA801-5=E9=87=91=E8=9D=B6=E9=BB=91=E5=90=8D?=
=?UTF-8?q?=E5=8D=95=E6=8C=89200=E5=AE=A2=E5=95=86=E5=88=86=E6=89=B9?=
=?UTF-8?q?=E8=90=BD=E5=BA=93=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/constant/HcsSyncConstant.java | 22 ++
.../support/hcs/HcsCreditCheckSupport.java | 290 ++++++++++++++++--
.../task/CustomerRiskRefreshTask.java | 72 +++++
.../springblade/hcs/config/HcsProperties.java | 5 +
doc/nacos/third-party-api.yaml | 4 +
5 files changed, 361 insertions(+), 32 deletions(-)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index 13069ebd..7f870d9a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -25,6 +25,8 @@
*/
package org.springblade.transport.constant;
+import java.util.List;
+
/**
* HCS(合同合规系统)同步常量(TMS 业务侧)
*
@@ -175,6 +177,26 @@ public final class HcsSyncConstant {
*/
public static final String CUSTOMER_KIND_INTERNAL = "internal";
+ /**
+ * 合同审核通过状态集合(首推/变更通过,客商风险定时刷新扫描口径与 HcsPushTask 一致)
+ */
+ public static final List CONTRACT_APPROVED_STATUSES = List.of("approved", "change_approved");
+
+ /**
+ * 合同已终止当前节点展示名(客商风险定时刷新排除已终止合同)
+ */
+ public static final String CONTRACT_TERMINATED_NODE = "已终止";
+
+ /**
+ * 客商风险等级业务字典编码(dictKey=C101-1 风险等级 0/1/2,dictValue=低风险/中风险/高风险)
+ */
+ public static final String RISK_LEVEL_DICT_CODE = "risk_level";
+
+ /**
+ * 客商风险定时刷新批量(200 客商/批:批内黑名单一次查询 + C101-1 逐个查询,查完立即提交数据库,批间互不影响)
+ */
+ public static final int CUSTOMER_RISK_REFRESH_BATCH = 200;
+
/**
* 合同附件类型字段名(与前端附件元数据 fileType 字段对齐,值取业务字典 contract_attachment_type)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
index 2b158e4e..4c74ac7c 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
@@ -26,6 +26,7 @@
package org.springblade.transport.support.hcs;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import com.baomidou.mybatisplus.extension.toolkit.Db;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
@@ -40,6 +41,10 @@ import org.springblade.hcs.dto.HcsRiskQueryDTO;
import org.springblade.hcs.dto.HcsRiskVO;
import org.springblade.hcs.exception.HcsException;
import org.springblade.hcs.feign.IHcsClient;
+import org.springblade.system.cache.DictBizCache;
+import org.springblade.system.cache.DictCache;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiBlacklistVO;
+import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.CustomerArchiveMapper;
@@ -52,11 +57,14 @@ import org.springblade.transport.pojo.vo.CustomerRiskVO;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.stream.Stream;
/**
* HCS 付款提交校验支撑组件(C101-1 客商风险 + C106-1 额度校验,设计文档 §5.4,D15/R2-Q3)
@@ -87,6 +95,7 @@ public class HcsCreditCheckSupport {
private final ContractManageMapper contractManageMapper;
private final CustomerArchiveMapper customerArchiveMapper;
private final HcsContractCheckLogMapper hcsContractCheckLogMapper;
+ private final IBfiService bfiService;
/**
* 付款提交同步校验入口(submit 事务内调用,校验结果字段随提交一并落库)
@@ -195,7 +204,7 @@ public class HcsCreditCheckSupport {
/**
* 客商风险查询(C101-1,展示与提交校验共用,前端接入设计 §3.3):
* 内部组织类型客商默认无风险(不调 C101-1);外部客商按编号查询风险等级与黑名单状态并留痕;
- * 高风险/黑名单时组装风险提示文案(风险项明细 C101-1 未返回,固定文案展示)
+ * riskTip 经 {@link #buildRiskDetail} 统一组装(等级 + 金蝶黑名单失信类别/列入事由),与列表悬浮明细同口径
*
* @param customerCode 客商编号
* @return 风险展示信息(customerKind/fdBlackStatus/fdRiskLevel/risky/riskTip)
@@ -208,16 +217,43 @@ public class HcsCreditCheckSupport {
.eq(CustomerArchive::getCustomerCode, customerCode)
.eq(CustomerArchive::getIsDeleted, 0)
.last("limit 1"));
- CustomerRiskVO riskVO = new CustomerRiskVO();
- riskVO.setCustomerCode(customerCode);
- riskVO.setCustomerName(customerArchive == null ? customerCode : customerArchive.getFullName());
+ String customerName = customerArchive == null ? customerCode : customerArchive.getFullName();
// 内部组织类型客商默认无风险;档案不存在按外部客商查询(不静默放行)
- boolean internal = isInternalCustomer(customerArchive);
- riskVO.setCustomerKind(internal ? HcsSyncConstant.CUSTOMER_KIND_INTERNAL : "external");
- if (internal) {
+ if (isInternalCustomer(customerArchive)) {
+ CustomerRiskVO riskVO = new CustomerRiskVO();
+ riskVO.setCustomerCode(customerCode);
+ riskVO.setCustomerName(customerName);
+ riskVO.setCustomerKind(HcsSyncConstant.CUSTOMER_KIND_INTERNAL);
riskVO.setRisky(Boolean.FALSE);
return riskVO;
}
+ RelativeRiskResult fetch = this.fetchRelativeRisk(customerCode, customerName);
+ CustomerRiskVO riskVO = fetch.risk();
+ // 表单实时提示与列表明细统一组装口径:追加金蝶黑名单段(A801-5 按档案信用代码查询,失败静默缺失)
+ String creditCode = customerArchive == null ? null : customerArchive.getUnifiedCreditCode();
+ BfiBlacklistVO hit = StringUtil.isBlank(creditCode) ? null
+ : this.queryBlacklistByCodes(List.of(creditCode.trim().toUpperCase())).get(creditCode.trim().toUpperCase());
+ String blackCategory = hit == null ? null
+ : (Func.isEmpty(hit.getAwrpCombofieldTitle()) ? hit.getAwrpCombofield() : hit.getAwrpCombofieldTitle());
+ riskVO.setRiskTip(buildRiskDetail(customerName, BLACK_STATUS_YES.equalsIgnoreCase(riskVO.getFdBlackStatus()),
+ riskVO.getFdRiskLevel(), blackCategory, hit == null ? null : hit.getAwrpTextareafield()));
+ try {
+ hcsContractCheckLogMapper.insert(this.buildRiskCheckLog(customerCode, riskVO, fetch.traceId()));
+ } catch (Exception exception) {
+ log.error("HCS客商风险查询留痕写入失败:fdNumber={}", customerCode, exception);
+ }
+ return riskVO;
+ }
+
+ /**
+ * C101-1 单客商远程查询并组装展示信息(不查档案、不写留痕,供单查与定时批量刷新复用);
+ * riskTip 不在本方法组装,由调用方经 {@link #buildRiskDetail} 统一组装
+ *
+ * @param customerCode 客商编号
+ * @param customerName 客商名称(展示信息用)
+ * @return 风险展示信息 + 合规 traceId
+ */
+ private RelativeRiskResult fetchRelativeRisk(String customerCode, String customerName) {
HcsRiskQueryDTO param = new HcsRiskQueryDTO();
// 【必填】客商编号(用哪个编码待联调,§9-U6)
param.setFdNumber(customerCode);
@@ -231,28 +267,36 @@ public class HcsCreditCheckSupport {
String blackStatus = risk == null ? null : risk.getFdBlackStatus();
String riskLevel = risk == null ? null : risk.getFdRiskLevel();
boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || BLACK_STATUS_YES.equalsIgnoreCase(blackStatus);
+ CustomerRiskVO riskVO = new CustomerRiskVO();
+ riskVO.setCustomerCode(customerCode);
+ riskVO.setCustomerName(customerName);
+ riskVO.setCustomerKind("external");
riskVO.setFdBlackStatus(blackStatus);
riskVO.setFdRiskLevel(riskLevel);
riskVO.setRiskLevelName(riskLevelName(blackStatus, riskLevel));
riskVO.setRisky(risky);
- if (risky) {
- // 风险提示固定短文案(C101-1 未返回风险项明细,不拼接枚举清单):黑名单与高风险区分表述
- riskVO.setRiskTip("【" + riskVO.getCustomerName() + "】:"
- + (BLACK_STATUS_YES.equalsIgnoreCase(blackStatus) ? "被列入外部黑名单" : "为高风险客商"));
- }
- try {
- HcsContractCheckLog checkLog = new HcsContractCheckLog();
- checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT);
- checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_RISK);
- checkLog.setResult(risky ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS);
- checkLog.setMsg("客商风险查询[" + customerCode + "]黑名单=" + blackStatus + ",风险等级=" + riskLevel + ",高风险/黑名单=" + risky);
- checkLog.setTraceId(result.getTraceId());
- checkLog.setCreateTime(LocalDateTime.now());
- hcsContractCheckLogMapper.insert(checkLog);
- } catch (Exception exception) {
- log.error("HCS客商风险查询留痕写入失败:fdNumber={}", customerCode, exception);
- }
- return riskVO;
+ return new RelativeRiskResult(riskVO, result.getTraceId());
+ }
+
+ /**
+ * C101-1 单次远程查询结果(风险展示信息 + 合规 traceId,留痕与批量刷新共用)
+ */
+ private record RelativeRiskResult(CustomerRiskVO risk, String traceId) {
+ }
+
+ /**
+ * 组装客商风险查询留痕(单查与定时批量刷新共用,msg 口径一致)
+ */
+ private HcsContractCheckLog buildRiskCheckLog(String customerCode, CustomerRiskVO risk, String traceId) {
+ HcsContractCheckLog checkLog = new HcsContractCheckLog();
+ checkLog.setBizType(HcsSyncConstant.BIZ_TYPE_CONTRACT);
+ checkLog.setCheckType(HcsSyncConstant.CHECK_TYPE_RISK);
+ checkLog.setResult(Boolean.TRUE.equals(risk.getRisky()) ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS);
+ checkLog.setMsg("客商风险查询[" + customerCode + "]黑名单=" + risk.getFdBlackStatus()
+ + ",风险等级=" + risk.getFdRiskLevel() + ",高风险/黑名单=" + risk.getRisky());
+ checkLog.setTraceId(traceId);
+ checkLog.setCreateTime(LocalDateTime.now());
+ return checkLog;
}
/**
@@ -287,18 +331,67 @@ public class HcsCreditCheckSupport {
}
/**
- * 风险等级转义(C101-1 枚举 0=低风险/1=中风险/2=高风险):黑名单状态优先展示,无等级返回空
+ * 风险等级转义:黑名单状态优先展示;等级走业务字典 risk_level 翻译({@link #resolveRiskLevelName}),无等级返回空
*/
private String riskLevelName(String blackStatus, String riskLevel) {
if (BLACK_STATUS_YES.equalsIgnoreCase(blackStatus)) {
return "黑名单";
}
- return switch (Objects.toString(riskLevel, "")) {
- case "2" -> "高风险";
- case "1" -> "中风险";
- case "0" -> "低风险";
- default -> null;
- };
+ return resolveRiskLevelName(riskLevel);
+ }
+
+ /**
+ * 客商风险等级名称翻译(业务字典 risk_level,dictKey=C101-1 风险等级 0=低风险/1=中风险/2=高风险);
+ * 业务字典未命中兜底系统字典,仍未命中返回 null(明细不拼等级段);列表组装与展示链路统一复用本方法
+ *
+ * @param riskLevel C101-1 风险等级
+ * @return 等级名称,未命中返回 null
+ */
+ public static String resolveRiskLevelName(String riskLevel) {
+ if (Func.isEmpty(riskLevel)) {
+ return null;
+ }
+ String levelName = DictBizCache.getValue(HcsSyncConstant.RISK_LEVEL_DICT_CODE, riskLevel);
+ if (StringUtil.isBlank(levelName)) {
+ levelName = DictCache.getValue(HcsSyncConstant.RISK_LEVEL_DICT_CODE, riskLevel);
+ }
+ return StringUtil.isBlank(levelName) ? null : levelName;
+ }
+
+ /**
+ * 组装客商风险明细文案(后台统一组装、前端只展示;列表悬浮明细与表单实时提示共用本方法,口径一致),
+ * 格式:【客商名称】:被列入外部黑名单(失信类别:xx);{列入事由};风险等级:xx,分段说明:
+ * ① 黑名单段:合同系统黑名单(blackStatusYes)或金蝶黑名单命中(类别/事由任一非空)→「被列入外部黑名单」,
+ * 失信类别(A801-5 awrp_combofield_title,缺失兜底 awrp_combofield)有值时括号展示区分禁止交易/风险警示类;
+ * ② 列入事由:金蝶 A801-5 awrp_textareafield 真实明细,非空时追加;
+ * ③ 等级段:C101-1 风险等级(risk_level 字典翻译,黑名单不转义、按实际等级展示),非空时追加
+ *
+ * @param customerName 客商名称
+ * @param blackStatusYes 合同系统黑名单状态(C101-1 fdBlackStatus=yes)
+ * @param riskLevel C101-1 风险等级(0/1/2)
+ * @param blackCategory 金蝶黑名单失信类别
+ * @param blackReason 金蝶黑名单列入事由
+ * @return 明细文案,无风险信息返回空字符串
+ */
+ public static String buildRiskDetail(String customerName, boolean blackStatusYes, String riskLevel, String blackCategory, String blackReason) {
+ List parts = new ArrayList<>();
+ boolean blacklist = blackStatusYes || Func.isNotEmpty(blackCategory) || Func.isNotEmpty(blackReason);
+ if (blacklist) {
+ parts.add(Func.isNotEmpty(blackCategory)
+ ? "被列入外部黑名单(失信类别:" + blackCategory + ")"
+ : "被列入外部黑名单");
+ }
+ if (Func.isNotEmpty(blackReason)) {
+ parts.add(blackReason);
+ }
+ String levelName = resolveRiskLevelName(riskLevel);
+ if (StringUtil.isNotBlank(levelName)) {
+ parts.add("风险等级:" + levelName);
+ }
+ if (parts.isEmpty()) {
+ return "";
+ }
+ return "【" + customerName + "】:" + String.join(";", parts);
}
/**
@@ -324,6 +417,139 @@ public class HcsCreditCheckSupport {
return riskList;
}
+ /**
+ * 客商风险每日定时刷新(C101-1,客商档案列表展示与筛选数据源):
+ * ① 取合同期限内(正式/临时期限任一覆盖当天)审核通过且未终止合同的甲乙方名称;
+ * ② 按名称反查客商档案,内部组织客商默认无风险跳过;
+ * ③ 逐个查询 C101-1(接口仅支持单编号,属远程调用非数据库 N+1;复用 {@link #fetchRelativeRisk},不重复查档案),
+ * 单个失败记 error 日志不中断(失败原因备查);
+ * ④ 外部客商按 200/批切分,每批黑名单(A801-5)一次批量查询 + C101-1 逐个远程查询,失信类别/列入事由落原始字段
+ * (明细文案由列表查询统一组装);
+ * ⑤ 每批成功结果立即批量落库(Db.updateBatchById,仅提交 id + 风险列,不整行回写)+ 留痕批量写入(Db.saveBatch),
+ * 风险/黑名单批次互不影响,单批失败不中断其余批次,避免逐条操作数据库
+ *
+ * @return 结果汇总描述
+ */
+ public String refreshCustomerRiskForActiveContracts() {
+ LocalDate today = LocalDate.now();
+ List contractList = contractManageMapper.selectList(Wrappers.lambdaQuery()
+ .select(ContractManage::getPartyA, ContractManage::getPartyB)
+ .in(ContractManage::getApprovalStatus, HcsSyncConstant.CONTRACT_APPROVED_STATUSES)
+ .ne(ContractManage::getCurrentNode, HcsSyncConstant.CONTRACT_TERMINATED_NODE)
+ .and(wrapper -> wrapper
+ .and(item -> item.le(ContractManage::getStartDate, today).ge(ContractManage::getEndDate, today))
+ .or(item -> item.le(ContractManage::getTemporaryStartDate, today).ge(ContractManage::getTemporaryEndDate, today))));
+ List partyNames = contractList.stream()
+ .flatMap(contract -> Stream.of(contract.getPartyA(), contract.getPartyB()))
+ .filter(Func::isNotEmpty)
+ .map(String::trim)
+ .distinct()
+ .toList();
+ if (partyNames.isEmpty()) {
+ return "合同期限内无生效合同,未刷新客商风险";
+ }
+ List archiveList = customerArchiveMapper.selectList(Wrappers.lambdaQuery()
+ .in(CustomerArchive::getFullName, partyNames)
+ .eq(CustomerArchive::getIsDeleted, 0));
+ List externalArchives = archiveList.stream()
+ .filter(customerArchive -> !isInternalCustomer(customerArchive))
+ .toList();
+ int internalCount = archiveList.size() - externalArchives.size();
+ int refreshCount = 0;
+ int failCount = 0;
+ // 外部客商按 200/批处理:每批黑名单一次批量查询 + C101-1 逐个远程查询(接口仅支持单编号,非数据库 N+1),
+ // 批内成功结果立即批量落库与留痕,风险/黑名单批次互不影响,单批失败不中断其余批次
+ for (int start = 0; start < externalArchives.size(); start += HcsSyncConstant.CUSTOMER_RISK_REFRESH_BATCH) {
+ List batch = externalArchives.subList(start, Math.min(start + HcsSyncConstant.CUSTOMER_RISK_REFRESH_BATCH, externalArchives.size()));
+ Map blacklistMap = this.queryBlacklistByCodes(batch.stream()
+ .map(CustomerArchive::getUnifiedCreditCode)
+ .filter(Func::isNotEmpty)
+ .map(code -> code.trim().toUpperCase())
+ .distinct()
+ .toList());
+ List batchRefreshList = new ArrayList<>();
+ List batchCheckLogs = new ArrayList<>();
+ for (CustomerArchive customerArchive : batch) {
+ try {
+ RelativeRiskResult fetch = this.fetchRelativeRisk(customerArchive.getCustomerCode(), customerArchive.getFullName());
+ CustomerRiskVO risk = fetch.risk();
+ // 仅提交风险列,避免整行回写覆盖刷新期间其它并发修改(审批状态/额度/金蝶推送等)
+ CustomerArchive riskPatch = new CustomerArchive();
+ riskPatch.setId(customerArchive.getId());
+ riskPatch.setHcsRiskLevel(risk.getFdRiskLevel());
+ riskPatch.setHcsBlackStatus(risk.getFdBlackStatus());
+ riskPatch.setHcsRiskTime(LocalDateTime.now());
+ // 金蝶黑名单命中落原始字段(失信类别/列入事由),明细文案由列表查询统一组装
+ BfiBlacklistVO hit = Func.isEmpty(customerArchive.getUnifiedCreditCode())
+ ? null : blacklistMap.get(customerArchive.getUnifiedCreditCode().trim().toUpperCase());
+ if (hit != null) {
+ String category = Func.isEmpty(hit.getAwrpCombofieldTitle()) ? hit.getAwrpCombofield() : hit.getAwrpCombofieldTitle();
+ riskPatch.setKingdeeBlackCategory(category);
+ riskPatch.setKingdeeBlackReason(hit.getAwrpTextareafield());
+ }
+ batchCheckLogs.add(this.buildRiskCheckLog(customerArchive.getCustomerCode(), risk, fetch.traceId()));
+ batchRefreshList.add(riskPatch);
+ } catch (Exception exception) {
+ failCount++;
+ log.error("定时刷新客商风险失败:customerCode={},fullName={}",
+ customerArchive.getCustomerCode(), customerArchive.getFullName(), exception);
+ }
+ }
+ // 批内成功结果立即提交数据库,批间互不影响
+ if (!batchRefreshList.isEmpty()) {
+ Db.updateBatchById(batchRefreshList);
+ refreshCount += batchRefreshList.size();
+ }
+ this.saveRiskCheckLogs(batchCheckLogs);
+ }
+ String summary = String.format("生效合同 %d 份,覆盖客商 %d 个,刷新 %d 个,内部客商跳过 %d 个,失败 %d 个",
+ contractList.size(), archiveList.size(), refreshCount, internalCount, failCount);
+ if (failCount > 0) {
+ log.warn("定时刷新客商风险存在失败:{}", summary);
+ }
+ return summary;
+ }
+
+ /**
+ * 金蝶黑名单批量查询(A801-5,定时刷新专用,调用方按 200 客商/批切分保证入参 ≤200):
+ * 键为大写税号;失信类别取 awrp_combofield_title、缺失兜底 awrp_combofield(口径同提审校验 validateBlacklist);
+ * 查询异常不阻断刷新,记 error 日志备查后返回空 map(该批黑名单段缺失)
+ *
+ * @param creditCodes 统一社会信用代码集合(大写,≤200)
+ * @return 大写税号 -> 黑名单记录
+ */
+ private Map queryBlacklistByCodes(List creditCodes) {
+ if (creditCodes.isEmpty()) {
+ return Map.of();
+ }
+ try {
+ Map blacklistMap = new HashMap<>();
+ for (BfiBlacklistVO item : this.bfiService.queryBlacklist("1", creditCodes)) {
+ if (Func.isNotEmpty(item.getNumber())) {
+ blacklistMap.put(item.getNumber().trim().toUpperCase(), item);
+ }
+ }
+ return blacklistMap;
+ } catch (Exception exception) {
+ log.error("定时刷新客商风险批量查询金蝶黑名单失败:size={}", creditCodes.size(), exception);
+ return Map.of();
+ }
+ }
+
+ /**
+ * 客商风险留痕批量写入(Db.saveBatch,避免逐条 insert);失败记 error 日志备查,不阻断刷新结果
+ */
+ private void saveRiskCheckLogs(List checkLogs) {
+ if (checkLogs.isEmpty()) {
+ return;
+ }
+ try {
+ Db.saveBatch(checkLogs);
+ } catch (Exception exception) {
+ log.error("定时刷新客商风险留痕批量写入失败:size={}", checkLogs.size(), exception);
+ }
+ }
+
/**
* 按名称反查客商档案(内部/外部类型判断与客商编码获取共用;查不到返回 null)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java
new file mode 100644
index 00000000..862f6cde
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java
@@ -0,0 +1,72 @@
+/**
+ * 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.transport.task;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.hcs.config.HcsProperties;
+import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.cloud.context.config.annotation.RefreshScope;
+import org.springframework.stereotype.Component;
+
+/**
+ * 客商风险每日定时刷新任务(C101-1)
+ *
+ * 合同期限范围内的生效合同,每日定时查询其甲乙方关联客商的风险等级/黑名单状态并落库
+ * (blade_customer_archive.hcs_risk_level/hcs_black_status/hcs_risk_time),
+ * 作为客商档案列表「客商风险」列展示与筛选的数据源;查询逐次留痕 blade_hcs_check_log,
+ * 单个失败记录失败原因日志备查。
+ * 业务逻辑收敛在 {@link HcsCreditCheckSupport#refreshCustomerRiskForActiveContracts()},便于手动触发联调。
+ *
+ * 调度配置(Nacos thirdParty.hcs.risk.*):refreshEnabled 开关、refreshCron 表达式。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RefreshScope
+@RequiredArgsConstructor
+public class CustomerRiskRefreshTask {
+
+ private final HcsCreditCheckSupport hcsCreditCheckSupport;
+ private final HcsProperties hcsProperties;
+
+ /** 客商风险每日刷新调度,默认每天 02:30 执行(错开金蝶客商同步 02:00) */
+ @Scheduled(cron = "${thirdParty.hcs.risk.refreshCron:0 30 2 * * ?}")
+ public void refreshCustomerRisk() {
+ if (!hcsProperties.getRisk().isRefreshEnabled()) {
+ return;
+ }
+ try {
+ String summary = hcsCreditCheckSupport.refreshCustomerRiskForActiveContracts();
+ log.info("定时刷新客商风险:{}", summary);
+ } catch (Exception exception) {
+ log.error("定时刷新客商风险异常", exception);
+ }
+ }
+
+}
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
index dc2b4b2a..56580bb0 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/config/HcsProperties.java
@@ -252,6 +252,11 @@ public class HcsProperties {
* 付款提交前风险校验开关
*/
private boolean checkOnPayment = true;
+ /**
+ * 客商风险每日定时刷新开关(thirdParty.hcs.risk.refresh-enabled,默认关闭):
+ * 刷新合同期限内生效合同关联客商的风险等级/黑名单状态并落库,支撑列表展示与筛选
+ */
+ private boolean refreshEnabled = false;
}
@Data
diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml
index efbaaef0..d09370cd 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -245,6 +245,10 @@ thirdParty:
risk:
check-on-contract: ${HCS_RISK_CHECK_ON_CONTRACT:true}
check-on-payment: ${HCS_RISK_CHECK_ON_PAYMENT:true}
+ # 客商风险每日定时刷新开关(刷新合同期限内生效合同关联客商并落库,支撑列表展示与筛选,默认关)
+ refresh-enabled: ${HCS_RISK_REFRESH_ENABLED:false}
+ # 客商风险刷新调度 cron,默认每天 02:30(错开金蝶客商同步 02:00)
+ refresh-cron: ${HCS_RISK_REFRESH_CRON:0 30 2 * * ?}
# 黑名单是否阻断(合同置 failed / 付款提交拦截)
# block-blacklist: ${HCS_RISK_BLOCK_BLACKLIST:true}
# 额度校验(C106-1,一期暂不对接)
From ed91793b64bc914d5a83eb7027f9772dc5cc99f4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 11:33:25 +0800
Subject: [PATCH 42/63] =?UTF-8?q?feat:=20=E5=AE=A2=E5=95=86=E6=A1=A3?=
=?UTF-8?q?=E6=A1=88=E5=88=97=E8=A1=A8=E9=A3=8E=E9=99=A9=E6=98=8E=E7=BB=86?=
=?UTF-8?q?=E5=90=8E=E5=8F=B0=E7=BB=9F=E4=B8=80=E7=BB=84=E8=A3=85=EF=BC=88?=
=?UTF-8?q?=E9=A3=8E=E9=99=A9=E7=AD=89=E7=BA=A7=E5=AD=97=E5=85=B8=E7=BF=BB?=
=?UTF-8?q?=E8=AF=91=E4=B8=8E=E9=87=91=E8=9D=B6=E9=BB=91=E5=90=8D=E5=8D=95?=
=?UTF-8?q?=E5=A4=B1=E4=BF=A1=E7=B1=BB=E5=88=AB=E5=88=97=E5=85=A5=E4=BA=8B?=
=?UTF-8?q?=E7=94=B1=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../impl/CustomerArchiveServiceImpl.java | 20 +++++++++++++++++++
1 file changed, 20 insertions(+)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
index 228cca04..7c0994ac 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
@@ -41,6 +41,10 @@ import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
+import org.springblade.system.cache.SysCache;
+import org.springblade.system.cache.UserCache;
+import org.springblade.transport.constant.HcsSyncConstant;
+import org.springblade.transport.excel.CustomerArchiveExcel;
import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultErrorVO;
@@ -92,6 +96,7 @@ import org.springblade.transport.pojo.vo.CustomerCreditScoreVO;
import org.springblade.transport.pojo.vo.CustomerInvoiceContactVO;
import org.springblade.transport.pojo.vo.CustomerInvoiceInfoVO;
import org.springblade.transport.pojo.vo.CustomerReceiptAccountVO;
+import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.support.KingdeeSyncLockSupport;
import org.springframework.stereotype.Service;
@@ -232,6 +237,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl records = baseMapper.selectCustomerArchivePage(page, customer, currentScopeUserId());
records.forEach(record -> record.setCreateUserName(UserCache.getUserRealName(record.getCreateUser())));
fillFundUseRisk(records);
+ this.fillRiskDetail(records);
return page.setRecords(records);
}
@@ -2144,6 +2150,19 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl records) {
+ for (CustomerArchiveVO record : records) {
+ String customerName = Func.isEmpty(record.getFullName()) ? record.getCustomerCode() : record.getFullName();
+ record.setHcsRiskDetail(HcsCreditCheckSupport.buildRiskDetail(customerName,
+ "yes".equalsIgnoreCase(record.getHcsBlackStatus()), record.getHcsRiskLevel(),
+ record.getKingdeeBlackCategory(), record.getKingdeeBlackReason()));
+ }
+ }
+
/**
* 资金使用情况 = 付款申请中「付款方=当前客商」且审核通过的已付金额合计;
* 资金使用额度风险 = 合计已付金额 / 客商最大资金使用额度。
@@ -2152,6 +2171,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl payerNames = new HashSet<>();
records.forEach(customer -> {
if (Func.isNotEmpty(customer.getFullName())) {
From 52d2f05524fd253cde1877e16e78442db7371e9d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 11:42:22 +0800
Subject: [PATCH 43/63] =?UTF-8?q?docs:=20=E5=AE=A2=E5=95=86=E9=BB=91?=
=?UTF-8?q?=E5=90=8D=E5=8D=95=E4=B8=8E=E9=A3=8E=E9=99=A9=E7=AD=89=E7=BA=A7?=
=?UTF-8?q?=E4=B8=9A=E5=8A=A1=E8=AF=B4=E6=98=8E=E4=B8=8E=E8=BF=90=E7=BB=B4?=
=?UTF-8?q?=E6=89=8B=E5=86=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...0-30-客商黑名单与风险等级业务说明(业务侧).md | 101 +++++++++++++
...0-30-客商黑名单与风险等级运维手册(运维侧).md | 137 ++++++++++++++++++
2 files changed, 238 insertions(+)
create mode 100644 doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md
create mode 100644 doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md
diff --git a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md
new file mode 100644
index 00000000..2faca5f6
--- /dev/null
+++ b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md
@@ -0,0 +1,101 @@
+# 客商黑名单与风险等级业务说明(业务侧)
+
+> 更新日期:2026-10-30 | 适用系统:运输 TMS | 读者:业务/运营人员
+>
+> 关联文档:《客商黑名单与风险等级运维手册(运维侧)》(同目录)
+
+## 1. 这是什么
+
+TMS 从**两个外部系统**获取客商风险数据,每日自动更新到客商档案,用于客商档案的展示、筛选,以及合同、付款环节的风险管控:
+
+| 数据 | 来源系统 | 说明 |
+|---|---|---|
+| **客商风险等级** | 合同合规系统(HCS) | 低风险 / 中风险 / 高风险,另含"黑名单"标识 |
+| **金蝶黑名单** | 金蝶(星瀚)· 国资委黑名单 | 命中后记录**失信类别**(禁止交易类 / 风险警示类)与**列入事由** |
+
+## 2. 名词解释
+
+| 名词 | 含义 |
+|---|---|
+| 黑名单 | 客商被列入外部(国资委)黑名单。金蝶分 **A 类(禁止交易类,不允许交易)** 和 **B 类(风险警示类,需关注)** |
+| 失信类别 | 黑名单的分类名称(如"禁止交易类"),来自金蝶黑名单记录 |
+| 列入事由 | 被列入黑名单的具体原因(如"涉贪污受贿裁判相关提及方(3年以上及其他)"),来自金蝶黑名单记录 |
+| 风险等级 | 合同合规系统对客商的风险评级:0=低风险、1=中风险、2=高风险 |
+| 内部组织客商 | 集团内我方签约主体(客商类型=内部组织),**不查询风险,默认无风险** |
+
+## 3. 总体流程
+
+```mermaid
+flowchart TD
+ A[每日凌晨自动刷新 02:30] --> B[取合同期限内生效合同的 甲乙方客商]
+ B --> C{内部组织客商?}
+ C -->|是| D[默认无风险, 跳过]
+ C -->|否| E[查合同合规系统风险等级]
+ C -->|否| F[查金蝶黑名单]
+ E --> G[结果写入客商档案 供列表展示与筛选]
+ F --> G
+```
+
+- 刷新范围:**合同期限内**(合同起止日期或临时期限覆盖当天)且**审核通过、未终止**的合同,其甲乙方关联客商。
+- 数据**每日更新一次**;当天新增的客商在表单里有实时提示(见 4.1)。
+
+## 4. 各业务环节说明
+
+### 4.1 新增 / 编辑客商(表单实时提示)
+
+- 录入**统一社会信用代码**满 18 位后,系统自动查金蝶客商档案,回填客商名称、法人等工商信息。
+- **客商名称**回填或填写后失焦时,系统实时查询该客商的风险;若为高风险/黑名单客商,名称下方显示红色提示,悬浮可见明细,格式:
+
+ > 【客商名称】:被列入外部黑名单(失信类别:xx);列入事由 xx;风险等级:高风险
+
+### 4.2 提交准入审批(金蝶黑名单硬校验)
+
+提交客商准入审批时,系统按客商**发票信息中的纳税人识别号**实时查询金蝶黑名单:
+
+| 命中情况 | 结果 |
+|---|---|
+| **A 类(禁止交易类)** | ❌ **禁止提交**,提示失信类别与列入事由 |
+| **B 类(风险警示类)** | ⚠️ 默认放行并记录告警(是否拦截由系统配置决定) |
+| 查询异常 | ❌ 暂缓提交(防止漏拦),稍后重试 |
+
+> 因此:维护客商**发票信息(税号)是黑名单校验的前提**,未维护税号时提审不会做黑名单校验。
+
+### 4.3 客商档案列表(展示 / 筛选 / 悬浮)
+
+**客商风险列**显示规则:
+
+| 客商情况 | 显示 | 颜色 |
+|---|---|---|
+| 高风险 且 黑名单 | 高风险、黑名单 | 红 |
+| 仅高风险 | 高风险 | 红 |
+| 仅黑名单 | 黑名单 | 红 |
+| 中风险 | 中风险 | 黄 |
+| 低风险 | 低风险 | 绿 |
+| 内部组织客商 | 无风险(默认,不查询) | 绿 |
+| 尚未查询到数据 | - | — |
+
+- 红色标签(黑名单/高风险)**鼠标悬浮**可查看明细:失信类别、列入事由、风险等级。
+- 搜索栏新增**客商风险**下拉筛选:黑名单 / 高风险 / 中风险 / 低风险。
+- 数据为每日凌晨刷新结果,当天有变动的以次日刷新为准。
+
+### 4.4 合同推送(合同合规系统)
+
+合同推送到合规系统前,甲乙双方客商各做一次风险校验:**高风险/黑名单客商的合同不能直接签约**,系统自动发起合同特批流程,特批通过后方可继续。
+
+### 4.5 付款申请
+
+提交付款申请时,若收款方为外部客商且属**高风险/黑名单**:
+
+- 系统弹出风险提示;
+- **必须上传「特批附件」**类型附件,否则无法提交审批;
+- 审批通过后自动发起付款特批,特批完成前禁止推送金蝶。
+
+## 5. 常见问题
+
+| 问题 | 说明 |
+|---|---|
+| 为什么客商风险列显示"-"? | 该客商不在任何合同期限内生效合同的关联范围内,尚未被每日刷新任务查询过;或系统刚上线数据尚未生成 |
+| 为什么内部组织客商显示"无风险"? | 内部组织为我方主体,按集团规定不查询风险,默认无风险 |
+| 新客商当天会出现在风险筛选里吗? | 表单实时提示即时生效;列表落库数据次日刷新后生效 |
+| 提交审批被黑名单拦截怎么办? | 客商属 A 类禁止交易,不可线上提交,走线下处置流程 |
+| 黑名单数据多久更新? | 每日凌晨 02:30 自动刷新(合同期限内关联客商) |
diff --git a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md
new file mode 100644
index 00000000..6cee8b25
--- /dev/null
+++ b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md
@@ -0,0 +1,137 @@
+# 客商黑名单与风险等级运维手册(运维侧)
+
+> 更新日期:2026-10-30 | 适用系统:运输 TMS(blade-transport) | 读者:运维/实施人员
+>
+> 关联文档:《客商黑名单与风险等级业务说明(业务侧)》(同目录)
+
+## 1. 链路总览
+
+```
+┌──────────────┐ C101-1 客商风险等级 ┌────────────────┐
+│ 合同合规系统 │ ◄────────────────────── │ │
+│ (HCS) │ A801-5 金蝶黑名单 │ TMS │
+└──────────────┘ ◄────────────────────── │ blade-transport│
+ └──────┬─────────┘
+ 每日 02:30 CustomerRiskRefreshTask │
+ ┌──────▼─────────┐
+ │ blade_customer_archive
+ │ hcs_risk_level / hcs_black_status
+ │ hcs_risk_time
+ │ kingdee_black_category / kingdee_black_reason
+ └────────────────┘
+```
+
+**两条数据线**:
+
+| 线 | 接口 | 查询键 | 落库字段 |
+|---|---|---|---|
+| 合同合规风险(C101-1) | `getRelativeRisk` | 客商编号(fdNumber) | `hcs_risk_level`、`hcs_black_status`、`hcs_risk_time` |
+| 金蝶黑名单(A801-5) | `queryBlacklist` | 税号/统一社会信用代码(number) | `kingdee_black_category`、`kingdee_black_reason` |
+
+**调用场景**(C101-1):
+
+| 场景 | 入口 | 行为 |
+|---|---|---|
+| 每日定时刷新 | `CustomerRiskRefreshTask` → `HcsCreditCheckSupport.refreshCustomerRiskForActiveContracts()` | 批量落库 |
+| 表单实时提示 | `GET /hcs/sync/risk/by-name`(名称反查编号) | 实时查 C101-1 + A801-5 组装 riskTip |
+| 列表单查/批量 | `GET /hcs/sync/risk`、`GET /hcs/sync/risk/batch` | 实时查(展示用) |
+| 合同推送校验 | `HcsContractSyncSupport.checkRelativeRisk` | 甲乙双方各查一次,高风险转特批 |
+| 付款提交校验 | `HcsCreditCheckSupport.checkRelativeRisk` | 高风险/黑名单须特批附件 |
+
+**调用场景**(A801-5):
+
+| 场景 | 入口 | 行为 |
+|---|---|---|
+| 每日定时刷新 | 同上(200 税号/批) | 落失信类别/列入事由 |
+| 提审黑名单校验 | `CustomerArchiveServiceImpl.validateBlacklist` | 按客商发票信息税号查询;**A 类硬拦截,B 类默认放行告警,查询异常阻断提审** |
+
+## 2. 定时任务
+
+`CustomerRiskRefreshTask`(每天 02:30,错开金蝶客商同步 02:00):
+
+1. 取合同期限内生效合同(`approval_status IN (approved, change_approved)`、`current_node != '已终止'`、正式或临时期限覆盖当天)的甲乙方名称——1 次 SQL;
+2. 按名称 `IN` 反查客商档案——1 次 SQL;内部组织客商跳过(默认无风险);
+3. 外部客商按 **200/批**:每批 A801-5 一次批量查询 + C101-1 逐个远程查询(接口仅支持单编号);
+4. 批内成功结果立即 `Db.updateBatchById` 落库 + `Db.saveBatch` 留痕,**批间互不影响,单批失败不中断其余批次**。
+
+## 3. 配置项(Nacos)
+
+| 配置键 | 环境变量 | 默认 | 说明 |
+|---|---|---|---|
+| `third-party.hcs.risk.refresh-enabled` | `HCS_RISK_REFRESH_ENABLED` | false | 每日刷新总开关(**上线需置 true**) |
+| `third-party.hcs.risk.refresh-cron` | `HCS_RISK_REFRESH_CRON` | `0 30 2 * * ?` | 刷新调度 |
+| `third-party.hcs.risk.check-on-contract` | `HCS_RISK_CHECK_ON_CONTRACT` | true | 合同推送前风险校验 |
+| `third-party.hcs.risk.check-on-payment` | `HCS_RISK_CHECK_ON_PAYMENT` | true | 付款提交前风险校验 |
+| `thirdparty.bfi.blacklist.blockRiskWarning` | `BFI_BLACKLIST_BLOCK_RISK_WARNING` | false | 金蝶黑名单 **B 类(风险警示)提审是否拦截**(A 类恒拦截) |
+
+配置模板:`doc/nacos/third-party-api.yaml`(hcs.risk 节点)。
+
+## 4. 数据库
+
+### 4.1 表结构(blade_customer_archive 新增列)
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| `hcs_risk_level` | varchar(8) | C101-1 风险等级:0=低/1=中/2=高风险 |
+| `hcs_black_status` | varchar(8) | C101-1 黑名单状态:yes/no/handing |
+| `hcs_risk_time` | datetime | 风险最近查询时间 |
+| `kingdee_black_category` | varchar(200) | 金蝶黑名单失信类别(awrp_combofield_title 兜底 awrp_combofield) |
+| `kingdee_black_reason` | varchar(1000) | 金蝶黑名单列入事由(awrp_textareafield) |
+
+索引:`idx_hcs_risk_level`、`idx_hcs_black_status`。
+
+### 4.2 脚本(幂等可重复执行)
+
+`doc/sql/transport/lqj/temp/`:
+
+| 脚本 | 内容 |
+|---|---|
+| `02_blade_customer_archive_hcs_risk_20261030.sql` | 加 5 列 + 2 索引(information_schema 判断,已存在跳过) |
+| `03_blade_customer_risk_dict_20261030.sql` | 业务字典 `customer_risk`(列表筛选下拉)与 `risk_level`(等级翻译) |
+
+> 若早期版本已执行过含 `hcs_black_category/reason` 的脚本,需手动删除残留列(现名已改 `kingdee_black_*`)。
+
+### 4.3 字典
+
+| code | dictKey → dictValue | 用途 |
+|---|---|---|
+| `customer_risk` | blacklist/high/middle/low → 黑名单/高风险/中风险/低风险 | 客商列表搜索栏筛选下拉 |
+| `risk_level` | 0/1/2 → 低风险/中风险/高风险 | 风险等级名称翻译(明细文案) |
+
+字典缺失时前端筛选走兜底选项、后端明细少拼等级段,不报错。
+
+## 5. 明细组装口径(后台统一)
+
+唯一出口:`HcsCreditCheckSupport.buildRiskDetail(customerName, blackStatusYes, riskLevel, blackCategory, blackReason)`,
+格式:`【客商名称】:被列入外部黑名单(失信类别:xx);{列入事由};风险等级:xx`
+
+- 黑名单段:合同系统黑名单 **或** 金蝶命中(类别/事由任一非空);
+- 等级段:`risk_level` 字典翻译(`DictBizCache` 未命中兜底 `DictCache`);
+- 列表(`hcs_risk_detail`,列表查询时组装不落库)与表单实时提示(`riskTip`)同口径。
+
+列表显示规则(前端 `customerRowRiskName`):高风险+黑名单 → "高风险、黑名单"(红);高风险、黑名单单项各显示(红);中风险(黄);低风险(绿);内部组织 → "无风险"(绿);无数据 → "-"。
+
+## 6. 日志与排查
+
+| 排查点 | 位置/关键字 |
+|---|---|
+| 刷新汇总 | 应用日志:`定时刷新客商风险:生效合同 X 份,覆盖客商 Y 个,刷新 Z 个…` |
+| 单客商刷新失败 | `定时刷新客商风险失败:customerCode=…`(error 级,含堆栈) |
+| 黑名单批量查询失败 | `定时刷新客商风险批量查询金蝶黑名单失败:size=…`(该批黑名单段缺失,下轮重刷) |
+| 留痕写入失败 | `定时刷新客商风险留痕批量写入失败:size=…` |
+| 全量留痕 | 表 `blade_hcs_check_log`(`biz_type=contract`,`check_type=risk`,`result=pass/fail/skip`,含 `trace_id`、`msg`) |
+| 提审黑名单拦截 | 提审报错文案含"国资委黑名单中(失信类别:…,列入事由:…)" |
+
+## 7. 常见操作
+
+| 操作 | 方法 |
+|---|---|
+| 开启/关闭每日刷新 | Nacos 改 `refresh-enabled`(`@RefreshScope`,改后自动生效,无需重启) |
+| 立即手动刷新一次 | 临时把 `refresh-cron` 改为近时刻(如 `0 5 14 * * ?`),执行后改回;或本地调用 `refreshCustomerRiskForActiveContracts()` |
+| 补字典 | 执行 `03_blade_customer_risk_dict_20261030.sql`(幂等) |
+| 某客商风险不更新 | 核对:客商名称是否与合同甲乙方**完全一致**(按名称匹配);客商是否内部组织(跳过);合同是否期限内且审核通过 |
+| C101-1 不可用 | 刷新任务该客商记失败跳过,不影响其他客商;列表显示上轮数据 |
+| A801-5 不可用 | 该批黑名单字段缺失(等级仍更新);提审黑名单校验会阻断提交(防止漏拦),恢复后重试 |
+
+---
+**注意**:黑名单/风险数据仅作展示与流程管控,强制安全校验在提审(A801-5 硬拦)、合同推送、付款提交链路的服务端事务内,前端展示不作为安全边界。
From a71fe022d6e7cbdca61f964e9060c0752fab4148 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 11:45:04 +0800
Subject: [PATCH 44/63] =?UTF-8?q?docs:=20=E4=B8=9A=E5=8A=A1=E8=AF=B4?=
=?UTF-8?q?=E6=98=8E=E5=AE=A2=E5=95=86=E9=A3=8E=E9=99=A9=E5=88=97=E9=BB=91?=
=?UTF-8?q?=E5=90=8D=E5=8D=95=E4=B8=8E=E7=AD=89=E7=BA=A7=E7=BB=84=E5=90=88?=
=?UTF-8?q?=E5=B1=95=E7=A4=BA=E8=A7=84=E5=88=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../2026-10-30-客商黑名单与风险等级业务说明(业务侧).md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md
index 2faca5f6..e57af84c 100644
--- a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md
+++ b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md
@@ -66,9 +66,9 @@ flowchart TD
| 客商情况 | 显示 | 颜色 |
|---|---|---|
-| 高风险 且 黑名单 | 高风险、黑名单 | 红 |
+| 黑名单 + 风险等级(组合展示,等级在前) | 高风险、黑名单 / 中风险、黑名单 / 低风险、黑名单 | 红 |
+| 仅黑名单(无等级数据) | 黑名单 | 红 |
| 仅高风险 | 高风险 | 红 |
-| 仅黑名单 | 黑名单 | 红 |
| 中风险 | 中风险 | 黄 |
| 低风险 | 低风险 | 绿 |
| 内部组织客商 | 无风险(默认,不查询) | 绿 |
From c40cbfeefbf39434434a7b15289bbd0f0dbb35a0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 11:53:47 +0800
Subject: [PATCH 45/63] =?UTF-8?q?docs:=20=E9=87=91=E8=9D=B6=E5=8D=95?=
=?UTF-8?q?=E6=8D=AE=E9=94=81=E5=AE=9A=E4=B8=8E=E8=A7=A3=E9=94=81=E4=B8=9A?=
=?UTF-8?q?=E5=8A=A1=E8=AF=B4=E6=98=8E=EF=BC=88=E9=94=81=E5=AE=9A-?=
=?UTF-8?q?=E8=A7=A3=E9=94=81-=E5=88=A0=E5=8D=95-=E8=A1=A5=E5=81=BF?=
=?UTF-8?q?=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...2026-10-30-金蝶单据锁定与解锁说明(业务版).md | 201 ++++++++++++++++++
1 file changed, 201 insertions(+)
create mode 100644 doc/docs/业务流程/2026-10-30-金蝶单据锁定与解锁说明(业务版).md
diff --git a/doc/docs/业务流程/2026-10-30-金蝶单据锁定与解锁说明(业务版).md b/doc/docs/业务流程/2026-10-30-金蝶单据锁定与解锁说明(业务版).md
new file mode 100644
index 00000000..6437bca6
--- /dev/null
+++ b/doc/docs/业务流程/2026-10-30-金蝶单据锁定与解锁说明(业务版).md
@@ -0,0 +1,201 @@
+# 金蝶单据锁定与解锁说明(业务版)
+
+> 业务域:正式结算单、开票申请、付款申请在「同步金蝶」成功后的单据锁定、金蝶侧解锁/删单通知、锁定失败自动补偿
+> 核心规则:**单据推送金蝶成功后,本系统会请金蝶"锁单";锁定期间本系统对应单据被冻结,不能作废或改动。金蝶侧解锁或删单后,会通知本系统,本系统据此解除锁定或自动还原单据。**
+> 最后更新:2026-10-30
+
+---
+
+## 一、为什么要锁单
+
+一张单在 TMS 和金蝶各存一份。如果 TMS 推完单后继续改、继续作废,金蝶那边还是老单,就会出现"两套账"。
+
+锁单就是把两边"绑住":
+
+1. 推送成功后,本系统立即请求金蝶把这张单锁住(金蝶侧不允许随意改动);
+2. 同时本系统把这边的单冻结(不能作废、不能改);
+3. 金蝶那边解锁或删单后,会主动通知本系统,本系统再解除冻结或把单据还原。
+
+锁定与否**不影响已推送成功的结果**:金蝶单一旦生成,不会因为锁单没成功而重复生成。
+
+---
+
+## 二、适用范围(总览)
+
+| 业务单据 | 什么时候推送 | 金蝶侧单据 | 对应关系 |
+| --- | --- | --- | --- |
+| 正式结算单(应付) | 审批通过后系统自动推送,也可页面点「同步金蝶」 | 财务应付单 | 一张结算单 → 一张金蝶单 |
+| 正式结算单(应收) | 同上 | 财务应收单 | 一张结算单 → 一张金蝶单 |
+| 开票申请单 | 审批通过后系统自动推送,也可页面点「同步金蝶」 | 开票单 | 一张申请单 → **按发票张次 N 张金蝶单,逐张锁定** |
+| 付款申请单 | 审批通过后系统自动推送(正式结算付款、预付款均适用),也可页面点「同步金蝶」 | 付款申请单 | 一张付款申请 → 一张金蝶单 |
+| 收票单 | — | — | **暂未纳入锁定范围**(接口已预留) |
+
+> 开票申请按"张"锁:一张申请里有几张发票,就有几条锁定记录,各张单独锁、单独补偿,互不影响。但只要其中任意一张被金蝶删除,整张开票申请会被作废(见场景三)。
+
+---
+
+## 三、页面上的"金蝶锁状态"怎么读
+
+列表和详情页的「金蝶锁状态」列会显示以下标签,悬停可看原因或操作人:
+
+| 标签 | 含义 | 单据能不能操作 | 业务该做什么 |
+| --- | --- | --- | --- |
+| **金蝶锁定中**(红) | 金蝶已锁单,单据被冻结 | 不能作废、不能改动(开票申请/付款申请点作废会提示"该单据已被金蝶锁定,不可修改";结算单本身"已同步金蝶"就不可编辑/作废) | 确实要改,先去金蝶侧解锁,等状态变为"金蝶锁单"后再处理 |
+| **锁定失败·补偿中**(橙) | 金蝶单已生成,但"锁单"这一步没成功,系统正在自动重试 | 不建议作废或重建 | **不要作废单据**,等待自动补偿;着急可在页面点「同步金蝶」立即补锁 |
+| **锁定失败·已达上限**(红) | 自动补偿已重试满 5 次仍未成功 | 同上 | 点「同步金蝶」立即补锁(会重新开始补偿);仍失败请把金蝶单号和失败原因给技术排查 |
+| **金蝶删单**(红) | 金蝶侧把这张单删除了 | 结算单/开票申请已自动作废;付款申请转为"生成失败" | 结算单/开票申请:重新走业务流程;付款申请:核实后重新推送 |
+| **金蝶锁单**(橙) | 金蝶侧解锁了,但单据还在 | 锁已解除,按单据自身状态规则操作 | 注意两边不再自动联动,改动/作废前先确认金蝶侧如何处理 |
+| **结算调整重推**(橙) | 结算调整生成新单后,原单的旧锁自动解除 | 原单只做标记,不删不改 | 无需处理,以调整后的新单为准 |
+| 空白 | 没有锁记录(从未推送,或锁定早已解除且无留痕) | 按单据自身状态规则操作 | — |
+
+---
+
+## 四、主流程(业务视角)
+
+```
+单据审批通过
+ └─ 系统自动推送金蝶(失败不影响审批结果)
+ ├─ 推送失败 → 金蝶状态记"生成失败",可重推,不产生金蝶单
+ └─ 推送成功 → 回填金蝶单号 → 请求金蝶锁单
+ ├─ 锁定成功 → 单据冻结(页面:金蝶锁定中)
+ └─ 锁定失败 → 系统每5分钟自动重锁,最多5次
+ (页面:锁定失败·补偿中 / 已达上限)
+
+金蝶侧后续动作:
+ ├─ 解锁(单据还在) → 通知本系统 → 解除锁定(页面:金蝶锁单)
+ └─ 删单 → 通知本系统 → 结算单/开票申请自动作废并还原
+ 付款申请转"生成失败",等人工重推
+```
+
+---
+
+## 五、场景一:推送成功后锁定
+
+**触发方式**
+
+1. 审批通过后系统自动推送(不需要人工点);
+2. 页面手动点「同步金蝶」(补推、重推、补锁都走这里)。
+
+**系统做了什么**
+
+1. 推送成功即回填金蝶单号,随后逐张请求金蝶锁单;
+2. 每张金蝶单对应一条锁定记录,先登记"待锁"再调金蝶,保证漏不掉;
+3. 单张锁失败不影响其他张,也不影响主单的"已同步"状态。
+
+**重复操作会怎样**
+
+- 已经推送成功、已有金蝶单号的,再点「同步金蝶」不会重复生成金蝶单;
+- 已锁定成功的单据不会重复锁。
+
+**锁定期间的业务影响**
+
+- 开票申请、付款申请:作废入口被拦截;
+- 正式结算单:本身"已同步金蝶"就不能编辑、调整、作废,锁定期同样被拦截。
+
+---
+
+## 六、场景二:金蝶解锁(单据还在)
+
+1. 金蝶侧对单据执行解锁后,通知本系统;
+2. 本系统把锁定记录置为"已解锁",页面显示橙色「金蝶锁单」标签,悬停显示**谁在什么时间解锁**;
+3. 本系统单据的业务状态**不会因此改变**(结算单仍是"已同步",开票申请/付款申请仍保持原审批状态);
+4. 重复收到同一条解锁通知不会重复处理,也不会报错。
+
+> 注意:解锁只解除"金蝶锁单"这一层限制。正式结算单即使解锁,仍受"已同步金蝶不可直接作废"的约束;要作废/调整需走结算调整,或让金蝶侧删单由系统自动作废。
+
+---
+
+## 七、场景三:金蝶删单(三种单据处理不同)
+
+金蝶侧删除单据后,系统按单据类型分别处理:
+
+| 业务单据 | 本系统处理 | 单据状态 | 后续 |
+| --- | --- | --- | --- |
+| 正式结算单 | 自动作废,并还原到推送前的同步信息:清空金蝶单号、金蝶内码、同步时间,同步状态改回"未同步" | 已作废(终态),当前节点"已作废" | 变更流水记录"金蝶删单回调自动废除",操作人显示金蝶侧操作人;需重新推单请新单重走流程 |
+| 开票申请 | 自动作废,清空金蝶单号、同步时间,同步状态改回"未同步",并刷新关联结算单的开票状态 | 已作废(终态) | 操作流水记"金蝶删单回调作废",操作人为金蝶侧操作人;需重新开票请重新发起申请 |
+| 付款申请 | **不作废源单**:金蝶状态由"已生成/付款中"改为"生成失败",清空金蝶单号、内码(原值留日志) | 审批状态不变 | **不自动重推**,人工核实后重新推送;页面显示红色「金蝶删单」标签,以便与普通推送失败区分 |
+
+补充说明:
+
+- 开票申请有多张金蝶单时,任意一张被删,**整张开票申请作废**;
+- 付款申请只有"已生成/付款中"状态会转为"生成失败",已完成付款等终态不受影响;
+- 结算单作废时会同步释放项目预付核销占用,避免残留;
+- 重复的删单通知不会重复作废、重复还原。
+
+---
+
+## 八、场景四:锁定失败与自动补偿
+
+**什么情况会锁定失败**:推送成功后,请求金蝶锁单时接口报错或超时。
+
+**系统怎么处理**
+
+1. 该张单据保留"锁定失败"记录并记录原因,页面显示橙色「锁定失败·补偿中」;
+2. 定时任务自动重锁,默认每 5 分钟一轮,最多自动重试 5 次;
+3. 5 次仍未成功,页面转为红色「锁定失败·已达上限」,不再自动重试。
+
+**业务注意事项**
+
+- **锁定失败不影响已推送成功的结果**,单据仍是"已同步金蝶",不会重复生成金蝶单;
+- 补偿期间建议**先不要作废或重建单据**,避免本系统与金蝶不一致;
+- 已达上限时,在页面点「同步金蝶」会立即补锁,并重新开始补偿计数;
+- 仍失败时,请提供单据号、金蝶单号、页面上的失败原因给技术排查。
+
+> 自动补偿是否开启由各环境配置决定(默认开启锁定、默认关闭定时补偿),如发现"一直没人补锁",请联系技术确认环境配置。
+
+---
+
+## 九、场景五:结算调整重推(本系统主动解锁)
+
+已推送金蝶的正式结算单需要调整时,走结算调整:系统生成一张新的结算单并推送金蝶,**原单的旧锁自动解除**,页面显示橙色「结算调整重推」标签。
+
+- 原单只加"已调整"标记,不删除、不改业务字段;
+- 后续收付款、开票以新单为准;
+- 这是本系统主动解锁,不是金蝶回调,操作人显示发起调整的人。
+
+---
+
+## 十、常见问题
+
+**1. 单据显示"金蝶锁定中",我要改内容/作废怎么办?**
+先到金蝶侧解锁,等本系统状态变成「金蝶锁单」后再处理。正式结算单还要注意"已同步金蝶"本身就不能直接作废,需要走结算调整或由金蝶删单。
+
+**2. 金蝶那边已经解锁了,为什么本系统还不能作废?**
+解锁只解除"金蝶锁单",不改变单据的业务状态。结算单仍受"已同步不可作废"限制;开票申请/付款申请解锁后恢复可作废,但作废不会反向同步金蝶,请确认金蝶侧单据的处理方式。
+
+**3. 显示"金蝶删单"是什么意思?**
+金蝶侧把单据删了。结算单、开票申请已自动作废,需要重新走流程;付款申请没有作废,但变成了"生成失败",核实后重新推送即可。
+
+**4. 显示"锁定失败"要不要人工处理?**
+先等系统自动补偿(每 5 分钟一次,最多 5 次)。已达上限时点「同步金蝶」立即补锁;补偿期间不要作废或重建单据。
+
+**5. 金蝶重复发解锁/删单通知,会不会把单据作废两次?**
+不会。系统按"当前是否仍锁定/是否已作废"判断,重复通知自动跳过。
+
+**6. 开票申请有多张发票,其中一张锁失败怎么办?**
+各张独立锁定、独立补偿,其他张照常冻结。页面会提示哪一张失败;该张补偿成功前,整张申请仍按"锁定失败"提示。
+
+**7. 已推送的单据能不能直接在 TMS 作废?**
+不能。开票申请、付款申请在锁定中被拦截;正式结算单已同步即不可作废。要么金蝶侧删单由系统自动作废,要么走结算调整生成新单。
+
+---
+
+## 十一、留痕在哪里查
+
+| 想查什么 | 在哪看 |
+| --- | --- |
+| 单据当前锁状态、失败原因、重试次数、解锁操作人与时间 | 列表/详情「金蝶锁状态」列,悬停查看提示 |
+| 结算单被金蝶删单自动作废的过程 | 结算单变更流水(内容含"金蝶删单回调自动废除") |
+| 开票申请被金蝶删单自动作废的过程 | 开票申请操作流水(动作"金蝶删单回调作废") |
+| 付款申请解锁原因 | 付款申请金蝶失败原因("金蝶侧单据已删除解锁……等待重新推送") |
+| 每张金蝶单的完整锁定/解锁明细 | 技术侧锁定记录表 `blade_kingdee_bill_lock`(一张金蝶单一行) |
+
+---
+
+## 十二、口径边界(给管理员/技术)
+
+1. 锁定总开关关闭时:推送后不锁单,金蝶的解锁/删单通知只应答、不处理;
+2. 补偿定时任务需按环境开启,默认每 5 分钟一轮,重试上限 5 次;
+3. 金蝶回调为无登录态公开接口,需保持网关白名单配置有效(异常时金蝶会重试);
+4. 锁单与解锁均为逐张调用,单张成败互不影响;解锁回调整批处理,任一张还原失败则整批回滚、由金蝶重试闭环;
+5. 后续新增单据(如收票单)接入锁定时,锁定状态、解锁标识与本文口径保持一致即可复用。
From 410c7c167cad4c7b5a24077b16c7e5e102c6a746 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 11:58:49 +0800
Subject: [PATCH 46/63] =?UTF-8?q?fix:=20=E5=AE=A2=E5=95=86=E9=A3=8E?=
=?UTF-8?q?=E9=99=A9=E7=AD=89=E7=BA=A7=E7=AD=9B=E9=80=89=E4=B8=8D=E5=86=8D?=
=?UTF-8?q?=E6=8E=92=E9=99=A4=E9=BB=91=E5=90=8D=E5=8D=95=EF=BC=88=E4=B8=8E?=
=?UTF-8?q?=E7=BB=84=E5=90=88=E5=B1=95=E7=A4=BA=E5=8F=A3=E5=BE=84=E4=B8=80?=
=?UTF-8?q?=E8=87=B4=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/mapper/CustomerArchiveMapper.xml | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
index fd345444..a0038ecc 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
@@ -181,20 +181,20 @@
AND create_time <= #{customer.createTimeEnd}
-
+
AND hcs_black_status = 'yes'
- AND hcs_risk_level = '2' AND (hcs_black_status IS NULL OR hcs_black_status != 'yes')
+ AND hcs_risk_level = '2'
- AND hcs_risk_level = '1' AND (hcs_black_status IS NULL OR hcs_black_status != 'yes')
+ AND hcs_risk_level = '1'
- AND hcs_risk_level = '0' AND (hcs_black_status IS NULL OR hcs_black_status != 'yes')
+ AND hcs_risk_level = '0'
From 4ef3e90d5d110be9542856c5ffda774581ca1155 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 13:33:56 +0800
Subject: [PATCH 47/63] =?UTF-8?q?fix:=20=E5=AE=A2=E5=95=86=E9=BB=91?=
=?UTF-8?q?=E5=90=8D=E5=8D=95=E5=8F=A3=E5=BE=84=E8=B0=83=E6=95=B4=E4=B8=BA?=
=?UTF-8?q?=E4=BB=85=E9=87=91=E8=9D=B6=E9=BB=91=E5=90=8D=E5=8D=95=E5=91=BD?=
=?UTF-8?q?=E4=B8=AD=EF=BC=88A801-5=EF=BC=89=EF=BC=8CC101-1=E9=BB=91?=
=?UTF-8?q?=E5=90=8D=E5=8D=95=E7=8A=B6=E6=80=81=E4=B8=8D=E7=94=A8=E4=BA=8E?=
=?UTF-8?q?=E5=B1=95=E7=A4=BA=E4=B8=8E=E7=AD=9B=E9=80=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/mapper/CustomerArchiveMapper.xml | 4 ++--
.../service/impl/CustomerArchiveServiceImpl.java | 2 +-
.../support/hcs/HcsCreditCheckSupport.java | 16 ++++++++--------
...6-10-30-客商黑名单与风险等级业务说明(业务侧).md | 8 +++++---
...6-10-30-客商黑名单与风险等级运维手册(运维侧).md | 2 +-
5 files changed, 17 insertions(+), 15 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
index a0038ecc..c3d3b11a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
@@ -181,11 +181,11 @@
AND create_time <= #{customer.createTimeEnd}
-
+
- AND hcs_black_status = 'yes'
+ AND (kingdee_black_category IS NOT NULL OR kingdee_black_reason IS NOT NULL)
AND hcs_risk_level = '2'
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
index 7c0994ac..10d92f1b 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
@@ -2158,7 +2158,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl parts = new ArrayList<>();
- boolean blacklist = blackStatusYes || Func.isNotEmpty(blackCategory) || Func.isNotEmpty(blackReason);
+ boolean blacklist = Func.isNotEmpty(blackCategory) || Func.isNotEmpty(blackReason);
if (blacklist) {
parts.add(Func.isNotEmpty(blackCategory)
? "被列入外部黑名单(失信类别:" + blackCategory + ")"
diff --git a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md
index e57af84c..33787616 100644
--- a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md
+++ b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级业务说明(业务侧).md
@@ -17,7 +17,7 @@ TMS 从**两个外部系统**获取客商风险数据,每日自动更新到客
| 名词 | 含义 |
|---|---|
-| 黑名单 | 客商被列入外部(国资委)黑名单。金蝶分 **A 类(禁止交易类,不允许交易)** 和 **B 类(风险警示类,需关注)** |
+| 黑名单 | 客商被列入外部(国资委)黑名单,**以金蝶黑名单查询(A801-5)命中为准**,命中记录含失信类别与列入事由。金蝶分 **A 类(禁止交易类,不允许交易)** 和 **B 类(风险警示类,需关注)** |
| 失信类别 | 黑名单的分类名称(如"禁止交易类"),来自金蝶黑名单记录 |
| 列入事由 | 被列入黑名单的具体原因(如"涉贪污受贿裁判相关提及方(3年以上及其他)"),来自金蝶黑名单记录 |
| 风险等级 | 合同合规系统对客商的风险评级:0=低风险、1=中风险、2=高风险 |
@@ -66,14 +66,16 @@ flowchart TD
| 客商情况 | 显示 | 颜色 |
|---|---|---|
-| 黑名单 + 风险等级(组合展示,等级在前) | 高风险、黑名单 / 中风险、黑名单 / 低风险、黑名单 | 红 |
-| 仅黑名单(无等级数据) | 黑名单 | 红 |
+| 金蝶黑名单命中 + 风险等级(组合展示,等级在前) | 高风险、黑名单 / 中风险、黑名单 / 低风险、黑名单 | 红 |
+| 金蝶黑名单命中(无等级数据) | 黑名单 | 红 |
| 仅高风险 | 高风险 | 红 |
| 中风险 | 中风险 | 黄 |
| 低风险 | 低风险 | 绿 |
| 内部组织客商 | 无风险(默认,不查询) | 绿 |
| 尚未查询到数据 | - | — |
+> "黑名单"仅指**金蝶黑名单命中**;合同合规系统返回的黑名单状态字段不用于列表展示(仅在付款提交强制校验中使用)。
+
- 红色标签(黑名单/高风险)**鼠标悬浮**可查看明细:失信类别、列入事由、风险等级。
- 搜索栏新增**客商风险**下拉筛选:黑名单 / 高风险 / 中风险 / 低风险。
- 数据为每日凌晨刷新结果,当天有变动的以次日刷新为准。
diff --git a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md
index 6cee8b25..e39f9934 100644
--- a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md
+++ b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md
@@ -105,7 +105,7 @@
唯一出口:`HcsCreditCheckSupport.buildRiskDetail(customerName, blackStatusYes, riskLevel, blackCategory, blackReason)`,
格式:`【客商名称】:被列入外部黑名单(失信类别:xx);{列入事由};风险等级:xx`
-- 黑名单段:合同系统黑名单 **或** 金蝶命中(类别/事由任一非空);
+- 黑名单段:**仅金蝶命中**(类别/事由任一非空);C101-1 的 `hcs_black_status`(fdBlackStatus)不参与展示,仅付款提交强制校验使用;
- 等级段:`risk_level` 字典翻译(`DictBizCache` 未命中兜底 `DictCache`);
- 列表(`hcs_risk_detail`,列表查询时组装不落库)与表单实时提示(`riskTip`)同口径。
From b2c6533fab593353b88c8490b260ca6061d20dca Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 14:08:08 +0800
Subject: [PATCH 48/63] =?UTF-8?q?refactor:=20=E5=AE=A2=E5=95=86=E9=BB=91?=
=?UTF-8?q?=E5=90=8D=E5=8D=95=E7=8A=B6=E6=80=81=E5=AD=97=E6=AE=B5=E6=94=B9?=
=?UTF-8?q?kingdee=5F=E5=89=8D=E7=BC=80=EF=BC=88hcs=5Fblack=5Fstatus?=
=?UTF-8?q?=E6=94=B9=E5=90=8Dkingdee=5Fblack=5Fstatus=E5=B9=B6=E8=BF=81?=
=?UTF-8?q?=E7=A7=BB=E6=95=B0=E6=8D=AE=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/CustomerArchive.java | 2 +-
.../mapper/CustomerArchiveMapper.xml | 4 +--
.../support/hcs/HcsCreditCheckSupport.java | 2 +-
.../task/CustomerRiskRefreshTask.java | 2 +-
...0-30-客商黑名单与风险等级运维手册(运维侧).md | 8 +++---
doc/sql/transport/blade_customer_archive.sql | 4 +--
...ade_customer_archive_hcs_risk_20261030.sql | 25 +++++++++++++------
7 files changed, 29 insertions(+), 18 deletions(-)
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
index 54987c07..4ada80e1 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
@@ -186,7 +186,7 @@ public class CustomerArchive extends TenantEntity {
private String hcsRiskLevel;
@Schema(description = "合规客商黑名单状态(C101-1,每日定时刷新):yes=是,no=否,handing=处理中")
- private String hcsBlackStatus;
+ private String kingdeeBlackStatus;
@Schema(description = "合规客商风险最近查询时间(C101-1)")
private LocalDateTime hcsRiskTime;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
index c3d3b11a..077b5a75 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml
@@ -55,7 +55,7 @@
-
+
@@ -114,7 +114,7 @@
kingdee_fail_reason,
kingdee_sync_time,
hcs_risk_level,
- hcs_black_status,
+ kingdee_black_status,
hcs_risk_time,
kingdee_black_category,
kingdee_black_reason,
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
index e591917c..ef42c4ac 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
@@ -477,7 +477,7 @@ public class HcsCreditCheckSupport {
CustomerArchive riskPatch = new CustomerArchive();
riskPatch.setId(customerArchive.getId());
riskPatch.setHcsRiskLevel(risk.getFdRiskLevel());
- riskPatch.setHcsBlackStatus(risk.getFdBlackStatus());
+ riskPatch.setKingdeeBlackStatus(risk.getFdBlackStatus());
riskPatch.setHcsRiskTime(LocalDateTime.now());
// 金蝶黑名单命中落原始字段(失信类别/列入事由),明细文案由列表查询统一组装
BfiBlacklistVO hit = Func.isEmpty(customerArchive.getUnifiedCreditCode())
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java
index 862f6cde..79522200 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/CustomerRiskRefreshTask.java
@@ -37,7 +37,7 @@ import org.springframework.stereotype.Component;
* 客商风险每日定时刷新任务(C101-1)
*
* 合同期限范围内的生效合同,每日定时查询其甲乙方关联客商的风险等级/黑名单状态并落库
- * (blade_customer_archive.hcs_risk_level/hcs_black_status/hcs_risk_time),
+ * (blade_customer_archive.hcs_risk_level/kingdee_black_status/hcs_risk_time),
* 作为客商档案列表「客商风险」列展示与筛选的数据源;查询逐次留痕 blade_hcs_check_log,
* 单个失败记录失败原因日志备查。
* 业务逻辑收敛在 {@link HcsCreditCheckSupport#refreshCustomerRiskForActiveContracts()},便于手动触发联调。
diff --git a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md
index e39f9934..60e6d87b 100644
--- a/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md
+++ b/doc/docs/业务流程/2026-10-30-客商黑名单与风险等级运维手册(运维侧).md
@@ -15,7 +15,7 @@
每日 02:30 CustomerRiskRefreshTask │
┌──────▼─────────┐
│ blade_customer_archive
- │ hcs_risk_level / hcs_black_status
+ │ hcs_risk_level / kingdee_black_status
│ hcs_risk_time
│ kingdee_black_category / kingdee_black_reason
└────────────────┘
@@ -25,7 +25,7 @@
| 线 | 接口 | 查询键 | 落库字段 |
|---|---|---|---|
-| 合同合规风险(C101-1) | `getRelativeRisk` | 客商编号(fdNumber) | `hcs_risk_level`、`hcs_black_status`、`hcs_risk_time` |
+| 合同合规风险(C101-1) | `getRelativeRisk` | 客商编号(fdNumber) | `hcs_risk_level`、`kingdee_black_status`、`hcs_risk_time` |
| 金蝶黑名单(A801-5) | `queryBlacklist` | 税号/统一社会信用代码(number) | `kingdee_black_category`、`kingdee_black_reason` |
**调用场景**(C101-1):
@@ -73,12 +73,12 @@
| 字段 | 类型 | 说明 |
|---|---|---|
| `hcs_risk_level` | varchar(8) | C101-1 风险等级:0=低/1=中/2=高风险 |
-| `hcs_black_status` | varchar(8) | C101-1 黑名单状态:yes/no/handing |
+| `kingdee_black_status` | varchar(8) | C101-1 黑名单状态:yes/no/handing(数据归金蝶口径,故用 kingdee_ 前缀) |
| `hcs_risk_time` | datetime | 风险最近查询时间 |
| `kingdee_black_category` | varchar(200) | 金蝶黑名单失信类别(awrp_combofield_title 兜底 awrp_combofield) |
| `kingdee_black_reason` | varchar(1000) | 金蝶黑名单列入事由(awrp_textareafield) |
-索引:`idx_hcs_risk_level`、`idx_hcs_black_status`。
+索引:`idx_hcs_risk_level`、`idx_kingdee_black_status`。
### 4.2 脚本(幂等可重复执行)
diff --git a/doc/sql/transport/blade_customer_archive.sql b/doc/sql/transport/blade_customer_archive.sql
index 0350134b..1c0268fd 100644
--- a/doc/sql/transport/blade_customer_archive.sql
+++ b/doc/sql/transport/blade_customer_archive.sql
@@ -47,7 +47,7 @@ CREATE TABLE `blade_customer_archive` (
`kingdee_fail_reason` varchar(500) NULL DEFAULT NULL COMMENT '金蝶推送失败原因',
`kingdee_sync_time` datetime NULL DEFAULT NULL COMMENT '金蝶推送时间',
`hcs_risk_level` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合规客商风险等级(C101-1,0=低风险/1=中风险/2=高风险)',
- `hcs_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合规客商黑名单状态(C101-1,yes=是/no=否/handing=处理中)',
+ `kingdee_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合规客商黑名单状态(C101-1,yes=是/no=否/handing=处理中)',
`hcs_risk_time` datetime DEFAULT NULL COMMENT '客商风险最近查询时间(C101-1)',
`kingdee_black_category` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '金蝶黑名单失信类别(A801-5 awrp_combofield_title,缺失兜底 awrp_combofield)',
`kingdee_black_reason` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '金蝶黑名单列入事由(A801-5 awrp_textareafield)',
@@ -65,7 +65,7 @@ CREATE TABLE `blade_customer_archive` (
KEY `idx_customer_archive_name` (`tenant_id`, `full_name`, `short_name`) USING BTREE,
KEY `idx_kingdee_status` (`kingdee_status`) USING BTREE,
KEY `idx_hcs_risk_level` (`hcs_risk_level`) USING BTREE,
- KEY `idx_hcs_black_status` (`hcs_black_status`) USING BTREE
+ KEY `idx_kingdee_black_status` (`kingdee_black_status`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '客商档案';
-- ----------------------------
diff --git a/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql b/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
index f67ca273..696a8780 100644
--- a/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
+++ b/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
@@ -13,11 +13,15 @@ PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
--- ② 列 hcs_black_status
+-- ② 列 kingdee_black_status(C101-1 黑名单状态;此前误用 hcs_ 前缀,存在旧列时 CHANGE 改名迁移数据)
SET @ddl = (SELECT IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND COLUMN_NAME = 'hcs_black_status') = 0,
- 'ALTER TABLE `blade_customer_archive` ADD COLUMN `hcs_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规客商黑名单状态(C101-1,yes=是/no=否/handing=处理中)'' AFTER `hcs_risk_level`',
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND COLUMN_NAME = 'kingdee_black_status') = 0,
+ (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND COLUMN_NAME = 'hcs_black_status') > 0,
+ 'ALTER TABLE `blade_customer_archive` CHANGE COLUMN `hcs_black_status` `kingdee_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规客商黑名单状态(C101-1,yes=是/no=否/handing=处理中)''',
+ 'ALTER TABLE `blade_customer_archive` ADD COLUMN `kingdee_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规客商黑名单状态(C101-1,yes=是/no=否/handing=处理中)'' AFTER `hcs_risk_level`')),
'SELECT 1'));
PREPARE stmt FROM @ddl;
EXECUTE stmt;
@@ -27,7 +31,7 @@ DEALLOCATE PREPARE stmt;
SET @ddl = (SELECT IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND COLUMN_NAME = 'hcs_risk_time') = 0,
- 'ALTER TABLE `blade_customer_archive` ADD COLUMN `hcs_risk_time` datetime DEFAULT NULL COMMENT ''客商风险最近查询时间(C101-1)'' AFTER `hcs_black_status`',
+ 'ALTER TABLE `blade_customer_archive` ADD COLUMN `hcs_risk_time` datetime DEFAULT NULL COMMENT ''客商风险最近查询时间(C101-1)'' AFTER `kingdee_black_status`',
'SELECT 1'));
PREPARE stmt FROM @ddl;
EXECUTE stmt;
@@ -43,16 +47,23 @@ PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
--- ⑤ 索引 idx_hcs_black_status
+-- ⑤ 索引 idx_kingdee_black_status
SET @ddl = (SELECT IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
- WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND INDEX_NAME = 'idx_hcs_black_status') = 0,
- 'ALTER TABLE `blade_customer_archive` ADD INDEX idx_hcs_black_status (hcs_black_status)',
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND INDEX_NAME = 'idx_kingdee_black_status') = 0,
+ 'ALTER TABLE `blade_customer_archive` ADD INDEX idx_kingdee_black_status (kingdee_black_status)',
'SELECT 1'));
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
+-- ⑥ 旧索引清理:② CHANGE 改名后旧索引 idx_hcs_black_status 会自动跟随新列残留,若存在则删除(由 idx_kingdee_black_status 取代)
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND INDEX_NAME = 'idx_hcs_black_status') > 0,
+ 'ALTER TABLE `blade_customer_archive` DROP INDEX idx_hcs_black_status',
+ 'SELECT 1'));
+
-- ⑥ 列 hcs_black_category / hcs_black_reason(A801-5 金蝶黑名单原始字段:失信类别、列入事由,明细文案由后台组装)
SET @ddl = (SELECT IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
From 6f941697e9b23a3b0f4c0d36845c6664f75bce3c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 14:11:49 +0800
Subject: [PATCH 49/63] =?UTF-8?q?refactor:=20=E5=90=88=E5=90=8C=E8=A1=A8?=
=?UTF-8?q?=E9=BB=91=E5=90=8D=E5=8D=95=E7=8A=B6=E6=80=81=E5=AD=97=E6=AE=B5?=
=?UTF-8?q?=E6=94=B9kingdee=5F=E5=89=8D=E7=BC=80=EF=BC=88hcs=5Fblack=5Fsta?=
=?UTF-8?q?tus=E6=94=B9=E5=90=8Dkingdee=5Fblack=5Fstatus=E5=B9=B6=E8=BF=81?=
=?UTF-8?q?=E7=A7=BB=E6=95=B0=E6=8D=AE=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/pojo/entity/ContractChange.java | 2 +-
.../transport/pojo/entity/ContractManage.java | 2 +-
.../support/hcs/HcsContractSyncSupport.java | 4 +--
..._contract_black_status_rename_20261030.sql | 32 +++++++++++++++++++
4 files changed, 36 insertions(+), 4 deletions(-)
create mode 100644 doc/sql/transport/lqj/temp/04_blade_contract_black_status_rename_20261030.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java
index 936ec485..0ca18f11 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java
@@ -208,7 +208,7 @@ public class ContractChange extends TenantEntity {
private String hcsRiskLevel;
@Schema(description = "合规客商黑名单状态(展示用)")
- private String hcsBlackStatus;
+ private String kingdeeBlackStatus;
@Schema(description = "审批状态")
private String approvalStatus;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java
index 655ae234..adb92905 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java
@@ -244,7 +244,7 @@ public class ContractManage extends TenantEntity {
private String hcsRiskLevel;
@Schema(description = "合规客商黑名单状态(C101-1,甲方/乙方取是)")
- private String hcsBlackStatus;
+ private String kingdeeBlackStatus;
@Schema(description = "合规风险校验时间")
private LocalDateTime hcsRiskTime;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
index 30fe801d..81b95b30 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
@@ -282,7 +282,7 @@ public class HcsContractSyncSupport {
// 黑名单状态:任一方黑名单即为是
boolean blacklist = riskPartyA.blacklist() || riskPartyB.blacklist();
contractManage.setHcsRiskLevel(riskLevel);
- contractManage.setHcsBlackStatus(blacklist ? BLACK_STATUS_YES : "no");
+ contractManage.setKingdeeBlackStatus(blacklist ? BLACK_STATUS_YES : "no");
contractManage.setHcsRiskTime(LocalDateTime.now());
contractManageMapper.updateById(contractManage);
boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || blacklist;
@@ -312,7 +312,7 @@ public class HcsContractSyncSupport {
contractManage.setCurrentNode(HcsSyncConstant.SPEC_REVIEW_NODE);
contractManageMapper.updateById(contractManage);
log.info("HCS合同高风险/黑名单挂起特批审核:contractNo={},riskLevel={},blackStatus={},挂起节点={}",
- contractManage.getContractNo(), contractManage.getHcsRiskLevel(), contractManage.getHcsBlackStatus(),
+ contractManage.getContractNo(), contractManage.getHcsRiskLevel(), contractManage.getKingdeeBlackStatus(),
contractManage.getCurrentNode());
}
diff --git a/doc/sql/transport/lqj/temp/04_blade_contract_black_status_rename_20261030.sql b/doc/sql/transport/lqj/temp/04_blade_contract_black_status_rename_20261030.sql
new file mode 100644
index 00000000..bc0a7a3d
--- /dev/null
+++ b/doc/sql/transport/lqj/temp/04_blade_contract_black_status_rename_20261030.sql
@@ -0,0 +1,32 @@
+-- 合同表黑名单状态列改 kingdee_ 前缀(黑名单数据归金蝶口径,与客商档案表字段命名统一):
+-- blade_contract_manage.hcs_black_status -> kingdee_black_status
+-- blade_contract_change.hcs_black_status -> kingdee_black_status
+-- 幂等可重复执行:新列不存在时,旧列存在则 CHANGE 改名迁移数据,否则新增;旧索引自动跟随列名,无需处理。
+
+-- ① blade_contract_manage
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_contract_manage' AND COLUMN_NAME = 'kingdee_black_status') = 0,
+ (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_contract_manage' AND COLUMN_NAME = 'hcs_black_status') > 0,
+ 'ALTER TABLE `blade_contract_manage` CHANGE COLUMN `hcs_black_status` `kingdee_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规客商黑名单状态(C101-1,任一方黑名单记是)''',
+ 'ALTER TABLE `blade_contract_manage` ADD COLUMN `kingdee_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规客商黑名单状态(C101-1,任一方黑名单记是)'' AFTER `hcs_risk_level`')),
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ② blade_contract_change
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_contract_change' AND COLUMN_NAME = 'kingdee_black_status') = 0,
+ (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_contract_change' AND COLUMN_NAME = 'hcs_black_status') > 0,
+ 'ALTER TABLE `blade_contract_change` CHANGE COLUMN `hcs_black_status` `kingdee_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规客商黑名单状态(展示用)''',
+ 'ALTER TABLE `blade_contract_change` ADD COLUMN `kingdee_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''合规客商黑名单状态(展示用)'' AFTER `hcs_risk_level`')),
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
From f42987e6c55cc21b33cf8f916df5cff46b9952cc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 14:21:33 +0800
Subject: [PATCH 50/63] =?UTF-8?q?feat:=20=E5=90=88=E5=90=8C=E6=8E=A8?=
=?UTF-8?q?=E9=80=81=E9=BB=91=E5=90=8D=E5=8D=95=E5=88=A4=E5=AE=9A=E5=A4=8D?=
=?UTF-8?q?=E7=94=A8HcsCreditCheckSupport=E9=87=91=E8=9D=B6=E5=91=BD?=
=?UTF-8?q?=E4=B8=AD=E5=85=AC=E5=85=B1=E6=96=B9=E6=B3=95=EF=BC=88=E4=B8=8D?=
=?UTF-8?q?=E5=86=8D=E5=8F=96C101-1=E9=BB=91=E5=90=8D=E5=8D=95=E7=8A=B6?=
=?UTF-8?q?=E6=80=81=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsContractSyncSupport.java | 29 ++++++++++---------
.../support/hcs/HcsCreditCheckSupport.java | 16 ++++++++++
2 files changed, 31 insertions(+), 14 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
index 81b95b30..7775a849 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
@@ -112,6 +112,7 @@ public class HcsContractSyncSupport {
private final HcsAttachmentSupport hcsAttachmentSupport;
private final HcsExeStageSyncSupport hcsExeStageSyncSupport;
private final HcsContractSpecSyncSupport hcsContractSpecSyncSupport;
+ private final HcsCreditCheckSupport hcsCreditCheckSupport;
/**
* 合同金蝶同步结果接收处理(合规 contractKingdee/result 推送,D903,投送人/钢厂/结算中心合同共用):
@@ -279,16 +280,16 @@ public class HcsContractSyncSupport {
RiskResult riskPartyB = queryRisk(contractManage, "乙方", contractManage.getPartyB());
// 风险等级:甲乙双方取先查到的非空值(无法量化比较,留痕双方原始值,联调后细化)
String riskLevel = StringUtil.isNotBlank(riskPartyA.riskLevel()) ? riskPartyA.riskLevel() : riskPartyB.riskLevel();
- // 黑名单状态:任一方黑名单即为是
- boolean blacklist = riskPartyA.blacklist() || riskPartyB.blacklist();
+ // 黑名单状态:任一方金蝶黑名单(A801-5)命中即为是(复用 HcsCreditCheckSupport.isKingdeeBlacklistHit 公共口径)
+ boolean blacklist = riskPartyA.kingdeeHit() || riskPartyB.kingdeeHit();
contractManage.setHcsRiskLevel(riskLevel);
contractManage.setKingdeeBlackStatus(blacklist ? BLACK_STATUS_YES : "no");
contractManage.setHcsRiskTime(LocalDateTime.now());
contractManageMapper.updateById(contractManage);
boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || blacklist;
- log.info("HCS合同风险校验完成:contractNo={},甲方[{}={}/{}],乙方[{}={}/{}],高风险/黑名单={}",
- contractManage.getContractNo(), contractManage.getPartyA(), riskPartyA.blackStatus(), riskPartyA.riskLevel(),
- contractManage.getPartyB(), riskPartyB.blackStatus(), riskPartyB.riskLevel(), risky);
+ log.info("HCS合同风险校验完成:contractNo={},甲方[金蝶黑名单={},等级={}],乙方[金蝶黑名单={},等级={}],高风险/黑名单={}",
+ contractManage.getContractNo(), riskPartyA.kingdeeHit(), riskPartyA.riskLevel(),
+ riskPartyB.kingdeeHit(), riskPartyB.riskLevel(), risky);
return risky;
}
@@ -346,13 +347,13 @@ public class HcsContractSyncSupport {
if (customerArchive == null) {
writeCheckLog(contractManage, HcsSyncConstant.CHECK_RESULT_SKIP,
standing + "方[" + partyName + "]反查不到客商编号,跳过风险校验(§9-U6)", null);
- return new RiskResult(null, null, false);
+ return new RiskResult(false, null);
}
// 内部组织类型客商默认无风险,不调 C101-1(与展示/提交/付款链路口径一致)
if (HcsCreditCheckSupport.isInternalCustomer(customerArchive)) {
writeCheckLog(contractManage, HcsSyncConstant.CHECK_RESULT_SKIP,
standing + "方[" + partyName + "]为内部组织客商,默认无风险跳过校验", null);
- return new RiskResult(null, null, false);
+ return new RiskResult(false, null);
}
HcsRiskQueryDTO param = new HcsRiskQueryDTO();
// 【必填】客商编号:按甲乙方名称反查 CustomerArchive 客商编码(§9-U6)
@@ -364,12 +365,12 @@ public class HcsContractSyncSupport {
throw new HcsException("C101-1 客商风险查询失败:" + (result == null ? "响应为空" : result.getMsg()));
}
HcsRiskVO risk = result.getData();
- String blackStatus = risk == null ? null : risk.getFdBlackStatus();
String riskLevel = risk == null ? null : risk.getFdRiskLevel();
- boolean blacklist = BLACK_STATUS_YES.equalsIgnoreCase(blackStatus);
- writeCheckLog(contractManage, blacklist ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS,
- standing + "方[" + partyName + "]黑名单=" + blackStatus + ",风险等级=" + riskLevel, result.getTraceId());
- return new RiskResult(blackStatus, riskLevel, blacklist);
+ // 黑名单判定复用公共方法:仅以金蝶黑名单(A801-5)命中为准,C101-1 黑名单状态不参与
+ boolean kingdeeHit = hcsCreditCheckSupport.isKingdeeBlacklistHit(customerArchive.getUnifiedCreditCode());
+ writeCheckLog(contractManage, kingdeeHit ? HcsSyncConstant.CHECK_RESULT_FAIL : HcsSyncConstant.CHECK_RESULT_PASS,
+ standing + "方[" + partyName + "]金蝶黑名单=" + kingdeeHit + ",风险等级=" + riskLevel, result.getTraceId());
+ return new RiskResult(kingdeeHit, riskLevel);
}
/**
@@ -780,8 +781,8 @@ public class HcsContractSyncSupport {
}
/**
- * 单方风险查询结果
+ * 单方风险查询结果(kingdeeHit=金蝶黑名单 A801-5 是否命中,公共口径见 {@link HcsCreditCheckSupport#isKingdeeBlacklistHit})
*/
- private record RiskResult(String blackStatus, String riskLevel, boolean blacklist) {
+ private record RiskResult(boolean kingdeeHit, String riskLevel) {
}
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
index ef42c4ac..a9371bec 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
@@ -358,6 +358,22 @@ public class HcsCreditCheckSupport {
return StringUtil.isBlank(levelName) ? null : levelName;
}
+ /**
+ * 金蝶黑名单命中判定(A801-5,公共方法):按统一社会信用代码/税号查询,
+ * 黑名单记录存在即命中;查询失败静默返回 false(queryBlacklistByCodes 内记 error 日志备查)。
+ * 展示组装(buildRiskDetail)、列表筛选、合同推送链路(checkRelativeRisk)统一复用本方法口径
+ *
+ * @param creditCode 统一社会信用代码/税号
+ * @return 是否命中金蝶黑名单
+ */
+ public boolean isKingdeeBlacklistHit(String creditCode) {
+ if (StringUtil.isBlank(creditCode)) {
+ return false;
+ }
+ String code = creditCode.trim().toUpperCase();
+ return this.queryBlacklistByCodes(List.of(code)).get(code) != null;
+ }
+
/**
* 组装客商风险明细文案(后台统一组装、前端只展示;列表悬浮明细与表单实时提示共用本方法,口径一致),
* 格式:【客商名称】:被列入外部黑名单(失信类别:xx);{列入事由};风险等级:xx,分段说明:
From d5033f3b05d60241a2bcb985e62e088d31373823 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 14:41:41 +0800
Subject: [PATCH 51/63] =?UTF-8?q?refactor:=20=E5=90=88=E5=90=8C=E8=A1=A8?=
=?UTF-8?q?=E7=A7=BB=E9=99=A4=E9=A3=8E=E9=99=A9=E7=95=99=E7=97=95=E5=AD=97?=
=?UTF-8?q?=E6=AE=B5=EF=BC=8C=E5=AE=A2=E5=95=86=E9=A3=8E=E9=99=A9=E6=94=B9?=
=?UTF-8?q?=E4=B8=BA=E5=88=97=E8=A1=A8=E8=AF=A6=E6=83=85=E5=AE=9E=E6=97=B6?=
=?UTF-8?q?=E5=85=B3=E8=81=94=E5=AE=A2=E5=95=86=E6=A1=A3=E6=A1=88=E7=BB=84?=
=?UTF-8?q?=E8=A3=85=EF=BC=88=E5=90=AB=E5=8F=98=E6=9B=B4=E5=8D=95=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/pojo/entity/ContractChange.java | 6 --
.../transport/pojo/entity/ContractManage.java | 9 ---
.../transport/pojo/vo/ContractChangeVO.java | 8 +++
.../transport/pojo/vo/ContractManageVO.java | 8 +++
.../transport/constant/HcsSyncConstant.java | 10 ++++
.../impl/ContractChangeServiceImpl.java | 35 +++++++++++
.../impl/ContractManageServiceImpl.java | 33 +++++++++++
.../support/hcs/HcsContractSyncSupport.java | 10 +---
.../support/hcs/HcsCreditCheckSupport.java | 59 +++++++++++++++++++
...ade_contract_risk_fields_drop_20261030.sql | 54 +++++++++++++++++
10 files changed, 210 insertions(+), 22 deletions(-)
create mode 100644 doc/sql/transport/lqj/temp/05_blade_contract_risk_fields_drop_20261030.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java
index 0ca18f11..dacae817 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java
@@ -204,12 +204,6 @@ public class ContractChange extends TenantEntity {
@Schema(description = "其他事项说明")
private String remark;
- @Schema(description = "合规客商风险等级(展示用)")
- private String hcsRiskLevel;
-
- @Schema(description = "合规客商黑名单状态(展示用)")
- private String kingdeeBlackStatus;
-
@Schema(description = "审批状态")
private String approvalStatus;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java
index adb92905..ec4d89f9 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractManage.java
@@ -240,15 +240,6 @@ public class ContractManage extends TenantEntity {
@Schema(description = "合规履约计划ID(C103-9 返回 fdExeplanId)")
private String hcsExeplanId;
- @Schema(description = "合规客商风险等级(C101-1,甲方/乙方取高)")
- private String hcsRiskLevel;
-
- @Schema(description = "合规客商黑名单状态(C101-1,甲方/乙方取是)")
- private String kingdeeBlackStatus;
-
- @Schema(description = "合规风险校验时间")
- private LocalDateTime hcsRiskTime;
-
@Schema(description = "合规特批流程ID(C103-8 返回)")
private String hcsSpecFdId;
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractChangeVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractChangeVO.java
index 086d498e..f43cbb7a 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractChangeVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractChangeVO.java
@@ -67,4 +67,12 @@ public class ContractChangeVO extends ContractChange {
@Schema(description = "同步状态名称")
private String syncStatusName;
+ @TableField(exist = false)
+ @Schema(description = "客商风险等级(甲乙双方关联客商档案实时组装,等级取高):0=低风险,1=中风险,2=高风险")
+ private String hcsRiskLevel;
+
+ @TableField(exist = false)
+ @Schema(description = "客商黑名单状态(甲乙双方关联客商档案实时组装,任一方金蝶黑名单命中记是):yes/no")
+ private String kingdeeBlackStatus;
+
}
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java
index 5f76a439..a842a147 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java
@@ -83,4 +83,12 @@ public class ContractManageVO extends ContractManage {
@Schema(description = "审核状态名称")
private String approvalStatusName;
+ @TableField(exist = false)
+ @Schema(description = "客商风险等级(甲乙双方关联客商档案实时组装,等级取高):0=低风险,1=中风险,2=高风险")
+ private String hcsRiskLevel;
+
+ @TableField(exist = false)
+ @Schema(description = "客商黑名单状态(甲乙双方关联客商档案实时组装,任一方金蝶黑名单命中记是):yes/no")
+ private String kingdeeBlackStatus;
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index 7f870d9a..272a735b 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -177,6 +177,16 @@ public final class HcsSyncConstant {
*/
public static final String CUSTOMER_KIND_INTERNAL = "internal";
+ /**
+ * C101-1 黑名单状态:是(客商档案/合同客商风险展示与组装用)
+ */
+ public static final String BLACK_STATUS_YES = "yes";
+
+ /**
+ * C101-1 黑名单状态:否
+ */
+ public static final String BLACK_STATUS_NO = "no";
+
/**
* 合同审核通过状态集合(首推/变更通过,客商风险定时刷新扫描口径与 HcsPushTask 一致)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java
index ab043070..29007ba8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java
@@ -36,7 +36,9 @@ import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ContractChangeMapper;
+import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
import org.springblade.transport.pojo.entity.ContractChange;
+import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.vo.ContractChangeVO;
import org.springblade.transport.pojo.vo.ContractManageVO;
@@ -77,12 +79,14 @@ public class ContractChangeServiceImpl extends BaseServiceImpl selectContractChangePage(IPage page, ContractChangeVO contractChange) {
IPage entityPage = page(page, buildQuery(contractChange).orderByDesc(ContractChange::getCreateTime));
IPage voPage = ContractChangeWrapper.build().pageVO(entityPage);
voPage.getRecords().forEach(this::fillReadonly);
+ this.fillContractRisk(voPage.getRecords());
return voPage;
}
@@ -90,6 +94,7 @@ public class ContractChangeServiceImpl extends BaseServiceImpl records) {
+ if (Func.isEmpty(records)) {
+ return;
+ }
+ java.util.Set partyNames = new java.util.HashSet<>();
+ records.forEach(change -> {
+ if (Func.isNotEmpty(change.getPartyA())) {
+ partyNames.add(change.getPartyA().trim());
+ }
+ if (Func.isNotEmpty(change.getPartyB())) {
+ partyNames.add(change.getPartyB().trim());
+ }
+ });
+ java.util.Map riskMap = this.hcsCreditCheckSupport.queryArchiveRiskMap(partyNames);
+ for (ContractChangeVO change : records) {
+ CustomerArchive partyARisk = Func.isEmpty(change.getPartyA()) ? null : riskMap.get(change.getPartyA().trim());
+ CustomerArchive partyBRisk = Func.isEmpty(change.getPartyB()) ? null : riskMap.get(change.getPartyB().trim());
+ change.setHcsRiskLevel(HcsCreditCheckSupport.higherRiskLevel(
+ partyARisk == null ? null : partyARisk.getHcsRiskLevel(),
+ partyBRisk == null ? null : partyBRisk.getHcsRiskLevel()));
+ change.setKingdeeBlackStatus(
+ HcsCreditCheckSupport.isKingdeeBlackHit(partyARisk) || HcsCreditCheckSupport.isKingdeeBlackHit(partyBRisk)
+ ? HcsSyncConstant.BLACK_STATUS_YES : HcsSyncConstant.BLACK_STATUS_NO);
+ }
+ }
@Override
@Transactional(rollbackFor = Exception.class)
public ContractChangeVO createFromContract(Long contractId, String changeType) {
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
index 721fa931..dac0cdca 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
@@ -43,6 +43,7 @@ import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.HcsContractExeStageMapper;
import org.springblade.transport.pojo.entity.ContractChange;
import org.springblade.transport.pojo.entity.ContractManage;
+import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.vo.ContractManageVO;
import org.springblade.transport.service.IContractExeStageService;
@@ -109,6 +110,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl entityPage = page(page, buildQuery(contractManage));
IPage voPage = ContractManageWrapper.build().pageVO(entityPage);
voPage.getRecords().forEach(this::fillReadonly);
+ this.fillContractRisk(voPage.getRecords());
return voPage;
}
@@ -116,6 +118,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl records) {
+ if (Func.isEmpty(records)) {
+ return;
+ }
+ Set partyNames = new HashSet<>();
+ records.forEach(contract -> {
+ if (Func.isNotEmpty(contract.getPartyA())) {
+ partyNames.add(contract.getPartyA().trim());
+ }
+ if (Func.isNotEmpty(contract.getPartyB())) {
+ partyNames.add(contract.getPartyB().trim());
+ }
+ });
+ Map riskMap = this.hcsCreditCheckSupport.queryArchiveRiskMap(partyNames);
+ for (ContractManageVO contract : records) {
+ CustomerArchive partyARisk = Func.isEmpty(contract.getPartyA()) ? null : riskMap.get(contract.getPartyA().trim());
+ CustomerArchive partyBRisk = Func.isEmpty(contract.getPartyB()) ? null : riskMap.get(contract.getPartyB().trim());
+ contract.setHcsRiskLevel(HcsCreditCheckSupport.higherRiskLevel(
+ partyARisk == null ? null : partyARisk.getHcsRiskLevel(),
+ partyBRisk == null ? null : partyBRisk.getHcsRiskLevel()));
+ contract.setKingdeeBlackStatus(
+ HcsCreditCheckSupport.isKingdeeBlackHit(partyARisk) || HcsCreditCheckSupport.isKingdeeBlackHit(partyBRisk)
+ ? HcsSyncConstant.BLACK_STATUS_YES : HcsSyncConstant.BLACK_STATUS_NO);
+ }
+ }
+
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveDraft(ContractManage contractManage) {
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
index 7775a849..7feb31d0 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
@@ -282,10 +282,7 @@ public class HcsContractSyncSupport {
String riskLevel = StringUtil.isNotBlank(riskPartyA.riskLevel()) ? riskPartyA.riskLevel() : riskPartyB.riskLevel();
// 黑名单状态:任一方金蝶黑名单(A801-5)命中即为是(复用 HcsCreditCheckSupport.isKingdeeBlacklistHit 公共口径)
boolean blacklist = riskPartyA.kingdeeHit() || riskPartyB.kingdeeHit();
- contractManage.setHcsRiskLevel(riskLevel);
- contractManage.setKingdeeBlackStatus(blacklist ? BLACK_STATUS_YES : "no");
- contractManage.setHcsRiskTime(LocalDateTime.now());
- contractManageMapper.updateById(contractManage);
+ // 判定结果仅在内存参与推送/特批流程(risky),风险展示数据由列表/详情实时关联客商档案组装,合同表不再落库风险留痕
boolean risky = HcsSyncConstant.RISK_LEVEL_HIGH.equals(riskLevel) || blacklist;
log.info("HCS合同风险校验完成:contractNo={},甲方[金蝶黑名单={},等级={}],乙方[金蝶黑名单={},等级={}],高风险/黑名单={}",
contractManage.getContractNo(), riskPartyA.kingdeeHit(), riskPartyA.riskLevel(),
@@ -312,9 +309,8 @@ public class HcsContractSyncSupport {
// TMS 流程挂起为"特批审核",特批通过恢复展示、驳回联动合同审批驳回
contractManage.setCurrentNode(HcsSyncConstant.SPEC_REVIEW_NODE);
contractManageMapper.updateById(contractManage);
- log.info("HCS合同高风险/黑名单挂起特批审核:contractNo={},riskLevel={},blackStatus={},挂起节点={}",
- contractManage.getContractNo(), contractManage.getHcsRiskLevel(), contractManage.getKingdeeBlackStatus(),
- contractManage.getCurrentNode());
+ log.info("HCS合同高风险/黑名单挂起特批审核:contractNo={},挂起节点={}",
+ contractManage.getContractNo(), contractManage.getCurrentNode());
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
index a9371bec..7a043ad0 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
@@ -60,6 +60,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -526,6 +527,64 @@ public class HcsCreditCheckSupport {
return summary;
}
+ /**
+ * 按甲乙方名称批量查客商档案风险数据(合同/变更列表与详情组装用,合同表不再落库风险留痕):
+ * 一次 IN 查询避免 N+1,键为客商名称;仅保留已有风险数据的档案(等级或金蝶黑名单字段非空),
+ * 名称重名时取第一条;内部组织客商无风险数据天然不入 map
+ *
+ * @param partyNames 甲/乙方名称集合
+ * @return 客商名称 -> 客商档案(含 hcsRiskLevel/kingdeeBlackCategory/kingdeeBlackReason)
+ */
+ public Map queryArchiveRiskMap(Collection partyNames) {
+ if (Func.isEmpty(partyNames)) {
+ return Map.of();
+ }
+ Map archiveMap = new HashMap<>();
+ for (CustomerArchive archive : customerArchiveMapper.selectList(Wrappers.lambdaQuery()
+ .in(CustomerArchive::getFullName, partyNames)
+ .eq(CustomerArchive::getIsDeleted, 0))) {
+ if (Func.isEmpty(archive.getFullName())) {
+ continue;
+ }
+ boolean hasRiskData = Func.isNotEmpty(archive.getHcsRiskLevel())
+ || Func.isNotEmpty(archive.getKingdeeBlackCategory()) || Func.isNotEmpty(archive.getKingdeeBlackReason());
+ if (hasRiskData) {
+ archiveMap.putIfAbsent(archive.getFullName(), archive);
+ }
+ }
+ return archiveMap;
+ }
+
+ /**
+ * 合同客商风险等级合并(甲乙双方取较高值,2>1>0,无有效等级返回 null)
+ */
+ public static String higherRiskLevel(String riskLevelA, String riskLevelB) {
+ int levelA = parseRiskLevel(riskLevelA);
+ int levelB = parseRiskLevel(riskLevelB);
+ if (levelA < 0 && levelB < 0) {
+ return null;
+ }
+ return String.valueOf(Math.max(levelA, levelB));
+ }
+
+ private static int parseRiskLevel(String riskLevel) {
+ if (Func.isEmpty(riskLevel)) {
+ return -1;
+ }
+ try {
+ return Integer.parseInt(riskLevel.trim());
+ } catch (NumberFormatException exception) {
+ return -1;
+ }
+ }
+
+ /**
+ * 客商档案金蝶黑名单命中判定(失信类别/列入事由任一非空)
+ */
+ public static boolean isKingdeeBlackHit(CustomerArchive archive) {
+ return archive != null && (Func.isNotEmpty(archive.getKingdeeBlackCategory()) || Func.isNotEmpty(archive.getKingdeeBlackReason()));
+ }
+
/**
* 金蝶黑名单批量查询(A801-5,定时刷新专用,调用方按 200 客商/批切分保证入参 ≤200):
* 键为大写税号;失信类别取 awrp_combofield_title、缺失兜底 awrp_combofield(口径同提审校验 validateBlacklist);
diff --git a/doc/sql/transport/lqj/temp/05_blade_contract_risk_fields_drop_20261030.sql b/doc/sql/transport/lqj/temp/05_blade_contract_risk_fields_drop_20261030.sql
new file mode 100644
index 00000000..35482e7e
--- /dev/null
+++ b/doc/sql/transport/lqj/temp/05_blade_contract_risk_fields_drop_20261030.sql
@@ -0,0 +1,54 @@
+-- 合同表移除客商风险留痕列(风险展示改为列表/详情实时关联客商档案组装,不再落库):
+-- blade_contract_manage DROP hcs_risk_level / kingdee_black_status / hcs_risk_time
+-- blade_contract_change DROP hcs_risk_level / kingdee_black_status
+-- 幂等可重复执行:列存在才 DROP。
+
+-- ① blade_contract_manage.hcs_risk_level
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_contract_manage' AND COLUMN_NAME = 'hcs_risk_level') > 0,
+ 'ALTER TABLE `blade_contract_manage` DROP COLUMN `hcs_risk_level`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ② blade_contract_manage.kingdee_black_status
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_contract_manage' AND COLUMN_NAME = 'kingdee_black_status') > 0,
+ 'ALTER TABLE `blade_contract_manage` DROP COLUMN `kingdee_black_status`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ③ blade_contract_manage.hcs_risk_time
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_contract_manage' AND COLUMN_NAME = 'hcs_risk_time') > 0,
+ 'ALTER TABLE `blade_contract_manage` DROP COLUMN `hcs_risk_time`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ④ blade_contract_change.hcs_risk_level
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_contract_change' AND COLUMN_NAME = 'hcs_risk_level') > 0,
+ 'ALTER TABLE `blade_contract_change` DROP COLUMN `hcs_risk_level`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- ⑤ blade_contract_change.kingdee_black_status
+SET @ddl = (SELECT IF(
+ (SELECT COUNT(*) FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_contract_change' AND COLUMN_NAME = 'kingdee_black_status') > 0,
+ 'ALTER TABLE `blade_contract_change` DROP COLUMN `kingdee_black_status`',
+ 'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
From a47b7af30b045aab47e5d9e471b0adcc6734b60e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 15:31:29 +0800
Subject: [PATCH 52/63] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E5=AE=A2?=
=?UTF-8?q?=E5=95=86=E9=A3=8E=E9=99=A9SQL=E6=97=A7=E7=B4=A2=E5=BC=95?=
=?UTF-8?q?=E6=B8=85=E7=90=86=E6=AE=B5=E7=BC=BA=E5=B0=91=E6=89=A7=E8=A1=8C?=
=?UTF-8?q?=E8=AF=AD=E5=8F=A5=EF=BC=88=E8=A1=A5PREPARE/EXECUTE/DEALLOCATE?=
=?UTF-8?q?=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql | 3 +++
1 file changed, 3 insertions(+)
diff --git a/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql b/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
index 696a8780..40b08f5f 100644
--- a/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
+++ b/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
@@ -63,6 +63,9 @@ SET @ddl = (SELECT IF(
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'blade_customer_archive' AND INDEX_NAME = 'idx_hcs_black_status') > 0,
'ALTER TABLE `blade_customer_archive` DROP INDEX idx_hcs_black_status',
'SELECT 1'));
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
-- ⑥ 列 hcs_black_category / hcs_black_reason(A801-5 金蝶黑名单原始字段:失信类别、列入事由,明细文案由后台组装)
SET @ddl = (SELECT IF(
From 65a53a56b654316d94104324152792550eecef19 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 19:55:36 +0800
Subject: [PATCH 53/63] =?UTF-8?q?feat:=20HCS=E5=90=88=E5=90=8C=E6=8E=A8?=
=?UTF-8?q?=E9=80=81=E6=96=B0=E5=A2=9E=E4=B8=9A=E5=8A=A1=E5=90=88=E5=90=8C?=
=?UTF-8?q?=E5=8F=B7=E5=AD=97=E6=AE=B5fdBusContractNumber=EF=BC=8C?=
=?UTF-8?q?=E5=80=BC=E5=90=8C=E5=90=88=E5=90=8C=E7=BC=96=E5=8F=B7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/constant/HcsContractFieldConstant.java | 5 +++++
.../transport/support/hcs/HcsContractSyncSupport.java | 2 ++
2 files changed, 7 insertions(+)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java
index 798c77d1..fa3bace4 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java
@@ -52,6 +52,11 @@ public final class HcsContractFieldConstant {
*/
public static final String FIELD_CONTRACT_NO = "CONTRACT_NO";
+ /**
+ * 业务合同号(fd 模板字段,值同合同编号 contractNo)
+ */
+ public static final String FIELD_FD_BUS_CONTRACT_NUMBER = "fdBusContractNumber";
+
/**
* 合同标题(fd 模板字段)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
index 7feb31d0..b2968ee1 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
@@ -457,6 +457,8 @@ public class HcsContractSyncSupport {
// 【必填】合同编号:ContractManage.contractNo
fields.add(field(HcsContractFieldConstant.FIELD_FD_NUMBER, "合同编号", contractManage.getContractNo()));
fields.add(field(HcsContractFieldConstant.FIELD_CONTRACT_NO, "合同编号", contractManage.getContractNo()));
+ // 【必填】业务合同号:合规侧 2026-10-30 新增,值同合同编号 contractNo
+ fields.add(field(HcsContractFieldConstant.FIELD_FD_BUS_CONTRACT_NUMBER, "业务合同号", contractManage.getContractNo()));
// 【必填】合同标题:ContractManage.contractName
fields.add(field(HcsContractFieldConstant.FIELD_FD_SUBJECT, "合同标题", contractManage.getContractName()));
fields.add(field(HcsContractFieldConstant.FIELD_CONTRACT_NAME, "合同名称", contractManage.getContractName()));
From 912a15e0ad36c5f42107e6a9ce18f9f95c5f50ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 20:09:52 +0800
Subject: [PATCH 54/63] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=BB=91?=
=?UTF-8?q?=E5=90=8D=E5=8D=95=E5=AE=A2=E5=95=86=E9=A3=8E=E9=99=A9=E6=8F=90?=
=?UTF-8?q?=E7=A4=BA=E4=B8=8D=E6=98=BE=E7=A4=BA=E5=B9=B6=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=AE=A2=E5=95=86=E6=8C=89=E7=BC=96=E5=8F=B7?=
=?UTF-8?q?=E6=9F=A5=E8=AF=A2?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/pojo/vo/CustomerRiskVO.java | 3 ++
.../controller/HcsSyncController.java | 10 ++++---
.../support/hcs/HcsCreditCheckSupport.java | 30 ++++++++++++++++---
3 files changed, 35 insertions(+), 8 deletions(-)
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerRiskVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerRiskVO.java
index da93615f..1a247302 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerRiskVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerRiskVO.java
@@ -63,6 +63,9 @@ public class CustomerRiskVO implements Serializable {
@Schema(description = "风险等级名称(服务端转义:黑名单/高风险/中风险/低风险,黑名单优先;无等级为空)")
private String riskLevelName;
+ @Schema(description = "是否命中金蝶黑名单(A801-5,黑名单展示口径;红字细分由前端按等级+本标记翻译)")
+ private Boolean kingdeeBlackHit;
+
@Schema(description = "是否高风险/黑名单客商(外部客商专用,内部组织恒为 false)")
private Boolean risky;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index ea8c0527..f18be7eb 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -216,13 +216,15 @@ public class HcsSyncController {
/**
* 客商风险等级查询(C101-1,展示用,前端接入设计 §3.3;强制校验仍在推送/付款链路):
- * 内部组织类型客商默认无风险不调合规,外部客商高风险/黑名单返回提示文案
+ * 内部组织类型客商默认无风险不调合规,外部客商高风险/黑名单返回提示文案;
+ * creditCode 供新增客商(档案未落库)查金蝶黑名单兜底,档案存在时以档案为准
*/
@GetMapping("/risk")
@ApiOperationSupport(order = 10)
- @Operation(summary = "客商风险等级查询", description = "C101-1 按客商编号查询风险等级与黑名单状态(展示用),内部客商默认无风险,高风险/黑名单返回风险提示文案,结果留痕")
- public R risk(@Parameter(description = "客商编号", required = true) @RequestParam String customerCode) {
- return R.data(hcsCreditCheckSupport.queryCustomerRisk(customerCode));
+ @Operation(summary = "客商风险等级查询", description = "C101-1 按客商编号查询风险等级与黑名单状态(展示用),内部客商默认无风险,高风险/黑名单返回风险提示文案,结果留痕;creditCode 为新增客商场景金蝶黑名单查询兜底")
+ public R risk(@Parameter(description = "客商编号", required = true) @RequestParam String customerCode,
+ @Parameter(description = "统一社会信用代码(新增客商档案未落库时兜底查金蝶黑名单)") @RequestParam(required = false) String creditCode) {
+ return R.data(hcsCreditCheckSupport.queryCustomerRisk(customerCode, creditCode));
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
index 7a043ad0..2bd22cf2 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
@@ -205,12 +205,26 @@ public class HcsCreditCheckSupport {
/**
* 客商风险查询(C101-1,展示与提交校验共用,前端接入设计 §3.3):
* 内部组织类型客商默认无风险(不调 C101-1);外部客商按编号查询风险等级与黑名单状态并留痕;
+ * 金蝶黑名单(A801-5)命中同样判定为风险客商(risky=true,C101-1 空数据不漏提示);
* riskTip 经 {@link #buildRiskDetail} 统一组装(等级 + 金蝶黑名单失信类别/列入事由),与列表悬浮明细同口径
*
* @param customerCode 客商编号
* @return 风险展示信息(customerKind/fdBlackStatus/fdRiskLevel/risky/riskTip)
*/
public CustomerRiskVO queryCustomerRisk(String customerCode) {
+ return this.queryCustomerRisk(customerCode, null);
+ }
+
+ /**
+ * 客商风险查询(带信用代码,新增客商档案表单实时校验用):
+ * 金蝶带出的客商尚未落库(本地档案反查不到)时,信用代码无法从档案取得,
+ * 由前端将金蝶档案带出的统一社会信用代码随编号一并传入,保证 A801-5 黑名单段不缺失
+ *
+ * @param customerCode 客商编号(金蝶档案带出的编码)
+ * @param creditCode 统一社会信用代码(档案存在时以档案为准,档案缺失时兜底入参)
+ * @return 风险展示信息
+ */
+ public CustomerRiskVO queryCustomerRisk(String customerCode, String creditCode) {
if (StringUtil.isBlank(customerCode)) {
throw new ServiceException("客商编号不能为空");
}
@@ -230,12 +244,20 @@ public class HcsCreditCheckSupport {
}
RelativeRiskResult fetch = this.fetchRelativeRisk(customerCode, customerName);
CustomerRiskVO riskVO = fetch.risk();
- // 表单实时提示与列表明细统一组装口径:追加金蝶黑名单段(A801-5 按档案信用代码查询,失败静默缺失)
- String creditCode = customerArchive == null ? null : customerArchive.getUnifiedCreditCode();
- BfiBlacklistVO hit = StringUtil.isBlank(creditCode) ? null
- : this.queryBlacklistByCodes(List.of(creditCode.trim().toUpperCase())).get(creditCode.trim().toUpperCase());
+ // 表单实时提示与列表明细统一组装口径:追加金蝶黑名单段(A801-5 按信用代码查询,失败静默缺失;
+ // 档案缺失(新增客商实时校验)时兜底用入参信用代码)
+ String archiveCreditCode = customerArchive == null ? null : customerArchive.getUnifiedCreditCode();
+ String effectiveCreditCode = StringUtil.isNotBlank(archiveCreditCode) ? archiveCreditCode : creditCode;
+ BfiBlacklistVO hit = StringUtil.isBlank(effectiveCreditCode) ? null
+ : this.queryBlacklistByCodes(List.of(effectiveCreditCode.trim().toUpperCase())).get(effectiveCreditCode.trim().toUpperCase());
String blackCategory = hit == null ? null
: (Func.isEmpty(hit.getAwrpCombofieldTitle()) ? hit.getAwrpCombofield() : hit.getAwrpCombofieldTitle());
+ // 黑名单判定仅以金蝶黑名单(A801-5)命中为准:C101-1 可能对已列入黑名单的客商返回空数据,命中时必须置 risky=true 才能触发前端提示;
+ // 命中结果落 kingdeeBlackHit 原始标记,红字文案细分(黑名单/高风险)由前端翻译
+ if (hit != null) {
+ riskVO.setRisky(Boolean.TRUE);
+ riskVO.setKingdeeBlackHit(Boolean.TRUE);
+ }
riskVO.setRiskTip(buildRiskDetail(customerName, riskVO.getFdRiskLevel(),
blackCategory, hit == null ? null : hit.getAwrpTextareafield()));
try {
From 0e1517df9c4dfd2196139ec0b8b9d751baac5749 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 20:34:25 +0800
Subject: [PATCH 55/63] =?UTF-8?q?feat:=20=E5=AE=A2=E5=95=86=E6=A1=A3?=
=?UTF-8?q?=E6=A1=88=E6=96=B0=E5=A2=9E=E4=BE=9B=E5=BA=94=E5=95=86=E7=BC=96?=
=?UTF-8?q?=E7=A0=81=E5=AD=97=E6=AE=B5=E5=B9=B6=E8=90=BD=E5=BA=93=E9=87=91?=
=?UTF-8?q?=E8=9D=B6A901-1=E7=BC=96=E7=A0=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/CustomerArchive.java | 3 ++
.../transport/excel/CustomerArchiveExcel.java | 3 ++
.../impl/CustomerArchiveServiceImpl.java | 48 ++++++++++++++++++-
.../impl/InvoiceApplicationServiceImpl.java | 17 +++----
.../impl/PaymentApplicationServiceImpl.java | 2 +-
.../support/KingdeeApBillSyncSupport.java | 32 +++----------
.../support/KingdeeArBillSyncSupport.java | 5 +-
.../KingdeeReceivedInvoiceSyncSupport.java | 2 +-
...mer_archive_add_supplier_code_20261030.sql | 16 +++++++
9 files changed, 85 insertions(+), 43 deletions(-)
create mode 100644 doc/sql/transport/lqj/temp/blade_customer_archive_add_supplier_code_20261030.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
index 4ada80e1..e35d1051 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java
@@ -52,6 +52,9 @@ public class CustomerArchive extends TenantEntity {
@Schema(description = "客商编号")
private String customerCode;
+ @Schema(description = "供应商编码(金蝶A901-1供应商查询返回number,建档/同步时落库)")
+ private String supplierCode;
+
@Schema(description = "客商简称")
private String shortName;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerArchiveExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerArchiveExcel.java
index ebe4eec2..8c25f3f1 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerArchiveExcel.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerArchiveExcel.java
@@ -51,6 +51,9 @@ public class CustomerArchiveExcel implements Serializable {
@ExcelProperty("客商编号")
private String customerCode;
+ @ExcelProperty("供应商编码")
+ private String supplierCode;
+
@ExcelProperty("客商简称")
private String shortName;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
index 10d92f1b..d670531e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
@@ -47,12 +47,14 @@ import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.excel.CustomerArchiveExcel;
import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiSupplierQueryDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultErrorVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBlacklistVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiCustomerVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiSupplierVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil;
import org.springblade.system.cache.SysCache;
@@ -282,6 +284,12 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl page = bfiService.querySupplier(param);
+ List rows = page == null ? null : page.getRows();
+ return rows == null || rows.isEmpty() ? null : rows.get(0).getNumber();
+ } catch (Exception exception) {
+ log.warn("A901-1供应商查询金蝶客商编码失败,unifiedCreditCode={}", unifiedCreditCode, exception);
+ return null;
+ }
+ }
+
+ /**
+ * 供应商编码落库:A901-1 按统一社会信用代码查金蝶供应商档案取 number,查到才覆盖(查不到/接口异常保留原值,不影响同步主流程)
+ */
+ private void fillSupplierCode(CustomerArchive customer, CustomerArchive after) {
+ String supplierCode = this.queryKingdeeSupplierNumber(customer.getUnifiedCreditCode());
+ if (Func.isNotEmpty(supplierCode)) {
+ after.setSupplierCode(supplierCode);
+ }
+ }
+
/**
* 金蝶已存在档案时,客商编号(customer_code)采纳金蝶客商编码,后续业务接口沿用客商编号即拿到金蝶编码;
* 金蝶编码为空或与当前编号一致时无需处理;同租户下已被其他客商占用时跳过回填,避免违反 uk_customer_archive_code 唯一约束
@@ -772,10 +814,11 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl savedBillNos = new ArrayList<>();
@@ -872,7 +867,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl items) {
BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO();
// ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ====================
@@ -985,8 +980,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpllambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, partyB).or().eq(CustomerArchive::getShortName, partyB))
.eq(CustomerArchive::getIsDeleted, 0).last("limit 1"));
- if (customer == null || Func.isBlank(customer.getUnifiedCreditCode())) {
+ if (customer == null) {
return null;
}
- return this.queryKingdeeSupplierNumber(customer.getUnifiedCreditCode().trim());
- }
-
- /** A901-1 按统一社会信用代码查询金蝶供应商档案,仅取已审核且可用的档案,返回值 number 即金蝶客商编码,无数据返回 null(开票申请受票方复用) */
- public String queryKingdeeSupplierNumber(String unifiedCreditCode) {
- BfiSupplierQueryDTO param = new BfiSupplierQueryDTO();
- param.setPageNo(1);
- param.setPageSize(1);
- // param.setStatus(BfiConstant.BILL_STATUS_AUDITED);
- param.setEnable(BfiConstant.ENABLE_ENABLED);
- param.setSocietycreditcode(List.of(unifiedCreditCode));
- BfiPageVO page = bfiService.querySupplier(param);
- List rows = page == null ? null : page.getRows();
- return rows == null || rows.isEmpty() ? null : rows.get(0).getNumber();
+ return Func.isBlank(customer.getSupplierCode()) ? customer.getCustomerCode() : customer.getSupplierCode();
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java
index 5a000afb..f9e214c1 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeArBillSyncSupport.java
@@ -202,15 +202,14 @@ public class KingdeeArBillSyncSupport {
String currency = kingdeeApBillSyncSupport.bfiCurrency(contract);
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),以同步当日作为应收单据日期
String billDateText = kingdeeApBillSyncSupport.bfiDateTimeText(LocalDate.now());
- // 往来户.编码:应收结算单付款方(客户)关联客商档案取统一社会信用代码,经 A901-1 供应商查询取金蝶客商编码
+ // 往来户.编码:应收结算单付款方(客户)关联客商档案客商编码
String payerName = settlement.getPayerName();
if (Func.isBlank(payerName)) {
throw new ServiceException("推送金蝶财务应收单失败,结算单付款方不能为空,请检查正式结算单[" + Func.toStr(subject) + "]付款方信息");
}
String customerCode = context.customerCodeCache().computeIfAbsent(payerName.trim(), kingdeeApBillSyncSupport::queryCustomerCode);
if (Func.isBlank(customerCode)) {
- throw new ServiceException("推送金蝶财务应收单失败,结算单付款方[" + payerName
- + "]未在客商档案中维护统一社会信用代码或金蝶侧未查到对应供应商档案,请补充后重试");
+ throw new ServiceException("推送金蝶财务应收单失败,结算单付款方[" + payerName + "]未在客商档案中维护客商编码,请补充后重试");
}
BfiArBillSaveDTO param = new BfiArBillSaveDTO();
// 【必填】单据类型.编码
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java
index 04e0fdd3..48a74c14 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeReceivedInvoiceSyncSupport.java
@@ -189,7 +189,7 @@ public class KingdeeReceivedInvoiceSyncSupport {
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract, context));
// 【必填】结算币别:合同结算币种
param.setCurrencyNumber(kingdeeApBillSyncSupport.bfiCurrency(contract));
- // 【必填】往来户.编码:合同乙方(开票供应商)统一社会信用代码经 A901-1 供应商查询取金蝶客商编码
+ // 【必填】往来户.编码:合同乙方(开票供应商)客商编码
param.setReceivablessuppNumber(kingdeeApBillSyncSupport.bfiAsstactNumber(contract, context));
// 【必填】汇率表.编码
param.setExratetableNumber(BfiConstant.EXRATE_TABLE_DEFAULT);
diff --git a/doc/sql/transport/lqj/temp/blade_customer_archive_add_supplier_code_20261030.sql b/doc/sql/transport/lqj/temp/blade_customer_archive_add_supplier_code_20261030.sql
new file mode 100644
index 00000000..8c696089
--- /dev/null
+++ b/doc/sql/transport/lqj/temp/blade_customer_archive_add_supplier_code_20261030.sql
@@ -0,0 +1,16 @@
+-- 客商档案:新增供应商编码字段
+-- 新增 supplier_code:金蝶A901-1供应商查询返回number,新增/同步客商时落库,金蝶单据推送直接复用,不再实时查接口
+
+-- 以下结构升级支持重复执行
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_customer_archive` ADD COLUMN `supplier_code` varchar(64) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''供应商编码(金蝶A901-1供应商查询返回number)'' AFTER `customer_code`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_customer_archive'
+ AND COLUMN_NAME = 'supplier_code'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
From 527368575b7819ceed28ea3c51e970ec056a238d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 20:41:26 +0800
Subject: [PATCH 56/63] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=AE=A2?=
=?UTF-8?q?=E5=95=86=E5=A1=AB=E5=86=99=E4=BF=A1=E7=94=A8=E4=BB=A3=E7=A0=81?=
=?UTF-8?q?=E5=90=8E=E5=8D=B3=E6=97=B6=E6=9F=A5=E8=AF=A2=E9=87=91=E8=9D=B6?=
=?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E7=BC=96=E7=A0=81=E6=8E=A5=E5=8F=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../controller/CustomerArchiveController.java | 10 ++++++++++
.../transport/service/ICustomerArchiveService.java | 8 ++++++++
.../service/impl/CustomerArchiveServiceImpl.java | 5 +++++
3 files changed, 23 insertions(+)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java
index b0580c5a..45eb0674 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java
@@ -198,6 +198,16 @@ public class CustomerArchiveController extends BladeController {
return R.data(customerArchiveService.syncKingdee(id));
}
+ /**
+ * 金蝶供应商编码查询(A901-1,新增客商填写信用代码后即时回显用)
+ */
+ @GetMapping("/query-supplier-code")
+ @ApiOperationSupport(order = 12)
+ @Operation(summary = "金蝶供应商编码查询", description = "A901-1 按统一社会信用代码查询金蝶供应商档案取编码,查不到返回空")
+ public R querySupplierCode(@Parameter(description = "统一社会信用代码", required = true) @RequestParam String unifiedCreditCode) {
+ return R.data(customerArchiveService.querySupplierCode(unifiedCreditCode));
+ }
+
/**
* 评分明细模板
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java
index e50f1a47..8f779ca2 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java
@@ -196,6 +196,14 @@ public interface ICustomerArchiveService extends BaseService {
*/
CustomerArchive syncKingdee(Long id);
+ /**
+ * A901-1 按统一社会信用代码查询金蝶供应商编码(新增客商填写信用代码后即时回显用),查不到返回 null
+ *
+ * @param unifiedCreditCode 统一社会信用代码
+ * @return 金蝶供应商编码
+ */
+ String querySupplierCode(String unifiedCreditCode);
+
/**
* 推送失败状态落库(独立事务):推送主事务已回滚,失败状态与原因单独落库供页面展示与手动重推
*
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
index d670531e..a83d23ee 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
@@ -667,6 +667,11 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl
Date: Fri, 30 Oct 2026 20:45:41 +0800
Subject: [PATCH 57/63] =?UTF-8?q?feat:=20HCS=E5=90=88=E5=90=8C=E6=8E=A8?=
=?UTF-8?q?=E9=80=81=E6=96=B0=E5=A2=9E=E5=BD=92=E6=A1=A3=E7=8A=B6=E6=80=81?=
=?UTF-8?q?fdArchiveStatus=EF=BC=8C=E5=8F=8C=E7=AB=A0=E5=BD=92=E6=A1=A3?=
=?UTF-8?q?=E6=96=87=E4=BB=B6=E4=B8=8A=E4=BC=A0=E5=90=8E=E5=8D=B3=E6=97=B6?=
=?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=90=88=E8=A7=84=E5=BE=85=E5=BD=92=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../constant/HcsContractFieldConstant.java | 11 ++++
.../transport/constant/HcsSyncConstant.java | 6 ++
.../impl/ContractManageServiceImpl.java | 38 ++++++++++++-
.../support/hcs/HcsAttachmentSupport.java | 8 ++-
.../support/hcs/HcsContractSyncSupport.java | 56 +++++++++++++++++--
.../support/hcs/HcsImmediatePushSupport.java | 22 ++++++++
6 files changed, 132 insertions(+), 9 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java
index fa3bace4..952d96f9 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsContractFieldConstant.java
@@ -57,6 +57,17 @@ public final class HcsContractFieldConstant {
*/
public static final String FIELD_FD_BUS_CONTRACT_NUMBER = "fdBusContractNumber";
+ /**
+ * 归档状态(fd 模板字段):toarchive=待归档; archiving=归档中; archived=已归档; archiveerror=归档异常;
+ * TMS 侧仅在双章归档文件上传(归档状态=已归档)后传 toarchive,其余状态由合规侧自行流转,未归档不传
+ */
+ public static final String FIELD_FD_ARCHIVE_STATUS = "fdArchiveStatus";
+
+ /**
+ * 归档状态取值:待归档(fdArchiveStatus,双章归档文件上传后 TMS 固定传此值)
+ */
+ public static final String ARCHIVE_STATUS_TO_ARCHIVE = "toarchive";
+
/**
* 合同标题(fd 模板字段)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
index 272a735b..ce920802 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/HcsSyncConstant.java
@@ -254,6 +254,12 @@ public final class HcsSyncConstant {
*/
public static final String CONTRACT_ATTACH_KEY_OTHER = "eopCdraftOther";
+ /**
+ * TMS 合同归档状态取值:已归档(ContractManage#archiveStatus,双章归档文件上传后由审核通过逻辑维护;
+ * 与合规 fdArchiveStatus 的 archived 不是同一含义,TMS 已归档对应合规 toarchive 待归档)
+ */
+ public static final String TMS_ARCHIVE_STATUS_ARCHIVED = "已归档";
+
/**
* 特批审核挂起节点展示名(currentNode,特批发起后挂起展示,结果回传后恢复)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
index dac0cdca..ea0274ce 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
@@ -50,6 +50,7 @@ import org.springblade.transport.service.IContractExeStageService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.hcs.HcsCreditCheckSupport;
+import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
import org.springblade.transport.wrapper.ContractManageWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -82,6 +83,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl originalKeys = new HashSet<>();
+ for (Object item : parseJsonArray(originalJson)) {
+ originalKeys.add(attachmentKey(item));
+ }
+ List newSealFiles = new ArrayList<>();
+ for (Object item : parseJsonArray(requestJson)) {
+ if (originalKeys.contains(attachmentKey(item))) {
+ continue;
+ }
+ if (item instanceof Map, ?> map) {
+ Object fileType = map.get("fileType");
+ if (fileType != null && FILE_TYPE_SEAL_ARCHIVE.equals(String.valueOf(fileType).trim())) {
+ newSealFiles.add(item);
+ }
+ }
+ }
+ return newSealFiles.isEmpty() ? null : JsonUtil.toJson(newSealFiles);
+ }
+
@SuppressWarnings("unchecked")
private String markContractFilesApproved(String contractFileJson) {
List files = parseJsonArray(contractFileJson);
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java
index 07bcfeb0..34f944d0 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsAttachmentSupport.java
@@ -66,6 +66,7 @@ import java.util.Objects;
* 任一附件转传失败 → 抛出异常由整单推送失败进补偿重试(失败原因落单据 hcs_sync_msg)。
*
* 合同链路挂载字段 fdEntityKey(v4 枚举):主文件 approved=true → eopCdraftSign(用印)、
+ * 双章归档文件(fileType=双章归档文件,归档材料非用印正文)→ eopCdraftOther(其他附件,2026-10-30)、
* 特批附件(fileType=特批附件)→ eopCdraftSpecial(随 C103-1 fdSepcailContract=true 特批流程)、其它附件 → eopCdraftOther。
*
* @author Chill
@@ -106,7 +107,7 @@ public class HcsAttachmentSupport {
}
/**
- * 解析合同主文件 JSON(仅 approved=true 的文件,挂用印字段)与其它附件 JSON(挂其他附件字段)并转传
+ * 解析合同主文件 JSON(仅 approved=true 的文件:双章归档文件挂其他附件字段,其余挂用印字段)与其它附件 JSON 并转传
*
* @param bizType 业务单据类型(HcsSyncConstant.BIZ_TYPE_*)
* @param bizId 业务单据id
@@ -119,8 +120,11 @@ public class HcsAttachmentSupport {
List attachmentRefs = new ArrayList<>();
for (Map, ?> file : parseJsonArray(contractFileJson)) {
if (isApproved(file)) {
+ // 双章归档文件挂其他附件面板(2026-10-30:归档材料非用印正文),其余用印材料挂用印字段
+ Object fileType = file.get(HcsSyncConstant.ATTACHMENT_TYPE_FIELD);
+ boolean sealArchive = fileType != null && HcsSyncConstant.ATTACHMENT_TYPE_SEAL.equals(String.valueOf(fileType).trim());
attachmentRefs.add(this.transferOne(bizType, bizId, file, operatorLoginName,
- HcsSyncConstant.CONTRACT_ATTACH_KEY_SEAL));
+ sealArchive ? HcsSyncConstant.CONTRACT_ATTACH_KEY_OTHER : HcsSyncConstant.CONTRACT_ATTACH_KEY_SEAL));
}
}
for (Map, ?> attachment : parseJsonArray(attachmentsJson)) {
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
index b2968ee1..946f1f54 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsContractSyncSupport.java
@@ -251,7 +251,7 @@ public class HcsContractSyncSupport {
markSynced(contractManage, fdContractId, resultMessage);
}
} else {
- updateContract(contractManage, risky);
+ updateContract(contractManage, null, risky);
resultMessage = "合同更新成功:" + contractManage.getContractNo();
markSynced(contractManage, contractManage.getHcsFdId(), resultMessage);
}
@@ -402,11 +402,12 @@ public class HcsContractSyncSupport {
}
/**
- * C103-11 合同更新(入参同发起,v4 新增顶层 fdId 定位已有合同);risky=true 时报文带 fdSepcailContract=true
+ * C103-11 合同更新(入参同发起,v4 新增顶层 fdId 定位已有合同);risky=true 时报文带 fdSepcailContract=true;
+ * attachmentRefs 归档推送传新增双章归档文件引用(挂其他附件面板),常规变更更新传 null 不挂载
*/
- private void updateContract(ContractManage contractManage, boolean risky) {
- HcsContractDraftDTO param = buildDraftParam(contractManage, null, risky);
- log.info("HCS合同推送开始:{}(C103-11 更新,附件不重复挂载待联调确认,特批流程={})", contractManage.getContractNo(), risky);
+ private void updateContract(ContractManage contractManage, List attachmentRefs, boolean risky) {
+ HcsContractDraftDTO param = buildDraftParam(contractManage, attachmentRefs, risky);
+ log.info("HCS合同推送开始:{}(C103-11 更新,挂载附件 {} 个,特批流程={})", contractManage.getContractNo(), attachmentRefs == null ? 0 : attachmentRefs.size(), risky);
HcsResult result = hcsClient.contractUpdate(param);
if (result == null || !result.isSuccess()) {
log.error("HCS合同更新失败响应:{},contractNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo());
@@ -414,6 +415,47 @@ public class HcsContractSyncSupport {
}
}
+ /**
+ * 双章归档文件上传后即时归档推送(C103-11 更新):报文随 fdArchiveStatus=toarchive(buildXformFields 按
+ * TMS 归档状态=已归档 传值),并把本次新增的双章归档文件转传挂载其他附件面板(eopCdraftOther,2026-10-30)。
+ * 合同尚未发起合规(无 hcs_fd_id)时跳过——首次推送 C103-1 报文自带归档状态与归档附件;
+ * 特批标志不重触发(risky 固定 false,特批随首次发起一次性完成),特批审核中跳过避免干扰在途流程;
+ * 失败由调用方留痕,可重新上传双章归档文件或变更重推补偿
+ *
+ * @param contractId 合同单ID
+ * @param newSealFilesJson 本次新增的双章归档文件元数据 JSON 数组(updateAttachments 按附件键排除原有行提取)
+ * @return 推送结果描述
+ */
+ public String pushArchiveUpdate(Long contractId, String newSealFilesJson) {
+ ContractManage contractManage = contractManageMapper.selectById(contractId);
+ if (contractManage == null) {
+ log.warn("HCS合同归档推送跳过(合同不存在):id={}", contractId);
+ return "合同不存在,跳过归档推送";
+ }
+ if (StringUtil.isBlank(contractManage.getHcsFdId())) {
+ log.info("HCS合同归档推送跳过(尚未发起合规,归档状态随首次推送报文携带):contractNo={}", contractManage.getContractNo());
+ return "合同尚未发起合规,归档状态随首次推送携带";
+ }
+ if (!List.of("approved", "change_approved").contains(contractManage.getApprovalStatus())
+ || HcsSyncConstant.SPEC_STATUS_PROCESSING.equals(contractManage.getHcsSpecStatus())
+ || !HcsSyncConstant.TMS_ARCHIVE_STATUS_ARCHIVED.equals(contractManage.getArchiveStatus())) {
+ log.info("HCS合同归档推送跳过(状态不满足):contractNo={},approvalStatus={},hcsSpecStatus={},archiveStatus={}",
+ contractManage.getContractNo(), contractManage.getApprovalStatus(), contractManage.getHcsSpecStatus(), contractManage.getArchiveStatus());
+ return "合同状态不满足归档推送条件,跳过";
+ }
+ // 本次新增双章归档文件转传(映射表命中复用合规附件ID防重复上传)并统一挂载其他附件面板
+ List attachmentRefs = hcsProperties.getAttachment().isTransferEnabled()
+ ? hcsAttachmentSupport.transferAttachments(HcsSyncConstant.BIZ_TYPE_CONTRACT, contractManage.getId(),
+ newSealFilesJson, hcsProperties.getOperatorLoginName()) : new ArrayList<>();
+ for (HcsAttachmentRefDTO attachmentRef : attachmentRefs) {
+ attachmentRef.setFdEntityKey(HcsSyncConstant.CONTRACT_ATTACH_KEY_OTHER);
+ }
+ updateContract(contractManage, attachmentRefs, false);
+ String resultMessage = "合同归档推送成功(fdArchiveStatus=toarchive,挂载归档附件 " + attachmentRefs.size() + " 个):" + contractManage.getContractNo();
+ markSynced(contractManage, contractManage.getHcsFdId(), resultMessage);
+ return resultMessage;
+ }
+
/**
* 组装 C103-1/C103-11 入参(字段标注照 §3.4 规范,映射表见设计文档 §5.2);
* attachmentRefs 新建传合规附件引用列表(顶层 attachment,fdId+fdEntityKey,必填,无附件传空列表),更新传 null 不挂载
@@ -459,6 +501,10 @@ public class HcsContractSyncSupport {
fields.add(field(HcsContractFieldConstant.FIELD_CONTRACT_NO, "合同编号", contractManage.getContractNo()));
// 【必填】业务合同号:合规侧 2026-10-30 新增,值同合同编号 contractNo
fields.add(field(HcsContractFieldConstant.FIELD_FD_BUS_CONTRACT_NUMBER, "业务合同号", contractManage.getContractNo()));
+ // 归档状态:TMS 已归档(双章归档文件已上传)→ toarchive 待归档(archiving/archived/archiveerror 由合规侧自行流转,未归档不传)
+ if (HcsSyncConstant.TMS_ARCHIVE_STATUS_ARCHIVED.equals(contractManage.getArchiveStatus())) {
+ fields.add(field(HcsContractFieldConstant.FIELD_FD_ARCHIVE_STATUS, "归档状态", HcsContractFieldConstant.ARCHIVE_STATUS_TO_ARCHIVE));
+ }
// 【必填】合同标题:ContractManage.contractName
fields.add(field(HcsContractFieldConstant.FIELD_FD_SUBJECT, "合同标题", contractManage.getContractName()));
fields.add(field(HcsContractFieldConstant.FIELD_CONTRACT_NAME, "合同名称", contractManage.getContractName()));
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java
index fc0339ce..eee77238 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsImmediatePushSupport.java
@@ -131,6 +131,17 @@ public class HcsImmediatePushSupport {
this.submitAfterCommit(HcsSyncConstant.BIZ_TYPE_CONTRACT, id, () -> this.pushContractNow(id), true);
}
+ /**
+ * 合同双章归档文件上传后即时归档推送(C103-11 更新带 fdArchiveStatus=toarchive + 归档附件挂其他附件面板):
+ * 须在附件保存事务内调用(afterCommit 触发),失败仅留痕(可重新上传双章归档文件或变更重推补偿)
+ *
+ * @param id 合同单ID
+ * @param newSealFilesJson 本次新增的双章归档文件元数据 JSON 数组
+ */
+ public void pushContractArchiveAfterCommit(Long id, String newSealFilesJson) {
+ this.submitAfterCommit(HcsSyncConstant.BIZ_TYPE_CONTRACT, id, () -> this.pushContractArchiveNow(id, newSealFilesJson), true);
+ }
+
/**
* 付款申请审批通过后即时发起付款特批(C106-3,无需特批则直接推履约数据):
* 须在审批事务内调用(afterCommit 触发);按"审批即发起特批"要求不受 immediate-enabled 开关控制
@@ -280,6 +291,17 @@ public class HcsImmediatePushSupport {
}
}
+ /**
+ * 合同归档推送执行体:合同未发起合规/状态不满足由 Support 内部跳过,失败仅留痕
+ */
+ private void pushContractArchiveNow(Long id, String newSealFilesJson) {
+ try {
+ hcsContractSyncSupport.pushArchiveUpdate(id, newSealFilesJson);
+ } catch (Exception exception) {
+ log.error("HCS合同归档推送失败(可重新上传双章归档文件或变更重推补偿):id={},{}", id, exception.getMessage(), exception);
+ }
+ }
+
/**
* 付款推送执行体:非审批通过/已同步短路跳过;失败已由 Support markFailed,仅留痕交扫描/手动重推补偿
*/
From 874bc8dfaac63e7f11719e262a5edb72bd4baecc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 20:47:50 +0800
Subject: [PATCH 58/63] =?UTF-8?q?fix:=20=E4=BE=9B=E5=BA=94=E5=95=86?=
=?UTF-8?q?=E7=BC=96=E7=A0=81=E6=9F=A5=E8=AF=A2=E5=8F=82=E6=95=B0=E5=90=8D?=
=?UTF-8?q?=E4=B8=8E=E5=89=8D=E7=AB=AFcreditCode=E4=BF=9D=E6=8C=81?=
=?UTF-8?q?=E4=B8=80=E8=87=B4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/controller/CustomerArchiveController.java | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java
index 45eb0674..af801acc 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java
@@ -204,8 +204,8 @@ public class CustomerArchiveController extends BladeController {
@GetMapping("/query-supplier-code")
@ApiOperationSupport(order = 12)
@Operation(summary = "金蝶供应商编码查询", description = "A901-1 按统一社会信用代码查询金蝶供应商档案取编码,查不到返回空")
- public R querySupplierCode(@Parameter(description = "统一社会信用代码", required = true) @RequestParam String unifiedCreditCode) {
- return R.data(customerArchiveService.querySupplierCode(unifiedCreditCode));
+ public R querySupplierCode(@Parameter(description = "统一社会信用代码", required = true) @RequestParam String creditCode) {
+ return R.data(customerArchiveService.querySupplierCode(creditCode));
}
/**
From 23c9c3c533c5e3926aa946816d6b5b477df86001 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 20:53:41 +0800
Subject: [PATCH 59/63] =?UTF-8?q?fix:=20=E6=96=B0=E5=A2=9E=E5=AE=A2?=
=?UTF-8?q?=E5=95=86=E9=A3=8E=E9=99=A9=E6=9F=A5=E8=AF=A2=E6=A1=A3=E6=A1=88?=
=?UTF-8?q?=E7=BC=BA=E5=A4=B1=E6=97=B6=E6=8F=90=E7=A4=BA=E5=90=8D=E7=A7=B0?=
=?UTF-8?q?=E5=85=9C=E5=BA=95=E7=94=A8=E5=85=A5=E5=8F=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/controller/HcsSyncController.java | 9 +++++----
.../support/hcs/HcsCreditCheckSupport.java | 17 +++++++++++------
2 files changed, 16 insertions(+), 10 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
index f18be7eb..0a721310 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java
@@ -217,14 +217,15 @@ public class HcsSyncController {
/**
* 客商风险等级查询(C101-1,展示用,前端接入设计 §3.3;强制校验仍在推送/付款链路):
* 内部组织类型客商默认无风险不调合规,外部客商高风险/黑名单返回提示文案;
- * creditCode 供新增客商(档案未落库)查金蝶黑名单兜底,档案存在时以档案为准
+ * creditCode/customerName 供新增客商(档案未落库)查金蝶黑名单、拼装提示名称兜底,档案存在时以档案为准
*/
@GetMapping("/risk")
@ApiOperationSupport(order = 10)
- @Operation(summary = "客商风险等级查询", description = "C101-1 按客商编号查询风险等级与黑名单状态(展示用),内部客商默认无风险,高风险/黑名单返回风险提示文案,结果留痕;creditCode 为新增客商场景金蝶黑名单查询兜底")
+ @Operation(summary = "客商风险等级查询", description = "C101-1 按客商编号查询风险等级与黑名单状态(展示用),内部客商默认无风险,高风险/黑名单返回风险提示文案,结果留痕;creditCode/customerName 为新增客商场景兜底入参")
public R risk(@Parameter(description = "客商编号", required = true) @RequestParam String customerCode,
- @Parameter(description = "统一社会信用代码(新增客商档案未落库时兜底查金蝶黑名单)") @RequestParam(required = false) String creditCode) {
- return R.data(hcsCreditCheckSupport.queryCustomerRisk(customerCode, creditCode));
+ @Parameter(description = "统一社会信用代码(新增客商档案未落库时兜底查金蝶黑名单)") @RequestParam(required = false) String creditCode,
+ @Parameter(description = "客商名称(新增客商档案未落库时提示文案名称兜底)") @RequestParam(required = false) String customerName) {
+ return R.data(hcsCreditCheckSupport.queryCustomerRisk(customerCode, creditCode, customerName));
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
index 2bd22cf2..235443a4 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsCreditCheckSupport.java
@@ -212,19 +212,21 @@ public class HcsCreditCheckSupport {
* @return 风险展示信息(customerKind/fdBlackStatus/fdRiskLevel/risky/riskTip)
*/
public CustomerRiskVO queryCustomerRisk(String customerCode) {
- return this.queryCustomerRisk(customerCode, null);
+ return this.queryCustomerRisk(customerCode, null, null);
}
/**
- * 客商风险查询(带信用代码,新增客商档案表单实时校验用):
+ * 客商风险查询(带信用代码/名称,新增客商档案表单实时校验用):
* 金蝶带出的客商尚未落库(本地档案反查不到)时,信用代码无法从档案取得,
- * 由前端将金蝶档案带出的统一社会信用代码随编号一并传入,保证 A801-5 黑名单段不缺失
+ * 由前端将金蝶档案带出的统一社会信用代码随编号一并传入,保证 A801-5 黑名单段不缺失;
+ * 档案缺失时客商名称同样兜底用传入名称(否则提示文案里只能回显编号)
*
- * @param customerCode 客商编号(金蝶档案带出的编码)
+ * @param customerCode 客商编号(金蝶档案带出的编码或手输编号)
* @param creditCode 统一社会信用代码(档案存在时以档案为准,档案缺失时兜底入参)
+ * @param customerNameParam 客商名称(档案缺失时兜底展示用)
* @return 风险展示信息
*/
- public CustomerRiskVO queryCustomerRisk(String customerCode, String creditCode) {
+ public CustomerRiskVO queryCustomerRisk(String customerCode, String creditCode, String customerNameParam) {
if (StringUtil.isBlank(customerCode)) {
throw new ServiceException("客商编号不能为空");
}
@@ -232,7 +234,10 @@ public class HcsCreditCheckSupport {
.eq(CustomerArchive::getCustomerCode, customerCode)
.eq(CustomerArchive::getIsDeleted, 0)
.last("limit 1"));
- String customerName = customerArchive == null ? customerCode : customerArchive.getFullName();
+ // 档案存在以档案名称为准;档案缺失(新增客商实时校验)兜底用前端传入名称,最后才回显编号
+ String customerName = customerArchive == null
+ ? (StringUtil.isNotBlank(customerNameParam) ? customerNameParam : customerCode)
+ : customerArchive.getFullName();
// 内部组织类型客商默认无风险;档案不存在按外部客商查询(不静默放行)
if (isInternalCustomer(customerArchive)) {
CustomerRiskVO riskVO = new CustomerRiskVO();
From 7810ab4cdb3366fcf424806ef2407719d706e1ee Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Fri, 30 Oct 2026 21:37:29 +0800
Subject: [PATCH 60/63] =?UTF-8?q?feat:=20=E5=90=88=E5=90=8C=E5=88=97?=
=?UTF-8?q?=E8=A1=A8=E5=AE=A2=E5=95=86=E9=A3=8E=E9=99=A9=E8=A1=8C=E7=BB=84?=
=?UTF-8?q?=E8=A3=85=E7=94=B2=E4=B9=99=E5=8F=8C=E6=96=B9=E6=98=8E=E7=BB=86?=
=?UTF-8?q?=E6=96=87=E6=A1=88=E4=BE=9B=E6=82=AC=E6=B5=AE=E5=B1=95=E7=A4=BA?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../transport/pojo/vo/ContractManageVO.java | 4 ++++
.../service/impl/ContractManageServiceImpl.java | 10 ++++++++++
2 files changed, 14 insertions(+)
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java
index a842a147..783fd123 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractManageVO.java
@@ -91,4 +91,8 @@ public class ContractManageVO extends ContractManage {
@Schema(description = "客商黑名单状态(甲乙双方关联客商档案实时组装,任一方金蝶黑名单命中记是):yes/no")
private String kingdeeBlackStatus;
+ @TableField(exist = false)
+ @Schema(description = "客商风险明细文案(甲乙双方风险信息合并,列表悬浮展示,与客商列表/表单实时提示同口径)")
+ private String hcsRiskDetail;
+
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
index ea0274ce..3eec25b4 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java
@@ -70,6 +70,7 @@ import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
+import java.util.stream.Stream;
/**
* 合同管理 服务实现类
@@ -160,6 +161,15 @@ public class ContractManageServiceImpl extends BaseServiceImpl
Date: Sat, 31 Oct 2026 10:16:27 +0800
Subject: [PATCH 61/63] =?UTF-8?q?feat:=20=E6=94=B6=E6=AC=BE=E4=BF=A1?=
=?UTF-8?q?=E6=81=AF=E5=BC=80=E6=88=B7=E8=A1=8C=E5=8C=B9=E9=85=8D=E9=87=91?=
=?UTF-8?q?=E8=9D=B6=E8=A1=8C=E5=90=8D=E8=A1=8C=E5=8F=B7=E5=B8=A6=E5=87=BA?=
=?UTF-8?q?=E8=81=94=E8=A1=8C=E5=8F=B7=E5=B9=B6=E4=BF=9D=E5=AD=98=E5=89=8D?=
=?UTF-8?q?=E6=A0=A1=E9=AA=8C?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 新增 A905-3 行名行号查询能力(BfiBankQueryDTO/BfiBankVO/IBfiClient.bebankQuery/IBfiService.queryBank)
- 客商档案新增 query-bank 按行名精确匹配(失焦回显联行号)与 query-bank-by-number 按行号校验(保存前拦截,异常上抛便于区分查不到与校验失败)
- 清理 BfiServiceImpl 内置发票查询 mock,统一走 BfiClientMock 桩文件
---
.../controller/CustomerArchiveController.java | 21 ++++
.../service/ICustomerArchiveService.java | 17 +++
.../impl/CustomerArchiveServiceImpl.java | 63 ++++++++++
.../thirdparty/bfi/feign/IBfiClient.java | 13 +++
.../bfi/pojo/dto/BfiBankQueryDTO.java | 77 +++++++++++++
.../thirdparty/bfi/pojo/vo/BfiBankVO.java | 109 ++++++++++++++++++
.../thirdparty/bfi/service/IBfiService.java | 10 ++
.../bfi/service/impl/BfiServiceImpl.java | 35 +++---
8 files changed, 323 insertions(+), 22 deletions(-)
create mode 100644 blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiBankQueryDTO.java
create mode 100644 blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBankVO.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java
index af801acc..c2c0b7ab 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java
@@ -38,6 +38,7 @@ import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.secure.utils.AuthUtil;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiBankVO;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.DateUtil;
@@ -208,6 +209,26 @@ public class CustomerArchiveController extends BladeController {
return R.data(customerArchiveService.querySupplierCode(creditCode));
}
+ /**
+ * 金蝶开户行查询(A905-3,收款信息填写开户行失焦精确匹配联行号用)
+ */
+ @GetMapping("/query-bank")
+ @ApiOperationSupport(order = 12)
+ @Operation(summary = "金蝶开户行查询", description = "A905-3 按行名精确匹配金蝶行名行号档案,返回行名与联行号,查不到返回空")
+ public R queryBank(@Parameter(description = "开户行名称", required = true) @RequestParam String bankName) {
+ return R.data(customerArchiveService.queryKingdeeBank(bankName));
+ }
+
+ /**
+ * 金蝶联行号校验(A905-3,收款信息保存前校验联行号真实存在用)
+ */
+ @GetMapping("/query-bank-by-number")
+ @ApiOperationSupport(order = 12)
+ @Operation(summary = "金蝶联行号校验", description = "A905-3 按行号精确匹配金蝶行名行号档案,查不到返回空,接口异常返回错误")
+ public R queryBankByNumber(@Parameter(description = "联行号", required = true) @RequestParam String cnapsCode) {
+ return R.data(customerArchiveService.queryKingdeeBankByNumber(cnapsCode));
+ }
+
/**
* 评分明细模板
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java
index 8f779ca2..c608c664 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java
@@ -25,6 +25,7 @@ package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiBankVO;
import org.springblade.transport.excel.CustomerArchiveExcel;
import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.vo.CustomerArchiveVO;
@@ -204,6 +205,22 @@ public interface ICustomerArchiveService extends BaseService {
*/
String querySupplierCode(String unifiedCreditCode);
+ /**
+ * A905-3 按行名精确匹配金蝶行名行号档案(收款信息填写开户行后失焦回显联行号用),查不到返回 null
+ *
+ * @param bankName 开户行名称(行名全称)
+ * @return 匹配到的行名行号档案,含联行号
+ */
+ BfiBankVO queryKingdeeBank(String bankName);
+
+ /**
+ * A905-3 按行号精确匹配金蝶行名行号档案(收款信息保存前校验联行号真实存在用),查不到返回 null,接口异常上抛由全局异常处理返回
+ *
+ * @param cnapsCode 联行号(行号)
+ * @return 匹配到的行名行号档案
+ */
+ BfiBankVO queryKingdeeBankByNumber(String cnapsCode);
+
/**
* 推送失败状态落库(独立事务):推送主事务已回滚,失败状态与原因单独落库供页面展示与手动重推
*
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
index a83d23ee..92ed75a8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java
@@ -46,8 +46,10 @@ import org.springblade.system.cache.UserCache;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.excel.CustomerArchiveExcel;
import org.springblade.thirdparty.bfi.constant.BfiConstant;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiBankQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiSupplierQueryDTO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiBankVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultErrorVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
@@ -672,6 +674,67 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl page = bfiService.queryBank(param);
+ List rows = page == null ? null : page.getRows();
+ if (rows == null || rows.isEmpty()) {
+ return null;
+ }
+ // 金蝶侧按行名过滤若为模糊匹配,本地再按全称精确比对取第一条
+ String trimmedName = bankName.trim();
+ return rows.stream()
+ .filter(bank -> trimmedName.equals(bank.getName()))
+ .findFirst()
+ .orElse(null);
+ } catch (Exception exception) {
+ log.warn("A905-3行名行号查询失败,bankName={}", bankName, exception);
+ return null;
+ }
+ }
+
+ /**
+ * A905-3 按行号查询金蝶行名行号档案:收款信息保存前校验联行号真实存在,查不到返回 null 由前端拦截保存;
+ * 与 {@link #queryKingdeeBank(String)} 不同,接口异常不上抛吞掉而是直接抛出,便于前端区分「查不到」与「校验失败」
+ */
+ @Override
+ public BfiBankVO queryKingdeeBankByNumber(String cnapsCode) {
+ if (Func.isEmpty(cnapsCode)) {
+ return null;
+ }
+ BfiBankQueryDTO param = new BfiBankQueryDTO();
+ param.setPageNo(1);
+ param.setPageSize(10);
+ param.setStatus(BfiConstant.BILL_STATUS_AUDITED);
+ param.setEnable(BfiConstant.ENABLE_ENABLED);
+ param.setNumber(List.of(cnapsCode.trim()));
+ BfiPageVO page = bfiService.queryBank(param);
+ List rows = page == null ? null : page.getRows();
+ if (rows == null || rows.isEmpty()) {
+ return null;
+ }
+ // 金蝶侧按行号过滤若为模糊匹配,本地再按编码精确比对取第一条
+ String trimmedCode = cnapsCode.trim();
+ return rows.stream()
+ .filter(bank -> trimmedCode.equals(bank.getNumber()))
+ .findFirst()
+ .orElse(null);
+ }
+
/**
* 金蝶已存在档案时,客商编号(customer_code)采纳金蝶客商编码,后续业务接口沿用客商编号即拿到金蝶编码;
* 金蝶编码为空或与当前编号一致时无需处理;同租户下已被其他客商占用时跳过回填,避免违反 uk_customer_archive_code 唯一约束
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java
index d017ce32..3044f5b0 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/feign/IBfiClient.java
@@ -30,6 +30,7 @@ import org.springblade.thirdparty.bfi.config.BfiFeignClientConfig;
import org.springblade.thirdparty.bfi.constant.BfiConstant;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveBodyDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillSaveBodyDTO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiBankQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiBlacklistQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimPushBodyDTO;
@@ -50,6 +51,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiWithdrawTokenDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiArBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiAttachmentUploadVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiBankVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillLockResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBlacklistVO;
@@ -101,6 +103,7 @@ public interface IBfiClient {
String CUSTOMER_QUERY_URL = "${thirdParty.bfi.customerQueryUrl:/ierp/kapi/v2/awrp/basedata/bd_customer/query}";
String CUSTOMER_SAVE_URL = "${thirdParty.bfi.customerSaveUrl:/ierp/kapi/v2/awrp/basedata/bd_customer/save}";
String SUPPLIER_QUERY_URL = "${thirdParty.bfi.supplierQueryUrl:/ierp/kapi/v2/awrp/basedata/bd_supplier/query}";
+ String BEBANK_QUERY_URL = "${thirdParty.bfi.bebankQueryUrl:/ierp/kapi/v2/awrp/basedata/bd_bebank/query}";
String AP_BILL_SAVE_URL = "${thirdParty.bfi.apBillSaveUrl:/ierp/kapi/v2/awrp/ap/ap/ap_finapbill/save}";
String AP_BILL_QUERY_URL = "${thirdParty.bfi.apBillQueryUrl:/ierp/kapi/v2/awrp/ap/ap_finapbill/query}";
String AR_BILL_SAVE_URL = "${thirdParty.bfi.arBillSaveUrl:/ierp/kapi/v2/awrp/ar/ar/ar_finarbill/save}";
@@ -295,6 +298,16 @@ public interface IBfiClient {
@RequestParam(value = "modifytimeS", required = false) String modifytimeS,
@RequestParam(value = "modifytimeE", required = false) String modifytimeE);
+ /**
+ * A905-3 行名行号查询
+ *
+ * @param accessToken 访问令牌,通过 getToken 获取
+ * @param param 查询参数
+ * @return 行名行号分页数据
+ */
+ @GetMapping(BEBANK_QUERY_URL)
+ BfiResultVO> bebankQuery(@RequestHeader(BfiConstant.HEADER_ACCESS_TOKEN) String accessToken, @SpringQueryMap BfiBankQueryDTO param);
+
/**
* A102-2 财务应付单保存
*
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiBankQueryDTO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiBankQueryDTO.java
new file mode 100644
index 00000000..b4c51c6b
--- /dev/null
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiBankQueryDTO.java
@@ -0,0 +1,77 @@
+/**
+ * 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.pojo.dto;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 业财融合(BFI)A905-3 行名行号查询参数
+ *
+ * @author Chill
+ */
+@Data
+@JsonInclude(JsonInclude.Include.NON_NULL)
+public class BfiBankQueryDTO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 【必填】页码,从1开始
+ */
+ private Integer pageNo;
+ /**
+ * 【必填】页数
+ */
+ private Integer pageSize;
+ /**
+ * 数据状态[A:暂存, B:已提交, C:已审核]
+ */
+ private String status;
+ /**
+ * 使用状态[0:禁用, 1:可用]
+ */
+ private String enable;
+ /**
+ * 名称(行名)
+ */
+ private List name;
+ /**
+ * 编码(行号)
+ */
+ private List number;
+ /**
+ * 修改时间-开始,格式:yyyy-MM-dd HH:mm:ss,限定时间范围不可超过6个月
+ */
+ private String modifytimeS;
+ /**
+ * 修改时间-结束,格式:yyyy-MM-dd HH:mm:ss
+ */
+ private String modifytimeE;
+}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBankVO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBankVO.java
new file mode 100644
index 00000000..1f602dd0
--- /dev/null
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBankVO.java
@@ -0,0 +1,109 @@
+/**
+ * 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.pojo.vo;
+
+import com.fasterxml.jackson.annotation.JsonAlias;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 业财融合(BFI)A905-3 行名行号查询返回结果
+ *
+ * @author Chill
+ */
+@Data
+public class BfiBankVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long id;
+ /**
+ * 编码(行号)
+ */
+ private String number;
+ /**
+ * 名称(行名)
+ */
+ private String name;
+ /**
+ * 数据状态[A:暂存, B:已提交, C:已审核]
+ */
+ private String status;
+ /**
+ * 使用状态[0:禁用, 1:可用]
+ */
+ private String enable;
+ /**
+ * 创建时间
+ */
+ private String createtime;
+ /**
+ * 修改时间
+ */
+ private String modifytime;
+ /**
+ * 主数据内码
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long masterid;
+ /**
+ * 联行号
+ */
+ @JsonAlias("union_number")
+ private String unionNumber;
+ /**
+ * Swift Code
+ */
+ @JsonAlias("swift_code")
+ private String swiftCode;
+ /**
+ * 银行类别名称
+ */
+ private String bankcatename;
+ /**
+ * 省份(银企)
+ */
+ private String provincetxt;
+ /**
+ * 城市(银企)
+ */
+ private String citytxt;
+ /**
+ * 分行代码
+ */
+ private String branchcode;
+ /**
+ * 疑似过期[Y:是, N:否]
+ */
+ private String isoverdue;
+}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java
index a8ff0944..3680e198 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java
@@ -30,6 +30,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillSaveDTO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiBankQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimAnnounceQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimPushDTO;
@@ -52,6 +53,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiSupplierQueryDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiArBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiAttachmentUploadVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiBankVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBlacklistVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiClaimAnnounceVO;
@@ -193,6 +195,14 @@ public interface IBfiService {
*/
BfiPageVO querySupplier(BfiSupplierQueryDTO param);
+ /**
+ * A905-3 行名行号查询
+ *
+ * @param param 查询参数,页码从1开始,修改时间范围不可超过6个月
+ * @return 行名行号分页数据
+ */
+ BfiPageVO queryBank(BfiBankQueryDTO param);
+
/**
* A102-2 财务应付单保存
*
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java
index f3301b41..e010b0f5 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java
@@ -43,6 +43,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillSaveBodyDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillSaveDTO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiBankQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiBlacklistQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimAnnounceQueryDTO;
@@ -79,6 +80,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiWithdrawTokenDTO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiArBillVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiAttachmentUploadVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiBankVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillLockResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
@@ -346,6 +348,16 @@ public class BfiServiceImpl implements IBfiService {
return result.getData();
}
+ @Override
+ public BfiPageVO queryBank(BfiBankQueryDTO param) {
+ if (param == null || param.getPageNo() == null || param.getPageSize() == null) {
+ throw new BfiException("查询BFI行名行号失败,分页参数不能为空");
+ }
+ String accessToken = getAccessToken();
+ BfiResultVO> result = commonCall("行名行号查询", () -> client.bebankQuery(accessToken, param));
+ return result.getData();
+ }
+
@Override
public BfiBillSaveResultVO saveApBill(BfiApBillSaveDTO param) {
if (param == null || StringUtil.isBlank(param.getAwrpSrcSys()) || StringUtil.isBlank(param.getAwrpSrcCode())) {
@@ -474,10 +486,7 @@ public class BfiServiceImpl implements IBfiService {
if (param == null || StringUtil.isBlank(param.getSerialNo()) || StringUtil.isBlank(param.getSellerTaxpayerId())) {
throw new BfiException("查询BFI开票申请发票失败,单据编号与销方税号不能为空");
}
- if (properties.isMockEnabled()) {
- log.info("【MOCK】查询BFI开票申请发票,serialNo={}", param.getSerialNo());
- return mockQueryInvoiceApplyInvoice();
- }
+ // mock 模式由 BfiClientMock 读取桩文件 invoiceApplyOpenApi.json(有桩用桩、无桩透传真实接口),此处不再内置固定响应
// data 原文需序列化为 JSON 后 Base64 编码
String data = Base64.getEncoder().encodeToString(JSONUtil.toJsonStr(param).getBytes(StandardCharsets.UTF_8));
BfiInvoiceApplyOpenApiBodyDTO body = new BfiInvoiceApplyOpenApiBodyDTO();
@@ -767,22 +776,4 @@ public class BfiServiceImpl implements IBfiService {
long expireMillis = tokenVO.getExpiresIn() - BfiConstant.TOKEN_EXPIRE_BUFFER_MILLIS;
bladeRedis.getValueOps().setIfAbsent(BfiConstant.BFI_TOKEN_CACHE_KEY, tokenVO.getAccessToken(), Duration.ofMillis(expireMillis));
}
-
- /**
- * Mock 开票申请发票查询(开发调试用)
- *
- * @return mock 发票查询结果
- */
- private BfiOpenApiResultVO> mockQueryInvoiceApplyInvoice() {
- String mockData = "W3siaW52b2ljZUxpc3QiOlt7ImFib2xpc2hSZWFzb24iOiIiLCJiaWxsTm8iOiJLUFNRLUpDWlgtMjAyNjAzLTAwMDU4MCIsImJ1eWVyQWRkcmVzc0FuZFRlbCI6IumSpuW3nuW4gumSpuW3nua4r+WvjOa4r+aWsOWfjjMw5qCLMDjoh7MxM+WPt+WSjDMx5qCLMDHoh7MwOeWPt+WVhumTujA3NzctMzY2NTExMSIsImJ1eWVyQmFua0FuZEFjY291bnQiOiLkuK3lm73lhpzkuJrpk7booYzogqHku73mnInpmZDlhazlj7jpkqblt57muK/mlK/ooYwgMjA3NDA0MDEwNDAwMTA3NjYiLCJidXllck5hbWUiOiLlub/opb/moYLlu7rlt6XnqIvotKjph4/mo4DmtYvmnInpmZDlhazlj7giLCJidXllclByb3BlcnR5IjowLCJidXllclJlY2lwaWVudE1haWwiOiIiLCJidXllclJlY2lwaWVudFBob25lIjoiIiwiYnV5ZXJUYXhwYXllcklkIjoiOTE0NTA3MDQwNTEwMDE2ODdRIiwiY2FuUmVkQW1vdW50IjozNjIyLjY0LCJjYW5SZWRUYXhBbW91bnQiOjIxNy4zNiwiY2FuY2VsZXIiOiIiLCJjaGVja0NvZGUiOiIiLCJkZXZpY2VObyI6IiIsImRvd25sb2FkRmxhZyI6MiwiZHJhd2VyIjoi6ZmI5bCP5bCPIiwiZ29vZHMiOlt7ImFtb3VudCI6MzYyMi42NCwiYmlsbFNvdXJjZUlkIjoiIiwiZGV0YWlsSWQiOiIyNDM3OTM3MTA2NTMwNjE5MzkzIiwiZGV0YWlsUm93Tm8iOjEsImdvb2RzTmFtZSI6IirpibTor4Hlkqjor6LmnI3liqEq5qOA5rWL6LS5IiwiaW5jbHVkZVRheEFtb3VudCI6Mzg0MC4wMCwiaW5jbHVkZVRheFByaWNlIjoiMzg0MCIsImxpbmVQcm9wZXJ0eSI6MCwicHJpY2UiOiIzNjIyLjY0MTUwOTQzMzk2IiwicHJpdklsZWdlQ29udGVudCI6IiIsInByaXZpbGVnZUZsYWciOjAsInF1YW50aXR5IjoiMSIsInJlbWFpblJlZEFtb3VudCI6MzYyMi42NCwicmVtYWluUmVkUXVhbnRpdHkiOiIxIiwicmVtYWluUmVkVGF4IjoyMTcuMzYsInJldmVudWVDb2RlIjoiMzA0MDYwMjk5MDAwMDAwMDAwMCIsInJldmVudWVOYW1lIjoiKumJtOivgeWSqOivouacjeWKoSoiLCJzZXEiOjAsInNwZWNpZmljYXRpb24iOiIiLCJ0YXhBbW91bnQiOjIxNy4zNiwidGF4RmxhZyI6IjEiLCJ0YXhSYXRlIjoiMC4wNiIsInVuaXRzIjoi6aG5IiwiemVyb1RheFJhdGVGbGFnIjoiIn1dLCJpZCI6MjQzNzkzNzEwNjUzMDYxOTM5MiwiaW5jbHVkZVRheEFtb3VudCI6Mzg0MC4wMCwiaW52ZW50b3J5RmxhZyI6MCwiaW52b2ljZUNvZGUiOiIiLCJpbnZvaWNlRGF0ZSI6MTc3MzgyNDc2NDAwMCwiaW52b2ljZUZpbGVVcmwiOiJodHRwczovL2FwaS5waWFvem9uZS5jb20vcnBhL2ZyZWUvcHJldmlldy8zNjYxOTQwMDM3ODk2OTEzOTIvOTE0NTAwMDBNQTVRMUcxTTU5LzVORlkyWEZHNlNWV0lXNzJPUDA1P3R5cGU9MSIsImludm9pY2VJbWFnZVVybCI6Imh0dHBzOi8vYXBpLnBpYW96b25lLmNvbS9ycGEvZnJlZS9wcmV2aWV3LzM2NjE5NDAwMzc4OTY5MTM5Mi85MTQ1MDAwME1BNVExRzFNNTkvNU5GWTJYRkc2U1ZXSVc3Mk9QMDU/dHlwZT0yIiwiaW52b2ljZU51bWJlciI6IjI2NDUyMDAwMDAwNDIzMjQwOTE2IiwiaW52b2ljZVBkZkZpbGVVcmwiOiJodHRwczovL2FwaS5waWFvem9uZS5jb20vcnBhL2ZyZWUvcHJldmlldy8zNjYxOTQwMDM3ODk2OTEzOTIvOTE0NTAwMDBNQTVRMUcxTTU5LzVORlkyWEZHNlNWV0lXNzJPUDA1P3R5cGU9MCIsImludm9pY2VQcm9wZXJ0eSI6MCwiaW52b2ljZVNvdXJjZSI6IjQiLCJpbnZvaWNlU3RhdHVzIjowLCJpbnZvaWNlVHlwZSI6IjA4eGRwIiwiaW52b2ljZVhtbEZpbGVVcmwiOiJodHRwczovL2FwaS5waWFvem9uZS5jb20vcnBhL2ZyZWUvcHJldmlldy8zNjYxOTQwMDM3ODk2OTEzOTIvOTE0NTAwMDBNQTVRMUcxTTU5LzVORlkyWEZHNlNWV0lXNzJPUDA1P3R5cGU9MyIsImlzc3VlU3RhdHVzIjoiMCIsIm1lcmdlRGlmZmVyVGF4QW1vdW50IjowLjAwLCJvcmdOYW1lIjoi5bm/6KW/5aOu5peP6Ieq5rK75Yy65bu6562R5bel56iL6LSo6YeP5qOA5rWL5Lit5b+D5pyJ6ZmQ5YWs5Y+4Iiwib3JpZ2luYWxJbnZvaWNlQ29kZSI6IiIsIm9yaWdpbmFsSW52b2ljZU51bWJlciI6IiIsInBheWVlIjoiIiwicHJpbnRGbGFnIjowLCJyZWRJbmZvQmlsbE5vIjoiIiwicmVkUmVhc29uIjoiIiwicmVtYXJrIjoiIiwicmVzdWx0IjoiIiwicmV2aWV3ZXIiOiIiLCJzZWxsZXJBZGRyZXNzQW5kVGVsIjoi5Y2X5a6B5biC6KW/5Lmh5aGY5Yy65YyX6ZmF6LevMeWPtzA3NzEtMzE3OTYwMSIsInNlbGxlckJhbmtBbmRBY2NvdW50Ijoi5bel5ZWG6ZO26KGM5Y2X5a6B5biC5rC45paw5pSv6KGMMjEwMjEwNDAwOTMwMDI5NjA0MSIsInNlbGxlck5hbWUiOiLlub/opb/lo67ml4/oh6rmsrvljLrlu7rnrZHlt6XnqIvotKjph4/mo4DmtYvkuK3lv4PmnInpmZDlhazlj7giLCJzZWxsZXJUYXhwYXllcklkIjoiOTE0NTAwMDBNQTVRMUcxTTU5Iiwic2VyaWFsTm8iOiIyMDBfMjAyNjAzMTgxNzA1NTAwMDFfMDAwMSIsInNrbSI6IiIsInNwZWNpYWxSZWRGbGFnIjowLCJzcGxpdE9yTWVyZ2VGbGFnIjoiMCIsInRheEZsYWciOiIxIiwidGVybWluYWxObyI6IiIsInRvdGFsQW1vdW50IjozNjIyLjY0LCJ0b3RhbFRheEFtb3VudCI6MjE3LjM2fV0sImJpbGxObyI6IktQU1EtSkNaWC0yMDI2MDMtMDAwNTgwIn1d";
- BfiOpenApiResultVO> result = new BfiOpenApiResultVO<>();
- result.setErrorCode("0");
- result.setSuccess(true);
- String decodedJson = new String(Base64.getDecoder().decode(mockData), StandardCharsets.UTF_8);
- JSONObject decodedObj = JSONUtil.parseObj(decodedJson);
- result.setErrorCode(decodedObj.getStr("errorCode"));
- result.setSuccess(decodedObj.getBool("success"));
- result.setData(decodedObj.getBeanList("data", BfiInvoiceApplyOpenApiBillVO.class));
- return result;
- }
}
From debb8e955f242c4c9488ea9f1f2bd726634b42ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Mon, 2 Nov 2026 09:42:00 +0800
Subject: [PATCH 62/63] =?UTF-8?q?fix:=20sql=E7=A7=BB=E5=8A=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../01_blade_payment_application_kingdee_fail_reason_20261028.sql | 0
.../01_blade_payment_spec_attach_fd_id_20261027.sql | 0
.../02_blade_customer_archive_hcs_risk_20261030.sql | 0
.../02_blade_kingdee_fail_reason_20261028.sql | 0
.../{temp => 20261102}/03_blade_customer_risk_dict_20261030.sql | 0
.../04_blade_contract_black_status_rename_20261030.sql | 0
.../05_blade_contract_risk_fields_drop_20261030.sql | 0
.../blade_customer_archive_add_supplier_code_20261030.sql | 0
8 files changed, 0 insertions(+), 0 deletions(-)
rename doc/sql/transport/lqj/{20261028 => 20261102}/01_blade_payment_application_kingdee_fail_reason_20261028.sql (100%)
rename doc/sql/transport/lqj/{20261027 => 20261102}/01_blade_payment_spec_attach_fd_id_20261027.sql (100%)
rename doc/sql/transport/lqj/{temp => 20261102}/02_blade_customer_archive_hcs_risk_20261030.sql (100%)
rename doc/sql/transport/lqj/{20261028 => 20261102}/02_blade_kingdee_fail_reason_20261028.sql (100%)
rename doc/sql/transport/lqj/{temp => 20261102}/03_blade_customer_risk_dict_20261030.sql (100%)
rename doc/sql/transport/lqj/{temp => 20261102}/04_blade_contract_black_status_rename_20261030.sql (100%)
rename doc/sql/transport/lqj/{temp => 20261102}/05_blade_contract_risk_fields_drop_20261030.sql (100%)
rename doc/sql/transport/lqj/{temp => 20261102}/blade_customer_archive_add_supplier_code_20261030.sql (100%)
diff --git a/doc/sql/transport/lqj/20261028/01_blade_payment_application_kingdee_fail_reason_20261028.sql b/doc/sql/transport/lqj/20261102/01_blade_payment_application_kingdee_fail_reason_20261028.sql
similarity index 100%
rename from doc/sql/transport/lqj/20261028/01_blade_payment_application_kingdee_fail_reason_20261028.sql
rename to doc/sql/transport/lqj/20261102/01_blade_payment_application_kingdee_fail_reason_20261028.sql
diff --git a/doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql b/doc/sql/transport/lqj/20261102/01_blade_payment_spec_attach_fd_id_20261027.sql
similarity index 100%
rename from doc/sql/transport/lqj/20261027/01_blade_payment_spec_attach_fd_id_20261027.sql
rename to doc/sql/transport/lqj/20261102/01_blade_payment_spec_attach_fd_id_20261027.sql
diff --git a/doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql b/doc/sql/transport/lqj/20261102/02_blade_customer_archive_hcs_risk_20261030.sql
similarity index 100%
rename from doc/sql/transport/lqj/temp/02_blade_customer_archive_hcs_risk_20261030.sql
rename to doc/sql/transport/lqj/20261102/02_blade_customer_archive_hcs_risk_20261030.sql
diff --git a/doc/sql/transport/lqj/20261028/02_blade_kingdee_fail_reason_20261028.sql b/doc/sql/transport/lqj/20261102/02_blade_kingdee_fail_reason_20261028.sql
similarity index 100%
rename from doc/sql/transport/lqj/20261028/02_blade_kingdee_fail_reason_20261028.sql
rename to doc/sql/transport/lqj/20261102/02_blade_kingdee_fail_reason_20261028.sql
diff --git a/doc/sql/transport/lqj/temp/03_blade_customer_risk_dict_20261030.sql b/doc/sql/transport/lqj/20261102/03_blade_customer_risk_dict_20261030.sql
similarity index 100%
rename from doc/sql/transport/lqj/temp/03_blade_customer_risk_dict_20261030.sql
rename to doc/sql/transport/lqj/20261102/03_blade_customer_risk_dict_20261030.sql
diff --git a/doc/sql/transport/lqj/temp/04_blade_contract_black_status_rename_20261030.sql b/doc/sql/transport/lqj/20261102/04_blade_contract_black_status_rename_20261030.sql
similarity index 100%
rename from doc/sql/transport/lqj/temp/04_blade_contract_black_status_rename_20261030.sql
rename to doc/sql/transport/lqj/20261102/04_blade_contract_black_status_rename_20261030.sql
diff --git a/doc/sql/transport/lqj/temp/05_blade_contract_risk_fields_drop_20261030.sql b/doc/sql/transport/lqj/20261102/05_blade_contract_risk_fields_drop_20261030.sql
similarity index 100%
rename from doc/sql/transport/lqj/temp/05_blade_contract_risk_fields_drop_20261030.sql
rename to doc/sql/transport/lqj/20261102/05_blade_contract_risk_fields_drop_20261030.sql
diff --git a/doc/sql/transport/lqj/temp/blade_customer_archive_add_supplier_code_20261030.sql b/doc/sql/transport/lqj/20261102/blade_customer_archive_add_supplier_code_20261030.sql
similarity index 100%
rename from doc/sql/transport/lqj/temp/blade_customer_archive_add_supplier_code_20261030.sql
rename to doc/sql/transport/lqj/20261102/blade_customer_archive_add_supplier_code_20261030.sql
From 847a9cae234cba564649c244c91e5b0f0fbc33fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com>
Date: Tue, 3 Nov 2026 18:20:59 +0800
Subject: [PATCH 63/63] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=E9=87=91?=
=?UTF-8?q?=E8=9D=B6=E4=BB=98=E6=AC=BE=E5=A4=84=E7=90=86=E6=9F=A5=E8=AF=A2?=
=?UTF-8?q?=E5=93=8D=E5=BA=94=E7=BC=BA=E5=A4=B1Content-Type=E5=AF=BC?=
=?UTF-8?q?=E8=87=B4=E8=A7=A3=E7=A0=81=E5=A4=B1=E8=B4=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../bfi/config/BfiFeignClientConfig.java | 28 +++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiFeignClientConfig.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiFeignClientConfig.java
index 535935c7..72396d90 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiFeignClientConfig.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/config/BfiFeignClientConfig.java
@@ -27,14 +27,20 @@ package org.springblade.thirdparty.bfi.config;
import feign.Request;
import feign.RequestInterceptor;
+import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springblade.thirdparty.bfi.interceptor.BfiRequestInterceptor;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
+import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
+import org.springframework.http.MediaType;
+import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
+import java.util.Collections;
+import java.util.List;
import java.util.concurrent.TimeUnit;
/**
@@ -71,4 +77,26 @@ public class BfiFeignClientConfig {
public Encoder feignFormEncoder(ObjectFactory messageConverters) {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
+
+ /**
+ * BFI 专用解码器:金蝶部分接口成功响应缺失 Content-Type 头(kong 网关 chunked 转发丢失,
+ * 如 cas_paybill/query),默认解码器按 application/octet-stream 匹配不到 JSON 转换器导致
+ * "no suitable HttpMessageConverter" 解码失败。缺失 Content-Type 时强制按 JSON 解码,
+ * 其余响应保持默认解码逻辑(含二进制下载接口)
+ */
+ @Bean
+ public Decoder bfiDecoder(ObjectFactory messageConverters) {
+ SpringDecoder defaultDecoder = new SpringDecoder(messageConverters);
+ MappingJackson2HttpMessageConverter jacksonConverter = new MappingJackson2HttpMessageConverter();
+ jacksonConverter.setSupportedMediaTypes(List.of(MediaType.APPLICATION_JSON, MediaType.ALL));
+ SpringDecoder forceJsonDecoder = new SpringDecoder(() -> new HttpMessageConverters(jacksonConverter));
+ return (response, type) -> {
+ boolean missingContentType = response.headers().keySet().stream()
+ .noneMatch("content-type"::equalsIgnoreCase);
+ if (missingContentType) {
+ return forceJsonDecoder.decode(response, type);
+ }
+ return defaultDecoder.decode(response, type);
+ };
+ }
}