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 001/106] =?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 002/106] =?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 003/106] =?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 004/106] =?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 005/106] =?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 006/106] =?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 007/106] =?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 008/106] =?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 009/106] =?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 010/106] =?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 011/106] =?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 012/106] =?UTF-8?q?:sparkles:=20=E8=A1=A5=E5=85=85?=
=?UTF-8?q?=E9=87=91=E8=9D=B6=E9=BB=91=E5=90=8D=E5=8D=95B=E7=B1=BB?=
=?UTF-8?q?=E6=8B=A6=E6=88=AA=E5=BC=80=E5=85=B3Nacos=E9=85=8D=E7=BD=AE?=
=?UTF-8?q?=EF=BC=88blockRiskWarning=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 013/106] =?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 014/106] =?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 015/106] =?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 016/106] =?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 017/106] =?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 018/106] =?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 019/106] =?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 020/106] =?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 021/106] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8DA901-1?=
=?UTF-8?q?=E4=BE=9B=E5=BA=94=E5=95=86=E6=9F=A5=E8=AF=A2=E6=9C=AA=E8=BF=87?=
=?UTF-8?q?=E6=BB=A4=E5=B7=B2=E5=AE=A1=E6=A0=B8=E5=8F=AF=E7=94=A8=E7=8A=B6?=
=?UTF-8?q?=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 022/106] =?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 023/106] =?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 024/106] =?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 025/106] =?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 026/106] =?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 027/106] =?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 028/106] =?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 029/106] =?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 030/106] =?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 031/106] =?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 032/106] =?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 033/106] =?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 034/106] =?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 035/106] =?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 036/106] =?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 037/106] =?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 3ddb2a3c0782792d6b37fd3862aed0bdb91e42ca 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:19:17 +0800
Subject: [PATCH 038/106] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9EHCS=20C107-3?=
=?UTF-8?q?=E8=AE=A4=E9=A2=86=E8=AE=B0=E5=BD=95=E4=BF=AE=E6=94=B9=E6=8E=A5?=
=?UTF-8?q?=E5=8F=A3=E5=B0=81=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../org/springblade/hcs/dto/HcsClaimRecordAddDTO.java | 4 ++++
.../java/org/springblade/hcs/feign/IHcsClient.java | 10 ++++++++++
2 files changed, 14 insertions(+)
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsClaimRecordAddDTO.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsClaimRecordAddDTO.java
index 8185a17c..627f7b7f 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsClaimRecordAddDTO.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/dto/HcsClaimRecordAddDTO.java
@@ -47,6 +47,10 @@ import java.math.BigDecimal;
public class HcsClaimRecordAddDTO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
+ /**
+ * 认领记录ID(C107-1 新增不传,C107-3 修改时必填,作废传 fdStatus=1)
+ */
+ private String fdId;
/**
* 收款流水(fdId 必填,为合规系统流水ID)
*/
diff --git a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java
index 916c36f0..c21952e4 100644
--- a/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java
+++ b/blade-third-party-api/blade-hcs-api/src/main/java/org/springblade/hcs/feign/IHcsClient.java
@@ -108,6 +108,7 @@ public interface IHcsClient {
String INVOICE_REG_ADD_URL = "${thirdParty.hcs.invoiceRegAddUrl:/openapi/eop-data/eopDataInvoiceRegistrationOpen/add}";
String INVOICE_REG_UPDATE_URL = "${thirdParty.hcs.invoiceRegUpdateUrl:/openapi/eop-data/eopDataInvoiceRegistrationOpen/update}";
String CLAIM_RECORD_ADD_URL = "${thirdParty.hcs.claimRecordAddUrl:/openapi/eop-data/eopDataCollectionFlowClaimRecordOpen/add}";
+ String CLAIM_RECORD_UPDATE_URL = "${thirdParty.hcs.claimRecordUpdateUrl:/openapi/eop-data/eopDataCollectionFlowClaimRecordOpen/update}";
String COLLECTION_FLOW_LIST_URL = "${thirdParty.hcs.collectionFlowListUrl:/openapi/eop-data/eopDataCollectionFlowOpen/list}";
/**
@@ -341,6 +342,15 @@ public interface IHcsClient {
@PostMapping(CLAIM_RECORD_ADD_URL)
HcsResult claimRecordAdd(@RequestBody HcsClaimRecordAddDTO param);
+ /**
+ * C107-3 修改认领记录(fdId 必填;作废认领传 fdStatus=1)
+ *
+ * @param param 请求参数(入参同新增,仅 fdId 必填)
+ * @return 修改认领记录 fdId
+ */
+ @PostMapping(CLAIM_RECORD_UPDATE_URL)
+ HcsResult claimRecordUpdate(@RequestBody HcsClaimRecordAddDTO param);
+
/**
* C104-2 查询收款流水列表(认领记录推送前按明细流水号定位合规侧收款流水ID)
*
From 7569594dcbaad41d2b54cb659c6c214648c91a6e 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:19:25 +0800
Subject: [PATCH 039/106] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=94=B6?=
=?UTF-8?q?=E6=AC=BE=E8=AE=A4=E9=A2=86=E5=90=88=E8=A7=84=E6=8E=A8=E9=80=81?=
=?UTF-8?q?=E8=AE=B0=E5=BD=95=E8=A1=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/ReceiptClaimHcsRecord.java | 96 +++++++++++++++++++
.../mapper/ReceiptClaimHcsRecordMapper.java | 39 ++++++++
...lade_receipt_claim_hcs_record_20261028.sql | 27 ++++++
3 files changed, 162 insertions(+)
create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaimHcsRecord.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimHcsRecordMapper.java
create mode 100644 doc/sql/transport/lqj/20261028/01_blade_receipt_claim_hcs_record_20261028.sql
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaimHcsRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaimHcsRecord.java
new file mode 100644
index 00000000..265eb54e
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ReceiptClaimHcsRecord.java
@@ -0,0 +1,96 @@
+/**
+ * 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.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.core.tenant.mp.TenantEntity;
+
+import java.io.Serial;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 收款认领合规推送记录实体类
+ *
+ * C107-1 认领记录推送结果落库(认领主单 × 合同一条):成功持久化合规侧 fdId 供 C107-3 作废引用,
+ * 失败落推送标记与原因(仅记标记,不做定时重试)。
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("blade_receipt_claim_hcs_record")
+@Schema(description = "收款认领合规推送记录")
+public class ReceiptClaimHcsRecord extends TenantEntity {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 认领主单id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long receiptClaimId;
+ /**
+ * 收款流水id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long receiptFlowId;
+ /**
+ * 合同id
+ */
+ @JsonSerialize(using = ToStringSerializer.class)
+ private Long contractId;
+ /**
+ * 合同编号
+ */
+ private String contractNo;
+ /**
+ * 该认领主单分摊到本合同的认领金额
+ */
+ private BigDecimal claimAmount;
+ /**
+ * 合规侧认领记录fdId(C107-1 返回,C107-3 作废必填引用)
+ */
+ private String hcsClaimRecordFdId;
+ /**
+ * 推送状态(success推送成功/failed推送失败/voided已作废)
+ */
+ private String pushStatus;
+ /**
+ * 推送失败原因(成功后清空)
+ */
+ private String failReason;
+ /**
+ * 推送成功时间
+ */
+ private LocalDateTime pushedTime;
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimHcsRecordMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimHcsRecordMapper.java
new file mode 100644
index 00000000..feac846d
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ReceiptClaimHcsRecordMapper.java
@@ -0,0 +1,39 @@
+/**
+ * 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.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.springblade.transport.pojo.entity.ReceiptClaimHcsRecord;
+
+/**
+ * 收款认领合规推送记录 Mapper 接口
+ *
+ * @author Chill
+ */
+@Mapper
+public interface ReceiptClaimHcsRecordMapper extends BaseMapper {
+}
diff --git a/doc/sql/transport/lqj/20261028/01_blade_receipt_claim_hcs_record_20261028.sql b/doc/sql/transport/lqj/20261028/01_blade_receipt_claim_hcs_record_20261028.sql
new file mode 100644
index 00000000..457ec803
--- /dev/null
+++ b/doc/sql/transport/lqj/20261028/01_blade_receipt_claim_hcs_record_20261028.sql
@@ -0,0 +1,27 @@
+-- 收款认领合规推送记录表:C107-1 认领记录推送结果落库(认领主单 × 合同一条)
+-- 成功持久化合规侧 fdId 供认领作废 C107-3 引用;失败落推送标记与原因(仅记标记,不做定时重试)
+-- 配套改造:收款流水菜单数据源 A301-2 → A301-1,认领事务提交后推送合规链(C103-9/C107-1/C108-1)
+
+CREATE TABLE IF NOT EXISTS `blade_receipt_claim_hcs_record` (
+ `id` bigint NOT NULL COMMENT '主键',
+ `tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户ID',
+ `receipt_claim_id` bigint NOT NULL COMMENT '认领主单id',
+ `receipt_flow_id` bigint NOT NULL COMMENT '收款流水id',
+ `contract_id` bigint DEFAULT NULL COMMENT '合同id',
+ `contract_no` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合同编号',
+ `claim_amount` decimal(19, 2) DEFAULT NULL COMMENT '该认领主单分摊到本合同的认领金额',
+ `hcs_claim_record_fd_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合规侧认领记录fdId(C107-1返回,C107-3作废必填引用)',
+ `push_status` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '推送状态(success推送成功/failed推送失败/voided已作废)',
+ `fail_reason` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '推送失败原因(成功后清空)',
+ `pushed_time` datetime DEFAULT NULL COMMENT '推送成功时间',
+ `create_user` bigint DEFAULT NULL COMMENT '创建人',
+ `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_user` bigint DEFAULT NULL COMMENT '修改人',
+ `update_time` datetime DEFAULT NULL COMMENT '修改时间',
+ `status` int DEFAULT NULL COMMENT '状态',
+ `is_deleted` int DEFAULT 0 COMMENT '是否已删除',
+ PRIMARY KEY (`id`),
+ KEY `idx_receipt_claim_id` (`receipt_claim_id`),
+ KEY `idx_receipt_flow_id` (`receipt_flow_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='收款认领合规推送记录表';
From 4a76e65b4d31369083997ae975dcd6e00a12e8a7 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:19:32 +0800
Subject: [PATCH 040/106] =?UTF-8?q?feat:=20=E5=90=88=E8=A7=84=E8=AE=A4?=
=?UTF-8?q?=E9=A2=86=E6=8E=A8=E9=80=81=E6=94=B9=E4=B8=BA=E6=8C=89=E8=AE=A4?=
=?UTF-8?q?=E9=A2=86=E4=B8=BB=E5=8D=95=E8=A7=A6=E5=8F=91=E5=B9=B6=E6=96=B0?=
=?UTF-8?q?=E5=A2=9EC107-3=E4=BD=9C=E5=BA=9F=E5=90=8C=E6=AD=A5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../support/hcs/HcsClaimSyncSupport.java | 276 +++++++++++++-----
1 file changed, 209 insertions(+), 67 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 8577b62d..973fdbea 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
@@ -11,8 +11,8 @@
* 2. Redistribution of this software's source code to any third party
* is strictly prohibited.
*
- * 3. Licensees may copyright their own code but cannot use segments
- * from this software for their own commercial purposes.
+ * 3. Licensees may copyright their own code but cannot use segments of this software
+ * for their own commercial purposes.
*
* Using this software signifies agreement to this license agreement and terms of use,
* and you expressly agree to be bound by the terms of use of this software.
@@ -33,17 +33,24 @@ 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;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.KingdeeReceiptFlowMapper;
+import org.springblade.transport.mapper.ReceiptClaimHcsRecordMapper;
import org.springblade.transport.mapper.ReceiptClaimMapper;
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
import org.springblade.transport.pojo.entity.ContractManage;
@@ -51,12 +58,14 @@ import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.HcsContractExeStage;
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
import org.springblade.transport.pojo.entity.ReceiptClaim;
+import org.springblade.transport.pojo.entity.ReceiptClaimHcsRecord;
import org.springblade.transport.pojo.entity.ReceiptClaimSettlement;
import org.springblade.transport.service.IContractExeStageService;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.time.LocalDate;
+import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.LinkedHashMap;
import java.util.List;
@@ -67,13 +76,14 @@ import java.util.stream.Collectors;
/**
* HCS 收款认领链推送编排支撑组件(C103-9 更新履约计划 + C107-1 认领记录 + C108-1 履约流水)
*
- * 触发点:金蝶认领推送(A301-3)成功之后(手动按钮与定时任务共用钩子)。
+ * 触发点:收款流水认领事务提交后按认领主单推送(作废认领事务提交后推 C107-3 作废同步)。
* 开关复用既有配置(不单独新增):exe-stage.sync-enabled 门禁 C103-9/C107-1(与合同链履约计划同步同开关),
* perf-record.sync-enabled 门禁 C108-1(与付款链履约记录同步同开关)。
* 编排顺序:先 C103-9(认领金额累加到收款履约计划行并重推,C107-1/C108-1 引用其返回的阶段ID),
- * 再按「认领主单 × 合同」推 C107-1(合规流水ID 经 C104-2 按明细流水号定位),最后按合同推 C108-1 履约流水。
+ * 再按「认领主单 × 合同」推 C107-1(合规流水ID 经 C104-2 按明细流水号定位,返回 fdId 落库
+ * blade_receipt_claim_hcs_record 供作废 C107-3 引用),最后按合同推 C108-1 履约流水。
* 合同未推送合规(hcs_fd_id 空)时仅累加计划行金额,随合同链路首推。
- * 推送失败仅记日志不回滚金蝶推送结果(单合同失败不影响其余合同)。
+ * 推送失败仅落失败标记与日志,不回滚认领主流程(单合同失败不影响其余合同,不做定时重试)。
*
* @author Chill
*/
@@ -86,60 +96,80 @@ public class HcsClaimSyncSupport {
* 认领完成状态(与 ReceiptFlowServiceImpl.CLAIMED 同值)
*/
private static final String CLAIMED = "claimed";
+ /**
+ * 推送状态:推送成功
+ */
+ private static final String PUSH_SUCCESS = "success";
+ /**
+ * 推送状态:推送失败(仅记标记,不做定时重试)
+ */
+ private static final String PUSH_FAILED = "failed";
+ /**
+ * 推送状态:已作废(C107-3 同步成功)
+ */
+ private static final String PUSH_VOIDED = "voided";
+ /**
+ * 认领记录状态:已认领
+ */
+ private static final int CLAIM_STATUS_CLAIMED = 0;
+ /**
+ * 认领记录状态:已作废
+ */
+ private static final int CLAIM_STATUS_VOIDED = 1;
private final IHcsClient hcsClient;
private final HcsProperties hcsProperties;
private final KingdeeReceiptFlowMapper receiptFlowMapper;
private final ReceiptClaimMapper claimMapper;
private final ReceiptClaimSettlementMapper claimSettlementMapper;
+ private final ReceiptClaimHcsRecordMapper claimHcsRecordMapper;
private final FormalSettlementMapper formalSettlementMapper;
private final ContractManageMapper contractManageMapper;
private final IContractExeStageService contractExeStageService;
private final HcsExeStageSyncSupport hcsExeStageSyncSupport;
- private final HcsPerfRecordAddSupport hcsPerfRecordAddSupport;
/**
- * 金蝶认领推送成功后推送合同合规(C103-9 → C107-1 → C108-1)
+ * 认领事务提交后推送合同合规(C103-9 → C107-1 → C108-1,按认领主单维度)
*
* 复用既有开关:exe-stage.sync-enabled(履约计划同步)门禁 C103-9/C107-1,
* perf-record.sync-enabled(履约记录同步)门禁 C108-1;开关关闭或推送异常均静默返回,
- * 不影响金蝶认领推送主流程与事务。
+ * 不影响认领主流程与事务。
*
- * @param flowId 收款流水ID
+ * @param claimId 认领主单ID
*/
- public void pushClaimToHcs(Long flowId) {
+ public void pushClaimToHcs(Long claimId) {
if (!hcsProperties.getExeStage().isSyncEnabled()) {
return;
}
try {
- doPushClaimToHcs(flowId);
+ doPushClaimToHcs(claimId);
} catch (Exception exception) {
- log.error("HCS收款认领链推送异常 flowId:{}", flowId, exception);
+ log.error("HCS收款认领链推送异常 claimId:{}", claimId, exception);
}
}
/**
- * 推送主体:归集有效认领分摊(按合同分组)→ 逐合同推送(单合同失败不影响其余合同)
+ * 推送主体:归集本认领主单有效分摊(按合同分组)→ 逐合同推送(单合同失败不影响其余合同)
*/
- private void doPushClaimToHcs(Long flowId) {
- KingdeeReceiptFlow flow = receiptFlowMapper.selectById(flowId);
+ private void doPushClaimToHcs(Long claimId) {
+ ReceiptClaim claim = claimMapper.selectById(claimId);
+ if (claim == null || !CLAIMED.equals(claim.getClaimStatus())) {
+ log.info("HCS收款认领链推送跳过:认领主单不存在或已作废 claimId={}", claimId);
+ return;
+ }
+ KingdeeReceiptFlow flow = receiptFlowMapper.selectById(claim.getReceiptFlowId());
if (flow == null) {
+ log.warn("HCS收款认领链推送跳过:收款流水不存在 claimId={},flowId={}", claimId, claim.getReceiptFlowId());
return;
}
- // 未作废认领主单(作废认领不同步合规)
- List claims = claimMapper.selectList(Wrappers.lambdaQuery()
- .eq(ReceiptClaim::getReceiptFlowId, flowId)
- .eq(ReceiptClaim::getClaimStatus, CLAIMED));
- if (claims.isEmpty()) {
- return;
- }
- // 有效分摊行(作废时已置 status=0)
+ // 本认领主单有效分摊行(作废时已置 status=0)
List allocations = claimSettlementMapper.selectList(
Wrappers.lambdaQuery()
- .eq(ReceiptClaimSettlement::getReceiptFlowId, flowId)
+ .eq(ReceiptClaimSettlement::getReceiptClaimId, claimId)
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0));
if (allocations.isEmpty()) {
+ log.info("HCS收款认领链推送跳过:认领主单无有效分摊 claimId={}", claimId);
return;
}
// 结算单批量定位合同(一次 IN 查询,避免逐行 N+1)
@@ -147,20 +177,18 @@ public class HcsClaimSyncSupport {
allocations.stream().map(ReceiptClaimSettlement::getFormalSettlementId)
.filter(Objects::nonNull).distinct().toList()).stream()
.collect(Collectors.toMap(FormalSettlement::getId, item -> item, (first, second) -> first));
- // 合同id → 认领主单id → 分摊金额
- Map> amountByContractByClaim = new LinkedHashMap<>();
+ // 合同id → 本认领主单分摊金额
+ Map amountByContract = new LinkedHashMap<>();
for (ReceiptClaimSettlement allocation : allocations) {
FormalSettlement settlement = settlements.get(allocation.getFormalSettlementId());
if (settlement == null || settlement.getContractId() == null
- || allocation.getReceiptClaimId() == null || allocation.getAllocatedReceiptAmount() == null) {
+ || allocation.getAllocatedReceiptAmount() == null) {
continue;
}
- amountByContractByClaim
- .computeIfAbsent(settlement.getContractId(), key -> new LinkedHashMap<>())
- .merge(allocation.getReceiptClaimId(), allocation.getAllocatedReceiptAmount(), BigDecimal::add);
+ amountByContract.merge(settlement.getContractId(), allocation.getAllocatedReceiptAmount(), BigDecimal::add);
}
- if (amountByContractByClaim.isEmpty()) {
- log.info("HCS收款认领链推送跳过:分摊行未关联合同 flowId={}", flowId);
+ if (amountByContract.isEmpty()) {
+ log.info("HCS收款认领链推送跳过:分摊行未关联合同 claimId={},flowId={}", claimId, flow.getId());
return;
}
// 合规侧收款流水ID(C107-1 必填引用,一次定位全部合同共用;定位失败仅跳过 C107-1,不阻塞履约计划更新)
@@ -168,47 +196,47 @@ public class HcsClaimSyncSupport {
try {
hcsCollectionFlowId = locateHcsCollectionFlowId(flow);
} catch (Exception exception) {
- log.error("HCS收款流水定位失败 flowId:{},跳过C107-1认领记录推送", flowId, exception);
+ log.error("HCS收款流水定位失败 flowId:{},跳过C107-1认领记录推送", flow.getId(), exception);
}
- for (Map.Entry> contractEntry : amountByContractByClaim.entrySet()) {
+ for (Map.Entry contractEntry : amountByContract.entrySet()) {
try {
- pushOneContract(flow, claims, contractEntry.getKey(), contractEntry.getValue(), hcsCollectionFlowId);
+ pushOneContract(flow, claim, contractEntry.getKey(), contractEntry.getValue(), hcsCollectionFlowId);
} catch (Exception exception) {
- log.error("HCS收款认领链推送失败 flowId:{},contractId:{}", flowId, contractEntry.getKey(), exception);
+ log.error("HCS收款认领链推送失败 claimId:{},flowId:{},contractId:{}",
+ claimId, flow.getId(), contractEntry.getKey(), exception);
}
}
}
/**
- * 推送单个合同:① C103-9 更新履约计划(认领金额累加重推)→ ② C107-1 认领记录(按认领主单分组)→ ③ C108-1 履约流水
+ * 推送单个合同:① C103-9 更新履约计划(认领金额累加重推)→ ② C107-1 认领记录(结果落库)→ ③ C108-1 履约流水
*/
- private void pushOneContract(KingdeeReceiptFlow flow, List claims, Long contractId,
- Map amountByClaim, String hcsCollectionFlowId) {
+ private void pushOneContract(KingdeeReceiptFlow flow, ReceiptClaim claim, Long contractId,
+ BigDecimal claimAmount, String hcsCollectionFlowId) {
ContractManage contractManage = contractManageMapper.selectById(contractId);
if (contractManage == null) {
log.warn("HCS收款认领链推送跳过:合同不存在 contractId={}", contractId);
return;
}
- BigDecimal claimedTotal = amountByClaim.values().stream().reduce(BigDecimal.ZERO, BigDecimal::add);
// ① C103-9 更新合同履约计划:认领金额累加到收款计划行;已同步行置 update 凭阶段ID 重推
- contractExeStageService.accumulateClaimedAmountOnPush(contractId, claimedTotal);
+ contractExeStageService.accumulateClaimedAmountOnPush(contractId, claimAmount);
HcsContractExeStage stageRow = contractExeStageService.findCollectionStageRow(contractId, HcsSyncConstant.FUND_NATURE_PROGRESS);
if (StringUtil.isNotBlank(contractManage.getHcsFdId())) {
// 行级重推(无待同步行返回提示,不视为失败);合同未推送合规时随合同链路首推
hcsExeStageSyncSupport.syncExeStages(contractManage);
}
- // ② C107-1 认领记录:按「认领主单 × 合同」推送,金额为该认领主单分摊到本合同的金额
+ // ② C107-1 认领记录:本认领主单分摊到本合同的金额,返回 fdId 落库供 C107-3 作废引用
if (StringUtil.isBlank(hcsCollectionFlowId)) {
- log.warn("HCS认领记录推送跳过:未定位到合规收款流水 flowId={},detailSerialNo={}", flow.getId(), flow.getDetailSerialNo());
+ log.warn("HCS认领记录推送跳过:未定位到合规收款流水 claimId={},flowId={},detailSerialNo={}",
+ claim.getId(), flow.getId(), flow.getDetailSerialNo());
+ saveClaimHcsRecord(claim, flow, contractManage, claimAmount, null, PUSH_FAILED, "C104-2未定位到合规收款流水");
} else {
- Map claimsById = claims.stream()
- .collect(Collectors.toMap(ReceiptClaim::getId, item -> item, (first, second) -> first));
- for (Map.Entry claimEntry : amountByClaim.entrySet()) {
- pushClaimRecord(flow, claimsById.get(claimEntry.getKey()), contractManage, stageRow, claimEntry.getValue(), hcsCollectionFlowId);
- }
+ String claimRecordFdId = pushClaimRecord(flow, claim, contractManage, stageRow, claimAmount, hcsCollectionFlowId);
+ saveClaimHcsRecord(claim, flow, contractManage, claimAmount, claimRecordFdId,
+ StringUtil.isBlank(claimRecordFdId) ? PUSH_FAILED : PUSH_SUCCESS, null);
}
// ③ C108-1 履约流水:按合同一条(开关 perf-record.sync-enabled,与付款链履约记录同步同开关),
- // 须已推送合规且存在已同步收款计划行(fd_plan_id 必填);履约日期为收款流水交易日期
+ // 须已推送合规且存在已同步收款计划行(fd_plan_id 必填)
if (!hcsProperties.getPerfRecord().isSyncEnabled()) {
log.info("HCS履约流水推送跳过:履约记录同步开关未开启 contractNo={}", contractManage.getContractNo());
return;
@@ -221,16 +249,18 @@ public class HcsClaimSyncSupport {
log.info("HCS履约流水推送跳过:无已同步收款履约计划行 contractNo={}", contractManage.getContractNo());
return;
}
- pushPerfRecord(flow, contractManage, stageRow, claimedTotal);
+ pushPerfRecord(flow, contractManage, stageRow, claimAmount, claim.getClaimDate());
}
/**
* C107-1 认领记录推送(单认领主单 × 单合同)
+ *
+ * @return 合规侧认领记录 fdId(响应未返回返回 null)
*/
- private void pushClaimRecord(KingdeeReceiptFlow flow, ReceiptClaim claim, ContractManage contractManage,
- HcsContractExeStage stageRow, BigDecimal claimAmount, String hcsCollectionFlowId) {
- if (claim == null || claimAmount == null || claimAmount.signum() <= 0) {
- return;
+ private String pushClaimRecord(KingdeeReceiptFlow flow, ReceiptClaim claim, ContractManage contractManage,
+ HcsContractExeStage stageRow, BigDecimal claimAmount, String hcsCollectionFlowId) {
+ if (claimAmount == null || claimAmount.signum() <= 0) {
+ return null;
}
HcsClaimRecordAddDTO param = new HcsClaimRecordAddDTO();
// 【必填】收款流水引用(合规侧流水ID,C104-2 按明细流水号定位)
@@ -259,7 +289,7 @@ public class HcsClaimSyncSupport {
param.setFdClaimDate(toMillis(claim.getClaimDate()));
param.setFdRemark(claim.getRemark());
// 状态 0=已认领(仅推送未作废认领)
- param.setFdStatus(0);
+ param.setFdStatus(CLAIM_STATUS_CLAIMED);
log.info("HCS认领记录推送开始 flowId:{},claimId:{},contractNo:{},认领金额 {}",
flow.getId(), claim.getId(), contractManage.getContractNo(), claimAmount);
HcsResult result = hcsClient.claimRecordAdd(param);
@@ -267,25 +297,137 @@ public class HcsClaimSyncSupport {
log.error("HCS认领记录推送失败响应:{},claimId={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), claim.getId());
throw new HcsException("C107-1 认领记录推送失败:" + (result == null ? "响应为空" : result.getMsg()));
}
- // shortcut: C107-1 返回 fdId 未持久化(认领作废同步 C107-3 未实现,重推会重复);需作废联动时先落库
+ String claimRecordFdId = result.getData() == null ? null : result.getData().getFdId();
log.info("HCS认领记录推送成功 flowId:{},claimId:{},contractNo:{},claimRecordFdId:{}",
flow.getId(), claim.getId(), contractManage.getContractNo(),
- result.getData() == null ? "响应未返回" : result.getData().getFdId());
+ StringUtil.isBlank(claimRecordFdId) ? "响应未返回" : claimRecordFdId);
+ return claimRecordFdId;
}
/**
- * C108-1 履约流水推送(单合同一条:申请金额=实际金额=该合同累计认领金额,履约日期=收款流水交易日期)
+ * C107-1 推送结果落库(认领主单 × 合同一条):成功持久化合规 fdId,失败落标记与原因
+ */
+ private void saveClaimHcsRecord(ReceiptClaim claim, KingdeeReceiptFlow flow, ContractManage contractManage,
+ BigDecimal claimAmount, String claimRecordFdId, String pushStatus, String failReason) {
+ ReceiptClaimHcsRecord record = new ReceiptClaimHcsRecord();
+ record.setReceiptClaimId(claim.getId());
+ record.setReceiptFlowId(flow.getId());
+ record.setContractId(contractManage.getId());
+ record.setContractNo(contractManage.getContractNo());
+ record.setClaimAmount(claimAmount);
+ record.setHcsClaimRecordFdId(claimRecordFdId);
+ record.setPushStatus(pushStatus);
+ record.setFailReason(failReason);
+ record.setPushedTime(PUSH_SUCCESS.equals(pushStatus) ? LocalDateTime.now() : null);
+ claimHcsRecordMapper.insert(record);
+ }
+
+ /**
+ * 认领作废事务提交后同步合规 C107-3(修改认领记录 fdStatus=1 已作废)
+ *
+ * 仅同步本认领主单推送成功的合规记录;单条失败独立记日志,不影响其余合同。
+ * 作废失败时本地记录保持 success(合规侧仍为已认领),需人工在合规侧处置。
+ *
+ * @param claimId 认领主单ID
+ */
+ public void voidClaimToHcs(Long claimId) {
+ if (!hcsProperties.getExeStage().isSyncEnabled()) {
+ return;
+ }
+ try {
+ List records = claimHcsRecordMapper.selectList(
+ Wrappers.lambdaQuery()
+ .eq(ReceiptClaimHcsRecord::getReceiptClaimId, claimId)
+ .eq(ReceiptClaimHcsRecord::getPushStatus, PUSH_SUCCESS));
+ if (records.isEmpty()) {
+ log.info("HCS认领作废同步跳过:无推送成功的合规认领记录 claimId={}", claimId);
+ return;
+ }
+ for (ReceiptClaimHcsRecord record : records) {
+ try {
+ voidOneClaimRecord(record);
+ } catch (Exception exception) {
+ log.error("HCS认领作废同步失败 claimId:{},recordId:{},hcsFdId:{}",
+ claimId, record.getId(), record.getHcsClaimRecordFdId(), exception);
+ }
+ }
+ } catch (Exception exception) {
+ log.error("HCS认领作废同步异常 claimId:{}", claimId, exception);
+ }
+ }
+
+ /**
+ * C107-3 单条作废:fdId 必填,fdStatus=1 已作废;成功后本地记录置 voided
+ */
+ private void voidOneClaimRecord(ReceiptClaimHcsRecord record) {
+ HcsClaimRecordAddDTO param = new HcsClaimRecordAddDTO();
+ // 【必填】合规侧认领记录ID(C107-1 推送成功时落库)
+ param.setFdId(record.getHcsClaimRecordFdId());
+ // 状态 1=已作废
+ param.setFdStatus(CLAIM_STATUS_VOIDED);
+ log.info("HCS认领作废同步开始 claimId:{},contractNo:{},hcsFdId:{}",
+ record.getReceiptClaimId(), record.getContractNo(), record.getHcsClaimRecordFdId());
+ HcsResult result = hcsClient.claimRecordUpdate(param);
+ if (result == null || !result.isSuccess()) {
+ throw new HcsException("C107-3 认领作废同步失败:" + (result == null ? "响应为空" : result.getMsg()));
+ }
+ // 本地推送记录置已作废
+ claimHcsRecordMapper.update(null, Wrappers.lambdaUpdate()
+ .eq(ReceiptClaimHcsRecord::getId, record.getId())
+ .set(ReceiptClaimHcsRecord::getPushStatus, PUSH_VOIDED));
+ log.info("HCS认领作废同步成功 claimId:{},contractNo:{},hcsFdId:{}",
+ record.getReceiptClaimId(), record.getContractNo(), record.getHcsClaimRecordFdId());
+ }
+
+ /**
+ * C108-1 履约流水推送(单合同一条:申请金额=实际金额=本次认领分摊金额)
*/
private void pushPerfRecord(KingdeeReceiptFlow flow, ContractManage contractManage,
- 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.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);
+ HcsContractExeStage stageRow, BigDecimal claimAmount, 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(claimAmount.toPlainString());
+ props.setFdFactAmount(claimAmount.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-1 同步落库)
+ 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(), claimAmount);
+ 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());
}
/**
From 0163f10d1c4e3a0af59b370d8c5ffc8f9083a717 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:19:43 +0800
Subject: [PATCH 041/106] =?UTF-8?q?feat:=20=E6=94=B6=E6=AC=BE=E6=B5=81?=
=?UTF-8?q?=E6=B0=B4=E5=90=8C=E6=AD=A5=E6=95=B0=E6=8D=AE=E6=BA=90A301-2?=
=?UTF-8?q?=E6=94=B9=E4=B8=BAA301-1=E5=B9=B6=E4=B8=8B=E7=BA=BF=E9=87=91?=
=?UTF-8?q?=E8=9D=B6=E6=8E=A8=E9=80=81=E9=93=BE=E8=B7=AF?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/KingdeeReceiptFlow.java | 44 +-
.../transport/config/BfiProperties.java | 39 +-
.../controller/ReceiptFlowController.java | 11 +-
.../service/IReceiptFlowService.java | 19 +-
.../service/impl/ReceiptFlowServiceImpl.java | 603 +++++-------------
.../transport/task/ReceiptClaimPushTask.java | 67 --
.../bfi/pojo/dto/BfiRecBillQueryDTO.java | 7 +-
.../thirdparty/bfi/service/IBfiService.java | 2 +-
8 files changed, 186 insertions(+), 606 deletions(-)
delete mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/task/ReceiptClaimPushTask.java
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java
index 533394ae..16a09b7b 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java
@@ -52,68 +52,68 @@ public class KingdeeReceiptFlow extends TenantEntity {
private static final long serialVersionUID = 1L;
/**
- * 金蝶认领公告id(A301-2 返回 id,认领推送 A301-3 必填)
+ * 金蝶认领公告id(历史 A301-2 同步遗留,A301-1 口径不再使用,恒为空)
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long billId;
/**
- * 金蝶付款人id(A301-2 公告对应往来单位 id,认领推送 A301-3 用)
+ * 金蝶付款人id(历史 A301-2 同步遗留,A301-1 口径不再使用,恒为空)
*/
private String payerId;
/**
- * 公告认领状态(A301-2 claimstatus:0未认领/1已认领)
+ * 公告认领状态(历史 A301-2 同步遗留,A301-1 口径不再使用,恒为空)
*/
private String kingdeeClaimStatus;
/**
- * 我已认领(A301-2 claimme)
+ * 我已认领(历史 A301-2 同步遗留,A301-1 口径不再使用,恒为空)
*/
private Boolean claimMe;
/**
- * 未认领金额(A301-2 unclaimamount)
+ * 未匹配收款金额(A301-1 unmatchamountrec,认领上限口径)
*/
private BigDecimal unclaimedAmount;
/**
- * 手续费(A301-2 fee)
+ * 手续费(A301-1 fee)
*/
private BigDecimal fee;
/**
- * 收款人(A301-2 recpayee)
+ * 收款人(A301-1 收款公司名称 org_name)
*/
private String payeeName;
/**
- * 收款通知单号(A301-2 billno)
+ * 金蝶收款单号(A301-1 billno)
*/
private String receiptNoticeNo;
/**
- * 付款人名称(A301-2 recviewpayer)
+ * 付款人名称(A301-1 payername)
*/
private String payerName;
/**
- * 收款金额(A301-2 reamount)
+ * 收款金额(A301-1 流水分录 bei_creditamount,兜底主单 actrecamt)
*/
private BigDecimal receiptAmount;
/**
- * 对方单位名称(A301-2 oppunit)
+ * 对方单位名称(A301-1 付款人名称)
*/
private String counterpartyName;
/**
- * 对方银行账号(A301-2 oppbanknumber)
+ * 对方银行账号(A301-1 付款账户 payeracctbanknum)
*/
private String counterpartyAccount;
/**
- * 对方开户行(A301-2 oppbank)
+ * 对方开户行(A301-1 付款银行 payerbankname)
*/
private String counterpartyBank;
/**
- * 摘要(A301-2 description)
+ * 摘要(A301-1 txt_description)
*/
private String summary;
/**
- * 交易日期(A301-2 tradetime)
+ * 交易日期(A301-1 业务日期 bizdate)
*/
private LocalDateTime transactionTime;
/**
- * 明细流水号(A301-2 detailid,回退 tradedetailno / bankdetailno,幂等键)
+ * 明细流水号(A301-1 流水分录 bei_detailid,兜底 bei_bankdetailno,幂等键)
*/
private String detailSerialNo;
/**
@@ -125,27 +125,27 @@ public class KingdeeReceiptFlow extends TenantEntity {
*/
private String claimStatus;
/**
- * 推送状态(none未推送/pushed已推送/failed推送失败;已推送为终态闸门,不允许二次推送)
+ * 推送状态(none未推送/pushed已推送/failed推送失败;历史 A301-3 推送遗留,A301-1 口径下恒为 none,仅存量的已推送数据保留展示)
*/
private String pushStatus;
/**
- * 最近一次推送失败原因(成功后清空)
+ * 最近一次推送失败原因(历史 A301-3 推送遗留)
*/
private String pushFailReason;
/**
- * 首次推送金蝶成功时间
+ * 首次推送金蝶成功时间(历史 A301-3 推送遗留)
*/
private LocalDateTime pushedTime;
/**
- * 金蝶推送来源单据编码(分摊结算单号下划线拼接,推送时生成落库;分摊不变则重试编码不变,A301-1 回写匹配键)
+ * 金蝶推送来源单据编码(历史 A301-3 推送遗留,A301-1 口径不再生成)
*/
private String awrpSrcCode;
/**
- * 金蝶收款单号(推送响应回填 / A301-1 回写推进)
+ * 金蝶收款单号(A301-1 同步落库 billno)
*/
private String kingdeeBillNo;
/**
- * 金蝶回写状态(none未推送/pushed已推送/approved审核通过/received已收款/failed失败)
+ * 金蝶单据状态(A301-1 billstatus 映射:none未推送/pushed已推送/approved审核通过/received已收款/failed失败,同步时随单据刷新)
*/
private String kingdeeBillStatus;
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
index 5fe089fe..0b579ecb 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
@@ -71,9 +71,9 @@ public class BfiProperties {
private ReceivedInvoice receivedInvoice = new ReceivedInvoice();
/**
- * 收款认领公告查询配置(A301-2)
+ * 收款流水同步配置(A301-1 收款单查询)
*/
- private ClaimAnnounce claimAnnounce = new ClaimAnnounce();
+ private RecBillSync recBillSync = new RecBillSync();
/**
* 付款结果定时回写配置(A103-2/A103-4,总开关同时控制应收勾稽、开票结果回写)
@@ -85,11 +85,6 @@ public class BfiProperties {
*/
private ReceivedInvoiceResult receivedInvoiceResult = new ReceivedInvoiceResult();
- /**
- * 收款认领推送配置(A301-3)
- */
- private ReceiptClaimPush receiptClaimPush = new ReceiptClaimPush();
-
/**
* 全票池发票定时同步配置(A801-1)
*/
@@ -197,20 +192,15 @@ public class BfiProperties {
}
/**
- * 收款认领公告查询配置项(A301-2)
+ * 收款流水同步配置项(A301-1 收款单查询)
*/
@Data
- public static class ClaimAnnounce {
+ public static class RecBillSync {
/**
- * 查询时间窗口(月数),默认 6(近半年)
+ * 同步业务日期窗口天数,默认 30(A301-1 契约约束业务日期范围不可超过 1 个月)
*/
- private int months = 6;
-
- /**
- * 认领人(claimusers):配置后公告查询固定使用该值,不再按当前登录用户姓名查询
- */
- private String claimusers;
+ private int days = 30;
}
/**
@@ -271,23 +261,6 @@ public class BfiProperties {
private boolean syncEnabled = false;
}
- /**
- * 收款认领推送配置项(A301-3)
- */
- @Data
- public static class ReceiptClaimPush {
-
- /**
- * 推送任务开关,默认关闭(联调稳定后开启)
- */
- private boolean syncEnabled = false;
-
- /**
- * 推送 single-flight 锁租约秒数,须大于最坏一轮耗时(逐认领人远程查公告 + 逐条推送)
- */
- private long lockLeaseSeconds = 1800;
- }
-
/**
* 全票池发票同步配置项(A801-1)
*/
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java
index 1bbee361..b1fc3b21 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptFlowController.java
@@ -35,7 +35,6 @@ 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.pojo.dto.ReceiptClaimPushResult;
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
import org.springblade.transport.service.IReceiptFlowService;
@@ -95,16 +94,8 @@ public class ReceiptFlowController extends BladeController {
@PostMapping("/sync")
@ApiOperationSupport(order = 5)
- @Operation(summary = "手动同步金蝶收款流水(A301-2 收款认领公告查询)")
+ @Operation(summary = "手动同步金蝶收款流水(A301-1 收款单查询)")
public R sync() {
return R.data(receiptFlowService.sync());
}
-
- @PostMapping("/push-kingdee")
- @ApiOperationSupport(order = 6)
- @Operation(summary = "推送金蝶收款认领(A301-3 收款认领结果推送,仅认领完成流水可推送)")
- public R pushKingdee(@RequestParam Long flowId) {
- ReceiptClaimPushResult result = receiptFlowService.pushClaimToKingdee(flowId);
- return result.isSuccess() ? R.data(result) : R.fail(result.getMessage());
- }
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java
index 89586f79..4985b5ba 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptFlowService.java
@@ -27,7 +27,6 @@ package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
-import org.springblade.transport.pojo.dto.ReceiptClaimPushResult;
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
@@ -53,25 +52,9 @@ public interface IReceiptFlowService extends BaseService {
Long claim(ReceiptClaimRequest request);
/**
- * 手动同步金蝶收款流水:直连 A301-2 收款认领公告查询,按明细流水号幂等 upsert 收款流水表
+ * 手动同步金蝶收款流水:直连 A301-1 收款单查询(收款处理查询),按流水分录明细流水号幂等 upsert 收款流水表
*
* @return 本次同步条数
*/
int sync();
-
- /**
- * 手动推送金蝶(A301-3 收款认领结果推送):认领完成且未推送过的流水才可推送,
- * 推送前以当前用户姓名重查 A301-2 最新公告对账。与定时任务共用同一推送实现。
- *
- * @param flowId 收款流水id
- * @return 推送结果(失败不抛异常,原因随结果返回)
- */
- ReceiptClaimPushResult pushClaimToKingdee(Long flowId);
-
- /**
- * 定时推送金蝶:扫描认领完成且未推送过的流水,按认领人姓名分组重查 A301-2 最新公告后逐条推送。
- *
- * @return 执行摘要
- */
- String pushClaimByTask();
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
index 899315a4..b47b4fe3 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
@@ -38,12 +38,9 @@ import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.Dept;
-import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimAnnounceQueryDTO;
-import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimPushDTO;
-import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimPushEntryDTO;
-import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO;
-import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO;
-import org.springblade.thirdparty.bfi.pojo.vo.BfiClaimAnnounceVO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillFlowVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.thirdparty.bfi.util.BfiUtil;
import org.springblade.transport.config.BfiProperties;
@@ -53,7 +50,6 @@ import org.springblade.transport.mapper.KingdeeReceiptFlowMapper;
import org.springblade.transport.mapper.ReceiptClaimMapper;
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
import org.springblade.transport.mapper.ReceiptFlowRecordMapper;
-import org.springblade.transport.pojo.dto.ReceiptClaimPushResult;
import org.springblade.transport.pojo.dto.ReceiptClaimRequest;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
@@ -63,13 +59,13 @@ import org.springblade.transport.pojo.entity.ReceiptFlowRecord;
import org.springblade.transport.pojo.vo.ReceiptFlowVO;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IReceiptFlowService;
-import org.springblade.transport.support.KingdeeSyncLockSupport;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.hcs.HcsClaimSyncSupport;
import org.springblade.transport.wrapper.ReceiptFlowWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
-import org.springframework.transaction.support.TransactionTemplate;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -77,9 +73,8 @@ import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.HashSet;
import java.util.LinkedHashMap;
+import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -109,18 +104,14 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl allocationMap = allocationMap(request.getSettlements());
// 结算单id列表(升序)
@@ -347,9 +333,9 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl announces = queryClaimAnnounces(claimusers);
+ List recBills = queryRecBills();
- if (announces.isEmpty()) {
+ if (recBills.isEmpty()) {
record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
- "金蝶认领公告未查询到数据");
+ "金蝶收款单未查询到数据");
return 0;
}
- // 同步口径:A301-2 按参与认领用户(当前登录用户真实姓名)拉取全量认领公告,
+ // 同步口径:A301-1 按业务日期窗口拉取金蝶收款单(收款处理),按流水分录展开,
// 以金蝶明细流水号为幂等键 upsert 收款流水表;本地已认领金额/状态(claimedAmount/claimStatus)
- // 由认领、作废驱动,不被公告顺带覆盖,公告侧状态单独存 kingdee_claim_status/unclaimed_amount。
+ // 由认领、作废驱动,不被同步顺带覆盖,未匹配收款金额单独存 unclaimed_amount 作为认领上限。
- // 第一遍:数据校验与过滤,收集有效的 detailSerialNo
- log.info("收款流水同步开始数据校验,共 {} 条认领公告", announces.size());
- Map validAnnounces = new LinkedHashMap<>();
+ // 第一遍:数据校验与过滤,主单+流水分录展开为流水行,收集有效的 detailSerialNo
+ log.info("收款流水同步开始数据校验,共 {} 张收款单", recBills.size());
+ Map validRows = new LinkedHashMap<>();
int skippedCount = 0;
- for (BfiClaimAnnounceVO announce : announces) {
- // 明细流水号优先级:detailid > tradedetailno > bankdetailno
- String detailSerialNo = firstNonBlank(announce.getDetailid(),
- announce.getTradedetailno(), announce.getBankdetailno());
- if (Func.isEmpty(detailSerialNo)) {
- log.warn("收款流水同步跳过:认领公告[{}]缺少明细流水号", announce.getBillno());
+ for (BfiRecBillVO recBill : recBills) {
+ List flowEntries = recBill.getInfoentry();
+ if (Func.isEmpty(flowEntries)) {
+ log.warn("收款流水同步跳过:收款单[{}]无流水分录", recBill.getBillno());
skippedCount++;
continue;
}
- detailSerialNo = detailSerialNo.trim();
- if (validAnnounces.containsKey(detailSerialNo)) {
- throw new ServiceException("明细流水号" + detailSerialNo + "重复");
+ for (BfiRecBillFlowVO flowEntry : flowEntries) {
+ // 明细流水号优先级:bei_detailid > bei_bankdetailno
+ String detailSerialNo = firstNonBlank(flowEntry.getBeiDetailid(), flowEntry.getBeiBankdetailno());
+ if (Func.isEmpty(detailSerialNo)) {
+ log.warn("收款流水同步跳过:收款单[{}]流水分录缺少明细流水号", recBill.getBillno());
+ skippedCount++;
+ continue;
+ }
+ detailSerialNo = detailSerialNo.trim();
+ if (validRows.containsKey(detailSerialNo)) {
+ throw new ServiceException("明细流水号" + detailSerialNo + "重复");
+ }
+ BigDecimal receiptAmount = parseEntryAmount(flowEntry.getBeiCreditamount(), recBill.getActrecamt());
+ if (receiptAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ log.warn("收款流水同步跳过:收款单[{}]流水分录收款金额为空或小于0", recBill.getBillno());
+ skippedCount++;
+ continue;
+ }
+ validRows.put(detailSerialNo, new BfiRecBillRow(recBill, flowEntry, detailSerialNo, receiptAmount));
}
- BigDecimal receiptAmount = money(announce.getReamount());
- if (receiptAmount.compareTo(BigDecimal.ZERO) <= 0) {
- log.warn("收款流水同步跳过:认领公告[{}]收款金额为空或小于0", announce.getBillno());
- skippedCount++;
- continue;
- }
- validAnnounces.put(detailSerialNo, announce);
}
- log.info("收款流水数据校验完成:有效 {} 条,跳过 {} 条", validAnnounces.size(), skippedCount);
+ log.info("收款流水数据校验完成:有效 {} 条,跳过 {} 条", validRows.size(), skippedCount);
- if (validAnnounces.isEmpty()) {
+ if (validRows.isEmpty()) {
log.info("收款流水同步无有效数据,记录操作日志后返回");
record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
- "金蝶认领公告无有效数据");
+ "金蝶收款单无有效数据");
return 0;
}
// 批量查询已存在的记录(FOR UPDATE 防并发)
- log.info("收款流水同步开始批量查询已存在记录,查询条件:detailSerialNo IN {} 条", validAnnounces.size());
+ log.info("收款流水同步开始批量查询已存在记录,查询条件:detailSerialNo IN {} 条", validRows.size());
List existingEntities = baseMapper.selectList(
Wrappers.lambdaQuery()
- .in(KingdeeReceiptFlow::getDetailSerialNo, validAnnounces.keySet())
+ .in(KingdeeReceiptFlow::getDetailSerialNo, validRows.keySet())
.last("FOR UPDATE")
);
Map existingMap = existingEntities.stream()
.collect(Collectors.toMap(KingdeeReceiptFlow::getDetailSerialNo, e -> e));
log.info("收款流水批量查询完成:已存在 {} 条,待新增 {} 条",
- existingEntities.size(), validAnnounces.size() - existingEntities.size());
+ existingEntities.size(), validRows.size() - existingEntities.size());
// 第二遍:构建新增和更新列表
log.info("收款流水同步开始构建新增/更新列表");
@@ -512,10 +508,9 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl entry : validAnnounces.entrySet()) {
+ for (Map.Entry entry : validRows.entrySet()) {
String detailSerialNo = entry.getKey();
- BfiClaimAnnounceVO announce = entry.getValue();
- BigDecimal receiptAmount = money(announce.getReamount());
+ BfiRecBillRow row = entry.getValue();
// 判断是新增还是更新
KingdeeReceiptFlow existing = existingMap.get(detailSerialNo);
@@ -532,8 +527,8 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl candidateRow(FormalSettlement settlement, BigDecimal claimedAmount) {
BigDecimal settlementAmount = money(settlement.getSettlementAmount());
Map row = new LinkedHashMap<>();
@@ -724,8 +726,8 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl
- * 仅认领完成且未推送过的流水可推送;推送前以当前用户姓名重查 A301-2 最新公告对账。
- * 推送失败不抛异常跨事务边界(保证失败状态落库),原因随结果对象返回。
+ * A301-1 收款单查询:业务日期窗口全量翻页,窗口天数取 Nacos 配置(默认近30天,契约约束 ≤1 个月)。
+ * 收款流水同步专用。
*/
- @Override
- public ReceiptClaimPushResult pushClaimToKingdee(Long flowId) {
- if (flowId == null) {
- throw new ServiceException("收款流水ID不能为空");
- }
- KingdeeReceiptFlow flow = existing(flowId);
- if (flow.getBillId() == null) {
- return ReceiptClaimPushResult.fail("认领公告id缺失,请先手动同步金蝶收款流水");
- }
- String claimusers = UserCache.getUserRealName(AuthUtil.getUserId());
- if (Func.isEmpty(claimusers)) {
- throw new ServiceException("当前用户无姓名,无法推送金蝶收款认领");
- }
- // 推送前对账:以当前用户身份重查 A301-2 拿最新公告(手动推送者须在公告认领范围内)
- BfiClaimAnnounceVO announce = queryClaimAnnounces(claimusers).stream()
- .filter(item -> flow.getBillId().equals(item.getId()))
- .findFirst().orElse(null);
- if (announce == null) {
- return ReceiptClaimPushResult.fail("金蝶侧未查询到该认领公告,当前用户可能不在公告认领范围内,请重新同步后重试");
- }
- return pushClaimLocked(flow.getId(), announce);
- }
-
- /**
- * 定时推送金蝶:扫描认领完成且未推送过的流水,按认领人姓名分组重查 A301-2 最新公告后逐条推送。
- * 多副本部署下由 single-flight 分布式锁保证同一轮全局最多一份在跑,已有轮次进行中则本轮跳过。
- *
- * @return 执行摘要
- */
- @Override
- public String pushClaimByTask() {
- return kingdeeSyncLockSupport.runSingleFlight(RECEIPT_CLAIM_PUSH_LOCK_PREFIX, bfiProperties.getReceiptClaimPush().getLockLeaseSeconds(),
- "定时推送收款认领", this::executeReceiptClaimPush);
- }
-
- /**
- * 定时推送收款认领主体:按认领人姓名串行处理——
- * 先重查该认领人的 A301-2 最新公告并推完其名下全部流水,再到下一个认领人。
- * A301-2 的 claimusers 必填且为单个用户姓名,定时任务无登录人上下文,
- * 故按认领人逐人查询公告,按公告id(bill_id)匹配流水。
- * 单条流水推送失败独立消化,不影响同批其他流水。
- *
- * @return 执行摘要
- */
- private String executeReceiptClaimPush() {
- List candidates = list(Wrappers.lambdaQuery()
- .isNotNull(KingdeeReceiptFlow::getBillId)
- .in(KingdeeReceiptFlow::getPushStatus, List.of(
- KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_NONE, KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_FAILED))
- .eq(KingdeeReceiptFlow::getClaimStatus, CLAIMED)
- .eq(KingdeeReceiptFlow::getStatus, 1)
- .orderByAsc(KingdeeReceiptFlow::getId));
- if (candidates.isEmpty()) {
- return "无待推送流水";
- }
- Map candidateById = candidates.stream()
- .collect(Collectors.toMap(KingdeeReceiptFlow::getId, Function.identity(),
- (first, second) -> first, LinkedHashMap::new));
- // 认领人 → 其有效认领的流水id
- Map> claimedFlowIdsByClaimer = claimMapper.selectList(
- Wrappers.lambdaQuery()
- .in(ReceiptClaim::getReceiptFlowId, candidateById.keySet())
- .eq(ReceiptClaim::getClaimStatus, CLAIMED)).stream()
- .filter(claim -> Func.isNotEmpty(claim.getClaimerName()) && claim.getReceiptFlowId() != null)
- .collect(Collectors.groupingBy(ReceiptClaim::getClaimerName,
- Collectors.mapping(ReceiptClaim::getReceiptFlowId, Collectors.toSet())));
- if (claimedFlowIdsByClaimer.isEmpty()) {
- return "待推送流水 " + candidates.size() + " 条,但无有效认领人,跳过";
- }
- int successCount = 0;
- int failedCount = 0;
- int skippedCount = 0;
- // 本轮已推送成功的流水:同一流水被多个认领人认领时,只在首个覆盖它的认领人处推送一次
- Set pushedFlowIds = new HashSet<>();
- // 按认领人逐人串行:查该认领人公告 → 推完其名下流水 → 下一个认领人
- for (String claimerName : claimedFlowIdsByClaimer.keySet().stream().sorted().toList()) {
- Set claimerFlowIds = claimedFlowIdsByClaimer.get(claimerName);
- List claimerFlows = candidateById.values().stream()
- .filter(flow -> claimerFlowIds.contains(flow.getId()))
- .filter(flow -> !pushedFlowIds.contains(flow.getId()))
- .toList();
- if (claimerFlows.isEmpty()) {
- continue;
- }
- // 该认领人的公告一次查询,按公告id归集
- Map announceByBillId = new HashMap<>();
- try {
- for (BfiClaimAnnounceVO announce : queryClaimAnnounces(claimerName)) {
- if (announce.getId() != null) {
- announceByBillId.putIfAbsent(announce.getId(), announce);
- }
- }
- } catch (Exception exception) {
- log.error("定时推送收款认领:认领人[{}]公告查询失败,其名下流水全部跳过", claimerName, exception);
- skippedCount += claimerFlows.size();
- continue;
- }
- for (KingdeeReceiptFlow flow : claimerFlows) {
- BfiClaimAnnounceVO announce = announceByBillId.get(flow.getBillId());
- if (announce == null) {
- skippedCount++;
- log.info("定时推送收款认领:认领人[{}]流水[{}]未在公告查询结果中命中,跳过",
- claimerName, flow.getId());
- continue;
- }
- try {
- ReceiptClaimPushResult result = pushClaimLocked(flow.getId(), announce);
- if (result.isSuccess()) {
- successCount++;
- pushedFlowIds.add(flow.getId());
- } else {
- failedCount++;
- }
- } catch (Exception exception) {
- failedCount++;
- log.error("定时推送收款认领:认领人[{}]流水[{}]推送异常", claimerName, flow.getId(), exception);
- }
- }
- }
- String summary = "扫描 " + candidates.size() + " 条,推送成功 " + successCount
- + " 条,推送失败 " + failedCount + " 条,公告未命中跳过 " + skippedCount + " 条";
- log.info("定时推送收款认领完成:{}", summary);
- return summary;
- }
-
- /**
- * 事务内推送:流水行锁覆盖闸门校验、对账、推送与状态落库全过程,
- * 防定时任务与手动按钮并发对同一流水双推;
- * 推送成功后触发合同合规认领链推送(C103-9/C107-1/C108-1,事务提交后执行,失败不影响金蝶推送结果)
- */
- private ReceiptClaimPushResult pushClaimLocked(Long flowId, BfiClaimAnnounceVO announce) {
- ReceiptClaimPushResult result = transactionTemplate.execute(status -> doPushClaimToKingdee(flowId, announce));
- if (result != null && result.isSuccess()) {
- hcsClaimSyncSupport.pushClaimToHcs(flowId);
- }
- return result;
- }
-
- /**
- * 推送单条流水:静态闸门 → 最新公告对账 → 按流水归集分摊行推送 → 成败状态落库
- */
- private ReceiptClaimPushResult doPushClaimToKingdee(Long flowId, BfiClaimAnnounceVO announce) {
- KingdeeReceiptFlow flow = lockedFlow(flowId);
- if (flow.getBillId() == null) {
- return ReceiptClaimPushResult.fail("认领公告id缺失,请先手动同步金蝶收款流水");
- }
- // 终态闸门:已推送流水不允许二次推送(作废重认领使等式再满足也不推,金蝶侧人工处置)
- if (KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_PUSHED.equals(flow.getPushStatus())) {
- return ReceiptClaimPushResult.fail("该流水已推送金蝶,不允许重复推送");
- }
- // 推送前对账:以最新公告刷新公告侧未认领金额并重算认领状态(快照落库,页面口径同步修正)
- flow.setUnclaimedAmount(money(announce.getUnclaimamount()));
- flow.setKingdeeClaimStatus(trimToNull(announce.getClaimstatus()));
- flow.setClaimMe(announce.getClaimme());
- BigDecimal claimedTotal = flowClaimedAmount(flow.getId());
- flow.setClaimedAmount(claimedTotal);
- BigDecimal claimCeiling = claimCeiling(flow);
- String claimStatus = amountClaimStatus(claimedTotal, claimCeiling);
- flow.setClaimStatus(claimStatus);
- updateById(flow);
- if (!CLAIMED.equals(claimStatus)) {
- return ReceiptClaimPushResult.fail("累计认领金额(" + claimedTotal + ")与可认领上限("
- + claimCeiling + ")不一致,不满足推送条件");
- }
- // 分录取该流水全部有效分摊行(结算单id升序,跨认领主单归集)
- List allocations = claimSettlementMapper.selectList(
- Wrappers.lambdaQuery()
- .eq(ReceiptClaimSettlement::getReceiptFlowId, flow.getId())
- .eq(ReceiptClaimSettlement::getStatus, 1)
- .eq(ReceiptClaimSettlement::getIsDeleted, 0)
- .orderByAsc(ReceiptClaimSettlement::getFormalSettlementId));
- if (allocations.isEmpty()) {
- return ReceiptClaimPushResult.fail("该流水无有效认领分摊,无法推送");
- }
- // 核心单据编号取第一张结算单同步金蝶回写的星瀚内码(应收/应付单金蝶id),TMS本地id金蝶无法识别
- FormalSettlement firstSettlement = formalSettlementMapper.selectById(allocations.get(0).getFormalSettlementId());
- if (firstSettlement == null || Func.isBlank(firstSettlement.getKingdeeBillId())) {
- return ReceiptClaimPushResult.fail("结算单" + allocations.get(0).getFormalSettlementNo()
- + "未同步金蝶(缺少金蝶单据id),不满足推送条件");
- }
- // 推送来源编码:分摊结算单号下划线拼接(A301-3 契约口径;分摊不变则重试编码不变,
- // 金蝶按 awrp_src_sys + awrp_src_code 幂等防重试双单)
- String awrpSrcCode = allocations.stream()
- .map(ReceiptClaimSettlement::getFormalSettlementNo)
- .filter(Func::isNotEmpty)
- .distinct()
- .collect(Collectors.joining("_"));
- flow.setAwrpSrcCode(awrpSrcCode);
-
- BfiClaimPushDTO param = buildClaimPushDTO(flow, claimedTotal, allocations, firstSettlement.getKingdeeBillId());
- log.info("调用BFI接口 收款认领结果推送开始 flowId:{},billId:{},awrpSrcCode:{},分摊明细 {} 行,认领金额 {}",
- flow.getId(), flow.getBillId(), flow.getAwrpSrcCode(), allocations.size(), claimedTotal);
- try {
- BfiBillSaveResultVO result = bfiService.pushClaimResult(param);
- return applyClaimPushSuccess(flow, extractClaimPushBillNo(result));
- } catch (Exception exception) {
- return applyClaimPushFailure(flow, exception);
- }
- }
-
- /**
- * A301-3 推送报文组装:billid=公告id,单条分录、金额=全部未推送金额(一笔流水一张金蝶收款单),
- * 核心单据编号=第一张结算单的金蝶单据id(星瀚内码)
- */
- private BfiClaimPushDTO buildClaimPushDTO(KingdeeReceiptFlow flow, BigDecimal claimedTotal,
- List allocations, String coreBillId) {
- BfiClaimPushDTO param = new BfiClaimPushDTO();
- // 【必填】认领公告id
- param.setBillid(flow.getBillId());
- // 【必填】来源系统编码
- param.setAwrpSrcSys(bfiProperties.getSrcSys());
- // 【必填】来源系统单据编码(分摊结算单号下划线拼接)
- param.setAwrpSrcCode(flow.getAwrpSrcCode());
- // 【必填】收款类型.编码
- param.setRecpaytypeNumber(KingdeeBfiConstant.BFI_CLAIM_REC_PAY_TYPE);
- // 【必填】付款人类型
- param.setPaymenttype(KingdeeBfiConstant.BFI_CLAIM_PAYMENT_TYPE);
- // 【必填】付款人名称
- param.setRecviewpayer(flow.getPayerName());
- // 【必填】付款人id
- param.setRecpayer(flow.getPayerId());
- // 【必填】单据体分录:仅一条,应收金额=全部未推送金额(累计有效认领)
- BfiClaimPushEntryDTO entry = new BfiClaimPushEntryDTO();
- // 【必填】应收金额(全部未推送金额)
- entry.setEReceivableamt(claimedTotal);
- // 分录行序号(第一张结算单id,分摊已按结算单id升序,多张取最小)
- entry.setSeq(allocations.get(0).getFormalSettlementId());
- // 核心单据类型
- // entry.setECorebilltype(KingdeeBfiConstant.BFI_CLAIM_CORE_BILL_TYPE);
- // 核心单据编号(第一张结算单的金蝶单据id,星瀚内码)
- entry.setECorebillno(coreBillId);
- param.setEntryentity(List.of(entry));
- return param;
- }
-
- /**
- * 解析 A301-3 推送结果:全项成功返回单据号,否则抛出汇总错误文本(由失败路径落库)。
- * 成功响应的 number 为空,金蝶收款单号取回执 keys 中的 awrp_src_code(来源编码)。
- */
- private String extractClaimPushBillNo(BfiBillSaveResultVO result) {
- if (result == null || result.getResult() == null || result.getResult().isEmpty()) {
- throw new ServiceException("金蝶认领结果推送失败,返回结果为空");
- }
- List items = result.getResult();
- boolean allSuccess = items.stream().allMatch(item -> Boolean.TRUE.equals(item.getBillStatus()));
- if (!allSuccess) {
- String errorText = items.stream()
- .map(BfiBillSaveResultItemVO::getErrorText)
- .filter(text -> !text.isEmpty())
- .collect(Collectors.joining(";"));
- throw new ServiceException("金蝶认领结果推送失败"
- + (errorText.isEmpty() ? "" : ":" + errorText));
- }
- return items.stream()
- .map(this::claimPushBillNoOf)
- .filter(Func::isNotEmpty)
- .findFirst().orElse(null);
- }
-
- /**
- * 单项推送结果 → 单据号:A301-3 成功响应 number 为空,取回执键值中的 awrp_src_code
- */
- private String claimPushBillNoOf(BfiBillSaveResultItemVO item) {
- if (Func.isNotEmpty(item.getNumber())) {
- return item.getNumber();
- }
- return item.getKeys() == null ? null : item.getKeys().get("awrp_src_code");
- }
-
- /**
- * 推送成功:流水置已推送并回填金蝶收款单号,级联刷新名下未作废认领主单的冗余展示字段
- */
- private ReceiptClaimPushResult applyClaimPushSuccess(KingdeeReceiptFlow flow, String kingdeeBillNo) {
- log.info("调用BFI接口 收款认领结果推送成功 flowId:{},金蝶收款单号:{}",
- flow.getId(), Func.isEmpty(kingdeeBillNo) ? "响应未返回" : kingdeeBillNo);
- flow.setPushStatus(KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_PUSHED);
- flow.setPushedTime(LocalDateTime.now());
- flow.setKingdeeBillNo(kingdeeBillNo);
- flow.setKingdeeBillStatus(KingdeeBfiConstant.KINGDEE_STATUS_PUSHED);
- // 失败原因需显式置空(实体 update 忽略 null 字段,走 lambdaUpdate)
- this.update(Wrappers.lambdaUpdate()
- .eq(KingdeeReceiptFlow::getId, flow.getId())
- .set(KingdeeReceiptFlow::getPushStatus, flow.getPushStatus())
- .set(KingdeeReceiptFlow::getPushFailReason, null)
- .set(KingdeeReceiptFlow::getPushedTime, flow.getPushedTime())
- .set(KingdeeReceiptFlow::getAwrpSrcCode, flow.getAwrpSrcCode())
- .set(KingdeeReceiptFlow::getKingdeeBillNo, kingdeeBillNo)
- .set(KingdeeReceiptFlow::getKingdeeBillStatus, flow.getKingdeeBillStatus()));
- // 级联刷新名下未作废认领主单的冗余展示字段
- claimMapper.update(null, Wrappers.lambdaUpdate()
- .eq(ReceiptClaim::getReceiptFlowId, flow.getId())
- .eq(ReceiptClaim::getClaimStatus, CLAIMED)
- .set(ReceiptClaim::getKingdeeBillNo, kingdeeBillNo)
- .set(ReceiptClaim::getKingdeeBillStatus, KingdeeBfiConstant.KINGDEE_STATUS_PUSHED));
- record(flow.getId(), null, "push", "推送金蝶收款认领", CLAIMED, CLAIMED, flow.getClaimedAmount(),
- "金蝶收款单号:" + (Func.isEmpty(kingdeeBillNo) ? "响应未返回" : kingdeeBillNo));
- return ReceiptClaimPushResult.success(kingdeeBillNo);
- }
-
- /**
- * 推送失败:流水置推送失败并记录原因,本地认领数据保留,可重新推送(重试复用同一来源编码)
- */
- private ReceiptClaimPushResult applyClaimPushFailure(KingdeeReceiptFlow flow, Exception exception) {
- String reason = Func.isEmpty(exception.getMessage())
- ? exception.getClass().getSimpleName() : exception.getMessage();
- reason = truncateText(reason, 1000);
- log.error("调用BFI接口 收款认领结果推送失败 flowId:{},billId:{},awrpSrcCode:{}",
- flow.getId(), flow.getBillId(), flow.getAwrpSrcCode(), exception);
- this.update(Wrappers.lambdaUpdate()
- .eq(KingdeeReceiptFlow::getId, flow.getId())
- .set(KingdeeReceiptFlow::getPushStatus, KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_FAILED)
- .set(KingdeeReceiptFlow::getPushFailReason, reason)
- .set(KingdeeReceiptFlow::getAwrpSrcCode, flow.getAwrpSrcCode()));
- record(flow.getId(), null, "push", "推送金蝶收款认领", CLAIMED, CLAIMED, flow.getClaimedAmount(),
- "推送失败:" + truncateText(reason, 200));
- return ReceiptClaimPushResult.fail("金蝶认领结果推送失败:" + reason);
- }
-
- /**
- * A301-2 收款认领公告查询:按认领人姓名全量翻页,时间窗口取 Nacos 配置(默认近半年)。
- * 认领公告同步、手动推送对账与定时任务对账共用。
- */
- private List queryClaimAnnounces(String claimusers) {
- int months = bfiProperties.getClaimAnnounce().getMonths();
- // 认领人直接取 Nacos 配置值
- String effectiveClaimusers = bfiProperties.getClaimAnnounce().getClaimusers();
- LocalDateTime endDate = LocalDateTime.now();
- LocalDateTime startDate = endDate.minusMonths(months);
+ private List queryRecBills() {
+ int days = bfiProperties.getRecBillSync().getDays();
+ LocalDate endDate = LocalDate.now();
+ LocalDate startDate = endDate.minusDays(days);
+ // 金蝶 A301-1 仅接受带时分秒的日期参数,传纯日期会报 bos.filterSqlBuilder 时区时间格式错误
+ String beginBizdate = startDate.atStartOfDay().format(BFI_DATE_TIME_FORMATTER);
+ String endBizdate = endDate.atTime(23, 59, 59).format(BFI_DATE_TIME_FORMATTER);
long start = System.currentTimeMillis();
- log.info("调用BFI接口 收款认领公告查询开始 claimusers:{},时间窗口:{} ~ {}(近{}个月)",
- effectiveClaimusers, startDate.format(BFI_DATE_TIME_FORMATTER), endDate.format(BFI_DATE_TIME_FORMATTER), months);
- BfiClaimAnnounceQueryDTO queryDTO = new BfiClaimAnnounceQueryDTO();
- queryDTO.setClaimusers(effectiveClaimusers);
- queryDTO.setModifytimeS(startDate.format(BFI_DATE_TIME_FORMATTER));
- queryDTO.setModifytimeE(endDate.format(BFI_DATE_TIME_FORMATTER));
+ log.info("调用BFI接口 收款单查询开始,业务日期窗口:{} ~ {}(近{}天)", beginBizdate, endBizdate, days);
+ BfiRecBillQueryDTO queryDTO = new BfiRecBillQueryDTO();
+ queryDTO.setBeginBizdate(beginBizdate);
+ queryDTO.setEndBizdate(endBizdate);
queryDTO.setPageSize(SYNC_PAGE_SIZE);
- List announces = BfiUtil.queryAllPages(pageNo -> {
+ List recBills = BfiUtil.queryAllPages(pageNo -> {
queryDTO.setPageNo(pageNo);
- return bfiService.queryClaimAnnounce(queryDTO);
+ return bfiService.queryRecBill(queryDTO);
});
- log.info("调用BFI接口 收款认领公告查询结束 claimusers:{},耗时:{}ms,拉取 {} 条",
- effectiveClaimusers, System.currentTimeMillis() - start, announces.size());
- return announces;
+ log.info("调用BFI接口 收款单查询结束 耗时:{}ms,拉取 {} 条", System.currentTimeMillis() - start, recBills.size());
+ return recBills;
}
/**
- * A301-2 认领公告 → 收款流水表字段拷贝(仅覆盖金蝶侧来源字段,不覆盖本地认领金额与状态)
+ * A301-1 收款单 → 收款流水表字段拷贝(仅覆盖金蝶侧来源字段,不覆盖本地认领金额与状态)。
+ * 认领上限口径:未匹配收款金额 unmatchamountrec 存 unclaimed_amount。
*/
- private void copyAnnounceRow(KingdeeReceiptFlow target, BfiClaimAnnounceVO source,
- String detailSerialNo, BigDecimal receiptAmount) {
- // 金蝶认领公告id
- target.setBillId(source.getId());
- // 收款通知单号
+ private void copyRecBillRow(KingdeeReceiptFlow target, BfiRecBillRow row) {
+ BfiRecBillVO source = row.recBill();
+ // 金蝶收款单号(既是收款通知单号也是金蝶单据号)
target.setReceiptNoticeNo(trimToEmpty(source.getBillno()));
- // 付款人名称(取自对方户名)
- target.setPayerName(trimToEmpty(source.getOppunit()));
- // 付款人id(取自对方账号)
- target.setPayerId(trimToEmpty(source.getOppbanknumber()));
- // 收款人
- target.setPayeeName(trimToNull(source.getRecpayee()));
- // 收款金额
- target.setReceiptAmount(receiptAmount);
- // 对方单位名称
- target.setCounterpartyName(trimToEmpty(source.getOppunit()));
- // 对方银行账号
- target.setCounterpartyAccount(trimToEmpty(source.getOppbanknumber()));
- // 对方开户行
- target.setCounterpartyBank(trimToEmpty(source.getOppbank()));
+ target.setKingdeeBillNo(trimToNull(source.getBillno()));
+ // 金蝶回写状态随单据状态同步刷新
+ target.setKingdeeBillStatus(billStatusOf(source.getBillstatus()));
+ // 付款人名称
+ target.setPayerName(trimToEmpty(source.getPayername()));
+ // 收款人(收款公司名称)
+ target.setPayeeName(trimToNull(source.getOrgName()));
+ // 收款金额(流水分录金额)
+ target.setReceiptAmount(row.receiptAmount());
+ // 对方单位名称(取付款人名称)
+ target.setCounterpartyName(trimToEmpty(source.getPayername()));
+ // 对方银行账号(付款账户)
+ target.setCounterpartyAccount(trimToEmpty(source.getPayeracctbanknum()));
+ // 对方开户行(付款银行)
+ target.setCounterpartyBank(trimToEmpty(source.getPayerbankname()));
// 摘要
- target.setSummary(trimToNull(source.getDescription()));
- // 交易日期
- target.setTransactionTime(parseDateTime(source.getTradetime()));
+ target.setSummary(trimToNull(source.getTxtDescription()));
+ // 交易日期(业务日期)
+ target.setTransactionTime(parseDateTime(source.getBizdate()));
// 明细流水号
- target.setDetailSerialNo(detailSerialNo);
- // 未认领金额
- target.setUnclaimedAmount(money(source.getUnclaimamount()));
+ target.setDetailSerialNo(row.detailSerialNo());
+ // 未匹配收款金额(认领上限口径)
+ target.setUnclaimedAmount(money(source.getUnmatchamountrec()));
// 手续费
target.setFee(source.getFee() == null ? null : source.getFee().setScale(2, RoundingMode.HALF_UP));
- // 公告认领状态
- target.setKingdeeClaimStatus(trimToNull(source.getClaimstatus()));
- // 我已认领
- target.setClaimMe(source.getClaimme());
// 来源更新时间
- target.setSourceUpdatedTime(target.getTransactionTime() == null
- ? LocalDateTime.now() : target.getTransactionTime());
+ target.setSourceUpdatedTime(LocalDateTime.now());
+ }
+
+ /**
+ * A301-1 单据状态 → 本地金蝶回写状态(同步时随单据刷新)
+ *
+ * @param billStatus 金蝶 billstatus(A暂存/B已提交/C已审核/D已收款/G已退单)
+ * @return 本地状态
+ */
+ private String billStatusOf(String billStatus) {
+ if ("D".equals(billStatus)) {
+ return KingdeeBfiConstant.KINGDEE_STATUS_RECEIVED;
+ }
+ if ("C".equals(billStatus)) {
+ return APPROVED;
+ }
+ if ("A".equals(billStatus) || "B".equals(billStatus)) {
+ return KingdeeBfiConstant.KINGDEE_STATUS_PUSHED;
+ }
+ // 其余状态(含 G 已退单/E 变更中)按处理失败记录,提示人工在金蝶核查
+ log.warn("金蝶收款单单据状态[{}]未匹配映射,同步按 failed 处理", billStatus);
+ return KingdeeBfiConstant.KINGDEE_STATUS_FAILED;
+ }
+
+ /**
+ * 流水分录收款金额解析:bei_creditamount 为字符串,空或非法时回退主单收款金额
+ */
+ private BigDecimal parseEntryAmount(String entryAmount, BigDecimal fallbackAmount) {
+ if (Func.isNotEmpty(entryAmount)) {
+ try {
+ return new BigDecimal(entryAmount.trim());
+ } catch (NumberFormatException exception) {
+ log.warn("解析金蝶流水分录收款金额失败,原始值:{},回退主单收款金额", entryAmount);
+ }
+ }
+ return money(fallbackAmount);
}
private String firstNonBlank(String... values) {
@@ -1237,13 +943,6 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl
- * 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
- * is strictly prohibited.
- *
- * 3. Licensees may copyright their own code but cannot use segments
- * from this software for their own commercial purposes.
- *
- * Using this software signifies agreement to this license agreement and terms of use,
- * and you expressly agree to be bound by the terms of use of this software.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
- * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
- * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- *
- * Author: Chill Zhuang (bladejava@qq.com)
- */
-package org.springblade.transport.task;
-
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.springblade.transport.config.BfiProperties;
-import org.springblade.transport.service.IReceiptFlowService;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-/**
- * 收款认领推送金蝶定时任务(A301-3 收款认领结果推送)
- *
- * 认领动作只在本地登记,本任务扫描「认领完成且未推送过」的收款流水自动推送金蝶,
- * 与收款流水页「推送金蝶」手动按钮共用同一服务方法(推送前按认领人重查 A301-2 最新公告对账)。
- * 单条流水推送失败独立消化、可重试,不影响同批其他流水。
- *
- * 调度配置(Nacos thirdParty.bfi.receiptClaimPush.*):syncEnabled 开关、cron 表达式。
- *
- * @author Chill
- */
-@Slf4j
-@Component
-@RequiredArgsConstructor
-public class ReceiptClaimPushTask {
-
- private final IReceiptFlowService receiptFlowService;
- private final BfiProperties bfiProperties;
-
- /** 收款认领推送调度,默认每10分钟执行一次 */
- @Scheduled(cron = "${thirdParty.bfi.receiptClaimPush.cron:0 4/10 * * * ?}")
- public void pushClaimToKingdee() {
- if (!bfiProperties.getReceiptClaimPush().isSyncEnabled()) {
- return;
- }
- try {
- String summary = receiptFlowService.pushClaimByTask();
- log.info("定时推送收款认领:{}", summary);
- } catch (Exception exception) {
- log.error("定时推送收款认领异常", exception);
- }
- }
-}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiRecBillQueryDTO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiRecBillQueryDTO.java
index 90ed5056..db3ea4ca 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiRecBillQueryDTO.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiRecBillQueryDTO.java
@@ -38,7 +38,7 @@ import java.util.List;
*
* 请求示例:
*
{@code
- * GET /ierp/kapi/v2/awrp/cas/cas_recbill/queryRecBill?begin_bizdate=2021-09-08&end_bizdate=2026-09-08&org_number=01.01.20.02.04.01&pageSize=10&pageNo=1
+ * GET /ierp/kapi/v2/awrp/cas/cas_recbill/queryRecBill?begin_bizdate=2021-09-08 00:00:00&end_bizdate=2026-09-08 23:59:59&org_number=01.01.20.02.04.01&pageSize=10&pageNo=1
* }
*
* @author Chill
@@ -57,12 +57,13 @@ public class BfiRecBillQueryDTO implements Serializable {
*/
private Integer pageSize;
/**
- * 【必填】开始业务日期,格式:yyyy-MM-dd,限定时间范围不可超过1个月
+ * 【必填】开始业务日期,格式:yyyy-MM-dd HH:mm:ss,限定时间范围不可超过1个月;
+ * 传纯日期 yyyy-MM-dd 会被金蝶拒绝(bos.filterSqlBuilder 时间格式错误)
*/
@JsonProperty("begin_bizdate")
private String beginBizdate;
/**
- * 【必填】结束业务日期,格式:yyyy-MM-dd
+ * 【必填】结束业务日期,格式:yyyy-MM-dd HH:mm:ss
*/
@JsonProperty("end_bizdate")
private String endBizdate;
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..8837a6ee 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
@@ -277,7 +277,7 @@ public interface IBfiService {
/**
* A301-1 收款单查询(收款处理查询)
*
- * @param param 查询参数,业务日期范围不可超过1个月
+ * @param param 查询参数,业务日期格式 yyyy-MM-dd HH:mm:ss,范围不可超过1个月
* @return 收款处理分页数据
*/
BfiPageVO queryRecBill(BfiRecBillQueryDTO param);
From 1200c5caa59ea8e29452d866b106919af03e787d 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:19:53 +0800
Subject: [PATCH 042/106] =?UTF-8?q?feat:=20=E8=AE=A4=E9=A2=86=E4=BD=9C?=
=?UTF-8?q?=E5=BA=9F=E5=90=8C=E6=AD=A5=E5=90=88=E8=A7=84=E5=B9=B6=E4=B8=8B?=
=?UTF-8?q?=E7=BA=BF=E9=87=91=E8=9D=B6=E8=AE=A4=E9=A2=86=E7=BB=93=E6=9E=9C?=
=?UTF-8?q?=E5=9B=9E=E5=86=99?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../ReceiptClaimRecordController.java | 9 +-
.../service/IReceiptClaimRecordService.java | 7 -
.../impl/ReceiptClaimRecordServiceImpl.java | 149 ++----------------
3 files changed, 14 insertions(+), 151 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptClaimRecordController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptClaimRecordController.java
index 3db05ad5..3b7b80fc 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptClaimRecordController.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ReceiptClaimRecordController.java
@@ -83,16 +83,9 @@ public class ReceiptClaimRecordController extends BladeController {
@PostMapping("/void")
@ApiOperationSupport(order = 4)
- @Operation(summary = "作废认领记录并生成金蝶认领冲单")
+ @Operation(summary = "作废认领记录(作废后同步合同合规系统)")
public R voidClaim(@RequestParam Long id) {
receiptClaimRecordService.voidClaim(id);
return R.success("作废成功");
}
-
- @PostMapping("/query-kingdee-result")
- @ApiOperationSupport(order = 5)
- @Operation(summary = "查询金蝶认领结果(A301-1 收款单查询回写)")
- public R queryKingdeeResult() {
- return R.data(receiptClaimRecordService.refreshKingdeeResult());
- }
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptClaimRecordService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptClaimRecordService.java
index efd68227..38eb870f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptClaimRecordService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IReceiptClaimRecordService.java
@@ -46,11 +46,4 @@ public interface IReceiptClaimRecordService extends BaseService {
void updateAttachments(ReceiptClaimAttachmentsRequest request);
void voidClaim(Long id);
-
- /**
- * A301-1 收款单查询回写认领结果:按来源单据编码匹配金蝶收款单,推进金蝶单据状态
- *
- * @return 回写条数
- */
- int refreshKingdeeResult();
}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java
index dc68308c..15d509ff 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptClaimRecordServiceImpl.java
@@ -33,12 +33,6 @@ import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
-import org.springblade.thirdparty.bfi.constant.BfiConstant;
-import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
-import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillVO;
-import org.springblade.thirdparty.bfi.service.IBfiService;
-import org.springblade.thirdparty.bfi.util.BfiUtil;
-import org.springblade.transport.constant.KingdeeBfiConstant;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.KingdeeReceiptFlowMapper;
import org.springblade.transport.mapper.ReceiptClaimMapper;
@@ -53,20 +47,21 @@ import org.springblade.transport.pojo.entity.ReceiptFlowRecord;
import org.springblade.transport.pojo.vo.ReceiptClaimRecordVO;
import org.springblade.transport.service.IReceiptClaimRecordService;
import org.springblade.transport.service.IFormalSettlementService;
+import org.springblade.transport.support.hcs.HcsClaimSyncSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.transaction.support.TransactionSynchronization;
+import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
-import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@@ -84,21 +79,13 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl selectPage(IPage page,
@@ -203,10 +190,6 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl relations = claimSettlementMapper.selectList(
Wrappers.lambdaQuery()
@@ -256,9 +239,8 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl
- * 回写口径:A301-1 契约本身不支持按来源单号过滤,故按近 30 天业务日期窗口全量翻页后,
- * 以金蝶收款单 sourcebillnumber == 流水 awrp_src_code(A301-3 推送时按流水生成、重试复用)在本地匹配;
- * 仅推进金蝶回写状态与补登收款单号,不改本地认领金额/分摊;名下未作废认领主单冗余展示字段级联刷新。
- *
- * @return 回写条数
- */
- @Override
- public int refreshKingdeeResult() {
- List pending = receiptFlowMapper.selectList(
- Wrappers.lambdaQuery()
- .eq(KingdeeReceiptFlow::getPushStatus, KingdeeBfiConstant.RECEIPT_CLAIM_PUSH_PUSHED)
- .in(KingdeeReceiptFlow::getKingdeeBillStatus,
- List.of(KingdeeBfiConstant.KINGDEE_STATUS_PUSHED, APPROVED))
- .eq(KingdeeReceiptFlow::getStatus, 1)
- .orderByAsc(KingdeeReceiptFlow::getId));
- if (pending.isEmpty()) {
- log.info("金蝶认领结果回写:无待回写流水(已推送/已审核且未终态)");
- return 0;
- }
- Set srcCodes = pending.stream()
- .map(KingdeeReceiptFlow::getAwrpSrcCode)
- .filter(Func::isNotEmpty)
- .collect(Collectors.toSet());
- if (srcCodes.isEmpty()) {
- return 0;
- }
- log.info("金蝶认领结果回写:待回写流水 {} 条", pending.size());
-
- LocalDate end = LocalDate.now();
- LocalDate begin = end.minusDays(RESULT_WINDOW_DAYS);
- long start = System.currentTimeMillis();
- log.info("调用BFI接口 收款单查询开始,业务日期窗口 {} ~ {}", begin, end);
- List rows = BfiUtil.queryAllPages(pageNo -> {
- BfiRecBillQueryDTO dto = new BfiRecBillQueryDTO();
- dto.setPageNo(pageNo);
- dto.setPageSize(RESULT_PAGE_SIZE);
- dto.setBeginBizdate(begin.format(DateTimeFormatter.ISO_LOCAL_DATE));
- dto.setEndBizdate(end.format(DateTimeFormatter.ISO_LOCAL_DATE));
- return bfiService.queryRecBill(dto);
+ // 作废事务提交后同步合规 C107-3(fdStatus=1 已作废,失败仅记日志不回滚作废)
+ TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
+ @Override
+ public void afterCommit() {
+ hcsClaimSyncSupport.voidClaimToHcs(claim.getId());
+ }
});
- log.info("调用BFI接口 收款单查询结束 耗时:{}ms,拉取 {} 条", System.currentTimeMillis() - start, rows.size());
-
- // 窗口内收款单按 sourcebillnumber 匹配待回写来源编码,同键多单取首条
- Map matched = rows.stream()
- .filter(row -> Func.isNotEmpty(row.getSourcebillnumber()))
- .filter(row -> srcCodes.contains(row.getSourcebillnumber()))
- .collect(Collectors.toMap(BfiRecBillVO::getSourcebillnumber, Function.identity(),
- (first, second) -> first, HashMap::new));
- int updated = 0;
- for (KingdeeReceiptFlow flow : pending) {
- BfiRecBillVO bill = matched.get(flow.getAwrpSrcCode());
- if (bill == null) {
- // 窗口内未查到:金蝶回单延迟/状态未同步,不改状态,由下次回写或人工处置兜底
- continue;
- }
- // 推送成功时金蝶收款单号先以来源编码占位(A301-3 响应 number 为空,取回执 keys.awrp_src_code),
- // 回写命中后以金蝶真实收款单号覆盖
- if (Func.isNotEmpty(bill.getBillno())) {
- flow.setKingdeeBillNo(bill.getBillno());
- }
- flow.setKingdeeBillStatus(billStatusOf(bill.getBillstatus()));
- receiptFlowMapper.updateById(flow);
- // 级联刷新名下未作废认领主单的冗余展示字段
- baseMapper.update(null, Wrappers.lambdaUpdate()
- .eq(ReceiptClaim::getReceiptFlowId, flow.getId())
- .eq(ReceiptClaim::getClaimStatus, CLAIMED)
- .set(ReceiptClaim::getKingdeeBillNo, flow.getKingdeeBillNo())
- .set(ReceiptClaim::getKingdeeBillStatus, flow.getKingdeeBillStatus()));
- updated++;
- log.info("金蝶认领结果回写 flowId:{},awrpSrcCode:{},金蝶收款单号:{},状态:{} → {}",
- flow.getId(), flow.getAwrpSrcCode(), bill.getBillno(), bill.getBillstatus(),
- flow.getKingdeeBillStatus());
- }
- log.info("金蝶认领结果回写完成:匹配 {} 条,更新 {} 条", matched.size(), updated);
- return updated;
- }
-
- /**
- * 金蝶收款单单据状态 → 本地认领金蝶状态
- *
- * @param billStatus 金蝶 billstatus(A暂存/B已提交/C已审核/D已收款/G已退单)
- * @return 本地状态
- */
- private String billStatusOf(String billStatus) {
- if (BfiConstant.BILL_STATUS_RECEIVED.equals(billStatus)) {
- return KingdeeBfiConstant.KINGDEE_STATUS_RECEIVED;
- }
- if (BfiConstant.BILL_STATUS_AUDITED.equals(billStatus)) {
- return APPROVED;
- }
- if (BfiConstant.BILL_STATUS_DRAFT.equals(billStatus)
- || BfiConstant.BILL_STATUS_SUBMITTED.equals(billStatus)) {
- return KingdeeBfiConstant.KINGDEE_STATUS_PUSHED;
- }
- // 其余状态(含 G 已退单)按处理失败回收,提示人工在金蝶核查
- log.warn("金蝶收款单状态[{}]未匹配映射,认领回写按 failed 处理", billStatus);
- return KingdeeBfiConstant.KINGDEE_STATUS_FAILED;
}
private void fillStatusNames(ReceiptClaimRecordVO record) {
@@ -399,17 +287,6 @@ public class ReceiptClaimRecordServiceImpl extends BaseServiceImpl
Date: Thu, 29 Oct 2026 11:20:03 +0800
Subject: [PATCH 043/106] =?UTF-8?q?docs:=20=E6=94=B6=E6=AC=BE=E6=B5=81?=
=?UTF-8?q?=E6=B0=B4=E8=8F=9C=E5=8D=95=E6=94=B9=E9=80=A0=E6=96=B9=E6=A1=88?=
=?UTF-8?q?=E6=96=87=E6=A1=A3?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...¹A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md | 194 ++++++++++++++++++
1 file changed, 194 insertions(+)
create mode 100644 doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
diff --git a/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md b/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
new file mode 100644
index 00000000..bff1fd6e
--- /dev/null
+++ b/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
@@ -0,0 +1,194 @@
+# 收款流水菜单改造:数据源 A301-2 改 A301-1,认领只登记本地并推送合同合规(C107-1 / C107-3)
+
+> 日期:2026-10-29
+> 范围:tms-api(blade-transport、blade-hcs-api、blade-transport-api)+ tms-erp-web(收款流水 / 认领记录页面)
+> 关联文档:
+> - [2026-09-24-收款认领金蝶全链路业务流程(A301-2公告同步-A301-3认领推送-A301-1收款单回写)](./2026-09-24-收款认领金蝶全链路业务流程(A301-2公告同步-A301-3认领推送-A301-1收款单回写).md)(本改造后 A301-3 链路整体下线)
+> - [2026-10-22-收款认领推送金蝶完成后同步合同合规系统(C103-9履约计划-C107-1认领记录-C108-1履约流水)业务流程梳理](../hcs合同/2026-10-22-收款认领推送金蝶完成后同步合同合规系统(C103-9履约计划-C107-1认领记录-C108-1履约流水)业务流程梳理.md)(触发点由 A301-3 成功后改为认领事务提交后)
+
+---
+
+## 1. 需求与决策结论
+
+收款流水菜单做两点改造:
+
+1. **数据源改为 A301-1 收款单查询**(原 A301-2 收款认领公告查询下线);
+2. **认领动作只做本地认领记录 + 推送合同合规系统**(原 A301-3 推送金蝶链路整体下线,合规推送链保持 C103-9 + C107-1 + C108-1 全链)。
+
+已确认的业务决策:
+
+| # | 决策点 | 结论 |
+| --- | --- | --- |
+| 1 | 认领上限口径 | 按 A301-1 的 **unmatchamountrec(未匹配收款金额)**,与收款金额取 min |
+| 2 | 合规推送范围 | **C103-9(履约计划金额累加)、C107-1(认领记录)、C108-1(履约流水)全保留** |
+| 3 | C107-1 推送失败补偿 | **不做定时重试,只落推送标记**(blade_receipt_claim_hcs_record.push_status = failed + 失败原因) |
+| 4 | 认领作废 | **需要 C107-3 作废同步**(fdStatus=1),C107-1 返回的合规 fdId 必须落库 |
+| 5 | 同步可见范围 | A301-1 无认领人维度,**全公司流水均进入页面,可接受** |
+
+---
+
+## 2. 现状与目标链路对比
+
+```
+【现状】
+A301-2 认领公告同步(按认领人姓名拉取)
+ → blade_kingdee_receipt_flow 落库(幂等键 detail_serial_no)
+ → 页面认领(只登记本地)
+ → A301-3 推送金蝶(手动按钮 + 10分钟定时任务,推送前重查 A301-2 对账)
+ → A301-3 成功后推合规链(C103-9 → C107-1 → C108-1)
+ → A301-1 按 awrp_src_code 回写金蝶单据状态(refreshKingdeeResult)
+
+【目标】
+A301-1 收款单查询(业务日期窗口 ≤1 个月,全量)
+ → blade_kingdee_receipt_flow 落库(幂等键 detail_serial_no,金蝶单号/状态随同步刷新)
+ → 页面认领(只登记本地)
+ → 认领事务提交后按认领主单推合规链(C103-9 → C107-1 → C108-1,C107-1 结果落库标记)
+ → 认领作废事务提交后推 C107-3(fdStatus=1 已作废)
+ → (A301-3 推送、awrp_src_code、A301-1 回写、定时任务、推送按钮全部下线)
+```
+
+关键变化:
+
+- 金蝶收款单在认领前就已存在(A301-1 即金蝶收款处理单),因此**金蝶收款单号 / 单据状态随同步直接落库**,不再依赖推送回填与回写推进;
+- 合规推送触发点从「A301-3 推送成功后(流水级)」改为「认领事务提交后(认领主单级)」,继续认领时每次认领各自推送一次,累计金额口径不变。
+
+---
+
+## 3. 后端改造明细(tms-api)
+
+### 3.1 HCS Feign(blade-third-party-api/blade-hcs-api)
+
+| 文件 | 改动 |
+| --- | --- |
+| `IHcsClient` | 新增 `CLAIM_RECORD_UPDATE_URL`(`/openapi/eop-data/eopDataCollectionFlowClaimRecordOpen/update`)与 `claimRecordUpdate` 方法(C107-3 修改认领记录) |
+| `HcsClaimRecordAddDTO` | 新增 `fdId` 字段:C107-1 新增不传,C107-3 修改必填(作废传 fdStatus=1),一个 DTO 复用两个接口 |
+
+### 3.2 收款流水同步:A301-2 → A301-1
+
+`ReceiptFlowServiceImpl#sync()` 重写,`BfiUtil.queryAllPages` + `bfiService.queryRecBill` 全量翻页(pageSize 200),请求参数 `begin_bizdate` / `end_bizdate`(窗口天数取 Nacos `thirdParty.bfi.recBillSync.days`,默认 30)。
+
+**字段映射(A301-1 → blade_kingdee_receipt_flow)**:
+
+| 流水表字段 | A301-1 来源 | 说明 |
+| --- | --- | --- |
+| receipt_notice_no / kingdee_bill_no | `billno` | 金蝶收款单号(两处同值) |
+| kingdee_bill_status | `billstatus` 映射 | A/B已提交→pushed、C已审核→approved、D已收款→received、其余(含G退单)→failed |
+| detail_serial_no | `infoentry[].bei_detailid`,兜底 `bei_bankdetailno` | **幂等键**;每条流水分录展开为一行流水 |
+| receipt_amount | `infoentry[].bei_creditamount`,兜底主单 `actrecamt` | 流水分录收款金额 |
+| unclaimed_amount | `unmatchamountrec` | 未匹配收款金额,**认领上限口径** |
+| payer_name / counterparty_name | `payername` | 付款人名称 |
+| payee_name | `org_name` | 收款公司名称 |
+| counterparty_account | `payeracctbanknum` | 付款账户 |
+| counterparty_bank | `payerbankname` | 付款银行 |
+| summary | `txt_description` | 摘要 |
+| transaction_time | `bizdate` | 业务日期 |
+| fee | `fee` | 手续费 |
+| source_updated_time | 同步时间 | — |
+| bill_id / payer_id / kingdee_claim_status / claim_me | — | **废弃不再写入**(历史 A301-2 专属字段,列保留兼容存量) |
+| push_status / push_fail_reason / pushed_time / awrp_src_code | — | **不再写入**(A301-3 下线;存量已推送数据保留展示) |
+
+同步保持原有框架:明细流水号重复抛异常、金额 ≤0 跳过、FOR UPDATE 批量防并发、按幂等键 upsert、批量操作日志;本地认领字段(claimed_amount / claim_status)不被同步覆盖,认领后状态按「累计认领 vs 未匹配金额」重算。
+
+### 3.3 认领与合规推送链
+
+`ReceiptFlowServiceImpl#claim()`:
+
+- 删除「认领公告id缺失」「公告未认领金额缺失」拦截(A301-1 无公告概念);
+- 认领上限:`claimCeiling = min(unclaimed_amount(unmatchamountrec), receipt_amount)`;
+- 认领主单冗余展示字段直取流水:`kingdeeBillNo` / `kingdeeBillStatus`(金蝶单据认领前已存在);
+- 事务提交后(`TransactionSynchronizationManager.afterCommit`)按**认领主单**触发 `HcsClaimSyncSupport#pushClaimToHcs(claimId)`,失败仅落标记不影响认领。
+
+`HcsClaimSyncSupport` 重写(编排顺序不变、触发维度改变):
+
+1. 归集**本认领主单**有效分摊行 → 按合同分组金额;
+2. C104-2 按明细流水号定位合规侧收款流水 ID(一次定位全部合同共用);
+3. 逐合同:C103-9 履约计划金额累加重推 → C107-1 认领记录(返回 fdId)→ C108-1 履约流水;
+4. **C107-1 结果落库** `blade_receipt_claim_hcs_record`(认领主单×合同一条):成功持久化合规 fdId(供 C107-3 引用),失败落 failed + 原因(只记标记,无定时重试);
+5. C108-1 的申请编号兜底、金蝶单据号(fd_jd_doc_no)取流水 `kingdee_bill_no`(A301-1 同步落库),金额改为本次认领分摊金额。
+
+开关复用既有配置:`exe-stage.sync-enabled` 门禁 C103-9/C107-1,`perf-record.sync-enabled` 门禁 C108-1。
+
+### 3.4 认领作废:C107-3 同步
+
+`ReceiptClaimRecordServiceImpl#voidClaim()`:
+
+- 删除「已推送金蝶不允许作废」拦截(不再有 TMS→金蝶推送,作废只回退本地 + 同步合规);
+- 事务提交后按认领主单推 C107-3:查 `blade_receipt_claim_hcs_record` 中 push_status=success 的记录,逐条 `claimRecordUpdate(fdId, fdStatus=1)`,成功后本地记录置 voided;单条失败独立记日志不回滚作废,合规侧需人工处置。
+
+### 3.5 A301-3 链路整体下线
+
+| 项 | 处置 |
+| --- | --- |
+| `pushClaimToKingdee` / `pushClaimByTask` / `executeReceiptClaimPush` / `pushClaimLocked` / `doPushClaimToKingdee` / `buildClaimPushDTO` / `extractClaimPushBillNo` / `applyClaimPushSuccess` / `applyClaimPushFailure` | 删除(ReceiptFlowServiceImpl) |
+| `ReceiptClaimPushTask`(10 分钟定时推送) | 文件删除 |
+| `ReceiptFlowController` `/push-kingdee` 端点 | 删除 |
+| `refreshKingdeeResult`(A301-1 按 awrp_src_code 回写)+ `/receipt-claim-record/query-kingdee-result` 端点 | 删除(A301-1 已是数据源,状态随同步刷新) |
+| `awrpSrcCode` 生成逻辑 | 删除 |
+| `BfiProperties` | `claimAnnounce` / `receiptClaimPush` 配置类删除,新增 `RecBillSync`(`days` 默认 30) |
+| `KingdeeReceiptFlow` 实体 | 字段注释更新为 A301-1 口径(bill_id 等标注废弃) |
+
+### 3.6 新增表
+
+`blade_receipt_claim_hcs_record`(收款认领合规推送记录,DDL 见 `doc/sql/transport/lqj/20261028/01_blade_receipt_claim_hcs_record_20261028.sql`):
+
+| 字段 | 说明 |
+| --- | --- |
+| receipt_claim_id / receipt_flow_id | 认领主单 / 收款流水 |
+| contract_id / contract_no | 合同 |
+| claim_amount | 本认领主单分摊到本合同的金额 |
+| hcs_claim_record_fd_id | 合规侧认领记录 fdId(C107-1 返回,C107-3 必填引用) |
+| push_status | success 推送成功 / failed 推送失败 / voided 已作废 |
+| fail_reason / pushed_time | 失败原因 / 成功时间 |
+
+---
+
+## 4. 前端改造明细(tms-erp-web)
+
+| 页面 / 文件 | 改动 |
+| --- | --- |
+| `views/payment/receipt-flow.vue` | 删「推送金蝶」操作按钮及 `canPush`/`handlePush`;删搜索「推送状态」;详情删「推送状态」「推送失败原因」,「金蝶未认领金额」→「未匹配金额」;同步提示改为"收款流水" |
+| `views/payment/receipt-claim-record.vue` | 删「查询金蝶认领结果」按钮;删搜索「推送状态」;作废按钮去掉 `pushStatus !== 'pushed'` 限制;作废确认提示增加"同步作废合同合规系统认领记录" |
+| `views/payment/receipt-flow-form.vue` / `receipt-flow-batch-form.vue` | 「认领通知单」→「收款单号」;「金蝶未认领金额」→「未匹配金额」;校验提示"公告未认领金额"→"未匹配金额" |
+| `views/payment/receipt-claim-record-form.vue` | 「认领通知单」→「收款单号」 |
+| `views/settlement/components/formal-settlement-editor.vue` | 认领列表列「认领通知单」→「收款单号」 |
+| `api/payment/receiptFlow.js` | 删 `pushKingdee`、`pushStatusOptions` |
+| `api/payment/receiptClaimRecord.js` | 删 `queryKingdeeResult`、`pushStatusOptions` |
+| `option/payment/receiptFlow.js` | 删「公告状态」「推送状态」「推送失败原因」「金蝶收款单号」(与单号列重复)列;「认领通知单」→「收款单号」、「付款金额」→「收款金额」、「金蝶未认领金额」→「未匹配金额」 |
+| `option/payment/receiptClaimRecord.js` | 「认领通知单」→「收款单号」、「付款金额」→「收款金额」 |
+
+菜单按钮权限提示:`receipt_flow_push` 权限码随按钮下线,可在菜单配置中清理。
+
+---
+
+## 5. 配置与存量数据
+
+**Nacos(thirdParty.bfi.*)**:
+
+- 新增(可选):`recBillSync.days`,默认 30,A301-1 契约约束业务日期窗口 ≤1 个月;
+- 可清理:`claimAnnounce.*`、`receiptClaimPush.*`(代码已不再读取)。
+
+**存量数据**:
+
+- A301-2 时代同步的流水(bill_id、kingdee_claim_status、claim_me 有值)与新口径并存,页面不再展示这些列;历史 push_status=pushed / awrp_src_code 仅保留展示,不再参与任何闸门;
+- 明细流水号为全链唯一衔接键:A301-1 `bei_detailid` ≈ A301-2 `detailid` ≈ 合规 C104-2 `fdDetailSerialNumber`,同源同值时新旧数据 upsert 自然衔接。
+
+---
+
+## 6. 风险与待验证项
+
+| # | 项 | 说明 | 等级 |
+| --- | --- | --- | --- |
+| 1 | **明细流水号同源性** | A301-1 `bei_detailid` 必须与原 A301-2 `detailid`、合规侧 `fdDetailSerialNumber` 同值,否则 C104-2 定位失败 → C107-1 全部跳过(仅记日志/落失败标记)。需联调拿真实报文核对 | 高 |
+| 2 | 单据级未匹配金额 | `unmatchamountrec` 为收款单级口径;一张收款单多条流水分录时各行共享同一上限,理论上限放大(单分录场景无影响) | 中 |
+| 3 | 合规履约计划金额不回退 | C103-9 认领时累加,作废仅同步 C107-3,不回退履约计划行金额(与改造前口径一致,未新增回退逻辑) | 中 |
+| 4 | 全公司可见范围 | A301-1 按组织/日期拉全量,不再按认领人过滤,页面数据范围扩大(业务已确认可接受) | 低 |
+| 5 | C107-1 失败无自动重试 | 仅落 failed 标记与原因,需人工在合规侧核对补录(业务已确认) | 低 |
+
+## 7. 验证清单(交由测试/联调执行)
+
+1. 手动同步:A301-1 拉取、流水按 infoentry 展开、重复明细流水号拦截、金额/单号/状态落库正确;
+2. 认领:上限按未匹配金额控制、认领完成状态判定、合规链推送成功后 `blade_receipt_claim_hcs_record` 有 success 记录且 fdId 非空;
+3. 继续认领:第二次认领各自推送一次 C107-1/C108-1,C103-9 累计金额正确;
+4. 作废:本地回退 + C107-3 同步、记录置 voided;无合规记录(推送失败)时作废静默跳过;
+5. 合规开关关闭(exe-stage/perf-record sync-enabled=false)时认领/作废主流程不受影响;
+6. 回归:认领记录页、结算单收款汇总刷新、认领人筛选。
From bed04849ed9df33b1644df551c61006b0ee5f444 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:29:04 +0800
Subject: [PATCH 044/106] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3BFI=E4=BB=98?=
=?UTF-8?q?=E6=AC=BE=E5=A4=84=E7=90=86=E6=9F=A5=E8=AF=A2=E5=BC=80=E5=A7=8B?=
=?UTF-8?q?=E6=97=A5=E6=9C=9F=E5=8F=82=E6=95=B0=E5=90=8D=E4=BB=A5=E5=8C=B9?=
=?UTF-8?q?=E9=85=8D=E9=87=91=E8=9D=B6=E5=A5=91=E7=BA=A6?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../org/springblade/thirdparty/bfi/feign/IBfiClient.java | 5 ++++-
.../thirdparty/bfi/pojo/dto/BfiPayBillQueryDTO.java | 3 ++-
2 files changed, 6 insertions(+), 2 deletions(-)
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..ee3c16a0 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
@@ -210,6 +210,9 @@ public interface IBfiClient {
/**
* A103-4 付款处理查询
+ *
+ * 注意:开始业务日期的请求参数名以金蝶契约为准,为 {@code beigin_bizdate}(并非 begin_bizdate),
+ * 传错会被忽略并返回「参数【beigin_bizdate】必填」。
*
* @param accessToken 访问令牌,通过 getToken 获取
* @param pageNo 页码,从1开始
@@ -224,7 +227,7 @@ public interface IBfiClient {
BfiResultVO> payBillQuery(@RequestHeader(BfiConstant.HEADER_ACCESS_TOKEN) String accessToken,
@RequestParam("pageNo") Integer pageNo,
@RequestParam("pageSize") Integer pageSize,
- @RequestParam("begin_bizdate") String beginBizdate,
+ @RequestParam("beigin_bizdate") String beginBizdate,
@RequestParam("end_bizdate") String endBizdate,
@RequestParam(value = "billno", required = false) List billno,
@RequestParam("org_number") List orgNumber);
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiPayBillQueryDTO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiPayBillQueryDTO.java
index 07a88861..385a2dfa 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiPayBillQueryDTO.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiPayBillQueryDTO.java
@@ -51,7 +51,8 @@ public class BfiPayBillQueryDTO implements Serializable {
*/
private Integer pageSize;
/**
- * 开始业务日期,格式:yyyy-MM-dd,限定时间范围不可超过1个月
+ * 开始业务日期,格式:yyyy-MM-dd,限定时间范围不可超过1个月;
+ * 请求参数名为 beigin_bizdate(金蝶契约拼写如此,非 begin_bizdate)
*/
private String beginBizdate;
/**
From b3b81a267446e57a11050c8f6eda3c5ba5fe9e36 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:38:48 +0800
Subject: [PATCH 045/106] =?UTF-8?q?feat:=20=E4=BB=98=E6=AC=BE=E5=A4=84?=
=?UTF-8?q?=E7=90=86=E6=9F=A5=E8=AF=A2=E4=B8=9A=E5=8A=A1=E6=97=A5=E6=9C=9F?=
=?UTF-8?q?=E7=AA=97=E5=8F=A3=E5=A4=A9=E6=95=B0=E6=94=B9=E4=B8=BA=E5=8F=AF?=
=?UTF-8?q?=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../org/springblade/transport/config/BfiProperties.java | 5 +++++
.../service/impl/PaymentApplicationServiceImpl.java | 8 +++++---
doc/nacos/blade-dev.yaml | 2 ++
doc/nacos/blade-prod.yaml | 2 ++
doc/nacos/third-party-api.yaml | 2 ++
5 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
index 0b579ecb..acec0027 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
@@ -247,6 +247,11 @@ public class BfiProperties {
* 回写 single-flight 锁租约秒数,须大于最坏一轮耗时(分批远程查询 + 逐单回写)
*/
private long lockLeaseSeconds = 1800;
+
+ /**
+ * A103-4 付款处理查询业务日期窗口天数,默认 60
+ */
+ private int queryWindowDays = 60;
}
/**
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 5d52c861..dce97de2 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
@@ -1185,15 +1185,17 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl
+ * 窗口天数取 Nacos thirdParty.bfi.payResult.queryWindowDays(默认 60)
*
* @param orgNumber 付款人(公司)编码
* @return 付款处理单列表
*/
private List queryPayBillPages(String orgNumber) {
LocalDate endDate = LocalDate.now();
- // 业务日期窗口控制在不超过1个月
- LocalDate beginDate = endDate.minusMonths(1).plusDays(1);
+ int windowDays = Math.max(bfiProperties.getPayResult().getQueryWindowDays(), 1);
+ LocalDate beginDate = endDate.minusDays(windowDays);
BfiPayBillQueryDTO param = new BfiPayBillQueryDTO();
param.setOrgNumber(List.of(orgNumber));
param.setBeginBizdate(beginDate.format(DateTimeFormatter.ISO_LOCAL_DATE));
diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml
index 6b47a7ca..ebdea597 100644
--- a/doc/nacos/blade-dev.yaml
+++ b/doc/nacos/blade-dev.yaml
@@ -190,6 +190,8 @@ thirdParty:
syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true}
# 调度 cron,默认每10分钟
cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?}
+ # A103-4 付款处理查询业务日期窗口天数
+ queryWindowDays: ${BFI_PAY_RESULT_QUERY_WINDOW_DAYS:60}
# 收款认领公告查询配置(A301-2)
claimAnnounce:
# 查询时间窗口(月数)
diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml
index a7ea8a30..0cb488f2 100644
--- a/doc/nacos/blade-prod.yaml
+++ b/doc/nacos/blade-prod.yaml
@@ -139,6 +139,8 @@ thirdParty:
syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:true}
# 调度 cron,默认每10分钟
cron: ${BFI_PAY_RESULT_CRON:0 */10 * * * ?}
+ # A103-4 付款处理查询业务日期窗口天数
+ queryWindowDays: ${BFI_PAY_RESULT_QUERY_WINDOW_DAYS:60}
# 收款认领推送金蝶定时任务(A301-3 收款认领结果推送)
receiptClaimPush:
# 推送任务开关,默认关闭(联调稳定后开启)
diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml
index efbaaef0..47b65871 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -92,6 +92,8 @@ thirdParty:
syncEnabled: ${BFI_PAY_RESULT_SYNC_ENABLED:false}
# 调度 cron,默认每10分钟
cron: ${BFI_PAY_RESULT_CRON:0 1/10 * * * ?}
+ # A103-4 付款处理查询业务日期窗口天数
+ queryWindowDays: ${BFI_PAY_RESULT_QUERY_WINDOW_DAYS:60}
# 金蝶收票单结果定时回写(A801-3 异常回收/指定补偿/单号补登)
receivedInvoiceResult:
From 4068d24ee93d76de1437b41eadd5f9604a35bf17 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 12:13:22 +0800
Subject: [PATCH 046/106] =?UTF-8?q?feat:=20=E6=94=B6=E6=AC=BE=E5=8D=95?=
=?UTF-8?q?=E6=9F=A5=E8=AF=A2=E4=B8=9A=E5=8A=A1=E6=97=A5=E6=9C=9F=E7=AA=97?=
=?UTF-8?q?=E5=8F=A3=E5=A4=A9=E6=95=B0=E9=BB=98=E8=AE=A4=E6=94=B9=E4=B8=BA?=
=?UTF-8?q?60=E5=A4=A9=E5=B9=B6=E8=A1=A5=E5=85=85Nacos=E9=85=8D=E7=BD=AE?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../org/springblade/transport/config/BfiProperties.java | 4 ++--
.../transport/service/impl/ReceiptFlowServiceImpl.java | 2 +-
.../thirdparty/bfi/pojo/dto/BfiRecBillQueryDTO.java | 2 +-
.../org/springblade/thirdparty/bfi/service/IBfiService.java | 2 +-
...据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md | 6 +++---
doc/nacos/blade-dev.yaml | 4 ++++
doc/nacos/blade-prod.yaml | 4 ++++
doc/nacos/third-party-api.yaml | 4 ++++
8 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
index acec0027..f865fe8f 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
@@ -198,9 +198,9 @@ public class BfiProperties {
public static class RecBillSync {
/**
- * 同步业务日期窗口天数,默认 30(A301-1 契约约束业务日期范围不可超过 1 个月)
+ * 同步业务日期窗口天数,默认 60
*/
- private int days = 30;
+ private int days = 60;
}
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
index b47b4fe3..04f8803c 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
@@ -776,7 +776,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl queryRecBills() {
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiRecBillQueryDTO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiRecBillQueryDTO.java
index db3ea4ca..a648c977 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiRecBillQueryDTO.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/dto/BfiRecBillQueryDTO.java
@@ -57,7 +57,7 @@ public class BfiRecBillQueryDTO implements Serializable {
*/
private Integer pageSize;
/**
- * 【必填】开始业务日期,格式:yyyy-MM-dd HH:mm:ss,限定时间范围不可超过1个月;
+ * 【必填】开始业务日期,格式:yyyy-MM-dd HH:mm:ss,窗口天数由调用方控制;
* 传纯日期 yyyy-MM-dd 会被金蝶拒绝(bos.filterSqlBuilder 时间格式错误)
*/
@JsonProperty("begin_bizdate")
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 8837a6ee..703f3f81 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
@@ -277,7 +277,7 @@ public interface IBfiService {
/**
* A301-1 收款单查询(收款处理查询)
*
- * @param param 查询参数,业务日期格式 yyyy-MM-dd HH:mm:ss,范围不可超过1个月
+ * @param param 查询参数,业务日期格式 yyyy-MM-dd HH:mm:ss,窗口天数由调用方控制
* @return 收款处理分页数据
*/
BfiPageVO queryRecBill(BfiRecBillQueryDTO param);
diff --git a/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md b/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
index bff1fd6e..2f5346ab 100644
--- a/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
+++ b/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
@@ -39,7 +39,7 @@ A301-2 认领公告同步(按认领人姓名拉取)
→ A301-1 按 awrp_src_code 回写金蝶单据状态(refreshKingdeeResult)
【目标】
-A301-1 收款单查询(业务日期窗口 ≤1 个月,全量)
+A301-1 收款单查询(业务日期窗口取 `thirdParty.bfi.recBillSync.days`,默认 60 天,全量)
→ blade_kingdee_receipt_flow 落库(幂等键 detail_serial_no,金蝶单号/状态随同步刷新)
→ 页面认领(只登记本地)
→ 认领事务提交后按认领主单推合规链(C103-9 → C107-1 → C108-1,C107-1 结果落库标记)
@@ -65,7 +65,7 @@ A301-1 收款单查询(业务日期窗口 ≤1 个月,全量)
### 3.2 收款流水同步:A301-2 → A301-1
-`ReceiptFlowServiceImpl#sync()` 重写,`BfiUtil.queryAllPages` + `bfiService.queryRecBill` 全量翻页(pageSize 200),请求参数 `begin_bizdate` / `end_bizdate`(窗口天数取 Nacos `thirdParty.bfi.recBillSync.days`,默认 30)。
+`ReceiptFlowServiceImpl#sync()` 重写,`BfiUtil.queryAllPages` + `bfiService.queryRecBill` 全量翻页(pageSize 200),请求参数 `begin_bizdate` / `end_bizdate`(窗口天数取 Nacos `thirdParty.bfi.recBillSync.days`,默认 60)。
**字段映射(A301-1 → blade_kingdee_receipt_flow)**:
@@ -164,7 +164,7 @@ A301-1 收款单查询(业务日期窗口 ≤1 个月,全量)
**Nacos(thirdParty.bfi.*)**:
-- 新增(可选):`recBillSync.days`,默认 30,A301-1 契约约束业务日期窗口 ≤1 个月;
+- 新增(可选):`recBillSync.days`,默认 60(A301-1 业务日期窗口天数);
- 可清理:`claimAnnounce.*`、`receiptClaimPush.*`(代码已不再读取)。
**存量数据**:
diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml
index ebdea597..ad5d667b 100644
--- a/doc/nacos/blade-dev.yaml
+++ b/doc/nacos/blade-dev.yaml
@@ -184,6 +184,10 @@ thirdParty:
exratetable: ${BFI_PAY_APPLY_EXRATE_TABLE:01}
# 外部单据类型
srcType: ${BFI_PAY_APPLY_SRC_TYPE:0KzXs}
+ # 收款流水同步配置(A301-1 收款单查询)
+ recBillSync:
+ # 业务日期窗口天数
+ days: ${BFI_RECBILL_SYNC_DAYS:60}
# 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询)
payResult:
# 回写任务开关
diff --git a/doc/nacos/blade-prod.yaml b/doc/nacos/blade-prod.yaml
index 0cb488f2..93d9729e 100644
--- a/doc/nacos/blade-prod.yaml
+++ b/doc/nacos/blade-prod.yaml
@@ -133,6 +133,10 @@ thirdParty:
exratetable: ${BFI_PAY_APPLY_EXRATE_TABLE:01}
# 外部单据类型
srcType: ${BFI_PAY_APPLY_SRC_TYPE:0KzXs}
+ # 收款流水同步配置(A301-1 收款单查询)
+ recBillSync:
+ # 业务日期窗口天数
+ days: ${BFI_RECBILL_SYNC_DAYS:60}
# 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询)
payResult:
# 回写任务开关
diff --git a/doc/nacos/third-party-api.yaml b/doc/nacos/third-party-api.yaml
index 47b65871..ea5c0f26 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -86,6 +86,10 @@ thirdParty:
exratetable: ${BFI_PAY_APPLY_EXRATE_TABLE:01}
# 外部单据类型
srcType: ${BFI_PAY_APPLY_SRC_TYPE:0KzXs}
+ # 收款流水同步配置(A301-1 收款单查询)
+ recBillSync:
+ # 业务日期窗口天数
+ days: ${BFI_RECBILL_SYNC_DAYS:60}
# 金蝶付款结果定时回写(A103-2 付款申请单查询 / A103-4 付款处理查询)
payResult:
# 回写任务开关
From 459d81872478321de949633c17e1c26c0babec0e 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 12:53:16 +0800
Subject: [PATCH 047/106] =?UTF-8?q?feat:=20BFI=E5=88=86=E9=A1=B5=E6=96=B0?=
=?UTF-8?q?=E5=A2=9E=E6=8C=89totalCount=E6=8E=A8=E5=AF=BC=E6=80=BB?=
=?UTF-8?q?=E9=A1=B5=E6=95=B0=E7=9A=84=E7=BF=BB=E9=A1=B5=E6=96=B9=E6=B3=95?=
=?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E5=9B=BA=E5=AE=9A=E9=A1=B5=E6=95=B0?=
=?UTF-8?q?=E6=88=AA=E6=96=AD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../service/impl/ReceiptFlowServiceImpl.java | 3 +-
.../thirdparty/bfi/util/BfiUtil.java | 81 +++++++++++++++++++
...¹A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md | 2 +-
3 files changed, 84 insertions(+), 2 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
index 04f8803c..f7ced5f8 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
@@ -777,6 +777,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl queryRecBills() {
@@ -792,7 +793,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl recBills = BfiUtil.queryAllPages(pageNo -> {
+ List recBills = BfiUtil.queryAllPagesByTotal(pageNo -> {
queryDTO.setPageNo(pageNo);
return bfiService.queryRecBill(queryDTO);
});
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java
index ed603bb2..6eb35686 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/util/BfiUtil.java
@@ -25,6 +25,7 @@
*/
package org.springblade.thirdparty.bfi.util;
+import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.thirdparty.bfi.pojo.dto.BfiBaseDTO;
@@ -44,6 +45,7 @@ import java.util.function.IntFunction;
*
* @author Chill
*/
+@Slf4j
public class BfiUtil {
/**
@@ -56,6 +58,11 @@ public class BfiUtil {
*/
public static final int DEFAULT_MAX_PAGES = 50;
+ /**
+ * 按 totalCount 推导总页数时的兜底最大页数,仅防金蝶侧返回异常导致死循环
+ */
+ public static final int DEFAULT_TOTAL_MAX_PAGES = 500;
+
private BfiUtil() {
}
@@ -139,4 +146,78 @@ public class BfiUtil {
}
return rows;
}
+
+ /**
+ * BFI 分页查询通用翻页:按首页返回的 totalCount 推导总页数,自动翻页汇总全量数据行
+ *
+ * 与 {@link #queryAllPages(IntFunction)} 的区别:总页数由 totalCount 决定,固定页数仅作兜底,
+ * 适合业务日期窗口较长、数据量可能超过固定页数上限的查询场景;达到兜底上限或未取满 totalCount 时打印告警,避免静默截断
+ *
+ * @param pageFetcher 页码 -> 分页结果,lambda 内负责设置页码(必要时含页大小)并调用接口
+ * @param 数据行类型
+ * @return 全量数据行
+ */
+ public static List queryAllPagesByTotal(IntFunction> pageFetcher) {
+ return queryAllPagesByTotal(pageFetcher, DEFAULT_TOTAL_MAX_PAGES);
+ }
+
+ /**
+ * BFI 分页查询通用翻页:按首页返回的 totalCount 推导总页数,自动翻页汇总全量数据行
+ *
+ * 终止条件:返回页为空、数据行为空、lastPage=true、数据行不满一页(按返回 pageSize 判断)、取满推导出的总页数
+ *
+ * totalCount 缺失或非正数时退化为 {@link #queryAllPages(IntFunction, int)} 的固定页数上限逻辑
+ *
+ * @param pageFetcher 页码 -> 分页结果,lambda 内负责设置页码(必要时含页大小)并调用接口
+ * @param maxPages 兜底最大页数,防止金蝶侧 totalCount/lastPage 返回异常时死循环
+ * @param 数据行类型
+ * @return 全量数据行
+ */
+ public static List queryAllPagesByTotal(IntFunction> pageFetcher, int maxPages) {
+ List rows = new ArrayList<>();
+ int totalPages = maxPages;
+ Integer totalCount = null;
+ for (int pageNo = 1; pageNo <= totalPages; pageNo++) {
+ BfiPageVO page = pageFetcher.apply(pageNo);
+ if (page == null || Func.isEmpty(page.getRows())) {
+ return rows;
+ }
+ rows.addAll(page.getRows());
+ if (pageNo == 1) {
+ totalCount = page.getTotalCount();
+ totalPages = resolveTotalPages(page, maxPages);
+ }
+ if (Boolean.TRUE.equals(page.getLastPage())) {
+ return rows;
+ }
+ if (page.getPageSize() != null && page.getRows().size() < page.getPageSize()) {
+ return rows;
+ }
+ }
+ if (totalCount != null && rows.size() < totalCount) {
+ log.warn("BFI分页查询未取满,totalCount={},已汇总 {} 条,可能存在数据截断", totalCount, rows.size());
+ }
+ return rows;
+ }
+
+ /**
+ * 按首页 totalCount 与 pageSize 推导总页数,超过兜底上限时按上限截断并告警
+ *
+ * @param page 首页分页结果
+ * @param maxPages 兜底最大页数
+ * @return 总页数
+ */
+ private static int resolveTotalPages(BfiPageVO> page, int maxPages) {
+ Integer totalCount = page.getTotalCount();
+ Integer pageSize = page.getPageSize();
+ if (totalCount == null || totalCount <= 0 || pageSize == null || pageSize <= 0) {
+ return maxPages;
+ }
+ int totalPages = (totalCount + pageSize - 1) / pageSize;
+ if (totalPages > maxPages) {
+ log.warn("BFI分页查询需要 {} 页(totalCount={}),超过兜底上限 {} 页,超出部分将被截断", totalPages, totalCount, maxPages);
+ return maxPages;
+ }
+ return totalPages;
+ }
}
diff --git a/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md b/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
index 2f5346ab..a4bf4b55 100644
--- a/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
+++ b/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
@@ -65,7 +65,7 @@ A301-1 收款单查询(业务日期窗口取 `thirdParty.bfi.recBillSync.days`
### 3.2 收款流水同步:A301-2 → A301-1
-`ReceiptFlowServiceImpl#sync()` 重写,`BfiUtil.queryAllPages` + `bfiService.queryRecBill` 全量翻页(pageSize 200),请求参数 `begin_bizdate` / `end_bizdate`(窗口天数取 Nacos `thirdParty.bfi.recBillSync.days`,默认 60)。
+`ReceiptFlowServiceImpl#sync()` 重写,`BfiUtil.queryAllPagesByTotal`(按 totalCount 推导总页数,兜底 500 页)+ `bfiService.queryRecBill` 全量翻页(pageSize 200),请求参数 `begin_bizdate` / `end_bizdate`(窗口天数取 Nacos `thirdParty.bfi.recBillSync.days`,默认 60)。
**字段映射(A301-1 → blade_kingdee_receipt_flow)**:
From d2bd36b4f8c8bb1d20e5860434be3a94c1d4a5c1 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 14:16:44 +0800
Subject: [PATCH 048/106] =?UTF-8?q?fix:=20=E6=94=B6=E6=AC=BE=E6=B5=81?=
=?UTF-8?q?=E6=B0=B4=E6=94=B6=E6=AC=BE=E9=87=91=E9=A2=9D=E6=94=B9=E5=8F=96?=
=?UTF-8?q?A301-1=E4=B8=BB=E5=8D=95=E5=85=A5=E8=B4=A6=E9=87=91=E9=A2=9Dinn?=
=?UTF-8?q?eraccountamount?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../pojo/entity/KingdeeReceiptFlow.java | 2 +-
.../service/impl/ReceiptFlowServiceImpl.java | 25 +++++--------------
.../thirdparty/bfi/pojo/vo/BfiRecBillVO.java | 5 ++++
...¹A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md | 2 +-
4 files changed, 13 insertions(+), 21 deletions(-)
diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java
index 16a09b7b..40d5c393 100644
--- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeReceiptFlow.java
@@ -89,7 +89,7 @@ public class KingdeeReceiptFlow extends TenantEntity {
*/
private String payerName;
/**
- * 收款金额(A301-1 流水分录 bei_creditamount,兜底主单 actrecamt)
+ * 收款金额(A301-1 主单入账金额 inneraccountamount)
*/
private BigDecimal receiptAmount;
/**
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
index f7ced5f8..1ea5162e 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
@@ -471,13 +471,14 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl
Date: Thu, 29 Oct 2026 14:29:34 +0800
Subject: [PATCH 049/106] =?UTF-8?q?perf:=20=E6=94=B6=E6=AC=BE=E6=B5=81?=
=?UTF-8?q?=E6=B0=B4=E5=90=8C=E6=AD=A5=E6=94=B9=E4=B8=BA=E6=B5=81=E5=BC=8F?=
=?UTF-8?q?=E5=88=86=E6=89=B9=E5=85=A5=E5=BA=93=E6=AF=8F5000=E6=9D=A1?=
=?UTF-8?q?=E4=B8=80=E6=89=B9=E7=8B=AC=E7=AB=8B=E4=BA=8B=E5=8A=A1?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../service/impl/ReceiptFlowServiceImpl.java | 317 +++++++++---------
1 file changed, 166 insertions(+), 151 deletions(-)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
index 1ea5162e..6eef4fae 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java
@@ -39,6 +39,7 @@ import org.springblade.system.cache.SysCache;
import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillFlowVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
@@ -66,6 +67,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
+import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.math.RoundingMode;
@@ -107,6 +109,14 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl recBills = queryRecBills();
-
- if (recBills.isEmpty()) {
- record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
- "金蝶收款单未查询到数据");
- return 0;
- }
-
- // 同步口径:A301-1 按业务日期窗口拉取金蝶收款单(收款处理),按流水分录展开,
+ int days = bfiProperties.getRecBillSync().getDays();
+ LocalDate endDate = LocalDate.now();
+ LocalDate startDate = endDate.minusDays(days);
+ // 金蝶 A301-1 仅接受带时分秒的日期参数,传纯日期会报 bos.filterSqlBuilder 时区时间格式错误
+ String beginBizdate = startDate.atStartOfDay().format(BFI_DATE_TIME_FORMATTER);
+ String endBizdate = endDate.atTime(23, 59, 59).format(BFI_DATE_TIME_FORMATTER);
+ log.info("收款流水同步开始,业务日期窗口:{} ~ {}(近{}天)", beginBizdate, endBizdate, days);
+ long start = System.currentTimeMillis();
+ // 同步口径:按业务日期窗口拉取金蝶收款单(收款处理),按流水分录展开,
// 以金蝶明细流水号为幂等键 upsert 收款流水表;本地已认领金额/状态(claimedAmount/claimStatus)
// 由认领、作废驱动,不被同步顺带覆盖,未匹配收款金额单独存 unclaimed_amount 作为认领上限。
-
- // 第一遍:数据校验与过滤,主单+流水分录展开为流水行,收集有效的 detailSerialNo
- log.info("收款流水同步开始数据校验,共 {} 张收款单", recBills.size());
- Map validRows = new LinkedHashMap<>();
+ BfiRecBillQueryDTO queryDTO = new BfiRecBillQueryDTO();
+ queryDTO.setBeginBizdate(beginBizdate);
+ queryDTO.setEndBizdate(endBizdate);
+ queryDTO.setPageSize(SYNC_PAGE_SIZE);
+ // 待入库缓冲:累计满 SYNC_BATCH_SIZE 即入库一批并清空
+ List pending = new ArrayList<>();
+ Set batchSerials = new HashSet<>();
+ int totalCount = 0;
+ int batchNo = 0;
+ int pullCount = 0;
int skippedCount = 0;
- for (BfiRecBillVO recBill : recBills) {
- List flowEntries = recBill.getInfoentry();
- if (Func.isEmpty(flowEntries)) {
- log.warn("收款流水同步跳过:收款单[{}]无流水分录", recBill.getBillno());
- skippedCount++;
- continue;
+ for (int pageNo = 1; pageNo <= SYNC_MAX_PAGES; pageNo++) {
+ queryDTO.setPageNo(pageNo);
+ BfiPageVO page = bfiService.queryRecBill(queryDTO);
+ List rows = page == null ? List.of() : page.getRows();
+ if (Func.isEmpty(rows)) {
+ break;
}
- for (BfiRecBillFlowVO flowEntry : flowEntries) {
- // 明细流水号优先级:bei_detailid > bei_bankdetailno
- String detailSerialNo = firstNonBlank(flowEntry.getBeiDetailid(), flowEntry.getBeiBankdetailno());
- if (Func.isEmpty(detailSerialNo)) {
- log.warn("收款流水同步跳过:收款单[{}]流水分录缺少明细流水号", recBill.getBillno());
- skippedCount++;
- continue;
+ pullCount += rows.size();
+ for (BfiRecBillVO recBill : rows) {
+ skippedCount += expandRecBillRows(recBill, pending, batchSerials);
+ if (pending.size() >= SYNC_BATCH_SIZE) {
+ batchNo++;
+ totalCount += syncBatch(batchNo, pending);
+ pending.clear();
+ batchSerials.clear();
}
- detailSerialNo = detailSerialNo.trim();
- if (validRows.containsKey(detailSerialNo)) {
- throw new ServiceException("明细流水号" + detailSerialNo + "重复");
- }
- // 收款金额取主单入账金额 inneraccountamount(金蝶口径,非流水分录 bei_creditamount)
- BigDecimal receiptAmount = money(recBill.getInneraccountAmount());
- if (receiptAmount.compareTo(BigDecimal.ZERO) <= 0) {
- log.warn("收款流水同步跳过:收款单[{}]收款金额(inneraccountamount)为空或小于0", recBill.getBillno());
- skippedCount++;
- continue;
- }
- validRows.put(detailSerialNo, new BfiRecBillRow(recBill, detailSerialNo, receiptAmount));
+ }
+ // 翻页终止:末页标识、数据不满一页(防金蝶侧 lastPage 异常死循环,另有 SYNC_MAX_PAGES 兜底)
+ if (Boolean.TRUE.equals(page.getLastPage())
+ || (page.getPageSize() != null && rows.size() < page.getPageSize())) {
+ break;
}
}
- log.info("收款流水数据校验完成:有效 {} 条,跳过 {} 条", validRows.size(), skippedCount);
-
- if (validRows.isEmpty()) {
- log.info("收款流水同步无有效数据,记录操作日志后返回");
+ if (!pending.isEmpty()) {
+ batchNo++;
+ totalCount += syncBatch(batchNo, pending);
+ pending.clear();
+ batchSerials.clear();
+ }
+ if (pullCount == 0) {
+ record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
+ "金蝶收款单未查询到数据");
+ } else if (totalCount == 0) {
record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
"金蝶收款单无有效数据");
- return 0;
}
+ log.info("金蝶收款流水同步完成,拉取收款单 {} 张,新增/更新 {} 条,跳过 {} 条,分 {} 批入库,耗时 {}ms",
+ pullCount, totalCount, skippedCount, batchNo, System.currentTimeMillis() - start);
+ return totalCount;
+ }
- // 批量查询已存在的记录(FOR UPDATE 防并发)
- log.info("收款流水同步开始批量查询已存在记录,查询条件:detailSerialNo IN {} 条", validRows.size());
- List existingEntities = baseMapper.selectList(
- Wrappers.lambdaQuery()
- .in(KingdeeReceiptFlow::getDetailSerialNo, validRows.keySet())
- .last("FOR UPDATE")
- );
- Map existingMap = existingEntities.stream()
- .collect(Collectors.toMap(KingdeeReceiptFlow::getDetailSerialNo, e -> e));
- log.info("收款流水批量查询完成:已存在 {} 条,待新增 {} 条",
- existingEntities.size(), validRows.size() - existingEntities.size());
-
- // 第二遍:构建新增和更新列表
- log.info("收款流水同步开始构建新增/更新列表");
- List toInsert = new ArrayList<>();
- List toUpdate = new ArrayList<>();
- int createdCount = 0;
- int updatedCount = 0;
-
- for (Map.Entry entry : validRows.entrySet()) {
- String detailSerialNo = entry.getKey();
- BfiRecBillRow row = entry.getValue();
-
- // 判断是新增还是更新
- KingdeeReceiptFlow existing = existingMap.get(detailSerialNo);
- boolean created = (existing == null);
-
- KingdeeReceiptFlow entity;
- if (created) {
- // 新增:初始化本地认领金额和状态
- entity = new KingdeeReceiptFlow();
- entity.setClaimedAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
- entity.setClaimStatus(UNCLAIMED);
- } else {
- // 更新:保留本地已认领金额和状态
- entity = existing;
+ /**
+ * 单张收款单展开为流水行并做批内校验(无分录/缺流水号/金额非法跳过,批内流水号重复拦截)
+ *
+ * @return 跳过条数
+ */
+ private int expandRecBillRows(BfiRecBillVO recBill, List pending, Set batchSerials) {
+ int skipped = 0;
+ List flowEntries = recBill.getInfoentry();
+ if (Func.isEmpty(flowEntries)) {
+ log.warn("收款流水同步跳过:收款单[{}]无流水分录", recBill.getBillno());
+ return 1;
+ }
+ for (BfiRecBillFlowVO flowEntry : flowEntries) {
+ // 明细流水号优先级:bei_detailid > bei_bankdetailno
+ String detailSerialNo = firstNonBlank(flowEntry.getBeiDetailid(), flowEntry.getBeiBankdetailno());
+ if (Func.isEmpty(detailSerialNo)) {
+ log.warn("收款流水同步跳过:收款单[{}]流水分录缺少明细流水号", recBill.getBillno());
+ skipped++;
+ continue;
}
-
- // 复制收款单数据到实体(不覆盖本地认领字段)
- copyRecBillRow(entity, row);
- // 根据本地累计认领金额与可认领上限重新计算认领状态
- entity.setClaimStatus(amountClaimStatus(money(entity.getClaimedAmount()),
- claimCeiling(entity)));
-
- if (created) {
- toInsert.add(entity);
- createdCount++;
- } else {
- toUpdate.add(entity);
- updatedCount++;
+ detailSerialNo = detailSerialNo.trim();
+ if (!batchSerials.add(detailSerialNo)) {
+ throw new ServiceException("明细流水号" + detailSerialNo + "重复");
}
+ // 收款金额取主单入账金额 inneraccountamount(金蝶口径,非流水分录 bei_creditamount)
+ BigDecimal receiptAmount = money(recBill.getInneraccountAmount());
+ if (receiptAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ log.warn("收款流水同步跳过:收款单[{}]收款金额(inneraccountamount)为空或小于0", recBill.getBillno());
+ skipped++;
+ continue;
+ }
+ pending.add(new BfiRecBillRow(recBill, detailSerialNo, receiptAmount));
}
- log.info("收款流水列表构建完成:待新增 {} 条,待更新 {} 条", createdCount, updatedCount);
+ return skipped;
+ }
- // 批量插入和更新
- if (!toInsert.isEmpty()) {
- log.info("收款流水同步开始批量插入 {} 条", toInsert.size());
- saveBatch(toInsert);
- log.info("收款流水批量插入完成");
- }
- if (!toUpdate.isEmpty()) {
- log.info("收款流水同步开始批量更新 {} 条", toUpdate.size());
- updateBatchById(toUpdate);
- log.info("收款流水批量更新完成");
- }
-
- // 记录操作日志(批量插入,避免逐行 N+1)
- log.info("收款流水同步开始记录操作日志,共 {} 条", toInsert.size() + toUpdate.size());
- List records = new ArrayList<>(toInsert.size() + toUpdate.size());
- for (KingdeeReceiptFlow entity : toInsert) {
- records.add(buildRecord(entity.getId(), null, "sync", "新增金蝶收款流水",
- entity.getClaimStatus(), entity.getClaimStatus(), BigDecimal.ZERO, entity.getDetailSerialNo()));
- }
- for (KingdeeReceiptFlow entity : toUpdate) {
- records.add(buildRecord(entity.getId(), null, "sync", "更新金蝶收款流水",
- entity.getClaimStatus(), entity.getClaimStatus(), BigDecimal.ZERO, entity.getDetailSerialNo()));
- }
- if (!records.isEmpty()) {
- Db.saveBatch(records);
- }
- log.info("收款流水操作日志记录完成");
-
- log.info("金蝶收款流水同步完成,拉取收款单 {} 张,新增 {} 条,更新 {} 条",
- recBills.size(), createdCount, updatedCount);
- return createdCount + updatedCount;
+ /**
+ * 单批入库(独立事务):批内 FOR UPDATE 防并发,按明细流水号 upsert,操作日志批量落库
+ *
+ * @param batchNo 批次号(日志用)
+ * @param rows 本批流水行
+ * @return 本批新增+更新条数
+ */
+ private int syncBatch(int batchNo, List rows) {
+ Integer count = transactionTemplate.execute(status -> {
+ // 批量查询已存在的记录(FOR UPDATE 防并发)
+ Map existingMap = baseMapper.selectList(
+ Wrappers.lambdaQuery()
+ .in(KingdeeReceiptFlow::getDetailSerialNo,
+ rows.stream().map(BfiRecBillRow::detailSerialNo).toList())
+ .last("FOR UPDATE"))
+ .stream().collect(Collectors.toMap(KingdeeReceiptFlow::getDetailSerialNo, e -> e));
+ List toInsert = new ArrayList<>();
+ List toUpdate = new ArrayList<>();
+ for (BfiRecBillRow row : rows) {
+ KingdeeReceiptFlow existing = existingMap.get(row.detailSerialNo());
+ boolean created = (existing == null);
+ KingdeeReceiptFlow entity;
+ if (created) {
+ // 新增:初始化本地认领金额和状态
+ entity = new KingdeeReceiptFlow();
+ entity.setClaimedAmount(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
+ entity.setClaimStatus(UNCLAIMED);
+ } else {
+ // 更新:保留本地已认领金额和状态
+ entity = existing;
+ }
+ // 复制收款单数据到实体(不覆盖本地认领字段)
+ copyRecBillRow(entity, row);
+ // 根据本地累计认领金额与可认领上限重新计算认领状态
+ entity.setClaimStatus(amountClaimStatus(money(entity.getClaimedAmount()),
+ claimCeiling(entity)));
+ if (created) {
+ toInsert.add(entity);
+ } else {
+ toUpdate.add(entity);
+ }
+ }
+ if (!toInsert.isEmpty()) {
+ saveBatch(toInsert);
+ }
+ if (!toUpdate.isEmpty()) {
+ updateBatchById(toUpdate);
+ }
+ // 记录操作日志(批量插入,避免逐行 N+1)
+ List