云
7 小时以前 3edf8c4fc07f9784e2d04ee7a98692bb457749eb
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteServiceImpl.java
@@ -3,27 +3,46 @@
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.*;
@@ -31,7 +50,7 @@
/**
 * MES 工艺路线 Service 实现类
 *
 * @author 芋道源码
 * @author 超级管理员
 */
@Service
@Validated
@@ -52,6 +71,12 @@
    @Resource
    @Lazy
    private MesMdItemService itemService;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
    private MesWmWarehouseAreaService warehouseAreaService;
    @Resource
    private MdmItemApi mdmItemApi;
    @Override
    public Long createRoute(MesProRouteSaveReqVO createReqVO) {
@@ -141,13 +166,115 @@
        // 3. 所有产品必须配置了 BOM 消耗
        List<MesProRouteProductDO> 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<MesProRouteProductBomDO> bomList = routeProductBomService
                    .getRouteProductBomList(routeId, null, product.getItemId());
            if (CollUtil.isEmpty(bomList)) {
                MesMdItemDO item = itemService.validateItemExists(product.getItemId());
                throw exception(PRO_ROUTE_ENABLE_PRODUCT_NO_BOM, item.getName());
            }
        }
        validateProcessOutputs(processList, productList);
    }
    private void validateProcessOutputs(List<MesProRouteProcessDO> processList,
                                        List<MesProRouteProductDO> productList) {
        if (CollUtil.isEmpty(productList)) {
            throw exception(PRO_ROUTE_LAST_OUTPUT_INVALID, processList.get(processList.size() - 1).getProcessId());
        }
        Set<Long> boundItemIds = productList.stream().map(MesProRouteProductDO::getItemId).collect(Collectors.toSet());
        Set<Long> 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<Long> 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<MesProRouteProcessOutputDO> 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<Long> 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<Long> 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<MdmItemRespDTO> 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
@@ -181,4 +308,52 @@
        return routeMapper.selectByIds(ids);
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MesProRouteImportRespVO importRouteList(List<MesProRouteImportExcelVO> 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;
    }
}