docs: план, ADR и контракты групп и курсов
This commit is contained in:
150
docs/adr/0001-groups-courses-foundation.md
Normal file
150
docs/adr/0001-groups-courses-foundation.md
Normal 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.
|
||||
Reference in New Issue
Block a user