From 5c8324b8a6ea0f765926d60228342907b71f1299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Thu, 22 Oct 2026 19:10:22 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E9=87=91=E8=9D=B6=E8=AE=A4=E9=A2=86?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E5=AE=8C=E6=88=90=E5=90=8E=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E5=90=88=E5=90=8C=E5=90=88=E8=A7=84=E7=B3=BB=E7=BB=9F=EF=BC=88?= =?UTF-8?q?C103-9/C107-1/C108-1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 HcsClaimSyncSupport 认领链编排:金蝶认领推送(A301-3)成功后按合同分组推送 ① C103-9 认领金额原子累加收款计划行并重推(未推送合规随合同链路首推) ② C107-1 按「认领主单×合同」推送认领记录(合规流水ID经C104-2定位) ③ C108-1 按合同新增履约流水(fd_plan_id=收款计划行合规阶段ID) - IContractExeStageService 新增 findCollectionStageRow / accumulateClaimedAmountOnPush - ReceiptFlowServiceImpl.pushClaimLocked 事务提交后挂钩,失败仅日志不影响金蝶推送结果 - 单合同失败不影响其余合同;开关关闭零开销 --- .../service/IContractExeStageService.java | 20 + .../impl/ContractExeStageServiceImpl.java | 54 +++ .../service/impl/ReceiptFlowServiceImpl.java | 11 +- .../support/hcs/HcsClaimSyncSupport.java | 370 ++++++++++++++++++ 4 files changed, 453 insertions(+), 2 deletions(-) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractExeStageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractExeStageService.java index 30c85811..6c8937b7 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractExeStageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractExeStageService.java @@ -30,6 +30,7 @@ import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.HcsContractExeStage; import org.springblade.transport.pojo.entity.PaymentApplication; +import java.math.BigDecimal; import java.util.List; /** @@ -56,6 +57,25 @@ public interface IContractExeStageService extends BaseServicelambdaQuery() + .eq(HcsContractExeStage::getContractId, contractId) + .eq(HcsContractExeStage::getPerformType, HcsSyncConstant.PERFORM_TYPE_COLLECTION) + .eq(HcsContractExeStage::getFundNature, fundNature) + .isNotNull(HcsContractExeStage::getHcsStageFdId) + .orderByAsc(HcsContractExeStage::getId) + .last("limit 1"), false); + if (natureRow != null) { + return natureRow; + } + } + // 兜底:无该款项性质已同步行时取首条已同步收款行 + return getOne(Wrappers.lambdaQuery() + .eq(HcsContractExeStage::getContractId, contractId) + .eq(HcsContractExeStage::getPerformType, HcsSyncConstant.PERFORM_TYPE_COLLECTION) + .isNotNull(HcsContractExeStage::getHcsStageFdId) + .orderByAsc(HcsContractExeStage::getId) + .last("limit 1"), false); + } + + @Override + public void accumulateClaimedAmountOnPush(Long contractId, BigDecimal claimedAmount) { + if (contractId == null || claimedAmount == null || claimedAmount.signum() <= 0) { + return; + } + HcsContractExeStage stageRow = this.findCollectionStageRow(contractId, HcsSyncConstant.FUND_NATURE_PROGRESS); + if (stageRow == null) { + log.info("收款认领未匹配到已同步收款履约计划行,跳过款项金额累加:contractId={}", contractId); + return; + } + // 原子累加防并发认领推送丢失更新(coalesce 兼容多数据库) + update(Wrappers.lambdaUpdate() + .eq(HcsContractExeStage::getId, stageRow.getId()) + .setSql("fund_amount = coalesce(fund_amount, 0) + " + claimedAmount.toPlainString())); + boolean resyncNeeded = StringUtil.isNotBlank(stageRow.getHcsStageFdId()); + if (resyncNeeded) { + // 已同步行置 update 凭已存合规阶段ID 随 C103-9 重推;未同步行保持 add 待合同链路首推 + update(Wrappers.lambdaUpdate() + .eq(HcsContractExeStage::getId, stageRow.getId()) + .set(HcsContractExeStage::getRowOp, HcsSyncConstant.ROW_OP_UPDATE) + .set(HcsContractExeStage::getHcsSyncStatus, HcsSyncConstant.HCS_STATUS_UNSYNCED) + .set(HcsContractExeStage::getHcsSyncMsg, "收款认领金额累加,待重推C103-9")); + } + log.info("收款认领履约计划款项金额累加:contractId={},stageRowId={},累加金额={},rowOp={}", + contractId, stageRow.getId(), claimedAmount, resyncNeeded ? "update" : "add(待合同链路首推)"); + } + @Override @Transactional(rollbackFor = Exception.class) public boolean submitRows(Long contractId, List rows) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java index e6c6aecd..899315a4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java @@ -65,6 +65,7 @@ import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IReceiptFlowService; import org.springblade.transport.support.KingdeeSyncLockSupport; import org.springblade.transport.support.TransportBusinessSupport; +import org.springblade.transport.support.hcs.HcsClaimSyncSupport; import org.springblade.transport.wrapper.ReceiptFlowWrapper; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -130,6 +131,7 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl selectPage(IPage page, ReceiptFlowVO query) { @@ -904,10 +906,15 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl doPushClaimToKingdee(flowId, announce)); + ReceiptClaimPushResult result = transactionTemplate.execute(status -> doPushClaimToKingdee(flowId, announce)); + if (result != null && result.isSuccess()) { + hcsClaimSyncSupport.pushClaimToHcs(flowId); + } + return result; } /** diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java new file mode 100644 index 00000000..f53798b4 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsClaimSyncSupport.java @@ -0,0 +1,370 @@ +/** + * 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.support.hcs; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import org.springblade.core.tool.utils.Func; +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.ReceiptClaimMapper; +import org.springblade.transport.mapper.ReceiptClaimSettlementMapper; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.HcsContractExeStage; +import org.springblade.transport.pojo.entity.KingdeeReceiptFlow; +import org.springblade.transport.pojo.entity.ReceiptClaim; +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.ZoneId; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * HCS 收款认领链推送编排支撑组件(C103-9 更新履约计划 + C107-1 认领记录 + C108-1 履约流水) + *

+ * 触发点:金蝶认领推送(A301-3)成功之后(手动按钮与定时任务共用钩子),开关 thirdParty.hcs.claim.sync-enabled。 + * 编排顺序:先 C103-9(认领金额累加到收款履约计划行并重推,C107-1/C108-1 引用其返回的阶段ID), + * 再按「认领主单 × 合同」推 C107-1(合规流水ID 经 C104-2 按明细流水号定位),最后按合同推 C108-1 履约流水。 + * 合同未推送合规(hcs_fd_id 空)时仅累加计划行金额,随合同链路首推。 + * 推送失败仅记日志不回滚金蝶推送结果(单合同失败不影响其余合同)。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HcsClaimSyncSupport { + + /** + * 认领完成状态(与 ReceiptFlowServiceImpl.CLAIMED 同值) + */ + private static final String CLAIMED = "claimed"; + + private final IHcsClient hcsClient; + private final HcsProperties hcsProperties; + private final KingdeeReceiptFlowMapper receiptFlowMapper; + private final ReceiptClaimMapper claimMapper; + private final ReceiptClaimSettlementMapper claimSettlementMapper; + private final FormalSettlementMapper formalSettlementMapper; + private final ContractManageMapper contractManageMapper; + private final IContractExeStageService contractExeStageService; + private final HcsExeStageSyncSupport hcsExeStageSyncSupport; + + /** + * 金蝶认领推送成功后推送合同合规(C103-9 → C107-1 → C108-1) + *

+ * 开关关闭或推送异常均静默返回,不影响金蝶认领推送主流程与事务。 + * + * @param flowId 收款流水ID + */ + public void pushClaimToHcs(Long flowId) { + if (!hcsProperties.getClaim().isSyncEnabled()) { + return; + } + try { + doPushClaimToHcs(flowId); + } catch (Exception exception) { + log.error("HCS收款认领链推送异常 flowId:{}", flowId, exception); + } + } + + /** + * 推送主体:归集有效认领分摊(按合同分组)→ 逐合同推送(单合同失败不影响其余合同) + */ + private void doPushClaimToHcs(Long flowId) { + KingdeeReceiptFlow flow = receiptFlowMapper.selectById(flowId); + if (flow == null) { + return; + } + // 未作废认领主单(作废认领不同步合规) + List claims = claimMapper.selectList(Wrappers.lambdaQuery() + .eq(ReceiptClaim::getReceiptFlowId, flowId) + .eq(ReceiptClaim::getClaimStatus, CLAIMED)); + if (claims.isEmpty()) { + return; + } + // 有效分摊行(作废时已置 status=0) + List allocations = claimSettlementMapper.selectList( + Wrappers.lambdaQuery() + .eq(ReceiptClaimSettlement::getReceiptFlowId, flowId) + .eq(ReceiptClaimSettlement::getStatus, 1) + .eq(ReceiptClaimSettlement::getIsDeleted, 0)); + if (allocations.isEmpty()) { + return; + } + // 结算单批量定位合同(一次 IN 查询,避免逐行 N+1) + Map settlements = formalSettlementMapper.selectBatchIds( + 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<>(); + for (ReceiptClaimSettlement allocation : allocations) { + FormalSettlement settlement = settlements.get(allocation.getFormalSettlementId()); + if (settlement == null || settlement.getContractId() == null + || allocation.getReceiptClaimId() == null || allocation.getAllocatedReceiptAmount() == null) { + continue; + } + amountByContractByClaim + .computeIfAbsent(settlement.getContractId(), key -> new LinkedHashMap<>()) + .merge(allocation.getReceiptClaimId(), allocation.getAllocatedReceiptAmount(), BigDecimal::add); + } + if (amountByContractByClaim.isEmpty()) { + log.info("HCS收款认领链推送跳过:分摊行未关联合同 flowId={}", flowId); + return; + } + // 合规侧收款流水ID(C107-1 必填引用,一次定位全部合同共用;定位失败仅跳过 C107-1,不阻塞履约计划更新) + String hcsCollectionFlowId = null; + try { + hcsCollectionFlowId = locateHcsCollectionFlowId(flow); + } catch (Exception exception) { + log.error("HCS收款流水定位失败 flowId:{},跳过C107-1认领记录推送", flowId, exception); + } + for (Map.Entry> contractEntry : amountByContractByClaim.entrySet()) { + try { + pushOneContract(flow, claims, contractEntry.getKey(), contractEntry.getValue(), hcsCollectionFlowId); + } catch (Exception exception) { + log.error("HCS收款认领链推送失败 flowId:{},contractId:{}", flowId, contractEntry.getKey(), exception); + } + } + } + + /** + * 推送单个合同:① C103-9 更新履约计划(认领金额累加重推)→ ② C107-1 认领记录(按认领主单分组)→ ③ C108-1 履约流水 + */ + private void pushOneContract(KingdeeReceiptFlow flow, List claims, Long contractId, + Map amountByClaim, 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); + HcsContractExeStage stageRow = contractExeStageService.findCollectionStageRow(contractId, HcsSyncConstant.FUND_NATURE_PROGRESS); + if (StringUtil.isNotBlank(contractManage.getHcsFdId())) { + // 行级重推(无待同步行返回提示,不视为失败);合同未推送合规时随合同链路首推 + hcsExeStageSyncSupport.syncExeStages(contractManage); + } + // ② C107-1 认领记录:按「认领主单 × 合同」推送,金额为该认领主单分摊到本合同的金额 + if (StringUtil.isBlank(hcsCollectionFlowId)) { + log.warn("HCS认领记录推送跳过:未定位到合规收款流水 flowId={},detailSerialNo={}", flow.getId(), flow.getDetailSerialNo()); + } 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); + } + } + // ③ C108-1 履约流水:按合同一条,须已推送合规且存在已同步收款计划行(fd_plan_id 必填) + if (StringUtil.isBlank(contractManage.getHcsFdId())) { + log.info("HCS履约流水推送跳过:合同尚未推送合规 contractNo={}", contractManage.getContractNo()); + return; + } + if (stageRow == null || StringUtil.isBlank(stageRow.getHcsStageFdId())) { + log.info("HCS履约流水推送跳过:无已同步收款履约计划行 contractNo={}", contractManage.getContractNo()); + return; + } + pushPerfRecord(flow, contractManage, stageRow, claimedTotal, latestClaimDate(claims)); + } + + /** + * C107-1 认领记录推送(单认领主单 × 单合同) + */ + private void pushClaimRecord(KingdeeReceiptFlow flow, ReceiptClaim claim, ContractManage contractManage, + HcsContractExeStage stageRow, BigDecimal claimAmount, String hcsCollectionFlowId) { + if (claim == null || claimAmount == null || claimAmount.signum() <= 0) { + return; + } + HcsClaimRecordAddDTO param = new HcsClaimRecordAddDTO(); + // 【必填】收款流水引用(合规侧流水ID,C104-2 按明细流水号定位) + HcsRefVO collectionFlow = new HcsRefVO(); + collectionFlow.setFdId(hcsCollectionFlowId); + param.setFdCollectionFlow(collectionFlow); + // 认领合同引用(fdId 为合规合同ID) + HcsRefVO contractRef = new HcsRefVO(); + contractRef.setFdId(contractManage.getHcsFdId()); + contractRef.setFdName(contractManage.getContractName()); + param.setFdContract(contractRef); + // 【必填】认领合同编号(须在合规系统本地存在) + param.setFdContractNumber(contractManage.getContractNo()); + // 对应履约项ID 与 认领款项类型(取合同收款计划行,无已同步行不传) + if (stageRow != null) { + param.setFdExePlanStageId(stageRow.getHcsStageFdId()); + param.setFdFundNature(stageRow.getFundNature()); + } + // 认领人/部门:TMS 与合规用户不同源,仅传名称(§9-U2) + param.setFdClaimer(HcsRefVO.ofName(claim.getClaimerName())); + param.setFdClaimerName(claim.getClaimerName()); + param.setFdClaimerDept(HcsRefVO.ofName(claim.getClaimerDeptName())); + param.setFdClaimerDeptName(claim.getClaimerDeptName()); + // 【必填】认领金额 + param.setFdClaimAmount(claimAmount); + param.setFdClaimDate(toMillis(claim.getClaimDate())); + param.setFdRemark(claim.getRemark()); + // 状态 0=已认领(仅推送未作废认领) + param.setFdStatus(0); + log.info("HCS认领记录推送开始 flowId:{},claimId:{},contractNo:{},认领金额 {}", + flow.getId(), claim.getId(), contractManage.getContractNo(), claimAmount); + HcsResult result = hcsClient.claimRecordAdd(param); + if (result == null || !result.isSuccess()) { + 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 未实现,重推会重复);需作废联动时先落库 + log.info("HCS认领记录推送成功 flowId:{},claimId:{},contractNo:{},claimRecordFdId:{}", + flow.getId(), claim.getId(), contractManage.getContractNo(), + result.getData() == null ? "响应未返回" : result.getData().getFdId()); + } + + /** + * C108-1 履约流水推送(单合同一条:申请金额=实际金额=该合同累计认领金额) + */ + private void pushPerfRecord(KingdeeReceiptFlow flow, ContractManage contractManage, + HcsContractExeStage stageRow, BigDecimal claimedTotal, LocalDate planDate) { + // 表单模板ID(配置 perf-record.xform-id,环境值 §9-U9,未配置不传) + String xformId = StringUtil.isBlank(hcsProperties.getPerfRecord().getXformId()) ? null : hcsProperties.getPerfRecord().getXformId(); + HcsPerfRecordPropsDTO props = new HcsPerfRecordPropsDTO(); + // 【必填】申请编号:收款通知单号,兜底明细流水号 + props.setFdApplyNo(firstNonBlank(flow.getReceiptNoticeNo(), flow.getDetailSerialNo())); + // 申请金额 / 实际金额:该合同累计认领金额 + props.setFdApplyAmount(claimedTotal.toPlainString()); + props.setFdFactAmount(claimedTotal.toPlainString()); + // 【必填】履约日期:最近认领日期(毫秒时间戳) + props.setFdPlanDate(toMillis(planDate)); + // 履约类型:收款 + props.setFdPalnType(HcsSyncConstant.PERFORM_TYPE_COLLECTION); + // 履约性质:收款计划行款项性质 + props.setFdProperty(stageRow.getFundNature()); + // 履约计划id / 来源履约计划项ID:收款计划行合规阶段ID(HCS 只认自己的阶段ID) + props.setFdId(stageRow.getHcsStageFdId()); + props.setFdPlanId(stageRow.getHcsStageFdId()); + // 金蝶单据号:A301-3 推送返回的金蝶收款单号 + props.setFdJdDocNo(flow.getKingdeeBillNo()); + // 【必填】合同ID:HCS 合同ID + props.setFdContractId(contractManage.getHcsFdId()); + // 业务ID:TMS 收款流水ID(溯源) + props.setFdBusinessId(String.valueOf(flow.getId())); + props.setFdCreateTime(System.currentTimeMillis()); + props.setFdXformId(xformId); + HcsPerfRecordFormDataDTO formData = new HcsPerfRecordFormDataDTO(); + formData.setFdXformId(xformId); + formData.setDynamicProps(props); + // 机制数据(C108-1 必填):认领附件暂不转传,传空列表 + HcsMechanismsDTO mechanisms = new HcsMechanismsDTO(); + mechanisms.setAttachment(List.of()); + HcsPerfRecordSaveDTO param = new HcsPerfRecordSaveDTO(); + param.setCurUser(HcsUserRefVO.ofLoginName(hcsProperties.getOperatorLoginName())); + param.setFormData(formData); + param.setMechanisms(mechanisms); + log.info("HCS履约流水新建开始 flowId:{},contractNo:{},认领金额 {}", flow.getId(), contractManage.getContractNo(), claimedTotal); + HcsResult result = hcsClient.perfRecordAdd(param); + if (result == null || !result.isSuccess() || result.getData() == null || StringUtil.isBlank(result.getData().getFdId())) { + log.error("HCS履约流水新建失败响应:{},contractNo={}", result == null ? "无响应" : String.format("msg=%s, traceId=%s", result.getMsg(), result.getTraceId()), contractManage.getContractNo()); + throw new HcsException("C108-1 履约流水新建失败:" + (result == null ? "响应为空" : result.getMsg())); + } + // shortcut: 履约流水 fdId 未持久化(认领链无 C108-2 更新场景);需推进实际金额更新时先落库 + log.info("HCS履约流水新建成功 flowId:{},contractNo:{},perfFdId:{}", + flow.getId(), contractManage.getContractNo(), result.getData().getFdId()); + } + + /** + * C104-2 定位合规侧收款流水ID:按明细流水号精确查询(合规流水由合规自 ERP 同步,TMS 镜像同源) + */ + private String locateHcsCollectionFlowId(KingdeeReceiptFlow flow) { + if (StringUtil.isBlank(flow.getDetailSerialNo())) { + log.warn("HCS收款流水定位跳过:流水缺少明细流水号 flowId={}", flow.getId()); + return null; + } + HcsPageQueryDTO param = new HcsPageQueryDTO(); + param.setColumns(List.of("fdId", "fdClaimNotice", "fdDetailSerialNumber", "fdCollectionAmount", "fdClaimStatus", "fdClaimedAmount")); + param.setConditions(Map.of("fdDetailSerialNumber", flow.getDetailSerialNo())); + param.setOffset(0); + param.setPageSize(10); + HcsResult> result = hcsClient.collectionFlowList(param); + if (result == null || !result.isSuccess() || result.getData() == null) { + throw new HcsException("C104-2 收款流水查询失败:" + (result == null ? "响应为空" : result.getMsg())); + } + return result.getData().getContent().stream() + .filter(item -> flow.getDetailSerialNo().equals(item.getFdDetailSerialNumber())) + .map(HcsCollectionFlowVO::getFdId) + .filter(StringUtil::isNotBlank) + .findFirst().orElse(null); + } + + /** + * 最近认领日期(C108-1 履约日期) + */ + private LocalDate latestClaimDate(List claims) { + return claims.stream() + .map(ReceiptClaim::getClaimDate) + .filter(Objects::nonNull) + .max(LocalDate::compareTo) + .orElse(LocalDate.now()); + } + + private String firstNonBlank(String... values) { + for (String value : values) { + if (Func.isNotEmpty(value)) { + return value.trim(); + } + } + return null; + } + + private Long toMillis(LocalDate localDate) { + return localDate == null ? null : localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } +}