gaoluyang
20 小时以前 3fa88d03676ec8b62c80fc50636d2c8e973c0470
fix(mdm): 物料管理
导入失败展示修改及同步生产物料问题
已修改3个文件
94 ■■■■■ 文件已修改
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 1 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -3,6 +3,7 @@
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.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
@@ -32,10 +33,13 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.validation.annotation.Validated;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -73,6 +77,9 @@
    @Resource
    private MesMdItemTypeApi mesMdItemTypeApi;
    @Resource
    private PlatformTransactionManager transactionManager;
    @Override
@@ -129,7 +136,7 @@
     *
     * MDM 的 unitMeasureId/categoryId 与 MES 的 unitMeasureId/itemTypeId 分属两套主数据,
     * 这里先在 MDM 侧解析出 code,再交由 MesMdItemApi 在 MES 侧解析为对应 id。
     * 同步失败将抛出异常,触发外层 @Transactional 整体回滚(MDM 操作一并回滚)。
     * 同步失败将抛出异常,回滚本行事务(本行的 MDM 操作一并回滚,不影响其它行)。
     */
    private void syncItemToMes(MdmItemSaveReqVO reqVO) {
        // 解析计量单位 id -> code
@@ -433,7 +440,6 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    @LogRecord(type = MDM_ITEM_TYPE, subType = MDM_ITEM_IMPORT_SUB_TYPE, bizNo = "0",
            success = MDM_ITEM_IMPORT_SUCCESS)
    public MdmItemImportRespVO importItemList(List<MdmItemImportExcelVO> importList, Boolean isUpdateSupport) {
@@ -458,13 +464,12 @@
            unitCodeIdMap.put(unit.getCode(), unit.getId());
        }
        // 2. 遍历导入
        // 2. 遍历导入(每行一个独立事务:单行失败只回滚该行,不影响其余行)
        TransactionTemplate importTxTemplate = new TransactionTemplate(transactionManager);
        for (MdmItemImportExcelVO importVO : importList) {
            // 2.1 基础校验
            if (StrUtil.isBlank(importVO.getCode()) || StrUtil.isBlank(importVO.getName())) {
                String failKey = StrUtil.isNotBlank(importVO.getName()) ? importVO.getName()
                        : (StrUtil.isNotBlank(importVO.getCode()) ? importVO.getCode() : "(名称和编码为空的行)");
                respVO.getFailureList().put(failKey, "物料编码和物料名称不能为空");
                respVO.getFailureList().put(buildFailKey(importVO), "物料编码和物料名称不能为空");
                continue;
            }
            // 2.2 解析分类
@@ -472,7 +477,7 @@
            if (StrUtil.isNotBlank(importVO.getCategoryCode())) {
                categoryId = categoryCodeIdMap.get(importVO.getCategoryCode());
                if (categoryId == null) {
                    respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), "物料分类编码不存在:" + importVO.getCategoryCode());
                    respVO.getFailureList().put(buildFailKey(importVO), "物料分类编码不存在:" + importVO.getCategoryCode());
                    continue;
                }
            }
@@ -481,7 +486,7 @@
            if (StrUtil.isNotBlank(importVO.getUnitMeasureCode())) {
                unitMeasureId = unitCodeIdMap.get(importVO.getUnitMeasureCode());
                if (unitMeasureId == null) {
                    respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), "计量单位编码不存在:" + importVO.getUnitMeasureCode());
                    respVO.getFailureList().put(buildFailKey(importVO), "计量单位编码不存在:" + importVO.getUnitMeasureCode());
                    continue;
                }
            }
@@ -490,7 +495,7 @@
            if (StrUtil.isNotBlank(importVO.getUnitMeasureCode2())) {
                unitMeasureId2 = unitCodeIdMap.get(importVO.getUnitMeasureCode2());
                if (unitMeasureId2 == null) {
                    respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), "辅单位1编码不存在:" + importVO.getUnitMeasureCode2());
                    respVO.getFailureList().put(buildFailKey(importVO), "辅单位1编码不存在:" + importVO.getUnitMeasureCode2());
                    continue;
                }
            }
@@ -499,7 +504,7 @@
            if (StrUtil.isNotBlank(importVO.getUnitMeasureCode3())) {
                unitMeasureId3 = unitCodeIdMap.get(importVO.getUnitMeasureCode3());
                if (unitMeasureId3 == null) {
                    respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), "辅单位2编码不存在:" + importVO.getUnitMeasureCode3());
                    respVO.getFailureList().put(buildFailKey(importVO), "辅单位2编码不存在:" + importVO.getUnitMeasureCode3());
                    continue;
                }
            }
