From e08be9a297ce09fd93f206a904e30a674a4e314d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Fri, 9 Oct 2026 19:52:15 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=AD=A3=E5=BC=8F=E7=BB=93=E7=AE=97?= =?UTF-8?q?=E5=8D=95/=E5=BC=80=E7=A5=A8=E7=94=B3=E8=AF=B7=E5=90=8C?= =?UTF-8?q?=E6=AD=A5=E9=87=91=E8=9D=B6=E5=90=8E=E8=81=94=E5=B8=A6=E9=94=81?= =?UTF-8?q?=E5=8D=95=EF=BC=8C=E6=94=AF=E6=8C=81=E8=A7=A3=E9=94=81=E5=9B=9E?= =?UTF-8?q?=E8=B0=83=E4=B8=8E=E9=87=91=E8=9D=B6=E5=88=A0=E5=8D=95=E8=BF=98?= =?UTF-8?q?=E5=8E=9F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/entity/KingdeeBillLock.java | 87 +++++ .../constant/KingdeeLockConstant.java | 91 ++++++ .../KingdeeBfiPublicController.java | 67 ++++ .../mapper/KingdeeBillLockMapper.java | 18 ++ .../service/IFormalSettlementService.java | 9 + .../service/IInvoiceApplicationService.java | 9 + .../service/IKingdeeBillLockService.java | 62 ++++ .../impl/FormalSettlementServiceImpl.java | 60 ++++ .../impl/InvoiceApplicationServiceImpl.java | 61 ++++ .../impl/KingdeeBillLockServiceImpl.java | 306 ++++++++++++++++++ .../task/KingdeeBillLockRetryTask.java | 53 +++ .../blade_kingdee_bill_lock_20261009.sql | 32 ++ 12 files changed, 855 insertions(+) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/KingdeeBillLock.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/constant/KingdeeLockConstant.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/controller/KingdeeBfiPublicController.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeBillLockMapper.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/IKingdeeBillLockService.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/KingdeeBillLockServiceImpl.java create mode 100644 blade-service/blade-transport/src/main/java/org/springblade/transport/task/KingdeeBillLockRetryTask.java create mode 100644 doc/sql/transport/blade_kingdee_bill_lock_20261009.sql 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;