Мы разрабатываем кастомные системы лояльности для интернет-магазинов, маркетплейсов и сервисов. Недавно мы внедрили решение, которое в первые месяцы работы увеличило средний чек на 25% и повысило возврат пользователей на 40%. Типичная ситуация: клиент использует готовый плагин, но не может настроить множители начисления для премиальных категорий или реализовать сгорание баллов по правилу FIFO. Кроме того, стандартные плагины часто генерируют N+1 запросы при проверке баланса, что приводит к падению производительности на высоких нагрузках. Мы используем стек Laravel 11, PostgreSQL и Redis для обеспечения отзывчивости даже при 1000 запросов в секунду. Например, один из наших клиентов — интернет-магазин электроники — столкнулся с падением скорости оформления заказа из-за N+1 запросов к лояльности. После внедрения кастомного решения с кэшированием и батчевыми вставками время обработки заказа сократилось с 2 секунд до 200 мс. В этой статье разбираем архитектуру, типовые проблемы и наш подход к реализации.
Какие проблемы решаем
Большинство готовых решений не учитывают специфику бизнеса. Типичные боли клиентов:
- N+1 запросы при начислении баллов — каждая операция дёргает БД отдельно. Мы решаем это через батчевые вставки и кэширование баланса в Redis.
- Конкурентное списание — гонки, когда два заказа списывают одни и те же баллы. Используем
SELECT ... FOR UPDATEи транзакции. - Сложные правила кампаний — множители для категорий, подарки за минимальную корзину, временные акции. Реализуем через конфигурируемые таблицы с JSONB-условиями.
- Сгорание баллов — расчёт FIFO и обработка частичного списания.
Как это работает: архитектура ядра
Центральный элемент — транзакционный лог (Wikipedia). Баланс всегда выводится из последовательности операций, что гарантирует аудит.
-- Бонусный счёт пользователя
CREATE TABLE loyalty_accounts (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT UNIQUE REFERENCES users(id),
balance DECIMAL(12,2) DEFAULT 0,
lifetime_earned DECIMAL(12,2) DEFAULT 0,
tier_id BIGINT REFERENCES loyalty_tiers(id),
expires_at DATE,
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Все движения баллов (append-only лог)
CREATE TABLE loyalty_transactions (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT REFERENCES loyalty_accounts(id),
type VARCHAR(32) NOT NULL, -- 'earn', 'redeem', 'expire', 'adjust', 'refund'
amount DECIMAL(12,2) NOT NULL,
balance_after DECIMAL(12,2) NOT NULL,
reason VARCHAR(255),
source_type VARCHAR(64), -- 'order', 'manual', 'birthday', 'referral'
source_id BIGINT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Уровни программы
CREATE TABLE loyalty_tiers (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(64) NOT NULL, -- Bronze, Silver, Gold, Platinum
min_lifetime DECIMAL(12,2) NOT NULL,
earn_multiplier DECIMAL(4,2) DEFAULT 1.0,
redeem_rate DECIMAL(4,2) DEFAULT 1.0,
perks JSONB
);
Транзакционный лог — принципиальное архитектурное решение. Баланс всегда считается из истории либо хранится денормализованно и пересчитывается при расхождении. Это позволяет аудитировать любое движение.
Как избежать потери баллов при конкурентных списаниях?
Главная проблема — два одновременных заказа могут списать одни и те же баллы. Решение — блокировка строки счёта и атомарные транзакции.
class LoyaltyService {
public function earnPoints(User $user, float $amount, string $sourceType, int $sourceId): LoyaltyTransaction {
$account = LoyaltyAccount::firstOrCreate(['user_id' => $user->id]);
$tier = $account->tier ?? LoyaltyTier::where('min_lifetime', 0)->orderBy('min_lifetime')->first();
$points = round($amount * $tier->earn_multiplier * config('loyalty.earn_rate'));
return DB::transaction(function() use ($account, $points, $sourceType, $sourceId) {
$newBalance = $account->balance + $points;
$account->update([
'balance' => $newBalance,
'lifetime_earned' => $account->lifetime_earned + $points,
]);
$newTier = LoyaltyTier::where('min_lifetime', '<=', $account->lifetime_earned)
->orderByDesc('min_lifetime')
->first();
if ($newTier && $newTier->id !== $account->tier_id) {
$account->update(['tier_id' => $newTier->id]);
event(new TierUpgraded($account->user, $newTier));
}
return LoyaltyTransaction::create([
'account_id' => $account->id,
'type' => 'earn',
'amount' => $points,
'balance_after'=> $newBalance,
'source_type' => $sourceType,
'source_id' => $sourceId,
'reason' => 'Начисление за покупку',
]);
});
}
}
Почему стоит выбирать кастомную систему лояльности вместо готовых решений?
Готовые плагины часто не дают гибкости в правилах начисления, интеграции с CRM и аналитике. Сравним в таблице:
| Критерий | Готовое решение | Кастомная разработка |
|---|---|---|
| Конфигурация кампаний | Ограниченный набор шаблонов | Любая логика с произвольными условиями |
| Интеграция | Только стандартные CMS | Через API с любыми системами (1С, ERP, CRM) |
| Масштабирование | Зависит от платформы | Оптимизировано под нагрузку (Redis, очереди) |
| Аналитика | Только базовые отчёты | Кастомные дашборды и сегментация |
Кастомная система окупается за 4–6 месяцев за счёт роста среднего чека (20–30%) и увеличения LTV в 1,5 раза. Кастомное решение в 2 раза эффективнее готового по конверсии в повторную покупку.
Какие уровни лояльности можно настроить?
Типичная иерархия уровней — от Bronze до Platinum — с разными множителями начисления и привилегиями. Например:
| Уровень | Минимальные пожизненные траты | Множитель начисления | Привилегии |
|---|---|---|---|
| Bronze | 0 ₽ | 1.0 | Базовая программа |
| Silver | 5000 ₽ | 1.2 | Приоритетная поддержка |
| Gold | 20000 ₽ | 1.5 | Бесплатная доставка |
| Platinum | 50000 ₽ | 2.0 | Персональный менеджер |
Условия можно кастомизировать под любой бизнес.
Процесс реализации
- Аналитика: изучаем бизнес-процессы, собираем требования, готовим прототип.
- Проектирование: архитектура БД, API, UI-виджетов. Согласуем логику кампаний.
- Разработка: итеративная поставка каждые 2–3 дня.
- Тестирование: unit- и integration-тесты, нагрузочное тестирование (до 1000 RPS).
- Деплой: настройка CI/CD, мониторинг (Sentry, Grafana), документация.
Что входит в работу
- Архитектура и документация: описание схемы БД, API (OpenAPI), инструкции для администраторов.
- Исходный код: бэкенд + фронтенд, покрытие тестами не менее 80%.
- Доступы: выделенный репозиторий, стенды для разработки и staging.
- Поддержка: 1 месяц пост-продакшн сопровождения, далее — по SLA.
Сроки ориентировочно
Базовая версия с начислением, списанием и историей транзакций — 1,5–2 недели. Расширенная с уровнями, кампаниями и сгоранием баллов — 3–4 недели. Мобильная карта лояльности с QR-кодом и интеграция с POS — плюс 2–3 недели.
Мы — команда с 10-летним опытом в e-commerce, реализовали более 50 проектов систем лояльности. Свяжитесь с нами для оценки вашего проекта — оценим за 1 день. Получите консультацию по архитектуре и срокам.







