Files
LMS/docs/04-sequences.md
Vladislav Kopnin 6ffc6304e3 docs: add UML and architecture documentation (Mermaid)
- docs/README.md — index and short system overview
- 01-architecture.md — context, per-service components, Gradle modules,
  authentication/authorization, web interceptors (flowcharts)
- 02-deployment.md — deployment diagram, containers/env/volumes, build stages,
  nginx, update sequence
- 03-domain-model.md — class diagrams for identity/academic/progress and web,
  ER model of all schemas (cross-schema references noted)
- 04-sequences.md — 10 sequence diagrams (login, JWT check, forced password
  change, lab import, distribution, submission, defense, grades+publication,
  content publishing, group page aggregation)
- 05-states.md — state diagrams for defense session, grade publication, lab
  status, content visibility, submission lifecycle, variant assignment, password
MS
git log --oneline -3
2026-09-16 18:44:14 +03:00

13 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: страница группы (оценки, защиты, поиск, фильтры, сортировка)