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

14 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»

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

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

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

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