Разработка партнёрской программы (Affiliate Platform)
Партнёрская программа позволяет владельцам трафика продвигать продукт за комиссию. Партнёр получает уникальную ссылку, приводит клиента, система фиксирует это и начисляет вознаграждение. Технически задача — надёжный трекинг, честный расчёт комиссий и удобный личный кабинет партнёра.
Схема данных
CREATE TABLE affiliates (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID UNIQUE NOT NULL REFERENCES users(id),
ref_code VARCHAR(20) UNIQUE NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','active','suspended','terminated')),
commission_rate NUMERIC(5,2) NOT NULL DEFAULT 10, -- % от суммы
commission_type VARCHAR(20) NOT NULL DEFAULT 'percent'
CHECK (commission_type IN ('percent','fixed','hybrid')),
fixed_amount NUMERIC(12,2), -- для fixed/hybrid типов
cookie_days INTEGER NOT NULL DEFAULT 30,
payout_min NUMERIC(12,2) NOT NULL DEFAULT 1000,
approved_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE affiliate_links (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
affiliate_id UUID NOT NULL REFERENCES affiliates(id),
campaign_name VARCHAR(200),
landing_url VARCHAR(500) NOT NULL,
params JSONB DEFAULT '{}', -- UTM и кастомные параметры
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE affiliate_clicks (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
affiliate_id UUID NOT NULL REFERENCES affiliates(id),
link_id UUID REFERENCES affiliate_links(id),
ip INET,
user_agent TEXT,
referrer VARCHAR(500),
fingerprint VARCHAR(64), -- device fingerprint для cross-device
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE INDEX idx_affiliate_clicks_date ON affiliate_clicks(affiliate_id, created_at DESC);
CREATE TABLE affiliate_conversions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
affiliate_id UUID NOT NULL REFERENCES affiliates(id),
click_id UUID REFERENCES affiliate_clicks(id),
customer_id UUID REFERENCES users(id),
order_id UUID,
order_amount NUMERIC(15,2),
commission NUMERIC(15,2) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','confirmed','rejected','paid')),
rejection_reason VARCHAR(200),
hold_days INTEGER NOT NULL DEFAULT 30,
available_at DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
Трекинг кликов
import hashlib
from django.core.cache import cache
def track_affiliate_click(request, ref_code: str, landing_url: str) -> str | None:
affiliate = Affiliate.objects.filter(
ref_code=ref_code, status='active'
).first()
if not affiliate:
return None
# Device fingerprint для кросс-девайс атрибуции
fingerprint_raw = f'{request.META.get("HTTP_USER_AGENT")}:{request.META.get("ACCEPT_LANGUAGE")}'
fingerprint = hashlib.sha256(fingerprint_raw.encode()).hexdigest()[:32]
click = AffiliateClick.objects.create(
affiliate=affiliate,
ip=get_client_ip(request),
user_agent=request.META.get('HTTP_USER_AGENT', ''),
referrer=request.META.get('HTTP_REFERER', ''),
fingerprint=fingerprint,
)
# Кука для атрибуции
cookie_value = str(click.id)
return cookie_value # устанавливается в response
def set_affiliate_cookie(response, click_id: str, cookie_days: int):
response.set_cookie(
'aff_click',
click_id,
max_age=cookie_days * 86400,
httponly=True,
samesite='Lax',
)
Атрибуция конверсии
def attribute_conversion(order, request):
"""Вызывается после успешной оплаты заказа"""
click_id = request.COOKIES.get('aff_click')
affiliate = None
click = None
if click_id:
click = AffiliateClick.objects.filter(id=click_id).first()
if click:
# Проверяем, не истёк ли cookie window
cutoff = timezone.now() - timedelta(days=click.affiliate.cookie_days)
if click.created_at >= cutoff:
affiliate = click.affiliate
if not affiliate:
# Fallback: ищем по fingerprint (кросс-девайс)
fingerprint = compute_fingerprint(request)
recent_click = AffiliateClick.objects.filter(
fingerprint=fingerprint,
created_at__gte=timezone.now() - timedelta(days=30)
).order_by('-created_at').first()
if recent_click:
affiliate = recent_click.affiliate
click = recent_click
if affiliate:
commission = calculate_commission(affiliate, order.total_amount)
AffiliateConversion.objects.create(
affiliate=affiliate,
click=click,
customer=order.user,
order=order,
order_amount=order.total_amount,
commission=commission,
hold_days=affiliate.hold_days,
available_at=date.today() + timedelta(days=affiliate.hold_days),
)
# Инвалидируем куку после конверсии
return True
return False
def calculate_commission(affiliate, order_amount: Decimal) -> Decimal:
if affiliate.commission_type == 'percent':
return (order_amount * affiliate.commission_rate / 100).quantize(Decimal('0.01'))
elif affiliate.commission_type == 'fixed':
return affiliate.fixed_amount
else: # hybrid
percent_part = order_amount * affiliate.commission_rate / 100
return (percent_part + affiliate.fixed_amount).quantize(Decimal('0.01'))
Многоуровневая партнёрская программа
class AffiliateRelation(models.Model):
"""Дерево партнёров для многоуровневой программы"""
affiliate = models.OneToOneField(Affiliate, on_delete=models.CASCADE)
parent = models.ForeignKey(
'self', null=True, blank=True,
on_delete=models.SET_NULL, related_name='children'
)
level = models.IntegerField(default=1)
REFERRAL_RATES = {
1: Decimal('10'), # прямой партнёр: 10%
2: Decimal('3'), # партнёр партнёра: 3%
3: Decimal('1'), # третий уровень: 1%
}
def distribute_multilevel_commission(conversion):
"""Начисляем комиссию по всей цепочке вверх"""
relation = AffiliateRelation.objects.filter(
affiliate=conversion.affiliate
).first()
level = 1
current = relation
while current and level <= 3:
rate = REFERRAL_RATES.get(level)
if rate:
commission = (conversion.order_amount * rate / 100).quantize(Decimal('0.01'))
AffiliateConversion.objects.create(
affiliate=current.affiliate,
order=conversion.order,
order_amount=conversion.order_amount,
commission=commission,
status='pending',
hold_days=current.affiliate.hold_days,
available_at=date.today() + timedelta(days=current.affiliate.hold_days),
)
current = current.parent
level += 1
Выплаты партнёрам
@shared_task
def process_affiliate_payouts():
"""Ежедневно: выплачиваем партнёрам с доступным балансом"""
affiliates_with_balance = (
Affiliate.objects
.annotate(
available=Coalesce(
Subquery(
AffiliateConversion.objects.filter(
affiliate=OuterRef('pk'),
status='confirmed',
available_at__lte=date.today()
).values('affiliate').annotate(s=Sum('commission')).values('s')
),
Decimal('0')
)
)
.filter(available__gte=F('payout_min'), status='active')
)
for affiliate in affiliates_with_balance:
initiate_payout(affiliate, affiliate.available)
Антифрод
def check_conversion_fraud(conversion) -> bool:
"""Базовые фрод-проверки"""
# Самореферрал
if conversion.customer == conversion.affiliate.user:
conversion.status = 'rejected'
conversion.rejection_reason = 'self_referral'
conversion.save()
return True
# Слишком много конверсий с одного IP за час
recent_from_ip = AffiliateConversion.objects.filter(
click__ip=conversion.click.ip if conversion.click else None,
created_at__gte=timezone.now() - timedelta(hours=1)
).count()
if recent_from_ip > 10:
flag_for_review(conversion, 'high_conversion_rate_from_ip')
return True
# Конверсия через несколько секунд после клика
if conversion.click:
time_to_convert = (conversion.created_at - conversion.click.created_at).total_seconds()
if time_to_convert < 30:
flag_for_review(conversion, 'instant_conversion')
return True
return False
Сроки
Базовая партнёрская программа (одноуровневая, percent-комиссия, личный кабинет, выплата на карту): 3–4 недели. С многоуровневой структурой, антифродом, детальной аналитикой по кампаниям и автоматическими выплатами: 6–8 недель.







