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

@@ -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, журнал, идемпотентность, откат, выпуск.