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

362 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–6 выполнены. Этап 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 в `identity.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-матрица и negative-тесты «свой/чужой курс».
- Upgrade-тест миграций.
- Индексы; пагинация и N+1 — follow-up (§ 9.1).
- Аудит скачиваний и чувствительных действий (`identity.audit_events`,
`/admin/audit`).
- CI merge gate: `test ktlintCheck detekt` на каждом push/MR.
- Проверка backup: создание, retention, `verify`, restore на staging.
- Документация, deploy/runbook, smoke.
---
## 4. Ориентировочные миграции
Номера уточняются при реализации, чтобы не конфликтовать с максимумами master
(identity V3, **academic V5**, progress V12). Максимумы этой ветки:
identity V10, **academic V12**, progress V13.
| Сервис | Миграции | Содержание |
|---|---|---|
| identity | V4–V10 | роли, поля групп, статусы студентов, `membership_history`, `promotion_runs/items`, `accounts.status`; `V10` — `audit_events` без внешних ключей и индексы по `occurred_at`/`action` |
| academic | V6–V12 + post-bootstrap step | курсы, преподаватели, группы курса, `labs.course_id` (nullable), материалы курса/аудитории, частичные индексы, активы; `V10` — уникальный индекс `lower(title)` по курсам; `V11` — backfill `audience='ALL'` для глобальных глав; `V12` — backfill связей `article_assets` из markdown-ссылок; `SET NOT NULL` — post-bootstrap шаг вне Flyway |
| progress | V13 | `grade_comments.author_account_id`, `lab_grades.updated_by_account_id`, индекс `lab_grades(lab_id)`; аудит реализован в identity `V10` |
### Переход со старой 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`.
- **Legacy-активы.** Связанные активы наследуют видимость статьи (миграция `V12`),
но файлы без markdown-ссылки остаются публичными; нужна инвентаризация и
привязка или удаление.
- **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 — выполнен.** Права преподавателя на запись (лабораторные,
ведомость, группы, материалы), центральный аудит `identity.audit_events`
с `/admin/audit`, CI merge gate и полный hardening. Проверены прогон
тестов/линтеров (**267**), upgrade миграций с master-схемы (identity
`V4–V10`, academic `V6–V12`, progress `V13`), security-матрица, gateway,
видимость активов и deploy-runbook. Отчёт: [16-stage6-report.md](16-stage6-report.md).
### 9.1. Известные пробелы после этапа 6
- **Legacy-активы без привязки.** Маршрут `/api/v1/content/assets/{name}`
проверяет `article_assets → article → section → chapter`, а `V12`
восстанавливает связи из markdown. Файлы без ссылок в статьях остаются
публичными; нужна инвентаризация и привязка или удаление.
- **Импорт.** Старый формат не поддерживается; ведущие нули номера группы
нормализуются (`01` → `1`).
- **JWT 30 дней.** Web перепроверяет статус каждые 45 секунд, но прямой API
с уже выданным токеном живёт до истечения; нужны короткий TTL + refresh
или revocation.
- **Пагинация и N+1.** Журнал аудита ограничен `limit` без offset/keyset;
часть списков групп, материалов и ведомости отдаётся целиком; на больших
объёмах нужны пагинация и пакетные выборки.
## 10. Definition of Done
- Все решения раздела 1 реализованы.
- Текущие лабы и материалы работают в курсе C.
- Преподаватель видит только свои курсы и профили своих студентов, пишет
в лабы/ведомость/группы/материалы только своего курса.
- Профиль показывает статистику, комментарии и все попытки; файлы скачиваются.
- Повышение, академотпуск, выпуск и архив работают по ТЗ.
- Аудит чувствительных действий пишется в `identity.audit_events` и доступен
админу на `/admin/audit`.
- Security-матрица и миграционный upgrade-тест зелёные.
- CI на каждом push/MR прогоняет `test ktlintCheck detekt`.
- Backup перед деплоем обязателен; restore проверен на staging.
- Документация, deploy/runbook и smoke обновлены.