Files
LMS/docs/14-stage4-report.md

13 KiB
Raw Blame History

14. Отчёт этапа 4: повышение и жизненный цикл

Дата: 2026-10-27. Статус: этап 4 выполнен; этап 5 (UX и контент курса) — следующий.

1. Повышение

Алгоритм

План строится по активным группам и студентам:

  • Активная группа с курсом меньше предела (БО — 4, МО — 2) переводится на следующий курс: меняются course и имя по шаблону поток-курсномерстепень.
  • Активная группа на последнем курсе получает статус GRADUATED.
  • Активные студенты выпускаемой группы получают GRADUATED.
  • Студенты в академическом отпуске переводятся в параллель следующего набора того же курса: enrollmentYear = год повышения - (курс - 1). Если такой группы нет, она создаётся в плане; при применении — в БД.
  • Студенты в статусах EXPELLED, GRADUATED, ARCHIVED в план не попадают.
  • Группы без курса/степени/потока/номера и студенты без зачисления пропускаются с предупреждением; план при этом не блокируется.

Порядок применения: сначала группы (от старших курсов), затем студенты. Каждый элемент хранит before/after в JSONB. Перед применением все before сверяются с текущим состоянием; при расхождении вся транзакция отменяется с 409, частичного применения нет.

API

Все эндпоинты требуют роль ADMIN:

Метод Путь Назначение
GET /api/v1/promotion список запусков
GET /api/v1/promotion/{id} план/результат с элементами
POST /api/v1/promotion/dry-run построить план; тело { "year": 2026 } необязательно
POST /api/v1/promotion/{id}/apply применить запланированный dry-run
POST /api/v1/promotion/{id}/rollback откатить завершённое применение

Правила:

  • year — 2000–2100; без года берётся текущий.
  • Повторный dry-run за год возвращает существующий план в статусе PLANNED.
  • Применять можно только DRY_RUN в статусе PLANNED. Запуск переходит в APPLY/RUNNING, затем COMPLETED.
  • Один успешный APPLY на год защищён частичным уникальным индексом promotion_runs(year) WHERE mode = 'APPLY' AND status IN ('RUNNING','COMPLETED'); конкурентное применение даёт 409.
  • Повторное применение уже завершённого запуска идемпотентно возвращает результат и не создаёт новых записей истории.
  • Откат доступен только для COMPLETED. Перед откатом проверяется, что текущее состояние совпадает с after; ручные изменения дают 409. Восстанавливаются курс, имя и статус групп, статусы/группы студентов, удаляются созданные планом пустые целевые группы. История не стирается: добавляются записи RETURN с причиной «Откат повышения».

Планировщик

  • PromotionScheduler строит dry-run 31 августа в 00:00 по Europe/Moscow (cron = "0 0 0 31 8 *").
  • При старте сервиса выполняется догон: если сегодня 31 августа или позже и за год ещё нет запусков, dry-run строится сразу.
  • Догон идемпотентен: advisory lock не даёт двум инстансам создать два плана.
  • Планировщик включён по умолчанию; отключается lms.promotion.scheduler-enabled=false (используется в тестах).
  • Автоматически применяется только dry-run. Применение — всегда вручную админом после проверки плана.

2. Жизненный цикл студента

Эндпоинты требуют роль ADMIN:

Метод Путь Тело Результат
POST /api/v1/students/{id}/academic-leave reason? перевод в параллель следующего набора, статус ACADEMIC_LEAVE
POST /api/v1/students/{id}/return groupId, reason? возврат в указанную активную группу, статус ACTIVE
POST /api/v1/students/{id}/expel reason? статус EXPELLED, зачисление сохраняется
POST /api/v1/students/{id}/archive — статус студента ARCHIVED, аккаунт ARCHIVED
POST /api/v1/students/{id}/restore groupId, reason? восстановление из архива/отчисления в активную группу

Правила:

  • Академотпуск доступен только ACTIVE; группа должна иметь заполненные поток, курс, номер и степень. Целевая параллель ищется по когорте и создаётся при отсутствии.
  • Возврат доступен только из ACADEMIC_LEAVE; целевая группа должна быть активной.
  • Отчислить нельзя уже отчисленного, выпущенного или архивного студента. Зачисление остаётся для истории, вход закрывает статус.
  • Архив закрывает и студента, и аккаунт (archived_at); зачисление сохраняется. Восстановление возвращает аккаунт в ACTIVE и переводит студента в выбранную группу.
  • Каждое действие пишет membership_history с типом, причиной и актором.

