Розробка платформи партнерської програми (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 для cross-device атрибуції
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 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:
# Перевіряємо, не istink cookie window
cutoff = timezone.now() - timedelta(days=click.affiliate.cookie_days)
if click.created_at >= cutoff:
affiliate = click.affiliate
if not affiliate:
# Fallback: шукаємо за fingerprint (cross-device)
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 тижнів.







