Разработка реферальной системы
Реферальная программа — это механизм, при котором существующий пользователь получает вознаграждение за привлечение нового. На практике это набор технических задач: генерация уникальных ссылок, атрибуция переходов, учёт выполненных условий, начисление и выплата вознаграждений.
Модель данных
CREATE TABLE referral_codes (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT REFERENCES users(id),
code VARCHAR(32) UNIQUE NOT NULL,
type VARCHAR(32) DEFAULT 'personal', -- personal/promo/partner
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE referral_clicks (
id BIGSERIAL PRIMARY KEY,
code_id BIGINT REFERENCES referral_codes(id),
ip INET,
user_agent TEXT,
landed_at TIMESTAMPTZ DEFAULT NOW(),
converted BOOLEAN DEFAULT FALSE
);
CREATE TABLE referrals (
id BIGSERIAL PRIMARY KEY,
referrer_id BIGINT REFERENCES users(id), -- кто привёл
referred_id BIGINT REFERENCES users(id), -- кого привели
code_id BIGINT REFERENCES referral_codes(id),
status VARCHAR(32) DEFAULT 'pending', -- pending/qualified/rewarded/cancelled
qualified_at TIMESTAMPTZ, -- выполнено условие начисления
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE TABLE referral_rewards (
id BIGSERIAL PRIMARY KEY,
referral_id BIGINT REFERENCES referrals(id),
recipient_id BIGINT REFERENCES users(id), -- реферер или реферал (двусторонние программы)
type VARCHAR(32), -- 'cashback', 'bonus_points', 'discount'
amount DECIMAL(14,2),
currency CHAR(3) DEFAULT 'RUB',
status VARCHAR(32) DEFAULT 'pending', -- pending/paid/cancelled
paid_at TIMESTAMPTZ
);
Генерация уникального кода
class ReferralCodeService {
public function generateForUser(User $user): ReferralCode {
// Проверяем, нет ли уже кода
if ($existing = ReferralCode::where('user_id', $user->id)->first()) {
return $existing;
}
do {
// Читаемый код на основе имени пользователя + случайный суффикс
$base = strtoupper(substr(preg_replace('/[^a-z]/i', '', $user->name), 0, 4));
$code = $base . strtoupper(Str::random(4));
} while (ReferralCode::where('code', $code)->exists());
return ReferralCode::create([
'user_id' => $user->id,
'code' => $code,
]);
}
}
Реферальная ссылка и cookie
Реферальный параметр передаётся через URL: https://example.com/register?ref=IVAN4X2K. Нужно сохранить атрибуцию даже если пользователь не зарегистрируется сразу:
// Middleware: ReferralTracker
class ReferralTrackerMiddleware {
public function handle(Request $request, Closure $next): Response {
$code = $request->query('ref');
if ($code && !session()->has('referral_code')) {
$referralCode = ReferralCode::where('code', $code)->first();
if ($referralCode) {
session(['referral_code' => $code]);
// Логируем клик
ReferralClick::create([
'code_id' => $referralCode->id,
'ip' => $request->ip(),
'user_agent' => $request->userAgent(),
]);
}
}
return $next($request);
}
}
Атрибуция при регистрации
// В UserRegistrationService
public function register(array $data): User {
$user = User::create($data);
$referralCode = session()->pull('referral_code');
if ($referralCode) {
$code = ReferralCode::where('code', $referralCode)->first();
if ($code && $code->user_id !== $user->id) {
Referral::create([
'referrer_id' => $code->user_id,
'referred_id' => $user->id,
'code_id' => $code->id,
'status' => 'pending',
]);
// Обновляем клик как сконвертированный
ReferralClick::where('code_id', $code->id)
->where('converted', false)
->latest('landed_at')
->first()
?->update(['converted' => true]);
}
}
return $user;
}
Условия начисления вознаграждения
Реферал считается «квалифицированным» только при выполнении определённого условия: первая оплата, достижение порога покупок, истечение trial-периода. Это реализуется через Events:
// Событие: первая оплата нового пользователя
class FirstPurchaseMade {
public function __construct(public Order $order) {}
}
// Listener
class QualifyReferralOnFirstPurchase {
public function handle(FirstPurchaseMade $event): void {
$referral = Referral::where('referred_id', $event->order->user_id)
->where('status', 'pending')
->first();
if (!$referral) return;
DB::transaction(function() use ($referral, $event) {
$referral->update([
'status' => 'qualified',
'qualified_at' => now(),
]);
$program = ReferralProgram::active()->first();
// Вознаграждение рефереру
ReferralReward::create([
'referral_id' => $referral->id,
'recipient_id' => $referral->referrer_id,
'type' => $program->reward_type,
'amount' => $this->calculateReward($program, $event->order),
'status' => 'pending',
]);
// Двусторонняя программа: бонус и новому пользователю
if ($program->reward_referred) {
ReferralReward::create([
'referral_id' => $referral->id,
'recipient_id' => $referral->referred_id,
'type' => $program->referred_reward_type,
'amount' => $program->referred_reward_amount,
'status' => 'pending',
]);
}
});
}
private function calculateReward(ReferralProgram $program, Order $order): float {
return match($program->reward_type) {
'fixed' => $program->reward_amount,
'percentage' => round($order->total * $program->reward_percent / 100, 2),
default => 0,
};
}
}
Выплата вознаграждений
Накопленные вознаграждения выплачиваются пакетно (раз в неделю) или сразу — зависит от типа:
// Бонусные баллы — мгновенно
class CreditBonusPoints implements ShouldQueue {
public function handle(ReferralRewardCreated $event): void {
$reward = $event->reward;
if ($reward->type !== 'bonus_points') return;
BonusAccount::firstOrCreate(['user_id' => $reward->recipient_id])
->increment('balance', $reward->amount);
$reward->update(['status' => 'paid', 'paid_at' => now()]);
}
}
// Cashback — через очередь выплат
// Накапливаем и выплачиваем batch-ем через платёжный сервис
Личный кабинет реферера
Страница статистики реферальной программы:
interface ReferralStats {
code: string;
link: string;
clicks_total: number;
registered: number;
qualified: number;
earned_total: number;
pending_amount: number;
referrals: Array<{
id: number;
name: string; // только имя, не полные данные
status: string;
joined_at: string;
reward_amount: number | null;
}>;
}
Защита от фрода
Базовые проверки:
// В QualifyReferralOnFirstPurchase::handle()
// 1. Самореферал (пользователь ввёл собственный код)
if ($referral->referrer_id === $event->order->user_id) {
$referral->update(['status' => 'cancelled']);
return;
}
// 2. Один IP зарегистрировал несколько аккаунтов
$sameIpUsers = DB::table('user_registrations')
->where('ip', $event->order->user->registration_ip)
->where('created_at', '>', now()->subDays(7))
->count();
if ($sameIpUsers > 3) {
$referral->update(['status' => 'fraud_suspect']);
// Флаг для ручной проверки
return;
}
Сроки реализации
Базовая реферальная система с кодами, атрибуцией и начислением фиксированного вознаграждения: 1–1,5 недели. Двусторонняя программа с процентными бонусами, личным кабинетом и защитой от фрода: 2–2,5 недели. Многоуровневая (MLM-подобная) реферальная система с деревом рефералов: плюс 1–2 недели.







