Files
LMS/docs/04-sequences.md
Vladislav Kopnin 5ffeaa0217 Content: drag & drop reordering and moving of chapters, sections and articles
- academic: POST /api/v1/content/reorder applies a full tree arrangement in one
  request — chapter order, section order and section->chapter moves, article
  order and article->section moves (positions and parent ids updated)
- web: POST /admin/content/reorder accepts the tree from JS (CSRF via query
  param), calls the gateway with the admin token
- /learn admin view: draggable handles (⠿) for chapters, sections and articles;
  drop before/after to reorder, drop into a chapter/section to move; visual
  indicators; structure is collected from DOM and saved, then page reloads
- docs: class diagram method + new sequence diagram for drag & drop
2026-09-16 18:51:21 +03:00

352 lines
14 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}
```
## 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: один запрос применяет и порядок, и перенос между родителями
```