исправление багов

This commit is contained in:
Zuev
2026-07-13 03:28:18 +03:00
parent 39c58440cf
commit 85f61436b6
76 changed files with 9219 additions and 1258 deletions

View File

@@ -135,6 +135,13 @@ Bearer-токен проверяется на backend. Frontend-скрытие
| `schedule_rule_slot_subgroups` | Подгруппы лабораторного слота |
| `schedule_overrides` | Точечные переносы, отмены и замены конкретных сгенерированных пар |
Полная замена `academic_calendar_days` выполняется через транзакционный
`AcademicCalendarGridService`. Сервис сначала проверяет и строит весь новый набор, включая
уникальность `(course, date)`, соответствие даты учебному году, номеру недели и ISO-дню,
и разрешает все коды активностей. Только после этого прежняя сетка удаляется и новый набор
записывается одной транзакцией. Ошибка любой строки сохраняет прежнюю сетку целиком, а кэш
расписания очищается только после commit.
Генератор `ScheduleGeneratorService` рендерит расписание по запросу:
1. Определяет семестр для каждой даты диапазона.
2. Вычисляет номер недели и чётность.
@@ -152,7 +159,13 @@ Bearer-токен проверяется на backend. Frontend-скрытие
В генерацию попадают только активные на дату правила, дисциплины, группы, преподаватели и аудитории. Для будущих дат аудитория с `is_available=false` не выводится в расписании, но прошлые занятия остаются доступными для просмотра.
Расширенный поиск расписания ограничивает широкие запросы: если не указаны `groupId` и `departmentId`, сервис не будет обходить больше 50 активных групп и вернёт ошибку валидации. Запросы по одному `teacherId` без группы или кафедры строятся через генерацию расписания преподавателя, чтобы не выполнять полный перебор групп.
Расширенный поиск расписания ограничивает широкие запросы: если не указаны `groupId`,
`departmentId` и teacher-only режим, сервис не будет обходить больше 50 активных групп и
вернёт ошибку валидации. Запросы по одному `teacherId` сначала строятся через генерацию
базового расписания преподавателя. Для overrides с совпадающим `newTeacher` сервис один раз
на каждую релевантную дату достраивает базовый день, выбирает только целевые слоты, применяет
единый снимок изменений и затем фильтрует итогового преподавателя. При отсутствии таких
замен полный список групп не загружается.
Лабораторные работы могут делиться на подгруппы через `schedule_rule_slot_subgroups`. Если подгруппы выбраны, занятие выводится только для родительских групп этих подгрупп, а лимит лабораторных часов списывается отдельно по каждой подгруппе. Если лабораторная проводится у нескольких групп одновременно, один слот может содержать разные подгруппы разных групп. Лекции и практики не делятся на подгруппы.
@@ -178,26 +191,47 @@ Bearer-токен проверяется на backend. Frontend-скрытие
### Валидация правил расписания
- **Правило:** обязательны дисциплина, семестр, хотя бы один положительный лимит часов по типу занятий, положительные недели начала и хотя бы одна группа.
- **Правило:** обязательны дисциплина, семестр, положительные недели начала и хотя бы одна группа; каждый лимит часов неотрицателен и чётен, а сумма лимитов положительна. Ноль разрешён для неиспользуемого типа занятия.
- **Покрытие типов:** если для лекций, лабораторных или практик указан лимит часов, должен быть хотя бы один слот этого типа; слот типа не сохраняется с нулевым лимитом часов.
- **Слот:** день недели должен быть от 1 до 7, чётность недели обязательна и принимает только `BOTH`, `ODD` или `EVEN`.
- **Связанные сущности:** базовый временной слот, преподаватель, аудитория и тип занятия должны существовать в БД.
- **Связанные сущности:** базовый временной слот, преподаватель, аудитория и тип занятия должны существовать в БД; преподавателем может быть только активный пользователь с ролью `TEACHER`.
- **Подгруппы:** `subgroupIds` разрешены только для лабораторных слотов, должны относиться к группам правила, и в одном слоте можно выбрать не больше одной подгруппы каждой группы.
- **Формат:** `lessonFormat` обязателен и хранится в слоте правила.
- **Формат:** `lessonFormat` обязателен и принимает только `Очно` или `Онлайн`.
- **Жизненный цикл:** архивные преподаватели, аудитории, группы и дисциплины не принимаются в новых правилах.
- **Правило расписания:** `status=ARCHIVED` или дата вне `valid_from` / `valid_to` исключают правило из генерации.
- **Доступность аудитории:** `is_available=false` запрещает новые назначения, но не удаляет историю.
- **Конфликты слотов:** при создании и обновлении правил проверяются активные правила того же семестра. Конфликт возникает при пересечении дня, базового временного слота, чётности (`BOTH` пересекается с любой чётностью) и активных недель слота, если совпадает преподаватель, аудитория или учебная группа. Активные недели считаются из лимита часов типа занятия, недели начала, чётности и порядка слотов внутри правила; например, занятие на 1-3 неделях не конфликтует с тем же ресурсом с 4 недели. Для лабораторных занятий разные подгруппы одной группы могут идти параллельно, но занятие для всей группы конфликтует с любой её подгруппой. Backend возвращает `409 Conflict` с ранее созданным `conflictRule`, чтобы frontend мог предложить перенос этого правила.
- **Конфликты слотов:** сначала попарно проверяются слоты самого нового payload, включая точные дубли, затем — активные правила того же семестра. Конфликт возникает при пересечении дня, базового временного слота, чётности (`BOTH` пересекается с любой чётностью) и активных недель слота, если совпадает преподаватель, аудитория или учебная группа. `ODD` и `EVEN` между собой не конфликтуют. Активные недели считаются из лимита часов типа занятия, недели начала, чётности и порядка слотов внутри правила; например, занятие на 1-3 неделях не конфликтует с тем же ресурсом с 4 недели. Для лабораторных занятий разные подгруппы одной группы могут идти параллельно, но занятие для всей группы конфликтует с любой её подгруппой. Backend возвращает `409 Conflict`; `conflictRule` присутствует только для конфликта с сохранённым правилом, а внутренний конфликт описывается полями и русскими причинами без искусственной записи.
- **Конкурентная запись:** публичные методы `ScheduleRuleService` являются транзакционными. Создание сначала блокирует строку семестра, а update блокирует правило и старый/новый семестры в стабильном порядке, поэтому два backend-pod не могут одновременно пройти проверку одного семестра по устаревшему снимку.
### Точечные изменения расписания
Учебный отдел может создать изменение конкретной пары:
- `CANCEL` — отменить пару;
- `MOVE` — перенести пару на другой временной слот или в другую аудиторию;
- `MOVE` — перенести пару на другой фактический интервал или в другую аудиторию;
- `REPLACE` — заменить преподавателя, аудиторию или формат.
Изменения не переписывают базовое правило, а накладываются поверх сгенерированного расписания на конкретную дату.
`CANCEL` не принимает новые ресурсы. Для `MOVE` обязателен новый временной слот или
аудитория, для `REPLACE` — преподаватель, аудитория или формат. Дополнительные изменения
можно объединять в одном payload, но основное действие должно фактически менять свою
часть пары. Формат ограничен значениями `Очно` и `Онлайн`.
Изменения не переписывают базовое правило, а накладываются поверх сгенерированного
расписания на конкретную дату. Перед записью `ScheduleOverrideService`:
1. захватывает transaction advisory lock PostgreSQL для tenant-БД и даты;
2. строит базовый день для всех групп без интерактивного лимита широкого поиска;
3. доказывает существование исходной пары с учётом семестра, календарного графика,
чётности, недели начала, лимита часов и lifecycle;
4. применяет сохранённые overrides и кандидат общей логикой `ScheduleQueryService`;
5. проверяет полуоткрытые временные интервалы `[start, end)` и итоговые ресурсы;
6. сохраняет изменение только при отсутствии конфликта.
Совпадение преподавателя или аудитории в пересекающееся время всегда является конфликтом.
Для общей учебной группы занятие целой группы конфликтует с любой её подгруппой; разные
подгруппы одной группы могут идти параллельно при свободных преподавателях и аудиториях.
Конфликт возвращается как `409 Conflict` с русским сообщением. Блокировка PostgreSQL общая
для backend-pod, поэтому два конкурентных изменения одной даты проверяются последовательно.
---