- 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
352 lines
14 KiB
Markdown
352 lines
14 KiB
Markdown
# Диаграммы последовательностей
|
||
|
||
## 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: один запрос применяет и порядок, и перенос между родителями
|
||
```
|