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 a5446b8e..5929669a 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 @@ -44,6 +44,12 @@ public class FormalSettlementVO extends FormalSettlement { @TableField(exist = false) private Integer attachmentCount; @TableField(exist = false) private String attachmentSyncStatus; @TableField(exist = false) private String attachmentSyncStatusName; + @TableField(exist = false) @Schema(description = "金蝶锁状态:locked-锁定中/failed-锁定失败;unlocked为终态不返回,null=无锁或已解锁") + private String kingdeeLockStatus; + @TableField(exist = false) @Schema(description = "金蝶锁失败原因(仅锁定失败时有值)") + private String kingdeeLockFailReason; + @TableField(exist = false) @Schema(description = "金蝶锁重试次数(达上限5后不再自动补偿,需重推同步金蝶)") + private Integer kingdeeTryCount; @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 cfc9ebcb..c54945aa 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 @@ -52,6 +52,9 @@ public class InvoiceApplicationVO extends InvoiceApplication { @TableField(exist = false) private Integer attachmentCount; @TableField(exist = false) private String attachmentSyncStatus; @TableField(exist = false) private String attachmentSyncStatusName; + @TableField(exist = false) private String kingdeeLockStatus; + @TableField(exist = false) private String kingdeeLockFailReason; + @TableField(exist = false) private Integer kingdeeTryCount; @TableField(exist = false) private String createUserName; @TableField(exist = false) private List settlements; @TableField(exist = false) private List sheets; 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 4385f234..7af9adfc 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 @@ -13,6 +13,7 @@ import org.springblade.thirdparty.bfi.pojo.dto.BfiBillLockDTO; import org.springblade.transport.pojo.entity.KingdeeBillLock; import java.util.List; +import java.util.Map; /** * 金蝶单据锁定记录服务 @@ -24,7 +25,8 @@ public interface IKingdeeBillLockService extends IService { /** * 推送金蝶成功后锁单(由各推送链路在主单事务提交后调用): * 每张金蝶目标单一行锁记录(先落 failed 再调 lockBusiData,成功翻 locked); - * 同源单的目标单合并为一次调用(编号逗号拼接批量传参);已 locked 幂等跳过,failed 原地重锁并重置补偿计数; + * 锁单接口 1对1:逐张调用(单次一个源单号对一个金蝶单号),单张成败互不影响; + * 已 locked 幂等跳过,failed 原地重锁并重置补偿计数; * 异常独立捕获,不影响主单同步状态 * * @param sourceBillType WLTMS 单据类型(SETTLEMENT/INVOICE_APPLY) @@ -55,9 +57,22 @@ public interface IKingdeeBillLockService extends IService { void checkEditable(String sourceBillType, Long sourceBillId); /** - * 锁定补偿:扫 failed 且未超重试上限的锁记录重调 lockBusiData(由定时任务调用) + * 批量查源单当前锁状态(供列表/详情页填充展示标识): + * 每源单取最新一条非终态锁记录(locked/failed),unlocked 为终态不返回(前端视为无锁/已解锁) * - * @return 补偿摘要 + * @param sourceBillType WLTMS 单据类型 + * @param sourceBillIds 源单主键集合 + * @return sourceBillId -> 最新锁记录,无活跃锁的源单不在 Map 中 + */ + Map mapActiveLocks(String sourceBillType, List sourceBillIds); + + /** + * 锁定补偿:扫 failed 且 try_count 未达重试上限(RETRY_MAX_COUNT)的锁记录,逐张重调 lockBusiData + * (接口 1对1:单次一个源单号对一个金蝶单号,单张成败互不影响;定时任务定时触发,亦可人工触发); + * 成功翻 locked,失败 try_count+1 并记失败原因; + * 计数烧满上限后停止自动补偿(人工重推「同步金蝶」会重置计数重新接管)。锁记录落库均为批量更新 + * + * @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 3febe7b9..70bc94fc 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 @@ -42,6 +42,7 @@ 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.pojo.entity.KingdeeBillLock; import org.springblade.transport.constant.TransportBusiConstant; import org.springblade.transport.mapper.ReceivablePayableDetailMapper; import org.springblade.transport.mapper.ReceivablePayableCargoFeeMapper; @@ -209,9 +210,34 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl result = page(page, wrapper.orderByDesc(FormalSettlement::getCreateTime)); IPage voPage = result.convert(this::toVO); fillAttachmentSyncStatus(voPage.getRecords()); + fillKingdeeLockStatus(voPage.getRecords()); return voPage; } + /** + * 填充金蝶锁状态标识(locked-锁定中/failed-锁定失败;unlocked 终态不返回): + * 锁表按本页源单一次批量查询,避免 N+1;locked 时前端可禁用编辑/作废, + * failed 时展示失败原因(tryCount 达上限 5 提示重推同步金蝶) + * + * @param rows 本页 VO 列表 + */ + private void fillKingdeeLockStatus(List rows) { + if (Func.isEmpty(rows)) { + return; + } + Map lockMap = kingdeeBillLockService.mapActiveLocks(KingdeeLockConstant.SOURCE_SETTLEMENT, + rows.stream().map(FormalSettlementVO::getId).toList()); + for (FormalSettlementVO vo : rows) { + KingdeeBillLock lockRecord = lockMap.get(vo.getId()); + if (lockRecord == null) { + continue; + } + vo.setKingdeeLockStatus(lockRecord.getLockStatus()); + vo.setKingdeeLockFailReason(lockRecord.getFailReason()); + vo.setKingdeeTryCount(lockRecord.getTryCount()); + } + } + /** * 填充附件同步聚合态(无附件/未上传/部分未传/已上传):按结算类型分业务类型一次 GROUP BY 查询,避免 N+1 * @@ -280,6 +306,7 @@ public class FormalSettlementServiceImpl extends BaseServiceImpl sources = sourceMapper.selectList(Wrappers.lambdaQuery() .eq(FormalSettlementSource::getFormalSettlementId, id).orderByAsc(FormalSettlementSource::getCreateTime)); vo.setSources(sources); 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 6afc2bdc..b5b3cd00 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 @@ -56,6 +56,7 @@ 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.pojo.entity.KingdeeBillLock; import org.springblade.transport.constant.TransportBusiConstant; import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.CustomerArchiveMapper; @@ -180,9 +181,34 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl voPage = page(page, wrapper).convert(this::toListVO); fillAttachmentSyncStatus(voPage.getRecords()); + fillKingdeeLockStatus(voPage.getRecords()); return voPage; } + /** + * 填充金蝶锁状态标识(locked-锁定中/failed-锁定失败;unlocked 终态不返回): + * 锁表按本页源单一次批量查询,避免 N+1;locked 时前端可禁用编辑/作废, + * failed 时展示失败原因(tryCount 达上限 5 提示重推同步金蝶) + * + * @param rows 本页 VO 列表 + */ + private void fillKingdeeLockStatus(List rows) { + if (Func.isEmpty(rows)) { + return; + } + Map lockMap = kingdeeBillLockService.mapActiveLocks(KingdeeLockConstant.SOURCE_INVOICE_APPLY, + rows.stream().map(InvoiceApplicationVO::getId).toList()); + for (InvoiceApplicationVO vo : rows) { + KingdeeBillLock lockRecord = lockMap.get(vo.getId()); + if (lockRecord == null) { + continue; + } + vo.setKingdeeLockStatus(lockRecord.getLockStatus()); + vo.setKingdeeLockFailReason(lockRecord.getFailReason()); + vo.setKingdeeTryCount(lockRecord.getTryCount()); + } + } + /** * 填充附件同步聚合态(无附件/未上传/部分未传/已上传):同步表按本页 biz_id 一次 GROUP BY 查询,避免 N+1 * @@ -208,6 +234,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(InvoiceApplicationSettlement::getInvoiceApplicationId, id) .orderByAsc(InvoiceApplicationSettlement::getCreateTime))); @@ -546,7 +573,7 @@ public class InvoiceApplicationServiceImpl extends BaseServiceImpl - * 锁定:推送链路事务提交后按源单分组批量 LOCK(每组一行到多行锁记录,先落 failed 再调 lockBusiData, - * 成功翻 locked),失败由补偿定时任务重试;解锁:金蝶 unLockNotifyBusiData 回调(支持逗号拼接批量), + * 锁定:推送链路事务提交后逐张 LOCK(接口 1对1:单次一个源单号对一个金蝶单号,每张先落 failed 行占位再调 + * lockBusiData,成功翻 locked;单张成败互不影响),失败由补偿定时任务重试;落库统一批量更新。 + * 解锁:金蝶 unLockNotifyBusiData 回调(支持逗号拼接批量), * DELETE 时分发废除源单并还原关联数据。 * 与结算单/开票申请服务互相依赖,为避免构造器循环依赖,单据服务经 {@code SpringUtil.getBean} 运行时获取 * (与本工程 doSyncKingdee 自代理同一惯用法) @@ -105,31 +106,36 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl lockedRecords = new ArrayList<>(); + List failedRecords = new ArrayList<>(); + for (KingdeeBillLock lockRecord : pendingRecords) { + try { + bfiService.lockBusiData(this.buildLockParam(sourceBillType, sourceBillNo, targetBillType, lockRecord.getTargetBillNo(), operator)); 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())); - return null; - } catch (Exception exception) { - String failReason = limitText(exception.getMessage(), 500); - for (KingdeeBillLock lockRecord : pendingRecords) { + lockedRecords.add(lockRecord); + log.info("金蝶单据锁定成功:源单[{}:{}],金蝶单[{}]", sourceBillType, sourceBillNo, lockRecord.getTargetBillNo()); + } catch (Exception exception) { + String failReason = limitText(exception.getMessage(), 500); lockRecord.setTryCount((lockRecord.getTryCount() == null ? 0 : lockRecord.getTryCount()) + 1); lockRecord.setFailReason(failReason); - this.updateById(lockRecord); + failedRecords.add(lockRecord); + log.error("金蝶单据锁定失败(待补偿重锁):源单[{}:{}],金蝶单[{}],原因:{}", sourceBillType, sourceBillNo, lockRecord.getTargetBillNo(), failReason, exception); } - String failedNos = pendingRecords.stream().map(KingdeeBillLock::getTargetBillNo).collect(Collectors.joining(",")); - log.error("金蝶单据批量锁定失败(待补偿重锁):源单[{}:{}],金蝶单{}张[{}]", sourceBillType, sourceBillNo, pendingRecords.size(), failedNos, exception); - return failedNos; } + this.updateBatchById(lockedRecords); + this.updateBatchById(failedRecords); + if (failedRecords.isEmpty()) { + log.info("金蝶单据锁定全部成功:源单[{}:{}],金蝶单{}张[{}]", sourceBillType, sourceBillNo, lockedRecords.size(), + lockedRecords.stream().map(KingdeeBillLock::getTargetBillNo).collect(Collectors.joining(","))); + return null; + } + String failedNos = failedRecords.stream().map(KingdeeBillLock::getTargetBillNo).collect(Collectors.joining(",")); + log.warn("金蝶单据锁定汇总:源单[{}:{}],成功{}张,失败{}张[{}]", sourceBillType, sourceBillNo, lockedRecords.size(), failedRecords.size(), failedNos); + return failedNos; } @Override @@ -219,6 +225,26 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl mapActiveLocks(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) + .in(KingdeeBillLock::getLockStatus, List.of(KingdeeLockConstant.STATUS_LOCKED, KingdeeLockConstant.STATUS_FAILED)) + .orderByAsc(KingdeeBillLock::getId)); + // 升序取回后写覆盖:每源单保留最新一条;unlocked 终态不参与查询 + return lockRecords.stream().collect(Collectors.toMap(KingdeeBillLock::getSourceBillId, Function.identity(), (first, second) -> second)); + } + + /** + * 锁定补偿:扫 failed 且 try_count 未达重试上限(RETRY_MAX_COUNT)的锁记录,逐张重调 lockBusiData + * (接口 1对1:单次一个源单号对一个金蝶单号,单张成败互不影响,由定时任务定时触发,亦可人工触发)。 + *

成功翻 locked(补锁时间、清失败原因);失败 try_count+1 并记失败原因,计数烧满上限后不再自动重试 + * (人工重推「同步金蝶」重置计数重新接管);成败落库均为批量更新(updateBatchById),避免逐张 update + */ @Override public String retryFailedLocks() { List failedList = this.list(Wrappers.lambdaQuery() @@ -227,49 +253,49 @@ public class KingdeeBillLockServiceImpl extends ServiceImpl> 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); + log.info("金蝶单据锁定补偿开始:待补偿{}张", failedList.size()); + LocalDateTime now = LocalDateTime.now(); + List lockedRecords = new ArrayList<>(); + List failedRecords = new ArrayList<>(); + for (KingdeeBillLock lockRecord : failedList) { + String operator = Func.isBlank(lockRecord.getLockOperator()) ? "system" : lockRecord.getLockOperator(); 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(); + bfiService.lockBusiData(this.buildLockParam(lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(), + lockRecord.getTargetBillType(), lockRecord.getTargetBillNo(), operator)); + lockRecord.setLockStatus(KingdeeLockConstant.STATUS_LOCKED); + lockRecord.setLockTime(now); + lockRecord.setFailReason(null); + lockedRecords.add(lockRecord); + log.info("金蝶单据锁定补偿成功:源单[{}:{}],金蝶单[{}]", + lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(), lockRecord.getTargetBillNo()); } 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); + lockRecord.setTryCount((lockRecord.getTryCount() == null ? 0 : lockRecord.getTryCount()) + 1); + lockRecord.setFailReason(failReason); + failedRecords.add(lockRecord); + log.error("金蝶单据锁定补偿失败(try_count已+1,待下次补偿):源单[{}:{}],金蝶单[{}],原因:{}", + lockRecord.getSourceBillType(), lockRecord.getSourceBillNo(), lockRecord.getTargetBillNo(), failReason, exception); } } - return "待补偿" + failedList.size() + "张,成功" + success + "张"; + this.updateBatchById(lockedRecords); + this.updateBatchById(failedRecords); + log.info("金蝶单据锁定补偿结束:待补偿{}张,成功{}张,仍失败{}张[{}]", failedList.size(), lockedRecords.size(), failedRecords.size(), + failedRecords.stream().map(KingdeeBillLock::getTargetBillNo).collect(Collectors.joining(","))); + return "待补偿" + failedList.size() + "张,成功" + lockedRecords.size() + "张"; } /** * 组装 lockBusiData 参数:WLTMS 调金蝶,源系统取配置(默认 WLTMS),目标系统 XH, - * 操作类型固定 LOCK/SAVE,多张目标单编号逗号拼接批量传参 + * 操作类型固定 LOCK/SAVE;接口 1对1:单次仅一个源单号对一个金蝶单号 * * @param sourceBillType WLTMS 单据类型 * @param sourceBillNo WLTMS 单号 * @param targetBillType 金蝶目标单据类型 - * @param targetBillNos 金蝶单据编号集合 + * @param targetBillNo 金蝶单据编号(单张) * @param operator 操作人 * @return 锁单参数 */ - private BfiBillLockDTO buildLockParam(String sourceBillType, String sourceBillNo, String targetBillType, List targetBillNos, String operator) { + private BfiBillLockDTO buildLockParam(String sourceBillType, String sourceBillNo, String targetBillType, String targetBillNo, String operator) { BfiBillLockDTO param = new BfiBillLockDTO(); param.setLockAction(KingdeeLockConstant.ACTION_LOCK); param.setSourceSystem(Func.isBlank(bfiProperties.getSrcSys()) ? KingdeeLockConstant.SYSTEM_WLTMS : bfiProperties.getSrcSys()); @@ -278,7 +304,7 @@ public class KingdeeBillLockServiceImpl extends ServiceImpllockBusiData:WLTMS 调金蝶,sourceSystem=WLTMS、targetSystem=XH、lockAction=LOCK; * unLockNotifyBusiData:金蝶调 WLTMS,sourceSystem=XH、targetSystem=WLTMS、lockAction=UNLOCK, * operationType=DELETE 时 WLTMS 侧需废除源单并还原关联数据。 - * 批量传参约定:多张单据编号以英文逗号拼接(sourceBillNo 与 targetBillNo 按顺序一一对应), + * 传参约定:接口 1对1,单次仅一个源单号(sourceBillNo)对一个金蝶单号(targetBillNo), * time/operator 按接口文档为选传字段(示例报文未携带),非空时才送

* * @author Chill @@ -64,19 +64,19 @@ public class BfiBillLockDTO extends BfiBaseDTO { */ private String operationType; /** - * 【必填】源单据类型:调用方单据类型(WLTMS 自定义:SETTLEMENT/INVOICE_APPLY,金蝶固定:ApFin_other_BT_S 等),多张同类型 + * 【必填】源单据类型:调用方单据类型(WLTMS 自定义:SETTLEMENT/INVOICE_APPLY,金蝶固定:ApFin_other_BT_S 等) */ private String sourceBillType; /** - * 【必填】源单据编号,多张英文逗号拼接 + * 【必填】源单据编号,与 targetBillNo 一对一 */ private String sourceBillNo; /** - * 【必填】目标单据类型:接收方单据类型(金蝶固定:ApFin_other_BT_S 等,WLTMS 自定义:SETTLEMENT 等),多张同类型 + * 【必填】目标单据类型:接收方单据类型(金蝶固定:ApFin_other_BT_S 等,WLTMS 自定义:SETTLEMENT 等) */ private String targetBillType; /** - * 【必填】目标单据编号,多张英文逗号拼接,与 sourceBillNo 顺序一一对应 + * 【必填】目标单据编号,与 sourceBillNo 一对一 */ private String targetBillNo; /** diff --git a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java index 43282b71..50a26ba2 100644 --- a/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java +++ b/blade-third-party-api/blade-bfi-api/src/main/java/org/springblade/thirdparty/bfi/service/IBfiService.java @@ -317,7 +317,7 @@ public interface IBfiService { BfiAttachmentUploadVO uploadAttachment(String entityNumber, String billPkId, String controlKey, String fileName, byte[] fileContent); /** - * 单据锁定(lockBusiData):推送金蝶成功后锁定目标单据,多张单据编号逗号拼接批量传参, + * 单据锁定(lockBusiData):推送金蝶成功后锁定目标单据,接口 1对1(单次一个源单号对一个金蝶单号), * 解锁结果由金蝶回调 unLockNotifyBusiData 通知 * * @param param 锁单参数,操作类型固定 LOCK,源系统 WLTMS、目标系统 XH diff --git a/doc/docs/业务流程/2026-10-09-金蝶单据锁定与解锁业务流程(lockBusiData锁定-unLockNotifyBusiData回调-失败补偿-删单还原).md b/doc/docs/业务流程/2026-10-09-金蝶单据锁定与解锁业务流程(lockBusiData锁定-unLockNotifyBusiData回调-失败补偿-删单还原).md index 232f1e44..f20c0087 100644 --- a/doc/docs/业务流程/2026-10-09-金蝶单据锁定与解锁业务流程(lockBusiData锁定-unLockNotifyBusiData回调-失败补偿-删单还原).md +++ b/doc/docs/业务流程/2026-10-09-金蝶单据锁定与解锁业务流程(lockBusiData锁定-unLockNotifyBusiData回调-失败补偿-删单还原).md @@ -1,6 +1,6 @@ # 金蝶单据锁定与解锁业务流程(lockBusiData 锁定 - unLockNotifyBusiData 回调 - 失败补偿 - 删单还原) -- 日期:2026-10-09 +- 日期:2026-10-09(2026-10-10 变更:lockBusiData 确认为 1对1 接口,不支持逗号拼接批量,锁定/补偿改为逐张调用) - 范围:正式结算单(应付 A102-2 / 应收 A302-2)、开票申请单(A303-1)推送金蝶成功后的单据锁定、金蝶解锁/删单回调、锁定失败定时补偿 - 设计文档:`docs/superpowers/specs/2026-10-09-kingdee-bill-lock-unlock-design.md` - 锁记录表:`blade_kingdee_bill_lock`(一行 = 一张金蝶目标单) @@ -15,7 +15,7 @@ - lockBusiData:WLTMS 调金蝶,源系统 WLTMS、目标系统 XH、lockAction=LOCK - unLockNotifyBusiData:金蝶调 WLTMS,源系统 XH、目标系统 WLTMS、lockAction=UNLOCK -- 批量约定:多张单据编号英文逗号拼接(sourceBillNo 与 targetBillNo 顺序一一对应),同一源单的多张金蝶单合并一次调用 +- 传参约定:接口 1对1,单次仅一个源单号(sourceBillNo)对一个金蝶单号(targetBillNo);多张金蝶单(开票申请一张主单对 N 张金蝶单)逐张调用,单张成败互不影响,成败结果批量落库 - 出入参同构,应答为 `{updateCount, updateResultList}` 裸结构(逐单 1-成功/0-失败) ## 二、主线流程 @@ -28,9 +28,9 @@ │ └─ 事务提交 ├─ 锁定(lockAfterPush): │ ├─ 每张金蝶目标单一行锁记录,先落 failed 占位(保证补偿扫表无遗漏) - │ ├─ 同源单多张目标单合并一次调用:LOCK + SAVE,编号逗号拼接 - │ ├─ 应答 updateCount>0 → 全部翻 locked ──→ 单据冻结 - │ └─ 失败/超时 → 保持 failed,tryCount+1,失败原因落 fail_reason(主单同步状态不受影响) + │ ├─ 接口1对1:逐张调用 LOCK + SAVE,单张成败互不影响 + │ ├─ 单张应答成功 → 该张翻 locked ──→ 单据冻结 + │ └─ 单张失败/超时 → 保持 failed,tryCount+1,失败原因落 fail_reason(主单同步状态不受影响) └─ 附件联带上传(A902,原有动作,后置不受影响) ``` @@ -42,7 +42,7 @@ ### 2. 金蝶侧解锁回调(金蝶 → WLTMS) -金蝶在星瀚侧对单据解锁或删单后,回调 `POST /kingdee-bfi/unLockNotifyBusiData`(`UNLOCK` + 逗号拼接批量): +金蝶在星瀚侧对单据解锁或删单后,回调 `POST /kingdee-bfi/unLockNotifyBusiData`(`UNLOCK`;targetBillNo 兼容单张/逗号拼接): ``` 校验:sourceSystem=XH、targetSystem=WLTMS、lockAction=UNLOCK(否则应答失败) @@ -69,7 +69,7 @@ ``` KingdeeBillLockRetryTask(thirdParty.bfi.billLock.retryEnabled,默认每5分钟) └─ 扫 lock_status=failed 且 try_count<5 的锁记录 - ├─ 按源单分组,组内合并一次 lockBusiData 重锁 + ├─ 接口1对1:逐张重调 lockBusiData,单张成败互不影响;成败结果批量落库 ├─ 成功 → 翻 locked,进入正常锁定态 └─ 失败 → try_count+1,超过 5 次停试,留 fail_reason 待人工排查 ``` @@ -103,9 +103,13 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked 1. **先占位再锁**:锁记录落库先于远程调用,补偿任务扫 `failed` 就是完整的待重试集合,不漏单 2. **锁失败不回滚推送**:金蝶单已生成,本地回滚只会制造断链;代价是短暂"已推未锁"窗口,由补偿任务闭合 -3. **批量同成败**:接口应答只有聚合 updateCount,无逐单成败,整批一起翻状态,最简且不错账 +3. **1对1逐张锁**:lockBusiData 单次仅支持一个源单号对一个金蝶单号,逐张调用互不连坐;成败结果 updateBatchById 批量落库,避免逐张 update 4. **回调幂等**:按 targetBillType+targetBillNo 找最新 locked 记录,找不到/已 unlocked 直接跳过应答成功,防金蝶重试风暴 5. **DELETE 还原口径**:作废源单 + 清金蝶同步信息 + 关联状态刷新;作废是终态,重推需新单重走流程(口径待业务联调确认) +6. **前端锁状态展示**(2026-10-10 新增):结算单/开票申请列表与详情 VO 填充 `kingdeeLockStatus`/`kingdeeLockFailReason`/`kingdeeTryCount` + (后端 `mapActiveLocks` 每页一次批量查锁表,无 N+1;unlocked 终态不返回即 null=无锁/已解锁)。 + 前端渲染:`locked` 红 tag「金蝶锁定中」并禁用编辑/作废(后端守卫是权威);`failed` 且 `tryCount<5` 橙 tag「锁定失败·补偿中」tooltip 显示原因; + `failed` 且 `tryCount≥5` 红 tag「锁定失败·已达重试上限」提示重推同步金蝶;null 不显示 ## 五、代码落点速查 @@ -113,6 +117,7 @@ failed(锁定失败,待补偿)→ locked(金蝶锁定中)→ unlocked | --- | --- | | 锁单 Feign/DTO/应答 VO | `blade-bfi-api`:`BfiBillLockDTO`、`BfiBillLockResultVO`、`IBfiClient.lockBusiData`、`IBfiService.lockBusiData` | | 锁类型常量与映射 | `transport/constant/KingdeeLockConstant` | +| 锁状态展示填充 | `IKingdeeBillLockService.mapActiveLocks`(批量查活跃锁)+ `FormalSettlementServiceImpl/InvoiceApplicationServiceImpl.fillKingdeeLockStatus`(列表/详情填充)+ 两个 VO 的 `kingdeeLockStatus/kingdeeLockFailReason/kingdeeTryCount` 字段 | | 锁记录实体/服务 | `KingdeeBillLock`、`IKingdeeBillLockService` + `KingdeeBillLockServiceImpl` | | 回调端点 | `KingdeeBfiPublicController`:`POST /kingdee-bfi/unLockNotifyBusiData`(PERMIT_ALL + TenantIgnore) | | 补偿定时任务 | `KingdeeBillLockRetryTask`(`thirdParty.bfi.billLock.retryEnabled`,默认每5分钟) |