docs: этапы 3–4 — роли, профиль и жизненный цикл
This commit is contained in:
174
docs/14-stage4-report.md
Normal file
174
docs/14-stage4-report.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user