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