feat(identity): группы-когорты, история и импорт v2

This commit is contained in:
2026-10-26 15:47:49 +03:00
parent 24a92632fd
commit 07a223b759
34 changed files with 1459 additions and 248 deletions

View File

@@ -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
View 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 курсов.