package cn.iocoder.yudao.module.mes.service.pro.task.schedule;
|
|
import java.math.BigDecimal;
|
import java.math.RoundingMode;
|
import java.time.*;
|
import java.util.*;
|
|
/** 纯计算:日班09–18点,排除周末和节假日,工作站及共享设备不重叠。 */
|
public final class MesProScheduleEngine {
|
private MesProScheduleEngine() {}
|
|
public record Calendar(Set<LocalDate> holidays, LocalDate horizon) {
|
public boolean isWorkingDay(LocalDate date) {
|
return date.getDayOfWeek() != DayOfWeek.SATURDAY
|
&& date.getDayOfWeek() != DayOfWeek.SUNDAY && !holidays.contains(date);
|
}
|
|
public LocalDateTime nextWorkingTime(LocalDateTime value) {
|
LocalDateTime result = value.withNano(0);
|
while (!result.toLocalDate().isAfter(horizon)) {
|
if (!isWorkingDay(result.toLocalDate()) || !result.toLocalTime().isBefore(LocalTime.of(18, 0))) {
|
result = result.toLocalDate().plusDays(1).atTime(9, 0);
|
} else if (result.toLocalTime().isBefore(LocalTime.of(9, 0))) {
|
return result.toLocalDate().atTime(9, 0);
|
} else {
|
return result;
|
}
|
}
|
throw new IllegalArgumentException("未来一年内没有足够可用工作时间,请调整排产配置");
|
}
|
|
public LocalDateTime endTime(LocalDateTime start, int hours) {
|
long seconds = Math.multiplyExact((long) hours, 3600L);
|
LocalDateTime current = nextWorkingTime(start);
|
while (seconds > 0) {
|
LocalDateTime close = current.toLocalDate().atTime(18, 0);
|
long available = Duration.between(current, close).getSeconds();
|
if (seconds <= available) return current.plusSeconds(seconds);
|
seconds -= available;
|
current = nextWorkingTime(close);
|
}
|
return current;
|
}
|
}
|
|
public record Busy(Long workstationId, Set<Long> machineryIds, LocalDateTime start, LocalDateTime end) {}
|
public record Slot(LocalDateTime start, LocalDateTime end, int hours) {}
|
|
public static int duration(BigDecimal quantity, BigDecimal capacity) {
|
if (quantity == null || quantity.signum() <= 0 || capacity == null || capacity.signum() <= 0) {
|
throw new IllegalArgumentException("排产数量和设备小时产能必须大于0");
|
}
|
return quantity.divide(capacity, 0, RoundingMode.CEILING).intValueExact();
|
}
|
|
public static boolean sharesResources(Long station, Set<Long> machines, Busy busy) {
|
return Objects.equals(station, busy.workstationId())
|
|| !Collections.disjoint(machines, busy.machineryIds());
|
}
|
|
public static boolean overlaps(LocalDateTime start, LocalDateTime end, Busy busy) {
|
return start.isBefore(busy.end()) && end.isAfter(busy.start());
|
}
|
|
public static Slot findSlot(Long station, Set<Long> machines, BigDecimal quantity, BigDecimal capacity,
|
LocalDateTime earliest, List<Busy> busy, Calendar calendar) {
|
int hours = duration(quantity, capacity);
|
return findSlotHours(station, machines, hours, earliest, busy, calendar);
|
}
|
|
public static Slot findSlotHours(Long station, Set<Long> machines, int hours,
|
LocalDateTime earliest, List<Busy> busy, Calendar calendar) {
|
if (hours <= 0) throw new IllegalArgumentException("生产工时必须大于0");
|
LocalDateTime start = calendar.nextWorkingTime(earliest);
|
// 每次至少跳过一个占用区间,避免无限尝试。
|
for (int i = 0; i <= busy.size(); i++) {
|
LocalDateTime end = calendar.endTime(start, hours);
|
LocalDateTime conflictEnd = null;
|
for (Busy interval : busy) {
|
if (sharesResources(station, machines, interval) && overlaps(start, end, interval)) {
|
if (conflictEnd == null || interval.end().isAfter(conflictEnd)) conflictEnd = interval.end();
|
}
|
}
|
if (conflictEnd == null) return new Slot(start, end, hours);
|
start = calendar.nextWorkingTime(conflictEnd);
|
}
|
throw new IllegalArgumentException("无法找到无冲突时段,请检查已有任务计划");
|
}
|
}
|