Разработка платформы для продажи билетов (Ticketing)

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Услуги, которые мы предлагаем
Показано 1 из 1Все 2062 услуг
Разработка платформы для продажи билетов (Ticketing)
Сложный
от 2 недель до 3 месяцев
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.webp
    Разработка сайта компании B2B ADVANCE
    1358
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1251
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    956
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1188
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    929
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Разработка веб-сайта для компании ФИКСПЕР
    947

Разработка платформы для продажи билетов (Ticketing)

Представьте: старт продаж на концерт топ-исполнителя — в первые минуты систему атакуют десятки тысяч запросов. Без правильной архитектуры база данных падает, а места продаются дважды. Наша команда проектирует системы, которые выдерживают до 100 000 RPS и гарантируют, что каждое место продаётся строго один раз. Если два пользователя одновременно нажимают «Купить», без надёжной блокировки система продаст одно место дважды. Возврат денег и испорченная репутация — лишь вершина айсберга. Мы проектируем высоконагруженные тикетинговые системы, которые выдерживают пиковые нагрузки в десятки тысяч запросов в секунду, гарантируя консистентность.

Наш опыт показывает, что ключевая проблема — борьба с race condition на уровне базы данных. Наивный SELECT → UPDATE без блокировки приводит к состоянию гонки. Правильный подход — использовать SELECT FOR UPDATE SKIP LOCKED для захвата мест без ожидания, а освобождение просроченных броней поручить фоновым задачам. Пессимистичная блокировка с SKIP LOCKED в 5 раз надёжнее оптимистичной при пиковых нагрузках.

Проблемы, которые решает грамотная архитектура — разработка платформы для

  • Двойные продажи – когда система не блокирует строку при резервировании, два покупателя могут получить один и тот же билет. Последствия: возврат средств, потеря комиссий и доверия.
  • Протухающие резервации – если пользователь не завершил оплату в течение 15 минут, места должны автоматически возвращаться в продажу. Без Celery beat и атомарного обновления счётчиков это превращается в ад.
  • Масштабирование при пиковых нагрузках – например, старт продаж на концерт топ-исполнителя. База должна выдерживать тысячи одновременных запросов, не перегружая CPU и не вызывая взаимоблокировок.

Как проектируется схема мест и резервирования

Мы строим схему базы данных на основе PostgreSQL с использованием UUID, расширенных ограничений и индексов. Пример DDL для ядра платформы:

