179 lines
28 KiB
Markdown
179 lines
28 KiB
Markdown
# Концепция динамической генерации расписания
|
||
|
||
Данный документ представляет собой подробное архитектурное описание новой системы управления расписанием. Система переходит от статического хранения каждой отдельной пары к параметрическому: мы сохраняем **правила проведения** дисциплины и **календарную сетку**, а фактическое расписание на любую дату вычисляется «на лету» (генерируется).
|
||
|
||
> **Контекст миграции:** Новая система полностью заменяет существующие таблицы `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<RenderedLesson> buildScheduleForGroup(Long groupId, LocalDate startDate, LocalDate endDate)` — расписание группы.
|
||
* `List<RenderedLesson> 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) для Дня Недели, Временного слота, Подгруппы, Чётности, Преподавателя, Аудитории и Типа занятия.
|