баг-фикс 30/34
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
| Роль | Enum | Возможности |
|
||||
|------|------|------------|
|
||||
| **Администратор** | `ADMIN` | Полный доступ: пользователи, справочники, тенанты, роли, архивирование и восстановление. |
|
||||
| **Учебный отдел** | `EDUCATION_OFFICE` | Редактирование расписания, точечные переносы/замены/отмены, временные слоты, аудитории, загруженность. |
|
||||
| **Учебный отдел** | `EDUCATION_OFFICE` | Редактирование расписания, точечные переносы/замены/отмены, учебные периоды и календарные графики, временные слоты, формы обучения, аудитории и загруженность; read-only справочники специальностей и профилей. |
|
||||
| **Кафедра** | `DEPARTMENT` | Дисциплины своей кафедры, загрузка дисциплин, привязки преподавателей, заявки на создание преподавателей, комментарии, расписание и нагрузка кафедры. |
|
||||
| **Просмотр расписаний** | `SCHEDULE_VIEWER` | Read-only просмотр расписаний по группам, преподавателям, аудиториям и кафедрам в режиме одной активной совмещённой таблицы чётной/нечётной недели. |
|
||||
| **Преподаватель** | `TEACHER` | Просмотр своего расписания. В перспективе — подача заявок на перенос. |
|
||||
@@ -27,9 +27,17 @@ Bearer-токен проверяется на backend. Frontend-скрытие
|
||||
|
||||
- загрузка и комментарии дисциплин идут через `/api/department/*`;
|
||||
- общий `/api/subjects` доступен кафедре только на чтение;
|
||||
- привязки `/api/teacher-subjects` разрешены только если преподаватель и дисциплина относятся к кафедре текущего пользователя;
|
||||
- привязки `/api/teacher-subjects` разрешены только если дисциплина принадлежит кафедре текущего пользователя, а у преподавателя на текущую дату действует основная или дополнительная связь с ней;
|
||||
- добавление существующего преподавателя через `/api/department/teachers/{teacherId}/assignments` создаёт связь только со своей кафедрой;
|
||||
- заявки `/api/department/teacher-requests` создаются и просматриваются кафедрой только в рамках своей кафедры, а создание пользователя выполняет администратор после проверки.
|
||||
- все `/api/workload/*`, включая свободные аудитории, принудительно используют кафедру из `AuthContext`; запрос с чужим `departmentId` отклоняется с `403`.
|
||||
|
||||
Глобальный workload и произвольный фильтр кафедры разрешены ролям `ADMIN`,
|
||||
`EDUCATION_OFFICE` и `SCHEDULE_VIEWER`.
|
||||
|
||||
Ролевая матрица учебного отдела согласована с видимыми экранами: календарный график может
|
||||
читать специальности и профили, а раздел настроек форм обучения выполняет GET/POST/DELETE.
|
||||
Изменение самих специальностей и профилей остаётся административной операцией.
|
||||
|
||||
---
|
||||
|
||||
@@ -64,6 +72,8 @@ Bearer-токен проверяется на backend. Frontend-скрытие
|
||||
- **Календарь:** на каждый учебный год группе назначается конкретный календарный учебный график
|
||||
- **Дисциплины графика:** при назначении графика группе отображаются дисциплины, вручную привязанные к номерам семестров этого графика
|
||||
- **Завершение обучения:** если текущий курс больше `course_count` назначенного календарного графика, группа считается завершившей обучение и не попадает в обычные списки выбора. Историческое расписание по датам периода обучения остаётся доступным.
|
||||
- **Целостность назначения:** год, специальность, профиль и форма графика должны совпадать с группой, а вычисленный курс должен входить в `1..course_count`. После назначения несовместимое изменение группы или графика отклоняется целиком с `409 Conflict`; назначение не удаляется и не становится устаревшим.
|
||||
- **Размерность графика:** уменьшение `course_count` запрещено, пока существуют строки сетки старших курсов или дисциплины семестров выше `course_count * 2`. Даты сетки всегда находятся внутри учебного года графика.
|
||||
|
||||
### Аудитории (Classrooms)
|
||||
|
||||
@@ -81,9 +91,11 @@ Bearer-токен проверяется на backend. Frontend-скрытие
|
||||
|
||||
### Дисциплины (Subjects)
|
||||
|
||||
- **Поля:** Название (уникальное), код, кафедра, описание
|
||||
- **Поля:** Название (глобально уникальное без учёта регистра), код, кафедра, описание
|
||||
- Привязка преподавателей через `teacher_subjects` (Many-to-Many)
|
||||
- Кафедра может добавлять комментарии к дисциплине и загружать список дисциплин через кабинет кафедры
|
||||
- Повторный импорт собственной дисциплины обновляет ту же запись и восстанавливает её из архива
|
||||
- Совпадение названия с дисциплиной другой кафедры даёт `409 Conflict` и никогда не меняет владельца
|
||||
|
||||
### Жизненный цикл справочников
|
||||
|
||||
@@ -98,18 +110,24 @@ Bearer-токен проверяется на backend. Frontend-скрытие
|
||||
|
||||
### Кафедральные связи преподавателей
|
||||
|
||||
Основная кафедра преподавателя хранится в `users.department_id` для совместимости, а актуальные и исторические связи фиксируются в `teacher_department_assignments`. Преподаватель может быть связан с несколькими кафедрами: одна связь остаётся основной, дополнительные связи создаются как неосновные.
|
||||
Актуальные и исторические связи преподавателя с кафедрами определяются только по
|
||||
`teacher_department_assignments`. Поле `users.department_id` сохраняется как legacy-зеркало
|
||||
основной кафедры на текущую дату и не используется для бизнес-решений или исторических
|
||||
отчётов. Преподаватель может быть связан с несколькими кафедрами: одна связь остаётся
|
||||
основной, дополнительные связи создаются как неосновные.
|
||||
|
||||
Правила:
|
||||
|
||||
- у преподавателя должна быть одна открытая основная кафедра;
|
||||
- периоды двух основных кафедр одного преподавателя не могут пересекаться;
|
||||
- преподаватель может иметь несколько открытых неосновных кафедр;
|
||||
- одна открытая пара `teacher_id` + `department_id` запрещает дубли одной и той же связи;
|
||||
- при переводе старая запись закрывается датой `valid_to`, новая открывается с `valid_from`;
|
||||
- при переводе старая запись закрывается днём перед `valid_from`, новая начинается с `valid_from`;
|
||||
- будущий перевод не меняет текущую принадлежность и legacy-зеркало до даты вступления в силу;
|
||||
- кафедра может добавить существующего активного преподавателя только на свою кафедру, без смены его основной кафедры;
|
||||
- кафедра может отправить заявку на создание нового преподавателя, но заявка не хранит пароль;
|
||||
- администратор при одобрении заявки может скорректировать кафедру, логин, ФИО и должность, задаёт пароль и создаёт пользователя с ролью `TEACHER`;
|
||||
- расписание и отчёты за прошлые периоды не теряют связь с прежней кафедрой.
|
||||
- исторические списки включают архивного преподавателя, если он и назначение действовали на целевую дату;
|
||||
- нагрузка определяется для каждой даты занятия отдельно и разделяется между кафедрами при переводе внутри отчётного периода.
|
||||
|
||||
---
|
||||
|
||||
@@ -135,6 +153,14 @@ Bearer-токен проверяется на backend. Frontend-скрытие
|
||||
| `schedule_rule_slot_subgroups` | Подгруппы лабораторного слота |
|
||||
| `schedule_overrides` | Точечные переносы, отмены и замены конкретных сгенерированных пар |
|
||||
|
||||
Учебные годы и семестры изменяются через транзакционный `AcademicPeriodService`. Границы
|
||||
считаются включительными: разные учебные годы не могут иметь общую дату, а семестры не
|
||||
могут пересекаться внутри одного года. Семестр целиком лежит в границах своего учебного
|
||||
года; сужение года, исключающее существующий семестр, отклоняется. Создание и изменение
|
||||
семестра блокируют строку родительского года, а PostgreSQL exclusion constraints разрешают
|
||||
глобальные конкурентные гонки между разными backend-pod. Благодаря отсутствию пересечений
|
||||
поиск семестра для даты возвращает не более одного результата и не зависит от порядка строк.
|
||||
|
||||
Полная замена `academic_calendar_days` выполняется через транзакционный
|
||||
`AcademicCalendarGridService`. Сервис сначала проверяет и строит весь новый набор, включая
|
||||
уникальность `(course, date)`, соответствие даты учебному году, номеру недели и ISO-дню,
|
||||
@@ -155,6 +181,14 @@ Bearer-токен проверяется на backend. Frontend-скрытие
|
||||
10. Останавливает вывод слотов конкретного типа, когда достигнут его лимит часов.
|
||||
11. Применяет точечные изменения из `schedule_overrides` в расширенном поиске и отчётах.
|
||||
|
||||
Перед обходом дат генератор создаёт снимок на один запрос. Семестры диапазона, назначения
|
||||
календарей всех выбранных групп, дневная сетка календарей, правила и эффективные сетки
|
||||
звонков загружаются batch-запросами и индексируются по дате и идентификаторам. Поиск
|
||||
семестра, проверка разрешённого дня и подстановка времени внутри циклов выполняются только
|
||||
по lookup-картам. `ScheduleQueryService` передаёт все группы в один
|
||||
`buildScheduleForGroups()`, поэтому число запросов не растёт как
|
||||
`дни × группы × правила`; singleton-кэш и общее между запросами состояние не используются.
|
||||
|
||||
Расход часов считается в пределах одного построения расписания: при первом использовании семестра генератор одним последовательным проходом прогревает проведённые часы от начала семестра до начала запрошенного диапазона, затем ведёт локальный прогресс по правилу, типу занятия, группе и подгруппе. Обратного пересчёта прошлых дат для каждого слота нет. Singleton-кэш в сервисе не используется, поэтому данные расписания не накапливаются в heap между запросами и не устаревают после изменений правил, календаря или подгрупп.
|
||||
|
||||
В генерацию попадают только активные на дату правила, дисциплины, группы, преподаватели и аудитории. Для будущих дат аудитория с `is_available=false` не выводится в расписании, но прошлые занятия остаются доступными для просмотра.
|
||||
@@ -167,6 +201,12 @@ Bearer-токен проверяется на backend. Frontend-скрытие
|
||||
единый снимок изменений и затем фильтрует итогового преподавателя. При отсутствии таких
|
||||
замен полный список групп не загружается.
|
||||
|
||||
Отчёты workload и свободные аудитории не являются интерактивным поиском и используют
|
||||
отдельный `searchForAggregation`. Он обходит все активные группы разрешённого scope без
|
||||
лимита 50, но переиспользует общую проверку диапазона, снимок точечных изменений, фильтрацию
|
||||
и дедупликацию. Поэтому tenant с 51 или 100 группами получает полный агрегат, а защита
|
||||
широкого `/api/schedule/search` остаётся прежней.
|
||||
|
||||
Лабораторные работы могут делиться на подгруппы через `schedule_rule_slot_subgroups`. Если подгруппы выбраны, занятие выводится только для родительских групп этих подгрупп, а лимит лабораторных часов списывается отдельно по каждой подгруппе. Если лабораторная проводится у нескольких групп одновременно, один слот может содержать разные подгруппы разных групп. Лекции и практики не делятся на подгруппы.
|
||||
|
||||
Обычные пары генерируются только на коде `Т` (`allow_schedule = true`). Экзамены, каникулы, практики, нерабочие дни, праздники `*` и дни вне учебного года `=` считаются пропуском: занятие не переносится и не списывает академические часы. Если у группы нет назначения графика на учебный год, `GET /api/schedule` возвращает пустой список для этой группы без ошибки.
|
||||
@@ -175,8 +215,19 @@ Bearer-токен проверяется на backend. Frontend-скрытие
|
||||
|
||||
Сетки времени хранятся в `time_slot_scopes`, сами пары — в `time_slots`. Базовая сетка (`DEFAULT`) применяется по умолчанию, субботняя (`WEEKDAY`, `day_of_week = 6`) применяется автоматически по субботам, а пользовательские сетки (`MANUAL`) применяются только через `time_slot_date_assignments` на конкретные даты. Ручное назначение выполняется из ячейки редактора календарного графика, потому что оно относится к конкретной учебной дате.
|
||||
|
||||
Создание и изменение слота выполняет транзакционный `TimeSlotService`. Время начала должно
|
||||
быть раньше окончания, продолжительность вычисляется только backend, а внутри одной сетки
|
||||
запрещены одинаковые номера пар и пересекающиеся полуоткрытые интервалы. Поэтому соседние
|
||||
интервалы, например `08:00–09:30` и `09:30–11:00`, допустимы. Операции блокируют строки
|
||||
затронутых сеток в стабильном порядке; exclusion constraint PostgreSQL дополнительно
|
||||
защищает инвариант между экземплярами backend и при прямых конкурентных записях.
|
||||
|
||||
Правило расписания выбирает базовую пару по номеру. При генерации `ScheduleGeneratorService` сначала проверяет ручное назначение даты, затем автоматическую субботнюю сетку, затем базовую сетку. Если в выбранной сетке нет пары с нужным номером, используется базовый слот. Ручная сетка меняет только время занятий и не включает пары в дни, где календарный учебный график запрещает обычное расписание.
|
||||
|
||||
Правила расписания могут ссылаться только на слоты сетки `DEFAULT`. Используемый базовый
|
||||
слот нельзя перенести в `WEEKDAY` или `MANUAL`, а сетку с используемыми слотами нельзя
|
||||
сделать небазовой. Эти условия проверяются сервисом и транзакционными триггерами БД.
|
||||
|
||||
При миграции одинаковые стартовые слоты создаются для базовой и субботней сетки:
|
||||
|
||||
| № | Время |
|
||||
|
||||
Reference in New Issue
Block a user