баг-фикс 30/34

This commit is contained in:
Zuev
2026-07-19 14:40:43 +03:00
parent 3d798c13e3
commit bc0e1ab1b4
172 changed files with 13431 additions and 2910 deletions

View File

@@ -3,7 +3,8 @@
Все прикладные эндпоинты имеют префикс `/api/`. Служебные проверки Kubernetes доступны
под `/actuator/health/`. Ответы возвращаются в формате JSON.
Необработанные ошибки проходят через единый `GlobalExceptionHandler`. Для `400`, `404` и `500` используется общий JSON-формат:
Необработанные ошибки проходят через единый `GlobalExceptionHandler`. Для `400`, `404`,
`409` и `500` используется общий JSON-формат:
```json
{
@@ -17,6 +18,11 @@
Контроллеры, у которых исторически есть собственная обработка ошибок, могут возвращать более короткий объект с полем `message`.
Нарушения ограничений PostgreSQL также обрабатываются централизованно. Известные CHECK
возвращают `400`, а UNIQUE, FK и GiST exclusion conflicts — `409` с безопасным русским
сообщением. Тексты JDBC, SQL, имена ограничений и внутренние причины исключений в JSON не
передаются; неизвестное нарушение получает обобщённое сообщение.
---
## Служебные проверки состояния
@@ -108,7 +114,36 @@
}
```
Для несуществующего пользователя, неверного пароля и архивной учётной записи возвращается
одинаковый ответ `401`, поэтому endpoint не раскрывает наличие или состояние пользователя.
**Временная блокировка (429):**
```http
HTTP/1.1 429 Too Many Requests
Retry-After: 60
```
```json
{
"success": false,
"message": "Слишком много попыток входа. Повторите позже",
"token": null,
"role": null,
"redirect": null,
"departmentId": null,
"userId": null
}
```
Backend ведёт общий для всех pod счётчик по tenant, нормализованному `username` и IP.
После пяти отказов по умолчанию комбинация блокируется на 60 секунд; следующие отказы
после окончания блокировки увеличивают задержку до максимума 15 минут. `Retry-After`
содержит оставшееся целое число секунд. Успешный вход сбрасывает счётчик.
> После получения access JWT клиент должен передавать его в заголовке: `Authorization: Bearer <token>`.
> Штатный web-клиент хранит access JWT только в памяти страницы; после reload он получает
> новый access JWT через `HttpOnly` refresh-cookie и `POST /api/auth/refresh`.
Поддерживаемые роли: `ADMIN`, `EDUCATION_OFFICE`, `DEPARTMENT`, `SCHEDULE_VIEWER`, `TEACHER`, `STUDENT`.
@@ -198,7 +233,9 @@ Refresh-токен ротируется при каждом успешном о
Список преподавателей привязанных к конкретной кафедре (роль `TEACHER`, код кафедры `departmentId`). Ответ использует ту же структуру `UserResponse`, что и `GET /api/users`.
Выборка учитывает активные записи `teacher_department_assignments` на текущую дату и legacy-привязку `users.department_id`. Роль `DEPARTMENT` может запрашивать только свою кафедру.
Выборка использует записи `teacher_department_assignments`, действующие на текущую дату.
Дополнительная связь (`is_primary=false`) также включает преподавателя в список кафедры.
Роль `DEPARTMENT` может запрашивать только свою кафедру.
### `POST /api/users`
@@ -247,9 +284,16 @@ Refresh-токен ротируется при каждом успешном о
}
```
Если `validFrom` находится в будущем, текущая основная кафедра остаётся действующей до дня,
предшествующего переводу. Поле совместимости `users.department_id` переключается только
когда новая основная запись уже действует на текущую дату. Пересекающиеся периоды двух
основных кафедр одного преподавателя отклоняются с `409 Conflict`.
### `GET /api/users/teachers/by-department/{departmentId}?date=2026-06-01`
Список преподавателей кафедры на конкретную дату по таблице истории и legacy-привязке `users.department_id`. Роль `DEPARTMENT` может запрашивать только свою кафедру.
Список преподавателей кафедры на конкретную дату по таблице истории назначений. Архивный
преподаватель входит в исторический ответ, если на указанную дату действовали и пользователь,
и его назначение. Роль `DEPARTMENT` может запрашивать только свою кафедру.
---
@@ -294,7 +338,7 @@ Refresh-токен ротируется при каждом успешном о
- `DEPARTMENT` не может создавать аудитории, кафедры, специальности, группы, пользователей или правила расписания через API;
- `DEPARTMENT` создаёт и комментирует дисциплины через `/api/department/*`, где кафедра берётся из текущего пользователя;
- `/api/teacher-subjects` для `DEPARTMENT` разрешает связывать только преподавателей и дисциплины своей кафедры;
- `/api/teacher-subjects` для `DEPARTMENT` разрешает связывать только дисциплины своей кафедры и преподавателей с основной или дополнительной связью с ней на текущую дату;
- `SCHEDULE_VIEWER` имеет read-only доступ к просмотру расписаний, справочникам-фильтрам и загруженности.
---
@@ -386,11 +430,18 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
"orderNumber": 1,
"scopeId": 1,
"startTime": "08:00:00",
"endTime": "09:30:00",
"durationMinutes": 90
"endTime": "09:30:00"
}
```
`durationMinutes` в запросе не требуется и не считается доверенным значением: backend
всегда вычисляет длительность как разницу `endTime - startTime` и возвращает результат в
ответе. Начало должно быть раньше окончания, а длительность — не меньше одной минуты.
Внутри одной сетки запрещены одинаковые номера пар и пересекающиеся полуоткрытые интервалы;
соседние слоты, у которых окончание первого совпадает с началом второго, разрешены.
Конфликт номера или интервала возвращает `409 Conflict`. Слот, на который уже ссылается
правило расписания, нельзя перенести из базовой сетки `DEFAULT`.
**Создание ручной сетки:**
```json
{
@@ -424,6 +475,31 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
| `PUT` | `/api/admin/calendar/activity-types/{id}` | Обновить код активности |
| `DELETE` | `/api/admin/calendar/activity-types/{id}` | Удалить код активности |
**Тело учебного года:**
```json
{
"title": "2026-2027",
"startDate": "2026-09-01",
"endDate": "2027-06-30"
}
```
**Тело семестра:**
```json
{
"semesterType": "autumn",
"startDate": "2026-09-01",
"endDate": "2027-01-31"
}
```
Границы учебных периодов включительны. Учебные годы не могут пересекаться, семестры одного
года также не могут пересекаться и должны полностью находиться внутри его границ. Следующий
период разрешено начать на следующий календарный день после окончания предыдущего.
Повторное название года, повторный тип семестра или пересечение возвращаются как
`409 Conflict`; неверный порядок дат и выход семестра за границы года — как `400 Bad Request`.
При сужении учебного года все существующие семестры должны оставаться внутри новых границ.
**Тело создания/обновления кода активности:**
```json
{
@@ -465,6 +541,13 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
`studyFormId` берётся из общего справочника форм обучения `GET /api/education-forms`; отдельного справочника форм для календарных графиков нет.
`courseCount` должен быть в диапазоне `1..8`.
При `PUT /api/admin/academic-calendars/{id}` backend блокирует график и повторно проверяет
все сохранённые назначения, дневную сетку и дисциплины. Изменение года, специальности,
профиля, формы или количества курсов, которое сделает данные несовместимыми, возвращает
`409 Conflict` с русским сообщением и не изменяет график. В частности, нельзя уменьшить
`courseCount`, пока существуют строки старших курсов или дисциплины семестров выше
`courseCount * 2`.
**Ячейка сетки графика:**
```json
{
@@ -632,6 +715,10 @@ CRUD доступен по:
разрешён только до 50 активных групп; при большем количестве групп API вернёт `400` с
просьбой уточнить группу или кафедру.
Ограничение относится только к интерактивному `/api/schedule/search`. Endpoints
`/api/workload/*` используют отдельный агрегирующий путь и обрабатывают все активные группы
разрешённого кафедрального scope, в том числе при количестве больше 50.
Пример:
```http
@@ -691,6 +778,21 @@ GET /api/schedule/search?classroomId=1&startDate=2026-05-20&endDate=2026-05-27
Общие параметры для отчётов: `startDate`, `endDate`, опционально `departmentId`.
Роли `ADMIN`, `EDUCATION_OFFICE` и `SCHEDULE_VIEWER` могут не передавать `departmentId`
для глобального отчёта или выбрать конкретную кафедру. Для роли `DEPARTMENT` backend всегда
использует кафедру из access JWT: отсутствие параметра означает свою кафедру, а попытка
передать чужой `departmentId` возвращает `403 Forbidden`. То же ограничение применяется к
`GET /api/workload/free-classrooms`, хотя этот endpoint не принимает `departmentId`.
Кафедра каждой записи определяется по `teacher_department_assignments` на дату занятия:
сначала используется основное, затем дополнительное назначение. Поэтому период, включающий
дату перевода, разделяет нагрузку одного преподавателя между прежней и новой кафедрами,
а историческая нагрузка не зависит от текущего значения `users.department_id`.
Workload и поиск свободных аудиторий строятся через специализированный агрегирующий метод
`ScheduleQueryService`: он сохраняет проверку диапазона, применение overrides, фильтр пары и
дедупликацию, но не наследует UI-лимит 50 групп из `/api/schedule/search`.
Пример:
```http
@@ -727,7 +829,15 @@ GET /api/workload/teachers?departmentId=1&startDate=2026-05-20&endDate=2026-06-0
| `POST` | `/api/department/teacher-requests` | Создать заявку на нового преподавателя |
| `GET` | `/api/department/schedule` | Расписание кафедры |
`GET /api/department/teachers` возвращает актуальных преподавателей кафедры по `teacher_department_assignments` и дополнительно учитывает старую привязку `users.department_id`, чтобы не терять преподавателей без записи в истории назначений.
`POST /api/department/subjects/import` нормализует пробелы по краям и сравнивает названия
без учёта регистра. Повторный импорт дисциплины своей кафедры обновляет код и восстанавливает
архивную запись, не создавая новый ID. Если такое название уже принадлежит другой кафедре,
API возвращает `409 Conflict`; владелец записи не изменяется. Повторы одного названия внутри
payload обрабатываются один раз, используется последнее значение.
`GET /api/department/teachers` возвращает актуальных преподавателей кафедры только по
`teacher_department_assignments`. Основные и дополнительные назначения учитываются на
текущую дату; архивные и ещё не начавшиеся назначения исключаются.
`POST /api/department/teachers/{teacherId}/assignments` создаёт дополнительную открытую связь преподавателя с кафедрой (`is_primary=false`). Для роли `DEPARTMENT` кафедра берётся из текущего пользователя, администратор может передать `departmentId`.
@@ -793,6 +903,11 @@ GET /api/workload/teachers?departmentId=1&startDate=2026-05-20&endDate=2026-06-0
При создании специальности автоматически создаётся профиль `Без профиля`.
`EDUCATION_OFFICE` имеет read-only доступ к `GET /api/specialties`,
`GET /api/specialties/profiles` и `GET /api/specialties/{id}/profiles`, необходимый для
инициализации календарных учебных графиков. Создание, изменение, архивирование специальностей
и CRUD профилей остаются доступны только `ADMIN`.
**Тело создания/обновления профиля:**
```json
{
@@ -880,6 +995,11 @@ GET /api/workload/teachers?departmentId=1&startDate=2026-05-20&endDate=2026-06-0
}
```
Если у группы уже есть календарные назначения, изменение года начала обучения,
специальности, профиля или формы обучения повторно проверяется для каждого учебного года.
Несовместимое изменение возвращает `409 Conflict`; группа и её назначения остаются без
изменений.
### `DELETE /api/groups/{id}`
Архивирование группы. Запись остаётся в истории, поэтому расписание за прошлые даты не теряет связь с группой.
@@ -963,7 +1083,10 @@ GET /api/workload/teachers?departmentId=1&startDate=2026-05-20&endDate=2026-06-0
}
```
Назначаемый график должен относиться к тому же учебному году, специальности, профилю и форме обучения, что и группа.
Назначаемый график должен относиться к тому же учебному году, специальности, профилю и
форме обучения, что и группа. Вычисленный курс группы должен находиться в диапазоне
`1..courseCount`. Сохранение выполняется транзакционно с блокировкой группы, графика,
учебного года и существующего назначения; конкурентный конфликт возвращает `409 Conflict`.
---
@@ -1074,6 +1197,11 @@ GET /api/workload/teachers?departmentId=1&startDate=2026-05-20&endDate=2026-06-0
## Формы обучения
`GET /api/education-forms` доступен `ADMIN`, `EDUCATION_OFFICE`, `DEPARTMENT` и
`SCHEDULE_VIEWER`. Создание `POST /api/education-forms` и удаление
`DELETE /api/education-forms/{id}` разрешены `ADMIN` и `EDUCATION_OFFICE`; удаление по-прежнему
отклоняется, если форма используется группой или календарным графиком.
### `GET /api/education-forms`
Список форм обучения.
@@ -1125,6 +1253,10 @@ GET /api/workload/teachers?departmentId=1&startDate=2026-05-20&endDate=2026-06-0
}
```
Для роли `DEPARTMENT` дисциплина должна принадлежать кафедре текущего пользователя, а у
преподавателя на текущую дату должна действовать основная или дополнительная связь с этой
кафедрой.
### `DELETE /api/teacher-subjects`
```json