# Модель данных и классов Ниже — диаграммы классов по сервисам (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 { <> 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 { <> 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)" ```