package cn.iocoder.yudao.module.mes.service.pro.route; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.BooleanUtil; import cn.hutool.core.util.ObjUtil; import cn.hutool.core.util.StrUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; 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.pro.route.vo.MesProRouteImportExcelVO; import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteImportRespVO; import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRoutePageReqVO; import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.MesProRouteSaveReqVO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessOutputDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductDO; import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProductBomDO; 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.mysql.pro.route.MesProRouteMapper; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService; import cn.iocoder.yudao.module.mes.service.wm.warehouse.MesWmWarehouseAreaService; import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi; import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO; import cn.iocoder.yudao.module.mdm.enums.MdmItemTypeEnum; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import jakarta.annotation.Resource; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.validation.annotation.Validated; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Set; import java.util.Comparator; import java.util.Arrays; import java.util.Objects; import java.util.stream.Collectors; import java.util.concurrent.atomic.AtomicInteger; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*; /** * MES 工艺路线 Service 实现类 * * @author 超级管理员 */ @Service @Validated public class MesProRouteServiceImpl implements MesProRouteService { @Resource private MesProRouteMapper routeMapper; @Resource @Lazy private MesProRouteProcessService routeProcessService; @Resource @Lazy private MesProRouteProductService routeProductService; @Resource @Lazy private MesProRouteProductBomService routeProductBomService; @Resource @Lazy private MesMdItemService itemService; @Resource private MesMdItemTypeService itemTypeService; @Resource private MesWmWarehouseAreaService warehouseAreaService; @Resource private MdmItemApi mdmItemApi; @Override public Long createRoute(MesProRouteSaveReqVO createReqVO) { // 1. 校验编码唯一性 validateRouteCodeUnique(null, createReqVO.getCode()); // 2. 插入 MesProRouteDO route = BeanUtils.toBean(createReqVO, MesProRouteDO.class); route.setStatus(CommonStatusEnum.DISABLE.getStatus()); routeMapper.insert(route); return route.getId(); } @Override public void updateRoute(MesProRouteSaveReqVO updateReqVO) { // 1.1 校验存在 validateRouteExists(updateReqVO.getId()); // 1.2 校验编码唯一性 validateRouteCodeUnique(updateReqVO.getId(), updateReqVO.getCode()); // 2. 更新 MesProRouteDO updateObj = BeanUtils.toBean(updateReqVO, MesProRouteDO.class); routeMapper.updateById(updateObj); } @Override public void updateRouteStatus(Long id, Integer status) { // 1.1 校验存在 MesProRouteDO route = validateRouteExists(id); // 1.2 启用时的校验 if (CommonStatusEnum.ENABLE.getStatus().equals(status)) { validateRouteEnable(id); } // 2. 更新状态 routeMapper.updateById(new MesProRouteDO().setId(id).setStatus(status)); } @Override @Transactional(rollbackFor = Exception.class) public void deleteRoute(Long id) { // 1.1 校验存在 validateRouteExists(id); // 1.2 已启用的工艺路线,不允许删除 validateRouteNotEnable(id); // 2.1 级联删除 routeProcessService.deleteRouteProcessByRouteId(id); routeProductService.deleteRouteProductByRouteId(id); routeProductBomService.deleteRouteProductBomByRouteId(id); // 2.2 删除工艺路线 routeMapper.deleteById(id); } @Override public MesProRouteDO validateRouteExists(Long id) { MesProRouteDO route = routeMapper.selectById(id); if (route == null) { throw exception(PRO_ROUTE_NOT_EXISTS); } return route; } private void validateRouteCodeUnique(Long id, String code) { MesProRouteDO route = routeMapper.selectByCode(code); if (route == null) { return; } if (ObjUtil.notEqual(route.getId(), id)) { throw exception(PRO_ROUTE_CODE_DUPLICATE); } } /** * 启用工艺路线时的校验 */ private void validateRouteEnable(Long routeId) { // 1. 必须有工序 List processList = routeProcessService.getRouteProcessListByRouteId(routeId); if (CollUtil.isEmpty(processList)) { throw exception(PRO_ROUTE_ENABLE_NO_PROCESS); } // 2. 必须有关键工序 boolean hasKeyProcess = processList.stream().anyMatch(MesProRouteProcessDO::getKeyFlag); if (BooleanUtil.isFalse(hasKeyProcess)) { throw exception(PRO_ROUTE_ENABLE_NO_KEY_PROCESS); } // 3. 所有产品必须配置了 BOM 消耗 List productList = routeProductService.getRouteProductListByRouteId(routeId); for (MesProRouteProductDO product : productList) { MesMdItemDO item = itemService.validateItemExists(product.getItemId()); if (product.getWarehouseId() == null || product.getLocationId() == null || product.getAreaId() == null) { throw exception(PRO_ROUTE_PRODUCT_WAREHOUSE_REQUIRED, item.getName()); } warehouseAreaService.validateWarehouseAreaExists(product.getWarehouseId(), product.getLocationId(), product.getAreaId()); List bomList = routeProductBomService .getRouteProductBomList(routeId, null, product.getItemId()); if (CollUtil.isEmpty(bomList)) { throw exception(PRO_ROUTE_ENABLE_PRODUCT_NO_BOM, item.getName()); } } validateProcessOutputs(processList, productList); } private void validateProcessOutputs(List processList, List productList) { if (CollUtil.isEmpty(productList)) { throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, processList.get(processList.size() - 1).getProcessId()); } Set boundItemIds = productList.stream().map(MesProRouteProductDO::getItemId).collect(Collectors.toSet()); Set finishedItemIds = productList.stream() .map(MesProRouteProductDO::getItemId) .filter(itemId -> isItemUnderCategory(itemId, "FG")) .collect(Collectors.toSet()); MesProRouteProcessDO lastProcess = processList.stream() .max(Comparator.comparing(MesProRouteProcessDO::getSort)).orElseThrow(); for (MesProRouteProcessDO process : processList) { Set outputIds = parseOutputItemIds(process.getOutputItemId()); boolean last = process.getId().equals(lastProcess.getId()); if (CollUtil.isEmpty(outputIds)) { // 中间工序不一定形成可入库的独立半成品,允许不配置产出;末道工序必须产出成品。 if (last) { throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, process.getProcessId()); } continue; } for (Long outputId : outputIds) { if (!boundItemIds.contains(outputId)) { MesMdItemDO item = itemService.validateItemExists(outputId); throw exception(PRO_ROUTE_PROCESS_OUTPUT_NOT_BOUND, item.getName()); } } if (last) { if (finishedItemIds.isEmpty() || !outputIds.equals(finishedItemIds)) { throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, process.getProcessId()); } } else if (outputIds.stream().anyMatch(itemId -> !isItemUnderCategory(itemId, "SF"))) { throw exception(PRO_ROUTE_MIDDLE_OUTPUT_INVALID, process.getProcessId()); } } // 4. 末道工序成品产出必须按产品归属,避免“通用”产出串到其它产品的工单 List lastOutputs = routeProcessService .getRouteProcessOutputListByRouteProcessIds(Collections.singletonList(lastProcess.getId())); if (CollUtil.isNotEmpty(lastOutputs)) { boolean attributionInvalid = lastOutputs.stream().anyMatch(output -> output.getProductId() == null || !output.getProductId().equals(output.getItemId())); Set attributedProducts = lastOutputs.stream() .map(MesProRouteProcessOutputDO::getProductId) .filter(Objects::nonNull) .collect(Collectors.toSet()); if (attributionInvalid || !attributedProducts.containsAll(finishedItemIds)) { throw exception(PRO_ROUTE_LAST_OUTPUT_ATTRIBUTION_REQUIRED, lastProcess.getProcessId()); } } } private Set parseOutputItemIds(String value) { if (StrUtil.isBlank(value)) { return Collections.emptySet(); } return Arrays.stream(value.split(",")).map(String::trim).filter(StrUtil::isNotBlank) .map(Long::valueOf).collect(Collectors.toSet()); } private boolean isItemUnderCategory(Long itemId, String rootCode) { MesMdItemDO item = itemService.validateItemExists(itemId); MesMdItemTypeDO type = itemTypeService.getItemType(item.getItemTypeId()); while (type != null) { if ("SF".equalsIgnoreCase(type.getCode()) || (type.getName() != null && type.getName().contains("半成品"))) { return "SF".equals(rootCode); } if ("FG".equalsIgnoreCase(type.getCode()) || isFinishedCategoryName(type.getName())) { return "FG".equals(rootCode); } if (type.getParentId() == null || MesMdItemTypeDO.PARENT_ID_ROOT.equals(type.getParentId())) { break; } type = itemTypeService.getItemType(type.getParentId()); } // MES 分类未使用 FG/SF 编码时,按 MDM 主数据的物料类型判定。 CommonResult mdmResult = mdmItemApi.getItemByCode(item.getCode()); if (mdmResult == null || !mdmResult.isSuccess() || mdmResult.getData() == null) { return false; } MdmItemRespDTO mdmItem = mdmResult.getData(); Integer mdmType = mdmItem.getItemType(); // 历史同步可能把分类 ID 存在 itemType,不能把分类 2/3 误判为成品/半成品。 if (Objects.equals(mdmItem.getCategoryId(), mdmType == null ? null : mdmType.longValue())) { return false; } return "FG".equals(rootCode) ? MdmItemTypeEnum.PRODUCT.getType().equals(mdmType) : "SF".equals(rootCode) && MdmItemTypeEnum.SEMI_FINISHED.getType().equals(mdmType); } private boolean isFinishedCategoryName(String name) { return name != null && !name.contains("半成品") && !name.contains("非成品") && (name.contains("产成品") || name.contains("成品")); } @Override public MesProRouteDO getRoute(Long id) { return routeMapper.selectById(id); } @Override public PageResult getRoutePage(MesProRoutePageReqVO pageReqVO) { return routeMapper.selectPage(pageReqVO); } @Override public List getRouteListByStatus(Integer status) { return routeMapper.selectListByStatus(status); } @Override public void validateRouteNotEnable(Long routeId) { MesProRouteDO route = routeMapper.selectById(routeId); if (route != null && CommonStatusEnum.ENABLE.getStatus().equals(route.getStatus())) { throw exception(PRO_ROUTE_IS_ENABLE); } } @Override public List getRouteList(Collection ids) { if (CollUtil.isEmpty(ids)) { return Collections.emptyList(); } return routeMapper.selectByIds(ids); } @Override @Transactional(rollbackFor = Exception.class) public MesProRouteImportRespVO importRouteList(List importList, boolean updateSupport) { // 1. 校验非空 if (CollUtil.isEmpty(importList)) { throw exception(PRO_ROUTE_IMPORT_LIST_IS_EMPTY); } // 2. 逐行处理 MesProRouteImportRespVO respVO = MesProRouteImportRespVO.builder() .createCodes(new ArrayList<>()).updateCodes(new ArrayList<>()) .failureCodes(new LinkedHashMap<>()).build(); AtomicInteger index = new AtomicInteger(1); importList.forEach(importItem -> { int currentIndex = index.getAndIncrement(); String key = StrUtil.blankToDefault(importItem.getCode(), "第 " + currentIndex + " 行"); if (StrUtil.isBlank(importItem.getCode())) { respVO.getFailureCodes().put(key, "工艺路线编码不能为空"); return; } if (StrUtil.isBlank(importItem.getName())) { respVO.getFailureCodes().put(key, "工艺路线名称不能为空"); return; } // 2.1 判断:创建 or 更新 MesProRouteDO existRoute = routeMapper.selectByCode(importItem.getCode()); if (existRoute == null) { // 2.1.1 新增(与手工创建一致:默认禁用,通过启用接口校验后启用) MesProRouteDO route = BeanUtils.toBean(importItem, MesProRouteDO.class); route.setId(null); route.setStatus(CommonStatusEnum.DISABLE.getStatus()); routeMapper.insert(route); respVO.getCreateCodes().add(importItem.getCode()); } else if (updateSupport) { // 2.1.2 更新(状态由启用/禁用接口单独维护,导入不修改状态) MesProRouteDO updateObj = BeanUtils.toBean(importItem, MesProRouteDO.class); updateObj.setId(existRoute.getId()); updateObj.setCode(existRoute.getCode()); routeMapper.updateById(updateObj); respVO.getUpdateCodes().add(importItem.getCode()); } else { respVO.getFailureCodes().put(key, "工艺路线编码已存在"); } }); return respVO; } }