исправление багов

This commit is contained in:
Zuev
2026-07-13 03:28:18 +03:00
parent 39c58440cf
commit 85f61436b6
76 changed files with 9219 additions and 1258 deletions

View File

@@ -93,6 +93,9 @@ Redirect по ролям:
```
Refresh-токен ротируется при каждом успешном обновлении, а старый refresh-токен отзывается.
Один refresh-токен можно успешно использовать только один раз, в том числе при параллельных
запросах: первый запрос получает новую пару токенов, остальные получают `401`, а их cookie
очищается.
### `POST /api/auth/logout`
@@ -426,6 +429,14 @@ GET /api/schedule?groupId=1&startDate=2026-04-27&endDate=2026-05-03
}
```
`PUT /api/admin/academic-calendars/{id}/grid` выполняет атомарную полную замену. До
удаления прежних строк backend проверяет весь список: он должен быть непустым и не
содержать `null`, курс должен входить в `1..courseCount`, дата — в учебный год,
`dayOfWeek` — совпадать с ISO-днём даты, а `weekNumber`с номером семидневного периода
от начала учебного года. Ключ `(courseNumber, date)` не должен повторяться, каждый
`activityTypeId` или `activityCode` должен существовать. При любом `400` старая сетка
остаётся без изменений; `calendarId` из строки не переопределяет ID в URL.
**Привязка дисциплин к графику:**
```json
[
@@ -476,15 +487,32 @@ CRUD доступен по:
| `GET` | `/api/admin/schedule-rules/{id}` | Одно правило |
| `POST` | `/api/admin/schedule-rules` | Создать правило |
| `PUT` | `/api/admin/schedule-rules/{id}` | Обновить правило |
| `DELETE` | `/api/admin/schedule-rules/{id}` | Удалить правило |
| `DELETE` | `/api/admin/schedule-rules/{id}` | Архивировать правило |
`timeSlotId` должен ссылаться на базовый слот (`scopeApplyMode = DEFAULT`). Субботняя и ручные сетки не выбираются в правиле напрямую.
`subgroupIds` можно передавать только для лабораторного слота. Каждая подгруппа должна относиться к одной из групп правила. Если лабораторная проводится у нескольких групп одновременно, в одном слоте можно передать разные подгруппы этих групп, например `[10, 22]`. Для совместимости одиночный `subgroupId` тоже принимается, но новый формат — `subgroupIds`. Для лекций и практик оба поля должны быть пустыми, иначе API вернёт ошибку валидации. В одном слоте нельзя выбрать больше одной подгруппы одной и той же группы.
Часы и недели начала задаются отдельно для лекций, лабораторных и практик. Если для типа занятий указан ненулевой лимит часов, в правиле должен быть хотя бы один слот этого типа; если слот типа есть, его лимит часов должен быть больше нуля.
Часы и недели начала задаются отдельно для лекций, лабораторных и практик. Каждый лимит
часов обязателен, неотрицателен и кратен двум; ноль разрешён для неиспользуемого типа, но
суммарно хотя бы один тип должен иметь положительный лимит. Если лимит типа ненулевой, в
правиле должен быть хотя бы один слот этого типа; слот типа не принимается при нулевом
лимите. Вложенные идентификаторы, которых нет в БД, считаются ошибкой payload и дают `400`.
`parity` принимает только `BOTH`, `ODD` или `EVEN`. При создании и обновлении правила backend проверяет занятость в рамках семестра: конфликтом считается пересечение дня, базового временного слота, чётности и активных недель слотов, если совпадает преподаватель, аудитория или учебная группа. Активные недели рассчитываются по лимиту часов типа занятия, неделе начала, чётности и порядку слотов правила, поэтому правило, которое фактически идёт с 1 по 3 неделю, не блокирует тот же слот с 4 недели. Для лабораторных слотов подгруппы учитываются отдельно: разные подгруппы одной группы могут занимать один слот, но слот для всей группы конфликтует с любой её подгруппой. При конфликте API возвращает `409 Conflict`:
`teacherId` должен ссылаться на активного пользователя с ролью `TEACHER`, а
`lessonFormat` принимает только `Очно` или `Онлайн`. `parity` принимает только `BOTH`,
`ODD` или `EVEN`.
До сохранения backend попарно проверяет все слоты нового payload: точные дубли и
пересечения преподавателя, аудитории или аудитории обучающихся отклоняются. `ODD` и `EVEN`
не пересекаются; `BOTH` пересекается с обеими чётностями. Затем выполняется та же проверка
с активными правилами семестра. Активные недели рассчитываются по лимиту часов типа
занятия, неделе начала, чётности и порядку слотов правила, поэтому правило, которое
фактически идёт с 1 по 3 неделю, не блокирует тот же слот с 4 недели. Для лабораторных
слотов подгруппы учитываются отдельно: разные подгруппы одной группы могут занимать один
слот, но слот для всей группы конфликтует с любой её подгруппой. Создание правил одного
семестра сериализуется блокировкой строки семестра в PostgreSQL. Конфликт с уже сохранённым
правилом возвращает `409 Conflict`:
```json
{
@@ -501,7 +529,19 @@ CRUD доступен по:
}
```
`conflictFields` содержит технические причины пересечения: `teacher`, `classroom` и/или `group`. `conflictReasons` содержит те же причины в русских подписях для интерфейса.
`conflictFields` содержит технические причины пересечения: `teacher`, `classroom` и/или
`group`. Для точного дубля используется `slot`. При конфликте внутри нового payload
`conflictRule` отсутствует, потому что конфликтующей сохранённой записи ещё нет:
```json
{
"message": "Невозможно сохранить правило: слоты внутри правила конфликтуют",
"conflictFields": ["teacher", "group"],
"conflictReasons": ["Преподаватель", "Группа"]
}
```
`conflictReasons` содержит те же причины в русских подписях для интерфейса.
### `GET /api/lesson-types`
@@ -532,7 +572,17 @@ CRUD доступен по:
| `timeSlotId` | Временной слот |
| `parity` | `BOTH`, `ODD`, `EVEN` |
Если указан только `teacherId` без `groupId` и `departmentId`, поиск строит расписание преподавателя напрямую и не обходит все группы. Широкий поиск без `groupId` и `departmentId` разрешён только до 50 активных групп; при большем количестве групп API вернёт `400` с просьбой уточнить группу или кафедру.
Если указан только `teacherId` без `groupId` и `departmentId`, базовое расписание
преподавателя строится напрямую. Затем поиск учитывает точечные изменения, где преподаватель
назначен через `newTeacherId`: для каждой уникальной даты такой замены один раз строится
базовый день, из него добавляются только указанные `baseRuleSlotId`, после чего применяются
все overrides и выполняется окончательный фильтр преподавателя. Поэтому новый преподаватель
видит назначенную замену, а исходный больше её не видит. Если релевантных замен нет, обход
всех групп не выполняется.
Широкий пользовательский поиск без `groupId`, `departmentId` и teacher-only режима
разрешён только до 50 активных групп; при большем количестве групп API вернёт `400` с
просьбой уточнить группу или кафедру.
Пример:
@@ -562,7 +612,24 @@ GET /api/schedule/search?classroomId=1&startDate=2026-05-20&endDate=2026-05-27
}
```
`action=CANCEL` отменяет конкретную пару. `MOVE` и `REPLACE` могут менять аудиторию, преподавателя, формат и временной слот.
Правила payload:
- `CANCEL` отменяет конкретную пару; поля `newTimeSlotId`, `newClassroomId`,
`newTeacherId` и `newLessonFormat` должны отсутствовать;
- `MOVE` требует новый временной слот или аудиторию; преподавателя и формат можно изменить
в том же запросе;
- `REPLACE` требует нового преподавателя, аудиторию или формат; временной слот можно
изменить в том же запросе;
- формат принимает только `Очно` или `Онлайн`;
- `MOVE` и `REPLACE` должны фактически менять основные параметры действия. Другой ID
временного слота с тем же интервалом не считается переносом.
До сохранения backend строит базовую пару на `lessonDate` по тем же правилам, что и обычное
расписание: семестр, календарный график, чётность, активность сущностей и остаток часов.
Если пара не формируется или нарушена матрица действия, API возвращает `400` с русским
сообщением. Если результирующее время пересекается с занятым преподавателем, аудиторией,
группой или той же подгруппой, API возвращает `409 Conflict`; соседние интервалы и разные
подгруппы одной группы не конфликтуют.
## Загруженность
@@ -1059,7 +1126,12 @@ GET /api/workload/teachers?departmentId=1&startDate=2026-05-20&endDate=2026-06-0
**Логика:**
1. Создаёт HikariCP пул для нового тенанта
2. Запускает Flyway миграции на его БД
3. Обновляет Kubernetes ConfigMap
3. Обновляет внешний Kubernetes Secret `tenants-secret`
Backend соединяется с Kubernetes API только через проверенный service-account CA и
hostname verification. Если безопасно сохранить tenant-конфигурацию не удалось, операция
не возвращается как успешная. Значения credentials никогда не включаются в ответ или лог
Kubernetes updater.
### `DELETE /api/database/tenants/{domain}`