fix: 付款同步金蝶增加财务应付单防重预检与单据编号回填

- A102-2 推送前经 A102-4 按防重键(awrp_src_sys + awrp_src_code)幂等预检,已生成时终止推送并提示用户
- 财务应付单单据编号推送成功后即时回填 kingdeeBillNo
- 批量同步整批一次 A102-4 预检并缓存结果(KingdeeSyncContext 新增应付单/结算明细缓存),消除逐单远程查询与明细重复查询
- doSyncKingdee 补充开始/跳过/推送/完成关键节点日志
This commit is contained in:
刘泉佳
2026-09-18 11:55:14 +08:00
parent c655e9f3b9
commit e485e920a6
2 changed files with 144 additions and 33 deletions
@@ -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<String, String> cargoCodeByName, Map<Long, ContractManage> contractCache,
Map<String, String> customerCodeCache, Map<Long, ProjectApply> projectCache) {
Map<String, String> customerCodeCache, Map<Long, ProjectApply> projectCache,
Map<String, String> apBillNoCache,
Map<Long, List<FormalSettlementDetail>> 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<>());
}
}
@@ -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<PaymentApplic
|| KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(entity.getKingdeeStatus())
|| KingdeeBfiConstant.KINGDEE_STATUS_CLOSED.equals(entity.getKingdeeStatus())) {
// 存量数据金蝶单据id可能仍存于 kingdeeBillNo,兜底返回
return Func.isNotEmpty(entity.getKingdeeBillId()) ? entity.getKingdeeBillId() : entity.getKingdeeBillNo();
String existBillId = Func.isNotEmpty(entity.getKingdeeBillId()) ? entity.getKingdeeBillId() : entity.getKingdeeBillNo();
log.info("付款申请[{}]金蝶单据已生成(状态[{}]),跳过重复推送,返回单据id={}", entity.getPaymentNo(), entity.getKingdeeStatus(), existBillId);
return existBillId;
}
log.info("付款申请[{}]开始同步金蝶:paymentType={},申请金额={},kingdeeStatus={}",
entity.getPaymentNo(), entity.getPaymentType(), entity.getAppliedAmount(), entity.getKingdeeStatus());
String kingdeeBillId;
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType())) {
// 单据类型路由:按正式结算单结算类型——应付对应财务应付单(A102-2),应收对应财务应收单(A302-2,推送规则待业务确认)
@@ -398,15 +404,20 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 分录按结算明细货物编码+计量单位分组拆分(物料编码取货物类型档案编码、计量单位暂传货物单位、
// 数量取组内运输数量合计),财务应付单 detailentry 与付款申请 entry 一一对应
BfiEntryGroupResult entryGroupResult = resolveBfiEntryGroups(entity, context);
log.info("付款申请[{}]正式结算付款:开始推送金蝶财务应付单(A102-2),分录组{}条", entity.getPaymentNo(), entryGroupResult.entryGroups().size());
String apBillNo = pushBfiApBill(entity, entryGroupResult.entryGroups(), entryGroupResult.documentNos(), context);
// 财务应付单单据编号即时回填 kingdeeBillNo(后续付款申请单编号经 A103-2 定时回写任务覆盖)
if (Func.isNotEmpty(apBillNo)) {
entity.setKingdeeBillNo(apBillNo);
}
log.info("付款申请[{}]金蝶财务应付单(A102-2)推送成功,单据编号[{}]已回填 kingdeeBillNo", entity.getPaymentNo(), apBillNo);
kingdeeBillId = pushBfiPayApplyPush(entity, entryGroupResult.entryGroups(), context);
log.info("付款申请[{}]金蝶付款申请单(A103-1 应付下推)推送成功:kingdeeBillId={}", entity.getPaymentNo(), kingdeeBillId);
} else {
// 预付款:直接推付款申请单(A103-3),不绑应付单
log.info("付款申请[{}]预付款:直接推送金蝶付款申请单(A103-3)", entity.getPaymentNo());
kingdeeBillId = pushBfiPayApply(entity, context);
log.info("付款申请[{}]金蝶付款申请单(A103-3)推送成功:kingdeeBillId={}", entity.getPaymentNo(), kingdeeBillId);
}
// 星瀚单据内部主键落 kingdeeBillId(附件上传等使用);
// 财务应付单单据编号已在推送时回填 kingdeeBillNo,付款申请单编号与付款状态由定时回写任务经 A103-2/A103-4 查询回填
@@ -419,6 +430,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// syncPreSettlementAdvance(entity);
// syncFormalSettlementPayments(entity);
updateById(entity);
log.info("付款申请[{}]同步金蝶完成:kingdeeStatus={},kingdeeBillId={},kingdeeBillNo={}",
entity.getPaymentNo(), entity.getKingdeeStatus(), kingdeeBillId, entity.getKingdeeBillNo());
return kingdeeBillId;
}
@@ -440,6 +453,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
if (distinctIds.isEmpty()) throw new ServiceException("请至少选择一条审批通过的付款申请");
// 全批共享档案缓存(货物类型/合同/客商/项目),首单加载后全批复用,消除逐单重复查询(N+1)
KingdeeSyncContext context = KingdeeSyncContext.empty();
// 批量预检财务应付单是否已生成:整批一次 A102-4 批量查询,结果入缓存供逐单推送复用
preloadApBillExistNos(distinctIds, context);
IPaymentApplicationService self = SpringUtil.getBean(IPaymentApplicationService.class);
List<String> result = new ArrayList<>();
for (Long id : distinctIds) {
@@ -666,6 +681,37 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
});
}
/**
* A102-4 批量查询已生成财务应付单:来源单据号批量 + 外部系统编号(防重键),自动翻页,
* 用于推送前幂等预检(awrp_src_code 支持多值,整批一次查询消除逐单远程查询 N+1)
*
* @param awrpSrcCodes 来源单据号(付款申请号_结算明细单据号拼接)批次
* @return 已生成财务应付单的 来源单据号->单据编号 映射
*/
private Map<String, String> queryApBillExistNos(List<String> awrpSrcCodes) {
Map<String, String> existBillNos = new LinkedHashMap<>();
// 防重键批量分批查询,避免单次过滤条件过长
for (int from = 0; from < awrpSrcCodes.size(); from += PAY_RESULT_QUERY_BATCH) {
List<String> 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<PaymentApplic
/**
* 推送金蝶财务应付单(A102-2 财务应付单保存,正式结算付款第一步),仅校验保存成功。
* 推送前先经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询,金蝶已存在财务应付单时终止推送并提示用户。
* 明细分录按结算明细货物编码+计量单位分组推送,金额取各组结算金额(含税/不含税)。
*
* @param entity 付款申请实体
@@ -818,6 +865,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
* @return 财务应付单金蝶单据编号(返回结果 number 字段),用于回填 kingdeeBillNo
*/
private String pushBfiApBill(PaymentApplication entity, List<BfiEntryGroup> entryGroups, List<String> 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<PaymentApplic
// param.setRemark(entity.getPaymentNo() + (Func.isEmpty(entity.getSettlementNo()) ? "" : " " + entity.getSettlementNo()));
// 【必填】外部系统编号(候选键)
param.setAwrpSrcSys(bfiPayApplySrcSys);
// 【必填】外部系统来源单据号(候选键):付款申请号_结算明细单据号1_单据号2...
param.setAwrpSrcCode(ExternalSrcCodeSupport.buildSrcCode(entity.getPaymentNo(), documentNos));
// 【必填】外部系统来源单据号(候选键):付款申请号_结算明细单据号1_单据号2...(复用预检已组装的防重键)
param.setAwrpSrcCode(apBillSrcCode);
// 来源单据(非必传,有上游合同时传 awrp_contract_info)
// param.setAwrpSrcBt(KingdeeBfiConstant.BFI_CONTRACT_SRC_BT);
// 上游外部系统单据号/上游合同号(非必传)
@@ -1009,17 +1064,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
* @return 应付单分录解析结果(分录组 + 结算明细单据号)
*/
private BfiEntryGroupResult resolveBfiEntryGroups(PaymentApplication entity, KingdeeSyncContext context) {
List<FormalSettlementDetail> settlementDetails = formalSettlementDetails(entity);
List<FormalSettlementDetail> settlementDetails = settlementDetails(entity, context);
if (Func.isEmpty(settlementDetails)) {
throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位");
}
// 去重收集结算明细单据号(保持行序),用于拼接财务应付单外部系统来源单据号
List<String> documentNos = settlementDetails.stream()
.map(FormalSettlementDetail::getDocumentNo)
.filter(Func::isNotBlank)
.map(String::trim)
.distinct()
.toList();
List<String> documentNos = settlementDocumentNos(settlementDetails);
Map<String, String> cargoCodeByName = cargoCodeByName(context);
LinkedHashMap<String, BfiEntryGroup> groupMap = new LinkedHashMap<>();
// 优先按货物费用行逐条取数(blade_formal_settlement_detail_fee 每货物一条,金额为结算调整后的精确值)
@@ -1047,6 +1097,63 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return new BfiEntryGroupResult(new ArrayList<>(groupMap.values()), documentNos);
}
/** 结算明细单据号去重收集(保持行序),用于拼接财务应付单外部系统来源单据号 */
private List<String> settlementDocumentNos(List<FormalSettlementDetail> settlementDetails) {
return settlementDetails.stream()
.map(FormalSettlementDetail::getDocumentNo)
.filter(Func::isNotBlank)
.map(String::trim)
.distinct()
.toList();
}
/** 结算明细(上下文缓存优先):批量预检加载过的明细推送时直接复用,消除逐单重复查询 */
private List<FormalSettlementDetail> 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<Long> ids, KingdeeSyncContext context) {
List<PaymentApplication> applications = list(Wrappers.<PaymentApplication>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<String> srcCodes = new ArrayList<>();
for (PaymentApplication entity : applications) {
// 仅正式结算付款走财务应付单链路,预付款跳过
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType())) {
continue;
}
srcCodes.add(apBillSrcCode(entity, context));
}
// 仅查询缓存未命中的编码,已命中的(同批重复/已预热)直接复用
List<String> 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, ""));
}
/**
* 单条来源行(货物费用行或结算明细行)按货物类型并入分录组;一行含多种类型时金额与数量按份数均分(尾差计入最后一份)
*