Разработка краудфандинговой платформы
Краудфандинговая платформа — это не просто «сайт со страницей оплаты». Это система управления кампаниями, сбором средств, выплатами создателям, коммуникацией с бэкерами и соблюдением требований платёжного законодательства. Разница между работающим продуктом и списком фич — в деталях реализации каждой из этих частей.
Модели финансирования
Прежде чем проектировать схему данных, нужно определиться с моделью:
All-or-Nothing (AON) — средства списываются только при достижении цели. Kickstarter-модель. Технически сложнее: нужна pre-authorization (холдирование) или отложенный capture.
Keep-it-All (KIA) — средства списываются сразу, создатель получает всё независимо от цели. Indiegogo-модель. Проще реализовать, но юридически требует чёткого описания условий возврата.
Гибридная — цель фиксированная, но при превышении открываются stretch goals. Самый сложный вариант с точки зрения логики состояний кампании.
Схема данных
CREATE TABLE campaigns (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
creator_id UUID NOT NULL REFERENCES users(id),
title VARCHAR(200) NOT NULL,
slug VARCHAR(200) UNIQUE NOT NULL,
description TEXT,
goal_amount NUMERIC(15,2) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'RUB',
model VARCHAR(20) NOT NULL CHECK (model IN ('aon','kia','hybrid')),
status VARCHAR(20) NOT NULL DEFAULT 'draft'
CHECK (status IN ('draft','active','funded','failed','cancelled')),
starts_at TIMESTAMPTZ NOT NULL,
ends_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE pledges (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
campaign_id UUID NOT NULL REFERENCES campaigns(id),
backer_id UUID NOT NULL REFERENCES users(id),
amount NUMERIC(15,2) NOT NULL,
reward_id UUID REFERENCES rewards(id),
status VARCHAR(20) NOT NULL DEFAULT 'pending'
CHECK (status IN ('pending','authorized','captured','refunded','failed')),
payment_intent VARCHAR(200), -- Stripe PaymentIntent или ЮKassa payment_id
captured_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE rewards (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
campaign_id UUID NOT NULL REFERENCES campaigns(id),
title VARCHAR(200) NOT NULL,
description TEXT,
min_pledge NUMERIC(15,2) NOT NULL,
limit_qty INTEGER, -- NULL = без ограничений
claimed_qty INTEGER NOT NULL DEFAULT 0,
ships_at DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Индекс для быстрого подсчёта собранной суммы
CREATE INDEX idx_pledges_campaign_status
ON pledges(campaign_id, status)
WHERE status IN ('authorized','captured');
Платёжный флоу для AON-модели
AON требует двухэтапной оплаты: сначала авторизация (холдирование), потом capture при успехе кампании.
Stripe:
# Создание PaymentIntent с manual capture
import stripe
stripe.api_key = settings.STRIPE_SECRET_KEY
def authorize_pledge(pledge, card_token):
intent = stripe.PaymentIntent.create(
amount=int(pledge.amount * 100),
currency=pledge.campaign.currency.lower(),
payment_method=card_token,
capture_method='manual',
confirm=True,
metadata={
'pledge_id': str(pledge.id),
'campaign_id': str(pledge.campaign_id),
}
)
pledge.payment_intent = intent.id
pledge.status = 'authorized'
pledge.save()
return intent
def capture_pledges_for_campaign(campaign_id):
"""Вызывается при успешном завершении кампании"""
pledges = Pledge.objects.filter(
campaign_id=campaign_id,
status='authorized'
)
for pledge in pledges:
try:
stripe.PaymentIntent.capture(pledge.payment_intent)
pledge.status = 'captured'
pledge.captured_at = timezone.now()
pledge.save()
except stripe.error.InvalidRequestError as e:
# PaymentIntent истёк или уже отменён
pledge.status = 'failed'
pledge.save()
logger.error(f'Capture failed for pledge {pledge.id}: {e}')
def cancel_pledges_for_campaign(campaign_id):
"""Вызывается при провале кампании"""
pledges = Pledge.objects.filter(
campaign_id=campaign_id,
status='authorized'
)
for pledge in pledges:
stripe.PaymentIntent.cancel(pledge.payment_intent)
pledge.status = 'refunded'
pledge.save()
Важно: Stripe держит authorization максимум 7 дней для карт (до 30 дней для некоторых типов). Для кампаний длиннее 7 дней нужна другая стратегия — например, сохранение платёжного метода и capture в последний день.
Сохранение карты для длинных кампаний
# Сохраняем карту через SetupIntent, charge в конце кампании
def save_payment_method(user, card_token):
# Создаём или получаем Stripe Customer
if not user.stripe_customer_id:
customer = stripe.Customer.create(
email=user.email,
metadata={'user_id': str(user.id)}
)
user.stripe_customer_id = customer.id
user.save()
setup_intent = stripe.SetupIntent.create(
customer=user.stripe_customer_id,
payment_method=card_token,
confirm=True,
usage='off_session',
)
return setup_intent.payment_method
def charge_saved_card(pledge):
intent = stripe.PaymentIntent.create(
amount=int(pledge.amount * 100),
currency='rub',
customer=pledge.backer.stripe_customer_id,
payment_method=pledge.payment_method_id,
confirm=True,
off_session=True,
metadata={'pledge_id': str(pledge.id)},
)
return intent
Celery-задачи для завершения кампаний
# tasks.py
from celery import shared_task
from django.utils import timezone
@shared_task
def check_campaign_deadline(campaign_id):
campaign = Campaign.objects.get(id=campaign_id)
if campaign.ends_at > timezone.now():
return # Ещё не завершилась
total = Pledge.objects.filter(
campaign=campaign,
status__in=['authorized', 'captured']
).aggregate(total=Sum('amount'))['total'] or 0
if campaign.model == 'aon' and total < campaign.goal_amount:
campaign.status = 'failed'
campaign.save()
cancel_pledges_for_campaign.delay(campaign_id)
else:
campaign.status = 'funded'
campaign.save()
capture_pledges_for_campaign.delay(campaign_id)
notify_creator_campaign_success.delay(campaign_id)
# Celery beat расписание
CELERY_BEAT_SCHEDULE = {
'check-campaign-deadlines': {
'task': 'campaigns.tasks.check_all_deadlines',
'schedule': crontab(minute='*/15'),
},
}
Выплаты создателям
Stripe Connect — стандарт для маркетплейсов. Создатели подключают свои банковские счета через onboarding:
def create_connect_account(creator):
account = stripe.Account.create(
type='express',
country='RU',
email=creator.email,
capabilities={
'card_payments': {'requested': True},
'transfers': {'requested': True},
},
)
creator.stripe_account_id = account.id
creator.save()
# Ссылка на onboarding
link = stripe.AccountLink.create(
account=account.id,
refresh_url='https://site.com/dashboard/connect/refresh',
return_url='https://site.com/dashboard/connect/complete',
type='account_onboarding',
)
return link.url
def transfer_to_creator(campaign, net_amount):
"""net_amount = собранная сумма минус комиссия платформы"""
transfer = stripe.Transfer.create(
amount=int(net_amount * 100),
currency=campaign.currency.lower(),
destination=campaign.creator.stripe_account_id,
metadata={'campaign_id': str(campaign.id)},
)
return transfer
Фронтенд: прогресс-бар и real-time обновления
// Прогресс кампании через Server-Sent Events
// GET /api/campaigns/:id/progress
export async function* campaignProgressStream(campaignId: string) {
while (true) {
const stats = await getCampaignStats(campaignId)
yield `data: ${JSON.stringify(stats)}\n\n`
await sleep(10_000) // обновляем каждые 10 секунд
}
}
// React hook
function useCampaignProgress(campaignId: string) {
const [progress, setProgress] = useState<CampaignStats | null>(null)
useEffect(() => {
const source = new EventSource(`/api/campaigns/${campaignId}/progress`)
source.onmessage = (e) => setProgress(JSON.parse(e.data))
return () => source.close()
}, [campaignId])
return progress
}
Комиссионная модель
Стандарт: платформа берёт 5–8% от собранной суммы плюс транзакционная комиссия Stripe (1.4% + 25₽ для европейских карт, 2.9% + 30¢ для остальных). Логика списания платформенной комиссии через application_fee_amount при создании PaymentIntent:
intent = stripe.PaymentIntent.create(
amount=10_000, # 100 рублей
currency='rub',
application_fee_amount=600, # 6% платформенная комиссия
transfer_data={'destination': creator.stripe_account_id},
)
Сроки
MVP краудфандинговой платформы (KIA-модель, Stripe, кампании, награды, базовый личный кабинет): 6–8 недель. Полная AON-платформа с Connect, stretch goals, email-уведомлениями и аналитикой: 3–4 месяца.







