fix: - 修复BFI单据保存结果反序列化失败导致错误信息丢失。

- 财务应付单保存A102-2,传业务字段。并保存id
- 付款管理批量同步方法优化
This commit is contained in:
刘泉佳
2026-09-18 10:48:43 +08:00
parent d0c5483d8b
commit 20fddf2c60
13 changed files with 470 additions and 139 deletions
@@ -0,0 +1,39 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.constant;
/**
* 运输业务固定值。
*
* @author Chill
*/
public final class TransportBusiConstant {
/** 付款类型:结算付款(关联正式结算单的正式付款)。 */
public static final String PAYMENT_TYPE_SETTLEMENT = "settlement_payment";
private TransportBusiConstant() {
}
}
@@ -43,8 +43,36 @@ public interface IPaymentApplicationService extends BaseService<PaymentApplicati
void returnBill(PaymentApplicationStatusRequest request);
void voidBill(PaymentApplicationStatusRequest request);
String syncKingdee(Long id);
/**
* 同步单笔付款申请到金蝶(携带批量共享上下文,消除重复档案查询),编排层无事务:
* 单据级事务在 {@link #doSyncKingdee(Long, KingdeeSyncContext)} 中,失败状态落库在事务回滚后执行
*
* @param id 付款申请id
* @param context 批量共享上下文(档案缓存),单笔调用传 {@link KingdeeSyncContext#empty()}
* @return 金蝶单据id(存量数据兜底返回金蝶单据编号)
*/
String syncKingdee(Long id, KingdeeSyncContext context);
List<String> syncKingdeeBatch(List<Long> ids);
/**
* 单据级事务执行金蝶推送(由 syncKingdee 经自代理调用,保证整批同步不共用大事务)
*
* @param id 付款申请id
* @param context 批量共享上下文(档案缓存)
* @return 金蝶单据id
*/
String doSyncKingdee(Long id, KingdeeSyncContext context);
/**
* 独立事务落"生成失败"状态(在推送事务回滚、行锁释放后执行);
* 仅更新审批通过的单据,未审批/已删除单据自动跳过,保持原状态
*
* @param id 付款申请id
*/
void markKingdeeSyncFailed(Long id);
/**
* 定时回写金蝶付款结果(A103-2 付款申请单查询 / A103-4 付款处理查询)
*
@@ -0,0 +1,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.ProjectApply;
import java.util.HashMap;
import java.util.Map;
/**
* 金蝶同步上下文:批量同步时全批共享的档案缓存,消除逐单重复的远程/数据库查询(N+1)。
* <p>单笔同步使用 {@link #empty()},各缓存为空时自动回退为逐单查询,行为与原逻辑一致。</p>
*
* @author Chill
*/
public record KingdeeSyncContext(Map<String, String> cargoCodeByName, Map<Long, ContractManage> contractCache,
Map<String, String> customerCodeCache, Map<Long, ProjectApply> projectCache) {
/** 空上下文:缓存可写但初始为空,首条记录命中后全批复用 */
public static KingdeeSyncContext empty() {
return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
}
}
@@ -35,6 +35,7 @@ import org.springblade.transport.mapper.PaymentApplicationMapper;
import org.springblade.transport.mapper.PaymentApplicationInvoiceMapper;
import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.ReceivablePayableDetailMapper;
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
import org.springblade.transport.mapper.SettlementAdjustmentMapper;
@@ -233,7 +234,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
query.or(wrapper -> wrapper.in(PaymentApplication::getId, relationApplicationIds));
}
})
.eq(PaymentApplication::getPaymentType, "settlement_payment")
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
.eq(PaymentApplication::getIsDeleted, 0);
vo.setPaymentApplications(paymentApplicationMapper.selectList(paymentApplicationQuery
.orderByDesc(PaymentApplication::getCreateTime)).stream()
@@ -1175,7 +1176,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
private PaymentSummary calculatePaymentSummary(FormalSettlement settlement) {
LambdaQueryWrapper<PaymentApplication> query = Wrappers.<PaymentApplication>lambdaQuery()
.eq(PaymentApplication::getSettlementId, settlement.getId())
.eq(PaymentApplication::getPaymentType, "settlement_payment")
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
.eq(PaymentApplication::getIsDeleted, 0);
List<PaymentApplicationSettlement> relations = paymentApplicationSettlementMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
@@ -33,6 +33,7 @@ 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;
@@ -95,7 +96,9 @@ 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.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;
@@ -201,7 +204,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
public PaymentApplicationReferenceAmountVO referenceAmount(String paymentType, Long referenceId, Long excludeId) {
if (referenceId == null) throw new ServiceException("结算单不能为空");
BigDecimal settlementAmount;
if ("settlement_payment".equals(paymentType)) {
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(paymentType)) {
FormalSettlement settlement = formalSettlementMapper.selectById(referenceId);
if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
throw new ServiceException("正式结算单不存在");
@@ -263,7 +266,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
matchedInvoiceAmount = matchedInvoiceAmount.add(money(item.getMatchedAmount()));
}
entity.setMatchedInvoiceAmount(matchedInvoiceAmount);
if ("settlement_payment".equals(request.getPaymentType()) && request.getSettlementId() == null
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) && request.getSettlementId() == null
&& request.getSettlementIds() != null && !request.getSettlementIds().isEmpty()) {
request.setSettlementId(request.getSettlementIds().get(0));
}
@@ -353,8 +356,30 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
@Override
@Transactional(rollbackFor = Exception.class)
public String syncKingdee(Long id) {
return syncKingdee(id, KingdeeSyncContext.empty());
}
@Override
public String syncKingdee(Long id, KingdeeSyncContext context) {
try {
// 单据级事务经自代理调用:单条失败只回滚自身,避免批量同步共用大事务长持锁
return self().doSyncKingdee(id, context);
} catch (ServiceException exception) {
log.error("付款申请[{}]推送金蝶付款申请单异常", id, exception);
// 推送事务已回滚、行锁已释放,此处独立事务落"生成失败"状态
self().markKingdeeSyncFailed(id);
throw exception;
} catch (Exception exception) {
log.error("付款申请[{}]推送金蝶付款申请单异常", id, exception);
self().markKingdeeSyncFailed(id);
throw new ServiceException("推送金蝶付款申请失败:" + exception.getMessage());
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public String doSyncKingdee(Long id, KingdeeSyncContext context) {
PaymentApplication entity = lockedPayment(id);
if (!APPROVED.equals(entity.getApprovalStatus())) throw new ServiceException("仅审批通过的付款申请允许生成金蝶单据");
if (KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(entity.getKingdeeStatus())
@@ -365,32 +390,26 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return Func.isNotEmpty(entity.getKingdeeBillId()) ? entity.getKingdeeBillId() : entity.getKingdeeBillNo();
}
String kingdeeBillId;
try {
if ("settlement_payment".equals(entity.getPaymentType())) {
// 单据类型路由:按正式结算单结算类型——应付对应财务应付单(A102-2),应收对应财务应收单(A302-2,推送规则待业务确认)
requirePayableSettlement(entity);
// 正式结算付款:先推财务应付单(A102-2,仅校验保存成功,应付单id不落库,
// 后续如需可经 A102-4 按 awrp_src_sys + awrp_src_code 查询),再由应付下推付款申请(A103-1)
// 分录按结算明细货物编码+计量单位分组拆分(物料编码取货物类型档案编码、计量单位暂传货物单位、
// 数量取组内运输数量合计),财务应付单 detailentry 与付款申请 entry 一一对应
BfiEntryGroupResult entryGroupResult = resolveBfiEntryGroups(entity);
pushBfiApBill(entity, entryGroupResult.entryGroups(), entryGroupResult.documentNos());
kingdeeBillId = pushBfiPayApplyPush(entity, entryGroupResult.entryGroups());
} else {
// 预付款:直接推付款申请单(A103-3),不绑应付单
kingdeeBillId = pushBfiPayApply(entity);
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType())) {
// 单据类型路由:按正式结算单结算类型——应付对应财务应付单(A102-2),应收对应财务应收单(A302-2,推送规则待业务确认)
requirePayableSettlement(entity);
// 正式结算付款:先推财务应付单(A102-2,仅校验保存成功,应付单id不落库,
// 后续如需可经 A102-4 按 awrp_src_sys + awrp_src_code 查询),再由应付下推付款申请(A103-1)
// 分录按结算明细货物编码+计量单位分组拆分(物料编码取货物类型档案编码、计量单位暂传货物单位、
// 数量取组内运输数量合计),财务应付单 detailentry 与付款申请 entry 一一对应
BfiEntryGroupResult entryGroupResult = resolveBfiEntryGroups(entity, context);
String apBillNo = pushBfiApBill(entity, entryGroupResult.entryGroups(), entryGroupResult.documentNos(), context);
// 财务应付单单据编号即时回填 kingdeeBillNo(后续付款申请单编号经 A103-2 定时回写任务覆盖)
if (Func.isNotEmpty(apBillNo)) {
entity.setKingdeeBillNo(apBillNo);
}
} catch (ServiceException exception) {
log.error("付款申请[{}]推送金蝶付款申请单异常", entity.getPaymentNo(), exception);
markSyncFailed(entity);
throw exception;
} catch (Exception exception) {
log.error("付款申请[{}]推送金蝶付款申请单异常", entity.getPaymentNo(), exception);
markSyncFailed(entity);
throw new ServiceException("推送金蝶付款申请失败:" + exception.getMessage());
kingdeeBillId = pushBfiPayApplyPush(entity, entryGroupResult.entryGroups(), context);
} else {
// 预付款:直接推付款申请单(A103-3),不绑应付单
kingdeeBillId = pushBfiPayApply(entity, context);
}
// 星瀚单据内部主键落 kingdeeBillId(附件上传等使用);
// 金蝶单据编号(billno)与付款状态由定时回写任务经 A103-2/A103-4 查询回填
// 财务应付单单据编号已在推送时回填 kingdeeBillNo,付款申请单编号与付款状态由定时回写任务经 A103-2/A103-4 查询回填
entity.setKingdeeBillId(kingdeeBillId);
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_SYNCED);
// 付款完成模拟逻辑已停用(mockPaymentRecords / 履约付款数据直接置 paid),
@@ -405,14 +424,28 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
@Override
@Transactional(rollbackFor = Exception.class)
public void markKingdeeSyncFailed(Long id) {
if (id == null) return;
// 条件更新:仅审批通过的单据落"生成失败"状态,未审批/已删除单据自动跳过保持原状态
baseMapper.update(null, Wrappers.<PaymentApplication>lambdaUpdate()
.eq(PaymentApplication::getId, id)
.eq(PaymentApplication::getApprovalStatus, APPROVED)
.set(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
}
@Override
public List<String> syncKingdeeBatch(List<Long> ids) {
if (ids == null || ids.isEmpty()) throw new ServiceException("请至少选择一条审批通过的付款申请");
List<Long> distinctIds = ids.stream().filter(Objects::nonNull).distinct().toList();
if (distinctIds.isEmpty()) throw new ServiceException("请至少选择一条审批通过的付款申请");
// 全批共享档案缓存(货物类型/合同/客商/项目),首单加载后全批复用,消除逐单重复查询(N+1)
KingdeeSyncContext context = KingdeeSyncContext.empty();
IPaymentApplicationService self = SpringUtil.getBean(IPaymentApplicationService.class);
List<String> result = new ArrayList<>();
for (Long id : distinctIds) {
try {
result.add(syncKingdee(id));
// 经自代理逐条调用:单据级事务,单条失败仅回滚自身且不中断整批
result.add(self.syncKingdee(id, context));
} catch (Exception exception) {
result.add("付款申请[" + id + "]同步失败:" + exception.getMessage());
}
@@ -420,6 +453,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return result;
}
/** 自代理:跨方法调用确保事务注解生效(批量逐条事务、失败状态独立事务) */
private IPaymentApplicationService self() {
return SpringUtil.getBean(IPaymentApplicationService.class);
}
@Override
public String syncKingdeePaymentResult() {
// ① 待回写付款申请:审批通过且金蝶状态为 已生成/付款中(终态已付款/已关闭不再轮询)
@@ -485,7 +523,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
if (Func.isBlank(orgNumber)) {
try {
// A103-4 付款人(公司)编码必填:优先取 A103-2 返回付款组织编码,兜底按合同所属组织解析
orgNumber = bfiOrgNumber(requireContract(application));
orgNumber = bfiOrgNumber(requireContract(application, KingdeeSyncContext.empty()));
} catch (Exception exception) {
log.warn("付款申请[{}]付款处理查询跳过,付款组织编码获取失败:{}", application.getPaymentNo(), exception.getMessage());
continue;
@@ -702,8 +740,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
* @param entity 付款申请实体
* @return 金蝶付款申请单id
*/
private String pushBfiPayApply(PaymentApplication entity) {
ContractManage contract = requireContract(entity);
private String pushBfiPayApply(PaymentApplication entity, KingdeeSyncContext context) {
ContractManage contract = requireContract(entity, context);
String orgNumber = bfiOrgNumber(contract);
String currency = bfiCurrency(contract);
String applyDateText = (entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate()).format(DateTimeFormatter.BASIC_ISO_DATE);
@@ -733,10 +771,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// }
BfiPayApplySaveEntryDTO entry = new BfiPayApplySaveEntryDTO();
entry.setEPaymenttypeNumber("settlement_payment".equals(entity.getPaymentType()) ? KingdeeBfiConstant.BFI_PAYMENT_TYPE_FORMAL : KingdeeBfiConstant.BFI_PAYMENT_TYPE_PREPAY);
entry.setEPaymenttypeNumber(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType()) ? KingdeeBfiConstant.BFI_PAYMENT_TYPE_FORMAL : KingdeeBfiConstant.BFI_PAYMENT_TYPE_PREPAY);
entry.setEAsstacttype(bfiAsstactType(contract));
// 往来户.编码:合同乙方关联客商档案客商编码
entry.setEAsstactNumber(bfiAsstactNumber(contract));
entry.setEAsstactNumber(bfiAsstactNumber(contract, context));
entry.setEAssacct(entity.getBankAccount());
entry.setEBebankNumber(entity.getBankName());
// 应付金额
@@ -744,7 +782,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 申请金额
entry.setEApplyamount(applyAmount);
// entry.setEExpaydate(applyDateText);
entry.setProjectNumber(bfiProjectNumber(entity));
entry.setProjectNumber(bfiProjectNumber(entity, context));
entry.setAwrpEntryCode(entity.getPaymentNo() + "-1");
entry.setERemark(entity.getRemark());
param.setEntry(List.of(entry));
@@ -777,9 +815,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
* @param entity 付款申请实体
* @param entryGroups 结算明细聚合出的应付单分录组
* @param documentNos 结算明细单据号(去重),用于拼接外部系统来源单据号
* @return 财务应付单金蝶单据编号(返回结果 number 字段),用于回填 kingdeeBillNo
*/
private void pushBfiApBill(PaymentApplication entity, List<BfiEntryGroup> entryGroups, List<String> documentNos) {
ContractManage contract = requireContract(entity);
private String pushBfiApBill(PaymentApplication entity, List<BfiEntryGroup> entryGroups, List<String> documentNos, KingdeeSyncContext context) {
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);
if (Func.isBlank(billtypeNumber)) {
@@ -788,8 +827,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
String orgNumber = bfiOrgNumber(contract);
// 币种取合同结算币种
String currency = bfiCurrency(contract);
// TODO 待确认:日期格式沿用现有 A103-3 的 yyyyMMdd,若金蝶校验要求 yyyy-MM-dd 再统一调整
String billDateText = (entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate()).format(DateTimeFormatter.BASIC_ISO_DATE);
// 单据日期:yyyy-MM-dd 00:00:00
String billDateText = (entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate()).atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
BfiApBillSaveDTO param = new BfiApBillSaveDTO();
// 【必填】单据类型.编码
param.setBilltypeidNumber(billtypeNumber);
@@ -800,7 +839,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 【必填】往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商)
param.setAsstacttype(bfiAsstactType(contract));
// 【必填】往来户.编码:合同乙方关联客商档案客商编码
param.setAsstactNumber(bfiAsstactNumber(contract));
param.setAsstactNumber(bfiAsstactNumber(contract, context));
// 【必填】付款方式(CASH:现购, CREDIT:赊购)
param.setPurmode(KingdeeBfiConstant.BFI_AP_BILL_PUR_MODE_CASH);
// 【必填】结算币别.货币代码
@@ -828,10 +867,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 【必填】外部系统编号(候选键)
param.setAwrpSrcSys(bfiPayApplySrcSys);
// 【必填】外部系统来源单据号(候选键):付款申请号_结算明细单据号1_单据号2...
List<String> awrpSrcCodeParts = new ArrayList<>();
awrpSrcCodeParts.add(entity.getPaymentNo());
awrpSrcCodeParts.addAll(documentNos);
param.setAwrpSrcCode(String.join("_", awrpSrcCodeParts));
param.setAwrpSrcCode(ExternalSrcCodeSupport.buildSrcCode(entity.getPaymentNo(), documentNos));
// 来源单据(非必传,有上游合同时传 awrp_contract_info)
// param.setAwrpSrcBt(KingdeeBfiConstant.BFI_CONTRACT_SRC_BT);
// 上游外部系统单据号/上游合同号(非必传)
@@ -861,6 +897,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 【必填】外部系统来源分录标识(候选键),A103-1 下推时以 awrp_finapbill_entryid 关联此行
entry.setAwrpEntryCode(entity.getPaymentNo() + "-" + (index + 1));
// 调试
if (true){
// 【必填】物料编码.编码:结算明细货物类型匹配货物类型档案货物编码
entry.setMaterialNumber(KingdeeBfiConstant.BFI_AP_BILL_MATERIAL_NUMBER_TEST);
@@ -877,14 +915,12 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 付款计划 planentity 不传,由金蝶按明细自动汇总生成
// TODO 待业务确认:存在预付款抵扣时是否需传预付信息 preentry(y_billtype/y_billid/y_settleamt),本期不传、后续迭代
// 调试
// if (true) {
// param.setm
// }
BfiBillSaveResultVO saveResult = bfiService.saveApBill(param);
// 应付单id不落库,后续如需可经 A102-4 按防重键(awrp_src_sys + awrp_src_code)查询
requireBfiSaveSuccess(saveResult, "推送金蝶财务应付单");
return saveResult.getResult().get(0).getNumber();
}
/**
@@ -895,11 +931,11 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
* @param entryGroups 结算明细聚合出的应付单分录组
* @return 星瀚付款申请单id
*/
private String pushBfiPayApplyPush(PaymentApplication entity, List<BfiEntryGroup> entryGroups) {
ContractManage contract = requireContract(entity);
private String pushBfiPayApplyPush(PaymentApplication entity, List<BfiEntryGroup> entryGroups, KingdeeSyncContext context) {
ContractManage contract = requireContract(entity, context);
String orgNumber = bfiOrgNumber(contract);
// TODO 待确认:日期格式沿用现有 A103-3 的 yyyyMMdd,若金蝶校验要求 yyyy-MM-dd 再统一调整
String applyDateText = (entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate()).format(DateTimeFormatter.BASIC_ISO_DATE);
// 申请日期:yyyy-MM-dd 00:00:00(与 A102-2 单据日期格式统一)
String applyDateText = (entity.getApplyDate() == null ? LocalDate.now() : entity.getApplyDate()).atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
BfiPayApplyPushDTO param = new BfiPayApplyPushDTO();
param.setAwrpSrcCode(entity.getPaymentNo());
param.setAwrpSrcSys(bfiPayApplySrcSys);
@@ -928,7 +964,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 申请金额:与对应应付单分录结算金额(含税)保持一致
entry.setEApplyamount(entryGroup.amount());
entry.setEExpaydate(applyDateText);
entry.setProjectNumber(bfiProjectNumber(entity));
// entry.setProjectNumber(bfiProjectNumber(entity));
// 【必填】项目.项目编码(联调期临时传金蝶测试档案值)
entry.setProjectNumber(KingdeeBfiConstant.BFI_AP_BILL_PROJECT_NUMBER_TEST);
entry.setERemark(entity.getRemark());
entry.setAwrpSrcType(bfiPayApplySrcType);
entries.add(entry);
@@ -970,7 +1008,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
* @param entity 付款申请实体
* @return 应付单分录解析结果(分录组 + 结算明细单据号)
*/
private BfiEntryGroupResult resolveBfiEntryGroups(PaymentApplication entity) {
private BfiEntryGroupResult resolveBfiEntryGroups(PaymentApplication entity, KingdeeSyncContext context) {
List<FormalSettlementDetail> settlementDetails = formalSettlementDetails(entity);
if (Func.isEmpty(settlementDetails)) {
throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位");
@@ -982,7 +1020,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
.map(String::trim)
.distinct()
.toList();
Map<String, String> cargoCodeByName = cargoCodeByName();
Map<String, String> cargoCodeByName = cargoCodeByName(context);
LinkedHashMap<String, BfiEntryGroup> groupMap = new LinkedHashMap<>();
// 优先按货物费用行逐条取数(blade_formal_settlement_detail_fee 每货物一条,金额为结算调整后的精确值)
List<FormalSettlementDetailFee> detailFees = formalSettlementDetailFees(settlementDetails);
@@ -1075,25 +1113,30 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
.orderByAsc(FormalSettlementDetail::getLineNo));
}
/** 货物类型名称->货物编码映射(取自货物类型档案;名称重复且编码不一致时拦截) */
private Map<String, String> cargoCodeByName() {
/** 货物类型名称->货物编码映射(取自货物类型档案;名称重复且编码不一致时拦截;批量同步时全批共享一次加载结果) */
private Map<String, String> cargoCodeByName(KingdeeSyncContext context) {
Map<String, String> cache = context.cargoCodeByName();
if (!cache.isEmpty()) {
return cache;
}
R<List<CargoType>> response = sysClient.getCargoTypes();
if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) {
throw new ServiceException("推送金蝶财务应付单失败:货物类型档案读取失败,请稍后重试");
}
Map<String, String> cargoCodeByName = new HashMap<>();
Map<String, String> loaded = new HashMap<>();
for (CargoType cargoType : response.getData()) {
if (cargoType == null || Func.isBlank(cargoType.getCargoName()) || Func.isBlank(cargoType.getCargoCode())) {
continue;
}
String cargoName = cargoType.getCargoName().trim();
String cargoCode = cargoType.getCargoCode().trim();
String existingCode = cargoCodeByName.putIfAbsent(cargoName, cargoCode);
String existingCode = loaded.putIfAbsent(cargoName, cargoCode);
if (existingCode != null && !existingCode.equals(cargoCode)) {
throw new ServiceException("推送金蝶财务应付单失败:货物类型[" + cargoName + "]在货物类型档案中存在多个编码,请检查档案数据");
}
}
return cargoCodeByName;
cache.putAll(loaded);
return cache;
}
/**
@@ -1150,7 +1193,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
BfiBillSaveResultItemVO item = saveResult.getResult().get(0);
if (!Boolean.TRUE.equals(item.getBillStatus())) {
String errorText = Func.isEmpty(item.getErrors()) ? "未知错误" : String.join(";", item.getErrors());
String errorText = Func.isBlank(item.getErrorText()) ? "未知错误" : item.getErrorText();
throw new ServiceException(action + "失败:" + errorText);
}
}
@@ -1169,14 +1212,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return String.valueOf(billId);
}
private void markSyncFailed(PaymentApplication entity) {
entity.setKingdeeStatus(KingdeeBfiConstant.KINGDEE_STATUS_FAILED);
updateById(entity);
}
private ContractManage requireContract(PaymentApplication entity) {
private ContractManage requireContract(PaymentApplication entity, KingdeeSyncContext context) {
if (entity.getContractId() == null) throw new ServiceException("推送金蝶付款申请失败,付款申请未关联合同");
ContractManage contract = contractManageMapper.selectById(entity.getContractId());
// 批量同步时按合同id缓存复用(同一合同只查一次),单笔同步退化为直接查询
ContractManage contract = context.contractCache().computeIfAbsent(entity.getContractId(), contractManageMapper::selectById);
if (contract == null || Objects.equals(contract.getIsDeleted(), 1)) throw new ServiceException("推送金蝶付款申请失败,关联合同不存在");
return contract;
}
@@ -1206,7 +1245,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
return switch (paymentType == null ? "" : paymentType) {
case "project_advance" -> "项目预付款申请";
case "progress_advance" -> "进度预付款申请";
case "settlement_payment" -> "结算付款申请";
case TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT -> "结算付款申请";
default -> "付款申请";
};
}
@@ -1225,22 +1264,30 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
/** 财务应付单往来户.编码:合同乙方关联客商档案取客商编码,未维护时拦截 */
private String bfiAsstactNumber(ContractManage contract) {
private String bfiAsstactNumber(ContractManage contract, KingdeeSyncContext context) {
if (Func.isBlank(contract.getPartyB())) {
throw new ServiceException("推送金蝶财务应付单失败,合同乙方不能为空,请检查合同[" + Func.toStr(contract.getContractNo()) + "]乙方信息");
}
CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, contract.getPartyB()).or().eq(CustomerArchive::getShortName, contract.getPartyB()))
.eq(CustomerArchive::getIsDeleted, 0).last("limit 1"));
if (customer == null || Func.isBlank(customer.getCustomerCode())) {
// 批量同步时按乙方名称缓存客商编码(同批同一乙方只查一次),单笔同步退化为直接查询
String customerCode = context.customerCodeCache().computeIfAbsent(contract.getPartyB(), this::queryCustomerCode);
if (Func.isBlank(customerCode)) {
throw new ServiceException("推送金蝶财务应付单失败,合同乙方[" + contract.getPartyB() + "]未在客商档案中维护客商编码,请补充后重试");
}
return customer.getCustomerCode();
return customerCode;
}
private String bfiProjectNumber(PaymentApplication entity) {
/** 按乙方名称(全称或简称)查询客商编码,未维护返回 null */
private String queryCustomerCode(String partyB) {
CustomerArchive customer = customerArchiveMapper.selectOne(Wrappers.<CustomerArchive>lambdaQuery()
.and(wrapper -> wrapper.eq(CustomerArchive::getFullName, partyB).or().eq(CustomerArchive::getShortName, partyB))
.eq(CustomerArchive::getIsDeleted, 0).last("limit 1"));
return customer == null ? null : customer.getCustomerCode();
}
private String bfiProjectNumber(PaymentApplication entity, KingdeeSyncContext context) {
if (entity.getProjectId() == null) return KingdeeBfiConstant.BFI_PROJECT_NUMBER_NONE;
ProjectApply project = projectApplyMapper.selectById(entity.getProjectId());
// 批量同步时按项目id缓存复用(同一项目只查一次),单笔同步退化为直接查询
ProjectApply project = context.projectCache().computeIfAbsent(entity.getProjectId(), projectApplyMapper::selectById);
return project == null || Func.isEmpty(project.getProjectCode()) ? KingdeeBfiConstant.BFI_PROJECT_NUMBER_NONE : project.getProjectCode();
}
@@ -1344,7 +1391,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
private void syncFormalSettlementPayments(PaymentApplication entity) {
if (!"settlement_payment".equals(entity.getPaymentType())) return;
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType())) return;
List<PaymentApplicationSettlement> relations = settlementRelationMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
@@ -1392,7 +1439,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private void validateRequest(PaymentApplicationSaveRequest request) {
if (Func.isEmpty(request.getPaymentType())) throw new ServiceException("付款类型不能为空");
if (!List.of("project_advance", "progress_advance", "settlement_payment").contains(request.getPaymentType())) throw new ServiceException("付款类型不合法");
if (!List.of("project_advance", "progress_advance", TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT).contains(request.getPaymentType())) throw new ServiceException("付款类型不合法");
if (Func.isEmpty(request.getPaymentMethod())) throw new ServiceException("付款方式不能为空");
if (isBillPayment(request.getPaymentMethod()) && request.getBillLedgerId() == null) throw new ServiceException("汇票付款必须选择汇票台账");
if (request.getPaymentRatio() != null && (request.getPaymentRatio().compareTo(BigDecimal.ZERO) < 0 || request.getPaymentRatio().compareTo(BigDecimal.valueOf(100)) > 0)) throw new ServiceException("付款比例必须在0-100之间");
@@ -1401,7 +1448,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
&& request.getPreSettlementId() == null && Func.isEmpty(request.getSettlementIds())) throw new ServiceException("非项目预付必须关联结算单");
if ("project_advance".equals(request.getPaymentType()) && request.getProjectId() == null) throw new ServiceException("项目预付必须选择所属项目");
if ("progress_advance".equals(request.getPaymentType()) && request.getPreSettlementId() == null) throw new ServiceException("进度预付必须关联预结算单");
if ("settlement_payment".equals(request.getPaymentType()) && request.getSettlementId() == null
if (TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) && request.getSettlementId() == null
&& Func.isEmpty(request.getSettlementIds())) throw new ServiceException("结算付款必须关联正式结算单");
}
@@ -1483,7 +1530,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
private List<FormalSettlement> resolveFormalSettlements(PaymentApplicationSaveRequest request, PaymentApplication entity) {
if (!"settlement_payment".equals(request.getPaymentType())) return List.of();
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType())) return List.of();
List<Long> ids = request.getSettlementIds() == null ? List.of() : request.getSettlementIds();
if (ids.isEmpty() && request.getSettlementId() != null) ids = List.of(request.getSettlementId());
ids = ids.stream().filter(Objects::nonNull).distinct().toList();
@@ -1500,7 +1547,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
private boolean hasMultipleFormalReferences(PaymentApplicationSaveRequest request) {
return "settlement_payment".equals(request.getPaymentType()) && request.getSettlementIds() != null
return TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(request.getPaymentType()) && request.getSettlementIds() != null
&& request.getSettlementIds().stream().filter(Objects::nonNull).distinct().count() > 1;
}
@@ -1508,7 +1555,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
BigDecimal settlementAmount = settlements.stream().map(FormalSettlement::getSettlementAmount)
.map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal payableAmount = settlements.stream()
.map(item -> money(item.getSettlementAmount()).subtract(cumulativeAppliedAmount("settlement_payment", item.getId(), entity.getId())).max(BigDecimal.ZERO))
.map(item -> money(item.getSettlementAmount()).subtract(cumulativeAppliedAmount(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT, item.getId(), entity.getId())).max(BigDecimal.ZERO))
.reduce(BigDecimal.ZERO, BigDecimal::add);
if (money(entity.getAppliedAmount()).compareTo(payableAmount) > 0) throw new ServiceException("申请付款金额不能超过所选结算单可付款金额合计");
FormalSettlement first = settlements.get(0);
@@ -1523,14 +1570,14 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId()));
BigDecimal totalBase = settlements.stream().map(item -> money(item.getSettlementAmount()).subtract(
cumulativeAppliedAmount("settlement_payment", item.getId(), entity.getId())).max(BigDecimal.ZERO))
cumulativeAppliedAmount(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT, item.getId(), entity.getId())).max(BigDecimal.ZERO))
.reduce(BigDecimal.ZERO, BigDecimal::add);
BigDecimal remainingApplied = money(entity.getAppliedAmount());
BigDecimal remainingPaid = money(entity.getPaidAmount());
for (int index = 0; index < settlements.size(); index++) {
FormalSettlement settlement = settlements.get(index);
BigDecimal base = money(settlement.getSettlementAmount()).subtract(
cumulativeAppliedAmount("settlement_payment", settlement.getId(), entity.getId())).max(BigDecimal.ZERO);
cumulativeAppliedAmount(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT, settlement.getId(), entity.getId())).max(BigDecimal.ZERO);
BigDecimal applied = index == settlements.size() - 1 ? remainingApplied
: money(entity.getAppliedAmount()).multiply(base).divide(totalBase, 2, RoundingMode.DOWN);
BigDecimal paid = index == settlements.size() - 1 ? remainingPaid
@@ -1586,7 +1633,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
private BigDecimal cumulativeAppliedAmount(String paymentType, Long referenceId, Long excludeId) {
LambdaQueryWrapper<PaymentApplication> wrapper = Wrappers.<PaymentApplication>lambdaQuery()
.eq("settlement_payment".equals(paymentType), PaymentApplication::getSettlementId, referenceId)
.eq(TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(paymentType), PaymentApplication::getSettlementId, referenceId)
.eq("progress_advance".equals(paymentType), PaymentApplication::getPreSettlementId, referenceId)
.ne(excludeId != null, PaymentApplication::getId, excludeId);
return sumApplied(wrapper);
@@ -1701,7 +1748,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
}
private void validateNoTailPayment(PaymentApplication entity) {
if (!"settlement_payment".equals(entity.getPaymentType())) return;
if (!TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT.equals(entity.getPaymentType())) return;
List<Long> formalSettlementIds = new java.util.ArrayList<>();
if (entity.getSettlementId() != null) formalSettlementIds.add(entity.getSettlementId());
formalSettlementIds.addAll(settlementRelationMapper.selectList(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
@@ -9,6 +9,7 @@ import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.FormalSettlementDetailFeeMapper;
import org.springblade.transport.mapper.FormalSettlementDetailMapper;
import org.springblade.transport.mapper.FormalSettlementChangeRecordMapper;
@@ -337,7 +338,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
result.addAll(paymentApplicationMapper.selectList(Wrappers.<PaymentApplication>lambdaQuery()
.select(PaymentApplication::getSettlementId)
.in(PaymentApplication::getSettlementId, formalSettlementIds)
.eq(PaymentApplication::getPaymentType, "settlement_payment")
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
.eq(PaymentApplication::getIsDeleted, 0)
.notIn(PaymentApplication::getApprovalStatus, RETURNED, VOIDED)).stream()
.map(PaymentApplication::getSettlementId).filter(Objects::nonNull).toList());
@@ -355,7 +356,7 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
Wrappers.<PaymentApplication>lambdaQuery()
.select(PaymentApplication::getId)
.in(PaymentApplication::getId, applicationIds)
.eq(PaymentApplication::getPaymentType, "settlement_payment")
.eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
.eq(PaymentApplication::getIsDeleted, 0)
.notIn(PaymentApplication::getApprovalStatus, RETURNED, VOIDED)).stream()
.map(PaymentApplication::getId).filter(Objects::nonNull).toList());
@@ -0,0 +1,64 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments from this software for such purposes.
* Copyright of this software remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is not liable for any claims arising from secondary or illegal development.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.support;
import org.springblade.core.tool.utils.Func;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/**
* 外部系统来源单据号组装工具
* <p>
* 统一外部单据号(如金蝶 awrp_src_code)的拼接规则:主单据号 + 子单据号列表按下划线拼接;
* 自动去空白、去重,便于后续各链路通用编码与扩展处理
*
* @author Chill
*/
public class ExternalSrcCodeSupport {
/**
* 外部单据号拼接分隔符
*/
public static final String SEPARATOR = "_";
private ExternalSrcCodeSupport() {
}
/**
* 组装外部系统来源单据号:主单据号 + 子单据号按下划线拼接
* <p>各部分自动去首尾空格、过滤空白并去重;主单据号为空时仅拼接子单据号</p>
*
* @param mainCode 主单据号(如付款申请号)
* @param partCodes 子单据号列表(如结算明细单据号)
* @return 拼接后的外部来源单据号,无有效值时返回空串
*/
public static String buildSrcCode(String mainCode, Collection<String> partCodes) {
List<String> parts = new ArrayList<>();
if (Func.isNotBlank(mainCode)) {
parts.add(mainCode.trim());
}
if (Func.isNotEmpty(partCodes)) {
partCodes.stream().filter(Func::isNotBlank).map(String::trim).distinct().forEach(parts::add);
}
return String.join(SEPARATOR, parts);
}
}
@@ -23,6 +23,7 @@ import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.constant.KingdeeBfiConstant;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.vo.PaymentApplicationVO;
@@ -39,7 +40,7 @@ public class PaymentApplicationWrapper extends BaseEntityWrapper<PaymentApplicat
vo.setPaymentTypeName(switch (entity.getPaymentType() == null ? "" : entity.getPaymentType()) {
case "project_advance" -> "项目预付";
case "progress_advance" -> "进度预付";
case "settlement_payment" -> "结算付款";
case TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT -> "结算付款";
default -> entity.getPaymentType();
});
vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) {