docs: план, ADR и контракты групп и курсов
This commit is contained in:
@@ -1,8 +1,37 @@
|
|||||||
# 07. Группы, курсы и преподаватели, повышение, интеграция (ТЗ)
|
# 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. Зафиксированные решения
|
||||||
|
|
||||||
1. Сущность **курс** живёт в `academic-service`; владелец — **админ**, преподавателей на курс
|
1. Сущность **курс** живёт в `academic-service`; владелец — **админ**, преподавателей на курс
|
||||||
@@ -65,7 +94,10 @@
|
|||||||
|
|
||||||
### progress
|
### progress
|
||||||
Данные привязаны к `lab_id` и студенту (сдачи, оценки, комментарии, защиты, назначения вариантов,
|
Данные привязаны к `lab_id` и студенту (сдачи, оценки, комментарии, защиты, назначения вариантов,
|
||||||
публикации). Схема **не меняется**: доступ регулируется по цепочке `lab → course → (преподаватель | группа)`.
|
публикации). Схема **в основном не меняется**: доступ регулируется по цепочке
|
||||||
|
`lab → course → (преподаватель | группа)`. Для профиля студента курса добавляется
|
||||||
|
`grade_comments.author_account_id` (nullable) и индекс `(student_id, lab_id)`;
|
||||||
|
остальное — рантайм-проверки.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -97,13 +129,15 @@ erDiagram
|
|||||||
переводы, академ, возврат, выпуск, отчисление, восстановление.
|
переводы, академ, возврат, выпуск, отчисление, восстановление.
|
||||||
|
|
||||||
### academic
|
### 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_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)`.
|
- `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`
|
- Материалы: только у **корня** (`chapters`) добавляем `+ course_id` (nullable) и `+ audience`
|
||||||
(`ALL | STUDENTS | TEACHERS`, применяется при `course_id IS NULL`); `sections`/`articles` наследуют
|
(`ALL | STUDENTS | TEACHERS`, применяется при `course_id IS NULL`); `sections`/`articles` наследуют
|
||||||
видимость от главы.
|
видимость от главы. Текущие материалы переносятся в курс C и получают `course_id`.
|
||||||
- `chapters.slug`: снять глобальный UNIQUE, ввести два частичных индекса (глобальные и внутри курса).
|
- `chapters.slug`: снять глобальный UNIQUE, ввести два частичных индекса (глобальные и внутри курса).
|
||||||
- `article_assets`: видимость наследуется от статьи → раздела → главы (см. §6).
|
- `article_assets`: видимость наследуется от статьи → раздела → главы (см. §6).
|
||||||
|
|
||||||
@@ -175,6 +209,7 @@ SELECT student_id, group_id, 'ENROLL', enrolled_at FROM enrollments;
|
|||||||
```sql
|
```sql
|
||||||
CREATE TABLE courses (
|
CREATE TABLE courses (
|
||||||
id uuid PRIMARY KEY DEFAULT uuidv7(),
|
id uuid PRIMARY KEY DEFAULT uuidv7(),
|
||||||
|
course_no bigint GENERATED ALWAYS AS IDENTITY UNIQUE NOT NULL,
|
||||||
title varchar(200) NOT NULL,
|
title varchar(200) NOT NULL,
|
||||||
owner_account_id uuid NOT NULL,
|
owner_account_id uuid NOT NULL,
|
||||||
status varchar(16) NOT NULL DEFAULT 'ACTIVE',
|
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;
|
CREATE UNIQUE INDEX course_groups_active_uidx ON course_groups(course_id, group_id) WHERE removed_at IS NULL;
|
||||||
```
|
```
|
||||||
|
|
||||||
**V7 — лабораторные привязываем к курсу**
|
**V7 — лабораторные привязываем к курсу (nullable-фаза)**
|
||||||
```sql
|
```sql
|
||||||
-- «Курс по умолчанию» создаётся в сервисе (title='Общие лабораторные', owner=первый ADMIN)
|
-- Курс «Промышленное программирование на языке C» создаётся bootstrap-сервисом
|
||||||
|
-- после первого входа админа (owner = этот админ). В V7 только добавляем nullable-колонку.
|
||||||
ALTER TABLE labs ADD COLUMN course_id uuid REFERENCES courses(id) ON DELETE RESTRICT;
|
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 DROP CONSTRAINT labs_number_semester_unique;
|
||||||
ALTER TABLE labs ALTER COLUMN course_id SET NOT NULL;
|
CREATE UNIQUE INDEX labs_number_course_uidx ON labs (number, course_id);
|
||||||
DROP CONSTRAINT labs_number_semester_unique;
|
|
||||||
ALTER TABLE labs ADD CONSTRAINT labs_number_course_unique UNIQUE (number, course_id);
|
|
||||||
CREATE INDEX labs_course_idx ON labs (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 — материалы: курс и аудитория, слаг**
|
**V8 — материалы: курс и аудитория, слаг**
|
||||||
```sql
|
```sql
|
||||||
ALTER TABLE chapters ADD COLUMN course_id uuid REFERENCES courses(id) ON DELETE CASCADE;
|
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)
|
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;
|
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_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;
|
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 — активы: наследуемая видимость**
|
**V9 — активы: наследуемая видимость**
|
||||||
Признак видимости актив наследует от статьи→раздела→главы (course/audience). Требуется проверка доступа в
|
Признак видимости актив наследует от статьи→раздела→главы (course/audience). Требуется проверка доступа в
|
||||||
маршруте выдачи активов (см. §6). Отдельных колонок достаточно опционально; минимум — корректная авторизация.
|
маршруте выдачи активов (см. §6). Отдельных колонок достаточно опционально; минимум — корректная авторизация.
|
||||||
|
|
||||||
|
**V10 — курс C: завершение привязки лаб (post-bootstrap шаг)**
|
||||||
|
```sql
|
||||||
|
-- Выполняется после bootstrap-сервиса, когда все лабы получили course_id.
|
||||||
|
-- Это отдельный шаг/миграция: обычный Flyway-старт применяет миграции раньше
|
||||||
|
-- бизнес-логики и не может дождаться создания курса и назначения владельца.
|
||||||
|
ALTER TABLE labs ALTER COLUMN course_id SET NOT NULL;
|
||||||
|
```
|
||||||
|
|
||||||
### progress
|
### 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).
|
2. Доступ отзывается **немедленно** (не хранить список курсов/групп в JWT).
|
||||||
3. Файлы: преподаватель — только сдачи своих курсов; студент — только свои; **активы** — по видимости материала.
|
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. Материалы, публичный доступ и активы
|
## 6. Материалы, публичный доступ и активы
|
||||||
@@ -340,14 +437,20 @@ CREATE UNIQUE INDEX chapters_slug_course_uidx ON chapters (course_id, slug) WHER
|
|||||||
3. Одноимённые группы — уникальность по году набора; отображение с годом.
|
3. Одноимённые группы — уникальность по году набора; отображение с годом.
|
||||||
4. Академ vs групповой курс — правило перевода и автосоздание целевой группы.
|
4. Академ vs групповой курс — правило перевода и автосоздание целевой группы.
|
||||||
5. Публичные `ALL`-материалы и **активы** — проверка видимости, иначе утечка/ломка.
|
5. Публичные `ALL`-материалы и **активы** — проверка видимости, иначе утечка/ломка.
|
||||||
6. `chapters.slug`: глобальный UNIQUE → частичные индексы (глобальные/курсовые) + новая URL-схема,
|
6. `chapters.slug`: глобальный UNIQUE → частичные индексы (глобальные/курсовые) + новая URL-схема;
|
||||||
совместимость старых ссылок.
|
старые ссылки на текущие материалы не поддерживаются.
|
||||||
7. Миграция существующих лаб/материалов без курса (курс по умолчанию / `audience='ALL'`).
|
7. Миграция существующих лаб/материалов в курс C; «общих» лаб нет.
|
||||||
8. Немедленный отзыв доступа (преподаватель/группа/публикация материала).
|
8. Немедленный отзыв доступа (преподаватель/группа/публикация материала).
|
||||||
9. Аудит-лог: переводы, академ, повышение, назначения преподавателей, публикации, архив/удаление.
|
9. Аудит-лог: переводы, академ, повышение, назначения преподавателей, публикации, архив/удаление.
|
||||||
10. Производительность: индексы `course_id`/`group_id`, пагинация материалов и групп.
|
10. Производительность: индексы `course_id`/`group_id`, пагинация материалов и групп.
|
||||||
11. Бэкфилл `enrollment_year` для существующих групп — приблизительный, требует подтверждения админом.
|
11. Бэкфилл `enrollment_year` для существующих групп — приблизительный, требует подтверждения админом.
|
||||||
12. Лабы: уникальность номера — в рамках курса (`(number, course_id)`), прежний ключ по семестру снимается.
|
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.
|
0. Схема БД и миграции (аддитивно, с бэкфиллом) — по §4.
|
||||||
1. Группы: поля, разбор имени, «одна активная группа», импорт Excel, перемещение студента (админ).
|
1. Группы: поля, разбор имени, «одна активная группа», импорт Excel, перемещение студента (админ).
|
||||||
2. Курсы + роль `TEACHER` + назначения (с историей) + скоуп-авторизация и тесты.
|
2. Курсы + роль `TEACHER` + назначения (с историей) + скоуп-авторизация и тесты.
|
||||||
|
2a. Профиль студента курса: статистика по лабам, комментарии, все попытки, скачивание файлов.
|
||||||
3. Привязка лаб/материалов к курсу, аудитории материалов, публичные `ALL`-маршруты и активы, миграция данных.
|
3. Привязка лаб/материалов к курсу, аудитории материалов, публичные `ALL`-маршруты и активы, миграция данных.
|
||||||
4. Запись/снятие групп на курс.
|
4. Запись/снятие групп на курс.
|
||||||
5. Повышение курса: dry-run, журнал, идемпотентность, откат, выпуск.
|
5. Повышение курса: dry-run, журнал, идемпотентность, откат, выпуск.
|
||||||
|
|||||||
292
docs/08-groups-courses-plan.md
Normal file
292
docs/08-groups-courses-plan.md
Normal file
@@ -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 обновлены.
|
||||||
285
docs/09-teacher-student-profile.md
Normal file
285
docs/09-teacher-student-profile.md
Normal file
@@ -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 <JWT>
|
||||||
|
```
|
||||||
|
|
||||||
|
Параметры:
|
||||||
|
|
||||||
|
- `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 <JWT>
|
||||||
|
```
|
||||||
|
|
||||||
|
- Возвращает 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` или отдельная таблица.
|
||||||
|
- Ограничение количества попыток в ответе и пагинация.
|
||||||
91
docs/10-stage0-report.md
Normal file
91
docs/10-stage0-report.md
Normal file
@@ -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-БД перед деплоем.
|
||||||
@@ -15,6 +15,11 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid).
|
|||||||
| [05-states.md](05-states.md) | Диаграммы состояний: защита, публикация оценок, статус ЛР, публикация материалов |
|
| [05-states.md](05-states.md) | Диаграммы состояний: защита, публикация оценок, статус ЛР, публикация материалов |
|
||||||
| [06-authoring.md](06-authoring.md) | Работа с учебными материалами: структура, markdown-редактор, импорт zip |
|
| [06-authoring.md](06-authoring.md) | Работа с учебными материалами: структура, markdown-редактор, импорт zip |
|
||||||
| [07-groups-courses.md](07-groups-courses.md) | ТЗ: группы/потоки, курсы и преподаватели, повышение курса, академический отпуск, интеграция с «Контингент» |
|
| [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, миграции, доступ, статусы, повышение |
|
||||||
|
|
||||||
## Кратко о системе
|
## Кратко о системе
|
||||||
|
|
||||||
|
|||||||
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