Добавил возможность решения конкфликтов при создании правил
This commit is contained in:
@@ -12,6 +12,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.DayOfWeek;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
@@ -20,6 +21,12 @@ import java.util.*;
|
||||
public class ScheduleRuleAdminController {
|
||||
|
||||
private static final String APPLY_MODE_DEFAULT = "DEFAULT";
|
||||
private static final int ACADEMIC_HOURS_PER_SLOT = 2;
|
||||
private static final Set<ScheduleParity> VALID_SLOT_PARITIES = Set.of(
|
||||
ScheduleParity.BOTH,
|
||||
ScheduleParity.ODD,
|
||||
ScheduleParity.EVEN
|
||||
);
|
||||
|
||||
private final ScheduleRuleRepository scheduleRuleRepository;
|
||||
private final SubjectRepository subjectRepository;
|
||||
@@ -111,14 +118,16 @@ public class ScheduleRuleAdminController {
|
||||
}
|
||||
|
||||
try {
|
||||
applyRule(rule, request);
|
||||
ScheduleRule conflictRule = findConflictingRule(request, rule.getSlots(), rule.getId());
|
||||
ScheduleRule candidate = new ScheduleRule();
|
||||
applyRule(candidate, request);
|
||||
ScheduleRule conflictRule = findConflictingRule(request, candidate.getSlots(), rule.getId());
|
||||
if (conflictRule != null) {
|
||||
return ResponseEntity.status(HttpStatus.CONFLICT).body(Map.of(
|
||||
"message", "Невозможно сохранить правило: слот занят",
|
||||
"conflictRule", toDto(conflictRule)
|
||||
));
|
||||
}
|
||||
applyRule(rule, request);
|
||||
ScheduleRule saved = scheduleRuleRepository.save(rule);
|
||||
scheduleGeneratorService.clearCache();
|
||||
return ResponseEntity.ok(toDto(saved));
|
||||
@@ -206,9 +215,13 @@ public class ScheduleRuleAdminController {
|
||||
}
|
||||
|
||||
private boolean hasSlotConflict(Collection<ScheduleRuleSlot> newSlots, Collection<ScheduleRuleSlot> existingSlots) {
|
||||
Map<ScheduleRuleSlot, Set<Integer>> newActiveWeeks = activeWeeksBySlot(newSlots);
|
||||
Map<ScheduleRuleSlot, Set<Integer>> existingActiveWeeks = activeWeeksBySlot(existingSlots);
|
||||
for (ScheduleRuleSlot newSlot : newSlots) {
|
||||
for (ScheduleRuleSlot existingSlot : existingSlots) {
|
||||
if (sameTimeSlot(newSlot, existingSlot) && sameTeacherOrClassroom(newSlot, existingSlot)) {
|
||||
if (sameTimeSlot(newSlot, existingSlot)
|
||||
&& activeWeeksOverlap(newActiveWeeks.get(newSlot), existingActiveWeeks.get(existingSlot))
|
||||
&& sameTeacherOrClassroomOrAudience(newSlot, existingSlot)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -218,21 +231,182 @@ public class ScheduleRuleAdminController {
|
||||
|
||||
private boolean sameTimeSlot(ScheduleRuleSlot first, ScheduleRuleSlot second) {
|
||||
return Objects.equals(first.getDayOfWeek(), second.getDayOfWeek())
|
||||
&& Objects.equals(first.getParity(), second.getParity())
|
||||
&& parityOverlaps(first.getParity(), second.getParity())
|
||||
&& Objects.equals(first.getTimeSlot().getId(), second.getTimeSlot().getId());
|
||||
}
|
||||
|
||||
private boolean sameTeacherOrClassroom(ScheduleRuleSlot first, ScheduleRuleSlot second) {
|
||||
private boolean parityOverlaps(ScheduleParity first, ScheduleParity second) {
|
||||
if (Objects.equals(first, second)) {
|
||||
return true;
|
||||
}
|
||||
return first == ScheduleParity.BOTH || second == ScheduleParity.BOTH;
|
||||
}
|
||||
|
||||
private boolean sameTeacherOrClassroomOrAudience(ScheduleRuleSlot first, ScheduleRuleSlot second) {
|
||||
return Objects.equals(first.getTeacher().getId(), second.getTeacher().getId())
|
||||
|| Objects.equals(first.getClassroom().getId(), second.getClassroom().getId());
|
||||
|| Objects.equals(first.getClassroom().getId(), second.getClassroom().getId())
|
||||
|| sameAudience(first, second);
|
||||
}
|
||||
|
||||
private boolean sameAudience(ScheduleRuleSlot first, ScheduleRuleSlot second) {
|
||||
Set<Long> firstGroupIds = groupIds(first);
|
||||
Set<Long> secondGroupIds = groupIds(second);
|
||||
firstGroupIds.retainAll(secondGroupIds);
|
||||
|
||||
for (Long groupId : firstGroupIds) {
|
||||
if (slotAppliesToWholeGroup(first, groupId) || slotAppliesToWholeGroup(second, groupId)) {
|
||||
return true;
|
||||
}
|
||||
Set<Long> firstSubgroupIds = subgroupIdsForGroup(first, groupId);
|
||||
Set<Long> secondSubgroupIds = subgroupIdsForGroup(second, groupId);
|
||||
firstSubgroupIds.retainAll(secondSubgroupIds);
|
||||
if (!firstSubgroupIds.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private Set<Long> groupIds(ScheduleRuleSlot slot) {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
if (slot.getScheduleRule() == null) {
|
||||
return ids;
|
||||
}
|
||||
for (StudentGroup group : slot.getScheduleRule().getGroups()) {
|
||||
ids.add(group.getId());
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private boolean slotAppliesToWholeGroup(ScheduleRuleSlot slot, Long groupId) {
|
||||
return subgroupIdsForGroup(slot, groupId).isEmpty();
|
||||
}
|
||||
|
||||
private Set<Long> subgroupIdsForGroup(ScheduleRuleSlot slot, Long groupId) {
|
||||
Set<Long> ids = new HashSet<>();
|
||||
for (Subgroup subgroup : slot.getSubgroups()) {
|
||||
if (subgroup.getStudentGroup() != null
|
||||
&& Objects.equals(subgroup.getStudentGroup().getId(), groupId)) {
|
||||
ids.add(subgroup.getId());
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private Map<ScheduleRuleSlot, Set<Integer>> activeWeeksBySlot(Collection<ScheduleRuleSlot> slots) {
|
||||
Map<ScheduleRuleSlot, Set<Integer>> activeWeeks = new IdentityHashMap<>();
|
||||
slots.forEach(slot -> activeWeeks.put(slot, new HashSet<>()));
|
||||
if (slots.isEmpty()) {
|
||||
return activeWeeks;
|
||||
}
|
||||
|
||||
ScheduleRule rule = slots.iterator().next().getScheduleRule();
|
||||
if (rule == null || rule.getSemester() == null) {
|
||||
return activeWeeks;
|
||||
}
|
||||
|
||||
int maxWeeks = maxWeeksForRule(rule);
|
||||
for (ScheduleLessonCategory category : ScheduleLessonCategory.values()) {
|
||||
int totalHours = rule.academicHoursFor(category);
|
||||
if (totalHours <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
List<ScheduleRuleSlot> categorySlots = slots.stream()
|
||||
.filter(slot -> ScheduleLessonCategory.fromLessonType(slot.getLessonType()) == category)
|
||||
.sorted(this::compareSlotsForGeneration)
|
||||
.toList();
|
||||
if (categorySlots.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Map<String, Integer> consumedHours = new HashMap<>();
|
||||
int startWeek = Math.max(1, rule.startWeekFor(category));
|
||||
for (int week = startWeek; week <= maxWeeks; week++) {
|
||||
for (ScheduleRuleSlot slot : categorySlots) {
|
||||
if (!slotAppliesToWeek(slot, week)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (String scopeKey : consumptionScopes(slot, category)) {
|
||||
int consumed = consumedHours.getOrDefault(scopeKey, 0);
|
||||
if (consumed >= totalHours) {
|
||||
continue;
|
||||
}
|
||||
activeWeeks.get(slot).add(week);
|
||||
consumedHours.put(scopeKey, consumed + ACADEMIC_HOURS_PER_SLOT);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return activeWeeks;
|
||||
}
|
||||
|
||||
private boolean activeWeeksOverlap(Set<Integer> first, Set<Integer> second) {
|
||||
if (first == null || second == null || first.isEmpty() || second.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
Set<Integer> weeks = new HashSet<>(first);
|
||||
weeks.retainAll(second);
|
||||
return !weeks.isEmpty();
|
||||
}
|
||||
|
||||
private int maxWeeksForRule(ScheduleRule rule) {
|
||||
Semester semester = rule.getSemester();
|
||||
if (semester != null && semester.getStartDate() != null && semester.getEndDate() != null) {
|
||||
long days = Math.max(1, ChronoUnit.DAYS.between(semester.getStartDate(), semester.getEndDate()) + 1);
|
||||
return Math.max(1, (int) Math.ceil(days / 7.0));
|
||||
}
|
||||
return 18;
|
||||
}
|
||||
|
||||
private int compareSlotsForGeneration(ScheduleRuleSlot first, ScheduleRuleSlot second) {
|
||||
int dayCompare = Integer.compare(
|
||||
first.getDayOfWeek() == null ? 0 : first.getDayOfWeek(),
|
||||
second.getDayOfWeek() == null ? 0 : second.getDayOfWeek()
|
||||
);
|
||||
if (dayCompare != 0) {
|
||||
return dayCompare;
|
||||
}
|
||||
int orderCompare = Integer.compare(
|
||||
first.getTimeSlot() == null || first.getTimeSlot().getOrderNumber() == null ? 0 : first.getTimeSlot().getOrderNumber(),
|
||||
second.getTimeSlot() == null || second.getTimeSlot().getOrderNumber() == null ? 0 : second.getTimeSlot().getOrderNumber()
|
||||
);
|
||||
if (orderCompare != 0) {
|
||||
return orderCompare;
|
||||
}
|
||||
return Long.compare(first.getId() == null ? 0L : first.getId(), second.getId() == null ? 0L : second.getId());
|
||||
}
|
||||
|
||||
private boolean slotAppliesToWeek(ScheduleRuleSlot slot, int week) {
|
||||
ScheduleParity parity = slot.getParity();
|
||||
if (parity == null || parity == ScheduleParity.BOTH) {
|
||||
return true;
|
||||
}
|
||||
ScheduleParity weekParity = week % 2 == 0 ? ScheduleParity.EVEN : ScheduleParity.ODD;
|
||||
return parity == weekParity;
|
||||
}
|
||||
|
||||
private List<String> consumptionScopes(ScheduleRuleSlot slot, ScheduleLessonCategory category) {
|
||||
if (category == ScheduleLessonCategory.LABORATORY && !slot.getSubgroups().isEmpty()) {
|
||||
return slot.getSubgroups().stream()
|
||||
.map(subgroup -> "subgroup:" + subgroup.getId())
|
||||
.toList();
|
||||
}
|
||||
if (slot.getScheduleRule() == null || slot.getScheduleRule().getGroups().isEmpty()) {
|
||||
return List.of("rule");
|
||||
}
|
||||
return slot.getScheduleRule().getGroups().stream()
|
||||
.map(group -> "group:" + group.getId())
|
||||
.toList();
|
||||
}
|
||||
|
||||
private ScheduleRuleSlot buildSlot(ScheduleRule rule, ScheduleRuleSlotDto slotDto) {
|
||||
if (slotDto.dayOfWeek() == null || slotDto.dayOfWeek() < 1 || slotDto.dayOfWeek() > 7) {
|
||||
throw new IllegalArgumentException("День недели должен быть от 1 до 7");
|
||||
}
|
||||
if (slotDto.parity() == null) {
|
||||
throw new IllegalArgumentException("Чётность недели обязательна");
|
||||
if (slotDto.parity() == null || !VALID_SLOT_PARITIES.contains(slotDto.parity())) {
|
||||
throw new IllegalArgumentException("Чётность недели должна быть: каждая, нечётная или чётная");
|
||||
}
|
||||
TimeSlot timeSlot = timeSlotRepository.findById(slotDto.timeSlotId())
|
||||
.orElseThrow(() -> new IllegalArgumentException("Временной слот не найден"));
|
||||
|
||||
Reference in New Issue
Block a user