58 KiB
📋 Бизнес-логика
Ролевая модель
Система поддерживает шесть ролей пользователей:
| Роль | Enum | Возможности |
|---|---|---|
| Администратор | ADMIN |
Полный доступ: пользователи, справочники, тенанты, роли, архивирование и восстановление. |
| Учебный отдел | EDUCATION_OFFICE |
Редактирование расписания, точечные переносы/замены/отмены, учебные периоды и календарные графики, временные слоты, формы обучения, аудитории и загруженность; read-only справочники специальностей и профилей. |
| Кафедра | DEPARTMENT |
Дисциплины своей кафедры, загрузка дисциплин, привязки преподавателей, заявки на создание преподавателей, комментарии, расписание и нагрузка кафедры. |
| Просмотр расписаний | SCHEDULE_VIEWER |
Read-only просмотр расписаний по группам, преподавателям, аудиториям и кафедрам в режиме одной активной совмещённой таблицы чётной/нечётной недели. |
| Преподаватель | TEACHER |
Просмотр своего расписания. В перспективе — подача заявок на перенос. |
| Студент | STUDENT |
Только просмотр расписания (Read-only). |
После авторизации пользователь перенаправляется на свой интерфейс:
ADMIN→/admin/EDUCATION_OFFICE→/admin/#schedule-viewDEPARTMENT→/admin/#department-workspaceSCHEDULE_VIEWER→/admin/#schedule-viewTEACHER→/teacher/STUDENT→/student/
Bearer-токен проверяется на backend. Frontend-скрытие пунктов меню является только удобством, а не источником прав.
Для роли DEPARTMENT backend дополнительно ограничивает изменения рамками кафедры текущего пользователя:
- загрузка и комментарии дисциплин идут через
/api/department/*; - общий
/api/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. Изменение самих специальностей и профилей остаётся административной операцией.
Управление ресурсами
Кафедры (Departments)
Организационные единицы университета. К кафедре привязываются пользователи, группы и дисциплины.
- Имеют уникальный числовой
code - Предзаполнены: «Кафедра ИБ», «Кафедра ВТ», «Кафедра КТ»
Специальности (Specialties)
Учебные направления с кодом по ФГОС. У одной специальности может быть несколько профилей обучения, например базовый профиль и профиль конкретной образовательной программы.
- Примеры: «Информационная безопасность» (10.03.01), «Программная инженерия» (09.03.04)
- При создании специальности автоматически создаётся профиль
Без профиля
Формы обучения (Education Forms)
Уровни/формы обучения для привязки к группам и календарным учебным графикам.
- Предзаполнены: Бакалавриат, Магистратура, Специалитет
- Нельзя удалить форму обучения, если к ней привязаны группы или календарные графики
Учебные группы (Student Groups)
- Поля: Название, положительная численность, форма обучения, кафедра, специальность, профиль обучения, положительный год начала обучения
- Курс: вычисляется относительно учебного года:
год начала учебного года - year_start_study + 1, но до начала обучения отдаётся как0, а не отрицательное число - Подгруппы: Возможно деление группы на 2 или 3 подгруппы либо режим без деления (таблица
subgroups). Численность каждой подгруппы обязательна и положительна, а сумма численностей активных подгрупп не может превышать численность группы. Численность группы нельзя уменьшить ниже этой суммы. Изменения группы и подгрупп сериализуются блокировкой родительской группы и дополнительно защищены триггерами V1, поэтому параллельные запросы не нарушают инвариант. Нельзя удалить одну подгруппу из активного деления так, чтобы часть студентов не относилась ни к одной подгруппе. - Календарь: на каждый учебный год группе назначается конкретный календарный учебный график
- Дисциплины графика: при назначении графика группе отображаются дисциплины, вручную привязанные к номерам семестров этого графика
- Завершение обучения: если текущий курс больше
course_countназначенного календарного графика, группа считается завершившей обучение и не попадает в обычные списки выбора. Историческое расписание по датам периода обучения остаётся доступным. - Целостность назначения: год, специальность, профиль и форма графика должны совпадать с группой, а вычисленный курс должен входить в
1..course_count. После назначения несовместимое изменение группы или графика отклоняется целиком с409 Conflict; назначение не удаляется и не становится устаревшим. - Размерность графика: уменьшение
course_countзапрещено, пока существуют строки сетки старших курсов или дисциплины семестров вышеcourse_count * 2. Даты сетки всегда находятся внутри учебного года графика.
Аудитории (Classrooms)
- Поля: Название (уникальное), вместимость (> 0), корпус, этаж, доступность
- Оборудование: К каждой аудитории привязывается список оборудования (Many-to-Many) с указанием количества
- Статус: Флаг
is_availableдля блокирования назначения пар - Жизненный цикл:
status=ARCHIVEDозначает вывод из эксплуатации. Такая аудитория отображается в прошлом расписании, но запрещена для новых назначений.
Оборудование (Equipments)
Каталог оборудования для привязки к аудиториям.
- Предзаполнены: Проектор, ПК, Лаборатория, Интерактивная доска, Документ-камера, Аудиосистема
- Уникальность по названию
Дисциплины (Subjects)
- Поля: Название (глобально уникальное без учёта регистра), код, кафедра, описание
- Привязка преподавателей через
teacher_subjects(Many-to-Many) - Кафедра может добавлять комментарии к дисциплине и загружать список дисциплин через кабинет кафедры
- Повторный импорт собственной дисциплины обновляет ту же запись и восстанавливает её из архива
- Совпадение названия с дисциплиной другой кафедры даёт
409 Conflictи никогда не меняет владельца
Жизненный цикл справочников
Справочники, которые участвуют в расписании и отчётах, не удаляются физически. Для них используется архивирование:
ACTIVE— запись доступна для выбора;ARCHIVED— запись остаётся в истории, но не используется в будущих назначениях.
Архивирование уже применяется к пользователям, аудиториям, оборудованию, кафедрам, специальностям, группам, подгруппам, дисциплинам и профилям обучения. Исторические отчёты используют записи, действовавшие на дату занятия.
Методическая проверка активности учитывает период действия и status: архивная запись без active_to не считается активной, но запись с заполненным active_to остаётся активной для исторических дат до даты вывода из работы.
Кафедральные связи преподавателей
Актуальные и исторические связи преподавателя с кафедрами определяются только по
teacher_department_assignments. Поле users.department_id сохраняется как legacy-зеркало
основной кафедры на текущую дату и не используется для бизнес-решений или исторических
отчётов. Преподаватель может быть связан с несколькими кафедрами: одна связь остаётся
основной, дополнительные связи создаются как неосновные.
Правила:
- периоды двух основных кафедр одного преподавателя не могут пересекаться;
- преподаватель может иметь несколько открытых неосновных кафедр;
- одна открытая пара
teacher_id+department_idзапрещает дубли одной и той же связи; - при переводе старая запись закрывается днём перед
valid_from, новая начинается сvalid_from; - будущий перевод не меняет текущую принадлежность и legacy-зеркало до даты вступления в силу;
- кафедра может добавить существующего активного преподавателя только на свою кафедру, без смены его основной кафедры;
- кафедра может отправить заявку на создание нового преподавателя, но заявка не хранит пароль;
- администратор при одобрении заявки может скорректировать кафедру, логин, ФИО и должность, задаёт пароль и создаёт пользователя с ролью
TEACHER; - исторические списки включают архивного преподавателя, если он и назначение действовали на целевую дату;
- нагрузка определяется для каждой даты занятия отдельно и разделяется между кафедрами при переводе внутри отчётного периода.
Логика расписания
Динамическая модель расписания
Основная модель расписания строится из правил, а не из отдельных статических пар.
| Компонент | Назначение |
|---|---|
academic_years / semesters |
Учебные годы и семестры. Неделя 1 считается от semesters.start_date |
specialty_profiles |
Профили обучения внутри специальности |
academic_calendars |
Календарный учебный график профиля, формы обучения и учебного года |
academic_calendar_activity_types |
Коды Excel-графика: Т, Э, К, У, П, Пд, Н, Г, Д, ПА, С, *, = |
academic_calendar_periods |
Непересекающиеся непрерывные периоды активности по курсу |
academic_calendar_subjects |
Дисциплины графика по номерам учебных семестров |
student_group_calendar_assignments |
Назначение конкретного графика группе на учебный год |
time_slots |
Настраиваемая сетка пар для тенанта |
schedule_rules |
Лимиты часов и недели начала по лекциям, лабораторным и практикам |
schedule_rule_groups |
Группы правила, включая потоковые лекции |
schedule_rule_slots |
День, чётность, слот, преподаватель, аудитория, тип и формат занятия |
schedule_rule_slot_subgroups |
Подгруппы лабораторного слота |
schedule_overrides |
Точечные переносы, отмены и замены конкретных сгенерированных пар |
Название календарного учебного графика не является отдельным пользовательским атрибутом.
При создании и изменении backend всегда формирует его из кода специальности, названия
профиля, формы обучения и учебного года. Переданное клиентом значение title игнорируется.
Новый календарный график сохраняется вместе с начальной сеткой одной транзакцией. Для каждого
курса создаётся период Т от первой до последней даты учебного года включительно. Таким образом,
дни обычных занятий доступны генератору сразу, а пользователь редактирует только исключения:
сессию, каникулы, практику и другие активности. Редактирование реквизитов уже существующего
графика не сбрасывает его периоды. Миграция V2__backfill_default_theory_periods.sql применяет
такое заполнение к ранее созданным графикам только при полном отсутствии периодов; частично или
полностью настроенные сетки она не изменяет.
Учебные годы и семестры изменяются через транзакционный AcademicPeriodService. Границы
считаются включительными: разные учебные годы не могут иметь общую дату, а семестры не
могут пересекаться внутри одного года. Семестр целиком лежит в границах своего учебного
года; сужение года, исключающее существующий семестр, отклоняется. Создание и изменение
семестра блокируют строку родительского года, а PostgreSQL exclusion constraints разрешают
глобальные конкурентные гонки между разными backend-pod. Благодаря отсутствию пересечений
поиск семестра для даты возвращает не более одного результата и не зависит от порядка строк.
Полная замена дневной сетки выполняется через транзакционный
AcademicCalendarGridService. Сервис сначала проверяет весь дневной payload, включая
уникальность (course, date), соответствие даты учебному году, номеру недели и ISO-дню,
и разрешает все коды активностей. Затем соседние даты одного курса с одинаковой активностью
объединяются в один включительный период academic_calendar_periods. Только после этого
прежние периоды удаляются и новый набор записывается одной транзакцией. Ошибка любой строки
сохраняет прежнюю сетку целиком, а кэш расписания очищается только после commit. База
дополнительно запрещает пересекающиеся периоды одного курса и графика через exclusion
constraint. При чтении API разворачивает периоды обратно в дневные ячейки. Недели дневной сетки выровнены по ISO-неделе
понедельник–воскресенье: неделя 1 содержит первый день учебного года, а позиции до него
остаются пустыми. Поэтому при старте года во вторник следующий понедельник относится уже
к неделе 2.
Генератор ScheduleGeneratorService рендерит расписание по запросу:
- Определяет семестр для каждой даты диапазона.
- Вычисляет номер недели и чётность.
- Находит назначенный группе календарный учебный график на учебный год даты.
- Определяет код активности по курсу группы и конкретной дате.
- Пропускает день, если код активности не разрешает обычные пары.
- Загружает правила группы или преподавателя.
- Для каждого типа занятия проверяет свою неделю начала в семестре.
- Подставляет эффективную сетку времени даты: ручную, субботнюю или базовую.
- Считает уже проведённые часы отдельно для лекций, практик и каждой лабораторной подгруппы.
- Останавливает вывод слотов конкретного типа, когда достигнут его лимит часов.
- Применяет точечные изменения из
schedule_overridesв расширенном поиске и отчётах.
Перед обходом дат генератор создаёт снимок на один запрос. Семестры диапазона, назначения
календарей всех выбранных групп, пересекающиеся периоды активности, правила и эффективные
сетки звонков загружаются batch-запросами. Периоды группируются по календарю и курсу, а
поиск периода для даты выполняется бинарным поиском. Поиск семестра и подстановка времени
внутри циклов выполняются только по снимку запроса. ScheduleQueryService передаёт все группы в один
buildScheduleForGroups(), поэтому число запросов не растёт как
дни × группы × правила; singleton-кэш и общее между запросами состояние не используются.
Расход часов считается в пределах одного построения расписания: при первом использовании семестра генератор одним последовательным проходом прогревает проведённые часы от начала семестра до начала запрошенного диапазона, затем ведёт локальный прогресс по правилу, типу занятия, группе и подгруппе. Обратного пересчёта прошлых дат для каждого слота нет. Singleton-кэш в сервисе не используется, поэтому данные расписания не накапливаются в heap между запросами и не устаревают после изменений правил, календаря или подгрупп.
В генерацию попадают только активные на дату правила, дисциплины, группы, преподаватели и аудитории. Для будущих дат аудитория с is_available=false не выводится в расписании, но прошлые занятия остаются доступными для просмотра.
Диапазон расписания включает обе переданные границы и ограничен ровно 200 календарными
датами. Query- и generator-слои используют одинаковый расчёт end - start + 1: 200 дат
разрешены, 201 дата отклоняется до генерации. Один запрос поэтому покрывает учебный
семестр целиком.
Расширенный поиск расписания ограничивает широкие запросы: если не указаны groupId,
departmentId и teacher-only режим, сервис не будет обходить больше 50 активных групп и
вернёт ошибку валидации. Запросы по одному teacherId сначала строятся через генерацию
базового расписания преподавателя. Для overrides с совпадающим newTeacher сервис один раз
на каждую релевантную дату достраивает базовый день, выбирает только целевые слоты, применяет
единый снимок изменений и затем фильтрует итогового преподавателя. При отсутствии таких
замен полный список групп не загружается.
Отчёты workload и свободные аудитории не являются интерактивным поиском и используют
отдельный searchForAggregation. Он обходит все активные группы без лимита 50, но
переиспользует общую проверку диапазона, снимок точечных изменений, фильтрацию и
дедупликацию. Кафедральный scope применяется после генерации и точечных изменений по
любой действующей основной или дополнительной связи преподавателя с кафедрой на дату занятия,
а не по кафедре учебной группы.
Поэтому межкафедральные занятия входят в нагрузку преподавателя его кафедры. Tenant с 51
или 100 группами получает полный агрегат, а защита
широкого /api/schedule/search остаётся прежней.
Лабораторные работы могут делиться на подгруппы через schedule_rule_slot_subgroups. Если подгруппы выбраны, занятие выводится только для родительских групп этих подгрупп, а лимит лабораторных часов списывается отдельно по каждой подгруппе. Если лабораторная проводится у нескольких групп одновременно, один слот может содержать разные подгруппы разных групп. Лекции и практики не делятся на подгруппы.
Обычные пары генерируются только на коде Т (allow_schedule = true). Экзамены, каникулы, практики, нерабочие дни, праздники * и дни вне учебного года = считаются пропуском: занятие не переносится и не списывает академические часы. Если у группы нет назначения графика на учебный год, GET /api/schedule возвращает пустой список для этой группы без ошибки.
Временные слоты
Сетки времени хранятся в 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, а сетку с используемыми слотами нельзя
сделать небазовой. Эти условия проверяются сервисом и транзакционными триггерами БД.
При миграции одинаковые стартовые слоты создаются для базовой и субботней сетки:
| № | Время |
|---|---|
| 1 | 08:00 – 09:30 |
| 2 | 09:40 – 11:10 |
| 3 | 11:40 – 13:10 |
| 4 | 13:20 – 14:50 |
| 5 | 15:00 – 16:30 |
| 6 | 16:50 – 18:20 |
| 7 | 18:30 – 20:00 |
Валидация правил расписания
- Правило: обязательны дисциплина, семестр, положительные недели начала и хотя бы одна группа; каждый лимит часов неотрицателен и чётен, а сумма лимитов положительна. Ноль разрешён для неиспользуемого типа занятия.
- Покрытие типов: если для лекций, лабораторных или практик указан лимит часов, должен быть хотя бы один слот этого типа; слот типа не сохраняется с нулевым лимитом часов.
- Слот: день недели должен быть от 1 до 7, чётность недели обязательна и принимает только
BOTH,ODDилиEVEN. - Связанные сущности: базовый временной слот, преподаватель, аудитория и тип занятия должны существовать в БД; преподавателем может быть только активный пользователь с ролью
TEACHER. - Подгруппы:
subgroupIdsразрешены только для лабораторных слотов, должны относиться к группам правила, и в одном слоте можно выбрать не больше одной подгруппы каждой группы. - Формат:
lessonFormatобязателен и принимает толькоОчноилиОнлайн. - Жизненный цикл: архивные преподаватели, аудитории, группы и дисциплины не принимаются в новых правилах.
- Правило расписания:
status=ARCHIVEDили дата внеvalid_from/valid_toисключают правило из генерации. - Доступность аудитории:
is_available=falseзапрещает новые назначения, но не удаляет историю. - Конфликты слотов: сначала попарно проверяются слоты самого нового payload, включая точные дубли, затем — активные правила той же версии. Конфликт возникает при пересечении дня, базового временного слота, чётности (
BOTHпересекается с любой чётностью) и активных недель слота, если совпадает преподаватель, аудитория или учебная группа.ODDиEVENмежду собой не конфликтуют. Активные недели считаются из лимита часов типа занятия, недели начала, чётности и порядка слотов внутри правила; например, занятие на 1-3 неделях не конфликтует с тем же ресурсом с 4 недели. Для лабораторных занятий разные подгруппы одной группы могут идти параллельно, но занятие для всей группы конфликтует с любой её подгруппой. Backend возвращает409 Conflict;conflictRuleприсутствует только для конфликта с сохранённым правилом, а внутренний конфликт описывается полями и русскими причинами без искусственной записи. - Конкурентная запись: публичные методы
ScheduleRuleServiceявляются транзакционными. Создание блокирует строку семестра и выбранную версию, а update — правило, версию и старый/новый семестры в стабильном порядке. Публикация блокирует версию до завершения полной проверки, поэтому другой backend-pod не может дописать правило после валидации черновика. - Связанные слоты: update сохраняет ID переданных существующих слотов, чтобы не разрывать точечные изменения и операционный аудит. Удаление слота отклоняется, если на него ссылаются override, заявка преподавателя или решение по отсутствию.
Черновики, версии и публикация
Правила каждого семестра принадлежат явной версии расписания. Жизненный цикл версии:
- При создании семестра автоматически появляется пустая версия 1
PUBLISHED— основное расписание. - Конструктор по умолчанию открывает опубликованное расписание: изменения его правил сразу
видны конечным пользователям. Пользователь может переключиться на существующий
DRAFT. DRAFTсоздаётся пустым через API или прямо в конструкторе как полная копия выбранной опубликованной либо черновой версии; после создания он сразу становится текущим.- Полная проверка выявляет внутренние конфликты правил; diff сопоставляет правила по
стабильному
version_group_idи отдельно сравнивает сформированные занятия семестра. - Публикация требует причины и в одной транзакции архивирует прежнюю публикацию, затем
переводит проверенный черновик в
PUBLISHED. - Ранее опубликованная версия получает
ARCHIVEDи может быть восстановлена такой же атомарной операцией с обязательной причиной.
На уровне БД частичный уникальный индекс допускает только одну PUBLISHED-версию на
семестр. Блокировки версии и набора версий семестра не позволяют публикации пересечься с
редактированием расписания или конкурентной публикацией. Каждое создание, архивирование,
публикация и восстановление записывается в неизменяемый журнал с автором, временем и
причиной.
Обычная генерация для студентов, преподавателей и кабинетов просмотра всегда выбирает только опубликованные правила. Точечные изменения привязаны к слотам конкретной версии: после новой публикации overrides прежней версии сохраняются как аудит, но не влияют на актуальное расписание и не показываются в его операционном реестре.
Точечные изменения расписания
Учебный отдел может создать изменение конкретной пары:
CANCEL— отменить пару;MOVE— перенести пару на другую учебную дату того же семестра или другой фактический интервал;REPLACE— без изменения даты и времени заменить преподавателя, аудиторию или формат.
CANCEL не принимает целевую дату и новые ресурсы. Для MOVE обязателен новый временной
слот, для REPLACE — преподаватель, аудитория или формат. Перенос даты требует явно
выбранного слота эффективной сетки целевого дня. Дополнительные изменения преподавателя,
аудитории и формата можно объединить с MOVE, но основное действие должно фактически
менять свою часть пары. Формат ограничен значениями Очно и Онлайн; дисциплина и тип
занятия не изменяются.
Изменения не переписывают базовое правило, а накладываются поверх сгенерированного
расписания. Исходная пара всегда идентифицируется как baseRuleSlotId + lessonDate, а
targetLessonDate хранит только новую дату единственного занятия. Перед записью
ScheduleOverrideService:
- захватывает transaction advisory lock PostgreSQL для исходных и целевых дат в стабильном порядке;
- строит базовый день для всех групп без интерактивного лимита широкого поиска;
- доказывает существование исходной пары с учётом семестра, календарного графика, чётности, недели начала, лимита часов и lifecycle;
- для новой даты проверяет границы того же семестра, активность правила и дисциплины, учебный день всех затронутых групп и эффективную сетку времени;
- проверяет lifecycle итоговых преподавателя и аудитории на целевую дату;
- применяет сохранённые overrides и кандидат общей логикой
ScheduleQueryService; - проверяет полуоткрытые временные интервалы
[start, end)и итоговые ресурсы целевого дня; - сохраняет изменение только при отсутствии конфликта.
В поиске MOVE удаляет исходное вхождение и добавляет занятие в целевой день, включая
случай, когда в запрос попала только целевая дата. День недели, номер недели и чётность
вычисляются заново по семестру, а расход академических часов остаётся у исходной пары.
CANCEL скрывает занятие. Удаление override возвращает текущий вариант, который снова
формируется базовым правилом.
Совпадение преподавателя или аудитории в пересекающееся время всегда является конфликтом.
Для общей учебной группы занятие целой группы конфликтует с любой её подгруппой; разные
подгруппы одной группы могут идти параллельно при свободных преподавателях и аудиториях.
Конфликт возвращается как 409 Conflict с русским сообщением. Блокировка PostgreSQL общая
для backend-pod, поэтому два конкурентных переноса с разных исходных дат на одну целевую
дату и одинаковые ресурсы проверяются последовательно.
Отсутствия преподавателей и мастер замены
Отсутствие хранится отдельно от базовых правил расписания и проходит жизненный цикл
PENDING → APPROVED → RESOLVED; отклонённые и отменённые записи получают соответственно
REJECTED и CANCELLED. Преподаватель создаёт заявку только для себя со статусом
PENDING. Администратор, учебный отдел и кафедра могут регистрировать согласованное
отсутствие сразу. Кафедра видит и подтверждает только преподавателей, назначенных ей на
выбранный период через teacher_department_assignments.
Для согласованного отсутствия ScheduleQueryService строит фактические занятия
преподавателя в пределах периода. Мастер предлагает не более пяти проверенных вариантов
каждого типа:
- преподавателей со связью по дисциплине и допустимому типу занятия;
- ближайшие учебные даты вне периода отсутствия и эффективные временные слоты;
- активные свободные аудитории;
- отмену занятия или явное отклонение предложений.
Если у связи преподавателя с дисциплиной нет настроенных строк teacher_lesson_types, она
считается разрешающей все типы; при наличии настроек требуется точное совпадение типа.
Кандидаты-замены исключаются, если архивированы, заняты или сами отсутствуют в дату пары.
Каждый вариант до показа проходит read-only проверку ScheduleOverrideService, а выбранный
пакет повторно валидируется и сохраняется в одной транзакции. Поэтому время, аудитория,
группы и подгруппы проверяются тем же механизмом, что и ручные точечные изменения.
Применённые действия создают обычные schedule_overrides; базовое правило семестра не
изменяется. Таблица teacher_absence_decisions фиксирует как применённые, так и отклонённые
решения. Незаполненные строки мастер не меняет, и к ним можно вернуться позже.
Пожелания преподавателей на семестр
Преподаватель формирует набор пожеланий отдельно для каждого семестра. Интервальные записи привязаны к дню недели и паре базовой сетки времени, полная строгая недоступность — к конкретной дате семестра. Поддерживаются:
HARD_UNAVAILABLE— строго запрещённый интервал либо полностью недоступная дата;SOFT_PREFERREDиSOFT_UNWANTED— предпочтительный и нежелательный интервалы;CONSECUTIVEиNO_GAPS— пожелания к компактности расписания.
Запись преподавателя сначала имеет статус PENDING. Кафедра может рассматривать только
пожелания преподавателей, относившихся к ней в период семестра; учебный отдел и
администратор работают со всеми записями. Ответственный сотрудник также может сразу создать
согласованную запись. Отклонение требует комментария, а преподаватель может отозвать только
собственную ожидающую запись.
Только согласованные строгие ограничения влияют на валидацию. ScheduleRuleService
проверяет каждую активную неделю нового или изменённого правила, а ScheduleOverrideService
— фактическую дату результата разовой правки. Поэтому строгая недоступность одинаково
учитывается конструктором правил, мастером замены и заявками преподавателей. Мягкие
пожелания и компактность подсвечиваются в конструкторе, но не меняют опубликованное
расписание автоматически.
Заявки преподавателей на изменение занятия
Заявку можно создать только по собственному фактическому занятию, для которого ещё нет
разовой правки. Поддерживаются перенос даты/времени (MOVE), смена аудитории
(CHANGE_CLASSROOM) и отмена (CANCEL). Одновременно по одной паре допускается только одна
заявка PENDING.
До отправки интерфейс получает список ближайших учебных дат, временных слотов и аудиторий.
Каждый вариант проходит ScheduleOverrideService: проверяются принадлежность семестру,
календарный график групп, эффективная сетка времени, жизненный цикл ресурсов, пересечения
преподавателя, аудитории, групп и подгрупп, подтверждённые отсутствия и строгая
недоступность преподавателя. При создании заявки проверка выполняется повторно.
Кафедра видит заявки своих преподавателей, но применять изменение вправе только
ADMIN или EDUCATION_OFFICE. При одобрении в одной транзакции повторно проверяется и
создаётся обычный schedule_override, его ID сохраняется в заявке, а в неизменяемую
историю добавляется статус APPROVED. Отклонение требует комментария; преподаватель может
отозвать только собственную ожидающую заявку. История содержит автора, время, статус и
комментарий каждого перехода.
Анализ качества и локальная оптимизация расписания
Анализатор качества работает без собственной таблицы и не меняет правила расписания.
Пользователь выбирает конкретную версию семестра. Для PUBLISHED он строит фактические
занятия через ScheduleQueryService, поэтому в расчёт входят переносы, замены и отмены из
schedule_overrides. DRAFT и ARCHIVED генерируются напрямую по собственным правилам,
без точечных изменений текущей публикации. Семестр загружается частями не более 120 дней,
но оценка рассчитывается единообразно по всему периоду.
Итоговая оценка от 0 до 100 формируется из объяснимых штрафов:
- окна групп и преподавателей между занятиями одного дня;
- пятая и последующие пары группы или преподавателя за день;
- нехватка мест и заметно избыточная вместимость аудитории;
- занятие в подтверждённый строго недоступный или нежелательный интервал;
- занятие вне предпочтительного интервала преподавателя на выбранный день недели;
- нарушение пожеланий
NO_GAPSиCONSECUTIVE.
Неравномерность дневной нагрузки выводится отдельной диагностической метрикой в процентах и не добавляет скрытого штрафа к итоговой оценке.
Внутренне штраф рассчитывается для каждого фактического занятия, поэтому повторяющиеся пары одного правила увеличивают его вклад в оценку. Перед выдачей нарушения объединяются по правилу: одна карточка содержит суммарный штраф, число затронутых занятий, все типы и краткие причины. Это сохраняет воспроизводимость оценки, но направляет пользователя к изменению базового правила вместо ручного исправления каждой пары.
Для проблемы опубликованной версии помощник перебирает другие слоты эффективной сетки того же дня и
активные аудитории достаточной вместимости. Каждый вариант повторно проходит
ScheduleOverrideService, затем анализатор моделирует его влияние на общую оценку и
показывает улучшения и компромиссы. Занятия с ручным override считаются закреплёнными и не
получают рекомендаций. Применение возможно только после подтверждения пользователя через
обычный механизм schedule_overrides; автоматической публикации и полного solver в MVP
нет. Интерфейс анализа для опубликованной версии и черновика открывает выбранное правило
в конструкторе соответствующей версии; черновик остаётся изолированным до публикации.
Архив анализируется только для чтения. API локальных рекомендаций сохраняется для
совместимости и точечных сценариев, но реестр качества работает на уровне правил.
Привязка преподаватель ↔ дисциплина
Связь Many-to-Many через таблицу teacher_subjects:
- Указывается, какие дисциплины может вести конкретный преподаватель
- Дополнительные поля:
qualification_level,experience_years
Дополнительная связь через teacher_lesson_types:
- Определяет, какие типы занятий (лекция, практика, лаба) может вести преподаватель по конкретной дисциплине
Бизнес-правила (планируемые)
Примечание: Следующие правила описаны в требованиях, но пока не полностью реализованы в коде.
Проверка конфликтов
- Критический конфликт: Преподаватель не может одновременно находиться в двух разных аудиториях
- Исключение: Преподаватель может вести несколько пар одновременно (потоковая лекция), если все группы в одной аудитории
- Вместимость: Суммарная численность всех групп в слоте не должна превышать вместимость аудитории
Управление инцидентами
Регистрация отсутствий и Resolution Wizard реализованы. Дальнейшее развитие этого контура:
- автоматическая рассылка уведомлений группам и преподавателям;
- пакетная обработка нескольких одновременных отсутствий;
- ранжирование вариантов по окнам и дополнительной нагрузке.