начало работы построения динамического расписания и документация

This commit is contained in:
Zuev
2026-04-29 15:42:05 +03:00
parent 813e81be70
commit 8f71b9b2b5
43 changed files with 3894 additions and 457 deletions

View File

@@ -0,0 +1,317 @@
package com.magistr.app.controller;
import com.magistr.app.dto.*;
import com.magistr.app.model.*;
import com.magistr.app.repository.*;
import com.magistr.app.service.ScheduleGeneratorService;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/admin/calendar")
public class AcademicCalendarAdminController {
private final AcademicYearRepository academicYearRepository;
private final SemesterRepository semesterRepository;
private final HolidayRepository holidayRepository;
private final AcademicCalendarMatrixRepository calendarMatrixRepository;
private final ScheduleGeneratorService scheduleGeneratorService;
public AcademicCalendarAdminController(AcademicYearRepository academicYearRepository,
SemesterRepository semesterRepository,
HolidayRepository holidayRepository,
AcademicCalendarMatrixRepository calendarMatrixRepository,
ScheduleGeneratorService scheduleGeneratorService) {
this.academicYearRepository = academicYearRepository;
this.semesterRepository = semesterRepository;
this.holidayRepository = holidayRepository;
this.calendarMatrixRepository = calendarMatrixRepository;
this.scheduleGeneratorService = scheduleGeneratorService;
}
@GetMapping("/years")
public List<AcademicYearDto> getYears() {
return academicYearRepository.findAll().stream()
.sorted(Comparator.comparing(AcademicYear::getStartDate).reversed())
.map(this::toAcademicYearDto)
.toList();
}
@PostMapping("/years")
public ResponseEntity<?> createYear(@RequestBody AcademicYearDto request) {
String validationError = validateYear(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
if (academicYearRepository.findByTitle(request.title()).isPresent()) {
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год с таким названием уже существует"));
}
AcademicYear year = new AcademicYear();
applyYear(year, request);
return ResponseEntity.ok(toAcademicYearDto(academicYearRepository.save(year)));
}
@PutMapping("/years/{id}")
public ResponseEntity<?> updateYear(@PathVariable Long id, @RequestBody AcademicYearDto request) {
AcademicYear year = academicYearRepository.findById(id).orElse(null);
if (year == null) {
return ResponseEntity.notFound().build();
}
String validationError = validateYear(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
applyYear(year, request);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toAcademicYearDto(academicYearRepository.save(year)));
}
@DeleteMapping("/years/{id}")
public ResponseEntity<?> deleteYear(@PathVariable Long id) {
if (!academicYearRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
academicYearRepository.deleteById(id);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(Map.of("message", "Учебный год удалён"));
}
@GetMapping("/years/{academicYearId}/semesters")
public List<SemesterDto> getSemesters(@PathVariable Long academicYearId) {
return semesterRepository.findByAcademicYearIdOrderByStartDateAsc(academicYearId).stream()
.map(this::toSemesterDto)
.toList();
}
@PostMapping("/years/{academicYearId}/semesters")
public ResponseEntity<?> createSemester(@PathVariable Long academicYearId, @RequestBody SemesterDto request) {
AcademicYear year = academicYearRepository.findById(academicYearId).orElse(null);
if (year == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год не найден"));
}
String validationError = validateSemester(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
if (semesterRepository.findByAcademicYearIdAndSemesterType(academicYearId, request.semesterType()).isPresent()) {
return ResponseEntity.badRequest().body(Map.of("message", "Такой семестр уже есть в учебном году"));
}
Semester semester = new Semester();
semester.setAcademicYear(year);
applySemester(semester, request);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toSemesterDto(semesterRepository.save(semester)));
}
@PutMapping("/semesters/{id}")
public ResponseEntity<?> updateSemester(@PathVariable Long id, @RequestBody SemesterDto request) {
Semester semester = semesterRepository.findById(id).orElse(null);
if (semester == null) {
return ResponseEntity.notFound().build();
}
String validationError = validateSemester(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
applySemester(semester, request);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toSemesterDto(semesterRepository.save(semester)));
}
@GetMapping("/holidays")
public ResponseEntity<?> getHolidays(@RequestParam Long academicYearId) {
if (!academicYearRepository.existsById(academicYearId)) {
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год не найден"));
}
return ResponseEntity.ok(holidayRepository.findByAcademicYearIdOrderByDateAsc(academicYearId).stream()
.map(this::toHolidayDto)
.toList());
}
@PostMapping("/holidays")
public ResponseEntity<?> createHoliday(@RequestBody HolidayDto request) {
AcademicYear year = academicYearRepository.findById(request.academicYearId()).orElse(null);
if (year == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год не найден"));
}
if (request.date() == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Дата праздника обязательна"));
}
if (holidayRepository.existsByAcademicYearIdAndDate(year.getId(), request.date())) {
return ResponseEntity.badRequest().body(Map.of("message", "Эта дата уже добавлена в исключения"));
}
Holiday holiday = new Holiday();
holiday.setAcademicYear(year);
holiday.setDate(request.date());
holiday.setDescription(request.description());
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toHolidayDto(holidayRepository.save(holiday)));
}
@PutMapping("/holidays/{id}")
public ResponseEntity<?> updateHoliday(@PathVariable Long id, @RequestBody HolidayDto request) {
Holiday holiday = holidayRepository.findById(id).orElse(null);
if (holiday == null) {
return ResponseEntity.notFound().build();
}
if (request.date() == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Дата праздника обязательна"));
}
holiday.setDate(request.date());
holiday.setDescription(request.description());
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toHolidayDto(holidayRepository.save(holiday)));
}
@DeleteMapping("/holidays/{id}")
public ResponseEntity<?> deleteHoliday(@PathVariable Long id) {
if (!holidayRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
holidayRepository.deleteById(id);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(Map.of("message", "Праздник удалён"));
}
@GetMapping("/matrix")
public ResponseEntity<?> getMatrix(@RequestParam Long semesterId,
@RequestParam Integer courseNumber,
@RequestParam Long specialtyId) {
if (!semesterRepository.existsById(semesterId)) {
return ResponseEntity.badRequest().body(Map.of("message", "Семестр не найден"));
}
return ResponseEntity.ok(calendarMatrixRepository
.findBySemesterIdAndCourseNumberAndSpecialtyIdOrderByWeekNumberAsc(semesterId, courseNumber, specialtyId)
.stream()
.map(this::toMatrixDto)
.toList());
}
@PutMapping("/matrix")
@Transactional
public ResponseEntity<?> saveMatrix(@RequestBody List<AcademicCalendarMatrixDto> rows) {
if (rows == null || rows.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "Передайте хотя бы одну строку матрицы"));
}
try {
for (AcademicCalendarMatrixDto row : rows) {
Semester semester = semesterRepository.findById(row.semesterId())
.orElseThrow(() -> new IllegalArgumentException("Семестр не найден"));
if (row.courseNumber() == null || row.courseNumber() <= 0
|| row.specialtyId() == null
|| row.weekNumber() == null || row.weekNumber() <= 0) {
throw new IllegalArgumentException("Курс, специальность и номер недели обязательны");
}
AcademicCalendarMatrix matrix = calendarMatrixRepository
.findBySemesterIdAndCourseNumberAndSpecialtyIdAndWeekNumber(
row.semesterId(),
row.courseNumber(),
row.specialtyId(),
row.weekNumber()
)
.orElseGet(AcademicCalendarMatrix::new);
matrix.setSemester(semester);
matrix.setCourseNumber(row.courseNumber());
matrix.setSpecialtyId(row.specialtyId());
matrix.setWeekNumber(row.weekNumber());
matrix.setActivityType(row.activityType() == null ? AcademicActivityType.THEORY : row.activityType());
calendarMatrixRepository.save(matrix);
}
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(Map.of("message", "Матрица учебного графика сохранена"));
}
private String validateYear(AcademicYearDto request) {
if (request.title() == null || request.title().isBlank()) {
return "Название учебного года обязательно";
}
if (request.startDate() == null || request.endDate() == null) {
return "Даты начала и окончания учебного года обязательны";
}
if (request.endDate().isBefore(request.startDate())) {
return "Дата окончания не может быть раньше даты начала";
}
return null;
}
private String validateSemester(SemesterDto request) {
if (request.semesterType() == null) {
return "Тип семестра обязателен";
}
if (request.startDate() == null || request.endDate() == null) {
return "Даты начала и окончания семестра обязательны";
}
if (request.endDate().isBefore(request.startDate())) {
return "Дата окончания семестра не может быть раньше даты начала";
}
return null;
}
private void applyYear(AcademicYear year, AcademicYearDto request) {
year.setTitle(request.title().trim());
year.setStartDate(request.startDate());
year.setEndDate(request.endDate());
}
private void applySemester(Semester semester, SemesterDto request) {
semester.setSemesterType(request.semesterType());
semester.setStartDate(request.startDate());
semester.setEndDate(request.endDate());
}
private AcademicYearDto toAcademicYearDto(AcademicYear year) {
List<SemesterDto> semesters = semesterRepository.findByAcademicYearIdOrderByStartDateAsc(year.getId())
.stream()
.map(this::toSemesterDto)
.toList();
return new AcademicYearDto(year.getId(), year.getTitle(), year.getStartDate(), year.getEndDate(), semesters);
}
private SemesterDto toSemesterDto(Semester semester) {
return new SemesterDto(
semester.getId(),
semester.getAcademicYear().getId(),
semester.getAcademicYear().getTitle(),
semester.getSemesterType(),
semester.getStartDate(),
semester.getEndDate()
);
}
private HolidayDto toHolidayDto(Holiday holiday) {
return new HolidayDto(
holiday.getId(),
holiday.getDate(),
holiday.getAcademicYear().getId(),
holiday.getAcademicYear().getTitle(),
holiday.getDescription()
);
}
private AcademicCalendarMatrixDto toMatrixDto(AcademicCalendarMatrix matrix) {
return new AcademicCalendarMatrixDto(
matrix.getId(),
matrix.getSemester().getId(),
matrix.getCourseNumber(),
matrix.getSpecialtyId(),
matrix.getWeekNumber(),
matrix.getActivityType()
);
}
}

