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