Files
LMS/docs/04-sequences.md

395 lines
18 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.

# Диаграммы последовательностей
## 1. Вход в систему и выдача JWT
```mermaid
sequenceDiagram
autonumber
actor U as Пользователь
participant W as "web (BFF)"
participant G as gateway
participant I as identity
participant DB as PostgreSQL
U->>W: POST /login (login, password, _csrf)
W->>G: POST /api/v1/auth/login
Note over G: путь публичный (без токена)
G->>I: POST /api/v1/auth/login
I->>DB: SELECT accounts WHERE login = ?
DB-->>I: Account(password_hash, role, must_change_password)
I->>I: BCrypt.matches(password, hash)
alt пароль верный
I->>I: JwtEncoder: RS256, kid=lms, claims sub/login/role/exp
I-->>G: AuthResponse(token, role, name, mustChangePassword)
G-->>W: 200 AuthResponse
W->>W: session.invalidate(); новая сессия
W->>W: session[token]=JWT, session[user]=SessionUser
alt mustChangePassword
W-->>U: 302 /change-password
else
W-->>U: 302 /admin | /student
end
else пароль неверный
I-->>G: 401 UnauthorizedException
G-->>W: 401 ApiError
W-->>U: страница входа с ошибкой
end
```
## 2. Защищённый запрос: проверка JWT
```mermaid
sequenceDiagram
autonumber
participant W as "web (BFF)"
participant G as gateway
participant S as "сервис identity/academic/progress"
participant ID as identity /jwks.json
W->>G: GET /api/v1/... (Authorization: Bearer JWT)
G->>G: JwtAuthenticationFilter (order -100)
alt путь публичный
G->>S: запрос без проверки токена
else требуется токен
G->>ID: JWKS (кэшируется)
ID-->>G: открытые RSA-ключи
G->>G: проверка подписи и срока действия
alt токен валиден
G->>S: + X-Account-Id, X-Account-Login, X-Account-Role
else
G-->>W: 401 {"status":401,"detail":"..."}
end
end
S->>S: SecurityConfig: oauth2ResourceServer(jwt), @PreAuthorize
S-->>W: 200 данные / 403 при нехватке прав
```
## 3. Первый вход: принудительная смена пароля
```mermaid
sequenceDiagram
autonumber
actor S as Студент
participant W as "web (BFF)"
participant I as identity
participant DB as PostgreSQL
S->>W: POST /login (временный пароль)
W->>I: POST /api/v1/auth/login
I-->>W: token, mustChangePassword = true
W->>W: сессия: SessionUser(mustChangePassword=true)
W-->>S: 302 /change-password
S->>W: GET /learn (попытка уйти)
W->>W: MustChangePasswordInterceptor (/**)
W-->>S: 302 /change-password
S->>W: POST /change-password (old, new, new2)
W->>I: POST /api/v1/auth/change-password (Bearer)
I->>DB: UPDATE accounts SET password_hash=?, must_change_password=false, temp_password=NULL
I-->>W: 200 ok
W->>W: SessionUser.copy(mustChangePassword=false)
W-->>S: 302 /student
Note over S,W: до смены пароля доступны только /change-password, /logout и статика
```
## 4. Импорт лабораторной (ZIP)
```mermaid
sequenceDiagram
autonumber
actor A as Администратор
participant W as web
participant G as gateway
participant AC as academic
participant DB as PostgreSQL
A->>W: POST /admin/labs/import (file=lab1.zip, _csrf)
W->>G: POST /api/v1/labs/import (multipart, Bearer)
G->>AC: POST /api/v1/labs/import
AC->>AC: LabImportParser.parse(zip)
Note over AC: lab.yaml (метаданные, уровни) +<br/>README.md (условие) + variants/*.md + assets/
AC->>DB: проверка уникальности номера ЛР в семестре
alt номер занят
AC-->>W: 409 Conflict "Лабораторная уже существует"
else
AC->>DB: INSERT labs, lab_levels, variants
AC-->>W: LabImportResult(levelCount, variantCount)
end
W-->>A: редирект на /admin/labs с уведомлением
A->>W: POST /admin/labs/{id}/edit (даты, статус published, уровни, варианты)
W->>AC: PUT /api/v1/labs/{id}, POST/PUT/DELETE уровней и вариантов
AC->>DB: UPDATE labs / INSERT|UPDATE|DELETE lab_levels, variants
W-->>A: «Все изменения сохранены»
```
## 5. Раздача вариантов
```mermaid
sequenceDiagram
autonumber
actor A as Администратор
participant W as web
participant G as gateway
participant P as progress
participant AC as academic
participant ID as identity
participant DB as PostgreSQL
A->>W: POST /admin/labs/{id}/distribute (groupIds[], force)
W->>G: POST /api/v1/distribute (Bearer)
G->>P: DistributeRequest(labId, groupIds, force)
P->>DB: SELECT student_lab_levels, variant_assignments (labId)
P->>ID: GET /api/v1/groups, GET /api/v1/groups/{id}
ID-->>P: студенты выбранных групп
P->>AC: GET /api/v1/labs/{id}
AC-->>P: уровни и варианты лабораторной
Note over P: Алгоритм:<br/>1) пул = варианты уровня студента (без уровня — первый уровень)<br/>2) очередь студентов случайна и общая для выбранных групп<br/>3) выбирается наименее занятый вариант уровня,<br/> пока есть невыданные — повторов нет<br/>4) ручные закрепления и force не удаляет
P->>DB: INSERT/UPDATE variant_assignments (manual=false)
P-->>W: DistributeResult(assigned, total)
W-->>A: «Распределено: N из M»
Note over A,W: UI — большая модалка «Выдача вариантов» на странице группы<br/>(фрагмент по fetch). Эндпоинт отвечает редиректом — это норма;<br/>модалка обновляется на месте, тост показывается поверх неё
```
## 6. Сдача решения студентом
```mermaid
sequenceDiagram
autonumber
actor S as Студент
participant W as web
participant G as gateway
participant P as progress
participant AC as academic
participant FS as файловое хранилище
participant DB as PostgreSQL
S->>W: POST /student/labs/{id}/submit (files, _csrf)
W->>G: POST /api/v1/me/submissions/{labId} (multipart, Bearer)
G->>P: submit(studentId, labId, files, token)
P->>AC: GET /api/v1/labs/{id}
alt дедлайн прошёл
AC-->>P: deadlineAt < now
P-->>W: 400 "Приём решений закрыт"
W-->>S: ошибка на странице лабы
else приём открыт
P->>DB: SELECT max(seq) по студенту и ЛР
P->>FS: сохранить файлы в submissions/{studentId}/{labId}/{seq}
P->>DB: INSERT submissions
P-->>W: List<SubmissionDto>
W-->>S: 302 обратно на страницу лабы (история сдач)
end
```
## 7. Защита лабораторной
```mermaid
sequenceDiagram
autonumber
actor A as Преподаватель
actor S as Студент
participant W as web
participant P as progress
participant DB as PostgreSQL
A->>W: «Назначить задание» → POST /admin/defenses/save
W->>P: POST /api/v1/defenses/assign
P->>DB: INSERT/UPDATE defense_sessions (status=waiting, assignment_md)
A->>W: «выдать код» → POST /admin/defenses/{id}/issue-code
P->>DB: UPDATE code = 6-значный, status=waiting
P-->>W: код (flash-уведомление)
W-->>A: код показан преподавателю
S->>W: POST /student/defenses/start (labId, code)
W->>P: POST /api/v1/me/defenses/start
P->>DB: SELECT ... WHERE code=? AND student_id=?
alt код верный
P->>DB: UPDATE status=started, started_at=now
P-->>W: DefenseDto(secondsLeft)
W-->>S: страница защиты с таймером
else
P-->>W: 400 "Неверный код"
end
S->>W: POST /student/defenses/{id}/finish
W->>P: POST /api/v1/me/defenses/{id}/finish
P->>DB: UPDATE status=ready, ready_at=now
P-->>W: DefenseDto
W-->>S: «Работа сохранена»
Note over P,DB: DefenseService.syncTimeouts() переводит просроченные<br/>сессии в ready с ready_reason=timeout; кнопка «Готов» даёт ready_reason=button.<br/>В UI: «Готов к сдаче» (button) / «Вышло время» (timeout)
A->>W: «доп. задание: начать заново» → POST /admin/defenses/{id}/reset
P->>DB: UPDATE status=waiting, code=NULL, started_at=NULL
W-->>A: сессия снова ждёт кода
```
## 8. Оценка, комментарии и публикация (по студенту)
```mermaid
sequenceDiagram
autonumber
actor A as Преподаватель
actor S as Студент
participant W as web
participant P as progress
participant DB as PostgreSQL
A->>W: POST /admin/groups/{gid}/grades (studentId, labId, score, commentsJson)
W->>P: POST /api/v1/grades (GradeSetRequest)
P->>DB: UPSERT lab_grades (student_id, lab_id, score)
P->>DB: DELETE grade_comments по паре, затем INSERT новых
Note over DB: comment_templates — подсказки; частота считается<br/>по актуальным grade_comments (не по use_count)
P-->>W: GradeResponse
Note over DB: Студент оценку пока НЕ видит
A->>W: чекбокс публикации в ячейке (student×lab) → POST /admin/groups/{gid}/publish-student
W->>P: POST /api/v1/grades/publication {groupId, labId, studentIds[]} — заменяет набор
P->>DB: DELETE/INSERT grade_publication_students<br/>(легаси grade_publications снимается)
W-->>A: тост «Опубликовано: N из M», счётчик «глаза» обновлён
A->>W: «глаз» в шапке ЛР (всем / скрыть у всех) → POST /admin/groups/{gid}/publish (published=true|false)
W->>P: POST /api/v1/grades/publication {studentIds = все | []}
A->>W: поиск подсказок → GET /admin/comment-templates?q=
W->>P: GET /api/v1/comment-templates?q=
P-->>W: [{text, tone, count}] — частота по текущим оценкам
A->>W: удалить подсказку с частотой 0 → POST /admin/comment-templates/delete (text)
W->>P: DELETE /api/v1/comment-templates?text= (400, если где-то используется)
S->>W: GET /student/labs/{labId}
W->>P: GET /api/v1/me/grades?groupId=... (Bearer)
P->>DB: SELECT lab_grades + grade_comments; фильтр isPublished(group, lab, student)
P-->>W: только опубликованные этому студенту оценки
W-->>S: оценка и комментарии списками: позитивный / негативный / нейтральный
```
## 9. Учебные материалы: публикация и просмотр
```mermaid
sequenceDiagram
autonumber
actor A as Администратор
actor G as Гость/студент
participant W as web
participant G2 as gateway
participant AC as academic
participant DB as PostgreSQL
A->>W: GET /learn
W->>G2: GET /api/v1/content/tree/all (Bearer admin)
AC->>DB: SELECT chapters, sections, articles (включая скрытые)
W-->>A: дерево с бейджами «скрыто» и кнопками публикации
A->>W: POST /admin/content/articles/toggle-published (published=false)
W->>G2: PUT /api/v1/content/articles/{id}/published
AC->>DB: UPDATE articles SET published=false
W-->>A: «Материал скрыт от студентов»
G->>W: GET /learn
W->>G2: GET /api/v1/content/tree
AC->>DB: SELECT ... WHERE published = true
AC-->>W: только опубликованное
W-->>G: материалы без скрытых глав, разделов и статей
G->>W: GET /learn/{chapter}/{section}/{article}
W->>G2: GET /api/v1/content/{c}/{s}/{a}
alt статья (или её глава/раздел) скрыта
AC-->>W: 404 NotFoundException
W-->>G: 302 /learn
else опубликована
AC-->>W: ContentArticleView(bodyMd, published)
W->>W: MarkdownRenderer.render (таблицы, код, :::spoiler)
W-->>G: статья с подсветкой кода
end
Note over A,AC: админ открывает скрытую статью через /api/v1/content/all/{c}/{s}/{a}
```
> **Импорт ZIP — источник истины для глав, которые есть в архиве.** Разделы и статьи,
> которых нет в архиве, удаляются из этих глав (счётчик `removed` в результате импорта);
> главы, отсутствующие в архиве целиком, не затрагиваются. Поэтому после крупной
> перестройки структуры старую главу, которой больше нет в архиве, нужно удалить в интерфейсе.
## 10. Страница группы у администратора (агрегация данных)
```mermaid
sequenceDiagram
autonumber
actor A as Администратор
participant W as "web (AdminController.groupDetail)"
participant G as gateway
participant ID as identity
participant AC as academic
participant P as progress
A->>W: GET /admin/groups/{gid}
W->>G: GET /api/v1/groups/{gid}
G->>ID: GroupService.detail (студенты по ФИО)
W->>G: GET /api/v1/labs
G->>AC: LabService.list (опубликованные ЛР)
W->>G: GET /api/v1/assignments/group/{gid}
G->>P: уровни и варианты всех студентов группы (bulk)
loop по каждой лабораторной
W->>G: GET /api/v1/submissions?labId
G->>P: последние сдачи
W->>G: GET /api/v1/defenses?labId
G->>P: сессии защиты
W->>G: GET /api/v1/grades/publication?groupId&labId
G->>P: состояние публикации {all, studentIds}
end
W->>G: GET /api/v1/grades
G->>P: все оценки (матрица)
W->>W: сборка матрицы «студент × лабораторная»
W-->>A: страница группы (оценки, защиты, поиск, фильтры, сортировка)
```
## 11. Drag & drop материалов (порядок и перенос)
```mermaid
sequenceDiagram
autonumber
actor A as Администратор
participant UI as /learn (JS content-sort.js)
participant W as web
participant G as gateway
participant AC as academic
participant DB as PostgreSQL
A->>UI: тянет за значок ⠿ (глава, раздел или статья)
UI->>UI: dragover: выбирает цель и позицию<br/>(до/после либо «внутрь» контейнера)
A->>UI: отпускает кнопку мыши
UI->>UI: перемещает элемент в DOM (перенос между главами/разделами)
UI->>UI: collectTree() — собирает порядок глав, разделов и статей
UI->>W: POST /admin/content/reorder?_csrf=... (JSON)
W->>G: POST /api/v1/content/reorder (Bearer admin)
G->>AC: ReorderTree
AC->>DB: UPDATE chapters.position,<br/>sections.chapter_id/position,<br/>articles.section_id/position
AC-->>W: 200
W-->>UI: {"ok": true}
UI->>UI: без перезагрузки — DOM уже перестроен, статус «сохранено»
Note over AC,DB: один запрос применяет и порядок, и перенос между родителями
Note over UI: раскрытые разделы и позиция скролла сохраняются (localStorage / ui-state.js)
```
## 12. Синхронизация таймеров защиты (опрос раз в 10 секунд)
```mermaid
sequenceDiagram
autonumber
actor A as Преподаватель
actor S as Студент
participant W as web
participant P as progress
loop каждые 10 c (страница группы)
A->>W: GET /admin/defense-watch?labs=...
W->>P: GET /api/v1/defenses/watch?labs=...
P-->>W: статусы, secondsLeft, readyReason
W-->>A: обновление ячеек; переход в ready → тост «ФИО — ЛР: Готов к сдаче / Вышло время»
end
loop каждые 10 c (страница лабы студента)
S->>W: GET /student/labs/{id}/defense-status
W->>P: GET /api/v1/me/defenses/{labId}/status
P-->>W: status / secondsLeft / readyReason
W-->>S: подстройка таймера; при status != started — перезагрузка страницы
end
```