From 74b0d9c5ab1dc316b29f8525dcffe45225d4c43c Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期五, 16 十月 2026 10:29:23 +0800
Subject: [PATCH] Merge branch 'dev_海西州_惠牧' into dev_business

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProBatchScheduleService.java |  349 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 349 insertions(+), 0 deletions(-)

diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProBatchScheduleService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProBatchScheduleService.java
new file mode 100644
index 0000000..936c385
--- /dev/null
+++ b/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("褰撳墠宸ュ簭宸叉湁浠诲姟鏁伴噺鏃犳晥锛岃鍏堜慨姝e巻鍙蹭换鍔℃暟閲�");
+                    }
+                    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; // 涓�鍙颁綆浜ц兘宸ヤ綔绔欒秴鍑烘帓浜х獥鍙f椂锛屼粛缁х画灏濊瘯鍏朵粬宸ヤ綔绔欍��
+                        }
+                        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);
+    }
+}

--
Gitblit v1.9.3