- keyset-пагинация аудита с курсором (occurred_at, id) и UI «Показать ещё» - пакетный /api/v1/labs/details и переход progress на один запрос - абсолютный потолок сессии 30 дней (identity V12 + web), очистка refresh - админская инвентаризация legacy-активов (/admin/legacy-assets, скрипт) - E2E в Chromium/Firefox/WebKit (18/18), CI preflight - LDD: 51 claim, 346 тестов, docs и handoff обновлены
10 KiB
10 KiB
ADR 0001. Фундамент групп, курсов и доступа
Статус: принято. Дата: 2026-10-23. Контекст: реализация ТЗ 07 и плана 08.
Контекст
Сейчас в 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 должен принадлежать реальному админу. Поэтому:
- Миграции добавляют nullable-поля и новые таблицы.
- После первого входа админа bootstrap-сервис:
- создаёт курс C с
owner_account_idадмина; - привязывает к нему все текущие активные группы;
- проставляет
labs.course_idиchapters.course_id; - проверяет дубли
(number, course_id)и сообщает о конфликтах.
- создаёт курс C с
- После успешного 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-дневным— решение пересмотрено: access 15 минут, refresh 7 дней с ротацией и абсолютным потолком 30 дней (V11–V12). Для браузера токен не доступен.
6. Повышение курса
promotion_runs— шапка запуска.promotion_run_items— по-элементный план/результат для групп и студентов.membership_history— история студента; ссылается на запуск.- 31 августа в 00:00 МСК автоматически создаётся
DRY_RUN. - Применение — только вручную админом после проверки плана.
- Один успешный
APPLYна год; откат — по элементам журнала с проверкой конфликтов.
7. Профиль студента курса
- Преподаватель активного курса открывает профиль студента по клику на ФИО или отдельной кнопке.
- Профиль агрегирует:
- статистику по лабам курса;
- комментарии преподавателей (общий и отдельные) с автором и датой;
- все попытки сдачи и скачивание файлов любой попытки.
- Доступ проверяется на сервисах: преподаватель — только свой курс и студенты его групп; админ — любой; студент — только свой профиль (опционально).
- Контракт API: 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.
Последствия
- Схема становится шире, но миграции аддитивные и не ломают старый код.
- Появляется отдельный 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.