CREATE TABLE events (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    organizer_id    UUID NOT NULL REFERENCES users(id),
    title           VARCHAR(300) NOT NULL,
    slug            VARCHAR(300) UNIQUE NOT NULL,
    venue_id        UUID REFERENCES venues(id),
    starts_at       TIMESTAMPTZ NOT NULL,
    ends_at         TIMESTAMPTZ,
    status          VARCHAR(20) NOT NULL DEFAULT 'draft'
                    CHECK (status IN ('draft','on_sale','sold_out','cancelled','completed')),
    timezone        VARCHAR(50) NOT NULL DEFAULT 'Europe/Moscow',
    created_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE TABLE ticket_types (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    event_id        UUID NOT NULL REFERENCES events(id),
    name            VARCHAR(200) NOT NULL,   -- 'VIP', 'Стандарт', 'Студенческий'
    price           NUMERIC(12,2) NOT NULL,
    currency        CHAR(3) NOT NULL DEFAULT 'RUB',
    total_qty       INTEGER NOT NULL,
    reserved_qty    INTEGER NOT NULL DEFAULT 0,
    sold_qty        INTEGER NOT NULL DEFAULT 0,
    sale_starts_at  TIMESTAMPTZ,
    sale_ends_at    TIMESTAMPTZ,
    max_per_order   INTEGER NOT NULL DEFAULT 10,
    CONSTRAINT qty_valid CHECK (sold_qty + reserved_qty <= total_qty)
);

CREATE TABLE seats (
    id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    ticket_type_id  UUID NOT NULL REFERENCES ticket_types(id),
    section         VARCHAR(50),
    row             VARCHAR(10),
    number          VARCHAR(10),
    status          VARCHAR(20) NOT NULL DEFAULT 'available'
                    CHECK (status IN ('available','reserved','sold','blocked')),
    reserved_until  TIMESTAMPTZ,    -- когда истекает резервация
    order_id        UUID,
    UNIQUE (ticket_type_id, section, row, number)
);

CREATE INDEX idx_seats_available
    ON seats(ticket_type_id, status)
    WHERE status = 'available';

Этот дизайн обеспечивает быстрый поиск доступных мест через частичный индекс idx_seats_available. Поле reserved_until позволяет легко отчищать просроченные брони.

Как избежать двойной продажи билетов?

В основе надёжности лежит SELECT FOR UPDATE SKIP LOCKED. Этот механизм захватывает только те строки, которые не заняты другими транзакциями, и пропускает уже заблокированные. Реализация на Django:

from django.db import transaction
from django.utils import timezone
from datetime import timedelta


def reserve_seats(ticket_type_id: str, qty: int, session_id: str) -> list:
    """
    Резервируем N мест на 15 минут для сессии покупки.
    Возвращает список seat_id или raise если недостаточно.
    """
    with transaction.atomic():
        # Захватываем строки без ожидания — другие транзакции пропускают занятые
        seats = (
            Seat.objects
            .select_for_update(skip_locked=True)
            .filter(
                ticket_type_id=ticket_type_id,
                status='available'
            )
            .order_by('section', 'row', 'number')[:qty]
        )

        if len(seats) < qty:
            raise InsufficientSeatsError(
                f'Доступно {len(seats)} мест, запрошено {qty}'
            )

        seat_ids = [seat.id for seat in seats]
        reserved_until = timezone.now() + timedelta(minutes=15)

        Seat.objects.filter(id__in=seat_ids).update(
            status='reserved',
            reserved_until=reserved_until,
            order_id=None,  # будет заполнено после оплаты
        )

        TicketType.objects.filter(id=ticket_type_id).update(
            reserved_qty=F('reserved_qty') + qty
        )

        # Сохраняем в Redis для быстрого доступа
        redis_client.setex(
            f'reservation:{session_id}:{ticket_type_id}',
            900,  # 15 минут в секундах
            json.dumps(seat_ids)
        )

        return seat_ids

Код резервирует места на 15 минут с атомарным обновлением счётчиков reserved_qty и сохраняет информацию в Redis для быстрого доступа. Если мест не хватает — выбрасывается исключение, и фронтенд уведомляет пользователя.

Что делать с просроченными резервациями?

Фоновая задача Celery beat запускается каждые 2 минуты, находит истекшие резервации с order_id IS NULL, освобождает места и уведомляет клиентов через WebSocket:

@shared_task
def release_expired_reservations():
    """Celery beat: каждые 2 минуты"""
    now = timezone.now()
    expired_seats = Seat.objects.filter(
        status='reserved',
        reserved_until__lt=now,
        order_id__isnull=True
    )

    seat_ids = list(expired_seats.values_list('id', flat=True))
    if not seat_ids:
        return

    # Группируем по ticket_type для обновления счётчиков
    type_counts = (
        Seat.objects
        .filter(id__in=seat_ids)
        .values('ticket_type_id')
        .annotate(cnt=Count('id'))
    )

    with transaction.atomic():
        Seat.objects.filter(id__in=seat_ids).update(
            status='available',
            reserved_until=None,
        )
        for row in type_counts:
            TicketType.objects.filter(id=row['ticket_type_id']).update(
                reserved_qty=F('reserved_qty') - row['cnt']
            )

    # Уведомляем через WebSocket — билеты снова доступны
    for row in type_counts:
        channel_layer.group_send(
            f'event_{row["ticket_type_id"]}',
            {'type': 'seats_released', 'count': row['cnt']}
        )

Это гарантирует, что «зависшие» билеты быстро возвращаются в продажу, не требуя ручного вмешательства.

QR-код билета и верификация на входе

Для защиты от подделок мы используем HMAC-подписанные токены. Генерация и проверка:

Пример кода для генерации QR
import qrcode
import hmac
import hashlib
from io import BytesIO


def generate_ticket_token(ticket_id: str, secret: str) -> str:
    """HMAC-подписанный токен для QR-кода"""
    msg = f'ticket:{ticket_id}'.encode()
    signature = hmac.new(secret.encode(), msg, hashlib.sha256).hexdigest()
    return f'{ticket_id}:{signature[:16]}'


def verify_ticket_token(token: str, secret: str) -> str | None:
    """Возвращает ticket_id если токен валиден, иначе None"""
    parts = token.split(':')
    if len(parts) != 2:
        return None

    ticket_id, provided_sig = parts
    expected_token = generate_ticket_token(ticket_id, secret)
    expected_sig = expected_token.split(':')[1]

    if hmac.compare_digest(provided_sig, expected_sig):
        return ticket_id
    return None


def generate_qr_image(ticket) -> bytes:
    token = generate_ticket_token(str(ticket.id), settings.TICKET_SECRET)
    qr = qrcode.QRCode(version=1, error_correction=qrcode.constants.ERROR_CORRECT_H)
    qr.add_data(f'https://tickets.site.com/verify/{token}')
    qr.make(fit=True)
    img = qr.make_image(fill_color='black', back_color='white')
    buf = BytesIO()
    img.save(buf, format='PNG')
    return buf.getvalue()

API для сканеров на входе:

# POST /api/v1/tickets/verify
def verify_ticket_view(request):
    token = request.data.get('token')
    ticket_id = verify_ticket_token(token, settings.TICKET_SECRET)

    if not ticket_id:
        return Response({'valid': False, 'reason': 'invalid_token'}, status=400)

    ticket = Ticket.objects.select_related('event', 'seat').get(id=ticket_id)

    if ticket.scanned_at:
        return Response({
            'valid': False,
            'reason': 'already_used',
            'scanned_at': ticket.scanned_at.isoformat(),
        }, status=400)

    if ticket.event.starts_at.date() != date.today():
        return Response({'valid': False, 'reason': 'wrong_date'}, status=400)

    ticket.scanned_at = timezone.now()
    ticket.save()

    return Response({
        'valid': True,
        'holder': ticket.holder_name,
        'seat': f'{ticket.seat.section} ряд {ticket.seat.row} место {ticket.seat.number}',
        'ticket_type': ticket.ticket_type.name,
    })

На входе контролёр сканирует QR-код, приложение отправляет POST-запрос к API. Сервер проверяет подпись, статус билета и дату мероприятия. Если билет уже использован или дата не совпадает — возвращается ошибка.

Процесс разработки платформы

  1. Аналитика – фиксируем требования: типы мероприятий, схема зала, способы оплаты, интеграции.
  2. Проектирование схемы БД – определяем индексы, ограничения, выбираем между типами блокировок.
  3. Разработка бэкенда – API на Django REST или FastAPI, Celery для фоновых задач, WebSocket для real-time.
  4. Интеграция платёжного шлюза – ЮKassa, Stripe или Robokassa, настройка webhook для подтверждения оплаты.
  5. Тестирование под нагрузкой – используем locust для симуляции пиковых продаж.
  6. Деплой и мониторинг – Docker + Kubernetes, Prometheus + Grafana.

Сравнение подходов к блокировке мест

Метод Производительность Риск race condition Сложность реализации
Оптимистичная блокировка Высокая Средний (requires retry) Низкая
Пессимистичная (SELECT FOR UPDATE) Средняя Низкий Средняя
Очереди (RabbitMQ) Высокая Низкий Высокая

Мы выбираем пессимистичную блокировку с SKIP LOCKED как наилучший баланс для тикетинга.

Сравнение технологий бэкенда

Технология Производительность Скорость разработки Сообщество
Django Средняя Высокая Большое
FastAPI Высокая Высокая Растущее
Node.js (Express) Высокая Средняя Большое

Что входит в результат

  • MVP за 4–6 недель – базовая платформа без интерактивного плана зала, но с типами билетов, бронированием и оплатой.
  • Полная система за 3–4 месяца – с SVG-схемой зала, динамическим ценообразованием, сканером QR и личным кабинетом организатора.
  • Исходный код с документацией API (OpenAPI).
  • Инструкции по администрированию и обучение команды заказчика.
  • Гарантийная поддержка на 30 дней после запуска.

Наши компетенции

Более 7 лет разрабатываем highload-системы для мероприятий. В портфолио — проекты с пиковой нагрузкой 10 000 RPS и 100 000 проданных билетов за одну минуту. Мы гарантируем консистентность данных и отсутствие двойных продаж при правильной настройке.

Получите консультацию по архитектуре вашего проекта — наши инженеры проанализируют текущую систему и предложат оптимизацию. Для обсуждения вашего проекта свяжитесь с нами.

Интеграция платёжных систем: ЮKassa, Stripe, PayPal, Apple Pay, Google Pay

Конверсия упала на 12% сразу после редизайна. Команда запулила новый SPA-чекаут на Vue 3, забыв про обработку fallback-сценариев. Sentry зафиксировал шквал ошибок: Payment method not available, 3DS2 challenge flow failed, webhook signature verification failed. Пользователи бросали корзину на этапе выбора способа оплаты. Проверка показала, что Stripe Elements не получал корректный clientSecret после редиректа, а webhook-эндпоинт отвечал 500 из-за отсутствия идемпотентности. После замены checkout-формы на кастомную интеграцию с раздельным хранением event ID в Redis ошибки ушли, конверсия восстановилась за двое суток. Задача не в том, чтобы «подключить SDK» — платёжка требует синхронизации с требованиями банков, SCA в Европе и 54-ФЗ в России. Наш опыт — 7 лет интеграций для 50+ проектов, от интернет-магазинов до SaaS-платформ с миллионными оборотами.

Что входит в работу под ключ

  • Аудит текущего payment flow и требований (валюты, фискализация, подписки).
  • Выбор провайдера с учётом географии и бизнес-модели.
  • Backend-интеграция (Laravel/Node.js/Go) с обработкой webhook'ов, идемпотентностью и ретраями.
  • Frontend-виджет (Stripe Elements / ЮKassa SDK) с поддержкой Apple Pay и Google Pay.
  • Тестирование всех сценариев: успех, отказ, 3DS, возвраты, чек коррекции.
  • Мониторинг первых транзакций и документация.

Оценим проект за 1 день — для получения консультации напишите в чат.

Сравнение провайдеров: что выбрать

Критерий ЮKassa Stripe PayPal
Валюты RUB только 135+ 25+
Фискализация 54-ФЗ Встроена Нет (нужен ОФД) Нет
Поддержка Apple/Google Pay Через SDK Через PaymentElement Через Braintree
Комиссия за транзакцию 2.5–4% 2.9% + $0.30 2.99% + $0.49
Рекуррентные платежи Через автоплатежи Stripe Billing Reference Transactions
PCI DSS SAQ A (токены) SAQ A (Elements) SAQ A (токены)

Stripe выигрывает по гибкости: 135+ валют против одной у ЮKassa. Но для РФ с 54-ФЗ и СБП ЮKassa в 3 раза быстрее в интеграции — не нужен внешний ОФД. Для подписок Stripe Billing — готовый engine с trial'ами и email-уведомлениями в 2 клика.

Как выбрать подходящего провайдера?

Ключевых точек три. Где живут ваши клиенты? Только РФ — ЮKassa, глобально — Stripe. Нужна ли фискализация по 54-ФЗ? Да — ЮKassa, иначе Stripe + облачный ОФД. Планируете ли подписки? Да — Stripe Billing как эталон, ЮKassa требует собственной логики с автоплатежами. Экономия на комиссиях при выборе правильного провайдера — до 1.5% с оборота. Для проекта с 2 млн ₽ в месяц это 360 000 ₽ в год.

Где прячутся реальные сложности

Подключить тестовый режим — час. Правильно обработать все сценарии — несколько недель.

Webhook надёжность. Webhook может не дойти — сервер недоступен, таймаут, сеть. Провайдер повторяет с экспоненциальным backoff (Stripe — до 3 дней). Обработчик обязан быть идемпотентным: если payment.succeeded придёт дважды с одним payment_id, заказ обновится только раз. Реализуется через хранение event ID в Redis с TTL.

3DS2 и redirect flow. При оплате картой с 3DS2 пользователь уходит на страницу банка, затем возвращается по return_url. За это время сессия могла истечь, корзина очиститься. Статус проверяем не по query-параметрам, а прямым запросом к API провайдера при возврате.

Частичные возвраты и чеки. Клиент вернул часть товаров — нужен чек коррекции (ФНС) и частичный refund в ЮKassa. Stripe делает partial_refund нативно. В обоих случаях синхронизация статусов между платёжкой, БД и складом — отдельная задача.

Валютные ограничения. ЮKassa — только рубли. Если клиент из РФ платит в евро через Stripe, конвертация идёт через его банк, и вы не управляете курсом.

Почему webhook'и требуют идемпотентности?

Webhook может быть доставлен дважды из-за сетевых таймаутов или повторных попыток провайдера. Без идемпотентности второй вызов вызовет дублирование заказа или ошибочное начисление. Решение — сохранять уникальный ID события (например, Stripe event id + timestamp) в Redis с TTL 24 часа и проверять перед обработкой. Если ID уже существует — возвращаем 200, не выполняя бизнес-логику. Типичные ошибки при интеграции webhook'ов: не проверять подпись HMAC (любой может отправить фальшивый payment.succeeded), не использовать очередь (обработчик блокирует ответ — провайдер считает фейлом и шлёт повторно), не сохранять event ID (дубликаты рассинхронизируют статусы).

Как строим интеграцию

Архитектура. Никогда не храним данные карт — только токены провайдера. Flow: Order в БД → Payment Intent → редирект/виджет → webhook подтверждает → обновляем статус. База истины — статус в платёжной системе.

Для Laravel используем stripe/stripe-php или yookassa-sdk. Webhook — отдельный контроллер с VerifyCsrfToken исключением, проверка подписи в первой строке, Queue job для бизнес-логики.

Для Next.js/React — @stripe/stripe-js + @stripe/react-stripe-js. PaymentElement включает Apple/Google Pay автоматически. Пример:

const stripe = await stripePromise;
const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: { return_url: 'https://example.com/order/thank-you' },
});

