начало работы построения динамического расписания и документация

This commit is contained in:
Zuev
2026-04-29 15:42:05 +03:00
parent 813e81be70
commit 8f71b9b2b5
43 changed files with 3894 additions and 457 deletions

View File

@@ -25,7 +25,9 @@
"message": "OK",
"token": "550e8400-e29b-41d4-a716-446655440000",
"role": "ADMIN",
"redirect": "/admin/"
"redirect": "/admin/",
"departmentId": 1,
"userId": 1
}
```
@@ -36,7 +38,9 @@
"message": "Неверное имя пользователя или пароль",
"token": null,
"role": null,
"redirect": null
"redirect": null,
"departmentId": null,
"userId": null
}
```
@@ -95,7 +99,145 @@
---
## Расписание (Lessons)
## Динамическое расписание
Новая модель расписания строится из правил (`schedule_rules`) и слотов (`schedule_rule_slots`). Фактические занятия рендерятся на диапазон дат.
### `GET /api/schedule`
Получение расписания группы или преподавателя за период.
**Параметры:**
| Параметр | Обязателен | Описание |
|----------|------------|----------|
| `groupId` | Да, если нет `teacherId` | ID учебной группы |
| `teacherId` | Да, если нет `groupId` | ID преподавателя |
| `startDate` | Да | Начало периода в формате `YYYY-MM-DD` |
| `endDate` | Да | Конец периода в формате `YYYY-MM-DD` |
Передаётся ровно один параметр: `groupId` или `teacherId`. Максимальный диапазон — 120 дней.
**Пример:**
```http
GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
```
**Ответ:**
```json
[
{
"scheduleRuleId": 10,
"scheduleRuleSlotId": 31,
"date": "2026-04-27",
"dayOfWeek": 1,
"dayName": "Понедельник",
"weekNumber": 13,
"parity": "ODD",
"timeSlotId": 3,
"timeSlotOrder": 3,
"startTime": "11:40:00",
"endTime": "13:10:00",
"subjectId": 1,
"subjectName": "Высшая математика",
"teacherId": 2,
"teacherName": "Петров Препод Петрович",
"classroomId": 1,
"classroomName": "101 Ленинская",
"lessonTypeId": 1,
"lessonTypeName": "Лекция",
"lessonFormat": "Очно",
"subgroupId": null,
"groupIds": [1],
"groupNames": ["ИВТ-21-1"],
"activityType": "THEORY",
"totalAcademicHours": 72,
"consumedAcademicHoursBeforeLesson": 24,
"remainingAcademicHoursAfterLesson": 46
}
]
```
### `GET /api/admin/time-slots`
Список временных слотов занятий. CRUD доступен по:
| Метод | URL | Назначение |
|-------|-----|------------|
| `GET` | `/api/admin/time-slots` | Список слотов |
| `POST` | `/api/admin/time-slots` | Создать слот |
| `PUT` | `/api/admin/time-slots/{id}` | Обновить слот |
| `DELETE` | `/api/admin/time-slots/{id}` | Удалить слот |
**Тело создания/обновления:**
```json
{
"orderNumber": 1,
"startTime": "08:00:00",
"endTime": "09:30:00",
"durationMinutes": 90
}
```
### Календарный график администратора
| Метод | URL | Назначение |
|-------|-----|------------|
| `GET` | `/api/admin/calendar/years` | Учебные годы с семестрами |
| `POST` | `/api/admin/calendar/years` | Создать учебный год |
| `PUT` | `/api/admin/calendar/years/{id}` | Обновить учебный год |
| `DELETE` | `/api/admin/calendar/years/{id}` | Удалить учебный год |
| `GET` | `/api/admin/calendar/years/{academicYearId}/semesters` | Семестры учебного года |
| `POST` | `/api/admin/calendar/years/{academicYearId}/semesters` | Создать семестр |
| `PUT` | `/api/admin/calendar/semesters/{id}` | Обновить семестр |
| `GET` | `/api/admin/calendar/holidays?academicYearId=1` | Праздники учебного года |
| `POST` | `/api/admin/calendar/holidays` | Создать праздник |
| `PUT` | `/api/admin/calendar/holidays/{id}` | Обновить праздник |
| `DELETE` | `/api/admin/calendar/holidays/{id}` | Удалить праздник |
| `GET` | `/api/admin/calendar/matrix?semesterId=1&courseNumber=1&specialtyId=2` | Матрица учебного графика |
| `PUT` | `/api/admin/calendar/matrix` | Массовое сохранение матрицы |
### `POST /api/admin/schedule-rules`
Создание правила динамического расписания.
```json
{
"subjectId": 1,
"semesterId": 1,
"activeFromDate": "2026-02-01",
"totalAcademicHours": 72,
"groupIds": [1, 2],
"slots": [
{
"dayOfWeek": 1,
"parity": "BOTH",
"timeSlotId": 3,
"subgroupId": null,
"teacherId": 2,
"classroomId": 1,
"lessonTypeId": 1,
"lessonFormat": "Очно"
}
]
}
```
CRUD доступен по:
| Метод | URL | Назначение |
|-------|-----|------------|
| `GET` | `/api/admin/schedule-rules` | Список правил, фильтры `semesterId`, `groupId` |
| `GET` | `/api/admin/schedule-rules/{id}` | Одно правило |
| `POST` | `/api/admin/schedule-rules` | Создать правило |
| `PUT` | `/api/admin/schedule-rules/{id}` | Обновить правило |
| `DELETE` | `/api/admin/schedule-rules/{id}` | Удалить правило |
---
## Расписание (Lessons, deprecated)
Старые эндпоинты ещё физически есть в коде до удаления старых контроллеров и таблиц. Новый просмотр расписания использует `GET /api/schedule`, а новые seed-данные создаются сразу в динамической модели.
### `GET /api/users/lessons`

