docs: этап 2 — курсы, bootstrap и лабораторные
This commit is contained in:
150
docs/12-stage2-report.md
Normal file
150
docs/12-stage2-report.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# 12. Отчёт этапа 2: academic (курсы и лабораторные)
|
||||
|
||||
**Дата:** 2026-10-27.
|
||||
**Статус:** этап 2 выполнен; этап 3 (доступ и профиль студента курса) — следующий.
|
||||
|
||||
## 1. Что сделано
|
||||
|
||||
### Курсы
|
||||
|
||||
- Добавлены таблицы `courses`, `course_teachers`, `course_groups` (миграция `V6`).
|
||||
- `courses.course_no` — `BIGINT GENERATED ALWAYS AS IDENTITY`, уникальный номер
|
||||
для человекочитаемых URL; UUID остаётся первичным ключом.
|
||||
- CRUD курсов: создание, просмотр, переименование, перевод в архив и возврат
|
||||
в работу. Название нормализуется (trim), пустое заменяется на «Новый курс».
|
||||
- Название курса уникально без учёта регистра (миграция `V10`, индекс по
|
||||
`lower(title)`); повторное создание возвращает `409`.
|
||||
- Назначение и снятие преподавателей, привязка и отвязка групп хранятся
|
||||
с историей: снятие/отвязка проставляют `removed_at`, запись не удаляется.
|
||||
Повторное назначение после снятия создаёт новую активную запись.
|
||||
- Для архивированного курса запрещены новые назначения преподавателей
|
||||
и привязка групп.
|
||||
|
||||
### Bootstrap курса C
|
||||
|
||||
- `CourseBootstrapService` идемпотентно создаёт (или находит) курс
|
||||
«Промышленное программирование на языке C», назначает владельцем текущего
|
||||
админа и переносит в него существующие данные.
|
||||
- Конкурентные запуски сериализуются advisory-локом
|
||||
`pg_advisory_xact_lock(hashtext('academic.course.bootstrap'))`.
|
||||
- Перед переносом проверяются дубли номеров лабораторных без курса. При дублях
|
||||
bootstrap завершается `409`, транзакция откатывается, курс не создаётся.
|
||||
- Все активные группы, переданные из web, привязываются к курсу (повторный
|
||||
запуск не создаёт дублей).
|
||||
- Всем лабораторным и главам без курса проставляется `course_id = C`;
|
||||
у курсовых глав `audience` сбрасывается в `NULL`.
|
||||
- После переноса, если лабораторных без курса не осталось, сервис в рантайме
|
||||
выполняет `ALTER TABLE academic.labs ALTER COLUMN course_id SET NOT NULL`.
|
||||
Это **post-bootstrap шаг, а не Flyway-файл**: обычный старт Flyway не может
|
||||
дождаться создания курса и назначения владельца.
|
||||
|
||||
### Лабораторные
|
||||
|
||||
- `labs.course_id` добавлен nullable-миграцией `V7`; уникальность номера —
|
||||
в рамках курса: `(number, course_id)`.
|
||||
- Создание и импорт лабораторной требуют курс; архивированный курс отклоняется
|
||||
с `400`.
|
||||
- Список лабораторных фильтруется по `courseId`; API и UI показывают номер
|
||||
и название курса у каждой лабораторной.
|
||||
- Копирование лабораторной в другой курс (`LabCopyService`): копируются
|
||||
описание, уровни и варианты с новыми id, копия создаётся черновиком.
|
||||
Оценки, сдачи, защиты и назначения вариантов не копируются.
|
||||
- Копирование запрещено в тот же курс и в архивированный курс; занятый номер
|
||||
в целевом курсе даёт `409`.
|
||||
|
||||
### Админ-UI
|
||||
|
||||
- `/admin/courses` — список курсов с номером, названием, статусом и счётчиками
|
||||
преподавателей, групп и лабораторных; создание курса; блок первичного
|
||||
bootstrap, пока курсов нет.
|
||||
- `/admin/courses/{id}` — карточка курса: архивирование/возврат, привязка
|
||||
и отвязка групп, назначение и снятие преподавателей по UUID аккаунта,
|
||||
список лабораторных курса.
|
||||
- `/admin/labs` — фильтр по курсу, колонка курса, выбор курса (обязательный)
|
||||
в формах импорта и создания.
|
||||
- `/admin/labs/{id}` — карточка лабораторной показывает курс; кнопка
|
||||
«Копировать в курс» открывает форму выбора целевого курса, номера и названия.
|
||||
- В навигацию админа добавлен пункт «Курсы».
|
||||
|
||||
### Миграции
|
||||
|
||||
- `V6__courses.sql` — курсы, преподаватели, группы курса.
|
||||
- `V7__labs_course.sql` — `labs.course_id`, уникальность `(number, course_id)`.
|
||||
- `V8__chapters_course.sql` — `chapters.course_id/audience`, частичные индексы slug.
|
||||
- `V9__article_assets_indexes.sql` — индекс `article_assets(article_id)`.
|
||||
- `V10__course_title_unique.sql` — уникальный индекс `lower(title)` по курсам.
|
||||
|
||||
## 2. API
|
||||
|
||||
Все эндпоинты курсов доступны только роли `ADMIN`.
|
||||
|
||||
| Метод | Путь | Назначение |
|
||||
|---|---|---|
|
||||
| `GET` | `/api/v1/courses` | список курсов со счётчиками |
|
||||
| `POST` | `/api/v1/courses` | создать курс, владелец — текущий админ |
|
||||
| `GET` | `/api/v1/courses/{id}` | карточка курса с историей преподавателей и групп |
|
||||
| `PUT` | `/api/v1/courses/{id}` | изменить название и/или статус |
|
||||
| `POST` | `/api/v1/courses/{id}/teachers` | назначить преподавателя |
|
||||
| `DELETE` | `/api/v1/courses/{id}/teachers/{accountId}` | снять преподавателя |
|
||||
| `POST` | `/api/v1/courses/{id}/groups` | привязать группу |
|
||||
| `DELETE` | `/api/v1/courses/{id}/groups/{groupId}` | отвязать группу |
|
||||
| `POST` | `/api/v1/courses/bootstrap` | создать курс C и перенести данные |
|
||||
| `GET` | `/api/v1/labs?courseId=…` | список лабораторных, опционально по курсу |
|
||||
| `POST` | `/api/v1/labs` | создать лабораторную с обязательным `courseId` |
|
||||
| `POST` | `/api/v1/labs/import?courseId=…` | импорт ZIP в указанный курс |
|
||||
| `POST` | `/api/v1/labs/{id}/copy` | копировать лабораторную в другой курс |
|
||||
|
||||
`LabSummary` и `LabDetail` дополнены полями `courseId`, `courseNo`,
|
||||
`courseTitle`. В браузерных URL курса используется `course_no`; API работает
|
||||
с UUID.
|
||||
|
||||
## 3. Порядок bootstrap
|
||||
|
||||
1. Админ открывает `/admin/courses`. Пока курсов нет, показывается блок
|
||||
первичной настройки.
|
||||
2. Web передаёт в `POST /api/v1/courses/bootstrap` название (необязательно)
|
||||
и id всех активных групп.
|
||||
3. Сервис берёт advisory-лок, проверяет UUID групп, находит или создаёт курс
|
||||
по названию без учёта регистра (владелец — текущий админ).
|
||||
4. Привязывает активные группы, проверяет дубли номеров лабораторных без курса.
|
||||
5. Проставляет `course_id` лабораторным и главам.
|
||||
6. Если лабораторных без курса не осталось, выполняет `SET NOT NULL` для
|
||||
`labs.course_id`.
|
||||
7. Возвращает `courseId`, `courseNo`, название и счётчики: сколько групп
|
||||
привязано, лабораторных и глав перенесено, сколько осталось без курса.
|
||||
8. Повторный запуск безопасен: существующий курс переиспользуется, уже
|
||||
привязанные группы и перенесённые данные не дублируются.
|
||||
|
||||
## 4. Тесты
|
||||
|
||||
Добавлены интеграционные тесты на PostgreSQL (Testcontainers):
|
||||
|
||||
- `CourseServiceIntegrationTest` (6 тестов): отказ bootstrap при дублях
|
||||
номеров без создания курса, создание курса и перенос групп/лабораторных/глав,
|
||||
идемпотентность bootstrap, генерация уникальных `course_no`, запрет
|
||||
дубликата названия без учёта регистра, история назначений преподавателей
|
||||
и групп, отказ при некорректном UUID группы.
|
||||
- `LabCopyIntegrationTest` (5 тестов): копирование с уровнями и вариантами,
|
||||
обязательный курс при создании, запрет копирования в тот же курс, в
|
||||
архивированный курс и создание лабораторной в архивированном курсе.
|
||||
|
||||
Прогон 2026-10-27: `./gradlew :services:academic-service:test :web:test` —
|
||||
успешно, в новых наборах 0 failures / 0 errors. `git diff --check` — успешно.
|
||||
|
||||
## 5. Известные пробелы (этап 3)
|
||||
|
||||
- **Материалы курса и аудитории.** Схема `chapters.course_id/audience` готова
|
||||
(`V8`), но URL `/learn/course/{course_no}/...` и проверка аудитории ещё не
|
||||
реализованы: читалка материалов пока работает по прежним глобальным правилам.
|
||||
- **Активы.** `V9` добавляет только индекс; наследуемая видимость активов
|
||||
от статьи → раздела → главы и проверка доступа при выдаче файлов — этап 3.
|
||||
- **Проверка аккаунтов преподавателей.** Academic принимает UUID без обращения
|
||||
к identity: не проверяются существование аккаунта, роль `TEACHER` и статус
|
||||
`ACTIVE`. Нужна внутренняя проверка identity при назначении.
|
||||
- **Кабинет преподавателя и скоупинг.** API курсов пока admin-only; кабинет
|
||||
`/teacher/...`, доступ преподавателя только к своим курсам и студентам,
|
||||
профиль студента курса и negative-тесты «чужой курс/студент/сдача» —
|
||||
этап 3.
|
||||
- **Повторный bootstrap из UI.** Кнопка bootstrap показывается только когда
|
||||
курсов нет; повторный запуск возможен через API. Группы, появившиеся позже,
|
||||
привязываются вручную на странице курса.
|
||||
Reference in New Issue
Block a user