баг-фикс 30/34
This commit is contained in:
148
docs/API.md
148
docs/API.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user