package cn.iocoder.yudao.module.mes.service.md.item; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.pojo.PageResult; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; 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; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemBatchConfigDO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO; import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO; 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.controller.admin.md.item.vo.type.MesMdItemTypeListReqVO; import cn.iocoder.yudao.module.mes.enums.md.MesMdItemTypeEnum; 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; import org.springframework.validation.annotation.Validated; import java.math.BigDecimal; import java.util.*; 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.mes.enums.ErrorCodeConstants.*; /** * MES 物料产品 Service 实现类 * * 支持 MES 和 MDM 物料数据双向同步 * * @author 超级管理员 */ @Service @Validated public class MesMdItemServiceImpl implements MesMdItemService { @Resource private MesMdItemMapper itemMapper; @Resource private MesMdItemTypeService itemTypeService; @Resource private MesMdUnitMeasureService unitMeasureService; @Resource private MesMdItemBatchConfigService itemBatchConfigService; @Resource private MesMdProductBomService productBomService; @Resource private MesMdProductSopService productSopService; @Resource private MesMdProductSipService productSipService; @Resource private MesWmBarcodeService barcodeService; @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(); } // 存在逻辑删除记录(取消同步后重新同步):恢复该记录后走更新,避免主键冲突 if (itemMapper.selectAnyById(createReqVO.getId()) != null) { itemMapper.restoreById(createReqVO.getId()); updateItem(createReqVO); return createReqVO.getId(); } } // 插入 MesMdItemDO item = BeanUtils.toBean(createReqVO, MesMdItemDO.class); // 状态只能由基础数据(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()); // 校验数据 validateItemSaveData(updateReqVO); // 更新 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) { // 校验物料编码的唯一性 validateItemCodeUnique(reqVO.getId(), reqVO.getCode()); // 不再校验物料名称的唯一性:允许同名不同编码的物料(与 MDM 物料规则保持一致) // 校验物料分类存在 validateItemTypeExists(reqVO.getItemTypeId()); // 校验计量单位存在 validateUnitMeasureExists(reqVO.getUnitMeasureId()); } @Override public void updateItemStatus(Long id, Integer status) { // 校验存在 MesMdItemDO item = validateItemExists(id); if (CommonStatusEnum.isEnable(status)) { // 如果启用了批次管理,校验批次属性配置 if (Boolean.TRUE.equals(item.getBatchFlag())) { validateBatchConfigExists(id); } // 如果是产品类型,校验必须配置 BOM MesMdItemTypeDO itemType = itemTypeService.getItemType(item.getItemTypeId()); if (MesMdItemTypeEnum.isProduct(itemType.getItemOrProduct())) { if (CollUtil.isEmpty(productBomService.getProductBomListByItemId(id))) { throw exception(MD_ITEM_PRODUCT_BOM_REQUIRED); } } } // 更新状态 itemMapper.updateById(new MesMdItemDO().setId(id).setStatus(status)); } @Override @Transactional(rollbackFor = Exception.class) public void deleteItem(Long id) { // 校验存在 validateItemExists(id); // 级联删除批次属性配置 itemBatchConfigService.deleteItemBatchConfigByItemId(id); // 级联删除产品BOM productBomService.deleteProductBomByItemId(id); // 级联删除产品SOP productSopService.deleteProductSopByItemId(id); // 级联删除产品SIP productSipService.deleteProductSipByItemId(id); // 删除 itemMapper.deleteById(id); } @Override public MesMdItemDO validateItemExists(Long id) { MesMdItemDO item = itemMapper.selectById(id); if (item == null) { throw exception(MD_ITEM_NOT_EXISTS); } return item; } @Override public MesMdItemDO validateItemExistsAndEnable(Long id) { 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) { MesMdItemDO item = itemMapper.selectByCode(code); if (item == null) { return; } if (ObjUtil.notEqual(item.getId(), id)) { throw exception(MD_ITEM_CODE_DUPLICATE); } } private void validateItemTypeExists(Long itemTypeId) { if (itemTypeService.getItemType(itemTypeId) == null) { throw exception(MD_ITEM_TYPE_NOT_EXISTS); } } private void validateUnitMeasureExists(Long unitMeasureId) { if (unitMeasureService.getUnitMeasure(unitMeasureId) == null) { throw exception(MD_UNIT_MEASURE_NOT_EXISTS); } } /** * 如果未启用安全库存,清零库存上下限 */ private void clearStockIfNotSafe(MesMdItemDO item) { if (Boolean.TRUE.equals(item.getSafeStockFlag())) { return; } item.setMinStock(BigDecimal.ZERO).setMaxStock(BigDecimal.ZERO); } /** * 如果启用了批次管理且状态为开启,校验批次属性配置是否存在且至少一个属性为 true */ private void validateBatchConfigExists(Long itemId) { MesMdItemBatchConfigDO config = itemBatchConfigService.getItemBatchConfigByItemId(itemId); if (config == null) { throw exception(MD_ITEM_BATCH_CONFIG_NOT_EXISTS); } boolean hasAnyFlag = Boolean.TRUE.equals(config.getProduceDateFlag()) || Boolean.TRUE.equals(config.getExpireDateFlag()) || Boolean.TRUE.equals(config.getReceiptDateFlag()) || Boolean.TRUE.equals(config.getVendorFlag()) || Boolean.TRUE.equals(config.getClientFlag()) || Boolean.TRUE.equals(config.getSalesOrderCodeFlag()) || Boolean.TRUE.equals(config.getPurchaseOrderCodeFlag()) || Boolean.TRUE.equals(config.getWorkOrderFlag()) || Boolean.TRUE.equals(config.getTaskFlag()) || Boolean.TRUE.equals(config.getWorkstationFlag()) || Boolean.TRUE.equals(config.getToolFlag()) || Boolean.TRUE.equals(config.getMoldFlag()) || Boolean.TRUE.equals(config.getLotNumberFlag()) || Boolean.TRUE.equals(config.getQualityStatusFlag()); if (!hasAnyFlag) { throw exception(MD_ITEM_BATCH_CONFIG_AT_LEAST_ONE_FLAG); } } @Override public MesMdItemDO getItem(Long id) { return itemMapper.selectById(id); } @Override public MesMdItemDO getItemByCode(String code) { return itemMapper.selectByCode(code); } @Override public PageResult getItemPage(MesMdItemPageReqVO pageReqVO) { // 查找时,如果查找某个分类编号,则包含它的子分类 Set itemTypeIds = null; if (pageReqVO.getItemTypeId() != null) { itemTypeIds = new HashSet<>(); itemTypeIds.add(pageReqVO.getItemTypeId()); List children = itemTypeService.getItemTypeChildrenList(pageReqVO.getItemTypeId()); itemTypeIds.addAll(convertSet(children, MesMdItemTypeDO::getId)); } // 分页查询 return itemMapper.selectPage(pageReqVO, itemTypeIds); } @Override public List getItemList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return itemMapper.selectByIds(ids); } @Override public Long getItemCountByItemTypeId(Long itemTypeId) { return itemMapper.selectCountByItemTypeId(itemTypeId); } @Override public Long getItemCountByUnitMeasureId(Long unitMeasureId) { 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 public MesMdItemDO getItemByMdmItemId(Long mdmItemId) { if (mdmItemId == null) { return null; } 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; } @Override @Transactional(rollbackFor = Exception.class) public MesMdItemImportRespVO importItemList(List importList, Boolean updateSupport) { MesMdItemImportRespVO respVO = MesMdItemImportRespVO.empty(); if (CollUtil.isEmpty(importList)) { return respVO; } // 预加载计量单位、物料分类,按名称建索引,避免逐行查库 Map unitMeasureMap = new HashMap<>(); unitMeasureService.getUnitMeasureListByStatus(CommonStatusEnum.ENABLE.getStatus()) .forEach(unit -> unitMeasureMap.put(unit.getName().trim(), unit.getId())); Map itemTypeMap = new HashMap<>(); itemTypeService.getItemTypeList(new MesMdItemTypeListReqVO()) .forEach(type -> itemTypeMap.put(type.getName().trim(), type.getId())); int index = 0; for (MesMdItemImportExcelVO row : importList) { index++; String key = StrUtil.isNotBlank(row.getCode()) ? row.getCode().trim() : "第 " + index + " 行"; // 1. 基础校验 if (StrUtil.isBlank(row.getCode())) { respVO.getFailureCodes().put(key, "物料编码不能为空"); continue; } if (StrUtil.isBlank(row.getName())) { respVO.getFailureCodes().put(key, "物料名称不能为空"); continue; } String code = row.getCode().trim(); String unitName = StrUtil.trim(row.getUnitMeasureName()); Long unitMeasureId = unitName == null ? null : unitMeasureMap.get(unitName); if (unitMeasureId == null) { respVO.getFailureCodes().put(key, "计量单位[" + unitName + "]不存在"); continue; } String typeName = StrUtil.trim(row.getItemTypeName()); Long itemTypeId = typeName == null ? null : itemTypeMap.get(typeName); if (itemTypeId == null) { respVO.getFailureCodes().put(key, "物料分类[" + typeName + "]不存在"); continue; } // 2. 组装保存对象(颜色一并写入) MesMdItemSaveReqVO saveReqVO = new MesMdItemSaveReqVO(); saveReqVO.setCode(code); saveReqVO.setName(row.getName().trim()); saveReqVO.setSpecification(StrUtil.trim(row.getSpecification())); saveReqVO.setColor(StrUtil.trim(row.getColor())); saveReqVO.setUnitMeasureId(unitMeasureId); saveReqVO.setItemTypeId(itemTypeId); saveReqVO.setSafeStockFlag(row.getSafeStockFlag()); saveReqVO.setMinStock(row.getMinStock()); saveReqVO.setMaxStock(row.getMaxStock()); saveReqVO.setHighValue(row.getHighValue()); saveReqVO.setBatchFlag(row.getBatchFlag()); saveReqVO.setRemark(StrUtil.trim(row.getRemark())); // 3. 新增或更新,单行失败不影响整批 MesMdItemDO existItem = itemMapper.selectByCode(code); try { if (existItem == null) { createItem(saveReqVO); respVO.getCreateCodes().add(code); } else if (!Boolean.TRUE.equals(updateSupport)) { respVO.getFailureCodes().put(key, "物料编码已存在(勾选「是否覆盖」可更新)"); } else { saveReqVO.setId(existItem.getId()); saveReqVO.setMdmItemId(existItem.getMdmItemId()); updateItem(saveReqVO); respVO.getUpdateCodes().add(code); } } catch (Exception e) { respVO.getFailureCodes().put(key, e.getMessage() == null ? "导入失败" : e.getMessage()); } } return respVO; } }