docs: план, ADR и контракты групп и курсов
This commit is contained in:
@@ -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, журнал, идемпотентность, откат, выпуск.
|
||||
|
||||
Reference in New Issue
Block a user