12 KiB
12 KiB
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
- Админ открывает
/admin/courses. Пока курсов нет, показывается блок первичной настройки. - Web передаёт в
POST /api/v1/courses/bootstrapназвание (необязательно) и id всех активных групп. - Сервис берёт advisory-лок, проверяет UUID групп, находит или создаёт курс по названию без учёта регистра (владелец — текущий админ).
- Привязывает активные группы, проверяет дубли номеров лабораторных без курса.
- Проставляет
course_idлабораторным и главам. - Если лабораторных без курса не осталось, выполняет
SET NOT NULLдляlabs.course_id. - Возвращает
courseId,courseNo, название и счётчики: сколько групп привязано, лабораторных и глав перенесено, сколько осталось без курса. - Повторный запуск безопасен: существующий курс переиспользуется, уже привязанные группы и перенесённые данные не дублируются.
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. Группы, появившиеся позже, привязываются вручную на странице курса.