云
17 小时以前 ac2e2b5af3726ee64dc37c3ebf26989a0346f22a
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/route/MesProRouteProcessServiceImpl.java
@@ -17,12 +17,15 @@
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -40,6 +43,15 @@
@Service
@Validated
public class MesProRouteProcessServiceImpl implements MesProRouteProcessService {
    /**
     * 产出排序:主产出优先,其次按 sort,最后按 id
     */
    private static final Comparator<MesProRouteProcessOutputDO> OUTPUT_ORDER = Comparator
            .comparing((MesProRouteProcessOutputDO o) -> Boolean.TRUE.equals(o.getMainFlag()),
                    Comparator.reverseOrder())
            .thenComparing(MesProRouteProcessOutputDO::getSort, Comparator.nullsLast(Comparator.naturalOrder()))
            .thenComparing(MesProRouteProcessOutputDO::getId, Comparator.nullsLast(Comparator.naturalOrder()));
    @Resource
    private MesProRouteProcessMapper routeProcessMapper;
@@ -232,6 +244,7 @@
            }
            String filtered = outputs.stream()
                    .filter(output -> output.getProductId() == null || output.getProductId().equals(productId))
                    .sorted(OUTPUT_ORDER)
                    .map(MesProRouteProcessOutputDO::getItemId)
                    .filter(Objects::nonNull)
                    .distinct()
@@ -251,10 +264,30 @@
        }
        return outputs.stream()
                .filter(output -> output.getProductId() == null || output.getProductId().equals(productId))
                .sorted(OUTPUT_ORDER)
                .map(MesProRouteProcessOutputDO::getItemId)
                .filter(Objects::nonNull)
                .distinct()
                .collect(Collectors.toList());
    }
    @Override
    public List<MesProRouteProcessOutputDO> getProcessOutputs(Long routeProcessId, Long productId) {
        if (routeProcessId == null) {
            return Collections.emptyList();
        }
        List<MesProRouteProcessOutputDO> outputs = routeProcessOutputMapper.selectListByRouteProcessId(routeProcessId);
        if (CollUtil.isEmpty(outputs)) {
            return Collections.emptyList();
        }
        // 主产出优先排序,并按 itemId 去重
        Map<Long, MesProRouteProcessOutputDO> distinct = new LinkedHashMap<>();
        outputs.stream()
                .filter(output -> output.getProductId() == null || output.getProductId().equals(productId))
                .filter(output -> output.getItemId() != null)
                .sorted(OUTPUT_ORDER)
                .forEach(output -> distinct.putIfAbsent(output.getItemId(), output));
        return new ArrayList<>(distinct.values());
    }
    @Override
