From ec7e615557833ee2b8b15a303c62c7c86b57d678 Mon Sep 17 00:00:00 2001 From: Zuev Date: Sun, 22 Mar 2026 02:49:13 +0300 Subject: [PATCH] docs: Add comprehensive project documentation covering architecture, development, and APIs, and update AGENTS.md. --- .gitignore | 4 +- AGENTS.md | 169 +++-------------- docs/API.md | 420 +++++++++++++++++++++++++++++++++++++++++ docs/ARCHITECTURE.md | 142 ++++++++++++++ docs/BUSINESS_LOGIC.md | 149 +++++++++++++++ docs/DATABASE.md | 362 +++++++++++++++++++++++++++++++++++ docs/DEVELOPMENT.md | 275 +++++++++++++++++++++++++++ docs/FRONTEND.md | 203 ++++++++++++++++++++ docs/INFRASTRUCTURE.md | 137 ++++++++++++++ docs/README.md | 113 +++++++++++ 10 files changed, 1829 insertions(+), 145 deletions(-) create mode 100644 docs/API.md create mode 100644 docs/ARCHITECTURE.md create mode 100644 docs/BUSINESS_LOGIC.md create mode 100644 docs/DATABASE.md create mode 100644 docs/DEVELOPMENT.md create mode 100644 docs/FRONTEND.md create mode 100644 docs/INFRASTRUCTURE.md create mode 100644 docs/README.md diff --git a/.gitignore b/.gitignore index b362f1c..98c3a42 100755 --- a/.gitignore +++ b/.gitignore @@ -7,8 +7,6 @@ backend/build/ frontend/node_modules/ frontend/dist/ -.agents .idea/ .vscode/ -*.DS_Store -GEMINI.md \ No newline at end of file +*.DS_Store \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md index 1e900b4..033f2d5 100755 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,174 +28,59 @@ magistr/ │ ├── admin/ # Интерфейс администратора │ ├── teacher/ # Интерфейс преподавателя │ └── student/ # Интерфейс студента +├── docs/ # 📖 Документация проекта ├── compose.yaml # Docker Compose конфигурация └── .env # Переменные окружения ``` **Внешние зависимости (родительская директория)** -На уровень выше расположен `../caddy-proxy/`. Это реверс-прокси, обрабатывающий трафик для `magistr.zuev.company`. Если возникают проблемы с доменом или внешним доступом, проверяйте `Caddyfile` там. - -так же на уровень выше расположен конфиг kubernetes `../k8s/`, все файлы сборки на проде расположены там. +На уровень выше расположен конфиг kubernetes `../k8s/`, все файлы сборки на проде расположены там. --- -## Команды сборки и запуска - -### Docker Compose (основной способ) - -Сборка и запуск всех сервисов (backend, frontend, PostgreSQL) выполняется через Docker Compose. +## Быстрый справочник команд ```bash -# Сборка и запуск всех сервисов +# Сборка и запуск docker compose up -d --build -# Остановка всех сервисов -docker compose down +# Полный сброс БД +docker compose down -v && docker compose up -d -# Просмотр логов всех сервисов -docker compose logs -f - -# Просмотр логов конкретного сервиса +# Логи конкретного сервиса docker compose logs -f backend - -# Пересоздать контейнер базы данных (полный сброс данных и повтор миграций Flyway) -docker compose down -v -docker compose up -d db ``` -### Frontend - -Статические файлы обслуживаются через Apache (httpd:alpine). Изменения в файлах frontend требуют пересборки контейнера. +Подробнее — см. [`docs/README.md`](docs/README.md) и [`docs/INFRASTRUCTURE.md`](docs/INFRASTRUCTURE.md). --- -## Соглашения о коде (Code Style) +## Критические правила для агентов -### Java (Backend) - -**Именование:** -- Классы: PascalCase (например, `LessonsController`, `LessonResponse`) -- Методы и переменные: camelCase -- Константы: UPPER_SNAKE_CASE -- Пакеты: lowercase (например, `com.magistr.app.controller`) - -**Импорты:** -- Группировка: static imports, затем external packages, затем internal -- Используйте wildcard imports для пакетов того же модуля: `import com.magistr.app.model.*;` -- Порядок: java.*, javax.*, external.*, internal.* - -**Форматирование:** -- Отступы: 4 пробела (стандарт Java) -- Фигурные скобки: K&R style (открывающая на той же строке) -- Длина строки: до 120 символов -- Всегда используйте фигурные скобки для if/for/while - -**Типы и аннотации:** -- Используйте явные типы вместо `var` для возвращаемых значений публичных методов -- Аннотации JPA: `@Entity`, `@Table`, `@Id`, `@GeneratedValue`, `@Column` -- Используйте `@JsonInclude(JsonInclude.Include.NON_NULL)` для DTO -- Для логгирования используйте SLF4J: `LoggerFactory.getLogger(ClassName.class)` - -**Обработка ошибок:** -- Возвращайте `ResponseEntity` с соответствующим HTTP статусом -- Логируйте ошибки с полным стектрейсом: `logger.error("msg: {}", e.getMessage(), e)` -- Для валидации используйте отдельные классы-валидаторы (см. `DayAndWeekValidator`) - -**Архитектура контроллеров:** -- Используйте constructor injection для зависимостей -- Все endpoints имеют префикс `/api/` -- Возвращайте понятные сообщения об ошибках на русском языке - -### Frontend (JavaScript) - -**Именование:** -- Файлы: kebab-case (например, `main.js`, `schedule-view.js`) -- Функции и переменные: camelCase -- Константы: UPPER_SNAKE_CASE - -**Модули:** -- Используйте ES6 modules с `import`/`export` -- Всегда указывайте расширение при импорте: `import { x } from './api.js';` - -**Форматирование:** -- Отступы: 4 пробела -- Используйте template literals вместо конкатенации строк -- Предпочитайте `const` переменные, используйте `let` только при необходимости переприсваивания - -**Лучшие практики:** -- Используйте `async/await` для асинхронных операций -- Всегда обрабатывайте ошибки в блоках `catch` -- Используйте деструктуризацию объектов -- Кешируйте DOM-элементы в переменные - ---- - -## Работа с базой данных и мультитенантностью - -**Мультитенантность:** -- Приложение поддерживает множество клиентов (университетов). Каждый клиент имеет свою изолированную базу данных PostgreSQL. -- Маршрутизация к нужной БД происходит динамически на основе поддомена (`TenantInterceptor` -> `TenantContext` -> `TenantRoutingDataSource`). -- Список клиентов хранится в Kubernetes `ConfigMap` (`tenants-config`), который монтируется в под бэкенда как `/config/tenants.json`. -- Локально список берётся из файла `backend/tenants.json`. -- При добавлении нового клиента в интерфейсе `DatabaseController` через K8s API обновляет `ConfigMap`. Все реплики бэкенда заметят изменения и в фоне инициализируют новый пул соединений (`TenantConfigWatcher`). - -**Миграции схемы (Flyway):** -- Автогенерация Hibernate ОТКЛЮЧЕНА (`ddl-auto=none`). Структура баз данных управляется строго через **Flyway**. -- Все изменения схемы БД вносятся путем создания новых файлов в `backend/src/main/resources/db/migration/` (название строго `V2__add_new_table.sql` и т.д.). -- **ЗАПРЕЩЕНО** изменять существующие файлы миграций (например, `V1__init.sql`), которые уже закоммичены. Это сломает контрольные суммы Flyway. -- Flyway запускается программно при первом обращении к базе тенанта. Чтобы запустить Flyway для уже существующих тенантов (накатить V2), необходимо перезапустить бэкенд: `kubectl rollout restart deployment backend -n magistr`. -- Для локального сброса базы до изначального состояния: `docker compose down -v && docker compose up -d`. - -**Сущности и связи:** -- Foreign keys с `ON DELETE CASCADE` для поддержания целостности -- Используйте расширение `pgcrypto` для хеширования паролей (bcrypt) - ---- - -## Функциональные требования к системе (Бизнес-логика) - -### 1. Ролевая модель -- **Администратор (Деканат)**: Полный доступ, настройка топологии университета, управление аудиторным фондом, подтверждение переносов, регистрация инцидентов. -- **Преподаватель**: Просмотр своего расписания, подача заявок на перенос, отметка о своём отсутствии. -- **Студент**: Только просмотр расписания (Read-only). - -### 2. Управление ресурсами и топология -- **Управление аудиториями**: - - Указание вместимости. - - Привязка доступного оборудования (через сущность Equipments: Проектор, ПК, Лаборатория). - - Установка статуса "Не доступно" (блокирует назначение пар в этот период). -- **Управление группами**: - - Управление списком студентов (и возможность деления на подгруппы). -- **Управление дисциплинами**: - - Создание предметов и привязка их к преподавателям (какие дисциплины имеет право вести конкретный преподаватель). - -### 3. Логика расписания -- **Сетка**: 7 фиксированных слотов по 1.5 часа (08:00 - 09:30, и т.д.) + поддержка кастомного времени. -- **Проверка конфликтов**: - - *Критический конфликт*: Преподаватель не может находиться в двух разных аудиториях одновременно. - - *Уточнение по преподавателям*: Преподаватель может иметь несколько пар одновременно (для разных групп), только если они проходят в одной и той же аудитории (потоковая лекция). -- **Потоковые занятия**: - - Возможность назначить одну лекцию сразу нескольким группам (технически — несколько записей в БД или одна запись со списком групп). - - Проверка вместимости: вместимость аудитории должна покрывать суммарную численность всех групп, находящихся в этой аудитории в данный слот. - -### 4. Управление инцидентами (Инклюзия отсутствия) -- **Отсутствие (Sickness/Business Trip)**: Регистрация отсутствия преподавателя (с указанием причины и периода дат). -- **Обнаружение коллизий**: Автоматическая подсветка конфликтующих пар в расписании (Red Zone). -- **Система разрешения конфликтов (Resolution Wizard)**: - - Предложение подходящей замены преподавателя на этот слот. - - Предложение переноса занятия на другое время или в другую аудиторию. - ---- - -## Языковые требования +### Flyway миграции +- **ЗАПРЕЩЕНО** изменять существующие файлы миграций (например, `V1__init.sql`). Это сломает контрольные суммы Flyway. +- Новые миграции: `V{N}__{описание}.sql` в `backend/src/main/resources/db/migration/` +- Подробнее — см. [`docs/DATABASE.md`](docs/DATABASE.md) +### Языковые требования - **Все ответы и комментарии на русском языке** - Сообщения об ошибках и логи на русском - Пользовательский интерфейс на русском --- -## Существующие правила проекта +## Подробная документация -См. `.agent/rules/main.md` и `.agent/rules/database_schema.md` для полного контекста о функциональных требованиях и схеме БД. +Полная документация проекта находится в папке `docs/`: + +| Документ | Содержание | +|----------|-----------| +| [`docs/README.md`](docs/README.md) | Обзор проекта, стек технологий, быстрый старт | +| [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) | Архитектура системы, мультитенантность, аутентификация | +| [`docs/BUSINESS_LOGIC.md`](docs/BUSINESS_LOGIC.md) | Бизнес-логика, ролевая модель, правила расписания | +| [`docs/DATABASE.md`](docs/DATABASE.md) | Схема БД (ER-диаграмма), описание всех таблиц, Flyway | +| [`docs/API.md`](docs/API.md) | REST API эндпоинты с примерами запросов и ответов | +| [`docs/INFRASTRUCTURE.md`](docs/INFRASTRUCTURE.md) | Docker, Kubernetes, CI/CD, мониторинг (SigNoz) | +| [`docs/DEVELOPMENT.md`](docs/DEVELOPMENT.md) | Code Style, соглашения, пошаговое создание нового эндпоинта | +| [`docs/FRONTEND.md`](docs/FRONTEND.md) | Frontend архитектура, SPA-маршрутизация, CSS, адаптивность | diff --git a/docs/API.md b/docs/API.md new file mode 100644 index 0000000..f8e40c3 --- /dev/null +++ b/docs/API.md @@ -0,0 +1,420 @@ +# 🔌 REST API + +Все эндпоинты имеют префикс `/api/`. Ответы возвращаются в формате JSON. + +--- + +## Аутентификация + +### `POST /api/auth/login` + +Вход в систему. + +**Тело запроса:** +```json +{ + "username": "admin", + "password": "admin" +} +``` + +**Успешный ответ (200):** +```json +{ + "success": true, + "message": "OK", + "token": "550e8400-e29b-41d4-a716-446655440000", + "role": "ADMIN", + "redirect": "/admin/" +} +``` + +**Ошибка (401):** +```json +{ + "success": false, + "message": "Неверное имя пользователя или пароль", + "token": null, + "role": null, + "redirect": null +} +``` + +> После получения токена клиент должен передавать его в заголовке: `Authorization: Bearer ` + +--- + +## Пользователи + +### `GET /api/users` + +Список всех пользователей. + +**Ответ:** +```json +[ + { "id": 1, "username": "admin", "role": "ADMIN" }, + { "id": 2, "username": "Тестовый преподаватель", "role": "TEACHER" } +] +``` + +### `GET /api/users/teachers` + +Список только преподавателей (роль `TEACHER`). + +### `POST /api/users` + +Создание пользователя. + +**Тело запроса:** +```json +{ + "username": "Новый преподаватель", + "password": "password123", + "role": "TEACHER" +} +``` + +**Валидация:** +- `username` — обязателен +- `password` — минимум 4 символа +- `role` — `ADMIN`, `TEACHER` или `STUDENT` + +### `DELETE /api/users/{id}` + +Удаление пользователя. + +--- + +## Расписание (Lessons) + +### `GET /api/users/lessons` + +Список всех занятий с разрешёнными именами (преподаватель, группа, дисциплина, аудитория). + +**Ответ:** +```json +[ + { + "id": 1, + "teacherName": "Тестовый преподаватель", + "groupName": "ИВТ-21-1", + "classroomName": "101 Ленинская", + "educationFormName": "Бакалавриат", + "subjectName": "Высшая математика", + "typeLesson": "Лекция", + "lessonFormat": "Очно", + "day": "Понедельник", + "week": "Верхняя", + "time": "11:40 - 13:10" + } +] +``` + +### `GET /api/users/lessons/{teacherId}` + +Занятия конкретного преподавателя. + +### `POST /api/users/lessons/create` + +Создание занятия. + +**Тело запроса:** +```json +{ + "teacherId": 2, + "groupId": 1, + "subjectId": 1, + "lessonFormat": "Очно", + "typeLesson": "Лекция", + "classroomId": 1, + "day": "Понедельник", + "week": "Верхняя", + "time": "11:40 - 13:10" +} +``` + +**Валидация:** +| Поле | Правило | +|------|---------| +| `teacherId` | Обязателен, ≠ 0 | +| `groupId` | Обязателен, ≠ 0 | +| `subjectId` | Обязателен, ≠ 0 | +| `lessonFormat` | `Очно` или `Онлайн` | +| `typeLesson` | `Лекция`, `Практическая работа`, `Лабораторная работа` | +| `classroomId` | Обязателен, ≠ 0 | +| `day` | Пн–Сб (на русском) | +| `week` | `Верхняя`, `Нижняя`, `Обе` | +| `time` | Обязателен | + +### `PUT /api/users/lessons/update/{lessonId}` + +Обновление занятия. Поддерживает partial update — передаются только изменённые поля. + +### `DELETE /api/users/lessons/delete/{lessonId}` + +Удаление занятия. + +### `GET /api/users/lessons/ping` + +Проверка доступности контроллера. Возвращает строку `pong`. + +--- + +## Группы + +### `GET /api/groups` + +Список всех групп. + +**Ответ:** +```json +[ + { + "id": 1, + "name": "ИВТ-21-1", + "groupSize": 25, + "educationFormId": 1, + "educationFormName": "Бакалавриат" + } +] +``` + +### `POST /api/groups` + +Создание группы. + +```json +{ + "name": "ИБ-31м", + "groupSize": 20, + "educationFormId": 2 +} +``` + +### `DELETE /api/groups/{id}` + +Удаление группы. + +--- + +## Аудитории + +### `GET /api/classrooms` + +Список аудиторий с привязанным оборудованием. + +**Ответ:** +```json +[ + { + "id": 1, + "name": "101 Ленинская", + "capacity": 120, + "isAvailable": true, + "equipments": [ + { "id": 1, "name": "Проектор" }, + { "id": 4, "name": "Интерактивная доска" } + ] + } +] +``` + +### `POST /api/classrooms` + +Создание аудитории. + +```json +{ + "name": "404 Лаборатория", + "capacity": 30, + "isAvailable": true, + "equipmentIds": [1, 2, 3] +} +``` + +### `PUT /api/classrooms/{id}` + +Обновление аудитории (partial update). + +### `DELETE /api/classrooms/{id}` + +Удаление аудитории. + +--- + +## Дисциплины + +### `GET /api/subjects` + +Список всех дисциплин. + +### `POST /api/subjects` + +```json +{ "name": "Физика" } +``` + +### `DELETE /api/subjects/{id}` + +Удаление дисциплины. + +--- + +## Оборудование + +### `GET /api/equipments` + +Список всего оборудования. + +### `POST /api/equipments` + +```json +{ "name": "3D-принтер" } +``` + +### `DELETE /api/equipments/{id}` + +Удаление оборудования. + +--- + +## Формы обучения + +### `GET /api/education-forms` + +Список форм обучения. + +**Ответ:** +```json +[ + { "id": 1, "name": "Бакалавриат" }, + { "id": 2, "name": "Магистратура" } +] +``` + +### `POST /api/education-forms` + +```json +{ "name": "Аспирантура" } +``` + +### `DELETE /api/education-forms/{id}` + +Удаление формы обучения. **Невозможно**, если к ней привязаны группы. + +--- + +## Привязка «Преподаватель ↔ Дисциплина» + +### `GET /api/teacher-subjects` + +Список всех привязок. + +**Ответ:** +```json +[ + { + "userId": 2, + "userName": "Тестовый преподаватель", + "subjectId": 1, + "subjectName": "Высшая математика" + } +] +``` + +### `POST /api/teacher-subjects` + +```json +{ + "userId": 2, + "subjectId": 3 +} +``` + +### `DELETE /api/teacher-subjects` + +```json +{ + "userId": 2, + "subjectId": 3 +} +``` + +--- + +## Управление тенантами (Базы данных) + +### `GET /api/database/status` + +Статус текущего подключения (определяется по домену запроса). + +**Ответ:** +```json +{ + "tenant": "default", + "connected": true, + "configured": true, + "name": "Default", + "url": "jdbc:postgresql://db:5432/app_db" +} +``` + +### `GET /api/database/tenants` + +Список всех тенантов. + +### `POST /api/database/tenants` + +Добавление нового тенанта. + +```json +{ + "name": "СВФУ", + "domain": "swsu", + "url": "jdbc:postgresql://db-host:5432/swsu_db", + "username": "dbuser", + "password": "dbpass" +} +``` + +**Логика:** +1. Создаёт HikariCP пул для нового тенанта +2. Запускает Flyway миграции на его БД +3. Обновляет Kubernetes ConfigMap + +### `DELETE /api/database/tenants/{domain}` + +Удаление тенанта. + +### `POST /api/database/test` + +Тест подключения к произвольной БД (без регистрации тенанта). + +```json +{ + "url": "jdbc:postgresql://host:5432/testdb", + "username": "user", + "password": "pass" +} +``` + +**Ответ:** +```json +{ + "success": true, + "message": "Подключение успешно!" +} +``` + +--- + +## Коды ответов + +| Код | Описание | +|-----|----------| +| `200` | Успех | +| `400` | Ошибка валидации (с `message` в теле) | +| `401` | Неверные учётные данные | +| `404` | Ресурс / тенант не найден | +| `500` | Внутренняя ошибка сервера | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 0000000..f4e3cf9 --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,142 @@ +# 🏗 Архитектура системы + +## Общая схема + +```mermaid +graph TD + Client["🌐 Браузер"] -->|HTTPS| Caddy["Caddy Proxy"] + Caddy -->|:80| Frontend["Frontend
(Apache httpd:alpine)"] + Caddy -->|/api/*| Backend["Backend
(Spring Boot 3.2.5)"] + + Backend --> TenantRouter{"TenantRoutingDataSource"} + TenantRouter -->|swsu.zuev.company| DB1["PostgreSQL
swsu_db"] + TenantRouter -->|mgu.zuev.company| DB2["PostgreSQL
mgu_db"] + TenantRouter -->|...| DBn["PostgreSQL
tenant_n_db"] + + Backend -->|Метрики, Логи, Трейсы| OTel["OpenTelemetry Collector"] + OTel --> SigNoz["SigNoz"] +``` + +## Компоненты + +### Frontend (Apache httpd:alpine) +- **Тип:** Статические файлы (HTML/CSS/JS) +- **Контейнер:** `httpd:alpine` — лёгкий Apache HTTP Server +- **Порт:** 80 +- **Содержание:** Три изолированных интерфейса — `admin/`, `teacher/`, `student/` +- **JS-модули:** Vanilla JavaScript с ES6 Modules (`import`/`export`) + +### Backend (Spring Boot 3.2.5) +- **Тип:** REST API сервер +- **Язык:** Java 17 +- **Порт:** 8080 (внутренний) +- **ORM:** Hibernate (JPA), `ddl-auto=none` +- **Миграции:** Flyway (программный запуск при подключении тенанта) +- **Аутентификация:** bcrypt (через `BCryptPasswordEncoder`), UUID-токены + +### PostgreSQL +- **Версия:** `postgres:alpine3.23` +- **Локально:** Одна БД `app_db` (тенант `default`) +- **Продакшн:** Множество БД, по одной на каждый университет (тенант) + +### Caddy (реверс-прокси) +- **Расположение:** `../caddy-proxy/` +- **Назначение:** TLS-терминация, маршрутизация запросов к backend/frontend +- **Домен:** `*.zuev.company` + +--- + +## Мультитенантная архитектура + +Ключевая особенность системы — изоляция данных каждого университета в отдельной БД PostgreSQL. + +### Принцип работы + +```mermaid +sequenceDiagram + participant Browser as Браузер + participant Interceptor as TenantInterceptor + participant Context as TenantContext + participant Router as TenantRoutingDataSource + participant DB as PostgreSQL + + Browser->>Interceptor: GET /api/users
Host: swsu.zuev.company + Interceptor->>Interceptor: resolveTenant("swsu.zuev.company") → "swsu" + Interceptor->>Context: setCurrentTenant("swsu") + Note over Interceptor: Проверка: hasTenant("swsu")? + Interceptor-->>Browser: 404 если тенант не найден + + Note over Context,Router: Обработка запроса контроллером + Router->>Router: determineCurrentLookupKey() → "swsu" + Router->>DB: SQL запрос к swsu_db + DB-->>Browser: Ответ с данными +``` + +### Ключевые классы + +| Класс | Назначение | +|-------|-----------| +| `TenantInterceptor` | Извлекает поддомен из заголовка `Host` и определяет тенант | +| `TenantContext` | `ThreadLocal`-хранилище имени текущего тенанта | +| `TenantRoutingDataSource` | Наследует `AbstractRoutingDataSource`, маршрутизирует запросы к нужной БД | +| `TenantDataSourceConfig` | Загружает конфигурацию тенантов из JSON-файла, создаёт HikariCP пулы | +| `TenantConfigWatcher` | Периодически (каждые 30 сек) перечитывает `tenants.json`, синхронизирует тенантов | +| `ConfigMapUpdater` | Обновляет Kubernetes ConfigMap при добавлении/удалении тенанта через API | +| `TenantConfig` | POJO с параметрами тенанта: `name`, `domain`, `url`, `username`, `password` | + +### Определение тенанта + +Логика определения тенанта по заголовку `Host`: + +| Host | Результат | +|------|----------| +| `swsu.zuev.company` | `swsu` | +| `mgu.zuev.company` | `mgu` | +| `localhost` | `default` | +| `localhost:8080` | `default` | +| `192.168.1.1` | `default` | + +### Конфигурация тенантов + +Список тенантов хранится в JSON-файле: +- **Локально:** `backend/tenants.json` +- **Продакшн:** Kubernetes ConfigMap `tenants-config`, монтируется в `/config/tenants.json` + +Формат: +```json +[ + { + "name": "ЮЗГУ", + "domain": "swsu", + "url": "jdbc:postgresql://db-host:5432/swsu_db", + "username": "dbuser", + "password": "dbpass" + } +] +``` + +### Жизненный цикл тенанта + +1. **Добавление через API:** `POST /api/database/tenants` → создаёт HikariCP пул → запускает Flyway миграции → обновляет ConfigMap +2. **Синхронизация подов:** `TenantConfigWatcher` каждые 30 сек проверяет `tenants.json` → добавляет новые / удаляет отсутствующие тенанты +3. **Удаление:** `DELETE /api/database/tenants/{domain}` → закрывает пул → обновляет ConfigMap + +### Fallback при отсутствии тенантов + +Если при запуске нет ни одного настроенного тенанта: +1. Проверяется наличие `spring.datasource.url` → создаётся тенант `default` +2. Если datasource тоже нет → создаётся H2 in-memory заглушка для инициализации Spring JPA + +--- + +## Аутентификация + +Система использует **простую модель аутентификации** без JWT или Spring Security фильтров: + +1. Клиент отправляет `POST /api/auth/login` с `username` и `password` +2. Backend проверяет пароль через `BCryptPasswordEncoder` +3. При успехе возвращается: + - UUID-токен (для заголовка `Authorization: Bearer`) + - Роль пользователя (`ADMIN`, `TEACHER`, `STUDENT`) + - Redirect URL (`/admin/`, `/teacher/`, `/student/`) +4. Токен хранится в `localStorage` на клиенте diff --git a/docs/BUSINESS_LOGIC.md b/docs/BUSINESS_LOGIC.md new file mode 100644 index 0000000..08cf64e --- /dev/null +++ b/docs/BUSINESS_LOGIC.md @@ -0,0 +1,149 @@ +# 📋 Бизнес-логика + +## Ролевая модель + +Система поддерживает три роли пользователей: + +| Роль | Enum | Возможности | +|------|------|------------| +| **Администратор** (Деканат) | `ADMIN` | Полный доступ: CRUD пользователей, групп, аудиторий, дисциплин, расписания. Управление тенантами (БД). | +| **Преподаватель** | `TEACHER` | Просмотр своего расписания. В перспективе — подача заявок на перенос. | +| **Студент** | `STUDENT` | Только просмотр расписания (Read-only). | + +После авторизации пользователь перенаправляется на свой интерфейс: +- `ADMIN` → `/admin/` +- `TEACHER` → `/teacher/` +- `STUDENT` → `/student/` + +--- + +## Управление ресурсами + +### Кафедры (Departments) + +Организационные единицы университета. К кафедре привязываются пользователи, группы и дисциплины. + +- Имеют уникальный числовой `code` +- Предзаполнены: «Кафедра ИБ», «Кафедра ВТ», «Кафедра КТ» + +### Специальности (Specialties) + +Учебные направления с кодом по ФГОС. + +- Примеры: «Информационная безопасность» (10.03.01), «Программная инженерия» (09.03.04) + +### Формы обучения (Education Forms) + +Уровни/формы обучения для привязки к группам. + +- Предзаполнены: Бакалавриат, Магистратура, Специалитет +- Нельзя удалить форму обучения, если к ней привязаны группы + +### Учебные группы (Student Groups) + +- **Поля:** Название (уникальное), численность, форма обучения, кафедра, курс (1–6) +- **Подгруппы:** Возможно деление группы на подгруппы (таблица `subgroups`) + +### Аудитории (Classrooms) + +- **Поля:** Название (уникальное), вместимость (> 0), корпус, этаж, доступность +- **Оборудование:** К каждой аудитории привязывается список оборудования (Many-to-Many) с указанием количества +- **Статус:** Флаг `is_available` для блокирования назначения пар + +### Оборудование (Equipments) + +Каталог оборудования для привязки к аудиториям. + +- Предзаполнены: Проектор, ПК, Лаборатория, Интерактивная доска, Документ-камера, Аудиосистема +- Уникальность по названию + +### Дисциплины (Subjects) + +- **Поля:** Название (уникальное), код, кафедра, описание +- Привязка преподавателей через `teacher_subjects` (Many-to-Many) + +--- + +## Логика расписания + +### Сущность «Занятие» (Lesson) + +Каждая запись в расписании содержит: + +| Поле | Описание | Пример | +|------|----------|--------| +| `teacher_id` | Преподаватель | 2 | +| `group_id` | Учебная группа | 1 | +| `subject_id` | Дисциплина | 3 | +| `lesson_format` | Формат проведения | `Очно`, `Онлайн` | +| `type_lesson` | Тип занятия | `Лекция`, `Практическая работа`, `Лабораторная работа` | +| `classroom_id` | Аудитория | 1 | +| `day` | День недели | `Понедельник` ... `Суббота` | +| `week` | Чётность недели | `Верхняя`, `Нижняя`, `Обе` | +| `time` | Временной слот | `8:00 - 9:30` | + +### Временны́е слоты + +Система использует 7 фиксированных слотов по 90 минут: + +| № | Время | +|---|-------| +| 1 | 08:00 – 09:30 | +| 2 | 09:40 – 11:10 | +| 3 | 11:40 – 13:10 | +| 4 | 13:30 – 15:00 | +| 5 | 15:00 – 16:30 | +| 6 | 16:40 – 18:10 | +| 7 | 18:30 – 20:00 | + +### Валидация при создании/обновлении + +- **Дни:** только `Понедельник` – `Суббота` (`DayAndWeekValidator`) +- **Недели:** только `Верхняя`, `Нижняя`, `Обе` +- **Формат:** только `Очно`, `Онлайн` (`TypeAndFormatLessonValidator`) +- **Тип:** только `Лекция`, `Практическая работа`, `Лабораторная работа` +- Все ID (преподаватель, группа, дисциплина, аудитория) обязательны и не могут быть 0 + +### Данные к составлению расписания (Schedule Data) + +Таблица `schedule_data` хранит **плановую нагрузку** для составления расписания: + +| Поле | Описание | +|------|----------| +| `department_id` | Кафедра | +| `semester` | Номер семестра | +| `group_id` | Учебная группа | +| `subjects_id` | Дисциплина | +| `lesson_type_id` | Тип занятия | +| `number_of_hours` | Количество часов | +| `is_division` | Деление на подгруппы | +| `teacher_id` | Преподаватель | +| `semester_type` | Тип семестра (Весенний / Осенний) | +| `period` | Учебный год (напр. `2024/2025`) | + +--- + +## Привязка преподаватель ↔ дисциплина + +Связь Many-to-Many через таблицу `teacher_subjects`: +- Указывается, какие дисциплины может вести конкретный преподаватель +- Дополнительные поля: `qualification_level`, `experience_years` + +Дополнительная связь через `teacher_lesson_types`: +- Определяет, какие **типы занятий** (лекция, практика, лаба) может вести преподаватель по конкретной дисциплине + +--- + +## Бизнес-правила (планируемые) + +> **Примечание:** Следующие правила описаны в требованиях, но пока не полностью реализованы в коде. + +### Проверка конфликтов +- **Критический конфликт:** Преподаватель не может одновременно находиться в двух разных аудиториях +- **Исключение:** Преподаватель может вести несколько пар одновременно (потоковая лекция), если все группы в одной аудитории +- **Вместимость:** Суммарная численность всех групп в слоте не должна превышать вместимость аудитории + +### Управление инцидентами +- Регистрация отсутствия преподавателя (болезнь, командировка) с указанием периода +- Автоматическая подсветка конфликтующих пар (Red Zone) +- Resolution Wizard: предложение замены преподавателя или переноса занятия diff --git a/docs/DATABASE.md b/docs/DATABASE.md new file mode 100644 index 0000000..c025547 --- /dev/null +++ b/docs/DATABASE.md @@ -0,0 +1,362 @@ +# 🗄 База данных + +## Общая информация + +- **СУБД:** PostgreSQL (локально `postgres:alpine3.23`, продакшн — managed PostgreSQL) +- **Управление схемой:** Flyway (программный запуск) +- **Hibernate DDL:** Отключён (`ddl-auto=none`) +- **Расширения:** `pgcrypto` (bcrypt-хеширование паролей) +- **Мультитенантность:** Каждый тенант = отдельная БД + +--- + +## ER-диаграмма + +```mermaid +erDiagram + departments { + BIGSERIAL id PK + VARCHAR name + BIGINT code UK + } + + specialties { + BIGSERIAL id PK + VARCHAR name + VARCHAR specialty_code + } + + users { + BIGSERIAL id PK + VARCHAR username UK + VARCHAR password + VARCHAR role + VARCHAR full_name + VARCHAR job_title + BIGINT department_id FK + TIMESTAMP created_at + TIMESTAMP updated_at + } + + education_forms { + BIGSERIAL id PK + VARCHAR name UK + TEXT description + TIMESTAMP created_at + } + + student_groups { + BIGSERIAL id PK + VARCHAR name UK + BIGINT group_size + BIGINT education_form_id FK + BIGINT department_id FK + INT course + TIMESTAMP created_at + } + + subgroups { + BIGSERIAL id PK + BIGINT group_id FK + VARCHAR name + INT student_capacity + } + + subjects { + BIGSERIAL id PK + VARCHAR name UK + VARCHAR code + BIGINT department_id FK + TEXT description + TIMESTAMP created_at + } + + lesson_types { + BIGSERIAL id PK + VARCHAR name UK + VARCHAR color_code + INT duration_minutes + } + + equipments { + BIGSERIAL id PK + VARCHAR name UK + TEXT description + VARCHAR inventory_number + } + + classrooms { + BIGSERIAL id PK + VARCHAR name UK + INT capacity + VARCHAR building + INT floor + BOOLEAN is_available + TEXT description + TIMESTAMP created_at + } + + classroom_equipments { + BIGINT classroom_id FK,PK + BIGINT equipment_id FK,PK + INT quantity + TEXT notes + } + + teacher_subjects { + BIGINT user_id FK,PK + BIGINT subject_id FK,PK + VARCHAR qualification_level + INT experience_years + } + + teacher_lesson_types { + BIGINT user_id FK,PK + BIGINT subject_id FK,PK + BIGINT lesson_type_id FK,PK + } + + lessons { + BIGSERIAL id PK + BIGINT teacher_id FK + BIGINT group_id FK + BIGINT subject_id FK + VARCHAR lesson_format + VARCHAR type_lesson + BIGINT classroom_id FK + VARCHAR day + VARCHAR week + VARCHAR time + } + + schedule_data { + BIGSERIAL id PK + BIGINT department_id FK + INT semester + BIGINT group_id FK + BIGINT subjects_id FK + BIGINT lesson_type_id FK + INT number_of_hours + BOOLEAN is_division + BIGINT teacher_id FK + VARCHAR semester_type + VARCHAR period + } + + 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" + student_groups ||--o{ subgroups : "group_id" + student_groups ||--o{ lessons : "group_id" + student_groups ||--o{ schedule_data : "group_id" + users ||--o{ lessons : "teacher_id" + users ||--o{ teacher_subjects : "user_id" + users ||--o{ teacher_lesson_types : "user_id" + users ||--o{ schedule_data : "teacher_id" + subjects ||--o{ lessons : "subject_id" + subjects ||--o{ teacher_subjects : "subject_id" + subjects ||--o{ teacher_lesson_types : "subject_id" + subjects ||--o{ schedule_data : "subjects_id" + lesson_types ||--o{ teacher_lesson_types : "lesson_type_id" + lesson_types ||--o{ schedule_data : "lesson_type_id" + classrooms ||--o{ lessons : "classroom_id" + classrooms ||--o{ classroom_equipments : "classroom_id" + equipments ||--o{ classroom_equipments : "equipment_id" +``` + +--- + +## Описание таблиц + +### Справочники высшего уровня + +#### `departments` — Кафедры +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID кафедры | +| `name` | VARCHAR(255) | Название кафедры | +| `code` | BIGINT UNIQUE | Код кафедры | + +#### `specialties` — Специальности +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID специальности | +| `name` | VARCHAR(255) | Название специальности | +| `specialty_code` | VARCHAR(255) | Код ФГОС (напр. `10.03.01`) | + +### Пользователи + +#### `users` — Пользователи системы +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID пользователя | +| `username` | VARCHAR(50) UNIQUE | Логин | +| `password` | VARCHAR(255) | bcrypt-хеш пароля | +| `role` | VARCHAR(20) | `ADMIN`, `TEACHER`, `STUDENT` | +| `full_name` | VARCHAR(255) | ФИО | +| `job_title` | VARCHAR(255) | Должность | +| `department_id` | BIGINT FK → departments | Кафедра | +| `created_at` | TIMESTAMP | Дата создания | +| `updated_at` | TIMESTAMP | Дата обновления (авто-триггер) | + +> **Триггер:** `update_users_updated_at` автоматически обновляет `updated_at` при любом `UPDATE`. + +### Учебный процесс + +#### `education_forms` — Формы обучения +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID | +| `name` | VARCHAR(100) UNIQUE | Название (Бакалавриат, Магистратура, Специалитет) | +| `description` | TEXT | Описание | + +#### `student_groups` — Учебные группы +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID | +| `name` | VARCHAR(100) UNIQUE | Название группы (напр. `ИВТ-21-1`) | +| `group_size` | BIGINT | Количество студентов | +| `education_form_id` | BIGINT FK → education_forms | Форма обучения | +| `department_id` | BIGINT FK → departments | Кафедра | +| `course` | INT CHECK(1–6) | Курс | + +#### `subgroups` — Подгруппы +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID | +| `group_id` | BIGINT FK → student_groups (CASCADE) | Родительская группа | +| `name` | VARCHAR(100) | Название подгруппы | +| `student_capacity` | INT | Количество студентов | + +#### `subjects` — Дисциплины +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID | +| `name` | VARCHAR(200) UNIQUE | Название | +| `code` | VARCHAR(20) | Код предмета | +| `department_id` | BIGINT FK → departments | Кафедра | +| `description` | TEXT | Описание | + +### Аудиторный фонд + +#### `classrooms` — Аудитории +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID | +| `name` | VARCHAR(50) UNIQUE | Название (напр. `101 Ленинская`) | +| `capacity` | INT CHECK(> 0) | Вместимость | +| `building` | VARCHAR(50) | Корпус | +| `floor` | INT | Этаж | +| `is_available` | BOOLEAN | Доступна для назначения пар | +| `description` | TEXT | Описание | + +#### `equipments` — Оборудование +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID | +| `name` | VARCHAR(50) UNIQUE | Название | +| `description` | TEXT | Описание | +| `inventory_number` | VARCHAR(50) | Инвентарный номер | + +#### `classroom_equipments` — Привязка оборудования к аудиториям +| Колонка | Тип | Описание | +|---------|-----|----------| +| `classroom_id` | BIGINT PK, FK → classrooms (CASCADE) | Аудитория | +| `equipment_id` | BIGINT PK, FK → equipments (CASCADE) | Оборудование | +| `quantity` | INT CHECK(> 0) | Количество единиц | +| `notes` | TEXT | Примечания | + +### Расписание + +#### `lessons` — Основное расписание занятий +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID | +| `teacher_id` | BIGINT FK → users | Преподаватель | +| `group_id` | BIGINT FK → student_groups | Группа | +| `subject_id` | BIGINT FK → subjects | Дисциплина | +| `lesson_format` | VARCHAR(255) | `Очно` / `Онлайн` | +| `type_lesson` | VARCHAR(255) | `Лекция` / `Практическая работа` / `Лабораторная работа` | +| `classroom_id` | BIGINT FK → classrooms | Аудитория | +| `day` | VARCHAR(255) | День недели | +| `week` | VARCHAR(255) | `Верхняя` / `Нижняя` / `Обе` | +| `time` | VARCHAR(255) | Временной слот | + +#### `lesson_types` — Типы занятий (справочник) +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID | +| `name` | VARCHAR(50) UNIQUE | Название типа | +| `color_code` | VARCHAR(7) | HEX-цвет для UI (напр. `#FF6B6B`) | +| `duration_minutes` | INT | Длительность (по умолчанию 90) | + +### Связи «Преподаватель ↔ Дисциплина» + +#### `teacher_subjects` — Квалификация преподавателей +| Колонка | Тип | Описание | +|---------|-----|----------| +| `user_id` | BIGINT PK, FK → users (CASCADE) | Преподаватель | +| `subject_id` | BIGINT PK, FK → subjects (CASCADE) | Дисциплина | +| `qualification_level` | VARCHAR(50) | Уровень квалификации | +| `experience_years` | INT | Стаж | + +#### `teacher_lesson_types` — Типы занятий преподавателя +| Колонка | Тип | Описание | +|---------|-----|----------| +| `user_id` | BIGINT PK, FK → users (CASCADE) | Преподаватель | +| `subject_id` | BIGINT PK, FK → subjects (CASCADE) | Дисциплина | +| `lesson_type_id` | BIGINT PK, FK → lesson_types (CASCADE) | Тип занятия | + +#### `schedule_data` — Данные к составлению расписания +| Колонка | Тип | Описание | +|---------|-----|----------| +| `id` | BIGSERIAL PK | ID | +| `department_id` | BIGINT FK → departments | Кафедра | +| `semester` | INT | Номер семестра | +| `group_id` | BIGINT FK → student_groups | Группа | +| `subjects_id` | BIGINT FK → subjects | Дисциплина | +| `lesson_type_id` | BIGINT FK → lesson_types | Тип занятия | +| `number_of_hours` | INT | Количество часов | +| `is_division` | BOOLEAN | Деление на подгруппы | +| `teacher_id` | BIGINT FK → users | Преподаватель | +| `semester_type` | VARCHAR(255) | Весенний / Осенний | +| `period` | VARCHAR(255) | Учебный год | + +--- + +## Flyway миграции + +### Правила работы + +1. Все миграции находятся в `backend/src/main/resources/db/migration/` +2. Формат имени: `V{номер}__{описание}.sql` (напр. `V1__init.sql`, `V2__add_departments.sql`) +3. **ЗАПРЕЩЕНО** изменять уже закоммиченные файлы миграций — это сломает контрольные суммы Flyway +4. Flyway запускается **программно** при первом обращении к БД тенанта (`TenantConfigWatcher.initDatabaseForTenant()`) +5. Настройка `baselineOnMigrate=true` — если в БД уже есть данные, Flyway начнёт с baseline + +### Текущие миграции + +| Файл | Описание | +|------|----------| +| `V1__init.sql` | Инициализация: все таблицы, тестовые данные, триггеры, комментарии | + +### Накатывание на существующих тенантов + +Для применения новой миграции к уже существующим тенантам необходимо перезапустить backend: + +```bash +# Kubernetes +kubectl rollout restart deployment backend -n magistr + +# Docker Compose (локально) +docker compose restart backend +``` + +### Полный сброс БД (локально) + +```bash +docker compose down -v # Удаляет volumes (данные) +docker compose up -d # Пересоздаёт БД с нуля +``` diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md new file mode 100644 index 0000000..a0ba3a2 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,275 @@ +# 🛠 Руководство для разработчиков + +## Локальный запуск + +### Предварительные требования + +- Docker и Docker Compose +- Git +- (Опционально) Java 17 + Maven 3.9+ для запуска backend вне Docker + +### Первый запуск + +```bash +# Создать Docker-сеть +docker network create proxy + +# Собрать и запустить +docker compose up -d --build + +# Убедиться, что всё работает +docker compose logs -f +``` + +Приложение доступно: **http://localhost:80** + +### Пересборка после изменений + +```bash +# Пересобрать только backend +docker compose up -d --build backend + +# Пересобрать только frontend +docker compose up -d --build frontend +``` + +### Полный сброс данных + +```bash +docker compose down -v # Удаляет БД +docker compose up -d # Пересоздаёт с нуля +``` + +--- + +## Соглашения о коде + +### Java (Backend) + +#### Именование + +| Категория | Стиль | Пример | +|-----------|-------|--------| +| Классы | PascalCase | `LessonsController`, `LessonResponse` | +| Методы и переменные | camelCase | `getAllLessons()`, `teacherId` | +| Константы | UPPER_SNAKE_CASE | `ROLE_REDIRECTS` | +| Пакеты | lowercase | `com.magistr.app.controller` | + +#### Архитектурные правила + +- **Constructor Injection** — все зависимости через конструктор (не `@Autowired` на поля) +- **Controller → Repository** — контроллеры работают напрямую с репозиториями (без слоя service) +- **Префикс `/api/`** — все REST-эндпоинты +- **`ResponseEntity`** — все мутирующие методы возвращают `ResponseEntity` с HTTP-статусом +- **Сообщения на русском** — все ошибки и уведомления на русском языке + +#### Логирование + +Используйте SLF4J: + +```java +private static final Logger logger = LoggerFactory.getLogger(MyController.class); + +// Информационные сообщения +logger.info("Запрос на получение всех занятий"); + +// Ошибки с полным стектрейсом +logger.error("Ошибка при сохранении: {}", e.getMessage(), e); +``` + +#### Валидация + +- Для сложных правил — отдельные классы-валидаторы (`DayAndWeekValidator`, `TypeAndFormatLessonValidator`) +- Для простых — inline-проверки в контроллере с `ResponseEntity.badRequest()` + +#### Импорты + +```java +// 1. Static imports +import static org.junit.Assert.*; + +// 2. Java/Jakarta +import java.util.*; +import jakarta.persistence.*; + +// 3. External libraries +import org.springframework.web.bind.annotation.*; +import com.fasterxml.jackson.databind.ObjectMapper; + +// 4. Internal packages (wildcard для того же модуля) +import com.magistr.app.model.*; +import com.magistr.app.repository.*; +``` + +#### Форматирование + +- **Отступы:** 4 пробела +- **Скобки:** K&R style (открывающая на той же строке) +- **Длина строки:** до 120 символов +- **Фигурные скобки** обязательны для `if`/`for`/`while` + +### JavaScript (Frontend) + +#### Именование + +| Категория | Стиль | Пример | +|-----------|-------|--------| +| Файлы | kebab-case | `main.js`, `schedule-view.js` | +| Функции и переменные | camelCase | `loadUsers()`, `pageTitle` | +| Константы | UPPER_SNAKE_CASE | `API_BASE_URL` | + +#### Модули + +- ES6 Modules с `import`/`export` +- **Всегда указывать расширение:** `import { api } from './api.js';` + +#### Лучшие практики + +```javascript +// ✅ Предпочитайте const +const token = localStorage.getItem('token'); + +// ✅ Async/await вместо .then() +async function loadData() { + try { + const data = await api.get('/api/users'); + } catch (e) { + console.error('Ошибка:', e.message); + } +} + +// ✅ Template literals +const msg = `Найдено ${items.length} записей`; + +// ✅ Деструктуризация +const { id, name, role } = user; +``` + +#### Форматирование + +- **Отступы:** 4 пробела +- **Кавычки:** одинарные `'` +- **Точки с запятой:** обязательны + +--- + +## Создание нового эндпоинта (пошагово) + +### 1. Модель (если нужна новая таблица) + +Создайте Flyway миграцию `V{N}__{description}.sql`: + +```sql +-- backend/src/main/resources/db/migration/V3__add_absences.sql +CREATE TABLE IF NOT EXISTS absences ( + id BIGSERIAL PRIMARY KEY, + teacher_id BIGINT NOT NULL REFERENCES users(id), + reason VARCHAR(255) NOT NULL, + start_date DATE NOT NULL, + end_date DATE NOT NULL +); +``` + +Создайте JPA-сущность: + +```java +@Entity +@Table(name = "absences") +public class Absence { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + // ... +} +``` + +### 2. Репозиторий + +```java +public interface AbsenceRepository extends JpaRepository { + List findByTeacherId(Long teacherId); +} +``` + +### 3. DTO (опционально) + +```java +public record AbsenceResponse(Long id, String teacherName, String reason) {} +``` + +### 4. Контроллер + +```java +@RestController +@RequestMapping("/api/absences") +public class AbsenceController { + private final AbsenceRepository absenceRepository; + + public AbsenceController(AbsenceRepository absenceRepository) { + this.absenceRepository = absenceRepository; + } + + @GetMapping + public List getAll() { + return absenceRepository.findAll(); + } +} +``` + +--- + +## Работа с миграциями Flyway + +### Правила + +1. **Никогда** не изменяйте уже закоммиченные файлы миграций +2. Имя файла: `V{номер}__{описание}.sql` (два подчёркивания!) +3. Нумерация строго инкрементальная: `V1`, `V2`, `V3`, ... +4. После добавления — перезапустите backend для применения + +### Применение + +```bash +# Локально — сброс и повтор всех миграций +docker compose down -v && docker compose up -d + +# Продакшн — применить к существующим тенантам +kubectl rollout restart deployment backend -n magistr +``` + +--- + +## Структура пакетов (Backend) + +``` +com.magistr.app/ +├── Application.java # Точка входа +├── config/ +│ ├── AppConfig.java # Бины (BCryptPasswordEncoder) +│ ├── DataInitializer.java # Инициализация данных +│ └── tenant/ # Мультитенантность +│ ├── TenantConfig.java # POJO конфигурации тенанта +│ ├── TenantContext.java # ThreadLocal текущего тенанта +│ ├── TenantInterceptor.java # Определение тенанта из Host +│ ├── TenantRoutingDataSource.java # Маршрутизация к БД +│ ├── TenantDataSourceConfig.java # Spring-конфигурация +│ ├── TenantConfigWatcher.java # Периодическая синхронизация +│ └── ConfigMapUpdater.java # Обновление K8s ConfigMap +├── controller/ # REST-контроллеры +│ ├── AuthController.java +│ ├── LessonsController.java +│ ├── ClassroomController.java +│ ├── DatabaseController.java +│ ├── UserController.java +│ ├── GroupController.java +│ ├── SubjectController.java +│ ├── EquipmentController.java +│ ├── EducationFormController.java +│ └── TeacherSubjectController.java +├── dto/ # Data Transfer Objects +├── model/ # JPA-сущности +├── repository/ # Spring Data JPA +└── utils/ # Валидаторы + ├── DayAndWeekValidator.java + └── TypeAndFormatLessonValidator.java +``` diff --git a/docs/FRONTEND.md b/docs/FRONTEND.md new file mode 100644 index 0000000..c571a55 --- /dev/null +++ b/docs/FRONTEND.md @@ -0,0 +1,203 @@ +# 🎨 Frontend + +## Общая информация + +| Параметр | Значение | +|----------|----------| +| **Фреймворк** | Нет (Vanilla JavaScript) | +| **Модульная система** | ES6 Modules (`import`/`export`) | +| **Стили** | CSS (модульный подход) | +| **Шрифт** | [Inter](https://fonts.google.com/specimen/Inter) (Google Fonts) | +| **Веб-сервер** | Apache httpd:alpine | + +--- + +## Структура файлов + +``` +frontend/ +├── index.html # 🔐 Страница авторизации (общая) +├── script.js # Логика авторизации +├── style.css # Стили страницы авторизации +├── theme-toggle.js # Переключение светлой/тёмной темы +├── Dockerfile # httpd:alpine +│ +├── admin/ # 👨‍💼 Интерфейс администратора +│ ├── index.html # SPA-оболочка с sidebar +│ ├── css/ +│ │ ├── main.css # CSS-переменные, цвета, типографика +│ │ ├── layout.css # Раскладка (sidebar, topbar, content) +│ │ ├── components.css # Кнопки, таблицы, карточки, формы +│ │ └── modals.css # Модальные окна +│ ├── js/ +│ │ ├── main.js # Инициализация, маршрутизация, навигация +│ │ ├── api.js # HTTP-обёртка (fetch + Authorization) +│ │ ├── utils.js # Утилиты +│ │ ├── otel.js # OpenTelemetry (клиентская телеметрия) +│ │ └── views/ # Модули представлений +│ │ ├── users.js # Управление пользователями +│ │ ├── groups.js # Управление группами +│ │ ├── classrooms.js # Управление аудиториями +│ │ ├── subjects.js # Управление дисциплинами +│ │ ├── equipments.js # Управление оборудованием +│ │ ├── edu-forms.js # Формы обучения +│ │ ├── schedule.js # Расписание занятий +│ │ └── database.js # Управление тенантами +│ └── views/ # HTML-шаблоны представлений +│ ├── users.html +│ ├── groups.html +│ ├── classrooms.html +│ ├── subjects.html +│ ├── equipments.html +│ ├── edu-forms.html +│ ├── schedule.html +│ └── database.html +│ +├── teacher/ # 👩‍🏫 Интерфейс преподавателя +│ └── index.html # Просмотр расписания +│ +└── student/ # 🎓 Интерфейс студента + └── index.html # Просмотр расписания (read-only) +``` + +--- + +## Система маршрутизации (Admin SPA) + +Админ-панель работает как **Single Page Application** без фреймворка. + +Навигация реализована через `data-tab` атрибуты на элементах sidebar: + +```html +Пользователи +Группы +Расписание занятий +``` + +При клике на пункт меню `main.js`: +1. Загружает HTML-шаблон из `views/{tab}.html` через `fetch()` +2. Вставляет его в `#app-content` +3. Подключает соответствующий JS-модуль из `js/views/{tab}.js` +4. Обновляет заголовок страницы (`#page-title`) + +### Разделы админ-панели + +| Tab | Описание | API | +|-----|----------|-----| +| `users` | CRUD пользователей | `/api/users` | +| `groups` | CRUD групп | `/api/groups` | +| `edu-forms` | Формы обучения | `/api/education-forms` | +| `equipments` | Оборудование | `/api/equipments` | +| `classrooms` | Аудитории | `/api/classrooms` | +| `subjects` | Дисциплины | `/api/subjects` | +| `schedule` | Расписание | `/api/users/lessons` | +| `database` | Тенанты | `/api/database` | + +--- + +## API-клиент (`api.js`) + +Все HTTP-запросы проходят через обёртку `apiFetch()`: + +```javascript +export async function apiFetch(endpoint, method = 'GET', body = null) { + const response = await fetch(endpoint, { + method, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: body ? JSON.stringify(body) : null + }); + + if (!response.ok) { + throw new Error(data?.message || `Ошибка HTTP: ${response.status}`); + } + + return await response.json(); +} + +// Shortcut-методы +export const api = { + get: (url) => apiFetch(url, 'GET'), + post: (url, body) => apiFetch(url, 'POST', body), + put: (url, body) => apiFetch(url, 'PUT', body), + delete: (url, body) => apiFetch(url, 'DELETE', body) +}; +``` + +Токен берётся из `localStorage.getItem('token')`. + +--- + +## Аутентификация (Frontend) + +### Страница входа (`/index.html`) + +1. Пользователь вводит логин/пароль +2. `script.js` отправляет `POST /api/auth/login` +3. При успехе сохраняет в `localStorage`: + - `token` — UUID-токен + - `role` — роль пользователя +4. Перенаправляет на соответствующий интерфейс: + - `ADMIN` → `/admin/` + - `TEACHER` → `/teacher/` + - `STUDENT` → `/student/` + +### Проверка авторизации + +На каждой странице проверяется наличие токена и роли: + +```javascript +export function isAuthenticatedAsAdmin() { + const role = localStorage.getItem('role'); + return token && role === 'ADMIN'; +} +``` + +### Выход + +Кнопка «Выйти» очищает `localStorage` и перенаправляет на `/`. + +--- + +## CSS-архитектура + +### Модульный подход + +Стили разделены на 4 файла (порядок подключения важен): + +1. **`main.css`** — CSS-переменные (цвета, шрифты, отступы), глобальные стили, тёмная тема +2. **`layout.css`** — Sidebar, topbar, content area, responsive +3. **`components.css`** — Кнопки, таблицы, карточки, badge, формы +4. **`modals.css`** — Модальные окна + +### Темизация + +CSS-переменные позволяют поддерживать светлую/тёмную тему: + +```css +:root { + --bg-primary: #ffffff; + --text-primary: #1a1a2e; + --accent: #6366f1; +} + +[data-theme="dark"] { + --bg-primary: #0f0f23; + --text-primary: #e2e8f0; + --accent: #818cf8; +} +``` + +Переключение — через `theme-toggle.js`. + +--- + +## Адаптивность + +Интерфейс адаптирован под мобильные устройства: +- Sidebar скрывается на экранах < 768px +- Появляется кнопка-гамбургер (`#menu-toggle`) +- Sidebar выезжает как overlay +- Таблицы получают горизонтальный скролл diff --git a/docs/INFRASTRUCTURE.md b/docs/INFRASTRUCTURE.md new file mode 100644 index 0000000..6c80225 --- /dev/null +++ b/docs/INFRASTRUCTURE.md @@ -0,0 +1,137 @@ +# 🏭 Инфраструктура + +## Docker Compose (локальная разработка) + +### Сервисы + +```yaml +services: + backend: # Spring Boot (Java 17), порт 8080 + frontend: # Apache httpd:alpine, порт 80 + db: # PostgreSQL alpine3.23, порт 5432 +``` + +### Сеть + +Все сервисы работают в Docker-сети `proxy` (external). Перед первым запуском: + +```bash +docker network create proxy +``` + +### Переменные окружения + +Файл `.env` в корне проекта: + +```env +POSTGRES_USER=myuser +POSTGRES_PASSWORD=supersecretpassword +``` + +### Dockerfile (Backend) + +Backend собирается через multi-stage сборку Maven: +1. Этап сборки: `maven:3-eclipse-temurin-17-alpine` → `mvn package` +2. Этап запуска: `eclipse-temurin:17-jre-alpine` → `java -jar app.jar` + +### Dockerfile (Frontend) + +```dockerfile +FROM httpd:alpine +COPY . /usr/local/apache2/htdocs/ +RUN chown -R www-data:www-data /usr/local/apache2/htdocs/ +``` + +--- + +## Kubernetes (продакшн) + +### Расположение конфигурации + +Файлы Kubernetes манифестов: `../k8s/` + +### Ключевые ресурсы + +| Ресурс | Тип | Описание | +|--------|-----|----------| +| `backend` | Deployment | Spring Boot приложение | +| `frontend` | Deployment | Apache httpd | +| `tenants-config` | ConfigMap | JSON-список тенантов | + +### ConfigMap для тенантов + +ConfigMap `tenants-config` монтируется в под backend по пути `/config/tenants.json`. + +При добавлении тенанта через API: +1. `DatabaseController` обновляет in-memory DataSource +2. `ConfigMapUpdater` обновляет ConfigMap через Kubernetes API +3. `TenantConfigWatcher` на остальных подах подхватывает изменения (каждые 30 сек) + +### Обновление backend + +```bash +kubectl rollout restart deployment backend -n magistr +``` + +--- + +## Caddy (реверс-прокси) + +**Расположение:** `../caddy-proxy/` для локальной разработки, в продакшене - отдельный сервис + +В продакшене Caddy обрабатывает входящий трафик для `*.zuev.company`: +- Автоматическое получение TLS-сертификатов (Let's Encrypt) +- Маршрутизация `/api/*` → backend:8080 +- Маршрутизация статики → frontend:80 + +--- + +## CI/CD (Gitea Actions) + +### Пайплайн сборки Docker-образов + +Расположение: `.gitea/workflows/docker-build.yaml` + +Основные шаги: +1. Checkout кода +2. Login в Docker Registry +3. Build + Push образов (`backend`, `frontend`) +4. Генерация меток через `docker/metadata-action` + +--- + +## Мониторинг (SigNoz + OpenTelemetry) + +### Архитектура мониторинга + +```mermaid +graph LR + Backend["Spring Boot"] -->|OTLP gRPC| Collector["OTel Collector"] + Frontend["JS (otel.js)"] -->|OTLP HTTP| Collector + Collector --> SigNoz["SigNoz"] + + Collector -->|"Метрики PostgreSQL"| PgExporter["pg_exporter"] +``` + +### Интеграция Backend + +Backend отправляет через OpenTelemetry: +- **Логи** — через Logback + OTLP exporter +- **Трейсы** — автоинструментация Spring Boot +- **Метрики** — JVM метрики, HTTP метрики + +Tenant ID добавляется в: +- MDC (логи): `MDC.put("tenant.id", tenant)` +- Span атрибуты: `Span.current().setAttribute("tenant.id", tenant)` + +### Интеграция Frontend + +Файл `admin/js/otel.js` — клиентская телеметрия: +- Метрики производительности страниц +- Трейсы пользовательских действий + +### Дашборды SigNoz + +- JVM Dashboard (Heap, GC, Threads) +- PostgreSQL Dashboard (Connections, Queries) +- HTTP Dashboard (Requests, Latency, Errors) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3775cc5 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,113 @@ +# 📚 Magistr — Система управления университетским расписанием + +## Обзор + +**Magistr** — веб-приложение для управления расписанием занятий университета. Система поддерживает мультитенантную архитектуру (каждый университет = отдельная база данных), ролевую модель доступа (Администратор, Преподаватель, Студент) и полное управление аудиторным фондом, группами, дисциплинами и преподавательским составом. + +- **Продакшн:** [https://magistr.zuev.company](https://magistr.zuev.company) +- **Локальная разработка:** [http://localhost:80](http://localhost:80) + +--- + +## Стек технологий + +| Компонент | Технология | +|-----------|-----------| +| **Backend** | Java 17, Spring Boot 3.2.5 | +| **Frontend** | Vanilla JavaScript (ES6 Modules) + HTML/CSS | +| **База данных** | PostgreSQL (через Flyway миграции) | +| **Контейнеризация** | Docker, Docker Compose | +| **Продакшн** | Kubernetes, Caddy (реверс-прокси) | +| **Мониторинг** | SigNoz, OpenTelemetry | +| **CI/CD** | Gitea Actions | + +--- + +## Быстрый старт + +### Предварительные требования + +- Docker и Docker Compose +- Git + +### Локальный запуск + +```bash +# 1. Клонировать репозиторий +git clone magistr && cd magistr + +# 2. Создать Docker-сеть (если ещё не создана) +docker network create proxy + +# 3. Запустить все сервисы +docker compose up -d --build +``` + +После запуска приложение доступно по адресу: **http://localhost:80** + +**Учётные данные по умолчанию:** + +| Логин | Пароль | Роль | +|-------|--------|------| +| `admin` | `admin` | Администратор | +| `Тестовый преподаватель` | `1234567890` | Преподаватель | + +### Полезные команды + +```bash +# Просмотр логов +docker compose logs -f backend + +# Полный сброс базы данных (удаление данных + повтор миграций) +docker compose down -v +docker compose up -d + +# Остановка всех сервисов +docker compose down +``` + +--- + +## Структура проекта + +``` +magistr/ +├── backend/ # Java Spring Boot backend +│ └── src/main/ +│ ├── java/com/magistr/app/ +│ │ ├── controller/ # REST-контроллеры (10 шт.) +│ │ ├── model/ # JPA-сущности +│ │ ├── dto/ # Data Transfer Objects +│ │ ├── repository/ # Spring Data JPA репозитории +│ │ ├── config/ # Конфигурация приложения +│ │ │ └── tenant/ # Мультитенантность +│ │ └── utils/ # Валидаторы +│ └── resources/ +│ ├── application.properties +│ └── db/migration/ # Flyway SQL миграции +├── frontend/ # Статический фронтенд +│ ├── index.html # Страница авторизации +│ ├── admin/ # Админ-панель (деканат) +│ │ ├── js/views/ # Модули представлений +│ │ └── css/ # Стили +│ ├── teacher/ # Интерфейс преподавателя +│ └── student/ # Интерфейс студента +├── docs/ # 📖 Документация (вы здесь) +├── compose.yaml # Docker Compose конфигурация +├── .env # Переменные окружения +└── AGENTS.md # Руководство для AI-агентов +``` + +--- + +## 📖 Навигация по документации + +| Документ | Содержание | +|----------|-----------| +| [Архитектура](ARCHITECTURE.md) | Общая архитектура, мультитенантность, взаимодействие компонентов | +| [Бизнес-логика](BUSINESS_LOGIC.md) | Ролевая модель, правила расписания, управление ресурсами | +| [База данных](DATABASE.md) | Схема БД, описание таблиц, Flyway миграции | +| [REST API](API.md) | Все эндпоинты с примерами запросов и ответов | +| [Инфраструктура](INFRASTRUCTURE.md) | Docker, Kubernetes, CI/CD, мониторинг | +| [Разработка](DEVELOPMENT.md) | Code Style, соглашения, инструкции для разработчиков | +| [Frontend](FRONTEND.md) | Архитектура фронтенда, модули, стили |