создал систему календарного учебного графика

This commit is contained in:
Zuev
2026-04-30 23:40:49 +03:00
parent 96e9d8155f
commit 89c822a073
56 changed files with 3356 additions and 1644 deletions

View File

@@ -5,7 +5,6 @@ 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;
@@ -18,19 +17,19 @@ public class AcademicCalendarAdminController {
private final AcademicYearRepository academicYearRepository;
private final SemesterRepository semesterRepository;
private final HolidayRepository holidayRepository;
private final AcademicCalendarMatrixRepository calendarMatrixRepository;
private final AcademicCalendarActivityTypeRepository activityTypeRepository;
private final AcademicCalendarDayRepository calendarDayRepository;
private final ScheduleGeneratorService scheduleGeneratorService;
public AcademicCalendarAdminController(AcademicYearRepository academicYearRepository,
SemesterRepository semesterRepository,
HolidayRepository holidayRepository,
AcademicCalendarMatrixRepository calendarMatrixRepository,
AcademicCalendarActivityTypeRepository activityTypeRepository,
AcademicCalendarDayRepository calendarDayRepository,
ScheduleGeneratorService scheduleGeneratorService) {
this.academicYearRepository = academicYearRepository;
this.semesterRepository = semesterRepository;
this.holidayRepository = holidayRepository;
this.calendarMatrixRepository = calendarMatrixRepository;
this.activityTypeRepository = activityTypeRepository;
this.calendarDayRepository = calendarDayRepository;
this.scheduleGeneratorService = scheduleGeneratorService;
}
@@ -128,113 +127,60 @@ public class AcademicCalendarAdminController {
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());
@GetMapping("/activity-types")
public List<AcademicCalendarActivityTypeDto> getActivityTypes() {
return activityTypeRepository.findAllByOrderByDisplayOrderAscCodeAsc().stream()
.map(this::toActivityTypeDto)
.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", "Учебный год не найден"));
@PostMapping("/activity-types")
public ResponseEntity<?> createActivityType(@RequestBody AcademicCalendarActivityTypeDto request) {
String validationError = validateActivityType(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
if (request.date() == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Дата праздника обязательна"));
if (activityTypeRepository.findByCode(request.code().trim()).isPresent()) {
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());
AcademicCalendarActivityType activityType = new AcademicCalendarActivityType();
applyActivityType(activityType, request);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toHolidayDto(holidayRepository.save(holiday)));
return ResponseEntity.ok(toActivityTypeDto(activityTypeRepository.save(activityType)));
}
@PutMapping("/holidays/{id}")
public ResponseEntity<?> updateHoliday(@PathVariable Long id, @RequestBody HolidayDto request) {
Holiday holiday = holidayRepository.findById(id).orElse(null);
if (holiday == null) {
@PutMapping("/activity-types/{id}")
public ResponseEntity<?> updateActivityType(@PathVariable Long id,
@RequestBody AcademicCalendarActivityTypeDto request) {
AcademicCalendarActivityType activityType = activityTypeRepository.findById(id).orElse(null);
if (activityType == null) {
return ResponseEntity.notFound().build();
}
if (request.date() == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Дата праздника обязательна"));
String validationError = validateActivityType(request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
holiday.setDate(request.date());
holiday.setDescription(request.description());
if (activityTypeRepository.findByCode(request.code().trim())
.filter(existing -> !existing.getId().equals(id))
.isPresent()) {
return ResponseEntity.badRequest().body(Map.of("message", "Код активности уже существует"));
}
applyActivityType(activityType, request);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toHolidayDto(holidayRepository.save(holiday)));
return ResponseEntity.ok(toActivityTypeDto(activityTypeRepository.save(activityType)));
}
@DeleteMapping("/holidays/{id}")
public ResponseEntity<?> deleteHoliday(@PathVariable Long id) {
if (!holidayRepository.existsById(id)) {
@DeleteMapping("/activity-types/{id}")
public ResponseEntity<?> deleteActivityType(@PathVariable Long id) {
if (!activityTypeRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
holidayRepository.deleteById(id);
if (calendarDayRepository.existsByActivityTypeId(id)) {
return ResponseEntity.badRequest().body(Map.of("message", "Код активности используется в календарных графиках"));
}
activityTypeRepository.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", "Матрица учебного графика сохранена"));
return ResponseEntity.ok(Map.of("message", "Код активности удалён"));
}
private String validateYear(AcademicYearDto request) {
@@ -263,6 +209,19 @@ public class AcademicCalendarAdminController {
return null;
}
private String validateActivityType(AcademicCalendarActivityTypeDto request) {
if (request == null) {
return "Передайте код активности";
}
if (request.code() == null || request.code().isBlank()) {
return "Код активности обязателен";
}
if (request.name() == null || request.name().isBlank()) {
return "Название активности обязательно";
}
return null;
}
private void applyYear(AcademicYear year, AcademicYearDto request) {
year.setTitle(request.title().trim());
year.setStartDate(request.startDate());
@@ -275,6 +234,19 @@ public class AcademicCalendarAdminController {
semester.setEndDate(request.endDate());
}
private void applyActivityType(AcademicCalendarActivityType activityType, AcademicCalendarActivityTypeDto request) {
activityType.setCode(request.code().trim());
activityType.setName(request.name().trim());
activityType.setAllowSchedule(Boolean.TRUE.equals(request.allowSchedule()));
activityType.setColorCode(request.colorCode() == null || request.colorCode().isBlank()
? "#64748b"
: request.colorCode().trim());
activityType.setDisplayOrder(request.displayOrder() == null ? 100 : request.displayOrder());
activityType.setDescription(request.description() == null || request.description().isBlank()
? null
: request.description().trim());
}
private AcademicYearDto toAcademicYearDto(AcademicYear year) {
List<SemesterDto> semesters = semesterRepository.findByAcademicYearIdOrderByStartDateAsc(year.getId())
.stream()
@@ -294,24 +266,15 @@ public class AcademicCalendarAdminController {
);
}
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()
private AcademicCalendarActivityTypeDto toActivityTypeDto(AcademicCalendarActivityType activityType) {
return new AcademicCalendarActivityTypeDto(
activityType.getId(),
activityType.getCode(),
activityType.getName(),
activityType.getAllowSchedule(),
activityType.getColorCode(),
activityType.getDisplayOrder(),
activityType.getDescription()
);
}
}

View File

@@ -0,0 +1,255 @@
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.List;
import java.util.Map;
@RestController
@RequestMapping("/api/admin/academic-calendars")
public class AcademicCalendarController {
private final AcademicCalendarRepository calendarRepository;
private final AcademicCalendarDayRepository calendarDayRepository;
private final AcademicCalendarActivityTypeRepository activityTypeRepository;
private final AcademicYearRepository academicYearRepository;
private final SpecialtiesRepository specialtiesRepository;
private final SpecialtyProfileRepository profileRepository;
private final CalendarStudyFormRepository studyFormRepository;
private final ScheduleGeneratorService scheduleGeneratorService;
public AcademicCalendarController(AcademicCalendarRepository calendarRepository,
AcademicCalendarDayRepository calendarDayRepository,
AcademicCalendarActivityTypeRepository activityTypeRepository,
AcademicYearRepository academicYearRepository,
SpecialtiesRepository specialtiesRepository,
SpecialtyProfileRepository profileRepository,
CalendarStudyFormRepository studyFormRepository,
ScheduleGeneratorService scheduleGeneratorService) {
this.calendarRepository = calendarRepository;
this.calendarDayRepository = calendarDayRepository;
this.activityTypeRepository = activityTypeRepository;
this.academicYearRepository = academicYearRepository;
this.specialtiesRepository = specialtiesRepository;
this.profileRepository = profileRepository;
this.studyFormRepository = studyFormRepository;
this.scheduleGeneratorService = scheduleGeneratorService;
}
@GetMapping
public List<AcademicCalendarDto> getCalendars(@RequestParam(required = false) Long academicYearId,
@RequestParam(required = false) Long specialtyId,
@RequestParam(required = false) Long profileId) {
return calendarRepository.findAllWithDetails(academicYearId, specialtyId, profileId).stream()
.map(this::toCalendarDto)
.toList();
}
@GetMapping("/study-forms")
public List<CalendarStudyFormDto> getStudyForms() {
return studyFormRepository.findAllByOrderByNameAsc().stream()
.map(form -> new CalendarStudyFormDto(form.getId(), form.getName(), form.getDescription()))
.toList();
}
@GetMapping("/{id}")
public ResponseEntity<?> getCalendar(@PathVariable Long id) {
return calendarRepository.findByIdWithDetails(id)
.<ResponseEntity<?>>map(calendar -> ResponseEntity.ok(toCalendarDto(calendar)))
.orElseGet(() -> ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<?> createCalendar(@RequestBody AcademicCalendarDto request) {
try {
AcademicCalendar calendar = new AcademicCalendar();
applyCalendar(calendar, request);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toCalendarDto(calendarRepository.save(calendar)));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
}
@PutMapping("/{id}")
public ResponseEntity<?> updateCalendar(@PathVariable Long id, @RequestBody AcademicCalendarDto request) {
AcademicCalendar calendar = calendarRepository.findById(id).orElse(null);
if (calendar == null) {
return ResponseEntity.notFound().build();
}
try {
applyCalendar(calendar, request);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toCalendarDto(calendarRepository.save(calendar)));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteCalendar(@PathVariable Long id) {
if (!calendarRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
try {
calendarRepository.deleteById(id);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(Map.of("message", "Календарный график удалён"));
} catch (Exception e) {
return ResponseEntity.badRequest()
.body(Map.of("message", "Нельзя удалить график, который назначен учебным группам"));
}
}
@GetMapping("/{id}/grid")
public ResponseEntity<?> getGrid(@PathVariable Long id) {
if (!calendarRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(calendarDayRepository.findByCalendarIdWithActivity(id).stream()
.map(this::toGridDayDto)
.toList());
}
@PutMapping("/{id}/grid")
@Transactional
public ResponseEntity<?> saveGrid(@PathVariable Long id, @RequestBody List<AcademicCalendarGridDayDto> rows) {
AcademicCalendar calendar = calendarRepository.findByIdWithDetails(id).orElse(null);
if (calendar == null) {
return ResponseEntity.notFound().build();
}
if (rows == null || rows.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "Передайте хотя бы одну ячейку графика"));
}
try {
calendarDayRepository.deleteByCalendarId(id);
for (AcademicCalendarGridDayDto row : rows) {
AcademicCalendarDay day = buildGridDay(calendar, row);
calendarDayRepository.save(day);
}
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(Map.of("message", "Календарный учебный график сохранён"));
} catch (IllegalArgumentException e) {
return ResponseEntity.badRequest().body(Map.of("message", e.getMessage()));
}
}
private void applyCalendar(AcademicCalendar calendar, AcademicCalendarDto request) {
if (request == null) {
throw new IllegalArgumentException("Передайте данные календарного графика");
}
if (request.title() == null || request.title().isBlank()) {
throw new IllegalArgumentException("Название графика обязательно");
}
if (request.courseCount() == null || request.courseCount() <= 0) {
throw new IllegalArgumentException("Количество курсов должно быть больше нуля");
}
if (request.academicYearId() == null || request.specialtyId() == null
|| request.specialtyProfileId() == null || request.studyFormId() == null) {
throw new IllegalArgumentException("Учебный год, специальность, профиль и форма обучения обязательны");
}
AcademicYear year = academicYearRepository.findById(request.academicYearId())
.orElseThrow(() -> new IllegalArgumentException("Учебный год не найден"));
Speciality speciality = specialtiesRepository.findById(request.specialtyId())
.orElseThrow(() -> new IllegalArgumentException("Специальность не найдена"));
SpecialtyProfile profile = profileRepository.findById(request.specialtyProfileId())
.orElseThrow(() -> new IllegalArgumentException("Профиль обучения не найден"));
if (!profile.getSpeciality().getId().equals(speciality.getId())) {
throw new IllegalArgumentException("Профиль не относится к выбранной специальности");
}
CalendarStudyForm studyForm = studyFormRepository.findById(request.studyFormId())
.orElseThrow(() -> new IllegalArgumentException("Форма обучения графика не найдена"));
calendar.setTitle(request.title().trim());
calendar.setAcademicYear(year);
calendar.setSpeciality(speciality);
calendar.setSpecialtyProfile(profile);
calendar.setStudyForm(studyForm);
calendar.setCourseCount(request.courseCount());
}
private AcademicCalendarDay buildGridDay(AcademicCalendar calendar, AcademicCalendarGridDayDto row) {
if (row.courseNumber() == null || row.courseNumber() <= 0
|| row.date() == null
|| row.weekNumber() == null || row.weekNumber() <= 0
|| row.dayOfWeek() == null || row.dayOfWeek() < 1 || row.dayOfWeek() > 7) {
throw new IllegalArgumentException("Курс, дата, неделя и день недели обязательны");
}
if (row.date().isBefore(calendar.getAcademicYear().getStartDate())
|| row.date().isAfter(calendar.getAcademicYear().getEndDate())) {
throw new IllegalArgumentException("Дата выходит за пределы учебного года");
}
if (row.courseNumber() > calendar.getCourseCount()) {
throw new IllegalArgumentException("Номер курса выходит за пределы графика");
}
AcademicCalendarActivityType activityType = resolveActivityType(row);
AcademicCalendarDay day = new AcademicCalendarDay();
day.setAcademicCalendar(calendar);
day.setCourseNumber(row.courseNumber());
day.setDate(row.date());
day.setWeekNumber(row.weekNumber());
day.setDayOfWeek(row.dayOfWeek());
day.setActivityType(activityType);
return day;
}
private AcademicCalendarActivityType resolveActivityType(AcademicCalendarGridDayDto row) {
if (row.activityTypeId() != null) {
return activityTypeRepository.findById(row.activityTypeId())
.orElseThrow(() -> new IllegalArgumentException("Код активности не найден"));
}
if (row.activityCode() != null && !row.activityCode().isBlank()) {
return activityTypeRepository.findByCode(row.activityCode().trim())
.orElseThrow(() -> new IllegalArgumentException("Код активности не найден"));
}
throw new IllegalArgumentException("Код активности обязателен");
}
private AcademicCalendarDto toCalendarDto(AcademicCalendar calendar) {
AcademicYear year = calendar.getAcademicYear();
Speciality speciality = calendar.getSpeciality();
SpecialtyProfile profile = calendar.getSpecialtyProfile();
CalendarStudyForm studyForm = calendar.getStudyForm();
return new AcademicCalendarDto(
calendar.getId(),
calendar.getTitle(),
year.getId(),
year.getTitle(),
year.getStartDate(),
year.getEndDate(),
speciality.getId(),
speciality.getSpecialityCode(),
speciality.getSpecialityName(),
profile.getId(),
profile.getName(),
studyForm.getId(),
studyForm.getName(),
calendar.getCourseCount()
);
}
private AcademicCalendarGridDayDto toGridDayDto(AcademicCalendarDay day) {
AcademicCalendarActivityType activityType = day.getActivityType();
return new AcademicCalendarGridDayDto(
day.getId(),
day.getAcademicCalendar().getId(),
day.getCourseNumber(),
day.getDate(),
day.getWeekNumber(),
day.getDayOfWeek(),
activityType.getId(),
activityType.getCode(),
activityType.getName(),
activityType.getAllowSchedule()
);
}
}

View File

@@ -1,11 +1,17 @@
package com.magistr.app.controller;
import com.magistr.app.dto.CreateGroupRequest;
import com.magistr.app.dto.GroupCalendarAssignmentDto;
import com.magistr.app.dto.GroupResponse;
import com.magistr.app.model.AcademicCalendar;
import com.magistr.app.model.AcademicYear;
import com.magistr.app.model.EducationForm;
import com.magistr.app.model.Speciality;
import com.magistr.app.model.SpecialtyProfile;
import com.magistr.app.model.StudentGroup;
import com.magistr.app.repository.EducationFormRepository;
import com.magistr.app.repository.GroupRepository;
import com.magistr.app.model.StudentGroupCalendarAssignment;
import com.magistr.app.repository.*;
import com.magistr.app.service.ScheduleGeneratorService;
import com.magistr.app.utils.CourseAndSemesterCalculator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -13,7 +19,6 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.time.Year;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@@ -26,11 +31,29 @@ public class GroupController {
private final GroupRepository groupRepository;
private final EducationFormRepository educationFormRepository;
private final SpecialtiesRepository specialtiesRepository;
private final SpecialtyProfileRepository specialtyProfileRepository;
private final AcademicYearRepository academicYearRepository;
private final AcademicCalendarRepository academicCalendarRepository;
private final StudentGroupCalendarAssignmentRepository assignmentRepository;
private final ScheduleGeneratorService scheduleGeneratorService;
public GroupController(GroupRepository groupRepository,
EducationFormRepository educationFormRepository) {
EducationFormRepository educationFormRepository,
SpecialtiesRepository specialtiesRepository,
SpecialtyProfileRepository specialtyProfileRepository,
AcademicYearRepository academicYearRepository,
AcademicCalendarRepository academicCalendarRepository,
StudentGroupCalendarAssignmentRepository assignmentRepository,
ScheduleGeneratorService scheduleGeneratorService) {
this.groupRepository = groupRepository;
this.educationFormRepository = educationFormRepository;
this.specialtiesRepository = specialtiesRepository;
this.specialtyProfileRepository = specialtyProfileRepository;
this.academicYearRepository = academicYearRepository;
this.academicCalendarRepository = academicCalendarRepository;
this.assignmentRepository = assignmentRepository;
this.scheduleGeneratorService = scheduleGeneratorService;
}
@GetMapping
@@ -41,21 +64,7 @@ public class GroupController {
List<StudentGroup> groups = groupRepository.findAll();
List<GroupResponse> response = groups.stream()
.map(g -> {
int course = CourseAndSemesterCalculator.getActualCourse(g.getYearStartStudy());
int semester = CourseAndSemesterCalculator.getActualSemester(g.getYearStartStudy());
return new GroupResponse(
g.getId(),
g.getName(),
g.getGroupSize(),
g.getEducationForm().getId(),
g.getEducationForm().getName(),
g.getDepartmentId(),
course,
semester,
g.getSpecialityCode()
);
})
.map(this::mapToResponse)
.toList();
logger.info("Получено {} групп", response.size());
return response;
@@ -78,21 +87,7 @@ public class GroupController {
}
List<GroupResponse> response = groups.stream()
.map(g -> {
int course = CourseAndSemesterCalculator.getActualCourse(g.getYearStartStudy());
int semester = CourseAndSemesterCalculator.getActualSemester(g.getYearStartStudy());
return new GroupResponse(
g.getId(),
g.getName(),
g.getGroupSize(),
g.getEducationForm().getId(),
g.getEducationForm().getName(),
g.getDepartmentId(),
course,
semester,
g.getSpecialityCode()
);
})
.map(this::mapToResponse)
.toList();
logger.info("Найдено {} групп для кафедры с ID - {}", response.size(), departmentId);
@@ -110,51 +105,25 @@ public class GroupController {
logger.info("Получен запрос на создание новой группы: name = {}, groupSize = {}, educationFormId = {}, departmentId = {}, yearStartStudy = {}",
request.getName(), request.getGroupSize(), request.getEducationFormId(), request.getDepartmentId(), request.getYearStartStudy());
try {
if (request.getName() == null || request.getName().isBlank()) {
String errorMessage = "Название группы обязательно";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
if (groupRepository.findByName(request.getName().trim()).isPresent()) {
String errorMessage = "Группа с таким названием уже существует";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
if (request.getGroupSize() == null) {
String errorMessage = "Численность группы обязательна";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
if (request.getEducationFormId() == null) {
String errorMessage = "Форма обучения обязательна";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
if (request.getDepartmentId() == null || request.getDepartmentId() == 0) {
String errorMessage = "ID кафедры обязателен";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
// if (request.getCourse() == null || request.getCourse() == 0) {
// String errorMessage = "Курс обязателен";
// logger.error("Ошибка валидации: {}", errorMessage);
// return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
// }
if (request.getYearStartStudy() == null || request.getYearStartStudy() == 0) {
String errorMessage = "Год начала обучения обязателен";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
if (request.getSpecialityCode() == null || request.getSpecialityCode() == 0) {
String errorMessage = "Код специальности обязателен";
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
ResponseEntity<?> validationError = validateGroupRequest(request);
if (validationError != null) {
return validationError;
}
Optional<EducationForm> efOpt = educationFormRepository.findById(request.getEducationFormId());
if (efOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "Форма обучения не найдена"));
}
Speciality speciality = specialtiesRepository.findById(request.getEffectiveSpecialtyId())
.orElse(null);
if (speciality == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Специальность не найдена"));
}
SpecialtyProfile profile = specialtyProfileRepository.findById(request.getSpecialtyProfileId())
.orElse(null);
if (profile == null || !profile.getSpeciality().getId().equals(speciality.getId())) {
return ResponseEntity.badRequest().body(Map.of("message", "Профиль обучения не найден для выбранной специальности"));
}
StudentGroup group = new StudentGroup();
group.setName(request.getName().trim());
@@ -162,20 +131,13 @@ public class GroupController {
group.setEducationForm(efOpt.get());
group.setDepartmentId(request.getDepartmentId());
group.setYearStartStudy(request.getYearStartStudy());
group.setSpecialityCode(request.getSpecialityCode());
group.setSpeciality(speciality);
group.setSpecialtyProfile(profile);
groupRepository.save(group);
logger.info("Группа успешно создана с ID - {}", group.getId());
return ResponseEntity.ok(new GroupResponse(
group.getId(),
group.getName(),
group.getGroupSize(),
group.getEducationForm().getId(),
group.getEducationForm().getName(),
group.getDepartmentId(),
group.getYearStartStudy(),
group.getSpecialityCode()));
return ResponseEntity.ok(mapToResponse(group));
} catch (Exception e ) {
logger.error("Ошибка при создании группы: {}", e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
@@ -183,6 +145,55 @@ public class GroupController {
}
}
@PutMapping("/{id}")
public ResponseEntity<?> updateGroup(@PathVariable Long id, @RequestBody CreateGroupRequest request) {
logger.info("Получен запрос на обновление группы с ID - {}", id);
try {
Optional<StudentGroup> groupOpt = groupRepository.findById(id);
if (groupOpt.isEmpty()) {
logger.info("Группа с ID - {} не найдена", id);
return ResponseEntity.notFound().build();
}
ResponseEntity<?> validationError = validateGroupRequest(request);
if (validationError != null) {
return validationError;
}
Optional<EducationForm> efOpt = educationFormRepository.findById(request.getEducationFormId());
if (efOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("message", "Форма обучения не найдена"));
}
Speciality speciality = specialtiesRepository.findById(request.getEffectiveSpecialtyId())
.orElse(null);
if (speciality == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Специальность не найдена"));
}
SpecialtyProfile profile = specialtyProfileRepository.findById(request.getSpecialtyProfileId())
.orElse(null);
if (profile == null || !profile.getSpeciality().getId().equals(speciality.getId())) {
return ResponseEntity.badRequest().body(Map.of("message", "Профиль обучения не найден для выбранной специальности"));
}
StudentGroup group = groupOpt.get();
group.setName(request.getName().trim());
group.setGroupSize(request.getGroupSize());
group.setEducationForm(efOpt.get());
group.setDepartmentId(request.getDepartmentId());
group.setYearStartStudy(request.getYearStartStudy());
group.setSpeciality(speciality);
group.setSpecialtyProfile(profile);
groupRepository.save(group);
logger.info("Группа с ID - {} успешно обновлена", id);
return ResponseEntity.ok(mapToResponse(group));
} catch (Exception e) {
logger.error("Ошибка при обновлении группы с ID - {}: {}", id, e.getMessage(), e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("message", "Произошла ошибка при обновлении группы: " + e.getMessage()));
}
}
@DeleteMapping("/{id}")
public ResponseEntity<?> deleteGroup(@PathVariable Long id) {
logger.info("Получен запрос на удаление группы с ID - {}", id);
@@ -194,4 +205,126 @@ public class GroupController {
logger.info("Группа с ID - {} успешно удалена", id);
return ResponseEntity.ok(Map.of("message", "Группа удалена"));
}
private ResponseEntity<?> validateGroupRequest(CreateGroupRequest request) {
if (request.getName() == null || request.getName().isBlank()) {
return validationError("Название группы обязательно");
}
if (request.getGroupSize() == null) {
return validationError("Численность группы обязательна");
}
if (request.getEducationFormId() == null) {
return validationError("Форма обучения обязательна");
}
if (request.getDepartmentId() == null || request.getDepartmentId() == 0) {
return validationError("ID кафедры обязателен");
}
if (request.getYearStartStudy() == null || request.getYearStartStudy() == 0) {
return validationError("Год начала обучения обязателен");
}
if (request.getEffectiveSpecialtyId() == null || request.getEffectiveSpecialtyId() == 0) {
return validationError("Код специальности обязателен");
}
if (request.getSpecialtyProfileId() == null || request.getSpecialtyProfileId() == 0) {
return validationError("Профиль обучения обязателен");
}
return null;
}
private ResponseEntity<?> validationError(String errorMessage) {
logger.error("Ошибка валидации: {}", errorMessage);
return ResponseEntity.badRequest().body(Map.of("message", errorMessage));
}
private GroupResponse mapToResponse(StudentGroup group) {
int course = CourseAndSemesterCalculator.getActualCourse(group.getYearStartStudy());
int semester = CourseAndSemesterCalculator.getActualSemester(group.getYearStartStudy());
Speciality speciality = group.getSpeciality();
SpecialtyProfile profile = group.getSpecialtyProfile();
return new GroupResponse(
group.getId(),
group.getName(),
group.getGroupSize(),
group.getEducationForm().getId(),
group.getEducationForm().getName(),
group.getDepartmentId(),
group.getYearStartStudy(),
course,
semester,
speciality.getId(),
speciality.getSpecialityCode(),
speciality.getSpecialityName(),
profile.getId(),
profile.getName()
);
}
@GetMapping("/{id}/calendar-assignments")
public ResponseEntity<?> getCalendarAssignments(@PathVariable Long id) {
if (!groupRepository.existsById(id)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(assignmentRepository.findByGroupIdWithDetails(id).stream()
.map(this::toAssignmentDto)
.toList());
}
@PutMapping("/{id}/calendar-assignments")
public ResponseEntity<?> saveCalendarAssignment(@PathVariable Long id,
@RequestBody GroupCalendarAssignmentDto request) {
if (request == null || request.academicYearId() == null || request.calendarId() == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год и календарный график обязательны"));
}
StudentGroup group = groupRepository.findById(id).orElse(null);
if (group == null) {
return ResponseEntity.notFound().build();
}
AcademicYear academicYear = academicYearRepository.findById(request.academicYearId()).orElse(null);
if (academicYear == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Учебный год не найден"));
}
AcademicCalendar calendar = academicCalendarRepository.findByIdWithDetails(request.calendarId()).orElse(null);
if (calendar == null) {
return ResponseEntity.badRequest().body(Map.of("message", "Календарный график не найден"));
}
if (!calendar.getAcademicYear().getId().equals(academicYear.getId())) {
return ResponseEntity.badRequest().body(Map.of("message", "График относится к другому учебному году"));
}
if (!calendar.getSpeciality().getId().equals(group.getSpeciality().getId())
|| !calendar.getSpecialtyProfile().getId().equals(group.getSpecialtyProfile().getId())) {
return ResponseEntity.badRequest().body(Map.of("message", "График не соответствует специальности и профилю группы"));
}
StudentGroupCalendarAssignment assignment = assignmentRepository
.findByGroupIdAndAcademicYearIdWithDetails(group.getId(), academicYear.getId())
.orElseGet(StudentGroupCalendarAssignment::new);
assignment.setStudentGroup(group);
assignment.setAcademicYear(academicYear);
assignment.setAcademicCalendar(calendar);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(toAssignmentDto(assignmentRepository.save(assignment)));
}
@DeleteMapping("/{id}/calendar-assignments/{assignmentId}")
public ResponseEntity<?> deleteCalendarAssignment(@PathVariable Long id, @PathVariable Long assignmentId) {
StudentGroupCalendarAssignment assignment = assignmentRepository.findById(assignmentId).orElse(null);
if (assignment == null || !assignment.getStudentGroup().getId().equals(id)) {
return ResponseEntity.notFound().build();
}
assignmentRepository.delete(assignment);
scheduleGeneratorService.clearCache();
return ResponseEntity.ok(Map.of("message", "Назначение календарного графика удалено"));
}
private GroupCalendarAssignmentDto toAssignmentDto(StudentGroupCalendarAssignment assignment) {
return new GroupCalendarAssignmentDto(
assignment.getId(),
assignment.getStudentGroup().getId(),
assignment.getStudentGroup().getName(),
assignment.getAcademicYear().getId(),
assignment.getAcademicYear().getTitle(),
assignment.getAcademicCalendar().getId(),
assignment.getAcademicCalendar().getTitle()
);
}
}

View File

@@ -2,8 +2,11 @@ package com.magistr.app.controller;
import com.magistr.app.dto.CreateSpecialityRequest;
import com.magistr.app.dto.SpecialityResponse;
import com.magistr.app.dto.SpecialtyProfileDto;
import com.magistr.app.model.Speciality;
import com.magistr.app.model.SpecialtyProfile;
import com.magistr.app.repository.SpecialtiesRepository;
import com.magistr.app.repository.SpecialtyProfileRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
@@ -20,9 +23,12 @@ public class SpecialityController {
private static final Logger logger = LoggerFactory.getLogger(SpecialityController.class);
private final SpecialtiesRepository specialtiesRepository;
private final SpecialtyProfileRepository specialtyProfileRepository;
public SpecialityController(SpecialtiesRepository specialtiesRepository) {
public SpecialityController(SpecialtiesRepository specialtiesRepository,
SpecialtyProfileRepository specialtyProfileRepository) {
this.specialtiesRepository = specialtiesRepository;
this.specialtyProfileRepository = specialtyProfileRepository;
}
@GetMapping
@@ -75,6 +81,10 @@ public class SpecialityController {
speciality.setSpecialityName(request.getSpecialityName());
speciality.setSpecialityCode(request.getSpecialityCode());
specialtiesRepository.save(speciality);
SpecialtyProfile defaultProfile = new SpecialtyProfile();
defaultProfile.setSpeciality(speciality);
defaultProfile.setName("Без профиля");
specialtyProfileRepository.save(defaultProfile);
logger.info("Специальность успешно создана с ID: {}", speciality.getId());
@@ -159,4 +169,98 @@ public class SpecialityController {
logger.info("Специальность с ID - {} успешно удалена", id);
return ResponseEntity.ok(Map.of("message", "Специальность удалена"));
}
@GetMapping("/{specialtyId}/profiles")
public ResponseEntity<?> getProfiles(@PathVariable Long specialtyId) {
if (!specialtiesRepository.existsById(specialtyId)) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(specialtyProfileRepository.findBySpecialityIdOrderByNameAsc(specialtyId).stream()
.map(this::toProfileDto)
.toList());
}
@PostMapping("/{specialtyId}/profiles")
public ResponseEntity<?> createProfile(@PathVariable Long specialtyId, @RequestBody SpecialtyProfileDto request) {
Speciality speciality = specialtiesRepository.findById(specialtyId).orElse(null);
if (speciality == null) {
return ResponseEntity.notFound().build();
}
String validationError = validateProfile(specialtyId, null, request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
SpecialtyProfile profile = new SpecialtyProfile();
profile.setSpeciality(speciality);
profile.setName(request.name().trim());
profile.setDescription(trimToNull(request.description()));
return ResponseEntity.ok(toProfileDto(specialtyProfileRepository.save(profile)));
}
@PutMapping("/{specialtyId}/profiles/{profileId}")
public ResponseEntity<?> updateProfile(@PathVariable Long specialtyId,
@PathVariable Long profileId,
@RequestBody SpecialtyProfileDto request) {
SpecialtyProfile profile = specialtyProfileRepository.findById(profileId).orElse(null);
if (profile == null || !profile.getSpeciality().getId().equals(specialtyId)) {
return ResponseEntity.notFound().build();
}
String validationError = validateProfile(specialtyId, profileId, request);
if (validationError != null) {
return ResponseEntity.badRequest().body(Map.of("message", validationError));
}
profile.setName(request.name().trim());
profile.setDescription(trimToNull(request.description()));
return ResponseEntity.ok(toProfileDto(specialtyProfileRepository.save(profile)));
}
@DeleteMapping("/{specialtyId}/profiles/{profileId}")
public ResponseEntity<?> deleteProfile(@PathVariable Long specialtyId, @PathVariable Long profileId) {
SpecialtyProfile profile = specialtyProfileRepository.findById(profileId).orElse(null);
if (profile == null || !profile.getSpeciality().getId().equals(specialtyId)) {
return ResponseEntity.notFound().build();
}
try {
specialtyProfileRepository.delete(profile);
return ResponseEntity.ok(Map.of("message", "Профиль обучения удалён"));
} catch (Exception e) {
logger.error("Ошибка при удалении профиля обучения с ID - {}: {}", profileId, e.getMessage(), e);
return ResponseEntity.badRequest()
.body(Map.of("message", "Нельзя удалить профиль, который используется группами или календарными графиками"));
}
}
private String validateProfile(Long specialtyId, Long profileId, SpecialtyProfileDto request) {
if (request == null) {
return "Передайте данные профиля";
}
if (request.name() == null || request.name().isBlank()) {
return "Название профиля обязательно";
}
return specialtyProfileRepository.findBySpecialityIdAndName(specialtyId, request.name().trim())
.filter(existing -> !existing.getId().equals(profileId))
.map(existing -> "Профиль с таким названием уже существует")
.orElse(null);
}
private SpecialtyProfileDto toProfileDto(SpecialtyProfile profile) {
Speciality speciality = profile.getSpeciality();
return new SpecialtyProfileDto(
profile.getId(),
speciality.getId(),
speciality.getSpecialityCode(),
speciality.getSpecialityName(),
profile.getName(),
profile.getDescription()
);
}
private String trimToNull(String value) {
if (value == null || value.isBlank()) {
return null;
}
return value.trim();
}
}

View File

@@ -0,0 +1,12 @@
package com.magistr.app.dto;
public record AcademicCalendarActivityTypeDto(
Long id,
String code,
String name,
Boolean allowSchedule,
String colorCode,
Integer displayOrder,
String description
) {
}

View File

@@ -0,0 +1,21 @@
package com.magistr.app.dto;
import java.time.LocalDate;
public record AcademicCalendarDto(
Long id,
String title,
Long academicYearId,
String academicYearTitle,
LocalDate academicYearStartDate,
LocalDate academicYearEndDate,
Long specialtyId,
String specialtyCode,
String specialtyName,
Long specialtyProfileId,
String specialtyProfileName,
Long studyFormId,
String studyFormName,
Integer courseCount
) {
}

View File

@@ -0,0 +1,17 @@
package com.magistr.app.dto;
import java.time.LocalDate;
public record AcademicCalendarGridDayDto(
Long id,
Long calendarId,
Integer courseNumber,
LocalDate date,
Integer weekNumber,
Integer dayOfWeek,
Long activityTypeId,
String activityCode,
String activityName,
Boolean allowSchedule
) {
}

View File

@@ -1,13 +0,0 @@
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,8 @@
package com.magistr.app.dto;
public record CalendarStudyFormDto(
Long id,
String name,
String description
) {
}

View File

@@ -7,6 +7,8 @@ public class CreateGroupRequest {
private Long educationFormId;
private Long departmentId;
private Integer yearStartStudy;
private Long specialtyId;
private Long specialtyProfileId;
private Long specialityCode;
public String getName() {
@@ -56,4 +58,24 @@ public class CreateGroupRequest {
public void setSpecialityCode(Long specialityCode) {
this.specialityCode = specialityCode;
}
public Long getSpecialtyId() {
return specialtyId;
}
public void setSpecialtyId(Long specialtyId) {
this.specialtyId = specialtyId;
}
public Long getSpecialtyProfileId() {
return specialtyProfileId;
}
public void setSpecialtyProfileId(Long specialtyProfileId) {
this.specialtyProfileId = specialtyProfileId;
}
public Long getEffectiveSpecialtyId() {
return specialtyId != null ? specialtyId : specialityCode;
}
}

View File

@@ -0,0 +1,12 @@
package com.magistr.app.dto;
public record GroupCalendarAssignmentDto(
Long id,
Long groupId,
String groupName,
Long academicYearId,
String academicYearTitle,
Long calendarId,
String calendarTitle
) {
}

View File

@@ -14,21 +14,26 @@ public class GroupResponse {
private Integer yearStartStudy;
private Integer course;
private Integer semester;
private Long specialityCode;
private Long specialtyId;
private String specialtyCode;
private String specialtyName;
private Long specialtyProfileId;
private String specialtyProfileName;
public GroupResponse(Long id, String name, Long groupSize, Long educationFormId, String educationFormName, Long departmentId, Integer course, Integer semester, Long specialityCode) {
this.id = id;
this.name = name;
this.groupSize = groupSize;
this.educationFormId = educationFormId;
this.educationFormName = educationFormName;
this.departmentId = departmentId;
this.course = course;
this.semester = semester;
this.specialityCode = specialityCode;
}
public GroupResponse(Long id, String name, Long groupSize, Long educationFormId, String educationFormName, Long departmentId, Integer yearStartStudy, Long specialityCode) {
public GroupResponse(Long id,
String name,
Long groupSize,
Long educationFormId,
String educationFormName,
Long departmentId,
Integer yearStartStudy,
Integer course,
Integer semester,
Long specialtyId,
String specialtyCode,
String specialtyName,
Long specialtyProfileId,
String specialtyProfileName) {
this.id = id;
this.name = name;
this.groupSize = groupSize;
@@ -36,7 +41,13 @@ public class GroupResponse {
this.educationFormName = educationFormName;
this.departmentId = departmentId;
this.yearStartStudy = yearStartStudy;
this.specialityCode = specialityCode;
this.course = course;
this.semester = semester;
this.specialtyId = specialtyId;
this.specialtyCode = specialtyCode;
this.specialtyName = specialtyName;
this.specialtyProfileId = specialtyProfileId;
this.specialtyProfileName = specialtyProfileName;
}
public Long getId() {
@@ -76,6 +87,26 @@ public class GroupResponse {
}
public Long getSpecialityCode() {
return specialityCode;
return specialtyId;
}
public Long getSpecialtyId() {
return specialtyId;
}
public String getSpecialtyCode() {
return specialtyCode;
}
public String getSpecialtyName() {
return specialtyName;
}
public Long getSpecialtyProfileId() {
return specialtyProfileId;
}
public String getSpecialtyProfileName() {
return specialtyProfileName;
}
}

View File

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

View File

@@ -1,6 +1,5 @@
package com.magistr.app.dto;
import com.magistr.app.model.AcademicActivityType;
import com.magistr.app.model.ScheduleParity;
import java.time.LocalDate;
@@ -31,7 +30,7 @@ public record RenderedLessonDto(
Long subgroupId,
List<Long> groupIds,
List<String> groupNames,
AcademicActivityType activityType,
String activityType,
Integer totalAcademicHours,
Integer consumedAcademicHoursBeforeLesson,
Integer remainingAcademicHoursAfterLesson

View File

@@ -0,0 +1,11 @@
package com.magistr.app.dto;
public record SpecialtyProfileDto(
Long id,
Long specialtyId,
String specialtyCode,
String specialtyName,
String name,
String description
) {
}

View File

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

View File

@@ -0,0 +1,90 @@
package com.magistr.app.model;
import jakarta.persistence.*;
@Entity
@Table(name = "academic_calendars")
public class AcademicCalendar {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@ManyToOne(optional = false)
@JoinColumn(name = "academic_year_id", nullable = false)
private AcademicYear academicYear;
@ManyToOne(optional = false)
@JoinColumn(name = "specialty_id", nullable = false)
private Speciality speciality;
@ManyToOne(optional = false)
@JoinColumn(name = "specialty_profile_id", nullable = false)
private SpecialtyProfile specialtyProfile;
@ManyToOne(optional = false)
@JoinColumn(name = "study_form_id", nullable = false)
private CalendarStudyForm studyForm;
@Column(name = "course_count", nullable = false)
private Integer courseCount;
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 AcademicYear getAcademicYear() {
return academicYear;
}
public void setAcademicYear(AcademicYear academicYear) {
this.academicYear = academicYear;
}
public Speciality getSpeciality() {
return speciality;
}
public void setSpeciality(Speciality speciality) {
this.speciality = speciality;
}
public SpecialtyProfile getSpecialtyProfile() {
return specialtyProfile;
}
public void setSpecialtyProfile(SpecialtyProfile specialtyProfile) {
this.specialtyProfile = specialtyProfile;
}
public CalendarStudyForm getStudyForm() {
return studyForm;
}
public void setStudyForm(CalendarStudyForm studyForm) {
this.studyForm = studyForm;
}
public Integer getCourseCount() {
return courseCount;
}
public void setCourseCount(Integer courseCount) {
this.courseCount = courseCount;
}
}

View File

@@ -0,0 +1,86 @@
package com.magistr.app.model;
import jakarta.persistence.*;
@Entity
@Table(name = "academic_calendar_activity_types")
public class AcademicCalendarActivityType {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 10)
private String code;
@Column(nullable = false)
private String name;
@Column(name = "allow_schedule", nullable = false)
private Boolean allowSchedule;
@Column(name = "color_code", nullable = false, length = 7)
private String colorCode;
@Column(name = "display_order", nullable = false)
private Integer displayOrder;
@Column(columnDefinition = "TEXT")
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Boolean getAllowSchedule() {
return allowSchedule;
}
public void setAllowSchedule(Boolean allowSchedule) {
this.allowSchedule = allowSchedule;
}
public String getColorCode() {
return colorCode;
}
public void setColorCode(String colorCode) {
this.colorCode = colorCode;
}
public Integer getDisplayOrder() {
return displayOrder;
}
public void setDisplayOrder(Integer displayOrder) {
this.displayOrder = displayOrder;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@@ -0,0 +1,90 @@
package com.magistr.app.model;
import jakarta.persistence.*;
import java.time.LocalDate;
@Entity
@Table(name = "academic_calendar_days")
public class AcademicCalendarDay {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "calendar_id", nullable = false)
private AcademicCalendar academicCalendar;
@Column(name = "course_number", nullable = false)
private Integer courseNumber;
@Column(nullable = false)
private LocalDate date;
@Column(name = "week_number", nullable = false)
private Integer weekNumber;
@Column(name = "day_of_week", nullable = false)
private Integer dayOfWeek;
@ManyToOne(optional = false)
@JoinColumn(name = "activity_type_id", nullable = false)
private AcademicCalendarActivityType activityType;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public AcademicCalendar getAcademicCalendar() {
return academicCalendar;
}
public void setAcademicCalendar(AcademicCalendar academicCalendar) {
this.academicCalendar = academicCalendar;
}
public Integer getCourseNumber() {
return courseNumber;
}
public void setCourseNumber(Integer courseNumber) {
this.courseNumber = courseNumber;
}
public LocalDate getDate() {
return date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public Integer getWeekNumber() {
return weekNumber;
}
public void setWeekNumber(Integer weekNumber) {
this.weekNumber = weekNumber;
}
public Integer getDayOfWeek() {
return dayOfWeek;
}
public void setDayOfWeek(Integer dayOfWeek) {
this.dayOfWeek = dayOfWeek;
}
public AcademicCalendarActivityType getActivityType() {
return activityType;
}
public void setActivityType(AcademicCalendarActivityType activityType) {
this.activityType = activityType;
}
}

View File

@@ -1,77 +0,0 @@
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,42 @@
package com.magistr.app.model;
import jakarta.persistence.*;
@Entity
@Table(name = "calendar_study_forms")
public class CalendarStudyForm {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true, length = 100)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@@ -1,56 +0,0 @@
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,54 @@
package com.magistr.app.model;
import jakarta.persistence.*;
@Entity
@Table(name = "specialty_profiles")
public class SpecialtyProfile {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "specialty_id", nullable = false)
private Speciality speciality;
@Column(nullable = false, length = 500)
private String name;
@Column(columnDefinition = "TEXT")
private String description;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public Speciality getSpeciality() {
return speciality;
}
public void setSpeciality(Speciality speciality) {
this.speciality = speciality;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@@ -10,7 +10,7 @@ public class StudentGroup {
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false, length = 100)
@Column(nullable = false, length = 100)
private String name;
@Column(name = "group_size", nullable = false)
@@ -23,8 +23,13 @@ public class StudentGroup {
@Column(name = "department_id", nullable = false)
private Long departmentId;
@Column(name="specialty_code", nullable = false)
private Long specialityCode;
@ManyToOne(optional = false)
@JoinColumn(name = "specialty_id", nullable = false)
private Speciality speciality;
@ManyToOne(optional = false)
@JoinColumn(name = "specialty_profile_id", nullable = false)
private SpecialtyProfile specialtyProfile;
@Column(name="year_start_study", nullable = false)
private Integer yearStartStudy;
@@ -72,12 +77,20 @@ public class StudentGroup {
this.departmentId = departmentId;
}
public Long getSpecialityCode() {
return specialityCode;
public Speciality getSpeciality() {
return speciality;
}
public void setSpecialityCode(Long specialityCode) {
this.specialityCode = specialityCode;
public void setSpeciality(Speciality speciality) {
this.speciality = speciality;
}
public SpecialtyProfile getSpecialtyProfile() {
return specialtyProfile;
}
public void setSpecialtyProfile(SpecialtyProfile specialtyProfile) {
this.specialtyProfile = specialtyProfile;
}
public Integer getYearStartStudy() {

View File

@@ -0,0 +1,56 @@
package com.magistr.app.model;
import jakarta.persistence.*;
@Entity
@Table(name = "student_group_calendar_assignments")
public class StudentGroupCalendarAssignment {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(optional = false)
@JoinColumn(name = "group_id", nullable = false)
private StudentGroup studentGroup;
@ManyToOne(optional = false)
@JoinColumn(name = "academic_year_id", nullable = false)
private AcademicYear academicYear;
@ManyToOne(optional = false)
@JoinColumn(name = "calendar_id", nullable = false)
private AcademicCalendar academicCalendar;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public StudentGroup getStudentGroup() {
return studentGroup;
}
public void setStudentGroup(StudentGroup studentGroup) {
this.studentGroup = studentGroup;
}
public AcademicYear getAcademicYear() {
return academicYear;
}
public void setAcademicYear(AcademicYear academicYear) {
this.academicYear = academicYear;
}
public AcademicCalendar getAcademicCalendar() {
return academicCalendar;
}
public void setAcademicCalendar(AcademicCalendar academicCalendar) {
this.academicCalendar = academicCalendar;
}
}

View File

@@ -0,0 +1,14 @@
package com.magistr.app.repository;
import com.magistr.app.model.AcademicCalendarActivityType;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface AcademicCalendarActivityTypeRepository extends JpaRepository<AcademicCalendarActivityType, Long> {
List<AcademicCalendarActivityType> findAllByOrderByDisplayOrderAscCodeAsc();
Optional<AcademicCalendarActivityType> findByCode(String code);
}

View File

@@ -0,0 +1,43 @@
package com.magistr.app.repository;
import com.magistr.app.model.AcademicCalendarDay;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
public interface AcademicCalendarDayRepository extends JpaRepository<AcademicCalendarDay, Long> {
@Query("""
select day
from AcademicCalendarDay day
join fetch day.activityType
where day.academicCalendar.id = :calendarId
order by day.courseNumber asc, day.date asc
""")
List<AcademicCalendarDay> findByCalendarIdWithActivity(@Param("calendarId") Long calendarId);
@Query("""
select day
from AcademicCalendarDay day
join fetch day.activityType
where day.academicCalendar.id = :calendarId
and day.courseNumber = :courseNumber
and day.date = :date
""")
Optional<AcademicCalendarDay> findByCalendarIdAndCourseNumberAndDate(
@Param("calendarId") Long calendarId,
@Param("courseNumber") Integer courseNumber,
@Param("date") LocalDate date
);
boolean existsByActivityTypeId(Long activityTypeId);
@Modifying
@Query("delete from AcademicCalendarDay day where day.academicCalendar.id = :calendarId")
void deleteByCalendarId(@Param("calendarId") Long calendarId);
}

View File

@@ -1,23 +0,0 @@
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,41 @@
package com.magistr.app.repository;
import com.magistr.app.model.AcademicCalendar;
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;
import java.util.Optional;
public interface AcademicCalendarRepository extends JpaRepository<AcademicCalendar, Long> {
@Query("""
select calendar
from AcademicCalendar calendar
join fetch calendar.academicYear
join fetch calendar.speciality
join fetch calendar.specialtyProfile
join fetch calendar.studyForm
where (:academicYearId is null or calendar.academicYear.id = :academicYearId)
and (:specialtyId is null or calendar.speciality.id = :specialtyId)
and (:profileId is null or calendar.specialtyProfile.id = :profileId)
order by calendar.academicYear.startDate desc, calendar.title asc
""")
List<AcademicCalendar> findAllWithDetails(
@Param("academicYearId") Long academicYearId,
@Param("specialtyId") Long specialtyId,
@Param("profileId") Long profileId
);
@Query("""
select calendar
from AcademicCalendar calendar
join fetch calendar.academicYear
join fetch calendar.speciality
join fetch calendar.specialtyProfile
join fetch calendar.studyForm
where calendar.id = :id
""")
Optional<AcademicCalendar> findByIdWithDetails(@Param("id") Long id);
}

View File

@@ -0,0 +1,11 @@
package com.magistr.app.repository;
import com.magistr.app.model.CalendarStudyForm;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface CalendarStudyFormRepository extends JpaRepository<CalendarStudyForm, Long> {
List<CalendarStudyForm> findAllByOrderByNameAsc();
}

View File

@@ -4,12 +4,9 @@ import com.magistr.app.model.StudentGroup;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface GroupRepository extends JpaRepository<StudentGroup, Long> {
Optional<StudentGroup> findByName(String name);
List<StudentGroup> findByEducationFormId(Long educationFormId);
List<StudentGroup> findByDepartmentId(Long departmentId);

View File

@@ -1,16 +0,0 @@
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,14 @@
package com.magistr.app.repository;
import com.magistr.app.model.SpecialtyProfile;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
public interface SpecialtyProfileRepository extends JpaRepository<SpecialtyProfile, Long> {
List<SpecialtyProfile> findBySpecialityIdOrderByNameAsc(Long specialityId);
Optional<SpecialtyProfile> findBySpecialityIdAndName(Long specialityId, String name);
}

View File

@@ -0,0 +1,54 @@
package com.magistr.app.repository;
import com.magistr.app.model.StudentGroupCalendarAssignment;
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;
import java.util.Optional;
public interface StudentGroupCalendarAssignmentRepository extends JpaRepository<StudentGroupCalendarAssignment, Long> {
@Query("""
select assignment
from StudentGroupCalendarAssignment assignment
join fetch assignment.studentGroup
join fetch assignment.academicYear
join fetch assignment.academicCalendar calendar
join fetch calendar.speciality
join fetch calendar.specialtyProfile
join fetch calendar.studyForm
where assignment.studentGroup.id = :groupId
order by assignment.academicYear.startDate desc
""")
List<StudentGroupCalendarAssignment> findByGroupIdWithDetails(@Param("groupId") Long groupId);
@Query("""
select assignment
from StudentGroupCalendarAssignment assignment
join fetch assignment.academicYear
join fetch assignment.academicCalendar calendar
join fetch calendar.speciality
join fetch calendar.specialtyProfile
join fetch calendar.studyForm
where assignment.studentGroup.id = :groupId
and assignment.academicYear.id = :academicYearId
""")
Optional<StudentGroupCalendarAssignment> findByGroupIdAndAcademicYearIdWithDetails(
@Param("groupId") Long groupId,
@Param("academicYearId") Long academicYearId
);
@Query("""
select assignment
from StudentGroupCalendarAssignment assignment
join fetch assignment.academicCalendar
where assignment.studentGroup.id = :groupId
and assignment.academicYear.id = :academicYearId
""")
Optional<StudentGroupCalendarAssignment> findForSchedule(
@Param("groupId") Long groupId,
@Param("academicYearId") Long academicYearId
);
}

View File

@@ -1,9 +1,9 @@
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.AcademicCalendarDayRepository;
import com.magistr.app.repository.SemesterRepository;
import com.magistr.app.repository.StudentGroupCalendarAssignmentRepository;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
@@ -15,17 +15,17 @@ import java.util.Optional;
public class AcademicDateService {
private final SemesterRepository semesterRepository;
private final HolidayRepository holidayRepository;
private final AcademicCalendarMatrixRepository calendarMatrixRepository;
private final AcademicCalendarDayRepository calendarDayRepository;
private final StudentGroupCalendarAssignmentRepository assignmentRepository;
private final boolean evenWeekIsUpper;
public AcademicDateService(SemesterRepository semesterRepository,
HolidayRepository holidayRepository,
AcademicCalendarMatrixRepository calendarMatrixRepository,
AcademicCalendarDayRepository calendarDayRepository,
StudentGroupCalendarAssignmentRepository assignmentRepository,
@Value("${app.schedule.even-week-is-upper:false}") boolean evenWeekIsUpper) {
this.semesterRepository = semesterRepository;
this.holidayRepository = holidayRepository;
this.calendarMatrixRepository = calendarMatrixRepository;
this.calendarDayRepository = calendarDayRepository;
this.assignmentRepository = assignmentRepository;
this.evenWeekIsUpper = evenWeekIsUpper;
}
@@ -50,38 +50,40 @@ public class AcademicDateService {
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;
public Optional<AcademicCalendarActivityType> getActivityType(StudentGroup group, Semester semester, LocalDate date) {
if (group == null || semester == null || semester.getAcademicYear() == null || date == null) {
return Optional.empty();
}
return calendarMatrixRepository
.findBySemesterIdAndCourseNumberAndSpecialtyIdAndWeekNumber(
semester.getId(),
int courseNumber = getCourseNumber(group, date);
if (courseNumber < 1) {
return Optional.empty();
}
return assignmentRepository.findForSchedule(group.getId(), semester.getAcademicYear().getId())
.flatMap(assignment -> calendarDayRepository.findByCalendarIdAndCourseNumberAndDate(
assignment.getAcademicCalendar().getId(),
courseNumber,
group.getSpecialityCode(),
weekNumber
)
.map(AcademicCalendarMatrix::getActivityType)
.orElse(AcademicActivityType.THEORY);
date
))
.map(AcademicCalendarDay::getActivityType);
}
public boolean isTheoryDay(StudentGroup group, Semester semester, LocalDate date) {
return getActivityType(group, semester, date) == AcademicActivityType.THEORY
&& !isHoliday(semester, date);
return getActivityType(group, semester, date)
.map(AcademicCalendarActivityType::getAllowSchedule)
.orElse(false);
}
public boolean hasCalendarAssignment(StudentGroup group, Semester semester) {
if (group == null || semester == null || semester.getAcademicYear() == null) {
return false;
}
return assignmentRepository.findForSchedule(group.getId(), semester.getAcademicYear().getId()).isPresent();
}
}

View File

@@ -5,6 +5,8 @@ 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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import java.time.DayOfWeek;
@@ -17,6 +19,7 @@ import java.util.stream.Collectors;
@Service
public class ScheduleGeneratorService {
private static final Logger logger = LoggerFactory.getLogger(ScheduleGeneratorService.class);
private static final int ACADEMIC_HOURS_PER_SLOT = 2;
private static final long MAX_RANGE_DAYS = 120;
@@ -43,6 +46,7 @@ public class ScheduleGeneratorService {
Map<Long, List<ScheduleRule>> rulesBySemester = new HashMap<>();
List<RenderedLessonDto> result = new ArrayList<>();
Set<Long> warnedAcademicYears = new HashSet<>();
for (LocalDate date = startDate; !date.isAfter(endDate); date = date.plusDays(1)) {
Optional<Semester> semesterOpt = academicDateService.findSemester(date);
@@ -51,6 +55,11 @@ public class ScheduleGeneratorService {
}
Semester semester = semesterOpt.get();
if (!academicDateService.hasCalendarAssignment(group, semester)
&& warnedAcademicYears.add(semester.getAcademicYear().getId())) {
logger.info("Для группы '{}' не назначен календарный учебный график на учебный год {}",
group.getName(), semester.getAcademicYear().getTitle());
}
if (!academicDateService.isTheoryDay(group, semester, date)) {
continue;
}
@@ -148,10 +157,6 @@ public class ScheduleGeneratorService {
}
Semester semester = semesterOpt.get();
if (academicDateService.isHoliday(semester, date)) {
return;
}
List<StudentGroup> eligibleGroups = eligibleGroups(rule, targetGroup, semester, date);
if (eligibleGroups.isEmpty()) {
return;
@@ -216,9 +221,6 @@ public class ScheduleGeneratorService {
if (date.isBefore(semester.getStartDate()) || date.isAfter(semester.getEndDate())) {
continue;
}
if (academicDateService.isHoliday(semester, date)) {
continue;
}
if (eligibleGroups(rule, targetGroup, semester, date).isEmpty()) {
continue;
}
@@ -274,7 +276,7 @@ public class ScheduleGeneratorService {
slot.getSubgroupId(),
sortedGroups.stream().map(StudentGroup::getId).toList(),
sortedGroups.stream().map(StudentGroup::getName).collect(Collectors.toList()),
AcademicActivityType.THEORY,
"Т",
rule.getTotalAcademicHours(),
consumedBeforeLesson,
remainingAfterLesson

View File

@@ -20,13 +20,28 @@ INSERT INTO departments (name, code) VALUES
CREATE TABLE IF NOT EXISTS specialties (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
specialty_code VARCHAR(255) NOT NULL
specialty_code VARCHAR(255) UNIQUE NOT NULL
);
INSERT INTO specialties (name, specialty_code) VALUES
('Информационная безопасность', '10.03.01'),
('Информатика и вычислительная техника', '09.03.01'),
('Программная инженерия', '09.03.04');
('Программная инженерия', '09.03.04')
ON CONFLICT (specialty_code) DO NOTHING;
CREATE TABLE IF NOT EXISTS specialty_profiles (
id BIGSERIAL PRIMARY KEY,
specialty_id BIGINT NOT NULL REFERENCES specialties(id) ON DELETE CASCADE,
name VARCHAR(500) NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_specialty_profiles_name UNIQUE (specialty_id, name)
);
INSERT INTO specialty_profiles (specialty_id, name)
SELECT id, 'Без профиля'
FROM specialties
ON CONFLICT (specialty_id, name) DO NOTHING;
-- ==========================================
-- Пользователи и роли
@@ -70,20 +85,28 @@ ON CONFLICT (name) DO NOTHING;
-- ==========================================
CREATE TABLE IF NOT EXISTS student_groups (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
group_size BIGINT NOT NULL,
education_form_id BIGINT NOT NULL REFERENCES education_forms(id),
department_id BIGINT NOT NULL REFERENCES departments(id),
specialty_code INT NOT NULL REFERENCES specialties(id),
specialty_id BIGINT NOT NULL REFERENCES specialties(id),
specialty_profile_id BIGINT NOT NULL REFERENCES specialty_profiles(id),
year_start_study BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Тестовая базовая группа для работы
INSERT INTO student_groups (name, group_size, education_form_id, department_id, specialty_code, year_start_study)
VALUES ('ИВТ-21-1', 25, 1, 1, 2, 2025),
('ИБ-41м', 15, 2, 1, 1, 2024)
ON CONFLICT (name) DO NOTHING;
INSERT INTO student_groups (name, group_size, education_form_id, department_id, specialty_id, specialty_profile_id, year_start_study)
SELECT data.name, data.group_size, ef.id, dept.id, specialty.id, profile.id, data.year_start_study
FROM (VALUES
('ИВТ-21-1', 25::BIGINT, 'Бакалавриат', 'Кафедра ИБ', '09.03.01', 2025::BIGINT),
('ИБ-41м', 15::BIGINT, 'Магистратура', 'Кафедра ИБ', '10.03.01', 2024::BIGINT)
) AS data(name, group_size, education_form_name, department_name, specialty_code, year_start_study)
JOIN education_forms ef ON ef.name = data.education_form_name
JOIN departments dept ON dept.name = data.department_name
JOIN specialties specialty ON specialty.specialty_code = data.specialty_code
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
WHERE NOT EXISTS (SELECT 1 FROM student_groups existing WHERE existing.name = data.name);
-- ==========================================
-- Подгруппы (например: "ИВТ-21-1 Подгруппа 1")
@@ -272,45 +295,131 @@ 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 (
CREATE TABLE IF NOT EXISTS calendar_study_forms (
id BIGSERIAL PRIMARY KEY,
date DATE NOT NULL,
name VARCHAR(100) UNIQUE NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO calendar_study_forms (name, description) VALUES
('очная', 'Очная форма обучения')
ON CONFLICT (name) DO NOTHING;
CREATE TABLE IF NOT EXISTS academic_calendar_activity_types (
id BIGSERIAL PRIMARY KEY,
code VARCHAR(10) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
allow_schedule BOOLEAN NOT NULL DEFAULT FALSE,
color_code VARCHAR(7) NOT NULL DEFAULT '#64748b',
display_order INT NOT NULL DEFAULT 100,
description TEXT
);
INSERT INTO academic_calendar_activity_types (code, name, allow_schedule, color_code, display_order, description) VALUES
('Т', 'Теоретическое обучение', TRUE, '#10b981', 10, 'Обычные занятия динамического расписания разрешены'),
('Э', 'Экзаменационная сессия', FALSE, '#f59e0b', 20, 'Обычные занятия не генерируются'),
('К', 'Каникулы', FALSE, '#38bdf8', 30, 'Обычные занятия не генерируются'),
('У', 'Учебная практика', FALSE, '#a78bfa', 40, 'Обычные занятия не генерируются'),
('П', 'Производственная практика', FALSE, '#fb7185', 50, 'Обычные занятия не генерируются'),
('Пд', 'Преддипломная практика', FALSE, '#f97316', 60, 'Обычные занятия не генерируются'),
('Н', 'Научно-исследовательская работа', FALSE, '#22c55e', 70, 'Обычные занятия не генерируются'),
('Г', 'Подготовка к сдаче и сдача государственного экзамена', FALSE, '#ef4444', 80, 'Обычные занятия не генерируются'),
('Д', 'Подготовка к защите и защита выпускной квалификационной работы', FALSE, '#dc2626', 90, 'Обычные занятия не генерируются'),
('ПА', 'Повторная промежуточная аттестация', FALSE, '#eab308', 100, 'Обычные занятия не генерируются'),
('С', 'Симуляционный курс', FALSE, '#06b6d4', 110, 'Обычные занятия не генерируются'),
('*', 'Нерабочий праздничный день', FALSE, '#64748b', 120, 'День исключается из генерации расписания'),
('=', 'День вне учебного года', FALSE, '#475569', 130, 'День исключается из генерации расписания')
ON CONFLICT (code) DO NOTHING;
CREATE TABLE IF NOT EXISTS academic_calendars (
id BIGSERIAL PRIMARY KEY,
title VARCHAR(255) 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)
specialty_profile_id BIGINT NOT NULL REFERENCES specialty_profiles(id),
study_form_id BIGINT NOT NULL REFERENCES calendar_study_forms(id),
course_count INT NOT NULL DEFAULT 4,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT chk_academic_calendars_course_count CHECK (course_count > 0),
CONSTRAINT uq_academic_calendar_title UNIQUE (academic_year_id, specialty_profile_id, study_form_id, title)
);
CREATE INDEX IF NOT EXISTS idx_calendar_lookup
ON academic_calendar_matrix(semester_id, course_number, specialty_id, week_number);
CREATE INDEX IF NOT EXISTS idx_academic_calendars_context
ON academic_calendars(academic_year_id, specialty_id, specialty_profile_id, study_form_id);
INSERT INTO academic_calendar_matrix (semester_id, course_number, specialty_id, week_number, activity_type)
INSERT INTO academic_calendars (title, academic_year_id, specialty_id, specialty_profile_id, study_form_id, course_count)
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;
specialty.specialty_code || ' ' || profile.name || ' ' || study_form.name || ' ' || ay.title,
ay.id,
specialty.id,
profile.id,
study_form.id,
4
FROM academic_years ay
CROSS JOIN calendar_study_forms study_form
JOIN specialties specialty ON specialty.specialty_code IN ('09.03.01', '10.03.01')
JOIN specialty_profiles profile ON profile.specialty_id = specialty.id AND profile.name = 'Без профиля'
WHERE ay.title = '2025-2026'
ON CONFLICT (academic_year_id, specialty_profile_id, study_form_id, title) DO NOTHING;
CREATE TABLE IF NOT EXISTS academic_calendar_days (
id BIGSERIAL PRIMARY KEY,
calendar_id BIGINT NOT NULL REFERENCES academic_calendars(id) ON DELETE CASCADE,
course_number INT NOT NULL,
date DATE NOT NULL,
week_number INT NOT NULL,
day_of_week INT NOT NULL,
activity_type_id BIGINT NOT NULL REFERENCES academic_calendar_activity_types(id),
CONSTRAINT chk_calendar_days_course_positive CHECK (course_number > 0),
CONSTRAINT chk_calendar_days_week_positive CHECK (week_number > 0),
CONSTRAINT chk_calendar_days_day CHECK (day_of_week BETWEEN 1 AND 7),
CONSTRAINT uq_calendar_days UNIQUE (calendar_id, course_number, date)
);
CREATE INDEX IF NOT EXISTS idx_calendar_days_lookup
ON academic_calendar_days(calendar_id, course_number, date);
INSERT INTO academic_calendar_days (calendar_id, course_number, date, week_number, day_of_week, activity_type_id)
SELECT
calendar.id,
course.course_number,
days.date::DATE,
((days.date::DATE - ay.start_date) / 7) + 1,
EXTRACT(ISODOW FROM days.date::DATE)::INT,
activity.id
FROM academic_calendars calendar
JOIN academic_years ay ON ay.id = calendar.academic_year_id
JOIN academic_calendar_activity_types activity ON activity.code = 'Т'
CROSS JOIN GENERATE_SERIES(1, calendar.course_count) AS course(course_number)
CROSS JOIN GENERATE_SERIES(ay.start_date, ay.end_date, INTERVAL '1 day') AS days(date)
ON CONFLICT (calendar_id, course_number, date) DO NOTHING;
CREATE TABLE IF NOT EXISTS student_group_calendar_assignments (
id BIGSERIAL PRIMARY KEY,
group_id BIGINT NOT NULL REFERENCES student_groups(id) ON DELETE CASCADE,
academic_year_id BIGINT NOT NULL REFERENCES academic_years(id) ON DELETE CASCADE,
calendar_id BIGINT NOT NULL REFERENCES academic_calendars(id) ON DELETE CASCADE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT uq_group_calendar_year UNIQUE (group_id, academic_year_id)
);
CREATE INDEX IF NOT EXISTS idx_group_calendar_assignments_lookup
ON student_group_calendar_assignments(group_id, academic_year_id);
INSERT INTO student_group_calendar_assignments (group_id, academic_year_id, calendar_id)
SELECT
sg.id,
ay.id,
calendar.id
FROM student_groups sg
JOIN academic_years ay ON ay.title = '2025-2026'
JOIN academic_calendars calendar
ON calendar.academic_year_id = ay.id
AND calendar.specialty_profile_id = sg.specialty_profile_id
WHERE sg.name IN ('ИВТ-21-1', 'ИБ-41м')
ON CONFLICT (group_id, academic_year_id) DO NOTHING;
CREATE TABLE IF NOT EXISTS schedule_rules (
id BIGSERIAL PRIMARY KEY,
@@ -445,17 +554,26 @@ CREATE TRIGGER update_users_updated_at
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
CREATE TRIGGER update_academic_calendars_updated_at
BEFORE UPDATE ON academic_calendars
FOR EACH ROW
EXECUTE FUNCTION update_updated_at_column();
-- ==========================================
-- Комментарии к таблицам и полям (для документации)
-- ==========================================
COMMENT ON TABLE users IS 'Пользователи системы (студенты, преподаватели, администраторы)';
COMMENT ON TABLE departments IS 'Кафедры';
COMMENT ON TABLE specialties IS 'Специальности';
COMMENT ON TABLE specialty_profiles 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 calendar_study_forms IS 'Формы обучения календарного учебного графика';
COMMENT ON TABLE academic_calendar_activity_types IS 'Коды активностей календарного учебного графика';
COMMENT ON TABLE academic_calendars IS 'Календарные учебные графики по профилю, форме обучения и учебному году';
COMMENT ON TABLE academic_calendar_days IS 'Дневные ячейки календарного учебного графика';
COMMENT ON TABLE student_group_calendar_assignments IS 'Назначения календарных графиков учебным группам по учебному году';
COMMENT ON TABLE schedule_rules IS 'Правила динамической генерации расписания';
COMMENT ON TABLE schedule_rule_groups IS 'Привязка правил расписания к учебным группам';
COMMENT ON TABLE schedule_rule_slots IS 'Слоты проведения занятий внутри правила расписания';
@@ -489,7 +607,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.specialty_id IS 'ID специальности';
COMMENT ON COLUMN student_groups.specialty_profile_id IS 'ID профиля обучения';
COMMENT ON COLUMN student_groups.year_start_study IS 'Год начала обучения';
COMMENT ON COLUMN student_groups.created_at IS 'Дата и время создания';
@@ -526,7 +645,7 @@ COMMENT ON COLUMN classrooms.created_at IS 'Дата и время создан
COMMENT ON COLUMN classroom_equipments.classroom_id IS 'ID аудитории';
COMMENT ON COLUMN classroom_equipments.equipment_id IS 'ID оборудования';
COMMENT ON COLUMN classroom_equipments.quantity IS 'Дата и время создания'; -- Так было в V2
COMMENT ON COLUMN classroom_equipments.quantity IS 'Количество единиц оборудования';
COMMENT ON COLUMN classroom_equipments.notes IS 'Примечания к записи';
COMMENT ON COLUMN teacher_subjects.user_id IS 'ID преподавателя';
@@ -542,6 +661,37 @@ COMMENT ON COLUMN specialties.id IS 'ID специальности';
COMMENT ON COLUMN specialties.name IS 'Название специальности';
COMMENT ON COLUMN specialties.specialty_code IS 'Код специальности';
COMMENT ON COLUMN specialty_profiles.id IS 'ID профиля обучения';
COMMENT ON COLUMN specialty_profiles.specialty_id IS 'ID специальности';
COMMENT ON COLUMN specialty_profiles.name IS 'Название профиля обучения';
COMMENT ON COLUMN specialty_profiles.description IS 'Описание профиля обучения';
COMMENT ON COLUMN calendar_study_forms.id IS 'ID формы обучения графика';
COMMENT ON COLUMN calendar_study_forms.name IS 'Название формы обучения графика';
COMMENT ON COLUMN calendar_study_forms.description IS 'Описание формы обучения графика';
COMMENT ON COLUMN academic_calendar_activity_types.code IS 'Код активности из календарного графика';
COMMENT ON COLUMN academic_calendar_activity_types.name IS 'Название активности';
COMMENT ON COLUMN academic_calendar_activity_types.allow_schedule IS 'Разрешает ли код генерацию обычного расписания';
COMMENT ON COLUMN academic_calendars.title IS 'Название календарного учебного графика';
COMMENT ON COLUMN academic_calendars.academic_year_id IS 'ID учебного года';
COMMENT ON COLUMN academic_calendars.specialty_id IS 'ID специальности';
COMMENT ON COLUMN academic_calendars.specialty_profile_id IS 'ID профиля обучения';
COMMENT ON COLUMN academic_calendars.study_form_id IS 'ID формы обучения графика';
COMMENT ON COLUMN academic_calendars.course_count IS 'Количество курсов в графике';
COMMENT ON COLUMN academic_calendar_days.calendar_id IS 'ID календарного учебного графика';
COMMENT ON COLUMN academic_calendar_days.course_number IS 'Номер курса';
COMMENT ON COLUMN academic_calendar_days.date IS 'Дата';
COMMENT ON COLUMN academic_calendar_days.week_number IS 'Номер недели в учебном году';
COMMENT ON COLUMN academic_calendar_days.day_of_week IS 'День недели ISO';
COMMENT ON COLUMN academic_calendar_days.activity_type_id IS 'ID кода активности';
COMMENT ON COLUMN student_group_calendar_assignments.group_id IS 'ID учебной группы';
COMMENT ON COLUMN student_group_calendar_assignments.academic_year_id IS 'ID учебного года';
COMMENT ON COLUMN student_group_calendar_assignments.calendar_id IS 'ID назначенного календарного графика';
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 типа занятия';