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();
}
/**