docs: актуализация под текущее состояние (Bootstrap вместо htmx, RestClient, интерцепторы, контроллеры, публикация по студентам, watch-опрос, автосейв без перезагрузки)

This commit is contained in:
2026-10-13 15:59:37 +03:00
parent 4fa400ddb1
commit b1f00fd776
7 changed files with 152 additions and 45 deletions

View File

@@ -6,7 +6,7 @@
flowchart LR
user["Преподаватель / студент<br/>(браузер)"]
nginx["nginx<br/>lms.our-place.ru<br/>TLS, reverse proxy"]
web["web BFF<br/>Thymeleaf + htmx<br/>:8090"]
web["web BFF<br/>Thymeleaf + Bootstrap<br/>:8090"]
gw["gateway<br/>Spring Cloud Gateway<br/>:8080"]
id["identity-service<br/>:8081"]
ac["academic-service<br/>:8082"]
@@ -44,9 +44,9 @@ flowchart LR
```mermaid
flowchart TB
subgraph web["web (BFF, :8090)"]
wc["@Controller<br/>AuthPage, Learn, Student, Admin, ContentAdmin"]
wi["Interceptor'ы:<br/>Csrf, Auth, Admin, MustChangePassword"]
wapi["ApiClient (WebClient)<br/>+ DTO"]
wc["@Controller: AuthPage, Learn, Student,<br/>AdminGroup/Lab/Student/Distribute/Defense/Users, ContentAdmin<br/>@RestController: ContentAssetProxy"]
wi["Interceptor'ы:<br/>Csrf, NoCachePage, MustChangePassword, Auth, Admin"]
wapi["ApiClient (RestClient)<br/>+ DTO"]
wmd["MarkdownRenderer<br/>(libs:markdown)"]
wc --> wapi
wc --> wmd
@@ -67,16 +67,16 @@ flowchart TB
end
subgraph ac["academic-service (:8082)"]
acc["LabController, ContentPublicController,<br/>ContentAdminController"]
acc["LabController, ContentPublicController,<br/>ContentAdminController, ContentOrderController"]
acs["LabService, ContentService,<br/>LabImportParser, AssetStorage"]
acr["LabRepository, LabLevelRepository, VariantRepository,<br/>SemesterRepository, Content*Repository"]
acc --> acs --> acr
end
subgraph pr["progress-service (:8083)"]
prc["ProgressController, DefenseAdminController,<br/>DefenseMyController"]
prs["ProgressService, SubmissionService,<br/>DefenseService, GradePublicationService"]
prr["VariantAssignmentRepository, StudentLabLevelRepository,<br/>LabGradeRepository, GradeCommentRepository,<br/>CommentTemplateRepository, SubmissionRepository,<br/>DefenseSessionRepository, GradePublicationRepository"]
prc["ProgressController, SubmissionController, DefenseAdminController,<br/>DefenseMyController, StudentPurgeController, GroupLabDataController"]
prs["ProgressService, SubmissionService, DefenseService,<br/>GradePublicationService, StudentPurgeService"]
prr["VariantAssignmentRepository, StudentLabLevelRepository,<br/>LabGradeRepository, GradeCommentRepository, CommentTemplateRepository,<br/>SubmissionRepository, DefenseSessionRepository,<br/>GradePublicationRepository, GradePublicationStudentRepository"]
prcl["AcademicClient, IdentityClient"]
prc --> prs
prs --> prr
@@ -150,7 +150,8 @@ flowchart TD
```mermaid
flowchart LR
req["HTTP-запрос"] --> csrf["CsrfInterceptor /**<br/>GET: выдаёт token в сессию<br/>POST: сверяет _csrf"]
csrf --> must["MustChangePasswordInterceptor /**<br/>(кроме /change-password, /logout, /login, статики)"]
csrf --> nocahe["NoCachePageInterceptor /**<br/>(страницы без кэша; /css, /js исключены)"]
nocahe --> must["MustChangePasswordInterceptor /**<br/>(кроме /change-password, /logout, /login, статики)"]
must --> auth["AuthInterceptor<br/>/admin/**, /student/**, /change-password"]
auth --> adm["AdminInterceptor /admin/**"]
adm --> ctrl["Контроллер → ApiClient → gateway"]

View File

@@ -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 применяет миграции<br/>V1..V5 автоматически при старте сервисов
Note over DC: Flyway применяет миграции автоматически<br/>при старте сервисов (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).
- Порядок наполнения данными (админ → группы → лабораторная → варианты →

View File

@@ -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)"

View File

@@ -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 — большая модалка «Выдача вариантов» на странице группы<br/>(фрагмент по fetch). Эндпоинт отвечает редиректом — это норма;<br/>модалка обновляется на месте, тост показывается поверх неё
```
## 6. Сдача решения студентом
@@ -214,13 +215,13 @@ sequenceDiagram
P-->>W: DefenseDto
W-->>S: «Работа сохранена»
Note over P,DB: DefenseService.syncTimeouts() переводит просроченные<br/>сессии в ready с ready_reason=timeout
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. Оценка, комментарии и публикация
## 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 — подсказки; частота считается<br/>по актуальным 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<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 + 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,<br/>sections.chapter_id/position,<br/>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
```

View File

@@ -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 хранится;

View File

@@ -89,16 +89,20 @@ printf("Ответ: 42\n");
Администратору доступны действия не только на главной `/learn`, но и на каждой странице:
* **статья** — «Править» (правка прямо на странице: текст статьи открывается в редакторе
* **статья** — иконка-карандаш (правка прямо на странице: текст статьи открывается в редакторе
с панелью форматирования — жирный, курсив, заголовки, списки, ссылки, код, таблицы,
картинки, видео, спойлер), «Скрыть/Опубликовать», «Удалить», а также навигация
**← предыдущая / следующая →** (работает и между разделами) и **боковое содержание**
со всеми главами, разделами и статьями;
* **раздел** — «+ Статья», «Переименовать раздел», «Скрыть/Опубликовать»,
«Перенести статьи и удалить», «Удалить раздел»; у каждой статьи в списке — «Править»
«Перенести статьи и удалить», «Удалить раздел»; у каждой статьи в списке — иконка-карандаш
(открывает статью с готовым окном правки), скрытие и удаление;
* **глава** — «+ Раздел», «Переименовать главу», «Скрыть/Опубликовать», «Удалить главу»;
у каждого раздела — переименование, скрытие, перенос статей и удаление, у каждой статьи — «Править».
у каждого раздела — переименование, скрытие, перенос статей и удаление, у каждой статьи — иконка-карандаш.
На странице статьи боковое содержание: на десктопе закреплено (своя прокрутка), на мобильных
открывается кнопкой «Содержание» справа. На главной `/learn` на мобильных три кнопки
(«+ Статья», «+ Глава», «Импорт zip») идут под заголовком в один ряд.
Порядок меняется перетаскиванием за значок **⠿** (сохраняется сразу, без перезагрузки страницы):
на главной `/learn` — главы, разделы и статьи (в том числе перенос раздела в другую главу и

View File

@@ -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.