Тестирование. Stripe CLI: stripe listen --forward-to localhost:8000/webhook. Тест-карты для всех сценариев (3DS, decline, insufficient funds). Cypress-тест checkout flow в CI — обязательная гарантия стабильности.

Мы отлаживали интеграцию Stripe Billing для SaaS с 50 000 подписчиков. Проблема возникла с обработкой invoice.payment_succeeded: фронтенд обновлял подписку сразу после редиректа, но webhook мог задержаться на 10 секунд, и статус перезаписывался на incomplete. Решение — добавить polling API с проверкой статуса инвойса до показа успешной страницы. Это снизило количество ошибочных отписок на 18%.

Процесс и сроки

Аудит → выбор провайдера → backend → frontend → тесты → деплой → мониторинг.

Сценарий Срок
Один провайдер (ЮKassa или Stripe), базовый flow 1–2 недели
Несколько методов оплаты + Apple/Google Pay 2–4 недели
Мультивалютность + частичные возвраты + фискализация 4–8 недель
SaaS подписки через Stripe Billing 3–6 недель

Стоимость рассчитывается индивидуально. Закажите интеграцию, и ваш checkout не упадёт при следующем обновлении.

Ссылки:

Гарантируем: 7 лет опыта, 50+ успешных интеграций. Свяжитесь с нами для аудита вашего checkout'а — мы оценим проект и подберём оптимального провайдера.