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..0f5a0a04 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,96 @@ 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;
+ private BigDecimal unmatchAmountRec;
/**
- * 手续费(A301-2 fee)
+ * 已匹配收款金额(A301-1 matchamountrec)
+ */
+ private BigDecimal matchAmountRec;
+ /**
+ * 已匹配付款金额(A301-1 matchamountpay)
+ */
+ private BigDecimal matchAmountPay;
+ /**
+ * 未匹配付款金额(A301-1 unmatchamountpay)
+ */
+ private BigDecimal unmatchAmountPay;
+ /**
+ * 已结算金额(A301-1 settleamount)
+ */
+ private BigDecimal settleAmount;
+ /**
+ * 未结算金额(A301-1 unsettleamount)
+ */
+ private BigDecimal unsettleAmount;
+ /**
+ * 手续费(A301-1 fee)
*/
private BigDecimal fee;
/**
- * 收款人(A301-2 recpayee)
+ * 收款人(A301-1 收款公司名称 org_name)
*/
private String payeeName;
/**
- * 收款通知单号(A301-2 billno)
+ * 收款人(公司).编码(A301-1 org_number)
+ */
+ private String orgNumber;
+ /**
+ * 核算组织.编码(A301-1 openorg_number)
+ */
+ private String openOrgNumber;
+ /**
+ * 金蝶收款单号(A301-1 billno)
*/
private String receiptNoticeNo;
/**
- * 付款人名称(A301-2 recviewpayer)
+ * 付款人名称(A301-1 payername)
*/
private String payerName;
/**
- * 收款金额(A301-2 reamount)
+ * 收款金额(A301-1 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,幂等键)
+ * 明细流水号(收款单号 billno,幂等键;C104-2 按此定位合规侧收款流水)
*/
private String detailSerialNo;
/**
@@ -125,27 +153,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-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/config/BfiProperties.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/config/BfiProperties.java
index 5fe089fe..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
@@ -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(近半年)
+ * 同步业务日期窗口天数,默认 60
*/
- private int months = 6;
-
- /**
- * 认领人(claimusers):配置后公告查询固定使用该值,不再按当前登录用户姓名查询
- */
- private String claimusers;
+ private int days = 60;
}
/**
@@ -257,6 +247,11 @@ public class BfiProperties {
* 回写 single-flight 锁租约秒数,须大于最坏一轮耗时(分批远程查询 + 逐单回写)
*/
private long lockLeaseSeconds = 1800;
+
+ /**
+ * A103-4 付款处理查询业务日期窗口天数,默认 60
+ */
+ private int queryWindowDays = 60;
}
/**
@@ -271,23 +266,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/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/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/mapper/KingdeeReceiptFlowMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java
index 7d69772c..0e6b654d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java
@@ -27,8 +27,11 @@ package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
+import java.util.List;
+
/**
* 金蝶收款流水镜像 Mapper 接口
*
@@ -36,4 +39,13 @@ import org.springblade.transport.pojo.entity.KingdeeReceiptFlow;
*/
@Mapper
public interface KingdeeReceiptFlowMapper extends BaseMapper {
+
+ /**
+ * 组织管理(blade_dept)中的平台公司金蝶组织编码列表(是否平台公司=是 且 已维护金蝶编码)
+ *
+ * @return 平台公司金蝶组织编码
+ */
+ @Select("SELECT kingdee_dept_code FROM blade_dept WHERE is_platform_company = 1 "
+ + "AND kingdee_dept_code IS NOT NULL AND kingdee_dept_code != '' AND is_deleted = 0")
+ List selectPlatformOrgNumbers();
}
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/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/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/InvoiceReceiptServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java
index 33a64fbd..de5b6cbe 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceReceiptServiceImpl.java
@@ -50,6 +50,7 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiInvoicePoolVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.service.IBfiService;
+import org.springblade.thirdparty.bfi.util.BfiUtil;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.constant.KingdeeAttachmentConstant;
import org.springblade.transport.config.BfiProperties;
@@ -1304,11 +1305,11 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl2.5 万,从中点二分为两片递归续扫(递归深度受时间粒度自然收敛,
- * 单片 500 条以内 1 页即短页终止)
+ * 同步单个票种的时间窗口:翻页与触顶二分续扫统一交给 {@link BfiUtil#forEachWindowSlice},
+ * 页级客商单位过滤、镜像批量 upsert、已登记票跟随补绑在页回调中完成
*
- * @param sliceStart 切片起点(含)
- * @param sliceEnd 切片终点(不含,避免与下一片边界重复拉取)
- * @param customerTaxNos 客商发票信息税号集,null 表示客商单位过滤关闭(整轮共享)
- * @param stats 整轮统计累加器
+ * @param windowStart 窗口起点(含)
+ * @param windowEnd 窗口终点(含)
+ * @param invoiceType 发票种类编码,null 不限
+ * @param customerTaxNos 客商发票信息税号集,null 表示客商单位过滤关闭(整轮共享)
+ * @param stats 整轮统计累加器
*/
- private void syncWindowSlice(LocalDateTime sliceStart, LocalDateTime sliceEnd, String invoiceType,
+ private void syncPoolWindow(LocalDateTime windowStart, LocalDateTime windowEnd, String invoiceType,
Set customerTaxNos, SyncStats stats) {
- // 二分收敛保护:切片粒度细到 1 秒仍触顶说明远端异常(同秒 >2.5 万条不可能),不再二分直接记漏
- if (!sliceStart.isBefore(sliceEnd)) {
- log.error("金蝶全票池同步切片区间为空({} ~ {}),远端 updateTime 粒度异常,放弃该切片", sliceStart, sliceEnd);
- return;
- }
- boolean truncated = false;
- for (int pageNo = 1; pageNo <= MAX_POOL_PAGES; pageNo++) {
- List rows = this.pullPoolWindowPage(sliceStart, sliceEnd, invoiceType, pageNo);
- if (rows.isEmpty()) {
- break;
- }
- stats.pageCount++;
- stats.pulledCount += rows.size();
- // 客商单位过滤:销/购方税号未同时登记在客商发票信息中的发票不入镜像;
- // 短页判定用原始条数,过滤后的空页不能提前终止翻页
+ stats.sliceCount += BfiUtil.forEachWindowSlice("金蝶全票池同步", windowStart, windowEnd,
+ POOL_PAGE_SIZE, MAX_POOL_PAGES,
+ (sliceStart, sliceEnd, pageNo) -> this.pullPoolWindowPage(sliceStart, sliceEnd, invoiceType, pageNo),
+ rows -> this.handlePoolPage(rows, customerTaxNos, stats));
+ }
+
+ /**
+ * 票池同步单页处理:客商单位过滤 + 镜像批量 upsert + 已登记票跟随补绑。
+ * 客商过滤只影响入镜像行数,翻页节奏由 BfiUtil 按原始行数判断,不受过滤影响
+ *
+ * @param rows 本页拉取的票池发票(原始行)
+ * @param customerTaxNos 客商发票信息税号集,null 表示客商单位过滤关闭
+ * @param stats 整轮统计累加器
+ */
+ private void handlePoolPage(List rows, Set customerTaxNos, SyncStats stats) {
+ stats.pageCount++;
+ stats.pulledCount += rows.size();
+ // 客商单位过滤:销/购方税号未同时登记在客商发票信息中的发票不入镜像
+ if (customerTaxNos != null) {
int rawPageSize = rows.size();
- if (customerTaxNos != null) {
- rows = rows.stream().filter(row -> this.bothUnitsRegistered(row, customerTaxNos)).toList();
- stats.customerFiltered += rawPageSize - rows.size();
- }
- BatchUpsertResult upsertResult = this.batchUpsertInvoicePool(rows);
- stats.created += upsertResult.created();
- stats.updated += upsertResult.updated();
- stats.mirrorUnchanged += upsertResult.unchanged();
- // 已登记票跟随补绑:同页发票号码批量定位登记,绑定关系变更页级事务批量落库
- if (!upsertResult.mirrors().isEmpty()) {
- RefreshResult refreshResult = this.refreshBoundReceipts(stats.pageCount, upsertResult.mirrors());
- stats.bound += refreshResult.bound();
- stats.unchanged += refreshResult.unchanged();
- stats.failed += refreshResult.failed();
- }
- // 远程短页:该片到底,正常终止(按原始拉取条数判断,客商过滤不改变翻页节奏)
- if (rawPageSize < POOL_PAGE_SIZE) {
- stats.sliceCount++;
- return;
- }
- truncated = true;
+ rows = rows.stream().filter(row -> this.bothUnitsRegistered(row, customerTaxNos)).toList();
+ stats.customerFiltered += rawPageSize - rows.size();
}
- // 触顶:该切片数据超过单查询窗口上限,从中点二分续扫(updateTime 落在中点的行归左片 [start, mid),
- // 右片 [mid, end) 不含 mid,同 updateTime 行整体归左片,不重不漏)
- if (truncated) {
- LocalDateTime mid = sliceStart.plus(java.time.Duration.between(sliceStart, sliceEnd).dividedBy(2));
- if (!mid.isAfter(sliceStart)) {
- // 切片已细到不足 1 秒仍满页:理论不可能,防御死递归
- log.error("金蝶全票池同步切片 {} ~ {} 已达最小粒度仍满页(>{} 条),该时间片数据异常,跳过",
- sliceStart, sliceEnd, MAX_POOL_PAGES * POOL_PAGE_SIZE);
- return;
- }
- log.warn("金蝶全票池同步切片 {} ~ {} 触达翻页保护 {} 页({} 条),二分续扫 [{} ~ {}) + [{} ~ {})",
- sliceStart, sliceEnd, MAX_POOL_PAGES, MAX_POOL_PAGES * POOL_PAGE_SIZE, sliceStart, mid, mid, sliceEnd);
- this.syncWindowSlice(sliceStart, mid, invoiceType, customerTaxNos, stats);
- this.syncWindowSlice(mid, sliceEnd, invoiceType, customerTaxNos, stats);
- return;
+ BatchUpsertResult upsertResult = this.batchUpsertInvoicePool(rows);
+ stats.created += upsertResult.created();
+ stats.updated += upsertResult.updated();
+ stats.mirrorUnchanged += upsertResult.unchanged();
+ // 已登记票跟随补绑:同页发票号码批量定位登记,绑定关系变更页级事务批量落库
+ if (!upsertResult.mirrors().isEmpty()) {
+ RefreshResult refreshResult = this.refreshBoundReceipts(stats.pageCount, upsertResult.mirrors());
+ stats.bound += refreshResult.bound();
+ stats.unchanged += refreshResult.unchanged();
+ stats.failed += refreshResult.failed();
}
- stats.sliceCount++;
}
/**
@@ -1523,7 +1503,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl pullPoolWindowPage(LocalDateTime windowStart, LocalDateTime windowEnd,
+ private BfiPageVO pullPoolWindowPage(LocalDateTime windowStart, LocalDateTime windowEnd,
String invoiceType, int pageNo) {
BfiInvoicePoolQueryDataDTO data = new BfiInvoicePoolQueryDataDTO();
data.setPageNo(pageNo);
@@ -1536,7 +1516,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl poolPage = bfiService.queryInvoicePool(param);
log.info("金蝶全票池同步 第{}页拉取 {} 条", pageNo, poolPage.getRows() == null ? 0 : poolPage.getRows().size());
- return poolPage.getRows() == null ? List.of() : poolPage.getRows();
+ return poolPage;
}
/**
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 d03f5ec9..b80607b9 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/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..b6ebd497 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 allocationMap = allocationMap(request.getSettlements());
// 结算单id列表(升序)
@@ -347,10 +344,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl orgNumbers = baseMapper.selectPlatformOrgNumbers();
+ if (Func.isEmpty(orgNumbers)) {
+ throw new ServiceException("组织管理中未维护平台公司金蝶编码,无法同步金蝶收款流水,请先检查组织数据");
}
- List announces = queryClaimAnnounces(claimusers);
-
- if (announces.isEmpty()) {
- record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
- "金蝶认领公告未查询到数据");
- return 0;
+ log.info("收款流水同步按组织过滤,组织编码:{}", orgNumbers);
+ RecBillSyncState state = new RecBillSyncState();
+ state.sliceCount += BfiUtil.forEachWindowSlice("收款流水同步", windowStart, windowEnd,
+ SYNC_PAGE_SIZE, SYNC_MAX_PAGES,
+ (sliceStart, sliceEnd, pageNo) -> this.pullRecBillPage(sliceStart, sliceEnd, orgNumbers, pageNo),
+ rows -> this.handleRecBillPage(rows, state));
+ // 尾批不足 SYNC_BATCH_SIZE 的剩余入库
+ if (!state.pending.isEmpty()) {
+ state.batchNo++;
+ state.totalCount += this.syncBatch(state.batchNo, state.pending);
+ state.pending.clear();
+ state.batchSerials.clear();
}
+ if (state.pullCount == 0) {
+ this.record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
+ "金蝶收款单未查询到数据");
+ } else if (state.totalCount == 0) {
+ this.record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
+ "金蝶收款单无有效数据");
+ }
+ log.info("金蝶收款流水同步完成,翻页{}页切片{}片,拉取收款单 {} 张,新增/更新 {} 条,跳过 {} 条,"
+ + "分 {} 批入库,耗时 {}ms",
+ state.pageCount, state.sliceCount, state.pullCount, state.totalCount, state.skippedCount,
+ state.batchNo, System.currentTimeMillis() - start);
+ return state.totalCount;
+ }
- // 同步口径:A301-2 按参与认领用户(当前登录用户真实姓名)拉取全量认领公告,
- // 以金蝶明细流水号为幂等键 upsert 收款流水表;本地已认领金额/状态(claimedAmount/claimStatus)
- // 由认领、作废驱动,不被公告顺带覆盖,公告侧状态单独存 kingdee_claim_status/unclaimed_amount。
+ /**
+ * A301-1 按业务日期时间片分页拉取(组织编码过滤整轮共享,切片起止由 BfiUtil 二分传入)
+ *
+ * @param sliceStart 切片起点(含)
+ * @param sliceEnd 切片终点(含)
+ * @param orgNumbers 平台公司组织编码集
+ * @param pageNo 页码,从 1 开始
+ * @return 分页结果
+ */
+ private BfiPageVO pullRecBillPage(LocalDateTime sliceStart, LocalDateTime sliceEnd,
+ List orgNumbers, int pageNo) {
+ BfiRecBillQueryDTO queryDTO = new BfiRecBillQueryDTO();
+ queryDTO.setPageNo(pageNo);
+ queryDTO.setPageSize(SYNC_PAGE_SIZE);
+ queryDTO.setBeginBizdate(sliceStart.format(BFI_DATE_TIME_FORMATTER));
+ queryDTO.setEndBizdate(sliceEnd.format(BFI_DATE_TIME_FORMATTER));
+ queryDTO.setOrgNumber(orgNumbers);
+ return bfiService.queryRecBill(queryDTO);
+ }
- // 第一遍:数据校验与过滤,收集有效的 detailSerialNo
- log.info("收款流水同步开始数据校验,共 {} 条认领公告", announces.size());
- Map validAnnounces = new LinkedHashMap<>();
- int skippedCount = 0;
- for (BfiClaimAnnounceVO announce : announces) {
- // 明细流水号优先级:detailid > tradedetailno > bankdetailno
- String detailSerialNo = firstNonBlank(announce.getDetailid(),
- announce.getTradedetailno(), announce.getBankdetailno());
+ /**
+ * A301-1 单页数据处理:收款单主单粒度逐行校验入缓冲,累计满一批即独立事务入库。
+ * 切片边界同秒的收款单可能在相邻两片各拉一次,按批内收款单号去重幂等跳过,不重不漏
+ *
+ * @param rows 本页拉取的收款单
+ * @param state 整轮同步状态(待入库缓冲与统计累加)
+ */
+ private void handleRecBillPage(List rows, RecBillSyncState state) {
+ state.pageCount++;
+ state.pullCount += rows.size();
+ for (BfiRecBillVO recBill : rows) {
+ // 主单粒度:一张收款单一条流水,不展开流水分录;明细流水号取收款单号(幂等键,C104-2 按此定位合规流水)
+ String detailSerialNo = trimToEmpty(recBill.getBillno());
if (Func.isEmpty(detailSerialNo)) {
- log.warn("收款流水同步跳过:认领公告[{}]缺少明细流水号", announce.getBillno());
- skippedCount++;
+ log.warn("收款流水同步跳过:收款单缺少单号");
+ state.skippedCount++;
continue;
}
- detailSerialNo = detailSerialNo.trim();
- if (validAnnounces.containsKey(detailSerialNo)) {
- throw new ServiceException("明细流水号" + detailSerialNo + "重复");
+ if (!state.batchSerials.add(detailSerialNo)) {
+ // 切片边界重复拉取或同批脏数据重复单号,幂等跳过(upsert 本就按单号幂等)
+ log.warn("收款流水同步跳过:收款单[{}]本批已拉取,跳过重复处理", detailSerialNo);
+ state.skippedCount++;
+ continue;
}
- BigDecimal receiptAmount = money(announce.getReamount());
+ // 收款金额取主单 actrecamt(金蝶收款金额口径)
+ BigDecimal receiptAmount = money(recBill.getActrecamt());
if (receiptAmount.compareTo(BigDecimal.ZERO) <= 0) {
- log.warn("收款流水同步跳过:认领公告[{}]收款金额为空或小于0", announce.getBillno());
- skippedCount++;
+ // log.warn("收款流水同步跳过:收款单[{}]收款金额(actrecamt)为空或小于0", recBill.getBillno());
+ state.skippedCount++;
continue;
}
- validAnnounces.put(detailSerialNo, announce);
- }
- log.info("收款流水数据校验完成:有效 {} 条,跳过 {} 条", validAnnounces.size(), skippedCount);
-
- if (validAnnounces.isEmpty()) {
- log.info("收款流水同步无有效数据,记录操作日志后返回");
- record(null, null, "sync", "手动同步流水", null, null, BigDecimal.ZERO,
- "金蝶认领公告无有效数据");
- return 0;
- }
-
- // 批量查询已存在的记录(FOR UPDATE 防并发)
- log.info("收款流水同步开始批量查询已存在记录,查询条件:detailSerialNo IN {} 条", validAnnounces.size());
- List existingEntities = baseMapper.selectList(
- Wrappers.lambdaQuery()
- .in(KingdeeReceiptFlow::getDetailSerialNo, validAnnounces.keySet())
- .last("FOR UPDATE")
- );
- Map existingMap = existingEntities.stream()
- .collect(Collectors.toMap(KingdeeReceiptFlow::getDetailSerialNo, e -> e));
- log.info("收款流水批量查询完成:已存在 {} 条,待新增 {} 条",
- existingEntities.size(), validAnnounces.size() - existingEntities.size());
-
- // 第二遍:构建新增和更新列表
- log.info("收款流水同步开始构建新增/更新列表");
- List toInsert = new ArrayList<>();
- List toUpdate = new ArrayList<>();
- int createdCount = 0;
- int updatedCount = 0;
-
- for (Map.Entry entry : validAnnounces.entrySet()) {
- String detailSerialNo = entry.getKey();
- BfiClaimAnnounceVO announce = entry.getValue();
- BigDecimal receiptAmount = money(announce.getReamount());
-
- // 判断是新增还是更新
- 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;
- }
-
- // 复制公告数据到实体(不覆盖本地认领字段)
- copyAnnounceRow(entity, announce, detailSerialNo, receiptAmount);
- // 根据本地累计认领金额与可认领上限重新计算认领状态
- entity.setClaimStatus(amountClaimStatus(money(entity.getClaimedAmount()),
- claimCeiling(entity)));
-
- if (created) {
- toInsert.add(entity);
- createdCount++;
- } else {
- toUpdate.add(entity);
- updatedCount++;
+ state.pending.add(new BfiRecBillRow(recBill, detailSerialNo, receiptAmount));
+ if (state.pending.size() >= SYNC_BATCH_SIZE) {
+ state.batchNo++;
+ state.totalCount += this.syncBatch(state.batchNo, state.pending);
+ state.pending.clear();
+ state.batchSerials.clear();
}
}
- log.info("收款流水列表构建完成:待新增 {} 条,待更新 {} 条", createdCount, updatedCount);
+ }
- // 批量插入和更新
- if (!toInsert.isEmpty()) {
- log.info("收款流水同步开始批量插入 {} 条", toInsert.size());
- saveBatch(toInsert);
- log.info("收款流水批量插入完成");
- }
- if (!toUpdate.isEmpty()) {
- log.info("收款流水同步开始批量更新 {} 条", toUpdate.size());
- updateBatchById(toUpdate);
- log.info("收款流水批量更新完成");
- }
+ /**
+ * 单批入库(独立事务):批内 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 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);
+ }
+ return toInsert.size() + toUpdate.size();
+ });
+ log.info("收款流水同步第 {} 批入库完成,本批 {} 条", batchNo, count == null ? 0 : count);
+ return count == null ? 0 : count;
+ }
- // 记录操作日志(批量插入,避免逐行 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("收款流水操作日志记录完成");
+ /**
+ * A301-1 收款单查询返回的收款单 + 流水分录展开行(同步过程传递用)
+ */
+ private record BfiRecBillRow(BfiRecBillVO recBill,
+ String detailSerialNo, BigDecimal receiptAmount) {
+ }
- log.info("金蝶收款流水同步完成 claimusers:{},拉取 {} 条,新增 {} 条,更新 {} 条",
- claimusers, announces.size(), createdCount, updatedCount);
- return createdCount + updatedCount;
+ /**
+ * 收款流水整轮同步状态:待入库缓冲与统计累加(各切片共享,结束进日志摘要)
+ */
+ private static final class RecBillSyncState {
+ /**
+ * 待入库缓冲:累计满 SYNC_BATCH_SIZE 即入库一批并清空
+ */
+ final List pending = new ArrayList<>();
+ /**
+ * 批内收款单号去重(含切片边界重复拉取)
+ */
+ final Set batchSerials = new HashSet<>();
+ /**
+ * A301-1 原始拉取收款单张数
+ */
+ int pullCount;
+ /**
+ * 跳过条数(缺单号/金额异常/重复拉取)
+ */
+ int skippedCount;
+ /**
+ * 新增+更新条数
+ */
+ int totalCount;
+ /**
+ * 已入库批次数
+ */
+ int batchNo;
+ /**
+ * 实际处理的翻页总页数
+ */
+ int pageCount;
+ /**
+ * 实际处理的切片数
+ */
+ int sliceCount;
}
private Map candidateRow(FormalSettlement settlement, BigDecimal claimedAmount) {
@@ -724,19 +811,12 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl
- * 仅认领完成且未推送过的流水可推送;推送前以当前用户姓名重查 A301-2 最新公告对账。
- * 推送失败不抛异常跨事务边界(保证失败状态落库),原因随结果对象返回。
+ * A301-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);
- 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));
- queryDTO.setPageSize(SYNC_PAGE_SIZE);
- List announces = BfiUtil.queryAllPages(pageNo -> {
- queryDTO.setPageNo(pageNo);
- return bfiService.queryClaimAnnounce(queryDTO);
- });
- log.info("调用BFI接口 收款认领公告查询结束 claimusers:{},耗时:{}ms,拉取 {} 条",
- effectiveClaimusers, System.currentTimeMillis() - start, announces.size());
- return announces;
- }
-
- /**
- * A301-2 认领公告 → 收款流水表字段拷贝(仅覆盖金蝶侧来源字段,不覆盖本地认领金额与状态)
- */
- 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.setOrgNumber(trimToNull(source.getOrgNumber()));
+ // 核算组织.编码
+ target.setOpenOrgNumber(trimToNull(source.getOpenorgNumber()));
+ // 收款金额(主单 actrecamt)
+ 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.setUnmatchAmountRec(money(source.getUnmatchamountrec()));
+ // 已匹配收款金额 / 已匹配付款金额 / 未匹配付款金额
+ target.setMatchAmountRec(money(source.getMatchamountrec()));
+ target.setMatchAmountPay(money(source.getMatchamountpay()));
+ target.setUnmatchAmountPay(money(source.getUnmatchamountpay()));
+ // 已结算金额 / 未结算金额
+ target.setSettleAmount(money(source.getSettleamount()));
+ target.setUnsettleAmount(money(source.getUnsettleamount()));
// 手续费
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());
}
- private String firstNonBlank(String... values) {
- for (String value : values) {
- if (Func.isNotEmpty(value)) {
- return value.trim();
- }
+ /**
+ * 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;
}
- return null;
+ 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;
}
private LocalDateTime parseDateTime(String value) {
@@ -1237,13 +984,6 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl
- * 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());
}
/**
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 0b486e78..3fe88486 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,7 @@ 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)由本组件统一回写。
@@ -78,7 +78,7 @@ public class HcsPaymentSyncSupport {
private final ApplicationEventPublisher eventPublisher;
/**
- * 推送付款申请到 HCS:特批(need 时发起 C106-3 后挂起)→ 履约计划重推(C103-9 款项金额累加行)→ 履约记录(金蝶实际付款后新建/更新)→ 回填整体同步状态
+ * 推送付款申请到 HCS:特批(need 时发起 C106-3 后挂起)→ 金蝶实际付款门禁(未付款时整体跳过,付款回写后重新置待同步触发推送)→ 履约计划重推(C103-9 款项金额累加行)→ 履约记录(C108-1 新建/C108-2 更新)→ 回填整体同步状态
*
* @param payment 付款申请单(approvalStatus=approved)
* @return 推送结果描述
@@ -104,8 +104,16 @@ public class HcsPaymentSyncSupport {
markSynced(payment, message);
return message;
}
- // ② 履约计划重推(C103-9 行级更新):付款审批通过累加款项金额置 row_op=update 的计划行,
- // 凭已存合规阶段ID 重推;独立于履约记录开关,推送失败抛出由付款链置 failed 重试兜底
+ // ② 金蝶实际付款门禁:履约计划与履约记录均待金蝶实际付款后推送(需求口径:金蝶付款成功后再推给合同系统)——
+ // 审批通过仅本地累加计划金额置待同步,此处不重推;未付款时整体照常回写,
+ // 付款回写推进(applyPayApplyResult)重新置待同步后由扫描任务触发本推送
+ if (payment.getPaidAmount() == null || payment.getPaidAmount().signum() <= 0) {
+ String message = "金蝶未实际付款,待付款后推送履约计划与履约记录";
+ markSynced(payment, message);
+ return message;
+ }
+ // ③ 履约计划重推(C103-9 行级更新):付款审批通过累加款项金额置 row_op=update 的计划行,
+ // 凭已存合规阶段ID 重推;推送失败抛出由付款链置 failed 重试兜底
if (hcsProperties.getExeStage().isSyncEnabled()) {
String stageResult = this.pushExeStageUpdate(payment);
if (StringUtil.isNotBlank(stageResult)) {
@@ -115,24 +123,11 @@ public class HcsPaymentSyncSupport {
result.append(stageResult);
}
}
- // ③ 履约记录:金蝶实际付款后推送(需求口径:金蝶实际付款了再推给合同系统)——
- // 首次回写实际付款走 C108-1 新建,分期付款推进(paidAmount 变化置待同步)走 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(";");
- }
result.append(hcsPerfRecordSyncSupport.pushPerfRecord(payment));
markSynced(payment, result.toString());
return result.toString();
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/ReceiptClaimPushTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/ReceiptClaimPushTask.java
deleted file mode 100644
index ec681cf7..00000000
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/ReceiptClaimPushTask.java
+++ /dev/null
@@ -1,67 +0,0 @@
-/**
- * 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
- * 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-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java
index e0e59387..964b719c 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ReceiptFlowWrapper.java
@@ -70,15 +70,10 @@ public class ReceiptFlowWrapper extends BaseEntityWrapper "处理失败";
default -> "未推送";
});
- // 剩余可认领金额 = 可认领上限 − 累计认领金额(上限取公告未认领与收款金额较小值)
- BigDecimal unclaimedAmount = money(entity.getUnclaimedAmount());
+ // 剩余可认领金额 = 收款金额 − 累计认领金额(认领只登记本地,额度由本地累计认领控制)
BigDecimal receiptAmount = money(entity.getReceiptAmount());
- BigDecimal claimCeiling = unclaimedAmount;
- if (receiptAmount.compareTo(BigDecimal.ZERO) > 0 && unclaimedAmount.compareTo(BigDecimal.ZERO) > 0) {
- claimCeiling = unclaimedAmount.min(receiptAmount);
- }
BigDecimal claimedAmount = money(entity.getClaimedAmount());
- vo.setRemainingAmount(claimCeiling.subtract(claimedAmount).max(BigDecimal.ZERO));
+ vo.setRemainingAmount(receiptAmount.subtract(claimedAmount).max(BigDecimal.ZERO));
vo.setCreateUserName(UserCache.getUserRealName(entity.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(entity.getUpdateUser()));
return vo;
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 3044f5b0..dd983387 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
@@ -213,6 +213,9 @@ public interface IBfiClient {
/**
* A103-4 付款处理查询
+ *
+ * 注意:开始业务日期的请求参数名以金蝶契约为准,为 {@code beigin_bizdate}(并非 begin_bizdate),
+ * 传错会被忽略并返回「参数【beigin_bizdate】必填」。
*
* @param accessToken 访问令牌,通过 getToken 获取
* @param pageNo 页码,从1开始
@@ -227,7 +230,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;
/**
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..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
@@ -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,窗口天数由调用方控制;
+ * 传纯日期 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 3680e198..e0c79af3 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
@@ -287,7 +287,7 @@ public interface IBfiService {
/**
* A301-1 收款单查询(收款处理查询)
*
- * @param param 查询参数,业务日期范围不可超过1个月
+ * @param param 查询参数,业务日期格式 yyyy-MM-dd HH:mm:ss,窗口天数由调用方控制
* @return 收款处理分页数据
*/
BfiPageVO queryRecBill(BfiRecBillQueryDTO param);
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..685da253 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,15 +25,19 @@
*/
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;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
+import java.time.Duration;
+import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
+import java.util.function.Consumer;
import java.util.function.IntFunction;
/**
@@ -44,6 +48,7 @@ import java.util.function.IntFunction;
*
* @author Chill
*/
+@Slf4j
public class BfiUtil {
/**
@@ -56,6 +61,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 +149,163 @@ 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;
+ }
+
+ /**
+ * BFI 时间窗口二分切片续扫翻页,逐页回调处理,窗口数据超过单查询翻页上限时尾部不漏同步
+ *
+ * 适用按时间窗口增量同步且数据量可能超过单查询翻页上限的场景
+ * (A801-1 票池同步按 updateTime、A301-1 收款单同步按业务日期):
+ *
{@code
+ * int sliceCount = BfiUtil.forEachWindowSlice("金蝶全票池同步", windowStart, windowEnd,
+ * PAGE_SIZE, MAX_PAGES,
+ * (sliceStart, sliceEnd, pageNo) -> {
+ * data.setUpdateTime(sliceStart.format(FORMATTER));
+ * data.setUpdateTimeEnd(sliceEnd.format(FORMATTER));
+ * data.setPageNo(pageNo);
+ * data.setPageSize(PAGE_SIZE);
+ * return bfiService.queryInvoicePool(param);
+ * },
+ * rows -> batchUpsert(rows));
+ * }
+ *
+ * 单片独立翻页(最多 maxPages 页):空页或末页不满一页即该片到底;触达页数保护说明该时间片
+ * 数据超过单查询窗口上限(maxPages × pageSize),从中点二分为 [start, mid] + [mid, end] 两片递归续扫,
+ * 总覆盖不受单查询上限约束。切片边界同刻度的数据可能在相邻两片各拉一次,pageHandler 必须幂等;
+ * 切片细到不足 1 秒仍满页视为远端异常,告警跳过防御死递归。
+ * 终止只认空页与短页,不依赖 lastPage(末页不满一页天然触发短页终止,lastPage 异常提前终止反而漏拉)
+ *
+ * @param bizName 业务名称(日志前缀)
+ * @param windowStart 窗口起点(含)
+ * @param windowEnd 窗口终点(含)
+ * @param pageSize 分页大小(须与 pageFetcher 内实际设置的页大小一致)
+ * @param maxPages 单片最大页数保护
+ * @param pageFetcher 时间片起止 + 页码 → 分页结果,lambda 内负责设置时间窗口与页码并调用接口
+ * @param pageHandler 单页数据行处理(过滤、upsert、统计等),必须幂等
+ * @param 数据行类型
+ * @return 实际处理完的切片数(含空片,异常跳过的不计)
+ */
+ public static int forEachWindowSlice(String bizName, LocalDateTime windowStart, LocalDateTime windowEnd,
+ int pageSize, int maxPages, WindowPageFetcher pageFetcher, Consumer> pageHandler) {
+ // 二分收敛保护:切片区间为空说明时间粒度异常,放弃该片
+ if (!windowStart.isBefore(windowEnd)) {
+ log.error("{}切片区间为空({} ~ {}),时间粒度异常,放弃该切片", bizName, windowStart, windowEnd);
+ return 0;
+ }
+ for (int pageNo = 1; pageNo <= maxPages; pageNo++) {
+ BfiPageVO page = pageFetcher.fetch(windowStart, windowEnd, pageNo);
+ List rows = page == null || page.getRows() == null ? List.of() : page.getRows();
+ if (rows.isEmpty()) {
+ return 1;
+ }
+ pageHandler.accept(rows);
+ // 短页:该片到底,正常终止(不依赖 lastPage,末页不满一页天然触发)
+ if (rows.size() < pageSize) {
+ return 1;
+ }
+ }
+ // 触顶:该时间片数据超过单查询窗口上限,从中点二分续扫(边界同刻度数据由 pageHandler 幂等兜底)
+ LocalDateTime mid = windowStart.plus(Duration.between(windowStart, windowEnd).dividedBy(2));
+ if (!mid.isAfter(windowStart)) {
+ log.error("{}切片 {} ~ {} 已达最小粒度仍满页(>{} 条),该时间片数据异常,跳过",
+ bizName, windowStart, windowEnd, (long) maxPages * pageSize);
+ return 0;
+ }
+ log.warn("{}切片 {} ~ {} 触达单片翻页保护 {} 页(>{} 条),二分续扫 [{} ~ {}] + [{} ~ {}]",
+ bizName, windowStart, windowEnd, maxPages, (long) maxPages * pageSize, windowStart, mid, mid, windowEnd);
+ return forEachWindowSlice(bizName, windowStart, mid, pageSize, maxPages, pageFetcher, pageHandler)
+ + forEachWindowSlice(bizName, mid, windowEnd, pageSize, maxPages, pageFetcher, pageHandler);
+ }
+
+ /**
+ * 时间窗口切片翻页取数函数:按切片起止时间 + 页码拉取一页数据
+ *
+ * @param 数据行类型
+ */
+ @FunctionalInterface
+ public interface WindowPageFetcher {
+
+ /**
+ * 拉取单个时间片的一页数据
+ *
+ * @param sliceStart 切片起点(含)
+ * @param sliceEnd 切片终点(含)
+ * @param pageNo 页码,从 1 开始
+ * @return 分页结果
+ */
+ BfiPageVO fetch(LocalDateTime sliceStart, LocalDateTime sliceEnd, int pageNo);
+ }
}
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)
*
diff --git a/doc/docs/hcs合同/2026-10-29-履约计划与履约记录四场景业务说明(业务人员版).md b/doc/docs/hcs合同/2026-10-29-履约计划与履约记录四场景业务说明(业务人员版).md
new file mode 100644
index 00000000..5fba57d5
--- /dev/null
+++ b/doc/docs/hcs合同/2026-10-29-履约计划与履约记录四场景业务说明(业务人员版).md
@@ -0,0 +1,289 @@
+# 履约计划与履约记录业务说明(付款 / 开票 / 收票 / 收款)
+
+> 适用对象:合同、结算、付款、开票、收票相关业务人员
+> 更新日期:2026-10-29
+> 用途:说明 TMS 与合同合规系统之间「履约计划」「履约记录」是怎么产生、什么时候产生、先后顺序和注意事项。
+
+---
+
+## 一、先看懂三个概念
+
+| 概念 | 通俗理解 | 举例 |
+| --- | --- | --- |
+| **合同** | 业务发生的前提 | 与承运商签的运输合同、与客户签的运输服务合同 |
+| **履约计划** | 合同约定的**收付款计划台账**(打算收/付多少钱、什么时候收/付) | 合同约定进度款 100 万,分几次结算 |
+| **履约记录** | 计划实际执行的**流水**(真正付了、收了、开了票、收了票) | 本次实际付款 30 万、本次认领收款 20 万 |
+
+一句话记住:**计划是“打算”,记录是“实际发生”**。先有计划,才有记录;记录必须挂在某一条计划下面。
+
+---
+
+## 二、总体流程
+
+```
+合同审批通过
+ ↓
+生成 / 确认履约计划(付款计划、收款计划)
+ ↓
+合同与计划推送到合规系统(合规侧生成对应的计划编号)
+ ↓
+业务实际发生:付款 / 收款 / 开票 / 收票
+ ↓
+系统自动生成履约记录,回传合规系统
+```
+
+关键顺序:
+
+1. **合同先推合规**;
+2. **履约计划再推合规**;
+3. **计划推送成功后,履约记录才能生成**。
+
+如果合同还没推合规,或者计划还没推成功,记录会暂时生成不了,等前面的步骤完成后自动补上(收款场景除外,见第六节)。
+
+---
+
+## 三、履约计划是怎么来的
+
+### 3.1 计划从哪来
+
+| 来源 | 说明 |
+| --- | --- |
+| 合同里维护「履约规则」 | 业务人员在合同页签里填写付款/收款计划行 |
+| 合同审批通过自动生成 | 合同没有维护计划行时,系统自动补一条默认计划:承运商合同生成付款计划,客户合同生成收款计划 |
+| 业务发生自动累加 | 付款审批通过、收款认领后,计划金额自动累加 |
+
+### 3.2 计划金额什么时候变
+
+| 业务动作 | 计划金额变化 |
+| --- | --- |
+| 付款申请**审批通过** | 对应付款计划的金额 += 本次申请金额(本地记账,**金蝶实际付款后**才同步到合规) |
+| 收款流水**认领完成** | 对应收款计划的金额 += 本次认领分摊金额 |
+
+请注意:
+
+- 计划金额是**“已批准 / 已认领”的口径**,不是实际到账/实付口径;
+- 实际付了多少、收了多少,看履约记录;
+- 付款计划只在款项性质一致时累加(预付款不会加到进度款计划上);
+- 同一合同多次付款、多次认领,计划金额会持续累加。
+
+### 3.3 计划的修改
+
+- 合同审批通过后,已推送成功的计划行再修改,系统会自动重新推送合规;
+- 删除已推送的计划行,会在合规侧同步删除(不能直接物理删除);
+- 新增的计划行随下一次推送一起补到合规。
+
+---
+
+## 四、四个场景的履约记录
+
+### 4.1 付款履约记录
+
+**什么时候产生:金蝶实际付款之后。**
+
+业务过程:
+
+```
+付款申请审批通过
+ → 本地累加计划金额(暂不同步合规),不产生履约付款记录
+ → 推送金蝶付款申请
+金蝶完成实际付款
+ → 系统回写实付金额
+ → 计划金额同步到合规 + 生成履约付款记录
+```
+
+业务要点:
+
+| 项目 | 说明 |
+| --- | --- |
+| 生成条数 | 一张付款申请对应一条;**分次付款不新增,只更新同一条的实际金额** |
+| 金额 | 申请金额 + 实际付款金额 |
+| 日期 | 付款申请日期 |
+| 附件 | 付款场景会把付款附件一并传到合规 |
+| 特批 | 需要特批的付款,特批通过后也不会立刻生成记录,仍要等金蝶实际付款 |
+
+> 常见误区:付款审批通过后就去合规找履约付款记录——此时还没有,属于正常现象,金蝶付款后才会出现(付款计划金额的累加同样是金蝶实际付款后才同步到合规)。
+
+---
+
+### 4.2 收款履约记录
+
+**什么时候产生:收款流水认领完成之后。**
+
+业务过程:
+
+```
+收款流水到账并同步到 TMS
+ → 业务人员做认领(分摊到应收结算单)
+ → 认领提交后,系统自动:
+ ① 更新该合同的收款计划金额
+ ② 推送认领记录到合规
+ ③ 生成收款履约记录
+```
+
+业务要点:
+
+| 项目 | 说明 |
+| --- | --- |
+| 生成条数 | 每完成一次认领,按涉及的合同各生成一条 |
+| 金额 | **本次认领分摊到该合同的金额**(不是合同累计认领金额) |
+| 日期 | 认领日期 |
+| 作废 | 认领作废只会在合规侧把认领记录置为作废,**不会冲回计划金额,也不会冲回已生成的履约记录** |
+
+> 注意:收款链路的推送失败**不会自动重试**,如合规侧没有数据,请联系系统管理员排查。
+
+---
+
+### 4.3 开票履约记录
+
+**什么时候产生:金蝶开票完成、发票回到 TMS 之后。**
+
+业务过程:
+
+```
+开票申请审批通过
+ → 推送金蝶开票
+ → 推送合规开票登记(此时还没有发票日期)
+金蝶完成开票
+ → 系统回写发票信息(每张发票一条)
+ → 按每张发票生成履约开票记录
+```
+
+业务要点:
+
+| 项目 | 说明 |
+| --- | --- |
+| 生成条数 | **每张发票一条**;一张开票申请开多张发票,就会生成多条 |
+| 金额 | 发票价税合计 |
+| 日期 | 发票的开票日期 |
+| 标题 | `开票履约-发票号`,可在合规侧按此检索 |
+| 失败处理 | 合同或计划条件不满足时暂不生成,系统自动重试,条件满足后补上 |
+
+---
+
+### 4.4 收票履约记录
+
+**什么时候产生:收票单成功推送合规之后。**
+
+业务过程:
+
+```
+收票登记审批通过
+ → 推送金蝶收票单
+ → 推送合规收票单(C109)
+ → 推送成功后,按收票单里的每条结算信息生成履约收票记录
+```
+
+业务要点:
+
+| 项目 | 说明 |
+| --- | --- |
+| 生成条数 | **每条结算信息(分摊行)一条**;一张收票单分摊到多张结算单就会生成多条 |
+| 金额 | 分摊到该结算单的收票金额 |
+| 日期 | 收票单的发票日期 |
+| 标题 | `收票履约-发票号-结算单号` |
+| 失败处理 | 收票单还没成功推合规时暂不生成,等收票单推送成功后自动补上 |
+
+---
+
+### 4.5 四场景速查
+
+| 场景 | 什么时候出现 | 生成条数 | 金额口径 | 日期口径 |
+| --- | --- | --- | --- | --- |
+| 付款 | 金蝶实际付款后 | 一张付款申请一条(分期更新同一条) | 申请金额 / 实付金额 | 申请日期 |
+| 收款 | 认领提交后 | 每次认领 × 每个合同一条 | 本次认领分摊金额 | 认领日期 |
+| 开票 | 金蝶开票完成后 | 每张发票一条 | 发票价税合计 | 发票开票日期 |
+| 收票 | 收票单推合规成功后 | 每条结算分摊行一条 | 分摊收票金额 | 发票日期 |
+
+---
+
+## 五、合规侧怎么核对
+
+在合规系统履约记录列表中,按标题前缀检索最快:
+
+| 场景 | 标题前缀 |
+| --- | --- |
+| 付款 | `付款履约-` |
+| 收款 | `收款履约-` |
+| 开票 | `开票履约-` |
+| 收票 | `收票履约-` |
+
+核对时重点看四项:**合同是否正确、金额是否正确、日期口径是否符合上表、条数是否与业务动作匹配**。
+
+---
+
+## 六、异常与重试
+
+| 场景 | 推送失败后是否自动重试 | 业务需要做什么 |
+| --- | --- | --- |
+| 付款 | 是,系统定时自动重试 | 一般无需处理;长期失败联系管理员 |
+| 开票 | 是,系统定时自动重试 | 确认金蝶是否已开票完成;长期失败联系管理员 |
+| 收票 | 是,系统定时自动重试 | 先确认收票单是否已成功推送合规 |
+| 收款 | **否** | 联系系统管理员手动排查处理 |
+
+常见“暂时生成不了”的原因:
+
+1. 合同还没推送合规;
+2. 履约计划还没推送成功(没有可关联的计划);
+3. 对应单据还没审批通过;
+4. 收票单还没成功推送合规;
+5. 金蝶还没实际付款 / 还没完成开票。
+
+前四类在条件满足后会自动补推,不需要重复操作单据。
+
+---
+
+## 七、作废、驳回与变更
+
+| 业务动作 | 对合规履约数据的影响 |
+| --- | --- |
+| 付款审批驳回 | 不产生履约付款记录 |
+| 付款需要特批 | 特批期间不产生履约数据;特批通过后也要等金蝶实际付款 |
+| 付款申请作废 | 当前**不会冲回**已累加的计划金额,也不会冲回履约付款记录 |
+| 收款认领作废 | 合规侧认领记录作废;**计划金额与收款履约记录不冲回** |
+| 合同变更审批通过 | 计划行随变更重新推送合规,已生成的履约记录不受影响 |
+
+> 如业务上需要“作废后金额必须回退”,请单独提需求或在合规侧人工调整,不能依赖当前自动链路。
+
+---
+
+## 八、常见问题(FAQ)
+
+**1. 合同刚审批通过,合规为什么没有履约记录?**
+正常。此时只有履约计划,记录要等业务实际发生(付款、收款、开票、收票)后才生成。
+
+**2. 付款已经审批通过了,合规为什么没有付款履约记录?**
+正常。付款履约记录以**金蝶实际付款**为准,审批通过只更新计划金额。
+
+**3. 同一笔付款分两次支付,合规会出现两条记录吗?**
+不会。一张付款申请对应一条记录,第二次付款更新实际金额。
+
+**4. 同一笔收款分多次认领,合规会出现几条?**
+每次认领都会生成一条(按涉及合同分别生成),金额为本次分摊金额。
+
+**5. 一张开票申请开了多张发票,合规有几条?**
+每张发票一条。
+
+**6. 收票单有多条结算信息,合规有几条?**
+每条结算信息一条。
+
+**7. 合规侧一条记录都没有,先查什么?**
+按顺序确认:合同是否已推合规 → 履约计划是否已推成功 → 单据是否已审批通过 →(开票)金蝶是否已开票完成 →(收票)收票单是否已推合规。收款链路不会自动重试,需联系管理员。
+
+**8. 计划金额和履约记录金额为什么对不上?**
+两者口径不同:计划金额是“已批准/已认领”的累计口径,履约记录是“本次实际发生”的口径。付款场景下,计划看申请金额,记录看实付金额。
+
+**9. 作废后合规金额为什么没变?**
+当前作废不回冲计划金额和履约记录,需要在合规侧人工处理或单独提需求。
+
+---
+
+## 附录:业务名称与合规接口对照(供对接同学参考)
+
+| 业务名称 | 合规接口 |
+| --- | --- |
+| 合同推送 | C103-1 / C103-11 |
+| 履约计划推送 | C103-9 |
+| 付款履约记录 新建 / 更新 | C108-1 / C108-2 |
+| 收款认领记录 新增 / 作废 | C107-1 / C107-3 |
+| 开票登记 | C111 |
+| 收票单推送 | C109 |
diff --git a/doc/docs/hcs合同/2026-10-29-履约计划与履约记录四场景实现逻辑(付款-开票-收票-收款).md b/doc/docs/hcs合同/2026-10-29-履约计划与履约记录四场景实现逻辑(付款-开票-收票-收款).md
new file mode 100644
index 00000000..a305ae36
--- /dev/null
+++ b/doc/docs/hcs合同/2026-10-29-履约计划与履约记录四场景实现逻辑(付款-开票-收票-收款).md
@@ -0,0 +1,317 @@
+# 履约计划与履约记录四场景实现逻辑(付款 / 开票 / 收票 / 收款)
+
+> 日期:2026-10-29
+> 范围:`tms-api` blade-transport 中 TMS ↔ 合同合规系统(HCS)的「履约计划」(C103-9)与「履约记录」(C108-1/2)。
+> 目标:只讲清**逻辑、流程、先后顺序**,不展开代码细节。
+
+---
+
+## 0. 总览
+
+一句话:**履约计划是合同资金的“计划行”,履约记录是这些计划行的“实际发生流水”**。计划先行(先拿到 HCS 阶段ID),记录才能引用它;四个业务场景(付款/收款/开票/收票)各自在“业务真正发生”的时点产生一条或多条履约记录。
+
+### 0.1 全景顺序
+
+```
+合同审批通过
+ └─ 建默认履约计划行 → 推合同 C103-1(拿合同 hcs_fd_id)
+ └─ 推履约计划 C103-9(拿计划 hcs_exeplan_id + 每行 hcs_stage_fd_id)
+ ├─ 付款:审批通过 → 计划金额本地累加(不推合规) → 金蝶实际付款 → C103-9 重推 + C108-1/C108-2
+ ├─ 收款:认领提交 → 计划金额累加 → C107-1 → C108-1
+ ├─ 开票:金蝶开票完成、发票落库 → C111 登记 → 每张发票 C108-1
+ └─ 收票:收票单推合规 C109 成功 → 每条结算分摊行 C108-1
+```
+
+### 0.2 四条链的关键先后关系
+
+| 场景 | 前一条链必须先完成 | 履约记录产生时点 | 记录粒度 |
+| --- | --- | --- | --- |
+| 付款 | C103-1 拿到 `hcs_fd_id`;C103-9 计划行拿到 `hcs_stage_fd_id` | **金蝶实际付款后**(`paidAmount>0` 且金额推进) | 付款申请单(1 张单 1 条,分期走更新) |
+| 收款 | 同上,且收款计划行已同步 | **认领事务提交后** | 认领主单 × 合同(每次认领各 1 条) |
+| 开票 | 合同/收款计划行已同步;金蝶 A303-5 发票已落库 | **金蝶开票完成后** | 每张发票 1 条 |
+| 收票 | 合同/付款计划行已同步;**C109 收票单已推合规成功** | **C109 推送成功后** | 每条结算分摊行 1 条 |
+
+---
+
+## 1. 两个对象与 ID 链
+
+| 对象 | TMS 落点 | HCS 接口 | 作用 |
+| --- | --- | --- | --- |
+| 履约计划行 | `blade_contract_exe_stage`(`HcsContractExeStage`) | C103-9 行级 add/update/delete | 合同维度的付款/收款资金计划 |
+| 履约记录 | 各业务表上的 `hcs_perf_fd_id` | C108-1 新建 / C108-2 更新 | 计划的实际执行流水 |
+
+必须持久化的 ID 链(后面所有推送都依赖它):
+
+```
+blade_contract_manage.hcs_fd_id 合规合同ID(C103-1)
+blade_contract_manage.hcs_exeplan_id 合规履约计划ID(C103-9)
+blade_contract_exe_stage.hcs_stage_fd_id 合规阶段ID(C103-9 按行回填,= C108 的 fd_plan_id)
+blade_payment_application.hcs_perf_fd_id 付款履约记录ID(C108-1 返回)
+blade_invoice_application_invoice.hcs_perf_fd_id 开票履约记录ID
+blade_invoice_receipt_settlement.hcs_perf_fd_id 收票履约记录ID
+blade_receipt_claim_hcs_record.hcs_claim_record_fd_id C107-1 认领记录ID(供 C107-3 作废)
+```
+
+`fd_plan_id` 在 C108 接口中**必填**,只能取 HCS 自己返回的 `hcs_stage_fd_id`。因此没有已同步计划行时,履约记录一律推不出去,只能失败重试,等合同链把计划补推成功后自愈。
+
+---
+
+## 2. 履约计划(C103-9)
+
+### 2.1 计划行从哪来
+
+| 入口 | 行为 |
+| --- | --- |
+| 合同表单「履约规则」页签维护(`ContractExeStageServiceImpl#submitRows/removeRow`) | 新行 → `row_op=add`;已同步行编辑 → `update`;已同步行删除 → 软删 + `delete` 标记(不能物理删,delete 要凭阶段ID 上报) |
+| 合同审批通过(`createDefaultRowOnApprove`) | 无任何计划行时自动建默认行:承运商合同=付款/进度款,客户合同=收款/进度款;同时把该合同所有未拿到阶段ID 的行统一置 `add` |
+| 付款审批通过(`accumulatePaidAmountOnApprove`) | 按付款类型映射款项性质(正式结算=进度款,其余=预付款),对**精确匹配**该款项性质的已同步付款行,原子累加 `fund_amount += 申请金额`,并置 `update + unsynced` |
+| 收款认领(`accumulateClaimedAmountOnPush`) | 取收款/进度款计划行(无进度款则兜底首条已同步收款行),累加本次分摊到该合同的认领金额,置 `update + unsynced` |
+
+要点:
+
+- 金额累加只发生在“审批通过/认领提交”,**不是**在实际收付款时;因此计划金额反映的是“已批准/已认领的计划口径”,履约记录才反映实际发生额。
+- 付款累加严格要求款项性质一致,匹配不到就跳过,避免预付款计进进度款;收款累加允许兜底行。
+- 已同步行靠 `hcs_stage_fd_id` 重推;未同步行保持 `add`,随合同链首推(首次推送即携带累加后的金额)。
+
+### 2.2 同步逻辑(`HcsExeStageSyncSupport#syncExeStages`)
+
+1. 前置:合同必须有 `hcs_fd_id`。
+2. 取该合同所有 `row_op <> 'none'` 的行;删除行已软删,用原生 SQL 绕过逻辑删除才能取到。
+3. 整体操作类型:`hcs_exeplan_id` 为空 → `add`(首次),否则 `update`。
+4. 行级:`add` 行带 `fdUniqueId=TMS行ID`(重试稳定);`update/delete` 行必须带 `fdId=hcs_stage_fd_id`。
+5. 响应回填:回填 `hcs_exeplan_id`、逐行回填 `hcs_stage_fd_id`,清 `row_op`、置 `synced`;`add` 行没匹配到阶段ID → 置 `failed`,下轮重试。
+
+### 2.3 三个触发入口(谁调 C103-9)
+
+| 入口 | 位置 | 门禁 | 说明 |
+| --- | --- | --- | --- |
+| 合同链 | `HcsContractSyncSupport#pushContract` 第⑤步 | `exe-stage.sync-enabled` | 合同起草/更新成功后、特批挂起前同步计划 |
+| 付款链 | `HcsPaymentSyncSupport#pushPayment` 第③步 | `exe-stage.sync-enabled` | **金蝶实际付款门禁通过后**重推计划(审批通过仅本地累加金额不推送),再推履约记录 |
+| 收款链 | `HcsClaimSyncSupport#pushOneContract` 第①步 | `exe-stage.sync-enabled` | 先累加并重推计划,再推 C107-1/C108-1 |
+
+> 合同还没推合规时,计划行只做本地标记/累加,不报错,等合同链首推时一并带上。
+
+---
+
+## 3. 履约记录(C108)
+
+### 3.1 公共前置与公共字段
+
+所有场景的 C108-1 都要求:
+
+- 合同已推合规(`hcs_fd_id` 非空);
+- 存在已同步计划行(`hcs_stage_fd_id` 非空):付款/收票取**付款**计划行,收款/开票取**收款**计划行;
+- `perf-record.sync-enabled` 开启;
+- `fd_plan_id`/`fd_id` = 计划行 `hcs_stage_fd_id`,`fd_property` = 计划行款项性质。
+
+四个场景的差异主要在:**触发时点、粒度、金额、履约日期、类型、幂等键**。
+
+### 3.2 付款(`HcsPaymentSyncSupport` + `HcsPerfRecordSyncSupport`)
+
+```
+付款申请审批通过
+ → hcs_sync_status=unsynced;计划金额本地累加(row_op=update,不推合规)
+ → 即时推送/扫描:paidAmount=0 → 履约计划与履约记录均不推送
+ (提示“金蝶未实际付款”),整体仍置 synced
+
+金蝶实际付款 → A103-2 回写(applyPayApplyResult)
+ → paidAmount 首次>0 或金额变化 → hcs_sync_status=unsynced
+ → 扫描付款任务:先 C103-9 重推累加金额的计划行
+ 无 hcs_perf_fd_id → C108-1 新建,返回 fdId 立即落库
+ 有 hcs_perf_fd_id → C108-2 更新(分期付款推进实际金额)
+```
+
+- 金额:申请金额=`appliedAmount`,实际金额=`paidAmount`。
+- 日期:申请日期(`applyDate`)。
+- 类型:`付款`。
+- 幂等:单据粒度,`hcs_perf_fd_id` 决定新建还是更新;金额不变不会重复触发。
+- 附件:仅付款场景保留附件转传(C103-2 → `fd_plan_att`),其余场景传空列表。
+- 特批:`need` 发起 C106-3 后挂起;特批通过后**不补推任何履约数据**,只恢复审批并补推金蝶;驳回不产生履约数据。
+
+### 3.3 收款(`HcsClaimSyncSupport`)
+
+```
+认领登记(只登记本地,不推金蝶)
+ → 事务提交后 afterCommit → pushClaimToHcs(claimId)
+ ① 按“本次分摊到该合同”的金额累加收款计划行 → C103-9 重推
+ ② C107-1 认领记录(认领主单 × 合同一条,金额=本次分摊额)
+ ③ C108-1 履约流水(同一粒度:认领主单 × 合同一条)
+```
+
+- 触发点:**认领事务提交后**,不是金蝶回调。
+- 前置:`exe-stage.sync-enabled` 为整条链总门禁;C108-1 另受 `perf-record.sync-enabled` 门禁。
+- 定位合规流水:C104-2 按明细流水号查出合规收款流水ID;查不到只跳过 C107-1,不影响计划更新。
+- 金额:本次认领分摊到该合同的金额(申请金额=实际金额)。
+- 日期:认领日期(当前代码 `claim.claimDate`)。
+- 类型:当前代码传的是 `collection`(见第 6 节差异说明)。
+- 幂等/重试:事件驱动、**无定时重试**;C108-1 返回的 fdId 未落库(无 C108-2 场景),失败只记日志。
+- 作废:只推 C107-3 把 C107-1 记录置为已作废;**不回冲计划金额、不回冲履约记录**。
+
+### 3.4 开票(`HcsInvoicePerfSyncSupport`)
+
+```
+开票申请审批通过 → 推金蝶 A303-1 → 推合规 C111 开票登记(票据日期为空)
+金蝶开票完成 → A303-5 回写:发票逐张落库;申请单置 invoiced + hcs_sync_status=unsynced
+
+扫描开票任务(同一轮):
+ ① C111 推送/回填票据日期
+ ② 履约开票记录:扫描 hcs_perf_fd_id 为空且未超重试上限的发票
+ 申请单须审批通过
+ 合同回溯:开票申请 → 应收结算关联 → 正式结算单 → 合同
+ 每张发票一条 C108-1
+```
+
+- 金额:发票价税合计 `invoiceAmount`。
+- 日期:发票完成开票日期 `invoiceDate`。
+- 类型:`开票`。
+- 申请编号:发票号;标题:`开票履约-{发票号}`。
+- 幂等:发票粒度,`hcs_perf_fd_id` 非空即跳过;失败 `retry+1` + 原因,下轮补偿,超上限人工处理。
+- 合同回溯不到、无已同步收款计划行 → 记失败原因,等条件满足后自愈。
+
+### 3.5 收票(`HcsReceiptPerfSyncSupport`)
+
+```
+收票单审批通过 → 推金蝶收票单(A303-2 + A302-6)
+
+扫描收票任务(同一轮):
+ ① C109 收票单推送合规 → 成功置 hcs_sync_status=synced
+ ② 履约收票记录:扫描 hcs_perf_fd_id 为空且未超重试上限的分摊行
+ 所属收票单须审批通过且 hcs_sync_status=synced(C109 必须先成功)
+ 合同回溯:分摊行 → 正式结算单 → 合同
+ 每条结算分摊行一条 C108-1
+```
+
+- 金额:分摊收票金额 `allocatedInvoiceAmount`,为空兜底结算金额。
+- 日期:收票单发票日期 `invoiceDate`。
+- 类型:`收票`;关联**付款**计划行。
+- 申请编号:`{发票号}-{结算单号}`;标题:`收票履约-{申请编号}`。
+- 幂等:分摊行粒度,`hcs_perf_fd_id` 非空跳过;C109 未成功时记“尚未推送合规”,成功后下轮自动补偿。
+
+### 3.6 四场景对照
+
+| 维度 | 付款 | 收款 | 开票 | 收票 |
+| --- | --- | --- | --- | --- |
+| 触发时点 | 金蝶实际付款回写后 | 认领事务提交后 | 金蝶开票完成、发票落库后 | C109 推送成功后 |
+| 粒度 | 付款申请单 | 认领主单 × 合同 | 每张发票 | 每条结算分摊行 |
+| 申请金额 | 申请金额 | 本次分摊金额 | 发票价税合计 | 分摊收票金额 |
+| 实际金额 | 实付金额 | 同申请金额 | 同申请金额 | 同申请金额 |
+| 履约日期 | 申请日期 | 认领日期 | 发票开票日期 | 发票日期 |
+| 类型 | 付款 | collection(现状) | 开票 | 收票 |
+| 计划行 | 付款(按款项性质) | 收款(进度款优先) | 收款(进度款优先) | 付款(进度款优先) |
+| 幂等键 | `hcs_perf_fd_id`(可更新) | 无(每次认领新建) | `hcs_perf_fd_id` | `hcs_perf_fd_id` |
+| 失败重试 | 定时扫描 | 无 | 定时扫描 | 定时扫描 |
+| 更新接口 | C108-2 支持 | 不支持 | 不支持 | 不支持 |
+
+---
+
+## 4. 端到端时序(先后顺序)
+
+### 4.1 计划必须先于记录
+
+```mermaid
+sequenceDiagram
+ participant C as 合同
+ participant H as HCS
+ participant B as 业务单据(付款/认领/发票/收票)
+ C->>H: C103-1 合同起草
+ H-->>C: hcs_fd_id
+ C->>H: C103-9 履约计划(add)
+ H-->>C: hcs_exeplan_id + 每行 hcs_stage_fd_id
+ Note over C,H: 阶段ID 落库后,下面四条链才有 fd_plan_id
+ B->>H: C108-1 履约记录(fd_plan_id=阶段ID)
+```
+
+### 4.2 付款链
+
+```mermaid
+sequenceDiagram
+ participant P as 付款申请
+ participant H as HCS
+ participant K as 金蝶
+ P->>P: 审批通过:计划金额+=申请金额
+ P->>H: C103-9 重推计划
+ P->>P: paidAmount=0,跳过 C108
+ K-->>P: 实际付款回写(paidAmount 变化)
+ P->>H: C108-1(首次)/ C108-2(分期)
+```
+
+### 4.3 收款链
+
+```mermaid
+sequenceDiagram
+ participant U as 认领
+ participant H as HCS
+ U->>U: 认领登记(本地)
+ U->>H: ① C103-9 累加收款计划并重推
+ U->>H: ② C104-2 定位合规流水 → C107-1 认领记录
+ U->>H: ③ C108-1 履约流水(本次分摊金额)
+```
+
+### 4.4 开票 / 收票(任务内顺序)
+
+```
+开票任务:C111 开票登记 → 发票逐张 C108-1(同一轮)
+收票任务:C109 收票单 → 分摊行逐条 C108-1(同一轮,C109 必须先成功)
+```
+
+---
+
+## 5. 定时任务与开关
+
+| 任务 | 默认 cron | 内容 |
+| --- | --- | --- |
+| contract | `0 1/5 * * * ?` | C103-1/C103-11 → C103-9 |
+| 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 |
+| 收款 | 无独立任务 | 认领事务提交后事件驱动 |
+
+| 配置(`thirdParty.hcs.*`) | 作用 | 默认 |
+| --- | --- | --- |
+| `exe-stage.sync-enabled` | C103-9 总开关;同时门禁整条收款链 | true |
+| `perf-record.sync-enabled` | 四场景履约记录总开关 | false |
+| `perf-record.xform-id` | C108 表单模板ID | 空 |
+| `push.payment/invoice-application/invoice-receipt.sync-enabled` | 各任务开关 | false |
+| `push.max-retry` / `push.batch-size` | 重试上限 / 单轮批量 | 5 / 配置值 |
+| `attachment.transfer-enabled` | 付款履约记录附件转传 | true |
+
+手动入口:`POST /hcs/sync/{bizType}` 批量扫描一轮(开票/收票为组合动作,含履约记录);`POST /hcs/sync/{bizType}/{id}` 单条重推(仅主单据,不覆盖发票/分摊行粒度)。
+
+---
+
+## 6. 当前实现与设计文档的差异(重要)
+
+以下为代码实际情况,与《2026-10-27 履约计划与履约记录逻辑与流程梳理》《2026-10-28 C108 履约记录四场景推送改造流程梳理》存在出入,落地时以本节为准:
+
+| 点 | 设计文档口径 | 当前代码 |
+| --- | --- | --- |
+| 收款触发 | 金蝶认领推送 A301-3 成功后 | **认领事务提交后**(认领只登记本地,不推金蝶) |
+| 收款履约日期 | 收款流水交易日期 `transaction_time` | **认领日期 `claimDate`**(提交 `4a76e65b` 回退了该调整) |
+| 收款履约类型 | 中文「收款」 | **`collection`**(`PERFORM_TYPE_COLLECTION`,未使用 `PERF_TYPE_COLLECTION`) |
+| 收款金额 | 该合同累计认领金额 | **本次认领分摊到该合同的金额** |
+| 收款组装 | 复用 `HcsPerfRecordAddSupport` | **仍为 `HcsClaimSyncSupport` 内部独立组装** |
+| 付款链 C103-9 门禁 | 不受 `exe-stage` 开关限制 | **受 `exe-stage.sync-enabled` 控制** |
+| 收款链 fdId | 需落库以支持后续更新 | **未落库**(无 C108-2 场景) |
+| 付款作废 / 认领作废 | — | 均不回冲计划金额与履约记录(认领仅 C107-3 作废) |
+
+建议:若确认收款需按「交易日期 + 中文类型 + 通用组件」口径,应把 `HcsClaimSyncSupport#pushPerfRecord` 切到 `HcsPerfRecordAddSupport`,并同步修正日期与类型常量;否则应同步更新设计文档,避免联调口径不一致。
+
+---
+
+## 7. 代码索引
+
+| 职责 | 类 / 方法 |
+| --- | --- |
+| 计划行维护、差异标记、金额累加、默认行 | `ContractExeStageServiceImpl`:`submitRows` / `removeRow` / `accumulatePaidAmountOnApprove` / `accumulateClaimedAmountOnPush` / `createDefaultRowOnApprove` / `findPaymentStageRow` / `findCollectionStageRow` |
+| C103-9 同步与回填 | `HcsExeStageSyncSupport#syncExeStages` |
+| 付款链编排 | `HcsPaymentSyncSupport#pushPayment` |
+| 付款履约记录 C108-1/2 | `HcsPerfRecordSyncSupport#pushPerfRecord` |
+| 收款链编排 | `HcsClaimSyncSupport#pushClaimToHcs` / `#pushOneContract` / `#pushPerfRecord` / `#voidClaimToHcs` |
+| C108-1 通用组件(开票/收票共用) | `HcsPerfRecordAddSupport#pushPerfRecordAdd` |
+| 开票履约记录 | `HcsInvoicePerfSyncSupport#pushUnsyncedInvoicePerfRecords` |
+| 收票履约记录 | `HcsReceiptPerfSyncSupport#pushUnsyncedReceiptPerfRecords` |
+| 定时任务编排 | `HcsPushTask`:`pushHcsPayment` / `syncHcsInvoiceApplications` / `syncHcsInvoiceReceipts` |
+| 审批即时推送 | `HcsImmediatePushSupport#pushPaymentAndWait / pushPaymentAfterCommit` |
+| 金蝶实付回写触发 | `PaymentApplicationServiceImpl#applyPayApplyResult` |
+| 认领事务提交触发 | `ReceiptFlowServiceImpl`(`afterCommit` → `pushClaimToHcs`) |
+| 常量与映射 | `HcsSyncConstant`(`PERF_TYPE_*`、`PERFORM_TYPE_*`、`FUND_NATURE_*`、`fundNatureOfPayment`) |
diff --git a/doc/docs/业务流程/2026-10-16-收款流水多次认领与作废口径梳理.md b/doc/docs/业务流程/2026-10-16-收款流水多次认领与作废口径梳理.md
index e0bb492c..c5295b29 100644
--- a/doc/docs/业务流程/2026-10-16-收款流水多次认领与作废口径梳理.md
+++ b/doc/docs/业务流程/2026-10-16-收款流水多次认领与作废口径梳理.md
@@ -2,6 +2,11 @@
> 日期:2026-10-16
> 范围:`tms-api` blade-transport 收款流水认领、认领记录模块;`tms-erp-web` 认领记录页面
+>
+> **口径变更提示(2026-10-29)**:文中"公告未认领金额 `unclaimed_amount` / A301-2 口径"已废止,见
+> [2026-10-29-收款流水菜单改造](./2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md)。
+> 现认领上限为**收款金额 `receipt_amount`(A301-1 `actrecamt`)**,`remaining_amount = max(收款金额 − 累计认领金额, 0)`;
+> 金蝶未匹配金额 `unmatch_amount_rec` 仅作参考展示,不参与额度控制。认领/作废/多次认领的其余口径不变。
---
@@ -45,10 +50,10 @@ blade_kingdee_receipt_flow(收款流水 / 认领通知单,1)
1. 锁定流水(通知单)与所选结算单。
2. 校验两条累计上限:
- 每张结算单:已有累计认领 + 本次分摊 ≤ 结算总应收含税金额;
- - 流水:已有累计认领(`flowClaimedAmount`,只算未作废记录)+ 本次总额 ≤ 公告未认领金额(`unclaimed_amount`,A301-2 口径)。
+ - 流水:已有累计认领(`flowClaimedAmount`,只算未作废记录)+ 本次总额 ≤ 收款金额(`receipt_amount`,A301-1 口径;2026-10-29 前为公告未认领金额)。
3. **插入一条** `ReceiptClaim` 主单:`claim_amount` = 本次分摊总额,`claim_status = claimed`。
4. 按所选结算单逐张**插入 N 条** `ReceiptClaimSettlement` 分摊明细。
-5. 累加流水 `claimed_amount`,按累计值重算流水状态(< 未认领金额 → partial,= → claimed)。
+5. 累加流水 `claimed_amount`,按累计值重算流水状态(< 收款金额 → partial,≥ 收款金额 → claimed)。
6. 只登记本地,不推送金蝶;推送由定时任务/手动按钮触发。
所以:**同一通知单第二次认领是再插一条新主单**,不存在"找已有主单合并金额"的逻辑;校验全部按"未作废记录累计"实时计算,天然支持多次认领。
@@ -69,17 +74,17 @@ canClaim(row) {
2. **剩余可认领金额 > 0**:`remainingAmount` 由后端 `ReceiptFlowWrapper` 统一计算:
```
- remainingAmount = max(公告未认领金额 unclaimed_amount − 累计认领金额 claimed_amount, 0)
+ remainingAmount = max(收款金额 receipt_amount − 累计认领金额 claimed_amount, 0)
```
按认领状态换算:
| 流水认领状态 | remainingAmount | 认领按钮 |
| --- | --- | --- |
-| unclaimed 未认领(公告金额 > 0) | = 公告未认领金额 > 0 | 显示 |
+| unclaimed 未认领(收款金额 > 0) | = 收款金额 > 0 | 显示 |
| partial 部分认领 | > 0 | 显示 |
-| claimed 认领完成 | = 0(累计 ≥ 公告金额) | **不显示** |
-| 公告未认领金额为 0 的流水 | = 0 | **不显示** |
+| claimed 认领完成 | = 0(累计 ≥ 收款金额) | **不显示** |
+| 收款金额为 0 或缺失的流水 | = 0 | **不显示** |
| 无 `receipt_flow_claim` 权限 | — | **不显示** |
即:**按钮显隐只看金额余额与权限,不看认领状态字段本身**;`claimed` 状态的行因为剩余额度为 0 自然不显示按钮。按钮不显示且不满足"推送金蝶"条件时,操作列显示"-"。
diff --git a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/01-结算调整单列表-金蝶状态与推送入口.png b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/01-结算调整单列表-金蝶状态与推送入口.png
new file mode 100644
index 00000000..8303ad57
Binary files /dev/null and b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/01-结算调整单列表-金蝶状态与推送入口.png differ
diff --git a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/02-新增调整单-回带结算单信息与费用明细.png b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/02-新增调整单-回带结算单信息与费用明细.png
new file mode 100644
index 00000000..8410d6d1
Binary files /dev/null and b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/02-新增调整单-回带结算单信息与费用明细.png differ
diff --git a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/03-新增调整单-选单后金蝶冲销预检提示.png b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/03-新增调整单-选单后金蝶冲销预检提示.png
new file mode 100644
index 00000000..4d3bf538
Binary files /dev/null and b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/03-新增调整单-选单后金蝶冲销预检提示.png differ
diff --git a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/04-重新推送-高危二次确认.png b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/04-重新推送-高危二次确认.png
new file mode 100644
index 00000000..6f20c8d9
Binary files /dev/null and b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).assets/04-重新推送-高危二次确认.png differ
diff --git a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md
index a33bdcc2..7e0a0b4d 100644
--- a/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md
+++ b/doc/docs/业务流程/2026-10-16-结算调整单接入金蝶设计(未推送自动推-已推送冲销后重推新单).md
@@ -20,6 +20,8 @@
> 全程交互使用中文。
+.assets/01-结算调整单列表-金蝶状态与推送入口.png)
+
## 2. 名词与接口速查
| 代号 | 含义 |
@@ -119,6 +121,8 @@
- 排除收付完成/尾款等不可调整(`adjustableFormalSettlement`,约 295);
- 保存时自动回带客商、调整类型、合同、原结算金额等。
+.assets/02-新增调整单-回带结算单信息与费用明细.png)
+
### 6.2 选单后金蝶冲销预检(新增,软提示)
选定关联结算单号、回带信息后,前端调后端**预检接口**:
@@ -134,6 +138,8 @@
- **降级**:金蝶查询超时/不可用/接口异常时,预检返回"未校验",**不阻断**建单与回带,由后续审批分流与重推闸门兜底。
- 预检结果**只用于提示**,不作为后续环节的结论缓存:审批(操作 2)、重推(操作 3)各自按**当下**状态重新判定。
+.assets/03-新增调整单-选单后金蝶冲销预检提示.png)
+
> 为什么不把硬拦截放在选单:冲销是金蝶侧**不可逆/需人工撤销**的对外操作,而审批只是 TMS 内部决策。若选单即要求已冲销,会出现"旧单已冲销、调整单却被驳回/删除"的**应付单空档**(详见第 13 节)。
## 7. 操作 2|审批通过
@@ -179,6 +185,8 @@
7. **原单收尾**:标记 `adjustStatus='已调整'` + 旧锁记录置 `unlocked`;追加重推变更记录。业务字段不动。
8. **失败处理**:新单 `kingdeeSyncStatus=failed` + 失败原因,支持复用 `retryKingdeeFailed` 重试(重试即从第 4 步继续,克隆已存在则跳过,注意幂等)。
+.assets/04-重新推送-高危二次确认.png)
+
## 9. 关键机制
### 9.1 防重键与新单号
diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/01-结算调整单列表-各状态操作入口.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/01-结算调整单列表-各状态操作入口.png
new file mode 100644
index 00000000..992bc639
Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/01-结算调整单列表-各状态操作入口.png differ
diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/02-新增调整单-选单回带与费用录入.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/02-新增调整单-选单回带与费用录入.png
new file mode 100644
index 00000000..8410d6d1
Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/02-新增调整单-选单回带与费用录入.png differ
diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/03-新增调整单-金蝶冲销预检提示.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/03-新增调整单-金蝶冲销预检提示.png
new file mode 100644
index 00000000..4d3bf538
Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/03-新增调整单-金蝶冲销预检提示.png differ
diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/04-提交审批二次确认.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/04-提交审批二次确认.png
new file mode 100644
index 00000000..5c1d4282
Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/04-提交审批二次确认.png differ
diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/05-重新推送-高危二次确认.png b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/05-重新推送-高危二次确认.png
new file mode 100644
index 00000000..6f20c8d9
Binary files /dev/null and b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).assets/05-重新推送-高危二次确认.png differ
diff --git a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md
index 16522c1b..92290581 100644
--- a/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md
+++ b/doc/docs/业务流程/2026-10-17-结算调整单操作流程说明(分场景).md
@@ -34,7 +34,34 @@
- 审批中:只能"通过"或"驳回"。
- 审批通过:终态,只剩金蝶推送相关操作。
-### 2.2 各状态下的按钮
+### 2.2 总体流程图
+
+```
+ 调整单审批通过
+ │
+ 原结算单金蝶状态
+ ┌───────────────────┴────────────────────┐
+ ▼ ▼
+ 未推送(场景 A) 已推送(场景 B)
+ ① 调整回写原结算单 ① 原结算单业务数据【零改动】
+ ② 原单追加变更记录 ② 原单仅追加 1 条变更记录
+ ③ 调整单 → 审批通过 ③ 调整单 → 审批通过
+ ④ 自动推送金蝶 ④ 提示:请财务在金蝶手工冲销旧单
+ (应付/应收单 + 落锁 + 附件) 【财务在金蝶手工冲销旧单】
+ ⑤ 回填「已同步 + 金蝶单据号」 点「重新推送(高危)」并二次确认
+ ↓ ① 校验调整单已审批通过
+ 金蝶应付/应收单(含调整金额) ② 旧单必须已冲销,否则拒绝
+ ③ 克隆新正式结算单(应用调整)
+ ④ 用新单号推金蝶 → 全新应付/应收单
+ ⑤ 新单落锁 + 附件
+ ⑥ 调整单改挂新单 + 回填金蝶单据号
+ ⑦ 原单标「已调整」+ 旧锁解锁
+ ⑧ 推送失败置「生成失败」,可重试
+ ↓
+ 金蝶【全新】应付/应收单(旧单已冲销)
+```
+
+### 2.3 各状态下的按钮
| 状态 | 可用操作 | 说明 |
|---|---|---|
@@ -43,7 +70,9 @@
| 审批中 | 通过 / 驳回 | 审批人操作 |
| 审批通过 | 推送金蝶 / 重新推送(合一按钮)| 按金蝶状态自动分流,高危确认 |
-### 2.3 金蝶同步状态(调整单上的展示字段)
+.assets/01-结算调整单列表-各状态操作入口.png)
+
+### 2.4 金蝶同步状态(调整单上的展示字段)
| 状态 | 含义 |
|---|---|
@@ -67,6 +96,10 @@
> 预检只是提示,不拦人;真正的拦截在"重新推送"那一步。填单时必须至少录入一条调整费用,否则无法提交。
+.assets/02-新增调整单-选单回带与费用录入.png)
+
+.assets/03-新增调整单-金蝶冲销预检提示.png)
+
### 3.2 保存草稿 / 编辑 / 删除
- 草稿和已驳回的单可反复编辑;删除仅限草稿,删除后单号作废、不留痕迹。
@@ -77,6 +110,8 @@
- 草稿 / 已驳回 → 审批中,进入审批人的待办。
- 提交后单据锁定,提交人不能再改。
+.assets/04-提交审批二次确认.png)
+
### 3.4 审批通过 ★ 核心分流点
审批人点"通过"后,系统按**原结算单是否推送过金蝶**自动走两条完全不同的路(见第四、五节场景 A / B):
@@ -102,6 +137,8 @@
按钮点击均有高危二次确认弹窗。
+.assets/05-重新推送-高危二次确认.png)
+
---
## 四、分场景流程
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..84c209fa
--- /dev/null
+++ b/doc/docs/业务流程/2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md
@@ -0,0 +1,201 @@
+# 收款流水菜单改造:数据源 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 | 认领上限口径 | **收款金额 `receipt_amount`(A301-1 `actrecamt`)**。原定 `unmatchamountrec` 与收款金额取 min,因金蝶侧财务先核销导致大量收款单未匹配金额为 0、TMS 永久无法认领,2026-10-29 修正:认领只登记本地,额度由本地累计认领控制,`unmatchamountrec` 仅参考展示 |
+| 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 收款单查询(业务日期窗口取 `thirdParty.bfi.recBillSync.days`,默认 60 天,全量)
+ → 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.queryAllPagesByTotal`(按 totalCount 推导总页数,兜底 500 页)+ `bfiService.queryRecBill` 全量翻页(pageSize 200),请求参数 `begin_bizdate` / `end_bizdate`(窗口天数取 Nacos `thirdParty.bfi.recBillSync.days`,默认 60)。
+
+**字段映射(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 | 主单 `billno` | 明细流水号=收款单号,**幂等键**;主单粒度,一张收款单一条流水,不展开流水分录 |
+| receipt_amount | 主单 `actrecamt` | 收款金额(金蝶收款金额口径) |
+| unmatch_amount_rec | `unmatchamountrec` | 未匹配收款金额(金蝶参考展示,**不参与认领额度**;原列名 unclaimed_amount 已改名) |
+| match_amount_rec | `matchamountrec` | 已匹配收款金额 |
+| match_amount_pay | `matchamountpay` | 已匹配付款金额 |
+| unmatch_amount_pay | `unmatchamountpay` | 未匹配付款金额 |
+| settle_amount | `settleamount` | 已结算金额 |
+| unsettle_amount | `unsettleamount` | 未结算金额 |
+| payer_name / counterparty_name | `payername` | 付款人名称 |
+| payee_name | `org_name` | 收款人(收款公司名称) |
+| org_number | `org_number` | 收款人(公司).编码 |
+| open_org_number | `openorg_number` | 核算组织.编码 |
+| 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 = receipt_amount`(收款金额 `actrecamt`,金蝶未匹配金额不参与额度控制);
+- 认领主单冗余展示字段直取流水:`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`,默认 60(A301-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 | 认领额度与金蝶匹配解耦 | 上限改用收款金额后,金蝶侧已匹配(核销)的收款单同样可认领并推合规;金蝶匹配状态变化不影响 TMS 已认领额度(按 `claimed_amount` 本地累计)。原 `unmatchamountrec` 口径下 797 条流水有 704 条额度为 0,需求无法闭环 | 中 |
+| 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. 回归:认领记录页、结算单收款汇总刷新、认领人筛选。
diff --git a/doc/docs/业务流程/2026-10-29-收款流水认领操作流程(收款金额本地额度·单条批量与作废).md b/doc/docs/业务流程/2026-10-29-收款流水认领操作流程(收款金额本地额度·单条批量与作废).md
new file mode 100644
index 00000000..080ea58c
--- /dev/null
+++ b/doc/docs/业务流程/2026-10-29-收款流水认领操作流程(收款金额本地额度·单条批量与作废).md
@@ -0,0 +1,254 @@
+# 收款流水认领操作流程(收款金额本地额度 · 单条/批量/作废)
+
+> 日期:2026-10-29
+> 范围:`tms-api` blade-transport 收款流水(同步、认领、认领记录)+ `tms-erp-web` 收款流水 / 认领记录页面
+> 关联文档:
+> - [2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步)](./2026-10-29-收款流水菜单改造(数据源A301-2改A301-1-认领只登记本地并推送合规C107-1与C107-3作废同步).md)(改造设计与字段映射)
+> - [2026-10-16-收款流水多次认领与作废口径梳理](./2026-10-16-收款流水多次认领与作废口径梳理.md)(多次认领、逐条作废口径)
+
+---
+
+## 0. 一页速览
+
+| 项 | 内容 |
+| --- | --- |
+| 菜单入口 | 首付款管理 → **收款流水**(`/payment/receipt-flow`)、**认领记录**(`/payment/receipt-claim-record`) |
+| 数据来源 | 金蝶 A301-1 收款单查询(收款处理单),**手动同步**触发,无定时任务 |
+| 流水粒度 | 一张金蝶收款单 = 一条流水(`detail_serial_no` = 收款单号,幂等键) |
+| 可认领上限 | **收款金额 `receipt_amount`(金蝶 `actrecamt`)** |
+| 剩余可认领 | `max(收款金额 − 已认领金额 claimed_amount, 0)` |
+| 未匹配金额 | 金蝶 `unmatchamountrec`,**仅参考展示,不参与额度控制** |
+| 认领动作 | 只登记本地(认领主单 + 结算单分摊),**不推金蝶** |
+| 合规推送 | 认领事务提交后推 C103-9 → C107-1 → C108-1;作废推 C107-3 |
+| 状态 | `unclaimed` 未认领 / `partial` 部分认领 / `claimed` 认领完成 |
+
+---
+
+## 1. 角色与权限
+
+| 操作 | 权限码 | 说明 |
+| --- | --- | --- |
+| 查看收款流水 | `receipt_flow_view` | 菜单权限 `receipt_flow` |
+| 认领 / 批量认领 | `receipt_flow_claim` | 无此权限时操作列显示 `-`,顶部无「批量认领」按钮 |
+| 手动同步流水 | `receipt_flow_sync` | 无此权限时顶部无「手动同步流水」按钮 |
+| 查看认领记录 | `receipt_claim_record` | **只显示当前登录人自己的认领记录** |
+| 作废认领记录 | `receipt_claim_record_void` | 仅本人、且状态为「已认领」的记录可作废 |
+
+管理员(`admin`)默认直通按钮权限判断;认领记录页的数据范围仍按当前登录人过滤,管理员也只能看到自己做的认领。
+
+---
+
+## 2. 流程总览
+
+```
+【金蝶】A301-1 收款单
+ │ ① 手动同步(可按窗口重复执行,幂等)
+ ▼
+【TMS】收款流水列表(blade_kingdee_receipt_flow)
+ │ ② 勾选/单条 → 选择应收正式结算单 → 填分摊金额
+ ▼
+【TMS】认领(blade_receipt_claim + blade_receipt_claim_settlement)
+ │ ③ 更新流水累计认领金额/状态、刷新结算单收款汇总
+ │ ④ 事务提交后推合规:C103-9 → C107-1 → C108-1(失败只落标记)
+ ▼
+合规侧认领记录(C107-1 返回 fdId 落库)
+ │ ⑤ 需要撤销时在「认领记录」页作废
+ ▼
+本地回退金额 + 推 C107-3(fdStatus=1 已作废)
+```
+
+---
+
+## 3. 场景一:同步收款流水
+
+**入口**:收款流水页顶部「手动同步流水」。
+
+**前置条件**:组织管理中已维护“是否平台公司 = 是”且已填金蝶编码的组织,否则直接报错「组织管理中未维护平台公司金蝶编码,无法同步金蝶收款流水,请先检查组织数据」。
+
+**动作**:
+
+1. 按业务日期窗口取数:结束日期 = 当天,开始日期 = 当天 − `thirdParty.bfi.recBillSync.days`(默认 60 天),起止时间格式 `yyyy-MM-dd HH:mm:ss`;
+2. 按上述组织编码过滤,全量翻页拉取(pageSize 200,兜底 500 页,结束条件为末页/不满一页);
+3. 以收款单号去重,按幂等键 upsert:新增初始化 `claimed_amount = 0`、`claim_status = unclaimed`;已存在只更新金蝶来源字段,**不覆盖本地认领金额与状态**;
+4. 收款金额 ≤ 0 的收款单跳过;
+5. 每批 5000 条独立事务入库,并写同步操作日志。
+
+**结果提示**:「流水同步完成,共同步 N 条收款流水」,列表自动刷新。
+
+**注意**:
+
+- 同步窗口从今天倒推,历史更早的单据不会进入;需要更早数据时调整 Nacos `thirdParty.bfi.recBillSync.days` 后重新同步(该配置支持热更新)。
+- 金蝶侧改动(金额、状态、匹配/结算金额)每次同步都会刷新;**TMS 已认领金额与状态不受影响**,但状态会按新的收款金额重算。
+
+---
+
+## 4. 场景二:单条认领
+
+### 4.1 入口与显示条件
+
+列表操作列「认领」需同时满足:
+
+1. 有 `receipt_flow_claim` 权限(admin 直通);
+2. 剩余可认领金额 > 0(即收款金额 > 累计已认领金额)。
+
+不满足时操作列显示 `-`。列表列「未匹配金额」为金蝶口径参考值,**该列为 0 不影响认领**。
+
+### 4.2 操作步骤
+
+1. 点「认领」进入认领页面,顶部只读展示:收款单号、付款人、收款金额、交易时间、对方户名/开户行/账号、明细流水号、已认领金额、未匹配金额(金蝶参考)、剩余可认领金额、摘要、认领人、认领日期、认领人部门;
+2. 点「关联结算单」选择器,弹窗按结算单号/项目/合同搜索:
+ - 候选范围:应收类型 + 已审批通过 + 有效状态 + 该结算单自身仍有剩余可认领金额;
+ - 多选时必须**同一项目、同一组织、同付款方、同收款方**,否则提示「关联结算单必须属于同一项目、组织及收付款方」;
+3. 在「结算信息」明细中逐张填写「分摊收款金额」:
+ - 单张结算单分摊上限 = 结算总金额 − 已认领收款金额;
+ - 本次分摊合计必须 ≤ 当前页面的「剩余可认领金额」;
+4. 维护附件(可选)、备注(≤200 字);
+5. 点「确认」。
+
+### 4.3 确认后系统做了什么
+
+| 步骤 | 内容 |
+| --- | --- |
+| 1 | 行锁流水与所选结算单,防止并发超额 |
+| 2 | 校验:每张结算单「已认领 + 本次分摊 ≤ 结算总金额」;流水「已认领 + 本次分摊 ≤ 收款金额」 |
+| 3 | 插入 1 条认领主单 `blade_receipt_claim`(`claim_amount` = 本次分摊合计) |
+| 4 | 按结算单插入 N 条分摊 `blade_receipt_claim_settlement`(记录认领前已认领金额、本次分摊金额) |
+| 5 | 刷新各结算单收款汇总;流水 `claimed_amount` 累加,重算 `claim_status`(0 → unclaimed,< 收款金额 → partial,≥ 收款金额 → claimed) |
+| 6 | 写流水操作记录(认领动作、金额、关联结算单数) |
+| 7 | **事务提交后**按认领主单推合规:C103-9(履约计划金额累加)→ C107-1(认领记录,返回 fdId 落库)→ C108-1(履约流水);任一步失败只落 `blade_receipt_claim_hcs_record` 失败标记,不回滚认领 |
+| 8 | 返回收款流水列表,提示「认领成功」 |
+
+> 合规开关关闭(`exe-stage.sync-enabled` / `perf-record.sync-enabled`)时,认领主流程不受影响,只是不推合规。
+
+---
+
+## 5. 场景三:批量认领
+
+**入口**:列表勾选多条流水 → 顶部「批量认领」(需 `receipt_flow_claim` 权限)。
+
+**前置校验**:所选流水的**付款人 + 对方账号必须完全一致**,否则提示「批量认领所选流水的付款人、对方账号必须一致」。
+
+**操作步骤**:
+
+1. 进入「批量认领收款流水」页面,顶部明细列出所选流水(收款单号、收款金额、已认领金额、未匹配金额、剩余可认领金额、摘要);
+2. 选择关联应收结算单(候选规则同单条认领,多选同样要求同项目/组织/收付款方);
+3. 在「结算单 × 流水」分摊矩阵中逐格填写金额:
+ - 单格上限 = min(该结算单剩余可认领 − 该结算单其它格已填, 该流水剩余可认领 − 该流水其它格已填);
+4. 附件、备注同单条认领;
+5. 点「确认」。
+
+**提交逻辑**:
+
+- 先校验结算单维度(累计分摊 ≤ 结算单剩余)与流水维度(累计分摊 ≤ 流水剩余可认领);
+- 按流水逐条提交认领:**每条流水生成一条认领主单**,各自推一次合规(C103-9 累计金额按各次认领累加);
+- 分摊为 0 的格子不提交;全部为 0 时提示「没有需要入库的分摊收款金额」;
+- 全部成功提示「批量认领成功,共 N 条流水」并返回列表。
+
+> 批量提交是逐条循环调用认领接口,非单一事务:中途某条失败时,之前的流水已认领成功,需要按提示逐条核查(可在「认领记录」页确认已成功的部分)。
+
+---
+
+## 6. 场景四:部分认领与继续认领
+
+1. 首次分摊合计小于收款金额 → 流水状态变「部分认领」(`partial`),剩余可认领金额 = 收款金额 − 累计已认领;
+2. 再次点「认领」进入页面时,会**自动预填本流水历史关联且仍有剩余可认领金额的结算单**(分摊金额默认 0,需重新填写);
+3. 每次认领都是一条新的认领主单,合规侧每次各推一次 C107-1/C108-1,C103-9 按合同累计;
+4. 累计达到收款金额 → 状态「认领完成」,操作列不再显示「认领」。
+
+---
+
+## 7. 场景五:作废认领记录
+
+**入口**:认领记录页(仅本人记录)→ 操作列「作废」(需 `receipt_claim_record_void`,且记录状态为「已认领」)。
+
+**操作**:点击后二次确认,提示「作废后将回退本次认领金额,并同步作废合同合规系统认领记录」。
+
+**作废后系统做了什么**:
+
+| 步骤 | 内容 |
+| --- | --- |
+| 1 | 行锁认领主单、收款流水、关联结算单 |
+| 2 | 本次认领的分摊行批量置为无效(`status = 0`) |
+| 3 | 刷新关联结算单收款汇总 |
+| 4 | 流水 `claimed_amount` 回退本次认领金额(不小于 0),并按收款金额重算状态(可能回到 unclaimed / partial) |
+| 5 | 认领主单置「已作废」,记录作废人、作废时间与操作日志 |
+| 6 | 事务提交后逐条推 C107-3(按落库 fdId 传 `fdStatus=1`),成功记录置 `voided`;单条失败只记日志,**不回滚作废**,需人工在合规侧处置 |
+
+**注意**:
+
+- 已作废记录不可重复作废;
+- 作废不生成金蝶冲销单,金蝶收款单本身不受影响(金蝶侧红冲由财务人工处置);
+- 认领记录允许本人维护附件(`/receipt-claim-record/attachments`)。
+
+---
+
+## 8. 状态与金额口径
+
+### 8.1 流水认领状态
+
+| 状态 | 含义 | 判定 |
+| --- | --- | --- |
+| `unclaimed` 未认领 | 尚未认领 | 累计认领金额 = 0 |
+| `partial` 部分认领 | 已认领一部分 | 0 < 累计认领金额 < 收款金额 |
+| `claimed` 认领完成 | 额度用尽 | 累计认领金额 ≥ 收款金额 |
+
+### 8.2 关键金额
+
+| 金额 | 来源/公式 | 用途 |
+| --- | --- | --- |
+| 收款金额 `receipt_amount` | A301-1 `actrecamt` | 认领上限 |
+| 已认领金额 `claimed_amount` | 本地认领累加(作废回退) | 已用额度 |
+| 剩余可认领金额 `remainingAmount` | `max(收款金额 − 已认领金额, 0)` | 按钮显隐、前置校验 |
+| 未匹配金额 `unmatch_amount_rec` | A301-1 `unmatchamountrec` | 金蝶参考展示,**不参与校验** |
+| 结算单剩余可认领 | 结算总金额 − 该结算单已认领收款金额 | 分摊上限(结算单维度) |
+
+### 8.3 金蝶单据状态(随同步刷新)
+
+| `billstatus` | 本地 `kingdee_bill_status` | 页面展示 |
+| --- | --- | --- |
+| A 暂存 / B 已提交 | `pushed` | 已推送 |
+| C 已审核 | `approved` | 审核通过 |
+| D 已收款 | `received` | 已收款 |
+| G 已退单 / E 变更中 / 其它 | `failed` | 处理失败(需人工在金蝶核查) |
+
+---
+
+## 9. 常见提示与处置
+
+| 提示 | 原因 | 处置 |
+| --- | --- | --- |
+| 操作列显示 `-` | 无 `receipt_flow_claim` 权限,或已认领完成(剩余可认领 = 0) | 配权限;或确认是否已全额认领 |
+| 「该流水可认领金额为0,无需认领」 | 收款金额 ≤ 0(数据异常,正常同步已跳过此类单据) | 核查金蝶收款单金额 |
+| 「累计认领金额不能超过可认领上限,剩余可认领 X」 | 分摊合计超过剩余可认领 | 按提示减少分摊 |
+| 前端「累计认领金额不能超过剩余可认领金额」 | 同上,前端预校验拦截 | 同上 |
+| 「结算单XXX的累计认领金额不能超过结算总应收含税金额」 | 单张结算单分摊超过其剩余 | 调整该结算单分摊 |
+| 「关联结算单必须属于同一项目、组织及收付款方」 | 多选结算单不一致 | 重新选择同项目同收付款方的结算单 |
+| 「批量认领所选流水的付款人、对方账号必须一致」 | 勾选流水不属于同一付款账户 | 拆分批次 |
+| 「组织管理中未维护平台公司金蝶编码…」 | 同步前置缺失 | 维护组织金蝶编码后再同步 |
+| 未匹配金额为 0 但可以认领 | 金蝶侧财务已核销,属正常 | 忽略该列,按剩余可认领操作 |
+| 认领记录页看不到某条记录 | 只显示当前登录人自己的记录 | 由本人查看/作废,或到结算单认领明细核查 |
+| 合规推送失败 | C103-9/C107-1/C108-1 返回异常 | 查 `blade_receipt_claim_hcs_record` 失败原因,人工在合规侧补录(无自动重试) |
+
+---
+
+## 10. 与 2026-10-29 之前的差异
+
+| 项 | 改造前 | 现在 |
+| --- | --- | --- |
+| 数据源 | A301-2 收款认领公告 | A301-1 收款单查询 |
+| 认领上限 | `min(未匹配收款金额, 收款金额)` | **收款金额** |
+| 未匹配金额的作用 | 决定是否可认领(大量单据为 0 → 无法认领) | 仅参考展示 |
+| 认领后 | A301-3 推金蝶(手动/定时) | 只登记本地 + 推合规,金蝶不写 |
+| 推送失败 | 定时重试 | 只落失败标记,人工处理 |
+
+---
+
+## 11. 操作自查清单
+
+1. 同步后列表条数、收款单号、收款金额、金蝶状态与金蝶一致;
+2. 剩余可认领金额 = 收款金额 − 已认领金额,且列表/详情/认领页三处一致;
+3. 单条认领:分摊合计 = 收款金额 → 状态「认领完成」,按钮消失;小于 → 「部分认领」;
+4. 继续认领:剩余额度正确、历史结算单预填、合规侧新增一条认领记录;
+5. 批量认领:同一付款账户多流水一次提交,各自生成认领记录;
+6. 作废:金额回退、状态回退、合规侧记录变为已作废(`blade_receipt_claim_hcs_record.push_status = voided`);
+7. 超额场景(流水维度、结算单维度)均被拦截并给出可读提示。
diff --git a/doc/nacos/blade-dev.yaml b/doc/nacos/blade-dev.yaml
index 6b47a7ca..ad5d667b 100644
--- a/doc/nacos/blade-dev.yaml
+++ b/doc/nacos/blade-dev.yaml
@@ -184,12 +184,18 @@ 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:
# 回写任务开关
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..93d9729e 100644
--- a/doc/nacos/blade-prod.yaml
+++ b/doc/nacos/blade-prod.yaml
@@ -133,12 +133,18 @@ 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:
# 回写任务开关
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 d09370cd..f0680aeb 100644
--- a/doc/nacos/third-party-api.yaml
+++ b/doc/nacos/third-party-api.yaml
@@ -86,12 +86,18 @@ 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:
# 回写任务开关
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:
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='收款认领合规推送记录表';
diff --git a/doc/sql/transport/lqj/20261029/01_blade_kingdee_receipt_flow_org_fields_20261029.sql b/doc/sql/transport/lqj/20261029/01_blade_kingdee_receipt_flow_org_fields_20261029.sql
new file mode 100644
index 00000000..53f31618
--- /dev/null
+++ b/doc/sql/transport/lqj/20261029/01_blade_kingdee_receipt_flow_org_fields_20261029.sql
@@ -0,0 +1,28 @@
+-- 收款流水表增加金蝶收款单组织/公司编码字段(A301-1 收款单查询同步落库)
+-- openorg_number 核算组织.编码、org_number 收款人(公司).编码;org_name 已由 payee_name 承载不加列
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `org_number` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''收款人(公司).编码(A301-1 org_number)'' AFTER `payee_name`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_kingdee_receipt_flow'
+ AND COLUMN_NAME = 'org_number'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `open_org_number` varchar(50) COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ''核算组织.编码(A301-1 openorg_number)'' AFTER `org_number`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_kingdee_receipt_flow'
+ AND COLUMN_NAME = 'open_org_number'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
diff --git a/doc/sql/transport/lqj/20261029/02_blade_kingdee_receipt_flow_match_settle_amounts_20261029.sql b/doc/sql/transport/lqj/20261029/02_blade_kingdee_receipt_flow_match_settle_amounts_20261029.sql
new file mode 100644
index 00000000..3dcd5bfa
--- /dev/null
+++ b/doc/sql/transport/lqj/20261029/02_blade_kingdee_receipt_flow_match_settle_amounts_20261029.sql
@@ -0,0 +1,67 @@
+-- 收款流水表增加金蝶收款单匹配/结算金额字段(A301-1 收款单查询同步落库)
+-- unmatchamountrec 未匹配收款金额已由 unclaimed_amount 承载,不重复加列
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `match_amount_rec` decimal(19, 2) DEFAULT NULL COMMENT ''已匹配收款金额(A301-1 matchamountrec)'' AFTER `unclaimed_amount`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_kingdee_receipt_flow'
+ AND COLUMN_NAME = 'match_amount_rec'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `match_amount_pay` decimal(19, 2) DEFAULT NULL COMMENT ''已匹配付款金额(A301-1 matchamountpay)'' AFTER `match_amount_rec`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_kingdee_receipt_flow'
+ AND COLUMN_NAME = 'match_amount_pay'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `unmatch_amount_pay` decimal(19, 2) DEFAULT NULL COMMENT ''未匹配付款金额(A301-1 unmatchamountpay)'' AFTER `match_amount_pay`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_kingdee_receipt_flow'
+ AND COLUMN_NAME = 'unmatch_amount_pay'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `settle_amount` decimal(19, 2) DEFAULT NULL COMMENT ''已结算金额(A301-1 settleamount)'' AFTER `unmatch_amount_pay`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_kingdee_receipt_flow'
+ AND COLUMN_NAME = 'settle_amount'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `blade_kingdee_receipt_flow` ADD COLUMN `unsettle_amount` decimal(19, 2) DEFAULT NULL COMMENT ''未结算金额(A301-1 unsettleamount)'' AFTER `settle_amount`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_kingdee_receipt_flow'
+ AND COLUMN_NAME = 'unsettle_amount'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;
diff --git a/doc/sql/transport/lqj/20261029/03_blade_kingdee_receipt_flow_unmatch_amount_rec_rename_20261029.sql b/doc/sql/transport/lqj/20261029/03_blade_kingdee_receipt_flow_unmatch_amount_rec_rename_20261029.sql
new file mode 100644
index 00000000..23ba1916
--- /dev/null
+++ b/doc/sql/transport/lqj/20261029/03_blade_kingdee_receipt_flow_unmatch_amount_rec_rename_20261029.sql
@@ -0,0 +1,15 @@
+-- 收款流水表未匹配收款金额列改名:unclaimed_amount(A301-2 公告未认领金额遗留列名)
+-- → unmatch_amount_rec(与 A301-1 字段口径及其他匹配金额列命名统一)
+
+SET @ddl_sql = (
+ SELECT IF(COUNT(*) = 1,
+ 'ALTER TABLE `blade_kingdee_receipt_flow` RENAME COLUMN `unclaimed_amount` TO `unmatch_amount_rec`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = DATABASE()
+ AND TABLE_NAME = 'blade_kingdee_receipt_flow'
+ AND COLUMN_NAME = 'unclaimed_amount'
+);
+PREPARE ddl_stmt FROM @ddl_sql;
+EXECUTE ddl_stmt;
+DEALLOCATE PREPARE ddl_stmt;