From a473387574845a3b24fde8d873c63219f8b258bd Mon Sep 17 00:00:00 2001 From: rcx <3084692334@qq.com> Date: Tue, 15 Sep 2026 13:37:02 +0800 Subject: [PATCH] =?UTF-8?q?fix(oms):=20=E8=80=81=E9=A2=84=E7=BA=A6?= =?UTF-8?q?=E5=8D=95(=E5=AE=A2=E6=88=B7=E9=A2=84=E7=BA=A6)=E4=B8=8B?= =?UTF-8?q?=E5=8F=91=E7=94=9F=E6=88=90=E7=9A=84=E4=B8=9A=E5=8A=A1=E5=8D=95?= =?UTF-8?q?=E8=A1=A5=E4=B8=8B=E5=8D=95=E5=86=BB=E7=BB=93=E9=92=A9=E5=AD=90?= =?UTF-8?q?,=20=E6=B6=88=E9=99=A4=E6=9C=80=E5=90=8E=E4=B8=80=E4=B8=AA?= =?UTF-8?q?=E4=B8=8D=E5=86=BB=E7=BB=93=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - issueOrder 路径原直存业务单绕过 insert 冻结钩子, 模式1仓库下不冻结; 排查确认入口活跃在用(App端/下发链路近期均有功能提交), 予以补齐 - 新增 resolveStockLockMode/buildStockLockModeJsonByEntities/tryFreezeOnLegacyIssue 公开复用方法, compensateUnfreeze 改 public - 下发顺序重排: 明细真实判定stamp → 单头JSON真实值 → 冻结(失败阻断下发, R2无批次同insert语义) → try-catch落库(失败补偿解冻, 与insert"冻结先行+补偿"模式一致) - 业务单号沿用老预约单号, 冻结幂等/取消下发/删除/反审解冻均以此单号闭环 - 至此模式1仓库下所有业务单入口(新增/批量/Excel单工作簿/多工作簿导入/编辑/老预约单下发)全部冻结 --- ...ReservationStockOutOrderDomainService.java | 86 ++++++++++++++++++- .../ReserveStockOutOrderDomainService.java | 32 ++++--- 2 files changed, 105 insertions(+), 13 deletions(-) diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/service/ReservationStockOutOrderDomainService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/service/ReservationStockOutOrderDomainService.java index 87ef47f9d..d35c93c7f 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/service/ReservationStockOutOrderDomainService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/service/ReservationStockOutOrderDomainService.java @@ -378,6 +378,90 @@ public class ReservationStockOutOrderDomainService { return JSON.toJSONString(warehouseJsonMap.values()); } + /** + * 行级库存锁定模式判定(公开供老预约单下发路径复用):1-下单冻结 2-分配库存冻结(默认) + */ + public Integer resolveStockLockMode(Long warehouseId) { + return isOrderFreezeWarehouse(warehouseId) ? 1 : 2; + } + + /** + * 实体版仓级库存锁定模式JSON(公开供老预约单下发路径复用): + * 按明细行仓库去重组装(LinkedHashMap 保序),行仓库为空取默认仓库; + * 明细行须已完成 stockLockMode stamp + */ + public String buildStockLockModeJsonByEntities(List details, Long defaultWarehouseId) { + if (CollectionUtils.isEmpty(details)) { + return null; + } + Map> warehouseJsonMap = new LinkedHashMap<>(); + for (ReservationOutMaterialDetail detail : details) { + Long warehouseId = detail.getWarehouseId() != null ? detail.getWarehouseId() : defaultWarehouseId; + warehouseJsonMap.computeIfAbsent(warehouseId, k -> { + Map item = new LinkedHashMap<>(); + item.put("warehouseId", k); + item.put("warehouseCode", detail.getWarehouseCode()); + item.put("warehouseName", detail.getWarehouseName()); + item.put("stockLockMode", detail.getStockLockMode() != null ? detail.getStockLockMode() : 2); + return item; + }); + } + return JSON.toJSONString(warehouseJsonMap.values()); + } + + /** + * 老预约单下发路径的下单冻结钩子(与 insert 的 tryFreezeOnCreate 同语义): + * 过滤模式 1 仓库明细 → R2 强制批次校验 → 调 WMS 冻结(可用-、冻结+,失败阻断下发)。 + * 幂等由 WMS 保证(同业务单号活跃冻结记录直接成功)。 + * + * @param businessOrderNumber 业务单号(老预约单路径=保留的老预约单号) + * @return true-已冻结(本地落库失败需补偿解冻 compensateUnfreeze);false-无模式 1 明细未冻结 + */ + public boolean tryFreezeOnLegacyIssue(String businessOrderNumber, List details, Long defaultWarehouseId) { + if (CollectionUtils.isEmpty(details)) { + return false; + } + List freezeDetails = new ArrayList<>(); + for (ReservationOutMaterialDetail detail : details) { + Long warehouseId = detail.getWarehouseId() != null ? detail.getWarehouseId() : defaultWarehouseId; + if (!isOrderFreezeWarehouse(warehouseId)) { + continue; + } + // ★R2:下单冻结必须知道冻结哪条库存行 + if (detail.getMaterialInventoryId() == null || detail.getMaterialInventoryId() <= 0) { + throw new ServiceException("出库单[" + (businessOrderNumber != null ? businessOrderNumber : "当前单据") + "]物料[" + + (detail.getMaterialCode() != null ? detail.getMaterialCode() : detail.getMaterialName()) + + "]未选择库存批次,下单冻结模式仓库必须先选择库存批次"); + } + OutMaterialDetail out = new OutMaterialDetail(); + out.setMaterialInventoryId(detail.getMaterialInventoryId()); + out.setMaterialBaseInfoId(detail.getMaterialBaseInfoId()); + out.setMaterialCode(detail.getMaterialCode()); + out.setMaterialName(detail.getMaterialName()); + out.setWarehouseId(warehouseId); + out.setQuantity(detail.getQuantity()); + out.setBatchNumber(detail.getBatchNumber()); + out.setBatchRefNo(detail.getBatchRefNo()); + out.setSheetRefNo(detail.getSheetRefNo()); + out.setBoxPalletNo(detail.getBoxPalletNo()); + out.setUniqueId(detail.getUniqueId()); + freezeDetails.add(out); + } + if (freezeDetails.isEmpty()) { + return false; + } + OmsFreezeInventoryParam param = new OmsFreezeInventoryParam(); + param.setOutOrderNumber(businessOrderNumber); + param.setDetails(freezeDetails); + fillOperator(param); + AjaxResult result = wmsServiceFeign.omsFreezeInventory(param); + if (result == null || !"200".equals(String.valueOf(result.get("code")))) { + throw new ServiceException("下单冻结库存失败,无法下发出库单:" + + (result == null ? "WMS服务不可用" : result.get("msg"))); + } + return true; + } + /** * 出库单保存时下单冻结:筛选下单冻结模式仓库的明细,先冻结WMS库存再落库(失败抛异常阻断保存)。 * ★下单冻结模式仓库明细强制必选库存批次:未关联 materialInventoryId 无法确定冻结哪条库存行,直接阻断。 @@ -464,7 +548,7 @@ public class ReservationStockOutOrderDomainService { * 本地落库失败的补偿解冻(对齐执行并下发的补偿模式):补偿失败仅告警, * 凭业务单号在WMS冻结记录(out_order_freeze_record,relate_no 前缀=业务单号)中人工处理 */ - private void compensateUnfreeze(String outOrderNumber) { + public void compensateUnfreeze(String outOrderNumber) { try { OmsFreezeInventoryParam param = new OmsFreezeInventoryParam(); param.setOutOrderNumber(outOrderNumber); diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reserveStockOutOrder/service/ReserveStockOutOrderDomainService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reserveStockOutOrder/service/ReserveStockOutOrderDomainService.java index acb65e0dc..0e853b52d 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reserveStockOutOrder/service/ReserveStockOutOrderDomainService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reserveStockOutOrder/service/ReserveStockOutOrderDomainService.java @@ -431,20 +431,14 @@ public class ReserveStockOutOrderDomainService { Long shipperId = reservationStockOutOrder.getShipperId(); String jsbz = reservationStockInOrderMapper.getJsbz(shipperId); reservationStockOutOrder.setSettlementCurrency(jsbz); - // 单头库存锁定模式JSON:老预约单下发的业务单不走下单冻结钩子(WMS分配时才冻结),固定 2-分配库存冻结(单仓单元素) - Map stockLockModeItem = new LinkedHashMap<>(); - stockLockModeItem.put("warehouseId", reservationStockOutOrder.getWarehouseId()); - stockLockModeItem.put("warehouseCode", reservationStockOutOrder.getWarehouseCode()); - stockLockModeItem.put("warehouseName", reservationStockOutOrder.getWarehouseName()); - stockLockModeItem.put("stockLockMode", 2); - reservationStockOutOrder.setStockLockModeJson(JSON.toJSONString(java.util.Collections.singletonList(stockLockModeItem))); - // 保留旧单号,用于查询源明细数据 + // 单头库存锁定模式JSON:延迟到明细构建后按真实判定组装(见下方 buildStockLockModeJsonByEntities) + // 保留旧单号,用于查询源明细数据(业务单沿用老预约单号,冻结/解冻/取消下发/反审均以此单号为键) String oldOutOrderNumber = reservationStockOutOrder.getOutOrderNumber(); // 生成出库业务单号,防止与已生成的出库业务单号重复 String newOrderNumber = reservationStockOutOrderDomainService.generateReserveOrderNumber(); // reservationStockOutOrder.setOutOrderNumber(newOrderNumber); // reservationStockOutOrder.setBusinessOrderNo(newOrderNumber); - reservationStockOutOrderService.save(reservationStockOutOrder); + // 查询老预约单明细并构建业务单明细(stamp 真实判定的库存锁定模式)——须先于冻结钩子与落库 List materialDetailList = outMaterialDetailService.list(new QueryWrapper().lambda().eq(ReserveOutMaterialDetail::getOutOrderNumber, oldOutOrderNumber)); List inMaterialDetailList = new ArrayList<>(); int index = 1; @@ -452,15 +446,29 @@ public class ReserveStockOutOrderDomainService { ReservationOutMaterialDetail reservationOutMaterialDetail = new ReservationOutMaterialDetail(); BeanUtils.copyProperties(reserveOutMaterialDetail, reservationOutMaterialDetail,"materialDetailId"); // reservationOutMaterialDetail.setOutOrderNumber(newOrderNumber); // 使用新单号保持与主表关联 - // 老预约单下发生成的业务单不走下单冻结钩子(客户预约单在WMS分配时才冻结),明细模式固定为 2-分配库存冻结 - reservationOutMaterialDetail.setStockLockMode(2); + // 明细库存锁定模式:按行仓库(空取单头)实时判定,与下单冻结钩子同口径 + reservationOutMaterialDetail.setStockLockMode(reservationStockOutOrderDomainService.resolveStockLockMode( + reservationOutMaterialDetail.getWarehouseId() != null ? reservationOutMaterialDetail.getWarehouseId() : reservationStockOutOrder.getWarehouseId())); String lot = generateDateSerialNumber(); lot = lot+"-"+index; index++; reservationOutMaterialDetail.setLotNo(lot); inMaterialDetailList.add(reservationOutMaterialDetail); } - reservationOutMaterialDetailService.saveBatch(inMaterialDetailList); + // 单头库存锁定模式JSON(仓级,真实判定值,元素不含执行单号) + reservationStockOutOrder.setStockLockModeJson(reservationStockOutOrderDomainService.buildStockLockModeJsonByEntities(inMaterialDetailList, reservationStockOutOrder.getWarehouseId())); + // ★下单冻结钩子(与 insert 的 tryFreezeOnCreate 同语义):模式 1 仓库明细先冻结 WMS 库存,失败阻断下发 + boolean frozen = reservationStockOutOrderDomainService.tryFreezeOnLegacyIssue(oldOutOrderNumber, inMaterialDetailList, reservationStockOutOrder.getWarehouseId()); + try { + reservationStockOutOrderService.save(reservationStockOutOrder); + reservationOutMaterialDetailService.saveBatch(inMaterialDetailList); + } catch (Exception e) { + if (frozen) { + // WMS 已冻结但本地落库失败 → 补偿解冻(老预约单保持待下发状态可重试,WMS 侧幂等) + reservationStockOutOrderDomainService.compensateUnfreeze(oldOutOrderNumber); + } + throw e; + } return update; }