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
This commit is contained in:
10
README.md
10
README.md
@@ -2,6 +2,16 @@
|
||||
|
||||
Микросервисная система для проведения практических занятий (Kotlin + Spring Boot + PostgreSQL 18).
|
||||
|
||||
## Документация
|
||||
|
||||
UML- и архитектурные диаграммы (Mermaid) — в каталоге [`docs/`](docs/README.md):
|
||||
|
||||
- [Архитектура](docs/01-architecture.md) — контекст, компоненты, модули, аутентификация;
|
||||
- [Развёртывание](docs/02-deployment.md) — Docker, nginx, тома, обновление;
|
||||
- [Модель данных и классов](docs/03-domain-model.md) — диаграммы классов по сервисам и ER-модель;
|
||||
- [Диаграммы последовательностей](docs/04-sequences.md) — вход, импорт ЛР, раздача вариантов, сдача, защита, оценки, материалы;
|
||||
- [Диаграммы состояний](docs/05-states.md) — защита, публикация оценок и материалов, сдача, пароль.
|
||||
|
||||
## Модули
|
||||
|
||||
- `libs/common-http` — общий формат ошибок и обработчик исключений.
|
||||
|
||||
160
docs/01-architecture.md
Normal file
160
docs/01-architecture.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# Архитектура системы
|
||||
|
||||
## 1. Контекст (какие внешние участники и системы)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
user["Преподаватель / студент<br/>(браузер)"]
|
||||
nginx["nginx<br/>lms.our-place.ru<br/>TLS, reverse proxy"]
|
||||
web["web BFF<br/>Thymeleaf + htmx<br/>:8090"]
|
||||
gw["gateway<br/>Spring Cloud Gateway<br/>:8080"]
|
||||
id["identity-service<br/>:8081"]
|
||||
ac["academic-service<br/>:8082"]
|
||||
pr["progress-service<br/>:8083"]
|
||||
db[("PostgreSQL 18<br/>схемы identity / academic / progress")]
|
||||
fs[("Файловое хранилище<br/>content-assets, submissions")]
|
||||
|
||||
user -->|HTTPS| nginx
|
||||
nginx -->|HTTP 127.0.0.1:8090| web
|
||||
web -->|Bearer JWT| gw
|
||||
gw --> id
|
||||
gw --> ac
|
||||
gw --> pr
|
||||
id --> db
|
||||
ac --> db
|
||||
pr --> db
|
||||
ac --> fs
|
||||
pr --> fs
|
||||
pr -->|REST, внутренняя сеть| ac
|
||||
pr -->|REST, внутренняя сеть| id
|
||||
id -.->|JWKS: /api/v1/auth/jwks.json| gw
|
||||
id -.->|JWKS| ac
|
||||
id -.->|JWKS| pr
|
||||
```
|
||||
|
||||
- Браузер никогда не получает JWT: токен живёт в HTTP-сессии web-сервиса
|
||||
(`lms.token`), а в браузер уходит только cookie сессии (`HttpOnly`, 30 дней).
|
||||
- Все REST-запросы web проксирует на gateway, добавляя `Authorization: Bearer`.
|
||||
- Gateway проверяет подпись токена по JWKS identity и передаёт вниз заголовки
|
||||
`X-Account-Id`, `X-Account-Login`, `X-Account-Role`.
|
||||
- Каждый сервис дополнительно валидирует JWT самостоятельно (resource server).
|
||||
|
||||
## 2. Компоненты по сервисам
|
||||
|
||||
```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"]
|
||||
wmd["MarkdownRenderer<br/>(libs:markdown)"]
|
||||
wc --> wapi
|
||||
wc --> wmd
|
||||
wi --> wc
|
||||
end
|
||||
|
||||
subgraph gw["gateway (:8080)"]
|
||||
routes["Маршруты<br/>identity / academic / progress"]
|
||||
filter["JwtAuthenticationFilter<br/>order -100"]
|
||||
filter --> routes
|
||||
end
|
||||
|
||||
subgraph id["identity-service (:8081)"]
|
||||
idc["AuthController, GroupController, StudentController"]
|
||||
ids["AuthService, GroupService, Passwords,<br/>StudentImportParser, CredentialsExporter"]
|
||||
idr["AccountRepository, GroupRepository,<br/>EnrollmentRepository, StudentRepository, SettingRepository"]
|
||||
idc --> ids --> idr
|
||||
end
|
||||
|
||||
subgraph ac["academic-service (:8082)"]
|
||||
acc["LabController, ContentPublicController,<br/>ContentAdminController"]
|
||||
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"]
|
||||
prcl["AcademicClient, IdentityClient"]
|
||||
prc --> prs
|
||||
prs --> prr
|
||||
prs --> prcl
|
||||
end
|
||||
|
||||
wapi -->|HTTP| routes
|
||||
routes --> idc
|
||||
routes --> acc
|
||||
routes --> prc
|
||||
prcl -->|GET /api/v1/labs/{id}| acc
|
||||
prcl -->|GET /api/v1/groups и /{id}| idc
|
||||
```
|
||||
|
||||
## 3. Модули Gradle
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
web[":web"] --> common[":libs:common-http"]
|
||||
web --> md[":libs:markdown"]
|
||||
gw[":services:gateway"] --> sec[":libs:security-jwt"]
|
||||
gw --> common
|
||||
id[":services:identity-service"] --> common
|
||||
id --> sec
|
||||
ac[":services:academic-service"] --> common
|
||||
ac --> sec
|
||||
ac --> md
|
||||
pr[":services:progress-service"] --> common
|
||||
pr --> sec
|
||||
tests[":libs:test-support"] -.-> id
|
||||
tests -.-> ac
|
||||
tests -.-> pr
|
||||
```
|
||||
|
||||
| Модуль | Назначение |
|
||||
|---|---|
|
||||
| `:libs:common-http` | единый формат ошибок (`ApiError`), иерархия `LmsException`, `LmsExceptionHandler`, генератор UUIDv7 (`Ids`) |
|
||||
| `:libs:security-jwt` | свойства JWT, парсинг RSA-ключей, автоконфигурация `JwtEncoder`/`JwtDecoder`/`PasswordEncoder`, enum `LmsRole` |
|
||||
| `:libs:markdown` | markdown → безопасный HTML (OWASP-санитайзер), таблицы, спойлеры `:::spoiler` |
|
||||
| `:libs:test-support` | константы для интеграционных тестов с PostgreSQL |
|
||||
|
||||
## 4. Аутентификация и авторизация
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
a["Клиент: POST /api/v1/auth/login"] --> b["gateway: путь публичный<br/>(login/setup/health/jwks)"]
|
||||
b --> c["identity: AuthService.login"]
|
||||
c --> d{"BCrypt совпал?"}
|
||||
d -->|нет| e["401 + ApiError"]
|
||||
d -->|да| f["JwtEncoder (RSA-2048, kid=lms)<br/>claims: sub, login, role, exp"]
|
||||
f --> g["AuthResponse{token,...}"]
|
||||
|
||||
h["Клиент: запрос к защищённому API<br/>Authorization: Bearer ..."] --> i["gateway JwtAuthenticationFilter"]
|
||||
i --> j{"Токен валиден по JWKS?"}
|
||||
j -->|нет| k["401 Unauthorized"]
|
||||
j -->|да| l["Добавляет X-Account-Id/Login/Role"]
|
||||
l --> m["Сервис: SecurityConfig<br/>oauth2ResourceServer(jwt)"]
|
||||
m --> n{"@PreAuthorize<br/>ROLE_ADMIN / isAuthenticated"}
|
||||
n -->|нет| o["403 Forbidden"]
|
||||
n -->|да| p["Контроллер → сервис → БД"]
|
||||
```
|
||||
|
||||
- Роли: `ADMIN`, `STUDENT`; конвертер ролей читает claim `role` как `ROLE_<role>`.
|
||||
- Публичные пути: `/actuator/**`, `/api/v1/auth/{health,setup,login,jwks.json}`,
|
||||
`/api/v1/content/tree`, `/api/v1/content/assets/**`,
|
||||
`/api/v1/content/{chapter}/{section}/{article}`.
|
||||
- RSA-ключи identity хранятся в таблице `settings` и публикуются в JWKS.
|
||||
|
||||
## 5. Рендеринг страниц и защита форм (web)
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
req["HTTP-запрос"] --> csrf["CsrfInterceptor /**<br/>GET: выдаёт token в сессию<br/>POST: сверяет _csrf"]
|
||||
csrf --> must["MustChangePasswordInterceptor /**<br/>(кроме /change-password, /logout, /login, статики)"]
|
||||
must --> auth["AuthInterceptor<br/>/admin/**, /student/**, /change-password"]
|
||||
auth --> adm["AdminInterceptor /admin/**"]
|
||||
adm --> ctrl["Контроллер → ApiClient → gateway"]
|
||||
```
|
||||
|
||||
- `GlobalModelAdvice` кладёт `currentUser` (SessionUser) во все шаблоны.
|
||||
- Скрытые материалы и полное дерево материалов доступны только администратору.
|
||||
108
docs/02-deployment.md
Normal file
108
docs/02-deployment.md
Normal file
@@ -0,0 +1,108 @@
|
||||
# Развёртывание
|
||||
|
||||
## 1. Диаграмма развёртывания
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
internet["Интернет"]
|
||||
subgraph host["Сервер lms.our-place.ru (Ubuntu, Docker)"]
|
||||
nginx["nginx (host)<br/>80/443, TLS (certbot)<br/>proxy → 127.0.0.1:8090"]
|
||||
subgraph net["docker network: lms"]
|
||||
web["container: web<br/>lms-web:latest<br/>:8090"]
|
||||
gw["container: gateway<br/>lms-gateway:latest<br/>:8080"]
|
||||
id["container: identity<br/>lms-identity:latest<br/>:8081"]
|
||||
ac["container: academic<br/>lms-academic:latest<br/>:8082"]
|
||||
pr["container: progress<br/>lms-progress:latest<br/>:8083"]
|
||||
pg["container: postgres<br/>postgres:18<br/>:5432"]
|
||||
vol1[("volume lms_pgdata")]
|
||||
vol2[("volume lms_assets<br/>/data/content-assets")]
|
||||
vol3[("volume lms_submissions<br/>/data/submissions")]
|
||||
end
|
||||
end
|
||||
|
||||
internet -->|HTTPS| nginx
|
||||
nginx -->|HTTP| web
|
||||
web --> gw
|
||||
gw --> id
|
||||
gw --> ac
|
||||
gw --> pr
|
||||
id --> pg
|
||||
ac --> pg
|
||||
pr --> pg
|
||||
ac --> vol2
|
||||
pr --> vol3
|
||||
pg --> vol1
|
||||
```
|
||||
|
||||
Наружу публикуется только web на `127.0.0.1:8090`; доступ снаружи — через nginx и TLS.
|
||||
|
||||
## 2. Контейнеры и переменные окружения
|
||||
|
||||
| Сервис | Образ / стадия Dockerfile | Порт | Ключевые переменные | Тома |
|
||||
|---|---|---|---|---|
|
||||
| postgres | `postgres:18` | 5432 (внутр.) | `POSTGRES_DB`, `POSTGRES_USER`, `POSTGRES_PASSWORD` | `lms_pgdata` |
|
||||
| identity | `target: identity` | 8081 | `LMS_DB_URL` (схема `identity`), `LMS_DB_USER`, `LMS_DB_PASSWORD` | — |
|
||||
| academic | `target: academic` | 8082 | `LMS_DB_URL` (`academic`), `LMS_CONTENT_ASSETS_DIR`, `LMS_JWT_JWK_SET_URI` | `lms_assets` |
|
||||
| progress | `target: progress` | 8083 | `LMS_DB_URL` (`progress`), `LMS_SUBMISSIONS_DIR`, `LMS_IDENTITY_URI`, `LMS_ACADEMIC_URI`, `LMS_JWT_JWK_SET_URI` | `lms_submissions` |
|
||||
| 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_JWT_JWK_SET_URI` обязателен для gateway, academic и progress —
|
||||
в контейнерах это `http://identity:8081/api/v1/auth/jwks.json` (иначе 401).
|
||||
|
||||
## 3. Сборка
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
src["Исходники<br/>services/, libs/, web/, buildSrc/"]
|
||||
subgraph build["Dockerfile, стадия build (eclipse-temurin:21-jdk)"]
|
||||
g["./gradlew bootJar -x test<br/>(Gradle-кэш в cache-mount)"]
|
||||
end
|
||||
jar["bootJar-артефакты"]
|
||||
subgraph run["Runtime-стадии (eclipse-temurin:21-jre)"]
|
||||
r["java -jar app.jar<br/>TZ=Europe/Moscow"]
|
||||
end
|
||||
src --> g --> jar --> r
|
||||
```
|
||||
|
||||
Java на сервере не нужна: сборка и запуск — внутри контейнеров.
|
||||
`deploy/deploy.sh up` собирает образы и запускает стек из `docker-compose.prod.yml`.
|
||||
|
||||
## 4. Nginx
|
||||
|
||||
`deploy/nginx/lms.our-place.ru.conf`:
|
||||
|
||||
- `listen 80` (TLS добавляет `certbot --nginx`), `server_name lms.our-place.ru`;
|
||||
- ACME-челлендж через `/.well-known/acme-challenge/`;
|
||||
- `client_max_body_size 120m` — загрузка ZIP-архивов лабораторных и решений;
|
||||
- `proxy_pass http://127.0.0.1:8090` c заголовками
|
||||
`Host`, `X-Real-IP`, `X-Forwarded-For`, `X-Forwarded-Proto`, `X-Forwarded-Host`;
|
||||
- таймауты 300 с на чтение/отправку.
|
||||
|
||||
Web-сервис запускается с `server.forward-headers-strategy: framework`,
|
||||
поэтому корректно формирует https-редиректы за прокси.
|
||||
|
||||
## 5. Обновление и данные
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
autonumber
|
||||
participant Dev as Разработчик
|
||||
participant Git as Gitea
|
||||
participant Srv as Сервер /opt/LMS
|
||||
participant DC as docker compose
|
||||
participant PG as postgres
|
||||
|
||||
Dev->>Git: git push origin master
|
||||
Srv->>Git: git pull
|
||||
Srv->>DC: ./deploy/deploy.sh up
|
||||
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 автоматически при старте сервисов
|
||||
```
|
||||
|
||||
- Резервные копии: `pg_dump` (том `lms_pgdata`) и `tar` томов `lms_assets`,
|
||||
`lms_submissions` — команды в [deploy/DEPLOY.md](../deploy/DEPLOY.md).
|
||||
- Порядок наполнения данными (админ → группы → лабораторная → варианты →
|
||||
оценки/защита → материалы) описан в [deploy/IMPORT.md](../deploy/IMPORT.md).
|
||||
639
docs/03-domain-model.md
Normal file
639
docs/03-domain-model.md
Normal file
@@ -0,0 +1,639 @@
|
||||
# Модель данных и классов
|
||||
|
||||
Ниже — диаграммы классов по сервисам (JPA-сущности + репозитории + сервисы),
|
||||
ER-модель БД и модель web-слоя.
|
||||
|
||||
## 1. identity-service
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Account {
|
||||
+UUID id
|
||||
+String login
|
||||
+String passwordHash
|
||||
+LmsRole role
|
||||
+Boolean mustChangePassword
|
||||
+String tempPassword
|
||||
+Instant createdAt
|
||||
+Instant updatedAt
|
||||
}
|
||||
class Student {
|
||||
+UUID accountId
|
||||
+String studentNo
|
||||
+String lastName
|
||||
+String firstName
|
||||
+String middleName
|
||||
+String basis
|
||||
+String note
|
||||
+String fullName
|
||||
}
|
||||
class GroupEntity {
|
||||
+UUID id
|
||||
+String name
|
||||
+Int taskDurationMin
|
||||
+Instant createdAt
|
||||
}
|
||||
class Enrollment {
|
||||
+UUID id
|
||||
+Instant enrolledAt
|
||||
}
|
||||
class Setting {
|
||||
+String key
|
||||
+String value
|
||||
}
|
||||
class LmsRole {
|
||||
<<enumeration>>
|
||||
ADMIN
|
||||
STUDENT
|
||||
}
|
||||
|
||||
Account "1" --> "1" Student : @MapsId account_id
|
||||
GroupEntity "1" --> "*" Enrollment : group_id
|
||||
Student "1" --> "*" Enrollment : student_id
|
||||
Account --> LmsRole : role
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class AuthController
|
||||
class GroupController
|
||||
class StudentController
|
||||
class AuthService {
|
||||
+adminExists() Boolean
|
||||
+setup(SetupRequest) AuthResponse
|
||||
+login(LoginRequest) AuthResponse
|
||||
+me(UUID) MeResponse
|
||||
+changePassword(UUID, ChangePasswordRequest)
|
||||
}
|
||||
class GroupService {
|
||||
+list() List~GroupSummary~
|
||||
+create(CreateGroupRequest) GroupSummary
|
||||
+createStudent(UUID, CreateStudentRequest) Credential
|
||||
+candidates(UUID) List~StudentCandidate~
|
||||
+enroll(UUID, UUID) GroupDetail
|
||||
+unenroll(UUID, UUID) GroupDetail
|
||||
+import(String, String, ByteArray) ImportResult
|
||||
+groupOfStudent(UUID) GroupSummary
|
||||
+detail(UUID) GroupDetail
|
||||
+resetPassword(UUID) ResetPasswordResponse
|
||||
+credentials(UUID, String) ExportFile
|
||||
}
|
||||
class AccountRepository
|
||||
class GroupRepository
|
||||
class EnrollmentRepository
|
||||
class StudentRepository
|
||||
class SettingRepository
|
||||
class StudentImportParser {
|
||||
+parse(String, ByteArray) ParsedGroup
|
||||
}
|
||||
class CredentialsExporter {
|
||||
+csv(List~StudentRow~) ByteArray
|
||||
+xlsx(List~StudentRow~) ByteArray
|
||||
}
|
||||
class Passwords {
|
||||
+generate(Int) String
|
||||
}
|
||||
|
||||
AuthController --> AuthService
|
||||
GroupController --> GroupService
|
||||
StudentController --> GroupService
|
||||
AuthService --> AccountRepository
|
||||
AuthService --> StudentRepository
|
||||
GroupService --> GroupRepository
|
||||
GroupService --> EnrollmentRepository
|
||||
GroupService --> StudentRepository
|
||||
GroupService --> AccountRepository
|
||||
GroupService --> StudentImportParser
|
||||
GroupService --> CredentialsExporter
|
||||
GroupService --> Passwords
|
||||
```
|
||||
|
||||
## 2. academic-service
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class Semester {
|
||||
+UUID id
|
||||
+String title
|
||||
+Int year
|
||||
+Boolean active
|
||||
}
|
||||
class Lab {
|
||||
+UUID id
|
||||
+Int number
|
||||
+String title
|
||||
+String descriptionMd
|
||||
+Instant deadlineAt
|
||||
+Instant defenseAt
|
||||
+BigDecimal pointsMax
|
||||
+String status
|
||||
+Instant createdAt
|
||||
}
|
||||
class LabLevel {
|
||||
+UUID id
|
||||
+String code
|
||||
+String title
|
||||
+String description
|
||||
+Int position
|
||||
+BigDecimal pointsMax
|
||||
}
|
||||
class Variant {
|
||||
+UUID id
|
||||
+String number
|
||||
+String title
|
||||
+String statementMd
|
||||
+Int position
|
||||
}
|
||||
class ContentChapter {
|
||||
+UUID id
|
||||
+String title
|
||||
+String slug
|
||||
+Int position
|
||||
+Boolean published
|
||||
}
|
||||
class ContentSection {
|
||||
+UUID id
|
||||
+UUID chapterId
|
||||
+String title
|
||||
+String slug
|
||||
+Int position
|
||||
+Boolean published
|
||||
}
|
||||
class ContentArticle {
|
||||
+UUID id
|
||||
+UUID sectionId
|
||||
+String title
|
||||
+String slug
|
||||
+String bodyMd
|
||||
+Int position
|
||||
+Boolean published
|
||||
}
|
||||
|
||||
Semester "1" --> "*" Lab : semester_id
|
||||
Lab "1" --> "*" LabLevel : lab_id
|
||||
LabLevel "1" --> "*" Variant : lab_level_id
|
||||
Lab "1" --> "*" Variant : lab_id
|
||||
ContentChapter "1" --> "*" ContentSection : chapter_id
|
||||
ContentSection "1" --> "*" ContentArticle : section_id
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class LabController
|
||||
class ContentPublicController
|
||||
class ContentAdminController
|
||||
class LabService {
|
||||
+importFromFile(String, ByteArray) LabImportResult
|
||||
+list() List~LabSummary~
|
||||
+detail(UUID) LabDetail
|
||||
+create(LabCreateRequest) LabDetail
|
||||
+update(UUID, LabUpdateRequest) LabDetail
|
||||
+delete(UUID)
|
||||
+toggleStatus(UUID)
|
||||
+addLevel(UUID, LevelSaveRequest)
|
||||
+renameLevel(UUID, UUID, LevelRenameRequest)
|
||||
+deleteLevel(UUID, UUID)
|
||||
+addVariant(UUID, VariantSaveRequest)
|
||||
+updateVariant(UUID, UUID, VariantSaveRequest)
|
||||
+deleteVariant(UUID, UUID)
|
||||
}
|
||||
class ContentService {
|
||||
+tree(Boolean) ContentTree
|
||||
+treeAll() ContentTree
|
||||
+findArticle(String, String, String, Boolean) ContentArticleView
|
||||
+setChapterPublished(UUID, Boolean)
|
||||
+setSectionPublished(UUID, Boolean)
|
||||
+setArticlePublished(UUID, Boolean)
|
||||
+listAll() List~ContentArticleView~
|
||||
+createChapter(String, String) ChapterTreeNode
|
||||
+createSection(UUID, String, String) SectionTreeNode
|
||||
+createArticlePlaced(ArticleCreatePayload) ContentArticleView
|
||||
+updateArticle(UUID, String, String)
|
||||
+moveArticle(UUID, UUID) ContentArticleView
|
||||
+deleteArticle(UUID)
|
||||
+importZip(String, ByteArray) ContentImportResult
|
||||
}
|
||||
class LabImportParser {
|
||||
+parse(String, ByteArray) LabImportBundle
|
||||
}
|
||||
class AssetStorage {
|
||||
+store(String, ByteArray) StoredAsset
|
||||
+read(String) Pair
|
||||
}
|
||||
class LabRepository
|
||||
class LabLevelRepository
|
||||
class VariantRepository
|
||||
class SemesterRepository
|
||||
class ContentChapterRepository
|
||||
class ContentSectionRepository
|
||||
class ContentArticleRepository
|
||||
|
||||
LabController --> LabService
|
||||
ContentPublicController --> ContentService
|
||||
ContentAdminController --> ContentService
|
||||
LabService --> LabRepository
|
||||
LabService --> LabLevelRepository
|
||||
LabService --> VariantRepository
|
||||
LabService --> SemesterRepository
|
||||
LabService --> LabImportParser
|
||||
ContentService --> ContentChapterRepository
|
||||
ContentService --> ContentSectionRepository
|
||||
ContentService --> ContentArticleRepository
|
||||
ContentService --> AssetStorage
|
||||
```
|
||||
|
||||
## 3. progress-service
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class VariantAssignment {
|
||||
+UUID id
|
||||
+UUID studentId
|
||||
+UUID labId
|
||||
+UUID variantId
|
||||
+Boolean manual
|
||||
+Instant assignedAt
|
||||
}
|
||||
class StudentLabLevel {
|
||||
+UUID studentId
|
||||
+UUID labId
|
||||
+UUID labLevelId
|
||||
+Boolean manual
|
||||
}
|
||||
class LabGrade {
|
||||
+UUID studentId
|
||||
+UUID labId
|
||||
+BigDecimal score
|
||||
+Instant updatedAt
|
||||
}
|
||||
class GradePublication {
|
||||
+UUID groupId
|
||||
+UUID labId
|
||||
+Instant publishedAt
|
||||
}
|
||||
class GradeComment {
|
||||
+UUID id
|
||||
+UUID studentId
|
||||
+UUID labId
|
||||
+String text
|
||||
+String tone
|
||||
+Instant createdAt
|
||||
}
|
||||
class CommentTemplate {
|
||||
+UUID id
|
||||
+String text
|
||||
+String tone
|
||||
+Int useCount
|
||||
}
|
||||
class Submission {
|
||||
+UUID id
|
||||
+UUID studentId
|
||||
+UUID labId
|
||||
+Int seq
|
||||
+Instant createdAt
|
||||
}
|
||||
class DefenseSession {
|
||||
+UUID id
|
||||
+UUID studentId
|
||||
+UUID labId
|
||||
+String assignmentMd
|
||||
+String status
|
||||
+String code
|
||||
+Instant startedAt
|
||||
+Instant readyAt
|
||||
+String readyReason
|
||||
}
|
||||
class DefenseStatus {
|
||||
<<enumeration>>
|
||||
waiting
|
||||
started
|
||||
ready
|
||||
}
|
||||
|
||||
CommentTemplate "1" --> "*" GradeComment : подсказки (text/tone)
|
||||
GradeComment "*" --> "1" LabGrade : поясняет оценку
|
||||
LabGrade "1" --> "0..1" GradePublication : видна студенту после публикации
|
||||
DefenseSession --> DefenseStatus : status
|
||||
Note for VariantAssignment : studentId / labId / variantId — идентификаторы других сервисов (FK на уровне БД нет)
|
||||
Note for LabGrade : groupId для публикации берётся из identity
|
||||
```
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ProgressController
|
||||
class DefenseAdminController
|
||||
class DefenseMyController
|
||||
class ProgressService {
|
||||
+setLevel(LevelAssignmentRequest, String) LevelAssignmentResponse
|
||||
+getLevel(UUID, UUID) StudentLabLevel
|
||||
+getLevelsForLab(UUID) List~StudentLabLevel~
|
||||
+distribute(DistributeRequest, String) DistributeResult
|
||||
+assignWithInfo(AssignRequest, String) VariantAssignmentResponse
|
||||
+assignmentsForLabEnriched(UUID, String) List~VariantAssignmentResponse~
|
||||
+setGrade(GradeSetRequest) GradeResponse
|
||||
+suggestComments(String) List~CommentOut~
|
||||
+myComments(UUID) List~GradeComment~
|
||||
+getStudentAssignment(UUID, UUID, String) MeAssignmentResponse
|
||||
}
|
||||
class SubmissionService {
|
||||
+submit(UUID, UUID, List~MultipartFile~, String) SubmissionDto
|
||||
+mySubmissions(UUID, UUID) List~SubmissionDto~
|
||||
+download(UUID, UUID, UUID, Boolean) Pair
|
||||
+latestPerStudent(UUID) List~SubmissionDto~
|
||||
}
|
||||
class DefenseService {
|
||||
+syncTimeouts() Int
|
||||
+assign(UUID, UUID, String) DefenseDto
|
||||
+issueCode(UUID) String
|
||||
+reset(UUID)
|
||||
+listForLab(UUID) List~DefenseDto~
|
||||
+startByCode(UUID, String) DefenseDto
|
||||
+finish(UUID, UUID) DefenseDto
|
||||
+mySessions(UUID) List~DefenseDto~
|
||||
+resetByStudentLab(UUID, UUID)
|
||||
}
|
||||
class GradePublicationService {
|
||||
+setPublished(UUID, UUID, Boolean)
|
||||
+isPublished(UUID, UUID) Boolean
|
||||
+publishedLabIdsForGroup(UUID) List~String~
|
||||
+publishedGroupIdsForLab(UUID) List~String~
|
||||
}
|
||||
class AcademicClient {
|
||||
+getLab(String, String) LabInfo
|
||||
+getLabSummary(String, String) LabInfo
|
||||
}
|
||||
class IdentityClient {
|
||||
+getGroup(String, String) GroupInfo
|
||||
+listGroups(String) List~Map~
|
||||
}
|
||||
class SubmissionStorage {
|
||||
+store(UUID, UUID, Int, List~MultipartFile~) StoredSubmission
|
||||
+zipOf(Path) ByteArray
|
||||
+listFiles(Path) List~String~
|
||||
+dirFor(UUID, UUID, Int) Path
|
||||
}
|
||||
class VariantAssignmentRepository
|
||||
class StudentLabLevelRepository
|
||||
class LabGradeRepository
|
||||
class GradeCommentRepository
|
||||
class CommentTemplateRepository
|
||||
class SubmissionRepository
|
||||
class DefenseSessionRepository
|
||||
class GradePublicationRepository
|
||||
|
||||
ProgressController --> ProgressService
|
||||
DefenseAdminController --> DefenseService
|
||||
DefenseMyController --> DefenseService
|
||||
ProgressController --> SubmissionService
|
||||
ProgressController --> GradePublicationService
|
||||
ProgressService --> VariantAssignmentRepository
|
||||
ProgressService --> StudentLabLevelRepository
|
||||
ProgressService --> LabGradeRepository
|
||||
ProgressService --> GradeCommentRepository
|
||||
ProgressService --> CommentTemplateRepository
|
||||
ProgressService --> AcademicClient
|
||||
ProgressService --> IdentityClient
|
||||
SubmissionService --> SubmissionRepository
|
||||
SubmissionService --> SubmissionStorage
|
||||
SubmissionService --> AcademicClient
|
||||
DefenseService --> DefenseSessionRepository
|
||||
GradePublicationService --> GradePublicationRepository
|
||||
```
|
||||
|
||||
## 4.web (BFF)
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class AuthPageController
|
||||
class LearnController
|
||||
class StudentController
|
||||
class AdminController
|
||||
class ContentAdminController
|
||||
class ApiClient {
|
||||
+login(String, String) AuthResponseDto
|
||||
+contentTree(String, Boolean) ContentTreeNodeDto
|
||||
+contentArticle(String, String, String, String, Boolean) ContentArticleDto
|
||||
+setContentPublished(String, String, String, Boolean)
|
||||
+groupDetail(String, String) GroupDetailDto
|
||||
+distribute(String, DistributeRequestPayload) DistributeResultDto
|
||||
+setGrade(String, String, String, String, String) GradeResponseDto
|
||||
+submit(String, String, List~MultipartFile~, String) List~SubmissionDto~
|
||||
+myGrades(String, String) List~GradeResponseDto~
|
||||
}
|
||||
class SessionUser {
|
||||
+String id
|
||||
+String login
|
||||
+String role
|
||||
+String name
|
||||
+Boolean mustChangePassword
|
||||
+isAdmin() Boolean
|
||||
+isStudent() Boolean
|
||||
}
|
||||
class SessionKeys {
|
||||
+String TOKEN
|
||||
+String USER
|
||||
}
|
||||
class CsrfInterceptor
|
||||
class AuthInterceptor
|
||||
class AdminInterceptor
|
||||
class MustChangePasswordInterceptor
|
||||
class MarkdownRenderer {
|
||||
+render(String) String
|
||||
}
|
||||
|
||||
AuthPageController --> ApiClient
|
||||
LearnController --> ApiClient
|
||||
StudentController --> ApiClient
|
||||
AdminController --> ApiClient
|
||||
ContentAdminController --> ApiClient
|
||||
LearnController --> MarkdownRenderer
|
||||
AuthPageController --> SessionUser
|
||||
AuthPageController --> SessionKeys
|
||||
CsrfInterceptor ..> SessionKeys
|
||||
AuthInterceptor ..> SessionUser
|
||||
AdminInterceptor ..> SessionUser
|
||||
MustChangePasswordInterceptor ..> SessionUser
|
||||
```
|
||||
|
||||
## 5. ER-модель БД
|
||||
|
||||
Схемы `identity`, `academic`, `progress` живут в одной БД `lms`.
|
||||
Между схемами внешних ключей нет: progress хранит `studentId`/`labId`/`groupId`
|
||||
как UUID и получает связанные данные по REST.
|
||||
|
||||
```mermaid
|
||||
erDiagram
|
||||
ACCOUNTS {
|
||||
uuid id PK
|
||||
varchar login UK
|
||||
varchar password_hash
|
||||
varchar role
|
||||
boolean must_change_password
|
||||
varchar temp_password
|
||||
timestamptz created_at
|
||||
timestamptz updated_at
|
||||
}
|
||||
STUDENTS {
|
||||
uuid account_id PK
|
||||
varchar student_no UK
|
||||
varchar last_name
|
||||
varchar first_name
|
||||
varchar middle_name
|
||||
varchar basis
|
||||
text note
|
||||
}
|
||||
GROUPS {
|
||||
uuid id PK
|
||||
varchar name UK
|
||||
int task_duration_min
|
||||
timestamptz created_at
|
||||
}
|
||||
ENROLLMENTS {
|
||||
uuid id PK
|
||||
uuid group_id FK
|
||||
uuid student_id FK
|
||||
timestamptz enrolled_at
|
||||
}
|
||||
SETTINGS {
|
||||
varchar key PK
|
||||
varchar value
|
||||
}
|
||||
|
||||
ACCOUNTS ||--|| STUDENTS : "account_id"
|
||||
GROUPS ||--o{ ENROLLMENTS : "group_id"
|
||||
STUDENTS ||--o{ ENROLLMENTS : "student_id"
|
||||
|
||||
SEMESTERS {
|
||||
uuid id PK
|
||||
varchar title
|
||||
int year
|
||||
boolean active
|
||||
}
|
||||
LABS {
|
||||
uuid id PK
|
||||
int number
|
||||
varchar title
|
||||
uuid semester_id FK
|
||||
text description_md
|
||||
timestamptz deadline_at
|
||||
timestamptz defense_at
|
||||
numeric points_max
|
||||
varchar status
|
||||
timestamptz created_at
|
||||
}
|
||||
LAB_LEVELS {
|
||||
uuid id PK
|
||||
uuid lab_id FK
|
||||
varchar code
|
||||
varchar title
|
||||
text description
|
||||
int position
|
||||
numeric points_max
|
||||
}
|
||||
VARIANTS {
|
||||
uuid id PK
|
||||
uuid lab_id FK
|
||||
uuid lab_level_id FK
|
||||
varchar number
|
||||
varchar title
|
||||
text statement_md
|
||||
int position
|
||||
}
|
||||
CHAPTERS {
|
||||
uuid id PK
|
||||
varchar title UK
|
||||
varchar slug UK
|
||||
int position
|
||||
boolean published
|
||||
}
|
||||
SECTIONS {
|
||||
uuid id PK
|
||||
uuid chapter_id FK
|
||||
varchar title
|
||||
varchar slug
|
||||
int position
|
||||
boolean published
|
||||
}
|
||||
ARTICLES {
|
||||
uuid id PK
|
||||
uuid section_id FK
|
||||
varchar title
|
||||
varchar slug
|
||||
text body_md
|
||||
int position
|
||||
boolean published
|
||||
}
|
||||
|
||||
SEMESTERS ||--o{ LABS : "semester_id"
|
||||
LABS ||--o{ LAB_LEVELS : "lab_id"
|
||||
LAB_LEVELS ||--o{ VARIANTS : "lab_level_id"
|
||||
CHAPTERS ||--o{ SECTIONS : "chapter_id"
|
||||
SECTIONS ||--o{ ARTICLES : "section_id"
|
||||
|
||||
VARIANT_ASSIGNMENTS {
|
||||
uuid id PK
|
||||
uuid student_id
|
||||
uuid lab_id
|
||||
uuid variant_id
|
||||
boolean manual
|
||||
timestamptz assigned_at
|
||||
}
|
||||
STUDENT_LAB_LEVELS {
|
||||
uuid student_id PK
|
||||
uuid lab_id PK
|
||||
uuid lab_level_id
|
||||
boolean manual
|
||||
}
|
||||
LAB_GRADES {
|
||||
uuid student_id PK
|
||||
uuid lab_id PK
|
||||
numeric score
|
||||
timestamptz updated_at
|
||||
}
|
||||
GRADE_PUBLICATIONS {
|
||||
uuid group_id PK
|
||||
uuid lab_id PK
|
||||
timestamptz published_at
|
||||
}
|
||||
GRADE_COMMENTS {
|
||||
uuid id PK
|
||||
uuid student_id
|
||||
uuid lab_id
|
||||
text text
|
||||
varchar tone
|
||||
timestamptz created_at
|
||||
}
|
||||
COMMENT_TEMPLATES {
|
||||
uuid id PK
|
||||
varchar text UK
|
||||
varchar tone
|
||||
int use_count
|
||||
}
|
||||
SUBMISSIONS {
|
||||
uuid id PK
|
||||
uuid student_id
|
||||
uuid lab_id
|
||||
int seq
|
||||
timestamptz created_at
|
||||
}
|
||||
DEFENSE_SESSIONS {
|
||||
uuid id PK
|
||||
uuid student_id
|
||||
uuid lab_id
|
||||
text assignment_md
|
||||
varchar status
|
||||
varchar code
|
||||
timestamptz started_at
|
||||
timestamptz ready_at
|
||||
varchar ready_reason
|
||||
}
|
||||
|
||||
VARIANT_ASSIGNMENTS }o--|| VARIANTS : "variant_id (не FK)"
|
||||
VARIANT_ASSIGNMENTS }o--|| LABS : "lab_id (не FK)"
|
||||
STUDENT_LAB_LEVELS }o--|| LABS : "lab_id (не FK)"
|
||||
LAB_GRADES }o--|| LABS : "lab_id (не FK)"
|
||||
GRADE_PUBLICATIONS }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)"
|
||||
```
|
||||
324
docs/04-sequences.md
Normal file
324
docs/04-sequences.md
Normal file
@@ -0,0 +1,324 @@
|
||||
# Диаграммы последовательностей
|
||||
|
||||
## 1. Вход в систему и выдача JWT
|
||||
|
||||
```mermaid
|
||||
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
|
||||
|
||||
```mermaid
|
||||
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. Первый вход: принудительная смена пароля
|
||||
|
||||
```mermaid
|
||||
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)
|
||||
|
||||
```mermaid
|
||||
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. Раздача вариантов
|
||||
|
||||
```mermaid
|
||||
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. Сдача решения студентом
|
||||
|
||||
```mermaid
|
||||
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. Защита лабораторной
|
||||
|
||||
```mermaid
|
||||
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. Оценка, комментарии и публикация
|
||||
|
||||
```mermaid
|
||||
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. Учебные материалы: публикация и просмотр
|
||||
|
||||
```mermaid
|
||||
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. Страница группы у администратора (агрегация данных)
|
||||
|
||||
```mermaid
|
||||
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: страница группы (оценки, защиты, поиск, фильтры, сортировка)
|
||||
```
|
||||
127
docs/05-states.md
Normal file
127
docs/05-states.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Диаграммы состояний
|
||||
|
||||
## 1. Сессия защиты (`defense_sessions.status`)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> waiting : преподаватель назначил задание
|
||||
waiting --> waiting : выдан/обновлён код
|
||||
waiting --> started : студент ввёл верный код
|
||||
started --> ready : студент завершил работу
|
||||
started --> ready : истекло время (syncTimeouts, readyReason=timeout)
|
||||
ready --> waiting : «доп. задание: начать заново» (reset)
|
||||
ready --> [*] : задание принято
|
||||
note right of waiting
|
||||
code = NULL — ждёт кода
|
||||
code != NULL — код выдан
|
||||
end note
|
||||
```
|
||||
|
||||
## 2. Публикация оценок (пара «группа + лабораторная»)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> hidden : оценка выставлена (lab_grades)
|
||||
hidden --> published : «опубликовать оценки» (grade_publications)
|
||||
published --> hidden : «скрыть оценки»
|
||||
note right of hidden
|
||||
студент не видит ни балл,
|
||||
ни комментарии
|
||||
end note
|
||||
note right of published
|
||||
студенты группы видят балл и
|
||||
комментарии, сгруппированные по тону
|
||||
end note
|
||||
```
|
||||
|
||||
## 3. Статус лабораторной (`labs.status`)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> draft : импорт/создание
|
||||
draft --> published : публикация
|
||||
published --> draft : снятие с публикации
|
||||
note right of draft
|
||||
студентам не видна;
|
||||
доступна для настройки
|
||||
end note
|
||||
note right of published
|
||||
видна студентам: условие, вариант,
|
||||
сдача; оценки — только после публикации
|
||||
end note
|
||||
```
|
||||
|
||||
## 4. Публикация материалов (главы, разделы, статьи)
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
state "опубликовано" as pub
|
||||
state "скрыто" as hid
|
||||
[*] --> pub : импорт/создание (published=true)
|
||||
pub --> hid : «Скрыть» (published=false)
|
||||
hid --> pub : «Опубликовать»
|
||||
note right of hid
|
||||
Не видно гостям и студентам:
|
||||
глава целиком скрывает разделы и статьи,
|
||||
раздел — свои статьи.
|
||||
Администратор видит их с бейджем «скрыто»
|
||||
и может открыть для предпросмотра.
|
||||
end note
|
||||
```
|
||||
|
||||
## 5. Жизненный цикл сдачи решения
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> notSubmitted : лабораторная выдана
|
||||
notSubmitted --> submitted : сдача (seq = 1, файлы на диске)
|
||||
submitted --> submitted : переотправка (seq + 1, хранится вся история)
|
||||
submitted --> closed : наступил дедлайн (deadlineAt)
|
||||
submitted --> closed : выдано индивидуальное задание на защиту
|
||||
closed --> [*]
|
||||
note right of closed
|
||||
Форма отправки скрыта,
|
||||
доступно только скачивание
|
||||
ранее отправленных решений
|
||||
end note
|
||||
note right of submitted
|
||||
Скачать можно любую попытку;
|
||||
преподаватель видит последнюю
|
||||
end note
|
||||
```
|
||||
|
||||
## 6. Вариант студента
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> none : студент в группе, ЛР опубликована
|
||||
none --> auto : распределение («Раздать»)
|
||||
auto --> manual : «Закрепить» конкретный вариант
|
||||
auto --> auto : повторное распределение без force (не трогает занятые)
|
||||
manual --> manual : force не перезаписывает ручные закрепления
|
||||
auto --> none : force (сброс авто-назначений)
|
||||
note right of auto
|
||||
подбирается наименее занятый вариант
|
||||
своего уровня; пока есть невыданные —
|
||||
повторов нет
|
||||
end note
|
||||
```
|
||||
|
||||
## 7. Пароль студента
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> temporary : аккаунт создан / «Сбросить пароль»
|
||||
temporary --> own : смена на /change-password
|
||||
own --> temporary : «Сбросить пароль» преподавателем
|
||||
note right of temporary
|
||||
must_change_password = true,
|
||||
temp_password хранится;
|
||||
пока пароль не сменён, доступны только
|
||||
/change-password, /logout и статика
|
||||
end note
|
||||
note right of own
|
||||
must_change_password = false,
|
||||
temp_password = NULL
|
||||
end note
|
||||
```
|
||||
30
docs/README.md
Normal file
30
docs/README.md
Normal file
@@ -0,0 +1,30 @@
|
||||
# Документация LMS
|
||||
|
||||
UML- и архитектурные диаграммы проекта. Все диаграммы — в формате **Mermaid**,
|
||||
поэтому они рендерятся прямо в Gitea/GitHub, в VS Code (расширение Markdown
|
||||
Preview Mermaid Support) и в IntelliJ IDEA (плагин Mermaid).
|
||||
|
||||
## Содержание
|
||||
|
||||
| Документ | Что описывает |
|
||||
|---|---|
|
||||
| [01-architecture.md](01-architecture.md) | Контекст системы, компоненты, модули Gradle, схема аутентификации |
|
||||
| [02-deployment.md](02-deployment.md) | Диаграмма развёртывания: Docker, nginx, тома, порты, переменные окружения |
|
||||
| [03-domain-model.md](03-domain-model.md) | Диаграммы классов по сервисам, ER-модель БД, модель web-слоя |
|
||||
| [04-sequences.md](04-sequences.md) | Диаграммы последовательностей: вход, импорт ЛР, раздача вариантов, сдача, защита, оценки, материалы |
|
||||
| [05-states.md](05-states.md) | Диаграммы состояний: защита, публикация оценок, статус ЛР, публикация материалов |
|
||||
|
||||
## Кратко о системе
|
||||
|
||||
LMS — микросервисное приложение на Kotlin/Spring Boot 3 (Java 21) с
|
||||
server-side-rendered UI на Thymeleaf + htmx:
|
||||
|
||||
- **gateway** (`:8080`) — единая точка входа для REST API: маршрутизация и проверка JWT;
|
||||
- **identity-service** (`:8081`) — аккаунты, группы, студенты, аутентификация (JWT + JWKS);
|
||||
- **academic-service** (`:8082`) — семестры, лабораторные (уровни, варианты), учебные материалы;
|
||||
- **progress-service** (`:8083`) — варианты/уровни студентов, сдачи, оценки и комментарии, защиты;
|
||||
- **web** (`:8090`) — BFF: сессии, CSRF, страницы админа и студента, markdown-рендер;
|
||||
- **PostgreSQL 18** — одна БД `lms` со схемами `identity`, `academic`, `progress`.
|
||||
|
||||
Поток запроса: браузер → nginx → web → gateway → сервис → PostgreSQL.
|
||||
Подробности — в [01-architecture.md](01-architecture.md).
|
||||
Reference in New Issue
Block a user