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 04540b78f..121a73be9 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,8 +21,7 @@ import java.util.Map;
* @description: TODO
* @date 2024/5/10 8:58
**/
-// 目标地址由 nacos 配置项 feign.url.mhd-wms-service 指定,未配置时按服务名走服务发现
-@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE,url = "http://10.102.192.32:8016",fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
+@FeignClient(contextId = "remoteWmsServiceFeign",value = ServiceNameConstants.WMS_SERVICE,url = "http://10.33.0.109:8016",fallbackFactory = RemoteWmsFallbackFactory.class, configuration = FeignAutoConfiguration.class)
public interface WmsServiceFeign {
/**
@@ -113,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/application/service/outEntrustOrder/OutEntrustOrderApplicationService.java b/mhd_oms/src/main/java/com/mhd/oms/application/service/outEntrustOrder/OutEntrustOrderApplicationService.java
index bcdfacb14..0b6ed4003 100644
--- a/mhd_oms/src/main/java/com/mhd/oms/application/service/outEntrustOrder/OutEntrustOrderApplicationService.java
+++ b/mhd_oms/src/main/java/com/mhd/oms/application/service/outEntrustOrder/OutEntrustOrderApplicationService.java
@@ -11,6 +11,7 @@ import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
import com.mhd.oms.domain.outEntrustOrder.service.OutEntrustOrderDomainService;
+import com.mhd.oms.domain.outEntrustOrder.repository.listener.OutEntrustExcelParseService;
import com.mhd.system.api.SystemServiceFeign;
import com.mhd.system.api.WmsServiceFeign;
import com.mhd.system.api.domain.NoticeMaterialDetailDTO;
@@ -20,8 +21,10 @@ import com.mhd.system.api.model.LoginUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
+import java.io.IOException;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Date;
@@ -52,6 +55,9 @@ public class OutEntrustOrderApplicationService {
@Resource
private WmsServiceFeign wmsServiceFeign;
+ @Resource
+ private OutEntrustExcelParseService outEntrustExcelParseService;
+
/**
* 分页查询出仓委托单列表(不做默认状态过滤,显示全部数据)
*/
@@ -108,6 +114,45 @@ public class OutEntrustOrderApplicationService {
return outEntrustOrderDomainService.insert(outEntrustOrderDO);
}
+ /**
+ * 导入出仓委托单(模板与WMS出库管理导入一致:前11行表头+第12行起明细)
+ * 解析校验通过后复用新增链路落库:生成CKWT委托单号,状态待审核/未下发
+ *
+ * @return 校验错误信息;空串表示导入成功
+ */
+ public StringBuilder importOutEntrustOrder(MultipartFile file, Long warehouseId, String warehouseCode, String warehouseName) throws IOException {
+ //解析+校验(客户解析/物料匹配/字典转换/必填项)
+ OutEntrustExcelParseService.ParseResult parseResult =
+ outEntrustExcelParseService.parseOutEntrustOrderExcel(file, warehouseName, warehouseCode, warehouseId);
+ if (parseResult.getErrorMsg().length() > 0) {
+ return parseResult.getErrorMsg();
+ }
+ OutEntrustOrderDO outEntrustOrderDO = parseResult.getOrder();
+ //审计信息(与WMS出库导入一致,导入人=当前登录人)
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (loginUser != null) {
+ String userRealName;
+ if (loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null
+ && !loginUser.getUserPo().getUserName().trim().isEmpty()) {
+ userRealName = loginUser.getUserPo().getUserName();
+ } else if (loginUser.getRealname() != null && !loginUser.getRealname().trim().isEmpty()) {
+ userRealName = loginUser.getRealname();
+ } else {
+ userRealName = loginUser.getUsername();
+ }
+ Date now = new Date();
+ outEntrustOrderDO.setCreateBy(loginUser.getUserid());
+ outEntrustOrderDO.setCreateByName(userRealName);
+ outEntrustOrderDO.setCreateTime(now);
+ outEntrustOrderDO.setUpdateBy(loginUser.getUserid());
+ outEntrustOrderDO.setUpdateByName(userRealName);
+ outEntrustOrderDO.setUpdateTime(now);
+ }
+ //复用新增链路:必填校验→默认普通出库→仓库校验→组织信息→汇总申报数量→生成CKWT委托单号(待审核/未下发)
+ insert(outEntrustOrderDO);
+ return new StringBuilder();
+ }
+
/**
* 修改出仓委托单(仅待审核/已驳回且未下发可修改;已驳回单编辑后状态重置为待审核)
*/
diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/facade/IExecutionStockOutOrderService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/facade/IExecutionStockOutOrderService.java
index f6b99a4ba..e4b32f470 100644
--- a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/facade/IExecutionStockOutOrderService.java
+++ b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/facade/IExecutionStockOutOrderService.java
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.mhd.oms.domain.executionOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
+import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import java.util.List;
@@ -21,6 +22,11 @@ public interface IExecutionStockOutOrderService extends IService queryList(ExecutionStockOutOrderDO stockOutOrderDO);
+ /**
+ * 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
+ */
+ public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO);
+
/**
* 新增出库单
*/
diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/mapper/ExecutionStockOutOrderMapper.java b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/mapper/ExecutionStockOutOrderMapper.java
index 173c8151a..e56e380b5 100644
--- a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/mapper/ExecutionStockOutOrderMapper.java
+++ b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/mapper/ExecutionStockOutOrderMapper.java
@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mhd.oms.domain.executionOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
+import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import org.apache.ibatis.annotations.Param;
//import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO;
@@ -23,6 +24,11 @@ public interface ExecutionStockOutOrderMapper extends BaseMapper queryList(ExecutionStockOutOrderDO stockOutOrderDO);
+ /**
+ * 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
+ */
+ public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO);
+
/**
* 出库单提货单打印-表头
*/
diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/persistence/ExecutionStockOutOrderImpl.java b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/persistence/ExecutionStockOutOrderImpl.java
index 34e567487..96fa29231 100644
--- a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/persistence/ExecutionStockOutOrderImpl.java
+++ b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/repository/persistence/ExecutionStockOutOrderImpl.java
@@ -6,6 +6,7 @@ import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
import com.mhd.oms.domain.executionStockOutOrder.repository.mapper.ExecutionStockOutOrderMapper;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
+import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
@@ -34,6 +35,15 @@ public class ExecutionStockOutOrderImpl extends ServiceImpl当前筛选条件下不分页的全量合计,随分页列表一起返回
+ * (totalPlanQuantity/totalOutboundQuantity),计划数量与出库数量分开计算
+ *
+ * @author rcx
+ * @date 2026-09-14
+ */
+@Data
+@ApiModel(value = "出库执行单列表合计对象", description = "出库执行单列表合计响应对象")
+public class ExecutionStockOutOrderTotalPO implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ @ApiModelProperty("计划数量总数(一级明细计划数量之和)")
+ private BigDecimal totalPlanQuantity;
+
+ @ApiModelProperty("出库数量总数(一级明细出库数量之和)")
+ private BigDecimal totalOutboundQuantity;
+}
diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/service/ExecutionStockOutOrderApplicationService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/service/ExecutionStockOutOrderApplicationService.java
index 320fa25fa..5b1971c29 100644
--- a/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/service/ExecutionStockOutOrderApplicationService.java
+++ b/mhd_oms/src/main/java/com/mhd/oms/domain/executionStockOutOrder/service/ExecutionStockOutOrderApplicationService.java
@@ -26,6 +26,7 @@ import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
import com.mhd.oms.domain.executionStockOutOrder.repository.mapper.ExecutionStockOutOrderMapper;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
+import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDTO;
import com.mhd.oms.domain.gwLog.entity.GwLog;
@@ -225,6 +226,15 @@ public class ExecutionStockOutOrderApplicationService {
return stockOutOrderPOS;
}
+ /**
+ * 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量。
+ * 须在 queryList/queryList1 之后用同一个DO调用:数据权限条件由列表查询原地写入DO,
+ * 合计查询才能继承生效后的筛选;且 PageHelper startPage 只拦截下一条SQL,本查询不会被分页。
+ */
+ public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO) {
+ return stockOutOrderDomainService.queryListTotal(stockOutOrderDO);
+ }
+
/**
* 新增出库单
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..1dbeed71b 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
@@ -30,6 +30,7 @@ import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder;
import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService;
import com.mhd.oms.domain.executionStockOutOrder.repository.mapper.ExecutionStockOutOrderMapper;
import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderPO;
+import com.mhd.oms.domain.executionStockOutOrder.repository.po.ExecutionStockOutOrderTotalPO;
import com.mhd.oms.domain.executionStockOutOrder.repository.todo.ExecutionStockOutOrderDO;
import com.mhd.oms.domain.materialBaseInfo.service.MaterialBaseInfoResolveService;
import com.mhd.oms.domain.reservationDeliveryDetailsLink.entity.ReservationDeliveryDetailsLink;
@@ -114,6 +115,13 @@ public class ExecutionStockOutOrderDomainService {
return stockOutOrderService.queryList(stockOutOrderDO);
}
+ /**
+ * 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量
+ */
+ public ExecutionStockOutOrderTotalPO queryListTotal(ExecutionStockOutOrderDO stockOutOrderDO) {
+ return stockOutOrderService.queryListTotal(stockOutOrderDO);
+ }
+
/**
* 从单号列表中提取最大序号
* 单号格式: prefix + 数字序号(如 RK260807001 中 prefix=RK260807, 序号=001)
@@ -440,8 +448,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 +514,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 +536,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/outEntrustOrder/repository/listener/NoConvertConverter.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/NoConvertConverter.java
new file mode 100644
index 000000000..07d44ae51
--- /dev/null
+++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/NoConvertConverter.java
@@ -0,0 +1,58 @@
+package com.mhd.oms.domain.outEntrustOrder.repository.listener;
+
+import com.alibaba.excel.converters.Converter;
+import com.alibaba.excel.enums.CellDataTypeEnum;
+import com.alibaba.excel.metadata.GlobalConfiguration;
+import com.alibaba.excel.metadata.data.ReadCellData;
+import com.alibaba.excel.metadata.property.ExcelContentProperty;
+
+/**
+ * 自定义转换器,不进行任何类型转换,直接返回原始值
+ * 覆盖EasyExcel默认的StringNumberConverter,避免数字格式化错误
+ *
+ * @author rcx
+ * @date 2026-09-14
+ */
+public class NoConvertConverter implements Converter {
+
+ @Override
+ public Class> supportJavaTypeKey() {
+ return String.class;
+ }
+
+ @Override
+ public CellDataTypeEnum supportExcelTypeKey() {
+ // 关键:返回NUMBER类型,覆盖StringNumberConverter
+ return CellDataTypeEnum.NUMBER;
+ }
+
+ @Override
+ public String convertToJavaData(ReadCellData> cellData, ExcelContentProperty contentProperty,
+ GlobalConfiguration globalConfiguration) {
+ // 不进行任何格式化,直接返回原始值的字符串表示
+ if (cellData == null) {
+ return "";
+ }
+
+ // 根据单元格类型获取原始值
+ try {
+ CellDataTypeEnum type = cellData.getType();
+ if (type == CellDataTypeEnum.STRING) {
+ return cellData.getStringValue() != null ? cellData.getStringValue() : "";
+ } else if (type == CellDataTypeEnum.NUMBER) {
+ // 直接转换数字为字符串,不进行格式化
+ return cellData.getNumberValue() != null ? cellData.getNumberValue().toString() : "";
+ } else if (type == CellDataTypeEnum.BOOLEAN) {
+ return cellData.getBooleanValue() != null ? cellData.getBooleanValue().toString() : "";
+ } else if (type == CellDataTypeEnum.EMPTY) {
+ return "";
+ } else {
+ // 其他类型尝试获取字符串值
+ return cellData.getStringValue() != null ? cellData.getStringValue() : "";
+ }
+ } catch (Exception e) {
+ // 如果获取失败,返回空字符串
+ return "";
+ }
+ }
+}
diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustExcelParseService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustExcelParseService.java
new file mode 100644
index 000000000..38cfa81ad
--- /dev/null
+++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustExcelParseService.java
@@ -0,0 +1,481 @@
+package com.mhd.oms.domain.outEntrustOrder.repository.listener;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.alibaba.excel.EasyExcel;
+import com.alibaba.excel.ExcelReader;
+import com.alibaba.excel.read.metadata.ReadSheet;
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONObject;
+import com.alibaba.fastjson2.TypeReference;
+import com.mhd.common.core.domain.po.SysDictDataVo;
+import com.mhd.common.core.domain.po.UserPo;
+import com.mhd.common.core.exception.ServiceException;
+import com.mhd.common.core.utils.StringUtils;
+import com.mhd.common.core.web.domain.AjaxResult;
+import com.mhd.common.security.utils.SecurityUtils;
+import com.mhd.oms.domain.outEntrustOrder.repository.mapper.OutEntrustOrderMapper;
+import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO;
+import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO;
+import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO;
+import com.mhd.system.api.SystemServiceFeign;
+import com.mhd.system.api.UserServiceFeign;
+import com.mhd.system.api.model.LoginUser;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 出仓委托单Excel解析服务
+ * 模板与WMS出库管理导入一致:前11行为表头,第12行起为明细
+ *
+ * @author rcx
+ * @date 2026-09-14
+ */
+@Slf4j
+@Service
+public class OutEntrustExcelParseService {
+
+ @Resource
+ private SystemServiceFeign systemServiceFeign;
+
+ @Resource
+ private UserServiceFeign userServiceFeign;
+
+ @Autowired
+ private OutEntrustOrderMapper outEntrustOrderMapper;
+
+ /**
+ * 解析结果:委托单对象 + 校验错误信息(空串表示通过)
+ */
+ @Getter
+ public static class ParseResult {
+ private final OutEntrustOrderDO order;
+ private final StringBuilder errorMsg;
+
+ public ParseResult(OutEntrustOrderDO order, StringBuilder errorMsg) {
+ this.order = order;
+ this.errorMsg = errorMsg;
+ }
+ }
+
+ /**
+ * 解析出仓委托单Excel(不落库,落库由应用服务走新增链路)
+ *
+ * @param file 上传的Excel文件
+ * @param warehouseName 仓库名称(导入弹窗选择)
+ * @param warehouseCode 仓库编码
+ * @param warehouseId 仓库ID
+ * @return 解析结果:委托单对象 + 校验错误信息;错误信息非空时不应落库
+ */
+ public ParseResult parseOutEntrustOrderExcel(MultipartFile file, String warehouseName, String warehouseCode, Long warehouseId) throws IOException {
+
+ OutEntrustHeaderListener headerListener = new OutEntrustHeaderListener();
+
+ ReadSheet headerSheet = EasyExcel.readSheet(0)
+ .headRowNumber(0)
+ .build();
+
+ ExcelReader headerReader = EasyExcel.read(file.getInputStream())
+ .autoCloseStream(false)
+ .registerReadListener(headerListener)
+ .registerConverter(new NoConvertConverter())
+ .headRowNumber(0)
+ .build();
+
+ // 通过监听器控制只读取前11行
+ headerReader.read(headerSheet);
+
+ InputStream itemInputStream = file.getInputStream();
+ OutEntrustItemListener itemListener = new OutEntrustItemListener();
+
+ // 商品明细需要跳过前11行,从第12行开始读取
+ ExcelReader itemReader = EasyExcel.read(itemInputStream)
+ .autoCloseStream(false)
+ .registerReadListener(itemListener)
+ .registerConverter(new NoConvertConverter())
+ .headRowNumber(12)
+ .build();
+
+ ReadSheet itemSheet = EasyExcel.readSheet(0).build();
+ itemReader.read(itemSheet);
+
+ itemReader.finish();
+ List itemList = itemListener.getItemList();
+ OutEntrustOrderDO outEntrustOrder = headerListener.getOutEntrustOrder();
+
+ // 提前获取货主ID,用于查询物料基础信息时过滤
+ String customerName = outEntrustOrder.getShipperName();
+ UserPo shipperUser = resolveCustomerShipperUserPoForImport(customerName);
+ Long shipperId = shipperUser != null ? shipperUser.getUserId() : null;
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ Long topOrganizationId = loginUser != null && loginUser.getUserPo() != null
+ ? loginUser.getUserPo().getTopOrganizationId() : null;
+ initList(itemList, shipperId, topOrganizationId);
+ outEntrustOrder.setMaterialDetailList(itemList);
+ outEntrustOrder.setWarehouseName(warehouseName);
+ outEntrustOrder.setWarehouseCode(warehouseCode);
+ outEntrustOrder.setWarehouseId(warehouseId);
+
+ //业务员默认当前登录人
+ if (loginUser != null) {
+ outEntrustOrder.setSalesmanId(String.valueOf(loginUser.getUserid()));
+ outEntrustOrder.setSalesmanName(loginUser.getUserPo() != null ? loginUser.getUserPo().getUserName() : loginUser.getUsername());
+ }
+ //货主信息回填(名称/编号以用户中心为准)
+ if (shipperUser != null) {
+ outEntrustOrder.setShipperId(shipperUser.getUserId());
+ outEntrustOrder.setShipperCode(shipperUser.getUserMemberCode());
+ outEntrustOrder.setShipperName(shipperUser.getUserName());
+ }
+ initDict(outEntrustOrder);
+ //校验必填字段
+ StringBuilder errorMsg = check(outEntrustOrder);
+ // 日期单元格有值但解析失败:与物料一致性错误同等对待,阻断导入并明确提示,杜绝静默丢日期
+ for (String dateError : headerListener.getDateParseErrors()) {
+ errorMsg.append(dateError).append("; ");
+ }
+ return new ParseResult(outEntrustOrder, errorMsg);
+ }
+
+ /**
+ * 校验导入明细是否已在当前货主下维护物料基础信息
+ */
+ private StringBuilder validateShipperMaterialDetails(OutEntrustOrderDO outEntrustOrder) {
+ StringBuilder sb = new StringBuilder();
+ List details = outEntrustOrder.getMaterialDetailList();
+ if (details == null || details.isEmpty()) {
+ return sb;
+ }
+ Long shipperId = outEntrustOrder.getShipperId();
+ for (OutEntrustOrderDetailDO row : details) {
+ if (row.getMaterialBaseInfoId() != null) {
+ continue;
+ }
+ boolean hasIdentifier = StringUtils.isNotBlank(row.getMaterialNo())
+ || StringUtils.isNotBlank(row.getSkucode())
+ || StringUtils.isNotBlank(row.getCommodityName());
+ if (!hasIdentifier) {
+ continue;
+ }
+ String desc = buildImportMaterialRowDesc(row);
+ if (ObjectUtil.isEmpty(shipperId)) {
+ sb.append("请先确认客户(货主)信息;无法校验以下物料是否已在货主下维护:").append(desc).append("; ");
+ } else {
+ sb.append("当前货主下未维护以下物料:").append(desc).append("; ");
+ }
+ }
+ return sb;
+ }
+
+ /**
+ * 提示文案只强调 Excel 导入列「商品名称」;商品名称为空时用导入料号/SKU 辅助说明。
+ */
+ private String buildImportMaterialRowDesc(OutEntrustOrderDetailDO row) {
+ String commodityName = StringUtils.trimToEmpty(row.getCommodityName());
+ if (StringUtils.isNotBlank(commodityName)) {
+ return "商品名称「" + commodityName + "」";
+ }
+ String materialNo = StringUtils.trimToEmpty(row.getMaterialNo());
+ String sku = StringUtils.trimToEmpty(row.getSkucode());
+ StringBuilder b = new StringBuilder("导入商品名称为空");
+ if (StringUtils.isNotBlank(materialNo)) {
+ b.append(",料号「").append(materialNo).append("」");
+ }
+ if (StringUtils.isNotBlank(sku)) {
+ b.append(",SKU「").append(sku).append("」");
+ }
+ return b.toString();
+ }
+
+ private StringBuilder check(OutEntrustOrderDO outEntrustOrder) {
+ StringBuilder errorMsg = new StringBuilder();
+ errorMsg.append(validateShipperMaterialDetails(outEntrustOrder));
+ if (outEntrustOrder.getMaterialDetailList() == null || outEntrustOrder.getMaterialDetailList().isEmpty()) {
+ errorMsg.append("物料明细列表不能为空或匹配不上物料信息; ");
+ }
+ if (StringUtils.isEmpty(outEntrustOrder.getToReceiver())) errorMsg.append("To不能为空; ");
+ if (StringUtils.isEmpty(outEntrustOrder.getCarrier())) errorMsg.append("承运商不能为空; ");
+ if (StringUtils.isEmpty(outEntrustOrder.getTransportModeCode())) errorMsg.append("运输方式不能为空; ");
+ if (StringUtils.isEmpty(outEntrustOrder.getSupervisionModeCode())) errorMsg.append("监督方式不能为空; ");
+ if (StringUtils.isEmpty(outEntrustOrder.getDepartureArrivalCountry())) errorMsg.append("出发/到达国家不能为空; ");
+ if (StringUtils.isEmpty(outEntrustOrder.getContactPerson())) errorMsg.append("联系人不能为空; ");
+ if (ObjectUtil.isEmpty(outEntrustOrder.getShipperId())) errorMsg.append("客户名称不能为空或匹配不上客户信息; ");
+ if (StringUtils.isEmpty(outEntrustOrder.getDeclareCustoms())) errorMsg.append("申报地关区不能为空; ");
+ if (StringUtils.isEmpty(outEntrustOrder.getEntryExitCustoms())) errorMsg.append("进出境关别不能为空; ");
+ return errorMsg;
+ }
+
+ /**
+ * @description 初始化物料明细列表,查询物料基础信息并设置物料基础信息ID(与WMS出库管理导入逻辑一致:料号 > SKU码 > 商品名称)
+ * @param itemList 出仓委托单明细列表
+ * @param shipperId 货主ID(用于过滤物料基础信息,确保查询到的是该货主绑定的物料)
+ * @param topOrganizationId 一级组织ID(用于过滤物料基础信息)
+ */
+ private void initList(List itemList, Long shipperId, Long topOrganizationId) {
+ for (OutEntrustOrderDetailDO detail : itemList) {
+ // 未识别货主时不查物料,避免匹配到其他货主的物料基础信息
+ if (shipperId == null) {
+ continue;
+ }
+
+ // 根据明细表中的字段查询物料基础信息(优先:料号 > 商品SKU码 > 商品名称)
+ MaterialBaseInfoPO materialBaseInfoPO = null;
+
+ // 优先使用料号查询(materialNo对应materialCode)
+ String materialNo = detail.getMaterialNo();
+ if (StringUtils.isNotBlank(materialNo)) {
+ List materialList = outEntrustOrderMapper.getMaterialByMaterialCode(materialNo, shipperId, topOrganizationId);
+ if (materialList != null && !materialList.isEmpty()) {
+ materialBaseInfoPO = materialList.get(0);
+ // ==== 料号与规格型号/SKU码一致性校验:任何一项不符直接报对应错误 ====
+ // 双方(trim后)必须完全一致:Excel留空而档案有值、或Excel填了值而档案为空,均视为不一致报错
+ String excelSpec = StringUtils.trimToEmpty(detail.getSpecificationModel());
+ String dbSpec = StringUtils.trimToEmpty(materialBaseInfoPO.getSpecificationModel());
+ if (!excelSpec.equals(dbSpec)) {
+ throw new ServiceException(String.format("导入失败:料号「%s」规格型号与物料档案不一致(Excel:%s / 档案:%s)",
+ materialNo, excelSpec.isEmpty() ? "空" : excelSpec, dbSpec.isEmpty() ? "空" : dbSpec));
+ }
+ String excelSku = StringUtils.trimToEmpty(detail.getSkucode());
+ String dbSku = StringUtils.trimToEmpty(materialBaseInfoPO.getBarCode());
+ if (!excelSku.equals(dbSku)) {
+ throw new ServiceException(String.format("导入失败:料号「%s」SKU码与物料档案不一致(Excel:%s / 档案:%s)",
+ materialNo, excelSku.isEmpty() ? "空" : excelSku, dbSku.isEmpty() ? "空" : dbSku));
+ }
+ } else {
+ // 料号已填写但在该货主下查不到 → 直接报错(与WMS出库导入一致),不再被SKU码/商品名称静默兜底
+ throw new ServiceException(String.format("导入失败:料号「%s」在当前客户(货主)下不存在,请核对料号或在物料管理中维护", materialNo));
+ }
+ }
+
+ // 如果料号查询不到,使用商品SKU码查询(skucode对应barCode)
+ if (materialBaseInfoPO == null && StringUtils.isNotBlank(detail.getSkucode())) {
+ List materialList = outEntrustOrderMapper.getMaterialByBarCode(detail.getSkucode(), shipperId, topOrganizationId);
+ if (materialList != null && !materialList.isEmpty()) {
+ materialBaseInfoPO = materialList.get(0);
+ }
+ }
+
+ // 如果料号和SKU码都查询不到,使用商品名称查询(commodityName对应materialName)
+ if (materialBaseInfoPO == null && StringUtils.isNotBlank(detail.getCommodityName())) {
+ List materialList = outEntrustOrderMapper.getMaterialByMaterialName(detail.getCommodityName(), shipperId, topOrganizationId);
+ if (materialList != null && !materialList.isEmpty()) {
+ materialBaseInfoPO = materialList.get(0);
+ }
+ }
+
+ // 设置物料基础信息ID,并从物料基础信息中回填料号、物料编码、物料名称等信息
+ if (materialBaseInfoPO != null) {
+ detail.setMaterialBaseInfoId(materialBaseInfoPO.getMaterialBaseInfoId());
+ // 单价统一从物料档案回填
+ detail.setUnitPrice(materialBaseInfoPO.getUnitPrice());
+ // 如果Excel中没有料号,从物料基础信息中回填
+ if (StringUtils.isBlank(detail.getMaterialNo())) {
+ detail.setMaterialNo(materialBaseInfoPO.getMaterialCode());
+ }
+ // 回填物料编码
+ if (StringUtils.isBlank(detail.getMaterialCode())) {
+ detail.setMaterialCode(materialBaseInfoPO.getMaterialCode());
+ }
+ // 回填商品编号
+ if (StringUtils.isBlank(detail.getCommodityNo())) {
+ detail.setCommodityNo(materialBaseInfoPO.getMaterialCode());
+ }
+ // 回填物料名称(如果Excel中的商品名称为空,使用物料基础信息中的名称)
+ if (StringUtils.isBlank(detail.getCommodityName())) {
+ detail.setCommodityName(materialBaseInfoPO.getMaterialName());
+ }
+ // 回填物料名称到materialName字段
+ detail.setMaterialName(materialBaseInfoPO.getMaterialName());
+ // 回填条形码
+ detail.setBarCode(materialBaseInfoPO.getBarCode());
+ // 如果Excel中没有SKU码,从物料基础信息中回填
+ if (StringUtils.isBlank(detail.getSkucode())) {
+ detail.setSkucode(materialBaseInfoPO.getBarCode());
+ }
+ }
+ // 单位:优先使用Excel中的"单位"字段,如果为空则使用"申报单位",如果还为空则从物料主数据获取(与WMS出库导入处理一致)
+ String unit = StringUtils.isNotBlank(detail.getUnit()) ? detail.getUnit() : null;
+ if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(detail.getDeclareUnit())) {
+ unit = detail.getDeclareUnit();
+ }
+ if (StringUtils.isBlank(unit) && materialBaseInfoPO != null && StringUtils.isNotBlank(materialBaseInfoPO.getUnitName())) {
+ unit = materialBaseInfoPO.getUnitName();
+ }
+ detail.setUnit(unit);
+ }
+ }
+
+ private void initDict(OutEntrustOrderDO outEntrustOrder) {
+ if (ObjectUtil.isNotNull(outEntrustOrder.getContainerNoName())) {
+ AjaxResult containerNo = systemServiceFeign.selectListByDictType("cabinetNo");
+ if (ObjectUtil.isNotNull(containerNo) && "200".equals(String.valueOf(containerNo.get("code")))) {
+ List containerNoList = JSON.parseArray(JSONObject.toJSONString(containerNo.get("data")), SysDictDataVo.class);
+ for (SysDictDataVo sysDictDataVo : containerNoList) {
+ if (outEntrustOrder.getContainerNoName().equals(sysDictDataVo.getDictLabel())) {
+ outEntrustOrder.setContainerNo(sysDictDataVo.getDictValue());
+ break;
+ }
+ }
+ }
+ }
+
+ if (ObjectUtil.isNotNull(outEntrustOrder.getTransportModeName())) {
+ AjaxResult transportMode = systemServiceFeign.selectListByDictType("transportMode");
+ if (ObjectUtil.isNotNull(transportMode) && "200".equals(String.valueOf(transportMode.get("code")))) {
+ List transportModeList = JSON.parseArray(JSONObject.toJSONString(transportMode.get("data")), SysDictDataVo.class);
+ for (SysDictDataVo sysDictDataVo : transportModeList) {
+ if (outEntrustOrder.getTransportModeName().equals(sysDictDataVo.getDictLabel())) {
+ outEntrustOrder.setTransportModeCode(sysDictDataVo.getDictValue());
+ break;
+ }
+ }
+ }
+ }
+
+ if (ObjectUtil.isNotNull(outEntrustOrder.getSupervisionModeName())) {
+ AjaxResult supervisionMode = systemServiceFeign.selectListByDictType("controlType");
+ if (ObjectUtil.isNotNull(supervisionMode) && "200".equals(String.valueOf(supervisionMode.get("code")))) {
+ List supervisionModeList = JSON.parseArray(JSONObject.toJSONString(supervisionMode.get("data")), SysDictDataVo.class);
+ for (SysDictDataVo sysDictDataVo : supervisionModeList) {
+ if (outEntrustOrder.getSupervisionModeName().equals(sysDictDataVo.getDictLabel())) {
+ outEntrustOrder.setSupervisionModeCode(sysDictDataVo.getDictValue());
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 导入出库单:解析出的货主用户是否属于当前登录用户同一组织(organizationId)。
+ * 仅当登录用户带有 organizationId 时启用过滤;否则保持与旧行为一致。
+ */
+ private boolean shipperMatchesImporterOrganization(UserPo userPo, Long loginOrganizationId) {
+ if (loginOrganizationId == null) {
+ return true;
+ }
+ if (userPo == null) {
+ return false;
+ }
+ Long oid = userPo.getOrganizationId();
+ return oid != null && loginOrganizationId.equals(oid);
+ }
+
+ /**
+ * 按客户名称或数字ID解析货主用户,逻辑与WMS出库管理导入一致:getInfoByName → 货主列表兜底,均做 organizationId 校验。
+ */
+ private UserPo resolveCustomerShipperUserPoForImport(String customerName) {
+ if (StringUtils.isEmpty(customerName)) {
+ return null;
+ }
+ Long loginOrganizationId = null;
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ if (loginUser != null && loginUser.getUserPo() != null) {
+ loginOrganizationId = loginUser.getUserPo().getOrganizationId();
+ }
+ String shipperName = customerName.trim();
+ if (StringUtils.isEmpty(shipperName)) {
+ return null;
+ }
+
+ // 纯数字:按用户ID查询
+ if (shipperName.matches("\\d+")) {
+ try {
+ Long possibleId = Long.valueOf(shipperName);
+ AjaxResult ajaxResult = userServiceFeign.getInfo(possibleId);
+ if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) {
+ UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class);
+ if (userPo != null && shipperMatchesImporterOrganization(userPo, loginOrganizationId)) {
+ return userPo;
+ }
+ }
+ } catch (Exception e) {
+ // ignore
+ }
+ return null;
+ }
+
+ // 按名称查询(用户中心)
+ try {
+ AjaxResult ajaxResult = userServiceFeign.getInfoByName(shipperName);
+ if ("200".equals(String.valueOf(ajaxResult.get("code"))) && ajaxResult.get("data") != null) {
+ UserPo userPo = JSON.parseObject(JSONObject.toJSONString(ajaxResult.get("data")), UserPo.class);
+ if (userPo != null && shipperMatchesImporterOrganization(userPo, loginOrganizationId)) {
+ return userPo;
+ }
+ }
+ } catch (Exception e) {
+ // 继续货主列表兜底
+ }
+
+ // 货主列表兜底(与WMS出库导入一致)
+ try {
+ AjaxResult shipperListResult = userServiceFeign.userShipperListAll();
+ if ("200".equals(String.valueOf(shipperListResult.get("code"))) && shipperListResult.get("data") != null) {
+ Object data = shipperListResult.get("data");
+ List