подправил создание правил расписания

This commit is contained in:
Zuev
2026-05-08 13:41:25 +03:00
parent e12a5bcccd
commit 02c5ea9118
20 changed files with 592 additions and 126 deletions

View File

@@ -191,7 +191,7 @@ public class ScheduleRuleAdminController {
LessonType lessonType = lessonTypesRepository.findById(slotDto.lessonTypeId())
.orElseThrow(() -> new IllegalArgumentException("Тип занятия не найден"));
ScheduleLessonCategory category = ScheduleLessonCategory.fromLessonType(lessonType);
Subgroup subgroup = resolveSubgroup(rule, slotDto.subgroupId(), category);
Set<Subgroup> subgroups = resolveSubgroups(rule, slotDto, category);
if (slotDto.lessonFormat() == null || slotDto.lessonFormat().isBlank()) {
throw new IllegalArgumentException("Формат занятия обязателен");
}
@@ -201,7 +201,7 @@ public class ScheduleRuleAdminController {
slot.setDayOfWeek(slotDto.dayOfWeek());
slot.setParity(slotDto.parity());
slot.setTimeSlot(timeSlot);
slot.setSubgroup(subgroup);
slot.setSubgroups(subgroups);
slot.setTeacher(teacher);
slot.setClassroom(classroom);
slot.setLessonType(lessonType);
@@ -209,21 +209,44 @@ public class ScheduleRuleAdminController {
return slot;
}
private Subgroup resolveSubgroup(ScheduleRule rule, Long subgroupId, ScheduleLessonCategory category) {
if (subgroupId == null) {
return null;
private Set<Subgroup> resolveSubgroups(ScheduleRule rule, ScheduleRuleSlotDto slotDto, ScheduleLessonCategory category) {
LinkedHashSet<Long> subgroupIds = new LinkedHashSet<>();
if (slotDto.subgroupIds() != null) {
slotDto.subgroupIds().stream()
.filter(Objects::nonNull)
.forEach(subgroupIds::add);
}
if (slotDto.subgroupId() != null) {
subgroupIds.add(slotDto.subgroupId());
}
if (subgroupIds.isEmpty()) {
return new LinkedHashSet<>();
}
if (category != ScheduleLessonCategory.LABORATORY) {
throw new IllegalArgumentException("Подгруппы можно выбирать только для лабораторных занятий");
}
Subgroup subgroup = subgroupRepository.findById(subgroupId)
.orElseThrow(() -> new IllegalArgumentException("Подгруппа не найдена"));
boolean subgroupBelongsToRuleGroups = rule.getGroups().stream()
.anyMatch(group -> group.getId().equals(subgroup.getStudentGroup().getId()));
if (!subgroupBelongsToRuleGroups) {
throw new IllegalArgumentException("Подгруппа должна относиться к одной из групп правила");
Set<Long> ruleGroupIds = rule.getGroups().stream()
.map(StudentGroup::getId)
.collect(java.util.stream.Collectors.toSet());
Set<Long> subgroupGroupIds = new HashSet<>();
LinkedHashSet<Subgroup> subgroups = new LinkedHashSet<>();
for (Long subgroupId : subgroupIds) {
Subgroup subgroup = subgroupRepository.findById(subgroupId)
.orElseThrow(() -> new IllegalArgumentException("Подгруппа не найдена"));
Long subgroupGroupId = subgroup.getStudentGroup().getId();
if (!ruleGroupIds.contains(subgroupGroupId)) {
throw new IllegalArgumentException("Подгруппа должна относиться к одной из групп правила");
}
if (!subgroupGroupIds.add(subgroupGroupId)) {
throw new IllegalArgumentException("В одном слоте можно выбрать не больше одной подгруппы каждой группы");
}
subgroups.add(subgroup);
}
return subgroup;
return subgroups;
}
private String validateTypeHours(ScheduleRuleDto request) {
@@ -318,6 +341,7 @@ public class ScheduleRuleAdminController {
private ScheduleRuleSlotDto toSlotDto(ScheduleRuleSlot slot) {
TimeSlot timeSlot = slot.getTimeSlot();
List<Subgroup> sortedSubgroups = sortedSubgroups(slot);
return new ScheduleRuleSlotDto(
slot.getId(),
slot.getDayOfWeek(),
@@ -326,8 +350,10 @@ public class ScheduleRuleAdminController {
timeSlot.getId(),
timeSlot.getOrderNumber(),
formatTimeSlot(timeSlot),
slot.getSubgroupId(),
slot.getSubgroup() == null ? null : slot.getSubgroup().getName(),
sortedSubgroups.isEmpty() ? null : sortedSubgroups.get(0).getId(),
sortedSubgroups.isEmpty() ? null : formatSubgroup(sortedSubgroups.get(0)),
sortedSubgroups.stream().map(Subgroup::getId).toList(),
sortedSubgroups.stream().map(this::formatSubgroup).toList(),
slot.getTeacher().getId(),
ScheduleGeneratorService.displayUserName(slot.getTeacher()),
slot.getClassroom().getId(),
@@ -348,4 +374,16 @@ public class ScheduleRuleAdminController {
private String formatTimeSlot(TimeSlot timeSlot) {
return timeSlot.getStartTime() + " - " + timeSlot.getEndTime();
}
private String formatSubgroup(Subgroup subgroup) {
return subgroup.getStudentGroup().getName() + ": " + subgroup.getName();
}
private List<Subgroup> sortedSubgroups(ScheduleRuleSlot slot) {
return slot.getSubgroups().stream()
.sorted(Comparator
.comparing((Subgroup subgroup) -> subgroup.getStudentGroup().getName())
.thenComparing(Subgroup::getName))
.toList();
}
}

View File

@@ -29,6 +29,8 @@ public record RenderedLessonDto(
String lessonFormat,
Long subgroupId,
String subgroupName,
List<Long> subgroupIds,
List<String> subgroupNames,
List<Long> groupIds,
List<String> groupNames,
String activityType,

View File

@@ -2,6 +2,8 @@ package com.magistr.app.dto;
import com.magistr.app.model.ScheduleParity;
import java.util.List;
public record ScheduleRuleSlotDto(
Long id,
Integer dayOfWeek,
@@ -12,6 +14,8 @@ public record ScheduleRuleSlotDto(
String timeSlotLabel,
Long subgroupId,
String subgroupName,
List<Long> subgroupIds,
List<String> subgroupNames,
Long teacherId,
String teacherName,
Long classroomId,

View File

@@ -2,6 +2,10 @@ package com.magistr.app.model;
import jakarta.persistence.*;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
@Entity
@Table(name = "schedule_rule_slots")
public class ScheduleRuleSlot {
@@ -25,9 +29,13 @@ public class ScheduleRuleSlot {
@JoinColumn(name = "time_slot_id", nullable = false)
private TimeSlot timeSlot;
@ManyToOne
@JoinColumn(name = "subgroup_id")
private Subgroup subgroup;
@ManyToMany
@JoinTable(
name = "schedule_rule_slot_subgroups",
joinColumns = @JoinColumn(name = "schedule_rule_slot_id"),
inverseJoinColumns = @JoinColumn(name = "subgroup_id")
)
private Set<Subgroup> subgroups = new LinkedHashSet<>();
@ManyToOne(optional = false)
@JoinColumn(name = "teacher_id", nullable = false)
@@ -85,15 +93,41 @@ public class ScheduleRuleSlot {
}
public Long getSubgroupId() {
return subgroup == null ? null : subgroup.getId();
return subgroups.stream().findFirst().map(Subgroup::getId).orElse(null);
}
public Subgroup getSubgroup() {
return subgroup;
return subgroups.stream().findFirst().orElse(null);
}
public void setSubgroup(Subgroup subgroup) {
this.subgroup = subgroup;
subgroups.clear();
if (subgroup != null) {
subgroups.add(subgroup);
}
}
public Set<Subgroup> getSubgroups() {
return subgroups;
}
public void setSubgroups(Set<Subgroup> subgroups) {
this.subgroups.clear();
if (subgroups != null) {
this.subgroups.addAll(subgroups);
}
}
public List<Long> getSubgroupIds() {
return subgroups.stream()
.map(Subgroup::getId)
.toList();
}
public List<String> getSubgroupNames() {
return subgroups.stream()
.map(Subgroup::getName)
.toList();
}
public User getTeacher() {

View File

@@ -19,8 +19,8 @@ public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long
left join fetch r.groups groups
left join fetch r.slots slots
left join fetch slots.timeSlot
left join fetch slots.subgroup subgroup
left join fetch subgroup.studentGroup
left join fetch slots.subgroups slotSubgroups
left join fetch slotSubgroups.studentGroup
left join fetch slots.teacher
left join fetch slots.classroom
left join fetch slots.lessonType
@@ -42,8 +42,8 @@ public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long
left join fetch r.groups groups
left join fetch r.slots slots
left join fetch slots.timeSlot
left join fetch slots.subgroup subgroup
left join fetch subgroup.studentGroup
left join fetch slots.subgroups slotSubgroups
left join fetch slotSubgroups.studentGroup
left join fetch slots.teacher
left join fetch slots.classroom
left join fetch slots.lessonType
@@ -64,8 +64,8 @@ public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long
left join fetch r.groups groups
left join fetch r.slots slots
left join fetch slots.timeSlot
left join fetch slots.subgroup subgroup
left join fetch subgroup.studentGroup
left join fetch slots.subgroups slotSubgroups
left join fetch slotSubgroups.studentGroup
left join fetch slots.teacher
left join fetch slots.classroom
left join fetch slots.lessonType
@@ -82,8 +82,8 @@ public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long
left join fetch r.groups groups
left join fetch r.slots slots
left join fetch slots.timeSlot
left join fetch slots.subgroup subgroup
left join fetch subgroup.studentGroup
left join fetch slots.subgroups slotSubgroups
left join fetch slotSubgroups.studentGroup
left join fetch slots.teacher
left join fetch slots.classroom
left join fetch slots.lessonType

View File

@@ -7,6 +7,6 @@ import org.springframework.data.repository.query.Param;
public interface ScheduleRuleSlotRepository extends JpaRepository<ScheduleRuleSlot, Long> {
@Query("select case when count(slot) > 0 then true else false end from ScheduleRuleSlot slot where slot.subgroup.id = :subgroupId")
@Query("select case when count(slot) > 0 then true else false end from ScheduleRuleSlot slot join slot.subgroups subgroup where subgroup.id = :subgroupId")
boolean existsBySubgroupId(@Param("subgroupId") Long subgroupId);
}

View File

@@ -201,22 +201,54 @@ public class ScheduleGeneratorService {
if (totalHours <= 0) {
continue;
}
List<StudentGroup> lessonGroups = groupsForSlot(slot, eligibleGroups);
if (lessonGroups.isEmpty()) {
List<LessonScope> lessonScopes = lessonScopesForSlot(slot, category, eligibleGroups);
if (lessonScopes.isEmpty()) {
continue;
}
String consumptionBucket = consumptionBucket(category, slot);
int consumedHours = consumedHoursByBucket.computeIfAbsent(
consumptionBucket,
key -> calculateConsumedHoursBeforeDate(rule, targetGroup, date, category, slot.getSubgroupId())
);
if (consumedHours >= totalHours) {
List<ActiveLessonScope> activeScopes = new ArrayList<>();
for (LessonScope scope : lessonScopes) {
String consumptionBucket = consumptionBucket(category, scope.subgroupId());
int consumedHours = consumedHoursByBucket.computeIfAbsent(
consumptionBucket,
key -> calculateConsumedHoursBeforeDate(rule, targetGroup, date, category, scope.subgroupId())
);
if (consumedHours >= totalHours) {
continue;
}
int remainingAfterLesson = Math.max(0, totalHours - consumedHours - ACADEMIC_HOURS_PER_SLOT);
activeScopes.add(new ActiveLessonScope(scope, consumedHours, remainingAfterLesson));
}
if (activeScopes.isEmpty()) {
continue;
}
int remainingAfterLesson = Math.max(0, totalHours - consumedHours - ACADEMIC_HOURS_PER_SLOT);
result.add(toRenderedLesson(rule, slot, date, weekNumber, dateParity, lessonGroups,
Map<Long, StudentGroup> lessonGroupById = new LinkedHashMap<>();
Map<Long, Subgroup> lessonSubgroupById = new LinkedHashMap<>();
activeScopes.forEach(scope -> {
scope.scope().groups().forEach(group -> lessonGroupById.putIfAbsent(group.getId(), group));
scope.scope().subgroups().forEach(subgroup -> lessonSubgroupById.putIfAbsent(subgroup.getId(), subgroup));
});
List<StudentGroup> lessonGroups = new ArrayList<>(lessonGroupById.values());
List<Subgroup> lessonSubgroups = new ArrayList<>(lessonSubgroupById.values());
int consumedHours = activeScopes.stream()
.mapToInt(ActiveLessonScope::consumedHours)
.min()
.orElse(0);
int remainingAfterLesson = activeScopes.stream()
.mapToInt(ActiveLessonScope::remainingAfterLesson)
.min()
.orElse(0);
result.add(toRenderedLesson(rule, slot, date, weekNumber, dateParity, lessonGroups, lessonSubgroups,
totalHours, consumedHours, remainingAfterLesson));
consumedHoursByBucket.put(consumptionBucket, consumedHours + ACADEMIC_HOURS_PER_SLOT);
for (ActiveLessonScope scope : activeScopes) {
consumedHoursByBucket.put(
consumptionBucket(category, scope.scope().subgroupId()),
scope.consumedHours() + ACADEMIC_HOURS_PER_SLOT
);
}
}
}
@@ -272,8 +304,8 @@ public class ScheduleGeneratorService {
.filter(slot -> Objects.equals(slot.getDayOfWeek(), dayOfWeek))
.filter(slot -> slot.getParity() == ScheduleParity.BOTH || slot.getParity() == dateParity)
.filter(slot -> ScheduleLessonCategory.fromLessonType(slot.getLessonType()) == category)
.filter(slot -> Objects.equals(slot.getSubgroupId(), subgroupId))
.filter(slot -> !groupsForSlot(slot, eligibleGroups).isEmpty())
.flatMap(slot -> lessonScopesForSlot(slot, category, eligibleGroups).stream())
.filter(scope -> Objects.equals(scope.subgroupId(), subgroupId))
.count();
consumed += (int) activeSlots * ACADEMIC_HOURS_PER_SLOT;
if (consumed >= rule.academicHoursFor(category)) {
@@ -283,26 +315,51 @@ public class ScheduleGeneratorService {
return consumed;
}
private String consumptionBucket(ScheduleLessonCategory category, ScheduleRuleSlot slot) {
return category.name() + ":" + (slot.getSubgroupId() == null ? "whole" : slot.getSubgroupId());
private String consumptionBucket(ScheduleLessonCategory category, Long subgroupId) {
return category.name() + ":" + (subgroupId == null ? "whole" : subgroupId);
}
private List<StudentGroup> groupsForSlot(ScheduleRuleSlot slot, List<StudentGroup> eligibleGroups) {
if (slot.getSubgroup() == null) {
return eligibleGroups;
private List<LessonScope> lessonScopesForSlot(ScheduleRuleSlot slot,
ScheduleLessonCategory category,
List<StudentGroup> eligibleGroups) {
if (category != ScheduleLessonCategory.LABORATORY || slot.getSubgroups().isEmpty()) {
return eligibleGroups.isEmpty()
? List.of()
: List.of(new LessonScope(eligibleGroups, List.of(), null));
}
Long subgroupGroupId = slot.getSubgroup().getStudentGroup().getId();
return eligibleGroups.stream()
.filter(group -> Objects.equals(group.getId(), subgroupGroupId))
Set<Long> eligibleGroupIds = eligibleGroups.stream()
.map(StudentGroup::getId)
.collect(Collectors.toSet());
Map<Long, StudentGroup> eligibleGroupById = eligibleGroups.stream()
.collect(Collectors.toMap(StudentGroup::getId, group -> group));
return slot.getSubgroups().stream()
.filter(subgroup -> eligibleGroupIds.contains(subgroup.getStudentGroup().getId()))
.sorted(Comparator
.comparing((Subgroup subgroup) -> subgroup.getStudentGroup().getName())
.thenComparing(Subgroup::getName))
.map(subgroup -> new LessonScope(
List.of(eligibleGroupById.get(subgroup.getStudentGroup().getId())),
List.of(subgroup),
subgroup.getId()
))
.toList();
}
private record LessonScope(List<StudentGroup> groups, List<Subgroup> subgroups, Long subgroupId) {
}
private record ActiveLessonScope(LessonScope scope, int consumedHours, int remainingAfterLesson) {
}
private RenderedLessonDto toRenderedLesson(ScheduleRule rule,
ScheduleRuleSlot slot,
LocalDate date,
int weekNumber,
ScheduleParity dateParity,
List<StudentGroup> groups,
List<Subgroup> subgroups,
int lessonTypeAcademicHours,
int consumedBeforeLesson,
int remainingAfterLesson) {
@@ -310,6 +367,11 @@ public class ScheduleGeneratorService {
List<StudentGroup> sortedGroups = groups.stream()
.sorted(Comparator.comparing(StudentGroup::getName))
.toList();
List<Subgroup> sortedSubgroups = subgroups.stream()
.sorted(Comparator
.comparing((Subgroup subgroup) -> subgroup.getStudentGroup().getName())
.thenComparing(Subgroup::getName))
.toList();
return new RenderedLessonDto(
rule.getId(),
@@ -332,8 +394,10 @@ public class ScheduleGeneratorService {
slot.getLessonType().getId(),
slot.getLessonType().getLessonType(),
slot.getLessonFormat(),
slot.getSubgroupId(),
slot.getSubgroup() == null ? null : slot.getSubgroup().getName(),
sortedSubgroups.size() == 1 ? sortedSubgroups.get(0).getId() : null,
sortedSubgroups.size() == 1 ? formatSubgroup(sortedSubgroups.get(0)) : null,
sortedSubgroups.stream().map(Subgroup::getId).toList(),
sortedSubgroups.stream().map(this::formatSubgroup).toList(),
sortedGroups.stream().map(StudentGroup::getId).toList(),
sortedGroups.stream().map(StudentGroup::getName).collect(Collectors.toList()),
"Т",
@@ -343,6 +407,10 @@ public class ScheduleGeneratorService {
);
}
private String formatSubgroup(Subgroup subgroup) {
return subgroup.getStudentGroup().getName() + ": " + subgroup.getName();
}
private TimeSlot resolveEffectiveTimeSlot(TimeSlot baseSlot, LocalDate date) {
if (baseSlot == null || baseSlot.getOrderNumber() == null) {
return baseSlot;