diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeBillLock.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeBillLock.java
new file mode 100644
index 00000000..1817ce2e
--- /dev/null
+++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeBillLock.java
@@ -0,0 +1,87 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.pojo.entity;
+
+import com.baomidou.mybatisplus.annotation.TableName;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import org.springblade.core.tenant.mp.TenantEntity;
+
+import java.io.Serial;
+import java.time.LocalDateTime;
+
+/**
+ * 金蝶单据锁定记录实体类
+ *
+ * 一行 = 一张金蝶目标单:推送成功后 lockBusiData 锁定(FAILED→LOCKED),金蝶经 unLockNotifyBusiData
+ * 回调解锁(→UNLOCKED,DELETE 时废除源单并还原关联数据);同时承担当前锁状态(编辑守卫/回调定位)
+ * 与锁定补偿扫表(FAILED+try_count)、锁定/解锁留痕
+ *
+ * @author Chill
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("blade_kingdee_bill_lock")
+@Schema(description = "金蝶单据锁定记录")
+public class KingdeeBillLock extends TenantEntity {
+ @Serial private static final long serialVersionUID = 1L;
+ /**
+ * WLTMS 单据类型(SETTLEMENT/INVOICE_APPLY,预留 PAY_APPLY/RECEIVED_INVOICE)
+ */
+ private String sourceBillType;
+ /**
+ * WLTMS 业务主键
+ */
+ private Long sourceBillId;
+ /**
+ * WLTMS 单号(正式结算单号/开票申请单号)
+ */
+ private String sourceBillNo;
+ /**
+ * 金蝶单据类型(ApFin_other_BT_S/arfin_other_BT_S/cdm_paybill_bank_elect_BT_S 等固定值)
+ */
+ private String targetBillType;
+ /**
+ * 金蝶单据编号(即业务单 kingdeeBillNo,开票申请按发票张次逐张一行)
+ */
+ private String targetBillNo;
+ /**
+ * 锁状态:failed-锁定失败(补偿重试对象)/locked-金蝶锁定中/unlocked-已解锁(终态)
+ */
+ private String lockStatus;
+ /**
+ * 最近动作来源操作类型:SAVE-单据新增,DELETE-单据删除(DELETE 回调触发废除还原)
+ */
+ private String operationType;
+ /**
+ * 锁定重试次数(补偿任务用,超上限停试)
+ */
+ private Integer tryCount;
+ /**
+ * 锁定时间
+ */
+ private LocalDateTime lockTime;
+ /**
+ * 解锁时间
+ */
+ private LocalDateTime unlockTime;
+ /**
+ * 推单发起人(锁定操作人)
+ */
+ private String lockOperator;
+ /**
+ * 回调操作人(金蝶侧操作人)
+ */
+ private String notifyOperator;
+ /**
+ * 锁定失败原因
+ */
+ private String failReason;
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeLockConstant.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeLockConstant.java
new file mode 100644
index 00000000..6737bc3e
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeLockConstant.java
@@ -0,0 +1,91 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.constant;
+
+import java.util.Map;
+
+/**
+ * 金蝶单据锁定/解锁(lockBusiData / unLockNotifyBusiData)固定值。
+ *
lockBusiData 由 WLTMS 调金蝶(源系统 WLTMS、目标系统 XH),
+ * unLockNotifyBusiData 由金蝶调 WLTMS(源系统 XH、目标系统 WLTMS);
+ * WLTMS 单据类型自定义,金蝶目标单据类型为固定值
+ *
+ * @author Chill
+ */
+public final class KingdeeLockConstant {
+
+ /** 锁动作:锁定 */
+ public static final String ACTION_LOCK = "LOCK";
+
+ /** 锁动作:解锁 */
+ public static final String ACTION_UNLOCK = "UNLOCK";
+
+ /** 来源操作类型:单据新增 */
+ public static final String OPERATION_SAVE = "SAVE";
+
+ /** 来源操作类型:单据删除(解锁回调携带 DELETE 时 WLTMS 废除源单并还原关联数据) */
+ public static final String OPERATION_DELETE = "DELETE";
+
+ /** 系统标识:WLTMS(运输管理系统,即本系统) */
+ public static final String SYSTEM_WLTMS = "WLTMS";
+
+ /** 系统标识:金蝶星瀚 XH */
+ public static final String SYSTEM_XH = "XH";
+
+ /** 锁状态:锁定失败(补偿定时任务的重试对象) */
+ public static final String STATUS_FAILED = "failed";
+
+ /** 锁状态:金蝶锁定中(源单冻结,不可编辑/作废) */
+ public static final String STATUS_LOCKED = "locked";
+
+ /** 锁状态:已解锁(终态;同单重推生成新锁记录) */
+ public static final String STATUS_UNLOCKED = "unlocked";
+
+ /** WLTMS 单据类型:正式结算单(应付/应收共用,按结算单 settlementType 区分金蝶目标类型) */
+ public static final String SOURCE_SETTLEMENT = "SETTLEMENT";
+
+ /** WLTMS 单据类型:开票申请单 */
+ public static final String SOURCE_INVOICE_APPLY = "INVOICE_APPLY";
+
+ /** WLTMS 单据类型:付款申请单(映射预留,本期不锁) */
+ public static final String SOURCE_PAY_APPLY = "PAY_APPLY";
+
+ /** WLTMS 单据类型:收票单(映射预留,本期不锁) */
+ public static final String SOURCE_RECEIVED_INVOICE = "RECEIVED_INVOICE";
+
+ /** 金蝶目标单据类型:财务应付单(A102-2,应付结算单推送后锁定) */
+ public static final String TARGET_AP_BILL = "ApFin_other_BT_S";
+
+ /** 金蝶目标单据类型:财务应收单(A302-2,应收结算单推送后锁定) */
+ public static final String TARGET_AR_BILL = "arfin_other_BT_S";
+
+ /** 金蝶目标单据类型:开票单(A303-1 开票申请单,按发票张次逐张一行锁记录) */
+ public static final String TARGET_INVOICE_BILL = "cdm_paybill_bank_elect_BT_S";
+
+ /** 金蝶目标单据类型:付款申请单(映射预留,本期不锁) */
+ public static final String TARGET_PAY_APPLY = "ap_payapply_oth_BT_S";
+
+ /** 金蝶目标单据类型:收票单(映射预留,本期不锁) */
+ public static final String TARGET_RECEIVED_INVOICE = "cdm_recbill_bank_BT_S";
+
+ /** 锁定重试上限:补偿任务超过该次数不再自动重试,人工排查(失败原因见 fail_reason) */
+ public static final int RETRY_MAX_COUNT = 5;
+
+ /** 金蝶目标单据类型 -> WLTMS 单据类型反查映射(解锁回调按 targetBillType 分发废除还原用) */
+ public static final Map TARGET_TO_SOURCE_MAP = Map.ofEntries(
+ Map.entry(TARGET_AP_BILL, SOURCE_SETTLEMENT),
+ Map.entry(TARGET_AR_BILL, SOURCE_SETTLEMENT),
+ Map.entry(TARGET_INVOICE_BILL, SOURCE_INVOICE_APPLY),
+ Map.entry(TARGET_PAY_APPLY, SOURCE_PAY_APPLY),
+ Map.entry(TARGET_RECEIVED_INVOICE, SOURCE_RECEIVED_INVOICE)
+ );
+
+ private KingdeeLockConstant() {
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/KingdeeBfiPublicController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/KingdeeBfiPublicController.java
new file mode 100644
index 00000000..fc63bae5
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/KingdeeBfiPublicController.java
@@ -0,0 +1,67 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.controller;
+
+import com.alibaba.fastjson2.JSON;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.core.secure.annotation.PreAuth;
+import org.springblade.core.secure.constant.AuthConstant;
+import org.springblade.core.tenant.annotation.TenantIgnore;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
+import org.springblade.thirdparty.bfi.pojo.vo.BfiBillLockResultVO;
+import org.springblade.transport.service.IKingdeeBillLockService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 金蝶单据锁定回调 控制器
+ *
+ * unLockNotifyBusiData:金蝶(星瀚)侧解锁/删单后回调 WLTMS 送结果,
+ * 无登录态公开端点(模式同 CustomerArchivePublicController);
+ * operationType=DELETE 时 WLTMS 废除源单并还原关联数据。
+ * 应答与 lockBusiData 出参同构:{updateCount, updateResultList},逐单处理(1-成功,0-未匹配/失败)
+ *
+ * @author Chill
+ */
+@Slf4j
+@RestController
+@AllArgsConstructor
+@TenantIgnore
+@PreAuth(AuthConstant.PERMIT_ALL)
+@RequestMapping("kingdee-bfi")
+@Tag(name = "金蝶单据锁定回调", description = "金蝶星瀚调用:单据解锁/删单通知")
+public class KingdeeBfiPublicController {
+
+ private final IKingdeeBillLockService kingdeeBillLockService;
+
+ /**
+ * 单据解锁通知(unLockNotifyBusiData):多张单据编号逗号拼接批量回调,逐张处理
+ *
+ * @param param 金蝶回调参数
+ * @return 处理结果,与 lockBusiData 出参同构
+ */
+ @PostMapping("unLockNotifyBusiData")
+ @Operation(summary = "单据解锁通知", description = "金蝶侧解锁/删单后回调;operationType=DELETE 时废除源单并还原关联数据")
+ public BfiBillLockResultVO unLockNotifyBusiData(@RequestBody BfiBillLockDTO param) {
+ log.info("金蝶单据解锁回调:{}", JSON.toJSONString(param));
+ try {
+ return BfiBillLockResultVO.of(kingdeeBillLockService.onUnlockNotify(param));
+ } catch (Exception exception) {
+ log.error("金蝶单据解锁回调处理失败:lockAction={},operationType={},targetBillType={},targetBillNo={}",
+ param == null ? null : param.getLockAction(), param == null ? null : param.getOperationType(),
+ param == null ? null : param.getTargetBillType(), param == null ? null : param.getTargetBillNo(), exception);
+ return BfiBillLockResultVO.of(0);
+ }
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeBillLockMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeBillLockMapper.java
new file mode 100644
index 00000000..89174b12
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeBillLockMapper.java
@@ -0,0 +1,18 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import org.apache.ibatis.annotations.Mapper;
+import org.springblade.transport.pojo.entity.KingdeeBillLock;
+
+/** 金蝶单据锁定记录 Mapper @author Chill */
+@Mapper
+public interface KingdeeBillLockMapper extends BaseMapper {
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java
index 2fdd3d55..075e6b9d 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IFormalSettlementService.java
@@ -42,6 +42,15 @@ public interface IFormalSettlementService extends BaseService
void voidBill(FormalSettlementStatusRequest request);
String syncKingdee(Long id);
+ /**
+ * 金蝶删单回调废除(unLockNotifyBusiData DELETE 分发):作废结算单、同步信息还原推送前状态
+ * (kingdeeSyncStatus→unsynced,清单据编号/星瀚内码/同步时间),操作人取金蝶回调报文
+ *
+ * @param id 正式结算单id
+ * @param operator 金蝶侧操作人
+ */
+ void voidByKingdee(Long id, String operator);
+
/**
* 单据级事务执行金蝶推送(由 syncKingdee 经自代理调用):
* A102-2/A302-2 保存并回写结算单单据编号与星瀚内码(kingdeeBillId)
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java
index 92e01890..863eceec 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IInvoiceApplicationService.java
@@ -56,6 +56,15 @@ public interface IInvoiceApplicationService extends BaseService
+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
+import org.springblade.transport.pojo.entity.KingdeeBillLock;
+
+import java.util.List;
+
+/**
+ * 金蝶单据锁定记录服务
+ *
+ * @author Chill
+ */
+public interface IKingdeeBillLockService extends IService {
+
+ /**
+ * 推送金蝶成功后锁单(由各推送链路在主单事务提交后调用):
+ * 每张金蝶目标单一行锁记录(先落 failed 再调 lockBusiData,成功翻 locked);
+ * 同源单的目标单合并为一次调用(编号逗号拼接批量传参);已 locked 幂等跳过,failed 原地重锁;
+ * 异常独立捕获,不影响主单同步状态
+ *
+ * @param sourceBillType WLTMS 单据类型(SETTLEMENT/INVOICE_APPLY)
+ * @param sourceBillId WLTMS 业务主键
+ * @param sourceBillNo WLTMS 单号
+ * @param targetBillType 金蝶目标单据类型
+ * @param targetBillNos 金蝶单据编号集合(开票申请为多张)
+ */
+ void lockAfterPush(String sourceBillType, Long sourceBillId, String sourceBillNo, String targetBillType, List targetBillNos);
+
+ /**
+ * 金蝶解锁回调(unLockNotifyBusiData):校验来源/目标系统与 lockAction,
+ * targetBillNo 支持逗号拼接批量,逐张按 targetBillType+targetBillNo 定位最新 locked 锁记录;
+ * operationType=DELETE 时按单据类型分发废除源单并还原关联数据
+ *
+ * @param param 金蝶回调参数
+ * @return 处理单据数(应答 updateCount)
+ */
+ int onUnlockNotify(BfiBillLockDTO param);
+
+ /**
+ * 编辑守卫:源单存在 locked 锁记录时抛 ServiceException(金蝶锁定中不可修改)
+ *
+ * @param sourceBillType WLTMS 单据类型
+ * @param sourceBillId WLTMS 业务主键
+ */
+ void checkEditable(String sourceBillType, Long sourceBillId);
+
+ /**
+ * 锁定补偿:扫 failed 且未超重试上限的锁记录重调 lockBusiData(由定时任务调用)
+ *
+ * @return 补偿摘要
+ */
+ String retryFailedLocks();
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
index 7ebf7728..683bb681 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/FormalSettlementServiceImpl.java
@@ -41,6 +41,7 @@ import org.springblade.transport.mapper.PaymentApplicationSettlementMapper;
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
import org.springblade.transport.constant.KingdeeAttachmentConstant;
import org.springblade.transport.constant.KingdeeBfiConstant;
+import org.springblade.transport.constant.KingdeeLockConstant;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.ReceivablePayableDetailMapper;
import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper;
@@ -82,6 +83,7 @@ import org.springblade.transport.pojo.vo.PreSettlementVO;
import org.springblade.transport.pojo.vo.SettlementAdjustmentVO;
import org.springblade.transport.service.IFormalSettlementService;
import org.springblade.transport.service.IInvoiceReceiptService;
+import org.springblade.transport.service.IKingdeeBillLockService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.service.IWaybillService;
@@ -131,6 +133,9 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaUpdate()
+ .eq(FormalSettlement::getId, id)
+ .set(FormalSettlement::getApprovalStatus, TransportBusiConstant.VOIDED)
+ .set(FormalSettlement::getCurrentNode, "已作废")
+ .set(FormalSettlement::getCurrentProcessor, operator)
+ .set(FormalSettlement::getVoidReason, "金蝶侧删单回调自动废除")
+ .set(FormalSettlement::getKingdeeSyncStatus, KingdeeBfiConstant.KINGDEE_STATUS_UNSYNCED)
+ .set(FormalSettlement::getKingdeeBillNo, null)
+ .set(FormalSettlement::getKingdeeBillId, null)
+ .set(FormalSettlement::getSyncedTime, null));
+ // 变更流水:回调无登录态,操作人取金蝶报文(兜底 system,与 saveChange 同款)
+ FormalSettlementChangeRecord record = new FormalSettlementChangeRecord();
+ record.setFormalSettlementId(id);
+ record.setChangeType("结算单基本信息");
+ record.setOperationType("调整");
+ record.setChangeContent("金蝶删单回调自动废除并还原同步信息(金蝶单据已删除,可还原重推)");
+ record.setChangeReason("金蝶操作人:" + Func.toStr(operator));
+ record.setOperatorName(Func.isEmpty(operator) ? "system" : operator);
+ record.setChangeTime(LocalDateTime.now());
+ changeRecordMapper.insert(record);
+ }
+
@Override
public String syncKingdee(Long id) {
// 单据级事务经自代理调用:单条失败只回滚自身
String billNo = SpringUtil.getBean(IFormalSettlementService.class).doSyncKingdee(id);
+ // 主单事务提交后锁定金蝶单据(lockBusiData):失败只留 failed 锁记录由补偿任务重锁,不污染主单同步状态
+ lockKingdeeBillAfterPush(id, billNo);
// 主单事务提交后联带上传附件(A902):失败只记附件同步状态,不回滚不污染主单同步状态,重推自动补传
syncAttachmentsAfterPush(id);
return billNo;
}
+ /**
+ * 金蝶单据锁定(lockBusiData):A102-2/A302-2 推送成功事务提交后调用,目标单据类型按结算类型路由;
+ * 锁定异常独立捕获,不影响主单同步结果
+ *
+ * @param id 正式结算单id
+ * @param billNo 金蝶单据编号
+ */
+ private void lockKingdeeBillAfterPush(Long id, String billNo) {
+ try {
+ if (Func.isBlank(billNo)) {
+ return;
+ }
+ FormalSettlement settlement = getById(id);
+ if (settlement == null) {
+ return;
+ }
+ kingdeeBillLockService.lockAfterPush(KingdeeLockConstant.SOURCE_SETTLEMENT, id, settlement.getFormalSettlementNo(),
+ "receivable".equals(settlement.getSettlementType())
+ ? KingdeeLockConstant.TARGET_AR_BILL : KingdeeLockConstant.TARGET_AP_BILL,
+ Func.toStrList(billNo));
+ } catch (Exception exception) {
+ log.error("正式结算单[{}]金蝶单据锁定联带动作异常(锁记录已落库待补偿)", id, exception);
+ }
+ }
+
/**
* 附件联带上传(A902):主单推送成功(含幂等预检命中)后调用,以本地 kingdeeBillId 定位金蝶单据;
* 本地缺失(存量数据/幂等跳过路径未带回内码)时经 A102-4/A302-4 按防重键反查回填;
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
index 4974787e..fccdef2a 100644
--- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/InvoiceApplicationServiceImpl.java
@@ -55,6 +55,7 @@ import org.springblade.thirdparty.bfi.service.IBfiService;
import org.springblade.transport.config.BfiProperties;
import org.springblade.transport.constant.KingdeeAttachmentConstant;
import org.springblade.transport.constant.KingdeeBfiConstant;
+import org.springblade.transport.constant.KingdeeLockConstant;
import org.springblade.transport.constant.TransportBusiConstant;
import org.springblade.transport.mapper.ContractManageMapper;
import org.springblade.transport.mapper.CustomerArchiveMapper;
@@ -93,6 +94,7 @@ import org.springblade.transport.pojo.vo.CustomerInvoiceInfoVO;
import org.springblade.transport.pojo.vo.InvoiceApplicationSheetVO;
import org.springblade.transport.pojo.vo.InvoiceApplicationVO;
import org.springblade.transport.service.IInvoiceApplicationService;
+import org.springblade.transport.service.IKingdeeBillLockService;
import org.springblade.transport.support.KingdeeApBillSyncSupport;
import org.springblade.transport.support.KingdeeArBillSyncSupport;
import org.springblade.transport.support.KingdeeSyncLockSupport;
@@ -157,6 +159,9 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpllambdaUpdate()
+ .eq(InvoiceApplication::getId, id)
+ .set(InvoiceApplication::getApprovalStatus, TransportBusiConstant.VOIDED)
+ .set(InvoiceApplication::getCurrentNode, "已作废")
+ .set(InvoiceApplication::getCurrentProcessor, operator)
+ .set(InvoiceApplication::getKingdeeStatus, KingdeeBfiConstant.KINGDEE_STATUS_UNSYNCED)
+ .set(InvoiceApplication::getKingdeeBillNo, null)
+ .set(InvoiceApplication::getSyncedTime, null));
+ // 操作流水:回调无登录态,操作人取金蝶报文(兜底 system,与 record 同款)
+ InvoiceApplicationRecord voidRecord = new InvoiceApplicationRecord();
+ voidRecord.setInvoiceApplicationId(id);
+ voidRecord.setActionType("kingdee_delete");
+ voidRecord.setActionName("金蝶删单回调作废");
+ voidRecord.setFromStatus(TransportBusiConstant.APPROVED);
+ voidRecord.setToStatus(TransportBusiConstant.VOIDED);
+ voidRecord.setOperatorName(Func.isEmpty(operator) ? "system" : operator);
+ voidRecord.setReason("金蝶侧删单回调自动作废并还原同步信息(金蝶开票申请单已删除,可还原重推)");
+ recordMapper.insert(voidRecord);
+ // 关联数据还原:关联正式结算单开票状态刷新
+ relationSettlementIds(id).forEach(this::refreshSettlementInvoiceStatus);
+ }
+
@Override
public String syncKingdee(Long id) {
// 单据级事务经自代理调用:单条失败只回滚自身
String billNo = SpringUtil.getBean(IInvoiceApplicationService.class).doSyncKingdee(id);
+ // 主单事务提交后锁定金蝶开票单(lockBusiData):每张发票张次一张金蝶单,多张逗号拼接批量锁定;
+ // 失败只留 failed 锁记录由补偿任务重锁,不污染主单同步状态
+ lockKingdeeBillAfterPush(id, billNo);
// 主单事务提交后联带上传附件(A902):失败只记附件同步状态,不回滚不污染主单同步状态,重推自动补传
syncAttachmentsAfterPush(id);
return billNo;
}
+ /**
+ * 金蝶开票单锁定(lockBusiData):A303-1 推送成功事务提交后调用,多张金蝶单批量锁定;
+ * 锁定异常独立捕获,不影响主单同步结果
+ *
+ * @param id 开票申请id
+ * @param billNo 金蝶开票单编号(多张逗号拼接)
+ */
+ private void lockKingdeeBillAfterPush(Long id, String billNo) {
+ try {
+ if (Func.isBlank(billNo)) {
+ return;
+ }
+ InvoiceApplication entity = getById(id);
+ if (entity == null) {
+ return;
+ }
+ kingdeeBillLockService.lockAfterPush(KingdeeLockConstant.SOURCE_INVOICE_APPLY, id, entity.getApplicationNo(),
+ KingdeeLockConstant.TARGET_INVOICE_BILL, Func.toStrList(billNo));
+ } catch (Exception exception) {
+ log.error("开票申请[{}]金蝶单据锁定联带动作异常(锁记录已落库待补偿)", id, exception);
+ }
+ }
+
/**
* 附件联带上传(A902):主单推送成功(含 A303-4 勾稽回填)后调用,以本地 kingdeeBillId 定位金蝶单据;
* 本地缺失(存量数据)跳过待重推补传;附件上传异常独立捕获,绝不影响主单同步结果
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/KingdeeBillLockServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/KingdeeBillLockServiceImpl.java
new file mode 100644
index 00000000..f6c92df1
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/KingdeeBillLockServiceImpl.java
@@ -0,0 +1,306 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.service.impl;
+
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+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.secure.utils.AuthUtil;
+import org.springblade.core.tool.utils.Func;
+import org.springblade.core.tool.utils.SpringUtil;
+import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO;
+import org.springblade.thirdparty.bfi.service.IBfiService;
+import org.springblade.transport.config.BfiProperties;
+import org.springblade.transport.constant.KingdeeLockConstant;
+import org.springblade.transport.constant.TransportBusiConstant;
+import org.springblade.transport.mapper.FormalSettlementMapper;
+import org.springblade.transport.mapper.InvoiceApplicationMapper;
+import org.springblade.transport.mapper.KingdeeBillLockMapper;
+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.service.IFormalSettlementService;
+import org.springblade.transport.service.IInvoiceApplicationService;
+import org.springblade.transport.service.IKingdeeBillLockService;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+/**
+ * 金蝶单据锁定记录服务实现
+ *
+ * 锁定:推送链路事务提交后按源单分组批量 LOCK(每组一行到多行锁记录,先落 failed 再调 lockBusiData,
+ * 成功翻 locked),失败由补偿定时任务重试;解锁:金蝶 unLockNotifyBusiData 回调(支持逗号拼接批量),
+ * DELETE 时分发废除源单并还原关联数据。
+ * 与结算单/开票申请服务互相依赖,为避免构造器循环依赖,单据服务经 {@code SpringUtil.getBean} 运行时获取
+ * (与本工程 doSyncKingdee 自代理同一惯用法)
+ *
+ * @author Chill
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class KingdeeBillLockServiceImpl extends ServiceImpl implements IKingdeeBillLockService {
+
+ /** 金蝶各接口统一日期文本格式(与 A102-2/A102-4/A103-1 等一致) */
+ private static final DateTimeFormatter BFI_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+
+ private final IBfiService bfiService;
+ private final BfiProperties bfiProperties;
+ private final FormalSettlementMapper formalSettlementMapper;
+ private final InvoiceApplicationMapper invoiceApplicationMapper;
+
+ @Override
+ public void lockAfterPush(String sourceBillType, Long sourceBillId, String sourceBillNo, String targetBillType, List targetBillNos) {
+ if (Func.isEmpty(targetBillNos)) {
+ return;
+ }
+ String operator = operatorName();
+ // 待锁目标单:已 locked 幂等跳过(人工重推「同步金蝶」幂等命中路径不会重复锁);
+ // 其余(新目标单/failed 重锁/同单重推)先落 failed 行占位,保证补偿扫表无遗漏
+ List pendingRecords = new ArrayList<>();
+ for (String targetBillNo : targetBillNos.stream().filter(Func::isNotBlank).distinct().toList()) {
+ KingdeeBillLock lockRecord = latestRecord(sourceBillType, sourceBillId, targetBillNo);
+ if (lockRecord != null && KingdeeLockConstant.STATUS_LOCKED.equals(lockRecord.getLockStatus())) {
+ continue;
+ }
+ if (lockRecord == null || KingdeeLockConstant.STATUS_UNLOCKED.equals(lockRecord.getLockStatus())) {
+ lockRecord = new KingdeeBillLock();
+ lockRecord.setSourceBillType(sourceBillType);
+ lockRecord.setSourceBillId(sourceBillId);
+ lockRecord.setSourceBillNo(sourceBillNo);
+ lockRecord.setTargetBillType(targetBillType);
+ lockRecord.setTargetBillNo(targetBillNo);
+ lockRecord.setLockStatus(KingdeeLockConstant.STATUS_FAILED);
+ lockRecord.setOperationType(KingdeeLockConstant.OPERATION_SAVE);
+ lockRecord.setTryCount(0);
+ lockRecord.setLockOperator(operator);
+ this.save(lockRecord);
+ }
+ pendingRecords.add(lockRecord);
+ }
+ if (pendingRecords.isEmpty()) {
+ return;
+ }
+ // 同源单目标单合并一次调用:编号逗号拼接批量传参,整批同成败(应答仅聚合 updateCount)
+ try {
+ bfiService.lockBusiData(this.buildLockParam(sourceBillType, sourceBillNo, targetBillType,
+ pendingRecords.stream().map(KingdeeBillLock::getTargetBillNo).toList(), operator));
+ LocalDateTime now = LocalDateTime.now();
+ for (KingdeeBillLock lockRecord : pendingRecords) {
+ lockRecord.setLockStatus(KingdeeLockConstant.STATUS_LOCKED);
+ lockRecord.setLockTime(now);
+ lockRecord.setFailReason(null);
+ this.updateById(lockRecord);
+ }
+ log.info("金蝶单据批量锁定成功:源单[{}:{}],金蝶单{}张[{}]", sourceBillType, sourceBillNo,
+ pendingRecords.size(), String.join(",", pendingRecords.stream().map(KingdeeBillLock::getTargetBillNo).toList()));
+ } catch (Exception exception) {
+ String failReason = limitText(exception.getMessage(), 500);
+ for (KingdeeBillLock lockRecord : pendingRecords) {
+ lockRecord.setTryCount((lockRecord.getTryCount() == null ? 0 : lockRecord.getTryCount()) + 1);
+ lockRecord.setFailReason(failReason);
+ this.updateById(lockRecord);
+ }
+ log.error("金蝶单据批量锁定失败(待补偿重锁):源单[{}:{}],金蝶单{}张", sourceBillType, sourceBillNo, pendingRecords.size(), exception);
+ }
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int onUnlockNotify(BfiBillLockDTO param) {
+ 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不能为空");
+ }
+ // 批量回调:targetBillNo 逗号拼接,逐张处理;未匹配锁定中记录的幂等跳过(重复通知防金蝶重试风暴)
+ int processed = 0;
+ for (String targetBillNo : Func.toStrList(param.getTargetBillNo())) {
+ KingdeeBillLock lockRecord = this.getOne(Wrappers.lambdaQuery()
+ .eq(KingdeeBillLock::getTargetBillType, param.getTargetBillType())
+ .eq(KingdeeBillLock::getTargetBillNo, targetBillNo)
+ .orderByDesc(KingdeeBillLock::getId)
+ .last("limit 1"), false);
+ if (lockRecord == null || !KingdeeLockConstant.STATUS_LOCKED.equals(lockRecord.getLockStatus())) {
+ log.warn("金蝶解锁回调未匹配锁定中的锁记录(幂等跳过):targetBillType={},targetBillNo={}",
+ param.getTargetBillType(), targetBillNo);
+ continue;
+ }
+ lockRecord.setLockStatus(KingdeeLockConstant.STATUS_UNLOCKED);
+ lockRecord.setOperationType(param.getOperationType());
+ lockRecord.setUnlockTime(LocalDateTime.now());
+ lockRecord.setNotifyOperator(param.getOperator());
+ this.updateById(lockRecord);
+ processed++;
+ 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;
+ }
+
+ /**
+ * 金蝶删单(DELETE 回调)分发:废除源单并还原关联数据(还原口径详见各 voidByKingdee 实现)
+ *
+ * @param lockRecord 锁记录(携带源单类型与源单主键)
+ * @param operator 金蝶侧操作人
+ */
+ private void restoreOnKingdeeDelete(KingdeeBillLock lockRecord, String operator) {
+ String sourceType = KingdeeLockConstant.TARGET_TO_SOURCE_MAP.get(lockRecord.getTargetBillType());
+ switch (sourceType == null ? "" : sourceType) {
+ case KingdeeLockConstant.SOURCE_SETTLEMENT -> {
+ FormalSettlement settlement = formalSettlementMapper.selectById(lockRecord.getSourceBillId());
+ if (settlement == null || Objects.equals(settlement.getIsDeleted(), 1)) {
+ log.warn("金蝶删单回调源结算单不存在:id={}", lockRecord.getSourceBillId());
+ return;
+ }
+ if (TransportBusiConstant.VOIDED.equals(settlement.getApprovalStatus())) {
+ log.info("金蝶删单回调源结算单[{}]已作废(幂等跳过)", settlement.getFormalSettlementNo());
+ return;
+ }
+ SpringUtil.getBean(IFormalSettlementService.class).voidByKingdee(settlement.getId(), operator);
+ }
+ case KingdeeLockConstant.SOURCE_INVOICE_APPLY -> {
+ InvoiceApplication application = invoiceApplicationMapper.selectById(lockRecord.getSourceBillId());
+ if (application == null || Objects.equals(application.getIsDeleted(), 1)) {
+ log.warn("金蝶删单回调源开票申请不存在:id={}", lockRecord.getSourceBillId());
+ return;
+ }
+ if (TransportBusiConstant.VOIDED.equals(application.getApprovalStatus())) {
+ log.info("金蝶删单回调源开票申请[{}]已作废(幂等跳过)", application.getApplicationNo());
+ return;
+ }
+ SpringUtil.getBean(IInvoiceApplicationService.class).voidByKingdee(application.getId(), operator);
+ }
+ default -> log.warn("金蝶删单回调暂不支持的单据类型:targetBillType={}(映射预留)", lockRecord.getTargetBillType());
+ }
+ }
+
+ @Override
+ public void checkEditable(String sourceBillType, Long sourceBillId) {
+ long lockedCount = this.count(Wrappers.lambdaQuery()
+ .eq(KingdeeBillLock::getSourceBillType, sourceBillType)
+ .eq(KingdeeBillLock::getSourceBillId, sourceBillId)
+ .eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_LOCKED));
+ if (lockedCount > 0) {
+ throw new ServiceException("该单据已被金蝶锁定,不可修改");
+ }
+ }
+
+ @Override
+ public String retryFailedLocks() {
+ List failedList = this.list(Wrappers.lambdaQuery()
+ .eq(KingdeeBillLock::getLockStatus, KingdeeLockConstant.STATUS_FAILED)
+ .lt(KingdeeBillLock::getTryCount, KingdeeLockConstant.RETRY_MAX_COUNT));
+ if (Func.isEmpty(failedList)) {
+ return "无待补偿锁记录";
+ }
+ // 按源单分组,组内合并一次调用(编号逗号拼接批量传参,整批同成败)
+ Map> groupBySource = failedList.stream()
+ .collect(Collectors.groupingBy(record -> record.getSourceBillType() + ":" + record.getSourceBillId(), LinkedHashMap::new, Collectors.toList()));
+ int success = 0;
+ for (List group : groupBySource.values()) {
+ KingdeeBillLock first = group.get(0);
+ try {
+ bfiService.lockBusiData(this.buildLockParam(first.getSourceBillType(), first.getSourceBillNo(), first.getTargetBillType(),
+ group.stream().map(KingdeeBillLock::getTargetBillNo).toList(),
+ Func.isBlank(first.getLockOperator()) ? "system" : first.getLockOperator()));
+ LocalDateTime now = LocalDateTime.now();
+ for (KingdeeBillLock lockRecord : group) {
+ lockRecord.setLockStatus(KingdeeLockConstant.STATUS_LOCKED);
+ lockRecord.setLockTime(now);
+ lockRecord.setFailReason(null);
+ this.updateById(lockRecord);
+ }
+ success += group.size();
+ } catch (Exception exception) {
+ String failReason = limitText(exception.getMessage(), 500);
+ for (KingdeeBillLock lockRecord : group) {
+ lockRecord.setTryCount((lockRecord.getTryCount() == null ? 0 : lockRecord.getTryCount()) + 1);
+ lockRecord.setFailReason(failReason);
+ this.updateById(lockRecord);
+ }
+ log.error("金蝶单据锁定补偿失败:源单[{}:{}],金蝶单{}张", first.getSourceBillType(), first.getSourceBillNo(), group.size(), exception);
+ }
+ }
+ return "待补偿" + failedList.size() + "张,成功" + success + "张";
+ }
+
+ /**
+ * 组装 lockBusiData 参数:WLTMS 调金蝶,源系统取配置(默认 WLTMS),目标系统 XH,
+ * 操作类型固定 LOCK/SAVE,多张目标单编号逗号拼接批量传参
+ *
+ * @param sourceBillType WLTMS 单据类型
+ * @param sourceBillNo WLTMS 单号
+ * @param targetBillType 金蝶目标单据类型
+ * @param targetBillNos 金蝶单据编号集合
+ * @param operator 操作人
+ * @return 锁单参数
+ */
+ private BfiBillLockDTO buildLockParam(String sourceBillType, String sourceBillNo, String targetBillType, List targetBillNos, String operator) {
+ BfiBillLockDTO param = new BfiBillLockDTO();
+ param.setLockAction(KingdeeLockConstant.ACTION_LOCK);
+ param.setSourceSystem(Func.isBlank(bfiProperties.getSrcSys()) ? KingdeeLockConstant.SYSTEM_WLTMS : bfiProperties.getSrcSys());
+ param.setTargetSystem(KingdeeLockConstant.SYSTEM_XH);
+ param.setOperationType(KingdeeLockConstant.OPERATION_SAVE);
+ param.setSourceBillType(sourceBillType);
+ param.setSourceBillNo(sourceBillNo);
+ param.setTargetBillType(targetBillType);
+ param.setTargetBillNo(String.join(",", targetBillNos));
+ param.setTime(LocalDateTime.now().format(BFI_DATE_TIME));
+ param.setOperator(operator);
+ return param;
+ }
+
+ /**
+ * 查同源单同目标单最新一条锁记录
+ *
+ * @param sourceBillType WLTMS 单据类型
+ * @param sourceBillId WLTMS 业务主键
+ * @param targetBillNo 金蝶单据编号
+ * @return 最新锁记录,无则 null
+ */
+ private KingdeeBillLock latestRecord(String sourceBillType, Long sourceBillId, String targetBillNo) {
+ return this.getOne(Wrappers.lambdaQuery()
+ .eq(KingdeeBillLock::getSourceBillType, sourceBillType)
+ .eq(KingdeeBillLock::getSourceBillId, sourceBillId)
+ .eq(KingdeeBillLock::getTargetBillNo, targetBillNo)
+ .orderByDesc(KingdeeBillLock::getId)
+ .last("limit 1"), false);
+ }
+
+ /** 当前操作人:无登录态(定时任务/回调)时兜底 system(与 saveChange 同款兜底) */
+ private String operatorName() {
+ return Func.isEmpty(AuthUtil.getUserName()) ? "system" : AuthUtil.getUserName();
+ }
+
+ /** 失败原因文本截断(fail_reason 列宽 500) */
+ private String limitText(String text, int maxLength) {
+ if (Func.isBlank(text)) {
+ return null;
+ }
+ return text.length() <= maxLength ? text : text.substring(0, maxLength);
+ }
+}
diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeBillLockRetryTask.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeBillLockRetryTask.java
new file mode 100644
index 00000000..f9ad88df
--- /dev/null
+++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeBillLockRetryTask.java
@@ -0,0 +1,53 @@
+/**
+ * BladeX Commercial License Agreement
+ * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
+ *
+ * Use of this software is governed by the Commercial License Agreement obtained after purchasing a license from BladeX.
+ *
+ * Author: Chill Zhuang (bladejava@qq.com)
+ */
+package org.springblade.transport.task;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springblade.transport.service.IKingdeeBillLockService;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+/**
+ * 金蝶单据锁定补偿定时任务
+ *
+ * 扫描锁定失败的锁记录(lock_status=failed 且未超重试上限)重调 lockBusiData,
+ * 补偿推送成功但锁单失败的单据,不依赖人工重推「同步金蝶」。
+ * 业务逻辑收敛在 {@link IKingdeeBillLockService#retryFailedLocks()},任务类只做开关与调度(模式同 PaymentKingdeeResultTask)。
+ *
+ * 调度配置(Nacos thirdParty.bfi.billLock.*):retryEnabled 开关、retryCron 表达式。
+ *
+ * @author Chill
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class KingdeeBillLockRetryTask {
+
+ private final IKingdeeBillLockService kingdeeBillLockService;
+
+ /** 锁定补偿开关,代码默认关闭(安全兜底),由 Nacos thirdParty.bfi.billLock.retryEnabled 按环境显式开启 */
+ @Value("${thirdParty.bfi.billLock.retryEnabled:false}")
+ private boolean retryEnabled;
+
+ /** 锁定补偿调度,默认每5分钟执行一次 */
+ @Scheduled(cron = "${thirdParty.bfi.billLock.retryCron:0 */5 * * * ?}")
+ public void retryFailedBillLocks() {
+ if (!retryEnabled) {
+ return;
+ }
+ try {
+ String summary = kingdeeBillLockService.retryFailedLocks();
+ log.info("定时补偿金蝶单据锁定:{}", summary);
+ } catch (Exception exception) {
+ log.error("定时补偿金蝶单据锁定异常", exception);
+ }
+ }
+}
diff --git a/doc/sql/transport/blade_kingdee_bill_lock_20261009.sql b/doc/sql/transport/blade_kingdee_bill_lock_20261009.sql
new file mode 100644
index 00000000..98c6ac33
--- /dev/null
+++ b/doc/sql/transport/blade_kingdee_bill_lock_20261009.sql
@@ -0,0 +1,32 @@
+-- 金蝶单据锁定记录表:lockBusiData 锁定 / unLockNotifyBusiData 解锁回调 / 锁定补偿
+-- 一行 = 一张金蝶目标单:锁定留痕(人/时间/失败原因)+ 当前锁状态(编辑守卫/回调定位/补偿扫表)
+-- lock_status:failed-锁定失败(补偿重试对象)/ locked-金蝶锁定中 / unlocked-已解锁(终态)
+
+CREATE TABLE `blade_kingdee_bill_lock` (
+ `id` bigint NOT NULL COMMENT '主键',
+ `tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户ID',
+ `source_bill_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'WLTMS单据类型:SETTLEMENT正式结算单/INVOICE_APPLY开票申请单(预留PAY_APPLY付款申请单/RECEIVED_INVOICE收票单)',
+ `source_bill_id` bigint NOT NULL COMMENT 'WLTMS业务主键',
+ `source_bill_no` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'WLTMS单号',
+ `target_bill_type` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '金蝶单据类型:ApFin_other_BT_S应付单/arfin_other_BT_S应收单/cdm_paybill_bank_elect_BT_S开票单等固定值',
+ `target_bill_no` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '金蝶单据编号(开票申请按发票张次逐张一行)',
+ `lock_status` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '锁状态:failed锁定失败/locked金蝶锁定中/unlocked已解锁',
+ `operation_type` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '最近动作来源操作类型:SAVE单据新增/DELETE单据删除',
+ `try_count` int DEFAULT '0' COMMENT '锁定重试次数(补偿任务用,超上限停试)',
+ `lock_time` datetime DEFAULT NULL COMMENT '锁定时间',
+ `unlock_time` datetime DEFAULT NULL COMMENT '解锁时间',
+ `lock_operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '推单发起人(锁定操作人)',
+ `notify_operator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '回调操作人(金蝶侧操作人)',
+ `fail_reason` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '锁定失败原因',
+ `create_user` bigint DEFAULT NULL COMMENT '创建人',
+ `create_dept` bigint DEFAULT NULL COMMENT '创建部门',
+ `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+ `update_user` bigint DEFAULT NULL COMMENT '修改人',
+ `update_time` datetime DEFAULT NULL COMMENT '修改时间',
+ `status` int DEFAULT NULL COMMENT '状态',
+ `is_deleted` int DEFAULT '0' COMMENT '是否已删除',
+ PRIMARY KEY (`id`),
+ INDEX `idx_kbl_source` (`source_bill_type`, `source_bill_id`),
+ INDEX `idx_kbl_target` (`target_bill_type`, `target_bill_no`),
+ INDEX `idx_kbl_status` (`lock_status`, `try_count`)
+) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '金蝶单据锁定记录' ROW_FORMAT = DYNAMIC;