Files
LMS/docs/03-domain-model.md

22 KiB

Модель данных и классов

Ниже — диаграммы классов по сервисам (JPA-сущности + репозитории + сервисы), ER-модель БД и модель web-слоя.

1. identity-service

classDiagram
    class Account {
        +UUID id
        +String login
        +String passwordHash
        +LmsRole role
        +AccountStatus status
        +Instant archivedAt
        +Boolean mustChangePassword
        +String tempPassword
        +Instant createdAt
        +Instant updatedAt
    }
    class Student {
        +UUID accountId
        +String studentNo
        +String lastName
        +String firstName
        +String middleName
        +String basis
        +String note
        +StudentStatus status
        +Short enrollmentYear
        +String fullName
    }
    class GroupEntity {
        +UUID id
        +String name
        +String stream
        +Short course
        +Short number
        +String degree
        +Short enrollmentYear
        +GroupStatus status
        +Int taskDurationMin
        +Instant createdAt
    }
    class Enrollment {
        +UUID id
        +Instant enrolledAt
    }
    class MembershipHistory {
        +UUID id
        +UUID studentId
        +UUID fromGroupId
        +UUID toGroupId
        +MembershipAction action
        +String reason
        +UUID actorAccountId
        +UUID promotionRunId
        +Instant at
    }
    class Setting {
        +String key
        +String value
    }
    class LmsRole {
        <<enumeration>>
        ADMIN
        TEACHER
        STUDENT
    }

    Account "1" --> "1" Student : @MapsId account_id
    GroupEntity "1" --> "*" Enrollment : group_id
    Student "1" --> "*" Enrollment : student_id
    Student "1" --> "*" MembershipHistory : student_id
    GroupEntity "1" --> "*" MembershipHistory : from/to_group_id
    Account --> LmsRole : role
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, UUID) Credential
        +candidates(UUID) List~StudentCandidate~
        +enroll(UUID, UUID, UUID) GroupDetail
        +unenroll(UUID, UUID, UUID) GroupDetail
        +groupOfStudent(UUID) GroupSummary
        +detail(UUID) GroupDetail
        +resetPassword(UUID) ResetPasswordResponse
        +credentials(UUID, String) ExportFile
    }
    class GroupImportService {
        +import(String, ByteArray, UUID) ImportResult
    }
    class GroupCohortResolver {
        +resolve(CreateGroupRequest) GroupCohort
        +validate(GroupCohort) GroupCohort
        +defaultEnrollmentYear(Short) Short
    }
    class AccountRepository
    class GroupRepository
    class EnrollmentRepository
    class StudentRepository
    class MembershipHistoryRepository
    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
    GroupController --> GroupImportService
    StudentController --> GroupService
    AuthService --> AccountRepository
    AuthService --> StudentRepository
    GroupService --> GroupRepository
    GroupService --> EnrollmentRepository
    GroupService --> StudentRepository
    GroupService --> AccountRepository
    GroupService --> MembershipHistoryRepository
    GroupService --> GroupCohortResolver
    GroupImportService --> GroupRepository
    GroupImportService --> EnrollmentRepository
    GroupImportService --> StudentRepository
    GroupImportService --> AccountRepository
    GroupImportService --> MembershipHistoryRepository
    GroupImportService --> StudentImportParser
    GroupService --> CredentialsExporter
    GroupService --> Passwords

2. academic-service

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
classDiagram
    class LabController
    class ContentPublicController
    class ContentAdminController
    class ContentOrderController
    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
    ContentOrderController --> ContentService
    LabService --> LabRepository
    LabService --> LabLevelRepository
    LabService --> VariantRepository
    LabService --> SemesterRepository
    LabService --> LabImportParser
    ContentService --> ContentChapterRepository
    ContentService --> ContentSectionRepository
    ContentService --> ContentArticleRepository
    ContentService --> AssetStorage

3. progress-service

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
        +String generalComment
        +Instant updatedAt
    }
    class GradePublication {
        +UUID groupId
        +UUID labId
        +Instant publishedAt
    }
    class GradePublicationStudent {
        +UUID groupId
        +UUID labId
        +UUID studentId
        +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
    }

    GradePublication "1" --> "*" GradePublicationStudent : легаси «вся группа» и персональные
    Note for GradePublicationStudent : персональная публикация оценки/комментариев студенту
    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
