# Концепция динамической генерации расписания Данный документ представляет собой подробное архитектурное описание новой системы управления расписанием. Система переходит от статического хранения каждой отдельной пары к параметрическому: мы сохраняем **правила проведения** дисциплины и **календарную сетку**, а фактическое расписание на любую дату вычисляется «на лету» (генерируется). > **Контекст миграции:** Новая система полностью заменяет существующие таблицы `lessons` (статическое расписание) и `schedule_data` (плановая нагрузка). Обе таблицы будут мигрированы в единую модель `schedule_rules` + `schedule_rule_slots`, которая совмещает хранение нагрузки (часы) и расписания (слоты) в одной структуре. --- ## 1. Подробное описание компонентов системы Новая архитектура строится на строгом разделении данных на три логических слоя: Календарь (основа отсчета времени), Правила (шаблоны занятий) и Генератор (движок рендеринга фактического расписания). ### 1.1 Справочная база времени (Календарный учебный график) Чтобы система понимала, *когда* можно ставить пары, а когда нет, вводится понятие календарного графика. Он состоит из трёх взаимосвязанных сущностей: * **Академические периоды (Учебные года и Семестры).** Иерархия из двух уровней: * **Учебный год** — контейнер с названием и датами (напр. «2024/2025», `01.09.2024` — `30.06.2025`). * **Семестр** — дочерняя сущность учебного года. Содержит дату начала, от которой отсчитывается «Неделя 1» данного семестра. Нумерация недель начинается заново для каждого семестра. Тип семестра (`autumn` / `spring`) определяет, какой набор правил активен. Именно от даты начала семестра отсчитывается «Неделя 1». Конвенция чётности (верхняя = чётная или нечётная) **настраивается на уровне тенанта**, так как у разных университетов разные традиции. Это избавляет систему от уязвимостей, связанных с плавающими днями начала учёбы, високосными годами и смещениями дней недели. * **Справочник исключений (Праздники и Выходные).** В этой таблице хранятся конкретные даты `YYYY-MM-DD`, когда университет юридически или физически закрыт (например, государственные праздники). Если по правилу пара должна быть в этот день, алгоритм будет знать, что его нужно пропустить без штрафов и ошибок. * **Матрица учебного графика.** Это цифровая копия эксель-таблицы (`Курс + Специальность` → `Номер недели` → `Тип деятельности`). Привязка идёт к `course_number` + `specialty_id`, а **не** к конкретной группе, так как учебный график одинаков для всех групп одного курса одной специальности. Номер текущего курса группы вычисляется из поля `year_start_study` модели `StudentGroup` относительно текущей даты по формуле: `course = текущий_учебный_год - year_start_study + 1`. Типы деятельности включают `THEORY` (Теория, пары идут в штатном режиме), `EXAM` (Э — экзаменационная сессия), `VACATION` (К — каникулы), `PRACTICE` (У, П — практика). Если, например, у 3-го курса на 18-й неделе стоит статус `EXAM`, алгоритм даже не будет пытаться генерировать для них теоретические лекции, а отобразит блок «Экзаменационная сессия». ### 1.2 Справочник временных слотов (Time Slots) Вместо хардкода фиксированных 7 пар, система хранит временные слоты в отдельной **настраиваемой таблице**. Каждый тенант (университет) может иметь собственное количество пар, их длительность и временные рамки. Слот содержит: * `order_number` — порядковый номер пары в дне (1, 2, 3...). * `start_time` — время начала (напр. `08:00`). * `end_time` — время окончания (напр. `09:30`). * `duration_minutes` — длительность пары в минутах. Это позволяет каждому университету настраивать количество и продолжительность пар без модификации кода. ### 1.3 Движок правил (Schedule Rules) Старый подход подразумевал, что каждая пара в базе (каждая клеточка) — это изолированная запись `lessons` («понедельник, 1-я пара, математика»). Новая система вводит сущность сводного **Правила Дисциплины**. Одно правило описывает расписание целого курса по конкретному предмету для одной или нескольких студенческих групп (включая потоковые лекции). **Базовые параметры (Лимиты Правила):** * `subject_id` — ID преподаваемой дисциплины. * `semester_id` — ID семестра, к которому привязано правило. Одна и та же дисциплина может читаться в разных семестрах с разными параметрами. * `startDate` — Дата или номер недели семестра, с которой предмет начинает читаться (поскольку не все предметы идут строго с 1-й недели семестра). * `totalHours` — Полный объём выделенных **академических часов** (1 ак. час = 45 минут; одна пара = 2 ак. часа). Это важнейший **лимитатор**, который обеспечивает автоматическую остановку генерации: как только заявленные часы будут вычитаны, предмет перестает отображаться в расписании студентов на последующих неделях. **Связь с группами (Many-to-Many):** Одно правило может быть связано с несколькими группами через промежуточную таблицу `schedule_rule_groups`. Это обеспечивает поддержку **потоковых лекций** — когда один преподаватель читает лекцию нескольким группам одновременно в одной аудитории. При этом правило создаётся один раз, а группы к нему привязываются списком. **Массив паттернов (Слоты правила):** Само «тело» правила разбивается на подчинённые слоты. Если предмет идёт в Пн и Ср, это будет 2 слота внутри одного Правила. Слот содержит: * `dayOfWeek`: день недели (1–7, Пн–Вс). * `parity`: тип четности — `ENUM('BOTH', 'EVEN', 'ODD')`. `BOTH` — каждую неделю, `EVEN` — по чётным (нижним) неделям, `ODD` — по нечётным (верхним). Конкретное соответствие «чётная = верхняя или нижняя» определяется настройкой тенанта. * `time_slot_id`: FK на таблицу `time_slots` — порядковый номер и время пары. * `subgroup_id`: FK на подгруппу (NULL = вся группа). *Это гарантирует, что мы сможем ставить разным подгруппам пересекающиеся занятия в разных аудиториях без алгоритмических конфликтов.* * `teacher_id`: FK на преподавателя слота. * `classroom_id`: FK на аудиторию слота. * `lesson_type_id`: FK на тип занятия (`Лекция`, `Практическая работа`, `Лабораторная работа`). * `lesson_format`: формат проведения (`Очно` / `Онлайн`). > **Обоснование:** Хранение `teacher_id`, `classroom_id`, `lesson_type_id` и `lesson_format` в **слотах**, а не в главном правиле, позволяет гибко описывать ситуации вроде: лекции в понедельник читает лектор Иванов (Аудитория 100), а лабораторные в среду ведёт практик Петров (Аудитория 102В) — в рамках одного правила по предмету «Программирование», расходуя общий `totalHours`. ### 1.4 Генератор (Рендерер) расписания Это слой бизнес-логики (служба `ScheduleGeneratorService` в Java), который работает исключительно в оперативной памяти бэкенда и производит расчёт расписания «on-demand» (по требованию) при запросе от клиента фронтенда. **Пошаговый алгоритм работы генератора:** 1. Фронтенд (Интерфейс пользователя) запрашивает: *«Дай мне расписание группы ИТ-21 на конкретный период, например, с 14 октября по 20 октября»*. 2. Генератор определяет семестр по запрошенным датам и вычисляет, что 14 октября соответствует, к примеру, 7-й неделе семестра (вычисление от `startDate` семестра). 3. Он сверяется с *Матрицей учебного графика*. Для этого генератор определяет текущий курс группы по формуле `текущий_учебный_год - year_start_study + 1` и находит `specialty_id` группы. Если у данного курса/специальности сейчас стоит `VACATION` (Каникулы) или `PRACTICE` (Практика), генератор сразу возвращает пустой ответ или ответ со статусом периода. 4. Если статус недели позволяет проводить занятия (`THEORY`), генератор поднимает из Базы Данных все активные **Правила** для запрошенной группы (через таблицу `schedule_rule_groups`), привязанные к текущему семестру. 5. **Механика Лимитатора часов:** Для каждого правила алгоритм «симулирует» прогон времени с даты старта правила до текущей запрошенной недели. Он подсчитывает количество успешно проведённых ак. часов (по 2 ак. часа за каждый отработанный слот), пропуская даты, попавшие в справочник праздников, и недели с типом деятельности отличным от `THEORY`. 6. Если у правила лимит `totalHours` достиг значения `0`, программа понимает, что курс вычитан, и предмет не отображается. Если часы ещё остались, алгоритм проецирует шаблоны (слоты правила) на запрошенную текущую неделю с учётом чётности, аудиторий и подгрупп, отдавая готовый JSON-массив в браузер пользователя. **Генерация расписания для преподавателя:** Аналогричный алгоритм, но поиск правил идёт не по привязке к группе, а по `teacher_id` в слотах. Генератор собирает все `schedule_rule_slots`, где `teacher_id` = ID текущего преподавателя, получает родительские правила и рендерит расписание, обогащая каждую запись списком групп из `schedule_rule_groups`. **Кеширование:** Для оптимизации производительности (т.к. симуляция прогона за весь семестр для каждого запроса ресурсоёмка) предусмотрен кеш: * Список праздников текущего учебного года кешируется при первом обращении и инвалидируется при изменении таблицы `holidays`. * Матрица учебного графика кешируется по ключу `(course, specialty_id, semester_id)`. * Результаты подсчёта `consumed_hours` для каждого правила могут кешироваться с инвалидацией при изменении праздников или правил. --- ## 2. Архитектурные Решения На основе обсуждений были задокументированы следующие концептуальные решения по архитектуре: 1. **Реакция на праздники (Продление курса):** Алгоритм воспринимает праздник как «пропуск хода», не отнимая проведённые часы от `totalHours`. Это означает, что пара **не переносится** на другой день или время — она просто пропускается без вычета часов. Фактически предмет будет отображаться в расписании дольше (больше недель), пока `totalHours` не будет полностью исчерпан. Преподаватель честно выработает положенный объём часов за счёт увеличения количества недель преподавания. 2. **Нормализация через связанные таблицы:** Мы не используем сырые массивы (`INTEGER[]`) или JSONB-колонки. Реализована структура со строгой нормализацией: * Главная таблица: `schedule_rules` (хранит лимиты и дату старта). * Подчинённая таблица: `schedule_rule_slots` (хранит конкретный день, чётность, номер пары, преподавателя, аудиторию, тип и формат — прикреплённые к ID главного правила через Foreign Key). * Связующая таблица: `schedule_rule_groups` (Many-to-Many между правилом и группами). Это позволяет базе данных строить сложные выборки в стиле «Покажи загруженность кабинета №21 во вторник на второй паре по чётным неделям», исключая тяжёлый парсинг JSON. 3. **Поддержка подгрупп внутри слотов:** В таблицу `schedule_rule_slots` введено поле `subgroup_id` (Id подгруппы, nullable). Алгоритм генератора сможет рендерить два предмета для одной группы одновременно и без конфликтов, если они ассоциированы с разными подгруппами одной материнской группы. 4. **Обогащённые слоты (Вариант Б):** `teacher_id`, `classroom_id`, `lesson_type_id` и `lesson_format` хранятся в каждой строке `schedule_rule_slots`, а не в главном правиле. Это позволяет описывать лекции и практики одного предмета в рамках одного правила, расходуя общий `totalHours`. 5. **Потоковые лекции через Many-to-Many:** Одно правило связывается с несколькими группами через `schedule_rule_groups`. Для потоковой лекции создаётся одно правило, к которому привязываются все участвующие группы. 6. **Настраиваемость по тенантам:** Архитектурно все тенанты одинаковы — каждый университет получает идентичную пустую базу данных. Временные слоты (количество, длительность, время начала/окончания пар), конвенция чётности и прочие параметры не требуют специального механизма: каждый университет просто заполняет свою БД самостоятельно через панель администратора. --- ## 3. Подробный План Действий по Реализации Интеграция новой архитектуры затронет весь стек приложения (DB → Backend → API → Frontend). Работу предлагается вести строго поэтапно: ### Этап 1. База Данных (Flyway Миграции) **Схема Временных слотов:** * `time_slots` (id, order_number, start_time TIME, end_time TIME, duration_minutes INT). * Заполняется администратором. Нет фиксированных значений — каждый тенант настраивает свою сетку пар. **Схема Календарного графика:** * `academic_years` (id, title VARCHAR, start_date DATE, end_date DATE). * `semesters` (id, academic_year_id FK, semester_type ENUM('autumn','spring'), start_date DATE, end_date DATE). * Именно от `semesters.start_date` отсчитывается «Неделя 1». * `holidays` (id, date DATE, academic_year_id FK, description VARCHAR). * `academic_calendar_matrix` (id, semester_id FK, course_number INT, specialty_id FK, week_number INT, activity_type ENUM('THEORY','EXAM','VACATION','PRACTICE')). * Привязка к `course_number` + `specialty_id`, а НЕ к конкретной группе. **Схема Движка Правил:** * `schedule_rules` (id, subject_id FK, semester_id FK, active_from_date DATE, total_academic_hours INT). * `total_academic_hours` — в академических часах (1 ак. час = 45 мин, одна пара = 2 ак. часа). * `schedule_rule_groups` (schedule_rule_id FK, group_id FK) — PK составной. * Связующая таблица для потоковых лекций. * `schedule_rule_slots` (id, schedule_rule_id FK, day_of_week INT CHECK(1–7), parity ENUM('BOTH','EVEN','ODD'), time_slot_id FK, subgroup_id FK NULL, teacher_id FK, classroom_id FK, lesson_type_id FK, lesson_format VARCHAR). **Скрипт Миграции (Data ETL):** Написание SQL/Java скрипта для миграции данных из двух источников: 1. **Из `schedule_data`** → `schedule_rules` + `schedule_rule_groups`: перенос плановой нагрузки (`number_of_hours` → `total_academic_hours`, `group_id`, `subjects_id`, `teacher_id`, `lesson_type_id`, `is_division`, `semester_type`, `period`). 2. **Из `lessons`** → `schedule_rule_slots`: перенос расписания с трансформацией данных: * `day` (строка «Понедельник»...«Суббота») → `day_of_week` (INT 1–6). * `time` (строка «8:00 - 9:30») → `time_slot_id` (FK на `time_slots`). * `week` (строка «Верхняя»/«Нижняя»/«Обе») → `parity` (ENUM `ODD`/`EVEN`/`BOTH`). * Группировка записей с одинаковым `(subject_id, group_id)` в одно правило. После успешной миграции и верификации данных — удаление таблиц `lessons` и `schedule_data`. ### Этап 2. Бэкенд и Вычислительное Ядро (Java + Spring Boot) * `AcademicDateService.java` — сервис утилит для календарной математики: * Перевод дат в номер недели семестра. * Определение чётности недели с учётом настройки тенанта. * Проверка попадания дня в справочник `holidays`. * Вычисление текущего курса группы: `текущий_учебный_год - year_start_study + 1`. * `ScheduleRuleRepository.java` — JPA репозитории для извлечения графа правил из базы данных, с оптимизацией N+1 проблемы через `JOIN FETCH` со слотами и группами. * `ScheduleGeneratorService.java` — Сердце системы. Основные методы: * `List buildScheduleForGroup(Long groupId, LocalDate startDate, LocalDate endDate)` — расписание группы. * `List buildScheduleForTeacher(Long teacherId, LocalDate startDate, LocalDate endDate)` — расписание преподавателя (поиск по `teacher_id` в слотах, обогащение информацией о группах). * Реализует всю бизнес-логику из пункта 1.4 (подсчёт вычитанных часов, пропуск праздников, кеширование). * Адаптация валидаторов пересечения аудиторий: теперь валидатор должен работать не на уровне «каждой пары», а симулировать весь семестр на этапе сохранения нового Правила в панели администратора. ### Этап 3. Обновление REST API (Контроллеры) * **Новый эндпоинт расписания:** `GET /api/schedule` переходит на диапазонную модель. Параметры: `?groupId=123&startDate=2024-10-14&endDate=2024-10-20` или `?teacherId=456&startDate=...&endDate=...`. Ответ — массив объектов с полными датами `YYYY-MM-DD`. * **Обратная совместимость:** Старый эндпоинт `GET /api/users/lessons` будет помечен как `@Deprecated` и продолжит работать до полной миграции фронтенда. После завершения миграции фронтенда — удаление. * **CRUD-контроллеры для админки:** * `/api/admin/time-slots` (настройка сетки временных слотов). * `/api/admin/calendar/years` (учебные годы и семестры). * `/api/admin/calendar/matrix` (настройка каникул и сессий по курсам/специальностям/неделям). * `/api/admin/calendar/holidays` (добавление исключений). * `/api/admin/schedule-rules` (управление жизненным циклом Правил, их слотами и привязкой к группам). ### Этап 4. Интерфейсы Frontend (Vanilla JS + HTML) * **Страницы просмотра (Студенты и Преподаватели):** * Реализация переключателя календарных дат (Date Picker или кнопки-перелистывания недель). * Логика, которая при свайпе или клике запрашивает у API конкретный диапазон дат и перерисовывает DOM-дерево. * Для преподавателей — отображение всех групп, привязанных к каждому занятию. * **Панель Администратора (SPA-интерфейсы):** * **Вкладка «Временные слоты»:** Настройка сетки пар — количество, время начала/окончания, длительность. * **Вкладка «Учебный график»:** Визуальная сетка-матрица (недели по горизонтали, Курсы/Специальности по вертикали), где админ может закрашивать пересечения разными цветами, назначая статусы (Практика, Каникулы, Теория, Экзамены). * **Вкладка «Конструктор Правил»:** Глобально новый визуальный инструмент расписания. Админ выбирает Группы (одну или несколько для потока) и Дисциплину, задаёт `totalHours` в академических часах, а затем динамически добавляет строчки массива слотов через кнопку «Добавить занятие» со списками (Selects) для Дня Недели, Временного слота, Подгруппы, Чётности, Преподавателя, Аудитории и Типа занятия.