@@ -275,8 +308,13 @@
        if (CollUtil.isNotEmpty(reqVO.getOutputs())) {
            itemIds = reqVO.getOutputs().stream()
                    .filter(Objects::nonNull)
                    .sorted(Comparator.comparing(MesProRouteProcessSaveReqVO.OutputItem::getSort,
                            Comparator.nullsLast(Comparator.naturalOrder())))
                    .sorted(Comparator
                            .comparing((MesProRouteProcessSaveReqVO.OutputItem o) -> Boolean.TRUE.equals(o.getMainFlag()),
                                    Comparator.reverseOrder())
                            .thenComparing(MesProRouteProcessSaveReqVO.OutputItem::getSort,
                                    Comparator.nullsLast(Comparator.naturalOrder()))
                            .thenComparing(MesProRouteProcessSaveReqVO.OutputItem::getItemId,
                                    Comparator.nullsLast(Comparator.naturalOrder())))
                    .map(MesProRouteProcessSaveReqVO.OutputItem::getItemId)
                    .filter(Objects::nonNull)
                    .distinct()
@@ -307,29 +345,37 @@
        Set<String> keys = new HashSet<>();
        int index = 0;
        if (CollUtil.isNotEmpty(reqVO.getOutputs())) {
            for (MesProRouteProcessSaveReqVO.OutputItem output : reqVO.getOutputs()) {
                if (output == null || output.getItemId() == null) {
                    continue;
                }
            List<MesProRouteProcessSaveReqVO.OutputItem> items = reqVO.getOutputs().stream()
                    .filter(Objects::nonNull)
                    .filter(output -> output.getItemId() != null)
                    .collect(Collectors.toList());
            // 校验:同一归属产品分组内主产出唯一、配比合法
            validateOutputs(items);
            for (MesProRouteProcessSaveReqVO.OutputItem output : items) {
                index++;
                if (!keys.add(output.getProductId() + "_" + output.getItemId())) {
                    continue;
                }
                boolean main = Boolean.TRUE.equals(output.getMainFlag());
                BigDecimal quantity = main ? BigDecimal.ONE : output.getQuantity();
                outputs.add(buildOutput(routeProcess, output.getProductId(), output.getItemId(),
                        output.getSort() != null ? output.getSort() : index, output.getRemark()));
                        output.getSort() != null ? output.getSort() : index, quantity, main, output.getRemark()));
            }
        } else {
            // 兼容只传 outputItemIds 的调用方:产出是路线产品时归属到自身,否则视为通用
            // 兼容只传 outputItemIds 的调用方:产出是路线产品时归属到自身,否则视为通用;
            // 每个归属产品分组的第一条默认作为主产出
            Set<Long> mainAssignedProducts = new HashSet<>();
            for (Long itemId : reqVO.getOutputItemIds()) {
                if (itemId == null) {
                    continue;
                }
                index++;
                Long productId = routeProductIds.contains(itemId) ? itemId : null;
                if (!keys.add(productId + "_" + itemId)) {
                    continue;
                }
                outputs.add(buildOutput(routeProcess, productId, itemId, index, null));
                index++;
                boolean main = mainAssignedProducts.add(productId);
                outputs.add(buildOutput(routeProcess, productId, itemId, index, BigDecimal.ONE, main, null));
            }
        }
        if (CollUtil.isNotEmpty(outputs)) {
@@ -337,14 +383,48 @@
        }
    }
    /**
     * 校验工序产出配置:同一归属产品分组内最多一个主产出、多个产出时必须指定主产出,非主产出配比必须大于 0
     */
    private void validateOutputs(List<MesProRouteProcessSaveReqVO.OutputItem> items) {
        if (CollUtil.isEmpty(items)) {
            return;
        }
        Map<Long, List<MesProRouteProcessSaveReqVO.OutputItem>> groupByProduct = new HashMap<>();
        for (MesProRouteProcessSaveReqVO.OutputItem item : items) {
            groupByProduct.computeIfAbsent(item.getProductId(), key -> new ArrayList<>()).add(item);
        }
        for (List<MesProRouteProcessSaveReqVO.OutputItem> group : groupByProduct.values()) {
            long mainCount = group.stream().filter(item -> Boolean.TRUE.equals(item.getMainFlag())).count();
            if (mainCount > 1) {
                throw exception(PRO_ROUTE_PROCESS_OUTPUT_MAIN_DUPLICATE);
            }
            if (mainCount == 0 && group.size() > 1) {
                throw exception(PRO_ROUTE_PROCESS_OUTPUT_MAIN_REQUIRED);
            }
            for (MesProRouteProcessSaveReqVO.OutputItem item : group) {
                if (Boolean.TRUE.equals(item.getMainFlag())) {
                    continue;
                }
                // 未填配比默认按 1 处理,填了则必须大于 0
                if (item.getQuantity() != null && item.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
                    throw exception(PRO_ROUTE_PROCESS_OUTPUT_RATIO_INVALID);
                }
            }
        }
    }
    private MesProRouteProcessOutputDO buildOutput(MesProRouteProcessDO routeProcess, Long productId,
                                                   Long itemId, Integer sort, String remark) {
                                                   Long itemId, Integer sort, BigDecimal quantity,
                                                   Boolean mainFlag, String remark) {
        return MesProRouteProcessOutputDO.builder()
                .routeProcessId(routeProcess.getId())
                .routeId(routeProcess.getRouteId())
                .processId(routeProcess.getProcessId())
                .productId(productId)
                .itemId(itemId)
                .quantity(mainFlag != null && mainFlag ? BigDecimal.ONE : quantity)
                .mainFlag(Boolean.TRUE.equals(mainFlag))
                .sort(sort)
                .remark(remark)
                .build();