云
17 小时以前 31168ec141483a6d74bb0413d01c6aaecbc5fb9d
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -2,10 +2,15 @@
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;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.operatelog.core.util.OperateLogUtils;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportExcelVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemImportRespVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemPageReqVO;
import cn.iocoder.yudao.module.mdm.controller.admin.item.vo.MdmItemSaveReqVO;
import cn.iocoder.yudao.module.mdm.dal.dataobject.category.MdmItemCategoryDO;
@@ -14,6 +19,7 @@
import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemBatchConfigMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.item.MdmItemMapper;
import cn.iocoder.yudao.module.mdm.dal.mysql.unit.MdmUnitMeasureMapper;
import cn.iocoder.yudao.module.mdm.service.brand.MdmBrandService;
import cn.iocoder.yudao.module.mdm.service.category.MdmItemCategoryService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
@@ -22,19 +28,24 @@
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemTypeRespDTO;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.mzt.logapi.starter.annotation.LogRecord;
import jakarta.annotation.Resource;
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;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mdm.enums.ErrorCodeConstants.*;
import static cn.iocoder.yudao.module.mdm.enums.LogRecordConstants.*;
/**
 * MDM 物料主数据 Service 实现类
@@ -62,7 +73,13 @@
    private MdmItemBatchConfigMapper itemBatchConfigMapper;
    @Resource
    private MdmUnitMeasureMapper unitMeasureMapper;
    @Resource
    private MesMdItemTypeApi mesMdItemTypeApi;
    @Resource
    private PlatformTransactionManager transactionManager;
    @Override
@@ -119,7 +136,7 @@
     *
     * MDM 的 unitMeasureId/categoryId 与 MES 的 unitMeasureId/itemTypeId 分属两套主数据,
     * 这里先在 MDM 侧解析出 code,再交由 MesMdItemApi 在 MES 侧解析为对应 id。
     * 同步失败将抛出异常,触发外层 @Transactional 整体回滚(MDM 操作一并回滚)。
     * 同步失败将抛出异常,回滚本行事务(本行的 MDM 操作一并回滚,不影响其它行)。
     */
    private void syncItemToMes(MdmItemSaveReqVO reqVO) {
        // 解析计量单位 id -> code
@@ -422,4 +439,152 @@
        }
    }
    @Override
    @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) {
        if (CollUtil.isEmpty(importList)) {
            throw exception(MDM_ITEM_IMPORT_LIST_IS_EMPTY);
        }
        MdmItemImportRespVO respVO = MdmItemImportRespVO.builder()
                .createList(new ArrayList<>()).updateList(new ArrayList<>())
                .failureList(new LinkedHashMap<>()).build();
        // 1. 预热:分类编码 -> ID(MES 物料分类,与物料表单下拉数据源一致)、单位编码 -> ID
        Map<String, Long> categoryCodeIdMap = new HashMap<>();
        for (String categoryCode : importList.stream()
                .map(MdmItemImportExcelVO::getCategoryCode).filter(StrUtil::isNotBlank).distinct().toList()) {
            CommonResult<MesMdItemTypeRespDTO> categoryResult = mesMdItemTypeApi.getItemTypeByCode(categoryCode);
            if (categoryResult != null && categoryResult.getData() != null) {
                categoryCodeIdMap.put(categoryCode, categoryResult.getData().getId());
            }
        }
        Map<String, Long> unitCodeIdMap = new HashMap<>();
        for (MdmUnitMeasureDO unit : unitMeasureMapper.selectList()) {
            unitCodeIdMap.put(unit.getCode(), unit.getId());
        }
        // 2. 遍历导入(每行一个独立事务:单行失败只回滚该行,不影响其余行)
        TransactionTemplate importTxTemplate = new TransactionTemplate(transactionManager);
        for (MdmItemImportExcelVO importVO : importList) {
            // 2.1 基础校验
            if (StrUtil.isBlank(importVO.getCode()) || StrUtil.isBlank(importVO.getName())) {
                respVO.getFailureList().put(buildFailKey(importVO), "物料编码和物料名称不能为空");
                continue;
            }
            // 2.2 解析分类
            Long categoryId = null;
            if (StrUtil.isNotBlank(importVO.getCategoryCode())) {
                categoryId = categoryCodeIdMap.get(importVO.getCategoryCode());
                if (categoryId == null) {
                    respVO.getFailureList().put(buildFailKey(importVO), "物料分类编码不存在:" + importVO.getCategoryCode());
                    continue;
                }
            }
            // 2.3 解析主单位
            Long unitMeasureId = null;
            if (StrUtil.isNotBlank(importVO.getUnitMeasureCode())) {
                unitMeasureId = unitCodeIdMap.get(importVO.getUnitMeasureCode());
                if (unitMeasureId == null) {
                    respVO.getFailureList().put(buildFailKey(importVO), "计量单位编码不存在:" + importVO.getUnitMeasureCode());
                    continue;
                }
            }
            // 2.4 解析辅单位1
            Long unitMeasureId2 = null;
            if (StrUtil.isNotBlank(importVO.getUnitMeasureCode2())) {
                unitMeasureId2 = unitCodeIdMap.get(importVO.getUnitMeasureCode2());
                if (unitMeasureId2 == null) {
                    respVO.getFailureList().put(buildFailKey(importVO), "辅单位1编码不存在:" + importVO.getUnitMeasureCode2());
                    continue;
                }
            }
            // 2.5 解析辅单位2
            Long unitMeasureId3 = null;
            if (StrUtil.isNotBlank(importVO.getUnitMeasureCode3())) {
                unitMeasureId3 = unitCodeIdMap.get(importVO.getUnitMeasureCode3());
                if (unitMeasureId3 == null) {
                    respVO.getFailureList().put(buildFailKey(importVO), "辅单位2编码不存在:" + importVO.getUnitMeasureCode3());
                    continue;
                }
            }
            // 2.6 组装 SaveReqVO(复用 create/update 逻辑)
            MdmItemSaveReqVO saveReqVO = new MdmItemSaveReqVO();
            saveReqVO.setCode(importVO.getCode());
            saveReqVO.setName(importVO.getName());
            saveReqVO.setBarCode(importVO.getBarCode());
            saveReqVO.setSpecification(importVO.getSpecification());
            saveReqVO.setCategoryId(categoryId);
            saveReqVO.setUnitMeasureId(unitMeasureId);
            saveReqVO.setUnitMeasureId2(unitMeasureId2);
            saveReqVO.setUnitMeasureRate1(importVO.getUnitMeasureRate1());
            saveReqVO.setUnitMeasureId3(unitMeasureId3);
            saveReqVO.setUnitMeasureRate2(importVO.getUnitMeasureRate2());
            saveReqVO.setPurchasePrice(importVO.getPurchasePrice());
            saveReqVO.setSalesPrice(importVO.getSalesPrice());
            saveReqVO.setCostPrice(importVO.getCostPrice());
            saveReqVO.setSafetyStock(importVO.getSafetyStock());
            saveReqVO.setMinStock(importVO.getMinStock());
            saveReqVO.setMaxStock(importVO.getMaxStock());
            saveReqVO.setIsBatchManaged(importVO.getIsBatchManaged());
            saveReqVO.setExpiryDay(importVO.getExpiryDay());
            saveReqVO.setStatus(importVO.getStatus() != null ? importVO.getStatus() : cn.iocoder.yudao.framework.common.enums.CommonStatusEnum.ENABLE.getStatus());
            saveReqVO.setSyncMes(importVO.getSyncMes() != null ? importVO.getSyncMes() : false);
            saveReqVO.setRemark(importVO.getRemark());
            saveReqVO.setItemType(1); // 默认物料类型:物料
            // 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 {
                importTxTemplate.executeWithoutResult(status -> {
                    try {
                        if (isUpdate) {
                            updateItem(saveReqVO);
                        } else {
                            createItem(saveReqVO);
                        }
                    } catch (Throwable t) {
                        rowError.set(t);
                        throw t;
                    }
                });
                // 只有本行事务提交成功才计入成功清单
                (isUpdate ? respVO.getUpdateList() : respVO.getCreateList()).add(importVO.getName());
            } catch (Exception ex) {
                // 单条失败,记录并继续
                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 文案引用
        OperateLogUtils.putImportSummary(respVO.getCreateList(), respVO.getUpdateList(), respVO.getFailureList());
        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(), "(名称和编码为空的行)"));
    }
}