From bc31cda78817c62aa552be40f39eccaa03262923 Mon Sep 17 00:00:00 2001
From: xiaoyi <908147524@qq.com>
Date: 星期六, 07 十一月 2026 01:21:22 +0800
Subject: [PATCH] feat: 补齐排产约束与缺料校验,新增风险监测,优化隐患通知与招投标联动

---
 yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProBatchScheduleService.java |   59 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
 1 files changed, 56 insertions(+), 3 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
index c32c99d..1faa949 100644
--- 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
@@ -14,6 +14,13 @@
 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.dal.mysql.pro.workorder.MesProWorkOrderBomMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.materialstock.MesWmMaterialStockMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.wm.warehouse.MesWmWarehouseMapper;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.materialstock.MesWmMaterialStockDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.wm.warehouse.MesWmWarehouseDO;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
 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;
@@ -42,6 +49,9 @@
 public class MesProBatchScheduleService {
     @Resource private MesProTaskMapper taskMapper;
     @Resource private MesProWorkOrderMapper workOrderMapper;
+    @Resource private MesProWorkOrderBomMapper workOrderBomMapper;
+    @Resource private MesWmMaterialStockMapper materialStockMapper;
+    @Resource private MesWmWarehouseMapper warehouseMapper;
     @Resource private MesDvMachineryMapper machineryMapper;
     @Resource private MesMdWorkstationService workstationService;
     @Resource private MesProWorkOrderProcessService workOrderProcessService;
@@ -128,6 +138,34 @@
         result.setPreviewTime(now());
         var orders = workOrderMapper.selectByIds(request.getWorkOrderIds());
         var orderMap = orders.stream().collect(Collectors.toMap(MesProWorkOrderDO::getId, order -> order));
+        var bomByOrder = new HashMap<Long, List<MesProWorkOrderBomDO>>();
+        for (var order : orders) bomByOrder.put(order.getId(), workOrderBomMapper.selectListByWorkOrderId(order.getId()).stream()
+                .sorted(Comparator.comparing(MesProWorkOrderBomDO::getId)).toList());
+        var materialIds = bomByOrder.values().stream().flatMap(Collection::stream).map(MesProWorkOrderBomDO::getItemId)
+                .filter(Objects::nonNull).collect(Collectors.toSet());
+        var warehouses = warehouseMapper.selectList().stream().collect(Collectors.toMap(MesWmWarehouseDO::getId, w -> w));
+        var stocks = materialIds.isEmpty() ? List.<MesWmMaterialStockDO>of() : materialStockMapper.selectList(
+                new LambdaQueryWrapperX<MesWmMaterialStockDO>().in(MesWmMaterialStockDO::getItemId, materialIds));
+        Map<Long, BigDecimal> availableStock = new HashMap<>();
+        for (var stock : stocks) {
+            var warehouse = warehouses.get(stock.getWarehouseId());
+            if (warehouse == null || Boolean.TRUE.equals(warehouse.getFrozen()) || Boolean.TRUE.equals(stock.getFrozen())
+                    || MesWmWarehouseDO.WIP_VIRTUAL_WAREHOUSE.equals(warehouse.getCode())) continue;
+            var available = Optional.ofNullable(stock.getQuantity()).orElse(BigDecimal.ZERO)
+                    .subtract(Optional.ofNullable(stock.getFrozenQuantity()).orElse(BigDecimal.ZERO))
+                    .subtract(Optional.ofNullable(stock.getReservedQuantity()).orElse(BigDecimal.ZERO)).max(BigDecimal.ZERO);
+            availableStock.merge(stock.getItemId(), available, BigDecimal::add);
+        }
+        var materialInfo = materialIds.isEmpty() ? Map.<Long, cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO>of()
+                : itemService.getItemMap(materialIds);
+        for (Long itemId : materialIds) {
+            var item = materialInfo.get(itemId);
+            if (item != null && Boolean.TRUE.equals(item.getSafeStockFlag()) && item.getMinStock() != null) {
+                availableStock.compute(itemId, (key, value) -> Optional.ofNullable(value).orElse(BigDecimal.ZERO)
+                        .subtract(item.getMinStock().max(BigDecimal.ZERO)).max(BigDecimal.ZERO));
+            }
+        }
+        var stockSnapshot = new TreeMap<>(availableStock);
         var stations = workstationService.getWorkstationListByStatus(0).stream()
                 .sorted(Comparator.comparing(MesMdWorkstationDO::getId)).toList();
         var machineMap = machineryMapper.selectList().stream().collect(Collectors.toMap(MesDvMachineryDO::getId, machine -> machine));
@@ -154,8 +192,7 @@
                         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();
+        var sorted = orders.stream().sorted(MesSchedulePriority.comparator()).toList();
         List<Long> orderSequence = new ArrayList<>();
         Set<Long> visiting = new HashSet<>();
         for (var order : sorted) addChildrenFirst(order.getId(), orderMap, orderSequence, visiting);
@@ -182,6 +219,20 @@
                         || (order.getQuantityProduced() != null && order.getQuantityProduced().compareTo(order.getQuantity()) >= 0)) {
                     throw new IllegalArgumentException("宸ュ崟鏁伴噺鏃犳晥鎴栧凡鐢熶骇瀹屾垚");
                 }
