| | |
| | | |
| | | 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; |
| | | 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.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; |
| | |
| | | import cn.iocoder.yudao.module.mes.api.item.MesMdItemTypeApi; |
| | | 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 实现类 |
| | | * |
| | | * @author 芋道源码 |
| | | * @author 超级管理员 |
| | | */ |
| | | @Service |
| | | @Validated |
| | |
| | | private MdmBrandService brandService; |
| | | |
| | | @Resource |
| | | @Lazy |
| | | private MesMdItemApi mesMdItemApi; |
| | | @Resource |
| | | private MdmItemBatchConfigMapper itemBatchConfigMapper; |
| | | |
| | | @Resource |
| | | private MdmUnitMeasureMapper unitMeasureMapper; |
| | | |
| | | @Resource |
| | | private MesMdItemTypeApi mesMdItemTypeApi; |
| | | |
| | | @Resource |
| | | private PlatformTransactionManager transactionManager; |
| | | |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | MdmItemDO item = BeanUtils.toBean(createReqVO, MdmItemDO.class); |
| | | itemMapper.insert(item); |
| | | |
| | | // 同步到 MES(勾选 syncMes 时) |
| | | // 保存批次配置(如果有) |
| | | saveBatchConfig(item.getId(), createReqVO); |
| | | |
| | | // 勾选同步时,同步到 MES |
| | | if (Boolean.TRUE.equals(createReqVO.getSyncMes())) { |
| | | createReqVO.setId(item.getId()); |
| | | syncItemToMes(createReqVO); |
| | | } |
| | | return item.getId(); |
| | |
| | | MdmItemDO updateObj = BeanUtils.toBean(updateReqVO, MdmItemDO.class); |
| | | itemMapper.updateById(updateObj); |
| | | |
| | | // 同步到 MES(勾选 syncMes 时) |
| | | // 保存批次配置(如果有) |
| | | saveBatchConfig(updateReqVO.getId(), updateReqVO); |
| | | |
| | | // 根据同步开关决定:打开则同步到 MES,关闭则取消同步(从 MES 删除) |
| | | if (Boolean.TRUE.equals(updateReqVO.getSyncMes())) { |
| | | syncItemToMes(updateReqVO); |
| | | } else { |
| | | mesMdItemApi.deleteItem(updateReqVO.getId()); |
| | | } |
| | | } |
| | | |
| | |
| | | * |
| | | * MDM 的 unitMeasureId/categoryId 与 MES 的 unitMeasureId/itemTypeId 分属两套主数据, |
| | | * 这里先在 MDM 侧解析出 code,再交由 MesMdItemApi 在 MES 侧解析为对应 id。 |
| | | * 同步失败将抛出异常,触发外层 @Transactional 整体回滚(MDM 操作一并回滚)。 |
| | | * 同步失败将抛出异常,回滚本行事务(本行的 MDM 操作一并回滚,不影响其它行)。 |
| | | */ |
| | | private void syncItemToMes(MdmItemSaveReqVO reqVO) { |
| | | // 解析计量单位 id -> code |
| | |
| | | } |
| | | unitCode = unitMeasure.getCode(); |
| | | } |
| | | // 解析分类 id -> code(使用 MES 物料分类 API) |
| | | // 解析分类 id -> code(使用 MDM 本地分类服务) |
| | | String itemTypeCode = null; |
| | | if (reqVO.getCategoryId() != null) { |
| | | CommonResult<MesMdItemTypeRespDTO> result = mesMdItemTypeApi.getItemType(reqVO.getCategoryId()); |
| | | if (result != null && result.isSuccess() && result.getData() != null) { |
| | | itemTypeCode = result.getData().getCode(); |
| | | } |
| | | } else if (reqVO.getItemType() != null) { |
| | | // 根据 itemType 查找对应分类编码 |
| | | // itemType: 1原料, 2半成品, 3成品, 4辅料 |
| | | String itemTypeName = convertItemTypeToName(reqVO.getItemType()); |
| | | CommonResult<List<MesMdItemTypeRespDTO>> listResult = mesMdItemTypeApi.getItemTypeList(null); |
| | | if (listResult != null && listResult.isSuccess() && listResult.getData() != null) { |
| | | itemTypeCode = listResult.getData().stream() |
| | | .filter(c -> c.getName() != null && c.getName().equals(itemTypeName)) |
| | | .findFirst() |
| | | .map(MesMdItemTypeRespDTO::getCode) |
| | | .orElse(null); |
| | | } |
| | | CommonResult<MesMdItemTypeRespDTO> itemType = mesMdItemTypeApi.getItemType(reqVO.getCategoryId()); |
| | | itemTypeCode = itemType.getData().getCode(); |
| | | } |
| | | // 组装同步 DTO |
| | | MesMdItemSyncReqDTO syncReqDTO = new MesMdItemSyncReqDTO(); |
| | | syncReqDTO.setCode(reqVO.getCode()); |
| | | syncReqDTO.setMdmItemId(reqVO.getId()); |
| | | syncReqDTO.setName(reqVO.getName()); |
| | | syncReqDTO.setSpecification(reqVO.getSpecification()); |
| | | syncReqDTO.setUnitCode(unitCode); |
| | |
| | | syncReqDTO.setMaxStock(reqVO.getMaxStock()); |
| | | syncReqDTO.setHighValue(reqVO.getIsHighValue()); |
| | | syncReqDTO.setBatchFlag(reqVO.getIsBatchManaged()); |
| | | syncReqDTO.setStatus(reqVO.getStatus()); |
| | | syncReqDTO.setRemark(reqVO.getRemark()); |
| | | // 批次配置字段 |
| | | syncReqDTO.setProduceDateFlag(reqVO.getProduceDateFlag()); |
| | | syncReqDTO.setExpireDateFlag(reqVO.getExpireDateFlag()); |
| | | syncReqDTO.setReceiptDateFlag(reqVO.getReceiptDateFlag()); |
| | | syncReqDTO.setVendorFlag(reqVO.getVendorFlag()); |
| | | syncReqDTO.setPurchaseOrderCodeFlag(reqVO.getPurchaseOrderCodeFlag()); |
| | | syncReqDTO.setLotNumberFlag(reqVO.getLotNumberFlag()); |
| | | syncReqDTO.setQualityStatusFlag(reqVO.getQualityStatusFlag()); |
| | | // create 走 createItem,update 走 updateItem(MES 侧按 code upsert) |
| | | if (reqVO.getId() == null) { |
| | | mesMdItemApi.createItem(syncReqDTO); |
| | |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateItemStatus(Long id, Integer status) { |
| | | // 校验存在 |
| | | validateItemExists(id); |
| | | // 更新状态 |
| | | itemMapper.updateById(new MdmItemDO().setId(id).setStatus(status)); |
| | | // 同步状态到 MES |
| | | mesMdItemApi.updateItemStatus(id, status); |
| | | } |
| | | |
| | | @Override |
| | |
| | | public void deleteItem(Long id) { |
| | | // 校验存在 |
| | | MdmItemDO item = validateItemExists(id); |
| | | // 解决软删除唯一键冲突:如果已存在相同 code 且已软删除的记录,先物理删除它 |
| | | // MdmItemDO existingDeleted = itemMapper.selectByCodeAndDeleted(item.getCode(), true); |
| | | // if (existingDeleted != null && !existingDeleted.getId().equals(id)) { |
| | | // itemMapper.physicalDeleteById(existingDeleted.getId()); |
| | | // } |
| | | // 校验物料是否被业务引用 |
| | | validateItemNoReference(id); |
| | | // 软删除当前记录 |
| | | itemMapper.deleteById(id); |
| | | // 同步删除到 MES |
| | | mesMdItemApi.deleteItem(id); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void updateItemSync(Long id, Boolean sync) { |
| | | // 校验存在 |
| | | MdmItemDO item = validateItemExists(id); |
| | | if (Boolean.TRUE.equals(sync)) { |
| | | // 同步到 MES:从 DB 读取完整物料数据组装后走 syncItemToMes |
| | | MdmItemSaveReqVO reqVO = BeanUtils.toBean(item, MdmItemSaveReqVO.class); |
| | | // DO 字段名为 highValue,与 VO 的 isHighValue 不自动匹配,手动补齐 |
| | | reqVO.setIsHighValue(item.getHighValue()); |
| | | // 补齐批次属性配置 |
| | | MdmItemBatchConfigDO config = itemBatchConfigMapper.selectOne( |
| | | MdmItemBatchConfigDO::getItemId, id); |
| | | if (config != null) { |
| | | reqVO.setProduceDateFlag(config.getProduceDateFlag()); |
| | | reqVO.setExpireDateFlag(config.getExpireDateFlag()); |
| | | reqVO.setReceiptDateFlag(config.getReceiptDateFlag()); |
| | | reqVO.setVendorFlag(config.getVendorFlag()); |
| | | reqVO.setPurchaseOrderCodeFlag(config.getPurchaseOrderCodeFlag()); |
| | | reqVO.setLotNumberFlag(config.getLotNumberFlag()); |
| | | reqVO.setQualityStatusFlag(config.getQualityStatusFlag()); |
| | | } |
| | | syncItemToMes(reqVO); |
| | | } else { |
| | | // 取消同步:从 MES 删除该物料(MES 无此物料时内部为空操作) |
| | | mesMdItemApi.deleteItem(id); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 校验物料未被业务引用(销售、采购、生产、质检、库存等) |
| | | * |
| | | * @param id 物料编号 |
| | | */ |
| | | private void validateItemNoReference(Long id) { |
| | | List<Map<String, Object>> references = itemMapper.selectItemReferenceCount(id); |
| | | if (CollUtil.isNotEmpty(references)) { |
| | | String refDesc = references.stream() |
| | | .map(r -> r.get("source") + "(" + r.get("cnt") + ")") |
| | | .collect(Collectors.joining("、")); |
| | | throw exception(MDM_ITEM_EXISTS_REFERENCE, refDesc); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public void deleteItemList(Collection<Long> ids) { |
| | | if (CollUtil.isEmpty(ids)) { |
| | | return; |
| | | } |
| | | // 逐个删除(复用单条删除逻辑,保持校验) |
| | | ids.forEach(this::deleteItem); |
| | | } |
| | | |
| | | @Override |
| | |
| | | if (categoryId == null) { |
| | | return; |
| | | } |
| | | CommonResult<MesMdItemTypeRespDTO> result = mesMdItemTypeApi.getItemType(categoryId); |
| | | if (result == null || !result.isSuccess() || result.getData() == null) { |
| | | // 使用 MDM 本地分类服务校验 |
| | | MdmItemCategoryDO category = itemCategoryService.getItemCategory(categoryId); |
| | | if (category == null) { |
| | | throw exception(MDM_ITEM_CATEGORY_NOT_EXISTS); |
| | | } |
| | | } |
| | |
| | | return itemMapper.selectByCode(code); |
| | | } |
| | | |
| | | @Override |
| | | public List<MdmItemDO> getItemSimpleList() { |
| | | return itemMapper.selectList(MdmItemDO::getStatus, |
| | | cn.iocoder.yudao.framework.common.enums.CommonStatusEnum.ENABLE.getStatus()); |
| | | } |
| | | |
| | | /** |
| | | * 保存批次配置(到 MDM 本地表) |
| | | */ |
| | | private void saveBatchConfig(Long itemId, MdmItemSaveReqVO reqVO) { |
| | | // 检查是否有批次配置数据 |
| | | if (reqVO.getProduceDateFlag() == null |
| | | && reqVO.getExpireDateFlag() == null |
| | | && reqVO.getReceiptDateFlag() == null |
| | | && reqVO.getVendorFlag() == null |
| | | && reqVO.getPurchaseOrderCodeFlag() == null |
| | | && reqVO.getLotNumberFlag() == null |
| | | && reqVO.getQualityStatusFlag() == null) { |
| | | return; |
| | | } |
| | | |
| | | // 查询已有配置 |
| | | cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO existing = |
| | | itemBatchConfigMapper.selectOne( |
| | | cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO::getItemId, itemId); |
| | | |
| | | cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO config = |
| | | new cn.iocoder.yudao.module.mdm.dal.dataobject.item.MdmItemBatchConfigDO(); |
| | | config.setItemId(itemId); |
| | | config.setProduceDateFlag(Boolean.TRUE.equals(reqVO.getProduceDateFlag())); |
| | | config.setExpireDateFlag(Boolean.TRUE.equals(reqVO.getExpireDateFlag())); |
| | | config.setReceiptDateFlag(Boolean.TRUE.equals(reqVO.getReceiptDateFlag())); |
| | | config.setVendorFlag(Boolean.TRUE.equals(reqVO.getVendorFlag())); |
| | | config.setPurchaseOrderCodeFlag(Boolean.TRUE.equals(reqVO.getPurchaseOrderCodeFlag())); |
| | | config.setLotNumberFlag(Boolean.TRUE.equals(reqVO.getLotNumberFlag())); |
| | | config.setQualityStatusFlag(Boolean.TRUE.equals(reqVO.getQualityStatusFlag())); |
| | | // 清空不需要的字段 |
| | | config.setClientFlag(false); |
| | | config.setSalesOrderCodeFlag(false); |
| | | config.setWorkOrderFlag(false); |
| | | config.setTaskFlag(false); |
| | | config.setWorkstationFlag(false); |
| | | config.setToolFlag(false); |
| | | config.setMoldFlag(false); |
| | | |
| | | if (existing != null) { |
| | | config.setId(existing.getId()); |
| | | itemBatchConfigMapper.updateById(config); |
| | | } else { |
| | | itemBatchConfigMapper.insert(config); |
| | | } |
| | | } |
| | | |
| | | @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(), "(名称和编码为空的行)")); |
| | | } |
| | | |
| | | } |