View File

@@ -41,7 +41,8 @@
### Учебные группы (Student Groups)
- **Поля:** Название (уникальное), численность, форма обучения, кафедра, курс (16)
- **Поля:** Название (уникальное), численность, форма обучения, кафедра, специальность, год начала обучения
- **Курс:** вычисляется относительно учебного года: `год начала учебного года - year_start_study + 1`
- **Подгруппы:** Возможно деление группы на подгруппы (таблица `subgroups`)
### Аудитории (Classrooms)
@@ -66,7 +67,33 @@
## Логика расписания
### Сущность «Занятие» (Lesson)
### Динамическая модель расписания
Основная модель расписания строится из правил, а не из отдельных статических пар.
| Компонент | Назначение |
|-----------|------------|
| `academic_years` / `semesters` | Учебные годы и семестры. Неделя 1 считается от `semesters.start_date` |
| `holidays` | Даты, когда занятия не проводятся и часы не списываются |
| `academic_calendar_matrix` | Тип недели для курса и специальности: теория, сессия, каникулы, практика |
| `time_slots` | Настраиваемая сетка пар для тенанта |
| `schedule_rules` | Лимит часов дисциплины в семестре |
| `schedule_rule_groups` | Группы правила, включая потоковые лекции |
| `schedule_rule_slots` | День, чётность, слот, преподаватель, аудитория, тип и формат занятия |
Генератор `ScheduleGeneratorService` рендерит расписание по запросу:
1. Определяет семестр для каждой даты диапазона.
2. Вычисляет номер недели и чётность.
3. Проверяет праздники и матрицу учебного графика.
4. Загружает правила группы или преподавателя.
5. Симулирует уже проведённые занятия от `active_from_date`.
6. Останавливает вывод правила, когда достигнут `total_academic_hours`.
Праздник считается пропуском: занятие не переносится и не списывает академические часы.
### Временная старая сущность «Занятие» (Lesson)
`lessons` пока физически остаётся в схеме до удаления старого кода, но базовая миграция больше не заполняет её тестовыми данными. Новые экраны просмотра используют `GET /api/schedule`.
Каждая запись в расписании содержит:
@@ -84,7 +111,7 @@
### Временны́е слоты
Система использует 7 фиксированных слотов по 90 минут:
Сетка пар хранится в `time_slots` и настраивается для каждого тенанта. При миграции создаются базовые слоты:
| № | Время |
|---|-------|
@@ -104,9 +131,9 @@
- **Тип:** только `Лекция`, `Практическая работа`, `Лабораторная работа`
- Все ID (преподаватель, группа, дисциплина, аудитория) обязательны и не могут быть 0
### Данные к составлению расписания (Schedule Data)
### Временные старые данные к составлению расписания (Schedule Data)
Таблица `schedule_data` хранит **плановую нагрузку** для составления расписания:
Таблица `schedule_data` пока физически остаётся в схеме до удаления старого кода. Новая базовая миграция создаёт `schedule_rules` и `schedule_rule_groups` напрямую, без ETL из `schedule_data`.
| Поле | Описание |
|------|----------|

