| | |
| | | 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.dal.dataobject.item.MdmItemBatchConfigDO; |
| | | import cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemDO; |
| | | import cn.iocoder.yudao.module.mdm.dal.dataobject.unit.MdmUnitMeasureDO; |
| | | import cn.iocoder.yudao.module.mdm.dal.mysql.category.MdmItemCategoryMapper; |
| | | 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.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 实现类 |
| | |
| | | private MdmItemBatchConfigMapper itemBatchConfigMapper; |
| | | |
| | | @Resource |
| | | private MdmItemCategoryMapper itemCategoryMapper; |
| | | |
| | | @Resource |
| | | private MdmUnitMeasureMapper unitMeasureMapper; |
| | | |
| | | @Resource |
| | | private MesMdItemTypeApi mesMdItemTypeApi; |
| | | |
| | | @Resource |
| | | private PlatformTransactionManager transactionManager; |
| | | |
| | | |
| | | @Override |
| | |
| | | * |
| | | * MDM 的 unitMeasureId/categoryId 与 MES 的 unitMeasureId/itemTypeId 分属两套主数据, |
| | | * 这里先在 MDM 侧解析出 code,再交由 MesMdItemApi 在 MES 侧解析为对应 id。 |
| | | * 同步失败将抛出异常,触发外层 @Transactional 整体回滚(MDM 操作一并回滚)。 |
| | | * 同步失败将抛出异常,回滚本行事务(本行的 MDM 操作一并回滚,不影响其它行)。 |
| | | */ |
| | | private void syncItemToMes(MdmItemSaveReqVO reqVO) { |
| | | // 解析计量单位 id -> code |
| | |
| | | } |
| | | |
| | | @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) { |
| | | if (CollUtil.isEmpty(importList)) { |
| | | throw exception(MDM_ITEM_IMPORT_LIST_IS_EMPTY); |
| | |
| | | .createList(new ArrayList<>()).updateList(new ArrayList<>()) |
| | | .failureList(new LinkedHashMap<>()).build(); |
| | | |
| | | // 1. 预热:分类编码 -> ID、单位编码 -> ID |
| | | // 1. 预热:分类编码 -> ID(MES 物料分类,与物料表单下拉数据源一致)、单位编码 -> ID |
| | | Map<String, Long> categoryCodeIdMap = new HashMap<>(); |
| | | for (MdmItemCategoryDO category : itemCategoryMapper.selectList()) { |
| | | if (category.getCode() != null) { |
| | | categoryCodeIdMap.put(category.getCode(), category.getId()); |
| | | 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<>(); |
| | |
| | | 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())) { |
| | | respVO.getFailureList().put(StrUtil.blankToDefault(importVO.getName(), importVO.getCode()), "物料编码和物料名称不能为空"); |
| | | respVO.getFailureList().put(buildFailKey(importVO), "物料编码和物料名称不能为空"); |
| | | continue; |
| | | } |
| | | // 2.2 解析分类 |
| | |
| | | if (StrUtil.isNotBlank(importVO.getCategoryCode())) { |
| | | categoryId = categoryCodeIdMap.get(importVO.getCategoryCode()); |
| | | if (categoryId == null) { |
| | | respVO.getFailureList().put(importVO.getName(), "物料分类编码不存在:" + importVO.getCategoryCode()); |
| | | respVO.getFailureList().put(buildFailKey(importVO), "物料分类编码不存在:" + importVO.getCategoryCode()); |
| | | continue; |
| | | } |
| | | } |
| | |
| | | if (StrUtil.isNotBlank(importVO.getUnitMeasureCode())) { |
| | | unitMeasureId = unitCodeIdMap.get(importVO.getUnitMeasureCode()); |
| | | if (unitMeasureId == null) { |
| | | respVO.getFailureList().put(importVO.getName(), "计量单位编码不存在:" + importVO.getUnitMeasureCode()); |
| | | respVO.getFailureList().put(buildFailKey(importVO), "计量单位编码不存在:" + importVO.getUnitMeasureCode()); |
| | | continue; |
| | | } |
| | | } |
| | |
| | | if (StrUtil.isNotBlank(importVO.getUnitMeasureCode2())) { |
| | | unitMeasureId2 = unitCodeIdMap.get(importVO.getUnitMeasureCode2()); |
| | | if (unitMeasureId2 == null) { |
| | | respVO.getFailureList().put(importVO.getName(), "辅单位1编码不存在:" + importVO.getUnitMeasureCode2()); |
| | | respVO.getFailureList().put(buildFailKey(importVO), "辅单位1编码不存在:" + importVO.getUnitMeasureCode2()); |
| | | continue; |
| | | } |
| | | } |
| | |
| | | if (StrUtil.isNotBlank(importVO.getUnitMeasureCode3())) { |
| | | unitMeasureId3 = unitCodeIdMap.get(importVO.getUnitMeasureCode3()); |
| | | if (unitMeasureId3 == null) { |
| | | respVO.getFailureList().put(importVO.getName(), "辅单位2编码不存在:" + importVO.getUnitMeasureCode3()); |
| | | respVO.getFailureList().put(buildFailKey(importVO), "辅单位2编码不存在:" + importVO.getUnitMeasureCode3()); |
| | | continue; |
| | | } |
| | | } |
| | |
| | | |
| | | // 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(importVO.getName(), "物料编码已存在"); |
| | | 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(importVO.getName(), 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 文案引用 |
| | | 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(), "(名称和编码为空的行)")); |
| | | } |
| | | |
| | | } |