Banners
1. Summary
Goal: Отображение рекламных баннеров на главной странице в формате слайдера и отслеживание взаимодействий пользователей (просмотры, клики).
User Value: Пользователь видит несколько актуальных рекламных предложений с удобной навигацией. Платформа получает детальную аналитику эффективности каждого баннера.
2. Business Logic
Banner Slider
Баннеры отображаются на главной в формате слайдера (до 5 штук), если выполнены условия:
isActive: true— баннер активирован админомstartDate ≤ now OR startDate = null— кампания началасьendDate ≥ now OR endDate = null— кампания не закончилась- Сортировка по
createdAt DESC— новые баннеры показываются первыми
Функции слайдера:
| Функция | Значение | Описание |
|---|---|---|
| Авто-ротация | 15 сек | Автоматическая смена слайда |
| Анимация | 300ms | Плавный fade-переход |
| Индикаторы | Dots | Точки для навигации внизу |
| Лимит | 5 | Максимум баннеров в слайдере |
Установите startDate и endDate для автоматического показа в нужный период без ручного вмешательства. Несколько активных баннеров ротируются автоматически.
Interaction Tracking
VIEW: Трекается для каждого слайда при его показе (при авто-ротации или ручном переключении):
- Слайд становится активным → проверка дедупликации
- Если прошло достаточно времени → создаёт запись в
BannerAnalytics - VIEW трекается один раз за сессию для каждого баннера
CLICK: Трекается при клике на текущий активный баннер:
- Проверяет дедупликацию (cooldown 60 мин)
- Открывает ссылку в новой вкладке
- Записывает клик при переходе на внешний ресурс
Deduplication
Временные окна дедупликации
| Действие | Cooldown | Причина |
|---|---|---|
| VIEW | 15 минут | Защита от накрутки при обновлении страницы |
| CLICK | 60 минут | Защита от многократных кликов |
Алгоритм: Поиск последней записи в BannerAnalytics с тем же userId, bannerId, action в пределах временного окна.
Protection
| Действие | Rate Limit | Auth | Validation |
|---|---|---|---|
| Получить баннер | content (200/min) | Telegram | — |
| Записать взаимодействие | mutations (5/min) | Telegram | action: view | click |
См. Security Matrix для полного обзора защит.
Edge Cases
| Ситуация | Поведение | Код |
|---|---|---|
| ✅ Нет активных баннеров | 200 OK, data: [] | — |
| ❌ Баннер не найден | 200 OK, recorded: false | BANNER_NOT_FOUND |
| ⏱️ Повторный просмотр < 15 мин | 200 OK, recorded: false | DUPLICATE_VIEW_WITHIN_15MIN |
| ⏱️ Повторный клик < 1 час | 200 OK, recorded: false | DUPLICATE_CLICK_WITHIN_1HOUR |
| 🔒 Пользователь не авторизован | 200 OK, recorded: false | USER_NOT_AUTHENTICATED |
| 🎯 Скрытие баннеров | Скрыты на 10 мин | LocalStorage |
При ошибке БД во время проверки дедупликации — действие разрешается. Это предотвращает блокировку пользователя из-за технических проблем.
3. ADR (Architectural Decisions)
Почему дедупликация на уровне сервиса, а не БД?
Проблема: Нужно предотвратить накрутку статистики при многократных просмотрах/кликах.
Решение: Сервис BannerDeduplicationService проверяет последнее событие в БД перед записью нового.
Альтернативы (отклонены):
- Unique constraint — не подходит, т.к. один пользователь может взаимодействовать многократно, но с интервалом
- Redis rate limiting — избыточно для текущих объёмов, добавляет инфраструктурную сложность
- Frontend throttling — легко обходится, не защищает от злоумышленников
Последствия: Простое решение на уровне сервиса, легко тестируется и модифицируется.
Почему возвращать 200 OK даже при отклонении?
Проблема: Отклонённые дублирующие запросы — это нормальное поведение, не ошибка.
Решение: Возвращаем { success: true, recorded: false, reason: '...' } вместо 4xx.
Последствия: Фронтенд не показывает ошибку пользователю, но может использовать reason для аналитики.
Почему удалён BannerType enum?
Проблема: В референсном проекте goBind баннеры показывались на разных страницах (BIND, FPS, PRESET). В goloot баннер только на Home.
Решение: Удалить enum BannerType и поле type из модели. Один активный баннер выбирается по createdAt DESC.
Альтернативы (отклонены):
- Оставить для будущего использования — нарушает YAGNI, создаёт мёртвый код
- Мягкое удаление — половинчатое решение, UI всё равно показывал бы пустые значения
Последствия: Упрощение схемы и админки, удалены ~200 строк type-related кода из admin.
4. Architecture
Flow: Record Interaction
Key Components
Backend:
| Компонент | Путь | Описание |
|---|---|---|
| BannerService | backend/src/domains/banners/services/banner.service.ts | Основная логика, getActiveBanners() |
| BannerDeduplicationService | backend/src/domains/banners/services/banner-deduplication.service.ts | Проверка дублей |
| Routes | backend/src/domains/banners/routes/user-banners.routes.ts | User API |
| Schemas | backend/src/domains/banners/schemas/*.schemas.ts | Валидация |
Frontend:
| Компонент | Путь | Описание |
|---|---|---|
| BannerSlider | frontend/src/components/ui/BannerSlider.tsx | Слайдер с авто-ротацией |
| useBanners | frontend/src/hooks/useBanners.ts | Хук для получения баннеров |
| Config | frontend/src/config/banner.config.ts | Константы слайдера |
CRUD операции с баннерами находятся в отдельном домене: backend/src/domains/admin/routes/banners.ts
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| AdBanner | Конфигурация баннера | title, imageUrl, link, isActive, startDate, endDate |
| BannerAnalytics | Статистика взаимодействий | bannerId, userId, action, createdAt |
Relationships
Indexes
| Индекс | Поля | Назначение |
|---|---|---|
banner_user_action_idx | bannerId, userId, action | Дедупликация |
banner_time_idx | bannerId, createdAt | Хронология событий |
6. API Endpoints
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /api/banners/home | Получить активные баннеры (массив до 5) | → |
| POST | /api/banners/:id/:action | Записать просмотр/клик | → |
Баннеры также доступны через Bootstrap API (/api/bootstrap) в поле banners[]. Это позволяет загружать данные одним запросом при старте приложения.
7. Related
- Promo Codes — другой канал маркетинговых активностей
- UTM Tracking — отслеживание источников трафика
- Notifications — пуш-уведомления