云
昨天 74b0d9c5ab1dc316b29f8525dcffe45225d4c43c
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProBatchScheduleService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,349 @@
package cn.iocoder.yudao.module.mes.service.pro.task.schedule;
import cn.hutool.crypto.digest.DigestUtil;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.*;
import cn.iocoder.yudao.module.mes.controller.admin.pro.task.vo.MesProBatchSchedulePreviewRespVO.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.route.MesProRouteProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.task.MesProTaskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.pro.workorder.MesProWorkOrderMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.enums.pro.*;
import cn.iocoder.yudao.module.mes.service.md.workstation.MesMdWorkstationService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.route.*;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import cn.iocoder.yudao.module.mes.service.pro.task.MesProTaskService;
import cn.iocoder.yudao.module.mes.service.cal.holiday.MesCalHolidayService;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.*;
import java.util.*;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@Service
public class MesProBatchScheduleService {
    @Resource private MesProTaskMapper taskMapper;
    @Resource private MesProWorkOrderMapper workOrderMapper;
    @Resource private MesDvMachineryMapper machineryMapper;
    @Resource private MesMdWorkstationService workstationService;
    @Resource private MesProWorkOrderProcessService workOrderProcessService;
    @Resource private MesProRouteService routeService;
    @Resource private MesProRouteProcessService routeProcessService;
    @Resource private MesProRouteProductService routeProductService;
    @Resource private MesProProcessService processService;
    @Resource private MesMdItemService itemService;
    @Resource private MesCalHolidayService holidayService;
    @Resource @Lazy private cn.iocoder.yudao.module.mes.service.pd.processparamtemplate.MesPdProcessParamTemplateService paramTemplateService;
    @Resource private MesProScheduleGuardService guard;
    @Resource private CrmCustomerApi customerApi;
    @Resource @Lazy private MesProTaskService taskService;
    private LocalDateTime now() { return LocalDateTime.now(ZoneId.of("Asia/Shanghai")).withNano(0); }
    public MesProBatchSchedulePreviewRespVO preview(MesProBatchScheduleReqVO request) {
        validateRequest(request);
        return build(request);
    }
    @Transactional(rollbackFor = Exception.class, isolation = Isolation.SERIALIZABLE, timeout = 60)
    public List<Long> execute(MesProBatchScheduleReqVO request) {
        validateRequest(request);
        if (request.getPreviewTime() == null || request.getPreviewHash() == null
                || request.getPreviewTime().isAfter(now()) || request.getPreviewTime().plusMinutes(15).isBefore(now())) {
            throw exception(PRO_TASK_SCHEDULE_PREVIEW_CHANGED);
        }
        guard.lock();
        workOrderMapper.lockScheduleOrders(request.getWorkOrderIds().stream().sorted().toList());
        var preview = build(request);
        if (!preview.isCanExecute()) {
            throw exception(PRO_TASK_SCHEDULE_INVALID, preview.getOrders().stream()
                    .filter(order -> order.getError() != null).map(OrderPlan::getError).collect(Collectors.joining(";")));
        }
        if (!Objects.equals(request.getPreviewHash(), preview.getPreviewHash())) {
            throw exception(PRO_TASK_SCHEDULE_PREVIEW_CHANGED);
        }
        if (preview.getTasks().stream().anyMatch(task -> task.getStartTime().isBefore(now()))) {
            throw exception(PRO_TASK_SCHEDULE_PREVIEW_CHANGED);
        }
        if (!preview.getWarnings().isEmpty() && !Boolean.TRUE.equals(request.getAllowLate())) {
            throw exception(PRO_TASK_SCHEDULE_INVALID, "请先确认预览中的交期延期风险");
        }
        List<Long> ids = new ArrayList<>();
        for (var plan : preview.getTasks()) {
            var task = new MesProTaskSaveReqVO();
            task.setWorkOrderId(plan.getWorkOrderId());
            task.setRouteId(plan.getRouteId());
            task.setProcessId(plan.getProcessId());
            task.setItemId(plan.getItemId());
            task.setParamTemplateId(plan.getParamTemplateId());
            task.setColorCode(plan.getColorCode());
            task.setWorkstationId(plan.getWorkstationId());
            task.setQuantity(plan.getQuantity());
            task.setDuration(plan.getDuration());
            task.setStartTime(plan.getStartTime());
            task.setEndTime(plan.getEndTime());
            task.setRemark("一键排产:09:00–18:00,排除周末及节假日");
            ids.add(taskService.createTask(task));
        }
        // æ¯é“工序均补齐后,工单的累计排产数量只记一次,不能按工序数累加。
        for (var order : preview.getOrders()) {
            workOrderMapper.updateById(new MesProWorkOrderDO().setId(order.getWorkOrderId())
                    .setQuantityScheduled(order.getQuantity()));
        }
        return ids;
    }
    private void validateRequest(MesProBatchScheduleReqVO request) {
        var ids = request.getWorkOrderIds();
        if (ids == null || ids.isEmpty() || ids.size() > 30 || ids.stream().anyMatch(id -> id == null || id <= 0)
                || new HashSet<>(ids).size() != ids.size()) {
            throw exception(PRO_TASK_SCHEDULE_INVALID, "请选择1至30个不同的有效工单");
        }
        if (request.getStartTime() == null || request.getStartTime().isBefore(now().minusMinutes(15))
                || request.getStartTime().isAfter(now().plusDays(365))) {
            throw exception(PRO_TASK_SCHEDULE_INVALID, "最早开工时间应为当前时间至未来一年以内,请重新选择");
        }
    }
    private MesProBatchSchedulePreviewRespVO build(MesProBatchScheduleReqVO request) {
        var result = new MesProBatchSchedulePreviewRespVO();
        result.setPreviewTime(now());
        var orders = workOrderMapper.selectByIds(request.getWorkOrderIds());
        var orderMap = orders.stream().collect(Collectors.toMap(MesProWorkOrderDO::getId, order -> order));
        var stations = workstationService.getWorkstationListByStatus(0).stream()
                .sorted(Comparator.comparing(MesMdWorkstationDO::getId)).toList();
        var machineMap = machineryMapper.selectList().stream().collect(Collectors.toMap(MesDvMachineryDO::getId, machine -> machine));
        var stationMachines = guard.machineIdsByStation();
        Map<Long, MesProTaskDO> allRelevantTasks = new TreeMap<>();
        taskMapper.selectSchedulingTasks().forEach(task -> allRelevantTasks.put(task.getId(), task));
        taskMapper.selectListByWorkOrderIds(request.getWorkOrderIds()).stream()
                .filter(task -> !Objects.equals(task.getStatus(), MesProTaskStatusEnum.CANCELED.getStatus()))
                .forEach(task -> allRelevantTasks.put(task.getId(), task));
        var existingTasks = new ArrayList<>(allRelevantTasks.values());
        var holidays = holidayService.getHolidayList(request.getStartTime().toLocalDate().atStartOfDay(), request.getStartTime().plusYears(1));
        Set<LocalDate> holidayDates = holidays.stream().filter(day -> Objects.equals(day.getType(), 2))
                .map(day -> day.getDay().toLocalDate()).collect(Collectors.toSet());
        var calendar = new MesProScheduleEngine.Calendar(holidayDates, request.getStartTime().toLocalDate().plusYears(1));
        List<MesProScheduleEngine.Busy> busy = new ArrayList<>();
        Set<Long> unknownStations = new HashSet<>();
        for (var task : existingTasks) {
            if (MesProTaskStatusEnum.isEndStatus(task.getStatus()) || task.getWorkstationId() == null) continue;
            if (task.getStartTime() == null || task.getEndTime() == null || !task.getEndTime().isAfter(task.getStartTime())
                    || task.getEndTime().isBefore(now())) {
                unknownStations.add(task.getWorkstationId());
            } else {
                busy.add(new MesProScheduleEngine.Busy(task.getWorkstationId(),
                        stationMachines.getOrDefault(task.getWorkstationId(), Set.of()), task.getStartTime(), task.getEndTime()));
            }
        }
        var sorted = orders.stream().sorted(Comparator.comparing(MesProWorkOrderDO::getRequestDate,
                Comparator.nullsLast(Comparator.naturalOrder())).thenComparing(MesProWorkOrderDO::getId)).toList();
        List<Long> orderSequence = new ArrayList<>();
        Set<Long> visiting = new HashSet<>();
        for (var order : sorted) addChildrenFirst(order.getId(), orderMap, orderSequence, visiting);
        for (Long id : request.getWorkOrderIds()) if (!orderMap.containsKey(id)) orderSequence.add(id);
        Map<Long, LocalDateTime> ends = new HashMap<>();
        Map<Long, String> templateHashes = new TreeMap<>();
        final List<MesProScheduleEngine.Busy> existingBusy = List.copyOf(busy);
        for (Long id : orderSequence) {
            var order = orderMap.get(id);
            var orderPlan = new OrderPlan();
            orderPlan.setWorkOrderId(id);
            result.getOrders().add(orderPlan);
            try {
                if (order == null) throw new IllegalArgumentException("工单不存在或已删除");
                orderPlan.setWorkOrderCode(order.getCode());
                orderPlan.setWorkOrderName(order.getName());
                orderPlan.setQuantity(order.getQuantity());
                orderPlan.setRequestDate(order.getRequestDate());
                if (!Objects.equals(order.getStatus(), MesProWorkOrderStatusEnum.CONFIRMED.getStatus())
                        || !Objects.equals(order.getType(), MesProWorkOrderTypeEnum.SELF.getType())) {
                    throw new IllegalArgumentException("仅支持已确认的自行生产工单");
                }
                if (order.getQuantity() == null || order.getQuantity().signum() <= 0
                        || (order.getQuantityProduced() != null && order.getQuantityProduced().compareTo(order.getQuantity()) >= 0)) {
                    throw new IllegalArgumentException("工单数量无效或已生产完成");
                }
                LocalDateTime earliest = calendar.nextWorkingTime(request.getStartTime());
                // çˆ¶å·¥å•必须等子工单;同一批次按依赖顺序排,未纳入的未完成子工单阻止排产。
                for (var child : workOrderMapper.selectList(MesProWorkOrderDO::getParentId, id)) {
                    if (Objects.equals(child.getStatus(), MesProWorkOrderStatusEnum.FINISHED.getStatus())) continue;
                    if (!ends.containsKey(child.getId())) throw new IllegalArgumentException("请将未完成子工单" + child.getCode() + "一并选中并先完成排产");
                    if (ends.get(child.getId()).isAfter(earliest)) earliest = ends.get(child.getId());
                }
                Long routeId = order.getRouteId();
                if (routeId == null) {
                    var relation = routeProductService.getRouteProductByItemId(order.getProductId());
                    if (relation != null) routeId = relation.getRouteId();
                }
                if (routeId == null) throw new IllegalArgumentException("未配置工艺路线,请先维护工单工序及产品路线");
                var route = routeService.validateRouteExists(routeId);
                if (!Objects.equals(route.getStatus(), 0)) throw new IllegalArgumentException("工艺路线已停用");
                List<MesProRouteProcessDO> processes = processes(id, routeId);
                if (processes.isEmpty()) throw new IllegalArgumentException("没有工序,请先维护工单工序");
                Set<Long> processIds = new HashSet<>();
                Set<Integer> sorts = new HashSet<>();
                Map<Long, Integer> processSorts = processes.stream().filter(process -> process.getProcessId() != null && process.getSort() != null)
                        .collect(Collectors.toMap(MesProRouteProcessDO::getProcessId, MesProRouteProcessDO::getSort, (first, duplicate) -> first));
                List<TaskPlan> plans = new ArrayList<>();
                Map<Long, BigDecimal> targetQuantities = calcTargetQuantities(processes, order.getQuantity());
                List<MesProScheduleEngine.Busy> trialBusy = new ArrayList<>(busy);
                for (var process : processes) {
                    if (process.getProcessId() == null || process.getSort() == null || !processIds.add(process.getProcessId()) || !sorts.add(process.getSort())) {
                        throw new IllegalArgumentException("工序及顺序必须完整且不能重复,请先维护工单工序");
                    }
                    if (process.getNextProcessId() != null && (!processSorts.containsKey(process.getNextProcessId())
                            || processSorts.get(process.getNextProcessId()) <= process.getSort())) {
                        throw new IllegalArgumentException("工序衔接与顺序不一致,请先维护工单工序");
                    }
                    int prepareMinutes = process.getPrepareTime() == null ? 0 : process.getPrepareTime();
                    int waitMinutes = process.getWaitTime() == null ? 0 : process.getWaitTime();
                    if (prepareMinutes < 0 || waitMinutes < 0) throw new IllegalArgumentException("工序准备时间和等待时间不能为负数");
                    if (process.getParamTemplateId() != null) {
                        paramTemplateService.validateProcessParamTemplateExists(process.getParamTemplateId());
                        var details = paramTemplateService.getProcessParamTemplateDetailListByTemplateId(process.getParamTemplateId());
                        if (details.isEmpty()) {
                            throw new IllegalArgumentException("工序参数模板没有明细,请先完善参数模板");
                        }
                        templateHashes.put(process.getParamTemplateId(), DigestUtil.sha256Hex(JsonUtils.toJsonString(details)));
                    }
                    processService.validateProcessExistsAndEnable(process.getProcessId());
                    var processData = processService.getProcess(process.getProcessId());
                    if (processData == null) throw new IllegalArgumentException("工序配置已变化,请重新维护后预览");
                    var ownTasks = existingTasks.stream().filter(task -> Objects.equals(task.getWorkOrderId(), id)
                            && Objects.equals(task.getProcessId(), process.getProcessId())).toList();
                    if (ownTasks.stream().anyMatch(task -> task.getQuantity() == null || task.getQuantity().signum() <= 0)) {
                        throw new IllegalArgumentException("当前工序已有任务数量无效,请先修正历史任务数量");
                    }
                    var assigned = ownTasks.stream().map(task -> task.getQuantity() == null ? BigDecimal.ZERO : task.getQuantity()).reduce(BigDecimal.ZERO, BigDecimal::add);
                    // å„道工序按收率倒推的目标产出量:末道=订单量,前面各道逐级放大
                    var targetQuantity = targetQuantities.getOrDefault(process.getProcessId(), order.getQuantity());
                    var remaining = targetQuantity.subtract(assigned);
                    if (remaining.signum() < 0) throw new IllegalArgumentException("工序" + processData.getName() + "已有排产数量超限,请先核对");
                    for (var task : ownTasks) {
                        if (!MesProTaskStatusEnum.isEndStatus(task.getStatus()) && (task.getStartTime() == null
                                || task.getEndTime() == null || !task.getEndTime().isAfter(task.getStartTime()) || task.getEndTime().isBefore(now()))) {
                            throw new IllegalArgumentException("工序" + processData.getName() + "已有任务时间不完整或已超时未完成,请先维护");
                        }
                    }
                    LocalDateTime existingEnd = ownTasks.stream().map(task -> task.getEndTime() != null ? task.getEndTime() : task.getFinishDate())
                            .filter(Objects::nonNull).max(Comparator.naturalOrder()).orElse(earliest);
                    if (remaining.signum() == 0) {
                        if (existingEnd.isAfter(earliest)) earliest = existingEnd;
                        earliest = earliest.plusMinutes(waitMinutes);
                        continue;
                    }
                    Long itemId = order.getProductId();
                    if (process.getOutputItemId() != null && !process.getOutputItemId().isBlank()) {
                        String[] output = process.getOutputItemId().split(",");
                        if (output.length != 1) throw new IllegalArgumentException("多产出工序请使用手工排产确认各产出数量");
                        itemId = Long.valueOf(output[0].trim());
                    }
                    var item = itemService.validateItemExists(itemId);
                    if (!Objects.equals(item.getStatus(), 0)) throw new IllegalArgumentException("工序产出产品已停用");
                    TaskPlan best = null;
                    for (var station : stations) {
                        if (!Objects.equals(station.getProcessId(), process.getProcessId())) continue;
                        Set<Long> machineIds = stationMachines.getOrDefault(station.getId(), Set.of());
                        if (unknownStations.contains(station.getId()) || machineIds.isEmpty()) continue;
                        if (unknownStations.stream().anyMatch(other -> !Collections.disjoint(machineIds, stationMachines.getOrDefault(other, Set.of())))) continue;
                        List<MesDvMachineryDO> machines = machineIds.stream().map(machineMap::get).filter(Objects::nonNull)
                                .filter(machine -> Objects.equals(machine.getStatus(), 1) || Objects.equals(machine.getStatus(), 2)).toList();
                        if (machines.isEmpty() || machines.stream().anyMatch(machine -> machine.getProductionEfficiency() == null || machine.getProductionEfficiency().signum() <= 0)) continue;
                        Set<Long> usableIds = machines.stream().map(MesDvMachineryDO::getId).collect(Collectors.toCollection(TreeSet::new));
                        if (machines.stream().anyMatch(machine -> Objects.equals(machine.getStatus(), 2) && existingBusy.stream().noneMatch(interval -> interval.machineryIds().contains(machine.getId())))) continue;
                        BigDecimal capacity = machines.stream().map(machine -> machine.getProductionEfficiency().multiply(BigDecimal.valueOf(
                                machine.getConveyorBelt() == null || machine.getConveyorBelt() <= 0 ? 1 : machine.getConveyorBelt())))
                                .reduce(BigDecimal.ZERO, BigDecimal::add);
                        MesProScheduleEngine.Slot slot;
                        try {
                            int hours = Math.addExact(MesProScheduleEngine.duration(remaining, capacity),
                                    Math.toIntExact(((long) prepareMinutes + 59) / 60));
                            slot = MesProScheduleEngine.findSlotHours(station.getId(), machineIds, hours, earliest, trialBusy, calendar);
                        } catch (IllegalArgumentException | ArithmeticException unavailable) {
                            continue; // ä¸€å°ä½Žäº§èƒ½å·¥ä½œç«™è¶…出排产窗口时,仍继续尝试其他工作站。
                        }
                        if (best == null || slot.end().isBefore(best.getEndTime())) {
                            best = new TaskPlan();
                            best.setWorkOrderId(id); best.setWorkOrderCode(order.getCode());
                            best.setRouteId(routeId); best.setRouteName(route.getName());
                            best.setProcessId(process.getProcessId()); best.setProcessName(processData.getName()); best.setSort(process.getSort());
                            best.setItemId(itemId); best.setItemName(item.getName()); best.setParamTemplateId(process.getParamTemplateId()); best.setColorCode(process.getColorCode());
                            best.setWorkstationId(station.getId()); best.setWorkstationName(station.getName());
                            best.setMachineryIds(new ArrayList<>(usableIds)); best.setMachineryNames(machines.stream().map(MesDvMachineryDO::getName).collect(Collectors.joining("、")));
                            best.setHourlyCapacity(capacity); best.setQuantity(remaining); best.setDuration(slot.hours()); best.setStartTime(slot.start()); best.setEndTime(slot.end());
                        }
                    }
                    if (best == null) throw new IllegalArgumentException("工序" + processData.getName() + "没有可用工作站及有效设备产能,或资源存在未知/超时任务,请先维护");
                    plans.add(best);
                    trialBusy.add(new MesProScheduleEngine.Busy(best.getWorkstationId(), stationMachines.getOrDefault(best.getWorkstationId(), Set.of()), best.getStartTime(), best.getEndTime()));
                    earliest = best.getEndTime().isAfter(existingEnd) ? best.getEndTime() : existingEnd;
                    earliest = earliest.plusMinutes(waitMinutes);
                }
                if (plans.isEmpty()) throw new IllegalArgumentException("所有工序已完成排产,不可重复提交");
                if (result.getTasks().size() + plans.size() > 200) throw new IllegalArgumentException("一次最多生成200个任务,请拆分工单批次排产");
                result.getTasks().addAll(plans); busy = trialBusy;
                orderPlan.setTaskCount(plans.size()); orderPlan.setEndTime(earliest); ends.put(id, earliest);
                if (order.getRequestDate() != null && earliest.toLocalDate().isAfter(order.getRequestDate().toLocalDate())) {
                    orderPlan.setLate(true); result.getWarnings().add("工单" + order.getCode() + "预计完成时间晚于交期");
                }
            } catch (ServiceException | IllegalArgumentException | ArithmeticException error) {
                orderPlan.setError((orderPlan.getWorkOrderCode() == null ? "工单#" + id : orderPlan.getWorkOrderCode()) + ":" + error.getMessage());
            }
        }
        result.setCanExecute(result.getOrders().stream().noneMatch(order -> order.getError() != null) && !result.getTasks().isEmpty());
        // ä¸åŒ…含生成时间;同一输入与业务数据应得到稳定的预览指纹。
        var fingerprint = new LinkedHashMap<String, Object>();
        fingerprint.put("startTime", request.getStartTime()); fingerprint.put("orders", result.getOrders());
        fingerprint.put("tasks", result.getTasks()); fingerprint.put("holidays", holidayDates.stream().sorted().toList());
        fingerprint.put("workOrders", orders.stream().sorted(Comparator.comparing(MesProWorkOrderDO::getId)).toList());
        fingerprint.put("paramTemplates", templateHashes);
        result.setPreviewHash(DigestUtil.sha256Hex(JsonUtils.toJsonString(fingerprint)));
        return result;
    }
    private List<MesProRouteProcessDO> processes(Long id, Long routeId) {
        var saved = workOrderProcessService.getWorkOrderProcessListByWorkOrderId(id);
        List<MesProRouteProcessDO> result = new ArrayList<>();
        if (!saved.isEmpty()) {
            for (var row : saved) result.add(new MesProRouteProcessDO().setProcessId(row.getProcessId()).setSort(row.getSort())
                    .setParamTemplateId(row.getParamTemplateId()).setYieldRate(row.getYieldRate()).setOutputItemId(row.getOutputItemId()).setColorCode(row.getColorCode())
                    .setPrepareTime(row.getPrepareTime()).setWaitTime(row.getWaitTime()).setNextProcessId(row.getNextProcessId()).setLinkType(row.getLinkType()));
        } else result.addAll(routeProcessService.getRouteProcessListByRouteId(routeId));
        result.sort(Comparator.comparing(MesProRouteProcessDO::getSort, Comparator.nullsLast(Comparator.naturalOrder())));
        return result;
    }
    /**
     * æŒ‰å·¥åºæ”¶çŽ‡ä»Žæœ«é“å¾€å‰å€’æŽ¨å„é“å·¥åºçš„ç›®æ ‡äº§å‡ºæ•°é‡ï¼šæœ«é“=订单量,前面各道除以后方工序的累计收率后向上取整;
     * æ”¶çŽ‡ä¸ºç©ºæŒ‰ 1.0 å¤„理。
     */
    private Map<Long, BigDecimal> calcTargetQuantities(List<MesProRouteProcessDO> processes, BigDecimal orderQuantity) {
        return MesProScheduleQuantityService.targetQuantities(processes, orderQuantity);
    }
    private void addChildrenFirst(Long id, Map<Long, MesProWorkOrderDO> orders, List<Long> sequence, Set<Long> visiting) {
        if (sequence.contains(id)) return;
        if (!visiting.add(id)) throw exception(PRO_TASK_SCHEDULE_INVALID, "工单父子关系存在循环,请先修复");
        orders.values().stream().filter(order -> Objects.equals(order.getParentId(), id)).sorted(Comparator.comparing(MesProWorkOrderDO::getId))
                .forEach(order -> addChildrenFirst(order.getId(), orders, sequence, visiting));
        visiting.remove(id); sequence.add(id);
    }
}