From bd0c4d098156a99a347111d4cdc5df73edf16538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Wed, 28 Oct 2026 19:30:59 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=B1=A5=E7=BA=A6?= =?UTF-8?q?=E5=BC=80=E7=A5=A8=E8=AE=B0=E5=BD=95=E4=B8=8E=E5=B1=A5=E7=BA=A6?= =?UTF-8?q?=E6=94=B6=E7=A5=A8=E8=AE=B0=E5=BD=95=E6=8E=A8=E9=80=81=E9=93=BE?= =?UTF-8?q?=E8=B7=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/HcsSyncController.java | 5 +- .../hcs/HcsInvoicePerfSyncSupport.java | 222 ++++++++++++++++++ .../hcs/HcsReceiptPerfSyncSupport.java | 211 +++++++++++++++++ .../transport/task/HcsPushTask.java | 30 ++- 4 files changed, 462 insertions(+), 6 deletions(-) create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java index 220ce98b..ea8c0527 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/HcsSyncController.java @@ -130,8 +130,9 @@ public class HcsSyncController { case HcsSyncConstant.BIZ_TYPE_PROJECT -> hcsPushTask.pushUnsyncedProjects(); case HcsSyncConstant.BIZ_TYPE_CONTRACT -> hcsPushTask.pushUnsyncedContracts(); case HcsSyncConstant.BIZ_TYPE_PAYMENT -> hcsPushTask.pushUnsyncedPayments(); - case HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION -> hcsPushTask.pushUnsyncedInvoiceApplications(); - case HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT -> hcsPushTask.pushUnsyncedInvoiceReceipts(); + // 开票/收票走组合动作(C111/C109 + 履约记录 C108-1),与定时任务行为一致 + case HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION -> hcsPushTask.syncHcsInvoiceApplications(); + case HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT -> hcsPushTask.syncHcsInvoiceReceipts(); default -> throw new IllegalArgumentException("不支持的HCS推送业务类型:" + bizType); }; return R.data(result); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java new file mode 100644 index 00000000..a831adcc --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsInvoicePerfSyncSupport.java @@ -0,0 +1,222 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support.hcs; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.Func; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.hcs.config.HcsProperties; +import org.springblade.hcs.exception.HcsException; +import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.transport.mapper.ContractManageMapper; +import org.springblade.transport.mapper.InvoiceApplicationInvoiceMapper; +import org.springblade.transport.mapper.InvoiceApplicationMapper; +import org.springblade.transport.mapper.InvoiceApplicationSettlementMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.HcsContractExeStage; +import org.springblade.transport.pojo.entity.InvoiceApplication; +import org.springblade.transport.pojo.entity.InvoiceApplicationInvoice; +import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement; +import org.springblade.transport.service.IContractExeStageService; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.ZoneId; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * HCS 履约开票记录同步支撑组件(C108-1,需求:金蝶开票完成后推送合同系统) + *

+ * 粒度:金蝶回写的每张发票(blade_invoice_application_invoice)对应一条履约开票记录; + * 触发:A303-5 开票结果回写落库发票后,随开票登记扫描任务(HcsPushTask invoice-application)推送; + * 履约日期为发票完成开票日期(invoice_date),履约类型 collection(收款方向,关联合同收款履约计划行); + * 合同经应收结算链路回溯(开票申请 → 应收结算关联 → 正式结算单合同,同 C111 §9-U10 口径); + * 幂等凭 hcs_perf_fd_id(非空跳过),失败置重试计数与原因由下轮扫描自动补偿。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HcsInvoicePerfSyncSupport { + + private final HcsProperties hcsProperties; + private final InvoiceApplicationInvoiceMapper invoiceApplicationInvoiceMapper; + private final InvoiceApplicationMapper invoiceApplicationMapper; + private final InvoiceApplicationSettlementMapper invoiceApplicationSettlementMapper; + private final FormalSettlementMapper formalSettlementMapper; + private final ContractManageMapper contractManageMapper; + private final IContractExeStageService contractExeStageService; + private final HcsPerfRecordAddSupport hcsPerfRecordAddSupport; + + /** + * 扫描并推送待同步的履约开票记录(每张发票一条 C108-1),返回结果汇总 + * + * @return 汇总描述 + */ + public String pushUnsyncedInvoicePerfRecords() { + if (!hcsProperties.getPerfRecord().isSyncEnabled()) { + return "履约记录同步开关未开启"; + } + int maxRetry = hcsProperties.getPush().getMaxRetry(); + // 待推送发票:未建过履约记录且重试未超限 + List invoiceList = invoiceApplicationInvoiceMapper.selectList( + Wrappers.lambdaQuery() + .isNull(InvoiceApplicationInvoice::getHcsPerfFdId) + .and(wrapper -> wrapper.isNull(InvoiceApplicationInvoice::getHcsPerfRetryCount) + .or().lt(InvoiceApplicationInvoice::getHcsPerfRetryCount, maxRetry)) + .last("limit " + hcsProperties.getPush().getBatchSize())); + if (invoiceList.isEmpty()) { + return "履约开票记录:无待推送发票"; + } + // 批量装配上下文(申请单/合同/计划行,一次 IN 查询避免逐行 N+1) + Map applicationById = loadApplications(invoiceList); + Map contractByApplicationId = loadContracts(List.copyOf(applicationById.values())); + int successCount = 0; + int failCount = 0; + for (InvoiceApplicationInvoice invoice : invoiceList) { + try { + pushOneInvoice(invoice, applicationById.get(invoice.getInvoiceApplicationId()), + contractByApplicationId.get(invoice.getInvoiceApplicationId())); + successCount++; + } catch (Exception exception) { + markFailed(invoice, StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage()); + failCount++; + } + } + String summary = String.format("履约开票记录:扫描 %d 张,成功 %d 张,失败 %d 张(重试上限 %d)", + invoiceList.size(), successCount, failCount, maxRetry); + if (failCount > 0) { + log.warn("HCS履约开票记录推送存在失败:{}", summary); + } + return summary; + } + + /** + * 推送单张发票的履约开票记录 + */ + private void pushOneInvoice(InvoiceApplicationInvoice invoice, InvoiceApplication application, ContractManage contractManage) { + if (application == null || !"approved".equals(application.getApprovalStatus())) { + throw new HcsException("开票申请不存在或未审批通过:invoiceApplicationId=" + invoice.getInvoiceApplicationId()); + } + if (contractManage == null) { + throw new HcsException("应收结算链路回溯不到合同(applicationNo=" + application.getApplicationNo() + ",同 C111 §9-U10 口径)"); + } + // 关联合同的收款履约计划行(fd_plan_id 必填,无已同步计划行时通用组件抛出重试,合同链路首推后自愈) + HcsContractExeStage stageRow = contractExeStageService.findCollectionStageRow( + contractManage.getId(), HcsSyncConstant.FUND_NATURE_PROGRESS); + BigDecimal amount = invoice.getInvoiceAmount() == null ? BigDecimal.ZERO : invoice.getInvoiceAmount(); + String perfFdId = hcsPerfRecordAddSupport.pushPerfRecordAdd(contractManage, stageRow, + invoice.getInvoiceNo(), amount, toMillis(invoice.getInvoiceDate()), + HcsSyncConstant.PERFORM_TYPE_COLLECTION, invoice.getSerialNo(), invoice.getId(), + "开票履约-" + invoice.getInvoiceNo()); + markSynced(invoice, perfFdId); + } + + /** + * 批量装配发票所属开票申请(过滤已作废) + */ + private Map loadApplications(List invoiceList) { + List applicationIds = invoiceList.stream() + .map(InvoiceApplicationInvoice::getInvoiceApplicationId).filter(Objects::nonNull).distinct().toList(); + if (applicationIds.isEmpty()) { + return Map.of(); + } + return invoiceApplicationMapper.selectBatchIds(applicationIds).stream() + .collect(Collectors.toMap(InvoiceApplication::getId, item -> item, (first, second) -> first)); + } + + /** + * 批量回溯合同:申请单 → 应收结算关联 → 正式结算单合同(一次 IN 查询,同 C111 §9-U10 口径) + */ + private Map loadContracts(List applicationList) { List applicationIds = applicationList.stream().map(InvoiceApplication::getId).toList(); + if (applicationIds.isEmpty()) { + return Map.of(); + } + Map relationByApplicationId = invoiceApplicationSettlementMapper.selectList( + Wrappers.lambdaQuery() + .in(InvoiceApplicationSettlement::getInvoiceApplicationId, applicationIds)).stream() + .filter(relation -> relation.getFormalSettlementId() != null) + .collect(Collectors.toMap(InvoiceApplicationSettlement::getInvoiceApplicationId, relation -> relation, + (first, second) -> first, LinkedHashMap::new)); + if (relationByApplicationId.isEmpty()) { + return Map.of(); + } + Map settlementById = formalSettlementMapper.selectBatchIds( + relationByApplicationId.values().stream() + .map(InvoiceApplicationSettlement::getFormalSettlementId).distinct().toList()).stream() + .filter(settlement -> settlement.getContractId() != null) + .collect(Collectors.toMap(FormalSettlement::getId, item -> item, (first, second) -> first)); + if (settlementById.isEmpty()) { + return Map.of(); + } + Map contractById = contractManageMapper.selectBatchIds( + settlementById.values().stream().map(FormalSettlement::getContractId).distinct().toList()).stream() + .collect(Collectors.toMap(ContractManage::getId, item -> item, (first, second) -> first)); + Map contractByApplicationId = new LinkedHashMap<>(); + relationByApplicationId.forEach((applicationId, relation) -> { + FormalSettlement settlement = settlementById.get(relation.getFormalSettlementId()); + if (settlement != null) { + contractByApplicationId.put(applicationId, contractById.get(settlement.getContractId())); + } + }); + return contractByApplicationId; + } + + /** + * 推送成功回填:履约记录ID 立即持久化(幂等键),清失败原因 + */ + private void markSynced(InvoiceApplicationInvoice invoice, String perfFdId) { + invoice.setHcsPerfFdId(perfFdId); + invoice.setHcsPerfRetryCount(0); + invoice.setHcsPerfSyncMsg(null); + invoiceApplicationInvoiceMapper.updateById(invoice); + log.info("HCS履约开票记录推送成功:invoiceNo={},perfFdId={}", invoice.getInvoiceNo(), perfFdId); + } + + /** + * 推送失败回写:重试次数+1 + 失败原因(下轮扫描自动补偿) + */ + private void markFailed(InvoiceApplicationInvoice invoice, String message) { + invoice.setHcsPerfRetryCount(invoice.getHcsPerfRetryCount() == null ? 1 : invoice.getHcsPerfRetryCount() + 1); + invoice.setHcsPerfSyncMsg(HcsSyncConstant.truncateMsg(message)); + invoiceApplicationInvoiceMapper.updateById(invoice); + log.error("HCS履约开票记录推送失败:invoiceNo={},retryCount={},{}", invoice.getInvoiceNo(), invoice.getHcsPerfRetryCount(), message); + } + + private Long toMillis(java.time.LocalDateTime localDateTime) { + return localDateTime == null ? null + : localDateTime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java new file mode 100644 index 00000000..96e37fde --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/hcs/HcsReceiptPerfSyncSupport.java @@ -0,0 +1,211 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is + * not liable for any claims arising from secondary or illegal development. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.support.hcs; + +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springblade.core.tool.utils.StringUtil; +import org.springblade.hcs.config.HcsProperties; +import org.springblade.hcs.exception.HcsException; +import org.springblade.transport.constant.HcsSyncConstant; +import org.springblade.transport.mapper.ContractManageMapper; +import org.springblade.transport.mapper.FormalSettlementMapper; +import org.springblade.transport.mapper.InvoiceReceiptMapper; +import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.FormalSettlement; +import org.springblade.transport.pojo.entity.HcsContractExeStage; +import org.springblade.transport.pojo.entity.InvoiceReceipt; +import org.springblade.transport.pojo.entity.InvoiceReceiptSettlement; +import org.springblade.transport.service.IContractExeStageService; +import org.springframework.stereotype.Component; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.ZoneId; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * HCS 履约收票记录同步支撑组件(C108-1,需求:收票单每条结算信息对应一条履约收票记录) + *

+ * 粒度:收票登记结算单分摊行(blade_invoice_receipt_settlement)对应一条履约收票记录; + * 触发:收票单推送合规(C109)成功后,随收票单扫描任务(HcsPushTask invoice-receipt)推送, + * 扫描范围限定所属收票单 hcs_sync_status=synced(C109 已推送),失败下轮自动补偿; + * 履约日期为发票日期(收票单 invoice_date),履约类型 payment(付款方向,关联合同付款履约计划行); + * 合同经应付结算链路回溯(结算分摊行 → 正式结算单合同); + * 幂等凭 hcs_perf_fd_id(非空跳过),失败置重试计数与原因由下轮扫描自动补偿。 + * + * @author Chill + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class HcsReceiptPerfSyncSupport { + + private final HcsProperties hcsProperties; + private final InvoiceReceiptSettlementMapper invoiceReceiptSettlementMapper; + private final InvoiceReceiptMapper invoiceReceiptMapper; + private final FormalSettlementMapper formalSettlementMapper; + private final ContractManageMapper contractManageMapper; + private final IContractExeStageService contractExeStageService; + private final HcsPerfRecordAddSupport hcsPerfRecordAddSupport; + + /** + * 扫描并推送待同步的履约收票记录(每条结算信息一条 C108-1),返回结果汇总 + * + * @return 汇总描述 + */ + public String pushUnsyncedReceiptPerfRecords() { + if (!hcsProperties.getPerfRecord().isSyncEnabled()) { + return "履约记录同步开关未开启"; + } + int maxRetry = hcsProperties.getPush().getMaxRetry(); + // 待推送结算信息:未建过履约记录且重试未超限 + List settlementList = invoiceReceiptSettlementMapper.selectList( + Wrappers.lambdaQuery() + .isNull(InvoiceReceiptSettlement::getHcsPerfFdId) + .and(wrapper -> wrapper.isNull(InvoiceReceiptSettlement::getHcsPerfRetryCount) + .or().lt(InvoiceReceiptSettlement::getHcsPerfRetryCount, maxRetry)) + .last("limit " + hcsProperties.getPush().getBatchSize())); + if (settlementList.isEmpty()) { + return "履约收票记录:无待推送结算信息"; + } + // 批量装配上下文(收票单/结算单/合同/计划行,一次 IN 查询避免逐行 N+1) + Map receiptById = loadReceipts(settlementList); + Map settlementById = loadSettlements(settlementList); + int successCount = 0; + int failCount = 0; + for (InvoiceReceiptSettlement settlementLine : settlementList) { + try { + pushOneSettlementLine(settlementLine, receiptById.get(settlementLine.getInvoiceReceiptId()), + settlementById.get(settlementLine.getFormalSettlementId())); + successCount++; + } catch (Exception exception) { + markFailed(settlementLine, StringUtil.isBlank(exception.getMessage()) ? "推送异常" : exception.getMessage()); + failCount++; + } + } + String summary = String.format("履约收票记录:扫描 %d 条,成功 %d 条,失败 %d 条(重试上限 %d)", + settlementList.size(), successCount, failCount, maxRetry); + if (failCount > 0) { + log.warn("HCS履约收票记录推送存在失败:{}", summary); + } + return summary; + } + + /** + * 推送单条结算信息的履约收票记录 + */ + private void pushOneSettlementLine(InvoiceReceiptSettlement settlementLine, InvoiceReceipt receipt, FormalSettlement settlement) { + if (receipt == null || !"approved".equals(receipt.getApprovalStatus())) { + throw new HcsException("收票单不存在或未审批通过:invoiceReceiptId=" + settlementLine.getInvoiceReceiptId()); + } + // C109 收票单须已推送合规(履约记录引用合规合同与计划行) + if (!HcsSyncConstant.HCS_STATUS_SYNCED.equals(receipt.getHcsSyncStatus())) { + throw new HcsException("收票单尚未推送合规(invoiceNo=" + receipt.getInvoiceNo() + "),待 C109 推送成功后重试"); + } + if (settlement == null || settlement.getContractId() == null) { + throw new HcsException("结算分摊行回溯不到合同(settlementId=" + settlementLine.getFormalSettlementId() + ")"); + } + ContractManage contractManage = contractManageMapper.selectById(settlement.getContractId()); + if (contractManage == null) { + throw new HcsException("合同不存在:contractId=" + settlement.getContractId()); + } + // 关联合同的付款履约计划行(fd_plan_id 必填,无已同步计划行时通用组件抛出重试,合同链路首推后自愈) + HcsContractExeStage stageRow = contractExeStageService.findPaymentStageRow( + contractManage.getId(), HcsSyncConstant.FUND_NATURE_PROGRESS); + // 金额:分摊到该结算单的收票金额 + BigDecimal amount = settlementLine.getAllocatedInvoiceAmount() == null + ? settlementLine.getSettlementAmount() : settlementLine.getAllocatedInvoiceAmount(); + if (amount == null) { + throw new HcsException("结算分摊行金额为空(id=" + settlementLine.getId() + ")"); + } + // 申请编号:发票号-结算单号(同一发票分摊多结算单时逐条可区分) + String settlementNo = StringUtil.isBlank(settlementLine.getFormalSettlementNo()) ? settlement.getFormalSettlementNo() : settlementLine.getFormalSettlementNo(); + String applyNo = receipt.getInvoiceNo() + "-" + (settlementNo == null ? "" : settlementNo); + String perfFdId = hcsPerfRecordAddSupport.pushPerfRecordAdd(contractManage, stageRow, + applyNo, amount, toMillis(receipt.getInvoiceDate()), + HcsSyncConstant.PERFORM_TYPE_PAYMENT, receipt.getKingdeeReceivedInvoiceNo(), settlementLine.getId(), + "收票履约-" + applyNo); + markSynced(settlementLine, perfFdId); + } + + /** + * 批量装配结算信息所属收票单(须审批通过且 C109 已推送) + */ + private Map loadReceipts(List settlementList) { + List receiptIds = settlementList.stream() + .map(InvoiceReceiptSettlement::getInvoiceReceiptId).filter(Objects::nonNull).distinct().toList(); + if (receiptIds.isEmpty()) { + return Map.of(); + } + return invoiceReceiptMapper.selectBatchIds(receiptIds).stream() + .collect(Collectors.toMap(InvoiceReceipt::getId, item -> item, (first, second) -> first)); + } + + /** + * 批量装配结算信息关联的正式结算单 + */ + private Map loadSettlements(List settlementList) { + List settlementIds = settlementList.stream() + .map(InvoiceReceiptSettlement::getFormalSettlementId).filter(Objects::nonNull).distinct().toList(); + if (settlementIds.isEmpty()) { + return Map.of(); + } + return formalSettlementMapper.selectBatchIds(settlementIds).stream() + .collect(Collectors.toMap(FormalSettlement::getId, item -> item, (first, second) -> first, LinkedHashMap::new)); + } + + /** + * 推送成功回填:履约记录ID 立即持久化(幂等键),清失败原因 + */ + private void markSynced(InvoiceReceiptSettlement settlementLine, String perfFdId) { + settlementLine.setHcsPerfFdId(perfFdId); + settlementLine.setHcsPerfRetryCount(0); + settlementLine.setHcsPerfSyncMsg(null); + invoiceReceiptSettlementMapper.updateById(settlementLine); + log.info("HCS履约收票记录推送成功:settlementId={},perfFdId={}", settlementLine.getId(), perfFdId); + } + + /** + * 推送失败回写:重试次数+1 + 失败原因(下轮扫描自动补偿) + */ + private void markFailed(InvoiceReceiptSettlement settlementLine, String message) { + settlementLine.setHcsPerfRetryCount(settlementLine.getHcsPerfRetryCount() == null ? 1 : settlementLine.getHcsPerfRetryCount() + 1); + settlementLine.setHcsPerfSyncMsg(HcsSyncConstant.truncateMsg(message)); + invoiceReceiptSettlementMapper.updateById(settlementLine); + log.error("HCS履约收票记录推送失败:settlementId={},retryCount={},{}", settlementLine.getId(), settlementLine.getHcsPerfRetryCount(), message); + } + + private Long toMillis(LocalDate localDate) { + return localDate == null ? null : localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java index 2738bab2..70bdaadf 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/HcsPushTask.java @@ -46,10 +46,12 @@ import org.springblade.transport.service.IContractChangeService; import org.springblade.transport.service.IProjectChangeService; import org.springblade.transport.support.hcs.HcsContractSpecSyncSupport; import org.springblade.transport.support.hcs.HcsContractSyncSupport; +import org.springblade.transport.support.hcs.HcsInvoicePerfSyncSupport; import org.springblade.transport.support.hcs.HcsInvoiceRegSyncSupport; import org.springblade.transport.support.hcs.HcsPaymentSyncSupport; import org.springblade.transport.support.hcs.HcsProjectSyncSupport; import org.springblade.transport.support.hcs.HcsReceiptBillSyncSupport; +import org.springblade.transport.support.hcs.HcsReceiptPerfSyncSupport; import org.springframework.cloud.context.config.annotation.RefreshScope; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @@ -110,6 +112,8 @@ public class HcsPushTask { private final HcsPaymentSyncSupport hcsPaymentSyncSupport; private final HcsInvoiceRegSyncSupport hcsInvoiceRegSyncSupport; private final HcsReceiptBillSyncSupport hcsReceiptBillSyncSupport; + private final HcsInvoicePerfSyncSupport hcsInvoicePerfSyncSupport; + private final HcsReceiptPerfSyncSupport hcsReceiptPerfSyncSupport; private final IContractChangeService contractChangeService; private final IProjectChangeService projectChangeService; private final BladeRedis bladeRedis; @@ -148,19 +152,37 @@ public class HcsPushTask { } /** - * 开票登记(C111)定时推送:扫描审批通过且待同步/待补偿的开票申请(含金蝶开票完成后回填),独立开关与 cron 调度 + * 开票登记(C111)定时推送:扫描审批通过且待同步/待补偿的开票申请(含金蝶开票完成后回填), + * 并推送履约开票记录(C108-1,金蝶回写发票逐张推送),独立开关与 cron 调度 */ @Scheduled(cron = "${thirdParty.hcs.push.invoice-application.cron:0 3/5 * * * ?}") public void pushHcsInvoiceApplication() { - this.runOnce("开票登记", HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION, hcsProperties.getPush().getInvoiceApplication().isSyncEnabled(), this::pushUnsyncedInvoiceApplications); + this.runOnce("开票登记", HcsSyncConstant.BIZ_TYPE_INVOICE_APPLICATION, hcsProperties.getPush().getInvoiceApplication().isSyncEnabled(), this::syncHcsInvoiceApplications); } /** - * 收票单(C109)定时推送:扫描审批通过且待同步/待补偿的收票单,独立开关与 cron 调度 + * 开票链路组合动作:开票登记(C111)推送 + 履约开票记录(C108-1)推送(定时任务与手动批量入口共用) + */ + public String syncHcsInvoiceApplications() { + String pushSummary = this.pushUnsyncedInvoiceApplications(); + return pushSummary + ";" + hcsInvoicePerfSyncSupport.pushUnsyncedInvoicePerfRecords(); + } + + /** + * 收票单(C109)定时推送:扫描审批通过且待同步/待补偿的收票单,并推送履约收票记录 + * (C108-1,收票单每条结算信息一条,限 C109 已推送),独立开关与 cron 调度 */ @Scheduled(cron = "${thirdParty.hcs.push.invoice-receipt.cron:0 4/5 * * * ?}") public void pushHcsInvoiceReceipt() { - this.runOnce("收票单", HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT, hcsProperties.getPush().getInvoiceReceipt().isSyncEnabled(), this::pushUnsyncedInvoiceReceipts); + this.runOnce("收票单", HcsSyncConstant.BIZ_TYPE_INVOICE_RECEIPT, hcsProperties.getPush().getInvoiceReceipt().isSyncEnabled(), this::syncHcsInvoiceReceipts); + } + + /** + * 收票链路组合动作:收票单(C109)推送 + 履约收票记录(C108-1)推送(定时任务与手动批量入口共用) + */ + public String syncHcsInvoiceReceipts() { + String pushSummary = this.pushUnsyncedInvoiceReceipts(); + return pushSummary + ";" + hcsReceiptPerfSyncSupport.pushUnsyncedReceiptPerfRecords(); } /**