View File

@@ -51,7 +51,8 @@ erDiagram
BIGINT group_size
BIGINT education_form_id FK
BIGINT department_id FK
INT course
BIGINT specialty_code FK
BIGINT year_start_study
TIMESTAMP created_at
}
@@ -142,15 +143,82 @@ erDiagram
VARCHAR semester_type
VARCHAR period
}
time_slots {
BIGSERIAL id PK
INT order_number UK
TIME start_time
TIME end_time
INT duration_minutes
}
academic_years {
BIGSERIAL id PK
VARCHAR title UK
DATE start_date
DATE end_date
}
semesters {
BIGSERIAL id PK
BIGINT academic_year_id FK
VARCHAR semester_type
DATE start_date
DATE end_date
}
holidays {
BIGSERIAL id PK
DATE date
BIGINT academic_year_id FK
VARCHAR description
}
academic_calendar_matrix {
BIGSERIAL id PK
BIGINT semester_id FK
INT course_number
BIGINT specialty_id FK
INT week_number
VARCHAR activity_type
}
schedule_rules {
BIGSERIAL id PK
BIGINT subject_id FK
BIGINT semester_id FK
DATE active_from_date
INT total_academic_hours
}
schedule_rule_groups {
BIGINT schedule_rule_id FK,PK
BIGINT group_id FK,PK
}
schedule_rule_slots {
BIGSERIAL id PK
BIGINT schedule_rule_id FK
INT day_of_week
VARCHAR parity
BIGINT time_slot_id FK
BIGINT subgroup_id FK
BIGINT teacher_id FK
BIGINT classroom_id FK
BIGINT lesson_type_id FK
VARCHAR lesson_format
}
departments ||--o{ users : "department_id"
departments ||--o{ student_groups : "department_id"
departments ||--o{ subjects : "department_id"
departments ||--o{ schedule_data : "department_id"
education_forms ||--o{ student_groups : "education_form_id"
specialties ||--o{ student_groups : "specialty_code"
student_groups ||--o{ subgroups : "group_id"
student_groups ||--o{ lessons : "group_id"
student_groups ||--o{ schedule_data : "group_id"
student_groups ||--o{ schedule_rule_groups : "group_id"
users ||--o{ lessons : "teacher_id"
users ||--o{ teacher_subjects : "user_id"
users ||--o{ teacher_lesson_types : "user_id"
@@ -159,11 +227,24 @@ erDiagram
subjects ||--o{ teacher_subjects : "subject_id"
subjects ||--o{ teacher_lesson_types : "subject_id"
subjects ||--o{ schedule_data : "subjects_id"
subjects ||--o{ schedule_rules : "subject_id"
lesson_types ||--o{ teacher_lesson_types : "lesson_type_id"
lesson_types ||--o{ schedule_data : "lesson_type_id"
lesson_types ||--o{ schedule_rule_slots : "lesson_type_id"
classrooms ||--o{ lessons : "classroom_id"
classrooms ||--o{ schedule_rule_slots : "classroom_id"
classrooms ||--o{ classroom_equipments : "classroom_id"
equipments ||--o{ classroom_equipments : "equipment_id"
academic_years ||--o{ semesters : "academic_year_id"
academic_years ||--o{ holidays : "academic_year_id"
semesters ||--o{ academic_calendar_matrix : "semester_id"
semesters ||--o{ schedule_rules : "semester_id"
specialties ||--o{ academic_calendar_matrix : "specialty_id"
schedule_rules ||--o{ schedule_rule_groups : "schedule_rule_id"
schedule_rules ||--o{ schedule_rule_slots : "schedule_rule_id"
time_slots ||--o{ schedule_rule_slots : "time_slot_id"
subgroups ||--o{ schedule_rule_slots : "subgroup_id"
users ||--o{ schedule_rule_slots : "teacher_id"
```
---
@@ -220,7 +301,8 @@ erDiagram
| `group_size` | BIGINT | Количество студентов |
| `education_form_id` | BIGINT FK → education_forms | Форма обучения |
| `department_id` | BIGINT FK → departments | Кафедра |
| `course` | INT CHECK(16) | Курс |
| `specialty_code` | BIGINT FK → specialties | Специальность |
| `year_start_study` | BIGINT | Год начала обучения, используется для вычисления текущего курса |
#### `subgroups` — Подгруппы
| Колонка | Тип | Описание |
@@ -270,7 +352,10 @@ erDiagram
### Расписание
#### `lessons` — Основное расписание занятий
#### `lessons` — Временная таблица старой модели
Таблица пока остаётся в схеме до удаления старого кода, но новая базовая миграция больше не заполняет её тестовыми данными. Целевой источник расписания — `schedule_rules` и `schedule_rule_slots`.
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
@@ -309,7 +394,10 @@ erDiagram
| `subject_id` | BIGINT PK, FK → subjects (CASCADE) | Дисциплина |
| `lesson_type_id` | BIGINT PK, FK → lesson_types (CASCADE) | Тип занятия |
#### `schedule_data` — Данные к составлению расписания
#### `schedule_data` — Временная таблица старой модели нагрузки
Таблица пока остаётся в схеме до удаления старого кода, но новая базовая миграция больше не использует её как источник seed-данных.
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
@@ -324,6 +412,81 @@ erDiagram
| `semester_type` | VARCHAR(255) | Весенний / Осенний |
| `period` | VARCHAR(255) | Учебный год |
### Динамическое расписание
#### `time_slots` — Временные слоты занятий
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `order_number` | INT UNIQUE | Номер пары в дне |
| `start_time` | TIME | Время начала |
| `end_time` | TIME | Время окончания |
| `duration_minutes` | INT | Длительность в минутах |
#### `academic_years` — Учебные годы
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `title` | VARCHAR(20) UNIQUE | Название, например `2025-2026` |
| `start_date` | DATE | Дата начала |
| `end_date` | DATE | Дата окончания |
#### `semesters` — Семестры
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `academic_year_id` | BIGINT FK → academic_years | Учебный год |
| `semester_type` | VARCHAR(20) | `autumn` или `spring` |
| `start_date` | DATE | Дата начала, от неё считается неделя 1 |
| `end_date` | DATE | Дата окончания |
#### `holidays` — Праздники и исключения
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `date` | DATE | Неучебная дата |
| `academic_year_id` | BIGINT FK → academic_years | Учебный год |
| `description` | VARCHAR(255) | Описание |
#### `academic_calendar_matrix` — Матрица учебного графика
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `semester_id` | BIGINT FK → semesters | Семестр |
| `course_number` | INT | Курс |
| `specialty_id` | BIGINT FK → specialties | Специальность |
| `week_number` | INT | Номер недели семестра |
| `activity_type` | VARCHAR(20) | `THEORY`, `EXAM`, `VACATION`, `PRACTICE` |
#### `schedule_rules` — Правила расписания
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `subject_id` | BIGINT FK → subjects | Дисциплина |
| `semester_id` | BIGINT FK → semesters | Семестр |
| `active_from_date` | DATE | Дата начала действия правила |
| `total_academic_hours` | INT | Лимит академических часов |
#### `schedule_rule_groups` — Группы правила
| Колонка | Тип | Описание |
|---------|-----|----------|
| `schedule_rule_id` | BIGINT PK, FK → schedule_rules | Правило |
| `group_id` | BIGINT PK, FK → student_groups | Группа |
#### `schedule_rule_slots` — Слоты правила
| Колонка | Тип | Описание |
|---------|-----|----------|
| `id` | BIGSERIAL PK | ID |
| `schedule_rule_id` | BIGINT FK → schedule_rules | Правило |
| `day_of_week` | INT CHECK(17) | День недели: 1 — понедельник |
| `parity` | VARCHAR(10) | `BOTH`, `EVEN`, `ODD` |
| `time_slot_id` | BIGINT FK → time_slots | Временной слот |
| `subgroup_id` | BIGINT FK → subgroups, NULL | Подгруппа |
| `teacher_id` | BIGINT FK → users | Преподаватель |
| `classroom_id` | BIGINT FK → classrooms | Аудитория |
| `lesson_type_id` | BIGINT FK → lesson_types | Тип занятия |
| `lesson_format` | VARCHAR(30) | `Очно` или `Онлайн` |
---
## Flyway миграции
@@ -340,7 +503,7 @@ erDiagram
| Файл | Описание |
|------|----------|
| `V1__init.sql` | Инициализация: все таблицы, тестовые данные, триггеры, комментарии |
| `V1__init.sql` | Инициализация: справочники, динамическое расписание, тестовые правила, триггеры, комментарии |
### Накатывание на существующих тенантов

View File

@@ -7,7 +7,7 @@
| **Фреймворк** | Нет (Vanilla JavaScript) |
| **Модульная система** | ES6 Modules (`import`/`export`) |
| **Стили** | CSS (модульный подход) |
| **Шрифт** | [Inter](https://fonts.google.com/specimen/Inter) (Google Fonts) |
| **Шрифт** | Inter на странице входа, системный шрифт в кабинетах расписания |
| **Веб-сервер** | Apache httpd:alpine |
---
@@ -70,10 +70,10 @@ frontend/
│ └── general.html # Общие настройки (заглушка)
├── teacher/ # 👩‍🏫 Интерфейс преподавателя
│ └── index.html # Просмотр расписания
│ └── index.html # Недельный просмотр динамического расписания преподавателя
└── student/ # 🎓 Интерфейс студента
└── index.html # Просмотр расписания (read-only)
└── index.html # Недельный просмотр динамического расписания группы
```
---
@@ -106,7 +106,7 @@ frontend/
| `equipments` | Оборудование | `/api/equipments` |
| `classrooms` | Аудитории | `/api/classrooms` |
| `subjects` | Дисциплины | `/api/subjects` |
| `schedule` | Расписание | `/api/users/lessons` |
| `schedule` | Старый админский экран расписания до удаления | `/api/users/lessons`; новые правила — `/api/admin/schedule-rules` |
| `database` | Тенанты | `/api/database` |
| `department` | Кафедры | `/api/departments` |
| `departments-data` | Создание кафедры/специальности | `/api/departments` |
@@ -166,6 +166,8 @@ export const api = {
3. При успехе сохраняет в `localStorage`:
- `token` — UUID-токен
- `role` — роль пользователя
- `departmentId` — кафедра пользователя
- `userId` — ID пользователя для личного расписания преподавателя
4. Перенаправляет на соответствующий интерфейс:
- `ADMIN``/admin/`
- `TEACHER``/teacher/`
@@ -188,6 +190,32 @@ export function isAuthenticatedAsAdmin() {
---
## Кабинеты расписания
### Преподаватель (`/teacher/`)
Страница показывает недельную сетку занятий преподавателя. ID преподавателя берётся из `localStorage.userId`, который сохраняется после `POST /api/auth/login`.
Основные элементы:
- навигация по неделям: предыдущая, текущая, следующая;
- выбор даты через `input[type="date"]`;
- запрос `GET /api/schedule?teacherId={userId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`;
- отображение дисциплины, времени, типа занятия, аудитории и всех групп правила.
Если пользователь вошёл до появления поля `userId`, страница попросит выполнить вход заново.
### Студент (`/student/`)
В текущей модели студент не связан с конкретной группой, поэтому страница использует выбор группы из `/api/groups`.
Основные элементы:
- селект группы с сохранением выбора в `localStorage.studentGroupId`;
- недельная сетка по дням;
- запрос `GET /api/schedule?groupId={groupId}&startDate={YYYY-MM-DD}&endDate={YYYY-MM-DD}`;
- отображение дисциплины, времени, преподавателя, аудитории, формата и типа занятия.
---
## CSS-архитектура
### Модульный подход