7 天以前 e418f64a7ca0d6ec76096c46a2b543dab8495119
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -2,14 +2,8 @@
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemImportExcelVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemImportRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.md.item.vo.MesMdItemSaveReqVO;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
@@ -19,11 +13,13 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper;
import cn.iocoder.yudao.module.mes.enums.md.MesMdItemTypeEnum;
import cn.iocoder.yudao.module.mes.enums.md.autocode.MesMdAutoCodeRuleCodeEnum;
import cn.iocoder.yudao.module.mes.enums.wm.BarcodeBizTypeEnum;
import cn.iocoder.yudao.module.mes.service.md.unitmeasure.MesMdUnitMeasureService;
import cn.iocoder.yudao.module.mes.service.md.autocode.MesMdAutoCodeRecordService;
import cn.iocoder.yudao.module.mes.service.wm.barcode.MesWmBarcodeService;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemSyncApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemSyncReqDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -31,7 +27,6 @@
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
@@ -40,11 +35,9 @@
/**
 * MES 物料产品 Service 实现类
 *
 * 改造说明:MES 物料主数据来源于 MDM
 * - getItem/getItemList/validateItemExists 调用 MDM API
 * - MES 特有配置(批次配置、BOM、SOP、SIP)保留在 MES 模块管理
 * 支持 MES 和 MDM 物料数据双向同步
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@@ -52,9 +45,6 @@
    @Resource
    private MesMdItemMapper itemMapper;
    @Resource
    private MdmItemApi mdmItemApi;
    @Resource
    private MesMdItemTypeService itemTypeService;
@@ -73,24 +63,47 @@
    @Resource
    private MesMdAutoCodeRecordService autoCodeRecordService;
    @Resource
    private MdmItemSyncApi mdmItemSyncApi;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createItem(MesMdItemSaveReqVO createReqVO) {
        // 校验数据
        validateItemSaveData(createReqVO);
        // 如果指定了 ID,检查该 ID 是否已存在(用于 MDM 同步时保持 ID 一致)
        if (createReqVO.getId() != null) {
            MesMdItemDO existItem = itemMapper.selectById(createReqVO.getId());
            if (existItem != null) {
                // ID 已存在,转为更新
                updateItem(createReqVO);
                return createReqVO.getId();
            }
        }
        // 插入
        MesMdItemDO item = BeanUtils.toBean(createReqVO, MesMdItemDO.class);
        item.setStatus(CommonStatusEnum.DISABLE.getStatus()); // 默认禁用。信息完成后,再启用
        // 状态只能由基础数据(MDM)控制:MDM 同步时沿用其状态;MES 本地新增默认禁用
        item.setStatus(createReqVO.getMdmItemId() != null && createReqVO.getStatus() != null
                ? createReqVO.getStatus() : CommonStatusEnum.DISABLE.getStatus());
        clearStockIfNotSafe(item); // 如果未启用安全库存,清零库存上下限
        itemMapper.insert(item);
        // 自动生成条码
        barcodeService.autoGenerateBarcode(BarcodeBizTypeEnum.ITEM.getValue(),
                item.getId(), item.getCode(), item.getName());
        // 同步到 MDM(勾选 syncMdm 时)
        if (Boolean.TRUE.equals(createReqVO.getSyncMdm())) {
            syncItemToMdm(item, createReqVO);
        }
        return item.getId();
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateItem(MesMdItemSaveReqVO updateReqVO) {
        // 校验存在
        validateItemExists(updateReqVO.getId());
@@ -100,7 +113,17 @@
        // 更新
        MesMdItemDO updateObj = BeanUtils.toBean(updateReqVO, MesMdItemDO.class);
        clearStockIfNotSafe(updateObj); // 如果未启用安全库存,清零库存上下限
        // 状态只能由基础数据(MDM)控制,MES 本地更新不允许修改状态
        if (updateReqVO.getMdmItemId() == null) {
            updateObj.setStatus(null);
        }
        itemMapper.updateById(updateObj);
        // 同步到 MDM(勾选 syncMdm 时)
        if (Boolean.TRUE.equals(updateReqVO.getSyncMdm())) {
            MesMdItemDO item = itemMapper.selectById(updateReqVO.getId());
            syncItemToMdm(item, updateReqVO);
        }
    }
    private void validateItemSaveData(MesMdItemSaveReqVO reqVO) {
@@ -156,14 +179,20 @@
    @Override
    public MesMdItemDO validateItemExists(Long id) {
        MdmItemRespDTO item = mdmItemApi.validateItemExists(id).getCheckedData();
        return BeanUtils.toBean(item, MesMdItemDO.class);
        MesMdItemDO item = itemMapper.selectById(id);
        if (item == null) {
            throw exception(MD_ITEM_NOT_EXISTS);
        }
        return item;
    }
    @Override
    public MesMdItemDO validateItemExistsAndEnable(Long id) {
        MdmItemRespDTO item = mdmItemApi.validateItemExistsAndEnable(id).getCheckedData();
        return BeanUtils.toBean(item, MesMdItemDO.class);
        MesMdItemDO item = validateItemExists(id);
        if (ObjUtil.notEqual(CommonStatusEnum.ENABLE.getStatus(), item.getStatus())) {
            throw exception(MD_ITEM_IS_DISABLE);
        }
        return item;
    }
    private void validateItemCodeUnique(Long id, String code) {
@@ -189,11 +218,6 @@
    private void validateItemTypeExists(Long itemTypeId) {
        if (itemTypeService.getItemType(itemTypeId) == null) {
            throw exception(MD_ITEM_TYPE_NOT_EXISTS);
        }
        // 校验必须是叶子分类(没有子分类)
        List<MesMdItemTypeDO> children = itemTypeService.getItemTypeChildrenList(itemTypeId);
        if (CollUtil.isNotEmpty(children)) {
            throw exception(MD_ITEM_TYPE_NOT_LEAF);
        }
    }
@@ -242,18 +266,26 @@
    @Override
    public MesMdItemDO getItem(Long id) {
        // 从 MDM 获取物料主数据
        MdmItemRespDTO item = mdmItemApi.getItem(id).getCheckedData();
        return BeanUtils.toBean(item, MesMdItemDO.class);
        return itemMapper.selectById(id);
    }
    @Override
    public MesMdItemDO getItemByCode(String code) {
        return itemMapper.selectByCode(code);
    }
    @Override
    public PageResult<MesMdItemDO> getItemPage(MesMdItemPageReqVO pageReqVO) {
        // 调用 MDM API 分页查询(MES 物料对应 MDM 物料类型=物料)
        cn.iocoder.yudao.framework.common.pojo.PageResult<MdmItemRespDTO> mdmPageResult =
                mdmItemApi.getItemPage(1, pageReqVO.getStatus(), pageReqVO.getPageNo(), pageReqVO.getPageSize()).getCheckedData();
        List<MesMdItemDO> itemList = BeanUtils.toBean(mdmPageResult.getList(), MesMdItemDO.class);
        return new PageResult<>(itemList, mdmPageResult.getTotal());
        // 查找时,如果查找某个分类编号,则包含它的子分类
        Set<Long> itemTypeIds = null;
        if (pageReqVO.getItemTypeId() != null) {
            itemTypeIds = new HashSet<>();
            itemTypeIds.add(pageReqVO.getItemTypeId());
            List<MesMdItemTypeDO> children = itemTypeService.getItemTypeChildrenList(pageReqVO.getItemTypeId());
            itemTypeIds.addAll(convertSet(children, MesMdItemTypeDO::getId));
        }
        // 分页查询
        return itemMapper.selectPage(pageReqVO, itemTypeIds);
    }
    @Override
@@ -261,107 +293,124 @@
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        List<MdmItemRespDTO> itemList = mdmItemApi.getItemList(ids).getCheckedData();
        return BeanUtils.toBean(itemList, MesMdItemDO.class);
        return itemMapper.selectByIds(ids);
    }
    @Override
    public Long getItemCountByItemTypeId(Long itemTypeId) {
        // MES 分类需要映射到 MDM 分类
        return mdmItemApi.getItemCountByCategoryId(itemTypeId).getCheckedData();
        return itemMapper.selectCountByItemTypeId(itemTypeId);
    }
    @Override
    public Long getItemCountByUnitMeasureId(Long unitMeasureId) {
        return mdmItemApi.getItemCountByUnitMeasureId(unitMeasureId).getCheckedData();
        return itemMapper.selectCountByUnitMeasureId(unitMeasureId);
    }
    /**
     * 同步物料到 MDM
     *
     * @param item     MES 物料数据
     * @param saveReqVO 保存请求(包含批次配置)
     */
    private void syncItemToMdm(MesMdItemDO item, MesMdItemSaveReqVO saveReqVO) {
        // 1. 获取物料类型对应的 itemType
        Long itemType = null;
        if (item.getItemTypeId() != null) {
            MesMdItemTypeDO itemTypeDO = itemTypeService.getItemType(item.getItemTypeId());
            if (itemTypeDO != null) {
                // MES: ITEM=物料, PRODUCT=产品 -> MDM: 1=原料, 2=半成品, 3=成品, 4=辅料
//                itemType = convertMesItemTypeToMdm(itemTypeDO.getItemOrProduct(), itemTypeDO.getName());
                itemType = itemTypeDO.getId();
            }
        }
        // 2. 获取计量单位编码
        String unitCode = null;
        Long unitMeasureId = null;
        if (item.getUnitMeasureId() != null) {
            MesMdUnitMeasureDO unitMeasure = unitMeasureService.getUnitMeasure(item.getUnitMeasureId());
            if (unitMeasure != null) {
                unitCode = unitMeasure.getCode();
            }
            unitMeasureId = item.getUnitMeasureId();
        }
        // 3. 获取批次配置
        MesMdItemBatchConfigDO batchConfig = itemBatchConfigService.getItemBatchConfigByItemId(item.getId());
        // 4. 组装同步 DTO
        MdmItemSyncReqDTO syncReqDTO = new MdmItemSyncReqDTO();
        syncReqDTO.setCode(item.getCode());
        syncReqDTO.setName(item.getName());
        syncReqDTO.setSpecification(item.getSpecification());
        syncReqDTO.setItemType(itemType);
        syncReqDTO.setUnitMeasureId(unitMeasureId);
        syncReqDTO.setCategoryId(item.getItemTypeId());
        syncReqDTO.setStatus(item.getStatus());
        syncReqDTO.setMinStock(item.getMinStock());
        syncReqDTO.setMaxStock(item.getMaxStock());
        syncReqDTO.setIsBatchManaged(item.getBatchFlag());
        syncReqDTO.setIsHighValue(item.getHighValue());
        syncReqDTO.setRemark(item.getRemark());
        // 5. 同步批次配置
        if (batchConfig != null) {
            syncReqDTO.setProduceDateFlag(batchConfig.getProduceDateFlag());
            syncReqDTO.setExpireDateFlag(batchConfig.getExpireDateFlag());
            syncReqDTO.setReceiptDateFlag(batchConfig.getReceiptDateFlag());
            syncReqDTO.setVendorFlag(batchConfig.getVendorFlag());
            syncReqDTO.setPurchaseOrderCodeFlag(batchConfig.getPurchaseOrderCodeFlag());
            syncReqDTO.setLotNumberFlag(batchConfig.getLotNumberFlag());
            syncReqDTO.setQualityStatusFlag(batchConfig.getQualityStatusFlag());
        }
        // 6. 调用 MDM 同步 API
        mdmItemSyncApi.syncItemToMdm(syncReqDTO);
    }
    /**
     * 将 MES 物料类型转换为 MDM 物料类型
     * MES: ITEM(物料)/PRODUCT(产品)
     * MDM: 1原料, 2半成品, 3成品, 4辅料
     */
    private Integer convertMesItemTypeToMdm(String itemOrProduct, String typeName) {
        if (itemOrProduct == null) {
            return null;
        }
        // 根据名称匹配
        if (typeName != null) {
            if (typeName.contains("原料")) {
                return 1;
            } else if (typeName.contains("半成品")) {
                return 2;
            } else if (typeName.contains("成品")) {
                return 3;
            } else if (typeName.contains("辅料")) {
                return 4;
            }
        }
        // 根据 ITEM/PRODUCT 兜底
        if ("PRODUCT".equals(itemOrProduct)) {
            return 3; // 产品 -> 成品
        }
        return 1; // 默认原料
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesMdItemImportRespVO importItemList(List<MesMdItemImportExcelVO> importItems, boolean updateSupport) {
        // 1. 参数校验
        if (CollUtil.isEmpty(importItems)) {
            throw exception(MD_ITEM_IMPORT_LIST_IS_EMPTY);
    public MesMdItemDO getItemByMdmItemId(Long mdmItemId) {
        if (mdmItemId == null) {
            return null;
        }
        // 2. 遍历,逐个创建 or 更新
        MesMdItemImportRespVO respVO = MesMdItemImportRespVO.builder()
                .createCodes(new ArrayList<>()).updateCodes(new ArrayList<>())
                .failureCodes(new LinkedHashMap<>()).build();
        AtomicInteger index = new AtomicInteger(1);
        importItems.forEach(importItem -> {
            int currentIndex = index.getAndIncrement();
            // 2.1 校验字段
            if (StrUtil.isBlank(importItem.getCode())) {
                // 空编码时自动生成
                importItem.setCode(autoCodeRecordService.generateAutoCode(MesMdAutoCodeRuleCodeEnum.MD_ITEM_CODE.getCode()));
            }
            String key = importItem.getCode();
            if (StrUtil.isBlank(importItem.getName())) {
                respVO.getFailureCodes().put(key, "物料名称不能为空");
                return;
            }
            if (StrUtil.isBlank(importItem.getUnitMeasureCode())) {
                respVO.getFailureCodes().put(key, "单位编码不能为空");
                return;
            }
            // 将单位编码转换为单位 ID
            MesMdUnitMeasureDO unitMeasure = unitMeasureService.getUnitMeasureByCode(importItem.getUnitMeasureCode());
            if (unitMeasure == null) {
                respVO.getFailureCodes().put(key, "单位编码[" + importItem.getUnitMeasureCode() + "]不存在");
                return;
            }
            if (importItem.getItemTypeId() == null) {
                respVO.getFailureCodes().put(key, "物料分类编号不能为空");
                return;
            }
            // 2.2 校验分类是否存在
            try {
                validateItemTypeExists(importItem.getItemTypeId());
            } catch (ServiceException ex) {
                respVO.getFailureCodes().put(key, ex.getMessage());
                return;
            }
            // 2.3 判断:创建 or 更新
            MesMdItemDO existItem = itemMapper.selectByCode(importItem.getCode());
            if (existItem == null) {
                // 2.3.1 创建
                try {
                    validateItemNameUnique(null, importItem.getName());
                } catch (ServiceException ex) {
                    respVO.getFailureCodes().put(key, ex.getMessage());
                    return;
                }
                MesMdItemDO item = BeanUtils.toBean(importItem, MesMdItemDO.class);
                item.setUnitMeasureId(unitMeasure.getId());
                item.setStatus(CommonStatusEnum.DISABLE.getStatus()); // 默认禁用。信息完成后,再启用
                clearStockIfNotSafe(item);
                itemMapper.insert(item);
                // 自动生成条码
                barcodeService.autoGenerateBarcode(BarcodeBizTypeEnum.ITEM.getValue(),
                        item.getId(), item.getCode(), item.getName());
                respVO.getCreateCodes().add(importItem.getCode());
            } else if (updateSupport) {
                // 2.3.2 更新
                try {
                    validateItemNameUnique(existItem.getId(), importItem.getName());
                } catch (ServiceException ex) {
                    respVO.getFailureCodes().put(key, ex.getMessage());
                    return;
                }
                MesMdItemDO updateObj = BeanUtils.toBean(importItem, MesMdItemDO.class);
                updateObj.setId(existItem.getId());
                updateObj.setUnitMeasureId(unitMeasure.getId());
                clearStockIfNotSafe(updateObj);
                itemMapper.updateById(updateObj);
                respVO.getUpdateCodes().add(importItem.getCode());
            } else {
                // 不支持更新
                respVO.getFailureCodes().put(key, "物料编码已存在");
            }
        });
        return respVO;
        return itemMapper.selectByMdmItemId(mdmItemId);
    }
}
    @Override
    public MesMdItemDO validateItemExistsByMdmItemId(Long mdmItemId) {
        MesMdItemDO item = getItemByMdmItemId(mdmItemId);
        if (item == null) {
            throw exception(MD_ITEM_NOT_EXISTS);
        }
        return item;
    }
}