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}, handler));
|
}
|
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());
|
}
|
}
|