diff --git a/docs/01-architecture.md b/docs/01-architecture.md index a7cc3b9..4e59b30 100644 --- a/docs/01-architecture.md +++ b/docs/01-architecture.md @@ -6,7 +6,7 @@ flowchart LR user["Преподаватель / студент
(браузер)"] nginx["nginx
lms.our-place.ru
TLS, reverse proxy"] - web["web BFF
Thymeleaf + htmx
:8090"] + web["web BFF
Thymeleaf + Bootstrap
:8090"] gw["gateway
Spring Cloud Gateway
:8080"] id["identity-service
:8081"] ac["academic-service
:8082"] @@ -44,9 +44,9 @@ flowchart LR ```mermaid flowchart TB subgraph web["web (BFF, :8090)"] - wc["@Controller
AuthPage, Learn, Student, Admin, ContentAdmin"] - wi["Interceptor'ы:
Csrf, Auth, Admin, MustChangePassword"] - wapi["ApiClient (WebClient)
+ DTO"] + wc["@Controller: AuthPage, Learn, Student,
AdminGroup/Lab/Student/Distribute/Defense/Users, ContentAdmin
@RestController: ContentAssetProxy"] + wi["Interceptor'ы:
Csrf, NoCachePage, MustChangePassword, Auth, Admin"] + wapi["ApiClient (RestClient)
+ DTO"] wmd["MarkdownRenderer
(libs:markdown)"] wc --> wapi wc --> wmd @@ -67,16 +67,16 @@ flowchart TB end subgraph ac["academic-service (:8082)"] - acc["LabController, ContentPublicController,
ContentAdminController"] + acc["LabController, ContentPublicController,
ContentAdminController, ContentOrderController"] acs["LabService, ContentService,
LabImportParser, AssetStorage"] acr["LabRepository, LabLevelRepository, VariantRepository,
SemesterRepository, Content*Repository"] acc --> acs --> acr end subgraph pr["progress-service (:8083)"] - prc["ProgressController, DefenseAdminController,
DefenseMyController"] - prs["ProgressService, SubmissionService,
DefenseService, GradePublicationService"] - prr["VariantAssignmentRepository, StudentLabLevelRepository,
LabGradeRepository, GradeCommentRepository,
CommentTemplateRepository, SubmissionRepository,
DefenseSessionRepository, GradePublicationRepository"] + prc["ProgressController, SubmissionController, DefenseAdminController,
DefenseMyController, StudentPurgeController, GroupLabDataController"] + prs["ProgressService, SubmissionService, DefenseService,
GradePublicationService, StudentPurgeService"] + prr["VariantAssignmentRepository, StudentLabLevelRepository,
LabGradeRepository, GradeCommentRepository, CommentTemplateRepository,
SubmissionRepository, DefenseSessionRepository,
GradePublicationRepository, GradePublicationStudentRepository"] prcl["AcademicClient, IdentityClient"] prc --> prs prs --> prr @@ -150,7 +150,8 @@ flowchart TD ```mermaid flowchart LR req["HTTP-запрос"] --> csrf["CsrfInterceptor /**
GET: выдаёт token в сессию
POST: сверяет _csrf"] - csrf --> must["MustChangePasswordInterceptor /**
(кроме /change-password, /logout, /login, статики)"] + csrf --> nocahe["NoCachePageInterceptor /**
(страницы без кэша; /css, /js исключены)"] + nocahe --> must["MustChangePasswordInterceptor /**
(кроме /change-password, /logout, /login, статики)"] must --> auth["AuthInterceptor
/admin/**, /student/**, /change-password"] auth --> adm["AdminInterceptor /admin/**"] adm --> ctrl["Контроллер → ApiClient → gateway"] diff --git a/docs/02-deployment.md b/docs/02-deployment.md index 2eeab89..e17e665 100644 --- a/docs/02-deployment.md +++ b/docs/02-deployment.md @@ -47,6 +47,8 @@ flowchart TB | gateway | `target: gateway` | 8080 | `LMS_IDENTITY_URI`, `LMS_ACADEMIC_URI`, `LMS_PROGRESS_URI`, `LMS_JWT_JWK_SET_URI` | — | | web | `target: web` | 8090 | `LMS_GATEWAY_URL`, `SERVER_PORT` | — | +Общие для сервисов переменные БД: `LMS_DB_USER`, `LMS_DB_PASSWORD`, `LMS_DB_NAME`. + `LMS_JWT_JWK_SET_URI` обязателен для gateway, academic и progress — в контейнерах это `http://identity:8081/api/v1/auth/jwks.json` (иначе 401). @@ -99,9 +101,10 @@ sequenceDiagram DC->>DC: docker build (5 стадий) DC->>PG: старт postgres (volume lms_pgdata) DC->>DC: старт identity → academic → progress → gateway → web - Note over DC: Flyway применяет миграции
V1..V5 автоматически при старте сервисов + Note over DC: Flyway применяет миграции автоматически
при старте сервисов (identity V1..V3, academic V1..V5, progress V1..V8; у web миграций нет) ``` +- Локально (`docker-compose.yml`) поднимается только `postgres`; прод-стек — `docker-compose.prod.yml`. - Резервные копии: `pg_dump` (том `lms_pgdata`) и `tar` томов `lms_assets`, `lms_submissions` — команды в [deploy/DEPLOY.md](../deploy/DEPLOY.md). - Порядок наполнения данными (админ → группы → лабораторная → варианты → diff --git a/docs/03-domain-model.md b/docs/03-domain-model.md index c435d31..8c4878d 100644 --- a/docs/03-domain-model.md +++ b/docs/03-domain-model.md @@ -182,6 +182,7 @@ classDiagram class LabController class ContentPublicController class ContentAdminController + class ContentOrderController class LabService { +importFromFile(String, ByteArray) LabImportResult +list() List~LabSummary~ @@ -234,6 +235,7 @@ classDiagram LabController --> LabService ContentPublicController --> ContentService ContentAdminController --> ContentService + ContentOrderController --> ContentService LabService --> LabRepository LabService --> LabLevelRepository LabService --> VariantRepository @@ -274,6 +276,12 @@ classDiagram +UUID labId +Instant publishedAt } + class GradePublicationStudent { + +UUID groupId + +UUID labId + +UUID studentId + +Instant publishedAt + } class GradeComment { +UUID id +UUID studentId @@ -313,6 +321,8 @@ classDiagram ready } + GradePublication "1" --> "*" GradePublicationStudent : легаси «вся группа» и персональные + Note for GradePublicationStudent : персональная публикация оценки/комментариев студенту CommentTemplate "1" --> "*" GradeComment : подсказки (text/tone) GradeComment "*" --> "1" LabGrade : поясняет оценку LabGrade "1" --> "0..1" GradePublication : видна студенту после публикации @@ -324,8 +334,11 @@ classDiagram ```mermaid classDiagram class ProgressController + class SubmissionController class DefenseAdminController class DefenseMyController + class StudentPurgeController + class GroupLabDataController class ProgressService { +setLevel(LevelAssignmentRequest, String) LevelAssignmentResponse +getLevel(UUID, UUID) StudentLabLevel @@ -334,7 +347,7 @@ classDiagram +assignWithInfo(AssignRequest, String) VariantAssignmentResponse +assignmentsForLabEnriched(UUID, String) List~VariantAssignmentResponse~ +setGrade(GradeSetRequest) GradeResponse - +suggestComments(String) List~CommentOut~ + +suggestComments(String) List~CommentSuggestion~ +myComments(UUID) List~GradeComment~ +getStudentAssignment(UUID, UUID, String) MeAssignmentResponse } @@ -356,10 +369,12 @@ classDiagram +resetByStudentLab(UUID, UUID) } class GradePublicationService { - +setPublished(UUID, UUID, Boolean) - +isPublished(UUID, UUID) Boolean - +publishedLabIdsForGroup(UUID) List~String~ - +publishedGroupIdsForLab(UUID) List~String~ + +state(UUID, UUID) PublicationState + +setStudents(UUID, UUID, List~UUID~) + +isPublished(UUID, UUID, UUID) Boolean + } + class StudentPurgeService { + +purge(UUID) } class AcademicClient { +getLab(String, String) LabInfo @@ -383,12 +398,17 @@ classDiagram class SubmissionRepository class DefenseSessionRepository class GradePublicationRepository + class GradePublicationStudentRepository ProgressController --> ProgressService + SubmissionController --> SubmissionService DefenseAdminController --> DefenseService DefenseMyController --> DefenseService + StudentPurgeController --> StudentPurgeService + GroupLabDataController --> ProgressService ProgressController --> SubmissionService ProgressController --> GradePublicationService + StudentPurgeService --> SubmissionStorage ProgressService --> VariantAssignmentRepository ProgressService --> StudentLabLevelRepository ProgressService --> LabGradeRepository @@ -401,6 +421,7 @@ classDiagram SubmissionService --> AcademicClient DefenseService --> DefenseSessionRepository GradePublicationService --> GradePublicationRepository + GradePublicationService --> GradePublicationStudentRepository ``` ## 4.web (BFF) @@ -410,13 +431,29 @@ classDiagram class AuthPageController class LearnController class StudentController - class AdminController + class AdminGroupController + class AdminLabController + class AdminStudentController + class AdminDistributeController + class AdminDefenseController + class AdminUsersController class ContentAdminController + class ContentAssetProxy class ApiClient { +login(String, String) AuthResponseDto +contentTree(String, Boolean) ContentTreeNodeDto +contentArticle(String, String, String, String, Boolean) ContentArticleDto +setContentPublished(String, String, String, Boolean) + +suggestComments(String, String) List~CommentSuggestionDto~ + +deleteCommentTemplate(String, String) + +publicationState(String, String, String) PublicationStateDto + +setPublication(String, PublicationSetPayload) + +purgeStudent(String, String) + +searchStudents(String, String) List~AdminUserDto~ + +deleteStudent(String, String) DeleteUserDto + +defenseWatch(String, List~String~) List~DefenseDto~ + +myDefenseStatus(String, String) Map + +groupLabData(String, String) List~Map~ +groupDetail(String, String) GroupDetailDto +distribute(String, DistributeRequestPayload) DistributeResultDto +setGrade(String, String, String, String, String) GradeResponseDto @@ -437,6 +474,7 @@ classDiagram +String USER } class CsrfInterceptor + class NoCachePageInterceptor class AuthInterceptor class AdminInterceptor class MustChangePasswordInterceptor @@ -447,8 +485,14 @@ classDiagram AuthPageController --> ApiClient LearnController --> ApiClient StudentController --> ApiClient - AdminController --> ApiClient + AdminGroupController --> ApiClient + AdminLabController --> ApiClient + AdminStudentController --> ApiClient + AdminDistributeController --> ApiClient + AdminDefenseController --> ApiClient + AdminUsersController --> ApiClient ContentAdminController --> ApiClient + ContentAssetProxy --> ApiClient LearnController --> MarkdownRenderer AuthPageController --> SessionUser AuthPageController --> SessionKeys @@ -544,7 +588,7 @@ erDiagram } CHAPTERS { uuid id PK - varchar title UK + varchar title varchar slug UK int position boolean published @@ -598,6 +642,12 @@ erDiagram uuid lab_id PK timestamptz published_at } + GRADE_PUBLICATION_STUDENTS { + uuid group_id PK + uuid lab_id PK + uuid student_id PK + timestamptz published_at + } GRADE_COMMENTS { uuid id PK uuid student_id @@ -636,6 +686,7 @@ erDiagram STUDENT_LAB_LEVELS }o--|| LABS : "lab_id (не FK)" LAB_GRADES }o--|| LABS : "lab_id (не FK)" GRADE_PUBLICATIONS }o--|| LABS : "lab_id (не FK)" + GRADE_PUBLICATION_STUDENTS }o--|| LABS : "lab_id (не FK)" GRADE_COMMENTS }o--|| LABS : "lab_id (не FK)" SUBMISSIONS }o--|| LABS : "lab_id (не FK)" DEFENSE_SESSIONS }o--|| LABS : "lab_id (не FK)" diff --git a/docs/04-sequences.md b/docs/04-sequences.md index f616d2e..2b1189a 100644 --- a/docs/04-sequences.md +++ b/docs/04-sequences.md @@ -146,6 +146,7 @@ sequenceDiagram P->>DB: INSERT/UPDATE variant_assignments (manual=false) P-->>W: DistributeResult(assigned, total) W-->>A: «Распределено: N из M» + Note over A,W: UI — большая модалка «Выдача вариантов» на странице группы
(фрагмент по fetch). Эндпоинт отвечает редиректом — это норма;
модалка обновляется на месте, тост показывается поверх неё ``` ## 6. Сдача решения студентом @@ -214,13 +215,13 @@ sequenceDiagram P-->>W: DefenseDto W-->>S: «Работа сохранена» - Note over P,DB: DefenseService.syncTimeouts() переводит просроченные
сессии в ready с ready_reason=timeout + Note over P,DB: DefenseService.syncTimeouts() переводит просроченные
сессии в ready с ready_reason=timeout; кнопка «Готов» даёт ready_reason=button.
В UI: «Готов к сдаче» (button) / «Вышло время» (timeout) A->>W: «доп. задание: начать заново» → POST /admin/defenses/{id}/reset P->>DB: UPDATE status=waiting, code=NULL, started_at=NULL W-->>A: сессия снова ждёт кода ``` -## 8. Оценка, комментарии и публикация +## 8. Оценка, комментарии и публикация (по студенту) ```mermaid sequenceDiagram @@ -235,19 +236,28 @@ sequenceDiagram 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++ для автоподсказок) + Note over DB: comment_templates — подсказки; частота считается
по актуальным grade_comments (не по 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 + 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
(легаси 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 + grade_publications - P-->>W: только опубликованные оценки группы + P->>DB: SELECT lab_grades + grade_comments; фильтр isPublished(group, lab, student) + P-->>W: только опубликованные этому студенту оценки W-->>S: оценка и комментарии списками: позитивный / негативный / нейтральный ``` @@ -314,16 +324,18 @@ sequenceDiagram 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/assignments?labId - G->>P: варианты студентов 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 + /grades/publications?groupId - G->>P: оценки + W->>G: GET /api/v1/grades + G->>P: все оценки (матрица) W->>W: сборка матрицы «студент × лабораторная» W-->>A: страница группы (оценки, защиты, поиск, фильтры, сортировка) ``` @@ -351,6 +363,32 @@ sequenceDiagram AC->>DB: UPDATE chapters.position,
sections.chapter_id/position,
articles.section_id/position AC-->>W: 200 W-->>UI: {"ok": true} - UI->>UI: location.reload() — страница из серверного состояния + UI->>UI: без перезагрузки — DOM уже перестроен, статус «сохранено» Note over AC,DB: один запрос применяет и порядок, и перенос между родителями + Note over UI: раскрытые разделы и позиция скролла сохраняются (localStorage / ui-state.js) +``` + +## 12. Синхронизация таймеров защиты (опрос раз в 10 секунд) + +```mermaid +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 ``` diff --git a/docs/05-states.md b/docs/05-states.md index e1851f4..fed403e 100644 --- a/docs/05-states.md +++ b/docs/05-states.md @@ -17,23 +17,32 @@ stateDiagram-v2 end note ``` -## 2. Публикация оценок (пара «группа + лабораторная») +## 2. Публикация оценок (пара «студент + лабораторная») ```mermaid stateDiagram-v2 [*] --> hidden : оценка выставлена (lab_grades) - hidden --> published : «опубликовать оценки» (grade_publications) - published --> hidden : «скрыть оценки» + hidden --> published : чекбокс публикации в ячейке студент×ЛР (grade_publication_students) + published --> hidden : снять чекбокс + hidden --> published : «опубликовать всем» (глаз в шапке ЛР) + published --> hidden : «скрыть у всех» note right of hidden - студент не видит ни балл, - ни комментарии + студент не видит ни балл, ни комментарии. + Проверка доступа — по (student, lab): + групповая публикация (легаси, grade_publications) + открывает всем, иначе — по списку студентов. end note note right of published - студенты группы видят балл и - комментарии, сгруппированные по тону + видно только выбранным студентам; подсказки + комментариев показывают частоту и позволяют + удалять шаблоны с частотой 0 end note ``` +Кнопки: чекбокс в ячейке публикует/скрывает оценку конкретному студенту (AJAX, без перезагрузки, +тост с «N из M»); «глаз» в шапке лабораторной — массово «всем/скрыть у всех» (частичное состояние — +жёлтый со счётчиком). + ## 3. Статус лабораторной (`labs.status`) ```mermaid @@ -113,7 +122,7 @@ stateDiagram-v2 stateDiagram-v2 [*] --> temporary : аккаунт создан / «Сбросить пароль» temporary --> own : смена на /change-password - own --> temporary : «Сбросить пароль» преподавателем + own --> temporary : «Сбросить пароль» администратором note right of temporary must_change_password = true, temp_password хранится; diff --git a/docs/06-authoring.md b/docs/06-authoring.md index a1a549a..77101eb 100644 --- a/docs/06-authoring.md +++ b/docs/06-authoring.md @@ -89,16 +89,20 @@ printf("Ответ: 42\n"); Администратору доступны действия не только на главной `/learn`, но и на каждой странице: -* **статья** — «Править» (правка прямо на странице: текст статьи открывается в редакторе +* **статья** — иконка-карандаш (правка прямо на странице: текст статьи открывается в редакторе с панелью форматирования — жирный, курсив, заголовки, списки, ссылки, код, таблицы, картинки, видео, спойлер), «Скрыть/Опубликовать», «Удалить», а также навигация **← предыдущая / следующая →** (работает и между разделами) и **боковое содержание** со всеми главами, разделами и статьями; * **раздел** — «+ Статья», «Переименовать раздел», «Скрыть/Опубликовать», - «Перенести статьи и удалить», «Удалить раздел»; у каждой статьи в списке — «Править» + «Перенести статьи и удалить», «Удалить раздел»; у каждой статьи в списке — иконка-карандаш (открывает статью с готовым окном правки), скрытие и удаление; * **глава** — «+ Раздел», «Переименовать главу», «Скрыть/Опубликовать», «Удалить главу»; - у каждого раздела — переименование, скрытие, перенос статей и удаление, у каждой статьи — «Править». + у каждого раздела — переименование, скрытие, перенос статей и удаление, у каждой статьи — иконка-карандаш. + +На странице статьи боковое содержание: на десктопе закреплено (своя прокрутка), на мобильных +открывается кнопкой «Содержание» справа. На главной `/learn` на мобильных три кнопки +(«+ Статья», «+ Глава», «Импорт zip») идут под заголовком в один ряд. Порядок меняется перетаскиванием за значок **⠿** (сохраняется сразу, без перезагрузки страницы): на главной `/learn` — главы, разделы и статьи (в том числе перенос раздела в другую главу и diff --git a/docs/README.md b/docs/README.md index 562104c..bcaf253 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,13 +19,14 @@ Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid). ## Кратко о системе LMS — микросервисное приложение на Kotlin/Spring Boot 3 (Java 21) с -server-side-rendered UI на Thymeleaf + htmx: +server-side-rendered UI: Thymeleaf + Bootstrap 5 (CSS и иконки вендорены локально) +и собственный JS (htmx не используется): - **gateway** (`:8080`) — единая точка входа для REST API: маршрутизация и проверка JWT; - **identity-service** (`:8081`) — аккаунты, группы, студенты, аутентификация (JWT + JWKS); - **academic-service** (`:8082`) — семестры, лабораторные (уровни, варианты), учебные материалы; - **progress-service** (`:8083`) — варианты/уровни студентов, сдачи, оценки и комментарии, защиты; -- **web** (`:8090`) — BFF: сессии, CSRF, страницы админа и студента, markdown-рендер; +- **web** (`:8090`) — BFF: сессии, CSRF, страницы админа и студента, markdown-рендер, Static (Bootstrap/Icons); - **PostgreSQL 18** — одна БД `lms` со схемами `identity`, `academic`, `progress`. Поток запроса: браузер → nginx → web → gateway → сервис → PostgreSQL.