# 🗄 База данных ## Общая информация - **СУБД:** PostgreSQL (локально `postgres:alpine3.23`, продакшн — managed PostgreSQL) - **Управление схемой:** Flyway (программный запуск) - **Hibernate DDL:** Отключён (`ddl-auto=none`) - **Расширения:** `pgcrypto` (bcrypt-хеширование паролей) - **Мультитенантность:** Каждый тенант = отдельная БД --- ## ER-диаграмма ```mermaid 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 } 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 } 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" 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{ 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{ 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 | Год начала обучения, используется для вычисления текущего курса | #### `subgroups` — Подгруппы | Колонка | Тип | Описание | |---------|-----|----------| | `id` | BIGSERIAL PK | ID | | `group_id` | BIGINT FK → student_groups (CASCADE) | Родительская группа | | `name` | VARCHAR(100) | Название подгруппы | | `student_capacity` | INT | Количество студентов | Уникальность задаётся парой `(group_id, 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` гарантирует не больше одной открытой основной кафедры у преподавателя. #### `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(> 0) | Количество курсов в сетке | | `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(1–7) | День недели ISO | | `activity_type_id` | BIGINT FK → academic_calendar_activity_types | Код активности | #### `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 | Причина изменения | #### `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(1–7) | День недели: 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 | Комментарий к закреплению | #### `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` не позволяет создать две разные правки для одной и той же пары. --- ## 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 > Текущая JWT-правка является осознанным исключением по прямой просьбе пользователя: `V1__init.sql` обновлён как новая базовая схема, а применение предполагает полный сброс tenant-БД без переноса старых Flyway checksum. ### Текущие миграции | Файл | Описание | |------|----------| | `V1__init.sql` | Инициализация: справочники, роли, refresh-сессии JWT, lifecycle-поля, история кафедр преподавателей, комментарии дисциплин, календарные учебные графики, динамическое расписание, версии/закрепления правил, точечные изменения расписания, тестовые правила, триггеры, комментарии | ### Накатывание на существующих тенантов Для применения новой базовой схемы к уже существующим тенантам нужен полный сброс БД. Перезапуск backend без сброса не изменит уже применённую `V1__init.sql`. ```bash # Kubernetes kubectl rollout restart deployment backend -n magistr # Docker Compose (локально) docker compose restart backend ``` ### Полный сброс БД (локально) ```bash docker compose down -v # Удаляет volumes (данные) docker compose up -d # Пересоздаёт БД с нуля ```