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

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

Розробка та обслуговування будь-яких видів сайтів:

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

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

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

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

Часті запитання

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1418
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1286
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    983
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1243
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    998

Розробка платформи для продажу квитків (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 'UAH', 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 проданих квитків за одну хвилину. Ми гарантуємо консистентність даних і відсутність подвійних продажів при правильному налаштуванні.

Отримайте консультацію з архітектури вашого проєкту — наші інженери проаналізують поточну систему та запропонують оптимізацію. Для обговорення вашого проєкту зв'яжіться з нами.