Merge branch 'department_dev' of ssh://gitea.zuev.company:2222/Zuev/magistr into department_dev
This commit is contained in:
6
.agents/rules/1.md
Normal file
6
.agents/rules/1.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
trigger: always_on
|
||||
glob:
|
||||
description:
|
||||
---
|
||||
|
||||
85
.agents/skills/AutoUpdateDocs.md
Normal file
85
.agents/skills/AutoUpdateDocs.md
Normal file
@@ -0,0 +1,85 @@
|
||||
---
|
||||
name: AutoUpdateDocs
|
||||
description: Автоматическое обновление документации проекта после изменений в коде
|
||||
---
|
||||
|
||||
# Скилл: Автоматическое обновление документации
|
||||
|
||||
## Когда активировать
|
||||
|
||||
Этот скилл **ДОЛЖЕН** выполняться автоматически после любых изменений, затрагивающих:
|
||||
|
||||
- **Контроллеры** (`backend/src/main/java/com/magistr/app/controller/`) → обновить `docs/API.md`
|
||||
- **Модели или миграции** (`model/`, `db/migration/`) → обновить `docs/DATABASE.md`
|
||||
- **Конфигурация тенантов** (`config/tenant/`) → обновить `docs/ARCHITECTURE.md`
|
||||
- **Бизнес-правила или валидаторы** (`utils/`) → обновить `docs/BUSINESS_LOGIC.md`
|
||||
- **Frontend** (`frontend/`) → обновить `docs/FRONTEND.md`
|
||||
- **Docker/Kubernetes** (`compose.yaml`, `Dockerfile`, `../k8s/`) → обновить `docs/INFRASTRUCTURE.md`
|
||||
- **Code style или структура пакетов** → обновить `docs/DEVELOPMENT.md`
|
||||
- **Общая структура проекта** → обновить `docs/README.md`
|
||||
|
||||
## Карта соответствия «файл → документация»
|
||||
|
||||
| Изменённый файл/директория | Файл документации |
|
||||
|----------------------------|-------------------|
|
||||
| `controller/*Controller.java` | `docs/API.md` |
|
||||
| `db/migration/V*__.sql` | `docs/DATABASE.md` |
|
||||
| `model/*.java` | `docs/DATABASE.md` |
|
||||
| `dto/*.java` | `docs/API.md` |
|
||||
| `config/tenant/*.java` | `docs/ARCHITECTURE.md` |
|
||||
| `utils/*.java` | `docs/BUSINESS_LOGIC.md` |
|
||||
| `frontend/admin/js/views/*.js` | `docs/FRONTEND.md` |
|
||||
| `frontend/admin/css/*.css` | `docs/FRONTEND.md` |
|
||||
| `compose.yaml`, `Dockerfile` | `docs/INFRASTRUCTURE.md` |
|
||||
| `application.properties` | `docs/ARCHITECTURE.md` |
|
||||
|
||||
## Пошаговая инструкция
|
||||
|
||||
### 1. Определить затронутые файлы документации
|
||||
|
||||
После выполнения задачи пользователя — проверить по таблице выше, какие файлы документации нужно обновить.
|
||||
|
||||
### 2. Прочитать текущую документацию
|
||||
|
||||
Открыть соответствующий файл из `docs/` и найти секцию, которую нужно обновить.
|
||||
|
||||
### 3. Внести точечные изменения
|
||||
|
||||
Обновить **только затронутые секции**, не переписывая весь файл. Примеры:
|
||||
|
||||
#### Новый контроллер → `docs/API.md`
|
||||
Добавить новую секцию с описанием эндпоинтов:
|
||||
- Метод + URL
|
||||
- Тело запроса (JSON пример)
|
||||
- Ответ (JSON пример)
|
||||
- Валидация
|
||||
|
||||
#### Новая миграция → `docs/DATABASE.md`
|
||||
- Добавить новую таблицу в ER-диаграмму (Mermaid)
|
||||
- Добавить описание таблицы и колонок
|
||||
- Добавить запись в таблицу «Текущие миграции»
|
||||
|
||||
#### Новый view → `docs/FRONTEND.md`
|
||||
- Добавить в дерево файлов
|
||||
- Добавить в таблицу «Разделы админ-панели»
|
||||
|
||||
### 4. Обновить AGENTS.md (при необходимости)
|
||||
|
||||
Если изменения затрагивают:
|
||||
- Структуру директорий → обновить дерево в `AGENTS.md`
|
||||
- Критические правила (Flyway, новые ограничения) → обновить секцию «Критические правила»
|
||||
|
||||
### 5. Сообщить пользователю
|
||||
|
||||
В конце ответа кратко упомянуть, какие файлы документации были обновлены:
|
||||
|
||||
> 📝 Обновлена документация: `docs/API.md` (добавлен эндпоинт `POST /api/absences`)
|
||||
|
||||
## Правила
|
||||
|
||||
1. **Язык:** Вся документация на русском языке
|
||||
2. **Формат:** Сохранять существующий стиль оформления файла (заголовки, таблицы, примеры кода)
|
||||
3. **Не удалять:** Не удалять существующие секции без явного запроса пользователя
|
||||
4. **Mermaid:** При изменении схемы БД — обязательно обновлять ER-диаграмму в `docs/DATABASE.md`
|
||||
5. **Минимальные правки:** Не переписывать весь файл ради добавления одной строки — использовать точечные изменения
|
||||
6. **Консистентность:** Если одно и то же понятие упоминается в нескольких файлах `docs/`, обновить все вхождения
|
||||
29
.agents/skills/SKILL.md
Normal file
29
.agents/skills/SKILL.md
Normal file
@@ -0,0 +1,29 @@
|
||||
Контекст проекта:
|
||||
|
||||
Backend: Java 17, Spring Framework. Учитывай возможности этой версии языка и стандарты фреймворка.
|
||||
|
||||
Frontend: HTML, CSS, JavaScript.
|
||||
|
||||
Правила написания кода и комментариев:
|
||||
|
||||
Проверка: Перед написанием кода изучи проект, его структуру и используемые технологии. Не предлагай решения, которые не соответствуют текущей архитектуре или стеку.
|
||||
|
||||
Язык: Все комментарии и объяснения должны быть строго на русском языке.
|
||||
|
||||
Комментирование кода: Оставляй комментарии, объясняющие, за что отвечает та или иная часть кода. Перед крупными или смысловыми блоками обязательно ставь поясняющие метки (например: ``, /* таблица subjects */, // логика обработки subjects).
|
||||
|
||||
Обоснование решений: При написании нового кода кратко и максимально понятно объясняй, почему мы используем именно это решение, а не другое.
|
||||
|
||||
Современные подходы: На фронтенде используй самые современные и актуальные подходы (например, Flexbox, CSS Grid, семантические теги).
|
||||
|
||||
Правила работы с ошибками (Обучающий режим):
|
||||
|
||||
Если ты находишь ошибку в моем коде, не пиши сразу готовый исправленный код.
|
||||
|
||||
Дай мне точную подсказку, чтобы я мог сам найти и исправить баг (например: "У тебя не закрыт тег в 15 строке", "Ты забыл поставить аннотацию в контроллере Spring" или "Проверь отступы в таком-то классе"). Моя цель — научиться.
|
||||
|
||||
Правила работы с дизайном (UI/UX):
|
||||
|
||||
Перед добавлением новых стилей всегда сначала изучай, какие стили уже используются в проекте, чтобы сохранять единообразие.
|
||||
|
||||
Если ты видишь, что текущий дизайн откровенно плох, нелогичен или устарел — смело предлагай свои идеи по улучшению (цветовая палитра, отступы, шрифты). Я открыт к предложениям по улучшению визуала.
|
||||
177
.agents/skills/frontend-design/LICENSE.txt
Normal file
177
.agents/skills/frontend-design/LICENSE.txt
Normal file
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
42
.agents/skills/frontend-design/SKILL.md
Normal file
42
.agents/skills/frontend-design/SKILL.md
Normal file
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Создание выразительных, готовых к продакшену frontend-интерфейсов с высоким качеством дизайна. Используйте этот навык, когда пользователь просит разработать веб-компоненты, страницы, артефакты, постеры или приложения (например, сайты, лендинги, дашборды, React-компоненты, HTML/CSS верстку или когда нужно стилизовать/улучшить любой веб-интерфейс). Генерирует креативный, отточенный код и UI-дизайн, избегая шаблонной эстетики ИИ.
|
||||
license: Полные условия в LICENSE.txt
|
||||
---
|
||||
|
||||
Этот навык направляет создание выразительных, готовых к продакшену frontend-интерфейсов, которые избегают шаблонной "ИИ-эстетики". Создавайте реально работающий код с исключительным вниманием к эстетическим деталям и творческим решениям.
|
||||
|
||||
Пользователь предоставляет требования к фронтенду: компонент, страницу, приложение или интерфейс для разработки. Требования могут включать контекст о цели, аудитории или технических ограничениях.
|
||||
|
||||
## Дизайн-мышление
|
||||
|
||||
Перед написанием кода поймите контекст и примите СМЕЛОЕ эстетическое направление:
|
||||
- **Цель**: Какую проблему решает этот интерфейс? Кто им пользуется?
|
||||
- **Тон**: Выберите крайность: брутальный минимализм, максималистский хаос, ретро-футуризм, органический/природный, люксовый/утонченный, игривый/игрушечный, редакционный/журнальный, брутализм/грубый, арт-деко/геометрический, мягкий/пастельный, индустриальный/утилитарный и т.д. Вариантов очень много. Используйте их для вдохновения, но создайте дизайн, верный выбранному эстетическому направлению.
|
||||
- **Ограничения**: Технические требования (фреймворк, производительность, доступность).
|
||||
- **Отличительная черта**: Что делает это НЕЗАБЫВАЕМЫМ? Какую единственную вещь кто-то запомнит?
|
||||
|
||||
**КРИТИЧЕСКИ ВАЖНО**: Выберите четкое концептуальное направление и выполните его с точностью. Смелый максимализм и утонченный минимализм — оба работают, ключ кроется в осознанности намерений, а не в интенсивности.
|
||||
|
||||
Затем реализуйте рабочий код (HTML/CSS/JS, React, Vue и т.д.), который:
|
||||
- Готов к продакшену и функционален
|
||||
- Визуально поразителен и легко запоминается
|
||||
- Согласован с четкой эстетической точкой зрения
|
||||
- Тщательно проработан в каждой детали
|
||||
|
||||
## Руководство по эстетике фронтенда
|
||||
|
||||
Сфокусируйтесь на:
|
||||
- **Типографика**: Выбирайте шрифты, которые красивы, уникальны и интересны. Избегайте общих шрифтов, таких как Arial и Inter; вместо этого делайте выбор в пользу выразительных, неожиданных и характерных вариантов, которые повышают уровень эстетики фронтенда. Сочетайте акцидентный шрифт (display) с утонченным текстовым (body).
|
||||
- **Цвет и тема**: Придерживайтесь согласованной эстетики. Используйте CSS-переменные для консистентности. Доминирующие цвета с резкими акцентами работают намного лучше, чем робкие, равномерно распределенные палитры.
|
||||
- **Анимация (Motion)**: Используйте анимации для эффектов и микро-взаимодействий. Отдавайте предпочтение CSS-решениям для HTML. Используйте библиотеки анимаций для React, если они доступны. Фокусируйтесь на моментах с высоким влиянием: одна хорошо срежиссированная загрузка страницы с каскадным появлением элементов (animation-delay) создает больше восторга, чем множество разрозненных микро-взаимодействий. Используйте триггеры при скролле (scroll-triggering) и состояния наведения (hover), которые удивляют.
|
||||
- **Пространственная композиция**: Неожиданные макеты. Асимметрия. Перекрытие. Диагональное направление. Элементы, ломающие сетку. Обильное негативное пространство ИЛИ контролируемая плотность элементов.
|
||||
- **Фоны и визуальные детали**: Создавайте атмосферу и глубину вместо использования скучных сплошных цветов по умолчанию. Добавляйте контекстуальные эффекты и текстуры, соответствующие общей эстетике. Применяйте творческие формы: градиентные сетки, шумовые текстуры, геометрические паттерны, слоистые прозрачности, драматичные тени, декоративные рамки, кастомные курсоры и эффекты зернистости (grain).
|
||||
|
||||
НИКОГДА не используйте шаблонную сгенерированную ИИ эстетику: заезженные семейства шрифтов (Inter, Roboto, Arial, системные шрифты), клишированные цветовые схемы (особенно фиолетовые градиенты на белом фоне), предсказуемые макеты и паттерны компонентов, а также типовой скучный дизайн без характера, не учитывающий контекст.
|
||||
|
||||
Интерпретируйте творчески и делайте неожиданные выборы, которые кажутся действительно разработанными под данный контекст. Ни один дизайн не должен быть шаблонным ("под копирку"). Варьируйте между светлыми и темными темами, разными шрифтами, различной эстетикой. НИКОГДА не сходитесь к общим выборам (например, Space Grotesk) в разных генерациях кода.
|
||||
|
||||
**ВАЖНО**: Сопоставляйте сложность реализации с эстетическим видением. Максималистские дизайны требуют сложного кода с масштабными анимациями и эффектами. Минималистские или утонченные дизайны требуют сдержанности, точности и крайне внимательного отношения к отступам, типографике и тонким деталям. Элегантность исходит из хорошего воплощения видения.
|
||||
|
||||
Помните: ИИ способен на выдающуюся творческую работу. Не сдерживайтесь, покажите, что можно создать на самом деле, когда вы мыслите нестандартно и полностью привержены особому видению.
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -7,8 +7,7 @@ backend/build/
|
||||
frontend/node_modules/
|
||||
frontend/dist/
|
||||
|
||||
.agents
|
||||
.idea/
|
||||
.vscode/
|
||||
*.DS_Store
|
||||
GEMINI.md
|
||||
skills-lock.json
|
||||
170
AGENTS.md
170
AGENTS.md
@@ -28,174 +28,60 @@ 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, адаптивность |
|
||||
| [`docs/LOGGING.md`](docs/LOGGING.md) | Логирование: SLF4J + Logback, MDC, OpenTelemetry → SigNoz |
|
||||
|
||||
420
docs/API.md
Normal file
420
docs/API.md
Normal file
@@ -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 <token>`
|
||||
|
||||
---
|
||||
|
||||
## Пользователи
|
||||
|
||||
### `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` | Внутренняя ошибка сервера |
|
||||
142
docs/ARCHITECTURE.md
Normal file
142
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,142 @@
|
||||
# 🏗 Архитектура системы
|
||||
|
||||
## Общая схема
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Client["🌐 Браузер"] -->|HTTPS| Caddy["Caddy Proxy"]
|
||||
Caddy -->|:80| Frontend["Frontend<br/>(Apache httpd:alpine)"]
|
||||
Caddy -->|/api/*| Backend["Backend<br/>(Spring Boot 3.2.5)"]
|
||||
|
||||
Backend --> TenantRouter{"TenantRoutingDataSource"}
|
||||
TenantRouter -->|swsu.zuev.company| DB1["PostgreSQL<br/>swsu_db"]
|
||||
TenantRouter -->|mgu.zuev.company| DB2["PostgreSQL<br/>mgu_db"]
|
||||
TenantRouter -->|...| DBn["PostgreSQL<br/>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<br/>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` на клиенте
|
||||
149
docs/BUSINESS_LOGIC.md
Normal file
149
docs/BUSINESS_LOGIC.md
Normal file
@@ -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: предложение замены преподавателя или переноса занятия
|
||||
362
docs/DATABASE.md
Normal file
362
docs/DATABASE.md
Normal file
@@ -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 # Пересоздаёт БД с нуля
|
||||
```
|
||||
275
docs/DEVELOPMENT.md
Normal file
275
docs/DEVELOPMENT.md
Normal file
@@ -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<Absence, Long> {
|
||||
List<Absence> 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<Absence> 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
|
||||
```
|
||||
203
docs/FRONTEND.md
Normal file
203
docs/FRONTEND.md
Normal file
@@ -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
|
||||
<a href="#" class="nav-item" data-tab="users">Пользователи</a>
|
||||
<a href="#" class="nav-item" data-tab="groups">Группы</a>
|
||||
<a href="#" class="nav-item" data-tab="schedule">Расписание занятий</a>
|
||||
```
|
||||
|
||||
При клике на пункт меню `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
|
||||
- Таблицы получают горизонтальный скролл
|
||||
137
docs/INFRASTRUCTURE.md
Normal file
137
docs/INFRASTRUCTURE.md
Normal file
@@ -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)
|
||||
167
docs/LOGGING.md
Normal file
167
docs/LOGGING.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# 📋 Логирование
|
||||
|
||||
## Стек технологий
|
||||
|
||||
| Компонент | Технология |
|
||||
|-----------|------------|
|
||||
| Фасад | SLF4J (`org.slf4j.Logger`) |
|
||||
| Реализация | Logback (поставляется с `spring-boot-starter-web`) |
|
||||
| Конфигурация | Стандартная Spring Boot (без кастомного `logback.xml`) |
|
||||
| Экспорт (прод) | OpenTelemetry Java Agent → OTLP → SigNoz |
|
||||
| Контекст тенанта | SLF4J MDC (`tenant.id`) |
|
||||
|
||||
---
|
||||
|
||||
## Архитектура
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Code["Java-код<br/>log.info(...)"] --> SLF4J["SLF4J API"]
|
||||
SLF4J --> Logback["Logback"]
|
||||
Logback -->|"Локальная разработка"| Console["stdout / stderr"]
|
||||
Logback -->|"Продакшн"| OTelAgent["OTel Java Agent<br/>(Logback Appender)"]
|
||||
OTelAgent -->|"OTLP HTTP"| SigNoz["SigNoz"]
|
||||
```
|
||||
|
||||
### Локальная разработка
|
||||
|
||||
Логи выводятся в `stdout` контейнера в стандартном формате Spring Boot:
|
||||
|
||||
```
|
||||
2026-03-22 12:00:00.123 INFO 1 --- [main] c.m.app.config.DataInitializer : Initializing databases for 1 tenant(s)...
|
||||
```
|
||||
|
||||
Просмотр логов:
|
||||
|
||||
```bash
|
||||
docker compose logs -f backend
|
||||
```
|
||||
|
||||
### Продакшн (Kubernetes)
|
||||
|
||||
OpenTelemetry Java Agent подключается как `-javaagent` в [Dockerfile](file:///mnt/HDD/magistr/magistr/backend/Dockerfile) и автоматически перехватывает логи Logback, экспортируя их в SigNoz по OTLP.
|
||||
|
||||
```dockerfile
|
||||
ENTRYPOINT ["java", "-javaagent:opentelemetry-javaagent.jar", "-jar", "app.jar"]
|
||||
```
|
||||
|
||||
Конфигурация агента задаётся через переменные окружения в [backend.yaml](file:///mnt/HDD/magistr/k8s/backend.yaml):
|
||||
|
||||
| Переменная | Значение | Назначение |
|
||||
|------------|----------|------------|
|
||||
| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://192.168.1.100:4318` | Адрес SigNoz Collector |
|
||||
| `OTEL_SERVICE_NAME` | `magistr-backend` | Имя сервиса в SigNoz |
|
||||
| `OTEL_RESOURCE_ATTRIBUTES` | `deployment.environment=default` | Окружение |
|
||||
| `OTEL_LOGS_EXPORTER` | `otlp` | Экспорт логов через OTLP |
|
||||
| `OTEL_METRICS_EXPORTER` | `otlp` | Экспорт метрик через OTLP |
|
||||
| `OTEL_TRACES_EXPORTER` | `otlp` | Экспорт трейсов через OTLP |
|
||||
| `OTEL_INSTRUMENTATION_LOGBACK_APPENDER_EXPERIMENTAL_CAPTURE_MDC_ATTRIBUTES` | `tenant.id` | Захват MDC-атрибута в логи |
|
||||
|
||||
> [!NOTE]
|
||||
> В локальной разработке OpenTelemetry Agent также встроен в Docker-образ, но без переменных `OTEL_*` он работает в режиме noop — логи идут только в stdout.
|
||||
|
||||
---
|
||||
|
||||
## Мультитенантный контекст (MDC)
|
||||
|
||||
Каждый HTTP-запрос обогащается tenant ID через [TenantInterceptor](file:///mnt/HDD/magistr/magistr/backend/src/main/java/com/magistr/app/config/tenant/TenantInterceptor.java):
|
||||
|
||||
```java
|
||||
// preHandle — при входе запроса
|
||||
MDC.put("tenant.id", tenant);
|
||||
Span.current().setAttribute("tenant.id", tenant);
|
||||
|
||||
// afterCompletion — после завершения
|
||||
MDC.remove("tenant.id");
|
||||
```
|
||||
|
||||
Это позволяет:
|
||||
- Фильтровать логи по тенанту в SigNoz
|
||||
- Коррелировать логи с трейсами через Span-атрибуты
|
||||
- Идентифицировать, к какому университету относится каждая запись
|
||||
|
||||
---
|
||||
|
||||
## Использование в коде
|
||||
|
||||
### Классы с логированием
|
||||
|
||||
| Класс | Уровни | Что логируется |
|
||||
|-------|--------|----------------|
|
||||
| `TenantInterceptor` | DEBUG, WARN | Резолвинг тенанта, неизвестный тенант (404) |
|
||||
| `TenantDataSourceConfig` | INFO, WARN, ERROR | Загрузка тенантов, fallback на H2 |
|
||||
| `TenantRoutingDataSource` | INFO, WARN | Добавление/удаление тенантов, тест соединения |
|
||||
| `TenantConfigWatcher` | INFO, ERROR, WARN | Изменения ConfigMap, Flyway миграции |
|
||||
| `ConfigMapUpdater` | INFO, WARN, ERROR | Обновление ConfigMap в K8s |
|
||||
| `DataInitializer` | INFO | Инициализация БД при старте |
|
||||
| `LessonsController` | INFO, DEBUG, ERROR | CRUD-операции с занятиями, валидация |
|
||||
|
||||
### Паттерн использования
|
||||
|
||||
```java
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public class MyClass {
|
||||
private static final Logger log = LoggerFactory.getLogger(MyClass.class);
|
||||
|
||||
public void doSomething() {
|
||||
log.info("Операция выполнена: param={}", value);
|
||||
log.error("Ошибка: {}", e.getMessage(), e); // со стектрейсом
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Рекомендации по уровням
|
||||
|
||||
| Уровень | Когда использовать |
|
||||
|---------|-------------------|
|
||||
| `ERROR` | Необработанные ошибки, сбои подключения к БД, провалы миграций |
|
||||
| `WARN` | Неизвестный тенант, нет конфигурации, fallback-сценарии |
|
||||
| `INFO` | Успешные операции, CRUD-действия, старт/стоп компонентов |
|
||||
| `DEBUG` | Детали резолвинга тенанта, ping-запросы |
|
||||
|
||||
---
|
||||
|
||||
## Настройка уровня логирования
|
||||
|
||||
В [application.properties](file:///mnt/HDD/magistr/magistr/backend/src/main/resources/application.properties) (по умолчанию закомментировано):
|
||||
|
||||
```properties
|
||||
# Включить DEBUG для всего приложения
|
||||
#logging.level.root=DEBUG
|
||||
|
||||
# Только для пакета приложения
|
||||
logging.level.com.magistr.app=DEBUG
|
||||
|
||||
# Только для конкретного класса
|
||||
logging.level.com.magistr.app.config.tenant.TenantInterceptor=DEBUG
|
||||
```
|
||||
|
||||
Также можно задавать через переменные окружения:
|
||||
|
||||
```bash
|
||||
LOGGING_LEVEL_ROOT=DEBUG
|
||||
LOGGING_LEVEL_COM_MAGISTR_APP=DEBUG
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Просмотр логов
|
||||
|
||||
### Локально (Docker Compose)
|
||||
|
||||
```bash
|
||||
# Все логи backend
|
||||
docker compose logs -f backend
|
||||
|
||||
# Фильтрация по ключевому слову
|
||||
docker compose logs -f backend | grep "tenant"
|
||||
```
|
||||
|
||||
### Продакшн (SigNoz)
|
||||
|
||||
Логи доступны в веб-интерфейсе SigNoz → раздел **Logs**:
|
||||
- Фильтрация по `service.name = magistr-backend`
|
||||
- Фильтрация по `tenant.id` (из MDC)
|
||||
- Корреляция с трейсами через общий `trace_id`
|
||||
113
docs/README.md
Normal file
113
docs/README.md
Normal file
@@ -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 <repo-url> 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) | Архитектура фронтенда, модули, стили |
|
||||
11
frontend/admin/css/departments-data.css
Normal file
11
frontend/admin/css/departments-data.css
Normal file
@@ -0,0 +1,11 @@
|
||||
/* Стили для формы создания кафедр и специальностей */
|
||||
.departments-data-icon {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
#departments-tbody .loading-row,
|
||||
#specialties-tbody .loading-row {
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
padding: 2rem;
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
<link rel="stylesheet" href="css/components.css">
|
||||
<link rel="stylesheet" href="css/modals.css">
|
||||
<link rel="stylesheet" href="css/department.css">
|
||||
<link rel="stylesheet" href="css/departments-data.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -58,6 +59,13 @@
|
||||
</svg>
|
||||
Кафедра
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-tab="departments-data">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"></path>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"></rect>
|
||||
</svg>
|
||||
Создание кафедры/специальности
|
||||
</a>
|
||||
<a href="#" class="nav-item" data-tab="groups">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import './otel.js';
|
||||
|
||||
import { isAuthenticatedAsAdmin } from './api.js';
|
||||
import { applyRippleEffect, closeAllDropdownsOnOutsideClick } from './utils.js';
|
||||
|
||||
@@ -10,6 +12,7 @@ import { initSubjects } from './views/subjects.js';
|
||||
import {initSchedule} from "./views/schedule.js";
|
||||
import {initDatabase} from "./views/database.js";
|
||||
import {initDepartment} from "./views/department.js";
|
||||
import {initDepartmentsData} from "./views/departments-data.js";
|
||||
|
||||
// Configuration
|
||||
const ROUTES = {
|
||||
@@ -22,6 +25,7 @@ const ROUTES = {
|
||||
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
|
||||
database: { title: 'База данных', file: 'views/database.html', init: initDatabase },
|
||||
department: { title: 'Кафедры', file: 'views/department.html', init: initDepartment },
|
||||
'departments-data': { title: 'Создание кафедры/специальности', file: 'views/departments-data.html', init: initDepartmentsData },
|
||||
};
|
||||
|
||||
let currentTab = null;
|
||||
|
||||
47
frontend/admin/js/otel.js
Normal file
47
frontend/admin/js/otel.js
Normal file
@@ -0,0 +1,47 @@
|
||||
import { WebTracerProvider } from 'https://esm.sh/@opentelemetry/sdk-trace-web@1.22.0';
|
||||
import { getWebAutoInstrumentations } from 'https://esm.sh/@opentelemetry/auto-instrumentations-web@0.37.0';
|
||||
import { OTLPTraceExporter } from 'https://esm.sh/@opentelemetry/exporter-trace-otlp-http@0.49.1';
|
||||
import { BatchSpanProcessor } from 'https://esm.sh/@opentelemetry/sdk-trace-base@1.22.0';
|
||||
import { registerInstrumentations } from 'https://esm.sh/@opentelemetry/instrumentation@0.49.1';
|
||||
import { ZoneContextManager } from 'https://esm.sh/@opentelemetry/context-zone@1.22.0';
|
||||
import { Resource } from 'https://esm.sh/@opentelemetry/resources@1.22.0';
|
||||
import { SemanticResourceAttributes } from 'https://esm.sh/@opentelemetry/semantic-conventions@1.22.0';
|
||||
|
||||
// Инициализация провайдера метрик и трейсов с именем сервиса
|
||||
const provider = new WebTracerProvider({
|
||||
resource: new Resource({
|
||||
[SemanticResourceAttributes.SERVICE_NAME]: 'magistr-frontend-admin',
|
||||
}),
|
||||
});
|
||||
|
||||
// Экспортер отправляет данные на относительный путь /otel/v1/traces.
|
||||
// На проде Caddy перехватит этот запрос и проксирует в SigNoz OTLP Collector (порт 4318).
|
||||
const traceExporter = new OTLPTraceExporter({
|
||||
url: window.location.origin + '/otel/v1/traces',
|
||||
});
|
||||
|
||||
// Использование BatchSpanProcessor для буферизации трейсов перед отправкой
|
||||
provider.addSpanProcessor(new BatchSpanProcessor(traceExporter));
|
||||
|
||||
// Использование ZoneContextManager для поддержки асинхронных операций (Promise, setTimeout, etc)
|
||||
provider.register({
|
||||
contextManager: new ZoneContextManager(),
|
||||
});
|
||||
|
||||
// Регистрация авто-инструментаций для бразуера (document-load, xml-http-request, fetch, history, etc)
|
||||
registerInstrumentations({
|
||||
instrumentations: [
|
||||
getWebAutoInstrumentations({
|
||||
'@opentelemetry/instrumentation-fetch': {
|
||||
propagateTraceHeaderCorsUrls: /.*/,
|
||||
clearTimingResources: true,
|
||||
},
|
||||
'@opentelemetry/instrumentation-xml-http-request': {
|
||||
propagateTraceHeaderCorsUrls: /.*/,
|
||||
clearTimingResources: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
console.log('OpenTelemetry Web SDK initialized successfully.');
|
||||
103
frontend/admin/js/views/departments-data.js
Normal file
103
frontend/admin/js/views/departments-data.js
Normal file
@@ -0,0 +1,103 @@
|
||||
import { api } from '../api.js';
|
||||
import { escapeHtml, showAlert, hideAlert } from '../utils.js';
|
||||
|
||||
export async function initDepartmentsData() {
|
||||
const deptTbody = document.getElementById('departments-tbody');
|
||||
const specTbody = document.getElementById('specialties-tbody');
|
||||
|
||||
const createDeptForm = document.getElementById('create-department-form');
|
||||
const createSpecForm = document.getElementById('create-specialty-form');
|
||||
|
||||
let departments = [];
|
||||
let specialties = [];
|
||||
|
||||
async function loadData() {
|
||||
// Load Departments
|
||||
try {
|
||||
departments = await api.get('/api/departments');
|
||||
renderDepartments();
|
||||
} catch (e) {
|
||||
deptTbody.innerHTML = '<tr><td colspan="3" class="loading-row">-</td></tr>';
|
||||
}
|
||||
|
||||
// Load Specialties
|
||||
try {
|
||||
specialties = await api.get('/api/specialties');
|
||||
renderSpecialties();
|
||||
} catch (e) {
|
||||
specTbody.innerHTML = '<tr><td colspan="3" class="loading-row">-</td></tr>';
|
||||
}
|
||||
}
|
||||
|
||||
function renderDepartments() {
|
||||
if (!departments || !departments.length) {
|
||||
deptTbody.innerHTML = '<tr><td colspan="3" class="loading-row">-</td></tr>';
|
||||
return;
|
||||
}
|
||||
deptTbody.innerHTML = departments.map(d => `
|
||||
<tr>
|
||||
<td>${d.id}</td>
|
||||
<td>${escapeHtml(d.departmentName || d.name)}</td>
|
||||
<td>${escapeHtml(String(d.departmentCode || d.code))}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function renderSpecialties() {
|
||||
if (!specialties || !specialties.length) {
|
||||
specTbody.innerHTML = '<tr><td colspan="3" class="loading-row">-</td></tr>';
|
||||
return;
|
||||
}
|
||||
specTbody.innerHTML = specialties.map(s => `
|
||||
<tr>
|
||||
<td>${s.id}</td>
|
||||
<td>${escapeHtml(s.specialityName || s.name)}</td>
|
||||
<td>${escapeHtml(s.specialityCode || s.specialtyCode || s.specialty_code)}</td>
|
||||
</tr>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
createDeptForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-dept-alert');
|
||||
const name = document.getElementById('dept-name').value.trim();
|
||||
const code = document.getElementById('dept-code').value.trim();
|
||||
|
||||
if (!name || !code) {
|
||||
showAlert('create-dept-alert', 'Заполните все поля', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.post('/api/departments', { departmentName: name, departmentCode: Number(code) });
|
||||
showAlert('create-dept-alert', `Кафедра "${name}" создана`, 'success');
|
||||
createDeptForm.reset();
|
||||
loadData();
|
||||
} catch (error) {
|
||||
showAlert('create-dept-alert', error.message || 'Ошибка создания кафедры', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
createSpecForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
hideAlert('create-spec-alert');
|
||||
const name = document.getElementById('spec-name').value.trim();
|
||||
const code = document.getElementById('spec-code').value.trim();
|
||||
|
||||
if (!name || !code) {
|
||||
showAlert('create-spec-alert', 'Заполните все поля', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.post('/api/specialties', { specialityName: name, specialityCode: code });
|
||||
showAlert('create-spec-alert', `Специальность "${name}" создана`, 'success');
|
||||
createSpecForm.reset();
|
||||
loadData();
|
||||
} catch (error) {
|
||||
showAlert('create-spec-alert', error.message || 'Ошибка создания специальности', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
loadData();
|
||||
}
|
||||
80
frontend/admin/views/departments-data.html
Normal file
80
frontend/admin/views/departments-data.html
Normal file
@@ -0,0 +1,80 @@
|
||||
<!-- ===== Departments and Specialties Tab ===== -->
|
||||
<div class="card create-card">
|
||||
<h2>Создание кафедры</h2>
|
||||
<form id="create-department-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="dept-name">Название кафедры</label>
|
||||
<input type="text" id="dept-name" placeholder="Например: Кафедра ИБ" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="dept-code">Код кафедры</label>
|
||||
<input type="number" id="dept-code" placeholder="Например: 1" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-dept-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Кафедры</h2>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="departments-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Код</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="departments-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card create-card">
|
||||
<h2>Создание специальности</h2>
|
||||
<form id="create-specialty-form">
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label for="spec-name">Название специальности</label>
|
||||
<input type="text" id="spec-name" placeholder="Например: Программная инженерия" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="spec-code">Код специальности</label>
|
||||
<input type="text" id="spec-code" placeholder="Например: 09.03.04" required>
|
||||
</div>
|
||||
<button type="submit" class="btn-primary">Создать</button>
|
||||
</div>
|
||||
<div class="form-alert" id="create-spec-alert" role="alert"></div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header-row">
|
||||
<h2>Специальности</h2>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="specialties-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Название</th>
|
||||
<th>Код специальности</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="specialties-tbody">
|
||||
<tr>
|
||||
<td colspan="3" class="loading-row">Загрузка...</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user