gaoluyang
10 天以前 7bd10d34e8a8dc2437ec8551eb511e37634d7306
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/wm/materialstock/MesWmMaterialStockServiceImpl.java
@@ -8,6 +8,7 @@
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.SetUtils;
import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockAdjustReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockFreezeReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.wm.materialstock.vo.MesWmMaterialStockImportExcelVO;
@@ -38,8 +39,10 @@
import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService;
import cn.iocoder.yudao.module.mes.service.wm.transaction.dto.MesWmTransactionSaveReqDTO;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseDefaultService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseLocationService;
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseService;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -57,6 +60,7 @@
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.filterList;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.mes.enums.LogRecordConstants.*;
/**
 * MES 库存台账 Service 实现类
@@ -82,6 +86,9 @@
    @Resource
    @Lazy
    private MesWmWarehouseService warehouseService;
    @Resource
    @Lazy
    private MesWmWarehouseDefaultService warehouseDefaultService;
    @Resource
    @Lazy
    private MesWmTransactionService wmTransactionService;
@@ -242,6 +249,10 @@
    @Override
    public void checkAreaMixingRule(Long areaId, Long itemId, Long batchId) {
        // 未指定库位(客户未启用库区/库位管理)时不校验混放,后续入库事务会落到仓库默认库位
        if (areaId == null) {
            return;
        }
        // 1.1 校验库位必须存在
        MesWmWarehouseAreaDO area = areaService.validateWarehouseAreaExists(areaId);
@@ -278,20 +289,87 @@
            throw exception(WM_MATERIAL_STOCK_REQUIRED);
        }
        MesWmMaterialStockDO stock = validateMaterialStockExists(materialStockId);
        // 2. 校验库存记录的物料、批次、仓库、库区、库位等信息与前端选择的一致,避免串单或越权提交
        if (ObjUtil.notEqual(stock.getItemId(), itemId)
                || ObjUtil.notEqual(stock.getBatchId(), batchId)
                || (batchCode != null && ObjUtil.notEqual(stock.getBatchCode(), batchCode))
                || ObjUtil.notEqual(stock.getWarehouseId(), warehouseId)
                || ObjUtil.notEqual(stock.getLocationId(), locationId)
                || ObjUtil.notEqual(stock.getAreaId(), areaId)) {
            throw exception(WM_MATERIAL_STOCK_SELECTION_MISMATCH);
        // 2. 校验库存记录的物料、批次、仓库、库区、库位等信息与单据一致,避免串单或越权提交。
        //    逐个维度比对,命中时提示具体差异(库存侧 vs 单据侧)并给出处理办法,避免笼统报"信息不一致"
        String mismatch = buildSelectedStockMismatch(
                stock, itemId, batchId, batchCode, warehouseId, locationId, areaId);
        if (mismatch != null) {
            throw new ServiceException(WM_MATERIAL_STOCK_SELECTION_MISMATCH.getCode(),
                    "所选库存记录与单据不一致:" + mismatch + "。请重新选择该物料正确的库存;若仓库/库区/库位/批次被禁用无法修改,请删除本条明细后重新添加。");
        }
        // 3. 校验库存数量充足(如果前端传了 quantity,则必须保证库存数量 >= quantity)
        if (quantity != null && stock.getQuantity() != null && stock.getQuantity().compareTo(quantity) < 0) {
            throw exception(WM_MATERIAL_STOCK_INSUFFICIENT);
        }
        return stock;
    }
    /**
     * 逐维度比对待选库存与单据传入的信息,返回第一个不一致维度的可读描述;全部一致返回 null
     */
    private String buildSelectedStockMismatch(MesWmMaterialStockDO stock, Long itemId, Long batchId, String batchCode,
                                              Long warehouseId, Long locationId, Long areaId) {
        if (ObjUtil.notEqual(stock.getItemId(), itemId)) {
            return "所选库存归属物料[" + itemIdText(itemId) + "],单据物料为[" + itemIdText(stock.getItemId()) + "]";
        }
        if (ObjUtil.notEqual(stock.getBatchId(), batchId)) {
            return "所选库存批次[" + batchText(stock.getBatchId(), stock.getBatchCode())
                    + "],单据批次为[" + batchText(batchId, batchCode) + "]";
        }
        if (batchCode != null && ObjUtil.notEqual(stock.getBatchCode(), batchCode)) {
            return "所选库存批次号[" + stock.getBatchCode() + "],单据批次号为[" + batchCode + "]";
        }
        if (ObjUtil.notEqual(stock.getWarehouseId(), warehouseId)) {
            return "所选库存仓库[" + warehouseText(stock.getWarehouseId())
                    + "],单据仓库为[" + warehouseText(warehouseId) + "]";
        }
        if (ObjUtil.notEqual(stock.getLocationId(), locationId)) {
            return "所选库存库区[" + locationText(stock.getLocationId())
                    + "],单据库区为[" + locationText(locationId) + "]";
        }
        if (ObjUtil.notEqual(stock.getAreaId(), areaId)) {
            return "所选库存库位[" + areaText(stock.getAreaId())
                    + "],单据库位为[" + areaText(areaId) + "]";
        }
        return null;
    }
    private String itemIdText(Long itemId) {
        return itemId == null ? "未选择" : ("物料ID=" + itemId);
    }
    private String batchText(Long batchId, String batchCode) {
        if (StrUtil.isNotBlank(batchCode)) {
            return batchCode;
        }
        return batchId == null ? "未选择" : ("批次ID=" + batchId);
    }
    private String warehouseText(Long warehouseId) {
        if (warehouseId == null) {
            return "未选择";
        }
        MesWmWarehouseDO warehouse = warehouseService.getWarehouse(warehouseId);
        return warehouse != null && StrUtil.isNotBlank(warehouse.getName())
                ? warehouse.getName() : ("仓库ID=" + warehouseId);
    }
    private String locationText(Long locationId) {
        if (locationId == null) {
            return "未选择";
        }
        MesWmWarehouseLocationDO location = locationService.getWarehouseLocation(locationId);
        return location != null && StrUtil.isNotBlank(location.getName())
                ? location.getName() : ("库区ID=" + locationId);
    }
    private String areaText(Long areaId) {
        if (areaId == null) {
            return "未选择";
        }
        MesWmWarehouseAreaDO area = areaService.getWarehouseArea(areaId);
        return area != null && StrUtil.isNotBlank(area.getName())
                ? area.getName() : ("库位ID=" + areaId);
    }
    @Override
