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.
20 KiB
20 KiB
Модель данных и классов
Ниже — диаграммы классов по сервисам (JPA-сущности + репозитории + сервисы), ER-модель БД и модель web-слоя.
1. identity-service
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
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
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
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)"