From 20fddf2c60e2917aba2fcfd6c6bfb695d360a6a6 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 10:48:43 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20-=20=E4=BF=AE=E5=A4=8DBFI=E5=8D=95?=
=?UTF-8?q?=E6=8D=AE=E4=BF=9D=E5=AD=98=E7=BB=93=E6=9E=9C=E5=8F=8D=E5=BA=8F?=
=?UTF-8?q?=E5=88=97=E5=8C=96=E5=A4=B1=E8=B4=A5=E5=AF=BC=E8=87=B4=E9=94=99?=
=?UTF-8?q?=E8=AF=AF=E4=BF=A1=E6=81=AF=E4=B8=A2=E5=A4=B1=E3=80=82=20-=20?=
=?UTF-8?q?=E8=B4=A2=E5=8A=A1=E5=BA=94=E4=BB=98=E5=8D=95=E4=BF=9D=E5=AD=98?=
=?UTF-8?q?A102-2,=E4=BC=A0=E4=B8=9A=E5=8A=A1=E5=AD=97=E6=AE=B5=E3=80=82?=
=?UTF-8?q?=E5=B9=B6=E4=BF=9D=E5=AD=98id=20-=20=E4=BB=98=E6=AC=BE=E7=AE=A1?=
=?UTF-8?q?=E7=90=86=E6=89=B9=E9=87=8F=E5=90=8C=E6=AD=A5=E6=96=B9=E6=B3=95?=
=?UTF-8?q?=E4=BC=98=E5=8C=96?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../constant/TransportBusiConstant.java | 39 ++++
.../service/IPaymentApplicationService.java | 28 +++
.../transport/service/KingdeeSyncContext.java | 46 ++++
.../impl/FormalSettlementServiceImpl.java | 5 +-
.../impl/PaymentApplicationServiceImpl.java | 217 +++++++++++-------
.../impl/SettlementAdjustmentServiceImpl.java | 5 +-
.../support/ExternalSrcCodeSupport.java | 64 ++++++
.../wrapper/PaymentApplicationWrapper.java | 3 +-
.../bfi/pojo/vo/BfiBillSaveResultErrorVO.java | 75 ++++++
.../bfi/pojo/vo/BfiBillSaveResultItemVO.java | 25 +-
.../bfi/pojo/vo/BfiBillSaveResultVO.java | 2 +
.../bfi/service/impl/BfiServiceImpl.java | 14 +-
...¼�付款同步金蝶(财务应付单+付款申请下推)技术文档.md | 86 +++----
13 files changed, 470 insertions(+), 139 deletions(-)
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java
create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/support/ExternalSrcCodeSupport.java
create mode 100644 blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultErrorVO.java
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java
new file mode 100644
index 00000000..bbe3679c
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/TransportBusiConstant.java
@@ -0,0 +1,39 @@
+/**
+ * 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.constant;
+
+/**
+ * 运输业务固定值。
+ *
+ * @author Chill
+ */
+public final class TransportBusiConstant {
+
+ /** 付款类型:结算付款(关联正式结算单的正式付款)。 */
+ public static final String PAYMENT_TYPE_SETTLEMENT = "settlement_payment";
+
+ private TransportBusiConstant() {
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java
index b27d5c28..be0a5cc5 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IPaymentApplicationService.java
@@ -43,8 +43,36 @@ public interface IPaymentApplicationService extends BaseService syncKingdeeBatch(List 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 付款处理查询)
*
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
new file mode 100644
index 00000000..b63d01af
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/KingdeeSyncContext.java
@@ -0,0 +1,46 @@
+/**
+ * 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.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)。
+ *
单笔同步使用 {@link #empty()},各缓存为空时自动回退为逐单查询,行为与原逻辑一致。
+ *
+ * @author Chill
+ */
+public record KingdeeSyncContext(Map cargoCodeByName, Map contractCache,
+ Map customerCodeCache, Map projectCache) {
+
+ /** 空上下文:缓存可写但初始为空,首条记录命中后全批复用 */
+ public static KingdeeSyncContext empty() {
+ return new KingdeeSyncContext(new HashMap<>(), new HashMap<>(), new HashMap<>(), new HashMap<>());
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
index 493511f2..349459b6 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
@@ -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 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 query = Wrappers.lambdaQuery()
.eq(PaymentApplication::getSettlementId, settlement.getId())
- .eq(PaymentApplication::getPaymentType, "settlement_payment")
+ .eq(PaymentApplication::getPaymentType, TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT)
.eq(PaymentApplication::getIsDeleted, 0);
List relations = paymentApplicationSettlementMapper.selectList(
Wrappers.lambdaQuery()
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 9c8d7f5e..c058a26e 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
@@ -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 BaseServiceImpllambdaUpdate()
+ .eq(PaymentApplication::getId, id)
+ .eq(PaymentApplication::getApprovalStatus, APPROVED)
+ .set(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED));
+ }
+
+ @Override
public List syncKingdeeBatch(List ids) {
if (ids == null || ids.isEmpty()) throw new ServiceException("请至少选择一条审批通过的付款申请");
List 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 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 entryGroups, List documentNos) {
- ContractManage contract = requireContract(entity);
+ private String pushBfiApBill(PaymentApplication entity, List entryGroups, List 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 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 entryGroups) {
- ContractManage contract = requireContract(entity);
+ private String pushBfiPayApplyPush(PaymentApplication entity, List 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 settlementDetails = formalSettlementDetails(entity);
if (Func.isEmpty(settlementDetails)) {
throw new ServiceException("推送金蝶财务应付单失败:正式结算单无结算明细,无法解析物料与计量单位");
@@ -982,7 +1020,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl cargoCodeByName = cargoCodeByName();
+ Map cargoCodeByName = cargoCodeByName(context);
LinkedHashMap groupMap = new LinkedHashMap<>();
// 优先按货物费用行逐条取数(blade_formal_settlement_detail_fee 每货物一条,金额为结算调整后的精确值)
List detailFees = formalSettlementDetailFees(settlementDetails);
@@ -1075,25 +1113,30 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl货物编码映射(取自货物类型档案;名称重复且编码不一致时拦截) */
- private Map cargoCodeByName() {
+ /** 货物类型名称->货物编码映射(取自货物类型档案;名称重复且编码不一致时拦截;批量同步时全批共享一次加载结果) */
+ private Map cargoCodeByName(KingdeeSyncContext context) {
+ Map cache = context.cargoCodeByName();
+ if (!cache.isEmpty()) {
+ return cache;
+ }
R> response = sysClient.getCargoTypes();
if (response == null || !response.isSuccess() || Func.isEmpty(response.getData())) {
throw new ServiceException("推送金蝶财务应付单失败:货物类型档案读取失败,请稍后重试");
}
- Map cargoCodeByName = new HashMap<>();
+ Map 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 "项目预付款申请";
case "progress_advance" -> "进度预付款申请";
- case "settlement_payment" -> "结算付款申请";
+ case TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT -> "结算付款申请";
default -> "付款申请";
};
}
@@ -1225,22 +1264,30 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery()
- .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.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 relations = settlementRelationMapper.selectList(
Wrappers.lambdaQuery()
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
@@ -1392,7 +1439,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 0)) throw new ServiceException("付款比例必须在0-100之间");
@@ -1401,7 +1448,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 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 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 1;
}
@@ -1508,7 +1555,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 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 BaseServiceImpllambdaQuery()
.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 wrapper = Wrappers.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 formalSettlementIds = new java.util.ArrayList<>();
if (entity.getSettlementId() != null) formalSettlementIds.add(entity.getSettlementId());
formalSettlementIds.addAll(settlementRelationMapper.selectList(Wrappers.lambdaQuery()
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java
index f43bfae2..d33dafa1 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java
@@ -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 BaseServiceImpllambdaQuery()
.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 BaseServiceImpllambdaQuery()
.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());
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/ExternalSrcCodeSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/ExternalSrcCodeSupport.java
new file mode 100644
index 00000000..9cd5135a
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/ExternalSrcCodeSupport.java
@@ -0,0 +1,64 @@
+/**
+ * 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;
+
+import org.springblade.core.tool.utils.Func;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 外部系统来源单据号组装工具
+ *
+ * 统一外部单据号(如金蝶 awrp_src_code)的拼接规则:主单据号 + 子单据号列表按下划线拼接;
+ * 自动去空白、去重,便于后续各链路通用编码与扩展处理
+ *
+ * @author Chill
+ */
+public class ExternalSrcCodeSupport {
+
+ /**
+ * 外部单据号拼接分隔符
+ */
+ public static final String SEPARATOR = "_";
+
+ private ExternalSrcCodeSupport() {
+ }
+
+ /**
+ * 组装外部系统来源单据号:主单据号 + 子单据号按下划线拼接
+ *
各部分自动去首尾空格、过滤空白并去重;主单据号为空时仅拼接子单据号
+ *
+ * @param mainCode 主单据号(如付款申请号)
+ * @param partCodes 子单据号列表(如结算明细单据号)
+ * @return 拼接后的外部来源单据号,无有效值时返回空串
+ */
+ public static String buildSrcCode(String mainCode, Collection partCodes) {
+ List 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);
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java
index d23438bc..3f4e11b2 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/PaymentApplicationWrapper.java
@@ -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 "项目预付";
case "progress_advance" -> "进度预付";
- case "settlement_payment" -> "结算付款";
+ case TransportBusiConstant.PAYMENT_TYPE_SETTLEMENT -> "结算付款";
default -> entity.getPaymentType();
});
vo.setApprovalStatusName(switch (entity.getApprovalStatus() == null ? "" : entity.getApprovalStatus()) {
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultErrorVO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultErrorVO.java
new file mode 100644
index 00000000..d471d80e
--- /dev/null
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultErrorVO.java
@@ -0,0 +1,75 @@
+/**
+ * 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.thirdparty.bfi.pojo.vo;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 业财融合(BFI)单据保存返回错误明细
+ *
+ * @author Chill
+ */
+@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
+public class BfiBillSaveResultErrorVO implements Serializable {
+ @Serial
+ private static final long serialVersionUID = 1L;
+ /**
+ * 实体键
+ */
+ private String entityKey;
+ /**
+ * 分录行号
+ */
+ private Integer entryRowIndex;
+ /**
+ * 单据键值,含 awrp_src_code、awrp_src_sys
+ */
+ private Map keys;
+ /**
+ * 行错误信息
+ */
+ private List rowMsg;
+ /**
+ * 子分录行号
+ */
+ private Integer subEntryRowIndex;
+
+ /**
+ * 拼接行错误信息
+ *
+ * @return 错误信息文本
+ */
+ public String getRowMsgText() {
+ return rowMsg == null || rowMsg.isEmpty() ? "" : String.join(";", rowMsg);
+ }
+}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultItemVO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultItemVO.java
index 165fca8c..341cdca8 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultItemVO.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultItemVO.java
@@ -25,6 +25,7 @@
*/
package org.springblade.thirdparty.bfi.pojo.vo;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
@@ -33,6 +34,7 @@ import java.io.Serial;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
+import java.util.stream.Collectors;
/**
* 业财融合(BFI)A103-1 / A103-3 付款申请保存返回结果明细
@@ -40,6 +42,7 @@ import java.util.Map;
* @author Chill
*/
@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
public class BfiBillSaveResultItemVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@@ -52,9 +55,13 @@ public class BfiBillSaveResultItemVO implements Serializable {
*/
private Boolean billStatus;
/**
- * 错误信息
+ * 分录id集合
*/
- private List errors;
+ private Map entryIds;
+ /**
+ * 错误明细
+ */
+ private List errors;
/**
* 单据id
*/
@@ -69,7 +76,19 @@ public class BfiBillSaveResultItemVO implements Serializable {
*/
private String number;
/**
- * 操作类型:Add 新增、Push 下推
+ * 操作类型:Add 新增、Push 下推、Update 修改
*/
private String type;
+
+ /**
+ * 拼接错误明细信息
+ *
+ * @return 错误信息文本
+ */
+ public String getErrorText() {
+ return errors == null || errors.isEmpty() ? "" : errors.stream()
+ .map(BfiBillSaveResultErrorVO::getRowMsgText)
+ .filter(text -> !text.isEmpty())
+ .collect(Collectors.joining(";"));
+ }
}
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultVO.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultVO.java
index 9cea1bc5..d4fd36e4 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultVO.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/pojo/vo/BfiBillSaveResultVO.java
@@ -25,6 +25,7 @@
*/
package org.springblade.thirdparty.bfi.pojo.vo;
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.io.Serial;
@@ -37,6 +38,7 @@ import java.util.List;
* @author Chill
*/
@Data
+@JsonIgnoreProperties(ignoreUnknown = true)
public class BfiBillSaveResultVO implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java
index 2de3b1d7..5f19471c 100644
--- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java
+++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/impl/BfiServiceImpl.java
@@ -41,7 +41,6 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiApBillSaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillSaveBodyDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiArBillSaveDTO;
-import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimAnnounceQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimPushBodyDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiClaimPushDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiCustomerQueryDTO;
@@ -63,10 +62,10 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplyQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveBodyDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiPayApplySaveDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiPayBillQueryDTO;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveBodyDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiReceivedInvoiceSaveDTO;
-import org.springblade.thirdparty.bfi.pojo.dto.BfiRecBillQueryDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiTokenDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiVerifyTokenDTO;
import org.springblade.thirdparty.bfi.pojo.dto.BfiWithdrawTokenDTO;
@@ -82,8 +81,8 @@ import org.springblade.thirdparty.bfi.pojo.vo.BfiOpenApiResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPageVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayApplyVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiPayBillVO;
-import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiRecBillVO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiReceivedInvoiceVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiResultVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiTokenVO;
import org.springblade.thirdparty.bfi.pojo.vo.BfiVerifyTokenVO;
@@ -529,10 +528,13 @@ public class BfiServiceImpl implements IBfiService {
if (result != null && result.isSuccess()) {
return result;
}
- log.error("调用BFI接口 {}异常响应参数:{}", type, JSONUtil.toJsonStr(result));
- String message = Optional.ofNullable(result).map(BfiResultVO::getMessage).orElse("");
- throw new BfiException("调用BFI接口" + type + "异常 " + message);
+ log.error("调用BFI接口 {}异常响应参数:{}", type, JSONUtil.toJsonStr(result));
+ String message = Optional.ofNullable(result).map(BfiResultVO::getMessage).filter(StringUtil::isNotBlank)
+ .orElseGet(() -> Optional.ofNullable(result).map(BfiResultVO::getErrorCode)
+ .map(errorCode -> "错误码 " + errorCode).orElse("响应为空或解析失败"));
+ throw new BfiException("调用BFI接口" + type + "异常 " + message);
} catch (BfiException e) {
+ log.error("调用BFI接口 {},BfiException异常", type, e);
throw e;
} catch (Exception e) {
log.error("调用BFI接口 {}异常", type, e);
diff --git a/doc/docs/业务流程/2026-09-16-付款管理正式付款同步金蝶(财务应付单+付款申请下推)技术文档.md b/doc/docs/业务流程/2026-09-16-付款管理正式付款同步金蝶(财务应付单+付款申请下推)技术文档.md
index 28d5410a..c0df77c1 100644
--- a/doc/docs/业务流程/2026-09-16-付款管理正式付款同步金蝶(财务应付单+付款申请下推)技术文档.md
+++ b/doc/docs/业务流程/2026-09-16-付款管理正式付款同步金蝶(财务应付单+付款申请下推)技术文档.md
@@ -3,7 +3,7 @@
> 适用系统:TMS(BladeX 微服务平台,服务:blade-transport)
> 对接系统:金蝶云·星瀚(业财融合 BFI,经 iPaaS 网关)
> 关联文档:`doc/docs/TMS业财融合-集成流程图文字版与业务流程梳理.md`、`doc/docs/业财融合系统接口文档.xlsx`、`doc/docs/业务流程/付款申请同步金蝶(星瀚)技术文档.md`(A103-3 预付款路径)
-> 最后更新:2026-09-16
+> 最后更新:2026-09-18(字段映射与联调默认值按当前代码重新梳理)
---
@@ -63,12 +63,12 @@
| 约定 | 说明 |
| --- | --- |
| 状态校验 | 仅 `approvalStatus = approved` 允许同步 |
-| 幂等跳过 | `kingdeeStatus = synced` 的单据直接返回已存单据号,不重复推送 |
-| ①防重键 | 星瀚应付单侧 `awrp_src_code`(=paymentNo)+ `awrp_src_sys`,②失败后重推时①走更新语义,不重复生成应付单 |
-| ②防重键 | 付款申请单侧 `awrp_src_code`(=paymentNo)+ `awrp_src_sys`;分录行 `awrp_entry_code`(=paymentNo + "-1")不得重复 |
-| ②关联键 | `awrp_finapbill_entryid` = ①明细的 `awrp_entry_code`(=paymentNo + "-1"),**不依赖①返回的内部 id** |
+| 幂等跳过 | `kingdeeStatus = synced/paying/paid/closed` 的单据直接返回已存单据号,不重复推送 |
+| ①防重键 | 星瀚应付单侧 `awrp_src_code`(=付款申请号_结算明细单据号1_单据号2...,去重拼接)+ `awrp_src_sys`,②失败后重推时①走更新语义,不重复生成应付单 |
+| ②防重键 | 付款申请单侧 `awrp_src_code`(=paymentNo)+ `awrp_src_sys`;分录行 `awrp_entry_code`(=paymentNo + "-序号")不得重复 |
+| ②关联键 | `awrp_finapbill_entryid` = ①明细的 `awrp_entry_code`(=paymentNo + "-序号"),**不依赖①返回的内部 id** |
| 失败重推 | 任一步失败置 `kingdeeStatus = failed`,可重新勾选同步(①已成功的应付单按防重键走更新语义) |
-| 前置校验 | ①推送前校验物料/计量单位常量非空(见 5.2);未关联合同、合同不存在、组织编码为空、结算币种为空时同步失败 |
+| 前置校验 | 分录解析前校验:未关联正式结算明细、货物类型未维护档案编码、计量单位为空均报错;未关联合同、组织编码为空、结算币种为空时同步失败 |
---
@@ -95,37 +95,36 @@
| 接口字段 | 含义 | 必填 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| billtypeid_number | 单据类型.编码 | 是 | 按映射字典转换:`KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME`(默认"服务采购应付")经 `AP_BILL_BILLTYPE_MAP` 名称→编码 | 路由规则见 5.3;未维护名称报错 |
-| org_number | 结算组织.编码 | 是 | **合同所属组织**:`contract.organizationId` → `dept.deptCode`,空回退组织名称(复用 `bfiOrgNumber`) | 缺失报错 |
+| org_number | 结算组织.编码 | 是 | **合同所属组织**:`contract.organizationId` → `dept.deptCode`(复用 `bfiOrgNumber`) | 缺失报错 |
| bizdate | 单据日期 | 是 | `applyDate`(空取当天) | 沿用 yyyyMMdd,**待确认是否需 yyyy-MM-dd** |
-| asstacttype | 往来类型 | 是 | 常量 `bd_supplier` | 供应商 |
-| asstact_number | 往来户.编码 | 是 | 复用 `bfiSupplierCode`:payeeName 匹配客商档案 `customerCode`,未命中回退 payeeName | |
-| purmode | 付款方式 | 是 | 常量 `BFI_AP_BILL_PUR_MODE` | `CREDIT` 赊购(金蝶侧可调金额);`CASH` 会自动 100% 推付款 |
-| currency_number | 结算币别.货币代码 | 是 | **合同结算币种** `contract.settlementCurrency`(复用 `bfiCurrency`) | 缺失报错 |
-| payorg_number / purorg_number | 付款/采购组织.编码 | 是/否 | 同 org_number | |
+| asstacttype | 往来类型 | 是 | **按合同类别路由**(`bfiAsstactType`):`contractCategory = 客户合同` → `bd_customer`,否则 → `bd_supplier` | 与结算类型路由口径一致 |
+| asstact_number | 往来户.编码 | 是 | **合同乙方**(`bfiAsstactNumber`):`contract.partyB` → 客商档案按全称/简称匹配取 `customerCode` | 乙方为空或客商档案未维护编码均报错,不回退原值 |
+| purmode | 付款方式 | 是 | 常量 `BFI_AP_BILL_PUR_MODE_CASH` | `CASH` 现购(系统自动 100% 推付款) |
+| currency_number | 结算币别.货币代码 | 是 | **合同结算币种** `contract.settlementCurrency`(复用 `bfiCurrency`);`RMB` 统一映射为 `CNY` | 缺失报错 |
+| payorg_number | 付款组织.编码 | 是 | 同 org_number | 采购组织 purorg_number 暂不传 |
| payproperty_number | 款项性质.编码 | 是 | 常量 `BFI_AP_BILL_PAY_PROPERTY` | 暂按 `2001`,**待金蝶确认** |
-| isincludetax / ispricetotal | 录入含税价 / 录入总价 | 否 | `true` / `true` | 录入总价免单价数量换算 |
-| exratetable_number / exratedate | 汇率表/汇率日期 | 否 | 复用配置 `thirdParty.bfi.payApply.exratetable`(默认 01)/ 同 bizdate | |
-| asstactname | 往来单位名称 | 否 | `payeeName` | |
-| remark | 备注 | 否 | `paymentNo + " " + settlementNo` | |
-| awrp_src_sys / awrp_src_code | 外部系统编号/来源单据号 | 是 | 配置 `thirdParty.bfi.payApply.srcSys` / `paymentNo` | 应付单候选键 |
-| awrp_src_bt / awrp_src_con_no | 来源单据/上游合同号 | 否 | `awrp_contract_info` / `contractNo` | 有合同即传 |
+| isincludetax / ispricetotal | 录入含税价 / 录入总价 | 否 | 固定 `true` / `true`;`isintax` 固定 `false` | 录入总价免单价换算 |
+| awrp_src_sys | 外部系统编号 | 是 | 配置 `thirdParty.bfi.payApply.srcSys` | 应付单候选键 |
+| awrp_src_code | 外部系统来源单据号 | 是 | **`paymentNo + "_" + 结算明细单据号1_单据号2...`**(单据号去重、保持行序,`String.join("_")`) | 应付单候选键 |
| awrp_src_type | 外部单据类型 | 是 | 常量 `BFI_AP_BILL_SRC_TYPE` | 暂按 `TMS财务应付单`,**待金蝶确认** |
-未传的可选字段:settlementtype_number(结算方式)、taxrateid_number(税率)、duedate、bookdate、department_number、preentry(预付信息)等。
+未传的可选字段(代码注释保留):exratetable_number/exratedate(汇率表/汇率日期)、asstactname(往来单位名称)、remark(备注)、awrp_src_bt(来源单据)、awrp_src_con_no(上游合同号)、settlementtype_number(结算方式)、taxrateid_number(税率)、purorg_number(采购组织)、duedate、bookdate、department_number、preentry(预付信息)等。
-### 4.2 A102-2 明细(`BfiApBillSaveEntryDTO`,单条申请固定 1 行)
+### 4.2 A102-2 明细(`BfiApBillSaveEntryDTO`,按货物编码+计量单位聚合,多行)
+
+分录数据由 `resolveBfiEntryGroups` 解析:**货物费用行**(`blade_formal_settlement_detail_fee`,每货物一条)优先,无费用行时兜底**结算明细行**;按货物编码+计量单位聚合(一行含多种货物类型时金额数量均分),已移除按申请付款金额分摊逻辑,**金额直接取结算金额**。
| 接口字段 | 含义 | 必填 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
-| material_number | 物料编码.编码 | 是 | 常量 `BFI_AP_BILL_MATERIAL` | **TMS 无物料档案,当前为空串,推送前校验拦截;待金蝶提供运输服务物料编码** |
-| measureunit_number | 计量单位.编码 | 是 | 常量 `BFI_AP_BILL_MEASURE_UNIT` | **同上,待金蝶确认(如"吨")** |
-| quantity | 明细.数量 | 是 | 固定 `1` | 结算单无吨位数据,配合 ispricetotal=true 录入总价 |
-| e_pricetaxtotal | 明细.应付金额 | 是 | `appliedAmount`(2 位小数) | 含税应付金额 = 申请付款金额 |
-| project_number | 项目.项目编码 | 是 | 复用 `bfiProjectNumber`:`projectId` → `ProjectApply.projectCode`,无项目传 `999` | |
-| awrp_entry_code | 外部系统来源分录标识 | 是 | `paymentNo + "-1"` | 候选键;②下推时以 `awrp_finapbill_entryid` 关联此行 |
-| e_remark | 明细.备注 | 否 | `remark` | |
+| material_number | 物料编码.编码 | 是 | **联调临时:常量 `BFI_AP_BILL_MATERIAL_NUMBER_TEST`(`SP0324`)**(调试块 `if(true)` 强制覆盖) | 真实来源:货物类型 → 货物类型档案 `cargoCode`,档案对齐后恢复 |
+| measureunit_number | 计量单位.编码 | 是 | **联调临时:常量 `BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST`(`TON`)**(同上) | 真实来源:快照运费计算单位 `quantityUnit`(档案暂无编码字段) |
+| quantity | 明细.数量 | 是 | 组内 `transportQuantity` 合计 | ≤0 兜底 1 |
+| e_pricetaxtotal | 明细.应付金额 | 是 | 组内 `settlementAmountTax`(结算金额含税)合计 | **必传**,缺失金蝶报"应付金额不能为空" |
+| e_amount | 明细.金额(不含税) | 否 | 组内 `settlementAmountNoTax`(结算金额不含税)合计 | |
+| project_number | 项目.项目编码 | 是 | **联调临时:常量 `BFI_AP_BILL_PROJECT_NUMBER_TEST`(`999`)** | 真实来源:`projectId` → `ProjectApply.projectCode`,无项目传 `999` |
+| awrp_entry_code | 外部系统来源分录标识 | 是 | `paymentNo + "-" + 组序号` | 候选键;②下推时以 `awrp_finapbill_entryid` 关联此行 |
-未传:planentity 付款计划(不传由金蝶按明细自动汇总)、e_amount/e_tax、taxrateid_number、awrp_src_con_eno。
+未传:planentity 付款计划(不传由金蝶按明细自动汇总)、e_tax、taxrateid_number、e_remark、awrp_src_con_eno。
### 4.3 A103-1 主表(`BfiPayApplyPushDTO`)
@@ -139,20 +138,20 @@
| awrp_textfield1 | 付款渠道 | 否 | 复用 `bfiPaymentMethodLabel`:`paymentMethod` 经字典 `pay_method` 翻译为标签 | |
| creator_name | 申请人 | 否 | `applicantName` | |
-### 4.4 A103-1 分录(`BfiPayApplyPushEntryDTO`,固定 1 行)
+### 4.4 A103-1 分录(`BfiPayApplyPushEntryDTO`,与应付单明细一一对应多行)
| 接口字段 | 含义 | 必填 | 来源 | 说明 |
| --- | --- | --- | --- | --- |
| e_paymenttype_number | 付款类型.编码 | 是 | 常量 `201` | 正式付款 |
-| awrp_finapbill_entryid | 结算单分录id | 是 | `paymentNo + "-1"` | **对应 A102-2 明细 awrp_entry_code** |
-| awrp_entry_code | 对方唯一行 id | 是 | `paymentNo + "-1"` | 不得重复 |
+| awrp_finapbill_entryid | 结算单分录id | 是 | `paymentNo + "-" + 组序号` | **对应 A102-2 明细 awrp_entry_code** |
+| awrp_entry_code | 对方唯一行 id | 是 | `paymentNo + "-" + 组序号` | 不得重复 |
| e_assacct | 往来账户 | 是 | `bankAccount` | 收款账号(页面"收款账户") |
| e_bebank_number | 往来银行.编码 | 是 | `bankName` | **必填编码,当前暂传银行名称,待银行档案编码映射**(与 A103-3 遗留项一致) |
-| e_applyamount | 申请金额 | 是 | `appliedAmount`(2 位小数) | |
+| e_applyamount | 申请金额 | 是 | 组内 `settlementAmountTax`(结算金额含税) | 与对应应付单分录 `e_pricetaxtotal` 一致 |
| e_expaydate | 期望付款日 | 否 | 同 applydate | |
-| project_number | 项目.项目编码 | 是 | 复用 `bfiProjectNumber` | 无项目传 `999` |
-| awrp_src_type | 外部单据类型 | 是 | 复用配置 `payApply.srcType` | |
+| project_number | 项目.项目编码 | 是 | **联调临时:常量 `BFI_AP_BILL_PROJECT_NUMBER_TEST`(`999`)**(真实取值 `bfiProjectNumber` 代码注释保留) | 无项目传 `999` |
| e_remark | 备注 | 否 | `remark` | |
+| awrp_src_type | 外部单据类型 | 是 | 复用配置 `payApply.srcType` | |
未传:e_duedate、e_settlementtype_number、e_costcenter_number、awrp_splb_number。
@@ -170,12 +169,16 @@ A102-2 所需的金蝶侧固定档案编码**不进 Nacos 配置**,统一定
| --- | --- | --- | --- |
| BFI_AP_BILL_BILLTYPE_NAME | `服务采购应付` | A102-2 单据类型默认名称,经 `AP_BILL_BILLTYPE_MAP` 转换为编码(`ApFin_service_BT_S`) | 待金蝶确认 |
| AP_BILL_BILLTYPE_MAP | 9 项映射 | 财务应付单类型字典(借贷项调整/费用采购应付/其他应付/产品委外应付/标准采购应付/采购费用应付/销售费用应付/服务采购应付/资产采购应付) | 全量字典 |
-| AR_BILL_BILLTYPE_MAP | 5 项映射 | 财务应收单类型字典(借贷项调整/其他应收/销售费用应收/服务销售应收/标准销售应收),当前付款链路未启用 | 全量字典 |
+| AR_BILL_BILLTYPE_MAP | 5 项映射 | 财务应收单类型字典,当前付款链路未启用 | 全量字典 |
| PAY_APPLY_BILLTYPE_MAP | 2 项映射 | 付款申请单类型字典(采购付款申请/其他付款申请),使用规则待业务确认 | 全量字典 |
+| BFI_SUPPLIER_ASSTACT_TYPE | `bd_supplier` | 往来类型-供应商 | 启用 |
+| BFI_CUSTOMER_ASSTACT_TYPE | `bd_customer` | 往来类型-客户(合同类别为"客户合同"时使用) | 启用 |
| BFI_AP_BILL_PAY_PROPERTY | `2001` | A102-2 款项性质 | 待金蝶确认 |
-| BFI_AP_BILL_PUR_MODE | `CREDIT` | A102-2 付款方式(赊购) | 待业务确认(CASH=现购自动推付款) |
-| BFI_AP_BILL_MATERIAL | 空串 | A102-2 明细物料编码 | **待金蝶提供,为空时推送前拦截报错** |
-| BFI_AP_BILL_MEASURE_UNIT | 空串 | A102-2 明细计量单位 | **待金蝶确认,为空时推送前拦截报错** |
+| BFI_AP_BILL_PUR_MODE_CASH | `CASH` | A102-2 付款方式(现购,自动 100% 推付款) | 启用 |
+| BFI_AP_BILL_PUR_MODE_CREDIT | `CREDIT` | A102-2 付款方式(赊购,金蝶侧可调金额) | 备用 |
+| BFI_AP_BILL_MATERIAL_NUMBER_TEST | `SP0324` | **联调临时**:明细物料编码,调试块强制覆盖真实取值 | 联调后恢复档案取值 |
+| BFI_AP_BILL_MEASUREUNIT_NUMBER_TEST | `TON` | **联调临时**:明细计量单位编码,同上 | 联调后恢复档案取值 |
+| BFI_AP_BILL_PROJECT_NUMBER_TEST | `999` | **联调临时**:A102-2/A103-1 项目编码 | 档案对齐后恢复 `bfiProjectNumber` |
| BFI_AP_BILL_SRC_TYPE | `TMS财务应付单` | A102-2 外部单据类型 | 待金蝶确认 |
### 5.3 单据类型路由与映射转换
@@ -195,6 +198,8 @@ A102-2 所需的金蝶侧固定档案编码**不进 Nacos 配置**,统一定
| 结算方式/税率 | settlementtype_number、taxrateid_number 不传 | 金蝶是否强制、编码取值 |
| 预付抵扣 | preentry(y_billtype/y_billid/y_settleamt)不传 | 存在预付款抵扣时是否需传,本期不传、后续迭代 |
| 银行编码 | e_bebank_number 暂传银行名称 | 必填编码,待银行档案编码映射(与 A103-3 遗留项相同) |
+| 物料/计量单位/项目编码 | 联调期临时传金蝶测试档案值(SP0324/TON/999,见 4.2/4.4 与 5.2) | 档案对齐后移除 A102-2 明细中的调试块并恢复真实取值 |
+| 币种映射 | 合同结算币种 `RMB` 统一映射为 `CNY`(`bfiCurrency`) | 长期保留 |
| 付款申请单/收入确认单类型映射 | 字典已存 `PAY_APPLY_BILLTYPE_MAP`(2 项);收入确认单(ar_revcfmbill_*)字典未入库 | 付款申请单、收入确认单的映射使用规则待业务确认 |
---
@@ -212,7 +217,8 @@ A102-2 所需的金蝶侧固定档案编码**不进 Nacos 配置**,统一定
| 单笔同步 `syncKingdee` | 校验失败/推送失败置 `failed` 并抛 `ServiceException`,前端提示原因(含金蝶 errors 明细) |
| ①成功②失败 | `kingdeeStatus = failed`;重推时①按防重键走更新语义,继续② |
| 批量同步 `syncKingdeeBatch` | 逐单循环,**单条失败不中断批次**;返回结果列表附 `付款申请[id]同步失败:原因` |
-| 物料/计量单位常量为空 | 推送前拦截,报错提示待金蝶侧确认补常量 |
+| 货物类型未维护档案编码 / 计量单位为空 | 分录解析(`collectEntryGroups`)时拦截,报错提示补充档案 |
+| 合同乙方为空 / 客商档案未维护客商编码 | 往来户取值(`bfiAsstactNumber`)时拦截,报错提示合同号与乙方名称 |
| 金蝶接口级失败 | `BfiServiceImpl.commonCall` 抛 `BfiException`,同上处理 |
| 重复点击同步 | 已 `synced` 单据跳过;`failed`/`unsynced` 允许重推 |
@@ -220,7 +226,7 @@ A102-2 所需的金蝶侧固定档案编码**不进 Nacos 配置**,统一定
## 8. 遗留事项
-1. **物料/计量单位编码**:A102-2 明细必填,待金蝶侧提供后补充 `BFI_AP_BILL_MATERIAL` / `BFI_AP_BILL_MEASURE_UNIT` 常量,在此之前正式付款同步会主动拦截报错。
+1. **物料/计量单位/项目编码(联调临时)**:A102-2 明细当前经调试块固定传金蝶测试档案值(`SP0324`/`TON`,见 `pushBfiApBill` 内 `if(true)` 调试代码),项目编码 A102-2/A103-1 固定传 `999`;金蝶侧档案对齐后需**删除调试块**并恢复真实取值(货物类型档案编码 / 快照计量单位 / `bfiProjectNumber`)。
2. **应付单 id 不落库**:A102-2 仅校验保存成功;后续 A102-4 勾稽、应付单附件、反审核删除等需要 id 的场景,按防重键经 `queryApBill` 二次查询。
3. **银行编码映射**:e_bebank_number 暂传银行名称,待建立银行档案编码映射(与预付款路径遗留项相同)。
4. **预付抵扣 preentry**:正式付款涉及预付款抵扣时是否传预付信息,本期不传、后续迭代。