баг-фикс завершён

This commit is contained in:
Zuev
2026-07-19 20:16:12 +03:00
parent bc0e1ab1b4
commit ee876f1acd
43 changed files with 1228 additions and 258 deletions

View File

@@ -38,6 +38,10 @@ final class DatabaseConstraintViolationMapper {
badRequest("chk_calendar_days_week_positive", "Номер недели должен быть положительным"),
badRequest("chk_calendar_days_day", "День недели должен быть от 1 до 7"),
badRequest("chk_academic_calendar_subjects_semester_positive", "Номер семестра должен быть положительным"),
badRequest("chk_student_groups_group_size_positive", "Численность группы должна быть больше нуля"),
badRequest("chk_student_groups_year_start_positive", "Год начала обучения должен быть больше нуля"),
badRequest("chk_subgroups_student_capacity_positive", "Численность подгруппы должна быть больше нуля"),
badRequest("chk_student_group_subgroup_capacity", "Сумма численностей активных подгрупп не может превышать численность группы"),
badRequest("chk_schedule_rules_type_hours_non_negative", "Количество академических часов не может быть отрицательным"),
badRequest("chk_schedule_rules_has_type_hours", "В правиле должен быть хотя бы один тип занятия с часами"),
badRequest("chk_schedule_rules_start_weeks_positive", "Неделя начала занятия должна быть положительной"),

View File

@@ -6,7 +6,6 @@ import com.magistr.app.dto.CreateGroupRequest;
import com.magistr.app.dto.GroupCalendarAssignmentDto;
import com.magistr.app.dto.GroupResponse;
import com.magistr.app.model.AcademicCalendarSubject;
import com.magistr.app.model.EducationForm;
import com.magistr.app.model.Role;
import com.magistr.app.model.Speciality;
import com.magistr.app.model.SpecialtyProfile;
@@ -31,7 +30,6 @@ import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
@@ -43,9 +41,6 @@ public class GroupController {
private static final Logger logger = LoggerFactory.getLogger(GroupController.class);
private final GroupRepository groupRepository;
private final EducationFormRepository educationFormRepository;
private final SpecialtiesRepository specialtiesRepository;
private final SpecialtyProfileRepository specialtyProfileRepository;
private final AcademicCalendarSubjectRepository calendarSubjectRepository;
private final StudentGroupCalendarAssignmentRepository assignmentRepository;
private final ScheduleGeneratorService scheduleGeneratorService;
@@ -54,25 +49,18 @@ public class GroupController {
private final BusinessTimeService businessTime;
public GroupController(GroupRepository groupRepository,
EducationFormRepository educationFormRepository,
SpecialtiesRepository specialtiesRepository,
SpecialtyProfileRepository specialtyProfileRepository,
AcademicCalendarSubjectRepository calendarSubjectRepository,
StudentGroupCalendarAssignmentRepository assignmentRepository,
ScheduleGeneratorService scheduleGeneratorService,
StudentGroupLifecycleService groupLifecycleService,
AcademicStructureService academicStructureService) {
this(groupRepository, educationFormRepository, specialtiesRepository,
specialtyProfileRepository, calendarSubjectRepository, assignmentRepository,
this(groupRepository, calendarSubjectRepository, assignmentRepository,
scheduleGeneratorService, groupLifecycleService, academicStructureService,
BusinessTimeService.systemDefault());
}
@Autowired
public GroupController(GroupRepository groupRepository,
EducationFormRepository educationFormRepository,
SpecialtiesRepository specialtiesRepository,
SpecialtyProfileRepository specialtyProfileRepository,
AcademicCalendarSubjectRepository calendarSubjectRepository,
StudentGroupCalendarAssignmentRepository assignmentRepository,
ScheduleGeneratorService scheduleGeneratorService,
@@ -80,9 +68,6 @@ public class GroupController {
AcademicStructureService academicStructureService,
BusinessTimeService businessTime) {
this.groupRepository = groupRepository;
this.educationFormRepository = educationFormRepository;
this.specialtiesRepository = specialtiesRepository;
this.specialtyProfileRepository = specialtyProfileRepository;
this.calendarSubjectRepository = calendarSubjectRepository;
this.assignmentRepository = assignmentRepository;
this.scheduleGeneratorService = scheduleGeneratorService;
@@ -144,46 +129,10 @@ public class GroupController {
@PostMapping
@RequireRoles({Role.ADMIN})
public ResponseEntity<?> createGroup(@RequestBody CreateGroupRequest request) {
logger.info("Получен запрос на создание новой группы: name = {}, groupSize = {}, educationFormId = {}, departmentId = {}, yearStartStudy = {}",
request.getName(), request.getGroupSize(), request.getEducationFormId(), request.getDepartmentId(), request.getYearStartStudy());
try {
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());
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 - {}", group.getId());
return ResponseEntity.ok(mapToResponse(group));
} catch (Exception e) {
logger.error("Ошибка при создании группы", e);
throw e;
}
logger.info("Получен запрос на создание новой группы");
StudentGroup group = academicStructureService.createGroup(request);
logger.info("Группа успешно создана с ID - {}", group.getId());
return ResponseEntity.ok(mapToResponse(group));
}
@PutMapping("/{id}")
@@ -222,36 +171,6 @@ public class GroupController {
return ResponseEntity.ok(mapToResponse(group));
}
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) {
LocalDate today = businessTime.today();
int course = CourseAndSemesterCalculator.getActualCourse(group.getYearStartStudy(), today);

View File

@@ -10,6 +10,7 @@ import com.magistr.app.repository.ScheduleRuleSlotRepository;
import com.magistr.app.repository.SubgroupRepository;
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;
@@ -53,8 +54,9 @@ public class SubgroupController {
@PostMapping("/api/groups/{groupId}/subgroups")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
@Transactional
public ResponseEntity<?> create(@PathVariable Long groupId, @RequestBody SubgroupDto request) {
StudentGroup group = groupRepository.findById(groupId).orElse(null);
StudentGroup group = groupRepository.findByIdForUpdate(groupId).orElse(null);
if (group == null) {
return ResponseEntity.notFound().build();
}
@@ -81,9 +83,14 @@ public class SubgroupController {
@PutMapping("/api/groups/{groupId}/subgroups/{id}")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
@Transactional
public ResponseEntity<?> update(@PathVariable Long groupId,
@PathVariable Long id,
@RequestBody SubgroupDto request) {
StudentGroup group = groupRepository.findByIdForUpdate(groupId).orElse(null);
if (group == null) {
return ResponseEntity.notFound().build();
}
Subgroup subgroup = subgroupRepository.findByIdAndStudentGroupId(id, groupId).orElse(null);
if (subgroup == null) {
return ResponseEntity.notFound().build();
@@ -98,7 +105,7 @@ public class SubgroupController {
if (duplicateName) {
return ResponseEntity.badRequest().body(Map.of("message", "Подгруппа с таким названием уже есть в группе"));
}
String capacityError = validateGroupCapacity(subgroup.getStudentGroup(), id, request.studentCapacity());
String capacityError = validateGroupCapacity(group, id, request.studentCapacity());
if (capacityError != null) {
return ResponseEntity.badRequest().body(Map.of("message", capacityError));
}
@@ -112,7 +119,11 @@ public class SubgroupController {
@DeleteMapping("/api/groups/{groupId}/subgroups/{id}")
@RequireRoles({Role.ADMIN, Role.EDUCATION_OFFICE})
@Transactional
public ResponseEntity<?> delete(@PathVariable Long groupId, @PathVariable Long id) {
if (groupRepository.findByIdForUpdate(groupId).isEmpty()) {
return ResponseEntity.notFound().build();
}
Subgroup subgroup = subgroupRepository.findByIdAndStudentGroupId(id, groupId).orElse(null);
if (subgroup == null) {
return ResponseEntity.notFound().build();

View File

@@ -17,7 +17,7 @@ public class Subgroup extends LifecycleEntity {
@Column(nullable = false, length = 100)
private String name;
@Column(name = "student_capacity")
@Column(name = "student_capacity", nullable = false)
private Integer studentCapacity;
public Long getId() {

View File

@@ -19,6 +19,7 @@ import com.magistr.app.repository.GroupRepository;
import com.magistr.app.repository.SpecialtiesRepository;
import com.magistr.app.repository.SpecialtyProfileRepository;
import com.magistr.app.repository.StudentGroupCalendarAssignmentRepository;
import com.magistr.app.repository.SubgroupRepository;
import com.magistr.app.utils.CourseAndSemesterCalculator;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
@@ -41,6 +42,7 @@ public class AcademicStructureService {
private final EducationFormRepository educationFormRepository;
private final GroupRepository groupRepository;
private final StudentGroupCalendarAssignmentRepository assignmentRepository;
private final SubgroupRepository subgroupRepository;
private final ScheduleGeneratorService scheduleGeneratorService;
public AcademicStructureService(AcademicCalendarRepository calendarRepository,
@@ -52,6 +54,7 @@ public class AcademicStructureService {
EducationFormRepository educationFormRepository,
GroupRepository groupRepository,
StudentGroupCalendarAssignmentRepository assignmentRepository,
SubgroupRepository subgroupRepository,
ScheduleGeneratorService scheduleGeneratorService) {
this.calendarRepository = calendarRepository;
this.calendarDayRepository = calendarDayRepository;
@@ -62,9 +65,22 @@ public class AcademicStructureService {
this.educationFormRepository = educationFormRepository;
this.groupRepository = groupRepository;
this.assignmentRepository = assignmentRepository;
this.subgroupRepository = subgroupRepository;
this.scheduleGeneratorService = scheduleGeneratorService;
}
@Transactional
public StudentGroup createGroup(CreateGroupRequest request) {
validateGroupRequest(request);
GroupDimensions candidate = resolveGroupDimensions(request);
StudentGroup group = new StudentGroup();
applyGroupDimensions(group, request, candidate);
StudentGroup saved = saveGroup(group);
clearScheduleCacheAfterCommit();
return saved;
}
@Transactional
public AcademicCalendar updateCalendar(Long id, AcademicCalendarDto request) {
validateCalendarRequest(request);
@@ -122,6 +138,13 @@ public class AcademicStructureService {
validateGroupRequest(request);
StudentGroup group = groupRepository.findByIdForUpdate(id)
.orElseThrow(() -> new NoSuchElementException("Учебная группа не найдена"));
long activeSubgroupCapacity = subgroupRepository.sumActiveStudentCapacityByGroupId(id);
if (request.getGroupSize() < activeSubgroupCapacity) {
throw new IllegalArgumentException(
"Численность группы не может быть меньше суммы численностей активных подгрупп ("
+ activeSubgroupCapacity + " чел.)"
);
}
GroupDimensions candidate = resolveGroupDimensions(request);
boolean incompatibleAssignment = assignmentRepository.findByGroupIdWithDetails(id).stream()
@@ -139,13 +162,7 @@ public class AcademicStructureService {
);
}
group.setName(request.getName().trim());
group.setGroupSize(request.getGroupSize());
group.setEducationForm(candidate.studyForm());
group.setDepartmentId(request.getDepartmentId());
group.setYearStartStudy(request.getYearStartStudy());
group.setSpeciality(candidate.speciality());
group.setSpecialtyProfile(candidate.profile());
applyGroupDimensions(group, request, candidate);
StudentGroup saved = saveGroup(group);
clearScheduleCacheAfterCommit();
return saved;
@@ -217,26 +234,38 @@ public class AcademicStructureService {
if (request.getName() == null || request.getName().isBlank()) {
throw new IllegalArgumentException("Название группы обязательно");
}
if (request.getGroupSize() == null) {
throw new IllegalArgumentException("Численность группы обязательна");
if (request.getGroupSize() == null || request.getGroupSize() <= 0) {
throw new IllegalArgumentException("Численность группы должна быть больше нуля");
}
if (request.getEducationFormId() == null) {
if (request.getEducationFormId() == null || request.getEducationFormId() <= 0) {
throw new IllegalArgumentException("Форма обучения обязательна");
}
if (request.getDepartmentId() == null || request.getDepartmentId() == 0) {
if (request.getDepartmentId() == null || request.getDepartmentId() <= 0) {
throw new IllegalArgumentException("ID кафедры обязателен");
}
if (request.getYearStartStudy() == null || request.getYearStartStudy() == 0) {
throw new IllegalArgumentException("Год начала обучения обязателен");
if (request.getYearStartStudy() == null || request.getYearStartStudy() <= 0) {
throw new IllegalArgumentException("Год начала обучения должен быть больше нуля");
}
if (request.getEffectiveSpecialtyId() == null || request.getEffectiveSpecialtyId() == 0) {
if (request.getEffectiveSpecialtyId() == null || request.getEffectiveSpecialtyId() <= 0) {
throw new IllegalArgumentException("Код специальности обязателен");
}
if (request.getSpecialtyProfileId() == null || request.getSpecialtyProfileId() == 0) {
if (request.getSpecialtyProfileId() == null || request.getSpecialtyProfileId() <= 0) {
throw new IllegalArgumentException("Профиль обучения обязателен");
}
}
private void applyGroupDimensions(StudentGroup group,
CreateGroupRequest request,
GroupDimensions candidate) {
group.setName(request.getName().trim());
group.setGroupSize(request.getGroupSize());
group.setEducationForm(candidate.studyForm());
group.setDepartmentId(request.getDepartmentId());
group.setYearStartStudy(request.getYearStartStudy());
group.setSpeciality(candidate.speciality());
group.setSpecialtyProfile(candidate.profile());
}
private CalendarDimensions resolveCalendarDimensions(AcademicCalendarDto request) {
AcademicYear academicYear = academicYearRepository.findById(request.academicYearId())
.orElseThrow(() -> new IllegalArgumentException("Учебный год не найден"));

View File

@@ -284,7 +284,8 @@ public class ScheduleGeneratorService {
if (endDate.isBefore(startDate)) {
throw new IllegalArgumentException("Дата окончания не может быть раньше даты начала");
}
if (java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) > MAX_RANGE_DAYS) {
long inclusiveDays = java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) + 1;
if (inclusiveDays > MAX_RANGE_DAYS) {
throw new IllegalArgumentException("Диапазон расписания не может превышать 120 дней");
}
}

View File

@@ -298,7 +298,8 @@ public class ScheduleQueryService {
if (endDate.isBefore(startDate)) {
throw new IllegalArgumentException("Дата окончания не может быть раньше даты начала");
}
if (ChronoUnit.DAYS.between(startDate, endDate) > MAX_RANGE_DAYS) {
long inclusiveDays = ChronoUnit.DAYS.between(startDate, endDate) + 1;
if (inclusiveDays > MAX_RANGE_DAYS) {
throw new IllegalArgumentException("Диапазон расписания не может превышать 120 дней");
}
}