Files
magistr/docs/DATABASE.md

737 lines
33 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🗄 База данных
## Общая информация
- **СУБД:** 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(17) | День недели 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 | Причина изменения |
`ScheduleRule` использует собственные поля жизненного цикла `status`, `valid_from` и `valid_to`: архивированное правило или правило вне периода действия не участвует в генерации расписания. В отличие от справочников на `LifecycleEntity`, таблица не содержит `active_from`/`active_to`, поэтому состояние правила проверяется по `valid_*`.
#### `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 | Комментарий к закреплению |
#### `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 # Пересоздаёт БД с нуля
```