@@ -531,25 +536,37 @@
            // 2.7 已存在(按编码判断)
            MdmItemDO existByCode = itemMapper.selectByCode(importVO.getCode());
            if (existByCode != null && !Boolean.TRUE.equals(isUpdateSupport)) {
                respVO.getFailureList().put(buildFailKey(importVO), "物料编码已存在");
                continue;
            }
            if (existByCode != null) {
                saveReqVO.setId(existByCode.getId());
            }
            boolean isUpdate = existByCode != null;
            // 事务内先留住原始异常:同步到 MES 失败时会把事务标记为 rollback-only,
            // 提交阶段抛出的只是笼统的 UnexpectedRollbackException,真实原因(如"物料名称已存在")会丢
            AtomicReference<Throwable> rowError = new AtomicReference<>();
            try {
                if (existByCode != null) {
                    if (!Boolean.TRUE.equals(isUpdateSupport)) {
                        respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), "物料编码已存在");
                        continue;
                importTxTemplate.executeWithoutResult(status -> {
                    try {
                        if (isUpdate) {
                            updateItem(saveReqVO);
                        } else {
                            createItem(saveReqVO);
                        }
                    } catch (Throwable t) {
                        rowError.set(t);
                        throw t;
                    }
                    saveReqVO.setId(existByCode.getId());
                    updateItem(saveReqVO);
                    respVO.getUpdateList().add(importVO.getName());
                } else {
                    createItem(saveReqVO);
                    respVO.getCreateList().add(importVO.getName());
                }
                });
                // 只有本行事务提交成功才计入成功清单
                (isUpdate ? respVO.getUpdateList() : respVO.getCreateList()).add(importVO.getName());
            } catch (Exception ex) {
                // 单条失败,记录并继续
                String reason = ex instanceof cn.iocoder.yudao.framework.common.exception.ServiceException
                        ? ((cn.iocoder.yudao.framework.common.exception.ServiceException) ex).getMessage()
                        : ex.getMessage();
                respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), reason == null ? "导入失败" : reason);
                Throwable cause = rowError.get() != null ? rowError.get() : ex;
                String reason = cause instanceof ServiceException ? ((ServiceException) cause).getMessage() : cause.getMessage();
                respVO.getFailureList().put(buildFailKey(importVO), reason == null ? "导入失败" : reason);
            }
        }
        // 记录导入结果汇总,供 @LogRecord 的 success 文案引用
@@ -557,4 +574,17 @@
        return respVO;
    }
    /**
     * 失败清单的 key:用「编码 名称」,而不是单用名称。
     * 物料允许同名不同编码后,只按名称做 key 会让同名行互相覆盖、少报失败行;
     * 拼接后既能唯一标识、也能一眼看出是哪个物料。
     */
    private static String buildFailKey(MdmItemImportExcelVO importVO) {
        if (StrUtil.isNotBlank(importVO.getCode()) && StrUtil.isNotBlank(importVO.getName())) {
            return importVO.getCode() + " " + importVO.getName();
        }
        return StrUtil.blankToDefault(importVO.getCode(),
                StrUtil.blankToDefault(importVO.getName(), "(名称和编码为空的行)"));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -32,6 +32,7 @@
    // ========== MES 基础数据-物料(1-040-102-000) ==========
    ErrorCode MD_ITEM_NOT_EXISTS = new ErrorCode(1_040_102_000, "物料不存在");
    ErrorCode MD_ITEM_CODE_DUPLICATE = new ErrorCode(1_040_102_001, "物料编码已存在");
    // 1_040_102_002:物料名称唯一校验已取消(允许同名不同编码,与 MDM 物料对齐),此码仅保留占位,请勿再用于校验
    ErrorCode MD_ITEM_NAME_DUPLICATE = new ErrorCode(1_040_102_002, "物料名称已存在");
    ErrorCode MD_ITEM_BATCH_REQUIRED = new ErrorCode(1_040_102_004, "当前物料启用了批次管理,请选择批次");
    ErrorCode MD_ITEM_IS_DISABLE = new ErrorCode(1_040_102_005, "物料已禁用");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/item/MesMdItemServiceImpl.java
@@ -135,8 +135,7 @@
    private void validateItemSaveData(MesMdItemSaveReqVO reqVO) {
        // 校验物料编码的唯一性
        validateItemCodeUnique(reqVO.getId(), reqVO.getCode());
        // 校验物料名称的唯一性
        validateItemNameUnique(reqVO.getId(), reqVO.getName());
        // 不再校验物料名称的唯一性:允许同名不同编码的物料(与 MDM 物料规则保持一致)
        // 校验物料分类存在
        validateItemTypeExists(reqVO.getItemTypeId());
        // 校验计量单位存在
@@ -208,16 +207,6 @@
        }
        if (ObjUtil.notEqual(item.getId(), id)) {
            throw exception(MD_ITEM_CODE_DUPLICATE);
        }
    }
    private void validateItemNameUnique(Long id, String name) {
        MesMdItemDO item = itemMapper.selectByName(name);
        if (item == null) {
            return;
        }
        if (ObjUtil.notEqual(item.getId(), id)) {
            throw exception(MD_ITEM_NAME_DUPLICATE);
        }
    }