Files
LMS/docs/08-groups-courses-plan.md

357 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 08. План реализации: группы, курсы, преподаватели
**Статус:** план согласован, этапы 0–5 выполнены. Проверка этапа 6 выполнена
(тесты, upgrade миграций, security, deploy-runbook), но этап 6 закрыт не
полностью: остаются права преподавателя на запись, видимость активов и CI —
см. § 9.1 и [16-stage6-report.md](16-stage6-report.md).
**Основа:** [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. Ориентировочные миграции
Номера уточняются при реализации, чтобы не конфликтовать с максимумами master
(identity V3, **academic V5**, progress V12). Максимумы этой ветки:
identity V9, **academic V11**, progress V13 (+V14 при необходимости аудита).
| Сервис | Миграции | Содержание |
|---|---|---|
| identity | V4–V9 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status` |
| academic | V6–V11 + post-bootstrap step | курсы, преподаватели, группы курса, `labs.course_id` (nullable), материалы курса/аудитории, частичные индексы, активы; `V10` — уникальный индекс `lower(title)` по курсам; `V11` — backfill `audience='ALL'` для глобальных глав; `SET NOT NULL` — post-bootstrap шаг вне Flyway |
| progress | V13 (+ V14 при необходимости аудита) | `grade_comments.author_account_id`, `lab_grades.updated_by_account_id`, индекс `lab_grades(lab_id)`; аудит скачиваний — этап 6 |
### Переход со старой feature-миграции V10 на V13
Старая `progress V10__profile_authors.sql` — временная feature-миграция, её нет
в master.
- **Production / master:** есть только V11 и V12, записи V10 нет — V13 применяется
обычным порядком, ничего делать не нужно.
- **Dev/feature-БД, где `V10__profile_authors.sql` уже применена и имеет
`success=true`:** удалить только эту запись истории, ничего не удаляя из схемы;
V13 идемпотентна и сама создаст недостающие колонки и индекс. Используйте
`flyway repair` или точечно:
`DELETE FROM progress.flyway_schema_history WHERE version = '10' AND script = 'V10__profile_authors.sql' AND success;`.
- **Staging-БД**, которые ненадолго запускали master-коммит `bfdae41` (out-of-order
+ V10), тоже содержат запись V10 — для них действует тот же порядок.
- **Если V10 осталась с `success=false`:** сначала убедитесь, что её частичные
изменения откатились на уровне объектов, затем `flyway repair` или удаление
неуспешной строки; V13 досоздаст недостающее.
- Перед очисткой и деплоем проверьте состояние через
`deploy/preflight/progress-v10-v13.sql`.
- Переход **forward-only**: после V13 откат на сборку без V13 не пройдёт валидацию
Flyway. Не оставляйте `out-of-order` включённым.
- **Нельзя** переставлять V11/V12.
---
## 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 — выполнен.** Курсы и `course_no`, история преподавателей и групп,
bootstrap курса C, привязка лабораторных к курсу, копирование лаб,
админ-UI курсов и лабораторных. Отчёт: [12-stage2-report.md](12-stage2-report.md).
- **Этап 3 — выполнен.** Аккаунты преподавателей, курсовая область видимости,
внутренние API identity с `LMS_INTERNAL_TOKEN`, кабинет и состав групп
преподавателя, профиль студента курса и скачивание всех попыток.
Отчёт: [13-stage3-report.md](13-stage3-report.md).
- **Этап 4 — выполнен.** Повышение (dry-run/apply/rollback, планировщик
31 августа МСК и догон), жизненный цикл студента, архив группы,
усиленное подтверждение удаления, блокировка входа для архивных
и выпущенных групп. Отчёт: [14-stage4-report.md](14-stage4-report.md).
- **Этап 5 — выполнен.** Студенческие страницы курсов, материалы по
`/learn/course/{course_no}/...` и аудитории, публичные материалы `ALL`,
скрытые материалы для преподавателя, перепроверка статуса web-сессии.
Отчёт: [15-stage5-course-materials.md](15-stage5-course-materials.md).
- **Этап 6 — проверка выполнена, закрыт не полностью.** Проверены полный
прогон тестов/линтеров, upgrade миграций с master-схемы, security-матрица,
gateway и deploy-runbook. Отчёт: [16-stage6-report.md](16-stage6-report.md).
Не закрыты: права преподавателя на запись, видимость активов, аудит
скачиваний и CI. Остальные пункты hardening (индексы, backup/restore,
документация, smoke) описаны в отчёте и чек-листе.
### 9.1. Известные пробелы после этапа 6
- **Активы публичны.** `/api/v1/content/assets/{name}` не проверяет курс и
статью; нужна проверка `article_assets → article → section → chapter` и
visibility-aware маршруты.
- **Преподаватель — в основном read-only.** Авторство лабораторных/материалов,
распределение, оценки, комментарии и защита остаются admin-only в UI/API;
ТЗ ожидает преподавательский self-service.
- **Имена групп в кабинете преподавателя.** На странице курса показывается
UUID группы, batch-разрешения имён нет.
- **Аудит.** Есть история членства и журнал повышения, но нет audit-log
скачиваний, просмотров профилей, назначений и публикаций.
- **Импорт.** Старый формат не поддерживается; ведущие нули номера группы
нормализуются (`01` → `1`).
- **JWT 30 дней.** Web перепроверяет статус каждые 45 секунд, но прямой API
с уже выданным токеном живёт до истечения.
- **CI отсутствует.** Гейт — локальный `./gradlew test ktlintCheck detekt`;
автоматического pipeline и upgrade-теста в CI нет.
## 10. Definition of Done
- Все решения раздела 1 реализованы.
- Текущие лабы и материалы работают в курсе C.
- Преподаватель видит только свои курсы и профили своих студентов.
- Профиль показывает статистику, комментарии и все попытки; файлы скачиваются.
- Повышение, академотпуск, выпуск и архив работают по ТЗ.
- Security-матрица и миграционный upgrade-тест зелёные.
- Backup перед деплоем обязателен; restore проверен на staging.
- Документация, deploy/runbook и smoke обновлены.