fix: 修正金蝶解锁回调单号定位并新增解锁终态页面标识
- 回调方向与锁定相反:按金蝶单号+我方单号双条件反查最新locked锁记录(targetBillType回调为空不参与匹配) - 回调处理批量化:一次IN查询+updateBatchById批量落库,消除逐张查询/更新N+1 - 新增KingdeeUnlockMark展示契约与fillUnlockMarks通用填充,付款申请/结算单/开票申请VO回填解锁时间、操作人、动作名称 - 更新金蝶单据锁定与解锁业务流程文档(回调方向、解锁后数据变化、事务语义、联调实测结论)
This commit is contained in:
+8
-1
@@ -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;
|
||||
|
||||
+8
-1
@@ -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;
|
||||
|
||||
+36
@@ -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);
|
||||
}
|
||||
+8
-1
@@ -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;
|
||||
|
||||
+29
-2
@@ -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 -> 最新 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 <T> 实现 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:单次一个源单号对一个金蝶单号,单张成败互不影响;定时任务定时触发,亦可人工触发);
|
||||
|
||||
+2
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+2
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+66
-20
@@ -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;
|
||||
@@ -152,8 +154,8 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMappe
|
||||
public int onUnlockNotify(BfiBillLockDTO param) {
|
||||
// 总开关关闭:只应答不处理(返回 0 与「未匹配」同构,金蝶侧不会触发重试)
|
||||
if (!bfiProperties.getBillLock().isEnabled()) {
|
||||
log.warn("金蝶单据锁定总开关关闭,跳过解锁回调处理:operationType={},targetBillNo={}",
|
||||
param == null ? null : param.getOperationType(), param == null ? null : param.getTargetBillNo());
|
||||
log.warn("金蝶单据锁定总开关关闭,跳过解锁回调处理:operationType={},金蝶单号={}",
|
||||
param == null ? null : param.getOperationType(), param == null ? null : param.getSourceBillNo());
|
||||
return 0;
|
||||
}
|
||||
if (param == null || !KingdeeLockConstant.SYSTEM_XH.equals(param.getSourceSystem())
|
||||
@@ -161,35 +163,41 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl<KingdeeBillLockMappe
|
||||
|| !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();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -268,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:单次一个源单号对一个金蝶单号,单张成败互不影响,由定时任务定时触发,亦可人工触发)。
|
||||
|
||||
+2
@@ -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
|
||||
|
||||
+72
-22
@@ -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. 锁定失败补偿(定时兜底,不依赖人工)
|
||||
|
||||
```
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user