# Диаграммы последовательностей ## 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 (метаданные, уровни) +
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: Алгоритм:
1) пул = варианты уровня студента (без уровня — первый уровень)
2) очередь студентов случайна и общая для выбранных групп
3) выбирается наименее занятый вариант уровня,
пока есть невыданные — повторов нет
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 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() переводит просроченные
сессии в 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: выбирает цель и позицию
(до/после либо «внутрь» контейнера) 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,
sections.chapter_id/position,
articles.section_id/position AC-->>W: 200 W-->>UI: {"ok": true} UI->>UI: location.reload() — страница из серверного состояния Note over AC,DB: один запрос применяет и порядок, и перенос между родителями ```