Files
LMS/docs/03-domain-model.md
Vladislav Kopnin 1238265c8e Content import: sync sections and articles (archive is the source of truth)
- importZip now updates titles/positions of existing chapters and sections and
  removes sections/articles of archived chapters that are missing in the archive
  (so restructured material no longer leaves stale sections behind); removed
  count is reported in ContentImportResult and in the admin flash message
- docs: note about import semantics in the class diagram and sequences
2026-09-16 22:00:22 +03:00

643 lines
18 KiB
Markdown

# Модель данных и классов
Ниже — диаграммы классов по сервисам (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
+reorder(ReorderTree)
}
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
note for ContentService "Импорт ZIP — источник истины для глав из архива: устаревшие разделы и статьи удаляются (removed)"
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)"
```