Партнёрский дашборд (Affiliate Dashboard)
Affiliate-программа отличается от реферальной: партнёры (аффилиаты) — это внешние паблишеры, блогеры, сайты. Они размещают ссылки и получают комиссию за конверсии. Дашборд — инструмент партнёра для отслеживания кликов, конверсий и выплат.
Архитектура трекинга
model Affiliate {
id String @id @default(cuid())
userId String @unique
status AffiliateStatus @default(PENDING) // требует одобрения
commissionRate Decimal @default(0.20) // 20%
payoutThreshold Int @default(5000) // минимум для выплаты (50$)
payoutMethod String? // 'paypal' | 'bank' | 'crypto'
payoutDetails Json? // реквизиты
user User @relation(fields: [userId], references: [id])
links AffiliateLink[]
clicks AffiliateClick[]
conversions AffiliateConversion[]
payouts AffiliatePayout[]
}
model AffiliateLink {
id String @id @default(cuid())
affiliateId String
code String @unique // PARTNER123
targetUrl String // куда ведёт
campaign String? // метка для аналитики
createdAt DateTime @default(now())
affiliate Affiliate @relation(fields: [affiliateId], references: [id])
clicks AffiliateClick[]
}
model AffiliateClick {
id String @id @default(cuid())
linkId String
affiliateId String
ip String
userAgent String
referrer String?
clickedAt DateTime @default(now())
converted Boolean @default(false)
link AffiliateLink @relation(fields: [linkId], references: [id])
}
model AffiliateConversion {
id String @id @default(cuid())
affiliateId String
clickId String?
orderId String @unique
orderAmount Int // в копейках
commission Int // affiliate получает
status String @default('pending') // pending | approved | paid
createdAt DateTime @default(now())
}
Трекинг кликов
// app/api/aff/[code]/route.ts
import { NextRequest } from 'next/server';
import { db } from '@/lib/db';
import { redirect } from 'next/navigation';
export async function GET(
request: NextRequest,
{ params }: { params: { code: string } }
) {
const link = await db.affiliateLink.findUnique({
where: { code: params.code },
include: { affiliate: true }
});
if (!link || link.affiliate.status !== 'ACTIVE') {
redirect('/');
}
// Записываем клик (fire & forget)
db.affiliateClick.create({
data: {
linkId: link.id,
affiliateId: link.affiliateId,
ip: request.headers.get('x-forwarded-for') ?? 'unknown',
userAgent: request.headers.get('user-agent') ?? '',
referrer: request.headers.get('referer') ?? '',
}
}).catch(console.error);
// Cookie для атрибуции конверсии (30-дневное окно)
const response = Response.redirect(link.targetUrl, 302);
response.headers.set(
'Set-Cookie',
`aff_code=${params.code}; Max-Age=${30 * 24 * 60 * 60}; Path=/; HttpOnly; SameSite=Lax`
);
return response;
}
Дашборд партнёра
// app/affiliate/dashboard/page.tsx
export default async function AffiliateDashboard() {
const session = await auth();
const affiliate = await db.affiliate.findUnique({
where: { userId: session!.user.id },
include: {
links: {
include: {
_count: { select: { clicks: true } }
}
}
}
});
if (!affiliate) redirect('/affiliate/apply');
if (affiliate.status === 'PENDING') return <AffiliatePendingPage />;
// Статистика за последние 30 дней
const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const stats = await db.$transaction([
db.affiliateClick.count({
where: { affiliateId: affiliate.id, clickedAt: { gte: thirtyDaysAgo } }
}),
db.affiliateConversion.findMany({
where: { affiliateId: affiliate.id, createdAt: { gte: thirtyDaysAgo } }
}),
db.affiliateConversion.aggregate({
where: { affiliateId: affiliate.id, status: 'approved' },
_sum: { commission: true }
}),
]);
const [clickCount, conversions, balance] = stats;
const conversionRate = clickCount > 0
? (conversions.length / clickCount * 100).toFixed(1)
: '0';
return (
<div className="space-y-8">
<StatsGrid
clicks={clickCount}
conversions={conversions.length}
conversionRate={conversionRate}
balance={balance._sum.commission ?? 0}
commissionRate={affiliate.commissionRate}
/>
<LinksTable links={affiliate.links} affiliateId={affiliate.id} />
<ConversionsTable conversions={conversions} />
<PayoutSection
balance={balance._sum.commission ?? 0}
threshold={affiliate.payoutThreshold}
payoutMethod={affiliate.payoutMethod}
/>
</div>
);
}
Выплаты
// Инициирование выплаты через Stripe Transfers
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
export async function processPayout(affiliateId: string) {
const affiliate = await db.affiliate.findUnique({
where: { id: affiliateId },
include: {
conversions: {
where: { status: 'approved' }
}
}
});
const totalAmount = affiliate!.conversions.reduce(
(sum, c) => sum + c.commission, 0
);
if (totalAmount < affiliate!.payoutThreshold) {
throw new Error('Below minimum payout threshold');
}
// Stripe Transfer на connected account
const transfer = await stripe.transfers.create({
amount: totalAmount,
currency: 'usd',
destination: affiliate!.payoutDetails?.stripeAccountId as string,
metadata: { affiliateId },
});
// Создаём запись о выплате и обновляем конверсии
await db.$transaction([
db.affilaitePayout.create({
data: {
affiliateId,
amount: totalAmount,
stripeTransferId: transfer.id,
status: 'processing',
}
}),
db.affiliateConversion.updateMany({
where: { affiliateId, status: 'approved' },
data: { status: 'paid' }
}),
]);
}
Разработка партнёрской программы с трекингом кликов, конверсий и дашбордом партнёра — 5–8 рабочих дней.