3. Архив группы, удаление и вход

  • POST /api/v1/groups/{id}/archive переводит группу в ARCHIVED, не меняя статусы студентов.
  • Вход студента блокируется, если его текущая группа ARCHIVED или GRADUATED (AuthService.ensureGroupOpen). Студент без группы может войти, если его собственный статус и аккаунт активны.
  • Удаление пользователя требует параметр confirm, совпадающий с логином или ФИО студента (trim, точное совпадение). Иначе — 400, данные не удаляются. В админской форме появилось обязательное поле подтверждения.
  • «Убрать из группы» осталось отдельным действием: оно удаляет зачисление, но не является отчислением. Для отчисления с историей используется «Отчислить».

4. UI

  • /admin/promotion — список запусков, построение dry-run за выбранный год.
  • /admin/promotion/{id} — сводка, предупреждения, таблица элементов «было/стало», кнопки «Применить план» и «Откатить» по статусу.
  • На странице группы:
    • «Академ» — для активного студента;
    • «В архив» — для активного студента;
    • «Отчислить» — для активного студента;
    • «Вернуть в группу» — для ACADEMIC_LEAVE;
    • «Восстановить в группу» — для ARCHIVED/EXPELLED;
    • «Убрать из группы» — отдельно от отчисления.
  • «В архив» для группы — на странице группы; кнопка скрыта для уже архивной группы.
  • В навигацию админа добавлен пункт «Повышение».
  • Шаблоны и представления покрыты тестами, включая подписи статусов, счётчики и состояния before/after.

5. Тесты

Добавлены наборы:

  • PromotionServiceIntegrationTest (6): dry-run не меняет данные, применение переводит группы/выпускает студентов/двигает академ, повторное применение идемпотентно, откат восстанавливает данные и удаляет созданную группу, конфликт ручных изменений, guard даты и дедупликация планировщика.
  • StudentLifecycleIntegrationTest (11): академотпуск с созданием и переиспользованием параллели, возврат, отчисление с блокировкой входа, вход из архивной группы, активной группы и без группы, восстановление после отчисления и архива, архив группы без смены статусов студентов, подтверждение удаления.
  • AdminStage4TemplateTest (5) и PromotionDtoTest (2): журнал повышения, кнопки apply/rollback, lifecycle-кнопки по статусам, поле подтверждения удаления, JSON-контракты.
  • AuthServiceLoginBackoffTest обновлён под новую зависимость проверки группы.

По результатам последнего прогона (2026-10-27) перечисленные наборы прошли с 0 failures / 0 errors.

6. Известные пробелы и forward-only

  • Только ручное применение. Планировщик создаёт только dry-run; автоматического применения и уведомлений нет.
  • Один план на год. Повторный dry-run за год возвращает уже сохранённый PLANNED-план; пересборки или удаления плана в UI нет. После завершённого применения новый dry-run за тот же год запрещён до отката.
  • Откат целиком. Поэлементного отката нет; при конфликте откатывается весь запуск. После ручных изменений сначала нужно вернуть данные к состоянию after.
  • История не удаляется. Откат добавляет RETURN, а не стирает исходные записи; журнал остаётся аудиторским следом.
  • Групповой архив односторонний. Отдельного endpoint/UI для возврата группы из архива нет; студенты архивной группы не входят.
  • Lifecycle из ведомости. Возврат и восстановление в UI выполняются в текущую группу; выбор другой группы доступен только через API. Массовых операций по студентам нет.
  • JWT и блокировка. Вход закрывается при следующей аутентификации; уже выданный JWT действует до истечения (до 30 дней). Это остаточный риск, зафиксированный в плане; аудит и hardening — этап 6.
  • Нет новых миграций. Используются таблицы promotion_runs, promotion_run_items, membership_history, accounts.status из миграций V7–V9 этапа 1. JSONB пишется через JdbcTemplate из-за несовместимости Jackson 2/3 в Hibernate.