fix(oms): 老预约单(客户预约)下发生成的业务单补下单冻结钩子, 消除最后一个不冻结入口

- issueOrder 路径原直存业务单绕过 insert 冻结钩子, 模式1仓库下不冻结; 排查确认入口活跃在用(App端/下发链路近期均有功能提交), 予以补齐
- 新增 resolveStockLockMode/buildStockLockModeJsonByEntities/tryFreezeOnLegacyIssue 公开复用方法, compensateUnfreeze 改 public
- 下发顺序重排: 明细真实判定stamp → 单头JSON真实值 → 冻结(失败阻断下发, R2无批次同insert语义) → try-catch落库(失败补偿解冻, 与insert"冻结先行+补偿"模式一致)
- 业务单号沿用老预约单号, 冻结幂等/取消下发/删除/反审解冻均以此单号闭环
- 至此模式1仓库下所有业务单入口(新增/批量/Excel单工作簿/多工作簿导入/编辑/老预约单下发)全部冻结
This commit is contained in:
rcx
2026-09-15 13:37:02 +08:00
parent bb0cd350ad
commit a473387574
2 changed files with 105 additions and 13 deletions
@@ -378,6 +378,90 @@ public class ReservationStockOutOrderDomainService {
return JSON.toJSONString(warehouseJsonMap.values()); 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<ReservationOutMaterialDetail> details, Long defaultWarehouseId) {
if (CollectionUtils.isEmpty(details)) {
return null;
}
Map<Long, Map<String, Object>> warehouseJsonMap = new LinkedHashMap<>();
for (ReservationOutMaterialDetail detail : details) {
Long warehouseId = detail.getWarehouseId() != null ? detail.getWarehouseId() : defaultWarehouseId;
warehouseJsonMap.computeIfAbsent(warehouseId, k -> {
Map<String, Object> 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<ReservationOutMaterialDetail> details, Long defaultWarehouseId) {
if (CollectionUtils.isEmpty(details)) {
return false;
}
List<OutMaterialDetail> 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库存再落库(失败抛异常阻断保存)。 * 出库单保存时下单冻结:筛选下单冻结模式仓库的明细,先冻结WMS库存再落库(失败抛异常阻断保存)。
* ★下单冻结模式仓库明细强制必选库存批次:未关联 materialInventoryId 无法确定冻结哪条库存行,直接阻断。 * ★下单冻结模式仓库明细强制必选库存批次:未关联 materialInventoryId 无法确定冻结哪条库存行,直接阻断。
@@ -464,7 +548,7 @@ public class ReservationStockOutOrderDomainService {
* 本地落库失败的补偿解冻(对齐执行并下发的补偿模式):补偿失败仅告警, * 本地落库失败的补偿解冻(对齐执行并下发的补偿模式):补偿失败仅告警,
* 凭业务单号在WMS冻结记录(out_order_freeze_recordrelate_no 前缀=业务单号)中人工处理 * 凭业务单号在WMS冻结记录(out_order_freeze_recordrelate_no 前缀=业务单号)中人工处理
*/ */
private void compensateUnfreeze(String outOrderNumber) { public void compensateUnfreeze(String outOrderNumber) {
try { try {
OmsFreezeInventoryParam param = new OmsFreezeInventoryParam(); OmsFreezeInventoryParam param = new OmsFreezeInventoryParam();
param.setOutOrderNumber(outOrderNumber); param.setOutOrderNumber(outOrderNumber);
@@ -431,20 +431,14 @@ public class ReserveStockOutOrderDomainService {
Long shipperId = reservationStockOutOrder.getShipperId(); Long shipperId = reservationStockOutOrder.getShipperId();
String jsbz = reservationStockInOrderMapper.getJsbz(shipperId); String jsbz = reservationStockInOrderMapper.getJsbz(shipperId);
reservationStockOutOrder.setSettlementCurrency(jsbz); reservationStockOutOrder.setSettlementCurrency(jsbz);
// 单头库存锁定模式JSON老预约单下发的业务单不走下单冻结钩子(WMS分配时才冻结),固定 2-分配库存冻结(单仓单元素 // 单头库存锁定模式JSON延迟到明细构建后按真实判定组装(见下方 buildStockLockModeJsonByEntities
Map<String, Object> 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)));
// 保留旧单号,用于查询源明细数据
String oldOutOrderNumber = reservationStockOutOrder.getOutOrderNumber(); String oldOutOrderNumber = reservationStockOutOrder.getOutOrderNumber();
// 生成出库业务单号,防止与已生成的出库业务单号重复 // 生成出库业务单号,防止与已生成的出库业务单号重复
String newOrderNumber = reservationStockOutOrderDomainService.generateReserveOrderNumber(); String newOrderNumber = reservationStockOutOrderDomainService.generateReserveOrderNumber();
// reservationStockOutOrder.setOutOrderNumber(newOrderNumber); // reservationStockOutOrder.setOutOrderNumber(newOrderNumber);
// reservationStockOutOrder.setBusinessOrderNo(newOrderNumber); // reservationStockOutOrder.setBusinessOrderNo(newOrderNumber);
reservationStockOutOrderService.save(reservationStockOutOrder); // 查询老预约单明细并构建业务单明细(stamp 真实判定的库存锁定模式)——须先于冻结钩子与落库
List<ReserveOutMaterialDetail> materialDetailList = outMaterialDetailService.list(new QueryWrapper<ReserveOutMaterialDetail>().lambda().eq(ReserveOutMaterialDetail::getOutOrderNumber, oldOutOrderNumber)); List<ReserveOutMaterialDetail> materialDetailList = outMaterialDetailService.list(new QueryWrapper<ReserveOutMaterialDetail>().lambda().eq(ReserveOutMaterialDetail::getOutOrderNumber, oldOutOrderNumber));
List<ReservationOutMaterialDetail> inMaterialDetailList = new ArrayList<>(); List<ReservationOutMaterialDetail> inMaterialDetailList = new ArrayList<>();
int index = 1; int index = 1;
@@ -452,15 +446,29 @@ public class ReserveStockOutOrderDomainService {
ReservationOutMaterialDetail reservationOutMaterialDetail = new ReservationOutMaterialDetail(); ReservationOutMaterialDetail reservationOutMaterialDetail = new ReservationOutMaterialDetail();
BeanUtils.copyProperties(reserveOutMaterialDetail, reservationOutMaterialDetail,"materialDetailId"); BeanUtils.copyProperties(reserveOutMaterialDetail, reservationOutMaterialDetail,"materialDetailId");
// reservationOutMaterialDetail.setOutOrderNumber(newOrderNumber); // 使用新单号保持与主表关联 // reservationOutMaterialDetail.setOutOrderNumber(newOrderNumber); // 使用新单号保持与主表关联
// 老预约单下发生成的业务单不走下单冻结钩子(客户预约单在WMS分配时才冻结),明细模式固定为 2-分配库存冻结 // 明细库存锁定模式:按行仓库(空取单头)实时判定,与下单冻结钩子同口径
reservationOutMaterialDetail.setStockLockMode(2); reservationOutMaterialDetail.setStockLockMode(reservationStockOutOrderDomainService.resolveStockLockMode(
reservationOutMaterialDetail.getWarehouseId() != null ? reservationOutMaterialDetail.getWarehouseId() : reservationStockOutOrder.getWarehouseId()));
String lot = generateDateSerialNumber(); String lot = generateDateSerialNumber();
lot = lot+"-"+index; lot = lot+"-"+index;
index++; index++;
reservationOutMaterialDetail.setLotNo(lot); reservationOutMaterialDetail.setLotNo(lot);
inMaterialDetailList.add(reservationOutMaterialDetail); inMaterialDetailList.add(reservationOutMaterialDetail);
} }
// 单头库存锁定模式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); reservationOutMaterialDetailService.saveBatch(inMaterialDetailList);
} catch (Exception e) {
if (frozen) {
// WMS 已冻结但本地落库失败 → 补偿解冻(老预约单保持待下发状态可重试,WMS 侧幂等)
reservationStockOutOrderDomainService.compensateUnfreeze(oldOutOrderNumber);
}
throw e;
}
return update; return update;
} }