Compare commits
5 Commits
4915e6f33b
...
7ce0d1e501
| Author | SHA1 | Date | |
|---|---|---|---|
| 7ce0d1e501 | |||
|
|
3861fa05b5 | ||
|
|
599e284ea9 | ||
|
|
ec7e615557 | ||
|
|
9e7b35aa0b |
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/node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
|
|
||||||
.agents
|
|
||||||
.idea/
|
.idea/
|
||||||
.vscode/
|
.vscode/
|
||||||
*.DS_Store
|
*.DS_Store
|
||||||
GEMINI.md
|
skills-lock.json
|
||||||
170
AGENTS.md
170
AGENTS.md
@@ -28,174 +28,60 @@ magistr/
|
|||||||
│ ├── admin/ # Интерфейс администратора
|
│ ├── admin/ # Интерфейс администратора
|
||||||
│ ├── teacher/ # Интерфейс преподавателя
|
│ ├── teacher/ # Интерфейс преподавателя
|
||||||
│ └── student/ # Интерфейс студента
|
│ └── student/ # Интерфейс студента
|
||||||
|
├── docs/ # 📖 Документация проекта
|
||||||
├── compose.yaml # Docker Compose конфигурация
|
├── compose.yaml # Docker Compose конфигурация
|
||||||
└── .env # Переменные окружения
|
└── .env # Переменные окружения
|
||||||
```
|
```
|
||||||
|
|
||||||
**Внешние зависимости (родительская директория)**
|
**Внешние зависимости (родительская директория)**
|
||||||
|
|
||||||
На уровень выше расположен `../caddy-proxy/`. Это реверс-прокси, обрабатывающий трафик для `magistr.zuev.company`. Если возникают проблемы с доменом или внешним доступом, проверяйте `Caddyfile` там.
|
На уровень выше расположен конфиг kubernetes `../k8s/`, все файлы сборки на проде расположены там.
|
||||||
|
|
||||||
так же на уровень выше расположен конфиг kubernetes `../k8s/`, все файлы сборки на проде расположены там.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Команды сборки и запуска
|
## Быстрый справочник команд
|
||||||
|
|
||||||
### Docker Compose (основной способ)
|
|
||||||
|
|
||||||
Сборка и запуск всех сервисов (backend, frontend, PostgreSQL) выполняется через Docker Compose.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Сборка и запуск всех сервисов
|
# Сборка и запуск
|
||||||
docker compose up -d --build
|
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
|
docker compose logs -f backend
|
||||||
|
|
||||||
# Пересоздать контейнер базы данных (полный сброс данных и повтор миграций Flyway)
|
|
||||||
docker compose down -v
|
|
||||||
docker compose up -d db
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Frontend
|
Подробнее — см. [`docs/README.md`](docs/README.md) и [`docs/INFRASTRUCTURE.md`](docs/INFRASTRUCTURE.md).
|
||||||
|
|
||||||
Статические файлы обслуживаются через Apache (httpd:alpine). Изменения в файлах frontend требуют пересборки контейнера.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Соглашения о коде (Code Style)
|
## Критические правила для агентов
|
||||||
|
|
||||||
### Java (Backend)
|
### Flyway миграции
|
||||||
|
- **ЗАПРЕЩЕНО** изменять существующие файлы миграций (например, `V1__init.sql`). Это сломает контрольные суммы Flyway.
|
||||||
**Именование:**
|
- Новые миграции: `V{N}__{описание}.sql` в `backend/src/main/resources/db/migration/`
|
||||||
- Классы: PascalCase (например, `LessonsController`, `LessonResponse`)
|
- Подробнее — см. [`docs/DATABASE.md`](docs/DATABASE.md)
|
||||||
- Методы и переменные: 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)**:
|
|
||||||
- Предложение подходящей замены преподавателя на этот слот.
|
|
||||||
- Предложение переноса занятия на другое время или в другую аудиторию.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Языковые требования
|
|
||||||
|
|
||||||
|
### Языковые требования
|
||||||
- **Все ответы и комментарии на русском языке**
|
- **Все ответы и комментарии на русском языке**
|
||||||
- Сообщения об ошибках и логи на русском
|
- Сообщения об ошибках и логи на русском
|
||||||
- Пользовательский интерфейс на русском
|
- Пользовательский интерфейс на русском
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Существующие правила проекта
|
## Подробная документация
|
||||||
|
|
||||||
См. `.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/components.css">
|
||||||
<link rel="stylesheet" href="css/modals.css">
|
<link rel="stylesheet" href="css/modals.css">
|
||||||
<link rel="stylesheet" href="css/department.css">
|
<link rel="stylesheet" href="css/department.css">
|
||||||
|
<link rel="stylesheet" href="css/departments-data.css">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -58,6 +59,13 @@
|
|||||||
</svg>
|
</svg>
|
||||||
Кафедра
|
Кафедра
|
||||||
</a>
|
</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">
|
<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"
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||||
stroke-linecap="round" stroke-linejoin="round">
|
stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import './otel.js';
|
||||||
|
|
||||||
import { isAuthenticatedAsAdmin } from './api.js';
|
import { isAuthenticatedAsAdmin } from './api.js';
|
||||||
import { applyRippleEffect, closeAllDropdownsOnOutsideClick } from './utils.js';
|
import { applyRippleEffect, closeAllDropdownsOnOutsideClick } from './utils.js';
|
||||||
|
|
||||||
@@ -10,6 +12,7 @@ import { initSubjects } from './views/subjects.js';
|
|||||||
import {initSchedule} from "./views/schedule.js";
|
import {initSchedule} from "./views/schedule.js";
|
||||||
import {initDatabase} from "./views/database.js";
|
import {initDatabase} from "./views/database.js";
|
||||||
import {initDepartment} from "./views/department.js";
|
import {initDepartment} from "./views/department.js";
|
||||||
|
import {initDepartmentsData} from "./views/departments-data.js";
|
||||||
|
|
||||||
// Configuration
|
// Configuration
|
||||||
const ROUTES = {
|
const ROUTES = {
|
||||||
@@ -22,6 +25,7 @@ const ROUTES = {
|
|||||||
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
|
schedule: { title: 'Расписание занятий', file: 'views/schedule.html', init: initSchedule },
|
||||||
database: { title: 'База данных', file: 'views/database.html', init: initDatabase },
|
database: { title: 'База данных', file: 'views/database.html', init: initDatabase },
|
||||||
department: { title: 'Кафедры', file: 'views/department.html', init: initDepartment },
|
department: { title: 'Кафедры', file: 'views/department.html', init: initDepartment },
|
||||||
|
'departments-data': { title: 'Создание кафедры/специальности', file: 'views/departments-data.html', init: initDepartmentsData },
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentTab = null;
|
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