Add a non-required general comment for a student's lab grade (progress V9, API, admin modal, student card). Empty string is the default. Switch the UI to square corners and bump static asset versions.
696 lines
20 KiB
Markdown
696 lines
20 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 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
|
|
|
|
```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
|
|
+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
|
|
```
|
|
|
|
```mermaid
|
|
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)
|
|
|
|
```mermaid
|
|
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.
|
|
|
|
```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
|
|
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)"
|
|
```
|