@@ -333,7 +411,14 @@
            }
        }
        // 3. 校验库位混放规则
        // 3. 未指定库区/库位时,落到仓库默认库区/库位(客户未启用库区/库位管理)
        if (reqVO.getWarehouseId() != null
                && (reqVO.getLocationId() == null || reqVO.getAreaId() == null)) {
            MesWmWarehouseAreaDO defaultArea = warehouseDefaultService.getOrCreateDefaultArea(reqVO.getWarehouseId());
            reqVO.setLocationId(defaultArea.getLocationId());
            reqVO.setAreaId(defaultArea.getId());
        }
        // 3.1 校验库位混放规则
        checkAreaMixingRule(reqVO.getAreaId(), reqVO.getItemId(), batchId);
        // 4. 获取或创建库存记录
@@ -470,7 +555,16 @@
            throw exception(WM_MATERIAL_STOCK_FROZEN);
        }
        // 4. 校验目标库位混放规则
        // 4. 目标库区/库位未指定时,落到目标仓库的默认库区/库位(客户未启用库区/库位管理)
        if (reqVO.getToWarehouseId() == null) {
            reqVO.setToWarehouseId(sourceStock.getWarehouseId());
        }
        if ((reqVO.getToLocationId() == null || reqVO.getToAreaId() == null)) {
            MesWmWarehouseAreaDO defaultArea = warehouseDefaultService.getOrCreateDefaultArea(reqVO.getToWarehouseId());
            reqVO.setToLocationId(defaultArea.getLocationId());
            reqVO.setToAreaId(defaultArea.getId());
        }
        // 4.1 校验目标库位混放规则
        checkAreaMixingRule(reqVO.getToAreaId(), sourceStock.getItemId(), sourceStock.getBatchId());
        // 5. 生成移库单号
@@ -526,6 +620,8 @@
    }
    @Override
    @LogRecord(type = MES_WM_MATERIAL_STOCK_TYPE, subType = MES_WM_MATERIAL_STOCK_IMPORT_SUB_TYPE, bizNo = "0",
            success = MES_WM_MATERIAL_STOCK_IMPORT_SUCCESS)
    public MesWmMaterialStockImportRespVO importMaterialStockList(List<MesWmMaterialStockImportExcelVO> importList) {
        // 操作类型取值:增加 / 减少 / 覆盖
        final String OP_ADD = "增加";
@@ -688,6 +784,8 @@
                respVO.getFailureNames().put(key, ex.getMessage());
            }
        }
        // 记录导入结果汇总,供 @LogRecord 的 success 文案引用
        OperateLogUtils.putImportSummary(respVO.getCreateNames(), respVO.getUpdateNames(), respVO.getFailureNames());
        return respVO;
    }