Files
magistr/docs/DATABASE.md
2026-07-13 03:28:18 +03:00

41 KiB
Raw Blame History

🗄 База данных

Общая информация

  • СУБД: PostgreSQL (локально postgres:alpine3.23, продакшн — managed PostgreSQL)
  • Управление схемой: Flyway (программный запуск)
  • Hibernate DDL: Отключён (ddl-auto=none)
  • Расширения: pgcrypto (bcrypt-хеширование паролей)
  • Мультитенантность: Каждый тенант = отдельная БД

ER-диаграмма

erDiagram
    departments {
        BIGSERIAL id PK
        VARCHAR name
        BIGINT code UK
    }

    specialties {
        BIGSERIAL id PK
        VARCHAR name
        VARCHAR specialty_code UK
    }

    specialty_profiles {
        BIGSERIAL id PK
        BIGINT specialty_id FK
        VARCHAR name
        TEXT description
    }
    
    users {
        BIGSERIAL id PK
        VARCHAR username UK
        VARCHAR password
        VARCHAR role
        VARCHAR full_name
        VARCHAR job_title
        BIGINT department_id FK
        VARCHAR status
        DATE active_from
        DATE active_to
        TIMESTAMP archived_at
        TIMESTAMP created_at
        TIMESTAMP updated_at
    }

    auth_refresh_tokens {
        BIGSERIAL id PK
        BIGINT user_id FK
        VARCHAR tenant
        VARCHAR token_hash UK
        TIMESTAMP issued_at
        TIMESTAMP expires_at
        TIMESTAMP revoked_at
        VARCHAR rotated_to_token_hash
    }
    
    education_forms {
        BIGSERIAL id PK
        VARCHAR name UK
        TEXT description
        TIMESTAMP created_at
    }
    
    student_groups {
        BIGSERIAL id PK
        VARCHAR name
        BIGINT group_size
        BIGINT education_form_id FK
        BIGINT department_id FK
        BIGINT specialty_id FK
        BIGINT specialty_profile_id FK
        BIGINT year_start_study
        TIMESTAMP created_at
    }
    
    subgroups {
        BIGSERIAL id PK
        BIGINT group_id FK
        VARCHAR name
        INT student_capacity
    }
    
    subjects {
        BIGSERIAL id PK
        VARCHAR name UK
        VARCHAR code
        BIGINT department_id FK
        TEXT description
        TIMESTAMP created_at
    }
    
    lesson_types {
        BIGSERIAL id PK
        VARCHAR name UK
        VARCHAR color_code
        INT duration_minutes
    }
    
    equipments {
        BIGSERIAL id PK
        VARCHAR name UK
        TEXT description
        VARCHAR inventory_number
    }
    
    classrooms {
        BIGSERIAL id PK
        VARCHAR name UK
        INT capacity
        VARCHAR building
        INT floor
        BOOLEAN is_available
        VARCHAR status
        DATE active_from
        DATE active_to
        TIMESTAMP archived_at
        TEXT description
        TIMESTAMP created_at
    }
    
    classroom_equipments {
        BIGINT classroom_id FK,PK
        BIGINT equipment_id FK,PK
        INT quantity
        TEXT notes
    }
    
    teacher_subjects {
        BIGINT user_id FK,PK
        BIGINT subject_id FK,PK
        VARCHAR qualification_level
        INT experience_years
    }

    teacher_department_assignments {
        BIGSERIAL id PK
        BIGINT teacher_id FK
        BIGINT department_id FK
        DATE valid_from
        DATE valid_to
        BOOLEAN is_primary
        TEXT comment
    }

    teacher_creation_requests {
        BIGSERIAL id PK
        BIGINT department_id FK
        VARCHAR username
        VARCHAR full_name
        VARCHAR job_title
        TEXT comment
        VARCHAR status
        BIGINT requested_by FK
        BIGINT reviewed_by FK
        TIMESTAMP reviewed_at
        TEXT review_comment
        BIGINT created_teacher_id FK
    }

    subject_comments {
        BIGSERIAL id PK
        BIGINT subject_id FK
        BIGINT author_id FK
        TEXT comment
        TIMESTAMP created_at
    }
    
    teacher_lesson_types {
        BIGINT user_id FK,PK
        BIGINT subject_id FK,PK
        BIGINT lesson_type_id FK,PK
    }
    
    time_slot_scopes {
        BIGSERIAL id PK
        VARCHAR code UK
        VARCHAR name
        VARCHAR apply_mode
        INT day_of_week
        BOOLEAN system_scope
        INT display_order
    }

    time_slots {
        BIGSERIAL id PK
        BIGINT time_slot_scope_id FK
        INT order_number
        TIME start_time
        TIME end_time
        INT duration_minutes
    }

    time_slot_date_assignments {
        BIGSERIAL id PK
        DATE assignment_date UK
        BIGINT time_slot_scope_id FK
    }

    academic_years {
        BIGSERIAL id PK
        VARCHAR title UK
        DATE start_date
        DATE end_date
    }

    semesters {
        BIGSERIAL id PK
        BIGINT academic_year_id FK
        VARCHAR semester_type
        DATE start_date
        DATE end_date
    }

    academic_calendar_activity_types {
        BIGSERIAL id PK
        VARCHAR code UK
        VARCHAR name
        BOOLEAN allow_schedule
        VARCHAR color_code
        INT display_order
    }

    academic_calendars {
        BIGSERIAL id PK
        VARCHAR title
        BIGINT academic_year_id FK
        BIGINT specialty_id FK
        BIGINT specialty_profile_id FK
        BIGINT study_form_id FK
        INT course_count
        TIMESTAMP created_at
        TIMESTAMP updated_at
    }

    academic_calendar_days {
        BIGSERIAL id PK
        BIGINT calendar_id FK
        INT course_number
        DATE date
        INT week_number
        INT day_of_week
        BIGINT activity_type_id FK
    }

    academic_calendar_subjects {
        BIGSERIAL id PK
        BIGINT calendar_id FK
        INT semester_number
        BIGINT subject_id FK
        TIMESTAMP created_at
    }

    student_group_calendar_assignments {
        BIGSERIAL id PK
        BIGINT group_id FK
        BIGINT academic_year_id FK
        BIGINT calendar_id FK
    }

    schedule_rules {
        BIGSERIAL id PK
        BIGINT subject_id FK
        BIGINT semester_id FK
        VARCHAR status
        DATE valid_from
        DATE valid_to
        INT lecture_academic_hours
        INT laboratory_academic_hours
        INT practice_academic_hours
        INT lecture_start_week
        INT laboratory_start_week
        INT practice_start_week
    }

    schedule_rule_groups {
        BIGINT schedule_rule_id FK,PK
        BIGINT group_id FK,PK
    }

    schedule_rule_slots {
        BIGSERIAL id PK
        BIGINT schedule_rule_id FK
        INT day_of_week
        VARCHAR parity
        BIGINT time_slot_id FK
        BIGINT teacher_id FK
        BIGINT classroom_id FK
        BIGINT lesson_type_id FK
        VARCHAR lesson_format
        BOOLEAN time_locked
        BOOLEAN classroom_locked
        BOOLEAN teacher_locked
    }

    schedule_overrides {
        BIGSERIAL id PK
        BIGINT base_rule_slot_id FK
        DATE lesson_date
        VARCHAR action
        BIGINT new_time_slot_id FK
        BIGINT new_classroom_id FK
        BIGINT new_teacher_id FK
        VARCHAR new_lesson_format
        TEXT comment
    }

    schedule_rule_slot_subgroups {
        BIGINT schedule_rule_slot_id FK,PK
        BIGINT subgroup_id FK,PK
    }
    
    departments ||--o{ users : "department_id"
    departments ||--o{ student_groups : "department_id"
    departments ||--o{ subjects : "department_id"
    education_forms ||--o{ student_groups : "education_form_id"
    specialties ||--o{ specialty_profiles : "specialty_id"
    specialties ||--o{ student_groups : "specialty_id"
    specialty_profiles ||--o{ student_groups : "specialty_profile_id"
    student_groups ||--o{ subgroups : "group_id"
    student_groups ||--o{ schedule_rule_groups : "group_id"
    student_groups ||--o{ student_group_calendar_assignments : "group_id"
    users ||--o{ teacher_subjects : "user_id"
    users ||--o{ auth_refresh_tokens : "user_id"
    users ||--o{ teacher_department_assignments : "teacher_id"
    departments ||--o{ teacher_department_assignments : "department_id"
    departments ||--o{ teacher_creation_requests : "department_id"
    users ||--o{ teacher_creation_requests : "requested_by/reviewed_by/created_teacher_id"
    users ||--o{ teacher_lesson_types : "user_id"
    users ||--o{ subject_comments : "author_id"
    subjects ||--o{ teacher_subjects : "subject_id"
    subjects ||--o{ subject_comments : "subject_id"
    subjects ||--o{ teacher_lesson_types : "subject_id"
    subjects ||--o{ academic_calendar_subjects : "subject_id"
    subjects ||--o{ schedule_rules : "subject_id"
    lesson_types ||--o{ teacher_lesson_types : "lesson_type_id"
    lesson_types ||--o{ schedule_rule_slots : "lesson_type_id"
    classrooms ||--o{ schedule_rule_slots : "classroom_id"
    classrooms ||--o{ classroom_equipments : "classroom_id"
    equipments ||--o{ classroom_equipments : "equipment_id"
    academic_years ||--o{ semesters : "academic_year_id"
    academic_years ||--o{ academic_calendars : "academic_year_id"
    academic_years ||--o{ student_group_calendar_assignments : "academic_year_id"
    semesters ||--o{ schedule_rules : "semester_id"
    specialties ||--o{ academic_calendars : "specialty_id"
    specialty_profiles ||--o{ academic_calendars : "specialty_profile_id"
    education_forms ||--o{ academic_calendars : "study_form_id"
    academic_calendars ||--o{ academic_calendar_days : "calendar_id"
    academic_calendars ||--o{ academic_calendar_subjects : "calendar_id"
    academic_calendars ||--o{ student_group_calendar_assignments : "calendar_id"
    academic_calendar_activity_types ||--o{ academic_calendar_days : "activity_type_id"
    schedule_rules ||--o{ schedule_rule_groups : "schedule_rule_id"
    schedule_rules ||--o{ schedule_rule_slots : "schedule_rule_id"
    schedule_rule_slots ||--o{ schedule_rule_slot_subgroups : "schedule_rule_slot_id"
    schedule_rule_slots ||--o{ schedule_overrides : "base_rule_slot_id"
    time_slots ||--o{ schedule_overrides : "new_time_slot_id"
    classrooms ||--o{ schedule_overrides : "new_classroom_id"
    users ||--o{ schedule_overrides : "new_teacher_id"
    time_slot_scopes ||--o{ time_slots : "time_slot_scope_id"
    time_slot_scopes ||--o{ time_slot_date_assignments : "time_slot_scope_id"
    time_slots ||--o{ schedule_rule_slots : "time_slot_id"
    subgroups ||--o{ schedule_rule_slot_subgroups : "subgroup_id"
    users ||--o{ schedule_rule_slots : "teacher_id"

Описание таблиц

Справочники высшего уровня

departments — Кафедры

Колонка Тип Описание
id BIGSERIAL PK ID кафедры
name VARCHAR(255) Название кафедры
code BIGINT UNIQUE Код кафедры

specialties — Специальности

Колонка Тип Описание
id BIGSERIAL PK ID специальности
name VARCHAR(255) Название специальности
specialty_code VARCHAR(255) UNIQUE Код ФГОС (напр. 10.03.01)

specialty_profiles — Профили обучения

Колонка Тип Описание
id BIGSERIAL PK ID профиля
specialty_id BIGINT FK → specialties Специальность, внутри которой существует профиль
name VARCHAR(255) Название профиля, уникально внутри специальности
description TEXT Описание профиля

Пользователи

users — Пользователи системы

Колонка Тип Описание
id BIGSERIAL PK ID пользователя
username VARCHAR(50) UNIQUE Логин
password VARCHAR(255) bcrypt-хеш пароля
role VARCHAR(20) ADMIN, TEACHER, STUDENT
full_name VARCHAR(255) ФИО
job_title VARCHAR(255) Должность
department_id BIGINT FK → departments Кафедра
status VARCHAR(20) ACTIVE или ARCHIVED; архивный пользователь не может войти
active_from DATE Дата начала действия записи
active_to DATE Дата окончания действия записи
archived_at TIMESTAMP Когда пользователь архивирован
archive_reason TEXT Причина архивирования
created_at TIMESTAMP Дата создания
updated_at TIMESTAMP Дата обновления (авто-триггер)

Триггер: update_users_updated_at автоматически обновляет updated_at при любом UPDATE.

auth_refresh_tokens — Refresh-сессии JWT

Колонка Тип Описание
id BIGSERIAL PK ID refresh-сессии
user_id BIGINT FK → users (CASCADE) Пользователь
tenant VARCHAR(100) Тенант, для которого выдан refresh-токен
token_hash VARCHAR(64) UNIQUE SHA-256 хэш refresh-токена
issued_at TIMESTAMP Дата выдачи
expires_at TIMESTAMP Дата истечения
revoked_at TIMESTAMP Дата отзыва, NULL для активной сессии
rotated_to_token_hash VARCHAR(64) Хэш следующего refresh-токена после ротации
user_agent VARCHAR(512) User-Agent клиента
ip_address VARCHAR(64) IP-адрес клиента

Сырой refresh-токен никогда не хранится в БД. При каждом POST /api/auth/refresh старый refresh-токен отзывается, а клиент получает новый refresh-cookie.

Учебный процесс

education_forms — Формы обучения

Колонка Тип Описание
id BIGSERIAL PK ID
name VARCHAR(100) UNIQUE Название (Бакалавриат, Магистратура, Специалитет)
description TEXT Описание

Эта таблица используется и группами (student_groups.education_form_id), и календарными учебными графиками (academic_calendars.study_form_id). Отдельного справочника форм обучения для календарей нет.

student_groups — Учебные группы

Колонка Тип Описание
id BIGSERIAL PK ID
name VARCHAR(100) Название группы (напр. ИВТ-21-1), не уникальное
group_size BIGINT Количество студентов
education_form_id BIGINT FK → education_forms Форма обучения
department_id BIGINT FK → departments Кафедра
specialty_id BIGINT FK → specialties Специальность
specialty_profile_id BIGINT FK → specialty_profiles Профиль обучения группы
year_start_study BIGINT Год начала обучения, используется для вычисления текущего курса
status VARCHAR(20) Жизненный цикл группы: ACTIVE или ARCHIVED
active_from DATE Дата начала действия группы
active_to DATE Дата окончания действия группы для исторических расчётов
archived_at TIMESTAMP Дата и время архивирования
archive_reason TEXT Причина архивирования

subgroups — Подгруппы

Колонка Тип Описание
id BIGSERIAL PK ID
group_id BIGINT FK → student_groups (CASCADE) Родительская группа
name VARCHAR(100) Название подгруппы
student_capacity INT Количество студентов

Уникальность активных записей задаётся парой (group_id, lower(name)): в разных группах могут быть подгруппы с одинаковым названием, а архивные подгруппы не блокируют повторное создание подгруппы с тем же именем. Подгруппы применяются только для лабораторных занятий.

subjects — Дисциплины

Колонка Тип Описание
id BIGSERIAL PK ID
name VARCHAR(200) UNIQUE Название
code VARCHAR(20) Код предмета
department_id BIGINT FK → departments Кафедра
description TEXT Описание

Аудиторный фонд

classrooms — Аудитории

Колонка Тип Описание
id BIGSERIAL PK ID
name VARCHAR(50) UNIQUE Название (напр. 101 Ленинская)
capacity INT CHECK(> 0) Вместимость
building VARCHAR(50) Корпус
floor INT Этаж
is_available BOOLEAN Доступна для назначения пар
status VARCHAR(20) ACTIVE или ARCHIVED; архивные аудитории не выбираются в новых назначениях
active_from DATE Дата начала действия записи
active_to DATE Дата окончания действия записи
archived_at TIMESTAMP Когда аудитория выведена из эксплуатации
archive_reason TEXT Причина архивирования
description TEXT Описание

equipments — Оборудование

Колонка Тип Описание
id BIGSERIAL PK ID
name VARCHAR(50) UNIQUE Название
description TEXT Описание
inventory_number VARCHAR(50) Инвентарный номер

classroom_equipments — Привязка оборудования к аудиториям

Колонка Тип Описание
classroom_id BIGINT PK, FK → classrooms (CASCADE) Аудитория
equipment_id BIGINT PK, FK → equipments (CASCADE) Оборудование
quantity INT CHECK(> 0) Количество единиц
notes TEXT Примечания

Расписание

lesson_types — Типы занятий (справочник)

Колонка Тип Описание
id BIGSERIAL PK ID
name VARCHAR(50) UNIQUE Название типа
color_code VARCHAR(7) HEX-цвет для UI (напр. #FF6B6B)
duration_minutes INT Длительность (по умолчанию 90)

Связи «Преподаватель ↔ Дисциплина»

teacher_subjects — Квалификация преподавателей

Колонка Тип Описание
user_id BIGINT PK, FK → users (CASCADE) Преподаватель
subject_id BIGINT PK, FK → subjects (CASCADE) Дисциплина
qualification_level VARCHAR(50) Уровень квалификации
experience_years INT Стаж

teacher_lesson_types — Типы занятий преподавателя

Колонка Тип Описание
user_id BIGINT PK, FK → users (CASCADE) Преподаватель
subject_id BIGINT PK, FK → subjects (CASCADE) Дисциплина
lesson_type_id BIGINT PK, FK → lesson_types (CASCADE) Тип занятия

teacher_department_assignments — История кафедр преподавателя

Колонка Тип Описание
id BIGSERIAL PK ID исторической записи
teacher_id BIGINT FK → users Преподаватель
department_id BIGINT FK → departments Кафедра
valid_from DATE Дата начала принадлежности
valid_to DATE Дата окончания принадлежности, NULL для текущей кафедры
is_primary BOOLEAN Основная кафедра преподавателя
comment TEXT Комментарий к переводу
created_at TIMESTAMP Дата создания записи
created_by BIGINT FK → users Кто оформил перевод

Индекс uq_teacher_department_open_primary гарантирует не больше одной открытой основной кафедры у преподавателя. Индекс uq_teacher_department_open_pair запрещает две открытые связи одного преподавателя с одной кафедрой, но позволяет преподавателю иметь несколько открытых неосновных кафедр.

teacher_creation_requests — Заявки кафедр на создание преподавателей

Колонка Тип Описание
id BIGSERIAL PK ID заявки
department_id BIGINT FK → departments Кафедра, которая запрашивает преподавателя
username VARCHAR(50) Предлагаемый логин
full_name VARCHAR(255) ФИО преподавателя
job_title VARCHAR(255) Должность
comment TEXT Комментарий кафедры
status VARCHAR(20) PENDING, APPROVED или REJECTED
requested_by BIGINT FK → users Пользователь, создавший заявку
reviewed_by BIGINT FK → users Администратор, рассмотревший заявку
reviewed_at TIMESTAMP Дата рассмотрения
review_comment TEXT Комментарий администратора
created_teacher_id BIGINT FK → users Созданный преподаватель после одобрения
created_at TIMESTAMP Дата создания заявки
updated_at TIMESTAMP Дата последнего изменения

Заявка не хранит пароль. Пароль задаётся администратором только при одобрении, после чего создаётся пользователь с ролью TEACHER и основная запись в teacher_department_assignments. Частичный уникальный индекс uq_teacher_creation_requests_pending_username запрещает две открытые заявки с одним логином.

subject_comments — Комментарии к дисциплинам

Колонка Тип Описание
id BIGSERIAL PK ID комментария
subject_id BIGINT FK → subjects Дисциплина
author_id BIGINT FK → users Автор комментария
comment TEXT Текст комментария
created_at TIMESTAMP Дата создания

time_slot_scopes — Сетки времени

Колонка Тип Описание
id BIGSERIAL PK ID
code VARCHAR(50) UNIQUE Системный код сетки
name VARCHAR(120) Название в интерфейсе
apply_mode VARCHAR(20) DEFAULT, WEEKDAY, MANUAL
day_of_week INT NULL День недели для автоматической сетки
system_scope BOOLEAN Защищает базовую и субботнюю сетки от удаления
display_order INT Порядок в списках

Seed создаёт Базовая сетка (DEFAULT) и Субботняя сетка (WEEKDAY, day_of_week = 6). Дополнительные сетки создаются как MANUAL и применяются только через ручные назначения дат.

time_slots — Временные слоты занятий

Колонка Тип Описание
id BIGSERIAL PK ID
time_slot_scope_id BIGINT FK → time_slot_scopes Сетка времени
order_number INT Номер пары в дне
start_time TIME Время начала
end_time TIME Время окончания
duration_minutes INT Длительность в минутах

Уникальность задаётся индексом (time_slot_scope_id, order_number): в одной сетке может быть только один слот с номером пары.

time_slot_date_assignments — Ручные назначения сеток времени

Колонка Тип Описание
id BIGSERIAL PK ID
assignment_date DATE UNIQUE Дата ручного применения
time_slot_scope_id BIGINT FK → time_slot_scopes Ручная сетка времени

academic_years — Учебные годы

Колонка Тип Описание
id BIGSERIAL PK ID
title VARCHAR(20) UNIQUE Название, например 2025-2026
start_date DATE Дата начала
end_date DATE Дата окончания

semesters — Семестры

Колонка Тип Описание
id BIGSERIAL PK ID
academic_year_id BIGINT FK → academic_years Учебный год
semester_type VARCHAR(20) autumn или spring
start_date DATE Дата начала, от неё считается неделя 1
end_date DATE Дата окончания

academic_calendar_activity_types — Коды активностей графика

Колонка Тип Описание
id BIGSERIAL PK ID
code VARCHAR(10) UNIQUE Код из Excel-графика: Т, Э, К, У, П, Пд, Н, Г, Д, ПА, С, *, =
name VARCHAR(100) Расшифровка кода
allow_schedule BOOLEAN Разрешает генерацию обычных пар
color_code VARCHAR(7) HEX-цвет для редактора
display_order INT Порядок вывода в UI
description TEXT Дополнительное описание

academic_calendars — Календарные учебные графики

Колонка Тип Описание
id BIGSERIAL PK ID
title VARCHAR(255) Название графика
academic_year_id BIGINT FK → academic_years Учебный год
specialty_id BIGINT FK → specialties Специальность
specialty_profile_id BIGINT FK → specialty_profiles Профиль обучения
study_form_id BIGINT FK → education_forms Форма обучения из общего справочника
course_count INT CHECK(18) Количество курсов в сетке
created_at TIMESTAMP Дата создания
updated_at TIMESTAMP Дата обновления

academic_calendar_days — Дневная сетка календарного графика

Колонка Тип Описание
id BIGSERIAL PK ID
calendar_id BIGINT FK → academic_calendars (CASCADE) Календарный график
course_number INT Номер курса
date DATE Дата учебного года
week_number INT Номер недели от начала учебного года
day_of_week INT CHECK(17) День недели ISO
activity_type_id BIGINT FK → academic_calendar_activity_types Код активности

academic_calendar_subjects — Дисциплины календарного графика

Колонка Тип Описание
id BIGSERIAL PK ID
calendar_id BIGINT FK → academic_calendars (CASCADE) Календарный график
semester_number INT CHECK(> 0) Номер учебного семестра внутри графика: 1, 2, 3 ...
subject_id BIGINT FK → subjects Дисциплина из справочника
created_at TIMESTAMP Дата создания привязки

Уникальность задаётся по calendar_id + semester_number + subject_id, поэтому одну дисциплину нельзя дважды добавить в один семестр одного графика. Верхняя граница номера семестра проверяется backend по academic_calendars.course_count * 2.

student_group_calendar_assignments — Назначения графиков группам

Колонка Тип Описание
id BIGSERIAL PK ID
group_id BIGINT FK → student_groups (CASCADE) Учебная группа
academic_year_id BIGINT FK → academic_years (CASCADE) Учебный год
calendar_id BIGINT FK → academic_calendars (CASCADE) Назначенный график

schedule_rules — Правила расписания

Колонка Тип Описание
id BIGSERIAL PK ID
subject_id BIGINT FK → subjects Дисциплина
semester_id BIGINT FK → semesters Семестр
lecture_academic_hours INT Лимит академических часов лекций
laboratory_academic_hours INT Лимит академических часов лабораторных работ
practice_academic_hours INT Лимит академических часов практик
lecture_start_week INT Неделя семестра, с которой начинаются лекции
laboratory_start_week INT Неделя семестра, с которой начинаются лабораторные
practice_start_week INT Неделя семестра, с которой начинаются практики
status VARCHAR(20) ACTIVE или ARCHIVED
valid_from DATE Начало действия версии правила
valid_to DATE Окончание действия версии правила
version_group_id BIGINT Группа версий одного правила
change_reason TEXT Причина изменения

ScheduleRule использует собственные поля жизненного цикла status, valid_from и valid_to: архивированное правило или правило вне периода действия не участвует в генерации расписания. В отличие от справочников на LifecycleEntity, таблица не содержит active_from/active_to, поэтому состояние правила проверяется по valid_*.

Миграция V4 требует, чтобы лимиты лекций, лабораторных и практик были кратны двум. Неотрицательность каждого лимита и положительная сумма уже закреплены ограничениями V1; нули допустимы только как лимиты неиспользуемых типов занятия.

schedule_rule_groups — Группы правила

Колонка Тип Описание
schedule_rule_id BIGINT PK, FK → schedule_rules Правило
group_id BIGINT PK, FK → student_groups Группа

schedule_rule_slots — Слоты правила

Колонка Тип Описание
id BIGSERIAL PK ID
schedule_rule_id BIGINT FK → schedule_rules Правило
day_of_week INT CHECK(17) День недели: 1 — понедельник
parity VARCHAR(10) BOTH, EVEN, ODD
time_slot_id BIGINT FK → time_slots Базовый временной слот
teacher_id BIGINT FK → users Преподаватель
classroom_id BIGINT FK → classrooms Аудитория
lesson_type_id BIGINT FK → lesson_types Тип занятия
lesson_format VARCHAR(30) Очно или Онлайн
time_locked BOOLEAN Время закреплено учебным отделом
classroom_locked BOOLEAN Аудитория закреплена учебным отделом
teacher_locked BOOLEAN Преподаватель закреплён учебным отделом
locked_by BIGINT FK → users Кто выполнил закрепление
locked_at TIMESTAMP Когда выполнено закрепление
lock_comment TEXT Комментарий к закреплению

V4 добавляет uq_schedule_rule_slots_exact_payload: в одном правиле нельзя повторить одинаковые день, чётность, базовый временной слот, преподавателя, аудиторию, тип и формат занятия. Более широкие ресурсные пересечения и семантика подгрупп проверяются сервисом, поскольку зависят от нескольких таблиц и фактических активных недель.

schedule_rule_slot_subgroups — Подгруппы лабораторного слота

Колонка Тип Описание
schedule_rule_slot_id BIGINT PK, FK → schedule_rule_slots (CASCADE) Слот правила
subgroup_id BIGINT PK, FK → subgroups (CASCADE) Подгруппа

Связь заполняется только для лабораторных слотов. Для потоковой лабораторной можно выбрать разные подгруппы разных групп в одном слоте. Лекции и практики не делятся на подгруппы и не имеют записей в этой таблице. Триггер validate_schedule_rule_slot_subgroups проверяет тип занятия, принадлежность подгруппы к группам правила и запрет на две подгруппы одной группы в одном слоте; для быстрых выборок есть индекс idx_schedule_rule_slot_subgroups_subgroup.

schedule_overrides — Точечные изменения пар

Колонка Тип Описание
id BIGSERIAL PK ID изменения
base_rule_slot_id BIGINT FK → schedule_rule_slots Базовый слот правила
lesson_date DATE Дата конкретной пары
action VARCHAR(20) MOVE, CANCEL, REPLACE
new_time_slot_id BIGINT FK → time_slots Новый временной слот
new_classroom_id BIGINT FK → classrooms Новая аудитория
new_teacher_id BIGINT FK → users Новый преподаватель
new_lesson_format VARCHAR(30) Новый формат занятия
comment TEXT Причина изменения
created_by BIGINT FK → users Автор изменения
created_at TIMESTAMP Дата создания

Ограничение uq_schedule_overrides_slot_date не позволяет создать две разные правки для одной и той же пары. Миграция V3 добавляет структурные инварианты:

  • CANCEL не содержит новых ресурсов;
  • MOVE содержит новый временной слот или аудиторию;
  • REPLACE содержит нового преподавателя, аудиторию или формат;
  • new_lesson_format равен Очно, Онлайн либо NULL.

Фактическое существование пары, реальность изменения и ресурсные конфликты зависят от календаря конкретной даты и проверяются транзакционным сервисом, а не SQL CHECK.


Flyway миграции

Правила работы

  1. Все миграции находятся в backend/src/main/resources/db/migration/
  2. Формат имени: V{номер}__{описание}.sql (напр. V1__init.sql, V2__add_departments.sql)
  3. ЗАПРЕЩЕНО изменять уже закоммиченные файлы миграций — это сломает контрольные суммы Flyway. Исключение допускается только по прямой просьбе пользователя и при полном сбросе tenant-БД.
  4. Flyway запускается программно при первом обращении к БД тенанта (TenantConfigWatcher.initDatabaseForTenant())
  5. Настройка baselineOnMigrate=true — если в БД уже есть данные, Flyway начнёт с baseline

Текущие миграции

Файл Описание
V1__init.sql Инициализация: справочники, роли, refresh-сессии JWT, lifecycle-поля, история кафедр преподавателей, заявки кафедр на создание преподавателей, комментарии дисциплин, календарные учебные графики, привязки дисциплин к графикам по семестрам, динамическое расписание, версии/закрепления правил, точечные изменения расписания, тестовые правила, триггеры, комментарии
V2__subgroups_active_unique_name.sql Уникальность имени среди активных подгрупп одной группы
V3__schedule_override_invariants.sql Матрица payload и допустимый формат точечных изменений расписания
V4__schedule_rule_even_hours.sql Чётность лимитов академических часов и уникальность точного payload слота правила

Накатывание на существующих тенантов

V2V4 накатываются на существующие tenant-БД без изменения контрольных сумм V1V3. Перед добавлением ограничений V3 считает нарушения CANCEL, MOVE, REPLACE и формата. Если найдены legacy-строки, миграция полностью откатывается и сообщает только количества нарушений. Оператор должен исправить бизнес-данные tenant-БД и повторить миграцию; автоматическое удаление или переписывание overrides не выполняется.

Перед ограничениями V4 отдельно считаются нечётные лимиты лекций, лабораторных и практик, а также группы точных дублей слотов. Любое нарушение останавливает V4 с русским сообщением; ограничения не остаются частично применёнными и legacy-данные автоматически не меняются.

# После исправления legacy-данных перезапустите backend,
# чтобы TenantConfigWatcher повторил Flyway migrate для tenant-БД.
docker compose restart backend

Полный сброс БД (локально)

docker compose down -v    # Удаляет volumes (данные)
docker compose up -d      # Пересоздаёт БД с нуля