From b1d92fdda86046bb9a77e58de96a1efade3baecd Mon Sep 17 00:00:00 2001 From: rcx <3084692334@qq.com> Date: Mon, 14 Sep 2026 15:41:32 +0800 Subject: [PATCH] =?UTF-8?q?feat(oms,wms,system):=20OMS=E4=B8=8B=E5=8D=95?= =?UTF-8?q?=E5=86=BB=E7=BB=93=E5=BA=93=E5=AD=98=EF=BC=88=E9=98=B2=E8=B6=85?= =?UTF-8?q?=E5=8D=96=EF=BC=89=E2=80=94=E2=80=94=E4=BB=93=E5=BA=93=E7=BA=A7?= =?UTF-8?q?=E5=BA=93=E5=AD=98=E9=94=81=E5=AE=9A=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 仓库新增stock_lock_mode开关(1-下单冻结 2-分配库存冻结默认),查询/独立切换接口+在途冻结单切换守卫 - WMS新增out_order_freeze_record冻结记录表:下单冻结/编辑原子重锁/取消解冻/分配承接/交接扣减/反审还原全状态机,条件更新防并发超卖 - OMS下单即冻结(失败阻断+落库失败补偿解冻)、已下发禁编辑、cancelIssueOrder补调WMS清理(修存量不同步)、cancelAuditState镜像还原修复 - 达梦SQL脚本3个(WAREHOUSE加列/冻结记录建表/主键列名修复),测试库已执行并全流程实测通过 --- .../com/mhd/system/api/WmsServiceFeign.java | 41 +- .../api/domain/OmsFreezeInventoryParam.java | 42 ++ .../system/api/domain/WarehouseFeignPO.java | 4 + .../api/factory/RemoteWmsFallbackFactory.java | 26 +- .../common/core/domain/po/WarehousePO.java | 3 + .../WarehouseApplicationService.java | 70 +++ .../domain/warehouse/entity/Warehouse.java | 4 + .../warehouse/repository/po/WarehousePO.java | 4 + .../repository/todo/WarehouseDO.java | 4 + .../service/WarehouseDomainService.java | 23 + .../dto/warehouse/WarehouseDTO.java | 4 + .../facadeApi/warehouse/WarehouseApi.java | 27 + .../basic/warehouse/WarehouseMapper.xml | 3 +- .../ExecutionStockOutOrderDomainService.java | 10 +- ...ReservationStockOutOrderDomainService.java | 245 +++++++- .../StockOutOrderApplicationService.java | 42 +- .../HandoverTaskOrderDomainService.java | 5 + .../mapper/MaterialInventoryMapper.java | 37 ++ .../persistence/MaterialInventoryImpl.java | 40 +- .../persistence/OutMaterialDetailImpl.java | 62 +- .../entity/OutOrderFreezeRecord.java | 77 +++ .../facade/IOutOrderFreezeRecordService.java | 13 + .../mapper/OutOrderFreezeRecordMapper.java | 13 + .../persistence/OutOrderFreezeRecordImpl.java | 16 + .../OutOrderFreezeRecordDomainService.java | 536 ++++++++++++++++++ .../service/StockOutOrderDomainService.java | 61 +- .../stockOutOrder/StockOutOrderApi.java | 49 +- .../MaterialInventoryMapper.xml | 39 ++ ...ter_table_dm_warehouse_stock_lock_mode.sql | 33 ++ ...reate_table_dm_out_order_freeze_record.sql | 77 +++ ...x_dm_out_order_freeze_record_rename_id.sql | 24 + 31 files changed, 1587 insertions(+), 47 deletions(-) create mode 100644 mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/domain/OmsFreezeInventoryParam.java create mode 100644 mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/entity/OutOrderFreezeRecord.java create mode 100644 mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/facade/IOutOrderFreezeRecordService.java create mode 100644 mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/mapper/OutOrderFreezeRecordMapper.java create mode 100644 mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/persistence/OutOrderFreezeRecordImpl.java create mode 100644 mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/service/OutOrderFreezeRecordDomainService.java create mode 100644 sql/20260912_alter_table_dm_warehouse_stock_lock_mode.sql create mode 100644 sql/20260912_create_table_dm_out_order_freeze_record.sql create mode 100644 sql/20260912_fix_dm_out_order_freeze_record_rename_id.sql diff --git a/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/WmsServiceFeign.java b/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/WmsServiceFeign.java index 8170d6542..e7eaca654 100644 --- a/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/WmsServiceFeign.java +++ b/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/WmsServiceFeign.java @@ -21,7 +21,7 @@ import java.util.Map; * @description: TODO * @date 2024/5/10 8:58 **/ -@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE,url = "http://10.33.0.109:8016",fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class) +@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE,fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class) public interface WmsServiceFeign { /** @@ -112,11 +112,46 @@ public interface WmsServiceFeign { /** * oms下发前库存校验:可用库存不足时返回错误,OMS据此在推送前拦截下发, - * 避免单据先落库、分配才失败而在WMS残留"已审核"状态的垃圾单 + * 避免单据先落库、分配才失败而在WMS残留"已审核"状态的垃圾单。 + * businessOrderNumber(可空):OMS出库业务单号,下单冻结模式下校验可用量=库存可用+本单冻结记录量 */ @ApiOperation("oms下发前库存校验") @PostMapping(value = "/stockOutOrderApi/checkOmsAssignInventory") - public AjaxResult checkOmsAssignInventory(@RequestBody List outMaterialDetails); + public AjaxResult checkOmsAssignInventory(@RequestBody List outMaterialDetails, + @RequestParam(value = "businessOrderNumber", required = false) String businessOrderNumber); + + /** + * oms出库业务单下单冻结库存(防超卖):WMS按明细冻结库存(可用-、冻结+,库存数量不变), + * 条件更新防并发超卖,任一行可用不足整体失败并返回错误;同业务单号幂等。 + * 冻结失败必须阻断OMS下单(返回非200由OMS整体回滚)。 + */ + @ApiOperation("oms出库业务单下单冻结库存") + @PostMapping(value = "/stockOutOrderApi/omsFreezeInventory") + public AjaxResult omsFreezeInventory(@RequestBody OmsFreezeInventoryParam param); + + /** + * oms出库业务单取消/删除-释放冻结库存:可用+、冻结-(库存数量不变);按业务单号幂等; + * 冻结已被执行单承接时拒绝(须先取消下发) + */ + @ApiOperation("oms出库业务单释放冻结库存") + @PostMapping(value = "/stockOutOrderApi/omsUnfreezeInventory") + public AjaxResult omsUnfreezeInventory(@RequestBody OmsFreezeInventoryParam param); + + /** + * oms出库业务单编辑-原子重锁:WMS同一事务内先释放原冻结再按新明细重新冻结, + * 任一新明细可用不足整体回滚(旧冻结同样还原),满足"先释放再重新锁定"原子性要求 + */ + @ApiOperation("oms出库业务单编辑重锁库存") + @PostMapping(value = "/stockOutOrderApi/omsRelockInventory") + public AjaxResult omsRelockInventory(@RequestBody OmsFreezeInventoryParam param); + + /** + * 仓库是否存在活跃冻结记录(status in 1-业务单冻结 2-执行单承接): + * 仓库"库存锁定模式"(stock_lock_mode)切换守卫,存在在途冻结单时禁止切换 + */ + @ApiOperation("仓库是否存在活跃冻结记录") + @GetMapping(value = "/stockOutOrderApi/hasActiveFreezeByWarehouse/{warehouseId}") + public AjaxResult hasActiveFreezeByWarehouse(@PathVariable("warehouseId") Long warehouseId); /** * oms下发单明细保存(返回执行结果,明细保存含自动审核链路,失败时OMS据此回滚) diff --git a/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/domain/OmsFreezeInventoryParam.java b/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/domain/OmsFreezeInventoryParam.java new file mode 100644 index 000000000..14e9f4f51 --- /dev/null +++ b/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/domain/OmsFreezeInventoryParam.java @@ -0,0 +1,42 @@ +package com.mhd.system.api.domain; + +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.io.Serializable; +import java.util.List; + +/** + * OMS出库业务单冻结库存请求参数 + * + * @description OMS下单冻结库存模式(防超卖):OMS出库业务单「下单」即冻结WMS库存 + * (可用-、冻结+,库存数量不变,库存数量=可用数量+冻结数量); + * 「编辑」原子释放旧冻结+按新明细重锁;「取消」释放冻结。 + * 冻结/解冻以业务单号为幂等键。 + * @date 2026-09-12 + */ +@Data +public class OmsFreezeInventoryParam implements Serializable { + private static final long serialVersionUID = 1L; + + @ApiModelProperty("OMS出库业务单号(幂等键)") + private String outOrderNumber; + + @ApiModelProperty("执行出库单号(预留,冻结/解冻/重锁时不使用)") + private String executionOrderNumber; + + @ApiModelProperty("库存行ID集合(预留)") + private List materialInventoryIds; + + @ApiModelProperty("出库明细列表(须携带 materialInventoryId 定位库存行)") + private List details; + + @ApiModelProperty("操作人ID(Feign调用可能无登录上下文时的兜底)") + private Long operatorId; + + @ApiModelProperty("操作人姓名(Feign调用可能无登录上下文时的兜底)") + private String operatorName; + + @ApiModelProperty("备注") + private String remark; +} diff --git a/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/domain/WarehouseFeignPO.java b/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/domain/WarehouseFeignPO.java index cdc823580..063a4b5b9 100644 --- a/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/domain/WarehouseFeignPO.java +++ b/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/domain/WarehouseFeignPO.java @@ -124,6 +124,10 @@ public class WarehouseFeignPO extends BaseVOEntity{ @Excel(name = "作业模式") private String workMode; + @ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结") + @Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结") + private Integer stockLockMode; + @ApiModelProperty("订单号生成规则(0-日期累加 1-历史累加)") @Excel(name = "单号生成规则") private String orderNoGenerateRule; diff --git a/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/factory/RemoteWmsFallbackFactory.java b/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/factory/RemoteWmsFallbackFactory.java index f5ea93014..b5439ce09 100644 --- a/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/factory/RemoteWmsFallbackFactory.java +++ b/mhd-api/mhd-api-system/src/main/java/com/mhd/system/api/factory/RemoteWmsFallbackFactory.java @@ -102,11 +102,35 @@ public class RemoteWmsFallbackFactory implements FallbackFactory outMaterialDetails) { + public AjaxResult checkOmsAssignInventory(List outMaterialDetails, String businessOrderNumber) { //校验接口本身不可用时返回错误,OMS会阻止下发(不能在WMS状态未知时盲目推送) return AjaxResult.error("WMS库存校验服务不可用:" + cause.getMessage()); } + @Override + public AjaxResult omsFreezeInventory(OmsFreezeInventoryParam param) { + //返回错误而非静默成功:下单冻结失败必须阻断OMS下单落库(冻结成功与否状态未知时不允许继续) + return AjaxResult.error("WMS下单冻结库存失败(服务降级):" + cause.getMessage()); + } + + @Override + public AjaxResult omsUnfreezeInventory(OmsFreezeInventoryParam param) { + //返回错误而非静默成功:解冻失败让OMS取消/删除感知(解冻可重试,WMS解冻幂等) + return AjaxResult.error("WMS释放冻结库存失败(服务降级):" + cause.getMessage()); + } + + @Override + public AjaxResult omsRelockInventory(OmsFreezeInventoryParam param) { + //返回错误而非静默成功:编辑重锁是原子操作,失败必须让OMS编辑整体回滚 + return AjaxResult.error("WMS编辑重锁库存失败(服务降级):" + cause.getMessage()); + } + + @Override + public AjaxResult hasActiveFreezeByWarehouse(Long warehouseId) { + //返回错误(fail-closed):模式切换守卫校验不可用时拒绝切换,避免在途冻结单语义错乱 + return AjaxResult.error("WMS冻结记录校验服务不可用:" + cause.getMessage()); + } + @Override public AjaxResult omsInOrderAddDetail(List inMaterialDetails) { //返回错误而非静默成功:明细保存(含自动审核)失败必须让OMS感知并回滚下发标记 diff --git a/mhd-common/mhd-common-core/src/main/java/com/mhd/common/core/domain/po/WarehousePO.java b/mhd-common/mhd-common-core/src/main/java/com/mhd/common/core/domain/po/WarehousePO.java index bbb4c261a..58dc3b056 100644 --- a/mhd-common/mhd-common-core/src/main/java/com/mhd/common/core/domain/po/WarehousePO.java +++ b/mhd-common/mhd-common-core/src/main/java/com/mhd/common/core/domain/po/WarehousePO.java @@ -35,4 +35,7 @@ public class WarehousePO implements Serializable { @Excel(name = "是否启用: 1-否 2-是") private Integer isEnable; + + @ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结") + private Integer stockLockMode; } diff --git a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/application/service/warehouse/WarehouseApplicationService.java b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/application/service/warehouse/WarehouseApplicationService.java index 9c481ad5e..e1e038897 100644 --- a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/application/service/warehouse/WarehouseApplicationService.java +++ b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/application/service/warehouse/WarehouseApplicationService.java @@ -12,7 +12,9 @@ import com.mhd.basic.domain.warehouse.repository.todo.WarehouseDO; import com.mhd.basic.domain.warehouse.service.WarehouseDomainService; import com.mhd.common.core.enums.DictCode; import com.mhd.common.core.exception.ServiceException; +import com.mhd.common.core.web.domain.AjaxResult; import com.mhd.common.security.utils.SecurityUtils; +import com.mhd.system.api.WmsServiceFeign; import com.mhd.system.api.domain.SysDictData; import com.mhd.system.api.model.LoginUser; import com.mhd.system.service.ISysDictTypeService; @@ -43,6 +45,8 @@ public class WarehouseApplicationService { private ISysDictTypeService dictTypeService; @Autowired private IAssociationWarehouseService associationWarehouseService; + @Autowired + private WmsServiceFeign wmsServiceFeign; /** @@ -239,12 +243,78 @@ public class WarehouseApplicationService { /** * 修改仓库 */ + /** + * @description 查询仓库库存锁定模式(OMS下单冻结库存/防超卖功能的仓库级开关) + * 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存(可用-、冻结+,库存数量不变) + * 2-分配库存冻结:现有模式,WMS「分配库存」时才冻结 + * 仓库不存在或未配置(null)时返回默认值 2(与代码侧降级口径一致) + * @author gen + * @date 2026-09-12 + */ + public Integer getStockLockMode(Long warehouseId) { + WarehousePO warehousePO = warehouseDomainService.getInfo(warehouseId); + return warehousePO == null ? 2 + : cn.hutool.core.util.ObjectUtil.defaultIfNull(warehousePO.getStockLockMode(), 2); + } + public Boolean update(WarehouseDO warehouseDO) { + // 库存锁定模式切换守卫:模式发生变化时,存在在途冻结单(OMS下单冻结未释放)则禁止切换, + // 否则在途单的解冻/承接逻辑会按新模式错乱。fail-closed:校验服务不可用时拒绝切换 + if (warehouseDO.getWarehouseId() != null && warehouseDO.getStockLockMode() != null) { + WarehousePO existingPO = warehouseDomainService.getInfo(warehouseDO.getWarehouseId()); + if (existingPO != null) { + checkStockLockModeSwitchable(warehouseDO.getWarehouseId(), warehouseDO.getStockLockMode(), existingPO); + } + } //设置数据字典键值 setDataDict(warehouseDO); return warehouseDomainService.update(warehouseDO); } + /** + * @description 单独切换仓库库存锁定模式(不改动仓库其他信息) + * 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存 + * 2-分配库存冻结(默认):WMS「分配库存」时才冻结 + * 切换守卫与通用编辑一致:模式变化且存在在途冻结单时禁止切换,fail-closed + * @author gen + * @date 2026-09-14 + */ + public Boolean switchStockLockMode(Long warehouseId, Integer stockLockMode) { + if (warehouseId == null) { + throw new ServiceException("仓库ID不能为空"); + } + if (stockLockMode == null || (stockLockMode.intValue() != 1 && stockLockMode.intValue() != 2)) { + throw new ServiceException("库存锁定模式不合法,仅支持 1-下单冻结 2-分配库存冻结"); + } + WarehousePO existingPO = warehouseDomainService.getInfo(warehouseId); + if (existingPO == null) { + throw new ServiceException("仓库不存在"); + } + checkStockLockModeSwitchable(warehouseId, stockLockMode, existingPO); + return warehouseDomainService.switchStockLockMode(warehouseId, stockLockMode); + } + + /** + * @description 库存锁定模式切换守卫:模式未变化直接放行;模式变化时查询WMS是否存在 + * 在途下单冻结单(freeze_status 1/2),存在则禁止切换。 + * fail-closed:WMS服务不可用/返回异常时一律拒绝切换,防止切换后冻结语义错乱 + * @author gen + * @date 2026-09-14 + */ + private void checkStockLockModeSwitchable(Long warehouseId, Integer newMode, WarehousePO existingPO) { + if (newMode.compareTo(cn.hutool.core.util.ObjectUtil.defaultIfNull(existingPO.getStockLockMode(), 2)) == 0) { + return; + } + AjaxResult freezeCheck = wmsServiceFeign.hasActiveFreezeByWarehouse(warehouseId); + if (freezeCheck == null || !"200".equals(String.valueOf(freezeCheck.get("code")))) { + throw new ServiceException("库存锁定模式切换校验失败,请稍后重试:" + + (freezeCheck == null ? "WMS服务不可用" : freezeCheck.get("msg"))); + } + if (Boolean.TRUE.equals(freezeCheck.get("data"))) { + throw new ServiceException("该仓库存在下单冻结中的出库单,不可切换库存锁定模式"); + } + } + /** * @description 设置数据字典键值 * @author ZhouGY diff --git a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/entity/Warehouse.java b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/entity/Warehouse.java index e49996bf4..03beca6e6 100644 --- a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/entity/Warehouse.java +++ b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/entity/Warehouse.java @@ -130,6 +130,10 @@ public class Warehouse extends BaseVOEntity{ @Excel(name = "作业模式") private String workMode; + @ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结") + @Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结") + private Integer stockLockMode; + @ApiModelProperty("订单号生成规则0日期累加1历史累加") @Excel(name = "单号生成规则") private String orderNoGenerateRule; diff --git a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/repository/po/WarehousePO.java b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/repository/po/WarehousePO.java index e1b5f239d..8f5b1acf3 100644 --- a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/repository/po/WarehousePO.java +++ b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/repository/po/WarehousePO.java @@ -132,6 +132,10 @@ public class WarehousePO extends BaseVOEntity{ @Excel(name = "作业模式") private String workMode; + @ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结") + @Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结") + private Integer stockLockMode; + @ApiModelProperty("订单号生成规则") @Excel(name = "单号生成规则") diff --git a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/repository/todo/WarehouseDO.java b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/repository/todo/WarehouseDO.java index 2c0359c90..5a06970cb 100644 --- a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/repository/todo/WarehouseDO.java +++ b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/repository/todo/WarehouseDO.java @@ -140,6 +140,10 @@ public class WarehouseDO extends BaseVOEntity{ @Excel(name = "作业模式") private String workMode; + @ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结") + @Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结") + private Integer stockLockMode; + @ApiModelProperty("订单号生成规则") @Excel(name = "单号生成规则") diff --git a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/service/WarehouseDomainService.java b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/service/WarehouseDomainService.java index 4bd24e1f2..dc25395dd 100644 --- a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/service/WarehouseDomainService.java +++ b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/domain/warehouse/service/WarehouseDomainService.java @@ -142,6 +142,29 @@ public class WarehouseDomainService { .eq(Warehouse::getDelFlag, 1)); } + /** + * @description 单独切换仓库库存锁定模式(仅更新 STOCK_LOCK_MODE 与审计字段,不触碰仓库其他数据) + * 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存 + * 2-分配库存冻结(默认):WMS「分配库存」时才冻结 + * @author gen + * @date 2026-09-14 + */ + public Boolean switchStockLockMode(Long warehouseId, Integer stockLockMode) { + Long updateBy = null; + try { + LoginUser loginUser = SecurityUtils.getLoginUser(); + updateBy = loginUser == null ? null : loginUser.getUserid(); + } catch (Exception ignore) { + } + Long finalUpdateBy = updateBy; + return warehouseService.update(new UpdateWrapper().lambda() + .set(Warehouse::getStockLockMode, stockLockMode) + .set(finalUpdateBy != null, Warehouse::getUpdateBy, finalUpdateBy) + .set(Warehouse::getUpdateTime, new Date()) + .eq(Warehouse::getWarehouseId, warehouseId) + .eq(Warehouse::getDelFlag, 1)); + } + /** * @description 获取指定仓库下的所有库区及库区下所有库位 * @author ZhouGY diff --git a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/interfaces/dto/warehouse/WarehouseDTO.java b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/interfaces/dto/warehouse/WarehouseDTO.java index ce7bbeecb..22eb9f6b3 100644 --- a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/interfaces/dto/warehouse/WarehouseDTO.java +++ b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/interfaces/dto/warehouse/WarehouseDTO.java @@ -145,6 +145,10 @@ public class WarehouseDTO extends BaseVOEntity{ @Excel(name = "作业模式") private String workMode; + @ApiModelProperty("库存锁定模式: 1-下单冻结 2-分配库存冻结") + @Excel(name = "库存锁定模式: 1-下单冻结 2-分配库存冻结") + private Integer stockLockMode; + @ApiModelProperty("订单号生成规则") @Excel(name = "单号生成规则") diff --git a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/interfaces/facadeApi/warehouse/WarehouseApi.java b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/interfaces/facadeApi/warehouse/WarehouseApi.java index 7c9138208..e3268860e 100644 --- a/mhd-modules/mhd-system/src/main/java/com/mhd/basic/interfaces/facadeApi/warehouse/WarehouseApi.java +++ b/mhd-modules/mhd-system/src/main/java/com/mhd/basic/interfaces/facadeApi/warehouse/WarehouseApi.java @@ -104,6 +104,33 @@ public class WarehouseApi extends BaseController{ return AjaxResult.success(warehouseApplicationService.getInfo(warehouseId)); } + /** + * 查询仓库库存锁定模式(OMS下单冻结库存/防超卖功能的仓库级开关): + * 1-下单冻结(OMS出库业务单「下单」即冻结WMS库存:可用-、冻结+,库存数量不变; + * 分配不动数量;编辑=原子重锁;取消=释放;交接=库存-冻结-) + * 2-分配库存冻结(默认,现有行为:WMS「分配库存」时才 可用-冻结+) + * 仓库不存在或未配置时返回 2(与 OMS/WMS 代码侧降级口径一致) + */ + @ApiOperation("查询仓库库存锁定模式: 1-下单冻结 2-分配库存冻结") + @GetMapping(value = "/getStockLockMode/{warehouseId}") + public AjaxResult getStockLockMode(@PathVariable("warehouseId") Long warehouseId) + { + return AjaxResult.success(warehouseApplicationService.getStockLockMode(warehouseId)); + } + + /** + * 单独切换仓库库存锁定模式(不改动仓库其他信息): + * 模式变化且该仓库存在在途下单冻结出库单(freeze_status 1/2)时拒绝切换; + * WMS校验服务不可用时同样拒绝(fail-closed) + */ + @ApiOperation("切换仓库库存锁定模式: 1-下单冻结 2-分配库存冻结") + @PostMapping(value = "/switchStockLockMode/{warehouseId}/{stockLockMode}") + public AjaxResult switchStockLockMode(@PathVariable("warehouseId") Long warehouseId, + @PathVariable("stockLockMode") Integer stockLockMode) + { + return toAjax(warehouseApplicationService.switchStockLockMode(warehouseId, stockLockMode)); + } + @ApiOperation("批量作废仓库") @GetMapping("/nullifyByIds/{warehouseIds}") diff --git a/mhd-modules/mhd-system/src/main/resources/mapper/basic/warehouse/WarehouseMapper.xml b/mhd-modules/mhd-system/src/main/resources/mapper/basic/warehouse/WarehouseMapper.xml index fe8b9fbe0..e186399a0 100644 --- a/mhd-modules/mhd-system/src/main/resources/mapper/basic/warehouse/WarehouseMapper.xml +++ b/mhd-modules/mhd-system/src/main/resources/mapper/basic/warehouse/WarehouseMapper.xml @@ -43,6 +43,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -51,7 +52,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" warehouse_type, warehouse_type_name, warehouse_structure, warehouse_nature, warm_layer_code, warm_layer_name, director_name, director_phone, region_code, region_name, detail_address, longitude, latitude, building_region, usage_region, remark, opening_up, nullify, create_time, create_by, create_by_name, update_time, update_by, - update_by_name, del_flag, IS_ENABLE ,WORK_MODE,TASK_MODE,SAMPLE_OUT_ORDER_NO_PREFIX,ORDER_NO_GENERATE_RULE,IN_ORDER_NO_PREFIX, + update_by_name, del_flag, IS_ENABLE ,WORK_MODE,TASK_MODE,STOCK_LOCK_MODE,SAMPLE_OUT_ORDER_NO_PREFIX,ORDER_NO_GENERATE_RULE,IN_ORDER_NO_PREFIX, OUT_ORDER_NO_PREFIX from warehouse diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/service/ExecutionStockOutOrderDomainService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/service/ExecutionStockOutOrderDomainService.java index 6a1617cb8..bb352395a 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/service/ExecutionStockOutOrderDomainService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/service/ExecutionStockOutOrderDomainService.java @@ -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); 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 12d1a1ce1..9b91a0600 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 @@ -236,22 +236,32 @@ public class ReservationStockOutOrderDomainService { if (stockOutOrderDO.getReservationOrderSource() == null || stockOutOrderDO.getReservationOrderSource().isEmpty()) { stockOutOrderDO.setReservationOrderSource("手动创建"); } - stockOutOrderService.insert(stockOutOrderDO); - List 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 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 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 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 details = stockOutOrderDO.getMaterialDetailList(); + if (CollectionUtils.isEmpty(details)) { + return false; + } + // 样品出库交接不扣减库存,同样不参与下单冻结 + if ("yang_pin_chu_ku".equals(stockOutOrderDO.getBusinessType())) { + return false; + } + List 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 details = stockOutOrderDO.getMaterialDetailList(); + if (CollectionUtils.isEmpty(details)) { + return; + } + // 样品出库交接不扣减库存,同样不参与下单冻结 + if ("yang_pin_chu_ku".equals(stockOutOrderDO.getBusinessType())) { + return; + } + List freezeDetails = filterFreezeDetails(details, stockOutOrderDO.getWarehouseId(), existingPO.getOutOrderNumber()); + if (freezeDetails.isEmpty()) { + return; + } + String orderNumber = existingPO.getOutOrderNumber(); + // 已下发WMS(存在活动执行单)禁止编辑:执行单明细与冻结记录必须保持一致,须先取消下发 + List activeExecutions = executionStockOutOrderService.list(new QueryWrapper().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 filterFreezeDetails(List details, Long defaultWarehouseId, String businessOrderNumber) { + List 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 list = executionStockOutOrderService.list(new QueryWrapper().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 materialDetails = executionOutMaterialDetailService.list(new QueryWrapper().lambda() @@ -992,7 +1193,8 @@ public class ReservationStockOutOrderDomainService { if (!groupBaseValid) { return; } - //WMS库存校验(接口内部会聚合该仓全部明细的需求量后比对可用库存) + //WMS库存校验(接口内部会聚合该仓全部明细的需求量后比对可用库存; + // 传业务单号:下单冻结模式下行级可用量=库存可用+本单冻结记录量) List 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); } diff --git a/mhd_wms/src/main/java/com/mhd/wms/application/service/stockOutOrder/StockOutOrderApplicationService.java b/mhd_wms/src/main/java/com/mhd/wms/application/service/stockOutOrder/StockOutOrderApplicationService.java index b209f10ee..e43e6d37d 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/application/service/stockOutOrder/StockOutOrderApplicationService.java +++ b/mhd_wms/src/main/java/com/mhd/wms/application/service/stockOutOrder/StockOutOrderApplicationService.java @@ -66,6 +66,7 @@ import com.mhd.wms.domain.stockOutOrder.repository.mapper.StockOutOrderMapper; import com.mhd.wms.domain.stockOutOrder.repository.po.StockOutOrderPO; import com.mhd.wms.domain.stockOutOrder.repository.todo.StockOutOrderDO; import com.mhd.wms.domain.stockOutOrder.service.StockOutOrderDomainService; +import com.mhd.wms.domain.outOrderFreezeRecord.service.OutOrderFreezeRecordDomainService; import com.mhd.wms.interfaces.dto.stockOutOrder.StockOutOrderDTO; import lombok.extern.slf4j.Slf4j; import org.apache.http.client.methods.CloseableHttpResponse; @@ -99,6 +100,8 @@ public class StockOutOrderApplicationService { @Autowired private StockOutOrderDomainService stockOutOrderDomainService; @Autowired + private OutOrderFreezeRecordDomainService outOrderFreezeRecordDomainService; + @Autowired private OutMaterialDetailDomainService outMaterialDetailDomainService; @Autowired private MaterialInventoryDomainService materialInventoryDomainService; @@ -1062,6 +1065,13 @@ public class StockOutOrderApplicationService { } // 当前物料的可用库存 BigDecimal availableStock = materialInventory.getAllocationQuantity(); + // 下单冻结模式:加回本单该行的冻结记录量(下单时已冻结导致可用=0,冻结行会被跳过 + // 导致分配不到量;承接时数量不动,此处仅校验/选行口径) + BigDecimal orderFrozen = outOrderFreezeRecordDomainService.sumOrderFreezeQuantity( + stockOutOrderDb.getBusinessOrderNo(), materialInventory.getMaterialInventoryId()); + if (orderFrozen.compareTo(BigDecimal.ZERO) > 0) { + availableStock = availableStock.add(orderFrozen); + } // 如果当前物料没有库存,跳过 if (availableStock == null || availableStock.compareTo(BigDecimal.ZERO) <= 0) { continue; @@ -1149,14 +1159,23 @@ public class StockOutOrderApplicationService { // 当前物料的可用库存 BigDecimal availableStock = materialInventory.getAllocationQuantity(); + // 下单冻结模式:加回本单该行的冻结记录量(下单时已冻结导致可用=0,纯可用口径会把冻结明细 + // 跳过/报错,永远到不了 batchAssignUpdate 的承接分支;承接时数量不动,此处仅校验口径) + BigDecimal orderFrozen = outOrderFreezeRecordDomainService.sumOrderFreezeQuantity( + stockOutOrderDb.getBusinessOrderNo(), materialInventoryId); + if (orderFrozen.compareTo(BigDecimal.ZERO) > 0) { + availableStock = availableStock.add(orderFrozen); + } // 如果当前物料没有库存,跳过 if (availableStock == null || availableStock.compareTo(BigDecimal.ZERO) <= 0) { continue; } if (availableStock.compareTo(remainingQuantity) < 0) { throw new ServiceException( - String.format("出库单[%s]物料[%s]可用库存不足,计划数量:%s,当前可用库存:%s", - outOrderNumber, materialDesc, remainingQuantity, availableStock) + String.format("出库单[%s]物料[%s]可用库存不足,计划数量:%s,当前可用库存:%s(含下单冻结%s)", + outOrderNumber, materialDesc, remainingQuantity, + availableStock.stripTrailingZeros().toPlainString(), + orderFrozen.stripTrailingZeros().toPlainString()) ); } // detail.setMaterialInventoryId(materialInventory.getMaterialInventoryId()); @@ -1187,9 +1206,12 @@ public class StockOutOrderApplicationService { /** * OMS下发前库存校验:按库存ID聚合各明细计划数量,逐一比对WMS可用库存, * 全部比对完成后汇总所有不足项一次性抛错。用于OMS在下发推送前拦截 - * (此前单据先落库、分配才失败,会在WMS残留"已审核"状态的垃圾单) + * (此前单据先落库、分配才失败,会在WMS残留"已审核"状态的垃圾单)。 + * 下单冻结模式:businessOrderNumber 非空时行级可用量 = 库存可用 + 本单该行冻结记录量 + * (下单时已冻结导致可用=0,按纯可用校验必失败;冻结量加回后校验=防冻结漂移), + * 冻结量<需求即报"下单冻结记录不足";重复下发保护原样保留。 */ - public void checkOmsAssignInventory(List outMaterialDetails) { + public void checkOmsAssignInventory(List outMaterialDetails, String businessOrderNumber) { if (CollectionUtils.isEmpty(outMaterialDetails)) { return; } @@ -1232,9 +1254,17 @@ public class StockOutOrderApplicationService { } BigDecimal available = materialInventory.getAllocationQuantity() == null ? BigDecimal.ZERO : materialInventory.getAllocationQuantity(); + // 下单冻结模式:可用量加回本单该行的冻结记录量(下单冻结导致可用=0,纯可用校验必失败); + // 正常流冻结量≥需求(下单冻结/重锁保证),冻结量不足即报错=防冻结被异常释放的漂移 + BigDecimal frozen = BigDecimal.ZERO; + if (businessOrderNumber != null && !businessOrderNumber.isEmpty()) { + frozen = outOrderFreezeRecordDomainService.sumOrderFreezeQuantity(businessOrderNumber, materialInventoryId); + available = available.add(frozen); + } if (available.compareTo(need) < 0) { - errors.add(String.format("物料[%s]可用库存不足:需要%s,当前可用%s", - materialDesc, need.stripTrailingZeros().toPlainString(), available.stripTrailingZeros().toPlainString())); + errors.add(String.format("物料[%s]可用库存不足:需要%s,当前可用%s(含下单冻结%s)", + materialDesc, need.stripTrailingZeros().toPlainString(), + available.stripTrailingZeros().toPlainString(), frozen.stripTrailingZeros().toPlainString())); } } if (!errors.isEmpty()) { diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/handoverTaskOrder/service/HandoverTaskOrderDomainService.java b/mhd_wms/src/main/java/com/mhd/wms/domain/handoverTaskOrder/service/HandoverTaskOrderDomainService.java index 13b14ea91..56804b665 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/handoverTaskOrder/service/HandoverTaskOrderDomainService.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/handoverTaskOrder/service/HandoverTaskOrderDomainService.java @@ -80,6 +80,8 @@ public class HandoverTaskOrderDomainService { @Autowired private IHandoverTaskOrderService handoverTaskOrderService; @Autowired + private com.mhd.wms.domain.outOrderFreezeRecord.service.OutOrderFreezeRecordDomainService outOrderFreezeRecordDomainService; + @Autowired private MaterialInventoryMapper materialInventoryMapper; @Autowired private OutMaterialDetailMapper outMaterialDetailMapper; @@ -591,6 +593,9 @@ public class HandoverTaskOrderDomainService { } } } + // 下单冻结模式:交接扣减完成(库存-、冻结-),执行单承接的冻结记录置已交接终态 + // (无冻结记录时幂等无操作;数量扣减本身两种模式同逻辑,零改动) + outOrderFreezeRecordDomainService.markHandoverCompleted(orderNumber); return clearoutParamDOs; } diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/mapper/MaterialInventoryMapper.java b/mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/mapper/MaterialInventoryMapper.java index 7e0ed1acc..828ed2ec3 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/mapper/MaterialInventoryMapper.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/mapper/MaterialInventoryMapper.java @@ -140,4 +140,41 @@ public interface MaterialInventoryMapper extends BaseMapper public Map queryShipperParameters(@Param("shipperId") Long shipperId,@Param("parametersName") String parametersName); public Map queryMaterialClassify(@Param("id") Long id); + + /** + * OMS下单冻结-条件扣减:可用数量-quantity、冻结数量+quantity(库存数量不变), + * WHERE 带可用数量充足条件,返回影响行数(0=可用不足),由调用方判0抛异常整体回滚防止并发超卖 + * (不可用现有 GREATEST 截断版批量SQL——静默截为0会掩盖超卖) + * + * @param materialInventoryId 库存行ID + * @param quantity 冻结数量 + * @return 影响行数:1-成功 0-可用数量不足 + */ + public int omsFreezeDeductAvailable(@Param("materialInventoryId") Long materialInventoryId, @Param("quantity") BigDecimal quantity); + + /** + * OMS下单解冻-条件释放:可用数量+quantity、冻结数量-quantity(库存数量不变), + * WHERE 带冻结数量充足条件,返回影响行数(0=冻结不足),由调用方判0抛异常防止重复解冻/负数 + * + * @param materialInventoryId 库存行ID + * @param quantity 释放数量 + * @return 影响行数:1-成功 0-冻结数量不足 + */ + public int omsReleaseFreezeToAvailable(@Param("materialInventoryId") Long materialInventoryId, @Param("quantity") BigDecimal quantity); + + /** + * OMS下单冻结单出库反审-还原为冻结:实物库存+quantity、冻结数量+quantity(可用不变), + * 并还原重量/体积/面积;将已出库扣减状态还原为"下单已冻结待出库"(与交接扣减 ckjj 库存-冻结- 互逆) + * + * @param materialInventoryId 库存行ID + * @param quantity 还原数量 + * @param netWeight 还原净重 + * @param grossWeight 还原毛重 + * @param volume 还原体积 + * @param area 还原面积 + * @return 影响行数 + */ + public int omsRestoreInventoryToFrozen(@Param("materialInventoryId") Long materialInventoryId, @Param("quantity") BigDecimal quantity, + @Param("netWeight") BigDecimal netWeight, @Param("grossWeight") BigDecimal grossWeight, + @Param("volume") BigDecimal volume, @Param("area") BigDecimal area); } \ No newline at end of file diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/persistence/MaterialInventoryImpl.java b/mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/persistence/MaterialInventoryImpl.java index 10f3c5202..e0f4bdf73 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/persistence/MaterialInventoryImpl.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/materialInventory/repository/persistence/MaterialInventoryImpl.java @@ -647,7 +647,7 @@ public class MaterialInventoryImpl extends ServiceImpl { @@ -1016,15 +1022,61 @@ public class OutMaterialDetailImpl extends ServiceImpl outMaterialDetailList, String outOrderNumber, + LoginUser loginUser, String businessOrderNo) { + List normalAssignList = new ArrayList<>(); + for (OutMaterialDetail detail : outMaterialDetailList) { + if (detail.getNowAllocationQuantity() == null + || detail.getNowAllocationQuantity().compareTo(BigDecimal.ZERO) <= 0 + || detail.getMaterialInventoryId() == null) { + // 无效行交给现有逻辑自然跳过,保持行为一致 + normalAssignList.add(detail); + continue; + } + BigDecimal frozenSum = outOrderFreezeRecordDomainService.sumOrderFreezeQuantity(businessOrderNo, detail.getMaterialInventoryId()); + if (frozenSum.compareTo(BigDecimal.ZERO) > 0) { + if (frozenSum.compareTo(detail.getNowAllocationQuantity()) < 0) { + throw new ServiceException("物料[" + (detail.getMaterialCode() != null ? detail.getMaterialCode() : "") + + "]库存行[" + detail.getMaterialInventoryId() + "]本单下单冻结量[" + frozenSum + + "]小于本次分配数量[" + detail.getNowAllocationQuantity() + "],分配失败"); + } + // 下单冻结模式明细:数量不动,冻结记录承接给执行单(本分配事务内,失败随事务回滚) + outOrderFreezeRecordDomainService.undertakeFromFreezeRecords( + businessOrderNo, detail.getMaterialInventoryId(), outOrderNumber, detail.getNowAllocationQuantity()); + } else { + normalAssignList.add(detail); + } + } + if (!CollectionUtils.isEmpty(normalAssignList)) { + xgkc(normalAssignList, outOrderNumber, loginUser); + } + } + private synchronized void qxfp(List outMaterialDetailList,String outOrderNumber,LoginUser loginUser) { StockOutOrder stockOutOrder = stockOutOrderMapper.selectOne(new QueryWrapper().lambda() .eq(StockOutOrder::getDelFlag,1) .eq(StockOutOrder::getOutOrderNumber, outOrderNumber)); + // 下单冻结判据用业务单号(null 安全:查不到单/无单号时按无冻结记录处理,走原还原逻辑) + String businessOrderNo = stockOutOrder == null ? null : stockOutOrder.getBusinessOrderNo(); for (OutMaterialDetailDO outMaterialDetail : outMaterialDetailList) { Long materialInventoryId = outMaterialDetail.getMaterialInventoryId(); BigDecimal allocationQuantity = outMaterialDetail.getActualQuantity(); MaterialInventory materialInventoryPO = materialInventoryMapper.selectById(materialInventoryId); if (materialInventoryPO != null) { + // 下单冻结模式明细(本单该库存行存在活跃冻结记录):分配时数量未动,取消分配不还原数量 + // (不可用 continue——会跳过循环尾部的子行 childrenqxfp 处理,须 if 包裹还原块) + if (!outOrderFreezeRecordDomainService.hasOrderFreezeRecord(businessOrderNo, materialInventoryId)) { MaterialInventoryParamDO materialInventoryParamDO = new MaterialInventoryParamDO(); materialInventoryParamDO.setMaterialInventoryId(materialInventoryId); materialInventoryParamDO.setQuantity(allocationQuantity); @@ -1046,6 +1098,7 @@ public class OutMaterialDetailImpl extends ServiceImpl().lambda() .eq(StockOutOrder::getDelFlag,1) .eq(StockOutOrder::getOutOrderNumber, outOrderNumber)); + // 下单冻结判据用业务单号(null 安全:查不到单/无单号时按无冻结记录处理,走原还原逻辑) + String businessOrderNo = stockOutOrder == null ? null : stockOutOrder.getBusinessOrderNo(); for (OutMaterialDetailDO outMaterialDetail : outMaterialDetailList) { Long materialInventoryId = outMaterialDetail.getMaterialInventoryId(); BigDecimal allocationQuantity = outMaterialDetail.getActualQuantity(); MaterialInventory materialInventoryPO = materialInventoryMapper.selectById(materialInventoryId); if (materialInventoryPO != null) { + // 下单冻结模式明细(本单该库存行存在活跃冻结记录):分配时数量未动,取消分配不还原数量 + // (叶子子行无嵌套子项,可安全 continue) + if (outOrderFreezeRecordDomainService.hasOrderFreezeRecord(businessOrderNo, materialInventoryId)) { + continue; + } MaterialInventoryParamDO materialInventoryParamDO = new MaterialInventoryParamDO(); materialInventoryParamDO.setMaterialInventoryId(materialInventoryId); diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/entity/OutOrderFreezeRecord.java b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/entity/OutOrderFreezeRecord.java new file mode 100644 index 000000000..ba2c53480 --- /dev/null +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/entity/OutOrderFreezeRecord.java @@ -0,0 +1,77 @@ +package com.mhd.wms.domain.outOrderFreezeRecord.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.mhd.common.core.web.domain.BaseVOEntity; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +/** + * WMS出库单库存冻结记录对象 out_order_freeze_record + * + * @description OMS下单冻结库存模式(防超卖)的「业务单↔库存行↔冻结数量」占用关系: + * OMS出库业务单下单即冻结WMS库存(可用-、冻结+,库存数量不变), + * 编辑=原子释放旧冻结+按新明细重锁,取消=释放冻结,分配=承接(数量不动), + * 交接=扣库存+释放冻结,反审=还原为冻结。 + * 本表是编辑重锁/取消解冻/分配承接/交接标记/反审还原的对账依据与单据级隔离判据。 + * @date 2026-09-12 + */ +@Data +@TableName("out_order_freeze_record") +public class OutOrderFreezeRecord extends BaseVOEntity { + private static final long serialVersionUID = 1L; + + @ApiModelProperty("冻结记录id") + @TableId(type = IdType.AUTO) + private Long freezeRecordId; + + @ApiModelProperty("OMS出库业务单号") + private String businessOrderNumber; + + @ApiModelProperty("执行出库单号(分配承接时回填,可空)") + private String executionOrderNumber; + + @ApiModelProperty("明细唯一标识(OMS明细uniqueId)") + private String materialDetailUniqueId; + + @ApiModelProperty("库存行ID(material_inventory.material_inventory_id)") + private Long materialInventoryId; + + @ApiModelProperty("仓库ID") + private Long warehouseId; + + @ApiModelProperty("物料基础信息ID") + private Long materialBaseInfoId; + + @ApiModelProperty("冻结数量") + private BigDecimal freezeQuantity; + + @ApiModelProperty("冻结状态: 1-业务单冻结 2-执行单承接 3-已交接 4-已释放") + private Integer freezeStatus; + + @ApiModelProperty("关联单号(业务单号#明细uniqueId,与库存调整记录relate_no对应)") + private String relateNo; + + @ApiModelProperty("备注") + private String remark; + + /** + * 冻结状态: 业务单冻结(下单/编辑重锁后) + */ + public static final int STATUS_BUSINESS_FROZEN = 1; + /** + * 冻结状态: 执行单承接(WMS分配时数量不动,仅记录归属转移到执行单) + */ + public static final int STATUS_EXECUTION_UNDERTAKE = 2; + /** + * 冻结状态: 已交接(出库交接扣减库存后终态) + */ + public static final int STATUS_HANDOVER = 3; + /** + * 冻结状态: 已释放(取消/解冻/重锁时的旧记录) + */ + public static final int STATUS_RELEASED = 4; +} diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/facade/IOutOrderFreezeRecordService.java b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/facade/IOutOrderFreezeRecordService.java new file mode 100644 index 000000000..2ffe150b1 --- /dev/null +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/facade/IOutOrderFreezeRecordService.java @@ -0,0 +1,13 @@ +package com.mhd.wms.domain.outOrderFreezeRecord.repository.facade; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.mhd.wms.domain.outOrderFreezeRecord.entity.OutOrderFreezeRecord; + +/** + * WMS出库单库存冻结记录Service接口 + * + * @date 2026-09-12 + */ +public interface IOutOrderFreezeRecordService extends IService +{ +} diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/mapper/OutOrderFreezeRecordMapper.java b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/mapper/OutOrderFreezeRecordMapper.java new file mode 100644 index 000000000..8ff1e5ab9 --- /dev/null +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/mapper/OutOrderFreezeRecordMapper.java @@ -0,0 +1,13 @@ +package com.mhd.wms.domain.outOrderFreezeRecord.repository.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.mhd.wms.domain.outOrderFreezeRecord.entity.OutOrderFreezeRecord; + +/** + * WMS出库单库存冻结记录Mapper接口 + * + * @date 2026-09-12 + */ +public interface OutOrderFreezeRecordMapper extends BaseMapper +{ +} diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/persistence/OutOrderFreezeRecordImpl.java b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/persistence/OutOrderFreezeRecordImpl.java new file mode 100644 index 000000000..3bd7c24bf --- /dev/null +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/repository/persistence/OutOrderFreezeRecordImpl.java @@ -0,0 +1,16 @@ +package com.mhd.wms.domain.outOrderFreezeRecord.repository.persistence; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.mhd.wms.domain.outOrderFreezeRecord.entity.OutOrderFreezeRecord; +import com.mhd.wms.domain.outOrderFreezeRecord.repository.facade.IOutOrderFreezeRecordService; +import com.mhd.wms.domain.outOrderFreezeRecord.repository.mapper.OutOrderFreezeRecordMapper; +import org.springframework.stereotype.Service; + +/** + * WMS出库单库存冻结记录Service业务层处理 + * + * @date 2026-09-12 + */ +@Service +public class OutOrderFreezeRecordImpl extends ServiceImpl implements IOutOrderFreezeRecordService { +} diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/service/OutOrderFreezeRecordDomainService.java b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/service/OutOrderFreezeRecordDomainService.java new file mode 100644 index 000000000..84e174110 --- /dev/null +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/outOrderFreezeRecord/service/OutOrderFreezeRecordDomainService.java @@ -0,0 +1,536 @@ +package com.mhd.wms.domain.outOrderFreezeRecord.service; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.mhd.common.core.exception.ServiceException; +import com.mhd.common.security.utils.SecurityUtils; +import com.mhd.system.api.OmsServiceFeign; +import com.mhd.system.api.domain.MaterialInventoryOmsParamDO; +import com.mhd.system.api.domain.OmsFreezeInventoryParam; +import com.mhd.system.api.domain.OutMaterialDetail; +import com.mhd.system.api.model.LoginUser; +import com.mhd.wms.domain.materialInventory.entity.MaterialInventory; +import com.mhd.wms.domain.materialInventory.repository.facade.IMaterialInventoryService; +import com.mhd.wms.domain.materialInventory.repository.mapper.MaterialInventoryMapper; +import com.mhd.wms.domain.materialInventory.repository.todo.MaterialInventoryParamDO; +import com.mhd.wms.domain.outOrderFreezeRecord.entity.OutOrderFreezeRecord; +import com.mhd.wms.domain.outOrderFreezeRecord.repository.facade.IOutOrderFreezeRecordService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.List; + +/** + * WMS出库单库存冻结记录领域服务 + * + * @description OMS下单冻结库存模式(防超卖):解决OMS下单与WMS分配库存时间差导致的出库库存不足问题。 + * 库存数量 = 可用数量 + 冻结数量,交接前全程不变(出库交接才扣减实物库存)。 + * 「下单」→ 冻结(可用-、冻结+,条件更新防并发超卖); + * 「编辑」→ omsRelockInventory 同一事务先释放原锁定再按新明细重新锁定(原子操作); + * 「取消」→ omsUnfreezeInventory 释放冻结(可用+、冻结-); + * 「分配」→ 不动数量,冻结记录由业务单承接给执行单(undertakeFromFreezeRecords FIFO按量承接); + * 「取消下发」→ releaseExecutionUndertake 承接归还业务单; + * 「交接」→ markHandoverCompleted 置已交接终态(数量由现有 ckjj 扣减); + * 「反审」→ 已出库 reactivateHandoverFreeze 重激活 + 实物/冻结还原。 + * @date 2026-09-12 + */ +@Service +public class OutOrderFreezeRecordDomainService { + + private static final Logger log = LoggerFactory.getLogger(OutOrderFreezeRecordDomainService.class); + + @Autowired + private IOutOrderFreezeRecordService outOrderFreezeRecordService; + + @Autowired + private MaterialInventoryMapper materialInventoryMapper; + + @Autowired + private IMaterialInventoryService materialInventoryService; + + @Autowired + private OmsServiceFeign omsServiceFeign; + + // ================================ OMS 冻结三接口 ================================ + + /** + * OMS出库业务单下单冻结库存(幂等:同业务单号已有活跃冻结记录时直接返回成功) + * 明细必须携带 materialInventoryId 精确定位库存行(下单冻结模式仓库明细强制必选,OMS侧已前置校验) + */ + @Transactional(rollbackFor = Exception.class) + public Boolean omsFreezeInventory(OmsFreezeInventoryParam param) { + validateParam(param); + String businessOrderNumber = param.getOutOrderNumber(); + List activeRecords = listActiveRecords(businessOrderNumber); + if (!CollectionUtils.isEmpty(activeRecords)) { + // 幂等:Feign超时重试/重复提交场景,已有活跃冻结视为成功 + log.info("下单冻结幂等:出库业务单[{}]已存在{}条活跃冻结记录,跳过重复冻结", businessOrderNumber, activeRecords.size()); + return true; + } + freezeByDetails(param, businessOrderNumber); + return true; + } + + /** + * OMS出库业务单取消/删除-释放冻结库存(可用+、冻结-,库存数量不变;幂等:无活跃冻结直接成功) + */ + @Transactional(rollbackFor = Exception.class) + public Boolean omsUnfreezeInventory(String businessOrderNumber, Long operatorId, String operatorName) { + if (!StringUtils.hasText(businessOrderNumber)) { + throw new ServiceException("出库业务单号不能为空"); + } + List activeRecords = listActiveRecords(businessOrderNumber); + if (CollectionUtils.isEmpty(activeRecords)) { + // 幂等:未冻结(模式切换前老单/重复取消)直接成功 + log.info("释放冻结幂等:出库业务单[{}]无活跃冻结记录", businessOrderNumber); + return true; + } + // 已承接给执行单/已交接的冻结不可直接释放,必须先走取消下发/反审链路 + for (OutOrderFreezeRecord record : activeRecords) { + if (record.getFreezeStatus() != null && record.getFreezeStatus() == OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE) { + throw new ServiceException("出库业务单[" + businessOrderNumber + "]冻结已被执行单承接,请先取消下发"); + } + if (record.getFreezeStatus() != null && record.getFreezeStatus() == OutOrderFreezeRecord.STATUS_HANDOVER) { + throw new ServiceException("出库业务单[" + businessOrderNumber + "]冻结已交接(已出库),请走反审流程"); + } + } + LoginUserHolder holder = resolveLoginUser(operatorId, operatorName); + for (OutOrderFreezeRecord record : activeRecords) { + releaseOneRecord(record, businessOrderNumber, holder); + } + return true; + } + + /** + * OMS出库业务单编辑-原子重锁:同一事务内先释放原冻结,再按新明细重新冻结。 + * 任一新明细可用不足→抛异常整体回滚(旧冻结同样还原,满足"先释放再重新锁定"的原子性要求) + */ + @Transactional(rollbackFor = Exception.class) + public Boolean omsRelockInventory(OmsFreezeInventoryParam param) { + validateParam(param); + String businessOrderNumber = param.getOutOrderNumber(); + List activeRecords = listActiveRecords(businessOrderNumber); + for (OutOrderFreezeRecord record : activeRecords) { + if (record.getFreezeStatus() != null && record.getFreezeStatus() != OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN) { + throw new ServiceException("出库业务单[" + businessOrderNumber + "]已下发WMS(冻结已被承接),不可编辑,请先取消下发"); + } + } + LoginUserHolder holder = resolveLoginUser(param.getOperatorId(), param.getOperatorName()); + // 1. 先按原冻结记录精确释放(数量以冻结记录为准,防止与明细数量漂移) + for (OutOrderFreezeRecord record : activeRecords) { + releaseOneRecord(record, businessOrderNumber, holder); + } + // 2. 再按新明细重新冻结(任一行失败抛异常,整个事务回滚=旧冻结也还原) + freezeByDetails(param, businessOrderNumber); + return true; + } + + // ================================ 核心链路状态机方法 ================================ + + /** + * WMS分配-冻结承接(FIFO按量):校验本单本行冻结量≥本次分配量后,从本单该行 status=1 记录 + * 按id升序逐条扣减承接量:记录余量承接完→status=2+执行单号;余量不足整条→只改freeze_quantity为承接量, + * 剩余量另插一条status=1新记录(保证任一时刻记录数量之和=该单该行总冻结量,守恒)。 + * 数量字段不动(下单时已冻结)。在分配事务内调用,失败随分配整体回滚。 + * + * @param businessOrderNumber 业务单号(单据级隔离判据,防止误消费别单冻结) + * @param materialInventoryId 库存行ID + * @param executionOrderNumber 执行出库单号(WMS出库单号) + * @param undertakeQuantity 本次承接(分配)数量 + * @return true-承接成功(调用前需先校验冻结量充足) + */ + @Transactional(rollbackFor = Exception.class) + public Boolean undertakeFromFreezeRecords(String businessOrderNumber, Long materialInventoryId, + String executionOrderNumber, BigDecimal undertakeQuantity) { + BigDecimal remaining = undertakeQuantity; + List records = listOrderRowActiveRecords(businessOrderNumber, materialInventoryId); + records.sort(Comparator.comparing(r -> r.getFreezeRecordId() == null ? Long.MAX_VALUE : r.getFreezeRecordId())); + for (OutOrderFreezeRecord record : records) { + if (remaining.compareTo(BigDecimal.ZERO) <= 0) { + break; + } + BigDecimal recordQty = record.getFreezeQuantity() == null ? BigDecimal.ZERO : record.getFreezeQuantity(); + if (recordQty.compareTo(BigDecimal.ZERO) <= 0) { + continue; + } + if (recordQty.compareTo(remaining) <= 0) { + // 整条记录承接完 + record.setFreezeStatus(OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE); + record.setExecutionOrderNumber(executionOrderNumber); + record.setUpdateTime(new Date()); + outOrderFreezeRecordService.updateById(record); + remaining = remaining.subtract(recordQty); + } else { + // 部分承接:本条记承接量,余量拆一条新记录保留 status=1 + BigDecimal keepQty = recordQty.subtract(remaining); + record.setFreezeQuantity(remaining); + record.setFreezeStatus(OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE); + record.setExecutionOrderNumber(executionOrderNumber); + record.setUpdateTime(new Date()); + outOrderFreezeRecordService.updateById(record); + OutOrderFreezeRecord keepRecord = new OutOrderFreezeRecord(); + keepRecord.setBusinessOrderNumber(record.getBusinessOrderNumber()); + keepRecord.setMaterialDetailUniqueId(record.getMaterialDetailUniqueId()); + keepRecord.setMaterialInventoryId(record.getMaterialInventoryId()); + keepRecord.setWarehouseId(record.getWarehouseId()); + keepRecord.setMaterialBaseInfoId(record.getMaterialBaseInfoId()); + keepRecord.setFreezeQuantity(keepQty); + keepRecord.setFreezeStatus(OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN); + keepRecord.setRelateNo(record.getRelateNo()); + keepRecord.setCreateBy(record.getCreateBy()); + keepRecord.setCreateByName(record.getCreateByName()); + keepRecord.setCreateTime(new Date()); + keepRecord.setDelFlag(1); + outOrderFreezeRecordService.save(keepRecord); + remaining = BigDecimal.ZERO; + } + } + return true; + } + + /** + * 取消下发/反审(未出库)-解除执行单承接:冻结归还业务单(数量不动,仅状态 2→1) + */ + @Transactional(rollbackFor = Exception.class) + public Boolean releaseExecutionUndertake(String executionOrderNumber) { + if (!StringUtils.hasText(executionOrderNumber)) { + return true; + } + List records = outOrderFreezeRecordService.list(new LambdaQueryWrapper() + .eq(OutOrderFreezeRecord::getExecutionOrderNumber, executionOrderNumber) + .eq(OutOrderFreezeRecord::getFreezeStatus, OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE) + .eq(OutOrderFreezeRecord::getDelFlag, 1)); + for (OutOrderFreezeRecord record : records) { + record.setFreezeStatus(OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN); + record.setExecutionOrderNumber(null); + record.setUpdateTime(new Date()); + outOrderFreezeRecordService.updateById(record); + } + return true; + } + + /** + * 出库交接完成-冻结记录置已交接终态(数量已在交接时由现有 ckjj 扣减:库存-、冻结-) + */ + @Transactional(rollbackFor = Exception.class) + public Boolean markHandoverCompleted(String executionOrderNumber) { + if (!StringUtils.hasText(executionOrderNumber)) { + return true; + } + List records = outOrderFreezeRecordService.list(new LambdaQueryWrapper() + .eq(OutOrderFreezeRecord::getExecutionOrderNumber, executionOrderNumber) + .eq(OutOrderFreezeRecord::getFreezeStatus, OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE) + .eq(OutOrderFreezeRecord::getDelFlag, 1)); + for (OutOrderFreezeRecord record : records) { + record.setFreezeStatus(OutOrderFreezeRecord.STATUS_HANDOVER); + record.setUpdateTime(new Date()); + outOrderFreezeRecordService.updateById(record); + } + return true; + } + + /** + * 反审(已出库)-重新激活:已交接冻结记录(status=3)归还业务单(status=1,数量不动), + * 业务单回到未下发可编辑态,冻结继续由业务单持有(实物/冻结数量由调用方还原) + */ + @Transactional(rollbackFor = Exception.class) + public Boolean reactivateHandoverFreeze(String executionOrderNumber) { + if (!StringUtils.hasText(executionOrderNumber)) { + return true; + } + List records = outOrderFreezeRecordService.list(new LambdaQueryWrapper() + .eq(OutOrderFreezeRecord::getExecutionOrderNumber, executionOrderNumber) + .eq(OutOrderFreezeRecord::getFreezeStatus, OutOrderFreezeRecord.STATUS_HANDOVER) + .eq(OutOrderFreezeRecord::getDelFlag, 1)); + for (OutOrderFreezeRecord record : records) { + record.setFreezeStatus(OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN); + record.setExecutionOrderNumber(null); + record.setUpdateTime(new Date()); + outOrderFreezeRecordService.updateById(record); + } + return true; + } + + // ================================ 查询判据 ================================ + + /** + * 执行单是否存在活跃冻结记录(status in 2-承接 3-已交接):判断该执行单是否下单冻结模式单据 + */ + public boolean hasFreezeForExecutionOrder(String executionOrderNumber) { + if (!StringUtils.hasText(executionOrderNumber)) { + return false; + } + int count = outOrderFreezeRecordService.count(new LambdaQueryWrapper() + .eq(OutOrderFreezeRecord::getExecutionOrderNumber, executionOrderNumber) + .in(OutOrderFreezeRecord::getFreezeStatus, Arrays.asList( + OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE, + OutOrderFreezeRecord.STATUS_HANDOVER)) + .eq(OutOrderFreezeRecord::getDelFlag, 1)); + return count > 0; + } + + /** + * 本单该库存行的活跃冻结量合计(status in 1-业务单冻结 2-执行单承接)。 + * 单据级隔离判据:businessOrderNumber 必传(双单冻结同行时防止误消费别单冻结) + */ + public BigDecimal sumOrderFreezeQuantity(String businessOrderNumber, Long materialInventoryId) { + if (!StringUtils.hasText(businessOrderNumber) || materialInventoryId == null) { + return BigDecimal.ZERO; + } + List records = listOrderRowActiveRecords(businessOrderNumber, materialInventoryId); + BigDecimal sum = BigDecimal.ZERO; + for (OutOrderFreezeRecord record : records) { + if (record.getFreezeQuantity() != null) { + sum = sum.add(record.getFreezeQuantity()); + } + } + return sum; + } + + /** + * 本单该库存行是否存在活跃冻结记录(取消分配分支判据) + */ + public boolean hasOrderFreezeRecord(String businessOrderNumber, Long materialInventoryId) { + return sumOrderFreezeQuantity(businessOrderNumber, materialInventoryId).compareTo(BigDecimal.ZERO) > 0; + } + + /** + * 仓库是否存在活跃冻结记录(status in 1,2;仓库"库存锁定模式"切换守卫用: + * 存在在途冻结单时切换模式会导致在途单解冻/承接逻辑错乱) + */ + public boolean hasActiveFreezeByWarehouse(Long warehouseId) { + if (warehouseId == null) { + return false; + } + int count = outOrderFreezeRecordService.count(new LambdaQueryWrapper() + .eq(OutOrderFreezeRecord::getWarehouseId, warehouseId) + .in(OutOrderFreezeRecord::getFreezeStatus, Arrays.asList( + OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN, + OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE)) + .eq(OutOrderFreezeRecord::getDelFlag, 1)); + return count > 0; + } + + // ================================ 私有方法 ================================ + + private void validateParam(OmsFreezeInventoryParam param) { + if (param == null || !StringUtils.hasText(param.getOutOrderNumber())) { + throw new ServiceException("出库业务单号不能为空"); + } + if (CollectionUtils.isEmpty(param.getDetails())) { + throw new ServiceException("冻结明细不能为空"); + } + for (OutMaterialDetail detail : param.getDetails()) { + if (detail.getQuantity() == null || detail.getQuantity().compareTo(BigDecimal.ZERO) <= 0) { + throw new ServiceException("出库业务单[" + param.getOutOrderNumber() + "]明细[" + + (detail.getMaterialCode() != null ? detail.getMaterialCode() : "未知物料") + "]冻结数量必须大于0"); + } + } + } + + /** + * 按明细冻结:明细必须携带 materialInventoryId(下单冻结模式强制必选), + * 逐明细单行冻结——先写台账(调整前快照)再条件更新(0行=可用不足抛异常整体回滚), + * 全部扣减成功后统一写冻结记录+推OMS镜像(推镜像放事务末尾,缩小漂移窗口) + */ + private void freezeByDetails(OmsFreezeInventoryParam param, String businessOrderNumber) { + LoginUserHolder holder = resolveLoginUser(param.getOperatorId(), param.getOperatorName()); + List pendingFreezes = new ArrayList<>(); + for (OutMaterialDetail detail : param.getDetails()) { + if (detail.getMaterialInventoryId() == null || detail.getMaterialInventoryId() <= 0) { + throw new ServiceException("出库业务单[" + businessOrderNumber + "]物料[" + + (detail.getMaterialCode() != null ? detail.getMaterialCode() : detail.getMaterialBaseInfoId()) + + "]未关联库存行,无法冻结(下单冻结模式仓库明细必须选择库存批次)"); + } + MaterialInventory row = materialInventoryMapper.selectById(detail.getMaterialInventoryId()); + if (row == null || (row.getDelFlag() != null && row.getDelFlag() != 1)) { + throw new ServiceException("出库业务单[" + businessOrderNumber + "]物料[" + + (detail.getMaterialCode() != null ? detail.getMaterialCode() : "") + "]关联的库存行[" + + detail.getMaterialInventoryId() + "]不存在,冻结失败"); + } + BigDecimal freezeQuantity = detail.getQuantity(); + BigDecimal available = row.getAllocationQuantity() == null ? BigDecimal.ZERO : row.getAllocationQuantity(); + // 先行级预检给出友好错误(并发下仍由条件更新兜底判定) + if (available.compareTo(freezeQuantity) < 0) { + throw new ServiceException("出库业务单[" + businessOrderNumber + "]物料[" + + (detail.getMaterialCode() != null ? detail.getMaterialCode() : "") + "]库存行[" + + row.getMaterialInventoryId() + "]可用数量不足,需要:" + freezeQuantity + ",当前可用:" + available + ",下单冻结失败"); + } + String relateNo = buildRelateNo(businessOrderNumber, detail.getUniqueId()); + // 1.先写库存调整记录(调整前快照)2.条件更新扣可用加冻结(0行=并发下可用不足),失败抛异常整体回滚 + MaterialInventoryParamDO paramDO = buildParamDO(row, freezeQuantity, "7", relateNo); + materialInventoryService.kctzjl(paramDO, "下单冻结", holder.loginUser); + int affected = materialInventoryMapper.omsFreezeDeductAvailable(row.getMaterialInventoryId(), freezeQuantity); + if (affected != 1) { + throw new ServiceException("出库业务单[" + businessOrderNumber + "]物料[" + + (detail.getMaterialCode() != null ? detail.getMaterialCode() : "") + "]库存行[" + + row.getMaterialInventoryId() + "]可用数量不足,冻结失败(需求:" + freezeQuantity + ")"); + } + pendingFreezes.add(new PendingFreeze(row, freezeQuantity, detail.getUniqueId(), relateNo)); + } + // 全部扣减成功后:写冻结记录 + 推送OMS镜像库存 + for (PendingFreeze pending : pendingFreezes) { + insertFreezeRecord(pending.row, pending.quantity, businessOrderNumber, pending.uniqueId, pending.relateNo, holder); + MaterialInventoryParamDO paramDO = buildParamDO(pending.row, pending.quantity, "7", pending.relateNo); + pushOmsMirror(paramDO, "下单冻结"); + } + } + + /** + * 释放单条冻结记录:库存调整记录(下单解冻)+ 条件更新(可用+、冻结-)+ 记录置已释放 + 推OMS镜像 + */ + private void releaseOneRecord(OutOrderFreezeRecord record, String businessOrderNumber, LoginUserHolder holder) { + MaterialInventory row = materialInventoryMapper.selectById(record.getMaterialInventoryId()); + if (row == null || (row.getDelFlag() != null && row.getDelFlag() != 1)) { + throw new ServiceException("出库业务单[" + businessOrderNumber + "]冻结记录对应库存行[" + + record.getMaterialInventoryId() + "]不存在,解冻失败"); + } + MaterialInventoryParamDO paramDO = buildParamDO(row, record.getFreezeQuantity(), "3", record.getRelateNo()); + materialInventoryService.kctzjl(paramDO, "下单解冻", holder.loginUser); + int affected = materialInventoryMapper.omsReleaseFreezeToAvailable(record.getMaterialInventoryId(), record.getFreezeQuantity()); + if (affected != 1) { + throw new ServiceException("出库业务单[" + businessOrderNumber + "]库存行[" + + record.getMaterialInventoryId() + "]冻结数量不足,解冻失败"); + } + record.setFreezeStatus(OutOrderFreezeRecord.STATUS_RELEASED); + record.setUpdateTime(new Date()); + outOrderFreezeRecordService.updateById(record); + pushOmsMirror(buildParamDO(row, record.getFreezeQuantity(), "3", record.getRelateNo()), "下单解冻"); + } + + /** + * 组装库存变更参数:匹配键全部从【库存行】取值(OMS镜像按 + * materialBaseInfoId+warehouseId+inOrderNumber+lotNumber+batchNumber+purchaseDate 精确匹配, + * 从明细取会因缺 lotNo/purchaseDate 匹配到错误镜像行) + */ + private MaterialInventoryParamDO buildParamDO(MaterialInventory row, BigDecimal quantity, String type, String relateNo) { + MaterialInventoryParamDO paramDO = new MaterialInventoryParamDO(); + BigDecimal zero = BigDecimal.ZERO; + paramDO.setArea(zero); + paramDO.setVolume(zero); + paramDO.setNetWeight(zero); + paramDO.setGrossWeight(zero); + paramDO.setQuantity(quantity); + paramDO.setType(type); + paramDO.setMaterialInventoryId(row.getMaterialInventoryId()); + paramDO.setRelateNo(relateNo); + paramDO.setMaterialBaseInfoId(row.getMaterialBaseInfoId()); + paramDO.setWarehouseId(row.getWarehouseId()); + paramDO.setLotNumber(row.getLotNumber()); + paramDO.setBatchNumber(row.getBatchNumber()); + paramDO.setInOrderNumber(row.getInOrderNumber()); + paramDO.setProductionDate(row.getProductionDate()); + paramDO.setExpiryDate(row.getExpiryDate()); + paramDO.setPurchaseDate(row.getPurchaseDate()); + return paramDO; + } + + private void insertFreezeRecord(MaterialInventory row, BigDecimal quantity, String businessOrderNumber, + Long uniqueId, String relateNo, LoginUserHolder holder) { + OutOrderFreezeRecord record = new OutOrderFreezeRecord(); + record.setBusinessOrderNumber(businessOrderNumber); + record.setMaterialDetailUniqueId(uniqueId == null ? null : String.valueOf(uniqueId)); + record.setMaterialInventoryId(row.getMaterialInventoryId()); + record.setWarehouseId(row.getWarehouseId()); + record.setMaterialBaseInfoId(row.getMaterialBaseInfoId()); + record.setFreezeQuantity(quantity); + record.setFreezeStatus(OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN); + record.setRelateNo(relateNo); + record.setCreateBy(holder.loginUser.getUserid()); + record.setCreateByName(holder.operatorName); + record.setCreateTime(new Date()); + record.setDelFlag(1); + outOrderFreezeRecordService.save(record); + } + + /** + * 推送OMS镜像库存(type=7下单冻结:可用-冻结+ / type=3下单解冻:可用+冻结-),与分配/交接推送同模式 + */ + private void pushOmsMirror(MaterialInventoryParamDO paramDO, String kctzType) { + try { + MaterialInventoryOmsParamDO omsParamDO = new MaterialInventoryOmsParamDO(); + BeanUtils.copyProperties(paramDO, omsParamDO); + omsParamDO.setKctzType(kctzType); + omsServiceFeign.omsEdit(omsParamDO); + } catch (Exception e) { + // 镜像推送失败不阻断冻结主流程(与分配/交接链路同口径),由OMS侧定时对账兜底 + log.error("下单冻结推送OMS镜像库存失败:relateNo={}, kctzType={}", paramDO.getRelateNo(), kctzType, e); + } + } + + private List listActiveRecords(String businessOrderNumber) { + return outOrderFreezeRecordService.list(new LambdaQueryWrapper() + .eq(OutOrderFreezeRecord::getBusinessOrderNumber, businessOrderNumber) + .in(OutOrderFreezeRecord::getFreezeStatus, Arrays.asList( + OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN, + OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE, + OutOrderFreezeRecord.STATUS_HANDOVER)) + .eq(OutOrderFreezeRecord::getDelFlag, 1)); + } + + /** + * 本单该行的活跃冻结记录(status 1/2,承接/校验/取消分配判据共用) + */ + private List listOrderRowActiveRecords(String businessOrderNumber, Long materialInventoryId) { + return outOrderFreezeRecordService.list(new LambdaQueryWrapper() + .eq(OutOrderFreezeRecord::getBusinessOrderNumber, businessOrderNumber) + .eq(OutOrderFreezeRecord::getMaterialInventoryId, materialInventoryId) + .in(OutOrderFreezeRecord::getFreezeStatus, Arrays.asList( + OutOrderFreezeRecord.STATUS_BUSINESS_FROZEN, + OutOrderFreezeRecord.STATUS_EXECUTION_UNDERTAKE)) + .eq(OutOrderFreezeRecord::getDelFlag, 1)); + } + + private String buildRelateNo(String businessOrderNumber, Long uniqueId) { + return uniqueId != null ? businessOrderNumber + "#" + uniqueId : businessOrderNumber; + } + + /** + * Feign调用可能无登录上下文(SecurityUtils抛异常),容错获取并兜底(与 reverseExecutionOrder 同模式) + */ + private LoginUserHolder resolveLoginUser(Long operatorId, String operatorName) { + LoginUserHolder holder = new LoginUserHolder(); + try { + holder.loginUser = SecurityUtils.getLoginUser(); + } catch (Exception e) { + holder.loginUser = new LoginUser(); + holder.loginUser.setUserid(operatorId != null ? operatorId : 0L); + holder.loginUser.setUsername(StringUtils.hasText(operatorName) ? operatorName : "system"); + } + holder.operatorName = StringUtils.hasText(operatorName) ? operatorName + : (holder.loginUser.getUsername() != null ? holder.loginUser.getUsername() : "system"); + return holder; + } + + private static class LoginUserHolder { + private LoginUser loginUser; + private String operatorName; + } + + /** + * 待落库的冻结占位(先全部扣减成功,再统一写冻结记录+推镜像) + */ + private static class PendingFreeze { + private final MaterialInventory row; + private final BigDecimal quantity; + private final Long uniqueId; + private final String relateNo; + + private PendingFreeze(MaterialInventory row, BigDecimal quantity, Long uniqueId, String relateNo) { + this.row = row; + this.quantity = quantity; + this.uniqueId = uniqueId; + this.relateNo = relateNo; + } + } +} diff --git a/mhd_wms/src/main/java/com/mhd/wms/domain/stockOutOrder/service/StockOutOrderDomainService.java b/mhd_wms/src/main/java/com/mhd/wms/domain/stockOutOrder/service/StockOutOrderDomainService.java index 7455d4575..27a1f0690 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/domain/stockOutOrder/service/StockOutOrderDomainService.java +++ b/mhd_wms/src/main/java/com/mhd/wms/domain/stockOutOrder/service/StockOutOrderDomainService.java @@ -123,6 +123,8 @@ public class StockOutOrderDomainService { @Autowired private IStockOutOrderService stockOutOrderService; @Autowired + private com.mhd.wms.domain.outOrderFreezeRecord.service.OutOrderFreezeRecordDomainService outOrderFreezeRecordDomainService; + @Autowired private IOutMaterialDetailService outMaterialDetailService; @Autowired private IDeliveryDetailsLinkService deliveryDetailsLinkService; @@ -2611,6 +2613,13 @@ public class StockOutOrderDomainService { stockOutOrderService.updateById(stockOutOrderDB); return; } + // 下单冻结模式判断:该执行单持有承接/已交接冻结记录时,分配时数量未动(下单时已冻结), + // 取消下发不还原库存数量,仅解除承接(冻结归还业务单);无冻结记录走现有分配占用还原。 + // 两分支互斥:双走会虚增可用 + boolean freezeMode = outOrderFreezeRecordDomainService.hasFreezeForExecutionOrder(outOrderNumber); + if (freezeMode) { + outOrderFreezeRecordDomainService.releaseExecutionUndertake(outOrderNumber); + } for (OutMaterialDetail materialDetail : materialDetails) { //还原库存 BigDecimal allocationQuantity = materialDetail.getAllocationQuantity(); @@ -2619,7 +2628,7 @@ public class StockOutOrderDomainService { //以下两种情况都无可还原的冻结量,跳过还原直接删单,保证此类单据也能被取消/补偿清理: //1.库存不存在(如OMS漏传库存ID的历史脏单); //2.未分配的明细(allocationQuantity为空或0,如下发失败后的补偿清理) - if (materialInventory != null && allocationQuantity != null && allocationQuantity.compareTo(BigDecimal.ZERO) > 0) { + if (!freezeMode && materialInventory != null && allocationQuantity != null && allocationQuantity.compareTo(BigDecimal.ZERO) > 0) { materialInventory.setAllocationQuantity((materialInventory.getAllocationQuantity() == null ? BigDecimal.ZERO : materialInventory.getAllocationQuantity()).add(allocationQuantity)); materialInventory.setFreezeQuantity((materialInventory.getFreezeQuantity() == null ? BigDecimal.ZERO : materialInventory.getFreezeQuantity()).subtract(allocationQuantity)); materialInventoryMapper.updateById(materialInventory); @@ -2702,12 +2711,17 @@ public class StockOutOrderDomainService { // 出库/未出库分支: 未出库单据(WMS状态非9)实物库存未被扣减, 只还原分配占用(可用+冻结-), // 不做实物还原也不推OMS——否则会虚增库存; 已出库(9)分支还原实物并推OMS(防御保留, 业务单反审已拦截已出库单) boolean shipped = stockOutOrderDB.getStatus() != null && stockOutOrderDB.getStatus() == 9; + // 下单冻结模式判断:该执行单持有承接/已交接冻结记录时按下单冻结语义还原 + // (数量从"可用"口径转为"冻结"口径——分配时数量未动、交接扣的是下单冻结量,还原必须回到冻结待出库态) + boolean freezeMode = outOrderFreezeRecordDomainService.hasFreezeForExecutionOrder(outOrderNumber); for (OutMaterialDetail materialDetail : materialDetails) { MaterialInventory materialInventory = materialInventoryMapper.selectById(materialDetail.getMaterialInventoryId()); if (!shipped) { // ===== 未出库: 还原分配占用(与 cancelExecutionOrder 的取消分配同口径) ===== + // 下单冻结模式:分配时数量未动(下单时已冻结),跳过数量还原, + // 冻结归还业务单由循环后统一处理(2→1) BigDecimal allocationQuantity = materialDetail.getAllocationQuantity(); - if (materialInventory != null && allocationQuantity != null && allocationQuantity.compareTo(BigDecimal.ZERO) > 0) { + if (!freezeMode && materialInventory != null && allocationQuantity != null && allocationQuantity.compareTo(BigDecimal.ZERO) > 0) { materialInventory.setAllocationQuantity((materialInventory.getAllocationQuantity() == null ? BigDecimal.ZERO : materialInventory.getAllocationQuantity()).add(allocationQuantity)); materialInventory.setFreezeQuantity((materialInventory.getFreezeQuantity() == null ? BigDecimal.ZERO : materialInventory.getFreezeQuantity()).subtract(allocationQuantity)); materialInventory.setUpdateTime(new Date()); @@ -2761,15 +2775,31 @@ public class StockOutOrderDomainService { //库存调整记录【出库反审】(kctzjl 内部取调整前快照, 与交接扣减先记录后改库存的顺序一致) restoreParam.setRelateNo(outOrderNumber + "#" + materialDetail.getUniqueId()); materialInventoryService.kctzjl(restoreParam, "出库反审", loginUser); - //type=1 上架还原: 实物库存/可用数量加回, 冻结数量不动 - restoreParam.setType("1"); - materialInventoryService.xgkc(restoreParam); - //推送OMS还原预约库存(type=1: OMS侧实物/可用加回, 与交接 omsEdit 扣减互逆) - MaterialInventoryOmsParamDO omsParam = new MaterialInventoryOmsParamDO(); - BeanUtils.copyProperties(restoreParam, omsParam); - omsParam.setKctzType("出库反审"); - omsParam.setLoginUser(loginUser); - omsServiceFeign.omsEdit(omsParam); + if (freezeMode) { + //下单冻结模式:还原为"下单已冻结待出库"——实物+、冻结+(可用不变),与交接扣减(库存-冻结-)互逆 + materialInventoryMapper.omsRestoreInventoryToFrozen(materialInventoryId, + effectiveQuantity, netWeight, grossWeight, volume, area); + //推送OMS镜像(两步合成"实物+冻结+",缺一不可):type=1 实物/可用加回 → type=7 可用转冻结 + MaterialInventoryOmsParamDO omsFrozen = new MaterialInventoryOmsParamDO(); + BeanUtils.copyProperties(restoreParam, omsFrozen); + omsFrozen.setKctzType("出库反审"); + omsFrozen.setLoginUser(loginUser); + omsFrozen.setType("1"); + omsServiceFeign.omsEdit(omsFrozen); + omsFrozen.setType("7"); + omsFrozen.setKctzType("出库反审-还原冻结"); + omsServiceFeign.omsEdit(omsFrozen); + } else { + //type=1 上架还原: 实物库存/可用数量加回, 冻结数量不动 + restoreParam.setType("1"); + materialInventoryService.xgkc(restoreParam); + //推送OMS还原预约库存(type=1: OMS侧实物/可用加回, 与交接 omsEdit 扣减互逆) + MaterialInventoryOmsParamDO omsParam = new MaterialInventoryOmsParamDO(); + BeanUtils.copyProperties(restoreParam, omsParam); + omsParam.setKctzType("出库反审"); + omsParam.setLoginUser(loginUser); + omsServiceFeign.omsEdit(omsParam); + } } } //逻辑删除出库单明细 @@ -2779,6 +2809,15 @@ public class StockOutOrderDomainService { materialDetail.setReverseFlag(1); outMaterialDetailService.updateById(materialDetail); } + // 下单冻结模式:冻结记录归还业务单(未出库承接2→1;已出库已交接3→1重激活), + // 业务单回 status=1 可编辑/再下发/取消,冻结语义闭环 + if (freezeMode) { + if (shipped) { + outOrderFreezeRecordDomainService.reactivateHandoverFreeze(outOrderNumber); + } else { + outOrderFreezeRecordDomainService.releaseExecutionUndertake(outOrderNumber); + } + } //逻辑删除出库单(用按单号查出的DB对象更新: 传入对象无主键, 直接updateById不生效, 与 cancelExecutionOrder 一致) stockOutOrderDB.setDelFlag(2); stockOutOrderDB.setReverseFlag(1); diff --git a/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApi/stockOutOrder/StockOutOrderApi.java b/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApi/stockOutOrder/StockOutOrderApi.java index 424885b33..b2f9f0f51 100644 --- a/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApi/stockOutOrder/StockOutOrderApi.java +++ b/mhd_wms/src/main/java/com/mhd/wms/interfaces/facadeApi/stockOutOrder/StockOutOrderApi.java @@ -6,6 +6,7 @@ import com.mhd.common.core.web.controller.BaseController; import com.mhd.common.core.web.domain.AjaxResult; import com.mhd.common.core.web.page.TableDataInfo; import com.mhd.system.api.domain.DeliveryDetailsLink; +import com.mhd.system.api.domain.OmsFreezeInventoryParam; import com.mhd.wms.application.service.handoverTaskOrder.HandoverTaskOrderApplicationService; import com.mhd.wms.application.service.stockOutOrder.StockOutOrderApplicationService; import com.mhd.wms.domain.copyCodeReference.entity.CopyCodeReference; @@ -25,6 +26,7 @@ import com.mhd.wms.domain.stockOutOrder.repository.facade.IStockOutOrderService; import com.mhd.wms.domain.stockOutOrder.repository.listener.ExcelParseService; import com.mhd.wms.domain.stockOutOrder.repository.po.StockOutOrderPO; import com.mhd.wms.domain.stockOutOrder.repository.todo.StockOutOrderDO; +import com.mhd.wms.domain.outOrderFreezeRecord.service.OutOrderFreezeRecordDomainService; import com.mhd.wms.interfaces.assember.stockOutOrder.StockOutOrderAssembler; import com.mhd.wms.interfaces.dto.outMaterialDetail.OutMaterialDetailDTO; import com.mhd.wms.interfaces.dto.stockOutOrder.StockOutOrderDTO; @@ -58,6 +60,8 @@ import java.util.Map; public class StockOutOrderApi extends BaseController { @Autowired private StockOutOrderApplicationService stockOutOrderApplicationService; + @Autowired + private OutOrderFreezeRecordDomainService outOrderFreezeRecordDomainService; @Resource private StockOutOrderAssembler stockOutOrderAssembler; @@ -561,8 +565,9 @@ public class StockOutOrderApi extends BaseController { */ @ApiOperation("oms下发前库存校验") @PostMapping(value = "/checkOmsAssignInventory") - public AjaxResult checkOmsAssignInventory(@RequestBody List outMaterialDetails) { - stockOutOrderApplicationService.checkOmsAssignInventory(outMaterialDetails); + public AjaxResult checkOmsAssignInventory(@RequestBody List outMaterialDetails, + @RequestParam(value = "businessOrderNumber", required = false) String businessOrderNumber) { + stockOutOrderApplicationService.checkOmsAssignInventory(outMaterialDetails, businessOrderNumber); return AjaxResult.success("库存校验通过"); } @@ -584,6 +589,46 @@ public class StockOutOrderApi extends BaseController { return AjaxResult.success(); } + /** + * oms出库业务单下单冻结库存(防超卖): 按明细冻结WMS库存(可用-、冻结+,库存数量不变), + * 条件更新防并发超卖,任一行可用不足整体失败;同业务单号幂等 + */ + @ApiOperation("oms出库业务单下单冻结库存") + @PostMapping(value = "/omsFreezeInventory") + public AjaxResult omsFreezeInventory(@RequestBody OmsFreezeInventoryParam param) { + return AjaxResult.success(outOrderFreezeRecordDomainService.omsFreezeInventory(param)); + } + + /** + * oms出库业务单取消/删除-释放冻结库存: 可用+、冻结-(库存数量不变);按业务单号幂等; + * 冻结已被执行单承接时拒绝(须先取消下发) + */ + @ApiOperation("oms出库业务单释放冻结库存") + @PostMapping(value = "/omsUnfreezeInventory") + public AjaxResult omsUnfreezeInventory(@RequestBody OmsFreezeInventoryParam param) { + return AjaxResult.success(outOrderFreezeRecordDomainService.omsUnfreezeInventory( + param.getOutOrderNumber(), param.getOperatorId(), param.getOperatorName())); + } + + /** + * oms出库业务单编辑原子重锁: 同一事务先释放原冻结再按新明细重新冻结, + * 任一新明细可用不足整体回滚(旧冻结同样还原) + */ + @ApiOperation("oms出库业务单编辑重锁库存") + @PostMapping(value = "/omsRelockInventory") + public AjaxResult omsRelockInventory(@RequestBody OmsFreezeInventoryParam param) { + return AjaxResult.success(outOrderFreezeRecordDomainService.omsRelockInventory(param)); + } + + /** + * 仓库是否存在活跃冻结记录(仓库"库存锁定模式"切换守卫:在途冻结单存在时禁止切换) + */ + @ApiOperation("仓库是否存在活跃冻结记录") + @GetMapping(value = "/hasActiveFreezeByWarehouse/{warehouseId}") + public AjaxResult hasActiveFreezeByWarehouse(@PathVariable("warehouseId") Long warehouseId) { + return AjaxResult.success(outOrderFreezeRecordDomainService.hasActiveFreezeByWarehouse(warehouseId)); + } + /** * oms下发单配送明细保存 */ diff --git a/mhd_wms/src/main/resources/mapper/materialInventory/MaterialInventoryMapper.xml b/mhd_wms/src/main/resources/mapper/materialInventory/MaterialInventoryMapper.xml index 8004d7413..68d752e32 100644 --- a/mhd_wms/src/main/resources/mapper/materialInventory/MaterialInventoryMapper.xml +++ b/mhd_wms/src/main/resources/mapper/materialInventory/MaterialInventoryMapper.xml @@ -1894,4 +1894,43 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 1 = 1 AND MATERIAL_CLASSIFY_ID = #{id} AND DEL_FLAG = 1 + + + + UPDATE material_inventory + SET allocation_quantity = allocation_quantity - #{quantity}, + freeze_quantity = freeze_quantity + #{quantity}, + update_time = SYSDATE + WHERE material_inventory_id = #{materialInventoryId} + AND del_flag = 1 + AND allocation_quantity >= #{quantity} + + + + + UPDATE material_inventory + SET allocation_quantity = allocation_quantity + #{quantity}, + freeze_quantity = freeze_quantity - #{quantity}, + update_time = SYSDATE + WHERE material_inventory_id = #{materialInventoryId} + AND del_flag = 1 + AND freeze_quantity >= #{quantity} + + + + + UPDATE material_inventory + SET inventory_quantity = inventory_quantity + #{quantity}, + freeze_quantity = freeze_quantity + #{quantity}, + net_weight = net_weight + #{netWeight}, + gross_weight = gross_weight + #{grossWeight}, + volume = volume + #{volume}, + area = area + #{area}, + update_time = SYSDATE + WHERE material_inventory_id = #{materialInventoryId} + AND del_flag = 1 + \ No newline at end of file diff --git a/sql/20260912_alter_table_dm_warehouse_stock_lock_mode.sql b/sql/20260912_alter_table_dm_warehouse_stock_lock_mode.sql new file mode 100644 index 000000000..ac71f59a2 --- /dev/null +++ b/sql/20260912_alter_table_dm_warehouse_stock_lock_mode.sql @@ -0,0 +1,33 @@ +-- ============================================================ +-- 功能:仓库表新增"库存锁定模式"字段(OMS下单冻结库存/防超卖功能开关) +-- 数据库:达梦(DM8),兼容 Oracle 语法,表名/字段名统一大写 +-- 模式:NGWL_TEST_SYSTEM(mhd-system 服务对应业务库;生产环境按实际模式名替换) +-- 表名:WAREHOUSE +-- 日期:2026-09-12 +-- 说明: +-- 1-下单冻结:OMS出库业务单「下单」即冻结WMS库存(可用-、冻结+,库存数量不变) +-- 2-分配库存冻结:现有模式,WMS「分配库存」时才冻结(默认值,存量仓库行为零变化) +-- 不变式:库存数量 = 可用数量 + 冻结数量(出库交接前全程成立) +-- NULL 视为 2(代码侧 isOrderFreezeWarehouse 仅 =1 才走下单冻结,兜底降级安全) +-- ============================================================ + +-- 0. 执行前检查(确认列是否已存在,已存在则跳过本脚本) +-- SELECT OWNER, TABLE_NAME, COLUMN_NAME, DATA_TYPE +-- FROM ALL_TAB_COLUMNS +-- WHERE OWNER = 'NGWL_TEST_SYSTEM' +-- AND TABLE_NAME = 'WAREHOUSE' +-- AND COLUMN_NAME = 'STOCK_LOCK_MODE'; + +-- 1. 添加字段(达梦 ADD COLUMN 不支持 AFTER,列追加在表尾;带 DEFAULT 时存量行自动填默认值) +ALTER TABLE NGWL_TEST_SYSTEM.WAREHOUSE ADD STOCK_LOCK_MODE TINYINT DEFAULT 2; + +-- 2. 添加注释(达梦必须单独写) +COMMENT ON COLUMN NGWL_TEST_SYSTEM.WAREHOUSE.STOCK_LOCK_MODE IS '库存锁定模式: 1-下单冻结 2-分配库存冻结'; + +-- 3. 验证 +-- SELECT WAREHOUSE_ID, WAREHOUSE_CODE, STOCK_LOCK_MODE FROM NGWL_TEST_SYSTEM.WAREHOUSE WHERE ROWNUM <= 10; + +-- ---------------------------- +-- 回滚脚本(按需,须确认无 status∈(1,2) 的在途冻结单后再执行) +-- ALTER TABLE NGWL_TEST_SYSTEM.WAREHOUSE DROP COLUMN STOCK_LOCK_MODE; +-- ---------------------------- diff --git a/sql/20260912_create_table_dm_out_order_freeze_record.sql b/sql/20260912_create_table_dm_out_order_freeze_record.sql new file mode 100644 index 000000000..672cb6801 --- /dev/null +++ b/sql/20260912_create_table_dm_out_order_freeze_record.sql @@ -0,0 +1,77 @@ +-- ============================================================ +-- 功能:WMS出库单库存冻结记录表(OMS下单冻结模式/防超卖) +-- 数据库:达梦(DM8),兼容 Oracle 语法,表名/字段名统一大写 +-- 模式:NGWL_TEST_WMS(mhd-wms 服务对应业务库,与 MATERIAL_INVENTORY 同库; +-- 生产环境按实际模式名替换) +-- 表名:OUT_ORDER_FREEZE_RECORD +-- 日期:2026-09-12 +-- 说明: +-- 记录「业务单↔库存行↔冻结数量」占用关系与状态流转,是编辑重锁/取消解冻/ +-- 分配承接/交接标记/反审还原的对账依据与单据级隔离判据。 +-- 状态流转:1-业务单冻结(下单) → 2-执行单承接(WMS分配,数量不动) +-- → 3-已交接(出库交接扣减库存后终态);任一环节取消 → 4-已释放; +-- 反审(已出库) 3→1 重新激活。 +-- ============================================================ + +-- 0. 执行前检查(确认表是否已存在,已存在则跳过本脚本) +-- SELECT OWNER, TABLE_NAME FROM ALL_TABLES +-- WHERE OWNER = 'NGWL_TEST_WMS' AND TABLE_NAME = 'OUT_ORDER_FREEZE_RECORD'; + +-- 1. 建表 +CREATE TABLE OUT_ORDER_FREEZE_RECORD ( + FREEZE_RECORD_ID BIGINT IDENTITY(1,1) NOT NULL, + BUSINESS_ORDER_NUMBER VARCHAR(50) DEFAULT NULL, + EXECUTION_ORDER_NUMBER VARCHAR(50) DEFAULT NULL, + MATERIAL_DETAIL_UNIQUE_ID VARCHAR(64) DEFAULT NULL, + MATERIAL_INVENTORY_ID BIGINT NOT NULL, + WAREHOUSE_ID BIGINT DEFAULT NULL, + MATERIAL_BASE_INFO_ID BIGINT DEFAULT NULL, + FREEZE_QUANTITY DECIMAL(24,6) NOT NULL, + FREEZE_STATUS INT DEFAULT 1 NOT NULL, + RELATE_NO VARCHAR(100) DEFAULT NULL, + REMARK VARCHAR(500) DEFAULT NULL, + CREATE_BY BIGINT DEFAULT NULL, + CREATE_BY_NAME VARCHAR(50) DEFAULT NULL, + CREATE_TIME DATETIME DEFAULT NULL, + UPDATE_BY BIGINT DEFAULT NULL, + UPDATE_BY_NAME VARCHAR(50) DEFAULT NULL, + UPDATE_TIME DATETIME DEFAULT NULL, + DEL_FLAG INT DEFAULT 1, + CONSTRAINT PK_OUT_ORDER_FREEZE_RECORD PRIMARY KEY (FREEZE_RECORD_ID) +); + +-- 2. 表注释 +COMMENT ON TABLE OUT_ORDER_FREEZE_RECORD IS 'WMS出库单库存冻结记录表(OMS下单冻结模式)'; + +-- 3. 列注释(达梦必须单独写) +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.FREEZE_RECORD_ID IS '编号'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.BUSINESS_ORDER_NUMBER IS 'OMS出库业务单号'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.EXECUTION_ORDER_NUMBER IS '执行出库单号(分配承接时回填,可空)'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.MATERIAL_DETAIL_UNIQUE_ID IS '明细唯一标识(OMS明细uniqueId)'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.MATERIAL_INVENTORY_ID IS '库存行ID(MATERIAL_INVENTORY.MATERIAL_INVENTORY_ID)'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.WAREHOUSE_ID IS '仓库ID'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.MATERIAL_BASE_INFO_ID IS '物料基础信息ID'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.FREEZE_QUANTITY IS '冻结数量'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.FREEZE_STATUS IS '冻结状态: 1-业务单冻结 2-执行单承接 3-已交接 4-已释放'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.RELATE_NO IS '关联单号(业务单号#明细uniqueId,与库存调整记录RELATE_NO对应)'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.REMARK IS '备注'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.CREATE_BY IS '创建者'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.CREATE_BY_NAME IS '创建者姓名'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.CREATE_TIME IS '创建时间'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.UPDATE_BY IS '更新者'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.UPDATE_BY_NAME IS '更新人姓名'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.UPDATE_TIME IS '更新时间'; +COMMENT ON COLUMN OUT_ORDER_FREEZE_RECORD.DEL_FLAG IS '删除标记:0-无状态,1-正常,2-已删除'; + +-- 4. 索引(最热路径:按业务单号取记录[幂等/解冻/重锁/承接校验];按执行单号[解除承接/交接标记/重激活];按库存行[分配/取消分配判据]) +CREATE INDEX IDX_OOFR_BIZ_ORDER ON OUT_ORDER_FREEZE_RECORD (BUSINESS_ORDER_NUMBER, DEL_FLAG); +CREATE INDEX IDX_OOFR_EXEC_ORDER ON OUT_ORDER_FREEZE_RECORD (EXECUTION_ORDER_NUMBER); +CREATE INDEX IDX_OOFR_INV_ID ON OUT_ORDER_FREEZE_RECORD (MATERIAL_INVENTORY_ID); + +-- 5. 验证 +-- SELECT FREEZE_STATUS, COUNT(*) FROM OUT_ORDER_FREEZE_RECORD GROUP BY FREEZE_STATUS; + +-- ---------------------------- +-- 回滚脚本(按需,须确认无在途冻结单[status∈(1,2)]后再执行) +-- DROP TABLE OUT_ORDER_FREEZE_RECORD; +-- ---------------------------- diff --git a/sql/20260912_fix_dm_out_order_freeze_record_rename_id.sql b/sql/20260912_fix_dm_out_order_freeze_record_rename_id.sql new file mode 100644 index 000000000..f809cee71 --- /dev/null +++ b/sql/20260912_fix_dm_out_order_freeze_record_rename_id.sql @@ -0,0 +1,24 @@ +-- ============================================================ +-- 功能:修复冻结记录表主键列名(实体字段 freezeRecordId → 列 FREEZE_RECORD_ID, +-- 建表脚本误写为 ID,导致 MyBatis-Plus 查询报"无效的列名[FREEZE_RECORD_ID]") +-- 数据库:达梦(DM8) +-- 模式:NGWL_TEST_WMS(mhd-wms 服务数据源;生产按实际模式名替换) +-- 表名:OUT_ORDER_FREEZE_RECORD +-- 日期:2026-09-12 +-- ============================================================ + +-- 0. 执行前检查(确认当前列名) +-- SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS +-- WHERE OWNER = 'NGWL_TEST_WMS' AND TABLE_NAME = 'OUT_ORDER_FREEZE_RECORD' +-- ORDER BY COLUMN_ID; + +-- 1. 重命名主键列 ID → FREEZE_RECORD_ID(自增属性/主键约束随列保留) +ALTER TABLE NGWL_TEST_WMS.OUT_ORDER_FREEZE_RECORD RENAME COLUMN ID TO FREEZE_RECORD_ID; + +-- 2. 验证(应能看到 FREEZE_RECORD_ID 且不再有 ID 列) +-- SELECT COLUMN_NAME FROM ALL_TAB_COLUMNS +-- WHERE OWNER = 'NGWL_TEST_WMS' AND TABLE_NAME = 'OUT_ORDER_FREEZE_RECORD' +-- ORDER BY COLUMN_ID; +-- SELECT FREEZE_RECORD_ID, BUSINESS_ORDER_NUMBER, FREEZE_STATUS FROM NGWL_TEST_WMS.OUT_ORDER_FREEZE_RECORD; + +-- 回滚: ALTER TABLE NGWL_TEST_WMS.OUT_ORDER_FREEZE_RECORD RENAME COLUMN FREEZE_RECORD_ID TO ID;