Files
LMS/docs/04-sequences.md
Vladislav Kopnin a1392a469e Content import: sync sections and articles (archive is the source of truth)
- importZip now updates titles/positions of existing chapters and sections and
  removes sections/articles of archived chapters that are missing in the archive
  (so restructured material no longer leaves stale sections behind); removed
  count is reported in ContentImportResult and in the admin flash message
- docs: note about import semantics in the class diagram and sequences
2026-09-16 22:00:22 +03:00

357 lines
15 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»
```
## 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
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 новых
P->>DB: UPSERT comment_templates (use_count++ для автоподсказок)
P-->>W: GradeResponse
Note over DB: Студент оценку пока НЕ видит
A->>W: «опубликовать оценки» → POST /admin/groups/{gid}/publish
W->>P: POST /api/v1/grades/publish (labId, groupId, published=true)
P->>DB: INSERT/DELETE grade_publications (group_id, lab_id)
P-->>W: 200
S->>W: GET /student/labs/{labId}
W->>P: GET /api/v1/me/grades?groupId=... (Bearer)
P->>DB: SELECT lab_grades + grade_comments + grade_publications
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 (опубликованные ЛР)
loop по каждой лабораторной
W->>G: GET /api/v1/assignments?labId
G->>P: варианты студентов
W->>G: GET /api/v1/submissions?labId
G->>P: последние сдачи
W->>G: GET /api/v1/defenses?labId
G->>P: сессии защиты
end
W->>G: GET /api/v1/grades + /grades/publications?groupId
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: location.reload() — страница из серверного состояния
Note over AC,DB: один запрос применяет и порядок, и перенос между родителями
```