feat(identity): группы-когорты, история и импорт v2
This commit is contained in:
@@ -12,6 +12,8 @@ classDiagram
|
||||
+String login
|
||||
+String passwordHash
|
||||
+LmsRole role
|
||||
+AccountStatus status
|
||||
+Instant archivedAt
|
||||
+Boolean mustChangePassword
|
||||
+String tempPassword
|
||||
+Instant createdAt
|
||||
@@ -25,11 +27,19 @@ classDiagram
|
||||
+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
|
||||
}
|
||||
@@ -37,6 +47,17 @@ classDiagram
|
||||
+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
|
||||
@@ -44,12 +65,15 @@ classDiagram
|
||||
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
|
||||
```
|
||||
|
||||
@@ -68,20 +92,28 @@ classDiagram
|
||||
class GroupService {
|
||||
+list() List~GroupSummary~
|
||||
+create(CreateGroupRequest) GroupSummary
|
||||
+createStudent(UUID, CreateStudentRequest) Credential
|
||||
+createStudent(UUID, CreateStudentRequest, UUID) Credential
|
||||
+candidates(UUID) List~StudentCandidate~
|
||||
+enroll(UUID, UUID) GroupDetail
|
||||
+unenroll(UUID, UUID) GroupDetail
|
||||
+import(String, String, ByteArray) ImportResult
|
||||
+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
|
||||
@@ -96,6 +128,7 @@ classDiagram
|
||||
|
||||
AuthController --> AuthService
|
||||
GroupController --> GroupService
|
||||
GroupController --> GroupImportService
|
||||
StudentController --> GroupService
|
||||
AuthService --> AccountRepository
|
||||
AuthService --> StudentRepository
|
||||
@@ -103,7 +136,14 @@ classDiagram
|
||||
GroupService --> EnrollmentRepository
|
||||
GroupService --> StudentRepository
|
||||
GroupService --> AccountRepository
|
||||
GroupService --> StudentImportParser
|
||||
GroupService --> MembershipHistoryRepository
|
||||
GroupService --> GroupCohortResolver
|
||||
GroupImportService --> GroupRepository
|
||||
GroupImportService --> EnrollmentRepository
|
||||
GroupImportService --> StudentRepository
|
||||
GroupImportService --> AccountRepository
|
||||
GroupImportService --> MembershipHistoryRepository
|
||||
GroupImportService --> StudentImportParser
|
||||
GroupService --> CredentialsExporter
|
||||
GroupService --> Passwords
|
||||
```
|
||||
@@ -516,6 +556,8 @@ erDiagram
|
||||
varchar login UK
|
||||
varchar password_hash
|
||||
varchar role
|
||||
varchar status
|
||||
timestamptz archived_at
|
||||
boolean must_change_password
|
||||
varchar temp_password
|
||||
timestamptz created_at
|
||||
@@ -529,10 +571,18 @@ erDiagram
|
||||
varchar middle_name
|
||||
varchar basis
|
||||
text note
|
||||
varchar status
|
||||
smallint enrollment_year
|
||||
}
|
||||
GROUPS {
|
||||
uuid id PK
|
||||
varchar name UK
|
||||
varchar name
|
||||
varchar stream
|
||||
smallint course
|
||||
smallint number
|
||||
varchar degree
|
||||
smallint enrollment_year
|
||||
varchar status
|
||||
int task_duration_min
|
||||
timestamptz created_at
|
||||
}
|
||||
@@ -542,14 +592,27 @@ erDiagram
|
||||
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 ||--|| STUDENTS : "account_id"
|
||||
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
|
||||
|
||||
95
docs/11-stage1-report.md
Normal file
95
docs/11-stage1-report.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# 11. Отчёт этапа 1: identity
|
||||
|
||||
**Дата:** 2026-10-23.
|
||||
**Статус:** этап 1 выполнен; этап 2 (academic) — следующий.
|
||||
|
||||
## 1. Что сделано
|
||||
|
||||
### Роли и аккаунты
|
||||
|
||||
- Добавлена роль `TEACHER` в `LmsRole`; БД уже допускает её миграцией `V4`.
|
||||
- `Account` получил `status` (`ACTIVE/ARCHIVED`) и `archivedAt`.
|
||||
- Вход запрещён для архивных аккаунтов и студентов в статусах
|
||||
`EXPELLED/GRADUATED/ARCHIVED`.
|
||||
|
||||
### Группы-когорты
|
||||
|
||||
- `GroupEntity`: `stream`, `course`, `number`, `degree`, `enrollmentYear`,
|
||||
`status` (`ACTIVE/GRADUATED/ARCHIVED`).
|
||||
- `name` больше не уникально; уникальность — по кортежу когорты.
|
||||
- `GroupNameParser` разбирает `ПИ-11БО`, `ИВТ-ТОП-12БО` и т.п.
|
||||
- `GroupCohortResolver` валидирует степень, курс, номер и год набора,
|
||||
вычисляет год набора по умолчанию.
|
||||
- Создание группы через админку — по полям, имя собирается автоматически.
|
||||
|
||||
### Студенты и история
|
||||
|
||||
- `Student`: `status` и `enrollmentYear`.
|
||||
- `MembershipHistory`: `ENROLL`, `TRANSFER`, `IMPORT`, `EXPELL` и остальные
|
||||
действия; записи создаются при зачислении, переводе, импорте и отчислении.
|
||||
- При отчислении студент получает `EXPELLED`, аккаунт и данные сохраняются.
|
||||
- Повторное зачисление возвращает `ACTIVE` и пишет `TRANSFER`.
|
||||
|
||||
### Импорт Excel/CSV v2
|
||||
|
||||
Шаблон:
|
||||
|
||||
```text
|
||||
поток | курс | номер | степень | год набора | ФИО | № | база
|
||||
```
|
||||
|
||||
- Поддерживаются `.xls`, `.xlsx`, `.csv`, разделители `;`, `,`, tab.
|
||||
- ФИО можно одной колонкой или отдельными `Фамилия/Имя/Отчество`.
|
||||
- Все строки должны быть одной группой.
|
||||
- Проверяются степень, диапазон курса, год набора, пустые поля,
|
||||
дубли `№` внутри файла.
|
||||
- Импорт атомарный: при ошибках данные не меняются, возвращается отчёт
|
||||
с номерами строк.
|
||||
- Ключ слияния — `student_no`; группа ищется по кортежу, при отсутствии
|
||||
создаётся.
|
||||
- Новые студенты получают временный пароль; пароли показываются один раз.
|
||||
|
||||
### UI
|
||||
|
||||
- Страница групп: год набора, статус, создание по полям, описание нового импорта.
|
||||
- Страница группы: год набора, статус группы, статус студента.
|
||||
- Страница пользователей: статус студента.
|
||||
- При ошибке импорта показывается таблица «строка — ошибка».
|
||||
|
||||
## 2. Тесты
|
||||
|
||||
Добавлены:
|
||||
|
||||
- `GroupNameParserTest` — разбор имени, диапазоны курсов;
|
||||
- `StudentImportParserTest` — валидный шаблон, дубли, разные группы,
|
||||
недопустимый курс, неизвестная шапка;
|
||||
- `GroupServiceIntegrationTest` — создание когорты, дубликат, импорт,
|
||||
атомарность ошибок, перевод, отчисление, запрет входа после отчисления.
|
||||
|
||||
Проверки:
|
||||
|
||||
- `./gradlew :services:identity-service:test :web:test` — успешно;
|
||||
- `ktlintCheck` и `detekt` для identity/web — успешно.
|
||||
|
||||
## 3. Ограничения этапа
|
||||
|
||||
- Импорт поддерживает одну группу на файл; мультигрупповой файл — позже.
|
||||
- Ведущие нули номера группы нормализуются к числу (`01` → `1`);
|
||||
имя строится из числового номера. Если нужны разные `ПИ-11БО` и `ПИ-101БО`
|
||||
как отдельные когорты, потребуется хранить номер строкой.
|
||||
- Роль `TEACHER` уже есть в домене, но кабинет преподавателя и назначения
|
||||
на курс — этапы 2–3.
|
||||
- `ACADEMIC_LEAVE` пока может входить; режим «только чтение» — этап 3.
|
||||
- Отчисление пока простое; причины, подтверждение и аудит — этап 4.
|
||||
- Старый формат импорта (только студенческие колонки) больше не поддерживается.
|
||||
|
||||
## 4. Следующий этап
|
||||
|
||||
Этап 2 — academic:
|
||||
|
||||
- курсы, `course_teachers`, `course_groups`;
|
||||
- `course_no` и `/learn/course/{course_no}/...`;
|
||||
- `labs.course_id` и перенос лаб в курс C;
|
||||
- `chapters.course_id/audience`, перенос материалов;
|
||||
- активы и копирование лабораторных;
|
||||
- админ-UI курсов.
|
||||
Reference in New Issue
Block a user