Merge remote-tracking branch 'gitee/master'

付款申请预付联动冲突按拉下来的版本处理:金蝶生成即回显预结算预付信息,并保留多结算单保存逻辑。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-16 16:12:46 +08:00
13 changed files with 781 additions and 93 deletions
@@ -307,6 +307,12 @@ public class BfiProperties {
*/
private String invoiceTypes = "";
/**
* 客商单位过滤开关:开启后只同步开票单位(销方)与受票单位(购方)税号均已登记在
* 客商发票信息(blade_customer_invoice_info)中的发票,前端票池展示查询同用此开关过滤,默认关闭
*/
private boolean customerUnitsOnly = false;
/**
* 同步 single-flight 锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放
*/
@@ -44,11 +44,13 @@ public interface KingdeeInvoicePoolMapper extends BaseMapper<KingdeeInvoicePool>
/**
* 金蝶票据池镜像统一分页查询:仅出可用票(剩余可用金额>0 且未使用),认领/登记标记由 EXISTS 子查询直出
*
* @param page 分页参数
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
* @param tenantId 当前租户
* @param page 分页参数
* @param query 统一查询条件(已在上层完成 trim 与日期格式校验)
* @param tenantId 当前租户
* @param customerUnitsOnly 客商单位过滤开关:开启时只出销/购方税号均登记在客商发票信息中的发票(与定时同步同开关)
* @return 统一视图分页
*/
IPage<KingdeeInvoicePoolVO> selectInvoicePoolPage(IPage<KingdeeInvoicePoolVO> page,
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId);
@Param("query") KingdeeInvoicePoolQueryDTO query, @Param("tenantId") String tenantId,
@Param("customerUnitsOnly") boolean customerUnitsOnly);
}
@@ -39,12 +39,24 @@
AND p.available_amount &gt; 0 AND p.amount_use_status = '0'
</sql>
<!-- 客商单位过滤:开票单位(销方)与受票单位(购方)税号均须登记在有效客商发票信息中
(与定时同步拉取过滤同开关同口径;自定义 SQL 无租户插件,手工带租户与逻辑删除) -->
<sql id="customerUnitsFilter">
AND EXISTS (SELECT 1 FROM blade_customer_invoice_info ci
WHERE ci.is_deleted = 0 AND ci.status = 1 AND ci.tenant_id = p.tenant_id AND ci.tax_no = p.saler_tax_no)
AND EXISTS (SELECT 1 FROM blade_customer_invoice_info cj
WHERE cj.is_deleted = 0 AND cj.status = 1 AND cj.tenant_id = p.tenant_id AND cj.tax_no = p.buyer_tax_no)
</sql>
<select id="selectInvoicePoolPage" resultType="org.springblade.transport.pojo.vo.KingdeeInvoicePoolVO">
SELECT
<include refid="invoicePoolVoColumns"/>
FROM blade_kingdee_invoice_pool p
WHERE p.is_deleted = 0 AND p.status = 1 AND p.tenant_id = #{tenantId}
<include refid="usableFilter"/>
<if test="customerUnitsOnly">
<include refid="customerUnitsFilter"/>
</if>
<if test="query.invoiceNo != null and query.invoiceNo != ''">
AND p.invoice_no LIKE CONCAT('%', #{query.invoiceNo}, '%')
</if>
@@ -97,6 +97,7 @@ import org.springblade.transport.pojo.vo.InvoiceApplicationSheetVO;
import org.springblade.transport.pojo.vo.InvoiceApplicationVO;
import org.springblade.transport.service.IInvoiceApplicationService;
import org.springblade.transport.service.IKingdeeBillLockService;
import org.springblade.transport.service.KingdeeSyncContext;
import org.springblade.transport.support.KingdeeApBillSyncSupport;
import org.springblade.transport.support.KingdeeArBillSyncSupport;
import org.springblade.transport.support.KingdeeSyncLockSupport;
@@ -806,6 +807,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
if (dept == null || Func.isBlank(dept.getKingdeeDeptCode())) {
throw new ServiceException("推送金蝶开票申请失败:申请部门[" + Func.toStr(entity.getDeptId()) + "]未维护金蝶组织编码,请检查部门数据");
}
// 金蝶组织编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码
String kingdeeOrgNumber = kingdeeApBillSyncSupport.kingdeeOrgNumber(entity.getProjectId(), KingdeeSyncContext.empty());
CustomerArchive customer = entity.getReceiverCustomerId() == null ? null : customerArchiveMapper.selectById(entity.getReceiverCustomerId());
if (customer == null || Func.isBlank(customer.getCustomerCode())) {
throw new ServiceException("推送金蝶开票申请失败:受票方客户未在客商档案中维护客商编码,请补充后重试");
@@ -836,7 +839,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
+ "]无商品行,无法组装分录");
}
BfiInvoiceApplySaveDTO param = this.buildInvoiceApplyParam(entity, sheetBillNo, seller, salerAddr, salerBank,
dept, customer, firstArSrcCode, money(sheet.getInvoiceAmount()), items);
dept, kingdeeOrgNumber, customer, firstArSrcCode, money(sheet.getInvoiceAmount()), items);
log.info("金蝶开票申请推送:开票申请[{}]发票张[{}],billno=[{}],价税合计=[{}],分录{}条",
entity.getApplicationNo(), sheet.getSheetNo(), sheetBillNo, sheet.getInvoiceAmount(), items.size());
BfiBillSaveResultVO saveResult = bfiService.saveInvoiceApply(param);
@@ -885,16 +888,17 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
* 组装 A303-1 单张开票申请单入参(字段映射见设计文档 6.3/6.4),
* 购销方等主表公共字段随开票申请取数,价税合计与单据体分录取本张发票
*
* @param orgNumber 金蝶组织编码(项目业务部门反查平台公司)
* @param firstArSrcCode 上游来源单据号:首张关联结算单的金蝶应收单防重键 awrp_src_code(多张结算单取第一张)
*/
private BfiInvoiceApplySaveDTO buildInvoiceApplyParam(InvoiceApplication entity, String billNo,
CustomerArchive seller, String salerAddr, String salerBank, Dept dept, CustomerArchive customer,
CustomerArchive seller, String salerAddr, String salerBank, Dept dept, String orgNumber, CustomerArchive customer,
String firstArSrcCode, BigDecimal sheetAmount, List<BfiInvoiceApplySaveEntryDTO> items) {
BfiInvoiceApplySaveDTO param = new BfiInvoiceApplySaveDTO();
// ==================== 以下为【必填】字段(BfiInvoiceApplySaveDTO 标注),非必填字段先注释保留 ====================
// 【必填】组织.编码:申请部门 deptId → 金蝶组织编码
param.setOrgidNumber(dept.getKingdeeDeptCode());
// 【必填】申请部门.编码:同上
// 【必填】组织.编码:项目业务部门反查唯一平台公司 → 平台公司金蝶组织编码
param.setOrgidNumber(orgNumber);
// 【必填】申请部门.编码:申请部门 deptId → 金蝶部门编码
param.setAwrpOrgfieldNumber(dept.getKingdeeDeptCode());
// 【必填】业务单据编号(候选键):单张发票候选 billno(多张发票时开票申请单号-张次序号)
param.setBillno(billNo);
@@ -63,6 +63,7 @@ import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.InvoiceReceiptMapper;
import org.springblade.transport.mapper.InvoiceReceiptRecordMapper;
import org.springblade.transport.mapper.InvoiceReceiptSettlementMapper;
import org.springblade.transport.mapper.CustomerInvoiceInfoMapper;
import org.springblade.transport.mapper.KingdeeInvoicePoolMapper;
import org.springblade.transport.pojo.dto.InvoiceReceiptSaveRequest;
import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest;
@@ -70,6 +71,7 @@ import org.springblade.transport.pojo.dto.KingdeeInvoicePoolQueryDTO;
import org.springblade.transport.pojo.entity.ContractManage;
import org.springblade.transport.pojo.entity.CustomerArchive;
import org.springblade.transport.pojo.entity.CustomerContact;
import org.springblade.transport.pojo.entity.CustomerInvoiceInfo;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.FormalSettlementDetail;
import org.springblade.transport.pojo.entity.InvoiceReceipt;
@@ -146,6 +148,7 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
private final FormalSettlementDetailMapper formalSettlementDetailMapper;
private final CustomerArchiveMapper customerArchiveMapper;
private final CustomerContactMapper customerContactMapper;
private final CustomerInvoiceInfoMapper customerInvoiceInfoMapper;
private final IBfiService bfiService;
private final BfiProperties bfiProperties;
private final KingdeeApBillSyncSupport kingdeeApBillSyncSupport;
@@ -243,9 +246,10 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
return page;
}
/** 本地镜像真分页(可用过滤/认领登记标记均在 SQL 内直出) */
/** 本地镜像真分页(可用过滤/认领登记标记均在 SQL 内直出;客商单位过滤与定时同步同开关同口径) */
private IPage<KingdeeInvoicePoolVO> selectLocalPoolPage(KingdeeInvoicePoolQueryDTO query, long current, long size) {
return invoicePoolMapper.selectInvoicePoolPage(new Page<>(current, size), query, AuthUtil.getTenantId());
return invoicePoolMapper.selectInvoicePoolPage(new Page<>(current, size), query, AuthUtil.getTenantId(),
bfiProperties.getInvoiceReceipt().isCustomerUnitsOnly());
}
/**
@@ -1248,20 +1252,25 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
LocalDateTime windowStart = LocalDateTime.now().minusDays(bfiProperties.getInvoiceReceipt().getIncrementDays());
LocalDateTime windowEnd = LocalDateTime.now();
List<String> invoiceTypes = this.poolInvoiceTypeCodes();
// 客商单位过滤集:开关开启时一次性载入有效客商发票信息税号(销/购方税号都须命中才入镜像),整轮共享避免逐页查询
Set<String> customerTaxNos = bfiProperties.getInvoiceReceipt().isCustomerUnitsOnly() ? this.customerInvoiceTaxNos() : null;
SyncStats stats = new SyncStats();
log.info("金蝶全票池同步开始,增量窗口 {} ~ {},发票种类:{},单页 {} 条最多 {} 页(触顶自动二分切片)",
log.info("金蝶全票池同步开始,增量窗口 {} ~ {},发票种类:{},客商单位过滤:{},单页 {} 条最多 {} 页(触顶自动二分切片)",
windowStart.format(POOL_TIME_FORMATTER), windowEnd.format(POOL_TIME_FORMATTER),
invoiceTypes.isEmpty() ? "不限制" : invoiceTypes, POOL_PAGE_SIZE, MAX_POOL_PAGES);
invoiceTypes.isEmpty() ? "不限制" : invoiceTypes, customerTaxNos != null ? "开启" : "关闭",
POOL_PAGE_SIZE, MAX_POOL_PAGES);
if (invoiceTypes.isEmpty()) {
this.syncWindowSlice(windowStart, windowEnd, null, stats);
this.syncWindowSlice(windowStart, windowEnd, null, customerTaxNos, stats);
} else {
// 单值参数拆轮:每个票种编码独立完整切片续扫,互不挤占翻页额度
for (String invoiceType : invoiceTypes) {
this.syncWindowSlice(windowStart, windowEnd, invoiceType, stats);
this.syncWindowSlice(windowStart, windowEnd, invoiceType, customerTaxNos, stats);
}
}
String summary = "金蝶全票池同步:翻页" + stats.pageCount + "页切片" + stats.sliceCount + "片拉取"
+ stats.pulledCount + "张发票,镜像新增" + stats.created + "更新" + stats.updated + "无变化"
+ stats.pulledCount + "张发票"
+ (stats.customerFiltered > 0 ? "(客商单位过滤" + stats.customerFiltered + "张)" : "")
+ ",镜像新增" + stats.created + "更新" + stats.updated + "无变化"
+ stats.mirrorUnchanged + ",补绑" + stats.bound + ",登记无变化跳过" + stats.unchanged
+ ",失败" + stats.failed + ",耗时" + (System.currentTimeMillis() - startMillis) + "ms";
log.info("金蝶全票池同步结束,{}", summary);
@@ -1284,6 +1293,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
private static final class SyncStats {
/** A801-1 原始拉取张数 */
int pulledCount;
/** 客商单位过滤剔除张数(销/购方税号未同时登记在客商发票信息中,开关开启时统计) */
int customerFiltered;
/** 镜像新增条数(票池有、本地无) */
int created;
/** 镜像有变化并重写条数 */
@@ -1307,11 +1318,13 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
* 触顶说明该时间片数据 >2.5 万,从中点二分为两片递归续扫(递归深度受时间粒度自然收敛,
* 单片 500 条以内 1 页即短页终止)
*
* @param sliceStart 切片起点(含)
* @param sliceEnd 切片终点(不含,避免与下一片边界重复拉取)
* @param stats 整轮统计累加器
* @param sliceStart 切片起点(含)
* @param sliceEnd 切片终点(不含,避免与下一片边界重复拉取)
* @param customerTaxNos 客商发票信息税号集,null 表示客商单位过滤关闭(整轮共享)
* @param stats 整轮统计累加器
*/
private void syncWindowSlice(LocalDateTime sliceStart, LocalDateTime sliceEnd, String invoiceType, SyncStats stats) {
private void syncWindowSlice(LocalDateTime sliceStart, LocalDateTime sliceEnd, String invoiceType,
Set<String> customerTaxNos, SyncStats stats) {
// 二分收敛保护:切片粒度细到 1 秒仍触顶说明远端异常(同秒 >2.5 万条不可能),不再二分直接记漏
if (!sliceStart.isBefore(sliceEnd)) {
log.error("金蝶全票池同步切片区间为空({} ~ {}),远端 updateTime 粒度异常,放弃该切片", sliceStart, sliceEnd);
@@ -1325,6 +1338,13 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
}
stats.pageCount++;
stats.pulledCount += rows.size();
// 客商单位过滤:销/购方税号未同时登记在客商发票信息中的发票不入镜像;
// 短页判定用原始条数,过滤后的空页不能提前终止翻页
int rawPageSize = rows.size();
if (customerTaxNos != null) {
rows = rows.stream().filter(row -> this.bothUnitsRegistered(row, customerTaxNos)).toList();
stats.customerFiltered += rawPageSize - rows.size();
}
BatchUpsertResult upsertResult = this.batchUpsertInvoicePool(rows);
stats.created += upsertResult.created();
stats.updated += upsertResult.updated();
@@ -1336,8 +1356,8 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
stats.unchanged += refreshResult.unchanged();
stats.failed += refreshResult.failed();
}
// 远程短页:该片到底,正常终止
if (rows.size() < POOL_PAGE_SIZE) {
// 远程短页:该片到底,正常终止(按原始拉取条数判断,客商过滤不改变翻页节奏)
if (rawPageSize < POOL_PAGE_SIZE) {
stats.sliceCount++;
return;
}
@@ -1355,13 +1375,34 @@ public class InvoiceReceiptServiceImpl extends BaseServiceImpl<InvoiceReceiptMap
}
log.warn("金蝶全票池同步切片 {} ~ {} 触达翻页保护 {} 页({} 条),二分续扫 [{} ~ {}) + [{} ~ {})",
sliceStart, sliceEnd, MAX_POOL_PAGES, MAX_POOL_PAGES * POOL_PAGE_SIZE, sliceStart, mid, mid, sliceEnd);
this.syncWindowSlice(sliceStart, mid, invoiceType, stats);
this.syncWindowSlice(mid, sliceEnd, invoiceType, stats);
this.syncWindowSlice(sliceStart, mid, invoiceType, customerTaxNos, stats);
this.syncWindowSlice(mid, sliceEnd, invoiceType, customerTaxNos, stats);
return;
}
stats.sliceCount++;
}
/**
* 有效客商发票信息税号集(状态正常且未删除,MP 租户插件自动隔离租户):
* 客商单位过滤开关开启时,同步拉取与票池展示查询共用的单位白名单
*/
private Set<String> customerInvoiceTaxNos() {
return customerInvoiceInfoMapper.selectList(Wrappers.<CustomerInvoiceInfo>lambdaQuery()
.eq(CustomerInvoiceInfo::getStatus, 1)
.eq(CustomerInvoiceInfo::getIsDeleted, 0)).stream()
.map(CustomerInvoiceInfo::getTaxNo)
.filter(StringUtil::isNotBlank)
.collect(Collectors.toSet());
}
/**
* 开票单位(销方)与受票单位(购方)税号是否均已登记在客商发票信息中;税号缺失视为未登记(开关开启时不入镜像)
*/
private boolean bothUnitsRegistered(BfiInvoicePoolVO row, Set<String> customerTaxNos) {
return StringUtil.isNotBlank(row.getSalerTaxNo()) && StringUtil.isNotBlank(row.getBuyerTaxNo())
&& customerTaxNos.contains(row.getSalerTaxNo()) && customerTaxNos.contains(row.getBuyerTaxNo());
}
/** 已登记票跟随刷新结果统计(绑定/刷新/无变化/失败) */
private record RefreshResult(int bound, int unchanged, int failed) {
}
@@ -454,6 +454,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
entity.setCurrentProcessor(AuthUtil.getUserName());
entity.setRemark(request.getReason() == null ? entity.getRemark() : limit(request.getReason(), 200));
updateById(entity);
// 联动作废提前建档的预结算预付记录,避免残留记录阻断预结算作废
voidPreSettlementAdvance(entity);
refreshFormalSettlement(entity);
}
@@ -609,8 +611,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
// 真实付款结果改由定时回写任务驱动;联调环境需要模拟时恢复以下注释即可
// entity.setPaidAmount(money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP));
// mockPaymentRecords(entity);
// syncPreSettlementAdvance(entity);
// syncFormalSettlementPayments(entity);
// 金蝶付款申请单生成成功:提前联动预结算预付记录(approved/已付0)回显预付信息,付款完成回写时更新为已付款
syncPreSettlementAdvance(entity, TransportBusiConstant.APPROVED, money(entity.getPaidAmount()));
updateById(entity);
log.info("付款申请[{}]同步金蝶完成:kingdeeStatus={},kingdeeBillId={},kingdeeBillNo={}",
entity.getPaymentNo(), entity.getKingdeeStatus(), kingdeeBillId, entity.getKingdeeBillNo());
@@ -679,6 +682,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
.set(PaymentApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_FAILED)
.set(PaymentApplication::getKingdeeBillId, null)
.set(PaymentApplication::getKingdeeBillNo, null));
// 金蝶单据已删除/解锁重推:联动作废提前建档的预付记录,重推生成新单后重新建档
voidPreSettlementAdvance(entity);
}
/**
@@ -828,9 +833,10 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
String orgNumber = payApply == null ? null : payApply.getPayorgNumber();
if (Func.isBlank(orgNumber)) {
try {
// A103-4 付款人(公司)编码必填:优先取 A103-2 返回付款组织编码,兜底按合同所属组织解析
// A103-4 付款人(公司)编码必填:优先取 A103-2 返回付款组织编码,兜底按合同关联项目解析
KingdeeSyncContext context = KingdeeSyncContext.empty();
orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(
kingdeeApBillSyncSupport.requireContract(application.getContractId(), KingdeeSyncContext.empty()));
kingdeeApBillSyncSupport.requireContract(application.getContractId(), context), context);
} catch (Exception exception) {
log.warn("付款申请[{}]付款处理查询跳过,付款组织编码获取失败:{}", application.getPaymentNo(), exception.getMessage());
continue;
@@ -896,10 +902,15 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
updateById(entity);
if (KingdeeBfiConstant.KINGDEE_STATUS_PAID.equals(kingdeeStatus)) {
// 付款完成:联动履约付款数据(预结算预付记录、正式结算付款记录)
syncPreSettlementAdvance(entity);
syncPreSettlementAdvance(entity, "paid", money(entity.getAppliedAmount()));
syncFormalSettlementPayments(entity);
return true;
}
// 生成失败单据勾稽确认金蝶已生成(已生成/付款中):与推送成功同语义,提前落预付记录回显预付信息
if (KingdeeBfiConstant.KINGDEE_STATUS_SYNCED.equals(kingdeeStatus)
|| KingdeeBfiConstant.KINGDEE_STATUS_PAYING.equals(kingdeeStatus)) {
syncPreSettlementAdvance(entity, TransportBusiConstant.APPROVED, money(entity.getPaidAmount()));
}
return false;
}
@@ -1154,7 +1165,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
*/
private String pushBfiPayApply(PaymentApplication entity, KingdeeSyncContext context) {
ContractManage contract = kingdeeApBillSyncSupport.requireContract(entity.getContractId(), context);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract, context);
String currency = kingdeeApBillSyncSupport.bfiCurrency(contract);
// 申请日期:yyyy-MM-dd HH:mm:ss(与 A102-2/A103-1 统一,金蝶各接口日期格式要求一致)
String applyDateText = bfiDateTimeText(entity);
@@ -1260,7 +1271,7 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
*/
private String pushBfiPayApplyPush(PaymentApplication entity, KingdeeSyncContext context) {
ContractManage contract = kingdeeApBillSyncSupport.requireContract(entity.getContractId(), context);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract, context);
// 申请日期:yyyy-MM-dd HH:mm:ss(与 A102-2 单据日期格式统一)
String applyDateText = bfiDateTimeText(entity);
BfiPayApplyPushDTO param = new BfiPayApplyPushDTO();
@@ -1574,39 +1585,26 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
}
private void syncPreSettlementAdvance(PaymentApplication entity) {
/**
* 付款申请联动预结算预付记录:金蝶付款申请单生成成功(synced)即建档提前回显预付信息,
* 付款完成回写(paid)更新为已付款;按付款申请单号幂等,不产生重复记录
*
* @param billStatus 预付记录状态:approved(已生成待付)/ paid(已付款)
* @param paidAmount 已付金额:synced 阶段通常为0,paid 阶段取申请全额
*/
private void syncPreSettlementAdvance(PaymentApplication entity, String billStatus, BigDecimal paidAmount) {
if (entity.getPreSettlementId() == null) return;
if (!List.of("progress_advance", "settlement_payment").contains(entity.getPaymentType())) return;
List<PaymentApplicationSettlement> relations = settlementRelationMapper.selectList(
Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())
.eq(PaymentApplicationSettlement::getIsDeleted, 0)
.isNotNull(PaymentApplicationSettlement::getPreSettlementId));
if (relations.isEmpty()) {
if (entity.getPreSettlementId() == null) return;
upsertPreSettlementAdvance(entity.getPreSettlementId(), entity.getPaymentNo(), money(entity.getAppliedAmount()), entity);
refreshPreSettlementPaidLinks(entity.getPreSettlementId());
return;
}
for (PaymentApplicationSettlement relation : relations) {
String advanceNo = relations.size() == 1 ? entity.getPaymentNo()
: trimText(entity.getPaymentNo() + "-" + relation.getPreSettlementNo(), 100);
upsertPreSettlementAdvance(relation.getPreSettlementId(), advanceNo, money(relation.getAppliedAmount()), entity);
refreshPreSettlementPaidLinks(relation.getPreSettlementId());
}
}
private void upsertPreSettlementAdvance(Long preSettlementId, String advanceNo, BigDecimal appliedAmount,
PaymentApplication entity) {
PreSettlementAdvance advance = preSettlementAdvanceMapper.selectList(
Wrappers.<PreSettlementAdvance>lambdaQuery()
.eq(PreSettlementAdvance::getPreSettlementId, preSettlementId)
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
.eq(PreSettlementAdvance::getIsDeleted, 0)
.ne(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED)
.eq(PreSettlementAdvance::getAdvanceNo, advanceNo)
.eq(PreSettlementAdvance::getAdvanceNo, entity.getPaymentNo())
.last("limit 1")).stream().findFirst().orElse(null);
if (advance == null && Objects.equals(advanceNo, entity.getPaymentNo())) {
if (advance == null) {
advance = preSettlementAdvanceMapper.selectList(Wrappers.<PreSettlementAdvance>lambdaQuery()
.eq(PreSettlementAdvance::getPreSettlementId, preSettlementId)
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
.eq(PreSettlementAdvance::getIsDeleted, 0)
.ne(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED)
.eq(PreSettlementAdvance::getPaidAmount, BigDecimal.ZERO)
@@ -1615,34 +1613,40 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
}
if (advance == null) {
advance = new PreSettlementAdvance();
advance.setPreSettlementId(preSettlementId);
advance.setAdvanceNo(advanceNo);
advance.setAppliedAmount(money(appliedAmount));
advance.setPreSettlementId(entity.getPreSettlementId());
advance.setAdvanceNo(entity.getPaymentNo());
advance.setAppliedAmount(money(entity.getAppliedAmount()));
}
advance.setPaidAmount(money(appliedAmount));
advance.setBillStatus("paid");
advance.setPaidAmount(paidAmount);
advance.setBillStatus(billStatus);
advance.setKingdeeAdvanceNo(entity.getKingdeeBillNo());
if (advance.getId() == null) preSettlementAdvanceMapper.insert(advance);
else preSettlementAdvanceMapper.updateById(advance);
}
private void refreshPreSettlementPaidLinks(Long preSettlementId) {
refreshPreSettlementAdvanceSummary(preSettlementId);
refreshPreSettlementAdvanceSummary(entity.getPreSettlementId());
formalSettlementSourceMapper.selectList(Wrappers.<FormalSettlementSource>lambdaQuery()
.eq(FormalSettlementSource::getPreSettlementId, preSettlementId)
.eq(FormalSettlementSource::getPreSettlementId, entity.getPreSettlementId())
.eq(FormalSettlementSource::getIsDeleted, 0)).forEach(source -> {
source.setAdvanceAppliedAmount(findPreSettlementApplied(preSettlementId));
source.setAdvancePaidAmount(findPreSettlementPaid(preSettlementId));
source.setAdvanceAppliedAmount(findPreSettlementApplied(entity.getPreSettlementId()));
source.setAdvancePaidAmount(findPreSettlementPaid(entity.getPreSettlementId()));
formalSettlementSourceMapper.updateById(source);
});
formalSettlementService.refreshPaymentSummariesForPreSettlement(preSettlementId);
formalSettlementService.refreshPaymentSummariesForPreSettlement(entity.getPreSettlementId());
}
private String trimText(String value, int length) {
if (value == null || value.length() <= length) {
return value;
}
return value.substring(0, length);
/**
* 联动作废付款申请关联的预结算预付记录(付款申请作废、金蝶反审核解锁场景):
* 仅清理提前建档的未付记录,已付款记录不联动(付款完成的事实不回退),避免残留记录阻断预结算作废
*/
private void voidPreSettlementAdvance(PaymentApplication entity) {
if (entity.getPreSettlementId() == null) return;
if (!List.of("progress_advance", "settlement_payment").contains(entity.getPaymentType())) return;
preSettlementAdvanceMapper.update(null, Wrappers.<PreSettlementAdvance>lambdaUpdate()
.eq(PreSettlementAdvance::getPreSettlementId, entity.getPreSettlementId())
.eq(PreSettlementAdvance::getAdvanceNo, entity.getPaymentNo())
.eq(PreSettlementAdvance::getIsDeleted, 0)
.ne(PreSettlementAdvance::getBillStatus, "paid")
.set(PreSettlementAdvance::getBillStatus, TransportBusiConstant.VOIDED));
refreshPreSettlementAdvanceSummary(entity.getPreSettlementId());
}
private BigDecimal findPreSettlementPaid(Long preSettlementId) {
@@ -233,7 +233,7 @@ public class KingdeeApBillSyncSupport {
if (Func.isBlank(billtypeNumber)) {
throw new ServiceException("推送金蝶财务应付单失败:单据类型[" + KingdeeBfiConstant.BFI_AP_BILL_BILLTYPE_NAME + "]未在映射字典中维护");
}
String orgNumber = bfiOrgNumber(contract);
String orgNumber = bfiOrgNumber(contract, context);
// 币种取合同结算币种
String currency = bfiCurrency(contract);
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),流程拆分后以同步当日作为应付单据日期
@@ -925,22 +925,50 @@ public class KingdeeApBillSyncSupport {
}
/**
* 合同所属组织编码
* 合同金蝶组织编码:合同关联项目 → 项目业务部门沿部门树向上反查唯一平台公司 → 平台公司金蝶组织编码
*
* @param contract 合同实体
* @param context 金蝶同步上下文(缓存载体)
* @return 组织编码
*/
public String bfiOrgNumber(ContractManage contract) {
Dept dept = contract.getOrganizationId() == null ? null : SysCache.getDept(contract.getOrganizationId());
// dept为空,直接报错
if (dept == null) {
throw new ServiceException("推送金蝶失败,合同所属组织不存在,请检查合同所属组织配置");
public String bfiOrgNumber(ContractManage contract, KingdeeSyncContext context) {
if (contract.getProjectId() == null) {
throw new ServiceException("推送金蝶失败,合同[" + Func.toStr(contract.getContractNo()) + "]未关联项目,无法确定金蝶组织编码");
}
// kingdeeDeptCode为空,直接报错
if (Func.isBlank(dept.getKingdeeDeptCode())) {
throw new ServiceException("推送金蝶失败,合同所属组织编码不能为空,请检查组织[" + dept.getDeptName() + "]是否维护金蝶组织编码");
return this.kingdeeOrgNumber(contract.getProjectId(), context);
}
/**
* 金蝶组织编码:项目业务部门沿部门树向上反查唯一平台公司(是否平台公司=是),取该平台公司的金蝶组织编码;
* 项目未关联/未维护业务部门/业务部门未归属平台公司/平台公司未维护金蝶组织编码时拦截
*
* @param projectId 项目id
* @param context 金蝶同步上下文(缓存载体)
* @return 组织编码
*/
public String kingdeeOrgNumber(Long projectId, KingdeeSyncContext context) {
if (projectId == null) {
throw new ServiceException("推送金蝶失败,单据未关联项目,无法确定金蝶组织编码");
}
return dept.getKingdeeDeptCode();
ProjectApply project = context.projectCache().computeIfAbsent(projectId, projectApplyMapper::selectById);
if (project == null) {
throw new ServiceException("推送金蝶失败,项目[" + projectId + "]不存在,无法确定金蝶组织编码");
}
if (project.getBusinessDeptId() == null) {
throw new ServiceException("推送金蝶失败,项目[" + Func.toStr(project.getProjectName()) + "]未维护业务部门,无法确定金蝶组织编码");
}
// 沿部门树向上反查平台公司,上限 20 级防脏数据成环死循环
Dept dept = SysCache.getDept(project.getBusinessDeptId());
for (int depth = 0; dept != null && depth < 20; depth++) {
if (Objects.equals(dept.getIsPlatformCompany(), 1)) {
if (Func.isBlank(dept.getKingdeeDeptCode())) {
throw new ServiceException("推送金蝶失败,平台公司[" + dept.getDeptName() + "]未维护金蝶组织编码,请检查部门数据");
}
return dept.getKingdeeDeptCode();
}
dept = dept.getParentId() == null ? null : SysCache.getDept(dept.getParentId());
}
throw new ServiceException("推送金蝶失败,项目业务部门[" + Func.toStr(project.getBusinessDeptName()) + "]未归属平台公司,无法确定金蝶组织编码");
}
/**
@@ -955,7 +983,7 @@ public class KingdeeApBillSyncSupport {
return "RMB".equalsIgnoreCase(currency) ? "CNY" : currency;
}
/** 财务应付单往来类型:合同类别为“客户合同”时为 bd_customer(客户),否则 bd_supplier(供应商) */
/** 财务应付单/付款申请/应收单/收票单统一往来类型:固定 bd_customer(按合同类别区分供应商的逻辑已注释停用,恢复时同步影响付款申请) */
public String bfiAsstactType(ContractManage contract) {
return KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE;
/*return "客户合同".equals(contract.getContractCategory())
@@ -170,7 +170,7 @@ public class KingdeeArBillSyncSupport {
if (Func.isBlank(billtypeNumber)) {
throw new ServiceException("推送金蝶财务应收单失败:单据类型[" + KingdeeBfiConstant.BFI_AR_BILL_BILLTYPE_NAME + "]未在映射字典中维护");
}
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract);
String orgNumber = kingdeeApBillSyncSupport.bfiOrgNumber(contract, context);
// 币种取合同结算币种
String currency = kingdeeApBillSyncSupport.bfiCurrency(contract);
// 单据日期:yyyy-MM-dd HH:mm:ss(各接口统一格式),以同步当日作为应收单据日期
@@ -193,8 +193,8 @@ public class KingdeeArBillSyncSupport {
param.setRecorgNumber(orgNumber);
// 【必填】单据日期
param.setBizdate(billDateText);
// 【必填】往来类型:应收固定客户
param.setAsstacttype(KingdeeBfiConstant.BFI_CUSTOMER_ASSTACT_TYPE);
// 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径)
param.setAsstacttype(kingdeeApBillSyncSupport.bfiAsstactType(contract));
// 【必填】往来户.编码:结算单付款方(客户)
param.setAsstactNumber(customerCode);
// 【必填】付款方式(CASH:现销, CREDIT:赊销),现销由金蝶自动推收款
@@ -183,10 +183,10 @@ public class KingdeeReceivedInvoiceSyncSupport {
param.setInvoicetypef7Number(mirror.getInvoiceTypeCode());
// 【必填】付款方式:(与应付单一致,不允许金蝶自动推付款)
param.setPaymenttype(BfiConstant.PAY_MODE_CASH);
// 【必填】往来类型:进项收票固定供应商
param.setAsstacttype(BfiConstant.ASSTACT_TYPE_SUPPLIER);
// 【必填】结算组织.编码:合同所属组织(与应付单同源)
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract));
// 【必填】往来类型:统一固定客户(KingdeeApBillSyncSupport#bfiAsstactType,与应付单/付款申请同口径)
param.setAsstacttype(kingdeeApBillSyncSupport.bfiAsstactType(contract));
// 【必填】结算组织.编码:合同关联项目业务部门反查平台公司(与应付单同源)
param.setOrgNumber(kingdeeApBillSyncSupport.bfiOrgNumber(contract, context));
// 【必填】结算币别:合同结算币种
param.setCurrencyNumber(kingdeeApBillSyncSupport.bfiCurrency(contract));
// 【必填】往来户.编码:合同乙方(开票供应商)客商编码
@@ -0,0 +1,405 @@
# 金蝶(星瀚)BFI 推送入参取值映射表:页面字段 → 数据库表字段 / 配置对照
> 整理日期:2026-10-15
> 代码基线:`tms-api` 当前 master(分支 `feature/payment-apply-bfi-sync`,提交 `259407c3` 之后)。
> 用途:给联调/实施/排查用——每个送金蝶的入参,**页面上是哪个字段、落库在哪张表哪个字段、或来自哪个配置/常量**。逐字段核对代码(文件+行号见各节备注)。
> 配套文档:《2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).md》。
---
## 目录
- [0. 公共取数与通用规则](#0-公共取数与通用规则)
- [1. A100-1 获取令牌](#1-a100-1-获取令牌)
- [2. A902-2 客户保存](#2-a902-2-客户保存)
- [3. A801-5 黑名单查询](#3-a801-5-黑名单查询)
- [4. A102-2 财务应付单保存](#4-a102-2-财务应付单保存)
- [5. A302-2 财务应收单保存(与应付差异)](#5-a302-2-财务应收单保存与应付差异)
- [6. A103-1 付款申请下推](#6-a103-1-付款申请下推)
- [7. A103-3 预付款申请保存](#7-a103-3-预付款申请保存)
- [8. A303-1 开票申请单保存](#8-a303-1-开票申请单保存)
- [9. A303-2 收票单保存 + A302-6 指定应付](#9-a303-2-收票单保存--a302-6-指定应付)
- [10. A301-3 收款认领结果推送](#10-a301-3-收款认领结果推送)
- [11. lockBusiData 单据锁定](#11-lockbusidata-单据锁定)
- [12. A902 附件上传](#12-a902-附件上传)
- [13. 查询类接口入参简表](#13-查询类接口入参简表)
- [14. 与总纲记载不一致的核对结论](#14-与总纲记载不一致的核对结论)
---
## 0. 公共取数与通用规则
### 0.1 配置前缀(两套并存,勿混)
| 前缀 | 归属 | 内容 |
|---|---|---|
| `third-party.bfi.*` | bfi-api 客户端(令牌/网关) | baseUrl、authorization、clientId、clientSecret、username、accountId、language |
| `thirdparty.bfi.*` | blade-transport 业务 | srcSys、各业务开关/cron、payApply.*、invoiceApply.*、receivedInvoice.*、material.*、claimAnnounce.*、blacklist.*、billLock.*(宽松绑定兼容 camelCase) |
### 0.2 通用取数
| 项 | 规则 | 代码 |
|---|---|---|
| awrp_src_sys / systemsource / sourceSystem | Nacos `thirdparty.bfi.srcSys`,默认常量 `WLTMS` | BfiProperties(srcSys) |
| 组织 org/结算/付款/收款/申请组织 | 合同/申请单所属组织 → `SysCache.getDept()` → **部门金蝶组织编码**,空拦截 | `blade_contract_manage.organization_id` / 申请单 dept_id → `blade_dept.kingdee_dept_code`(KingdeeApBillSyncSupport.bfiOrgNumber L933-944) |
| 币种 | 合同结算币种,`RMB→CNY` | `blade_contract_manage.settlement_currency`(bfiCurrency L952-956) |
| 往来户 | 名称 → `blade_customer_archive` 按 `full_name/short_name` 匹配(limit 1)取 `customer_code`,查不到报错 | queryCustomerCode L966-984 |
| 日期文本 | 统一 `yyyy-MM-dd HH:mm:ss` | bfiDateTimeText L922-925 |
| 防重键 awrp_src_code | `ExternalSrcCodeSupport.buildSrcCode`:下划线拼接、去空白、去重保序 | ExternalSrcCodeSupport L54-63 |
### 0.3 分录拆分规则(A102-2/A302-2/A303-2 共用 `resolveEntryGroups`)
- 拆分维度:**货物 → 费用大类(装卸/运输)→ 税率** 三级分组,各组成一条分录;手动合计费用项(manual_flag=1)每行一条。
- 物料编码:装卸大类/手动非运费杂费 → `thirdparty.bfi.material.loading`(默认 XDWL11002004);项目业务模式或业务类型含「国际」→ `.international`(Item-00001081);国内按运单运输方式 → `.domestic.公路运输`=XDWL11010001 / `.铁路运输`=XDWL11010002 / `.水路运输`=XDWL11010003 / `.航空运输`=Item-00001082;运输方式未配置直接拦截。
- 税率:货物费用行按合同计费方案规则分组(`blade_contract_manage.billing_plan_json` 的 `rules[].feeType/feeItem/taxRate`);税率未配置并入无税率组。
- 数量:运输类=结算明细运输量(≤0 兜底 1,同税率拆多行每行都取运输量);装卸/合计费用项行=1;单价=金额÷数量反算(2 位 HALF_UP)。
- 0 元费用项(signum==0)与 0 元货物行整行跳过。
- 计量单位**当前不送**(DTO 有字段无赋值)。
---
## 1. A100-1 获取令牌
| 入参 | 含义 | 来源:配置键 / 生成方式 |
|---|---|---|
| client_id | 应用编码 | `third-party.bfi.clientId` |
| client_secret | 认证密钥 | `third-party.bfi.clientSecret` |
| username | 访问用户名 | `third-party.bfi.username` |
| accountId | 数据中心 ID | `third-party.bfi.accountId` |
| language | 语言 | `third-party.bfi.language`(默认 `zh_CN`) |
| nonce | 随机数 | 每次生成 `UUID` |
| timestamp | 时间戳(±5 分钟校验) | 每次生成 `yyyy-MM-dd HH:mm:ss` |
> 令牌缓存 Redis `blade:third:bfi:token`,TTL=expires_in−120s;`setIfAbsent` 写入。代码:BfiServiceImpl L136-156、BfiTokenDTO L44-65。
---
## 2. A902-2 客户保存
调用链:`CustomerArchiveServiceImpl.syncKingdee → pushCustomerToKingdee → CustomerBfiConverter.buildSaveDTO`
| 金蝶入参 | 含义 | 页面字段 | 表.字段 / 常量 |
|---|---|---|---|
| number | 客户编码(不传由星瀚生成) | 客商编号(提交时生成 KS+6 位) | blade_customer_archive.customer_code |
| name【必】 | 名称 | 客商名称 | blade_customer_archive.full_name |
| societycreditcode【必】 | 统一社会信用代码(金蝶候选键) | 统一社会信用代码 | blade_customer_archive.unified_credit_code |
| tx_register_no | 纳税人识别号 | 同上(同一字段) | blade_customer_archive.unified_credit_code |
| artificialperson | 法人代表 | 企业法人 | blade_customer_archive.legal_person |
| type【必】 | 伙伴类型(1~17) | 客商性质字典 | blade_customer_archive.customer_nature + 字典 `nature_of_client` dictKey(文本值反查 dictKey,无效抛异常) |
| bizpartner_address | 详细地址 | 注册地址 | blade_customer_archive.registered_address |
| bizpartner_phone | 联系电话 | 联系电话 | blade_customer_archive.contact_phone |
| businessterm | 营业期限 | 营业期限类型=长期→「长期」;否则「至 yyyy-MM-dd」 | blade_customer_archive.business_term_type / business_end_date |
| businessscope | 经营范围 | 经营范围多选 → 字典 `scope_of_business` 文本「、」拼接 | blade_customer_archive.business_scope |
| bizfunction | 业务职能 | 固定全选 | 常量 `"1,2,3,4"` |
| fcusbeizhu | 备注 | 备注(空不传) | blade_customer_archive.remark |
| awrp_datasourcesys | 数据来源 | 配置 | `thirdparty.bfi.srcSys` |
| awrp_cusfortrade【必】 | 是否贸易客商 | 固定 | 常量 `"A"` |
| entry_groupstandard[].standardid_number【必】 | 分类标准 | 固定 | 常量 `"JBFLBZ"` |
| entry_groupstandard[].groupid_number【必】 | 客户分组 | 客户类型:internal→1101,其余→1102 | blade_customer_archive.customer_kind + 常量 1101/1102 |
| entry_bank[].bankaccount | 银行账号(金蝶候选键) | 收款账号 | blade_customer_receipt_account.bank_account |
| entry_bank[].accountname | 账户名称 | **传开户行名称** | blade_customer_receipt_account.bank_name |
| entry_bank[].bank_number | 开户银行.编码(联行号) | 联行号 | blade_customer_receipt_account.cnaps_code |
| entry_bank[].currency_number【必】 | 币种 | 固定 | 常量 `"CNY"` |
| entry_bank[].isdefault_bank | 默认银行 | 固定 | `true` |
| entry_linkman[].contactperson | 联系人 | 客商负责人(空则整组分录不传) | blade_customer_archive.principal |
| entry_linkman[].phone | 电话 | 联系电话 | blade_customer_archive.contact_phone |
| entry_linkman[].isdefault_linkman | 默认联系人 | 固定 | `true` |
不传字段:status、linkman、establishdate、postal_code、createorg_number(已注释)等。
> 代码:CustomerBfiConverter L67-280、常量 CustomerBfiConstants / BfiConstant L288-313。
---
## 3. A801-5 黑名单查询
| 入参 | 含义 | 来源 |
|---|---|---|
| enable | 使用状态 | 固定 `"1"`(可用) |
| number | 企业税号数组 | 客商发票信息所有纳税人识别号,`trim().toUpperCase()` 去重 → blade_customer_invoice_info.tax_no |
| pageNo / pageSize | 分页 | 自动翻页(50 页保护)/ 固定 `2000` |
| name | 企业名称数组 | 不传 |
> 命中判定按应答 awrpCombofield:A 硬拦截;B 默认放行告警(`thirdparty.bfi.blacklist.blockRiskWarning=true` 时拦截);查询异常上抛阻断提审。代码:CustomerArchiveServiceImpl L1043-1092、BfiConstant L90。
---
## 4. A102-2 财务应付单保存
### 4.1 单据头
| 金蝶入参 | 含义 | 页面字段 | 表.字段 / 配置 / 常量 |
|---|---|---|---|
| awrp_src_code【必·候选键】 | 来源单据号(防重键) | 结算单号+结算明细单据号 | blade_formal_settlement.formal_settlement_no + blade_formal_settlement_detail.document_no(document_no 空兜底明细行 id)下划线拼接 |
| awrp_src_sys【必】 | 外部系统编号 | 配置 | `thirdparty.bfi.srcSys`(WLTMS) |
| awrp_src_type【必】 | 外部单据类型 | — | 常量 `TMS财务应付单` |
| billtypeid_number【必】 | 单据类型.编码 | — | 常量字典「服务采购应付」→ `ApFin_service_BT_S` |
| org_number / payorg_number【必】 | 结算/付款组织.编码 | 合同所属组织 | blade_contract_manage.organization_id → blade_dept.kingdee_dept_code |
| bizdate【必】 | 单据日期 | 同步当日 | LocalDate.now() |
| asstacttype【必】 | 往来类型 | — | 常量 `bd_customer`(固定,供应商分支已注释) |
| asstact_number【必】 | 往来户.编码 | 合同乙方 | blade_contract_manage.party_b → blade_customer_archive.customer_code |
| purmode【必】 | 付款方式 | — | 常量 `CASH`(现购) |
| currency_number【必】 | 结算币别 | 合同结算币种 | blade_contract_manage.settlement_currency(RMB→CNY) |
| payproperty_number【必】 | 款项性质.编码 | — | 常量 `2001`(材料采购) |
| isincludetax / ispricetotal | 含税价/总价录入 | — | `true` / `true` |
| isintax | 按应付金额分配 | — | `false` |
| exratedate / exchangerate | 汇率日期/汇率 | 结算单页面「汇率日期/结算汇率」,未维护不送 | blade_formal_settlement.exchange_rate_date / .exchange_rate |
| settlementtype_number | 结算方式 | **不送**(仅 A103-1 分录需要) | — |
| taxrateid_number / purorg_number / exratetable_number / planentity 等 | 可选 | **均不送**(planentity 由金蝶按分录自动汇总付款计划) | — |
### 4.2 明细分录 detailentry
| 金蝶入参 | 含义 | 来源 | 表.字段 / 配置 / 常量 |
|---|---|---|---|
| awrp_entry_code【必·候选键】 | 分录标识 | 拆分行=`明细单据号-货物序号-大类-税率`;合计费用项行=`结算单号-费用类型-费用项` | blade_formal_settlement_detail.document_no / blade_formal_settlement_summary_fee.fee_type+fee_item |
| material_number【必】 | 物料编码.编码 | 按拆分规则映射(见 0.3) | 配置 `thirdparty.bfi.material.loading / international / domestic.公路运输 / .铁路运输 / .水路运输 / .航空运输`;判定字段 blade_project_apply.business_mode / business_type、blade_formal_settlement_detail.transport_type |
| awrp_transport | 运输货物 | 货物名称(合计费用项行不传) | blade_formal_settlement_detail.cargo_name(一车多货取 blade_formal_settlement_detail_fee.cargo_name) |
| awrp_lpn | 车号 | 车号(合计费用项行不传) | blade_formal_settlement_detail.vehicle_no |
| quantity【必】 | 数量 | 运输量(≤0 兜底 1;装卸/合计行=1) | blade_formal_settlement_detail.transport_quantity(一车多货取 detail_fee.transport_quantity) |
| price | 单价 | 金额÷数量反算 | 派生值 |
| e_pricetaxtotal【必】 | 应付金额(含税) | 结算金额 | blade_formal_settlement_detail.settlement_amount_tax / blade_formal_settlement_summary_fee.settlement_amount |
| e_amount | 金额(不含税) | 不含税结算金额(仅运输类携带;合计行不传) | blade_formal_settlement_detail.settlement_amount_no_tax |
| project_number【必】 | 项目.编码 | 联调临时值 | 常量 `"999"` |
| taxrateid_number | 税率.编码 | 不送(税率只作拆行依据) | blade_contract_manage.billing_plan_json |
---
## 5. A302-2 财务应收单保存(与应付差异)
分录与应付**同源同口径**(复用 resolveEntryGroups,分录字段名前缀 e_:e_material_number/e_quantity/e_unitprice/e_recamount(含税)/e_amount(不含税))。头字段差异:
| 金蝶入参 | 应收取值 | 来源 |
|---|---|---|
| awrp_src_type | `TMS财务应收单` | 常量 |
| billtype_number | 「服务销售应收」→ `arfin_sersal_BT_S` | 常量映射 |
| recorg_number | 收款组织=结算组织 | 同 org_number |
| asstact_number | **结算单付款方**(非合同乙方) | blade_formal_settlement.payer_name → blade_customer_archive.customer_code |
| paymode | 现销 `CASH` | 常量 |
| payproperty_number | `1001`(一般销售) | 常量 |
| bizdate | 同步当日 | LocalDate.now() |
| settlementtype_number / duedate 等 | 不送(TODO 待金蝶确认) | — |
---
## 6. A103-1 付款申请下推
| 金蝶入参 | 含义 | 页面字段 | 表.字段 / 配置 / 常量 |
|---|---|---|---|
| awrp_src_code【必·候选键】 | 来源单据号 | 付款申请单号 | blade_payment_application.payment_no |
| awrp_src_sys【必】 | 外部系统 | 配置 | `thirdparty.bfi.srcSys` |
| applydate【必】 | 申请日期 | 付款申请页-申请日期(空取当天) | blade_payment_application.apply_date |
| entry[].e_paymenttype_number【必】 | 付款类型 | 正式付款 | 常量 `201` |
| entry[].e_settlementtype_number | 结算方式 | 付款方式含「汇票」→`012-02`(电子银行承兑汇票),其余→`02`(银行转账) | 字典 `pay_method` + blade_payment_application.payment_method |
| entry[].awrp_finapbill_entryid【必】 | 勾稽应付单分录标识 | 主结算单应付单防重键(本地组装) | blade_formal_settlement.formal_settlement_no + blade_formal_settlement_detail.document_no(主结算单=blade_payment_application.settlement_id,空取关联明细首个结算单) |
| entry[].awrp_entry_code【必】 | 行 id | 付款申请号-1 | payment_no + "-1" |
| entry[].e_assacct【必】 | 往来账户(收款账号) | 收款账号 | blade_payment_application.bank_account |
| entry[].e_bebank_number【必】 | 往来银行.编码(联行号) | 收款账户联行号(缺失拦截;兜底按合同乙方客商+收款账号匹配) | blade_customer_receipt_account.cnaps_code(按 blade_payment_application.receipt_account_id 直查) |
| entry[].e_applyamount【必】 | 申请金额 | 申请付款金额(2 位 HALF_UP) | blade_payment_application.applied_amount |
| entry[].project_number【必】 | 项目.编码 | 联调临时值 | 常量 `"999"` |
| entry[].awrp_src_type【必】 | 外部单据类型 | 配置 | `thirdparty.bfi.payApply.srcType`(默认 `0KzXs`) |
| exratetable_number / awrp_textfield1 | 汇率表/付款渠道 | **实际不送**(仅 A103-3 送;配置 exratetable=01 在此接口不生效) | — |
---
## 7. A103-3 预付款申请保存
| 金蝶入参 | 含义 | 来源 | 表.字段 / 配置 / 常量 |
|---|---|---|---|
| billtype_number【必】 | 单据类型 | 配置 | `thirdparty.bfi.payApply.billtype`(默认 `ap_payapply_oth_BT_S`) |
| settleorg/awrp_department/applyorg/payorg_number【必】 | 组织/部门 | 合同所属组织 | blade_contract_manage.organization_id → blade_dept.kingdee_dept_code |
| paycurrency/settlecurrency_number【必】 | 币种 | 合同结算币种(RMB→CNY) | blade_contract_manage.settlement_currency |
| applycause | 请款事由 | 按付款类型映射文案:项目垫资→「项目预付款申请」、进度垫资→「进度预付款申请」、结算付款→「结算付款申请」 | blade_payment_application.payment_type |
| exratetable_number【必】 | 汇率表 | 配置(**此接口实际送**) | `thirdparty.bfi.payApply.exratetable`(默认 `01`) |
| exratedate【必】 | 汇率日期 | 关联结算单维护值,兜底申请日期 | blade_formal_settlement.exchange_rate_date → blade_pre_settlement.exchange_rate_date → blade_payment_application.apply_date |
| awrp_textfield1 | 付款渠道 | 付款方式字典标签(**此接口实际送**) | 字典 `pay_method` 翻译 blade_payment_application.payment_method |
| entry[].e_paymenttype_number【必】 | 付款类型 | 结算付款→`201`;项目垫资/进度垫资→`202` | 常量 BFI_PAYMENT_TYPE_FORMAL / BFI_PAYMENT_TYPE_PREPAY |
| entry[].e_asstacttype / e_asstactnumber【必】 | 往来类型/户 | 固定客户 / 合同乙方→客商编码 | `bd_customer`;blade_contract_manage.party_b → customer_code |
| entry[].e_assacct / e_bebank_number【必】 | 账号/联行号 | 同 A103-1 | blade_payment_application.bank_account / blade_customer_receipt_account.cnaps_code |
| entry[].e_applyamount【必】 | 申请金额 | 申请付款金额 | blade_payment_application.applied_amount |
| entry[].project_number【必】 | 项目.编码 | **实际固定 999**(取项目档案真实编码的逻辑已注释停用) | 常量 `"999"`(备用:blade_project_apply.project_code) |
| entry[].e_remark | 备注 | 付款申请备注(空不传) | blade_payment_application.remark |
| entry[].awrp_entry_code / awrp_src_type【必】 | 行 id / 单据类型 | payment_no-1 / 配置 | `thirdparty.bfi.payApply.srcType` |
---
## 8. A303-1 开票申请单保存(每张发票张次一单)
### 8.1 主表
| 金蝶入参 | 含义 | 页面字段 | 表.字段 / 配置 / 常量 |
|---|---|---|---|
| billno【必·候选键】 | 单据编号 | 开票申请单号(多张加张次序号) | blade_invoice_application.application_no(多张 + "-" + blade_invoice_application_sheet.sheet_no) |
| orgid_number / awrp_orgfield_number【必】 | 组织/申请部门 | 申请部门 | blade_invoice_application.dept_id → blade_dept.kingdee_dept_code |
| billdate【必】 | 单据日期 | 申请日期(空取当天) | blade_invoice_application.application_date |
| billproperties / taxationstyle / buyerproperty | 单据性质/征税方式/购方类型 | 固定 | `"1"` / `"0"` / `"0"` |
| invoicetype【必】 | 发票种类 | 发票类型:数电专票→`08xdp`,其余→`10xdp` | blade_invoice_application.invoice_type |
| buyeremail【必】 | 推送邮箱 | 部门邮箱(空拦截) | blade_invoice_application.department_emails |
| buyertaxno / buyername【必】 | 购方税号/名称 | 受票方快照 | blade_invoice_application.taxpayer_no / receiver_name |
| buyeraddr / buyerbank | 购方地址电话/开户行账号 | 受票方快照 | blade_invoice_application.registered_address+contact_phone / bank_name+bank_account |
| salername【必】 | 销方名称 | 开票单位(空拦截) | blade_invoice_application.issuer_name |
| salertaxno【必】 | 销方税号 | 开票单位→客商档案 | blade_customer_archive.unified_credit_code(按 issuer_name 匹配) |
| saleraddr / salerbank | 销方地址电话/开户行账号(数电专票强制) | 客商档案(启用开票信息优先) | blade_customer_archive.registered_address+contact_phone;blade_customer_invoice_info.bank_name+bank_account |
| totalamount【必】 | 申请价税合计 | 本张开票金额 | blade_invoice_application_sheet.invoice_amount |
| hsbz | 是否含税 | 固定不含税 | 常量 `"0"` |
| specialtype | 特殊票种 | 本张商品行**全部**为货物运输类→`E04`,否则→`00`(名称/分类含「运费/货物运输」关键字) | 常量 BFI_INVOICE_SPECIAL_TYPE_FREIGHT / NONE + BFI_INVOICE_FREIGHT_KEYWORDS |
| freights[] | 货物运输单据体(E04 报送) | 结算明细快照,见 8.2 | blade_invoice_application_detail |
| customnameid_number【必】 | 客户.编码 | 受票方客户 | blade_invoice_application.receiver_customer_id → blade_customer_archive.customer_code |
| awrp_project_number【必】 | 项目.编码 | 联调临时值 | 常量 `"999"` |
| awrp_src_bt | 上游单据类型 | 固定 | `ar_finarbill` |
| awrp_src_con_no | 上游单据号 | 首张关联结算单金蝶应收防重键 | blade_formal_settlement.formal_settlement_no + detail.document_no(经 blade_invoice_application_settlement 关联) |
| systemsource | 来源系统 | 配置 | `thirdparty.bfi.srcSys` |
| showbuyeraddresstel / showbuyerbank | 票面展示开关 | 固定 | `true` |
### 8.2 freights[](按「车牌×起运×到达×货物×种类」去重归并,全空行剔除)
| 入参 | 来源:blade_invoice_application_detail 字段 |
|---|---|
| transporttype 运输工具种类 | transport_type(字典 `transport_type` 翻译,不在合法枚举兜底「其它运输工具」) |
| licenseplate 牌号 | vehicle_no |
| transportgoods 货物名称 | cargo_name |
| startplace 起运地 | departure_address |
| endplace 到达地 | arrival_address |
### 8.3 sim_original_bill_item[] 分录(blade_invoice_application_line 商品行,line_no 升序)
| 入参 | 来源:blade_invoice_application_line 字段 |
|---|---|
| awrp_entry_code【必】 | id(雪花 id) |
| rowtype / benchmark【必】 | 常量 `"2"`(商品行)/ `"0"`(数量基准) |
| goodsname【必】 | goods_name(货物或服务简称) |
| goodssimplename【必】 | goods_category(商品和服务分类) |
| taxratecodeid_number / goodscode【必】 | tax_classification_code(税收分类编码,空拦截) |
| num / unit | quantity(0 兜底 1)/ unit(空兜底「个」) |
| unitprice / amount / tax | unit_price_no_tax / amount_no_tax / tax_amount |
| taxrate | tax_rate(百分数÷100 转小数,0 送 "0.00") |
| remark | remark |
| awrp_src_type【必】 | 配置 `thirdparty.bfi.invoiceApply.srcType`(默认 `0KzXs`) |
(specification 规格型号、含税金额等本期不传。)
---
## 9. A303-2 收票单保存 + A302-6 指定应付
### A303-2 收票单批量保存
| 金蝶入参 | 含义 | 来源 | 表.字段 / 配置 / 常量 |
|---|---|---|---|
| awrp_src_code【必·候选键】 | 来源单据号 | 收票登记 id(一票一登记) | blade_invoice_receipt.id |
| awrp_src_sys / awrp_src_type【必】 | 系统/单据类型 | 配置 / 常量 | `thirdparty.bfi.srcSys`;`TMS收票登记` |
| invoiceno | 发票号码 | 收票登记发票号 | blade_invoice_receipt.invoice_no |
| receivedate【必】 | 收票日期 | 推送当日 | LocalDate.now() |
| issuedate【必】 | 开票日期 | 票池镜像票面 | blade_kingdee_invoice_pool.invoice_date |
| biztype【必】 | 业务类型 | 固定采购 | `PUR` |
| invoicetypef7_number【必】 | 发票类型.编码 | 票易通原始编码(缺失拦截并按票号回源 A801-1 补填) | blade_kingdee_invoice_pool.invoice_type_code |
| paymenttype【必】 | 付款方式 | 固定现购 | `CASH` |
| asstacttype【必】 | 往来类型 | 统一固定客户(与应付单/付款申请同口径) | `bd_customer` |
| org_number / currency_number【必】 | 组织/币种 | 分摊应付结算单关联合同 | blade_contract_manage.organization_id / settlement_currency |
| receivablessupp_number【必】 | 往来户.编码 | 合同乙方→客商编码 | blade_contract_manage.party_b → customer_code |
| exratetable_number【必】 | 汇率表 | 固定 | `"01"` |
| asstactname / buyertin / sellertin【必】 | 销方名称/购销方税号 | 票池镜像票面 | blade_kingdee_invoice_pool.issuer_name / buyer_tax_no / saler_tax_no |
| buyername【必】 | 购方名称 | 镜像受票单位 | blade_kingdee_invoice_pool.receiver_name |
| sellertel / sellerbank / selleracct | 销方电话/开户行/账号 | 镜像 | blade_kingdee_invoice_pool.phone / bank_name / bank_account |
| isincludetax | 含税录入 | 固定 | `true` |
| entry[] | 分录 | 与 A102-2 同源同口径(复用 resolveEntryGroups) | 同 4.2 |
### A302-6 收票指定应付
| 入参 | 来源 |
|---|---|
| op | 常量 `"assign"` |
| ap_invoice[0].awrp_src_code / awrp_src_sys | blade_invoice_receipt.id / `thirdparty.bfi.srcSys` |
| ap_finapbill[0].awrp_src_code / awrp_src_sys | 应付防重键(结算单号_明细单据号)/ 同上 |
---
## 10. A301-3 收款认领结果推送
| 金蝶入参 | 含义 | 来源 | 表.字段 / 配置 / 常量 |
|---|---|---|---|
| billid【必】 | 认领公告 id | 流水同步落库(缺失拦截「请先同步金蝶收款流水」) | blade_kingdee_receipt_flow.bill_id |
| awrp_src_sys【必】 | 来源系统 | 配置 | `thirdparty.bfi.srcSys` |
| awrp_src_code【必】 | 来源单据编码 | 该流水全部分摊结算单号去重下划线拼接(推送时生成落库,分摊不变重试复用) | blade_receipt_claim_settlement.formal_settlement_no(status=1,结算单 id 升序)→ 回写 blade_kingdee_receipt_flow.awrp_src_code |
| recpaytype_number【必】 | 收款类型 | **常量当前值 `"999"`**(注释「示例 101 待金蝶确认」) | KingdeeBfiConstant.BFI_CLAIM_REC_PAY_TYPE |
| paymenttype【必】 | 付款人类型 | 固定 | `bd_customer` |
| recviewpayer / recpayer【必】 | 付款人名称/id | 流水付款方 | blade_kingdee_receipt_flow.payer_name / payer_id |
| entryentity[0].e_receivableamt【必】 | 应收金额(单条分录) | 全部有效认领分摊金额合计 | SUM(blade_receipt_claim_settlement.allocated_receipt_amount) |
| entryentity[0].seq / e_corebillno【必】 | 行序/核心单据编号 | 第一张分摊结算单 id / 其金蝶星瀚内码(缺失拦截「未同步金蝶」) | blade_receipt_claim_settlement.formal_settlement_id / blade_formal_settlement.kingdee_bill_id |
| entryentity[0].e_corebilltype | 核心单据类型 | **注释不传** | — |
---
## 11. lockBusiData 单据锁定
| 入参 | 含义 | 来源 |
|---|---|---|
| lockAction【必】 | 操作 | 常量 `LOCK` |
| sourceSystem【必】 | 源系统 | `thirdparty.bfi.srcSys`(默认 WLTMS) |
| targetSystem【必】 | 目标系统 | 常量 `XH` |
| operationType【必】 | 操作类型 | 常量 `SAVE` |
| sourceBillType【必】 | 源单据类型 | 见下表常量 |
| sourceBillNo【必】 | 本地单据号 | 见下表 |
| targetBillType【必】 | 金蝶单据类型 | 见下表 |
| targetBillNo【必】 | 金蝶单号(逐张) | 见下表 |
| operator | 操作人 | 当前用户名(定时/补偿兜底 `system`;补偿取 blade_kingdee_bill_lock.lock_operator) |
| time | 操作时间 | 当前时间 `yyyy-MM-dd HH:mm:ss` |
| sourceBillType | sourceBillNo | targetBillType | targetBillNo |
|---|---|---|---|
| `SETTLEMENT` | blade_formal_settlement.formal_settlement_no | receivable→`arfin_other_BT_S`;否则 `ApFin_other_BT_S` | 推送返回 billNo(多张逗号拆分逐张锁) |
| `PAY_APPLY` | blade_payment_application.payment_no | `ap_payapply_oth_BT_S` | blade_payment_application.kingdee_bill_no |
| `INVOICE_APPLY` | blade_invoice_application.application_no | `cdm_paybill_bank_elect_BT_S` | 推送返回 billNo(多张逗号拆分) |
| `RECEIVED_INVOICE` | — | `cdm_recbill_bank_BT_S` | 预留,**本期不锁** |
> 锁记录表 blade_kingdee_bill_lock(lock_status: failed/locked/unlocked,try_count≤5);开关 `thirdparty.bfi.billLock.enabled`(默认 true)。代码:KingdeeBillLockServiceImpl L331-344、KingdeeLockConstant L24-87。
---
## 12. A902 附件上传
multipart 上传:`file`=文件流,`args`=JSON `{entityNumber, billPkId, controlKey}`。
| 入参 | 来源 |
|---|---|
| entityNumber(星止单据类型) | biz_type 映射:`pay_application`→ap_payapply;`formal_settlement_ap`→ap_finapbill;`formal_settlement_ar`→ar_finarbill;`invoice_application`→sim_original_bill;`invoice_receipt`→ap_invoice |
| billPkId(星瀚内码) | 推送落库的金蝶内码:blade_payment_application.kingdee_bill_id / blade_invoice_application.kingdee_bill_id / 结算单与收票推送返回内码(为空整单跳过待重推补传) |
| controlKey | 常量 `attachmentpanel` |
| 文件名 | 业务表 attachments_json 行内 originalName/name/fileName(空兜底 attachment;票池镜像兜底「发票文件」) |
| 文件流 | 业务表 attachments_json 的 url 下载;收票票池镜像按 path 经 A801-2 下载 |
- 附件存储:各业务单据 `attachments_json` 列(非 blade_attachment 表);同步流水落 `blade_kingdee_attachment_sync`(biz_type/biz_id/attachment_key/kingdee_path/sync_status/fail_reason)。
- 单文件软限制 20MB,超限拦截(重推时跳过该件继续其余)。
- 调用时机:推送成功事务提交后联带上传,失败不污染主单状态;重推补传。
> 代码:KingdeeAttachmentConstant L45-108、KingdeeAttachmentSyncSupport L125-264、BfiServiceImpl L613-638。
---
## 13. 查询类接口入参简表
| 接口 | 关键入参 | 取值来源 / 常量 / 配置 |
|---|---|---|
| A102-4 应付单查询 | awrp_src_code 多值(50/批)、awrp_src_sys、pageSize 200 | 应付防重键批次;`thirdparty.bfi.srcSys` |
| A302-4 应收单查询 | 同上(50/批、200) | 应收防重键批次 |
| A103-2 付款申请单查询 | awrp_src_code=paymentNo(50/批)、pageSize 200 | blade_payment_application.payment_no |
| A103-4 付款处理查询 | org_number(优先取 A103-2 返回 payorg_number,兜底合同组织)、业务日期今天回溯 1 个月、200/页 | A103-2 返回值 → blade_dept.kingdee_dept_code |
| A303-4 开票申请查询 | billno 多值(application_no、-张次序号)、pageSize=max(张数,10) | blade_invoice_application(.sheet) |
| A303-5 开票发票查询(openApi) | serialNo=金蝶单号逗号拆分(≤50/批)、sellerTaxpayerId=开票单位税号、请求体 Base64 | blade_invoice_application.kingdee_bill_no;blade_customer_archive.unified_credit_code |
| A801-3 收票单查询 | buyername(按镜像受票单位分组)、modifytimeS/E=回看 30 天、200/页;本地按 srcCode=登记 id 匹配 | blade_kingdee_invoice_pool.receiver_name;`thirdparty.bfi.receivedInvoice.queryWindowDays/resultWindowDays`(默认 30);blade_invoice_receipt.id |
| A801-1 票池查询 | invoiceStatus=`0`(正常票);invoiceType 单值(白名单逐轮);updateTime 窗口=incrementDays 增量、2.5 万触顶二分;500/页。**expenseStatus 不下推**(可用性本地过滤 amount_use_status=0 且 available_amount>0) | `thirdparty.bfi.invoiceReceipt.invoiceTypes / incrementDays`(30);本地表 blade_kingdee_invoice_pool |
| A801-2 附件下载 | path=文件地址(镜像优先、实时回源兜底;优先级 pdfurl→taxPdfUrl→downloadUrl,fileType=4 判 OFD) | blade_kingdee_invoice_pool.attachments_json |
| A301-1 收款单查询 | 业务日期今天回溯 30 天、200/页;勾稽 sourcebillnumber==awrp_src_code | blade_kingdee_receipt_flow.awrp_src_code |
| A301-2 认领公告查询 | claimusers=**Nacos 配置值**(登录人入参已废弃)、modifytimeS/E=回溯 months 个月、200/页;billstatus 字段存在但不传 | `thirdParty.bfi.claimAnnounce.claimusers`(必配)、`.months`(6) |
---
## 14. 与总纲记载不一致的核对结论(已按代码核实)
| # | 事项 | 代码事实 |
|---|---|---|
| 1 | A103-1 的 exratetable_number / awrp_textfield1 | **实际不送**(L1245/L1249 注释);配置 `payApply.exratetable=01` 只在 A103-3 生效 |
| 2 | A103-3 项目编码 | **实际固定 999**,「取项目档案 projectCode 真实编码」逻辑已注释停用(bfiProjectNumber L1508-1513)——总纲 12.2-#2「仅 A103-3 用真实编码」的说法作废,全部链路均为 999 |
| 3 | A301-3 recpaytype_number | 常量当前值 **"999"**(非旧文档写的 101;注释「示例 101 待金蝶确认」) |
| 4 | A301-2 claimusers | 只读 Nacos 配置,方法入参(当前登录人姓名)已不参与取值;billstatus 字段存在但不传 |
| 5 | A801-1 expenseStatus | 同步**不下推**(常量已定义未使用),「在用/已用票随窗口刷新」,可选票过滤在本地 SQL |
| 6 | 应付 asstact_number | 取**合同乙方 party_b**(payer_name 仅应收侧使用) |
> 校验提醒:以上配置键的**环境实际值**以 Nacos 为准(本文只列键名与代码默认值);blade_* 表列名按 Entity 驼峰↔下划线约定标注,个别快照列以建表 SQL 为准。
@@ -0,0 +1,53 @@
# 付款申请金蝶生成即回显预结算预付信息:synced 提前建档 + 作废联动清理
> 日期:2026-10-16
> 关联文档:
> - `doc/docs/业务流程/2026-09-29-预付款申请A103-3金蝶链路补齐方案(e_bebank_number统一-消失解锁-回写默认开启).md`(A103-3 推送与 A103-2 消失解锁)
> - `doc/docs/业务流程/2026-09-17-付款管理金蝶付款结果定时回写(A103-2付款申请单查询+A103-4付款处理查询)技术文档.md`(回写任务与 `applyPayApplyResult`)
> - `doc/docs/业务流程/2026-09-20-应付申请与付款申请同步处理全链路业务流程(A102-2下推-A102-4查询-A103-1生成-A103-2回写).md`
## 1. 背景与问题
排查预结算单 YJ2026101500001(已完成预结算审批、付款申请 FKD2026101500001 审批、金蝶付款申请单生成 FKSQ-00000229):预结算单详情页"预付信息"卡片始终空白。
根因:`blade_pre_settlement_advance`(预付记录表,详情接口 `advances` 字段的数据源)此前**只在金蝶回写"付款完成"(`kingdeeStatus=paid`)时才写入**(`applyPayApplyResult` → `syncPreSettlementAdvance`)。付款申请推进到"已生成"(synced)阶段不落记录,页面预付信息必须等付款完成才可见;且基本信息区的预付申请/已付金额来自付款申请实时汇总(`fillPaymentApplicationAmounts`),金额有值、明细卡片空白,容易误判。
## 2. 改造内容(`PaymentApplicationServiceImpl`)
| # | 变更点 | 变更前 | 变更后 |
|---|---|---|---|
| 1 | `syncPreSettlementAdvance` | 无参方法,写死 `billStatus="paid"`、已付=申请全额 | 参数化 `(entity, billStatus, paidAmount)`,建档与付款完成共用;按付款申请单号查重幂等 |
| 2 | `doSyncKingdee` synced 落库处 | 仅更新付款申请状态 | **生成成功即建档**:`approved` / 已付 0,同事务落库,预付信息提前回显 |
| 3 | `applyPayApplyResult` paid 分支 | 调原方法 | `paid` / 申请全额(语义不变),已建档时更新为已付款 |
| 4 | `applyPayApplyResult` 勾稽分支(新增) | failed 单据经 A103-2 勾稽确认 synced/paying 时不落预付记录 | 与推送成功同语义,`approved` / 已付 0 建档 |
| 5 | 新增 `voidPreSettlementAdvance` | 无 | 按付款申请单号将未付预付记录置 `voided`(`paid` 记录不联动,付款完成事实不回退),并刷新预结算汇总 |
| 6 | `voidBill`(付款申请作废) | 不清理预付记录 | 联动作废,避免残留记录阻断预结算作废(`PreSettlementServiceImpl#voidBill` 的"存在关联预付申请"拦截) |
| 7 | `unlockBill`(金蝶反审核解锁,含 A103-2 连续两轮消失判定) | 不清理预付记录 | 联动作废,重推生成新单后重新建档 |
建档/作废后的联动刷新(`refreshPreSettlementAdvanceSummary` 预结算 advance_no/预付金额列、`formal_settlement_source` 预付金额、正式结算付款汇总)复用原有尾部逻辑,无需额外改动。转正式结算时来源表预付金额不再恒为 0。
## 3. 状态与联动时序
| 付款申请阶段 | 预付记录(blade_pre_settlement_advance) | 页面预付信息 |
|---|---|---|
| 审批通过、未推送 | 不存在 | 空白 |
| 金蝶付款申请单生成成功(synced) | `approved` / 已付 0(提前建档) | **回显**(状态"审批通过") |
| 付款中(paying)/ 勾稽确认已生成 | `approved`(补建或维持) | 回显 |
| 付款完成(paid 回写) | `paid` / 已付全额 | 回显(状态"已付款") |
| 付款申请作废 / 金蝶反审核解锁 | 未付记录置 `voided`(`paid` 不动) | 消失(汇总同步扣减) |
前端零改动:`pre-settlement-editor.vue` 的 `advanceStatusName` 已含 `approved→审批通过` 映射。
## 4. 定时任务影响
- **付款结果回写任务(`syncKingdeePaymentResult`)**:零改动。轮询条件基于付款申请自身状态,不依赖预付记录表;paid 回写时命中已有记录更新,不产生重复。
- **失败重推(`retryKingdeeFailed`)**:failed 阶段未建档,重推成功经 synced 建档一次,幂等查重兜底。
- **A103-4 付款记录回填、金蝶单据锁补偿**:不涉及预付记录表,无影响。
## 5. 行为变化与存量数据
| 项 | 说明 |
|---|---|
| 预结算作废拦截提前 | "存在关联预付申请,请先作废预付申请"从付款完成后提前到金蝶生成即生效——作废预结算前需先作废付款申请(预期收紧) |
| 存量单据不自动补 | 改造上线前已 synced 的付款申请(如 FKD2026101500001)不会重推(状态门禁跳过 synced),预付记录要等 paid 回写补建;需立即回显的,手工补一条 `blade_pre_settlement_advance`(`advance_no=付款申请单号`、`bill_status='approved'`、已付 0) |
| 金额模糊匹配触发提前 | synced 建档时同额未付记录的兜底匹配与 paid 阶段同一逻辑,仅触发时点提前;如出现误绑定,可将模糊匹配收敛到 paid 分支 |
@@ -0,0 +1,130 @@
# 收款流水(认领通知单)多次认领与作废口径梳理
> 日期:2026-10-16
> 范围:`tms-api` blade-transport 收款流水认领、认领记录模块;`tms-erp-web` 认领记录页面
---
## 一、结论速览
| 问题 | 结论 |
| --- | --- |
| 一个通知单能被多次认领,每次认领生成一条还是多条认领记录? | **每次认领操作生成一条认领主单**(`blade_receipt_claim`),多次认领即多条主单;每次认领同时按所选拆分生成 N 条结算单分摊明细(`blade_receipt_claim_settlement`) |
| 认领记录页面是一条一条显示还是只显示一条? | **一条认领主单一行,逐条显示**,不按通知单合并;同一通知单认领 3 次就显示 3 行,每行"认领金额"只是该次认领的金额 |
| 作废是逐条作废归还金额,还是整个通知单全部作废? | **逐条作废、按条归还金额**:作废入口在单条认领记录上,回退金额 = 该条认领主单的 `claim_amount`,流水已认领金额只减这一条;其余认领记录不受影响 |
---
## 二、数据模型
```
blade_kingdee_receipt_flow(收款流水 / 认领通知单,1)
│ receipt_flow_id
├─< blade_receipt_claim(认领主单,N) ← 每次认领一条
│ claim_amount 本次认领总金额
│ claimer_id 认领人(记录页面按本人过滤)
│ claim_status claimed / voided
│ voided_by / voided_time 作废人 / 作废时间
│
└─< blade_receipt_claim_settlement(分摊明细,N × M) ← 每次认领按结算单拆行
receipt_claim_id 归属哪次认领
formal_settlement_id/no 关联应收正式结算单
allocated_receipt_amount 本次分摊金额
claimed_receipt_amount 认领前该结算单已认领金额
status 1 有效 / 0 无效(作废置 0,不物理删除)
```
关键点:**认领主单的粒度是"一次认领操作"**,不是通知单。通知单维度只有累计金额(`claimed_amount`)和状态(`claim_status`:unclaimed / partial / claimed),由每次认领/作废累加回退得出。
---
## 三、认领:一次操作生成一条主单 + N 条分摊
代码:`ReceiptFlowServiceImpl#claim(ReceiptClaimRequest)`
1. 锁定流水(通知单)与所选结算单。
2. 校验两条累计上限:
- 每张结算单:已有累计认领 + 本次分摊 ≤ 结算总应收含税金额;
- 流水:已有累计认领(`flowClaimedAmount`,只算未作废记录)+ 本次总额 ≤ 公告未认领金额(`unclaimed_amount`,A301-2 口径)。
3. **插入一条** `ReceiptClaim` 主单:`claim_amount` = 本次分摊总额,`claim_status = claimed`。
4. 按所选结算单逐张**插入 N 条** `ReceiptClaimSettlement` 分摊明细。
5. 累加流水 `claimed_amount`,按累计值重算流水状态(< 未认领金额 → partial,= → claimed)。
6. 只登记本地,不推送金蝶;推送由定时任务/手动按钮触发。
所以:**同一通知单第二次认领是再插一条新主单**,不存在"找已有主单合并金额"的逻辑;校验全部按"未作废记录累计"实时计算,天然支持多次认领。
### 认领按钮的显示条件(为什么有些行没有"认领")
收款流水列表操作列(`receipt-flow.vue` → `canClaim(row)`):
```js
canClaim(row) {
return this.hasPermission('receipt_flow_claim') && Number(row.remainingAmount || 0) > 0;
}
```
两个条件同时满足才显示"认领"按钮:
1. **按钮权限**:当前用户拥有 `receipt_flow_claim` 按钮权限(admin 直接放行,普通用户取决于菜单按钮配置);
2. **剩余可认领金额 > 0**:`remainingAmount` 由后端 `ReceiptFlowWrapper` 统一计算:
```
remainingAmount = max(公告未认领金额 unclaimed_amount − 累计认领金额 claimed_amount, 0)
```
按认领状态换算:
| 流水认领状态 | remainingAmount | 认领按钮 |
| --- | --- | --- |
| unclaimed 未认领(公告金额 > 0) | = 公告未认领金额 > 0 | 显示 |
| partial 部分认领 | > 0 | 显示 |
| claimed 认领完成 | = 0(累计 ≥ 公告金额) | **不显示** |
| 公告未认领金额为 0 的流水 | = 0 | **不显示** |
| 无 `receipt_flow_claim` 权限 | — | **不显示** |
即:**按钮显隐只看金额余额与权限,不看认领状态字段本身**;`claimed` 状态的行因为剩余额度为 0 自然不显示按钮。按钮不显示且不满足"推送金蝶"条件时,操作列显示"-"。
注意:顶部"批量认领"按钮只判断权限与是否勾选行(`receipt_flow_claim` + `selectedRows.length`),**不按行剩余金额过滤**;对无剩余额度的流水发起批量认领时,进入批量页面后由提交校验/后端 `claim()` 兜底拦截。
---
## 四、认领记录页面:逐条显示
后端:`ReceiptClaimMapper.xml#selectClaimRecordPage` → `FROM blade_receipt_claim c JOIN blade_kingdee_receipt_flow f`,固定条件 `c.claimer_id = 当前用户`。
- **列表一行 = 一条认领主单**,无按流水/通知单的 GROUP BY 合并;
- 同一通知单多次认领 → 多行,各行的认领金额、认领日期、金蝶单号都是当次认领自己的;
- 关联结算单列用 `GROUP_CONCAT(分摊明细.结算单号)` 拼成一串展示(只展示,不参与合并);
- 详情页(`selectClaimRecordDetail` + 分摊明细列表)展示的也是**该次认领**的分摊明细,明细里的"认领前已认领金额"记录的是认领那一刻结算单的累计值,可还原多次认领的先后关系;
- 前端 `receipt-claim-record.vue` 列表 + 行级"作废"按钮(`api.voidClaim(row.id)`),确认文案为"作废后将回退本次认领金额"——与逐条口径一致。
另:收款流水列表的详情弹窗里有一个"认领记录"区块,展示的是该流水的**操作日志**(`blade_receipt_flow_record`,action_type = claim/push),按时间线展示每次认领/推送动作,同样不是合并视图。
---
## 五、作废:逐条作废、按条归还金额
代码:`ReceiptClaimRecordServiceImpl#voidClaim(Long id)`,入参就是**单条认领主单 id**。
流程(单事务):
1. 锁定该条认领主单,校验:本人记录、`claim_status = claimed`(已作废不可再作废)。
2. 锁定关联流水,校验:**未推送金蝶**——已推送的不允许作废,提示"请在金蝶侧人工处置"。
3. 取该主单名下有效分摊明细(status=1),全部置 `status = 0`(保留审计,不删行)。
4. 批量刷新相关结算单收款汇总(`refreshPaymentSummaries`)。
5. **流水 `claimed_amount` 只减去本条主单的 `claim_amount`**(下限 0),并按减后累计重算流水认领状态(0 → unclaimed,< 未认领金额 → partial,≥ → claimed)。
6. 主单置 `voided`,记录作废人、作废时间;写 `void_claim` 操作日志(金额 = 本次回退金额)。
7. **不生成金蝶认领冲单**:金蝶单号/状态保留原值,金蝶侧冲销/红红由人工处置;冲单逻辑代码已注释保留(`buildKingdeeBillNo`),后续迭代可放开。
边界与影响:
- 作废某一条,只归还这一条的金额,**同一通知单的其他认领记录不受影响**,仍正常显示、仍可单独作废;
- 作废后该条记录仍在列表中(状态"已作废"),不允许再改附件、不可重复作废;
- 归还后的金额空间立即可被再次认领(认领校验按未作废累计实时计算);
- 附件/操作日志/分摊明细均保留,金额回退可追溯。
---
## 六、一句话总结
通知单(收款流水)是"累计额度"维度,认领记录是"每次操作"维度:**认领一次记一条、页面逐条展示、作废按条回退金额**;整单性质的只有流水上的累计已认领金额与认领状态,由各条记录的认领/作废实时推导,没有任何"按通知单整体作废"的入口。
+3
View File
@@ -57,6 +57,7 @@ thirdParty:
# 国内运输:运单运输方式 -> 金蝶物料编码(公路运输/铁路运输/水路运输/航空运输四种)
domestic:
"[公路运输]": ${BFI_MATERIAL_DOMESTIC_ROAD:XDWL11010001}
"[road]": ${BFI_MATERIAL_DOMESTIC_ROAD:XDWL11010001}
"[铁路运输]": ${BFI_MATERIAL_DOMESTIC_RAIL:XDWL11010002}
"[水路运输]": ${BFI_MATERIAL_DOMESTIC_WATER:XDWL11010003}
"[航空运输]": ${BFI_MATERIAL_DOMESTIC_AIR:Item-00001082}
@@ -118,6 +119,8 @@ thirdParty:
incrementDays: ${BFI_INVOICE_RECEIPT_INCREMENT_DAYS:30}
# 同步发票种类白名单(逗号分隔票易通编码,如 26,27 数电普票/专票),为空不限制
invoiceTypes: ${BFI_INVOICE_RECEIPT_INVOICE_TYPES:26,27}
# 客商单位过滤开关:开启后只同步开票单位(销方)与受票单位(购方)税号均已登记在客商发票信息中的发票,前端票池展示查询同用此开关过滤
customerUnitsOnly: ${BFI_INVOICE_RECEIPT_CUSTOMER_UNITS_ONLY:false}
# 补偿绑定单轮限量(未绑定登记按发票号码精确补查条数上限)
compensateLimit: ${BFI_INVOICE_RECEIPT_COMPENSATE_LIMIT:200}
# single-flight 分布式锁租约秒数,须大于最坏一轮耗时;进程异常退出时锁在租约到期后自动释放