ТЕХНИЧЕСКОЕ ЗАДАНИЕ (ТЗ) НА ПЛАТФОРМУ EVENTHUB Версия: 1.1 (расширенная с гибридной моделью повторяющихся событий) 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. Модерация и безопасность Жалобы на календари, события, отзывы Автоматическая модерация по ключевым словам и по порогу жалоб Ручная заморозка / разморозка администратором Бан-лист слов Аудит действий администраторов 2.8. Баг-трекер (автоматический) При ошибке создаётся тикет, группировка по хэшу ошибки Учёт количества повторений Уведомление пользователя при создании и при закрытии тикета Доступ только у администраторов 2.9. Платная подписка Личное использование бесплатно (personal) Коммерческое использование платно (commercial) Пробный период 30 дней Планы подписки: 1, 3, 6, 12 месяцев Заглушка платежного шлюза (для тестирования) Автоматическое истечение подписки 2.10. Административная панель Отдельный HTTPS-сервер (порт 8445) и отдельный WSS (порт 8446) Управление календарями, событиями, отзывами, жалобами, тикетами, бан-словами Статистика, информация о нодах кластера WebSocket-уведомления администраторам 2.11. Real-time уведомления (WebSocket) Пользователи: подписка на календарь, получение обновлений Администраторы: подписка на глобальные уведомления (жалобы, авто-заморозки) НЕФУНКЦИОНАЛЬНЫЕ ТРЕБОВАНИЯ 3.1. Производительность и масштабирование 100 000+ пользователей Горизонтальное масштабирование (увеличение нод) Mnesia с дисковыми копиями на нескольких нодах Отдельные ноды для исторических данных (disc_only_copies) Пагинация всех списков 3.2. Надёжность Супервизорное дерево OTP Let it crash – быстрый перезапуск процессов Автоматическое восстановление после падения ноды (Mnesia) 3.3. Безопасность JWT с ролью (user / admin) HTTPS / WSS (самоподписанный сертификат для dev, реальный для prod) Argon2 для хеширования паролей (erlang-argon2) OAuth2 для Google (опционально) Refresh token (запланирован) 3.4. Наблюдаемость Healthcheck эндпоинт Логирование (JSON, ротация) Prometheus метрики (запланированы) Аудит действий админов 3.5. CI/CD Drone CI (или GitLab CI / GitHub Actions) EUnit + Common Test + Tsung Сборка релиза Docker-образ Горячее обновление (rolling upgrade) кластера (3+ нод) Миграции Mnesia СТЕК ТЕХНОЛОГИЙ (С ВЕРСИЯМИ) Бэкенд: Erlang/OTP 28.2 Сборка: rebar3 3.27.0 HTTP/WebSocket: Cowboy 2.10.0 JSON: jsx 3.1.0 JWT: jwerl 1.1.0 Хеширование паролей: erlang-argon2 1.0.0 Тестирование: meck 0.9.2, gun 2.0.0 Нагрузочное тестирование: Tsung 1.8.0 CI/CD: Drone 2.0+ Контейнеризация: Docker 20.10+ База данных: Mnesia (встроенная, распределённая) ИЕРАРХИЧЕСКАЯ СТРУКТУРА КОДА src/ ├── infra/ # инфраструктура (приложение, супервизоры, аутентификация, подписки) ├── core/ # слой доступа к данным (DAO) и модели ├── logic/ # бизнес-логика (независимая от HTTP/WS) ├── services/ # внешние сервисы (заглушки/интеграции) └── handlers/ # обработчики HTTP/WebSocket Правила: Модули handlers/ не содержат бизнес-логики. Модули logic/ не знают о HTTP/WS. Модули core/ (DAO) работают только с Mnesia. Модули services/ могут быть заменены на реальные реализации без изменения остального кода. Заголовочный файл include/records.hrl содержит все записи таблиц Mnesia. ОСНОВНЫЕ API (КРАТКО) Пользовательские (порт 8080): POST /v1/register POST /v1/login GET /v1/user/me POST /v1/calendars + CRUD POST /v1/calendars/:id/events + CRUD POST /v1/events/:id/join POST /v1/events/:id/confirm/:user_id POST /v1/reviews POST /v1/reports POST /v1/subscription/activate POST /v1/events/:id/export (Google) GET /v1/events/:id/ical (Apple) GET /health WebSocket (порт 8081): ws://localhost:8081/ws?token=... Административные (порт 8445): GET /admin/stats/overview GET /admin/nodes GET /admin/calendars, PUT /admin/calendars/:id/freeze, /unfreeze GET /admin/events, PUT /admin/events/:id/freeze GET /admin/reviews, PUT /admin/reviews/:id/hide GET /admin/reports, PUT /admin/reports/:id/status GET /admin/tickets, PUT /admin/tickets/:id/status GET /admin/banned_words, POST, DELETE Административный WebSocket (порт 8446): wss://localhost:8446/admin/ws?token=... ВЕРСИОНИРОВАНИЕ И СТАТУС Текущая версия: 1.1 (MVP, альфа, включает гибридную модель повторяющихся событий) ОГРАНИЧЕНИЯ И ДОПУЩЕНИЯ В разработке используются самоподписанные SSL-сертификаты. Для production требуется реальный сертификат и настройка OAuth2 для Google. Заглушки внешних сервисов должны быть заменены перед запуском. ТРЕБОВАНИЯ К ОКРУЖЕНИЮ Erlang/OTP 28.2 rebar3 3.27.0 openssl Docker (опционально) Drone (опционально, для CI/CD)