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/test/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProScheduleGuardTest.java |  136 +++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 136 insertions(+), 0 deletions(-)

diff --git a/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProScheduleGuardTest.java b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProScheduleGuardTest.java
new file mode 100644
index 0000000..1bf6fbc
--- /dev/null
+++ b/yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/pro/task/schedule/MesProScheduleGuardTest.java
@@ -0,0 +1,136 @@
+package cn.iocoder.yudao.module.mes.service.pro.task.schedule;
+
+import cn.iocoder.yudao.framework.common.exception.ServiceException;
+import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkstationMachineDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.task.MesProTaskDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.*;
+import cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkstationMachineMapper;
+import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
+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.service.pro.task.MesProTaskServiceImpl;
+import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.util.ReflectionTestUtils;
+import java.lang.reflect.*;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.atomic.AtomicReference;
+import static org.junit.jupiter.api.Assertions.*;
+
+class MesProScheduleGuardTest {
+    private static <T> T stub(Class<T> type, InvocationHandler handler) {
+        return type.cast(Proxy.newProxyInstance(type.getClassLoader(), new Class<?>[]{type}, (proxy, method, args) -> {
+            if (method.getDeclaringClass() == Object.class) {
+                return switch (method.getName()) {
+                    case "toString" -> type.getSimpleName() + "Stub";
+                    case "hashCode" -> System.identityHashCode(proxy);
+                    case "equals" -> proxy == args[0];
+                    default -> throw new AssertionError(method.getName());
+                };
+            }
+            return handler.invoke(proxy, method, args);
+        }));
+    }
+    private final List<MesProTaskDO> tasks = new ArrayList<>();
+    private final MesProWorkOrderDO order = new MesProWorkOrderDO().setId(1L).setType(1).setQuantity(BigDecimal.valueOf(100));
+    private MesProTaskMapper mapper() {
+        return stub(MesProTaskMapper.class, (p,m,a) -> switch (m.getName()) {
+            case "selectListByWorkOrderId" -> tasks.stream().filter(task -> Objects.equals(task.getWorkOrderId(), a[0])).toList();
+            case "selectSchedulingTasks" -> tasks.stream().filter(task -> task.getStatus()!=4 && task.getStatus()!=5).toList();
+            default -> throw new AssertionError(m.getName());
+        });
+    }
+    private MesProScheduleGuardService guard() {
+        var guard = new MesProScheduleGuardService();
+        ReflectionTestUtils.setField(guard, "taskMapper", mapper());
+        ReflectionTestUtils.setField(guard, "quantityService", new MesProScheduleQuantityService() {
+            @Override public BigDecimal targetQuantity(MesProWorkOrderDO order, Long routeId, Long processId) {
+                return order.getQuantity();
+            }
+        });
+        ReflectionTestUtils.setField(guard, "workstationMachineMapper", stub(MesMdWorkstationMachineMapper.class, (p,m,a) -> List.of(
+                new MesMdWorkstationMachineDO().setWorkstationId(1L).setMachineryId(10L),
+                new MesMdWorkstationMachineDO().setWorkstationId(2L).setMachineryId(10L))));
+        ReflectionTestUtils.setField(guard, "machineryMapper", stub(MesDvMachineryMapper.class, (p,m,a) -> List.of()));
+        return guard;
+    }
+    private MesProTaskDO candidate() {
+        return new MesProTaskDO().setWorkOrderId(1L).setProcessId(1L).setWorkstationId(2L).setQuantity(BigDecimal.TEN)
+                .setStartTime(LocalDateTime.of(2026,10,15,9,0)).setEndTime(LocalDateTime.of(2026,10,15,10,0));
+    }
+
+    @Test void sharedEquipmentConflictIsCaughtForManualTask() {
+        tasks.add(candidate().setId(1L).setCode("EXISTING").setWorkOrderId(2L).setWorkstationId(1L).setStatus(0));
+        assertThrows(ServiceException.class, () -> guard().validate(order, candidate(), null));
+    }
+    @Test void boundaryTouchIsAllowed() {
+        tasks.add(candidate().setId(1L).setWorkOrderId(2L).setWorkstationId(1L).setStatus(0)
+                .setStartTime(LocalDateTime.of(2026,10,15,8,0)).setEndTime(LocalDateTime.of(2026,10,15,9,0)));
+        assertDoesNotThrow(() -> guard().validate(order, candidate(), null));
+    }
+    @Test void perProcessQuantityIncludesFinishedAndIgnoresCanceled() {
+        tasks.add(candidate().setId(1L).setQuantity(BigDecimal.valueOf(95)).setStatus(4));
+        assertThrows(ServiceException.class, () -> guard().validate(order, candidate(), null));
+        tasks.getFirst().setStatus(5);
+        assertDoesNotThrow(() -> guard().validate(order, candidate(), null));
+    }
+    @Test void editExcludesItselfAndIncompleteTimesAreRejected() {
+        tasks.add(candidate().setId(1L).setQuantity(BigDecimal.valueOf(100)).setStatus(0));
+        assertDoesNotThrow(() -> guard().validate(order, candidate(), 1L));
+        assertThrows(ServiceException.class, () -> guard().validate(order, candidate().setEndTime(null), 1L));
+    }
+    @Test void invalidHistoricalQuantityBlocksExtraScheduling() {
+        tasks.add(candidate().setId(1L).setQuantity(null).setStatus(4));
+        assertThrows(ServiceException.class, () -> guard().validate(order, candidate(), null));
+    }
+    @Test void schedulingLockRequiresTransaction() {
+        assertThrows(IllegalStateException.class, () -> guard().lock());
+    }
+    private MesProScheduleGuardService yieldGuard() {
+        var guard = guard();
+        var quantities = new MesProScheduleQuantityService();
+        ReflectionTestUtils.setField(quantities, "workOrderProcessService", stub(MesProWorkOrderProcessService.class, (p,m,a) -> List.of(
+                new MesProWorkOrderProcessDO().setProcessId(3L).setSort(30).setYieldRate(new BigDecimal("0.98")),
+                new MesProWorkOrderProcessDO().setProcessId(1L).setSort(10).setYieldRate(new BigDecimal("0.98")),
+                new MesProWorkOrderProcessDO().setProcessId(2L).setSort(20).setYieldRate(new BigDecimal("0.98")))));
+        ReflectionTestUtils.setField(guard, "quantityService", quantities);
+        order.setQuantity(new BigDecimal("200"));
+        return guard;
+    }
+    @Test void yieldAdjustedPreviewQuantitiesCanBeWrittenButExcessIsRejected() {
+        var guard = yieldGuard();
+        for (int i = 0; i < 3; i++) {
+            long processId = i + 1;
+            BigDecimal quantity = BigDecimal.valueOf(new int[]{209,205,200}[i]);
+            assertDoesNotThrow(() -> guard.validate(order, candidate().setProcessId(processId).setQuantity(quantity), null));
+            assertThrows(ServiceException.class, () -> guard.validate(order, candidate().setProcessId(processId).setQuantity(quantity.add(BigDecimal.ONE)), null));
+        }
+    }
+    @Test void yieldAdjustedLimitIncludesSplitFinishedTasksAndExcludesCanceledAndEditedTask() {
+        var guard = yieldGuard();
+        tasks.add(candidate().setId(1L).setQuantity(BigDecimal.valueOf(100)).setStatus(4));
+        tasks.add(candidate().setId(2L).setQuantity(BigDecimal.valueOf(209)).setStatus(5));
+        assertDoesNotThrow(() -> guard.validate(order, candidate().setQuantity(BigDecimal.valueOf(109)), null));
+        assertThrows(ServiceException.class, () -> guard.validate(order, candidate().setQuantity(BigDecimal.valueOf(110)), null));
+        assertDoesNotThrow(() -> guard.validate(order, candidate().setQuantity(BigDecimal.valueOf(209)), 1L));
+    }
+    @Test void aggregateScheduledQuantityUsesMinimumAcrossProcessesAndFallsAfterDeletion() {
+        var service = new MesProTaskServiceImpl();
+        tasks.add(candidate().setId(1L).setQuantity(BigDecimal.valueOf(100)).setStatus(0));
+        tasks.add(candidate().setId(2L).setProcessId(2L).setQuantity(BigDecimal.valueOf(40)).setStatus(0));
+        tasks.add(candidate().setId(3L).setProcessId(2L).setQuantity(BigDecimal.valueOf(20)).setStatus(4));
+        tasks.add(candidate().setId(4L).setProcessId(2L).setQuantity(BigDecimal.valueOf(30)).setStatus(5));
+        var saved = new AtomicReference<MesProWorkOrderDO>();
+        ReflectionTestUtils.setField(service, "taskMapper", mapper());
+        ReflectionTestUtils.setField(service, "workOrderProcessService", stub(MesProWorkOrderProcessService.class, (p,m,a) -> List.of(
+                new MesProWorkOrderProcessDO().setProcessId(1L), new MesProWorkOrderProcessDO().setProcessId(2L))));
+        ReflectionTestUtils.setField(service, "scheduleWorkOrderMapper", stub(MesProWorkOrderMapper.class, (p,m,a) -> { saved.set((MesProWorkOrderDO)a[0]); return 1; }));
+        ReflectionTestUtils.invokeMethod(service, "synchronizeScheduledQuantity", order, 100L);
+        assertEquals(BigDecimal.valueOf(60), saved.get().getQuantityScheduled());
+        tasks.removeIf(task -> task.getProcessId()==2L);
+        ReflectionTestUtils.invokeMethod(service, "synchronizeScheduledQuantity", order, 100L);
+        assertEquals(BigDecimal.ZERO, saved.get().getQuantityScheduled());
+    }
+}

--
Gitblit v1.9.3