View File

@@ -47,6 +47,6 @@ public class AuthController {
String redirect = ROLE_REDIRECTS.getOrDefault(roleName, "/");
Long departmentId = user.getDepartmentId();
return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect, departmentId));
return ResponseEntity.ok(new LoginResponse(true, "OK", token, roleName, redirect, departmentId, user.getId()));
}
}

View File

@@ -162,6 +162,7 @@ public class LessonsController {
}
//Запрос для получения всего списка занятий
@Deprecated
@GetMapping
public List<LessonResponse> getAllLessons() {
logger.info("Запрос на получение всех занятий");
@@ -221,6 +222,7 @@ public class LessonsController {
}
//Запрос на получение всех занятий для конкретного преподавателя
@Deprecated
@GetMapping("/{teacherId}")
public ResponseEntity<?> getLessonsById(@PathVariable Long teacherId) {
logger.info("Запрос на получение занятий для преподавателя с ID: {}", teacherId);
@@ -504,4 +506,4 @@ public class LessonsController {
response.put("time", lesson.getTime());
return response;
}
}
}

View File

@@ -0,0 +1,56 @@
package com.magistr.app.controller;
import com.magistr.app.dto.RenderedLessonDto;
import com.magistr.app.service.ScheduleGeneratorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/schedule")
public class ScheduleController {
private static final Logger logger = LoggerFactory.getLogger(ScheduleController.class);
private final ScheduleGeneratorService scheduleGeneratorService;
public ScheduleController(ScheduleGeneratorService scheduleGeneratorService) {
this.scheduleGeneratorService = scheduleGeneratorService;
}
@GetMapping
public ResponseEntity<?> getSchedule(
@RequestParam(required = false) Long groupId,
@RequestParam(required = false) Long teacherId,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate
) {
logger.info("Запрос динамического расписания: groupId={}, teacherId={}, startDate={}, endDate={}",
groupId, teacherId, startDate, endDate);
if ((groupId == null && teacherId == null) || (groupId != null && teacherId != null)) {
return ResponseEntity.badRequest()
.body(Map.of("message", "Передайте ровно один параметр: groupId или teacherId"));
}
try {
List<RenderedLessonDto> lessons = groupId != null
? scheduleGeneratorService.buildScheduleForGroup(groupId, startDate, endDate)
: scheduleGeneratorService.buildScheduleForTeacher(teacherId, startDate, endDate);
return ResponseEntity.ok(lessons);
} catch (IllegalArgumentException e) {
logger.info("Ошибка запроса динамического расписания: {}", e.getMessage());
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
} catch (Exception e) {
logger.error("Ошибка генерации динамического расписания: {}", e.getMessage(), e);
return ResponseEntity.internalServerError()
.body(Map.of("message", "Произошла ошибка при генерации расписания"));
}
}
}

View File

