Files
EventHubSpec/EventHubBackSpec.md
2026-04-28 13:19:46 +03:00

303 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
ТЕХНИЧЕСКОЕ ЗАДАНИЕ (ТЗ) НА ПЛАТФОРМУ EVENTHUB
Версия: 1.3 (актуальная реализация: раздельная JWT-аутентификация, версионированное админ-API, расширенная инфраструктура)
## 1. ЦЕЛИ И НАЗНАЧЕНИЕ
EventHub — платформа для управления событиями с поддержкой календарей, записи участников (включая специалистов), гибкого подтверждения, рейтингов, отзывов, модерации, встроенного баг-трекера и платной подписки. Целевая аудитория: владельцы календарей (бизнес), участники (клиенты), администраторы.
## 2. ФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ
### 2.1. Календари
- CRUD календаря (название, описание, теги, владелец)
- Расшаривание по ссылке (публичная/приватная)
- Приглашение пользователей с правами "запись" или "администрирование"
- Типы календарей: personal (бесплатный, без записи), commercial (платный, запись клиентов, специалисты)
- Гибкое подтверждение заявок: auto (автоматически), manual (вручную), timeout (авто через N секунд)
- Теги календаря, рейтинг (средняя оценка, количество голосов)
### 2.2. События (расширенная версия с повторяющимися событиями)
#### 2.2.1. Типы событий и модель хранения
События делятся на два типа:
- single — одиночное событие, имеет конкретные дату и время начала, длительность.
- recurring — повторяющееся событие (серия), определяется мастер-записью и правилом повторения.
Для эффективного хранения и быстрого поиска применяется гибридная модель с отложенной материализацией вхождений:
- Каждая повторяющаяся серия представлена одной мастер-записью в таблице events с полем event_type = recurring и полем recurrence_rule, содержащим правило повторения в формате, совместимом с iCalendar RRULE (или упрощённый JSON-представление: freq, interval, byday, until и т.д.).
- Конкретные вхождения (экземпляры) серии материализуются в таблице events только при необходимости:
* когда на данное вхождение записался хотя бы один участник;
* когда администратор вручную редактирует это вхождение (перенос времени, отмена, смена специалиста);
* (опционально) при наступлении даты вхождения для архивных целей.
- Материализованное вхождение имеет:
* event_type = single (или специальный флаг is_instance = true),
* ссылку master_id на родительскую мастер-запись,
* собственные start_time и duration, которые могут отличаться от вычисленных по правилу (например, при переносе конкретного дня).
- Отменённые вхождения из серии (исключения) хранятся в отдельной таблице recurrence_exceptions с указанием master_id, original_start_time и action = cancel | reschedule.
#### 2.2.2. Правила генерации вхождений при поиске
При поиске событий на заданный интервал дат (например, /v1/events?from=...&to=...) система:
1. Выбирает все одиночные события (event_type = single), попадающие в интервал.
2. Выбирает все активные мастер-записи (event_type = recurring), у которых:
- дата первого вхождения ≤ конец интервала,
- правило повторения допускает вхождения внутри интервала (учёт until или ограничения по количеству повторений).
3. Для каждой мастер-записи генерирует список вхождений в пределах запрошенного интервала (используя встроенную функцию разбора RRULE или собственный алгоритм).
4. Из сгенерированного списка исключаются вхождения, присутствующие в таблице recurrence_exceptions с действием cancel.
5. Если для какого-либо вхождения уже существует материализованная запись в таблице events (по совпадению master_id и start_time), то используются данные материализованного экземпляра (специалист, длительность, статус) вместо вычисленных по шаблону.
6. Итоговый список сортируется по времени начала и возвращается с пагинацией.
Ограничение: глубина генерации вхождений для одного запроса ограничена 1000 элементов; для длительных серий применяется пагинация по датам.
#### 2.2.3. Материализация при записи участника
При попытке записаться на вхождение повторяющегося события (POST /v1/events/:event_id/join):
- Если event_id ссылается на мастер-запись (event_type = recurring), то в теле запроса обязательно передаётся occurrence_start — конкретное время вхождения, на которое производится запись.
- Система в одной транзакции Mnesia:
* Проверяет, существует ли материализованное вхождение с master_id = EventId и start_time = OccurrenceStart.
* Если нет — создаёт новую запись в таблице events (тип single, master_id = EventId), копируя общие атрибуты мастер-записи (название, описание, календарь, специалист).
* Создаёт бронирование (booking), привязанное к идентификатору материализованного вхождения.
- Дальнейшие действия по подтверждению заявки выполняются уже с материализованным экземпляром.
#### 2.2.4. Изменение и удаление серий
- При редактировании мастер-записи (изменение названия, описания, правила повторения) обновляется только сама мастер-запись. Уже существующие материализованные вхождения остаются неизменными (сохраняют старые значения атрибутов). Это поведение может быть изменено администратором через специальный флаг «применить ко всем будущим вхождениям».
- При удалении мастер-записи все связанные материализованные вхождения также помечаются удалёнными (или удаляются каскадно), а бронирования на будущие вхождения аннулируются с уведомлением участников.
#### 2.2.5. Структура записей (records.hrl)
```erlang
-record(event, {
id :: binary(),
calendar_id :: binary(),
title :: binary(),
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. Требования к реализации
- Разбор и генерация RRULE должны быть реализованы в отдельном модуле logic_recurrence без внешних зависимостей (чистый Erlang).
- Все операции с материализацией и записью участников должны выполняться в транзакциях Mnesia для обеспечения консистентности.
- Для ускорения поиска материализованных вхождений по мастеру и дате создаётся составной индекс в Mnesia: {master_id, start_time} (через mnesia:add_table_index/2 или хранение в ets с ключом {master_id, start_time}).
### 2.3. Запись участников и подтверждение
- Запись через календарь
- Подтверждение согласно политике календаря
- Уведомления участника и владельца
- Привязка события к специалисту (отдельная запись в календаре специалиста)
### 2.4. Отзывы и рейтинги
- Только участники событий могут оставлять отзывы
- Оценка 1-5, текстовый комментарий
- Отзывы на событие или на календарь
- Модерация отзывов (администраторы могут скрывать)
### 2.5. Поиск и фильтрация
- По тексту, по тегам, по гео-позиции (радиус), по дате/времени
### 2.6. Расширенные возможности
- Экспорт в Google Calendar (через OAuth2)
- Экспорт в Apple Calendar (ICS-файл)
- Геокодирование (адрес -> координаты) через внешний API (заглушка / Nominatim)
### 2.7. Модерация и безопасность
- Жалобы на календари, события, отзывы
- Автоматическая модерация по ключевым словам и по порогу жалоб
- Ручная заморозка / разморозка администратором
- Бан-лист слов
- Аудит действий администраторов (детальная информация: кто, что, когда, IP, причина)
### 2.8. Баг-трекер (автоматический)
- При ошибке создаётся тикет, группировка по хэшу ошибки
- Учёт количества повторений
- Уведомление пользователя при создании и при закрытии тикета
- Доступ только у администраторов
### 2.9. Платная подписка
- Личное использование бесплатно (personal), коммерческое — платно (commercial)
- Пробный период 30 дней
- Планы подписки: monthly, quarterly, biannual, annual
- Заглушка платежного шлюза (для тестирования)
- Автоматическое истечение подписки
- Автоматическое понижение календарей до personal при истечении
### 2.10. Административная панель
- Отдельный HTTPS-сервер (порт 8445) и отдельный WSS (порт 8446)
- Управление календарями, событиями, отзывами, жалобами, тикетами, бан-словами
- Статистика, информация о нодах кластера
- WebSocket-уведомления администраторам
#### 2.10.1. Ролевая модель администраторов (реализована)
В проекте реализована трехуровневая ролевая модель: `superadmin`, `moderator`, `support`. Проверка ролей выполняется в каждом административном обработчике через `handler_auth:authenticate/1` и вспомогательную функцию `is_admin/1`.
#### 2.10.2. Эндпоинты для управления ролями и аудитом
- `GET /v1/admin/me` — получение текущей роли и разрешений администратора.
- `GET /v1/admin/admins` — список всех администраторов с ролями (только для `superadmin`).
- `PUT /v1/admin/admins/:id` — изменение роли администратора (только для `superadmin`).
- `POST /v1/admin/admins` — приглашение нового администратора с назначением роли (только для `superadmin`).
- `GET /v1/admin/audit` — журнал действий администраторов с фильтрацией по дате, пользователю, действию (только для `superadmin`).
#### 2.10.3. Статистика для дашборда с учётом ролей
Эндпоинт: `GET /v1/admin/stats`. Возвращает JSON, содержимое которого фильтруется в соответствии с ролью вызывающего.
- `superadmin` — системные метрики: все пользователи, события, жалобы, баги за период, графики регистраций/событий по дням, активность администраторов.
- `moderator` — собственные обработанные жалобы/события (количество, статусы, время реакции), общая статистика по модерации.
- `support` — количество открытых багов и жалоб, назначенных на текущего сотрудника, персональные задачи.
### 2.11. Real-time уведомления (WebSocket)
- Пользователи: порт 8081, маршрут `/ws`, подписка на календарь, получение обновлений
- Администраторы: порт 8446, маршрут `/admin/ws`, подписка на глобальные уведомления (жалобы, авто-заморозки)
### 2.12. Инфраструктура развертывания (Docker Compose)
- Балансировщик Traefik с поддержкой HTTPS/WSS, WAF (Coraza), Rate Limiting и Failover
- Кластер из трех нод Erlang с автоматическим обнаружением (DNS-лукап)
- Мониторинг: Prometheus (метрики), Grafana (дашборды), LogLynx (аналитика логов)
- Ротация логов (logrotate)
- Административный SPA (EventHubFrontAdmin) как отдельный сервис
- Сервис-заглушка (Fallback) для отказоустойчивости
## 3. НЕФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ
### 3.1. Производительность и масштабирование
- 100 000+ пользователей
- Горизонтальное масштабирование (увеличение нод)
- Mnesia с дисковыми копиями на нескольких нодах
- Отдельные ноды для исторических данных (disc_only_copies)
- Пагинация всех списков
- Автоматическое обнаружение узлов через DNS-лукап (libcluster в перспективе)
### 3.2. Надёжность
- Супервизорное дерево OTP
- Let it crash быстрый перезапуск процессов
- Автоматическое восстановление после падения ноды (Mnesia)
### 3.3. Безопасность
- Раздельная JWT-аутентификация:
- Пользовательские токены: секрет `JWT_SECRET`, audience = `<<"user">>`, эндпоинт `/v1/login`
- Административные токены: секрет `ADMIN_JWT_SECRET`, audience = `<<"admin">>`, эндпоинт `/v1/admin/login`
- Проверка ролей: `superadmin`, `moderator`, `support`
- HTTPS / WSS (самоподписанный сертификат для dev, реальный для prod)
- Argon2 для хеширования паролей (erlang-argon2)
- Refresh token реализован (хранение в Mnesia, сессии)
- OAuth2 для Google (опционально)
- При блокировке пользователя или отклонении сущности обязательно сохранять причину (поле `reason`)
### 3.4. Наблюдаемость
- Healthcheck эндпоинт (`GET /health`)
- Логирование (JSON, ротация)
- Prometheus метрики (реализованы на уровне приложения через `prometheus_cowboy`)
- Аудит действий администраторов (запись `admin_audit`)
### 3.5. CI/CD
- Drone CI (или GitLab CI / GitHub Actions)
- EUnit + Common Test + Tsung
- Сборка релиза
- Docker-образ
- Горячее обновление (rolling upgrade) кластера (3+ нод)
- Миграции Mnesia
## 4. СТЕК ТЕХНОЛОГИЙ (С ВЕРСИЯМИ)
- Бэкенд: Erlang/OTP 28.2
- Сборка: rebar3 3.27.0
- HTTP/WebSocket: Cowboy 2.10.0
- JSON: jsx 3.1.0
- JWT: jose 1.11.10
- Хеширование паролей: erlang-argon2 1.2.0
- Тестирование: meck 0.9.2, gun 2.0.0
- Мониторинг: prometheus_cowboy 0.2.0
- Нагрузочное тестирование: Tsung 1.8.0
- CI/CD: Drone 2.0+
- Контейнеризация: Docker 20.10+
- База данных: Mnesia (встроенная, распределённая)
## 5. ИЕРАРХИЧЕСКАЯ СТРУКТУРА КОДА
```
src/ test/
├── infra/ ├── unit/ # EUnit тесты
│ ├── eventhub_auth.erl # JWT └── api/ # Common Test интеграционные тесты
│ └── ...
├── core/ # слой доступа к данным (DAO)
├── logic/ # бизнес-логика
├── services/ # внешние сервисы
├── handlers/ # обработчики HTTP/WebSocket
│ ├── admin/ # административные обработчики
│ └── ...
└── middlewares/ # промежуточные слои (CORS, аутентификация)
```
**Правила:**
- Модули `handlers/` не содержат бизнес-логики.
- Модули `logic/` не знают о HTTP/WS.
- Модули `core/` (DAO) работают только с Mnesia.
- Модуль `infra/eventhub_auth.erl` реализует раздельную JWT-аутентификацию (пользователи и администраторы).
- Модули `services/` могут быть заменены на реальные реализации без изменения остального кода.
- Заголовочный файл `include/records.hrl` содержит все записи таблиц Mnesia.
## 6. ОСНОВНЫЕ API (КРАТКО)
### Пользовательские (порт 8080)
**Публичные:** `GET /health`, `POST /v1/register`, `POST /v1/login`, `POST /v1/refresh`
**Авторизованные:** `GET /v1/user/me`, `GET /v1/user/bookings`, `GET /v1/user/reviews`, `GET /v1/search`
**Календари:** `POST /v1/calendars` + CRUD, `GET /v1/calendars/:id`, `PUT /v1/calendars/:id`, `DELETE /v1/calendars/:id`
**События:** `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/bookings/:id`, `PUT /v1/bookings/:id`, `DELETE /v1/bookings/:id`
**Отзывы:** `POST /v1/reviews`, `GET /v1/reviews/:id`, `PUT /v1/reviews/:id`, `DELETE /v1/reviews/:id`
**Жалобы:** `POST /v1/reports`
**Тикеты:** `POST /v1/tickets`, `GET /v1/tickets`, `GET /v1/tickets/:id`, `PUT /v1/tickets/:id`, `DELETE /v1/tickets/:id`
**Подписки:** `GET /v1/subscription`, `POST /v1/subscription`
**Метрики:** `GET /metrics/[:registry]`
WebSocket (порт 8081): `ws://localhost:8081/ws?token=<jwt>`
### Административные (порт 8445)
**Версионирование:** Все административные эндпоинты имеют префикс `/v1/admin/`.
**Базовые:** `GET /v1/admin/health`, `GET /v1/admin/stats`, `POST /v1/admin/login`
**Пользователи:** `GET /v1/admin/users`, `GET /v1/admin/users/:id`, `PUT /v1/admin/users/:id`, `DELETE /v1/admin/users/:id`
**Отчёты (жалобы):** `GET /v1/admin/reports`, `GET /v1/admin/reports/:id`, `PUT /v1/admin/reports/:id`
**Отзывы:** `GET /v1/admin/reviews/:id`, `PUT /v1/admin/reviews/:id` (hide/show)
**Бан-слова:** `GET /v1/admin/banned-words`, `POST /v1/admin/banned-words`, `DELETE /v1/admin/banned-words/:word`
**Тикеты:** `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/subscriptions`, `POST /v1/admin/subscriptions`, `GET /v1/admin/subscriptions/:id`, `PUT /v1/admin/subscriptions/:id`, `DELETE /v1/admin/subscriptions/:id`
**Модерация:** `PUT /v1/admin/:target_type/:id` (target_type: calendar, event, review, user; action: freeze/unfreeze, hide/show, block/unblock)
Административный WebSocket (порт 8446): `wss://localhost:8446/admin/ws?token=<admin_jwt>`
## 6.1. Аутентификация и авторизация
Модуль `infra/eventhub_auth.erl` реализует раздельную JWT-аутентификацию:
- Пользовательские токены выпускаются через `POST /v1/login` с audience `<<"user">>` и секретом `JWT_SECRET`. Проверяются обработчиками через `handler_auth:authenticate/1``logic_auth:verify_jwt/1`.
- Административные токены выпускаются через `POST /v1/admin/login` с audience `<<"admin">>` и секретом `ADMIN_JWT_SECRET`. Проверяются обработчиками через `handler_auth:authenticate/1` с последующей проверкой роли `is_admin/1`.
Обработчики порта 8445 используют единый middleware `handler_auth:authenticate/1`, который извлекает токен из заголовка `Authorization: Bearer ...`, верифицирует его через `logic_auth:verify_jwt/1` и возвращает `{ok, UserId, Req}` или `{error, Code, Message, Req}`.
## 7. ВЕРСИОНИРОВАНИЕ И СТАТУС
Текущая версия: **1.3** (MVP, альфа). Включает:
- Гибридную модель повторяющихся событий
- Раздельную JWT-аутентификацию (пользователи/администраторы)
- Полноценную ролевую модель администрирования
- Версионированное админ-API (`/v1/admin/...`)
- Расширенную инфраструктуру (Traefik, WAF, мониторинг, failover)
## 8. ОГРАНИЧЕНИЯ И ДОПУЩЕНИЯ
- В разработке используются самоподписанные SSL-сертификаты.
- Для production требуется реальный сертификат и настройка OAuth2 для Google.
- Заглушки внешних сервисов должны быть заменены перед запуском.
- Автоматическое обнаружение узлов через libcluster находится в перспективе; текущая реализация использует статический JOIN_NODES с возможностью подключения через DNS-лукап.
## 9. ТРЕБОВАНИЯ К ОКРУЖЕНИЮ
- Erlang/OTP 28.2
- rebar3 3.27.0
- openssl
- Docker (опционально)
- Drone (опционально, для CI/CD)