From e485e920a6b6e81da25189f8c4f28c05fdc33a0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 18 Sep 2026 11:55:14 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BB=98=E6=AC=BE=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=E9=87=91=E8=9D=B6=E5=A2=9E=E5=8A=A0=E8=B4=A2=E5=8A=A1=E5=BA=94?= =?UTF-8?q?=E4=BB=98=E5=8D=95=E9=98=B2=E9=87=8D=E9=A2=84=E6=A3=80=E4=B8=8E?= =?UTF-8?q?=E5=8D=95=E6=8D=AE=E7=BC=96=E5=8F=B7=E5=9B=9E=E5=A1=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A102-2 推送前经 A102-4 按防重键(awrp_src_sys + awrp_src_code)幂等预检,已生成时终止推送并提示用户 - 财务应付单单据编号推送成功后即时回填 kingdeeBillNo - 批量同步整批一次 A102-4 预检并缓存结果(KingdeeSyncContext 新增应付单/结算明细缓存),消除逐单远程查询与明细重复查询 - doSyncKingdee 补充开始/跳过/推送/完成关键节点日志 --- .../transport/service/KingdeeSyncContext.java | 8 +- .../impl/PaymentApplicationServiceImpl.java | 169 ++++++++++++++---- 2 files changed, 144 insertions(+), 33 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java index b63d01af..5b689078 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java @@ -25,9 +25,11 @@ package org.springblade.transport.service; import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.ProjectApply; import java.util.HashMap; +import java.util.List; import java.util.Map; /** @@ -37,10 +39,12 @@ import java.util.Map; * @author Chill */ public record KingdeeSyncContext(Map cargoCodeByName, Map contractCache, - Map customerCodeCache, Map projectCache) { + Map customerCodeCache, Map projectCache, + Map apBillNoCache, + Map> settlementDetailCache) { /** 空上下文:缓存可写但初始为空,首条记录命中后全批复用 */ public static KingdeeSyncContext empty() { - return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); + return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>()); } } 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 c058a26e..637ed3f4 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 @@ -27,19 +27,18 @@ import lombok.extern.slf4j.Slf4j; 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.api.R; import org.springblade.core.tool.jackson.JsonUtil; import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.SpringUtil; -import org.springblade.core.tool.api.R; -import org.springblade.transport.constant.KingdeeBfiConstant; -import org.springblade.transport.constant.TransportBusiConstant; import org.springblade.system.cache.DictBizCache; import org.springblade.system.cache.SysCache; import org.springblade.system.feign.ISysClient; import org.springblade.system.pojo.entity.CargoType; -import org.springblade.system.pojo.entity.DictBiz; import org.springblade.system.pojo.entity.Dept; +import org.springblade.system.pojo.entity.DictBiz; +import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillQueryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveEntryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyPushDTO; @@ -48,6 +47,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyQueryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveEntryDTO; import org.springblade.thirdparty.bfi.pojo.dto.BfiPayBillQueryDTO; +import org.springblade.thirdparty.bfi.pojo.vo.BfiApBillVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultItemVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiBillSaveResultVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyEntryVO; @@ -55,53 +55,55 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO; import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO; import org.springblade.thirdparty.bfi.service.IBfiService; import org.springblade.thirdparty.bfi.util.BfiUtil; +import org.springblade.transport.constant.KingdeeBfiConstant; +import org.springblade.transport.constant.TransportBusiConstant; +import org.springblade.transport.mapper.BillLedgerMapper; +import org.springblade.transport.mapper.BillLedgerUsageMapper; +import org.springblade.transport.mapper.ContractManageMapper; +import org.springblade.transport.mapper.CustomerArchiveMapper; import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper; import org.springblade.transport.mapper.FormalSettlementDetailMapper; import org.springblade.transport.mapper.FormalSettlementMapper; import org.springblade.transport.mapper.FormalSettlementPaymentMapper; -import org.springblade.transport.mapper.BillLedgerMapper; -import org.springblade.transport.mapper.BillLedgerUsageMapper; -import org.springblade.transport.mapper.PreSettlementMapper; -import org.springblade.transport.mapper.ProjectApplyMapper; -import org.springblade.transport.mapper.CustomerArchiveMapper; -import org.springblade.transport.mapper.ContractManageMapper; +import org.springblade.transport.mapper.FormalSettlementSourceMapper; import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper; import org.springblade.transport.mapper.PaymentApplicationMapper; import org.springblade.transport.mapper.PaymentApplicationRecordMapper; import org.springblade.transport.mapper.PaymentApplicationSettlementMapper; -import org.springblade.transport.mapper.FormalSettlementSourceMapper; import org.springblade.transport.mapper.PreSettlementAdvanceMapper; +import org.springblade.transport.mapper.PreSettlementMapper; +import org.springblade.transport.mapper.ProjectApplyMapper; import org.springblade.transport.mapper.TemporaryCreditLimitMapper; import org.springblade.transport.pojo.dto.PaymentApplicationInvoiceRequest; import org.springblade.transport.pojo.dto.PaymentApplicationRecordRequest; import org.springblade.transport.pojo.dto.PaymentApplicationSaveRequest; import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; +import org.springblade.transport.pojo.entity.BillLedger; +import org.springblade.transport.pojo.entity.BillLedgerUsage; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.CustomerArchive; import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.FormalSettlementDetail; import org.springblade.transport.pojo.entity.FormalSettlementDetailFee; import org.springblade.transport.pojo.entity.FormalSettlementPayment; -import org.springblade.transport.pojo.entity.BillLedger; -import org.springblade.transport.pojo.entity.BillLedgerUsage; -import org.springblade.transport.pojo.entity.PreSettlement; -import org.springblade.transport.pojo.entity.ProjectApply; -import org.springblade.transport.pojo.entity.CustomerArchive; -import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.entity.FormalSettlementSource; import org.springblade.transport.pojo.entity.PaymentApplication; import org.springblade.transport.pojo.entity.PaymentApplicationInvoice; import org.springblade.transport.pojo.entity.PaymentApplicationRecord; import org.springblade.transport.pojo.entity.PaymentApplicationSettlement; -import org.springblade.transport.pojo.entity.FormalSettlementSource; +import org.springblade.transport.pojo.entity.PreSettlement; import org.springblade.transport.pojo.entity.PreSettlementAdvance; +import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.TemporaryCreditLimit; -import org.springblade.transport.pojo.vo.PaymentApplicationVO; import org.springblade.transport.pojo.vo.PaymentApplicationReferenceAmountVO; +import org.springblade.transport.pojo.vo.PaymentApplicationVO; +import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.service.IPaymentApplicationService; import org.springblade.transport.service.KingdeeSyncContext; -import org.springblade.transport.service.IFormalSettlementService; import org.springblade.transport.support.ExternalSrcCodeSupport; import org.springblade.transport.wrapper.PaymentApplicationWrapper; -import org.springframework.stereotype.Service; import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; @@ -387,8 +389,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl result = new ArrayList<>(); for (Long id : distinctIds) { @@ -666,6 +681,37 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl单据编号 映射 + */ + private Map queryApBillExistNos(List awrpSrcCodes) { + Map existBillNos = new LinkedHashMap<>(); + // 防重键批量分批查询,避免单次过滤条件过长 + for (int from = 0; from < awrpSrcCodes.size(); from += PAY_RESULT_QUERY_BATCH) { + List batch = awrpSrcCodes.subList(from, Math.min(from + PAY_RESULT_QUERY_BATCH, awrpSrcCodes.size())); + if (batch.isEmpty()) { + continue; + } + BfiApBillQueryDTO param = new BfiApBillQueryDTO(); + param.setAwrpSrcCode(batch); + param.setAwrpSrcSys(bfiPayApplySrcSys); + for (BfiApBillVO apBill : BfiUtil.queryAllPages(pageNo -> { + param.setPageNo(pageNo); + param.setPageSize(PAY_RESULT_PAGE_SIZE); + return bfiService.queryApBill(param); + })) { + if (Func.isNotEmpty(apBill.getAwrpSrcCode())) { + existBillNos.put(apBill.getAwrpSrcCode(), apBill.getBillno()); + } + } + } + return existBillNos; + } + /** * A103-4 分页查询:按付款人(公司)编码 + 最近1个月业务日期窗口(接口限制不超过1个月),自动翻页 * @@ -810,6 +856,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl entryGroups, List documentNos, KingdeeSyncContext context) { + // A102-4 幂等预检:按防重键查询金蝶是否已生成财务应付单(批量同步时整批已预热缓存,此处仅懒加载未命中编码),已处理则终止后续推送流程并提示用户 + String apBillSrcCode = ExternalSrcCodeSupport.buildSrcCode(entity.getPaymentNo(), documentNos); + String existBillNo = context.apBillNoCache().computeIfAbsent(apBillSrcCode, code -> + queryApBillExistNos(List.of(code)).getOrDefault(code, "")); + if (Func.isNotEmpty(existBillNo)) { + throw new ServiceException("推送金蝶失败:付款申请[" + entity.getPaymentNo() + "]的财务应付单已生成" + + (Func.isBlank(existBillNo) ? "" : "(单据编号" + existBillNo + ")") + ",请勿重复推送"); + } ContractManage contract = requireContract(entity, context); // 单据类型按映射字典转换:默认服务采购应付,金蝶确认其他类型后修改 BFI_AP_BILL_BILLTYPE_NAME 即可 String billtypeNumber = KingdeeBfiConstant.billtypeBy(KingdeeBfiConstant.AP_BILL_BILLTYPE_MAP, KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME); @@ -866,8 +921,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl settlementDetails = formalSettlementDetails(entity); + List settlementDetails = settlementDetails(entity, context); if (Func.isEmpty(settlementDetails)) { throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位"); } // 去重收集结算明细单据号(保持行序),用于拼接财务应付单外部系统来源单据号 - List documentNos = settlementDetails.stream() - .map(FormalSettlementDetail::getDocumentNo) - .filter(Func::isNotBlank) - .map(String::trim) - .distinct() - .toList(); + List documentNos = settlementDocumentNos(settlementDetails); Map cargoCodeByName = cargoCodeByName(context); LinkedHashMap groupMap = new LinkedHashMap<>(); // 优先按货物费用行逐条取数(blade_formal_settlement_detail_fee 每货物一条,金额为结算调整后的精确值) @@ -1047,6 +1097,63 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl(groupMap.values()), documentNos); } + /** 结算明细单据号去重收集(保持行序),用于拼接财务应付单外部系统来源单据号 */ + private List settlementDocumentNos(List settlementDetails) { + return settlementDetails.stream() + .map(FormalSettlementDetail::getDocumentNo) + .filter(Func::isNotBlank) + .map(String::trim) + .distinct() + .toList(); + } + + /** 结算明细(上下文缓存优先):批量预检加载过的明细推送时直接复用,消除逐单重复查询 */ + private List settlementDetails(PaymentApplication entity, KingdeeSyncContext context) { + return context.settlementDetailCache().computeIfAbsent(entity.getId(), key -> formalSettlementDetails(entity)); + } + + /** 财务应付单防重键来源单据号:付款申请号 + 结算明细单据号下划线拼接(与 A102-2 保存传参一致) */ + private String apBillSrcCode(PaymentApplication entity, KingdeeSyncContext context) { + return ExternalSrcCodeSupport.buildSrcCode(entity.getPaymentNo(), settlementDocumentNos(settlementDetails(entity, context))); + } + + /** + * 批量同步前 A102-4 预检:一次批量查询整批付款申请的财务应付单是否已生成, + * 结果(含"未生成"标记)写入上下文缓存,供逐单推送时复用,消除逐单远程查询(N+1)。 + * 仅预检会真正走到推送的正式结算单据(审批通过且未生成/生成失败),已生成/付款中等状态在 + * doSyncKingdee 入口即返回,无需预检 + * + * @param ids 待同步付款申请id批次 + * @param context 金蝶同步上下文(缓存载体) + */ + private void preloadApBillExistNos(List ids, KingdeeSyncContext context) { + List applications = list(Wrappers.lambdaQuery() + .in(PaymentApplication::getId, ids) + .eq(PaymentApplication::getApprovalStatus, APPROVED) + .notIn(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_SYNCED, + KingdeeBfiConstant.KINGDEE_STATUS_PAYING, KingdeeBfiConstant.KINGDEE_STATUS_PAID, KingdeeBfiConstant.KINGDEE_STATUS_CLOSED)); + List srcCodes = new ArrayList<>(); + for (PaymentApplication entity : applications) { + // 仅正式结算付款走财务应付单链路,预付款跳过 + if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType())) { + continue; + } + srcCodes.add(apBillSrcCode(entity, context)); + } + // 仅查询缓存未命中的编码,已命中的(同批重复/已预热)直接复用 + List pendingCodes = srcCodes.stream() + .filter(Func::isNotBlank) + .filter(code -> !context.apBillNoCache().containsKey(code)) + .distinct() + .toList(); + if (pendingCodes.isEmpty()) { + return; + } + context.apBillNoCache().putAll(queryApBillExistNos(pendingCodes)); + // 未命中编码补"未生成"标记(空串),避免逐单推送时重复远程查询 + pendingCodes.forEach(code -> context.apBillNoCache().putIfAbsent(code, "")); + } + /** * 单条来源行(货物费用行或结算明细行)按货物类型并入分录组;一行含多种类型时金额与数量按份数均分(尾差计入最后一份) *