@@ -0,0 +1,253 @@
package com.magistr.app.controller;
import com.magistr.app.dto.ScheduleRuleDto;
import com.magistr.app.dto.ScheduleRuleSlotDto;
import com.magistr.app.model.*;
import com.magistr.app.repository.*;
import com.magistr.app.service.ScheduleGeneratorService;
import org.springframework.http.ResponseEntity;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import java.time.DayOfWeek;
import java.util.*;
@RestController
@RequestMapping("/api/admin/schedule-rules")
public class ScheduleRuleAdminController {
private final ScheduleRuleRepository scheduleRuleRepository;
private final SubjectRepository subjectRepository;
private final SemesterRepository semesterRepository;
private final GroupRepository groupRepository;
private final TimeSlotRepository timeSlotRepository;
private final UserRepository userRepository;
private final ClassroomRepository classroomRepository;
private final LessonTypesRepository lessonTypesRepository;
private final ScheduleGeneratorService scheduleGeneratorService;
public ScheduleRuleAdminController(ScheduleRuleRepository scheduleRuleRepository,
SubjectRepository subjectRepository,
SemesterRepository semesterRepository,
GroupRepository groupRepository,
TimeSlotRepository timeSlotRepository,
UserRepository userRepository,
ClassroomRepository classroomRepository,
LessonTypesRepository lessonTypesRepository,
ScheduleGeneratorService scheduleGeneratorService) {
this.scheduleRuleRepository = scheduleRuleRepository;
this.subjectRepository = subjectRepository;
this.semesterRepository = semesterRepository;
this.groupRepository = groupRepository;
this.timeSlotRepository = timeSlotRepository;
this.userRepository = userRepository;
this.classroomRepository = classroomRepository;
this.lessonTypesRepository = lessonTypesRepository;
this.scheduleGeneratorService = scheduleGeneratorService;
}
@GetMapping
public List<ScheduleRuleDto> getAll(@RequestParam(required = false) Long semesterId,
@RequestParam(required = false) Long groupId) {
return scheduleRuleRepository.findAllWithDetails().stream()
.filter(rule -> semesterId == null || Objects.equals(rule.getSemester().getId(), semesterId))
.filter(rule -> groupId == null || rule.getGroups().stream().anyMatch(group -> Objects.equals(group.getId(), groupId)))
.map(this::toDto)
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<?> getById(@PathVariable Long id) {
return scheduleRuleRepository.findByIdWithDetails(id)
.<ResponseEntity<?>>map(rule -> ResponseEntity.ok(toDto(rule)))
.orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping
@Transactional
public ResponseEntity<?> create(@RequestBody ScheduleRuleDto request) {
String validationError = validateRule(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
try {
ScheduleRule rule = new ScheduleRule();
applyRule(rule, request);
ScheduleRule saved = scheduleRuleRepository.save(rule);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toDto(saved));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
}
@PutMapping("/{id}")
@Transactional
public ResponseEntity<?> update(@PathVariable Long id, @RequestBody ScheduleRuleDto request) {
ScheduleRule rule = scheduleRuleRepository.findByIdWithDetails(id).orElse(null);
if (rule == null) {
return ResponseEntity.notFound().build();
}
String validationError = validateRule(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
try {
applyRule(rule, request);
ScheduleRule saved = scheduleRuleRepository.save(rule);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toDto(saved));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
}
@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable Long id) {
if (!scheduleRuleRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
scheduleRuleRepository.deleteById(id);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(Map.of("message", "Правило расписания удалено"));
}
private String validateRule(ScheduleRuleDto request) {
if (request.subjectId() == null) {
return "Дисциплина обязательна";
}
if (request.semesterId() == null) {
return "Семестр обязателен";
}
if (request.activeFromDate() == null) {
return "Дата начала правила обязательна";
}
if (request.totalAcademicHours() == null || request.totalAcademicHours() <= 0) {
return "Количество академических часов должно быть больше нуля";
}
if (request.groupIds() == null || request.groupIds().isEmpty()) {
return "Нужно выбрать хотя бы одну группу";
}
if (request.slots() == null || request.slots().isEmpty()) {
return "Добавьте хотя бы один слот занятия";
}
return null;
}
private void applyRule(ScheduleRule rule, ScheduleRuleDto request) {
Subject subject = subjectRepository.findById(request.subjectId())
.orElseThrow(() -> new IllegalArgumentException("Дисциплина не найдена"));
Semester semester = semesterRepository.findById(request.semesterId())
.orElseThrow(() -> new IllegalArgumentException("Семестр не найден"));
List<StudentGroup> groups = groupRepository.findAllById(request.groupIds());
if (groups.size() != new HashSet<>(request.groupIds()).size()) {
throw new IllegalArgumentException("Одна или несколько групп не найдены");
}
rule.setSubject(subject);
rule.setSemester(semester);
rule.setActiveFromDate(request.activeFromDate());
rule.setTotalAcademicHours(request.totalAcademicHours());
rule.getGroups().clear();
rule.getGroups().addAll(groups);
rule.getSlots().clear();
for (ScheduleRuleSlotDto slotDto : request.slots()) {
rule.getSlots().add(buildSlot(rule, slotDto));
}
}
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("Чётность недели обязательна");
}
TimeSlot timeSlot = timeSlotRepository.findById(slotDto.timeSlotId())
.orElseThrow(() -> new IllegalArgumentException("Временной слот не найден"));
User teacher = userRepository.findById(slotDto.teacherId())
.orElseThrow(() -> new IllegalArgumentException("Преподаватель не найден"));
Classroom classroom = classroomRepository.findById(slotDto.classroomId())
.orElseThrow(() -> new IllegalArgumentException("Аудитория не найдена"));
LessonType lessonType = lessonTypesRepository.findById(slotDto.lessonTypeId())
.orElseThrow(() -> new IllegalArgumentException("Тип занятия не найден"));
if (slotDto.lessonFormat() == null || slotDto.lessonFormat().isBlank()) {
throw new IllegalArgumentException("Формат занятия обязателен");
}
ScheduleRuleSlot slot = new ScheduleRuleSlot();
slot.setScheduleRule(rule);
slot.setDayOfWeek(slotDto.dayOfWeek());
slot.setParity(slotDto.parity());
slot.setTimeSlot(timeSlot);
slot.setSubgroupId(slotDto.subgroupId());
slot.setTeacher(teacher);
slot.setClassroom(classroom);
slot.setLessonType(lessonType);
slot.setLessonFormat(slotDto.lessonFormat().trim());
return slot;
}
private ScheduleRuleDto toDto(ScheduleRule rule) {
List<StudentGroup> groups = rule.getGroups().stream()
.sorted(Comparator.comparing(StudentGroup::getName))
.toList();
List<ScheduleRuleSlotDto> slots = rule.getSlots().stream()
.sorted(Comparator
.comparing(ScheduleRuleSlot::getDayOfWeek)
.thenComparing(slot -> slot.getTimeSlot().getOrderNumber())
.thenComparing(ScheduleRuleSlot::getId))
.map(this::toSlotDto)
.toList();
return new ScheduleRuleDto(
rule.getId(),
rule.getSubject().getId(),
rule.getSubject().getName(),
rule.getSemester().getId(),
rule.getSemester().getAcademicYear().getTitle(),
rule.getSemester().getSemesterType(),
rule.getActiveFromDate(),
rule.getTotalAcademicHours(),
groups.stream().map(StudentGroup::getId).toList(),
groups.stream().map(StudentGroup::getName).toList(),
slots
);
}
private ScheduleRuleSlotDto toSlotDto(ScheduleRuleSlot slot) {
TimeSlot timeSlot = slot.getTimeSlot();
return new ScheduleRuleSlotDto(
slot.getId(),
slot.getDayOfWeek(),
dayName(slot.getDayOfWeek()),
slot.getParity(),
timeSlot.getId(),
timeSlot.getOrderNumber(),
formatTimeSlot(timeSlot),
slot.getSubgroupId(),
slot.getTeacher().getId(),
ScheduleGeneratorService.displayUserName(slot.getTeacher()),
slot.getClassroom().getId(),
slot.getClassroom().getName(),
slot.getLessonType().getId(),
slot.getLessonType().getLessonType(),
slot.getLessonFormat()
);
}
private String dayName(Integer dayOfWeek) {
if (dayOfWeek == null || dayOfWeek < 1 || dayOfWeek > 7) {
return "Неизвестно";
}
return ScheduleGeneratorService.dayName(DayOfWeek.of(dayOfWeek));
}
private String formatTimeSlot(TimeSlot timeSlot) {
return timeSlot.getStartTime() + " - " + timeSlot.getEndTime();
}
}

View File

@@ -0,0 +1,106 @@
package com.magistr.app.controller;
import com.magistr.app.dto.TimeSlotDto;
import com.magistr.app.model.TimeSlot;
import com.magistr.app.repository.TimeSlotRepository;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Duration;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/admin/time-slots")
public class TimeSlotAdminController {
private final TimeSlotRepository timeSlotRepository;
public TimeSlotAdminController(TimeSlotRepository timeSlotRepository) {
this.timeSlotRepository = timeSlotRepository;
}
@GetMapping
public List<TimeSlotDto> getAll() {
return timeSlotRepository.findAllByOrderByOrderNumberAsc().stream()
.map(this::toDto)
.toList();
}
@PostMapping
public ResponseEntity<?> create(@RequestBody TimeSlotDto request) {
String validationError = validate(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
if (timeSlotRepository.existsByOrderNumber(request.orderNumber())) {
return ResponseEntity.badRequest().body(Map.of("message", "Слот с таким номером пары уже существует"));
}
TimeSlot timeSlot = new TimeSlot();
apply(timeSlot, request);
return ResponseEntity.ok(toDto(timeSlotRepository.save(timeSlot)));
}
@PutMapping("/{id}")
public ResponseEntity<?> update(@PathVariable Long id, @RequestBody TimeSlotDto request) {
TimeSlot timeSlot = timeSlotRepository.findById(id).orElse(null);
if (timeSlot == null) {
return ResponseEntity.notFound().build();
}
String validationError = validate(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
if (timeSlotRepository.existsByOrderNumber(request.orderNumber())
&& !timeSlot.getOrderNumber().equals(request.orderNumber())) {
return ResponseEntity.badRequest().body(Map.of("message", "Слот с таким номером пары уже существует"));
}
apply(timeSlot, request);
return ResponseEntity.ok(toDto(timeSlotRepository.save(timeSlot)));
}
@DeleteMapping("/{id}")
public ResponseEntity<?> delete(@PathVariable Long id) {
if (!timeSlotRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
timeSlotRepository.deleteById(id);
return ResponseEntity.ok(Map.of("message", "Временной слот удалён"));
}
private String validate(TimeSlotDto request) {
if (request.orderNumber() == null || request.orderNumber() <= 0) {
return "Номер пары должен быть больше нуля";
}
if (request.startTime() == null || request.endTime() == null) {
return "Время начала и окончания обязательно";
}
if (!request.startTime().isBefore(request.endTime())) {
return "Время начала должно быть раньше времени окончания";
}
return null;
}
private void apply(TimeSlot timeSlot, TimeSlotDto request) {
timeSlot.setOrderNumber(request.orderNumber());
timeSlot.setStartTime(request.startTime());
timeSlot.setEndTime(request.endTime());
int duration = request.durationMinutes() != null && request.durationMinutes() > 0
? request.durationMinutes()
: (int) Duration.between(request.startTime(), request.endTime()).toMinutes();
timeSlot.setDurationMinutes(duration);
}
private TimeSlotDto toDto(TimeSlot timeSlot) {
return new TimeSlotDto(
timeSlot.getId(),
timeSlot.getOrderNumber(),
timeSlot.getStartTime(),
timeSlot.getEndTime(),
timeSlot.getDurationMinutes()
);
}
}

View File

@@ -0,0 +1,13 @@
package com.magistr.app.dto;
import com.magistr.app.model.AcademicActivityType;
public record AcademicCalendarMatrixDto(
Long id,
Long semesterId,
Integer courseNumber,
Long specialtyId,
Integer weekNumber,
AcademicActivityType activityType
) {
}

View File

@@ -0,0 +1,13 @@
package com.magistr.app.dto;
import java.time.LocalDate;
import java.util.List;
public record AcademicYearDto(
Long id,
String title,
LocalDate startDate,
LocalDate endDate,
List<SemesterDto> semesters
) {
}

View File

@@ -0,0 +1,12 @@
package com.magistr.app.dto;
import java.time.LocalDate;
public record HolidayDto(
Long id,
LocalDate date,
Long academicYearId,
String academicYearTitle,
String description
) {
}

View File

@@ -8,17 +8,23 @@ public class LoginResponse {
private String role;
private String redirect;
private Long departmentId;
private Long userId;
public LoginResponse() {
}
public LoginResponse(boolean success, String message, String token, String role, String redirect, Long departmentId) {
this(success, message, token, role, redirect, departmentId, null);
}
public LoginResponse(boolean success, String message, String token, String role, String redirect, Long departmentId, Long userId) {
this.success = success;
this.message = message;
this.token = token;
this.role = role;
this.redirect = redirect;
this.departmentId = departmentId;
this.userId = userId;
}
public boolean isSuccess() {
@@ -68,4 +74,12 @@ public class LoginResponse {
public void setDepartmentId(Long departmentId) {
this.departmentId = departmentId;
}
public Long getUserId() {
return userId;
}
public void setUserId(Long userId) {
this.userId = userId;
}
}

View File

@@ -0,0 +1,39 @@
package com.magistr.app.dto;
import com.magistr.app.model.AcademicActivityType;
import com.magistr.app.model.ScheduleParity;
import java.time.LocalDate;
import java.time.LocalTime;
import java.util.List;
public record RenderedLessonDto(
Long scheduleRuleId,
Long scheduleRuleSlotId,
LocalDate date,
Integer dayOfWeek,
String dayName,
Integer weekNumber,
ScheduleParity parity,
Long timeSlotId,
Integer timeSlotOrder,
LocalTime startTime,
LocalTime endTime,
Long subjectId,
String subjectName,
Long teacherId,
String teacherName,
Long classroomId,
String classroomName,
Long lessonTypeId,
String lessonTypeName,
String lessonFormat,
Long subgroupId,
List<Long> groupIds,
List<String> groupNames,
AcademicActivityType activityType,
Integer totalAcademicHours,
Integer consumedAcademicHoursBeforeLesson,
Integer remainingAcademicHoursAfterLesson
) {
}

View File

@@ -0,0 +1,21 @@
package com.magistr.app.dto;
import com.magistr.app.model.SemesterType;
import java.time.LocalDate;
import java.util.List;
public record ScheduleRuleDto(
Long id,
Long subjectId,
String subjectName,
Long semesterId,
String academicYearTitle,
SemesterType semesterType,
LocalDate activeFromDate,
Integer totalAcademicHours,
List<Long> groupIds,
List<String> groupNames,
List<ScheduleRuleSlotDto> slots
) {
}

View File

@@ -0,0 +1,22 @@
package com.magistr.app.dto;
import com.magistr.app.model.ScheduleParity;
public record ScheduleRuleSlotDto(
Long id,
Integer dayOfWeek,
String dayName,
ScheduleParity parity,
Long timeSlotId,
Integer timeSlotOrder,
String timeSlotLabel,
Long subgroupId,
Long teacherId,
String teacherName,
Long classroomId,
String classroomName,
Long lessonTypeId,
String lessonTypeName,
String lessonFormat
) {
}

View File

@@ -0,0 +1,15 @@
package com.magistr.app.dto;
import com.magistr.app.model.SemesterType;
import java.time.LocalDate;
public record SemesterDto(
Long id,
Long academicYearId,
String academicYearTitle,
SemesterType semesterType,
LocalDate startDate,
LocalDate endDate
) {
}

View File

@@ -0,0 +1,12 @@
package com.magistr.app.dto;
import java.time.LocalTime;
public record TimeSlotDto(
Long id,
Integer orderNumber,
LocalTime startTime,
LocalTime endTime,
Integer durationMinutes
) {
}

View File

@@ -0,0 +1,8 @@
package com.magistr.app.model;
public enum AcademicActivityType {
THEORY,
EXAM,
VACATION,
PRACTICE
}

View File

@@ -0,0 +1,77 @@
package com.magistr.app.model;
import jakarta.persistence.*;
@Entity
@Table(name = "academic_calendar_matrix")
public class AcademicCalendarMatrix {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "semester_id", nullable = false)
private Semester semester;
@Column(name = "course_number", nullable = false)
private Integer courseNumber;
@Column(name = "specialty_id", nullable = false)
private Long specialtyId;
@Column(name = "week_number", nullable = false)
private Integer weekNumber;
@Enumerated(EnumType.STRING)
@Column(name = "activity_type", nullable = false, length = 20)
private AcademicActivityType activityType;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Semester getSemester() {
return semester;
}
public void setSemester(Semester semester) {
this.semester = semester;
}
public Integer getCourseNumber() {
return courseNumber;
}
public void setCourseNumber(Integer courseNumber) {
this.courseNumber = courseNumber;
}
public Long getSpecialtyId() {
return specialtyId;
}
public void setSpecialtyId(Long specialtyId) {
this.specialtyId = specialtyId;
}
public Integer getWeekNumber() {
return weekNumber;
}
public void setWeekNumber(Integer weekNumber) {
this.weekNumber = weekNumber;
}
public AcademicActivityType getActivityType() {
return activityType;
}
public void setActivityType(AcademicActivityType activityType) {
this.activityType = activityType;
}
}

View File

@@ -0,0 +1,55 @@
package com.magistr.app.model;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
@Table(name = "academic_years")
public class AcademicYear {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 20)
private String title;
@Column(name = "start_date", nullable = false)
private LocalDate startDate;
@Column(name = "end_date", nullable = false)
private LocalDate endDate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
}

View File

@@ -0,0 +1,56 @@
package com.magistr.app.model;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
@Table(name = "holidays")
public class Holiday {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private LocalDate date;
@ManyToOne(optional = false)
@JoinColumn(name = "academic_year_id", nullable = false)
private AcademicYear academicYear;
@Column(length = 255)
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public AcademicYear getAcademicYear() {
return academicYear;
}
public void setAcademicYear(AcademicYear academicYear) {
this.academicYear = academicYear;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@@ -0,0 +1,7 @@
package com.magistr.app.model;
public enum ScheduleParity {
BOTH,
EVEN,
ODD
}

View File

@@ -0,0 +1,97 @@
package com.magistr.app.model;
import jakarta.persistence.*;
import java.time.LocalDate;
import java.util.HashSet;
import java.util.Set;
@Entity
@Table(name = "schedule_rules")
public class ScheduleRule {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "subject_id", nullable = false)
private Subject subject;
@ManyToOne(optional = false)
@JoinColumn(name = "semester_id", nullable = false)
private Semester semester;
@Column(name = "active_from_date", nullable = false)
private LocalDate activeFromDate;
@Column(name = "total_academic_hours", nullable = false)
private Integer totalAcademicHours;
@ManyToMany
@JoinTable(
name = "schedule_rule_groups",
joinColumns = @JoinColumn(name = "schedule_rule_id"),
inverseJoinColumns = @JoinColumn(name = "group_id")
)
private Set<StudentGroup> groups = new HashSet<>();
@OneToMany(mappedBy = "scheduleRule", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<ScheduleRuleSlot> slots = new HashSet<>();
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Subject getSubject() {
return subject;
}
public void setSubject(Subject subject) {
this.subject = subject;
}
public Semester getSemester() {
return semester;
}
public void setSemester(Semester semester) {
this.semester = semester;
}
public LocalDate getActiveFromDate() {
return activeFromDate;
}
public void setActiveFromDate(LocalDate activeFromDate) {
this.activeFromDate = activeFromDate;
}
public Integer getTotalAcademicHours() {
return totalAcademicHours;
}
public void setTotalAcademicHours(Integer totalAcademicHours) {
this.totalAcademicHours = totalAcademicHours;
}
public Set<StudentGroup> getGroups() {
return groups;
}
public void setGroups(Set<StudentGroup> groups) {
this.groups = groups;
}
public Set<ScheduleRuleSlot> getSlots() {
return slots;
}
public void setSlots(Set<ScheduleRuleSlot> slots) {
this.slots = slots;
}
}

View File

@@ -0,0 +1,125 @@
package com.magistr.app.model;
import jakarta.persistence.*;
@Entity
@Table(name = "schedule_rule_slots")
public class ScheduleRuleSlot {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "schedule_rule_id", nullable = false)
private ScheduleRule scheduleRule;
@Column(name = "day_of_week", nullable = false)
private Integer dayOfWeek;
@Enumerated(EnumType.STRING)
@Column(nullable = false, length = 10)
private ScheduleParity parity;
@ManyToOne(optional = false)
@JoinColumn(name = "time_slot_id", nullable = false)
private TimeSlot timeSlot;
@Column(name = "subgroup_id")
private Long subgroupId;
@ManyToOne(optional = false)
@JoinColumn(name = "teacher_id", nullable = false)
private User teacher;
@ManyToOne(optional = false)
@JoinColumn(name = "classroom_id", nullable = false)
private Classroom classroom;
@ManyToOne(optional = false)
@JoinColumn(name = "lesson_type_id", nullable = false)
private LessonType lessonType;
@Column(name = "lesson_format", nullable = false, length = 30)
private String lessonFormat;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public ScheduleRule getScheduleRule() {
return scheduleRule;
}
public void setScheduleRule(ScheduleRule scheduleRule) {
this.scheduleRule = scheduleRule;
}
public Integer getDayOfWeek() {
return dayOfWeek;
}
public void setDayOfWeek(Integer dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
public ScheduleParity getParity() {
return parity;
}
public void setParity(ScheduleParity parity) {
this.parity = parity;
}
public TimeSlot getTimeSlot() {
return timeSlot;
}
public void setTimeSlot(TimeSlot timeSlot) {
this.timeSlot = timeSlot;
}
public Long getSubgroupId() {
return subgroupId;
}
public void setSubgroupId(Long subgroupId) {
this.subgroupId = subgroupId;
}
public User getTeacher() {
return teacher;
}
public void setTeacher(User teacher) {
this.teacher = teacher;
}
public Classroom getClassroom() {
return classroom;
}
public void setClassroom(Classroom classroom) {
this.classroom = classroom;
}
public LessonType getLessonType() {
return lessonType;
}
public void setLessonType(LessonType lessonType) {
this.lessonType = lessonType;
}
public String getLessonFormat() {
return lessonFormat;
}
public void setLessonFormat(String lessonFormat) {
this.lessonFormat = lessonFormat;
}
}

View File

@@ -0,0 +1,68 @@
package com.magistr.app.model;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
@Table(name = "semesters")
public class Semester {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "academic_year_id", nullable = false)
private AcademicYear academicYear;
@Enumerated(EnumType.STRING)
@Column(name = "semester_type", nullable = false, length = 20)
private SemesterType semesterType;
@Column(name = "start_date", nullable = false)
private LocalDate startDate;
@Column(name = "end_date", nullable = false)
private LocalDate endDate;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public AcademicYear getAcademicYear() {
return academicYear;
}
public void setAcademicYear(AcademicYear academicYear) {
this.academicYear = academicYear;
}
public SemesterType getSemesterType() {
return semesterType;
}
public void setSemesterType(SemesterType semesterType) {
this.semesterType = semesterType;
}
public LocalDate getStartDate() {
return startDate;
}
public void setStartDate(LocalDate startDate) {
this.startDate = startDate;
}
public LocalDate getEndDate() {
return endDate;
}
public void setEndDate(LocalDate endDate) {
this.endDate = endDate;
}
}

View File

@@ -0,0 +1,66 @@
package com.magistr.app.model;
import jakarta.persistence.*;
import java.time.LocalTime;
@Entity
@Table(name = "time_slots")
public class TimeSlot {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "order_number", nullable = false)
private Integer orderNumber;
@Column(name = "start_time", nullable = false)
private LocalTime startTime;
@Column(name = "end_time", nullable = false)
private LocalTime endTime;
@Column(name = "duration_minutes", nullable = false)
private Integer durationMinutes;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Integer getOrderNumber() {
return orderNumber;
}
public void setOrderNumber(Integer orderNumber) {
this.orderNumber = orderNumber;
}
public LocalTime getStartTime() {
return startTime;
}
public void setStartTime(LocalTime startTime) {
this.startTime = startTime;
}
public LocalTime getEndTime() {
return endTime;
}
public void setEndTime(LocalTime endTime) {
this.endTime = endTime;
}
public Integer getDurationMinutes() {
return durationMinutes;
}
public void setDurationMinutes(Integer durationMinutes) {
this.durationMinutes = durationMinutes;
}
}

View File

@@ -0,0 +1,23 @@
package com.magistr.app.repository;
import com.magistr.app.model.AcademicCalendarMatrix;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface AcademicCalendarMatrixRepository extends JpaRepository<AcademicCalendarMatrix, Long> {
Optional<AcademicCalendarMatrix> findBySemesterIdAndCourseNumberAndSpecialtyIdAndWeekNumber(
Long semesterId,
Integer courseNumber,
Long specialtyId,
Integer weekNumber
);
List<AcademicCalendarMatrix> findBySemesterIdAndCourseNumberAndSpecialtyIdOrderByWeekNumberAsc(
Long semesterId,
Integer courseNumber,
Long specialtyId
);
}

View File

@@ -0,0 +1,11 @@
package com.magistr.app.repository;
import com.magistr.app.model.AcademicYear;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface AcademicYearRepository extends JpaRepository<AcademicYear, Long> {
Optional<AcademicYear> findByTitle(String title);
}

View File

@@ -0,0 +1,16 @@
package com.magistr.app.repository;
import com.magistr.app.model.Holiday;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
public interface HolidayRepository extends JpaRepository<Holiday, Long> {
List<Holiday> findByAcademicYearIdOrderByDateAsc(Long academicYearId);
List<Holiday> findByAcademicYearIdAndDateBetween(Long academicYearId, LocalDate startDate, LocalDate endDate);
boolean existsByAcademicYearIdAndDate(Long academicYearId, LocalDate date);
}

View File

@@ -0,0 +1,85 @@
package com.magistr.app.repository;
import com.magistr.app.model.ScheduleRule;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface ScheduleRuleRepository extends JpaRepository<ScheduleRule, Long> {
@Query("""
select distinct r
from ScheduleRule r
join r.groups g
left join fetch r.subject
left join fetch r.semester sem
left join fetch sem.academicYear
left join fetch r.groups groups
left join fetch r.slots slots
left join fetch slots.timeSlot
left join fetch slots.teacher
left join fetch slots.classroom
left join fetch slots.lessonType
where g.id = :groupId
and sem.id = :semesterId
""")
List<ScheduleRule> findByGroupIdAndSemesterId(
@Param("groupId") Long groupId,
@Param("semesterId") Long semesterId
);
@Query("""
select distinct r
from ScheduleRule r
join r.slots teacherSlot
left join fetch r.subject
left join fetch r.semester sem
left join fetch sem.academicYear
left join fetch r.groups groups
left join fetch r.slots slots
left join fetch slots.timeSlot
left join fetch slots.teacher
left join fetch slots.classroom
left join fetch slots.lessonType
where teacherSlot.teacher.id = :teacherId
and sem.id = :semesterId
""")
List<ScheduleRule> findByTeacherIdAndSemesterId(
@Param("teacherId") Long teacherId,
@Param("semesterId") Long semesterId
);
@Query("""
select distinct r
from ScheduleRule r
left join fetch r.subject
left join fetch r.semester sem
left join fetch sem.academicYear
left join fetch r.groups groups
left join fetch r.slots slots
left join fetch slots.timeSlot
left join fetch slots.teacher
left join fetch slots.classroom
left join fetch slots.lessonType
order by r.id desc
""")
List<ScheduleRule> findAllWithDetails();
@Query("""
select distinct r
from ScheduleRule r
left join fetch r.subject
left join fetch r.semester sem
left join fetch sem.academicYear
left join fetch r.groups groups
left join fetch r.slots slots
left join fetch slots.timeSlot
left join fetch slots.teacher
left join fetch slots.classroom
left join fetch slots.lessonType
where r.id = :id
""")
java.util.Optional<ScheduleRule> findByIdWithDetails(@Param("id") Long id);
}

View File

@@ -0,0 +1,7 @@
package com.magistr.app.repository;
import com.magistr.app.model.ScheduleRuleSlot;
import org.springframework.data.jpa.repository.JpaRepository;
public interface ScheduleRuleSlotRepository extends JpaRepository<ScheduleRuleSlot, Long> {
}

View File

@@ -0,0 +1,18 @@
package com.magistr.app.repository;
import com.magistr.app.model.Semester;
import com.magistr.app.model.SemesterType;
import org.springframework.data.jpa.repository.JpaRepository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface SemesterRepository extends JpaRepository<Semester, Long> {
Optional<Semester> findFirstByStartDateLessThanEqualAndEndDateGreaterThanEqual(LocalDate startDate, LocalDate endDate);
List<Semester> findByAcademicYearIdOrderByStartDateAsc(Long academicYearId);
Optional<Semester> findByAcademicYearIdAndSemesterType(Long academicYearId, SemesterType semesterType);
}

View File

@@ -0,0 +1,13 @@
package com.magistr.app.repository;
import com.magistr.app.model.TimeSlot;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface TimeSlotRepository extends JpaRepository<TimeSlot, Long> {
List<TimeSlot> findAllByOrderByOrderNumberAsc();
boolean existsByOrderNumber(Integer orderNumber);
}

View File

@@ -0,0 +1,87 @@
package com.magistr.app.service;
import com.magistr.app.model.*;
import com.magistr.app.repository.AcademicCalendarMatrixRepository;
import com.magistr.app.repository.HolidayRepository;
import com.magistr.app.repository.SemesterRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.temporal.ChronoUnit;
import java.util.Optional;
@Service
public class AcademicDateService {
private final SemesterRepository semesterRepository;
private final HolidayRepository holidayRepository;
private final AcademicCalendarMatrixRepository calendarMatrixRepository;
private final boolean evenWeekIsUpper;
public AcademicDateService(SemesterRepository semesterRepository,
HolidayRepository holidayRepository,
AcademicCalendarMatrixRepository calendarMatrixRepository,
@Value("${app.schedule.even-week-is-upper:false}") boolean evenWeekIsUpper) {
this.semesterRepository = semesterRepository;
this.holidayRepository = holidayRepository;
this.calendarMatrixRepository = calendarMatrixRepository;
this.evenWeekIsUpper = evenWeekIsUpper;
}
public Optional<Semester> findSemester(LocalDate date) {
return semesterRepository.findFirstByStartDateLessThanEqualAndEndDateGreaterThanEqual(date, date);
}
public int getWeekNumber(Semester semester, LocalDate date) {
long days = ChronoUnit.DAYS.between(semester.getStartDate(), date);
if (days < 0) {
return 0;
}
return (int) (days / 7) + 1;
}
public ScheduleParity getParity(Semester semester, LocalDate date) {
int weekNumber = getWeekNumber(semester, date);
boolean evenWeek = weekNumber % 2 == 0;
if (evenWeekIsUpper) {
return evenWeek ? ScheduleParity.ODD : ScheduleParity.EVEN;
}
return evenWeek ? ScheduleParity.EVEN : ScheduleParity.ODD;
}
public boolean isHoliday(Semester semester, LocalDate date) {
if (semester == null || semester.getAcademicYear() == null) {
return false;
}
return holidayRepository.existsByAcademicYearIdAndDate(semester.getAcademicYear().getId(), date);
}
public int getCourseNumber(StudentGroup group, LocalDate date) {
int academicYearStart = date.getMonthValue() >= 9 ? date.getYear() : date.getYear() - 1;
return academicYearStart - group.getYearStartStudy() + 1;
}
public AcademicActivityType getActivityType(StudentGroup group, Semester semester, LocalDate date) {
int weekNumber = getWeekNumber(semester, date);
int courseNumber = getCourseNumber(group, date);
if (weekNumber < 1 || courseNumber < 1) {
return AcademicActivityType.VACATION;
}
return calendarMatrixRepository
.findBySemesterIdAndCourseNumberAndSpecialtyIdAndWeekNumber(
semester.getId(),
courseNumber,
group.getSpecialityCode(),
weekNumber
)
.map(AcademicCalendarMatrix::getActivityType)
.orElse(AcademicActivityType.THEORY);
}
public boolean isTheoryDay(StudentGroup group, Semester semester, LocalDate date) {
return getActivityType(group, semester, date) == AcademicActivityType.THEORY
&& !isHoliday(semester, date);
}
}

View File

@@ -0,0 +1,295 @@
package com.magistr.app.service;
import com.magistr.app.dto.RenderedLessonDto;
import com.magistr.app.model.*;
import com.magistr.app.repository.GroupRepository;
import com.magistr.app.repository.ScheduleRuleRepository;
import com.magistr.app.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.format.TextStyle;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
@Service
public class ScheduleGeneratorService {
private static final int ACADEMIC_HOURS_PER_SLOT = 2;
private static final long MAX_RANGE_DAYS = 120;
private final ScheduleRuleRepository scheduleRuleRepository;
private final GroupRepository groupRepository;
private final UserRepository userRepository;
private final AcademicDateService academicDateService;
private final Map<String, Integer> consumedHoursCache = new ConcurrentHashMap<>();
public ScheduleGeneratorService(ScheduleRuleRepository scheduleRuleRepository,
GroupRepository groupRepository,
UserRepository userRepository,
AcademicDateService academicDateService) {
this.scheduleRuleRepository = scheduleRuleRepository;
this.groupRepository = groupRepository;
this.userRepository = userRepository;
this.academicDateService = academicDateService;
}
public List<RenderedLessonDto> buildScheduleForGroup(Long groupId, LocalDate startDate, LocalDate endDate) {
validateRange(startDate, endDate);
StudentGroup group = groupRepository.findById(groupId)
.orElseThrow(() -> new IllegalArgumentException("Группа не найдена"));
Map<Long, List<ScheduleRule>> rulesBySemester = new HashMap<>();
List<RenderedLessonDto> result = new ArrayList<>();
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
Optional<Semester> semesterOpt = academicDateService.findSemester(date);
if (semesterOpt.isEmpty()) {
continue;
}
Semester semester = semesterOpt.get();
if (!academicDateService.isTheoryDay(group, semester, date)) {
continue;
}
List<ScheduleRule> rules = rulesBySemester.computeIfAbsent(
semester.getId(),
semesterId -> scheduleRuleRepository.findByGroupIdAndSemesterId(groupId, semesterId)
);
for (ScheduleRule rule : sortRules(rules)) {
renderRuleForDate(rule, date, group, null, result);
}
}
return sortRenderedLessons(result);
}
public List<RenderedLessonDto> buildScheduleForTeacher(Long teacherId, LocalDate startDate, LocalDate endDate) {
validateRange(startDate, endDate);
if (!userRepository.existsById(teacherId)) {
throw new IllegalArgumentException("Преподаватель не найден");
}
Map<Long, List<ScheduleRule>> rulesBySemester = new HashMap<>();
List<RenderedLessonDto> result = new ArrayList<>();
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
Optional<Semester> semesterOpt = academicDateService.findSemester(date);
if (semesterOpt.isEmpty()) {
continue;
}
Semester semester = semesterOpt.get();
List<ScheduleRule> rules = rulesBySemester.computeIfAbsent(
semester.getId(),
semesterId -> scheduleRuleRepository.findByTeacherIdAndSemesterId(teacherId, semesterId)
);
for (ScheduleRule rule : sortRules(rules)) {
renderRuleForDate(rule, date, null, teacherId, result);
}
}
return sortRenderedLessons(result);
}
public void clearCache() {
consumedHoursCache.clear();
}
private void validateRange(LocalDate startDate, LocalDate endDate) {
if (startDate == null || endDate == null) {
throw new IllegalArgumentException("Дата начала и дата окончания обязательны");
}
if (endDate.isBefore(startDate)) {
throw new IllegalArgumentException("Дата окончания не может быть раньше даты начала");
}
if (java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) > MAX_RANGE_DAYS) {
throw new IllegalArgumentException("Диапазон расписания не может превышать 120 дней");
}
}
private List<ScheduleRule> sortRules(List<ScheduleRule> rules) {
return rules.stream()
.sorted(Comparator
.comparing((ScheduleRule rule) -> rule.getSubject().getName(), Comparator.nullsLast(String::compareTo))
.thenComparing(ScheduleRule::getId))
.toList();
}
private List<RenderedLessonDto> sortRenderedLessons(List<RenderedLessonDto> lessons) {
return lessons.stream()
.sorted(Comparator
.comparing(RenderedLessonDto::date)
.thenComparing(RenderedLessonDto::timeSlotOrder, Comparator.nullsLast(Integer::compareTo))
.thenComparing(RenderedLessonDto::subjectName, Comparator.nullsLast(String::compareTo)))
.toList();
}
private void renderRuleForDate(ScheduleRule rule,
LocalDate date,
StudentGroup targetGroup,
Long targetTeacherId,
List<RenderedLessonDto> result) {
if (rule.getActiveFromDate() != null && date.isBefore(rule.getActiveFromDate())) {
return;
}
if (rule.getTotalAcademicHours() == null || rule.getTotalAcademicHours() <= 0) {
return;
}
Optional<Semester> semesterOpt = academicDateService.findSemester(date);
if (semesterOpt.isEmpty() || !Objects.equals(semesterOpt.get().getId(), rule.getSemester().getId())) {
return;
}
Semester semester = semesterOpt.get();
if (academicDateService.isHoliday(semester, date)) {
return;
}
List<StudentGroup> eligibleGroups = eligibleGroups(rule, targetGroup, semester, date);
if (eligibleGroups.isEmpty()) {
return;
}
ScheduleParity dateParity = academicDateService.getParity(semester, date);
int dayOfWeek = date.getDayOfWeek().getValue();
List<ScheduleRuleSlot> slots = rule.getSlots().stream()
.filter(slot -> Objects.equals(slot.getDayOfWeek(), dayOfWeek))
.filter(slot -> slot.getParity() == ScheduleParity.BOTH || slot.getParity() == dateParity)
.filter(slot -> targetTeacherId == null || Objects.equals(slot.getTeacher().getId(), targetTeacherId))
.sorted(Comparator
.comparing((ScheduleRuleSlot slot) -> slot.getTimeSlot().getOrderNumber(), Comparator.nullsLast(Integer::compareTo))
.thenComparing(ScheduleRuleSlot::getId))
.toList();
if (slots.isEmpty()) {
return;
}
int consumedHours = calculateConsumedHoursBeforeDate(rule, targetGroup, date);
int weekNumber = academicDateService.getWeekNumber(semester, date);
for (ScheduleRuleSlot slot : slots) {
if (consumedHours >= rule.getTotalAcademicHours()) {
break;
}
int remainingAfterLesson = Math.max(0, rule.getTotalAcademicHours() - consumedHours - ACADEMIC_HOURS_PER_SLOT);
result.add(toRenderedLesson(rule, slot, date, weekNumber, dateParity, eligibleGroups, consumedHours, remainingAfterLesson));
consumedHours += ACADEMIC_HOURS_PER_SLOT;
}
}
private List<StudentGroup> eligibleGroups(ScheduleRule rule, StudentGroup targetGroup, Semester semester, LocalDate date) {
if (targetGroup != null) {
return academicDateService.isTheoryDay(targetGroup, semester, date) ? List.of(targetGroup) : List.of();
}
return rule.getGroups().stream()
.filter(group -> academicDateService.isTheoryDay(group, semester, date))
.sorted(Comparator.comparing(StudentGroup::getName))
.toList();
}
private int calculateConsumedHoursBeforeDate(ScheduleRule rule, StudentGroup targetGroup, LocalDate targetDate) {
Long groupId = targetGroup == null ? null : targetGroup.getId();
String cacheKey = rule.getId() + ":" + (groupId == null ? "all" : groupId) + ":" + targetDate;
return consumedHoursCache.computeIfAbsent(cacheKey, ignored -> calculateConsumedHours(rule, targetGroup, targetDate));
}
private int calculateConsumedHours(ScheduleRule rule, StudentGroup targetGroup, LocalDate targetDate) {
LocalDate start = rule.getActiveFromDate() == null
? rule.getSemester().getStartDate()
: rule.getActiveFromDate();
if (start.isBefore(rule.getSemester().getStartDate())) {
start = rule.getSemester().getStartDate();
}
int consumed = 0;
for (LocalDate date = start; date.isBefore(targetDate); date = date.plusDays(1)) {
Semester semester = rule.getSemester();
if (date.isBefore(semester.getStartDate()) || date.isAfter(semester.getEndDate())) {
continue;
}
if (academicDateService.isHoliday(semester, date)) {
continue;
}
if (eligibleGroups(rule, targetGroup, semester, date).isEmpty()) {
continue;
}
int dayOfWeek = date.getDayOfWeek().getValue();
ScheduleParity dateParity = academicDateService.getParity(semester, date);
long activeSlots = rule.getSlots().stream()
.filter(slot -> Objects.equals(slot.getDayOfWeek(), dayOfWeek))
.filter(slot -> slot.getParity() == ScheduleParity.BOTH || slot.getParity() == dateParity)
.count();
consumed += (int) activeSlots * ACADEMIC_HOURS_PER_SLOT;
if (consumed >= rule.getTotalAcademicHours()) {
return consumed;
}
}
return consumed;
}
private RenderedLessonDto toRenderedLesson(ScheduleRule rule,
ScheduleRuleSlot slot,
LocalDate date,
int weekNumber,
ScheduleParity dateParity,
List<StudentGroup> groups,
int consumedBeforeLesson,
int remainingAfterLesson) {
TimeSlot timeSlot = slot.getTimeSlot();
List<StudentGroup> sortedGroups = groups.stream()
.sorted(Comparator.comparing(StudentGroup::getName))
.toList();
return new RenderedLessonDto(
rule.getId(),
slot.getId(),
date,
date.getDayOfWeek().getValue(),
dayName(date.getDayOfWeek()),
weekNumber,
dateParity,
timeSlot.getId(),
timeSlot.getOrderNumber(),
timeSlot.getStartTime(),
timeSlot.getEndTime(),
rule.getSubject().getId(),
rule.getSubject().getName(),
slot.getTeacher().getId(),
displayUserName(slot.getTeacher()),
slot.getClassroom().getId(),
slot.getClassroom().getName(),
slot.getLessonType().getId(),
slot.getLessonType().getLessonType(),
slot.getLessonFormat(),
slot.getSubgroupId(),
sortedGroups.stream().map(StudentGroup::getId).toList(),
sortedGroups.stream().map(StudentGroup::getName).collect(Collectors.toList()),
AcademicActivityType.THEORY,
rule.getTotalAcademicHours(),
consumedBeforeLesson,
remainingAfterLesson
);
}
public static String dayName(DayOfWeek dayOfWeek) {
String value = dayOfWeek.getDisplayName(TextStyle.FULL, Locale.forLanguageTag("ru-RU"));
return value.substring(0, 1).toUpperCase(Locale.forLanguageTag("ru-RU")) + value.substring(1);
}
public static String displayUserName(User user) {
if (user.getFullName() != null && !user.getFullName().isBlank()) {
return user.getFullName();
}
return user.getUsername();
}
}

View File

@@ -210,7 +210,227 @@ CREATE TABLE IF NOT EXISTS teacher_lesson_types (
);
-- ==========================================
-- Основная таблица Расписания (Lessons)
-- Динамическое расписание: календарь, слоты и правила
-- ==========================================
CREATE TABLE IF NOT EXISTS time_slots (
id BIGSERIAL PRIMARY KEY,
order_number INT NOT NULL,
start_time TIME NOT NULL,
end_time TIME NOT NULL,
duration_minutes INT NOT NULL,
CONSTRAINT chk_time_slots_order_positive CHECK (order_number > 0),
CONSTRAINT chk_time_slots_time_range CHECK (start_time < end_time),
CONSTRAINT chk_time_slots_duration_positive CHECK (duration_minutes > 0),
CONSTRAINT uq_time_slots_order UNIQUE (order_number),
CONSTRAINT uq_time_slots_range UNIQUE (start_time, end_time)
);
INSERT INTO time_slots (order_number, start_time, end_time, duration_minutes) VALUES
(1, '08:00', '09:30', 90),
(2, '09:40', '11:10', 90),
(3, '11:40', '13:10', 90),
(4, '13:20', '14:50', 90),
(5, '15:00', '16:30', 90),
(6, '16:50', '18:20', 90),
(7, '18:30', '20:00', 90)
ON CONFLICT (start_time, end_time) DO NOTHING;
CREATE TABLE IF NOT EXISTS academic_years (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(20) NOT NULL UNIQUE,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CONSTRAINT chk_academic_years_dates CHECK (start_date <= end_date)
);
INSERT INTO academic_years (title, start_date, end_date) VALUES
('2024-2025', '2024-09-01', '2025-06-30'),
('2025-2026', '2025-09-01', '2026-06-30')
ON CONFLICT (title) DO NOTHING;
CREATE TABLE IF NOT EXISTS semesters (
id BIGSERIAL PRIMARY KEY,
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
semester_type VARCHAR(20) NOT NULL,
start_date DATE NOT NULL,
end_date DATE NOT NULL,
CONSTRAINT chk_semesters_type CHECK (semester_type IN ('autumn', 'spring')),
CONSTRAINT chk_semesters_dates CHECK (start_date <= end_date),
CONSTRAINT uq_semesters_year_type UNIQUE (academic_year_id, semester_type)
);
INSERT INTO semesters (academic_year_id, semester_type, start_date, end_date)
SELECT id, 'autumn', start_date, MAKE_DATE(SPLIT_PART(title, '-', 2)::INT, 1, 31)
FROM academic_years
ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
INSERT INTO semesters (academic_year_id, semester_type, start_date, end_date)
SELECT id, 'spring', MAKE_DATE(SPLIT_PART(title, '-', 2)::INT, 2, 1), end_date
FROM academic_years
ON CONFLICT (academic_year_id, semester_type) DO NOTHING;
CREATE INDEX IF NOT EXISTS idx_semesters_dates ON semesters(start_date, end_date);
CREATE TABLE IF NOT EXISTS holidays (
id BIGSERIAL PRIMARY KEY,
date DATE NOT NULL,
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
description VARCHAR(255),
CONSTRAINT uq_holidays_year_date UNIQUE (academic_year_id, date)
);
CREATE INDEX IF NOT EXISTS idx_holidays_date ON holidays(date);
CREATE TABLE IF NOT EXISTS academic_calendar_matrix (
id BIGSERIAL PRIMARY KEY,
semester_id BIGINT NOT NULL REFERENCES semesters(id) ON DELETE CASCADE,
course_number INT NOT NULL,
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
week_number INT NOT NULL,
activity_type VARCHAR(20) NOT NULL,
CONSTRAINT chk_calendar_course_positive CHECK (course_number > 0),
CONSTRAINT chk_calendar_week_positive CHECK (week_number > 0),
CONSTRAINT chk_calendar_activity_type CHECK (activity_type IN ('THEORY', 'EXAM', 'VACATION', 'PRACTICE')),
CONSTRAINT uq_calendar_matrix UNIQUE (semester_id, course_number, specialty_id, week_number)
);
CREATE INDEX IF NOT EXISTS idx_calendar_lookup
ON academic_calendar_matrix(semester_id, course_number, specialty_id, week_number);
INSERT INTO academic_calendar_matrix (semester_id, course_number, specialty_id, week_number, activity_type)
SELECT
s.id,
SPLIT_PART(ay.title, '-', 1)::INT - sg.year_start_study + 1 AS course_number,
sg.specialty_code AS specialty_id,
weeks.week_number,
'THEORY'
FROM semesters s
JOIN academic_years ay ON ay.id = s.academic_year_id
CROSS JOIN student_groups sg
CROSS JOIN GENERATE_SERIES(1, 22) AS weeks(week_number)
WHERE SPLIT_PART(ay.title, '-', 1)::INT - sg.year_start_study + 1 > 0
ON CONFLICT (semester_id, course_number, specialty_id, week_number) DO NOTHING;
CREATE TABLE IF NOT EXISTS schedule_rules (
id BIGSERIAL PRIMARY KEY,
subject_id BIGINT NOT NULL REFERENCES subjects(id),
semester_id BIGINT NOT NULL REFERENCES semesters(id) ON DELETE CASCADE,
active_from_date DATE NOT NULL,
total_academic_hours INT NOT NULL,
CONSTRAINT chk_schedule_rules_hours_positive CHECK (total_academic_hours > 0)
);
CREATE INDEX IF NOT EXISTS idx_schedule_rules_semester ON schedule_rules(semester_id);
CREATE INDEX IF NOT EXISTS idx_schedule_rules_subject ON schedule_rules(subject_id);
CREATE TABLE IF NOT EXISTS schedule_rule_groups (
schedule_rule_id BIGINT NOT NULL REFERENCES schedule_rules(id) ON DELETE CASCADE,
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
PRIMARY KEY (schedule_rule_id, group_id)
);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_groups_group ON schedule_rule_groups(group_id);
CREATE TABLE IF NOT EXISTS schedule_rule_slots (
id BIGSERIAL PRIMARY KEY,
schedule_rule_id BIGINT NOT NULL REFERENCES schedule_rules(id) ON DELETE CASCADE,
day_of_week INT NOT NULL,
parity VARCHAR(10) NOT NULL,
time_slot_id BIGINT NOT NULL REFERENCES time_slots(id),
subgroup_id BIGINT NULL REFERENCES subgroups(id) ON DELETE SET NULL,
teacher_id BIGINT NOT NULL REFERENCES users(id),
classroom_id BIGINT NOT NULL REFERENCES classrooms(id),
lesson_type_id BIGINT NOT NULL REFERENCES lesson_types(id),
lesson_format VARCHAR(30) NOT NULL,
CONSTRAINT chk_schedule_rule_slots_day CHECK (day_of_week BETWEEN 1 AND 7),
CONSTRAINT chk_schedule_rule_slots_parity CHECK (parity IN ('BOTH', 'EVEN', 'ODD')),
CONSTRAINT chk_schedule_rule_slots_format CHECK (lesson_format IN ('Очно', 'Онлайн'))
);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_rule ON schedule_rule_slots(schedule_rule_id);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_teacher ON schedule_rule_slots(teacher_id);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_classroom ON schedule_rule_slots(classroom_id);
CREATE INDEX IF NOT EXISTS idx_schedule_rule_slots_time ON schedule_rule_slots(day_of_week, parity, time_slot_id);
CREATE TEMP TABLE tmp_default_rules AS
SELECT
NEXTVAL(PG_GET_SERIAL_SEQUENCE('schedule_rules', 'id')) AS schedule_rule_id,
rule_key,
subject_id,
semester_id,
active_from_date,
total_academic_hours
FROM (
SELECT
data.rule_key,
subj.id AS subject_id,
sem.id AS semester_id,
data.active_from_date,
data.total_academic_hours
FROM (VALUES
('stream_math', 'Высшая математика', '2025-2026', 'spring', DATE '2026-02-02', 72),
('ivt_informatics_lab', 'Информатика', '2025-2026', 'spring', DATE '2026-02-02', 72),
('ivt_databases_practice', 'Базы данных', '2025-2026', 'spring', DATE '2026-02-02', 72),
('ib_philosophy', 'Философия', '2025-2026', 'spring', DATE '2026-02-02', 54)
) AS data(rule_key, subject_name, year_title, semester_type, active_from_date, total_academic_hours)
JOIN subjects subj ON subj.name = data.subject_name
JOIN academic_years ay ON ay.title = data.year_title
JOIN semesters sem ON sem.academic_year_id = ay.id AND sem.semester_type = data.semester_type
) prepared_rules;
INSERT INTO schedule_rules (id, subject_id, semester_id, active_from_date, total_academic_hours)
SELECT schedule_rule_id, subject_id, semester_id, active_from_date, total_academic_hours
FROM tmp_default_rules;
INSERT INTO schedule_rule_groups (schedule_rule_id, group_id)
SELECT
rules.schedule_rule_id,
sg.id
FROM (VALUES
('stream_math', 'ИВТ-21-1'),
('stream_math', 'ИБ-41м'),
('ivt_informatics_lab', 'ИВТ-21-1'),
('ivt_databases_practice', 'ИВТ-21-1'),
('ib_philosophy', 'ИБ-41м')
) AS data(rule_key, group_name)
JOIN tmp_default_rules rules ON rules.rule_key = data.rule_key
JOIN student_groups sg ON sg.name = data.group_name
ON CONFLICT DO NOTHING;
INSERT INTO schedule_rule_slots (
schedule_rule_id,
day_of_week,
parity,
time_slot_id,
teacher_id,
classroom_id,
lesson_type_id,
lesson_format
)
SELECT
rules.schedule_rule_id,
data.day_of_week,
data.parity,
ts.id,
teacher.id,
classroom.id,
lt.id,
data.lesson_format
FROM (VALUES
('stream_math', 1, 'BOTH', 3, 'Тестовый преподаватель', '101 Ленинская', 'Лекция', 'Очно'),
('ivt_informatics_lab', 3, 'BOTH', 1, 'Тестовый преподаватель', '202 IT Lab', 'Лабораторная работа', 'Очно'),
('ivt_databases_practice', 5, 'EVEN', 5, 'Тестовый преподаватель', '303 Обычная', 'Практика', 'Онлайн'),
('ib_philosophy', 2, 'ODD', 2, 'Тестовый преподаватель', '101 Ленинская', 'Лекция', 'Онлайн')
) AS data(rule_key, day_of_week, parity, time_slot_order, teacher_username, classroom_name, lesson_type_name, lesson_format)
JOIN tmp_default_rules rules ON rules.rule_key = data.rule_key
JOIN time_slots ts ON ts.order_number = data.time_slot_order
JOIN users teacher ON teacher.username = data.teacher_username
JOIN classrooms classroom ON classroom.name = data.classroom_name
JOIN lesson_types lt ON lt.name = data.lesson_type_name;
-- ==========================================
-- Временные таблицы старой модели. Будут удалены после очистки старого кода.
-- ==========================================
CREATE TABLE IF NOT EXISTS lessons (
id BIGSERIAL PRIMARY KEY,
@@ -225,17 +445,6 @@ CREATE TABLE IF NOT EXISTS lessons (
time VARCHAR(255) NOT NULL
);
INSERT INTO lessons (teacher_id, group_id, subject_id, lesson_format, type_lesson, classroom_id, day, week, time) VALUES
(2, 1, 1, 'Очно', 'Лекция', 1, 'Понедельник', 'Верхняя', '11:40 - 13:10'),
(1, 1, 2, 'Онлайн', 'Практическая работа', 2, 'Вторник', 'Нижняя', '15:00 - 16:30'),
(2, 1, 3, 'Очно', 'Лабораторная работа', 3, 'Среда', 'Верхняя', '8:00 - 9:30'),
(1, 1, 4, 'Онлайн', 'Лекция', 1, 'Четверг', 'Нижняя', '11:40 - 13:10'),
(2, 1, 5, 'Очно', 'Практическая работа', 2, 'Пятница', 'Верхняя', '15:00 - 16:30'),
(1, 1, 3, 'Онлайн', 'Лабораторная работа', 3, 'Суббота', 'Нижняя', '8:00 - 9:30');
-- ===============================
-- Создание таблицы данных расписания (schedule_data)
-- ===============================
CREATE TABLE IF NOT EXISTS schedule_data (
id BIGSERIAL PRIMARY KEY,
department_id BIGINT NOT NULL REFERENCES departments(id),
@@ -249,18 +458,6 @@ CREATE TABLE IF NOT EXISTS schedule_data (
period VARCHAR(255) NOT NULL
);
INSERT INTO schedule_data (department_id, group_id, subjects_id, lesson_type_id, number_of_hours, is_division, teacher_id, semester_type, period)
VALUES (1, 1, 1, 3, 2, true, 1, 'autumn', '2024-2025'),
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
(3, 1, 2, 1, 3, true, 1, 'autumn', '2023-2024'),
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
(2, 2, 3, 2, 1, false, 2, 'spring', '2025-2026'),
(1, 1, 1, 1, 2, true, 2, 'autumn', '2024-2025'),
(1, 2, 2, 3, 4, false, 2, 'autumn', '2024-2025'),
(1, 1, 4, 2, 1, false, 1, 'autumn', '2024-2025'),
(1, 2, 5, 1, 7, true, 1, 'autumn', '2024-2025');
-- ==========================================
-- Функция обновления timestamp
-- ==========================================
@@ -281,10 +478,18 @@ CREATE TRIGGER update_users_updated_at
-- Комментарии к таблицам и полям (для документации)
-- ==========================================
COMMENT ON TABLE users IS 'Пользователи системы (студенты, преподаватели, администраторы)';
COMMENT ON TABLE lessons IS 'Основное расписание занятий';
COMMENT ON TABLE lessons IS 'Временная таблица старой модели расписания до удаления старого кода';
COMMENT ON TABLE departments IS 'Кафедры';
COMMENT ON TABLE specialties IS 'Специальности';
COMMENT ON TABLE schedule_data IS 'Данные к составлению расписания';
COMMENT ON TABLE schedule_data IS 'Временная таблица старой модели плановой нагрузки до удаления старого кода';
COMMENT ON TABLE time_slots IS 'Настраиваемая сетка временных слотов занятий';
COMMENT ON TABLE academic_years IS 'Учебные годы';
COMMENT ON TABLE semesters IS 'Семестры учебного года';
COMMENT ON TABLE holidays IS 'Праздники и неучебные даты';
COMMENT ON TABLE academic_calendar_matrix IS 'Матрица учебного графика по курсу, специальности и неделе';
COMMENT ON TABLE schedule_rules IS 'Правила динамической генерации расписания';
COMMENT ON TABLE schedule_rule_groups IS 'Привязка правил расписания к учебным группам';
COMMENT ON TABLE schedule_rule_slots IS 'Слоты проведения занятий внутри правила расписания';
COMMENT ON COLUMN schedule_data.department_id IS 'Идентификатор кафедры';
COMMENT ON COLUMN schedule_data.group_id IS 'Идентификатор группы';
COMMENT ON COLUMN schedule_data.subjects_id IS 'Идентификатор предмета';
@@ -324,6 +529,8 @@ COMMENT ON COLUMN student_groups.name IS 'Название группы';
COMMENT ON COLUMN student_groups.group_size IS 'Количество студентов';
COMMENT ON COLUMN student_groups.education_form_id IS 'ID формы обучения, к которой относится группа';
COMMENT ON COLUMN student_groups.department_id IS 'ID кафедры';
COMMENT ON COLUMN student_groups.specialty_code IS 'ID специальности';
COMMENT ON COLUMN student_groups.year_start_study IS 'Год начала обучения';
COMMENT ON COLUMN student_groups.created_at IS 'Дата и время создания';
COMMENT ON COLUMN subgroups.id IS 'ID подгруппы';
@@ -388,4 +595,4 @@ COMMENT ON COLUMN specialties.specialty_code IS 'Код специальност
COMMENT ON COLUMN teacher_lesson_types.user_id IS 'ID преподавателя';
COMMENT ON COLUMN teacher_lesson_types.subject_id IS 'ID предмета';
COMMENT ON COLUMN teacher_lesson_types.lesson_type_id IS 'ID типа занятия';
COMMENT ON COLUMN teacher_lesson_types.lesson_type_id IS 'ID типа занятия';