Реализация программы лояльности в мобильном приложении
Программа лояльности в мобильном приложении — это не просто «копите баллы». За простым интерфейсом скрывается нетривиальная серверная логика: транзакционность начисления, предотвращение накруток, expiry баллов, и синхронизация состояния между сессиями. Недооценивать серверную часть здесь нельзя.
Модель данных и транзакционность
Баланс баллов нельзя хранить одним числом в поле user.points. Нужен журнал транзакций:
loyalty_transactions:
id UUID PK
user_id UUID FK
type ENUM('earn', 'redeem', 'expire', 'refund', 'bonus')
amount INTEGER -- положительное для earn, отрицательное для redeem
reference_id UUID -- purchase_id, action_id
reference_type VARCHAR
expires_at TIMESTAMP -- для earn-транзакций
created_at TIMESTAMP
-- Баланс = сумма amount по незаистёкшим транзакциям
-- SELECT COALESCE(SUM(amount), 0) FROM loyalty_transactions
-- WHERE user_id = ? AND (expires_at IS NULL OR expires_at > NOW())
Это позволяет: откатить начисление при возврате покупки, реализовать expiry с точностью до транзакции, аудировать любые изменения баланса.
Все операции изменения баланса — через транзакцию БД с уровнем изоляции SERIALIZABLE для счётчиков, или через оптимистичную блокировку. Без этого при параллельных запросах (несколько вкладок, повторный тап) баланс может уйти в минус.
Механики начисления
Базовые варианты: за покупку (N баллов за рубль), за действие (регистрация, отзыв, приглашение друга), бонусные периоды (x2 по выходным). Каждый тип — отдельная rule в таблице loyalty_rules с условиями и коэффициентами. Это позволяет менять механики без деплоя.
Предотвращение накруток: ограничение начислений за одно действие в единицу времени (rate limiting по user_id + action_type), верификация действий (например, отзыв засчитывается только после модерации), лимит на реферальные начисления.
Клиентская реализация
На мобильном клиенте — три ключевых экрана: баланс с историей транзакций, каталог вознаграждений, экран списания. Баланс синхронизируется при каждом открытии приложения и через WebSocket/SSE при активных операциях.
// Swift — подписка на обновления баланса через WebSocket
class LoyaltyViewModel: ObservableObject {
@Published var balance: Int = 0
@Published var transactions: [LoyaltyTransaction] = []
func subscribeToUpdates() {
webSocketService.subscribe(channel: "loyalty.\(userID)") { [weak self] event in
DispatchQueue.main.async {
self?.balance = event.newBalance
self?.transactions.insert(event.transaction, at: 0)
}
}
}
}
Уровни программы лояльности
Тиерная система (Bronze → Silver → Gold) требует пересчёта уровня при каждом изменении баланса. Лучше хранить total_earned (накопленное за период без учёта списаний) отдельно от текущего баланса — именно по этому значению определяется тир.
Даты сброса тира (обычно ежегодно) — отдельная фоновая задача или cron. Нужно уведомлять пользователей за 30 дней до даунгрейда тира.
Интеграция с IAP
При покупке через In-App Purchase: начисление баллов происходит на сервере после верификации транзакции, до finishTransaction на клиенте не нужно ждать — баллы можно начислять асинхронно. При refund через Apple/Google — обрабатываем webhook и создаём refund-транзакцию в журнале.
Сроки реализации — около 5 дней: проектирование схемы, серверная логика начисления и списания, клиентские экраны, уведомления.







