gaoluyang
2 天以前 feedd62ec46b9faada80505387526acf914cf4c5
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;
@@ -40,6 +41,7 @@
import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService;
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 +59,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 实现类
@@ -203,6 +206,13 @@
        }
        // 2. 不存在则新建
        return createMaterialStock(itemId, warehouseId, locationId, areaId, batchId, batchCode, vendorId, receiptTime);
    }
    @Override
    public MesWmMaterialStockDO createMaterialStock(Long itemId, Long warehouseId, Long locationId, Long areaId,
                                                     Long batchId, String batchCode, Long vendorId, LocalDateTime receiptTime) {
        // 始终创建新库存记录
        MesMdmItemDO item = mdmItemService.validateItemExists(itemId);
        MesWmMaterialStockDO newStock = MesWmMaterialStockDO.builder()
                .itemId(itemId).itemTypeId(item.getCategoryId())
@@ -213,7 +223,7 @@
                .frozen(false)
                .build();
        materialStockMapper.insert(newStock);
        // 3. 重新查询,确保拿到数据库生成的完整记录(id、createTime 等)
        // 重新查询,确保拿到数据库生成的完整记录(id、createTime 等)
        MesWmMaterialStockDO inserted = materialStockMapper.selectById(newStock.getId());
        return inserted != null ? inserted : newStock;
    }
@@ -271,20 +281,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
@@ -519,6 +596,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 = "增加";
@@ -681,6 +760,8 @@
                respVO.getFailureNames().put(key, ex.getMessage());
            }
        }
        // 记录导入结果汇总,供 @LogRecord 的 success 文案引用
        OperateLogUtils.putImportSummary(respVO.getCreateNames(), respVO.getUpdateNames(), respVO.getFailureNames());
        return respVO;
    }