+                var boms = bomByOrder.getOrDefault(id, List.of());
+                if (boms.isEmpty()) throw new IllegalArgumentException("宸ュ崟娌℃湁 BOM锛屾棤娉曟牳瀵圭墿鏂欓綈濂楋紝璇峰厛缁存姢");
+                BigDecimal unscheduled = order.getQuantity().subtract(Optional.ofNullable(order.getQuantityScheduled()).orElse(BigDecimal.ZERO));
+                if (unscheduled.signum() <= 0) throw new IllegalArgumentException("宸ュ崟宸插畬鎴愭帓浜�");
+                Map<Long, BigDecimal> demand = new HashMap<>();
+                for (var bom : boms) {
+                    if (bom.getItemId() == null || bom.getQuantity() == null || bom.getQuantity().signum() <= 0)
+                        throw new IllegalArgumentException("宸ュ崟 BOM 鐗╂枡鎴栭渶姹傛暟閲忔棤鏁堬紝璇峰厛缁存姢");
+                    var requiredItem = materialInfo.get(bom.getItemId());
+                    if (requiredItem == null || !Objects.equals(requiredItem.getStatus(), 0))
+                        throw new IllegalArgumentException("宸ュ崟 BOM 瀛樺湪宸插垹闄ゆ垨鍋滅敤鐨勭墿鏂欙紝璇峰厛缁存姢");
+                    demand.merge(bom.getItemId(), bom.getQuantity().multiply(unscheduled).divide(order.getQuantity(), 6, RoundingMode.UP), BigDecimal::add);
+                }
+                var trialStock = MesScheduleMaterialCheck.allocate(availableStock, demand);
                 LocalDateTime earliest = calendar.nextWorkingTime(request.getStartTime());
                 // 鐖跺伐鍗曞繀椤荤瓑瀛愬伐鍗曪紱鍚屼竴鎵规鎸変緷璧栭『搴忔帓锛屾湭绾冲叆鐨勬湭瀹屾垚瀛愬伐鍗曢樆姝㈡帓浜с��
                 for (var child : workOrderMapper.selectList(MesProWorkOrderDO::getParentId, id)) {
@@ -300,7 +351,7 @@
                 }
                 if (plans.isEmpty()) throw new IllegalArgumentException("鎵�鏈夊伐搴忓凡瀹屾垚鎺掍骇锛屼笉鍙噸澶嶆彁浜�");
                 if (result.getTasks().size() + plans.size() > 200) throw new IllegalArgumentException("涓�娆℃渶澶氱敓鎴�200涓换鍔★紝璇锋媶鍒嗗伐鍗曟壒娆℃帓浜�");
-                result.getTasks().addAll(plans); busy = trialBusy;
+                result.getTasks().addAll(plans); busy = trialBusy; availableStock = trialStock;
                 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() + "棰勮瀹屾垚鏃堕棿鏅氫簬浜ゆ湡");
@@ -316,6 +367,8 @@
         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);
+        fingerprint.put("materialStock", stockSnapshot);
+        fingerprint.put("workOrderBoms", new TreeMap<>(bomByOrder));
         result.setPreviewHash(DigestUtil.sha256Hex(JsonUtils.toJsonString(fingerprint)));
         return result;
     }

--
Gitblit v1.9.3