docs: план, ADR и контракты групп и курсов

This commit is contained in:
2026-10-26 15:47:01 +03:00
parent 920d96a0f5
commit cde04487e8
6 changed files with 945 additions and 18 deletions

View File

@@ -0,0 +1,150 @@
# ADR 0001. Фундамент групп, курсов и доступа
**Статус:** принято.
**Дата:** 2026-10-23.
**Контекст:** реализация [ТЗ 07](../07-groups-courses.md) и
[плана 08](../08-groups-courses-plan.md).
## Контекст
Сейчас в LMS нет сущности курса-дисциплины, ролей преподавателя, статусов
студентов и истории членства. Лабораторные привязаны к семестру, материалы —
к глобальному дереву глав. Доступ различает только `ADMIN` и «всё остальное»;
проверки в основном находятся в web-BFF.
Нужно перейти к модели:
- курс-дисциплина с преподавателями и группами;
- группы-когорты с потоком, курсом обучения, номером, степенью и годом набора;
- студенты со статусами и историей;
- лабораторные и материалы, принадлежащие курсу;
- проверки доступа на сервисах;
- профиль студента курса для преподавателя.
## Решения
### 1. Курс C и `course_no`
- Текущий единственный курс — «Промышленное программирование на языке C».
- UUID остаётся первичным ключом курса.
- Для URL добавляется `courses.course_no`:
`BIGINT GENERATED ALWAYS AS IDENTITY UNIQUE NOT NULL`.
- Курсовые материалы открываются по
`/learn/course/{course_no}/{chapter}/{section}/{article}`.
- `course_no` не является секретом; доступ проверяется отдельно.
- Старые ссылки на текущие материалы не поддерживаются: они не публиковались.
### 2. Все лабораторные и материалы принадлежат курсу
- «Общих» лабораторных нет.
- Все текущие лабы, главы, разделы, статьи и активы переносятся в курс C.
- `labs.course_id` вводится как nullable на время миграции, затем становится
`NOT NULL`.
- `chapters.course_id` остаётся nullable, потому что глобальные материалы
сохраняются как отдельный сценарий. `audience` применяется только к глобальным
главам (`course_id IS NULL`).
- `sections` и `articles` наследуют видимость от главы.
- Активы наследуют видимость от статьи → раздела → главы.
### 3. Двухфазная миграция и bootstrap
Обычный Flyway-старт применяет миграции до бизнес-логики, а курс C должен
принадлежать реальному админу. Поэтому:
1. Миграции добавляют nullable-поля и новые таблицы.
2. После первого входа админа bootstrap-сервис:
- создаёт курс C с `owner_account_id` админа;
- привязывает к нему все текущие активные группы;
- проставляет `labs.course_id` и `chapters.course_id`;
- проверяет дубли `(number, course_id)` и сообщает о конфликтах.
3. После успешного bootstrap отдельным шагом применяется
`ALTER TABLE labs ALTER COLUMN course_id SET NOT NULL`.
`SET NOT NULL` не входит в обычную цепочку Flyway: иначе миграция выполнится
раньше bootstrap и упадёт на существующих лабах. Точный механизм шага
(CLI, admin endpoint или отдельная Flyway-локация) фиксируется на этапе 2.
### 4. Проверки доступа на сервисах
- JWT содержит только `sub`, `role`, `login`, `name`; списки курсов и групп
в токен не кладутся.
- Каждый сервис проверяет доступ по своим данным и данным источника истины:
- identity — аккаунт, студент, группа, статус;
- academic — курс, преподаватель, группа курса, лабораторная, материал;
- progress — оценки, сдачи, защиты, публикации.
- Для межсервисных проверок используются внутренние эндпоинты
`/api/v1/internal/**`:
- identity: контекст студента (группа, статус, год набора);
- academic: доступ к курсу (активный преподаватель, привязанная группа,
статус курса), список лаб курса.
- Внутренние эндпоинты не маршрутизируются gateway и защищены отдельным
сервисным токеном (`X-Internal-Token`), а не только пользовательским JWT.
- Кросс-схемные SQL-запросы и внешние ключи между `identity`, `academic` и
`progress` не добавляются.
- Отзыв доступа к курсу/группе/публикации действует сразу, потому что
проверяется на каждом запросе по текущим данным.
### 5. Статусы и вход
- `accounts.status`: `ACTIVE | ARCHIVED`.
- `students.status`: `ACTIVE | ACADEMIC_LEAVE | EXPELLED | GRADUATED | ARCHIVED`.
- Вход запрещён для `EXPELLED`, `GRADUATED`, `ARCHIVED` и архивных аккаунтов.
- `ACADEMIC_LEAVE` — вход в режиме только чтения.
- Web перепроверяет статус с кэшем 30–60 секунд; сервисы проверяют статус
студента и группы при доступе к курсу.
- JWT остаётся 30-дневным; остаточный риск прямого API фиксируется в
документации. Для браузера токен не доступен.
### 6. Повышение курса
- `promotion_runs` — шапка запуска.
- `promotion_run_items` — по-элементный план/результат для групп и студентов.
- `membership_history` — история студента; ссылается на запуск.
- 31 августа в 00:00 МСК автоматически создаётся `DRY_RUN`.
- Применение — только вручную админом после проверки плана.
- Один успешный `APPLY` на год; откат — по элементам журнала с проверкой
конфликтов.
### 7. Профиль студента курса
- Преподаватель активного курса открывает профиль студента по клику на ФИО
или отдельной кнопке.
- Профиль агрегирует:
- статистику по лабам курса;
- комментарии преподавателей (общий и отдельные) с автором и датой;
- все попытки сдачи и скачивание файлов любой попытки.
- Доступ проверяется на сервисах: преподаватель — только свой курс и студенты
его групп; админ — любой; студент — только свой профиль (опционально).
- Контракт API: [09-teacher-student-profile.md](../09-teacher-student-profile.md).
### 8. Резервное копирование
- Backup обязателен перед каждым деплоем/миграцией; при ошибке backup деплой
прерывается.
- Backup включает БД (`pg_dump --format=custom`), volume решений и volume
материалов, `SHA256SUMS` и manifest.
- Ежедневный backup по systemd timer; retention по умолчанию 14 дней.
- Восстановление — только с подтверждением `RESTORE`; после restore проверяется
health web.
- Backup на том же сервере не считается достаточным: нужна внешняя копия и
периодическая проверка restore на staging.
- Инструкция: [deploy/BACKUP.md](../../deploy/BACKUP.md).
## Последствия
- Схема становится шире, но миграции аддитивные и не ломают старый код.
- Появляется отдельный bootstrap-шаг; его нельзя пропускать при развёртывании.
- Нужны внутренние эндпоинты и сервисный токен.
- Нужно обновить security-конфиги, gateway и web-интерфейс.
- Старые ссылки на материалы перестанут работать; это осознанное решение.
- До включения `NOT NULL` в `labs.course_id` возможны лабы без курса; preflight
и bootstrap-отчёт обязательны.
- Появляется обязательный backup-шаг; без него деплой не выполняется.
## Открытые вопросы
- Механизм post-bootstrap `SET NOT NULL`: CLI, admin endpoint или отдельная
Flyway-локация. Решение — этап 2.
- Формат аудита скачиваний профиля: общий `audit_events` или отдельная таблица.
- Поведение профиля для студентов, отвязанных от курса/архивных: история или
`404`. Решение — этап 3.