!2 金蝶解锁回调以及客商审核通过推送金蝶失败被吞异常,以及补充文档

Merge pull request !2 from mindless/feature/payment-apply-bfi-sync
This commit is contained in:
2026-10-15 16:58:41 +00:00
committed by Gitee
58 changed files with 1297 additions and 58 deletions
@@ -24,6 +24,7 @@ import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serial;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -33,7 +34,7 @@ import java.util.List;
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class FormalSettlementVO extends FormalSettlement {
public class FormalSettlementVO extends FormalSettlement implements KingdeeUnlockMark {
@Serial private static final long serialVersionUID = 1L;
@TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createStartDate;
@TableField(exist = false) @DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate createEndDate;
@@ -50,6 +51,12 @@ public class FormalSettlementVO extends FormalSettlement {
private String kingdeeLockFailReason;
@TableField(exist = false) @Schema(description = "金蝶锁重试次数(达上限5后不再自动补偿,需重推同步金蝶)")
private Integer kingdeeTryCount;
@TableField(exist = false) @Schema(description = "金蝶解锁时间(非空代表该单曾被金蝶解锁/删单)")
private LocalDateTime kingdeeUnlockTime;
@TableField(exist = false) @Schema(description = "金蝶侧解锁操作人(回调报文 operator)")
private String kingdeeUnlockOperator;
@TableField(exist = false) @Schema(description = "解锁动作名称:金蝶删单/金蝶锁单")
private String kingdeeUnlockOperationName;
@TableField(exist = false) private String preSettlementNos;
@TableField(exist = false) private String preSettlementNo;
@TableField(exist = false) private List<FormalSettlementSource> sources;
@@ -35,6 +35,7 @@ import org.springblade.transport.pojo.entity.InvoiceApplicationRecord;
import org.springblade.transport.pojo.entity.InvoiceApplicationSettlement;
import java.io.Serial;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -44,7 +45,7 @@ import java.util.List;
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class InvoiceApplicationVO extends InvoiceApplication {
public class InvoiceApplicationVO extends InvoiceApplication implements KingdeeUnlockMark {
@Serial private static final long serialVersionUID = 1L;
@TableField(exist = false) private String settlementNos;
@TableField(exist = false) private String approvalStatusName;
@@ -55,6 +56,12 @@ public class InvoiceApplicationVO extends InvoiceApplication {
@TableField(exist = false) private String kingdeeLockStatus;
@TableField(exist = false) private String kingdeeLockFailReason;
@TableField(exist = false) private Integer kingdeeTryCount;
/** 金蝶解锁终态标识(unlocked 锁记录回填):解锁时间非空即代表该单曾被金蝶解锁/删单 */
@TableField(exist = false) private LocalDateTime kingdeeUnlockTime;
/** 金蝶侧解锁操作人(回调报文 operator) */
@TableField(exist = false) private String kingdeeUnlockOperator;
/** 解锁动作名称:金蝶删单/金蝶锁单 */
@TableField(exist = false) private String kingdeeUnlockOperationName;
@TableField(exist = false) private String createUserName;
@TableField(exist = false) private List<InvoiceApplicationSettlement> settlements;
@TableField(exist = false) private List<InvoiceApplicationSheetVO> sheets;
@@ -0,0 +1,36 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.vo;
import java.time.LocalDateTime;
/**
* 金蝶解锁终态标识展示契约:需在页面展示「金蝶删单/解锁」标识的 VO 实现之,
* 由 {@code KingdeeBillLockService#fillUnlockMarks} 统一回填(列表/详情共用)
*
* @author Chill
*/
public interface KingdeeUnlockMark {
/**
* 金蝶解锁时间(unlocked 锁记录回填),非空即代表该单曾被金蝶解锁/删单
*/
void setKingdeeUnlockTime(LocalDateTime kingdeeUnlockTime);
/**
* 金蝶侧解锁操作人(回调报文 operator)
*/
void setKingdeeUnlockOperator(String kingdeeUnlockOperator);
/**
* 解锁动作名称:金蝶删单/金蝶锁单
*/
void setKingdeeUnlockOperationName(String kingdeeUnlockOperationName);
}
@@ -29,12 +29,13 @@ import org.springblade.transport.pojo.entity.PaymentApplicationSettlement;
import java.io.Serial;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/** 付款申请视图。 @author Chill */
@Data
@EqualsAndHashCode(callSuper = true)
public class PaymentApplicationVO extends PaymentApplication {
public class PaymentApplicationVO extends PaymentApplication implements KingdeeUnlockMark {
@Serial private static final long serialVersionUID = 1L;
@TableField(exist = false) private LocalDate applyStartDate;
@TableField(exist = false) private LocalDate applyEndDate;
@@ -49,6 +50,12 @@ public class PaymentApplicationVO extends PaymentApplication {
@TableField(exist = false) private String kingdeeLockStatus;
@TableField(exist = false) private String kingdeeLockFailReason;
@TableField(exist = false) private Integer kingdeeTryCount;
/** 金蝶解锁终态标识(unlocked 锁记录回填):解锁时间非空即代表该单曾被金蝶解锁/删单,供页面展示明显标识 */
@TableField(exist = false) private LocalDateTime kingdeeUnlockTime;
/** 金蝶侧解锁操作人(回调报文 operator) */
@TableField(exist = false) private String kingdeeUnlockOperator;
/** 解锁动作名称:金蝶删单/金蝶锁单 */
@TableField(exist = false) private String kingdeeUnlockOperationName;
@TableField(exist = false) private List<PaymentApplicationInvoice> invoices;
@TableField(exist = false) private List<PaymentApplicationRecord> paymentRecords;
@TableField(exist = false) private List<PaymentApplicationSettlement> settlements;
@@ -372,6 +372,14 @@ public class BfiProperties {
@Data
public static class BillLock {
/**
* 锁定/解锁总开关(thirdParty.bfi.billLock.enabled):推送后锁定 lockBusiData、
* 金蝶解锁回调 unLockNotifyBusiData、锁定补偿重锁三处共用;
* 默认开启保持既有锁定行为,Nacos 置 false 一键停用
* (推送后不再锁单、解锁回调只应答不处理、补偿跳过),存量锁记录守卫(不可编辑校验)不受影响
*/
private boolean enabled = true;
/**
* 锁定补偿开关,默认关闭(安全兜底)
*/
@@ -136,21 +136,25 @@ public class CustomerArchiveController extends BladeController {
}
/**
* 审核通过(通过后自动推送金蝶,推送失败不影响审批结果)
* 审核通过(通过后自动推送金蝶,推送失败不影响审批结果,失败原因以警告文案返回)
*/
@PostMapping("/approve")
@ApiOperationSupport(order = 6)
@Operation(summary = "审核通过", description = "传入id")
public R approve(@Parameter(description = "主键", required = true) @RequestParam Long id) {
boolean result = customerArchiveService.approve(id);
if (result) {
try {
customerArchiveService.syncKingdee(id);
} catch (Exception exception) {
log.error("客商审批通过后推送金蝶异常,id={}", id, exception);
}
if (!result) {
return R.status(false);
}
try {
customerArchiveService.syncKingdee(id);
return R.success("审核通过");
} catch (Exception exception) {
log.error("客商审批通过后推送金蝶异常,id={}", id, exception);
String reason = exception.getMessage() == null ? "金蝶接口调用异常" : exception.getMessage();
// 与结算/付款/开票审批口径一致:审批已生效,推送失败原因以 data=false 警告文案返回
return R.data(Boolean.FALSE, "审核通过,但推送金蝶失败:" + reason);
}
return R.status(result);
}
/**
@@ -196,6 +196,14 @@ public interface ICustomerArchiveService extends BaseService<CustomerArchive> {
*/
CustomerArchive syncKingdee(Long id);
/**
* 推送失败状态落库(独立事务):推送主事务已回滚,失败状态与原因单独落库供页面展示与手动重推
*
* @param id 主键
* @param reason 失败原因
*/
void markKingdeeSyncFailed(Long id, String reason);
/**
* 定时同步金蝶客商数据:待推送客商自动推送 + 已同步客商使用状态回写
*
@@ -9,8 +9,10 @@
package org.springblade.transport.service;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springblade.core.mp.base.BaseEntity;
import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
import org.springblade.transport.pojo.entity.KingdeeBillLock;
import org.springblade.transport.pojo.vo.KingdeeUnlockMark;
import java.util.List;
import java.util.Map;
@@ -39,8 +41,10 @@ public interface IKingdeeBillLockService extends IService<KingdeeBillLock> {
String lockAfterPush(String sourceBillType, Long sourceBillId, String sourceBillNo, String targetBillType, List<String> targetBillNos);
/**
* 金蝶解锁回调(unLockNotifyBusiData):校验来源/目标系统与 lockAction,
* targetBillNo 支持逗号拼接批量,逐张按 targetBillType+targetBillNo 定位最新 locked 锁记录;
* 金蝶解锁回调(unLockNotifyBusiData):校验来源/目标系统与 lockAction。
* 注意回调方向与锁定相反:回调 sourceBillNo=金蝶单号(即锁记录 targetBillNo,逗号拼接批量)、
* targetBillNo=WLTMS 单号(即锁记录 sourceBillNo)、targetBillType 可能为空,
* 故按金蝶单号+我方单号双条件反查最新 locked 锁记录;
* operationType=DELETE 时按单据类型分发废除源单并还原关联数据
*
* @param param 金蝶回调参数
@@ -66,6 +70,29 @@ public interface IKingdeeBillLockService extends IService<KingdeeBillLock> {
*/
Map<Long, KingdeeBillLock> mapActiveLocks(String sourceBillType, List<Long> sourceBillIds);
/**
* 批量查源单解锁终态留痕(供列表/详情页填充解锁标识):
* 每源单取最新一条 unlocked 锁记录(付款申请解锁后不作废,页面据此区分「金蝶删单/解锁」与普通推送失败)
*
* @param sourceBillType WLTMS 单据类型
* @param sourceBillIds 源单主键集合
* @return sourceBillId -&gt; 最新 unlocked 锁记录,无解锁留痕的源单不在 Map 中
*/
Map<Long, KingdeeBillLock> mapUnlockedLocks(String sourceBillType, List<Long> sourceBillIds);
/**
* 批量填充金蝶解锁终态标识(通用):对无活跃锁的行反查最新 unlocked 锁记录并回填解锁标识
* (解锁时间/操作人/动作名称),供前端展示明显解锁标识;有活跃锁的单必非解锁终态,跳过。
* 与 mapActiveLocks 配对在各单据 fillKingdeeLockStatus 中调用,列表/详情共用;
* 后续单据(如收票单)接入锁单时,VO 实现 KingdeeUnlockMark 即可复用
*
* @param sourceBillType WLTMS 单据类型
* @param rows 已填充活跃锁标识的 VO 行
* @param activeLockMap 活跃锁 Map(mapActiveLocks 返回值)
* @param &lt;T&gt; 实现 KingdeeUnlockMark 的 VO 类型(须继承 BaseEntity 以取主键)
*/
<T extends BaseEntity & KingdeeUnlockMark> void fillUnlockMarks(String sourceBillType, List<T> rows, Map<Long, KingdeeBillLock> activeLockMap);
/**
* 锁定补偿:扫 failed 且 try_count 未达重试上限(RETRY_MAX_COUNT)的锁记录,逐张重调 lockBusiData
* (接口 1对1:单次一个源单号对一个金蝶单号,单张成败互不影响;定时任务定时触发,亦可人工触发);
@@ -34,6 +34,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.SpringUtil;
import org.springblade.transport.mk.MkProcessNodeHelper;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.core.tenant.annotation.TenantIgnore;
@@ -96,6 +97,7 @@ import org.springblade.transport.pojo.vo.CustomerReceiptAccountVO;
import org.springblade.transport.service.ICustomerArchiveService;
import org.springblade.transport.support.KingdeeSyncLockSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@@ -528,14 +530,52 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
updateById(after);
return after;
}
pushCustomerToKingdee(id, customer, after, detail(id));
try {
pushCustomerToKingdee(id, customer, after, detail(id));
} catch (Exception exception) {
// 推送事务已回滚,此处独立事务落"推送失败"状态与原因,供页面展示与手动重推(与结算/付款/开票口径一致)
self().markKingdeeSyncFailed(id, exception.getMessage());
throw exception;
}
updateById(after);
return after;
}
/** 自代理:跨方法调用确保事务注解生效(失败状态独立事务) */
private ICustomerArchiveService self() {
return SpringUtil.getBean(ICustomerArchiveService.class);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
public void markKingdeeSyncFailed(Long id, String reason) {
if (id == null) {
return;
}
String failReason = truncateText(Func.toStrWithEmpty(reason, "金蝶接口调用异常"));
CustomerArchive before = getById(id);
// 条件更新:仅审批通过且未推送/推送失败的客商落失败状态,已推送/金蝶已有自动跳过保持原状态
int updated = baseMapper.update(null, Wrappers.<CustomerArchive>lambdaUpdate()
.eq(CustomerArchive::getId, id)
.eq(CustomerArchive::getApprovalStatus, APPROVAL_APPROVED)
.and(wrapper -> wrapper.isNull(CustomerArchive::getKingdeeStatus)
.or()
.notIn(CustomerArchive::getKingdeeStatus, List.of(KINGDEE_STATUS_SYNCED, KINGDEE_STATUS_EXIST)))
.set(CustomerArchive::getKingdeeStatus, KINGDEE_STATUS_FAILED)
.set(CustomerArchive::getKingdeeFailReason, failReason));
if (updated > 0 && before != null) {
// 变更记录留档(推送主事务已回滚,此处独立事务落状态与记录)
CustomerArchive after = copyCustomer(before);
after.setKingdeeStatus(KINGDEE_STATUS_FAILED);
after.setKingdeeFailReason(failReason);
addChangeRecord(id, "推送金蝶失败:" + failReason, before, after);
}
}
/**
* 组装 A902-2 报文推送金蝶并回写协同状态;detail 由调用方准备,
* 定时同步批量组装报文数据,避免逐条 detail 查询的 N+1 问题
* 定时同步批量组装报文数据,避免逐条 detail 查询的 N+1 问题;
* 推送失败先落 failed 状态与变更记录,再抛出异常由调用方提示前端(与结算/付款/开票口径一致)
*/
private void pushCustomerToKingdee(Long id, CustomerArchive customer, CustomerArchive after, CustomerArchiveVO detail) {
try {
@@ -549,12 +589,18 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
after.setKingdeeStatus(KINGDEE_STATUS_FAILED);
after.setKingdeeFailReason(buildSaveFailReason(result, item));
addChangeRecord(id, "推送金蝶失败:" + after.getKingdeeFailReason(), customer, after);
throw new ServiceException("推送金蝶失败:" + after.getKingdeeFailReason());
}
} catch (ServiceException exception) {
throw exception;
} catch (Exception exception) {
log.error("客商推送金蝶失败,id={},unifiedCreditCode={}", id, customer.getUnifiedCreditCode(), exception);
after.setKingdeeStatus(KINGDEE_STATUS_FAILED);
after.setKingdeeFailReason(truncateText(exception.getMessage() == null ? "金蝶接口调用异常" : exception.getMessage()));
addChangeRecord(id, "推送金蝶失败:" + after.getKingdeeFailReason(), customer, after);
throw exception instanceof RuntimeException runtimeException
? runtimeException
: new ServiceException("推送金蝶失败:" + after.getKingdeeFailReason());
}
}
@@ -715,11 +761,11 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
.map(CustomerArchive::getId)
.toList());
for (CustomerArchive customer : pushList) {
CustomerArchive after = copyCustomer(customer);
after.setKingdeeSyncTime(LocalDateTime.now());
try {
CustomerArchiveVO detail = buildSyncArchiveVO(customer,
receiptAccountsMap.getOrDefault(customer.getId(), new ArrayList<>()));
CustomerArchive after = copyCustomer(customer);
after.setKingdeeSyncTime(LocalDateTime.now());
pushCustomerToKingdee(customer.getId(), customer, after, detail);
updateList.add(after);
if (Objects.equals(after.getKingdeeStatus(), KINGDEE_STATUS_SYNCED)) {
@@ -735,6 +781,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
pushFailed++;
log.error("定时同步金蝶客商推送异常,id={},unifiedCreditCode={}",
customer.getId(), customer.getUnifiedCreditCode(), exception);
// 失败状态与原因已在推送方法内回填 after,落库供页面展示与重推
updateList.add(after);
}
}
// ②③ 共用:对已推送(synced)/金蝶已有(exist)客商按信用代码批量复查金蝶档案(审核状态 + 使用状态),仅调用一次 A902-1
@@ -233,6 +233,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl<FormalSettlemen
vo.setKingdeeLockFailReason(lockRecord.getFailReason());
vo.setKingdeeTryCount(lockRecord.getTryCount());
}
// 金蝶删单解锁会作废源单,按 unlocked 锁记录留痕补「金蝶删单」标识,区分正常作废与金蝶删单
kingdeeBillLockService.fillUnlockMarks(KingdeeLockConstant.SOURCE_SETTLEMENT, rows, lockMap);
}
/**
@@ -202,6 +202,8 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl<InvoiceApplic
vo.setKingdeeLockFailReason(lockRecord.getFailReason());
vo.setKingdeeTryCount(lockRecord.getTryCount());
}
// 金蝶删单解锁会作废源单,按 unlocked 锁记录留痕补「金蝶删单」标识,区分正常作废与金蝶删单
kingdeeBillLockService.fillUnlockMarks(KingdeeLockConstant.SOURCE_INVOICE_APPLY, rows, lockMap);
}
/**
@@ -13,6 +13,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseEntity;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.SpringUtil;
@@ -29,6 +30,7 @@ import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.InvoiceApplication;
import org.springblade.transport.pojo.entity.KingdeeBillLock;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.vo.KingdeeUnlockMark;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IInvoiceApplicationService;
import org.springblade.transport.service.IKingdeeBillLockService;
@@ -73,6 +75,11 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMappe
@Override
public String lockAfterPush(String sourceBillType, Long sourceBillId, String sourceBillNo, String targetBillType, List<String> targetBillNos) {
// 总开关关闭:不落锁记录、不调 lockBusiData,返回 null 不影响推送主流程
if (!bfiProperties.getBillLock().isEnabled()) {
log.info("金蝶单据锁定总开关关闭,跳过锁定:源单[{}:{}]", sourceBillType, sourceBillNo);
return null;
}
if (Func.isEmpty(targetBillNos)) {
return null;
}
@@ -145,40 +152,52 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMappe
@Override
@Transactional(rollbackFor = Exception.class)
public int onUnlockNotify(BfiBillLockDTO param) {
// 总开关关闭:只应答不处理(返回 0 与「未匹配」同构,金蝶侧不会触发重试)
if (!bfiProperties.getBillLock().isEnabled()) {
log.warn("金蝶单据锁定总开关关闭,跳过解锁回调处理:operationType={},金蝶单号={}",
param == null ? null : param.getOperationType(), param == null ? null : param.getSourceBillNo());
return 0;
}
if (param == null || !KingdeeLockConstant.SYSTEM_XH.equals(param.getSourceSystem())
|| !KingdeeLockConstant.SYSTEM_WLTMS.equals(param.getTargetSystem())
|| !KingdeeLockConstant.ACTION_UNLOCK.equals(param.getLockAction())) {
throw new ServiceException("金蝶解锁回调参数非法:sourceSystem须为XH、targetSystem须为WLTMS、lockAction须为UNLOCK");
}
if (Func.isBlank(param.getTargetBillNo())) {
throw new ServiceException("金蝶解锁回调参数非法:targetBillNo不能为空");
if (Func.isBlank(param.getSourceBillNo()) || Func.isBlank(param.getTargetBillNo())) {
throw new ServiceException("金蝶解锁回调参数非法:sourceBillNo、targetBillNo不能为空");
}
// 批量回调:targetBillNo 逗号拼接,逐张处理;未匹配锁定中记录的幂等跳过(重复通知防金蝶重试风暴)
int processed = 0;
for (String targetBillNo : Func.toStrList(param.getTargetBillNo())) {
KingdeeBillLock lockRecord = this.getOne(Wrappers.<KingdeeBillLock>lambdaQuery()
.eq(KingdeeBillLock::getTargetBillType, param.getTargetBillType())
.eq(KingdeeBillLock::getTargetBillNo, targetBillNo)
.orderByDesc(KingdeeBillLock::getId)
.last("limit 1"), false);
// 回调方向与锁定相反:回调 sourceBillNo=金蝶单号(即锁记录 targetBillNo)、targetBillNo=WLTMS 单号(即锁记录 sourceBillNo)、
// targetBillType 可能为空,故按金蝶单号+我方单号双条件反查;未匹配锁定中记录的幂等跳过(重复通知防金蝶重试风暴)
// 一次 IN 查询取回候选记录、内存取各单号最新一条,批量更新解锁状态,避免逐张查询/更新的 N+1
List<String> kingdeeBillNos = Func.toStrList(param.getSourceBillNo());
Map<String, KingdeeBillLock> latestLockMap = this.list(Wrappers.<KingdeeBillLock>lambdaQuery()
.in(KingdeeBillLock::getTargetBillNo, kingdeeBillNos)
.eq(KingdeeBillLock::getSourceBillNo, param.getTargetBillNo())
.orderByDesc(KingdeeBillLock::getId))
.stream()
.collect(Collectors.toMap(KingdeeBillLock::getTargetBillNo, Function.identity(), (first, second) -> first));
List<KingdeeBillLock> unlockRecords = new ArrayList<>();
for (String kingdeeBillNo : kingdeeBillNos) {
KingdeeBillLock lockRecord = latestLockMap.get(kingdeeBillNo);
if (lockRecord == null || !KingdeeLockConstant.STATUS_LOCKED.equals(lockRecord.getLockStatus())) {
log.warn("金蝶解锁回调未匹配锁定中的锁记录(幂等跳过):targetBillType={},targetBillNo={}",
param.getTargetBillType(), targetBillNo);
log.warn("金蝶解锁回调未匹配锁定中的锁记录(幂等跳过):金蝶单号={},回调targetBillNo={}",
kingdeeBillNo, param.getTargetBillNo());
continue;
}
lockRecord.setLockStatus(KingdeeLockConstant.STATUS_UNLOCKED);
lockRecord.setOperationType(param.getOperationType());
lockRecord.setUnlockTime(LocalDateTime.now());
lockRecord.setNotifyOperator(param.getOperator());
this.updateById(lockRecord);
processed++;
unlockRecords.add(lockRecord);
log.info("金蝶单据[{}:{}]解锁回调落库:operationType={},操作人={}",
lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(), param.getOperationType(), param.getOperator());
if (KingdeeLockConstant.OPERATION_DELETE.equals(param.getOperationType())) {
this.restoreOnKingdeeDelete(lockRecord, param.getOperator());
}
}
return processed;
this.updateBatchById(unlockRecords);
// DELETE 分发废除还原:按源单逐张执行(各单据类型还原动作为独立业务事务,无法合并)
if (KingdeeLockConstant.OPERATION_DELETE.equals(param.getOperationType())) {
unlockRecords.forEach(lockRecord -> this.restoreOnKingdeeDelete(lockRecord, param.getOperator()));
}
return unlockRecords.size();
}
/**
@@ -257,6 +276,44 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMappe
return lockRecords.stream().collect(Collectors.toMap(KingdeeBillLock::getSourceBillId, Function.identity(), (first, second) -> second));
}
@Override
public Map<Long, KingdeeBillLock> mapUnlockedLocks(String sourceBillType, List<Long> sourceBillIds) {
if (Func.isEmpty(sourceBillIds)) {
return Map.of();
}
List<KingdeeBillLock> lockRecords = this.list(Wrappers.<KingdeeBillLock>lambdaQuery()
.eq(KingdeeBillLock::getSourceBillType, sourceBillType)
.in(KingdeeBillLock::getSourceBillId, sourceBillIds)
.eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_UNLOCKED)
.orderByDesc(KingdeeBillLock::getId));
// 降序取回后首个生效:每源单保留最新一条解锁留痕
return lockRecords.stream().collect(Collectors.toMap(KingdeeBillLock::getSourceBillId, Function.identity(), (first, second) -> first));
}
@Override
public <T extends BaseEntity & KingdeeUnlockMark> void fillUnlockMarks(String sourceBillType, List<T> rows, Map<Long, KingdeeBillLock> activeLockMap) {
if (Func.isEmpty(rows)) {
return;
}
List<Long> unlockedIds = rows.stream()
.filter(row -> !activeLockMap.containsKey(row.getId()))
.map(BaseEntity::getId).toList();
if (Func.isEmpty(unlockedIds)) {
return;
}
Map<Long, KingdeeBillLock> unlockedMap = this.mapUnlockedLocks(sourceBillType, unlockedIds);
for (T row : rows) {
KingdeeBillLock unlockRecord = unlockedMap.get(row.getId());
if (unlockRecord == null) {
continue;
}
row.setKingdeeUnlockTime(unlockRecord.getUnlockTime());
row.setKingdeeUnlockOperator(unlockRecord.getNotifyOperator());
row.setKingdeeUnlockOperationName(KingdeeLockConstant.OPERATION_DELETE.equals(unlockRecord.getOperationType())
? "金蝶删单" : "金蝶锁单");
}
}
/**
* 锁定补偿:扫 failed 且 try_count 未达重试上限(RETRY_MAX_COUNT)的锁记录,逐张重调 lockBusiData
* (接口 1对1:单次一个源单号对一个金蝶单号,单张成败互不影响,由定时任务定时触发,亦可人工触发)。
@@ -265,6 +322,10 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMappe
*/
@Override
public String retryFailedLocks() {
// 总开关关闭:定时任务与手动补偿一并跳过
if (!bfiProperties.getBillLock().isEnabled()) {
return "金蝶单据锁定总开关关闭,跳过补偿";
}
List<KingdeeBillLock> failedList = this.list(Wrappers.<KingdeeBillLock>lambdaQuery()
.eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_FAILED)
.lt(KingdeeBillLock::getTryCount, KingdeeLockConstant.RETRY_MAX_COUNT));
@@ -240,6 +240,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
vo.setKingdeeLockFailReason(lockRecord.getFailReason());
vo.setKingdeeTryCount(lockRecord.getTryCount());
}
// 金蝶解锁终态标识:付款申请解锁后不作废,按 unlocked 锁记录留痕区分「金蝶删单/解锁」与普通推送失败
kingdeeBillLockService.fillUnlockMarks(KingdeeLockConstant.SOURCE_PAY_APPLY, rows, lockMap);
}
@Override
@@ -1,6 +1,7 @@
# 金蝶单据锁定与解锁业务流程(lockBusiData 锁定 - unLockNotifyBusiData 回调 - 失败补偿 - 删单还原)
- 日期:2026-10-09(2026-10-10 变更:① lockBusiData 确认为 1对1 接口,不支持逗号拼接批量,锁定/补偿改为逐张调用;② 付款申请单纳入锁单范围并完成接线)
- 日期:2026-10-09(2026-10-10 变更:① lockBusiData 确认为 1对1 接口,不支持逗号拼接批量,锁定/补偿改为逐张调用;② 付款申请单纳入锁单范围并完成接线。
2026-10-15 联调修正:③ 回调方向与锁定相反,解锁定位改为「金蝶单号+我方单号」双条件反查(targetBillType 回调为空不参与匹配);④ 回调处理批量化去 N+1;⑤ 付款申请 DELETE 还原链路实测通过;⑥ 解锁终态页面标识通用方案(KingdeeUnlockMark))
- 范围:正式结算单(应付 A102-2 / 应收 A302-2)、开票申请单(A303-1)、付款申请单(A103-1 应付下推 / A103-3 直接保存,2026-10-10 新纳入)推送金蝶成功后的单据锁定、金蝶解锁/删单回调、锁定失败定时补偿
- 设计文档:`docs/superpowers/specs/2026-10-09-kingdee-bill-lock-unlock-design.md`
- 锁记录表:`blade_kingdee_bill_lock`(一行 = 一张金蝶目标单)
@@ -46,31 +47,71 @@
### 2. 金蝶侧解锁回调(金蝶 → WLTMS)
金蝶在星瀚侧对单据解锁或删单后,回调 `POST /kingdee-bfi/unLockNotifyBusiData`(`UNLOCK`;targetBillNo 兼容单张/逗号拼接):
金蝶在星瀚侧对单据解锁或删单后,回调 `POST /kingdee-bfi/unLockNotifyBusiData`(`UNLOCK`)。
**回调方向与锁定相反(2026-10-15 真实报文确认)**:lockBusiData 由 WLTMS 发起(source=我方、target=金蝶),
unLockNotifyBusiData 由金蝶发起(source=金蝶、target=我方)。因此回调报文中:
`sourceBillNo`=金蝶单号(即锁记录 `targetBillNo`)、`targetBillNo`=WLTMS 单号(即锁记录 `sourceBillNo`)、
`targetBillType` **实测为空**(不参与匹配)。真实报文示例:
```json
{"lockAction":"UNLOCK","operationType":"DELETE","operator":"刘泉佳","sourceBillNo":"FKSQ-00000227",
"sourceBillType":"ap_payapply","sourceSystem":"XH","targetBillNo":"FKD2026101400001",
"targetBillType":"","targetSystem":"WLTMS","time":"2026-10-15 10:10:56"}
```
处理逻辑(`KingdeeBillLockServiceImpl.onUnlockNotify`):
```
校验:sourceSystem=XH、targetSystem=WLTMS、lockAction=UNLOCK(否则应答失败)
│
├─ operationType=SAVE(仅解锁,没删单)
│ └─ 锁记录 → unlocked,记录回调操作人/时间,解锁动作留痕
│
└─ operationType=DELETE(金蝶把单删了)
├─ 锁记录 → unlocked(同上)
└─ 按 targetBillType 反查 WLTMS 单据类型,分发还原:
├─ SETTLEMENT → 结算单作废(当前节点→已作废),
│ 同步信息还原推送前状态:kingdeeSyncStatus→unsynced、
│ 清 kingdeeBillNo/kingdeeBillId/syncedTime
│ 变更流水记"金蝶删单回调自动废除",操作人=金蝶报文 operator
├─ INVOICE_APPLY → 开票申请作废,kingdeeStatus→unsynced、清单据编号,
│ 刷新关联结算单开票状态,动作流水记 kingdee_delete
└─ PAY_APPLY → 付款申请复用 unlockBill(id, "金蝶删单回调")(ADR-0005 口径):
kingdeeStatus→生成失败、清 kingdeeBillId/kingdeeBillNo(原值日志留档),
不作废源单、不自动重推,人工核实后重推(A103-2 勾稽预检防重复推单)
校验:sourceSystem=XH、targetSystem=WLTMS、lockAction=UNLOCK、sourceBillNo/targetBillNo 非空(否则应答失败)
│
├─ 一次 IN 查询:target_bill_no in (回调sourceBillNo拆分, 支持逗号批量) and source_bill_no = 回调targetBillNo,
│ 按 id 降序取回,内存归并出各金蝶单号最新一条锁记录(避免逐张查询 N+1)
│
├─ 逐张匹配:无记录或非 locked → 幂等跳过(warn 留痕,重复通知防金蝶重试风暴)
│
├─ 命中 locked → 解锁状态批量落库:lock_status→unlocked、operationType、unlockTime、notifyOperator
│ (updateBatchById 一次批量更新,避免逐张 update N+1)
│
└─ operationType=DELETE(金蝶把单删了)→ 按「锁记录自身 targetBillType」反查 WLTMS 单据类型,分发还原:
├─ SETTLEMENT → 结算单作废(当前节点→已作废),
│ 同步信息还原推送前状态:kingdeeSyncStatus→unsynced、
│ 清 kingdeeBillNo/kingdeeBillId/syncedTime
│ 变更流水记"金蝶删单回调自动废除",操作人=金蝶报文 operator
├─ INVOICE_APPLY → 开票申请作废,kingdeeStatus→unsynced、清单据编号,
│ 刷新关联结算单开票状态,动作流水记 kingdee_delete
└─ PAY_APPLY → 付款申请复用 unlockBill(id, "金蝶删单回调")(ADR-0005 口径):
kingdeeStatus→生成失败、清 kingdeeBillId/kingdeeBillNo(原值日志留档),
不作废源单、不自动重推,人工核实后重推(A103-2 勾稽预检防重复推单)
应答:{updateCount: 处理张数, updateResultList: [1]}
```
重复回调幂等:单据已作废或锁记录已 unlocked → 该张跳过,不重复还原。
**事务语义**:整个回调处理在 `onUnlockNotify` 单一事务内(还原分支 `voidByKingdee`/`unlockBill` 均为 REQUIRED 传播加入同事务)——
任一张还原失败,**整批回滚**(包括已批量落库的解锁状态),应答失败触发金蝶重试,重试时按幂等口径从头重处理。
不存在"部分成功"中间态;若单张问题单阻塞整批回调,需人工处理后金蝶重试自动闭环。
**解锁后数据变化**(2026-10-15 实测口径):
锁记录(三类单据通用,`blade_kingdee_bill_lock`):
| 字段 | 变化 |
| --- | --- |
| lock_status | locked → unlocked(终态) |
| operation_type | 回调报文 operationType(SAVE-金蝶锁单(保存触发解锁,单据仍在)/ DELETE-金蝶删单) |
| unlock_time | WLTMS 处理时间(注意:非金蝶报文 time,金蝶侧操作时间以日志为准) |
| notify_operator | 回调报文 operator(金蝶侧操作人) |
业务单据(按类型):
| 业务单据 | 作废状态 | 金蝶同步信息 | 其他 |
| --- | --- | --- | --- |
| 正式结算单(SETTLEMENT) | approval_status → voided(已作废,终态),当前节点→已作废 | kingdeeSyncStatus → unsynced,清 kingdeeBillNo/kingdeeBillId/syncedTime | 变更流水记"金蝶删单回调自动废除",操作人=金蝶报文 operator |
| 开票申请(INVOICE_APPLY) | approval_status → voided(作废) | kingdeeStatus → unsynced,清单据编号 | 刷新关联结算单开票状态;动作流水记 kingdee_delete |
| 付款申请(PAY_APPLY) | **不作废**,approval_status 不变 | kingdeeStatus synced/paying → failed(仅这两态可变,幂等守卫),清 kingdeeBillId/kingdeeBillNo(原值日志留档) | 不自动重推,人工核实后重推;页面靠解锁标识区分金蝶删单与推送失败 |
### 3. 锁定失败补偿(定时兜底,不依赖人工)
```
@@ -100,7 +141,7 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked
│ │
│ ├─ 锁定成功:locked(冻结,不可动)
│ │ │
│ │ ├─ 金蝶解锁(SAVE):unlocked,流水留痕
│ │ ├─ 金蝶锁单(SAVE):unlocked,流水留痕
│ │ └─ 金蝶删单(DELETE):作废 + 同步信息还原 unsynced
│ │ (结算明细/付款/开票关联的后续动作
│ │ 由各自前置校验自然拦截重推)
@@ -113,8 +154,9 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked
1. **先占位再锁**:锁记录落库先于远程调用,补偿任务扫 `failed` 就是完整的待重试集合,不漏单
2. **锁失败不回滚推送**:金蝶单已生成,本地回滚只会制造断链;代价是短暂"已推未锁"窗口,由补偿任务闭合
3. **1对1逐张锁**:lockBusiData 单次仅支持一个源单号对一个金蝶单号,逐张调用互不连坐;成败结果 updateBatchById 批量落库,避免逐张 update
4. **回调幂等**:按 targetBillType+targetBillNo 找最新 locked 记录,找不到/已 unlocked 直接跳过应答成功,防金蝶重试风暴
5. **DELETE 还原口径**:作废源单 + 清金蝶同步信息 + 关联状态刷新;作废是终态,重推需新单重走流程(口径待业务联调确认)
4. **回调幂等**(2026-10-15 修正):回调方向与锁定相反,按「金蝶单号(回调 sourceBillNo)+ 我方单号(回调 targetBillNo)」双条件找最新 locked 记录(targetBillType 回调为空不参与匹配),找不到/已 unlocked 直接跳过应答成功,防金蝶重试风暴;
批量回调一次 IN 查询 + updateBatchById 批量落库,无逐张 N+1
5. **DELETE 还原口径**:作废源单 + 清金蝶同步信息 + 关联状态刷新(结算单/开票申请);付款申请不作废源单,走生成失败+人工重推(2026-10-15 联调实测通过);作废是终态,重推需新单重走流程
6. **前端锁状态展示**(2026-10-10 新增):结算单/开票申请列表与详情 VO 填充 `kingdeeLockStatus`/`kingdeeLockFailReason`/`kingdeeTryCount`
(后端 `mapActiveLocks` 每页一次批量查锁表,无 N+1;unlocked 终态不返回即 null=无锁/已解锁)。
前端渲染:`locked` 红 tag「金蝶锁定中」并禁用编辑/作废(后端守卫是权威);`failed` 且 `tryCount<5` 橙 tag「锁定失败·补偿中」tooltip 显示原因;
@@ -123,6 +165,12 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked
DELETE 还原不作废源单,复用既有 `unlockBill`(ADR-0005:生成失败+清编号内码+人工重推),与 A103-2 反审核删除兜底同口径;
推送结果即时回填 kingdeeBillNo(`requireBfiBillIdAndFillNo`),推送事务提交后即以该单号 `lockAfterPush`;
锁定失败摘要拼入「同步金蝶」返回值(与开票申请同款),页面直接可见
8. **解锁终态页面标识**(2026-10-15 新增,通用方案):付款申请解锁后不作废(仅 kingdeeStatus=failed),与推送失败无法区分;
结算单/开票申请虽变已作废,但看不出是金蝶删单导致。统一方案:
VO 实现 `KingdeeUnlockMark` 展示契约(解锁时间/操作人/动作名称三字段),
`KingdeeBillLockService.fillUnlockMarks` 对无活跃锁的行一次 IN 反查最新 unlocked 锁记录(`mapUnlockedLocks`)统一回填;
前端三个页面(付款申请/开票申请/正式结算单)金蝶锁状态列渲染:`kingdeeUnlockTime` 非空 →
「金蝶删单」红 tag /「金蝶锁单」橙 tag,tooltip 显示操作人+时间;后续单据(如收票单)接入锁单时 VO 实现契约即可复用
## 五、代码落点速查
@@ -131,6 +179,7 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked
| 锁单 Feign/DTO/应答 VO | `blade-bfi-api`:`BfiBillLockDTO`、`BfiBillLockResultVO`、`IBfiClient.lockBusiData`、`IBfiService.lockBusiData` |
| 锁类型常量与映射 | `transport/constant/KingdeeLockConstant`(PAY_APPLY/ap_payapply_oth_BT_S 已进 TARGET_TO_SOURCE_MAP) |
| 锁状态展示填充 | `IKingdeeBillLockService.mapActiveLocks`(批量查活跃锁)+ `FormalSettlementServiceImpl/InvoiceApplicationServiceImpl/PaymentApplicationServiceImpl.fillKingdeeLockStatus`(列表/详情填充)+ 三个 VO 的 `kingdeeLockStatus/kingdeeLockFailReason/kingdeeTryCount` 字段 |
| 解锁终态标识(2026-10-15) | 展示契约 `KingdeeUnlockMark`(三 VO 实现)+ `IKingdeeBillLockService.mapUnlockedLocks/fillUnlockMarks`(批量反查 unlocked 留痕并回填);VO 字段 `kingdeeUnlockTime/kingdeeUnlockOperator/kingdeeUnlockOperationName`;前端 `payment-application.vue`/`invoice-application.vue`/`formal-settlement-table-panel.vue` 的 `lockTag` 渲染金蝶删单/解锁标签 |
| 锁记录实体/服务 | `KingdeeBillLock`、`IKingdeeBillLockService` + `KingdeeBillLockServiceImpl` |
| 回调端点 | `KingdeeBfiPublicController`:`POST /kingdee-bfi/unLockNotifyBusiData`(PERMIT_ALL + TenantIgnore) |
| 补偿定时任务 | `KingdeeBillLockRetryTask`(`thirdParty.bfi.billLock.retryEnabled`,默认每5分钟) |
@@ -150,4 +199,5 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked
Nacos 实际配置已核对一致;环境级 blade-dev/test/prod.yaml 无 secure 段覆盖。
联调验证:服务启动后无令牌 POST 应返回业务参数校验错误(JSON),返回 401「请求未授权」即白名单未生效
4. 与金蝶确认 WLTMS 单据类型编码(SETTLEMENT/INVOICE_APPLY/PAY_APPLY)与 DELETE 还原口径
(付款申请 DELETE 不作废源单、走生成失败+人工重推,与 A103-2 反审核删除兜底同口径,待业务确认)
(付款申请 DELETE 不作废源单、走生成失败+人工重推,与 A103-2 反审核删除兜底同口径;
2026-10-15 付款申请链路已联调实测通过:回调方向反转定位、DELETE 还原、解锁标识均验证 OK)
@@ -0,0 +1,641 @@
# 金蝶(星瀚)BFI 全链路梳理:前端入口 → 后端链路 → 定时任务(业务域汇总 + 操作手册)
> 整理日期:2026-10-14
> 代码基线:`tms-api` 当前 master(`805ca9d3`,2026-10-13),全量按当前代码重新核实(上一版总纲基线为 09-24 `50573e7`)。
> 本文替代《2026-09-27-金蝶(星瀚)BFI接口调用全链路梳理(按业务域汇总·更新版)》作为新总纲:
> - **组织方式改为三层**:每个业务域按「前端入口(页面/按钮)→ 后端链路(触发/顺序/口径/落库)→ 定时任务」梳理;
> - 新增**操作手册**(第 11 节):按业务时序给出端到端操作步骤与异常处理操作;
> - 自 09-27 以来代码有 40+ 提交(09-28~10-13),**收票回写缺陷已修、调试残留已清、新增锁单/解锁、审批自动同步、failed 重推、票池镜像读回切、全票池定时同步、A801-5 黑名单、A801-2/A902 附件接线、收款认领重构**等,接口清单、分录口径、状态机均有实质变化,全文按当前代码重写。
---
## 目录
- [0. 本次更新要点(09-27 → 10-14)](#0-本次更新要点09-27--10-14)
- [1. 集成总览与公共机制](#1-集成总览与公共机制)
- [2. 业务域一:客商档案(A902-1 / A902-2 / A801-5 黑名单)](#2-业务域一客商档案a902-1--a902-2--a801-5-黑名单)
- [3. 业务域二:财务应付与付款(A102-2 / A102-4 / A103-1 / A103-3 / A103-2 / A103-4)](#3-业务域二财务应付与付款)
- [4. 业务域三:财务应收与开票(A302-2 / A302-4 / A303-4 / A303-1 / A303-5)](#4-业务域三财务应收与开票)
- [5. 业务域四:收票与金蝶票池(A801-1 / A303-2 / A302-6 / A801-3 / A801-2 / A902 附件)](#5-业务域四收票与金蝶票池)
- [6. 业务域五:收款认领(A301-2 / A301-3 / A301-1)](#6-业务域五收款认领a301-2--a301-3--a301-1)
- [7. 业务域六:公共取数(A801-4 汇率)](#7-业务域六公共取数a801-4-汇率)
- [8. 全景:接口依赖与先后顺序](#8-全景接口依赖与先后顺序)
- [9. 状态机汇总](#9-状态机汇总)
- [10. 定时任务与配置开关汇总](#10-定时任务与配置开关汇总)
- [11. 操作流程手册](#11-操作流程手册)
- [12. 风险与待确认清单](#12-风险与待确认清单)
---
## 0. 本次更新要点(09-27 → 10-14)
| # | 变化 | 说明 |
|---|---|---|
| 1 | **⚠️ 旧总纲缺陷清单大面积清零** | A801-3 定时回写防重键已修复(改按收票登记 id,`7b98e33d` 系列);`if(true)` 调试组合块(强制物料/随机取物料)**已全部删除**;客商定时 KS000067 硬编码**已移除**;客商银行 `bankNumber` 硬编码已改真实联行号;A103-3 `e_bebank_number` 改取客商收款账户联行号(与 A103-1 口径统一) |
| 2 | **★新增横切机制一:单据锁定与解锁(10-09/10)** | BFI 新增 `lockBusiData`(出站锁单)与 `POST /kingdee-bfi/unLockNotifyBusiData`(金蝶回调,免登放行)。正式结算单/开票申请/付款申请推送成功后联带锁金蝶单据;金蝶侧删单回调还原本地状态;锁失败定时+手动补偿;列表回显锁状态。专项文档:`2026-10-09-金蝶单据锁定与解锁业务流程….md` |
| 3 | **★新增横切机制二:审批通过自动同步(10-10)** | `KingdeeApprovalSyncListener`(AFTER_COMMIT 事件)监听 4 域审批通过事件,自动调各自 `syncKingdee`;4 个 Nacos 开关默认 false;失败统一落 `failed`,审批接口回显警告文案;MK 流程审批回调统一改走 `service.approve`,OA 路径同样触发。专项文档:`2026-10-05-审批通过自动同步金蝶设计….md` |
| 4 | **★新增横切机制三:failed 单据定时重推 + 推送失败统一落 failed(10-10)** | `KingdeeAutoSyncRetryTask` 定时重推 4 域 failed 单据(不扫 unsynced);结算/开票链路异常也落 `failed`(此前只回滚保持 unsynced),旧总纲 11.2-#3 缺陷关闭 |
| 5 | **★票池架构回切:镜像读 + 全票池定时同步(10-05/10-08)** | 选票接口改读本地镜像表(本地真分页);票号精确查未命中才窄查 A801-1 回源;新增 `InvoiceReceiptKingdeeSyncTask` 全票池瘦身定时同步(invoiceTypes 白名单下推远端、2.5 万窗口硬上限二分时间片、绑定回填);结算单认领确认补上**保存重查闸门**(`assertInvoicesClaimable`)。旧总纲「实时回源、无定时」口径作废 |
| 6 | **★分录拆分口径重写(10-13)** | 应付/应收/收票分录改为**按 货物 → 费用大类(装卸/运输)→ 税率 三级拆分**,一车多货再按货物拆;0 元费用项/货物行跳过;物料编码改 Nacos 配置 `thirdparty.bfi.material.*`(`BFI_MATERIAL_NUMBER_MAP` 常量与 `blade_kingdee_material` 表已废弃);**计量单位当前不送**;`specification`(规格型号)字段本期不传。专项文档:`2026-10-13-金蝶应付应收收票单明细分录拆分与物料映射逻辑梳理.md` |
| 7 | **★收款认领链路重构(09-28~10-01)** | 认领本地化:先本地认领落库,**推送改定时+手动双入口按流水归集**(`ReceiptClaimPushTask`,旧总纲「全部手动、无定时」作废);`awrp_src_code`=分摊结算单号下划线拼接(重试复用幂等);分录单条、`e_corebillno` 传**结算单金蝶内码**(缺内码拦截不推);已推送认领禁止作废;A301-1 回写以真实收款单号覆盖占位单号 |
| 8 | **★A801-5 黑名单查询启用(10-01/10-08)** | 客商提审时按发票纳税人识别号(去重大写)调 A801-5;A 类命中硬拦截,B 类默认放行告警(可配置拦截),**查询异常上抛阻断提审** |
| 9 | **附件链路接线(09-27~10-05)** | A902 附件上传:正式结算/开票申请/付款申请/收票 4 条链路推送成功后联带上传(失败不污染主单,重推补传);A801-2 附件下载:前端统一走 `GET /invoice-receipt/invoice-pool/attachment/download` 流式代理,PDF 版式(`taxPdfUrl`/`pdfurl`)优先,OFD 按 `fileType=4` 识别 |
| 10 | **开票申请 A303-1 口径更新(09-30~10-11)** | 上游来源单据 `awrp_src_con_no`=首张结算单金蝶应收单防重键;特殊票种:纯货物运输行送 E04 否则 00;E04 随单报送货物运输单据体(车牌/起运/到达/货物/种类去重归并);`hsbz` 统一 0(不含税);旅客运输/代收车船税单据体 DTO 已建未接 |
| 11 | **组织编码改取金蝶部门编码(10-01)** | 应付/应收/开票的组织字段取 `SysCache.getDept().getKingdeeDeptCode()`,为空直接拦截(需在部门档案维护金蝶组织编码) |
| 12 | **定时任务加 single-flight 分布式锁(09-28/29)** | 全部金蝶定时任务经 `KingdeeSyncLockSupport.runSingleFlight`(Redis 锁,键含租户 id),多副本与手动触发共用一道闸;付款结果回写开关在 Nacos 样例中已显式开启,其余任务默认关闭 |
| 13 | **付款申请「消失解锁」兜底(09-29)** | A103-2 连续 2 轮查询不到已生成单(Redis 计数,TTL 2100s)→ 自动解锁置 `failed`,可重推 |
---
## 1. 集成总览与公共机制
### 1.1 调用架构(前端 → 后端 → 金蝶)
```
tms-erp-web(Vue3,src/views/** + src/api/**)
│ HTTP(网关按服务名路由 /blade-transport/**)
▼
blade-transport(Controller → ServiceImpl;推送编排 + 状态落库 + 锁单 + 审批事件)
│ IBfiService(参数校验 + Redis 令牌缓存)
▼
blade-bfi-api(IBfiClient @FeignClient url=${thirdParty.bfi.baseUrl})
│ access_token(业务令牌) + Authorization(iPaaS 网关头,BfiRequestInterceptor 注入)
▼
iPaaS 网关(/gwzh/BFI,不直连金蝶)
▼
金蝶星瀚 /ierp/kapi/...
反向:金蝶删单 ──▶ POST /blade-transport/kingdee-bfi/unLockNotifyBusiData(免登 skip-url)
定时:blade-transport task/ 包 7 个任务(Spring @Scheduled,全库无 PowerJob/XxlJob 接线)
```
- 无 Feign 重试(NEVER_RETRY)、无 fallback;失败抛 `BfiException`,批量循环逐条 catch,单条失败不中断整批。
- 令牌:Redis `blade:third:bfi:token`,TTL=expires_in−120s;`setIfAbsent` 写入(已有缓存不覆盖)。
- 配置前缀两套并存(读同一批 Nacos 键):bfi-api `third-party.bfi.*`(baseUrl/认证/令牌),transport `thirdparty.bfi.*`(业务开关/口径/映射)。样例:`doc/nacos/third-party-api.yaml`。
### 1.2 接口清单总表(2026-10-14,含在用状态)
来源:`blade-bfi-api/.../feign/IBfiClient.java`(URL 可被 `thirdParty.bfi.*Url` 覆盖)。
| 操作码 | 星瀚端点 | 用途 | 业务侧在用状态 |
|---|---|---|---|
| A100-1 | POST /ierp/kapi/oauth2/getToken | 获取令牌 | ✅ 自动(每次调用前) |
| A100-2 / A100-3 | verifyToken / withdrawToken | 校验/撤回令牌 | ❌ 已实现无调用 |
| A102-2 | POST …/awrp/ap/ap_finapbill/save | 财务应付单保存 | ✅ 正式结算单(应付)推送 |
| A102-4 | GET …/awrp/ap/ap_finapbill/query | 财务应付单查询 | ✅ 预检 / 硬校验 / 批量预热 |
| A103-1 | POST …/awrp/ap/ap/ap_finapbill/push | 应付下推生成付款申请 | ✅ 付款申请推送(单分录) |
| A103-2 | GET …/awrp/ap/ap_payapply/query | 付款申请单查询 | ✅ 推送预检 + 状态回写 + 消失解锁计数 |
| A103-3 | POST …/awrp/ap/ap/ap_payapply/save | 预付款申请保存 | ✅ 预付款直推 |
| A103-4 | GET …/awrp/cas/cas_paybill/query | 付款处理查询 | ✅ 付款记录回写(含 voucher_json) |
| A301-1 | GET …/awrp/cas/cas_recbill/queryRecBill | 收款单查询 | ✅ 认领结果回写(手动) |
| A301-2 | GET …/awrp/cas/cas_claimannounce/query | 收款认领公告查询 | ✅ 流水同步(手动 + 定时认领推送前置重查) |
| A301-3 | POST …/awrp/cas/cas/cas_claimbill/claimbillPush | 收款认领结果推送 | ✅ 定时 + 手动双入口,按流水归集 |
| A302-2 | POST …/awrp/ar/ar_finarbill/save | 财务应收单保存 | ✅ 正式结算单(应收)推送 |
| A302-4 | GET …/awrp/ar/ar_finarbill/query | 财务应收单查询 | ✅ 预检 / 硬校验 / 定时勾稽纠偏 |
| A302-6 | POST …/awrp/ap/ap/ap_invoice/assign | 收票指定应付 | ✅ 收票推送第二步 |
| A303-1 | POST …/awrp/sim/sim_original_bill/save | 开票申请单新增 | ✅ 按发票张次逐张推送 |
| A303-2 | POST …/awrp/ap/ap_invoice/cusBatchSave | 收票单批量保存 | ✅ 收票登记推送 |
| A303-4 | GET …/awrp/sim/sim_original_bill/query | 开票申请单查询 | ✅ 批量 billno 幂等预检 / 附件补传反查星瀚内码 |
| A303-5 | POST /ierp/kapi/app/sim/openApi | 开票发票查询(票易通) | ✅ 定时回写开票结果 |
| A801-1 | POST …/awrp/rim/rim_invoice/query | 全票池发票查询(rim 契约) | ✅ 定时全量同步 + 票号回源 + 保存/认领闸门 + 附件回源 |
| A801-2 | POST …/awrp/bosext/attachment/download | 进项发票文件下载 | ✅ **已接线**:附件查看/下载流式代理 |
| A801-3 | GET …/awrp/ap/ap_invoice/apinvoicequery | 收票单查询 | ✅ 推送预检 + 定时回写(防重键已修复=登记 id) |
| A801-4 | GET …/awrp/base/bd_exrate_tree/Forexquery | 汇率查询 | ✅ 结算页面取汇率 |
| A801-5 | POST …/awrp/bdm/bdm_blacklist_config/blacklistquery | 黑名单查询 | ✅ **新增启用**:客商提审校验 |
| A902 | POST /ierp/kapi/v2/frame/attachment/uploadFile | 附件上传 | ✅ **已接线**:4 条推送链路联带上传 |
| — | POST /API_lockAction_Router | lockBusiData 单据锁定 | ✅ 推送成功后锁单({updateCount,updateResultList} 应答) |
| — | (回调)POST /kingdee-bfi/unLockNotifyBusiData | 解锁/删单通知(unLockNotifyBusiData) | ✅ 金蝶回调本系统(免登放行) |
> 仍未实现:A901-1 供应商查询、A913 项目、A101-1/2 合同、A102-1/A302-1 暂估单(合同/项目走合同合规系统)。
> 查询时间窗约束:A103-2/A103-4/A102-4/A302-4/A303-4/A301-1 ≤ 1 个月;A902-1 ≤ 6 个月;A301-2 无时间窗参数(按认领人+分页,时间窗口取 `thirdParty.bfi.claimAnnounce.months` 默认 6)。
### 1.3 防重键(候选键)总表(2026-10-14 口径)
金蝶侧按「来源系统 + 来源单据号」幂等(`awrp_src_sys` + `awrp_src_code`)。组装:`ExternalSrcCodeSupport.buildSrcCode`(下划线拼接、去空白、去重保序)。
| 金蝶单据 | awrp_src_code | srcType | 分录键 | 预检接口 |
|---|---|---|---|---|
| 财务应付单 A102-2 | `正式结算单号_结算明细单据号1_…` | TMS财务应付单 | 按 货物-费用大类-税率 拆分行顺序生成 | A102-4 |
| 财务应收单 A302-2 | 同应付口径 | TMS财务应收单 | 同应付口径 | A302-4 |
| 付款申请 A103-1 | `paymentNo` | 配置 `0KzXs` | 固定 `paymentNo-1`(单分录);勾稽键 `awrp_finapbill_entryid`=主结算单应付防重键(本地组装) | A103-2 |
| 预付款申请 A103-3 | 付款申请号口径 | `0KzXs` | `paymentNo-1` | A103-2 |
| 开票申请 A303-1 | billno:单张=`application_no`,多张=`application_no-张次序号`;`awrp_src_con_no`=首张结算单应收防重键 | `0KzXs` | 开票商品行 id | A303-4(批量 billno) |
| 收票单 A303-2 | **收票登记 id** | TMS收票登记 | 与 A102-2 分录同源同口径 | A801-3(预检与回写同键) |
| 收款认领 A301-3 | **分摊结算单号下划线拼接**(重试复用同一编码)★改 | WLTMS | 单条:seq/e_corebillno=第一张结算单**金蝶内码** | A301-1(sourcebillnumber 匹配) |
| 客户 A902-2 | `number`=TMS 客商编号(KS+6位);`societycreditcode` 为金蝶侧候选键 | awrp_datasourcesys=WLTMS | — | A902-1 |
### 1.4 公共档案取数(应付/应收/收票复用 `KingdeeApBillSyncSupport`)
| 取数项 | 规则(当前代码) |
|---|---|
| 组织 org/payorg/recorg | 合同/申请部门 → `SysCache.getDept().getKingdeeDeptCode()`(**金蝶部门编码**,为空拦截)★改 |
| 币种 | 合同 `settlement_currency`,RMB→CNY |
| 汇率 exchangerate/exratedate | 结算单页面维护值(A801-4 回填),未维护不送 |
| 往来户 asstact_number | 名称 → 本地 `blade_customer_archive` 匹配取 `customer_code`,查不到报错拦截 |
| 往来类型 asstacttype | **固定 `bd_customer`**★改(按合同类别区分供应商的逻辑已注释停用) |
| 物料编码 | 运输类按 运输方式/国际/装卸 → Nacos `thirdparty.bfi.material.domestic.{railway,highway,waterway,aviation}` / `international` / `loading` 映射(默认 XDWL1101000x / Item-00001081 / XDWL11002004);运输方式未配置直接拦截 |
| 计量单位 | **当前不送**(DTO 字段保留,无 set 调用) |
| 金额 | 含税=结算金额;不含税:明细行有、手工合计费用项行不传 |
| 数量/单价 | 运输类=运单运输量(兜底 1;同税率拆多行每行都取运输量);装卸/杂费=1;单价=金额/数量反算 |
| 0 元行 | 费用项 signum()==0 与 0 元货物行**整行跳过**★新增 |
| 运输货物/车号 | 明细行传 `awrp_transport`(货物名称)/ `awrp_lpn`(车号);合计费用项行不传 |
### 1.5 三大横切机制(新总纲新增,读业务域前先看)
#### ① 审批通过自动同步(`KingdeeApprovalSyncListener`)
```
业务审批通过(页面「通过」或 MK/OA 流程回调,统一走 service.approve)
▶ 事务提交后 AFTER_COMMIT 发布 KingdeeBillApprovedEvent
▶ KingdeeApprovalSyncListener.onBillApproved(@TransactionalEventListener,fallbackExecution=true)
▶ 按 4 个开关分别调用 syncKingdee(id):
settlement → FormalSettlementServiceImpl.syncKingdee (A102-2 / A302-2)
payment → PaymentApplicationServiceImpl.syncKingdee (A103-1 / A103-3)
invoiceApply→ InvoiceApplicationServiceImpl.syncKingdee (A303-1 按张次)
receipt → InvoiceReceiptServiceImpl 推送 (A303-2 + A302-6)
▶ 失败:独立事务落 failed + ThreadLocal 失败原因
▶ 4 个审批 Controller 统一 pollSyncFailure() 拼警告返回前端:"审批通过,但自动同步金蝶失败:xxx"
```
- 开关(默认全 false):`thirdParty.bfi.autoSync.settlement / payment / invoiceApply / receipt`。
- 客商档案不走事件机制:`CustomerArchiveController.approve` 内直接 `syncKingdee(id)`(失败不阻断审批)。
- 推送成功后各链路在**同一方法内继续执行 lockAfterPush** 锁金蝶单据(见②)。
#### ② 单据锁定与解锁(`KingdeeBillLockService` + `KingdeeBfiPublicController`)
```
推送成功 ─▶ lockAfterPush(先落 blade_kingdee_bill_lock failed 占位 → lockBusiData → 翻 locked)
锁失败 ─▶ KingdeeBillLockRetryTask 定时补偿(cron thirdParty.bfi.billLock.retryCron,默认每5分钟,开关默认 false)
+ 手动端点 POST /formal-settlement/retry-kingdee-bill-lock(通用,扫 failed 且 try_count<5)
金蝶删单 ─▶ POST /kingdee-bfi/unLockNotifyBusiData(免登,@TenantIgnore)
├─ SETTLEMENT:作废结算单 + kingdee_sync_status 回 unsynced、清 kingdee_bill_no/id
├─ INVOICE_APPLY:作废开票申请 + 同上还原
└─ PAY_APPLY:不作废源单,解锁置 failed、清 kingdee_bill_id/no(可重推)
本地拦截 ─▶ 付款申请/开票申请作废前 checkEditable:locked 即抛「该单据已被金蝶锁定」
(正式结算单作废未接 checkEditable,靠列表锁状态前端禁用)
列表回显 ─▶ mapActiveLocks() 填充 VO kingdeeLockStatus(locked/failed),前端展示「金蝶锁定中/锁定失败」并禁编辑
```
- 收票单本期**不锁**(`KingdeeLockConstant` 映射预留)。
- 付款申请另有**消失解锁**:A103-2 连续 2 轮查询不到已生成单(Redis 计数 TTL 2100s)→ 自动解锁置 failed。
#### ③ 定时任务 single-flight 分布式锁(`KingdeeSyncLockSupport`)
- 所有金蝶定时任务经 `runSingleFlight`:RedisLockClient 零等待可重入锁,锁键=前缀+租户 id(无用户上下文用 default);`blade.lock.enabled=false` 降级直跑。
- 手动端点与定时任务共用同一把锁——**手动触发时若定时正在跑会直接返回**(防重复回写)。
---
## 2. 业务域一:客商档案(A902-1 / A902-2 / A801-5 黑名单)
**定位**:全局数据前提。其他链路的往来户编码(`customer_code`)来自 `blade_customer_archive`;金蝶客户编码 = TMS 客商编号。
### 2.1 前端入口
| 页面 | 操作 | 后端端点 | 触发时机 |
|---|---|---|---|
| 客商档案(`/vehicle/customer-archive`) | 新增/编辑表单录统一社会信用代码(满 18 位防抖) | `GET /bfi/customer/query-by-credit-code`(A902-1) | 回填名称/法人/地址/经营范围等并锁定;金蝶状态 A/B 阻断保存 |
| 同上 | 行内「同步金蝶」链接(仅 approved 且 failed 显示) | `POST /customer-archive/sync-kingdee`(A902-2) | 失败人工重推 |
| 同上 | 行内「审批通过」 | `POST /customer-archive/approve` → 内部 `syncKingdee(id)` | **提审校验 A801-5 黑名单;通过后自动推送 A902-2** |
### 2.2 后端链路
**A902-1 客户查询(3 入口)**:前端回填 / 单条推送前复查(`syncKingdee` 先查一次,C→exist 终态、A/B→failed)/ 定时批量复查(50 信用代码/批翻页取全)。
**A801-5 黑名单(★新增,提审拦截)**:`CustomerArchiveServiceImpl.submitApproval` → 取客商各发票 `taxNo` 去重大写 → `queryBlacklist("1", taxNos)`(enable=1,pageSize 上限 2000,循环翻页取全)→ **A 类硬拦截**;B 类默认放行告警(`thirdparty.bfi.blacklist.blockRiskWarning=true` 时拦截);其他类别放行告警;**查询异常上抛阻断提审**。
**A902-2 客户保存(3 入口收敛 `pushCustomerToKingdee`)**:审批通过自动(失败不影响审批)/ 人工重推(synced/exist 幂等返回)/ 定时重推(pending=approved 且 none/failed)。先 A902-1 复查(金蝶已有 C 直接置 exist 跳过)再保存。只推新增建档(审批后变更不重推)。参数要点:`entry_groupstandard` 按 `customerKind` internal→1101 / 其余→1102;`entry_bank` 取 `blade_customer_receipt_account`(账号/开户行/CNY/默认 + **真实联行号 cnaps_code**,硬编码已删);`bizfunction` 固定 `1,2,3,4`。
**定时附加职责**(`CustomerKingdeeSyncTask`):synced/exist 且启用客商复查 C→exist;金蝶侧停用 → 联动停用 TMS 档案 `status=2`(单向)。
**响应存储**:`kingdee_status / kingdee_fail_reason / kingdee_sync_time`。
**金蝶侧效果**:
![金蝶客户列表收到 A902-2 推送的客商(编码=TMS 客商编号 KS000073,客户基本分类标准 1101 内部客户/1102 外部客商)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a902-金蝶客户接收.png)
![TMS 客商档案与金蝶客户档案对照:编码一致(KS000060)、以统一社会信用代码为两侧关联键](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a902-客商编码对照.png)
---
## 3. 业务域二:财务应付与付款
### 3.1 链路总览
```
环节一 正式结算单(应付 Tab)审批通过自动同步 / 手动「同步金蝶」
A102-4 预检 ▶ A102-2 保存应付单 ▶ 回写 kingdee_bill_no + synced ▶ lockBusiData 锁单
环节二 付款申请审批通过自动同步 / 「批量同步」
A102-4 硬校验(逐关联结算单应付已生成)▶ A103-2 幂等预检
▶ A103-1 下推(预付款走 A103-3)▶ 回写 kingdee_bill_id/no + synced ▶ lockBusiData 锁单
环节三 PaymentKingdeeResultTask 定时(或手动「同步付款结果」)
A103-2 状态回写(synced→paying→paid/closed;failed 勾稽纠偏;连续2轮查不到→消失解锁置failed)
▶ A103-4 付款处理查询 ▶ 付款记录落库(含 voucher_json 凭证/银行流水/电子回单)
```
**金蝶侧效果**:
![金蝶财务应付单 AP-202609-800044 与 TMS 正式结算单对照:外部系统单据号=防重键(WLTMS_FJS2026092400001_YF20260912…),应付金额、明细行(物料/车号/运输货物/数量)逐行对应](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a102-应付单对照.png)
> **前置说明(预结算单)**:预结算单(PreSettlement)**不直接推送金蝶**——无金蝶字段与调用,`KingdeeBillApprovedEvent` 也不覆盖它。与金蝶的关联仅三处:
> ① 非 RMB 预结算在页面经 A801-4 带出汇率随单保存,**汇率不自动结转到正式结算单**——A102-2/A302-2 推送取的是正式结算单自身维护的 `exchange_rate/exchange_rate_date`(`KingdeeApBillSyncSupport:272-277`),转正式后须重新维护;
> ② 进度预付付款申请可关联预结算单(`payment_application.pre_settlement_id`),A103-3 汇率日期优先取关联结算单(正式→预结算)维护值,兜底申请日期;
> ③ A103-2 付款 paid 回写时联动履约:回写预结算垫资 `blade_pre_settlement_advance`(paidAmount 整额置满、billStatus=paid、kingdeeAdvanceNo=金蝶付款单号)并刷新预结算汇总与正式结算来源快照。
> 预结算「预开票」金蝶推送方案(2026-09-29)**未实现**;预结算审批走 MK 流程(`MkProcessBizHandlers.PreSettlementHandler`),直接置 approved、不发布金蝶事件。
### 3.2 前端入口
| 页面 | 操作 | 后端端点 | 说明 |
|---|---|---|---|
| 正式结算单(`/settlement/formal-settlement`,应付/应收 Tab) | 工具栏「同步金蝶」(单选一条) | `POST /formal-settlement/sync-kingdee?id=` | 30s 超时;成功回写金蝶单号 |
| 同上 | 行内「审批通过」 | `POST /formal-settlement/approve` | 开关开启时**自动同步**;`data=false` 前端提示失败原因 |
| 同上 | 列表「金蝶锁定」标签列 | 无调用(后端 mapActiveLocks 回显) | locked 红色标签禁编辑/作废 |
| 付款申请(`/payment/payment-application`) | 工具栏「批量同步」 | `POST /payment-application/sync-kingdee-batch`(ids) | 整批预热→逐单独立事务,单条失败不中断 |
| 同上 | 工具栏「同步付款结果」 | `POST /payment-application/sync-kingdee-result` | 手动触发 A103-2/A103-4 回写 |
| 同上 | 行内「审批通过」 | `POST /payment-application/approve` | 开关开启时自动下推+锁单 |
| 同上 | 单笔「同步金蝶」 | `POST /payment-application/sync-kingdee?id=` | 端点在;**前端 `handleSyncRow` 未绑定模板(死代码)**,单笔入口实际只有审批自动同步 |
### 3.3 后端要点(当前口径)
- **A102-2/A302-2 分录**:结算明细行按「货物→费用大类→税率」拆分 + 手工合计费用项(manualFlag=1,每行一条,0 元跳过);明细行带运输货物/车号;调试覆盖块已删。
- **A103-1 下推**:单分录 `paymentNo-1`;`e_settlementtype_number` 按付款方式映射(汇票→012-02,其余→02);`e_bebank_number`=客商收款账户联行号(未维护拦截);`awrp_finapbill_entryid`=主结算单应付防重键(本地组装;⚠️ 支撑类注释仍写「A102-4 反查」,注释与代码不一致,见 12 节)。
- **A103-3 预付款**:`e_bebank_number` 同样取客商收款账户联行号(★已与 A103-1 统一);project 取项目档案真实编码(无则 999)。
![TMS 付款申请(FKD2026092400001,申请付款 195.11)与金蝶付款申请单(FKSQ-00000199)对照:核销金额、往来户/往来账户(收款账号)、结算方式(银行转账)逐项对应;金蝶侧由 A103-1 下推自动生成付款计划](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a103-付款申请对照.png)
![预付款对照:TMS 进度预付申请(240.12)下推金蝶生成「其他付款申请」FKSQ-00000219(进项货付款申请),分录预付款金额、往来户、结算方式对应;附件区可见 A902 联带上传的文件](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a103-3-预付款对照.png)
- **A103-2 回写状态映射**:`Alreadypay`→paid(终)>`billstatus=D`→closed(终)>`Inpayment`→paying>其余 synced;`entry[].e_paidamt` 汇总→paid_amount(缺失且 paid 兜底=申请金额);paid 联动履约(预结算垫资/结算付款记录);**failed 纳入勾稽纠偏**(查到即纠正 synced);**查不到→消失解锁计数**(仅已生成/付款中参与,连续 2 轮→解锁置 failed)。
- **A103-4 付款记录**:按付款组织编码分组查询(`org_number` 优先取 A103-2 返回值),窗口最近 1 个月,`sourcebillid`==kingdee_bill_id 优先、`sourcebillnumber` 兜底;upsert `blade_payment_application_record`(actpayamt/paid_date/整单 JSON→voucher_json);清理 MOCK-PAY- 模拟记录。
- **A902 附件**:推送成功事务提交后联带上传(`KingdeeAttachmentSyncSupport.syncUserAttachments`),失败不污染主单状态,重推补传。
- **异常落库**:手动/自动同步失败统一独立事务落 `failed` + 原因(截 500 字),由「手动重推/failed 重推任务」续推。
---
## 4. 业务域三:财务应收与开票
### 4.1 链路总览
```
环节一 正式结算单(应收 Tab)审批通过自动同步 / 手动「同步金蝶」
A302-4 预检 ▶ A302-2 保存应收单 ▶ 回写 kingdee_bill_no + synced ▶ lockBusiData 锁单
环节二 开票申请审批通过自动同步 / 工具栏「批量同步」(逐单)
A302-4 硬校验(应收已生成)▶ A303-4 批量 billno 幂等预检(部分张次已生成可断链续推)
▶ A303-1 按发票张次逐张新增(E04/货物运输单据体/A902 附件)▶ 张次+申请单回写 ▶ lockBusiData 逐张锁单
环节三 PaymentKingdeeResultTask 定时(或手动端点)
A302-4 勾稽纠偏(failed→synced / 补单号)
A303-5 开票发票查询(serialNo 按 application.kingdee_bill_no 逗号拆分)▶ 发票落库(含文件地址)▶ invoiced(终态)
```
### 4.2 前端入口
| 页面 | 操作 | 后端端点 | 说明 |
|---|---|---|---|
| 正式结算单(应收 Tab) | 同应付:同步金蝶 / 审批通过 / 锁状态列 | 同 3.2 | `settlementType=receivable` 路由 A302-2 |
| 开票申请(`/payment/invoice-application`) | 工具栏「批量同步」 | `POST /invoice-application/sync-kingdee?id=`(逐单 Promise.all) | 仅审批通过单 |
| 同上 | 行内「审批通过」 | `POST /invoice-application/approve` | 开关开启时自动按张次推送 |
| 同上 | 锁定状态列 | 无调用 | 「金蝶锁定中」禁编辑/作废 |
| 正式结算单编辑器「开票发票」区 | 查看/下载发票文件 | 金蝶发票云直链(开票结果回写的 invoicePdfFileUrl 等,不走 A801-2 代理) | 10-02 起落库发票文件地址 |
### 4.3 后端要点(当前口径)
**金蝶侧效果**:
![金蝶财务应收单 AR-260924-012021 与 TMS 正式结算单对照:应收金额、费用合计对应;明细行物料编码/车号/运输货物/数量逐行对应](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a302-应收单对照.png)
![开票申请张次拆分对照:TMS 开票内容第1张/第2张(各含商品行)分别生成金蝶票易通开票申请单——购销方、不含税金额、未关联金额(¥4,386)逐项对应](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a303-1-开票张次对照.png)
- **张次拆分**(`blade_invoice_application_sheet`):每张发票张次推一张金蝶开票申请单;billno=单张取 application_no、多张取 `-张次序号`;分录=本张商品行(按 invoice_sheet_id 分组)。
- **A303-1 头字段口径(★更新)**:`invoicetype` 数电专票 08xdp / 数电普票 10xdp;`specialtype`:本张商品行**全部**为货物运输类(名称/分类含运费关键字)→ **E04**,否则 **00**;E04 随单报 `freights[]` 货物运输单据体(车牌 licenseplate/起运 startplace/到达 endplace/货物 transportgoods/运输方式 transporttype 字典映射,按「车牌×起运×到达×货物×种类」去重归并,空行剔除);`hsbz` 统一 **0**(不含税);`awrp_src_bt=ar_finarbill` + `awrp_src_con_no`=首张结算单金蝶应收防重键(金蝶按防重键校验源单,传编号会报源单不存在);`awrp_project_number`=999。旅客运输/代收车船税单据体 DTO 已建**未接入**。
- **A303-5 回写**:按销方税号分组(开票单位→客商档案税号)≤50/批;落 `blade_invoice_application_invoice`(serial_no/invoice_no/invoice_date/invoice_amount/raw_json + 发票文件地址);唯一键防重;有新增发票→invoiced 终态。发票结果不反写结算单(文件地址随发票记录供结算详情查看)。
- **A902 附件**:开票申请推送成功后联带上传(以本地 kingdeeBillId 定位金蝶单据,星瀚内码缺失时经 A303-4 反查回填)。
---
## 5. 业务域四:收票与金蝶票池
### 5.1 架构现状(★10-05 回切后终态)
```
【数据面】InvoiceReceiptKingdeeSyncTask 定时(默认关,每30分钟)
A801-1 全票池增量同步(incrementDays=30)→ invoiceTypes 白名单下推远端逐票种切片轮询
→ 2.5 万窗口硬上限触顶二分时间片递归 → 镜像 blade_kingdee_invoice_pool 批量 upsert(无变化跳写)
→ refreshBoundReceipts:未绑定存量登记补绑(已绑定零写入)
手动端点:POST /invoice-receipt/sync-invoice-pool(同一方法+同一把锁)
【选票面】收票登记表单 / 结算单认领弹窗(KingdeeInvoicePoolDialog)
GET /invoice-receipt/invoice-pool、GET /formal-settlement/invoice-pool
→ 本地镜像真分页;仅「票号精确查 + 无认领/登记过滤 + 本地空页」时窄查 A801-1 回源一次再本地重查
行过滤:票号非空 + amountUseStatus=0 + availableAmount>0
【闸门】收票登记保存 persistMirrorFromRemote(按票号精确单查+可用性复核+FOR UPDATE upsert)
结算单认领确认 assertInvoicesClaimable(★10-05 补上:逐票窄查+可用复核,红冲/作废/被占用在此拦截)
【推送链】审批通过自动推送(receipt 开关)/ 工具栏「批量同步」
A102-4 预检(应付已生成)→ A801-3 幂等预检(防重键=收票登记 id)
→ A303-2 收票单保存(付款方式固定现购)→ pushed → A302-6 指定应付 → assigned(终态)
→ A902 附件联带上传;缺票种编码按票号窄查 A801-1 补填
【回写面】PaymentKingdeeResultTask.syncKingdeeReceivedInvoiceResult(默认关,每10分钟)/ 手动端点
A801-3 按「收票登记 id」匹配(★缺陷已修)→ 发票状态异常回收置 failed
/ pushed 补偿重发 A302-6 / assigned 缺单号补登;窗口内查不到不改状态
【附件面】GET /invoice-receipt/invoice-pool/attachment/download?invoiceNo=(A801-2 流式代理)
镜像优先、实时回源兜底;pdfurl 优先 taxPdfUrl 兜底,都缺退 downloadUrl(fileType=4 判 OFD)
```
### 5.2 前端入口
| 页面 | 操作 | 后端端点 | 说明 |
|---|---|---|---|
| 收票登记(`/payment/invoice-receipt`) | 工具栏「批量同步」 | `POST /invoice-receipt/push-kingdee`(ids) | 单笔失败不中断整批,逐单报因 |
| 收票登记表单 | 发票号码放大镜(选票弹窗) | `GET /invoice-receipt/invoice-pool` | 镜像分页 + 回源;确认/保存前防重校验 |
| 收票登记表单 | 附件 查看/下载 | `GET /invoice-receipt/invoice-receipt/invoice-pool/attachment/download`(A801-2 代理) | 120s blob;PDF 优先 |
| 收票登记 | 行内「审批通过」 | `POST /invoice-receipt/approve` | receipt 开关开启时自动推送 |
| 正式结算单编辑器 | 发票认领区「查询可认领发票」/「确认识领」 | `GET /formal-settlement/invoice-pool`、`POST /formal-settlement/claim-invoices` | 确认时走认领闸门;未保存单仅本地暂存 |
| 共享弹窗 `KingdeeInvoicePoolDialog` | 行附件 查看/下载 | 同 A801-2 代理 | 票池弹窗、结算认领行、付款申请发票附件行、收票附件表 4 处复用 |
### 5.3 后端要点
**金蝶侧效果**:
![TMS 收票管理列表「金蝶状态」列:已指定应付(终态)/推送失败(带失败原因)/未推送,金蝶单据号 AVT-202608-000396 悬浮展示](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-收票金蝶状态列表.png)
![A902 附件上传金蝶侧效果:金蝶财务应付单详情「附件」区收到 TMS 推送单据联带上传的文件;付款计划区同时可见下推生成的付款计划(外部系统来源标识=防重键)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a902-附件上传金蝶侧.png)
![A801-2 发票查看/下载入口:正式结算单详情「发票信息」区认领的发票行提供 查看/下载(走后端 A801-2 流式代理,PDF 版式优先);付款信息区同时回显付款申请的金蝶单号](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a801-2-发票查看下载.png)
- 推送计划校验:approved、状态≠assigned、已绑定镜像、`checkStatus=2`、**恰好分摊 1 张审批通过应付结算单(一票一应付)**、有结算明细;不满足写 `blockedReason` 不改状态。
- A801-3 预检命中且状态正常→跳过 A303-2 直接续推指定(断点恢复);发票状态异常(作废2/红冲3/部分红冲7/全部红冲8)→failed 报人工。
- 落库:`kingdee_received_invoice_no`、状态机 `unsynced→pushed→assigned/failed` →`kingdee_push_status`(唯一金蝶状态来源);终态/失败写 `blade_invoice_receipt_record`;pushed/assigned 禁止本地作废。
- 全票池同步细节:单查询窗口 MAX_POOL_PAGES×500=2.5 万上限,`updateTime` 单调二分不重不漏;镜像 upsert 轻量比对(流水号/价税合计/税额/可用金额/使用状态)无变化跳写。
---
## 6. 业务域五:收款认领(A301-2 / A301-3 / A301-1)
**定位**:业务先行认领。金蝶司库第一轮自动认领到客户维度 → TMS 二次认领到结算单维度 → **本地认领完成后**(定时或手动)回推金蝶生成收款单 → 手动查询回写结果。★09-28 重构后:认领与推送解耦,推送按流水归集、定时+手动双入口。
### 6.1 链路总览
```
阶段一 收款流水页「流水同步」(手动)
POST /receipt-flow/sync ▶ A301-2 认领公告查询(claimusers=Nacos thirdParty.bfi.claimAnnounce.claimusers,
★不再按登录用户姓名;未配置则以空值调用会查不到数据)
▶ 全量翻页(窗口 months=6)▶ 按 detail_serial_no 幂等 upsert blade_kingdee_receipt_flow
阶段二 流水详情/批量认领(手动)
POST /receipt-flow/claim ▶ 本地落库(claim + 分摊 + 流水 claimed_amount/claim_status 刷新 + 结算单收款汇总)
▶ 本地完成即结束,**不调金蝶**(认领本地化★)
阶段三 推送金蝶(定时 ReceiptClaimPushTask 默认关每10分钟 / 手动流水行内「推送金蝶」)
pushClaimToKingdee ▶ 按认领人逐人重查 A301-2 → 扫「认领完成未推送」流水按流水归集
▶ 逐流水:awrp_src_code=分摊结算单号拼接(重试复用幂等)→ A301-3 推送
(分录单条:e_receivableamt=全部未推送金额;e_corebillno=第一张结算单金蝶内码,缺内码拦截不推)
▶ 成功回写 kingdee_bill_no(回执 keys.awrp_src_code 占位)+ kingdee_bill_status=pushed
阶段四 认领记录页「查询金蝶认领结果」(手动)
POST /receipt-claim-record/query-kingdee-result ▶ A301-1 收款单查询(今天回溯 30 天)
▶ sourcebillnumber==awrp_src_code 匹配 ▶ 真实收款单号覆盖占位单号 + 状态推进(C→approved,D→received 终态)
```
### 6.2 前端入口
| 页面 | 操作 | 后端端点 | 说明 |
|---|---|---|---|
| 收款流水(`/payment/receipt-flow`) | 工具栏「流水同步」 | `POST /receipt-flow/sync`(A301-2) | 权限 `receipt_flow_sync` |
| 同上 | 流水详情/批量认领表单「确认」 | `POST /receipt-flow/claim` | 选择审批通过应收结算单按比例分摊;已推送金蝶的认领禁止作废(后端硬校验) |
| 同上 | 行内「推送金蝶」 | `POST /receipt-flow/push-kingdee?flowId=` | 确认弹窗后按流水归集推送 |
| 认领记录(`/payment/receipt-claim-record`) | 工具栏「查询金蝶认领结果」 | `POST /receipt-claim-record/query-kingdee-result`(A301-1) | 手动;窗口内查不到不改状态 |
### 6.3 后端要点
**金蝶侧效果**:
![A301-2 对照:金蝶「认领公告列表」(RLTZ 通知单,待认领/部分认领)与 TMS 收款流水「手动同步流水」拉取的同批公告对照](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-2-公告同步对照.png)
![A301-3 对照:TMS 认领记录(RLTZ-20260612-0073 认领 828.00)推送后,金蝶「认领处理列表」生成认领单 RL-20260929-0058(已确认,认领通知单 0073 对应)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-认领推送对照.png)
![A301-3 金蝶侧认领结果详情:收款认领结果行「核心单据编号」=结算单星瀚内码(2586341365516352512,即 e_corebillno 传金蝶内码的落地证据)](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a301-3-金蝶认领结果.png)
- 推送闸门:已推送(pushed)流水跳过不重推;`billid`(认领公告 id)缺失拦截「请先同步金蝶收款流水」;结算单金蝶内码缺失拦截不推。
- 幂等:`awrp_src_code` 重试复用同一编码;作废认领不再生成本地冲单(金蝶单据状态保留原值)。
- A301-3 字段待金蝶确认项仍在:`recpaytype_number`(常量当前值 "999",注释示例 101 待确认)、`paymenttype`、`recpayer`(金蝶付款人 master id)。
---
## 7. 业务域六:公共取数(A801-4 汇率)
| 项 | 说明 |
|---|---|
| 前端入口 | 预结算单/正式结算单编辑器:非 RMB 且选了汇率日期(或币种切换)时自动 `GET /bfi/exchange-rate?currencyCode=&effectdate=`,回填结算汇率并重算本位币金额 |
| 后端 | 按日期查 A801-4 全量汇率(页大小固定 10)→ 本地按 `orgcur_number` 匹配 → 返回 VO;不落库 |
| 下游 | 汇率/汇率日期随结算单落 `blade_formal_settlement.exchange_rate / exchange_rate_date`,随 A102-2/A302-2 推送;A103-1 下推不传(按应付单自动带出) |
![外币结算汇率推送效果:TMS 结算单结算汇率 6.736700(美元)推送后,金蝶应付单汇率表 6.7367、金额/金额(本位币) ¥9,923.16 自动换算,外部单据类型=TMS财务应付单](./2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册).assets/bfi-effect-a801-4-外币汇率推送.png)
---
## 8. 全景:接口依赖与先后顺序
```
┌──────── 客商档案(全局前提,软依赖) ────────┐
│ A801-5 黑名单(提审拦截)→ A902-2 保存(审批自动/人工/定时)→ A902-1 复查
└───────────────────┬────────────────────────┘
▼
【应付主线】 正式结算单(应付)审批 ─A102-4 预检─▶ A102-2 保存 ─▶ synced ─▶ lockBusiData
│ A902 附件联带上传 (汇率 ← A801-4 页面回填)
│ A102-4 硬校验
▼
付款申请审批 ─A103-2 预检─▶ A103-1 下推(预付款→A103-3)─▶ synced ─▶ lockBusiData
│
▼
定时 A103-2 状态回写(纠偏/消失解锁)─▶ A103-4 付款记录(voucher_json)
【应收主线】 正式结算单(应收)审批 ─A302-4 预检─▶ A302-2 保存 ─▶ synced ─▶ lockBusiData
│ A302-4 硬校验
▼
开票申请审批 ─A303-4 批量预检─▶ A303-1 逐张次新增(E04/freights/A902 附件)─▶ synced ─▶ 逐张锁单
│
▼
定时 A302-4 勾稽纠偏 + A303-5 开票结果 ─▶ invoiced(终态,发票文件落库)
【收票主线】 定时 A801-1 全票池同步 ─▶ 镜像表 ◀── 选票读镜像(票号未命中窄查回源)
│ 收票登记保存(闸门)/ 结算认领确认(闸门★新增)
▼
收票登记审批 ─A102-4 预检─A801-3 预检(登记id)─▶ A303-2 保存 ─▶ pushed ─▶ A302-6 ─▶ assigned
│(本期不锁单;A902 附件联带上传)
▼
定时 A801-3 回写(异常回收/指定补偿/单号补登,按登记 id 匹配★已修)
【收款认领主线】 手动 A301-2 公告同步 ─▶ blade_kingdee_receipt_flow
│ 页面认领(本地化,不调金蝶)
▼
定时/手动 A301-3 按流水归集推送(e_corebillno=结算单金蝶内码)─▶ pushed
▼
手动 A301-1 收款单查询 ─▶ 真实收款单号覆盖 + approved/received(终)
【横切】 审批通过 AFTER_COMMIT 自动同步(4 开关)│ 推送成功→lockBusiData(3 链路)
金蝶删单→unLockNotifyBusiData 回调还原 │ 锁失败定时/手动补偿
failed 单据→KingdeeAutoSyncRetryTask 定时重推
```
**硬顺序规则(代码强制)**:
| # | 规则 | 强制方式 |
|---|---|---|
| 1 | 客商档案先于一切推送(本地 customer_code 必须存在) | 查不到直接报错;金蝶侧审核状态不校验(软依赖) |
| 2 | 客商提审先过 A801-5 黑名单 | A 类命中/查询异常即阻断提审 |
| 3 | A102-2 先于 A103-1 与 A303-2 | A102-4 硬校验/预检 |
| 4 | A302-2 先于 A303-1 与 A301-3(e_corebillno 需金蝶内码) | A302-4 硬校验;内码缺失拦截不推 |
| 5 | A303-2 先于 A302-6 | 同一次推送内串行 |
| 6 | 推送先于回写(A103-2/A103-4、A302-4 勾稽、A303-5、A801-3、A301-1) | 查询键由推送链写入本地 |
| 7 | 锁单在推送成功之后、金蝶删单回调可还原 | lockAfterPush 事务提交后执行;unLockNotifyBusiData 免登回调 |
| 8 | A801-4 汇率查询先于 A102-2/A302-2(如需携带汇率) | 页面联动维护,未维护不传 |
**断链恢复语义**:本地失败/远端未落单→unsynced 或 failed 可重推;远端成功本地回滚→重推时预检幂等回填(A102-4/A302-4/A103-2/A303-4/A801-3/awrp_src_code 复用);推送成功后状态滞后→定时回写;批量语义→整批预热+逐单独立事务+单条失败不中断。
---
## 9. 状态机汇总(2026-10-14)
| 表.字段 | 状态集 | 迁移 | 写入方 |
|---|---|---|---|
| `blade_formal_settlement.kingdee_sync_status` | unsynced / synced / **failed**(★新增写入口) | unsynced→(A102-2/A302-2 成功)→synced;推送异常→failed(独立事务);failed 可手动/定时重推、A302-4 勾稽纠偏回 synced;金蝶删单回调→voided + unsynced 还原 | FormalSettlementServiceImpl |
| `blade_payment_application.kingdee_status` | unsynced / synced / paying / paid(终) / closed(终) / failed | 同前版映射;failed 可纠偏回 synced;**连续 2 轮查不到→消失解锁置 failed**;金蝶删单回调→failed+清单号(不作废源单) | PaymentApplicationServiceImpl + 定时 |
| `blade_invoice_application.kingdee_status` | unsynced / synced / invoiced(终) / **failed**(★新增写入口) | unsynced→(A303-1 全部张次成功)→synced→(A303-5 新发票)→invoiced;推送异常→failed;金蝶删单回调→作废还原 | InvoiceApplicationServiceImpl + 定时 |
| `blade_invoice_receipt.kingdee_push_status` | unsynced / pushed / assigned(终) / failed | 同前版;A801-3 回写匹配键已修复为登记 id(异常回收/补偿/补登生效) | InvoiceReceiptServiceImpl + 定时 |
| `blade_customer_archive.kingdee_status` | none / synced / failed / exist(终) | 同前版;提审新增 A801-5 黑名单拦截(不产生状态) | CustomerArchiveServiceImpl + 定时 |
| `blade_kingdee_bill_lock.lock_status` ★新增 | failed / locked / unlocked | 先落 failed 占位→lockBusiData 成功→locked;补偿重试(try_count<5)或金蝶解锁/删单回调→unlocked | KingdeeBillLockServiceImpl + 定时 + 手动端点 |
| `blade_receipt_claim.kingdee_bill_status` | none / pushed / approved / received(终) / failed / voided | none→(A301-3)→pushed→(A301-1: C)→approved→(D)→received;已推送禁止作废 | ReceiptFlowServiceImpl + ReceiptClaimRecordServiceImpl |
| `blade_kingdee_receipt_flow.claim_status` | unclaimed / partial / claimed | 认领分摊→partial/claimed;全部作废→unclaimed;公告同步不覆盖 | ReceiptFlowServiceImpl |
---
## 10. 定时任务与配置开关汇总
全部在 `blade-transport/task/`,Spring `@Scheduled`,统一 single-flight(`KingdeeSyncLockSupport`,`blade.lock.enabled=false` 降级直跑)。
| 任务类 | 方法 | 覆盖业务(接口) | cron 键(默认) | 开关键(代码默认) | 手动端点 |
|---|---|---|---|---|---|
| PaymentKingdeeResultTask | syncKingdeePaymentResult | ① A103-2/A103-4 付款结果 ② A302-4 应收勾稽 ③ A303-5 开票结果 | `thirdParty.bfi.payResult.cron`(`0 */10 * * * ?`) | `thirdParty.bfi.payResult.syncEnabled`(false;**Nacos 样例 dev/prod 已显式 true**) | `/payment-application/sync-kingdee-result`、`/formal-settlement/sync-kingdee-ar-result`、`/invoice-application/sync-kingdee-invoice-result` |
| PaymentKingdeeResultTask | syncKingdeeReceivedInvoiceResult | A801-3 收票结果回写 | `thirdParty.bfi.receivedInvoiceResult.cron`(每10分钟) | `…receivedInvoiceResult.syncEnabled`(false) | `/invoice-receipt/sync-kingdee-result` |
| CustomerKingdeeSyncTask | syncKingdeeCustomerData | A902-1 复查 + A902-2 重推 + 金蝶停用联动 | `thirdParty.bfi.customer.cron`(`0 0 2 * * ?`) | `…customer.syncEnabled`(false;KS000067 残留已移除) | `/customer-archive/sync-kingdee` |
| **ReceiptClaimPushTask** ★新增 | pushClaimToKingdee | A301-3 认领推送(按流水归集,前置重查 A301-2) | `thirdParty.bfi.receiptClaimPush.cron`(每10分钟) | `…receiptClaimPush.syncEnabled`(false) | `/receipt-flow/push-kingdee` |
| **InvoiceReceiptKingdeeSyncTask** ★新增 | syncKingdeeInvoicePool | A801-1 全票池瘦身同步 + 绑定回填 | `thirdParty.bfi.invoiceReceipt.cron`(每30分钟) | `…invoiceReceipt.syncEnabled`(false);增量窗口 `…incrementDays`(30);票种白名单 `…invoiceTypes` | `/invoice-receipt/sync-invoice-pool` |
| **KingdeeAutoSyncRetryTask** ★新增 | retryKingdeeFailed | 4 域 failed 单据重推(结算/付款/开票/收票;不扫 unsynced;无次数上限,靠幂等预检防重) | `thirdParty.bfi.autoSync.retryCron`(每10分钟) | `…autoSync.retryEnabled`(false) | 各域 `/sync-kingdee` |
| **KingdeeBillLockRetryTask** ★新增 | retryFailedBillLocks | 锁单失败补偿(failed 且 try_count<5 重调 lockBusiData) | `thirdParty.bfi.billLock.retryCron`(每5分钟) | `…billLock.retryEnabled`(false) | `/formal-settlement/retry-kingdee-bill-lock` |
**审批自动同步开关(监听器,非定时)**:`thirdParty.bfi.autoSync.settlement / payment / invoiceApply / receipt`(默认全 false)。
**其他业务键**:`thirdParty.bfi.claimAnnounce.claimusers`(A301-2 认领人,**必配**)、`…claimAnnounce.months`(6)、`thirdparty.bfi.blacklist.blockRiskWarning`(false)、`thirdparty.bfi.material.*`(物料映射)。
> ⚠️ Nacos 样例(`doc/nacos/`)仅显式配置了 `payResult.syncEnabled=true`;`receiptClaimPush/invoiceReceipt` 样例仍为 false;`customer/receivedInvoiceResult/autoSync/billLock` 键未出现在样例中——生效值取决于各环境实际 Nacos,缺省走代码 false。**开启生产定时前逐项核对第 12 节风险清单。**
---
## 11. 操作流程手册
### 11.1 一次性前置准备(实施/运维)
| # | 事项 | 操作 |
|---|---|---|
| 1 | 部门金蝶组织编码 | 部门档案维护 `kingdee_dept_code`;未维护的部门推送组织取数会直接拦截 |
| 2 | 主数据 SQL | 执行 `doc/sql/transport/kingdee_master_data_init_20260924.sql`(计量单位档案等);认领链路执行 `blade_receipt_flow_claim_20260924.sql`;10-13 迁移脚本见 `doc/sql/transport/lqj/` |
| 3 | Nacos 业务配置 | `thirdParty.bfi.claimAnnounce.claimusers`(认领人,与金蝶公告 claimusers 一致)、`thirdParty.bfi.invoiceReceipt.invoiceTypes`(票池同步票种白名单)、`thirdparty.bfi.material.*`(物料映射,缺省有默认值但建议显式配置) |
| 4 | 定时开关按需开启 | 见第 10 节表:付款结果回写样例已开;其余按环境显式开启 `syncEnabled/retryEnabled` |
| 5 | 审批自动同步开关 | `thirdParty.bfi.autoSync.settlement/payment/invoiceApply/receipt` 按域开启;未开启时走页面「同步金蝶」手动推送 |
| 6 | 客商收款账户 | 维护 `blade_customer_receipt_account` 联行号(cnaps_code)——付款申请/预付款/客商推送均取真实联行号,缺失拦截 |
### 11.2 日常业务操作时序(财务/业务)
```
① 客商建档
新增客商 → 录统一社会信用代码(自动查金蝶回填)→ 上传发票信息 → 提交审批
(后端自动过 A801-5 黑名单,A 类命中被拦截)→ 审批通过 → 后端自动推送 A902-2
→ 列表 kingdee_status:synced(金蝶受理)→ 定时复查升级 exist(金蝶审核通过)
② 结算(应付/应收)
预结算/正式结算单编辑:非 RMB 币种选择汇率日期 → 自动带出金蝶汇率 → 提交审批
→ 审批通过(autoSync.settlement 开启时自动推送;或列表勾选点「同步金蝶」)
→ 成功后列表显示金蝶单号 + 「金蝶锁定中」标签(单据已被金蝶锁定,不可编辑/作废)
③ 付款
付款申请(关联已同步结算单)→ 审批通过(autoSync.payment 开启时自动下推;或「批量同步」)
→ 后端校验应付已生成 → A103-1 下推/预付款 A103-3 直推 → 锁单
→ 等 A103-2/A103-4 定时回写付款状态与付款记录(或点「同步付款结果」立即回写)
→ 状态流转 synced→paying→paid/closed,付款记录含凭证 JSON
④ 开票
开票申请(关联已同步应收结算单)→ 审批通过(autoSync.invoiceApply 开启时自动按张次推送;或「批量同步」)
→ 每张发票张次生成一张金蝶开票申请单 → 锁单 → 开票申请列表显示逗号拼接金蝶单号
→ 等 A303-5 定时回写发票结果(或手动端点)→ 状态 invoiced
→ 正式结算单详情「开票发票」区查看/下载发票文件
⑤ 收票(进项)
收票登记表单:发票号放大镜从金蝶票池选票(镜像读,票号未命中自动回源)→ 保存(重查闸门校验可用性)
→ 分摊到一张审批通过应付结算单 → 审批通过(autoSync.receipt 开启时自动推送;或「批量同步」)
→ A303-2 保存→pushed→A302-6 指定应付→assigned(终态)
→ 等 A801-3 定时回写(pushed 断点自动补偿重发 A302-6)
⑥ 收款认领
收款流水页「流水同步」拉取金蝶认领公告 → 流水详情/批量认领:选审批通过应收结算单按比例分摊(本地落库)
→ 「推送金蝶」(行内手动,或等 ReceiptClaimPushTask 定时)→ 金蝶生成收款单
→ 认领记录页「查询金蝶认领结果」回写真实收款单号与状态(approved/received)
注意:已推送金蝶的认领不可作废,需金蝶侧人工处置
⑦ 查看附件
金蝶票池/认领发票/收票附件的「查看/下载」统一走后端 A801-2 代理(PDF 版式优先)
```
### 11.3 异常处理操作
| 场景 | 现象 | 处理 |
|---|---|---|
| 推送失败(failed) | 列表金蝶状态 failed + 失败原因;审批时前端提示「审批通过,但自动同步金蝶失败:xxx」 | 修复原因后:客商/结算/付款/开票页手动「同步金蝶」重推;或等 `KingdeeAutoSyncRetryTask`(开启后每 10 分钟自动重推 failed) |
| 锁单失败 | 列表「锁定失败」标签;`blade_kingdee_bill_lock` failed 记录 | 等 `KingdeeBillLockRetryTask` 自动补偿(≤5 次);或 `POST /formal-settlement/retry-kingdee-bill-lock` 手动补偿;人工重推会重置计数 |
| 金蝶侧删单 | 金蝶回调 `POST /kingdee-bfi/unLockNotifyBusiData` | 自动还原:结算单/开票申请作废+状态回 unsynced 清单号(可重推);付款申请置 failed 清单号(不作废源单) |
| 单据被锁定后需修改 | 「金蝶锁定中」禁编辑/作废 | 只能在金蝶侧解锁/删单,触发回调还原后再改(无本地强制解锁入口,有意设计) |
| 付款单在金蝶被删且无回调 | A103-2 连续 2 轮(约 20 分钟)查不到 | 自动消失解锁置 failed,按 failed 重推 |
| 断链重推(远端已落单本地未同步) | 重推时预检命中 | 自动幂等回填单号跳过重复推送,无需人工清库 |
| 收票单发票状态异常 | 定时回写发现红冲/作废 → failed「已回收推送状态」 | 人工在金蝶侧处理后重新登记/推送 |
| 票池选不到票 | 镜像无数据 | 检查全票池同步开关与 `invoiceTypes` 白名单;或 `POST /invoice-receipt/sync-invoice-pool` 手动同步;票号精确查会自动回源 |
| 收款流水同步为空 | A301-2 查不到公告 | 检查 Nacos `claimAnnounce.claimusers` 是否与金蝶公告认领人一致 |
| 定时任务不跑/重复跑 | — | 核对 Nacos 开关键名(第 10 节);single-flight 锁保证多副本/手动不并发,`blade.lock.enabled=false` 时无锁 |
---
## 12. 风险与待确认清单(2026-10-14)
### 12.1 已关闭(旧总纲遗留,当前代码已核实修复)
| 旧问题 | 状态 |
|---|---|
| A801-3 定时回写按发票号匹配防重键(回写失效) | ✅ 已修复(登记 id,`InvoiceReceiptServiceImpl:1003-1009`) |
| `if(true)` 调试组合块(强制/随机物料) | ✅ 全部删除,真实拆分+配置映射 |
| 客商定时 KS000067 硬编码 | ✅ 已移除 |
| 客商推送 bankNumber 硬编码 / A103-3 e_bebank_number 取名称字段 | ✅ 均改取客商收款账户联行号 |
| 结算/开票失败不落 failed | ✅ 统一落 failed(03b34310) |
| 结算单认领无保存重查闸门 | ✅ 已实现 assertInvoicesClaimable |
### 12.2 仍需关注 / 联调确认
| # | 事项 | 证据/说明 |
|---|---|---|
| 1 | **A103-1 勾稽键口径**:代码传本地组装的应付防重键,`PaymentApplicationServiceImpl:1431-1443` 注释仍写「A102-4 反查 awrp_entry_code_p、禁止自行拼接」——注释与代码矛盾,金蝶侧付款计划分录标识是否等于防重键需联调实测 | 高优先 |
| 2 | **项目编码 999 残留**:全部推送链路(应付/应收/付款/预付款/开票申请)仍传测试值 999;A103-3「取项目档案真实编码」的逻辑已注释停用(详见入参映射表 14-#2) | 上线前处理 |
| 3 | **计量单位不送**:分录 DTO 有字段无赋值——需与金蝶确认不送计量单位是否影响单据校验 | 联调确认 |
| 4 | **A301-3 字段待金蝶确认**:`recpaytype_number`(101)/`paymenttype`/`recpayer` 取值口径 | 联调确认 |
| 5 | **A301-2 claimusers 未配置风险**:缺省空串调 A301-2 查不到数据(无报错),需在 Nacos 必配 | 配置项 |
| 6 | **其他链路不校验客商 kingdee_status**:客商未推送/未审核也会带编码推单(软依赖) | 既有风险 |
| 7 | **正式结算单作废未接 checkEditable**:仅靠前端锁状态标签禁用,接口层可绕过 | 建议补拦截 |
| 8 | **failed 重推无次数上限**:`KingdeeAutoSyncRetryTask` 每 10 分钟扫全量 failed,持续失败的单据会反复调金蝶(靠预检防重但不封顶) | 观察线上量 |
| 9 | **付款申请前端单笔「同步金蝶」按钮未绑定**(`handleSyncRow` 死代码):单笔入口仅剩审批自动同步 | 前端待补 |
| 10 | **旅客运输/代收车船税单据体**:DTO 已建未接(仅 freights 接入) | 按需迭代 |
| 11 | **方案 vs 代码差异(沿旧总纲)**:付款多次下推未实现(单分录单次);附件已落地(A902 4 链路)✅;收款流水同步已有定时(receiptClaimPush)✅;黑名单 A801-5 已实现 ✅ | — |
---
## 附:文档关系
- 本文为新总纲(基线 2026-10-13 `805ca9d3`,2026-10-15 增补),替代 09-27 版;单链路细节仍以 `doc/docs/业务流程/` 对应专项文档为深入参考,冲突处以本文 + 当前代码为准。
- **各推送接口的逐字段入参取值映射(页面字段→表字段/配置对照)见《2026-10-15-金蝶(星瀚)BFI推送入参取值映射表(页面字段-表字段-配置对照).md》**,其第 14 节含与本文记载不一致的代码级核对结论。
- 09-27 版中被本文修订的关键结论:A801-3 回写缺陷、票池「实时回源无定时」架构、收款认领「全手动无定时」、分录调试覆盖、物料映射常量、failed 落库口径、A801-5/A801-2/A902「未实现」状态。
Binary file not shown.

After

Width:  |  Height:  |  Size: 246 KiB

@@ -0,0 +1,327 @@
# 金蝶(星瀚)BFI 对接操作手册(纯操作版)
> 整理日期:2026-10-14
> 适用对象:TMS 业务操作人员(财务/业务)、系统管理员、实施运维。
> 本文只讲**怎么操作**:在哪个页面、点什么按钮、前提是什么、成功的标志是什么、失败了怎么处理。
> 接口与代码细节见同目录《2026-10-14-金蝶(星瀚)BFI全链路梳理(前端-后端-定时任务·业务域汇总与操作手册)》。
---
## 1. 全局须知
金蝶测试环境地址:https://cqcs-test.gxwuzi.com:8022/ierp/login.html
账号密码:
​ 13545909224 liuquanjia@123
​ 13737128880 websoft5386339
### 1.1 金蝶单据状态含义(列表里看到的字段)
| 状态值 | 含义 | 还会变吗 |
|---|---|---|
| unsynced | 未推送金蝶 | 会(推送成功变 synced) |
| synced | 已推送金蝶,金蝶受理 | 会(继续走后续状态) |
| failed | 推送失败(有失败原因) | 会(重推成功变 synced) |
| paying | 金蝶付款中(仅付款申请) | 会(付款完成变 paid) |
| paid / closed | 已付款 / 已关闭(终态) | 不变 |
| invoiced | 已开票(终态,开票申请) | 不变 |
| assigned | 已指定应付(终态,收票) | 不变 |
| exist | 金蝶已有该客商且审核通过(终态,客商) | 不变 |
### 1.2 「金蝶锁定」标签是什么
结算单、付款申请、开票申请推送成功后,TMS 会**自动锁定金蝶侧单据**。列表出现红色「金蝶锁定中」标签时:**该单据不可修改、不可作废**。要改动只能先在金蝶侧解锁/删单,金蝶会回调 TMS 自动还原本地状态。
### 1.3 两条推送路径
- **自动**:审批「通过」时系统自动推送金蝶(需管理员开启对应开关,见第 4 节);开启后审批接口会直接返回推送结果,失败时页面提示「审批通过,但自动同步金蝶失败:xxx」。
- **手动**:列表勾选单据点工具栏「同步金蝶 / 批量同步 / 推送金蝶」按钮。开关没开时用这条路径。
两条路径效果完全一样,重复点不会重复推送(系统自动查重)。
---
## 2. 一次性准备(实施/管理员操作)
| # | 事项 | 操作人 | 操作内容 | 不做的后果 |
|---|---|---|---|---|
| 1 | 部门金蝶组织编码 | 管理员 | 在部门档案维护「金蝶组织编码」字段 | 该部门单据推送时直接报错拦截 |
| 2 | 客商收款账户联行号 | 财务 | 客商档案维护收款账户(账号/开户行,**联行号必填**) | 付款推送、客商推送报错拦截 |
| 3 | 主数据初始化 SQL | 运维 | 执行 `doc/sql/transport/` 下金蝶相关脚本(计量单位等主数据、认领表、10-13 迁移脚本 `lqj/`) | 对应链路数据缺失 |
| 4 | 认领人配置 | 运维 | Nacos 配 `thirdParty.bfi.claimAnnounce.claimusers`(值须与金蝶认领公告的认领人一致) | 收款流水同步查不到数据(不报错) |
| 5 | 票池票种白名单 | 运维 | Nacos 配 `thirdParty.bfi.invoiceReceipt.invoiceTypes`(要同步的发票种类编码,逗号分隔) | 票池同步不到对应票种 |
| 6 | 物料映射 | 运维 | Nacos 配 `thirdparty.bfi.material.*`(有默认值,建议显式确认) | 推送报「运输方式未配置」类拦截 |
| 7 | 定时任务开关 | 运维 | 按第 4 节清单逐项开启 | 只能全靠手动按钮 |
| 8 | 审批自动同步开关 | 运维 | 按第 4 节 `autoSync.*` 按域开启 | 审批通过后不自动推送,走手动 |
---
## 3. 日常操作流程
### 3.1 客商建档与同步
**页面:客商管理 → 客商档案**
![客商档案列表:行内「同步金蝶」链接仅在审批通过且推送失败(failed)的单据上显示;审批通过的客商由系统自动推送](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/01-客商档案列表.png)
1. 点「新增」,录入**统一社会信用代码**(满 18 位自动查金蝶回填工商信息并锁定相关字段)。
- 若提示金蝶侧该客商「审核中」——等金蝶审核完再保存/推送。
2. 维护**收款账户(联行号必填)**和发票信息(纳税人识别号用于黑名单校验)。
3. 提交审批:系统自动做**黑名单校验**。
- 提交被拦「命中黑名单」→ 该客商为国资委黑名单 A 类,**不能提交**,走线下处置。
4. 审批「通过」→ 系统自动推送金蝶 → 列表 `kingdee_status` 变 **synced**。
- 变 **failed** → 看失败原因(悬停/详情),修复后点行内「同步金蝶」重推。
5. synced 是「金蝶已受理」;金蝶审核通过后由定时任务升级为 **exist**(终态,无需操作)。
推送成功后金蝶侧效果(供确认推送结果时对照):
![金蝶客户列表收到推送的客商:编码=TMS 客商编号(KS000073),客户分类 1101 内部客户/1102 外部客商](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a902-金蝶客户接收.png)
![TMS 客商档案与金蝶客户档案对照:编码一致、以统一社会信用代码为关联键](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a902-客商编码对照.png)
![新增客商档案:统一社会信用代码录入满 18 位自动到金蝶查询并回填工商信息](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/02-客商档案新增-信用代码自动查询.png)
> 注意:审批通过后的**档案变更不会重新推送金蝶**。改了信用代码等关键信息需人工确认金蝶侧一致性。
### 3.2 预结算单(正式结算与预付款的前置)
**页面:结算管理 → 预结算单**
预结算单**本身不推送金蝶**(列表无金蝶状态字段),它是正式结算和进度预付的前置单据,与金蝶的关联有两处:①非人民币项目在此维护汇率(从金蝶自动带出);②进度预付款在金蝶付讫后自动回写本单的垫资记录。
![预结算单列表:工具栏「新增 / 预付申请 / 尾款结算」;草稿行可 编辑/删除/提交,审批中可 通过/驳回](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/11-预结算单列表.png)
1. 制单:「新增」录入结算金额与明细;**非人民币项目**:选择币种和汇率日期 → 系统自动从金蝶带出结算汇率并重算本位币。
![预结算单编辑页:汇率日期/结算汇率字段(人民币时禁用为 1.00,外币选择日期后自动带出)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/12-预结算单-汇率字段.png)
2. 提交审批 → 审批「通过」(或驳回后修改重新提交)。
3. 审批通过后的三个出口:
- **发起预付**:工具栏「预付申请」(限应付且已审批、未转正式)→ 生成进度预付付款申请,走 3.4 预付款推送金蝶;
- **转正式结算**:在正式结算单页面新建时选择本预结算单作为来源(可多张合并)→ 转入 3.3 正式结算推送;转正式后本单被锁定(节点显示「已锁定(正式结算)」并回写正式结算单号);
- **尾款结算**:应付单生成尾款结算单号(同样需先转入正式结算推金蝶)。
4. 注意:**正式结算单的汇率不自动继承预结算单**,转正式后需在正式结算单编辑时重新选汇率日期带出(见 3.8),否则推送金蝶不带汇率。
5. 金蝶付款回写联动:进度预付在金蝶付讫(paid)后,系统自动在本单垫资记录回写付款金额与金蝶付款单号,并刷新已付/未付汇总——无需手工登记(异常时管理员可用回写接口手工补)。
### 3.3 结算单推送(应付 / 应收)
**页面:结算管理 → 正式结算单(应付 / 应收 Tab)**
![正式结算单:应付/应收同页两个 Tab;工具栏「同步金蝶」需先勾选一条审批通过的单据;列表含结算汇率、金蝶单据状态、金蝶锁状态等列](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/03-正式结算单-同步金蝶.png)
前提:结算单已审批通过;非人民币结算单已维护汇率(见 3.8);客商已 synced/exist。
1. **自动路径**:审批「通过」时自动推送(开关开启时)。
2. **手动路径**:勾选一条单据 → 工具栏「同步金蝶」。
- 按钮是单选操作,一次推一张。
3. 成功标志:列表出现**金蝶单号**,状态变 **synced**,并出现「金蝶锁定中」标签。
4. 失败处理:状态变 failed → 看失败原因 → 常见原因:
- 「请先同步金蝶应付单」类 → 关联结算单还没推(应付/应收/收票有先后依赖,见 3.6);
- 客商编码取不到 → 先完成 3.1;
- 部门金蝶组织编码缺失 → 找管理员维护。
5. 纠偏:如果怀疑推送实际成功但本地状态没更新,等定时勾稽(或联系运维手动触发),系统会自动核对金蝶侧并纠正状态/补单号。
推送成功后金蝶侧效果(供确认推送结果时对照):
![金蝶财务应付单与 TMS 结算单对照:金蝶单上的「外部系统单据号」=TMS 防重键,应付金额与明细行(物料/车号/货物)逐行对应](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a102-应付单对照.png)
![金蝶财务应收单与 TMS 结算单对照:应收金额、费用合计对应,明细行物料/车号/运输货物逐行对应](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a302-应收单对照.png)
### 3.4 付款申请推送与结果回写
**页面:收付款管理 → 付款管理**
![付款管理:工具栏「批量同步」推送付款申请;列表自带「金蝶单据号 / 金蝶单据状态 / 金蝶锁状态 / 附件同步」列——图中首行即「已生成 + 金蝶锁定中 + 附件已上传」的已推送单据](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/04-付款管理-批量同步.png)
前提:付款申请已审批通过,且**关联的结算单已推送金蝶(synced)**。
1. 推送:
- 自动路径:审批「通过」时自动下推。
- 手动路径:勾选**审批通过**的单据 → 工具栏「批量同步」(一次可多张,单张失败不影响其他)。
- 预付款(项目垫资/进度垫资)走同样的按钮,系统自动走预付款通道。
2. 成功标志:状态 **synced** + 金蝶单号 + 锁定标签。
3. 查付款结果:
- 正常等定时任务回写(默认每 10 分钟,开关开启时);
- 急用点工具栏「**同步付款结果**」(勾选单据后显示;若未见到该按钮,说明当前环境以定时回写为主,等下一轮即可)。
4. 结果状态流转:synced → **paying**(金蝶付款中)→ **paid**(已付款,含付款记录/凭证信息)/ **closed**(关闭)。
5. 失败与特殊场景:
- failed → 修复后重推(批量同步按钮即可,系统会自动跳过金蝶已有的);
- 单据在金蝶被删:回调后自动置 failed 并清单号,直接重推;
- 金蝶删了但回调丢了:约 20 分钟后系统自动检测到「查不到」并解锁置 failed,再重推。
推送成功后金蝶侧效果(供确认推送结果时对照):
![TMS 付款申请与金蝶付款申请单对照:申请付款金额、收款账号、结算方式逐项对应;金蝶单号 FKSQ-… 与 TMS 列表回写的金蝶单号一致](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a103-付款申请对照.png)
![预付款推送效果:TMS 进度预付在金蝶生成「其他付款申请」,预付款金额、收款账户对应,附件随单上传](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a103-3-预付款对照.png)
> 注意:推送后单据被金蝶锁定,**不能改金额、不能作废**。发现金额错误必须先金蝶侧处理。
### 3.5 开票申请推送与开票结果
**页面:收付款管理 → 开票管理**
![开票管理:工具栏「批量同步」逐张推送(按发票张次拆单);搜索区可按「金蝶单据状态」筛选](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/05-开票管理-批量同步.png)
前提:开票申请已审批通过,且**关联结算单的应收单已推送金蝶**。
1. 推送:审批「通过」自动推送,或勾选后工具栏「批量同步」(逐张处理)。
- 系统按**发票张次**拆分:一张申请 3 张发票会生成 3 张金蝶开票申请单。
2. 成功标志:状态 **synced**,金蝶单号多张时逗号拼接显示;逐张锁定。
3. 查开票结果:等定时回写(默认每 10 分钟),状态变 **invoiced**(终态)。
4. 查看发票文件:正式结算单详情 →「开票发票」区 → 查看/下载(PDF 优先)。
推送成功后金蝶侧效果(供确认推送结果时对照):
![开票申请按发票张次拆分:TMS 开票内容第1张/第2张分别生成金蝶开票申请单,购销方、不含税金额逐项对应](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a303-1-开票张次对照.png)
### 3.6 收票登记(进项发票)
**页面:收付款管理 → 收票管理**
![收票管理:工具栏「批量同步」推送收票单并指定应付](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/06-收票管理-批量同步.png)
**硬性前提**:一张票只能登记一次;推送前必须已分摊到**恰好一张审批通过的应付结算单**,且该结算单已推送金蝶。
1. 选票:表单里点发票号放大镜 → 弹出「查询金蝶票据池」→ 勾选 → 确认(带出票面信息)。
![查询金蝶票据池弹窗:按发票号/日期/税号/票种等筛选;每行可「查看/下载」发票原件(PDF 优先);点「选择」带出到表单](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/07-收票登记-金蝶票池选票.png)
![img.png](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/img.png)
- 票池数据来自定时同步(默认每 30 分钟);票号精确搜索时若本地没有会**自动去金蝶现查**。
- 查不到票 → 等同步或让运维检查票种白名单配置/手动触发同步(见 4.2)。
2. 保存:系统自动**重查金蝶票池校验可用性**——票已被别人登记/认领/红冲/作废会保存失败并提示原因。
3. 分摊应付结算单后提交审批。
4. 推送:审批「通过」自动推送,或勾选 → 工具栏「批量同步」。
5. 状态流转:synced→(系统内)→ **pushed**(金蝶收票单已生成)→ **assigned**(已指定应付,终态)。
- 卡在 pushed 不用管:定时任务会自动补发「指定应付」。
- failed 且提示发票状态异常(红冲/作废)→ 该票在金蝶已不可用,需人工处理,不要重推。
6. 附件:表单附件区可查看/下载发票原件(系统自动从金蝶取,PDF 优先)。
推送成功后金蝶侧状态与效果(供确认推送结果时对照):
![收票管理列表「金蝶状态」列:已指定应付(终态)/推送失败(含失败原因)/未推送](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-收票金蝶状态列表.png)
![推送的单据附件已上传到金蝶侧单据的「附件」区](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a902-附件上传金蝶侧.png)
![正式结算单详情「发票信息」区:认领的发票可 查看/下载(系统从金蝶取原件,PDF 优先);付款信息区回显付款申请金蝶单号](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a801-2-发票查看下载.png)
### 3.7 收款认领
**页面:收付款管理 → 收款流水、认领记录**
这是「金蝶收到钱 → TMS 认领到结算单 → 回推金蝶」的流程:
1. **同步流水**:收款流水页 → 工具栏「**手动同步流水**」→ 拉取金蝶认领公告。
![收款流水:工具栏「手动同步流水」拉取金蝶认领公告、「批量认领」进入认领;列表展示认领通知单/付款人/对方户名等](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/08-收款流水-同步与推送.png)
- 查不到数据先检查认领人配置(第 2 节 #4)。
2. **认领**:点流水进入详情(或工具栏「批量认领」)→ 选择**审批通过且已推送金蝶的应收结算单**、填分摊金额 → 确认。
- 认领此时只落本地,不推金蝶。
- 分摊总额不能超过该流水未认领金额。
3. **推送金蝶**:
- 手动:流水详情/符合条件的行内「推送金蝶」;
- 自动:定时任务(默认每 10 分钟,开关开启时)自动推送认领完成的流水。
- 成功标志:流水显示金蝶收款单号、推送状态 pushed。
4. **回写结果**:认领记录页 → 工具栏「**查询金蝶认领结果**」→ 状态推进 approved(金蝶已审核)/ **received**(已收款,终态)。
![认领记录:工具栏「查询金蝶认领结果」回写金蝶收款单状态](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/09-认领记录-查询金蝶认领结果.png)
5. **作废限制**:已推送金蝶的认领**不能作废**(提示去金蝶侧人工处置)。未推送的可以作废。
推送成功后金蝶侧效果(供确认推送结果时对照):
![金蝶认领公告与 TMS 收款流水对照:「手动同步流水」拉取的就是金蝶认领公告列表里的 RLTZ 通知单](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-2-公告同步对照.png)
![TMS 认领记录推送后,金蝶「认领处理列表」生成对应认领单(认领通知单单号一致)](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-认领推送对照.png)
![金蝶认领单详情:认领结果行的核心单据编号=TMS 分摊结算单对应的金蝶单据](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a301-3-金蝶认领结果.png)
### 3.8 汇率维护(非人民币结算)
结算单(预结算/正式结算)编辑时:选择非 RMB 币种 → 选择**汇率日期** → 系统自动从金蝶带出汇率并重算本位币金额。带不出(金蝶该日无汇率)→ 换日期或联系金蝶侧确认。
![结算单详情:汇率日期/结算汇率字段由金蝶汇率接口自动带出,本位币金额随之重算](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/10-结算单-汇率自动带出.png)
外币结算推送后,金蝶侧按该汇率自动换算本位币:
![金蝶侧效果:汇率 6.7367、金额(本位币)自动换算,与 TMS 结算单的本位币合计一致](./2026-10-14-金蝶(星瀚)BFI对接操作手册(纯操作版).assets/bfi-effect-a801-4-外币汇率推送.png)
---
## 4. 定时任务与开关(运维视角)
### 4.1 任务清单(Nacos `thirdparty.bfi.*` 配置)
| 任务 | 干什么 | 开关键 | cron 键(默认频率) | 手动替代按钮 |
|---|---|---|---|---|
| 付款结果回写 | 付款状态(paid/closed)+ 付款记录/凭证回写;顺带应收勾稽纠偏、开票结果回写 | `thirdParty.bfi.payResult.syncEnabled`(样例已开) | `…payResult.cron`(10 分钟) | 付款「同步付款结果」、开票结果手动端点、结算勾稽手动端点 |
| 收票结果回写 | 收票单状态回写、pushed 断点补发指定应付 | `…receivedInvoiceResult.syncEnabled` | 每 10 分钟 | 收票「同步金蝶结果」 |
| 客商同步 | 金蝶审核状态复查(synced→exist)、停用联动、failed 重推 | `…customer.syncEnabled` | 每天 02:00 | 客商「同步金蝶」 |
| 收款认领推送 | 自动推送认领完成的流水到金蝶 | `…receiptClaimPush.syncEnabled` | 每 10 分钟 | 流水行内「推送金蝶」 |
| 全票池同步 | 拉金蝶全票池发票到本地镜像、补绑定 | `…invoiceReceipt.syncEnabled` | 每 30 分钟 | 手动端点 `POST /invoice-receipt/sync-invoice-pool` |
| failed 重推 | 自动重推所有推送失败的结算/付款/开票/收票单 | `…autoSync.retryEnabled` | 每 10 分钟 | 各页面「同步金蝶」 |
| 锁单补偿 | 推送成功但金蝶锁定失败的自动补锁(≤5 次) | `…billLock.retryEnabled` | 每 5 分钟 | 手动端点 `POST /formal-settlement/retry-kingdee-bill-lock` |
### 4.2 审批自动同步开关(按业务域独立)
`thirdParty.bfi.autoSync.settlement`(结算单)/ `…payment`(付款)/ `…invoiceApply`(开票)/ `…receipt`(收票),默认全关。**代码默认全部关闭,必须在 Nacos 显式开启**;客商档案审批推送不受此开关控制(始终自动)。
### 4.3 运维注意
- 所有定时任务带**分布式锁**:多实例部署只跑一份;手动按钮和定时共用锁,定时运行中手动触发会直接跳过(属正常)。
- 开了「failed 重推」后,持续失败的单据每 10 分钟会重试一次,**修复根因**(配置/数据)比反复观察更重要。
- 手动触发全票池同步与定时是同一逻辑,数据量大时耐心等待,不要反复点。
---
## 5. 常见问题与异常处理(FAQ)
| 现象 | 原因 | 处理 |
|---|---|---|
| 推送结算单不带汇率 | 正式结算单没维护汇率(预结算单的汇率**不自动继承**到正式结算单) | 在正式结算单编辑页重选汇率日期自动带出后再推 |
| 推送报「客商编码」类错误 | 客商未推送金蝶或档案缺数据 | 先完成客商同步(3.1),再推单据 |
| 推送报「请先同步金蝶应付单/应收单」 | 前置单据未推送 | 顺序:结算单(应付/应收) → 付款/开票/收票;先把前置结算单推送 |
| 推送报组织/部门编码类错误 | 部门没维护金蝶组织编码 | 管理员补维护后重推 |
| 推送报联行号类错误 | 客商收款账户联行号缺失 | 财务补维护收款账户后重推 |
| 提交客商审批被拦黑名单 | 纳税人识别号命中国资委黑名单(A 类) | 不可提交,走线下处置;B 类提示仅告警默认放行 |
| 审批通过但提示「自动同步金蝶失败」 | 推送失败但审批已生效 | 看失败原因 → 修复 → 手动「同步金蝶」重推(单据不会丢) |
| 列表「锁定失败」标签 | 推送成功但金蝶锁单接口失败 | 系统自动补偿(≤5 次);急用让运维调手动补偿端点 |
| 单据要改但显示「金蝶锁定中」 | 已推送并被金蝶锁定 | 到金蝶侧解锁/删单 → TMS 自动还原后即可编辑;不可强行作废 |
| 付款状态长时间不更新 | 回写定时未开启或金蝶侧未处理 | 点「同步付款结果」;仍无 → 金蝶侧查付款处理单 |
| 开票申请 synced 但没发票信息 | 开票结果未回写 | 等定时/联系运维手动触发;确认金蝶票易通已开票 |
| 票池选票查不到 | 镜像未同步到该票种 | 检查票种白名单配置;运维手动触发全票池同步;票号精确搜索可触发自动现查 |
| 保存收票登记被拦「可用金额为0」 | 票已被占用/认领/红冲 | 换票或在金蝶确认票据状态 |
| 收票卡在 pushed 不动 | 「指定应付」步骤未完成 | 开启收票回写定时自动补发,或重推一次 |
| 收款流水同步为空 | 认领人配置与金蝶不一致 | 检查 Nacos `claimAnnounce.claimusers` |
| 认领推送报「认领公告id缺失」 | 流水没同步过就认领了 | 先「流水同步」再操作 |
| 认领推送报金蝶内码缺失 | 分摊的结算单没推送金蝶 | 先完成结算单(应收)推送(3.3) |
| 已推送认领想作废 | 金蝶已生成收款单 | 不能作废,去金蝶侧人工处置 |
| 断网/金蝶宕机期间推了一半 | 部分单据 failed | 恢复后 failed 重推任务(或手动)自动续推,系统查重不会重复生成金蝶单据 |
---
## 6. 附:操作入口速查表
| 页面 | 按钮/操作 | 作用 |
|---|---|---|
| 客商档案 | (表单)录信用代码 | 自动查金蝶回填工商信息 |
| 客商档案 | 行内「同步金蝶」 | 失败重推客商 |
| 预结算单 | 工具栏「预付申请」/「尾款结算」 | 审批通过后发起进度预付 / 生成尾款结算号(预结算本身不推金蝶) |
| 正式结算单 | 新建时选择来源预结算单 | 预结算转正式结算(转后预结算锁定) |
| 正式结算单 | 工具栏「同步金蝶」(单选) | 推送应付/应收单到金蝶 |
| 正式结算单 | (编辑)汇率日期 | 自动带出金蝶汇率 |
| 正式结算单 | (编辑)发票认领「查询/确认识领」 | 从金蝶票池选票认领到结算单 |
| 付款申请 | 工具栏「批量同步」 | 下推付款申请/预付款到金蝶 |
| 付款申请 | 工具栏「同步付款结果」 | 立即回写付款状态与付款记录 |
| 开票申请 | 工具栏「批量同步」 | 按发票张次推送开票申请 |
| 收票登记 | (表单)发票号放大镜 | 从金蝶票池选票 |
| 收票登记 | 工具栏「批量同步」 | 推送收票单+指定应付 |
| 收票登记 | (表单)附件查看/下载 | 从金蝶取发票原件(PDF 优先) |
| 收款流水 | 工具栏「手动同步流水」 | 拉取金蝶认领公告 |
| 收款流水 | (详情/工具栏「批量认领」)认领 | 本地认领到结算单 |
| 收款流水 | 行内「推送金蝶」 | 推送认领结果到金蝶 |
| 认领记录 | 工具栏「查询金蝶认领结果」 | 回写金蝶收款单状态 |
| 运维专用 | `POST /invoice-receipt/sync-invoice-pool` | 手动全票池同步 |
| 运维专用 | `POST /formal-settlement/retry-kingdee-bill-lock` | 手动补偿锁单 |
| 运维专用 | `POST /formal-settlement/sync-kingdee-ar-result` | 手动应收勾稽纠偏 |
| 运维专用 | `POST /invoice-application/sync-kingdee-invoice-result` | 手动开票结果回写 |
---
> 本文基于 2026-10-14 代码现状整理;系统升级后按钮与提示如有出入,以页面实际为准,并核对全链路总纲文档的更新说明。
+2
View File
@@ -96,6 +96,8 @@ thirdParty:
lockLeaseSeconds: ${BFI_RECEIPT_CLAIM_PUSH_LOCK_LEASE_SECONDS:1800}
# 金蝶单据锁定定时补偿
billLock:
# 锁定/解锁总开关(推送后锁定+解锁回调+补偿重锁共用),默认开启保持既有行为
enabled: ${BFI_BILL_LOCK_ENABLED:true}
# 锁定补偿开关,默认关闭(安全兜底)
retryEnabled: ${BFI_BILL_LOCK_RETRY_ENABLED:false}
# 补偿调度 cron,默认每5分钟(:02 起步,错峰避让其他金蝶任务)