| 对比新文件 |
| | |
| | | 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()); |
| | | } |
| | | } |