feat(oms,wms,system): OMS下单冻结库存(防超卖)——仓库级库存锁定模式
- 仓库新增stock_lock_mode开关(1-下单冻结 2-分配库存冻结默认),查询/独立切换接口+在途冻结单切换守卫 - WMS新增out_order_freeze_record冻结记录表:下单冻结/编辑原子重锁/取消解冻/分配承接/交接扣减/反审还原全状态机,条件更新防并发超卖 - OMS下单即冻结(失败阻断+落库失败补偿解冻)、已下发禁编辑、cancelIssueOrder补调WMS清理(修存量不同步)、cancelAuditState镜像还原修复 - 达梦SQL脚本3个(WAREHOUSE加列/冻结记录建表/主键列名修复),测试库已执行并全流程实测通过
This commit is contained in:
+7
-3
@@ -440,8 +440,9 @@ public class ExecutionStockOutOrderDomainService {
|
||||
BigDecimal freeze = freezeAfterQuantity.subtract(freezeBeforeQuantity);
|
||||
BigDecimal allocationQuantity = reservationMaterialInventory.getAllocationQuantity();
|
||||
BigDecimal freezeQuantity = reservationMaterialInventory.getFreezeQuantity();
|
||||
allocationQuantity.add(allocation);
|
||||
freezeQuantity.subtract(freeze);
|
||||
// 修复:BigDecimal 不可变,add/subtract 返回新对象必须回赋(原实现未回赋,取消下发的镜像库存还原实际不生效)
|
||||
allocationQuantity = (allocationQuantity == null ? BigDecimal.ZERO : allocationQuantity).add(allocation);
|
||||
freezeQuantity = (freezeQuantity == null ? BigDecimal.ZERO : freezeQuantity).subtract(freeze);
|
||||
reservationMaterialInventory.setAllocationQuantity(allocationQuantity);
|
||||
reservationMaterialInventory.setFreezeQuantity(freezeQuantity);
|
||||
reservationMaterialInventory.setUpdateTime(new Date());
|
||||
@@ -505,7 +506,7 @@ public class ExecutionStockOutOrderDomainService {
|
||||
checkDetail.setMaterialName(detail.getMaterialName());
|
||||
checkList.add(checkDetail);
|
||||
}
|
||||
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList);
|
||||
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList, byId.getBusinessOutOrderNumber());
|
||||
if (checkResult == null || !"200".equals(String.valueOf(checkResult.get("code")))) {
|
||||
validateErrors.add("出库单[" + byId.getOutOrderNumber() + "]库存校验失败:"
|
||||
+ (checkResult == null ? "WMS服务不可用,已阻止下发" : checkResult.get("msg")));
|
||||
@@ -527,6 +528,9 @@ public class ExecutionStockOutOrderDomainService {
|
||||
try {
|
||||
StockOutOrder stockOutOrder = new StockOutOrder();
|
||||
BeanUtils.copyProperties(byId, stockOutOrder,"outOrderId");
|
||||
// 下单冻结模式:WMS单头带上业务单号(字段名不同copyProperties不会复制,必须显式set),
|
||||
// 供WMS分配承接/下发校验做【单据级】冻结记录过滤(双单冻结同行防误消费)
|
||||
stockOutOrder.setBusinessOrderNo(byId.getBusinessOutOrderNumber());
|
||||
Long warehouseId = stockOutOrder.getWarehouseId();
|
||||
Long topOrgId = stockInOrderMapper.getTopOrgId(warehouseId);
|
||||
Long orgId = stockInOrderMapper.getOrgId(warehouseId);
|
||||
|
||||
+227
-18
@@ -236,22 +236,32 @@ public class ReservationStockOutOrderDomainService {
|
||||
if (stockOutOrderDO.getReservationOrderSource() == null || stockOutOrderDO.getReservationOrderSource().isEmpty()) {
|
||||
stockOutOrderDO.setReservationOrderSource("手动创建");
|
||||
}
|
||||
stockOutOrderService.insert(stockOutOrderDO);
|
||||
List<ReservationDeliveryDetailsLink> reservationDeliveryDetailsLinkList = stockOutOrderDO.getReservationDeliveryDetailsLinkList();
|
||||
if (reservationDeliveryDetailsLinkList != null && !reservationDeliveryDetailsLinkList.isEmpty()){
|
||||
reservationDeliveryDetailsLinkList.forEach(reservationDeliveryDetailsLink -> {
|
||||
reservationDeliveryDetailsLink.setOrderNumber(stockOutOrderDO.getOutOrderNumber());
|
||||
reservationDeliveryDetailsLink.setOrderId(stockOutOrderDO.getOutOrderId());
|
||||
});
|
||||
if (!CollectionUtils.isEmpty(reservationDeliveryDetailsLinkList)) {
|
||||
reservationDeliveryDetailsLinkService.saveBatch(reservationDeliveryDetailsLinkList);
|
||||
// 下单冻结库存(防超卖):下单冻结模式仓库的明细先冻结WMS库存(可用-、冻结+,库存数量不变),
|
||||
// 冻结失败阻断下单;本地落库失败补偿解冻(对齐执行并下发的补偿模式)
|
||||
boolean frozen = tryFreezeOnCreate(stockOutOrderDO);
|
||||
try {
|
||||
stockOutOrderService.insert(stockOutOrderDO);
|
||||
List<ReservationDeliveryDetailsLink> reservationDeliveryDetailsLinkList = stockOutOrderDO.getReservationDeliveryDetailsLinkList();
|
||||
if (reservationDeliveryDetailsLinkList != null && !reservationDeliveryDetailsLinkList.isEmpty()){
|
||||
reservationDeliveryDetailsLinkList.forEach(reservationDeliveryDetailsLink -> {
|
||||
reservationDeliveryDetailsLink.setOrderNumber(stockOutOrderDO.getOutOrderNumber());
|
||||
reservationDeliveryDetailsLink.setOrderId(stockOutOrderDO.getOutOrderId());
|
||||
});
|
||||
if (!CollectionUtils.isEmpty(reservationDeliveryDetailsLinkList)) {
|
||||
reservationDeliveryDetailsLinkService.saveBatch(reservationDeliveryDetailsLinkList);
|
||||
}
|
||||
}
|
||||
List<ReservationOutMaterialDetailDO> materialDetailList = stockOutOrderDO.getMaterialDetailList();
|
||||
if (materialDetailList != null && !materialDetailList.isEmpty()) {
|
||||
return outMaterialDetailService.batchInsert(stockOutOrderDO.getOutOrderNumber(), stockOutOrderDO.getMaterialDetailList());
|
||||
}
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
if (frozen) {
|
||||
compensateUnfreeze(stockOutOrderDO.getOutOrderNumber());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
List<ReservationOutMaterialDetailDO> materialDetailList = stockOutOrderDO.getMaterialDetailList();
|
||||
if (materialDetailList != null && !materialDetailList.isEmpty()) {
|
||||
return outMaterialDetailService.batchInsert(stockOutOrderDO.getOutOrderNumber(), stockOutOrderDO.getMaterialDetailList());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -284,12 +294,186 @@ public class ReservationStockOutOrderDomainService {
|
||||
if (stockOutOrderDO.getCheckTaskDistribution() == null) {
|
||||
stockOutOrderDO.setCheckTaskDistribution(2);
|
||||
}
|
||||
stockOutOrderService.insert(stockOutOrderDO);
|
||||
outMaterialDetailService.batchInsert(stockOutOrderDO.getOutOrderNumber(), stockOutOrderDO.getMaterialDetailList());
|
||||
// 下单冻结库存(防超卖):与 insert 同口径,冻结失败阻断、落库失败补偿解冻
|
||||
boolean frozen = tryFreezeOnCreate(stockOutOrderDO);
|
||||
try {
|
||||
stockOutOrderService.insert(stockOutOrderDO);
|
||||
outMaterialDetailService.batchInsert(stockOutOrderDO.getOutOrderNumber(), stockOutOrderDO.getMaterialDetailList());
|
||||
} catch (Exception e) {
|
||||
if (frozen) {
|
||||
compensateUnfreeze(stockOutOrderDO.getOutOrderNumber());
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ================================ 下单冻结库存(防超卖) ================================
|
||||
|
||||
/**
|
||||
* 下单冻结模式仓库判断:stock_lock_mode=1-下单冻结(2-分配库存冻结为默认现有模式)。
|
||||
* 复用仓库信息兜底组件(Feign优先+本地跨库兜底);兜底时 stockLockMode=null → 按模式2处理(保守降级不冻结)
|
||||
*/
|
||||
private boolean isOrderFreezeWarehouse(Long warehouseId) {
|
||||
if (warehouseId == null) {
|
||||
return false;
|
||||
}
|
||||
WarehouseFeignPO warehouseFeignPO = getWarehouseInfoWithFallback(warehouseId);
|
||||
return warehouseFeignPO != null && warehouseFeignPO.getStockLockMode() != null && warehouseFeignPO.getStockLockMode() == 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出库单保存时下单冻结:筛选下单冻结模式仓库的明细,先冻结WMS库存再落库(失败抛异常阻断保存)。
|
||||
* ★下单冻结模式仓库明细强制必选库存批次:未关联 materialInventoryId 无法确定冻结哪条库存行,直接阻断。
|
||||
*
|
||||
* @return true-已冻结(落库失败需补偿解冻);false-无冻结模式明细未冻结
|
||||
*/
|
||||
private boolean tryFreezeOnCreate(ReservationStockOutOrderDO stockOutOrderDO) {
|
||||
List<ReservationOutMaterialDetailDO> details = stockOutOrderDO.getMaterialDetailList();
|
||||
if (CollectionUtils.isEmpty(details)) {
|
||||
return false;
|
||||
}
|
||||
// 样品出库交接不扣减库存,同样不参与下单冻结
|
||||
if ("yang_pin_chu_ku".equals(stockOutOrderDO.getBusinessType())) {
|
||||
return false;
|
||||
}
|
||||
List<OutMaterialDetail> freezeDetails = filterFreezeDetails(details, stockOutOrderDO.getWarehouseId(), stockOutOrderDO.getOutOrderNumber());
|
||||
if (freezeDetails.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
OmsFreezeInventoryParam param = new OmsFreezeInventoryParam();
|
||||
param.setOutOrderNumber(stockOutOrderDO.getOutOrderNumber());
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 出库单编辑-原子重锁:已下发的冻结单禁止编辑(判据=活动执行单存在,issueStatus 不可靠——下发不改该字段);
|
||||
* 未下发则WMS同一事务内释放原冻结+按新明细重锁,成功后才改本地
|
||||
*/
|
||||
private void handleFreezeOnUpdate(ReservationStockOutOrderDO stockOutOrderDO, ReservationStockOutOrderPO existingPO) {
|
||||
List<ReservationOutMaterialDetailDO> details = stockOutOrderDO.getMaterialDetailList();
|
||||
if (CollectionUtils.isEmpty(details)) {
|
||||
return;
|
||||
}
|
||||
// 样品出库交接不扣减库存,同样不参与下单冻结
|
||||
if ("yang_pin_chu_ku".equals(stockOutOrderDO.getBusinessType())) {
|
||||
return;
|
||||
}
|
||||
List<OutMaterialDetail> freezeDetails = filterFreezeDetails(details, stockOutOrderDO.getWarehouseId(), existingPO.getOutOrderNumber());
|
||||
if (freezeDetails.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
String orderNumber = existingPO.getOutOrderNumber();
|
||||
// 已下发WMS(存在活动执行单)禁止编辑:执行单明细与冻结记录必须保持一致,须先取消下发
|
||||
List<ExecutionStockOutOrder> activeExecutions = executionStockOutOrderService.list(new QueryWrapper<ExecutionStockOutOrder>().lambda()
|
||||
.eq(ExecutionStockOutOrder::getBusinessOutOrderNumber, orderNumber)
|
||||
.eq(ExecutionStockOutOrder::getDelFlag, 1));
|
||||
if (!CollectionUtils.isEmpty(activeExecutions)) {
|
||||
throw new ServiceException("出库单[" + orderNumber + "]已下发WMS,不可编辑,请先取消下发");
|
||||
}
|
||||
OmsFreezeInventoryParam param = new OmsFreezeInventoryParam();
|
||||
param.setOutOrderNumber(orderNumber);
|
||||
param.setDetails(freezeDetails);
|
||||
fillOperator(param);
|
||||
AjaxResult result = wmsServiceFeign.omsRelockInventory(param);
|
||||
if (result == null || !"200".equals(String.valueOf(result.get("code")))) {
|
||||
throw new ServiceException("编辑重锁库存失败,无法修改出库单:"
|
||||
+ (result == null ? "WMS服务不可用" : result.get("msg")));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除/关闭单据前释放冻结(WMS按业务单号幂等处理:无冻结直接成功;
|
||||
* 冻结已被执行单承接时WMS拒绝并提示先取消下发;已交接时拒绝并提示走反审流程)
|
||||
*/
|
||||
private void unfreezeIfFrozen(ReservationStockOutOrderPO stockOutOrderPO) {
|
||||
OmsFreezeInventoryParam param = new OmsFreezeInventoryParam();
|
||||
param.setOutOrderNumber(stockOutOrderPO.getOutOrderNumber());
|
||||
fillOperator(param);
|
||||
AjaxResult result = wmsServiceFeign.omsUnfreezeInventory(param);
|
||||
if (result == null || !"200".equals(String.valueOf(result.get("code")))) {
|
||||
throw new ServiceException("释放冻结库存失败:"
|
||||
+ (result == null ? "WMS服务不可用" : result.get("msg")));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本地落库失败的补偿解冻(对齐执行并下发的补偿模式):补偿失败仅告警,
|
||||
* 凭业务单号在WMS冻结记录(out_order_freeze_record,relate_no 前缀=业务单号)中人工处理
|
||||
*/
|
||||
private void compensateUnfreeze(String outOrderNumber) {
|
||||
try {
|
||||
OmsFreezeInventoryParam param = new OmsFreezeInventoryParam();
|
||||
param.setOutOrderNumber(outOrderNumber);
|
||||
fillOperator(param);
|
||||
AjaxResult result = wmsServiceFeign.omsUnfreezeInventory(param);
|
||||
if (result == null || !"200".equals(String.valueOf(result.get("code")))) {
|
||||
log.error("下单冻结补偿解冻失败,请人工核查WMS冻结记录:出库业务单[{}],返回:{}",
|
||||
outOrderNumber, result == null ? "WMS服务不可用" : result.get("msg"));
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
log.error("下单冻结补偿解冻异常,请人工核查WMS冻结记录:出库业务单[{}]", outOrderNumber, ex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选下单冻结模式仓库的明细并转换为冻结参数。
|
||||
* ★下单冻结模式仓库明细强制必选库存批次(R2):materialInventoryId 为空直接阻断保存;
|
||||
* 数量<=0的明细交由WMS侧兜底校验
|
||||
*/
|
||||
private List<OutMaterialDetail> filterFreezeDetails(List<ReservationOutMaterialDetailDO> details, Long defaultWarehouseId, String businessOrderNumber) {
|
||||
List<OutMaterialDetail> freezeDetails = new ArrayList<>();
|
||||
for (ReservationOutMaterialDetailDO detail : details) {
|
||||
Long warehouseId = detail.getWarehouseId() != null ? detail.getWarehouseId() : defaultWarehouseId;
|
||||
if (!isOrderFreezeWarehouse(warehouseId)) {
|
||||
continue;
|
||||
}
|
||||
// 下单冻结必须知道冻结哪条库存行:模式1仓库明细未关联库存直接阻断
|
||||
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);
|
||||
}
|
||||
return freezeDetails;
|
||||
}
|
||||
|
||||
/**
|
||||
* 填充操作人(Feign调用WMS时兜底传递,WMS侧无登录上下文时使用)
|
||||
*/
|
||||
private void fillOperator(OmsFreezeInventoryParam param) {
|
||||
try {
|
||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||
if (loginUser != null) {
|
||||
param.setOperatorId(loginUser.getUserid());
|
||||
param.setOperatorName(loginUser.getUsername());
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
// 无登录上下文场景(Feign内部调用),操作人留空由WMS兜底
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改出库单
|
||||
*/
|
||||
@@ -300,6 +484,9 @@ public class ReservationStockOutOrderDomainService {
|
||||
}
|
||||
//设置物料信息
|
||||
setMaterialDetailInfo(stockOutOrderDO);
|
||||
// 下单冻结模式编辑重锁(原子):先WMS侧释放原冻结+按新明细重锁(同一事务),成功后才改本地;
|
||||
// 已下发WMS的冻结单不可编辑,须先取消下发(判据=活动执行单,issueStatus 不可靠)
|
||||
handleFreezeOnUpdate(stockOutOrderDO, stockInOrderPO);
|
||||
//统计种类
|
||||
stockOutOrderDO.setMaterialQuantity(stockOutOrderDO.getMaterialDetailList().size());
|
||||
//统计总数量
|
||||
@@ -379,6 +566,8 @@ public class ReservationStockOutOrderDomainService {
|
||||
if (stockInOrderPO.getStatus() != 1){
|
||||
throw new ServiceException("只有已创建状态下能允许关闭出库单");
|
||||
}
|
||||
// 下单冻结模式:关闭单据不再出库,先释放冻结库存(幂等;冻结已被执行单承接时WMS会拒绝并提示先取消下发)
|
||||
unfreezeIfFrozen(stockInOrderPO);
|
||||
stockOutOrderDO.setStatus(11);
|
||||
return stockOutOrderService.update(stockOutOrderDO);
|
||||
}
|
||||
@@ -789,6 +978,18 @@ public class ReservationStockOutOrderDomainService {
|
||||
List<ExecutionStockOutOrder> list = executionStockOutOrderService.list(new QueryWrapper<ExecutionStockOutOrder>().lambda()
|
||||
.eq(ExecutionStockOutOrder::getBusinessOutOrderNumber, inOrderNumber)
|
||||
.eq(ExecutionStockOutOrder::getDelFlag, 1));
|
||||
// 取消下发必须同步WMS(修复存量不同步问题:此前只软删本地执行单,WMS残留单据与分配占用):
|
||||
// WMS侧还原库存/承接冻结并软删单据;下单冻结模式下仅解除承接(冻结归还业务单),
|
||||
// 分配库存冻结模式下还原分配占用;失败抛异常整体回滚本地软删(WMS清理幂等可重试)
|
||||
for (ExecutionStockOutOrder executionStockOutOrder : list) {
|
||||
StockOutOrder cancelParam = new StockOutOrder();
|
||||
cancelParam.setOutOrderNumber(executionStockOutOrder.getOutOrderNumber());
|
||||
AjaxResult cancelResult = wmsServiceFeign.omsCancelExecutionOrder(cancelParam);
|
||||
if (cancelResult == null || !"200".equals(String.valueOf(cancelResult.get("code")))) {
|
||||
throw new ServiceException("取消下发失败:WMS执行单[" + executionStockOutOrder.getOutOrderNumber()
|
||||
+ "]清理失败:" + (cancelResult == null ? "WMS服务不可用" : cancelResult.get("msg")));
|
||||
}
|
||||
}
|
||||
list.forEach(executionStockOutOrder -> {
|
||||
String outOrderNumber = executionStockOutOrder.getOutOrderNumber();
|
||||
List<ExecutionOutMaterialDetail> materialDetails = executionOutMaterialDetailService.list(new QueryWrapper<ExecutionOutMaterialDetail>().lambda()
|
||||
@@ -992,7 +1193,8 @@ public class ReservationStockOutOrderDomainService {
|
||||
if (!groupBaseValid) {
|
||||
return;
|
||||
}
|
||||
//WMS库存校验(接口内部会聚合该仓全部明细的需求量后比对可用库存)
|
||||
//WMS库存校验(接口内部会聚合该仓全部明细的需求量后比对可用库存;
|
||||
// 传业务单号:下单冻结模式下行级可用量=库存可用+本单冻结记录量)
|
||||
List<OutMaterialDetail> checkList = new ArrayList<>();
|
||||
for (ReservationOutMaterialDetail detail : details) {
|
||||
OutMaterialDetail checkDetail = new OutMaterialDetail();
|
||||
@@ -1002,7 +1204,7 @@ public class ReservationStockOutOrderDomainService {
|
||||
checkDetail.setMaterialName(detail.getMaterialName());
|
||||
checkList.add(checkDetail);
|
||||
}
|
||||
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList);
|
||||
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList, inOrderNumber);
|
||||
if (checkResult == null || !"200".equals(String.valueOf(checkResult.get("code")))) {
|
||||
String groupWarehouseName = reservationStockInOrderMapper.getWarehouseName(warehouseId);
|
||||
issueValidateErrors.add("仓库[" + (groupWarehouseName != null ? groupWarehouseName : warehouseId) + "]库存校验失败:"
|
||||
@@ -1174,6 +1376,13 @@ public class ReservationStockOutOrderDomainService {
|
||||
* 批量删除出库单
|
||||
*/
|
||||
public Boolean delete(Long[] outOrderIds) {
|
||||
// 下单冻结模式:删除单据前先释放冻结库存(幂等;冻结已被执行单承接时WMS会拒绝并提示先取消下发)
|
||||
for (Long outOrderId : outOrderIds) {
|
||||
ReservationStockOutOrderPO stockOutOrderPO = stockOutOrderService.getInfo(outOrderId);
|
||||
if (stockOutOrderPO != null) {
|
||||
unfreezeIfFrozen(stockOutOrderPO);
|
||||
}
|
||||
}
|
||||
return stockOutOrderService.delete(outOrderIds);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user