classDiagram
    class ProgressController
    class SubmissionController
    class DefenseAdminController
    class DefenseMyController
    class StudentPurgeController
    class GroupLabDataController
    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~CommentSuggestion~
        +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 {
        +state(UUID, UUID) PublicationState
        +setStudents(UUID, UUID, List~UUID~)
        +isPublished(UUID, UUID, UUID) Boolean
    }
    class StudentPurgeService {
        +purge(UUID)
    }
    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
    class GradePublicationStudentRepository

    ProgressController --> ProgressService
    SubmissionController --> SubmissionService
    DefenseAdminController --> DefenseService
    DefenseMyController --> DefenseService
    StudentPurgeController --> StudentPurgeService
    GroupLabDataController --> ProgressService
    ProgressController --> SubmissionService
    ProgressController --> GradePublicationService
    StudentPurgeService --> SubmissionStorage
    ProgressService --> VariantAssignmentRepository
    ProgressService --> StudentLabLevelRepository
    ProgressService --> LabGradeRepository
    ProgressService --> GradeCommentRepository
    ProgressService --> CommentTemplateRepository
    ProgressService --> AcademicClient
    ProgressService --> IdentityClient
    SubmissionService --> SubmissionRepository
    SubmissionService --> SubmissionStorage
    SubmissionService --> AcademicClient
    DefenseService --> DefenseSessionRepository
    GradePublicationService --> GradePublicationRepository
    GradePublicationService --> GradePublicationStudentRepository

4.web (BFF)

classDiagram
    class AuthPageController
    class LearnController
    class StudentController
    class AdminGroupController
    class AdminLabController
    class AdminStudentController
    class AdminDistributeController
    class AdminDefenseController
    class AdminUsersController
    class ContentAdminController
    class ContentAssetProxy
    class ApiClient {
        +login(String, String) AuthResponseDto
        +contentTree(String, Boolean) ContentTreeNodeDto
        +contentArticle(String, String, String, String, Boolean) ContentArticleDto
        +setContentPublished(String, String, String, Boolean)
        +suggestComments(String, String) List~CommentSuggestionDto~
        +deleteCommentTemplate(String, String)
        +publicationState(String, String, String) PublicationStateDto
        +setPublication(String, PublicationSetPayload)
        +purgeStudent(String, String)
        +searchStudents(String, String) List~AdminUserDto~
        +deleteStudent(String, String) DeleteUserDto
        +defenseWatch(String, List~String~) List~DefenseDto~
        +myDefenseStatus(String, String) Map
        +groupLabData(String, String) List~Map~
        +groupDetail(String, String) GroupDetailDto
        +distribute(String, DistributeRequestPayload) DistributeResultDto
        +setGrade(String, String, String, String, String) GradeResponseDto 
        +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 NoCachePageInterceptor
    class AuthInterceptor
    class AdminInterceptor
    class MustChangePasswordInterceptor
    class MarkdownRenderer {
        +render(String) String
    }

    AuthPageController --> ApiClient
    LearnController --> ApiClient
    StudentController --> ApiClient
    AdminGroupController --> ApiClient
    AdminLabController --> ApiClient
    AdminStudentController --> ApiClient
    AdminDistributeController --> ApiClient
    AdminDefenseController --> ApiClient
    AdminUsersController --> ApiClient
    ContentAdminController --> ApiClient
    ContentAssetProxy --> 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.

erDiagram
    ACCOUNTS {
        uuid id PK
        varchar login UK
        varchar password_hash
        varchar role
        varchar status
        timestamptz archived_at
        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
        varchar status
        smallint enrollment_year
    }
    GROUPS {
        uuid id PK
        varchar name
        varchar stream
        smallint course
        smallint number
        varchar degree
        smallint enrollment_year
        varchar status
        int task_duration_min
        timestamptz created_at
    }
    ENROLLMENTS {
        uuid id PK
        uuid group_id FK
        uuid student_id FK
        timestamptz enrolled_at
    }
    MEMBERSHIP_HISTORY {
        uuid id PK
        uuid student_id FK
        uuid from_group_id FK
        uuid to_group_id FK
        varchar action
        text reason
        uuid actor_account_id
        uuid promotion_run_id
        timestamptz at
    }
    SETTINGS {
        varchar key PK
        varchar value
    }

    ACCOUNTS ||--o| STUDENTS : "account_id"
    GROUPS ||--o{ ENROLLMENTS : "group_id"
    STUDENTS ||--o{ ENROLLMENTS : "student_id"
    STUDENTS ||--o{ MEMBERSHIP_HISTORY : "student_id"
    GROUPS ||--o{ MEMBERSHIP_HISTORY : "from/to_group_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
        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
        text general_comment
        timestamptz updated_at
    }
    GRADE_PUBLICATIONS {
        uuid group_id PK
        uuid lab_id PK
        timestamptz published_at
    }
    GRADE_PUBLICATION_STUDENTS {
        uuid group_id PK
        uuid lab_id PK
        uuid student_id PK
        timestamptz published_at
    }
    GRADE_COMMENTS {
        uuid id PK
        uuid student_id
        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_PUBLICATION_STUDENTS }o--|| LABS : "lab_id (не FK)"
    GRADE_COMMENTS }o--|| LABS : "lab_id (не FK)"
    SUBMISSIONS }o--|| LABS : "lab_id (не FK)"
    DEFENSE_SESSIONS }o--|| LABS : "lab_id (не FK)"