This commit is contained in:
2026-05-05 13:45:45 +03:00
parent 14904918cb
commit 5245cf7b0b

View File

@@ -1,8 +1,10 @@
ТЕХНИЧЕСКОЕ ЗАДАНИЕ (ТЗ) НА ПЛАТФОРМУ EVENTHUB # ТЕХНИЧЕСКОЕ ЗАДАНИЕ (ТЗ) НА ПЛАТФОРМУ EVENTHUB
Версия: 1.4 (актуальная реализация: раздельная JWT-аутентификация, версионированное админ-API, расширенная инфраструктура, аудит администраторов, расширенная статистика дашборда, улучшенная обработка ошибок) Версия: 1.5 (актуальная реализация: выполнены задачи #12#17)
## 1. ЦЕЛИ И НАЗНАЧЕНИЕ ## 1. ЦЕЛИ И НАЗНАЧЕНИЕ
EventHub — платформа для управления событиями с поддержкой календарей, записи участников (включая специалистов), гибкого подтверждения, рейтингов, отзывов, модерации, встроенного баг-трекера и платной подписки. EventHub — платформа для управления событиями с поддержкой календарей, записи участников
(включая специалистов), гибкого подтверждения, рейтингов, отзывов, модерации,
встроенного баг-трекера и платной подписки.
Целевая аудитория: владельцы календарей (бизнес), участники (клиенты), администраторы. Целевая аудитория: владельцы календарей (бизнес), участники (клиенты), администраторы.
@@ -12,296 +14,274 @@ EventHub — платформа для управления событиями
- CRUD календаря (название, описание, теги, владелец) - CRUD календаря (название, описание, теги, владелец)
- Расшаривание по ссылке (публичная/приватная) - Расшаривание по ссылке (публичная/приватная)
- Приглашение пользователей с правами "запись" или "администрирование" - Приглашение пользователей с правами "запись" или "администрирование"
- Типы календарей: personal (бесплатный, без записи), commercial (платный, запись клиентов, специалисты) - Типы календарей: personal (бесплатный, без записи), commercial (платный, запись клиентов,
- Гибкое подтверждение заявок: auto (автоматически), manual (вручную), timeout (авто через N секунд) специалисты)
- Гибкое подтверждение заявок: auto (автоматически), manual (вручную), timeout (авто через N
секунд)
- Теги календаря, рейтинг (средняя оценка, количество голосов) - Теги календаря, рейтинг (средняя оценка, количество голосов)
**Новые поля (задача #12):**
- `short_name` — короткое уникальное имя для API и поиска
- `category` — категория (enum)
- `color` — цвет отображения
- `image_url` — изображение календаря
- `settings` — дополнительные настройки (map)
### 2.1.1. Специалисты календаря (задача #12)
Реализована отдельная таблица `calendar_specialist`, связывающая пользователя (специалиста) с
календарём. Специалист может иметь отображаемое имя (`name`) и список специализаций
(`specialization`). Статус специалиста: `active` | `inactive`.
### 2.2. События (расширенная версия с повторяющимися событиями) ### 2.2. События (расширенная версия с повторяющимися событиями)
#### 2.2.1. Типы событий и модель хранения #### 2.2.1. Типы событий и модель хранения
События делятся на два типа: События могут быть одиночными (`event_type = single`) или повторяющимися (`event_type = recurring`).
- single — одиночное событие, имеет конкретные дату и время начала, длительность. Для повторяющихся событий:
- recurring — повторяющееся событие (серия), определяется мастер-записью и правилом повторения. - Мастер-событие содержит правило повторения (`recurrence_rule`) и является шаблоном.
- При создании повторяющегося события генерируются экземпляры (instances) на определённый
Для эффективного хранения и быстрого поиска применяется гибридная модель с отложенной материализацией вхождений: период (например, на месяц вперёд), которые хранятся как отдельные записи с полем
- Каждая повторяющаяся серия представлена одной мастер-записью в таблице events с полем event_type = recurring и полем recurrence_rule, содержащим правило повторения в формате, совместимом с iCalendar RRULE (или упрощённый JSON-представление: freq, interval, byday, until и т.д.). `is_instance = true` и ссылкой на мастер (`master_id`).
- Конкретные вхождения (экземпляры) серии материализуются в таблице events только при необходимости: - При изменении мастера можно выбрать обновление всех будущих экземпляров или только мастер-записи.
* когда на данное вхождение записался хотя бы один участник; - При удалении мастера удаляются все связанные экземпляры.
* когда администратор вручную редактирует это вхождение (перенос времени, отмена, смена специалиста); - Для поддержки исключений (отмена отдельного вхождения) используется таблица
* (опционально) при наступлении даты вхождения для архивных целей. `recurrence_exception`.
- Материализованное вхождение имеет:
* event_type = single (или специальный флаг is_instance = true),
* ссылку master_id на родительскую мастер-запись,
* собственные start_time и duration, которые могут отличаться от вычисленных по правилу (например, при переносе конкретного дня).
- Отменённые вхождения из серии (исключения) хранятся в отдельной таблице recurrence_exceptions с указанием master_id, original_start_time и action = cancel | reschedule.
#### 2.2.2. Правила генерации вхождений при поиске #### 2.2.2. Правила генерации вхождений при поиске
При поиске событий на заданный интервал дат (например, /v1/events?from=...&to=...) система: При поиске событий на заданный диапазон дат система должна:
1. Выбирает все одиночные события (event_type = single), попадающие в интервал. - Включать все одиночные события, попадающие в диапазон.
2. Выбирает все активные мастер-записи (event_type = recurring), у которых: - Для повторяющихся событий генерировать виртуальные вхождения на основе `recurrence_rule`,
- дата первого вхождения ≤ конец интервала, исключая те, что помечены как исключения.
- правило повторения допускает вхождения внутри интервала (учёт until или ограничения по количеству повторений). - Возвращать как одиночные, так и сгенерированные вхождения в едином списке.
3. Для каждой мастер-записи генерирует список вхождений в пределах запрошенного интервала (используя встроенную функцию разбора RRULE или собственный алгоритм).
4. Из сгенерированного списка исключаются вхождения, присутствующие в таблице recurrence_exceptions с действием cancel.
5. Если для какого-либо вхождения уже существует материализованная запись в таблице events (по совпадению master_id и start_time), то используются данные материализованного экземпляра (специалист, длительность, статус) вместо вычисленных по шаблону.
6. Итоговый список сортируется по времени начала и возвращается с пагинацией.
Ограничение: глубина генерации вхождений для одного запроса ограничена 1000 элементов; для длительных серий применяется пагинация по датам.
#### 2.2.3. Материализация при записи участника #### 2.2.3. Материализация при записи участника
При попытке записаться на вхождение повторяющегося события (POST /v1/events/:event_id/join): При записи участника на конкретное вхождение повторяющегося события:
- Если event_id ссылается на мастер-запись (event_type = recurring), то в теле запроса обязательно передаётся occurrence_start — конкретное время вхождения, на которое производится запись. - Система материализует (создаёт) физическую запись события для этого вхождения, если оно ещё
- Система в одной транзакции Mnesia: не было материализовано (например, для хранения количества записавшихся).
* Проверяет, существует ли материализованное вхождение с master_id = EventId и start_time = OccurrenceStart. - Материализованное событие имеет `is_instance = true` и ссылается на `master_id`.
* Если нет — создаёт новую запись в таблице events (тип single, master_id = EventId), копируя общие атрибуты мастер-записи (название, описание, календарь, специалист). - Запись участника (`booking`) всегда привязывается к конкретному экземпляру (материализованному
* Создаёт бронирование (booking), привязанное к идентификатору материализованного вхождения. или одиночному событию).
- Дальнейшие действия по подтверждению заявки выполняются уже с материализованным экземпляром.
#### 2.2.4. Изменение и удаление серий #### 2.2.4. Изменение и удаление серий
- При редактировании мастер-записи (изменение названия, описания, правила повторения) обновляется только сама мастер-запись. Уже существующие материализованные вхождения остаются неизменными (сохраняют старые значения атрибутов). Это поведение может быть изменено администратором через специальный флаг «применить ко всем будущим вхождениям». - При редактировании мастера можно применить изменения ко всем будущим экземплярам или создать
- При удалении мастер-записи все связанные материализованные вхождения также помечаются удалёнными (или удаляются каскадно), а бронирования на будущие вхождения аннулируются с уведомлением участников. новый мастер с отдельной серией.
- При удалении мастера удаляются все связанные экземпляры, если на них нет активных записей.
Если есть активные записи, мастер-событие помечается как `cancelled`, а существующие записи
остаются.
#### 2.2.5. Структура записей (records.hrl) #### 2.2.5. Структура записей (records.hrl)
```erlang Актуальная структура записей включает дополнительные поля, добавленные в рамках задачи #12:
-record(event, { - `event` — добавлены `attachments :: [binary()] | undefined`, `edit_history :: [map()] | undefined`
id :: binary(), - `booking` — добавлены `notes :: binary() | undefined`, `reminder_sent :: boolean()`
calendar_id :: binary(), - `review` — добавлены `likes :: non_neg_integer()`, `dislikes :: non_neg_integer()`,
title :: binary(), `edited_at :: calendar:datetime() | undefined`
description :: binary(),
event_type :: single | recurring,
start_time :: calendar:datetime(),
duration :: integer(), %% минуты
recurrence_rule :: binary() | undefined,
master_id :: binary() | undefined,
is_instance :: boolean(), %% true для материализованных вхождений
specialist_id :: binary() | undefined,
location :: #location{} | undefined,
tags :: [binary()],
status :: active | cancelled | completed,
created_at :: calendar:datetime(),
updated_at :: calendar:datetime()
}).
-record(recurrence_exception, {
master_id :: binary(),
original_start :: calendar:datetime(),
action :: cancel | reschedule,
new_start :: calendar:datetime() | undefined
}).
```
#### 2.2.6. Требования к реализации #### 2.2.6. Требования к реализации
- Разбор и генерация RRULE должны быть реализованы в отдельном модуле logic_recurrence без внешних зависимостей (чистый Erlang). - Все операции с событиями должны быть транзакционными.
- Все операции с материализацией и записью участников должны выполняться в транзакциях Mnesia для обеспечения консистентности. - Генерация вхождений должна быть эффективной (использовать `calendar:datetime_to_gregorian_seconds`
- Для ускорения поиска материализованных вхождений по мастеру и дате создаётся составной индекс в Mnesia: {master_id, start_time} (через mnesia:add_table_index/2 или хранение в ets с ключом {master_id, start_time}). и кэширование).
- При поиске событий для календаря учитывать права доступа пользователя.
### 2.3. Запись участников и подтверждение ### 2.3. Запись участников и подтверждение
- Запись через календарь - Пользователь может отправить заявку на участие в событии.
- Подтверждение согласно политике календаря - В зависимости от `confirmation` календаря заявка либо подтверждается автоматически, либо
- Уведомления участника и владельца ожидает ручного подтверждения владельцем, либо подтверждается по таймауту.
- Привязка события к специалисту (отдельная запись в календаре специалиста) - Бронирование имеет статусы: `pending`, `confirmed`, `cancelled`.
- Пользователь может отменить свою запись.
- Владелец календаря может подтвердить или отклонить заявку.
- При подтверждении фиксируется время (`confirmed_at`).
- Вместимость события (`capacity`) ограничивает количество подтверждённых записей.
### 2.4. Отзывы и рейтинги ### 2.4. Отзывы и рейтинги
- Только участники событий могут оставлять отзывы - Пользователи могут оставлять отзывы (рейтинг 15 и комментарий) на события или календари.
- Оценка 1-5, текстовый комментарий - Отзыв можно редактировать (сохраняется `edited_at`).
- Отзывы на событие или на календарь - Отзывы могут быть скрыты модератором.
- Модерация отзывов (администраторы могут скрывать) - При добавлении/изменении/удалении отзыва пересчитывается средний рейтинг события
(`rating_avg`, `rating_count`).
- Реализованы лайки/дизлайки отзывов (`likes`, `dislikes`).
- Возможность пожаловаться на отзыв (создание `report`).
### 2.5. Поиск и фильтрация ### 2.5. Поиск и фильтрация
- По тексту, по тегам, по гео-позиции (радиус), по дате/времени - Полнотекстовый поиск по названиям событий, календарей, тегам.
- Фильтрация по дате, категории, местоположению, рейтингу.
- Пагинация результатов.
- Поиск должен учитывать права доступа (не показывать скрытые/заблокированные календари).
### 2.6. Расширенные возможности ### 2.6. Расширенные возможности
- Экспорт в Google Calendar (через OAuth2) - Локация события (`location` запись с адресом, широтой, долготой).
- Экспорт в Apple Calendar (ICS-файл) - Онлайн-ссылка (`online_link`).
- Геокодирование (адрес -> координаты) через внешний API (заглушка / Nominatim) - Вложения к событию (`attachments`).
- История изменений события (`edit_history`).
- Заметки пользователя к бронированию (`notes`).
- Напоминания о событии (поле `reminder_sent` в бронировании, логика отправки не реализована).
### 2.7. Модерация и безопасность ### 2.7. Модерация и безопасность
- Жалобы на календари, события, отзывы - Пользователи могут отправлять жалобы (`report`) на календари, события, отзывы.
- Автоматическая модерация по ключевым словам и по порогу жалоб - Модераторы могут просматривать жалобы и принимать меры (скрывать контент, блокировать).
- Ручная заморозка / разморозка администратором - Список запрещённых слов (`banned_word`) для фильтрации контента.
- Бан-лист слов - Аудит действий администраторов (`admin_audit`).
- **Аудит действий администраторов:** Ведётся детальный журнал действий всех администраторов (кто, что, когда, IP, причина). Модель аудита (admin_audit) включает:
- admin_id — идентификатор администратора,
- email — email администратора,
- role — роль (superadmin, moderator, support),
- action — действие (например, add_banned_word, delete_banned_word, update_role),
- entity_type — тип сущности (banned_word, admin и т.д.),
- entity_id — идентификатор сущности,
- ip — IP-адрес,
- reason — причина действия (опционально),
- timestamp — время действия.
### 2.8. Баг-трекер (автоматический) ### 2.8. Баг-трекер (автоматический)
- При ошибке создаётся тикет, группировка по хэшу ошибки - При возникновении ошибок сервер автоматически создаёт тикет (`ticket`).
- Учёт количества повторений - Тикет содержит хеш ошибки, сообщение, стектрейс, контекст, количество повторений.
- Уведомление пользователя при создании и при закрытии тикета - Администраторы могут просматривать тикеты, назначать ответственных, менять статус.
- Доступ только у администраторов
### 2.9. Платная подписка ### 2.9. Платная подписка
- Личное использование бесплатно (personal), коммерческое — платно (commercial) - Пользователи могут оформить подписку (`subscription`) с разными планами: monthly, quarterly,
- Пробный период 30 дней biannual, annual.
- Планы подписки: monthly, quarterly, biannual, annual - Статус подписки: `active`, `expired`, `cancelled`.
- Заглушка платежного шлюза (для тестирования) - Отслеживание использования пробного периода (`trial_used`).
- Автоматическое истечение подписки
- Автоматическое понижение календарей до personal при истечении
### 2.10. Административная панель ### 2.10. Административная панель
- Отдельный HTTPS-сервер (порт 8445) и отдельный WSS (порт 8446) - Управление пользователями (просмотр, блокировка, изменение ролей).
- Управление календарями, событиями, отзывами, жалобами, тикетами, бан-словами - Просмотр статистики платформы.
- Статистика, информация о нодах кластера - Управление жалобами и тикетами.
- WebSocket-уведомления администраторам - Управление подписками.
- Аудит действий администраторов.
#### 2.10.1. Ролевая модель администраторов (реализована) #### 2.10.1. Ролевая модель администраторов
Реализована трехуровневая ролевая модель: superadmin, moderator, support. Проверка ролей выполняется в каждом административном обработчике через handler_auth:authenticate/1 и вспомогательную функцию is_admin/1. - `superadmin` — полный доступ, может управлять другими администраторами.
- `admin` — управление пользователями, контентом, подписками.
- `moderator` — модерация контента (жалобы, отзывы).
- `support` — работа с тикетами.
#### 2.10.2. Эндпоинты для управления ролями и аудитом #### 2.10.2. Эндпоинты для управления ролями и аудитом
- GET /v1/admin/me — получение текущей роли и разрешений администратора. - `GET /v1/admin/me`информация о текущем администраторе.
- GET /v1/admin/admins — список всех администраторов с ролями (только для superadmin). - `GET /v1/admin/admins` — список администраторов (только для superadmin).
- PUT /v1/admin/admins/:id — изменение роли администратора (только для superadmin). - `POST /v1/admin/admins/:id` — изменение роли администратора (superadmin).
- POST /v1/admin/admins — приглашение нового администратора с назначением роли (только для superadmin). - `GET /v1/admin/audit` — просмотр аудита.
- GET /v1/admin/audit — журнал действий администраторов с фильтрацией по дате, пользователю, действию (только для superadmin).
#### 2.10.3. Статистика для дашборда с учётом ролей #### 2.10.3. Статистика для дашборда с учётом ролей
Эндпоинт: GET /v1/admin/stats. Возвращает JSON, содержимое которого фильтруется в соответствии с ролью вызывающего: - `GET /v1/admin/stats` — базовая статистика (количество пользователей, событий, бронирований).
- superadmin — системные метрики: все пользователи, события, жалобы, баги за период, графики регистраций/событий по дням, активность администраторов. - В будущем планируется расширенная аналитика на основе данных из таблицы `stats`.
- moderator — собственные обработанные жалобы/события (количество, статусы, время реакции), общая статистика по модерации.
- support — количество открытых багов и жалоб, назначенных на текущего сотрудника, персональные задачи.
**Расширенная статистика (дополнение):**
- GET /v1/admin/statistics — агрегированная статистика по администраторам: общее количество действий за период, распределение по типам действий.
- Поддержка фильтрации: admin_id, action, date_from, date_to.
### 2.11. Real-time уведомления (WebSocket) ### 2.11. Real-time уведомления (WebSocket)
- Пользователи: порт 8081, маршрут /ws, подписка на календарь, получение обновлений - Пользовательский WebSocket (порт 8081): подписка на обновления событий календаря.
- Администраторы: порт 8446, маршрут /admin/ws, подписка на глобальные уведомления (жалобы, авто-заморозки) - Административный WebSocket (порт 8446): подписка на новые жалобы и тикеты.
- Создана таблица `notification` для хранения уведомлений (тип, заголовок, тело, флаг прочитано).
Полноценная логика доставки уведомлений будет реализована позже.
### 2.12. Инфраструктура развертывания (Docker Compose) ### 2.12. Инфраструктура развертывания (Docker Compose)
- Балансировщик Traefik с поддержкой HTTPS/WSS, WAF (Coraza), Rate Limiting и Failover - Docker Swarm с тремя репликами `eventhub-node{1..3}`.
- Кластер из трех нод Erlang с автоматическим обнаружением (DNS-лукап) - Traefik в качестве reverse-прокси и балансировщика.
- Мониторинг: Prometheus (метрики), Grafana (дашборды), LogLynx (аналитика логов) - Prometheus для сбора метрик, Grafana для визуализации.
- Ротация логов (logrotate) - Observer Web для мониторинга Erlang-узлов.
- Административный SPA (EventHubFrontAdmin) как отдельный сервис - Автоматическая ротация логов.
- Сервис-заглушка (Fallback) для отказоустойчивости
## 3. НЕФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ ## 3. НЕФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ
### 3.1. Производительность и масштабирование ### 3.1. Производительность и масштабирование
- 100 000+ пользователей - Поддержка 100 000+ пользователей.
- Горизонтальное масштабирование (увеличение нод) - Горизонтальное масштабирование добавлением новых узлов.
- Mnesia с дисковыми копиями на нескольких нодах - Все персистентные таблицы хранятся в `disc_copies` на каждом узле (задача #13).
- Отдельные ноды для исторических данных (disc_only_copies) - Сессионные таблицы (`session`, `admin_session`) оставлены в `ram_copies` для скорости.
- Пагинация всех списков - Индексы созданы для часто запрашиваемых полей (calendar_id, start_time, event_type, status и др.) (задача #13).
- Автоматическое обнаружение узлов через DNS-лукап (libcluster в перспективе) - Полная репликация горячих таблиц между всеми узлами кластера (задача #14).
- Автоматическое обнаружение узлов через DNS-имя `eventhub-node` или статический список (задача #14).
- Периодическая очистка «мёртвых» узлов из схемы Mnesia (каждые 30 секунд) (задача #14).
- Архивирование исторических данных (старше 30 дней) в отдельные Mnesia-узлы с `disc_only_copies` (задача #15).
- Пагинация всех списков.
### 3.2. Надёжность ### 3.2. Надёжность
- Супервизорное дерево OTP - Супервизорное дерево OTP.
- Let it crash быстрый перезапуск процессов - Философия "let it crash" — быстрый перезапуск упавших процессов.
- Автоматическое восстановление после падения ноды (Mnesia) - Автоматическое восстановление после падения ноды (Mnesia).
- **Улучшенная обработка ошибок:** - Валидация входных данных и единый формат ошибок.
- Валидация входных данных на всех критических эндпоинтах административного API. - Механизм миграций схемы данных: миграции компилируются вместе с проектом и автоматически применяются при старте, поддерживается откат (задача #17).
- Единый формат ошибок: {"error": "код", "message": "описание"}.
### 3.3. Безопасность ### 3.3. Безопасность
- Раздельная JWT-аутентификация: - JWT-аутентификация с разделением ролей (user, admin, superadmin, moderator, support).
- Пользовательские токены: секрет JWT_SECRET, audience = <<"user">>, эндпоинт /v1/login - Проверка прав доступа к календарям и событиям.
- Административные токены: секрет ADMIN_JWT_SECRET, audience = <<"admin">>, эндпоинт /v1/admin/login - Пароли хэшируются с использованием Argon2.
- Проверка ролей: superadmin, moderator, support - Защита от несанкционированного просмотра архивных данных (только владелец календаря) (задача #15).
- HTTPS / WSS (самоподписанный сертификат для dev, реальный для prod)
- Argon2 для хеширования паролей (erlang-argon2)
- Refresh token реализован (хранение в Mnesia, сессии)
- OAuth2 для Google (опционально)
- При блокировке пользователя или отклонении сущности обязательно сохранять причину (поле reason)
- **CORS:** Все ответы API включают заголовки:
- access-control-allow-origin: *
- access-control-expose-headers: Content-Range
### 3.4. Наблюдаемость ### 3.4. Наблюдаемость
- Healthcheck эндпоинт (GET /health) - Логирование в JSON-формате.
- Логирование (JSON, ротация) - Экспорт метрик для Prometheus (HTTP-эндпоинт `/metrics`).
- Prometheus метрики (реализованы на уровне приложения через prometheus_cowboy) - Встроенный Observer Web для мониторинга Erlang-системы.
- Аудит действий администраторов (запись admin_audit) - Сбор статистики использования (события, бронирования, отзывы) через триггеры Mnesia с сохранением в таблице `stats` (задача #16).
### 3.5. CI/CD ### 3.5. CI/CD
- Drone CI (или GitLab CI / GitHub Actions) - Контейнеризация (Docker).
- EUnit + Common Test + Tsung - Makefile для автоматизации задач.
- Сборка релиза - Возможность развёртывания в Kubernetes (в будущем).
- Docker-образ
- Горячее обновление (rolling upgrade) кластера (3+ нод)
- Миграции Mnesia
## 4. СТЕК ТЕХНОЛОГИЙ (С ВЕРСИЯМИ) ## 4. СТЕК ТЕХНОЛОГИЙ (С ВЕРСИЯМИ)
- Бэкенд: Erlang/OTP 28.2 - Erlang/OTP 28
- Сборка: rebar3 3.27.0 - Mnesia (встроенная БД)
- HTTP/WebSocket: Cowboy 2.10.0 - Cowboy 2.12 (HTTP-сервер)
- JSON: jsx 3.1.0 - JWT (jose 1.11.10)
- JWT: jose 1.11.10 - Prometheus (prometheus 4.11.0, prometheus_cowboy 2.1.0)
- Хеширование паролей: erlang-argon2 1.2.0 - Docker Compose v3.8
- Тестирование: meck 0.9.2, gun 2.0.0 - Traefik v3.1
- Мониторинг: prometheus_cowboy 0.2.0 - Grafana 11.2
- Нагрузочное тестирование: Tsung 1.8.0 - Prometheus 2.55
- CI/CD: Drone 2.0+ - Observer Web
- Контейнеризация: Docker 20.10+ - Logrotate
- База данных: Mnesia (встроенная, распределённая)
## 5. ИЕРАРХИЧЕСКАЯ СТРУКТУРА КОДА ## 5. ИЕРАРХИЧЕСКАЯ СТРУКТУРА КОДА
``` ```
src/ src/
test/ ├── core/ — бизнес-логика (core_user, core_event, core_booking, core_review, ...)
├── infra/ ├── unit/ # EUnit тесты ├── handlers/ — обработчики HTTP (handler_login, handler_calendar_view, ...)
├── eventhub_auth.erl # JWT └── api/ # Common Test интеграционные тесты ├── infra/ — инфраструктура (infra_mnesia, infra_sup, cluster_discovery,
└── ... archive_manager, archive_controller, stats_collector,
├── core/ # слой доступа к данным (DAO) │ migration_engine, ...)
├── logic/ # бизнес-логика ├── archive/ — архивирование и рендеринг (archive_controller, archive_manager,
├── services/ # внешние сервисы │ calendar_html_renderer, archive_fetcher)
├── handlers/ # обработчики HTTP/WebSocket ├── migrations/ — файлы миграций
│ ├── admin/ # административные обработчики └── eventhub_app.erl — точка входа приложения
│ └── ...
└── middlewares/ # промежуточные слои (CORS, аутентификация)
``` ```
**Правила:**
- Модули handlers/ не содержат бизнес-логики.
- Модули logic/ не знают о HTTP/WS.
- Модули core/ (DAO) работают только с Mnesia.
- Модуль infra/eventhub_auth.erl реализует раздельную JWT-аутентификацию (пользователи и администраторы).
- Модули services/ могут быть заменены на реальные реализации без изменения остального кода.
- Заголовочный файл include/records.hrl содержит все записи таблиц Mnesia.
## 6. ОСНОВНЫЕ API (КРАТКО) ## 6. ОСНОВНЫЕ API (КРАТКО)
### Пользовательские (порт 8080) ### Пользовательские (порт 8080)
- **Публичные:** GET /health, POST /v1/register, POST /v1/login, POST /v1/refresh - `POST /v1/register` — регистрация.
- **Авторизованные:** GET /v1/user/me, GET /v1/user/bookings, GET /v1/user/reviews, GET /v1/search - `POST /v1/login` — вход.
- **Календари:** POST /v1/calendars + CRUD, GET /v1/calendars/:id, PUT /v1/calendars/:id, DELETE /v1/calendars/:id - `POST /v1/refresh` — обновление токена.
- **События:** POST /v1/calendars/:calendar_id/events, GET /v1/events/:id, PUT /v1/events/:id, DELETE /v1/events/:id, GET /v1/events/:id/occurrences, POST /v1/events/:id/join, POST /v1/events/:id/confirm/:user_id, GET /v1/events/:id/bookings - `GET /v1/user/me` — профиль пользователя.
- **Бронирования:** GET /v1/bookings/:id, PUT /v1/bookings/:id, DELETE /v1/bookings/:id - `GET /v1/user/bookings` — бронирования пользователя.
- **Отзывы:** POST /v1/reviews, GET /v1/reviews/:id, PUT /v1/reviews/:id, DELETE /v1/reviews/:id - `GET /v1/user/reviews` — отзывы пользователя.
- **Жалобы:** POST /v1/reports - `GET /v1/search` — поиск.
- **Тикеты:** POST /v1/tickets, GET /v1/tickets, GET /v1/tickets/:id, PUT /v1/tickets/:id, DELETE /v1/tickets/:id - `GET /v1/calendars` — список календарей.
- **Подписки:** GET /v1/subscription, POST /v1/subscription - `GET /v1/calendars/:id` — календарь.
- **Метрики:** GET /metrics/[:registry] - `GET /v1/calendars/:calendar_id/events` — события календаря.
- `GET /v1/events/:id` — событие.
WebSocket (порт 8081): ws://localhost:8081/ws?token= - `GET /v1/events/:id/occurrences` — вхождения повторяющегося события.
- `POST /v1/events/:id/bookings` — запись на событие.
- `GET /v1/bookings/:id` — статус бронирования.
- `POST /v1/reviews` — создать отзыв.
- `GET /v1/reviews` — список отзывов.
- `PUT /v1/reviews/:id` — обновить отзыв.
- `POST /v1/reports` — пожаловаться.
- `GET /v1/tickets` — тикеты пользователя.
- `POST /v1/tickets` — создать тикет.
- `GET /v1/tickets/:id` — статус тикета.
- `GET /v1/subscription` — подписка пользователя.
- `GET /v1/calendars/:calendar_id/view?month=YYYY-MM` — HTML-календарь (владелец), включая архив.
### Административные (порт 8445) ### Административные (порт 8445)
- **Версионирование:** Все административные эндпоинты имеют префикс /v1/admin/. - `GET /v1/admin/health` — состояние сервера.
- **Базовые:** GET /v1/admin/health, GET /v1/admin/stats, POST /v1/admin/login - `GET /v1/admin/stats` — статистика.
- **Пользователи:** GET /v1/admin/users, GET /v1/admin/users/:id, PUT /v1/admin/users/:id, DELETE /v1/admin/users/:id - `POST /v1/admin/login` — вход администратора.
- **Отчёты (жалобы):** GET /v1/admin/reports, GET /v1/admin/reports/:id, PUT /v1/admin/reports/:id - `GET /v1/admin/users`, `GET /v1/admin/users/:id` — пользователи.
- **Отзывы:** GET /v1/admin/reviews/:id, PUT /v1/admin/reviews/:id (hide/show) - `GET /v1/admin/reports`, `GET /v1/admin/reports/:id` — жалобы.
- **Бан-слова:** GET /v1/admin/banned-words, POST /v1/admin/banned-words, DELETE /v1/admin/banned-words/:word - `DELETE /v1/admin/reviews/:id` — удалить отзыв.
- **Тикеты:** GET /v1/admin/tickets, POST /v1/admin/tickets, GET /v1/admin/tickets/:id, PUT /v1/admin/tickets/:id, DELETE /v1/admin/tickets/:id, GET /v1/admin/tickets/stats - `GET /v1/admin/banned-words`, `POST /v1/admin/banned-words` — запрещённые слова.
- **Подписки:** GET /v1/admin/subscriptions, POST /v1/admin/subscriptions, GET /v1/admin/subscriptions/:id, PUT /v1/admin/subscriptions/:id, DELETE /v1/admin/subscriptions/:id - `GET /v1/admin/tickets/stats` — статистика тикетов.
*- *Модерация:** PUT /v1/admin/:target_type/:id (target_type: calendar, event, review, user; action: freeze/unfreeze, hide/show, block/unblock) - `GET /v1/admin/tickets`, `GET /v1/admin/tickets/:id` — управление тикетами.
- `GET /v1/admin/subscriptions`, `POST /v1/admin/subscriptions/:id` — подписки.
Административный WebSocket (порт 8446): wss://localhost:8446/admin/ws?token= - `PUT /v1/admin/:target_type/:id` — модерация.
- `GET /v1/admin/me` — профиль администратора.
- `GET /v1/admin/admins`, `POST /v1/admin/admins/:id` — управление администраторами.
- `GET /v1/admin/audit` — аудит.
## 6.1. Аутентификация и авторизация ## 6.1. Аутентификация и авторизация
Модуль infra/eventhub_auth.erl реализует раздельную JWT-аутентификацию: - Пользователи и администраторы используют разные эндпоинты и JWT-токены.
- Пользовательские токены выпускаются через POST /v1/login с audience <<"user">> и секретом JWT_SECRET. Проверяются обработчиками через handler_auth:authenticate/1 → logic_auth:verify_jwt/1. - Access-токен имеет срок жизни 1 час, refresh-токен — 30 дней.
- Административные токены выпускаются через POST /v1/admin/login с audience <<"admin">> и секретом ADMIN_JWT_SECRET. Проверяются обработчиками через handler_auth:authenticate/1 с последующей проверкой роли is_admin/1. - Все защищённые эндпоинты требуют заголовок `Authorization: Bearer <token>`.
Обработчики порта 8445 используют единый middleware handler_auth:authenticate/1, который извлекает токен из заголовка Authorization: Bearer ..., верифицирует его через logic_auth:verify_jwt/1 и возвращает {ok, UserId, Req} или {error, Code, Message, Req}.
## 7. ВЕРСИОНИРОВАНИЕ И СТАТУС ## 7. ВЕРСИОНИРОВАНИЕ И СТАТУС
Текущая версия: 1.4 (MVP, альфа). Включает: Текущая версия: 1.5 (MVP, альфа). Включает:
- Гибридную модель повторяющихся событий - Гибридную модель повторяющихся событий
- Раздельную JWT-аутентификацию (пользователи/администраторы) - Раздельную JWT-аутентификацию (пользователи/администраторы)
- Полноценную ролевую модель администрирования - Полноценную ролевую модель администрирования
@@ -311,15 +291,25 @@ WebSocket (порт 8081): ws://localhost:8081/ws?token=
- Расширенную статистику для дашборда - Расширенную статистику для дашборда
- Улучшенную обработку ошибок и валидацию - Улучшенную обработку ошибок и валидацию
**Новое в версии 1.5:**
- Расширенная структура данных (новые поля и таблицы, задача #12)
- Дисковое хранение и индексы (задача #13)
- Репликация между узлами кластера с автоочисткой (задача #14)
- Архивирование исторических данных и серверный рендеринг календаря (задача #15)
- Сбор статистики через триггеры Mnesia (задача #16)
- Механизм миграций схемы данных (задача #17)
## 8. ОГРАНИЧЕНИЯ И ДОПУЩЕНИЯ ## 8. ОГРАНИЧЕНИЯ И ДОПУЩЕНИЯ
- В разработке используются самоподписанные SSL-сертификаты. - Система не поддерживает транзакционную целостность между несколькими таблицами на уровне
- Для production требуется реальный сертификат и настройка OAuth2 для Google. приложения (полагаемся на Mnesia).
- Заглушки внешних сервисов должны быть заменены перед запуском. - В текущей версии отсутствует полноценная система уведомлений (только таблица).
- Автоматическое обнаружение узлов через libcluster находится в перспективе; текущая реализация использует статический JOIN_NODES с возможностью подключения через DNS-лукап. - Загрузка файлов (вложения) пока не реализована.
- Серверный рендеринг календаря работает только для владельца календаря.
- Автоматическое архивирование через `archive_controller` в локальном режиме использует
`slave:start`, который устарел; в production планируется `peer`.
## 9. ТРЕБОВАНИЯ К ОКРУЖЕНИЮ ## 9. ТРЕБОВАНИЯ К ОКРУЖЕНИЮ
- Erlang/OTP 28.2 - Erlang/OTP 28
- rebar3 3.27.0 - Docker Engine 27.3.1+
- openssl - Docker Compose v3.8
- Docker (опционально) - Linux (продакшен) или WSL2 (разработка)
- Drone (опционально, для CI/CD)