From f23e24e80858cceee68b92d0d7fd4b1cf62f92b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=88=98=E6=B3=89=E4=BD=B3?= <1107186916@qq.com> Date: Thu, 15 Oct 2026 11:29:11 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BF=AE=E6=AD=A3=E9=87=91=E8=9D=B6?= =?UTF-8?q?=E8=A7=A3=E9=94=81=E5=9B=9E=E8=B0=83=E5=8D=95=E5=8F=B7=E5=AE=9A?= =?UTF-8?q?=E4=BD=8D=E5=B9=B6=E6=96=B0=E5=A2=9E=E8=A7=A3=E9=94=81=E7=BB=88?= =?UTF-8?q?=E6=80=81=E9=A1=B5=E9=9D=A2=E6=A0=87=E8=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 回调方向与锁定相反:按金蝶单号+我方单号双条件反查最新locked锁记录(targetBillType回调为空不参与匹配) - 回调处理批量化:一次IN查询+updateBatchById批量落库,消除逐张查询/更新N+1 - 新增KingdeeUnlockMark展示契约与fillUnlockMarks通用填充,付款申请/结算单/开票申请VO回填解锁时间、操作人、动作名称 - 更新金蝶单据锁定与解锁业务流程文档(回调方向、解锁后数据变化、事务语义、联调实测结论) --- .../transport/pojo/vo/FormalSettlementVO.java | 9 +- .../pojo/vo/InvoiceApplicationVO.java | 9 +- .../transport/pojo/vo/KingdeeUnlockMark.java | 36 +++++++ .../pojo/vo/PaymentApplicationVO.java | 9 +- .../service/IKingdeeBillLockService.java | 31 +++++- .../impl/FormalSettlementServiceImpl.java | 2 + .../impl/InvoiceApplicationServiceImpl.java | 2 + .../impl/KingdeeBillLockServiceImpl.java | 86 +++++++++++++---- .../impl/PaymentApplicationServiceImpl.java | 2 + ...BusiData锁定-unLockNotifyBusiData回调-失败补偿-删单还原).md | 94 ++++++++++++++----- 10 files changed, 233 insertions(+), 47 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeUnlockMark.java diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java index 5929669a..58805d53 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/FormalSettlementVO.java @@ -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 sources; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java index c54945aa..71a38aba 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/InvoiceApplicationVO.java @@ -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 settlements; @TableField(exist = false) private List sheets; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeUnlockMark.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeUnlockMark.java new file mode 100644 index 00000000..ee81225d --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/KingdeeUnlockMark.java @@ -0,0 +1,36 @@ +/** + * 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.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); +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java index d8bc998f..aa74d1f5 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/PaymentApplicationVO.java @@ -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 invoices; @TableField(exist = false) private List paymentRecords; @TableField(exist = false) private List settlements; diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IKingdeeBillLockService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IKingdeeBillLockService.java index 7af9adfc..6438f026 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IKingdeeBillLockService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IKingdeeBillLockService.java @@ -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 { String lockAfterPush(String sourceBillType, Long sourceBillId, String sourceBillNo, String targetBillType, List 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 { */ Map mapActiveLocks(String sourceBillType, List sourceBillIds); + /** + * 批量查源单解锁终态留痕(供列表/详情页填充解锁标识): + * 每源单取最新一条 unlocked 锁记录(付款申请解锁后不作废,页面据此区分「金蝶删单/解锁」与普通推送失败) + * + * @param sourceBillType WLTMS 单据类型 + * @param sourceBillIds 源单主键集合 + * @return sourceBillId -> 最新 unlocked 锁记录,无解锁留痕的源单不在 Map 中 + */ + Map mapUnlockedLocks(String sourceBillType, List sourceBillIds); + + /** + * 批量填充金蝶解锁终态标识(通用):对无活跃锁的行反查最新 unlocked 锁记录并回填解锁标识 + * (解锁时间/操作人/动作名称),供前端展示明显解锁标识;有活跃锁的单必非解锁终态,跳过。 + * 与 mapActiveLocks 配对在各单据 fillKingdeeLockStatus 中调用,列表/详情共用; + * 后续单据(如收票单)接入锁单时,VO 实现 KingdeeUnlockMark 即可复用 + * + * @param sourceBillType WLTMS 单据类型 + * @param rows 已填充活跃锁标识的 VO 行 + * @param activeLockMap 活跃锁 Map(mapActiveLocks 返回值) + * @param <T> 实现 KingdeeUnlockMark 的 VO 类型(须继承 BaseEntity 以取主键) + */ + void fillUnlockMarks(String sourceBillType, List rows, Map activeLockMap); + /** * 锁定补偿:扫 failed 且 try_count 未达重试上限(RETRY_MAX_COUNT)的锁记录,逐张重调 lockBusiData * (接口 1对1:单次一个源单号对一个金蝶单号,单张成败互不影响;定时任务定时触发,亦可人工触发); 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 d3f7a922..cc72d33b 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 @@ -233,6 +233,8 @@ public class FormalSettlementServiceImpl extends BaseServiceImpllambdaQuery() - .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 kingdeeBillNos = Func.toStrList(param.getSourceBillNo()); + Map latestLockMap = this.list(Wrappers.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 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 second)); } + @Override + public Map mapUnlockedLocks(String sourceBillType, List sourceBillIds) { + if (Func.isEmpty(sourceBillIds)) { + return Map.of(); + } + List lockRecords = this.list(Wrappers.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 void fillUnlockMarks(String sourceBillType, List rows, Map activeLockMap) { + if (Func.isEmpty(rows)) { + return; + } + List unlockedIds = rows.stream() + .filter(row -> !activeLockMap.containsKey(row.getId())) + .map(BaseEntity::getId).toList(); + if (Func.isEmpty(unlockedIds)) { + return; + } + Map 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:单次一个源单号对一个金蝶单号,单张成败互不影响,由定时任务定时触发,亦可人工触发)。 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index ccac6a62..742b5931 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -240,6 +240,8 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl