From cde04487e80dcdb3f254ce84498b99ab3264239b Mon Sep 17 00:00:00 2001 From: Vladislav Kopnin Date: Mon, 26 Oct 2026 15:47:01 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=BF=D0=BB=D0=B0=D0=BD,=20ADR=20?= =?UTF-8?q?=D0=B8=20=D0=BA=D0=BE=D0=BD=D1=82=D1=80=D0=B0=D0=BA=D1=82=D1=8B?= =?UTF-8?q?=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF=20=D0=B8=20=D0=BA=D1=83=D1=80?= =?UTF-8?q?=D1=81=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/07-groups-courses.md | 140 ++++++++-- docs/08-groups-courses-plan.md | 292 +++++++++++++++++++++ docs/09-teacher-student-profile.md | 285 ++++++++++++++++++++ docs/10-stage0-report.md | 91 +++++++ docs/README.md | 5 + docs/adr/0001-groups-courses-foundation.md | 150 +++++++++++ 6 files changed, 945 insertions(+), 18 deletions(-) create mode 100644 docs/08-groups-courses-plan.md create mode 100644 docs/09-teacher-student-profile.md create mode 100644 docs/10-stage0-report.md create mode 100644 docs/adr/0001-groups-courses-foundation.md diff --git a/docs/07-groups-courses.md b/docs/07-groups-courses.md index 2c5ebc8..e7fb68c 100644 --- a/docs/07-groups-courses.md +++ b/docs/07-groups-courses.md @@ -1,8 +1,37 @@ # 07. Группы, курсы и преподаватели, повышение, интеграция (ТЗ) -**Статус:** проект согласован, к реализации не приступаем до отдельного сигнала. +**Статус:** проект согласован, реализация ведётся по плану +[08-groups-courses-plan.md](08-groups-courses-plan.md). Документ описывает целевую модель, схему преобразований БД, правила и граничные случаи. +## 0. Решения, принятые при планировании (2026-10-23) + +При противоречии с разделами 1–14 приоритет у этого раздела и плана +[08-groups-courses-plan.md](08-groups-courses-plan.md). + +1. **«Общих» лабораторных нет.** Все лабы принадлежат курсу. Текущие лабы переносятся + в курс «Промышленное программирование на языке C»; все текущие активные группы + привязываются к нему. `labs.course_id` сначала nullable, после переноса — `NOT NULL`. +2. **Курс в URL — целочисленный `course_no`.** UUID остаётся первичным ключом; + `courses.course_no` — `BIGINT GENERATED ALWAYS AS IDENTITY`, уникальный и неизменяемый. + Курсовые материалы открываются по `/learn/course/{course_no}/...`. + Все текущие материалы переносятся в курс C и доступны только студентам его групп, + преподавателям курса и админу. Старые ссылки не поддерживаются. + `audience` применяется только к будущим глобальным материалам (`course_id IS NULL`). +3. **Статусы и вход.** `accounts.status (ACTIVE|ARCHIVED)` — для всех пользователей; + `students.status (ACTIVE|ACADEMIC_LEAVE|EXPELLED|GRADUATED|ARCHIVED)` — академический. + Вход закрыт для `EXPELLED`, `GRADUATED`, `ARCHIVED` и архивных аккаунтов. + `ACADEMIC_LEAVE` — вход в режиме только чтения. Web перепроверяет статус с кэшем + 30–60 секунд; сервисы проверяют доступ к курсу при каждом запросе. +4. **Журнал повышения.** `promotion_runs` + `promotion_run_items` + `membership_history`. + Dry-run сохраняется как план; применение идемпотентно; откат — по элементам журнала + с проверкой конфликтов. 31 августа в 00:00 МСК автоматически строится dry-run; + применение — только вручную админом. +5. **Профиль студента курса.** Преподаватель своего курса открывает профиль студента + по клику на ФИО или отдельной кнопке: статистика по лабам, комментарии преподавателей, + скачивание файлов любой попытки сдачи. Подробно — §5.1. +6. **Объём.** Полный ТЗ, этапы 0–6, один релиз с внутренними merge gate. + ## 1. Зафиксированные решения 1. Сущность **курс** живёт в `academic-service`; владелец — **админ**, преподавателей на курс @@ -65,7 +94,10 @@ ### progress Данные привязаны к `lab_id` и студенту (сдачи, оценки, комментарии, защиты, назначения вариантов, -публикации). Схема **не меняется**: доступ регулируется по цепочке `lab → course → (преподаватель | группа)`. +публикации). Схема **в основном не меняется**: доступ регулируется по цепочке +`lab → course → (преподаватель | группа)`. Для профиля студента курса добавляется +`grade_comments.author_account_id` (nullable) и индекс `(student_id, lab_id)`; +остальное — рантайм-проверки. --- @@ -97,13 +129,15 @@ erDiagram переводы, академ, возврат, выпуск, отчисление, восстановление. ### academic -- `courses(id, title, owner_account_id→accounts, status (ACTIVE|ARCHIVED), created_at, updated_at)`. +- `courses(id, course_no, title, owner_account_id→accounts, status (ACTIVE|ARCHIVED), created_at, updated_at)`; + `course_no` — `BIGINT GENERATED ALWAYS AS IDENTITY`, уникальный, для URL. - `course_teachers(id, course_id, account_id, assigned_at, removed_at)` — доступ = `removed_at IS NULL`; история сохраняется. - `course_groups(id, course_id, group_id, added_at, removed_at)`. -- `labs`: `+ course_id` (nullable — «общие» лабы), новый уникальный ключ `(number, course_id)` для лаб курса. +- `labs`: `+ course_id` (nullable только на время миграции, затем `NOT NULL`); «общих» лаб нет; + новый уникальный ключ `(number, course_id)` для лаб курса. - Материалы: только у **корня** (`chapters`) добавляем `+ course_id` (nullable) и `+ audience` (`ALL | STUDENTS | TEACHERS`, применяется при `course_id IS NULL`); `sections`/`articles` наследуют - видимость от главы. + видимость от главы. Текущие материалы переносятся в курс C и получают `course_id`. - `chapters.slug`: снять глобальный UNIQUE, ввести два частичных индекса (глобальные и внутри курса). - `article_assets`: видимость наследуется от статьи → раздела → главы (см. §6). @@ -175,6 +209,7 @@ SELECT student_id, group_id, 'ENROLL', enrolled_at FROM enrollments; ```sql CREATE TABLE courses ( id uuid PRIMARY KEY DEFAULT uuidv7(), + course_no bigint GENERATED ALWAYS AS IDENTITY UNIQUE NOT NULL, title varchar(200) NOT NULL, owner_account_id uuid NOT NULL, status varchar(16) NOT NULL DEFAULT 'ACTIVE', @@ -199,36 +234,61 @@ CREATE TABLE course_groups ( CREATE UNIQUE INDEX course_groups_active_uidx ON course_groups(course_id, group_id) WHERE removed_at IS NULL; ``` -**V7 — лабораторные привязываем к курсу** +**V7 — лабораторные привязываем к курсу (nullable-фаза)** ```sql --- «Курс по умолчанию» создаётся в сервисе (title='Общие лабораторные', owner=первый ADMIN) +-- Курс «Промышленное программирование на языке C» создаётся bootstrap-сервисом +-- после первого входа админа (owner = этот админ). В V7 только добавляем nullable-колонку. ALTER TABLE labs ADD COLUMN course_id uuid REFERENCES courses(id) ON DELETE RESTRICT; -UPDATE labs SET course_id = (SELECT id FROM courses ORDER BY created_at LIMIT 1) WHERE course_id IS NULL; -ALTER TABLE labs ALTER COLUMN course_id SET NOT NULL; -DROP CONSTRAINT labs_number_semester_unique; -ALTER TABLE labs ADD CONSTRAINT labs_number_course_unique UNIQUE (number, course_id); +ALTER TABLE labs DROP CONSTRAINT labs_number_semester_unique; +CREATE UNIQUE INDEX labs_number_course_uidx ON labs (number, course_id); CREATE INDEX labs_course_idx ON labs (course_id); ``` +**V7a — bootstrap курса C (сервис, не SQL-миграция)** +- Создать курс «Промышленное программирование на языке C» с `owner_account_id` текущего + админа; первый созданный курс получит `course_no = 1` автоматически. +- Привязать все текущие активные группы к курсу (`course_groups`). +- Проставить `labs.course_id` и `chapters.course_id` для всех существующих записей. +- Проверить дубли `(number, course_id)` и показать отчёт, если они есть. + **V8 — материалы: курс и аудитория, слаг** ```sql ALTER TABLE chapters ADD COLUMN course_id uuid REFERENCES courses(id) ON DELETE CASCADE; ALTER TABLE chapters ADD COLUMN audience varchar(16); -- ALL | STUDENTS | TEACHERS (только при course_id IS NULL) -UPDATE chapters SET audience = 'ALL' WHERE course_id IS NULL; -- сохранить прежнее поведение («видят все») +-- Bootstrap проставляет course_id = C для текущих глав; audience у курсовых глав остаётся NULL. +-- audience = 'ALL' используется только для будущих глобальных материалов. ALTER TABLE chapters DROP CONSTRAINT chapters_slug_key; CREATE UNIQUE INDEX chapters_slug_global_uidx ON chapters (slug) WHERE course_id IS NULL; CREATE UNIQUE INDEX chapters_slug_course_uidx ON chapters (course_id, slug) WHERE course_id IS NOT NULL; ``` -Обновить URL-схему материалов: глобальные — как сейчас, курсовые — `/learn/{course}/{chapter}/{section}/{article}`; -старые ссылки на глобальные материалы сохраняются (совместимость). +Обновить URL-схему материалов: глобальные — `/learn/{chapter}/{section}/{article}`; +курсовые — `/learn/course/{course_no}/{chapter}/{section}/{article}`. +Старые ссылки на текущие материалы не поддерживаются (решение 2026-10-23). **V9 — активы: наследуемая видимость** Признак видимости актив наследует от статьи→раздела→главы (course/audience). Требуется проверка доступа в маршруте выдачи активов (см. §6). Отдельных колонок достаточно опционально; минимум — корректная авторизация. +**V10 — курс C: завершение привязки лаб (post-bootstrap шаг)** +```sql +-- Выполняется после bootstrap-сервиса, когда все лабы получили course_id. +-- Это отдельный шаг/миграция: обычный Flyway-старт применяет миграции раньше +-- бизнес-логики и не может дождаться создания курса и назначения владельца. +ALTER TABLE labs ALTER COLUMN course_id SET NOT NULL; +``` + ### progress -Схема **не меняется**. Проверки доступа — рантайм (по `lab_id → course`). +Схема **в основном не меняется**. Проверки доступа — рантайм (по `lab_id → course`). + +**V10 — профиль студента курса: автор комментария** +```sql +ALTER TABLE grade_comments ADD COLUMN author_account_id uuid; +ALTER TABLE lab_grades ADD COLUMN updated_by_account_id uuid; +CREATE INDEX lab_grades_lab_idx ON lab_grades (lab_id); +``` +Индекс `grade_comments (student_id, lab_id)` уже существует (`grade_comments_grade_idx`). +Старые комментарии остаются без автора и отображаются как «автор не указан». --- @@ -246,6 +306,43 @@ CREATE UNIQUE INDEX chapters_slug_course_uidx ON chapters (course_id, slug) WHER 2. Доступ отзывается **немедленно** (не хранить список курсов/групп в JWT). 3. Файлы: преподаватель — только сдачи своих курсов; студент — только свои; **активы** — по видимости материала. +### 5.1. Профиль студента курса (требование 2026-10-23) + +Преподаватель текущего курса может открыть профиль студента: + +- по клику на ФИО в ведомости группы; +- по отдельной кнопке «Профиль». + +Доступ: + +- преподаватель — только если он активен в `course_teachers` (`removed_at IS NULL`), + а студент входит в группу, привязанную к этому курсу (`course_groups.removed_at IS NULL`); +- админ — любой студент любого курса; +- студент — свой профиль и только опубликованные оценки/комментарии + (опционально; в обязательный объём не входит); +- аноним — доступа нет; +- проверка выполняется на сервисах; чужой курс/студент/сдача → `403` или `404`. + +Состав профиля: + +- карточка студента: ФИО, №, группа, статус, год набора; +- сводка по курсу: количество лаб, сдано, проверено, средний балл, защиты; +- таблица по каждой лабе курса: номер/название, вариант и уровень, число попыток, + дата последней попытки, оценка, статус публикации, статус защиты, общий комментарий; +- комментарии преподавателей по каждой лабе: текст, тон, автор, дата; + общий комментарий оценки и отдельные `grade_comments`; +- все попытки сдачи по каждой лабе: номер, дата, файлы, скачивание любой попытки + (ZIP и, при необходимости, отдельные файлы). + +Ограничения: + +- преподаватель видит все лабы своего курса, включая черновики, но не чужие курсы; +- студент видит только опубликованное; +- скачивание файлов не даёт доступа к чужим сдачам; +- профиль не заменяет ведомость: оценки по-прежнему выставляются в ведомости; +- старые комментарии без автора отображаются как «автор не указан»; +- аудит скачиваний — этап 6. + --- ## 6. Материалы, публичный доступ и активы @@ -340,14 +437,20 @@ CREATE UNIQUE INDEX chapters_slug_course_uidx ON chapters (course_id, slug) WHER 3. Одноимённые группы — уникальность по году набора; отображение с годом. 4. Академ vs групповой курс — правило перевода и автосоздание целевой группы. 5. Публичные `ALL`-материалы и **активы** — проверка видимости, иначе утечка/ломка. -6. `chapters.slug`: глобальный UNIQUE → частичные индексы (глобальные/курсовые) + новая URL-схема, - совместимость старых ссылок. -7. Миграция существующих лаб/материалов без курса (курс по умолчанию / `audience='ALL'`). +6. `chapters.slug`: глобальный UNIQUE → частичные индексы (глобальные/курсовые) + новая URL-схема; + старые ссылки на текущие материалы не поддерживаются. +7. Миграция существующих лаб/материалов в курс C; «общих» лаб нет. 8. Немедленный отзыв доступа (преподаватель/группа/публикация материала). 9. Аудит-лог: переводы, академ, повышение, назначения преподавателей, публикации, архив/удаление. 10. Производительность: индексы `course_id`/`group_id`, пагинация материалов и групп. 11. Бэкфилл `enrollment_year` для существующих групп — приблизительный, требует подтверждения админом. 12. Лабы: уникальность номера — в рамках курса (`(number, course_id)`), прежний ключ по семестру снимается. +13. Профиль студента курса: скоуп по курсу, все попытки, комментарии с автором, + аудит скачиваний, отсутствие N+1. +14. Статус защиты в ведомости: различать «готова» (нажал студент) и «кончилось время» + (timeout), с разным цветом и подсказкой. +15. Резервное копирование: обязательный backup перед деплоем/миграцией, + ежедневный backup по расписанию, retention, `verify` и restore на staging. --- @@ -356,6 +459,7 @@ CREATE UNIQUE INDEX chapters_slug_course_uidx ON chapters (course_id, slug) WHER 0. Схема БД и миграции (аддитивно, с бэкфиллом) — по §4. 1. Группы: поля, разбор имени, «одна активная группа», импорт Excel, перемещение студента (админ). 2. Курсы + роль `TEACHER` + назначения (с историей) + скоуп-авторизация и тесты. +2a. Профиль студента курса: статистика по лабам, комментарии, все попытки, скачивание файлов. 3. Привязка лаб/материалов к курсу, аудитории материалов, публичные `ALL`-маршруты и активы, миграция данных. 4. Запись/снятие групп на курс. 5. Повышение курса: dry-run, журнал, идемпотентность, откат, выпуск. diff --git a/docs/08-groups-courses-plan.md b/docs/08-groups-courses-plan.md new file mode 100644 index 0000000..9e4335d --- /dev/null +++ b/docs/08-groups-courses-plan.md @@ -0,0 +1,292 @@ +# 08. План реализации: группы, курсы, преподаватели + +**Статус:** план согласован, реализация по этапам 0–6, один релиз. +**Основа:** [07-groups-courses.md](07-groups-courses.md). +**Дата решений:** 2026-10-23. + +Документ фиксирует решения, принятые при разборе ТЗ, и порядок реализации. +При противоречии с `07-groups-courses.md` приоритет у раздела 1 этого плана; +само ТЗ приводится в соответствие на этапе 0. + +--- + +## 1. Зафиксированные решения + +| № | Вопрос | Решение | +|---|---|---| +| 1 | Лабораторные вне курса | Не нужны. Все лабораторные принадлежат курсу. Текущие лабы переносятся в курс «Промышленное программирование на языке C». Все текущие активные группы привязываются к этому курсу. `labs.course_id` сначала nullable, после переноса — `NOT NULL`. Перед миграцией проверяются дубли номеров. | +| 2 | Идентификатор курса в URL | UUID остаётся первичным ключом. Добавляется целочисленный `courses.course_no` (`BIGINT GENERATED ALWAYS AS IDENTITY`, уникальный, неизменяемый). Курсовые материалы открываются по `/learn/course/{course_no}/...`. Все текущие материалы переносятся в курс C и доступны только студентам его групп, преподавателям курса и админу. Старые ссылки не поддерживаются. `audience` используется только для будущих глобальных материалов (`course_id IS NULL`). | +| 3 | Архив и вход | `accounts.status` (`ACTIVE/ARCHIVED`) — для всех пользователей; `students.status` (`ACTIVE/ACADEMIC_LEAVE/EXPELLED/GRADUATED/ARCHIVED`) — академический статус. Вход запрещён для `EXPELLED`, `GRADUATED`, `ARCHIVED` и архивных аккаунтов. `ACADEMIC_LEAVE` — вход в режиме только чтения. Web перепроверяет статус с кэшем 30–60 секунд; сервисы проверяют доступ к курсу при каждом запросе. | +| 4 | Журнал повышения | `promotion_runs` + `promotion_run_items` + `membership_history`. Dry-run сохраняется как план; применение идемпотентно; откат — по элементам журнала с проверкой конфликтов. | +| 5 | Объём | Полный ТЗ, этапы 0–6, один релиз с внутренними merge gate. | +| 6 | Запуск повышения | 31 августа в 00:00 МСК автоматически строится dry-run. Применение — только вручную админом после проверки плана. При пропуске даты dry-run строится при следующем старте. | +| 7 | Профиль студента курса | Преподаватель своего курса может открыть профиль студента по клику на ФИО или отдельной кнопке: статистика по лабам, комментарии преподавателей, скачивание файлов любой попытки сдачи. | +| 8 | Резервное копирование | Обязательный backup перед каждым деплоем/миграцией, ежедневный backup по расписанию, retention, проверка целостности и restore на staging. Без успешного backup деплой не продолжается. | + +--- + +## 2. Новое требование: профиль студента курса + +### 2.1. Доступ + +- **Преподаватель** — только если он активен в `course_teachers` для этого курса + (`removed_at IS NULL`), а студент входит в группу, привязанную к этому курсу + (`course_groups.removed_at IS NULL`). +- **Админ** — любой студент любого курса. +- **Студент** — свой профиль и только опубликованные оценки/комментарии + (опционально; в обязательный объём текущего требования не входит). +- **Аноним** — доступа нет. +- Проверка выполняется на сервисах, а не только в web-BFF. Чужой курс/студент + должен давать `403` или `404`, а не данные. + +### 2.2. Состав профиля + +Карточка студента: + +- ФИО, № студента, группа, статус, год набора; +- контактные/служебные поля, если появятся позже. + +Сводка по курсу: + +- количество лабораторных курса; +- сколько сдано, сколько проверено; +- средний балл; +- количество защит и их статусы. + +Таблица по каждой лабораторной курса: + +- номер и название лабы; +- вариант и уровень (если назначены); +- количество попыток сдачи; +- дата последней попытки; +- оценка; +- статус публикации оценки; +- статус защиты; +- общий комментарий к оценке; +- количество комментариев. + +Комментарии преподавателей по каждой лабе: + +- общий комментарий оценки (`lab_grades.general_comment`); +- отдельные комментарии (`grade_comments`) с текстом, тоном, автором и датой; +- для старых комментариев без автора — «автор не указан»; +- новые комментарии сохраняют `author_account_id`. + +Все попытки сдачи по каждой лабе: + +- номер попытки, дата, список файлов; +- скачивание любой попытки, а не только последней; +- скачивание в рамках своего курса; чужой `submissionId` запрещён; +- для преподавателя — ZIP попытки и, при необходимости, отдельные файлы; +- для студента — только свои попытки. + +### 2.3. API (предварительно) + +- `GET /api/v1/courses/{courseId}/students/{studentId}/profile` — агрегированный + профиль студента курса. +- `GET /api/v1/courses/{courseId}/submissions/{submissionId}/download` — ZIP попытки + для преподавателя/админа. +- `GET /api/v1/courses/{courseId}/submissions/{submissionId}/files/{name}` — + отдельный файл попытки (если включаем отдельные файлы). +- `GET /api/v1/me/courses/{courseId}/profile` — собственный профиль студента + (только опубликованное), опционально; в обязательный объём не входит. + +`courseId` в API — UUID. В браузерных URL используется `course_no`. + +### 2.4. UI + +- В ведомости группы ФИО студента становится ссылкой на профиль. +- В колонке действий появляется отдельная кнопка «Профиль». +- Страница преподавателя: `/teacher/courses/{courseNo}/students/{studentId}`. +- Админ открывает тот же профиль по `/admin/courses/{courseNo}/students/{studentId}` + или через общий маршрут с проверкой роли. +- Профиль не показывает чужие курсы и глобальные материалы. +- Для архивных/выпущенных студентов профиль открывается в режиме истории, если + студент был в курсе; иначе — `404`. + +### 2.5. Данные и миграции + +- `grade_comments` добавить `author_account_id uuid NULL`; индекс + `(student_id, lab_id)` уже существует. +- Для общего комментария оценки добавить `lab_grades.updated_by_account_id` + (автор последнего изменения) и индекс `lab_grades(lab_id)`. +- Все попытки уже хранятся в `submissions` (`student_id`, `lab_id`, `seq`); + отдельная таблица не нужна. +- Для производительности: один агрегирующий запрос/эндпоинт, пакетная выборка + комментариев, попыток, защит и публикаций по списку `lab_id`; без N+1. +- Аудит скачиваний: этап 6 (журнал `audit_events` или отдельная таблица). + +### 2.6. Ограничения + +- Преподаватель видит все лабы своего курса, включая черновики, но не может + видеть лабы других курсов. +- Студент видит только опубликованные оценки и комментарии. +- Скачивание файлов не даёт доступа к чужим сдачам. +- Профиль не заменяет ведомость: оценки по-прежнему выставляются в ведомости, + профиль — просмотр и скачивание. + +--- + +## 3. Этапы + +### Этап 0. Контракты и схема + +- Обновить `07-groups-courses.md` и ADR под решения раздела 1. +- Описать API профиля студента и формат ответа. +- Миграции identity/academic/progress. +- Preflight: группы, не подходящие под шаблон имени; дубли номеров лаб; + материалы без курса; комментарии без автора. +- Проверить миграции на пустой БД и на копии текущей. +- Определить механизм `labs.course_id NOT NULL`: обычный Flyway-старт не может + дождаться bootstrap-курса. Варианты — seed курса C в миграции с временно + nullable владельцем либо отдельный post-bootstrap шаг. Решение — в ADR. + +**Выход:** схема, контракты, отчёт о рисках; код бизнес-логики ещё не пишем. + +### Этап 1. Identity + +- Роли `ADMIN/TEACHER/STUDENT`. +- Поля групп и парсер имени. +- Статусы студентов и аккаунтов. +- `membership_history`. +- Импорт Excel v2 с построчным отчётом. +- Админ-UI групп и пользователей. + +### Этап 2. Academic + +- `courses`, `course_teachers`, `course_groups`. +- `course_no` и URL курса. +- `labs.course_id`, перенос лаб в курс C, копирование лаб. +- `chapters.course_id/audience`, перенос материалов в курс C. +- Частичные уникальные индексы slug. +- Активы: связь с главой/статьёй и наследуемая видимость. + +### Этап 3. Доступ и профиль (backend) + +- `TEACHER` в JWT и security-конфигах. +- Внутренние access-эндпоинты academic/identity. +- Проверки курса, группы, студента и публикации на сервисах. +- Профиль студента курса: агрегирующий endpoint, все попытки, комментарии, + скачивание файлов. +- Negative-тесты: чужой курс, чужой студент, чужая сдача, аноним. + +### Этап 4. Lifecycle + +- Повышение: dry-run, журнал, применение, откат, выпуск. +- Академический отпуск и возврат. +- Отчисление, выпуск, архив. +- Усиленное удаление и аудит. + +### Этап 5. UX + +- Кабинет преподавателя. +- Статус защиты в ведомости: «готова» / «кончилось время» (timeout), + с разным цветом и подсказкой; сейчас в матрице всегда показывается «готова». +- Страница профиля студента: ссылка с ФИО и отдельная кнопка. +- Студенческие страницы курсов. +- Админ-страницы курсов, повышения, отпусков, архива, импорта и копирования лаб. +- Публичные глобальные материалы `ALL`. + +### Этап 6. Hardening + +- Полная security-матрица. +- Upgrade-тест миграций. +- Индексы и пагинация. +- Аудит скачиваний и чувствительных действий. +- Проверка backup: создание, retention, `verify`, restore на staging. +- Документация, deploy/runbook, smoke. + +--- + +## 4. Ориентировочные миграции + +Номера уточняются при реализации, чтобы не конфликтовать с текущими максимумами +(identity V3, academic V5, progress V9). + +| Сервис | Миграции | Содержание | +|---|---|---| +| identity | V4–V9 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status` | +| academic | V6–V9 + post-bootstrap V10 | курсы, преподаватели, группы курса, `labs.course_id` (nullable), материалы курса/аудитории, частичные индексы, активы; `SET NOT NULL` — отдельный шаг после bootstrap | +| progress | V10 (+ V11 при необходимости аудита) | `grade_comments.author_account_id`, `lab_grades.updated_by_account_id`, индекс `lab_grades(lab_id)`; аудит скачиваний — этап 6 | + +--- + +## 5. Агенты + +| Агент | Зона | Границы | +|---|---|---| +| `lms-orchestrator` | контракты, ADR, merge, review | не пишет массово код | +| `lms-db` | миграции и тесты миграций | только `db/migration/**` и фикстуры | +| `lms-identity` | identity-service | только identity и его тесты | +| `lms-academic` | academic-service | только academic и его тесты | +| `lms-progress` | progress-service, профиль, сдачи | только progress и его тесты | +| `lms-web` | web, UI профиля, кабинеты | только `web/**` | +| `lms-security` | `libs/security-jwt`, gateway, security-review | без бизнес-логики | +| `lms-qa` | матрица ролей, upgrade, smoke | production-код не правит | +| `lms-docs` | docs, deploy/runbook | только документация | + +--- + +## 6. Skills + +- `lms-context` — инварианты и карта модулей. +- `lms-migrations` — Flyway, двухфазные миграции, preflight, legacy-БД. +- `lms-authz` — ролевая матрица, internal access, negative-тесты. +- `lms-identity-domain` — группы, статусы, история, импорт, повышение. +- `lms-academic-domain` — курсы, лабы, материалы, аудитории, активы. +- `lms-web-ui` — Thymeleaf/Bootstrap/JS, формы, навигация. +- `lms-testing` — Testcontainers, MockMvc с JWT, фикстуры. +- `lms-release` — lint/detekt/test, bootJar, `run.sh`, smoke. + +--- + +## 7. Контроль качества + +1. Миграции на пустой БД и на копии текущей; preflight-отчёт. +2. Security-матрица: аноним, студент свой/чужой, преподаватель свой/чужой, + админ, архивный/выпущенный. +3. Профиль: чужой курс/студент/сдача → `403/404`; все попытки скачиваются; + комментарии с автором; нет N+1. +4. `./gradlew ktlintCheck detekt test` после каждого этапа. +5. UI-smoke для четырёх ролей, старые и новые URL, публичные/приватные активы. +6. Аудит скачиваний и чувствительных действий. +7. Backup: обязательный перед деплоем, ежедневный, с проверкой restore. +8. Документация и runbook миграций. + +--- + +## 8. Риски и открытые вопросы + +- **Большой объём одного релиза.** Снижаем контрактами, merge gate и независимым QA. +- **Межсервисная авторизация.** Самый сложный участок; ADR до кода. +- **Перенос данных.** Нужны preflight и копия БД; группы с нестандартными именами + и дубли номеров лаб могут заблокировать `NOT NULL`. +- **Активы.** Сейчас публичны и не связаны со статьями; нужна миграция связей. +- **JWT 30 дней.** Средний вариант блокировки оставляет остаточный риск для + прямого API; фиксируем в документации. +- **Комментарии без автора.** Старые записи останутся с «автор не указан». +- **Архивные студенты.** Открывать ли профиль после отвязки группы — уточнить + на этапе 3; по умолчанию история курса доступна, если студент был в курсе. + +--- + +## 9. Статус реализации + +- **Этап 0 — выполнен.** Схема, contract API профиля, preflight, ADR, + проверка на копии БД. Отчёт: [10-stage0-report.md](10-stage0-report.md). +- **Этап 1 — выполнен.** Роли, группы-когорты, статусы, история, импорт v2, + админ-UI. Отчёт: [11-stage1-report.md](11-stage1-report.md). +- **Этап 2 — следующий.** Курсы, преподаватели, привязка групп, лабораторные + и материалы курса C, активы, копирование лаб. +- Этапы 3–6 — по плану. + +## 10. Definition of Done + +- Все решения раздела 1 реализованы. +- Текущие лабы и материалы работают в курсе C. +- Преподаватель видит только свои курсы и профили своих студентов. +- Профиль показывает статистику, комментарии и все попытки; файлы скачиваются. +- Повышение, академотпуск, выпуск и архив работают по ТЗ. +- Security-матрица и миграционный upgrade-тест зелёные. +- Backup перед деплоем обязателен; restore проверен на staging. +- Документация, deploy/runbook и smoke обновлены. diff --git a/docs/09-teacher-student-profile.md b/docs/09-teacher-student-profile.md new file mode 100644 index 0000000..0c08e2d --- /dev/null +++ b/docs/09-teacher-student-profile.md @@ -0,0 +1,285 @@ +# 09. Контракт API: профиль студента курса + +**Статус:** контракт этапа 0, реализация — этапы 3 и 5. +**Связанные документы:** [07-groups-courses.md](07-groups-courses.md), +[08-groups-courses-plan.md](08-groups-courses-plan.md), +[ADR 0001](adr/0001-groups-courses-foundation.md). + +## 1. Назначение + +Преподаватель своего курса открывает профиль студента и видит: + +- статистику по лабораторным курса; +- комментарии преподавателей по каждой лабе; +- все попытки сдачи и может скачать файлы любой попытки. + +Профиль ограничен одним курсом. Он не заменяет ведомость и не даёт доступа +к другим курсам или чужим сдачам. + +## 2. Авторизация и скоуп + +| Роль | Доступ | +|---|---| +| `ADMIN` | любой студент любого курса | +| `TEACHER` | только активный преподаватель курса (`course_teachers.removed_at IS NULL`) и только студент группы, привязанной к курсу (`course_groups.removed_at IS NULL`) | +| `STUDENT` | только свой профиль и только опубликованные оценки/комментарии (опционально) | +| Аноним | нет | + +Проверки выполняются на сервисах: + +1. JWT валиден, роль допустима. +2. Курс существует и не архивирован (для преподавателя). +3. Для `TEACHER` есть активная запись в `course_teachers`. +4. Студент существует, его аккаунт не архивный. +5. Группа студента привязана к курсу (`course_groups.removed_at IS NULL`). +6. Для скачивания сдача принадлежит этому студенту и лабе этого курса. + +Чужой курс, студент или сдача → `403`/`404` без раскрытия данных. + +## 3. Основной endpoint + +```http +GET /api/v1/courses/{courseId}/students/{studentId}/profile +Authorization: Bearer +``` + +Параметры: + +- `courseId` — UUID курса; +- `studentId` — UUID аккаунта студента (`students.account_id`). + +Ответ `200 application/json`: + +```json +{ + "course": { + "id": "3f2a7c1e-8b4d-4a6f-9c2e-1d5b7a9f0c3e", + "courseNo": 1, + "title": "Промышленное программирование на языке C" + }, + "student": { + "id": "7a1b2c3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", + "studentNo": "123456", + "fullName": "Иванов Иван Иванович", + "groupId": "11111111-2222-3333-4444-555555555555", + "groupName": "ПИ-21БО", + "status": "ACTIVE", + "enrollmentYear": 2025 + }, + "summary": { + "labsTotal": 8, + "submitted": 6, + "graded": 5, + "averageScore": 4.2, + "defensesTotal": 5, + "defensesReady": 3 + }, + "labs": [ + { + "labId": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee", + "number": 1, + "title": "Введение в C", + "deadlineAt": "2026-10-01T20:59:00Z", + "variant": { + "id": "bbbbbbbb-cccc-dddd-eeee-ffffffffffff", + "number": "3", + "title": "Вариант 3" + }, + "level": { + "id": "cccccccc-dddd-eeee-ffff-000000000000", + "code": "base", + "title": "Базовый" + }, + "attemptsCount": 2, + "lastSubmissionAt": "2026-09-28T18:12:00Z", + "grade": { + "score": 4.5, + "published": true, + "generalComment": "Хорошая работа, но есть замечания по стилю.", + "updatedBy": { + "id": "dddddddd-eeee-ffff-0000-111111111111", + "name": "Петров Пётр Петрович" + } + }, + "comments": [ + { + "id": "eeeeeeee-ffff-0000-1111-222222222222", + "text": "Разделите функцию на части.", + "tone": "neutral", + "author": { + "id": "dddddddd-eeee-ffff-0000-111111111111", + "name": "Петров Пётр Петрович" + }, + "createdAt": "2026-09-29T09:30:00Z" + } + ], + "defense": { + "sessionId": "ffffffff-0000-1111-2222-333333333333", + "status": "ready", + "readyReason": "timeout", + "code": null, + "assignmentMd": "Дополнительное задание..." + }, + "submissions": [ + { + "id": "00000000-1111-2222-3333-444444444444", + "seq": 1, + "createdAt": "2026-09-27T16:00:00Z", + "files": ["main.c", "Makefile"], + "downloadUrl": "/api/v1/courses/3f2a7c1e-8b4d-4a6f-9c2e-1d5b7a9f0c3e/submissions/00000000-1111-2222-3333-444444444444/download" + }, + { + "id": "00000000-5555-6666-7777-888888888888", + "seq": 2, + "createdAt": "2026-09-28T18:12:00Z", + "files": ["main.c", "Makefile", "README.md"], + "downloadUrl": "/api/v1/courses/3f2a7c1e-8b4d-4a6f-9c2e-1d5b7a9f0c3e/submissions/00000000-5555-6666-7777-888888888888/download" + } + ] + } + ] +} +``` + +Правила: + +- `labs` — все лабы курса для преподавателя/админа; для студента — только + опубликованные. +- `grade` и `comments` для студента отдаются только если оценка опубликована + (`grade_publications` / `grade_publication_students`). +- `comments` включает общий комментарий? Нет: общий комментарий — поле + `grade.generalComment`; отдельные комментарии — массив `comments`. +- `author` может быть `null`, если автор не сохранён (старые данные). +- `averageScore` — среднее по выставленным оценкам, `null` если оценок нет. +- `submissions` отсортированы по `seq`; файлы — имена без пути. +- `downloadUrl` относительный; web-BFF добавляет авторизацию. + +## 4. Скачивание попытки + +```http +GET /api/v1/courses/{courseId}/submissions/{submissionId}/download +Authorization: Bearer +``` + +- Возвращает ZIP со всеми файлами попытки. +- Проверки: сдача принадлежит студенту и лабе курса; преподаватель активен в + курсе; админ — всегда. +- Имя файла: `submission-{labId-prefix}-try{seq}.zip`. +- Ошибки: `403` — чужая сдача/курс, `404` — сдача не найдена. + +Опционально: + +```http +GET /api/v1/courses/{courseId}/submissions/{submissionId}/files/{filename} +``` + +- Отдаёт один файл. +- Имя проверяется на path traversal; файл должен быть в каталоге попытки. + +## 5. Внутренние эндпоинты + +Не маршрутизируются gateway, защищены `X-Internal-Token` и пользовательским JWT +там, где он нужен. + +### identity + +```http +GET /api/v1/internal/students/{studentId}/context +``` + +```json +{ + "accountId": "7a1b2c3d-...", + "role": "STUDENT", + "accountStatus": "ACTIVE", + "studentStatus": "ACTIVE", + "studentNo": "123456", + "fullName": "Иванов Иван Иванович", + "groupId": "11111111-...", + "groupName": "ПИ-21БО", + "enrollmentYear": 2025 +} +``` + +### academic + +```http +GET /api/v1/internal/courses/{courseId}/access?accountId={accountId}&groupId={groupId} +``` + +```json +{ + "courseId": "3f2a7c1e-...", + "courseStatus": "ACTIVE", + "teacherActive": true, + "groupAttached": true +} +``` + +```http +GET /api/v1/internal/courses/{courseId}/labs +``` + +Возвращает лабы курса с `id`, `number`, `title`, `deadlineAt`, `status`, +`pointsMax`, `variant/level` при необходимости. + +## 6. Ошибки + +| Код | Когда | +|---|---| +| `401` | нет/просрочен JWT | +| `403` | роль не подходит, чужой курс, преподаватель не активен, студент не в группе курса | +| `404` | курс, студент, лаба или сдача не найдены | +| `409` | профиль недоступен из-за конфликта данных (например, студент без группы) | + +Тело ошибки — общий `ApiError` проекта. + +## 7. Производительность + +- Один агрегирующий запрос на профиль, без N+1. +- Пакетная выборка по списку `labId`: + - оценки — `lab_grades` по `student_id` и `lab_id IN (...)`; + - комментарии — `grade_comments` по `student_id` и `lab_id IN (...)`; + - сдачи — `submissions` по `student_id` и `lab_id IN (...)`; + - защиты — `defense_sessions` по `student_id` и `lab_id IN (...)`; + - публикации — `grade_publications`/`grade_publication_students` по группе и лабам. +- Список лаб курса кэшируется на время запроса. +- Для больших курсов — пагинация попыток (по умолчанию все, но с лимитом). +- Индексы: `submissions(student_id, lab_id, seq)` уже уникален; + `grade_comments(student_id, lab_id)` уже есть; `lab_grades(lab_id)` добавлен; + `defense_sessions(student_id, lab_id)` уникален. + +## 8. UI + +- В ведомости группы ФИО — ссылка на профиль. +- В колонке действий — кнопка «Профиль». +- Маршрут преподавателя: `/teacher/courses/{courseNo}/students/{studentId}`. +- Админ: `/admin/courses/{courseNo}/students/{studentId}` или общий маршрут + с проверкой роли. +- В профиле: + - карточка студента; + - сводка; + - таблица лаб; + - блок комментариев; + - список попыток с кнопками скачивания. +- Статус защиты: «готова» / «кончилось время» (timeout). + +## 9. Тесты + +- `ADMIN` видит любого студента. +- `TEACHER` своего курса — `200`; чужого курса — `403`. +- `TEACHER` чужого студента — `403`. +- `STUDENT` свой профиль — `200`; чужой — `403`. +- Аноним — `401`. +- Скачивание своей попытки — `200`; чужой — `403`. +- Студент не видит неопубликованные оценки/комментарии. +- Все попытки возвращаются, а не только последняя. +- Комментарии содержат автора или `null`. +- Запрос не делает N+1 (проверка по числу SQL-запросов в интеграционном тесте). + +## 10. Открытые вопросы + +- Показывать ли профиль для архивных/отвязанных студентов (история или `404`). +- Нужен ли студенческий просмотр своего профиля в первом релизе. +- Формат аудита скачиваний: общий `audit_events` или отдельная таблица. +- Ограничение количества попыток в ответе и пагинация. diff --git a/docs/10-stage0-report.md b/docs/10-stage0-report.md new file mode 100644 index 0000000..5e16343 --- /dev/null +++ b/docs/10-stage0-report.md @@ -0,0 +1,91 @@ +# 10. Отчёт этапа 0: схема, контракты, preflight + +**Дата:** 2026-10-23. +**Статус:** этап 0 выполнен на локальной копии БД; перед production нужен +повторный preflight на свежей копии. + +## 1. Что сделано + +- ADR с ключевыми решениями: [adr/0001-groups-courses-foundation.md](adr/0001-groups-courses-foundation.md). +- Контракт API профиля студента: [09-teacher-student-profile.md](09-teacher-student-profile.md). +- Миграции: + - identity `V4–V9`: роли, группы-когорты, статусы студентов, повышение, + история членства, статусы аккаунтов; + - academic `V6–V9`: курсы, преподаватели, группы курса, `labs.course_id`, + материалы курса/аудитории, индексы активов; + - progress `V10`: автор комментария и последнего изменения оценки. +- Preflight-скрипты: + - `deploy/preflight/groups-courses.sql` — до миграций; + - `deploy/preflight/groups-courses-post.sql` — после миграций/bootstrap. +- UI: статус защиты в ведомости различает «готова» и «кончилось время». +- Резервное копирование: + - `deploy/backup.sh` — БД + решения + материалы, checksums, retention, + `list`, `verify`, `restore`; + - `deploy/deploy.sh up` делает обязательный backup перед обновлением; + - systemd timer для ежедневного backup; + - инструкция `deploy/BACKUP.md`. + +## 2. Проверка на пустой БД + +Все миграции identity, academic и progress применены последовательно на чистой +БД PostgreSQL 18. Ошибок нет. Тесты сервисов на Testcontainers проходят. + +## 3. Проверка на копии локальной БД + +Скопирована локальная БД `lms` (41 аккаунт, 40 студентов, 2 группы, 1 лаба, +3 главы, 16 разделов, 35 статей, 6 комментариев). + +Preflight до миграций: + +- групп, не подходящих под шаблон, нет; +- конфликтующих кортежей когорт нет; +- дублей номеров лабораторных нет; +- дублей slug глав нет; +- студентов без группы нет. + +Миграции применены успешно. + +После миграций: + +- `labs_without_course = 1` — ожидаемо до bootstrap; +- `chapters_without_course = 3` — ожидаемо до bootstrap; +- `courses = 0` — ожидаемо до bootstrap; +- `comments_without_author = 6` — ожидаемо для старых комментариев; +- группы разобраны полностью, `enrollment_year` заполнен. + +## 4. Симуляция bootstrap + +На копии вручную выполнено то, что будет делать bootstrap-сервис: + +1. Создан курс C с владельцем-админом, `course_no = 1`. +2. Админ назначен преподавателем курса. +3. Обе активные группы привязаны к курсу. +4. Все лабы и главы получили `course_id`. +5. Выполнено `ALTER TABLE labs ALTER COLUMN course_id SET NOT NULL`. + +Postflight: + +- `labs_without_course = 0`; +- `chapters_without_course = 0`; +- курс C ровно один; +- `comments_without_author = 6` — старые данные, ожидаемо; +- группы без полного разбора отсутствуют. + +## 5. Тесты и линтеры + +- `./gradlew :services:identity-service:test :services:academic-service:test :services:progress-service:test :web:test` — успешно. +- `./gradlew ktlintCheck detekt` — успешно. +- `node --check web/src/main/resources/static/js/admin-group.js` — успешно. +- `bash -n deploy/backup.sh deploy/deploy.sh` — успешно. +- `./deploy/backup.sh backup` на локальной БД — успешно; `verify` проверил + `db.dump`, `submissions.tar.gz`, `assets.tar.gz` по SHA256. +- `git diff --check` — успешно. + +## 6. Что осталось на этап 2 + +- Реализовать bootstrap-сервис курса C (создание курса, привязка групп, + назначение админа, перенос лаб/глав). +- Выбрать механизм post-bootstrap `SET NOT NULL` (CLI, admin endpoint или + отдельная Flyway-локация). +- Связать существующие активы со статьями при миграции контента. +- Повторить preflight на свежей копии production-БД перед деплоем. diff --git a/docs/README.md b/docs/README.md index bcaf253..a34c8b8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -15,6 +15,11 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid). | [05-states.md](05-states.md) | Диаграммы состояний: защита, публикация оценок, статус ЛР, публикация материалов | | [06-authoring.md](06-authoring.md) | Работа с учебными материалами: структура, markdown-редактор, импорт zip | | [07-groups-courses.md](07-groups-courses.md) | ТЗ: группы/потоки, курсы и преподаватели, повышение курса, академический отпуск, интеграция с «Контингент» | +| [08-groups-courses-plan.md](08-groups-courses-plan.md) | План реализации ТЗ 07: решения, этапы 0–6, профиль студента курса, агенты и skills | +| [09-teacher-student-profile.md](09-teacher-student-profile.md) | Контракт API профиля студента курса для преподавателя | +| [10-stage0-report.md](10-stage0-report.md) | Отчёт этапа 0: миграции, preflight и проверка на копии БД | +| [11-stage1-report.md](11-stage1-report.md) | Отчёт этапа 1: роли, группы-когорты, статусы, история, импорт v2 | +| [ADR 0001](adr/0001-groups-courses-foundation.md) | Ключевые архитектурные решения: курс C, миграции, доступ, статусы, повышение | ## Кратко о системе diff --git a/docs/adr/0001-groups-courses-foundation.md b/docs/adr/0001-groups-courses-foundation.md new file mode 100644 index 0000000..7e7372a --- /dev/null +++ b/docs/adr/0001-groups-courses-foundation.md @@ -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.