28 KiB
Концепция динамической генерации расписания
Данный документ представляет собой подробное архитектурное описание новой системы управления расписанием. Система переходит от статического хранения каждой отдельной пары к параметрическому: мы сохраняем правила проведения дисциплины и календарную сетку, а фактическое расписание на любую дату вычисляется «на лету» (генерируется).
Контекст миграции: Новая система полностью заменяет существующие таблицы
lessons(статическое расписание) иschedule_data(плановая нагрузка). Обе таблицы будут мигрированы в единую модельschedule_rules+schedule_rule_slots, которая совмещает хранение нагрузки (часы) и расписания (слоты) в одной структуре.
1. Подробное описание компонентов системы
Новая архитектура строится на строгом разделении данных на три логических слоя: Календарь (основа отсчета времени), Правила (шаблоны занятий) и Генератор (движок рендеринга фактического расписания).
1.1 Справочная база времени (Календарный учебный график)
Чтобы система понимала, когда можно ставить пары, а когда нет, вводится понятие календарного графика. Он состоит из трёх взаимосвязанных сущностей:
-
Академические периоды (Учебные года и Семестры). Иерархия из двух уровней:
- Учебный год — контейнер с названием и датами (напр. «2024/2025»,
01.09.2024—30.06.2025). - Семестр — дочерняя сущность учебного года. Содержит дату начала, от которой отсчитывается «Неделя 1» данного семестра. Нумерация недель начинается заново для каждого семестра. Тип семестра (
autumn/spring) определяет, какой набор правил активен.
Именно от даты начала семестра отсчитывается «Неделя 1». Конвенция чётности (верхняя = чётная или нечётная) настраивается на уровне тенанта, так как у разных университетов разные традиции. Это избавляет систему от уязвимостей, связанных с плавающими днями начала учёбы, високосными годами и смещениями дней недели.
- Учебный год — контейнер с названием и датами (напр. «2024/2025»,
-
Справочник исключений (Праздники и Выходные). В этой таблице хранятся конкретные даты
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» (по требованию) при запросе от клиента фронтенда.
Пошаговый алгоритм работы генератора:
- Фронтенд (Интерфейс пользователя) запрашивает: «Дай мне расписание группы ИТ-21 на конкретный период, например, с 14 октября по 20 октября».
- Генератор определяет семестр по запрошенным датам и вычисляет, что 14 октября соответствует, к примеру, 7-й неделе семестра (вычисление от
startDateсеместра). - Он сверяется с Матрицей учебного графика. Для этого генератор определяет текущий курс группы по формуле
текущий_учебный_год - year_start_study + 1и находитspecialty_idгруппы. Если у данного курса/специальности сейчас стоитVACATION(Каникулы) илиPRACTICE(Практика), генератор сразу возвращает пустой ответ или ответ со статусом периода. - Если статус недели позволяет проводить занятия (
THEORY), генератор поднимает из Базы Данных все активные Правила для запрошенной группы (через таблицуschedule_rule_groups), привязанные к текущему семестру. - Механика Лимитатора часов: Для каждого правила алгоритм «симулирует» прогон времени с даты старта правила до текущей запрошенной недели. Он подсчитывает количество успешно проведённых ак. часов (по 2 ак. часа за каждый отработанный слот), пропуская даты, попавшие в справочник праздников, и недели с типом деятельности отличным от
THEORY. - Если у правила лимит
totalHoursдостиг значения0, программа понимает, что курс вычитан, и предмет не отображается. Если часы ещё остались, алгоритм проецирует шаблоны (слоты правила) на запрошенную текущую неделю с учётом чётности, аудиторий и подгрупп, отдавая готовый JSON-массив в браузер пользователя.
Генерация расписания для преподавателя:
Аналогричный алгоритм, но поиск правил идёт не по привязке к группе, а по teacher_id в слотах. Генератор собирает все schedule_rule_slots, где teacher_id = ID текущего преподавателя, получает родительские правила и рендерит расписание, обогащая каждую запись списком групп из schedule_rule_groups.
Кеширование: Для оптимизации производительности (т.к. симуляция прогона за весь семестр для каждого запроса ресурсоёмка) предусмотрен кеш:
- Список праздников текущего учебного года кешируется при первом обращении и инвалидируется при изменении таблицы
holidays. - Матрица учебного графика кешируется по ключу
(course, specialty_id, semester_id). - Результаты подсчёта
consumed_hoursдля каждого правила могут кешироваться с инвалидацией при изменении праздников или правил.
2. Архитектурные Решения
На основе обсуждений были задокументированы следующие концептуальные решения по архитектуре:
-
Реакция на праздники (Продление курса): Алгоритм воспринимает праздник как «пропуск хода», не отнимая проведённые часы от
totalHours. Это означает, что пара не переносится на другой день или время — она просто пропускается без вычета часов. Фактически предмет будет отображаться в расписании дольше (больше недель), покаtotalHoursне будет полностью исчерпан. Преподаватель честно выработает положенный объём часов за счёт увеличения количества недель преподавания. -
Нормализация через связанные таблицы: Мы не используем сырые массивы (
INTEGER[]) или JSONB-колонки. Реализована структура со строгой нормализацией:- Главная таблица:
schedule_rules(хранит лимиты и дату старта). - Подчинённая таблица:
schedule_rule_slots(хранит конкретный день, чётность, номер пары, преподавателя, аудиторию, тип и формат — прикреплённые к ID главного правила через Foreign Key). - Связующая таблица:
schedule_rule_groups(Many-to-Many между правилом и группами). Это позволяет базе данных строить сложные выборки в стиле «Покажи загруженность кабинета №21 во вторник на второй паре по чётным неделям», исключая тяжёлый парсинг JSON.
- Главная таблица:
-
Поддержка подгрупп внутри слотов: В таблицу
schedule_rule_slotsвведено полеsubgroup_id(Id подгруппы, nullable). Алгоритм генератора сможет рендерить два предмета для одной группы одновременно и без конфликтов, если они ассоциированы с разными подгруппами одной материнской группы. -
Обогащённые слоты (Вариант Б):
teacher_id,classroom_id,lesson_type_idиlesson_formatхранятся в каждой строкеschedule_rule_slots, а не в главном правиле. Это позволяет описывать лекции и практики одного предмета в рамках одного правила, расходуя общийtotalHours. -
Потоковые лекции через Many-to-Many: Одно правило связывается с несколькими группами через
schedule_rule_groups. Для потоковой лекции создаётся одно правило, к которому привязываются все участвующие группы. -
Настраиваемость по тенантам: Архитектурно все тенанты одинаковы — каждый университет получает идентичную пустую базу данных. Временные слоты (количество, длительность, время начала/окончания пар), конвенция чётности и прочие параметры не требуют специального механизма: каждый университет просто заполняет свою БД самостоятельно через панель администратора.
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 скрипта для миграции данных из двух источников:
- Из
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). - Из
lessons→schedule_rule_slots: перенос расписания с трансформацией данных:day(строка «Понедельник»...«Суббота») →day_of_week(INT 1–6).time(строка «8:00 - 9:30») →time_slot_id(FK наtime_slots).week(строка «Верхняя»/«Нижняя»/«Обе») →parity(ENUMODD/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) для Дня Недели, Временного слота, Подгруппы, Чётности, Преподавателя, Аудитории и Типа занятия.