Files
LMS/docs/04-sequences.md

18 KiB
Raw Blame History

Диаграммы последовательностей

1. Вход в систему и выдача JWT

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

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. Первый вход: принудительная смена пароля

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)

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. Раздача вариантов

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. Сдача решения студентом

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. Защита лабораторной

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. Оценка, комментарии и публикация (по студенту)

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. Учебные материалы: публикация и просмотр

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. Страница группы у администратора (агрегация данных)

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 материалов (порядок и перенос)

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 секунд)

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