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> shipperList = null; + if (data instanceof List) { + List rawList = (List) data; + shipperList = new ArrayList<>(); + for (Object item : rawList) { + if (item instanceof Map) { + Map mapItem = (Map) item; + shipperList.add(mapItem); + } else { + Map mapItem = JSON.parseObject(JSONObject.toJSONString(item), new TypeReference>() { + }); + shipperList.add(mapItem); + } + } + } else { + String jsonStr = JSONObject.toJSONString(data); + shipperList = JSON.parseObject(jsonStr, new TypeReference>>() { + }); + } + + if (shipperList != null && !shipperList.isEmpty()) { + for (Map shipper : shipperList) { + Object userId = shipper.get("userId"); + Object userName = shipper.get("userName"); + if (userName == null) { + userName = shipper.get("name"); + } + if (userName == null) { + userName = shipper.get("shipperName"); + } + boolean matched = false; + if (StringUtils.isNotEmpty(shipperName) && userName != null) { + String normalizedUserName = String.valueOf(userName).trim(); + if (shipperName.equals(normalizedUserName) || normalizedUserName.contains(shipperName)) { + matched = true; + } + } + if (matched && userId != null) { + Long candidateId = Long.valueOf(String.valueOf(userId)); + AjaxResult infoRes = userServiceFeign.getInfo(candidateId); + if ("200".equals(String.valueOf(infoRes.get("code"))) && infoRes.get("data") != null) { + UserPo up = JSON.parseObject(JSONObject.toJSONString(infoRes.get("data")), UserPo.class); + if (shipperMatchesImporterOrganization(up, loginOrganizationId)) { + return up; + } + } + } + } + } + } + } catch (Exception e) { + // ignore + } + return null; + } +} diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustHeaderListener.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustHeaderListener.java new file mode 100644 index 000000000..c42569cc5 --- /dev/null +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustHeaderListener.java @@ -0,0 +1,201 @@ +package com.mhd.oms.domain.outEntrustOrder.repository.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO; +import lombok.Getter; +import org.apache.poi.ss.usermodel.DateUtil; // Excel序列号转日期,POI已随EasyExcel引入 +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 出仓委托单导入-表头信息监听器(处理前11行,与WMS出库管理导入模板一致) + * + * @author rcx + * @date 2026-09-14 + */ +@Getter +public class OutEntrustHeaderListener extends AnalysisEventListener> { + private final OutEntrustOrderDO outEntrustOrder = new OutEntrustOrderDO(); + private int rowNum = 0; + /** 日期单元格有值但格式无法识别的收集器(类上已有@Getter,自动生成getDateParseErrors()) */ + private final List dateParseErrors = new ArrayList<>(); + private final DateTimeFormatter dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + @Override + public void invoke(Map data, AnalysisContext context) { + // 将Map转换为Map + Map stringData = convertToStringMap(data); + // 将Map转换为List再转为数组 + List dataList = mapToList(stringData); + Object[] rowData = listToArray(dataList); + String stringValue1 = getStringValue(rowData[0]); + if (stringValue1 != null && !stringValue1.isEmpty()) { + if ("备注:".equals(stringValue1)) { + outEntrustOrder.setRemark(getStringValue(rowData[1])); + } + } + // 根据行号提取对应信息 + switch (rowNum) { + case 4: // 第5行:to、仓库联系人、车型及车牌、委托编码 + outEntrustOrder.setToReceiver(getStringValue(rowData[2])); + outEntrustOrder.setContactPerson(getStringValue(rowData[5])); + outEntrustOrder.setVehicleModelPlate(getStringValue(rowData[12])); + outEntrustOrder.setEntrustNo(getStringValue(rowData[16])); + break; + case 5: // 第6行:承运方、tel、柜号、司机签名 + outEntrustOrder.setCarrier(getStringValue(rowData[2])); + outEntrustOrder.setTel(getStringValue(rowData[5])); + outEntrustOrder.setContainerNoName(getStringValue(rowData[12])); + outEntrustOrder.setDriverSign(getStringValue(rowData[16])); + break; + case 6: // 第7行:出仓日期、fax、运输方式、申报地关区 + Date outboundDate = parseDateSmart(getStringValue(rowData[2]), "出仓日期"); + if (outboundDate != null) { + outEntrustOrder.setOutboundDate(outboundDate); + } + outEntrustOrder.setFax(getStringValue(rowData[5])); + outEntrustOrder.setTransportModeName(getStringValue(rowData[12])); + outEntrustOrder.setDeclareCustoms(getStringValue(rowData[16])); + break; + case 7: // 第8行:生产商、客户名称、报关姓名及联系方式、完成时间 + outEntrustOrder.setManufacturer(getStringValue(rowData[2])); + outEntrustOrder.setShipperName(getStringValue(rowData[5])); + outEntrustOrder.setCustomsDeclarerInfo(getStringValue(rowData[12])); + Date completeTime = parseDateSmart(getStringValue(rowData[16]), "完成时间"); + if (completeTime != null) { + outEntrustOrder.setCompleteTime(completeTime); + } + break; + case 8: // 第9行:监管方式、送货地址、海关是否查验货物、进出境关别 + outEntrustOrder.setSupervisionModeName(getStringValue(rowData[2])); + outEntrustOrder.setDeliveryAddr(getStringValue(rowData[5])); + String stringValue = getStringValue(rowData[12]); + outEntrustOrder.setCustomsInspectionFlag(stringValue); + outEntrustOrder.setEntryExitCustoms(getStringValue(rowData[16])); + break; + case 9: // 第10行:下单日期、起运国 + Date orderDate = parseDateSmart(getStringValue(rowData[2]), "下单日期"); + if (orderDate != null) { + outEntrustOrder.setOrderDate(orderDate); + } + outEntrustOrder.setDepartureArrivalCountry(getStringValue(rowData[5])); + break; + } + rowNum++; + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // 解析完成后的处理 + } + + /** + * 将List转换为数组 + */ + private Object[] listToArray(List list) { + // 创建一个足够大的数组来容纳最多30列数据 + Object[] array = new Object[30]; + for (int i = 0; i < 30 && i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + /** + * 将Object的Map转换为String的Map + */ + private Map convertToStringMap(Map dataMap) { + if (dataMap == null) { + return new HashMap<>(); + } + + Map stringMap = new HashMap<>(); + for (Map.Entry entry : dataMap.entrySet()) { + Object value = entry.getValue(); + String stringValue = value != null ? value.toString() : ""; + stringMap.put(entry.getKey(), stringValue); + } + + return stringMap; + } + + /** + * 将Map转换为List + */ + private List mapToList(Map map) { + if (map == null) { + return new ArrayList<>(); + } + + // 找到最大索引以确定列表大小 + int maxSize = map.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1) + 1; + maxSize = Math.max(maxSize, 30); // 至少30个位置 + + List list = new ArrayList<>(Collections.nCopies(maxSize, "")); + + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() >= 0 && entry.getKey() < maxSize) { + String value = entry.getValue(); + list.set(entry.getKey(), value != null ? value : ""); + } + } + + return list; + } + + /** + * 转换为字符串,处理null值 + */ + private String getStringValue(Object value) { + return value == null ? "" : value.toString().trim(); + } + + /** + * 智能解析日期:兼容 Excel 日期单元格序列号 + 常见文本格式。 + * 解析失败记入 dateParseErrors,由解析服务汇总返回给用户,不再静默丢失。 + * @param raw 单元格原始字符串 + * @param fieldLabel 字段中文名(用于错误提示) + */ + private Date parseDateSmart(String raw, String fieldLabel) { + String s = raw == null ? "" : raw.trim(); + if (s.isEmpty()) { + return null; // 单元格为空:保持原行为,不算错误 + } + // 1) Excel 日期序列号:NoConvertConverter 会把真日期单元格转成纯数字字符串(如 46234 = 2026-08-20) + // 区间 20000~60000 对应 1954~2064 年,避免把普通数字误判成日期 + if (s.matches("^\\d{1,5}(\\.\\d+)?$")) { + try { + double serial = Double.parseDouble(s); + if (serial >= 20000 && serial <= 60000) { + return DateUtil.getJavaDate(serial); + } + } catch (NumberFormatException ignore) { + } + } + // 2) 常见文本格式逐个尝试 + String[] patterns = { + "yyyy-MM-dd", "yyyy/M/d", "yyyy.M.d", "yyyy年M月d日", + "yyyy-MM-dd HH:mm:ss", "yyyy/M/d H:mm:ss", "yyyy-MM-dd H:mm" + }; + for (String p : patterns) { + try { + SimpleDateFormat sdf = new SimpleDateFormat(p); + sdf.setLenient(false); + return sdf.parse(s); + } catch (ParseException ignore) { + } + } + dateParseErrors.add(fieldLabel + "「" + s + "」格式无法识别,请改为日期格式或 2026-08-20 / 2026/8/20 / 2026年8月20日 等格式"); + return null; + } +} diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustItemListener.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustItemListener.java new file mode 100644 index 000000000..9bde977cf --- /dev/null +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/listener/OutEntrustItemListener.java @@ -0,0 +1,282 @@ +package com.mhd.oms.domain.outEntrustOrder.repository.listener; + +import com.alibaba.excel.context.AnalysisContext; +import com.alibaba.excel.event.AnalysisEventListener; +import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDetailDO; +import lombok.Getter; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 出仓委托单导入-商品明细监听器(从第12行开始,与WMS出库管理导入模板一致) + * + * @author rcx + * @date 2026-09-14 + */ +@Getter +public class OutEntrustItemListener extends AnalysisEventListener> { + private final List itemList = new ArrayList<>(); + + @Override + public void invoke(Map data, AnalysisContext context) { + // 将Map转换为Map + Map stringData = convertToStringMap(data); + // 将Map转换为List再转为数组 + List dataList = mapToList(stringData); + Object[] rowData = listToArray(dataList); + + // 提取序号(第一列),过滤空行和标题行 + String serialNoStr = getStringValue(rowData[0]); + String commodityName = getStringValue(rowData[1]); + String materialNo = getStringValue(rowData[2]); + + if (commodityName.isEmpty() && materialNo.isEmpty()) { + return; + } + if (serialNoStr.isEmpty() || "序号".equals(serialNoStr) || !isSequenceNumber(serialNoStr)) { + return; + } + + OutEntrustOrderDetailDO item = new OutEntrustOrderDetailDO(); + + // 填充商品明细数据 + //商品名称 料号 规格型号/ITEM 商品SKU码 Invoice No. 申报数量 申报单位 升 箱数 件数 尺寸 出库数量 "总净重 + //(KG)" 总毛重 (KG) "总体积 + //(CBM)" 总面积(SQM) Batch Ref NO's Sheet Ref NO's 原产国 箱号/卡板号 总价 币制 备注 + item.setCommodityName(getStringValue(rowData[1])); + item.setMaterialNo(getStringValue(rowData[2])); + item.setSpecificationModel(getStringValue(rowData[3])); + item.setSkucode(getStringValue(rowData[4])); + item.setInvoiceNo(getStringValue(rowData[5])); + item.setDeclareQuantity(parseBigDecimal(rowData[6])); + item.setDeclareUnit(getStringValue(rowData[7])); + item.setLiter(normalizeNumeric(getStringValue(rowData[8]))); + item.setCaseCount(parseBigDecimal(rowData[9])); + item.setPieceCount(parseBigDecimal(rowData[10])); + item.setSize(getStringValue(rowData[11])); + item.setOutboundQuantity(parseBigDecimal(rowData[12])); + item.setTotalNetWeight(parseBigDecimal(rowData[13])); + item.setTotalGrossWeight(parseBigDecimal(rowData[14])); + item.setTotalVolume(parseBigDecimal(rowData[15])); + item.setTotalArea(parseBigDecimal(rowData[16])); + item.setBatchRefNo(getStringValue(rowData[17])); + item.setSheetRefNo(getStringValue(rowData[18])); + item.setCountryOfOrigin(getStringValue(rowData[19])); + item.setBoxPalletNo(getStringValue(rowData[20])); + item.setTotalAmount(parseBigDecimal(rowData[21])); + item.setCurrency(getStringValue(rowData[22])); + item.setRemark(getStringValue(rowData[23])); + + itemList.add(item); + } + + @Override + public void doAfterAllAnalysed(AnalysisContext context) { + // 解析完成后的处理 + } + + /** + * 将List转换为数组 + */ + private Object[] listToArray(List list) { + // 创建一个足够大的数组来容纳最多30列数据 + Object[] array = new Object[30]; + for (int i = 0; i < 30 && i < list.size(); i++) { + array[i] = list.get(i); + } + return array; + } + + /** + * 将Object的Map转换为String的Map + */ + private Map convertToStringMap(Map dataMap) { + if (dataMap == null) { + return new HashMap<>(); + } + + Map stringMap = new HashMap<>(); + for (Map.Entry entry : dataMap.entrySet()) { + Object value = entry.getValue(); + String stringValue = convertToString(value); + stringMap.put(entry.getKey(), stringValue); + } + + return stringMap; + } + + /** + * 将Object转换为String,处理BigDecimal等数字类型,去除小数点后的零 + */ + private String convertToString(Object value) { + if (value == null) { + return ""; + } + + // 如果是BigDecimal类型,需要特殊处理,去除小数点后的零 + if (value instanceof BigDecimal) { + BigDecimal bd = (BigDecimal) value; + // 如果小数部分为0,转换为整数形式 + if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) { + return String.valueOf(bd.longValue()); + } + // 否则去除尾部零 + return bd.stripTrailingZeros().toPlainString(); + } + + // 如果是Double或Float类型,也需要处理 + if (value instanceof Double) { + Double d = (Double) value; + if (d == d.longValue()) { + return String.valueOf(d.longValue()); + } + return d.toString(); + } + + if (value instanceof Float) { + Float f = (Float) value; + if (f == f.longValue()) { + return String.valueOf(f.longValue()); + } + return f.toString(); + } + + if (value instanceof String) { + String str = value.toString().trim(); + if (str.isEmpty()) { + return ""; + } + return str; + } + + return value.toString().trim(); + } + + /** + * 将Map转换为List + */ + private List mapToList(Map map) { + if (map == null) { + return new ArrayList<>(); + } + + // 找到最大索引以确定列表大小 + int maxSize = map.keySet().stream().mapToInt(Integer::intValue).max().orElse(-1) + 1; + maxSize = Math.max(maxSize, 30); // 至少30个位置 + + List list = new ArrayList<>(Collections.nCopies(maxSize, "")); + + for (Map.Entry entry : map.entrySet()) { + if (entry.getKey() >= 0 && entry.getKey() < maxSize) { + String value = entry.getValue(); + list.set(entry.getKey(), value != null ? value : ""); + } + } + + return list; + } + + private String getStringValue(Object value) { + if (value == null) { + return ""; + } + + // 如果是BigDecimal类型,需要特殊处理,去除小数点后的零 + if (value instanceof BigDecimal) { + BigDecimal bd = (BigDecimal) value; + // 如果小数部分为0,转换为整数形式 + if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) { + return String.valueOf(bd.longValue()); + } + // 否则去除尾部零 + return bd.stripTrailingZeros().toPlainString(); + } + + // 如果是Double或Float类型,也需要处理 + if (value instanceof Double) { + Double d = (Double) value; + if (d == d.longValue()) { + return String.valueOf(d.longValue()); + } + return d.toString(); + } + + if (value instanceof Float) { + Float f = (Float) value; + if (f == f.longValue()) { + return String.valueOf(f.longValue()); + } + return f.toString(); + } + + if (value instanceof String) { + String str = value.toString().trim(); + if (str.isEmpty()) { + return ""; + } + // 尝试解析为数字,如果是整数形式的小数(如 "123.0"),转换为整数 + try { + BigDecimal bd = new BigDecimal(str); + // 如果小数部分为0,转换为整数形式 + if (bd.scale() > 0 && bd.stripTrailingZeros().scale() <= 0) { + return String.valueOf(bd.longValue()); + } + // 否则去除尾部零 + return bd.stripTrailingZeros().toPlainString(); + } catch (NumberFormatException e) { + // 不是数字字符串,直接返回 + return str; + } + } + + return value.toString().trim(); + } + + /** + * 解析BigDecimal,失败返回0 + */ + private BigDecimal parseBigDecimal(Object value) { + try { + String strValue = getStringValue(value); + if (strValue.isEmpty()) { + return BigDecimal.ZERO; + } + return new BigDecimal(strValue); + } catch (Exception e) { + return BigDecimal.ZERO; + } + } + + /** + * 升列归一化:去千分位逗号、去空白;空值统一存 null(避免前端合计时对空串求和); + * 纯数字归一为无多余零的字符串;非数字内容原样保留。 + */ + private String normalizeNumeric(String raw) { + if (raw == null) { + return null; + } + String s = raw.trim().replace(",", ""); + if (s.isEmpty()) { + return null; + } + try { + return new BigDecimal(s).stripTrailingZeros().toPlainString(); + } catch (NumberFormatException e) { + return s; + } + } + + public static boolean isSequenceNumber(String str) { + if (str == null || str.trim().isEmpty()) { + return false; + } + str = str.trim(); + + return str.matches("^[1-9]\\d*$") || str.matches("^[1-9]\\d*\\.\\d+$") || str.matches("^0\\.\\d+$"); + } +} diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/mapper/OutEntrustOrderMapper.java b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/mapper/OutEntrustOrderMapper.java index fcf8d0e61..32528c646 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/mapper/OutEntrustOrderMapper.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/outEntrustOrder/repository/mapper/OutEntrustOrderMapper.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.mhd.oms.domain.outEntrustOrder.entity.OutEntrustOrder; import com.mhd.oms.domain.outEntrustOrder.repository.po.OutEntrustOrderPO; import com.mhd.oms.domain.outEntrustOrder.repository.todo.OutEntrustOrderDO; +import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfoPO; import org.apache.ibatis.annotations.Param; import java.util.List; @@ -25,4 +26,25 @@ public interface OutEntrustOrderMapper extends BaseMapper { * 根据编号集合查询出仓委托单列表 */ List getInfoByIds(@Param("idList") List idList); + + /** + * 导入用:按料号查询货主下的物料基础信息(跨库WMS物料表) + */ + List getMaterialByMaterialCode(@Param("materialCode") String materialCode, + @Param("shipperId") Long shipperId, + @Param("topOrganizationId") Long topOrganizationId); + + /** + * 导入用:按SKU码(条形码)查询货主下的物料基础信息(跨库WMS物料表) + */ + List getMaterialByBarCode(@Param("barCode") String barCode, + @Param("shipperId") Long shipperId, + @Param("topOrganizationId") Long topOrganizationId); + + /** + * 导入用:按物料名称查询货主下的物料基础信息(跨库WMS物料表) + */ + List getMaterialByMaterialName(@Param("materialName") String materialName, + @Param("shipperId") Long shipperId, + @Param("topOrganizationId") Long topOrganizationId); } diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderApplicationService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderApplicationService.java index 19c8cdfba..1ad2051aa 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderApplicationService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockInOrder/service/ReservationStockInOrderApplicationService.java @@ -10,6 +10,7 @@ import com.mhd.common.core.domain.po.UserPo; import com.mhd.common.core.enums.DictCode; import com.mhd.common.core.exception.ServiceException; import com.mhd.common.core.utils.StringUtils; +import com.mhd.common.core.utils.poi.ExcelUtil; import com.mhd.common.core.web.domain.AjaxResult; import com.mhd.common.security.utils.SecurityUtils; import com.mhd.oms.domain.erpCountry.entity.ErpCountry; @@ -18,6 +19,7 @@ import com.mhd.oms.domain.gwLog.repository.mapper.GwLogMapper; import com.mhd.oms.domain.reservationInMaterialDetail.entity.ReservationInMaterialDetail; import com.mhd.oms.domain.reservationInMaterialDetail.repository.mapper.ReservationInMaterialDetailMapper; import com.mhd.oms.domain.reservationInMaterialDetail.repository.po.ReservationInMaterialDetailPO; +import com.mhd.oms.domain.reservationInMaterialDetail.repository.todo.ReservationInMaterialDetailDO; import com.mhd.oms.domain.reservationMaterialInventory.repository.facade.IReservationMaterialInventoryService; import com.mhd.oms.domain.reservationMaterialInventory.repository.mapper.ReservationMaterialInventoryMapper; import com.mhd.oms.domain.reservationStockInOrder.entity.ReservationStockInOrder; @@ -26,6 +28,7 @@ import com.mhd.oms.domain.reservationStockInOrder.repository.po.MaterialBaseInfo import com.mhd.oms.domain.reservationStockInOrder.repository.po.ReservationStockInOrderPO; import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO; import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDTO; +import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderImportRowDTO; import com.mhd.oms.domain.reservationStockInOrder.repository.vo.GwInDetail; import com.mhd.oms.domain.reservationStockInOrder.repository.vo.GwStockInOrder; import com.mhd.oms.interfaces.dto.erpCountry.ErpCountryDTO; @@ -47,6 +50,7 @@ import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; import java.io.IOException; @@ -1590,6 +1594,635 @@ public class ReservationStockInOrderApplicationService { // } // } + /** + * 导入入仓委托单(模板与WMS入库管理导入一致:第1-12行表头,第13行列名,第14行起明细) + * 以【委托编号NO】作为分组键,同一委托编号的多条明细归为一张入库单 + */ + public Boolean importData(MultipartFile file, Long warehouseId, String warehouseCode, String warehouseName) { + if (file == null || file.isEmpty()) { + throw new ServiceException("导入文件不能为空"); + } + LoginUser loginUser = SecurityUtils.getLoginUser(); + if (loginUser == null) { + throw new ServiceException("登录已失效,请重新登录"); + } + Long topOrganizationId = loginUser.getUserPo() != null ? loginUser.getUserPo().getTopOrganizationId() : null; + + try (InputStream is = file.getInputStream()) { + // 明细表的列名在第13行(索引12),数据从第14行开始 + int detailTitleRowIndex = 12; + + // 1. 读取表头信息(第1-12行),按标签扫描提取字段 + ReservationStockInOrderImportRowDTO headerInfo = new ReservationStockInOrderImportRowDTO(); + try (InputStream isForHeader = file.getInputStream()) { + Workbook wb = WorkbookFactory.create(isForHeader); + Sheet sheet = wb.getSheetAt(0); + fillHeaderInfoByLabels(sheet, headerInfo); + } catch (Exception e) { + log.error("读取Excel表头信息失败", e); + // 不抛出异常,继续尝试从明细行读取 + } + log.info("表头信息读取完成 - 委托编号NO: {}, 客户名称: {}, To: {}, 联系人: {}, 承运商: {}, 入仓日期: {}, 完成时间: {}, 下单日期: {}", + headerInfo.getConsignmentNo(), headerInfo.getShipperName(), headerInfo.getTo(), + headerInfo.getContactPerson(), headerInfo.getCarrier(), headerInfo.getWarehouseDate(), + headerInfo.getCompletionTime(), headerInfo.getOrderDate()); + + // 2. 读取明细数据(ExcelUtil从第13行列名开始) + ExcelUtil util = new ExcelUtil<>(ReservationStockInOrderImportRowDTO.class); + List rows = util.importExcel(is, detailTitleRowIndex); + if (rows == null || rows.isEmpty()) { + throw new ServiceException("导入数据为空。请检查Excel第13行是否为明细表列名(包含'商品名称'列),第14行开始是否为数据行"); + } + log.info("成功读取 {} 行明细数据", rows.size()); + + // 3. 将表头信息应用到每一行(明细行值为空时使用表头值) + applyHeaderToRows(headerInfo, rows); + + // 4. 过滤掉商品名称为空的空行 + List validRows = rows.stream() + .filter(r -> r != null && StringUtils.isNotBlank(r.getMaterialName())) + .collect(Collectors.toList()); + if (validRows.isEmpty()) { + throw new ServiceException("导入数据为空(可能全为空行)。共读取 " + rows.size() + + " 行数据,但商品名称都为空。请检查Excel第13行是否为明细表列名(包含'商品名称'列),第14行开始是否为数据行"); + } + + // 5. 仓库必填校验(与出库单导入保持一致,仓库由导入弹窗选择传入) + if (warehouseId == null) { + throw new ServiceException("导入失败:仓库ID不能为空"); + } + if (StringUtils.isBlank(warehouseCode)) { + throw new ServiceException("导入失败:仓库编码不能为空"); + } + if (StringUtils.isBlank(warehouseName)) { + throw new ServiceException("导入失败:仓库名称不能为空"); + } + + // 6. 以委托编号NO分组;若为空则归到同一组 + Map> groupMap = validRows.stream() + .collect(Collectors.groupingBy(r -> StringUtils.isBlank(r.getConsignmentNo()) ? "__EMPTY_CONSIGNMENT__" : r.getConsignmentNo())); + + List orderList = new ArrayList<>(); + for (Map.Entry> entry : groupMap.entrySet()) { + ReservationStockInOrderDO order = buildStockInOrderFromImport( + entry.getValue(), headerInfo, loginUser, topOrganizationId, + warehouseId, warehouseCode, warehouseName); + if (order != null) { + orderList.add(order); + } + } + + // 如果所有订单都被跳过(orderList为空),抛出异常提示用户 + if (orderList.isEmpty()) { + throw new ServiceException("导入失败:所有入库单的明细行都因数据不完整(数量为空或物料基础信息缺失)被跳过,请检查Excel数据"); + } + + return stockInOrderDomainService.batchInsert(orderList); + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + log.error("导入入库单失败", e); + throw new ServiceException("导入入库单失败:" + e.getMessage()); + } + } + + /** + * 单个委托编号分组的入库单组装(解析+校验,不落库) + * 明细行数量为空/容器类型行跳过;料号必填且必须在当前货主下存在,规格型号/SKU码与物料档案不一致直接报错 + * + * @return 组装完成的入库单;该组明细全部被跳过时返回null + */ + private ReservationStockInOrderDO buildStockInOrderFromImport(List groupRows, + ReservationStockInOrderImportRowDTO headerInfo, + LoginUser loginUser, Long topOrganizationId, + Long warehouseId, String warehouseCode, String warehouseName) { + ReservationStockInOrderImportRowDTO h = groupRows.get(0); + + // 客户名称优先使用表头,表头为空时使用明细行 + String customerName = StringUtils.isNotBlank(headerInfo.getShipperName()) ? headerInfo.getShipperName() : h.getShipperName(); + if (StringUtils.isBlank(customerName)) { + throw new ServiceException("导入失败:客户名称不能为空(委托编号NO=" + h.getConsignmentNo() + + ")。请确保Excel表头(第1-12行)中包含'客户名称'字段"); + } + + ReservationStockInOrderDO order = new ReservationStockInOrderDO(); + // 组织/审计字段 + if (loginUser.getUserPo() != null) { + order.setOrganizationId(loginUser.getUserPo().getOrganizationId()); + order.setOrganizationName(loginUser.getUserPo().getOrganizationName()); + order.setTopOrganizationId(loginUser.getUserPo().getTopOrganizationId()); + } + order.setCreateBy(loginUser.getUserid()); + order.setCreateByName(resolveUserRealName(loginUser)); + order.setCreateTime(new Date()); + + // 仓库(导入弹窗选择传入) + order.setWarehouseId(warehouseId); + order.setWarehouseCode(warehouseCode); + order.setWarehouseName(warehouseName); + + // 货主信息:名称/编号解析货主ID + String shipperCode = StringUtils.isNotBlank(headerInfo.getShipperCode()) ? headerInfo.getShipperCode() : h.getShipperCode(); + Long shipperId = getShipperIdByNameOrCode(customerName, shipperCode); + order.setShipperId(shipperId); + if (StringUtils.isNotBlank(customerName)) { + order.setShipperName(customerName); + } + if (StringUtils.isNotBlank(shipperCode)) { + order.setShipperCode(shipperCode); + } + order.setConsignmentNo(h.getConsignmentNo()); + order.setRemark(h.getRemark()); + // 导入入库单时,如果入库单类型为空,默认设置为普通入库 + order.setOrderTypeCode("pu_tong_ru_ku"); + order.setOrderTypeName("普通入库单"); + // 订单来源:数据导入(与入库业务单多页签导入保持一致) + order.setReservationOrderSource("数据导入"); + + // 报关表头字段(优先使用表头字段,表头为空则使用明细行字段) + order.setTo(firstNotBlank(headerInfo.getTo(), h.getTo())); + order.setContactPerson(firstNotBlank(headerInfo.getContactPerson(), h.getContactPerson())); + order.setVehicleInfo(firstNotBlank(headerInfo.getVehicleInfo(), h.getVehicleInfo())); + order.setTel(firstNotBlank(headerInfo.getTel(), h.getTel())); + order.setDriverSignature(firstNotBlank(headerInfo.getDriverSignature(), h.getDriverSignature())); + order.setFax(firstNotBlank(headerInfo.getFax(), h.getFax())); + order.setDeclarationArea(firstNotBlank(headerInfo.getDeclarationArea(), h.getDeclarationArea())); + order.setManufacturer(firstNotBlank(headerInfo.getManufacturer(), h.getManufacturer())); + order.setCustomsBrokerInfo(firstNotBlank(headerInfo.getCustomsBrokerInfo(), h.getCustomsBrokerInfo())); + order.setDeliveryAddress(firstNotBlank(headerInfo.getDeliveryAddress(), h.getDeliveryAddress())); + order.setEntryExitCustoms(firstNotBlank(headerInfo.getEntryExitCustoms(), h.getEntryExitCustoms())); + order.setOriginDestinationCountry(firstNotBlank(headerInfo.getOriginDestinationCountry(), h.getOriginDestinationCountry())); + order.setCarrier(firstNotBlank(headerInfo.getCarrier(), h.getCarrier())); + order.setContainerNo(firstNotBlank(headerInfo.getContainerNo(), h.getContainerNo())); + order.setTransportMethod(firstNotBlank(headerInfo.getTransportMethod(), h.getTransportMethod())); + order.setSupervisionMethod(firstNotBlank(headerInfo.getSupervisionMethod(), h.getSupervisionMethod())); + + // 报关字段默认值(优先使用表头字段) + Integer customsInspection = headerInfo.getCustomsInspection() != null ? headerInfo.getCustomsInspection() : h.getCustomsInspection(); + order.setCustomsInspection(customsInspection == null ? 0 : customsInspection); // 默认:否 + Integer needCustomsDeclaration = headerInfo.getNeedCustomsDeclaration() != null ? headerInfo.getNeedCustomsDeclaration() : h.getNeedCustomsDeclaration(); + order.setNeedCustomsDeclaration(needCustomsDeclaration == null ? 1 : needCustomsDeclaration); // 默认:是 + Integer needTransport = headerInfo.getNeedTransport() != null ? headerInfo.getNeedTransport() : h.getNeedTransport(); + order.setNeedTransport(needTransport == null ? 0 : needTransport); // 默认:否 + + // 日期字段(优先使用表头字段) + String warehouseDateStr = firstNotBlank(headerInfo.getWarehouseDate(), h.getWarehouseDate()); + if (StringUtils.isNotBlank(warehouseDateStr)) { + Date warehouseDate = parseDateString(warehouseDateStr); + if (warehouseDate != null) { + order.setWarehouseDate(warehouseDate); + } + } + String completionTimeStr = firstNotBlank(headerInfo.getCompletionTime(), h.getCompletionTime()); + if (StringUtils.isNotBlank(completionTimeStr)) { + Date completionTime = parseDateString(completionTimeStr); + if (completionTime != null) { + order.setCompletionTime(completionTime); + } + } + String orderDateStr = firstNotBlank(headerInfo.getOrderDate(), h.getOrderDate()); + if (StringUtils.isNotBlank(orderDateStr)) { + Date orderDate = parseDateString(orderDateStr); + if (orderDate != null) { + order.setOrderDate(orderDate); + } + } + + // 明细 + List details = new ArrayList<>(); + for (ReservationStockInOrderImportRowDTO r : groupRows) { + // 先检查数量(仓库数量优先,入库数量兜底),数量为空直接跳过该明细行 + BigDecimal quantity = r.getWarehouseQuantity(); + if (quantity == null) { + quantity = r.getInboundQuantity(); + } + if (quantity == null) { + log.warn("跳过明细行:仓库数量或入库数量不能为空 - 委托编号NO: [{}], 商品名称: [{}], 料号: [{}]", + h.getConsignmentNo(), r.getMaterialName(), r.getMaterialCode()); + continue; + } + + // 检查是否为容器类型等非物料信息,如果是则跳过 + String materialName = r.getMaterialName(); + if (StringUtils.isNotBlank(materialName)) { + String lowerName = materialName.trim(); + if (lowerName.contains("尺柜") || lowerName.contains("尺集装箱") + || lowerName.contains("Container") || lowerName.contains("container") + || (lowerName.contains("柜") && (lowerName.contains("20") || lowerName.contains("40") || lowerName.contains("45")))) { + log.info("跳过明细行:识别为容器类型信息,非实际物料 - 委托编号NO: [{}], 商品名称: [{}]", + h.getConsignmentNo(), materialName); + continue; + } + } + + // 导入文件中必须填写料号,根据料号获取商品信息,必须是货主下有的才可以匹配 + if (StringUtils.isBlank(r.getMaterialCode())) { + throw new ServiceException("导入失败:请在导入文件中填写正确的料号"); + } + + // 根据料号跨库查询物料基础信息(getByCode按组织过滤),查到后需在Java侧核对货主归属 + MaterialBaseInfoPO materialBaseInfoPO = safeGetMaterialByCode(r.getMaterialCode(), loginUser); + if (materialBaseInfoPO != null && shipperId != null + && (materialBaseInfoPO.getShipperId() == null || !materialBaseInfoPO.getShipperId().equals(shipperId))) { + // 查到但不属于该货主,等同于不存在,不允许导入 + materialBaseInfoPO = null; + } + if (materialBaseInfoPO == null) { + // 如果提供了料号但查询不到(或不属于该货主),直接报错,不允许导入 + throw new ServiceException(String.format("导入失败:该货主下不存在该料号(货主名称=%s,料号=%s)。请确保该料号已在物料管理中创建,且属于该货主", + order.getShipperName(), r.getMaterialCode())); + } + + // 校验料号与规格型号/SKU码一致性(与WMS入库管理导入一致):任何一项不符直接报对应错误 + String excelSpec = StringUtils.trimToEmpty(r.getSpecificationModel()); + String dbSpec = StringUtils.trimToEmpty(materialBaseInfoPO.getSpecificationModel()); + if (!excelSpec.equals(dbSpec)) { + throw new ServiceException(String.format("导入失败:料号[%s](商品名称:%s)规格型号与物料档案不一致(Excel:%s / 档案:%s)", + r.getMaterialCode(), r.getMaterialName(), + excelSpec.isEmpty() ? "空" : excelSpec, dbSpec.isEmpty() ? "空" : dbSpec)); + } + String excelSku = StringUtils.trimToEmpty(r.getSkuCode()); + String dbSku = StringUtils.trimToEmpty(materialBaseInfoPO.getBarCode()); + if (!excelSku.equals(dbSku)) { + throw new ServiceException(String.format("导入失败:料号[%s](商品名称:%s)SKU码与物料档案不一致(Excel:%s / 档案:%s)", + r.getMaterialCode(), r.getMaterialName(), + excelSku.isEmpty() ? "空" : excelSku, dbSku.isEmpty() ? "空" : dbSku)); + } + + ReservationInMaterialDetailDO d = new ReservationInMaterialDetailDO(); + d.setMaterialBaseInfoId(materialBaseInfoPO.getMaterialBaseInfoId()); + // 从物料档案获取名称(与WMS入库导入一致) + d.setMaterialName(materialBaseInfoPO.getMaterialName()); + d.setMaterialCode(r.getMaterialCode()); + d.setQuantity(quantity); + d.setSpecificationModel(r.getSpecificationModel()); + d.setSkuCode(r.getSkuCode()); + d.setInvoiceNo(r.getInvoiceNo()); + d.setDeclaredQuantity(r.getDeclaredQuantity()); + d.setDeclarationUnit(r.getDeclarationUnit()); + d.setLiter(r.getLiter()); + d.setBoxCount(r.getBoxCount()); + d.setPieceCount(r.getPieceCount()); + d.setDimensions(r.getDimensions()); + // 入库数量:优先使用入库数量,如果为空则使用仓库数量 + BigDecimal inboundQuantity = r.getInboundQuantity(); + if (inboundQuantity == null) { + inboundQuantity = r.getWarehouseQuantity(); + } + d.setInboundQuantity(inboundQuantity); + // 单位:优先使用Excel中的"单位"字段,如果为空则使用"申报单位",如果还为空则从物料主数据获取 + String unit = StringUtils.isNotBlank(r.getUnit()) ? r.getUnit() : null; + if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(r.getDeclarationUnit())) { + unit = r.getDeclarationUnit(); + } + if (StringUtils.isBlank(unit) && StringUtils.isNotBlank(materialBaseInfoPO.getUnitName())) { + unit = materialBaseInfoPO.getUnitName(); + } + d.setUnit(unit); + // 总净重/总毛重/总体积/总面积:优先使用主字段,如果为空则使用备用字段 + d.setTotalNetWeight(r.getTotalNetWeight() != null ? r.getTotalNetWeight() : r.getTotalNetWeight2()); + d.setTotalGrossWeight(r.getTotalGrossWeight() != null ? r.getTotalGrossWeight() : r.getTotalGrossWeight2()); + d.setTotalVolume(r.getTotalVolume() != null ? r.getTotalVolume() : r.getTotalVolume2()); + d.setTotalArea(r.getTotalArea() != null ? r.getTotalArea() : r.getTotalArea2()); + d.setBatchRefNo(r.getBatchRefNo()); + d.setSheetRefNo(r.getSheetRefNo()); + // 原产国:优先使用Excel中的数据,如果为空则从物料主数据获取 + String originCountry = StringUtils.isNotBlank(r.getOriginCountry()) ? r.getOriginCountry() : null; + if (StringUtils.isBlank(originCountry) && StringUtils.isNotBlank(materialBaseInfoPO.getOriginCountry())) { + originCountry = materialBaseInfoPO.getOriginCountry(); + } + d.setOriginCountry(originCountry); + d.setBoxPalletNo(r.getBoxPalletNo()); + d.setTotalPrice(r.getTotalPrice()); + // 币制:优先使用Excel中的数据,如果为空则从物料主数据获取 + String currency = StringUtils.isNotBlank(r.getCurrency()) ? r.getCurrency() : null; + if (StringUtils.isBlank(currency) && StringUtils.isNotBlank(materialBaseInfoPO.getCurrency())) { + currency = materialBaseInfoPO.getCurrency(); + } + d.setCurrency(currency); + d.setRemark(r.getDetailRemark()); + + details.add(d); + } + + // 如果该订单的所有明细行都被跳过(details为空),跳过该订单 + if (details.isEmpty()) { + log.warn("跳过入库单:委托编号NO [{}] 的所有明细行都因数据不完整被跳过,该入库单将不会被导入", h.getConsignmentNo()); + return null; + } + + order.setMaterialDetailList(details); + + // 导入入库单时,计划数量取所有明细行的入库数量之和 + BigDecimal totalInboundQuantity = details.stream() + .map(detail -> detail.getInboundQuantity() != null ? detail.getInboundQuantity() : BigDecimal.ZERO) + .reduce(BigDecimal.ZERO, BigDecimal::add); + order.setQuantity(totalInboundQuantity); + + // 导入时补全货主/供应商/数据字典信息(与手动新增链路一致) + try { + shipperParam(order); + } catch (Exception e) { + log.error("导入入库单时设置货主信息失败,委托编号NO:{},货主名称:{},错误:{}", + h.getConsignmentNo(), order.getShipperName(), e.getMessage()); + throw new ServiceException("导入失败:设置货主信息失败(委托编号NO=" + h.getConsignmentNo() + + ",货主名称=" + order.getShipperName() + ")。" + e.getMessage()); + } + supplierParam(order); + setDataDict(order); + return order; + } + + /** + * 导入用:按料号跨库查询物料基础信息(查询结果需在Java侧核对货主归属) + */ + private MaterialBaseInfoPO safeGetMaterialByCode(String materialCode, LoginUser loginUser) { + try { + Long orgId = loginUser.getUserPo() != null ? loginUser.getUserPo().getOrganizationId() : null; + return reservationStockInOrderMapper.getByCode(materialCode, orgId); + } catch (Exception e) { + log.warn("按料号查询物料基础信息失败,料号: [{}], 错误: {}", materialCode, e.getMessage()); + return null; + } + } + + /** + * 将表头信息应用到每一行明细(明细行值为空时使用表头值;客户名称/货主编号表头有值时强制应用) + */ + private void applyHeaderToRows(ReservationStockInOrderImportRowDTO headerInfo, List rows) { + for (ReservationStockInOrderImportRowDTO row : rows) { + if (row == null) { + continue; + } + // 应用表头中的委托编号NO + if (StringUtils.isBlank(row.getConsignmentNo()) && StringUtils.isNotBlank(headerInfo.getConsignmentNo())) { + row.setConsignmentNo(headerInfo.getConsignmentNo()); + } + // 表头有客户名称/货主编号时,强制应用到所有明细行 + if (StringUtils.isNotBlank(headerInfo.getShipperName())) { + row.setShipperName(headerInfo.getShipperName()); + } + if (StringUtils.isNotBlank(headerInfo.getShipperCode())) { + row.setShipperCode(headerInfo.getShipperCode()); + } + // 应用其他表头字段(如果明细行为空,则使用表头值) + row.setTo(firstNotBlank(row.getTo(), headerInfo.getTo())); + row.setContactPerson(firstNotBlank(row.getContactPerson(), headerInfo.getContactPerson())); + row.setVehicleInfo(firstNotBlank(row.getVehicleInfo(), headerInfo.getVehicleInfo())); + row.setCarrier(firstNotBlank(row.getCarrier(), headerInfo.getCarrier())); + row.setTel(firstNotBlank(row.getTel(), headerInfo.getTel())); + row.setDriverSignature(firstNotBlank(row.getDriverSignature(), headerInfo.getDriverSignature())); + row.setFax(firstNotBlank(row.getFax(), headerInfo.getFax())); + row.setDeclarationArea(firstNotBlank(row.getDeclarationArea(), headerInfo.getDeclarationArea())); + row.setManufacturer(firstNotBlank(row.getManufacturer(), headerInfo.getManufacturer())); + row.setCustomsBrokerInfo(firstNotBlank(row.getCustomsBrokerInfo(), headerInfo.getCustomsBrokerInfo())); + row.setDeliveryAddress(firstNotBlank(row.getDeliveryAddress(), headerInfo.getDeliveryAddress())); + row.setEntryExitCustoms(firstNotBlank(row.getEntryExitCustoms(), headerInfo.getEntryExitCustoms())); + row.setOriginDestinationCountry(firstNotBlank(row.getOriginDestinationCountry(), headerInfo.getOriginDestinationCountry())); + row.setContainerNo(firstNotBlank(row.getContainerNo(), headerInfo.getContainerNo())); + row.setTransportMethod(firstNotBlank(row.getTransportMethod(), headerInfo.getTransportMethod())); + row.setSupervisionMethod(firstNotBlank(row.getSupervisionMethod(), headerInfo.getSupervisionMethod())); + if (row.getCustomsInspection() == null) { + row.setCustomsInspection(headerInfo.getCustomsInspection()); + } + if (row.getNeedCustomsDeclaration() == null) { + row.setNeedCustomsDeclaration(headerInfo.getNeedCustomsDeclaration()); + } + if (row.getNeedTransport() == null) { + row.setNeedTransport(headerInfo.getNeedTransport()); + } + row.setWarehouseDate(firstNotBlank(row.getWarehouseDate(), headerInfo.getWarehouseDate())); + row.setCompletionTime(firstNotBlank(row.getCompletionTime(), headerInfo.getCompletionTime())); + row.setOrderDate(firstNotBlank(row.getOrderDate(), headerInfo.getOrderDate())); + row.setRemark(firstNotBlank(row.getRemark(), headerInfo.getRemark())); + } + } + + /** + * 扫描表头区域(第1-12行)按标签提取字段值: + * 值优先取标签同行右侧最近的合法单元格,其次取下一行同列,最后取标签文本冒号后的内容; + * 看起来仍是标签文本(含冒号或命中标签关键词)的值会被跳过 + */ + private void fillHeaderInfoByLabels(Sheet sheet, ReservationStockInOrderImportRowDTO headerInfo) { + for (int rowIdx = 0; rowIdx < 12; rowIdx++) { + Row row = sheet.getRow(rowIdx); + if (row == null) { + continue; + } + for (int colIdx = 0; colIdx < row.getPhysicalNumberOfCells(); colIdx++) { + Cell labelCell = row.getCell(colIdx); + if (labelCell == null) { + continue; + } + String labelValue = getCellValueAsString(labelCell); + if (StringUtils.isBlank(labelValue)) { + continue; + } + // 标签文本冒号后的内容(兜底值) + String extractedFromSameCell = null; + if (labelValue.contains(":")) { + String[] parts = labelValue.split(":", 2); + if (parts.length > 1) { + extractedFromSameCell = parts[1].trim(); + } + } + String value = findValueRightwards(row, colIdx); + if (StringUtils.isBlank(value)) { + value = findValueBelow(sheet, rowIdx, colIdx); + } + if (StringUtils.isBlank(value)) { + value = extractedFromSameCell; + } + if (StringUtils.isBlank(value)) { + continue; + } + dispatchHeaderField(labelValue, value, headerInfo); + } + } + } + + /** + * 在同行右侧(最多10列)查找第一个合法值 + */ + private String findValueRightwards(Row row, int colIdx) { + for (int searchColIdx = colIdx + 1; searchColIdx < colIdx + 10 && searchColIdx < row.getPhysicalNumberOfCells(); searchColIdx++) { + Cell valueCell = row.getCell(searchColIdx); + if (valueCell == null) { + continue; + } + String cellValue = getCellValueAsString(valueCell); + if (StringUtils.isNotBlank(cellValue)) { + return cellValue; + } + } + return null; + } + + /** + * 在下一行同列查找合法值(模板变体:值写在标签下一行) + */ + private String findValueBelow(Sheet sheet, int rowIdx, int colIdx) { + Row nextRow = sheet.getRow(rowIdx + 1); + if (nextRow == null) { + return null; + } + Cell nextRowCell = nextRow.getCell(colIdx); + if (nextRowCell == null) { + return null; + } + String nextRowValue = getCellValueAsString(nextRowCell); + return StringUtils.isNotBlank(nextRowValue) ? nextRowValue : null; + } + + /** + * 按标签关键词分发到表头字段 + */ + private void dispatchHeaderField(String labelValue, String value, ReservationStockInOrderImportRowDTO headerInfo) { + // 备注:值内容任意,不做标签过滤 + if (labelValue.contains("备注")) { + if (StringUtils.isBlank(headerInfo.getRemark())) { + headerInfo.setRemark(value); + } + return; + } + // 委托编号NO(支持"委托编号NO"、"委托编号"等格式,排除标签本身) + if (labelValue.contains("委托编号")) { + if (StringUtils.isBlank(headerInfo.getConsignmentNo()) + && !value.contains("委托编号") && !value.contains("NO") + && !"委托编号NO".equals(value) && !"委托编号".equals(value)) { + headerInfo.setConsignmentNo(value); + } + return; + } + // 客户名称 + if (labelValue.contains("客户名称")) { + if (StringUtils.isBlank(headerInfo.getShipperName()) + && !value.contains("客户") && !"客户名称".equals(value)) { + headerInfo.setShipperName(value); + } + return; + } + // To(需要精确匹配,避免误匹配) + if (isToLabel(labelValue)) { + if (StringUtils.isBlank(headerInfo.getTo()) && !isToLabel(value) && !looksLikeLabel(value)) { + headerInfo.setTo(value); + } + return; + } + // 值是标签文本(含冒号或命中标签关键词)时跳过 + if (value.contains(":") || value.contains(":") || looksLikeLabel(value)) { + return; + } + if (labelValue.contains("联系人") && StringUtils.isBlank(headerInfo.getContactPerson())) { + headerInfo.setContactPerson(value); + } else if ((labelValue.contains("车型") || labelValue.contains("车牌")) && StringUtils.isBlank(headerInfo.getVehicleInfo())) { + headerInfo.setVehicleInfo(value); + } else if ((labelValue.contains("承运方") || labelValue.contains("承运商")) && StringUtils.isBlank(headerInfo.getCarrier())) { + headerInfo.setCarrier(value); + } else if (labelValue.contains("Tel") && StringUtils.isBlank(headerInfo.getTel())) { + headerInfo.setTel(value); + } else if (labelValue.contains("司机签名") && StringUtils.isBlank(headerInfo.getDriverSignature())) { + headerInfo.setDriverSignature(value); + } else if (labelValue.contains("入仓日期") && StringUtils.isBlank(headerInfo.getWarehouseDate())) { + headerInfo.setWarehouseDate(value); + } else if (labelValue.contains("Fax") && StringUtils.isBlank(headerInfo.getFax())) { + headerInfo.setFax(value); + } else if (labelValue.contains("申报地关区") && StringUtils.isBlank(headerInfo.getDeclarationArea())) { + headerInfo.setDeclarationArea(value); + } else if (labelValue.contains("生产商") && StringUtils.isBlank(headerInfo.getManufacturer())) { + headerInfo.setManufacturer(value); + } else if (labelValue.contains("报关员") && StringUtils.isBlank(headerInfo.getCustomsBrokerInfo())) { + headerInfo.setCustomsBrokerInfo(value); + } else if (labelValue.contains("送货地址") && StringUtils.isBlank(headerInfo.getDeliveryAddress())) { + headerInfo.setDeliveryAddress(value); + } else if (labelValue.contains("进出境关别") && StringUtils.isBlank(headerInfo.getEntryExitCustoms())) { + headerInfo.setEntryExitCustoms(value); + } else if ((labelValue.contains("起运") || labelValue.contains("运抵国")) && StringUtils.isBlank(headerInfo.getOriginDestinationCountry())) { + headerInfo.setOriginDestinationCountry(value); + } else if (labelValue.contains("柜号") && StringUtils.isBlank(headerInfo.getContainerNo())) { + headerInfo.setContainerNo(value); + } else if (labelValue.contains("运输方式") && StringUtils.isBlank(headerInfo.getTransportMethod())) { + headerInfo.setTransportMethod(value); + } else if (labelValue.contains("监管方式") && StringUtils.isBlank(headerInfo.getSupervisionMethod())) { + headerInfo.setSupervisionMethod(value); + } else if (labelValue.contains("海关是否查验货物") && headerInfo.getCustomsInspection() == null) { + headerInfo.setCustomsInspection(parseYesNo(value)); + } else if (labelValue.contains("是否需要报关") && headerInfo.getNeedCustomsDeclaration() == null) { + headerInfo.setNeedCustomsDeclaration(parseYesNo(value)); + } else if (labelValue.contains("是否需要运输") && headerInfo.getNeedTransport() == null) { + headerInfo.setNeedTransport(parseYesNo(value)); + } else if (labelValue.contains("完成时间") && StringUtils.isBlank(headerInfo.getCompletionTime())) { + headerInfo.setCompletionTime(value); + } else if (labelValue.contains("下单日期") && StringUtils.isBlank(headerInfo.getOrderDate())) { + headerInfo.setOrderDate(value); + } + } + + /** + * 是否为To标签(精确匹配,避免把Value/Total等英文词误认为To) + */ + private boolean isToLabel(String text) { + if (text == null) { + return false; + } + String t = text.trim(); + return "To".equals(t) || "To:".equals(t) || "To:".equals(t) || t.startsWith("To:") || t.startsWith("To:"); + } + + /** + * 值是否看起来仍是标签文本(命中常见标签关键词) + */ + private boolean looksLikeLabel(String value) { + if (value == null) { + return true; + } + String v = value.trim(); + return v.contains("委托编号") || v.contains("司机签名") || v.contains("完成时间") || v.contains("下单日期") + || v.contains("入仓日期") || v.contains("联系人") || v.contains("Tel") || v.contains("Fax") + || v.contains("客户") || v.contains("地址") || v.contains("日期") || v.contains("方式") + || v.contains("关区") || v.contains("关别") || v.contains("报关员") || v.contains("生产商") + || v.contains("承运方") || v.contains("承运商") || v.contains("起运") || v.contains("运抵国") + || v.contains("地区") || v.contains("车型") || v.contains("车牌") || v.contains("柜号") + || v.contains("海关是否查验货物") || v.contains("是否需要报关") || v.contains("是否需要运输"); + } + + /** + * 是/否(或1/0)转Integer;无法识别返回null + */ + private Integer parseYesNo(String value) { + if (value == null) { + return null; + } + String v = value.trim(); + if (v.contains("是") || "1".equals(v)) { + return 1; + } + if (v.contains("否") || "0".equals(v)) { + return 0; + } + return null; + } + + /** + * 取第一个非空字符串 + */ + private String firstNotBlank(String a, String b) { + return StringUtils.isNotBlank(a) ? a : b; + } + + /** + * 获取登录人姓名(优先UserPo.userName,其次realname,最后username兜底) + */ + private String resolveUserRealName(LoginUser loginUser) { + if (loginUser.getUserPo() != null && StringUtils.isNotBlank(loginUser.getUserPo().getUserName())) { + return loginUser.getUserPo().getUserName(); + } + if (StringUtils.isNotBlank(loginUser.getRealname())) { + return loginUser.getRealname(); + } + return loginUser.getUsername(); + } + /** * 解析日期字符串为Date对象 * 支持多种日期格式:yyyy-MM-dd, yyyy/MM/dd, yyyy年MM月dd日, Date.toString()格式等 diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/facade/IReservationStockOutOrderService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/facade/IReservationStockOutOrderService.java index b0ce26bdd..36d728c3a 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/facade/IReservationStockOutOrderService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/facade/IReservationStockOutOrderService.java @@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.extension.service.IService; import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO; import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder; import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO; +import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO; import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO; @@ -22,6 +23,11 @@ public interface IReservationStockOutOrderService extends IService queryList(ReservationStockOutOrderDO stockOutOrderDO); + /** + * 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量 + */ + public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO); + /** * 新增出库单 */ diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/mapper/ReservationStockOutOrderMapper.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/mapper/ReservationStockOutOrderMapper.java index a459e02e2..62fc97bee 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/mapper/ReservationStockOutOrderMapper.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/mapper/ReservationStockOutOrderMapper.java @@ -6,6 +6,7 @@ import com.mhd.common.core.domain.po.UserPo; import com.mhd.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO; import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder; import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO; +import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO; import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO; import org.apache.ibatis.annotations.Param; //import com.mhd.wms.domain.stockOutOrder.entity.ReservationStockOutOrder; @@ -27,6 +28,11 @@ public interface ReservationStockOutOrderMapper extends BaseMapper queryList(ReservationStockOutOrderDO stockOutOrderDO); + /** + * 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量 + */ + public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO); + /** * 出库单提货单打印-表头 */ diff --git a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/persistence/ReservationStockOutOrderImpl.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/persistence/ReservationStockOutOrderImpl.java index c91598bc3..e7aea671c 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/persistence/ReservationStockOutOrderImpl.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/repository/persistence/ReservationStockOutOrderImpl.java @@ -11,6 +11,7 @@ import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrd import com.mhd.oms.domain.reservationStockOutOrder.repository.facade.IReservationStockOutOrderService; import com.mhd.oms.domain.reservationStockOutOrder.repository.mapper.ReservationStockOutOrderMapper; import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO; +import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO; import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO; import com.mhd.system.api.model.LoginUser; //import com.mhd.wms.domain.deliveTask.repository.facade.IDeliveTaskTypeService; @@ -48,6 +49,15 @@ public class ReservationStockOutOrderImpl extends ServiceImpl当前筛选条件下不分页的全量合计,随分页列表一起返回 + * (totalPlanQuantity/totalOutboundQuantity),计划数量与出库数量分开计算

+ * + * @author rcx + * @date 2026-09-14 + */ +@Data +@ApiModel(value = "出库业务单列表合计对象", description = "出库业务单列表合计响应对象") +public class ReservationStockOutOrderTotalPO 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/reservationStockOutOrder/service/ReservationStockOutOrderApplicationService.java b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/service/ReservationStockOutOrderApplicationService.java index e10484acd..b3186978b 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/service/ReservationStockOutOrderApplicationService.java +++ b/mhd_oms/src/main/java/com/mhd/oms/domain/reservationStockOutOrder/service/ReservationStockOutOrderApplicationService.java @@ -45,6 +45,7 @@ import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrd import com.mhd.oms.domain.reservationStockOutOrder.repository.facade.IReservationStockOutOrderService; import com.mhd.oms.domain.reservationStockOutOrder.repository.mapper.ReservationStockOutOrderMapper; import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO; +import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO; import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO; import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDTO; import com.mhd.oms.domain.reservationStockOutOrder.repository.vo.GwOutDetail; @@ -246,6 +247,15 @@ public class ReservationStockOutOrderApplicationService { return stockOutOrderPOS; } + /** + * 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量。 + * 须在 queryList/queryList1 之后用同一个DO调用:数据权限条件由列表查询原地写入DO, + * 合计查询才能继承生效后的筛选;且 PageHelper startPage 只拦截下一条SQL,本查询不会被分页。 + */ + public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO) { + return stockOutOrderDomainService.queryListTotal(stockOutOrderDO); + } + /** * 新增出库单 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..35fc4b80b 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 @@ -57,6 +57,7 @@ import com.mhd.oms.domain.reservationInventoryAdjustmentRecord.repository.mapper import com.mhd.oms.domain.reservationStockOutOrder.repository.facade.IReservationStockOutOrderService; import com.mhd.oms.domain.reservationStockOutOrder.repository.mapper.ReservationStockOutOrderMapper; import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO; +import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO; import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO; import com.mhd.oms.domain.reserveStockOutOrder.repository.mapper.ReserveStockOutOrderMapper; import com.mhd.system.api.SystemServiceFeign; @@ -142,6 +143,13 @@ public class ReservationStockOutOrderDomainService { public List queryList(ReservationStockOutOrderDO stockOutOrderDO) { return stockOutOrderService.queryList(stockOutOrderDO); } + + /** + * 列表合计:按查询条件全量(不分页)汇总一级明细的计划数量与出库数量 + */ + public ReservationStockOutOrderTotalPO queryListTotal(ReservationStockOutOrderDO stockOutOrderDO) { + return stockOutOrderService.queryListTotal(stockOutOrderDO); + } /** * 从单号列表中提取最大序号 * 单号格式: prefix + 数字序号(如 RK260807001 中 prefix=RK260807, 序号=001) @@ -236,22 +244,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 +302,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 +492,9 @@ public class ReservationStockOutOrderDomainService { } //设置物料信息 setMaterialDetailInfo(stockOutOrderDO); + // 下单冻结模式编辑重锁(原子):先WMS侧释放原冻结+按新明细重锁(同一事务),成功后才改本地; + // 已下发WMS的冻结单不可编辑,须先取消下发(判据=活动执行单,issueStatus 不可靠) + handleFreezeOnUpdate(stockOutOrderDO, stockInOrderPO); //统计种类 stockOutOrderDO.setMaterialQuantity(stockOutOrderDO.getMaterialDetailList().size()); //统计总数量 @@ -379,6 +574,8 @@ public class ReservationStockOutOrderDomainService { if (stockInOrderPO.getStatus() != 1){ throw new ServiceException("只有已创建状态下能允许关闭出库单"); } + // 下单冻结模式:关闭单据不再出库,先释放冻结库存(幂等;冻结已被执行单承接时WMS会拒绝并提示先取消下发) + unfreezeIfFrozen(stockInOrderPO); stockOutOrderDO.setStatus(11); return stockOutOrderService.update(stockOutOrderDO); } @@ -789,6 +986,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 +1201,8 @@ public class ReservationStockOutOrderDomainService { if (!groupBaseValid) { return; } - //WMS库存校验(接口内部会聚合该仓全部明细的需求量后比对可用库存) + //WMS库存校验(接口内部会聚合该仓全部明细的需求量后比对可用库存; + // 传业务单号:下单冻结模式下行级可用量=库存可用+本单冻结记录量) List checkList = new ArrayList<>(); for (ReservationOutMaterialDetail detail : details) { OutMaterialDetail checkDetail = new OutMaterialDetail(); @@ -1002,7 +1212,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 +1384,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_oms/src/main/java/com/mhd/oms/interfaces/facade/ReservationStockOutOrder/ReservationStockOutOrderApi.java b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/ReservationStockOutOrder/ReservationStockOutOrderApi.java index ee912d037..277ab2e10 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/ReservationStockOutOrder/ReservationStockOutOrderApi.java +++ b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/ReservationStockOutOrder/ReservationStockOutOrderApi.java @@ -1,15 +1,17 @@ package com.mhd.oms.interfaces.facade.ReservationStockOutOrder; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.PageInfo; +import com.mhd.common.core.constant.HttpStatus; 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.oms.domain.reservationOutMaterialDetail.repository.po.ReservationOutMaterialDetailPO; import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDO; import com.mhd.oms.domain.reservationStockInOrder.repository.todo.ReservationStockInOrderDTO; import com.mhd.oms.domain.reservationStockOutOrder.entity.ReservationStockOutOrder; import com.mhd.oms.domain.reservationStockOutOrder.repository.facade.IReservationStockOutOrderService; import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderPO; +import com.mhd.oms.domain.reservationStockOutOrder.repository.po.ReservationStockOutOrderTotalPO; import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDO; import com.mhd.oms.domain.reservationStockOutOrder.repository.todo.ReservationStockOutOrderDTO; import com.mhd.oms.domain.reservationStockOutOrder.service.ReservationStockOutOrderApplicationService; @@ -46,17 +48,19 @@ public class ReservationStockOutOrderApi extends BaseController { private IReservationStockOutOrderService stockOutOrderService; /** - * 分页查询出库单列表 + * 分页查询出库单列表(附:当前查询条件下全量不分页的计划数量总数/出库数量总数) */ @ApiOperation("查询出库单列表") @GetMapping("/list") - public TableDataInfo list(ReservationStockOutOrderDTO stockOutOrderDTO) + public HashMap list(ReservationStockOutOrderDTO stockOutOrderDTO) { //转换实体 ReservationStockOutOrderDO stockOutOrderDO = stockOutOrderAssembler.toDO(stockOutOrderDTO); startPage(); List list = stockOutOrderApplicationService.queryList(stockOutOrderDO); - return getDataTable(list); + // 合计须在列表查询后用同一个DO查询:数据权限条件由列表查询原地写入DO,且不再被分页拦截 + ReservationStockOutOrderTotalPO total = stockOutOrderApplicationService.queryListTotal(stockOutOrderDO); + return getDataTableWithTotal(list, total); } @@ -250,13 +254,31 @@ public class ReservationStockOutOrderApi extends BaseController { @ApiOperation("查询出库单列表") @GetMapping("/list1") - public TableDataInfo list1(ReservationStockOutOrderDTO stockOutOrderDTO) + public HashMap list1(ReservationStockOutOrderDTO stockOutOrderDTO) { //转换实体 ReservationStockOutOrderDO stockOutOrderDO = stockOutOrderAssembler.toDO(stockOutOrderDTO); startPage(); List list = stockOutOrderApplicationService.queryList1(stockOutOrderDO); - return getDataTable(list); + // 合计须在列表查询后用同一个DO查询:数据权限条件由列表查询原地写入DO,且不再被分页拦截 + ReservationStockOutOrderTotalPO total = stockOutOrderApplicationService.queryListTotal(stockOutOrderDO); + return getDataTableWithTotal(list, total); + } + + /** + * 分页结果 + 合计:data/total/code/msg 之外追加计划数量总数/出库数量总数(key与TableDataInfo序列化结果一致,仅多两个字段) + */ + private HashMap getDataTableWithTotal(List list, ReservationStockOutOrderTotalPO total) { + HashMap result = new HashMap<>(); + result.put("data", list); + result.put("total", new PageInfo<>(list).getTotal()); + result.put("code", HttpStatus.SUCCESS); + result.put("msg", "查询成功"); + if (total != null) { + result.put("totalPlanQuantity", total.getTotalPlanQuantity()); + result.put("totalOutboundQuantity", total.getTotalOutboundQuantity()); + } + return result; } @ApiOperation("生成出库预约单号") diff --git a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/executionStockOutOrder/ExecutionStockOutOrderApi.java b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/executionStockOutOrder/ExecutionStockOutOrderApi.java index d0c679bc3..f4583ed3d 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/executionStockOutOrder/ExecutionStockOutOrderApi.java +++ b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/executionStockOutOrder/ExecutionStockOutOrderApi.java @@ -1,13 +1,15 @@ package com.mhd.oms.interfaces.facade.executionStockOutOrder; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.github.pagehelper.PageInfo; +import com.mhd.common.core.constant.HttpStatus; 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.oms.domain.executionOutMaterialDetail.repository.po.ExecutionOutMaterialDetailPO; import com.mhd.oms.domain.executionStockOutOrder.entity.ExecutionStockOutOrder; import com.mhd.oms.domain.executionStockOutOrder.repository.facade.IExecutionStockOutOrderService; 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.executionStockOutOrder.service.ExecutionStockOutOrderApplicationService; @@ -45,17 +47,19 @@ public class ExecutionStockOutOrderApi extends BaseController { private IExecutionStockOutOrderService stockOutOrderService; /** - * 分页查询出库单列表 + * 分页查询出库单列表(附:当前查询条件下全量不分页的计划数量总数/出库数量总数) */ @ApiOperation("查询出库单列表") @GetMapping("/list") - public TableDataInfo list(ExecutionStockOutOrderDTO stockOutOrderDTO) + public HashMap list(ExecutionStockOutOrderDTO stockOutOrderDTO) { //转换实体 ExecutionStockOutOrderDO stockOutOrderDO = stockOutOrderAssembler.toDO(stockOutOrderDTO); startPage(); List list = stockOutOrderApplicationService.queryList(stockOutOrderDO); - return getDataTable(list); + // 合计须在列表查询后用同一个DO查询:数据权限条件由列表查询原地写入DO,且不再被分页拦截 + ExecutionStockOutOrderTotalPO total = stockOutOrderApplicationService.queryListTotal(stockOutOrderDO); + return getDataTableWithTotal(list, total); } @@ -234,13 +238,31 @@ public class ExecutionStockOutOrderApi extends BaseController { @ApiOperation("查询出库单列表") @GetMapping("/list1") - public TableDataInfo list1(ExecutionStockOutOrderDTO stockOutOrderDTO) + public HashMap list1(ExecutionStockOutOrderDTO stockOutOrderDTO) { //转换实体 ExecutionStockOutOrderDO stockOutOrderDO = stockOutOrderAssembler.toDO(stockOutOrderDTO); startPage(); List list = stockOutOrderApplicationService.queryList1(stockOutOrderDO); - return getDataTable(list); + // 合计须在列表查询后用同一个DO查询:数据权限条件由列表查询原地写入DO,且不再被分页拦截 + ExecutionStockOutOrderTotalPO total = stockOutOrderApplicationService.queryListTotal(stockOutOrderDO); + return getDataTableWithTotal(list, total); + } + + /** + * 分页结果 + 合计:data/total/code/msg 之外追加计划数量总数/出库数量总数(key与TableDataInfo序列化结果一致,仅多两个字段) + */ + private HashMap getDataTableWithTotal(List list, ExecutionStockOutOrderTotalPO total) { + HashMap result = new HashMap<>(); + result.put("data", list); + result.put("total", new PageInfo<>(list).getTotal()); + result.put("code", HttpStatus.SUCCESS); + result.put("msg", "查询成功"); + if (total != null) { + result.put("totalPlanQuantity", total.getTotalPlanQuantity()); + result.put("totalOutboundQuantity", total.getTotalOutboundQuantity()); + } + return result; } } \ No newline at end of file diff --git a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/outEntrustOrder/OutEntrustOrderApi.java b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/outEntrustOrder/OutEntrustOrderApi.java index 4f24f5cfe..f42808288 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/outEntrustOrder/OutEntrustOrderApi.java +++ b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/outEntrustOrder/OutEntrustOrderApi.java @@ -12,8 +12,10 @@ import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import javax.annotation.Resource; +import java.io.IOException; import java.util.HashMap; import java.util.List; @@ -80,6 +82,42 @@ public class OutEntrustOrderApi extends BaseController { return AjaxResult.success(outEntrustOrderApplicationService.getInfo(id)); } + /** + * 上传并解析出仓委托单Excel(模板与WMS出库管理导入一致:前11行表头+第12行起明细) + * 解析校验通过后落库为出仓委托单(生成CKWT委托单号,状态待审核/未下发) + */ + @ApiOperation("导入出仓委托单") + @PostMapping("/import") + public AjaxResult importOutEntrustOrder( + @RequestParam("file") MultipartFile file, + @RequestParam("warehouseId") Long warehouseId, + @RequestParam("warehouseCode") String warehouseCode, + @RequestParam("warehouseName") String warehouseName) { + + // 验证文件 + if (file.isEmpty()) { + return error("文件为空"); + } + + // 验证文件类型 + String fileName = file.getOriginalFilename(); + if (fileName == null || !fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { + return error("文件类型错误"); + } + + try { + // 解析Excel并返回结果 + StringBuilder result = outEntrustOrderApplicationService.importOutEntrustOrder(file, warehouseId, warehouseCode, warehouseName); + if (result.length() > 0) { + return AjaxResult.error(result.toString()); + } else { + return AjaxResult.success(result); + } + } catch (IOException e) { + return error("解析失败"); + } + } + /** * 批量审核:待审核/已驳回 → 已审核(通过)或已驳回 * 请求体:{"idList":[1,2],"orderStatus":3,"auditRemark":"备注"},orderStatus:2-审核通过 3-驳回(auditRemark选填) diff --git a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/reservationStockInOrder/ReservationStockInOrderApi.java b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/reservationStockInOrder/ReservationStockInOrderApi.java index cd6a3701e..e104d34a3 100644 --- a/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/reservationStockInOrder/ReservationStockInOrderApi.java +++ b/mhd_oms/src/main/java/com/mhd/oms/interfaces/facade/reservationStockInOrder/ReservationStockInOrderApi.java @@ -99,18 +99,28 @@ public class ReservationStockInOrderApi extends BaseController { } /** - * 导入入库单 + * 导入入仓委托单(模板与WMS入库管理导入一致:第1-12行表头,第13行列名,第14行起明细) + * 按【委托编号NO】分组,同一委托编号的多条明细归为一张入库单 */ -// @ApiOperation("导入入库单") -// @PostMapping(value = "/importData") -// public AjaxResult importData( -// @RequestParam("file") MultipartFile file, -// @RequestParam("warehouseId") Long warehouseId, -// @RequestParam("warehouseCode") String warehouseCode, -// @RequestParam("warehouseName") String warehouseName) -// { -// return AjaxResult.success(stockInOrderApplicationService.importData(file, warehouseId, warehouseCode, warehouseName)); -// } + @ApiOperation("导入入仓委托单") + @PostMapping(value = "/importData") + public AjaxResult importData( + @RequestParam("file") MultipartFile file, + @RequestParam("warehouseId") Long warehouseId, + @RequestParam("warehouseCode") String warehouseCode, + @RequestParam("warehouseName") String warehouseName) + { + // 验证文件 + if (file.isEmpty()) { + return error("文件为空"); + } + // 验证文件类型 + String fileName = file.getOriginalFilename(); + if (fileName == null || !fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { + return error("文件类型错误"); + } + return AjaxResult.success(stockInOrderApplicationService.importData(file, warehouseId, warehouseCode, warehouseName)); + } /** * 审核入库单 diff --git a/mhd_oms/src/main/resources/mapper/executionStockOutOrder/ExecutionStockOutOrderMapper.xml b/mhd_oms/src/main/resources/mapper/executionStockOutOrder/ExecutionStockOutOrderMapper.xml index ced13d638..ca551b9c2 100644 --- a/mhd_oms/src/main/resources/mapper/executionStockOutOrder/ExecutionStockOutOrderMapper.xml +++ b/mhd_oms/src/main/resources/mapper/executionStockOutOrder/ExecutionStockOutOrderMapper.xml @@ -117,6 +117,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + @@ -408,7 +409,14 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" + + + + + + select * from "NGWL_TEST_WMS".MATERIAL_BASE_INFO + where DEL_FLAG = 1 + and SHIPPER_ID = #{shipperId} + + and TOP_ORGANIZATION_ID = #{topOrganizationId} + + + + + + + + + diff --git a/mhd_oms/src/main/resources/mapper/reservationStockOutOrder/ReservationStockOutOrderMapper.xml b/mhd_oms/src/main/resources/mapper/reservationStockOutOrder/ReservationStockOutOrderMapper.xml index a95ebedbb..0855dadaa 100644 --- a/mhd_oms/src/main/resources/mapper/reservationStockOutOrder/ReservationStockOutOrderMapper.xml +++ b/mhd_oms/src/main/resources/mapper/reservationStockOutOrder/ReservationStockOutOrderMapper.xml @@ -437,6 +437,26 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" order by a.create_time desc nulls last + + + + + + + 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;