Розыгрыши для кафе: онлайн-оплата и математически проверяемая честность
Кафе вело розыгрыши вручную и постоянно слышало «вы сами себе накрутили». Мы сделали веб-систему, где гость выбирает номерной билет, платит онлайн, а победитель определяется так, что подкрутить результат физически невозможно.
К нам обратилось кафе, которое регулярно проводило розыгрыши среди гостей. Схема была привычной: пост в соцсетях, номерные билеты, а в финале — победитель, выбранный онлайн-генератором случайных чисел «вживую» на камеру. Работало это ровно до того момента, пока кто-то из участников не писал в комментариях: «Да вы сами себе накрутили, знаем мы эти генераторы». Доказать обратное было нечем. Каждый розыгрыш вместо праздника превращался в спор о честности. Мы предложили закрыть этот вопрос не словами, а математикой — так, чтобы недоверие стало технически невозможным. В этом кейсе рассказываем, как устроена веб-система розыгрышей, которую мы сделали под ключ, и почему её результат нельзя подделать даже самому владельцу кафе.
Проблема ручных розыгрышей
Ручной розыгрыш кажется простым только на поверхности. На деле в нём собирается сразу несколько болей, и все они бьют по репутации заведения. Первая и главная — недоверие. Когда организатор сам крутит генератор, у аудитории нет способа убедиться, что результат не выбран заранее. Даже если кафе абсолютно честное, доказать это невозможно: любой скриншот и любую трансляцию при желании можно объявить постановкой. Доверие держится на честном слове, а честное слово в интернете стоит недорого.
Вторая боль — учёт билетов. Продажа номерных билетов вручную означает таблицы, переписки, сверки: кто оплатил, кто занял номер, не продали ли один и тот же билет двум людям. При росте числа участников это превращается в источник ошибок и конфликтов. Третья боль — оплата. Собирать деньги переводами на карту неудобно и непрозрачно: приходится вручную сопоставлять поступления с участниками, а спорные ситуации решать на доверии.
Мы сформулировали задачу так: гость заходит на сайт, выбирает свободный номерной билет как место в кинозале, оплачивает его онлайн, и с этого момента всё происходит автоматически. Победитель определяется без участия человека и так, чтобы каждый участник мог лично проверить корректность результата. Никакой регистрации, никаких личных кабинетов — только имя и телефон, чтобы с победителем можно было связаться.
Билет как место в кино: атомарное резервирование
Интерфейс выбора билета мы сделали по знакомой всем метафоре — сетка номеров, как схема зала при покупке билетов в кино. Свободные номера доступны для клика, занятые подсвечены и недоступны. Гость выбирает номер, вводит имя и телефон и переходит к оплате. Всё интуитивно понятно, и именно в этой простоте прячется первая серьёзная техническая задача.
Представьте: до конца продаж остаётся один свободный билет с номером 42, и два гостя одновременно нажимают на него. Если система работает по наивной схеме «прочитал статус — увидел, что свободен — записал бронь», то оба запроса успеют прочитать статус «свободен» раньше, чем любой из них запишет бронь. В результате один билет продаётся двум людям. Это классическая ситуация гонки (race condition), и в деньгах она превращается в конфликт, скандал и возврат средств.
Мы решили это на уровне базы данных, а не приложения. Резервирование билета выполняется одной атомарной операцией вида UPDATE tickets SET status='RESERVED', guest=... WHERE id=42 AND status='FREE' RETURNING *. Ключевое здесь — условие WHERE status='FREE' внутри самого запроса на изменение. База данных гарантирует, что такая операция выполняется неделимо: только один из двух одновременных запросов реально изменит строку и получит её обратно через RETURNING. Второй запрос не найдёт строку в состоянии FREE и вернёт пустой результат — его гость мгновенно увидит, что билет только что заняли, и выберет другой.
Паттерн «прочитал — проверил — записал» мы в этом сценарии полностью запретили себе на уровне архитектурного принципа. Проверка и запись должны быть одной операцией, иначе между ними всегда остаётся щель, в которую проскакивает вторая транзакция. Это не оптимизация «на всякий случай» — это единственный корректный способ продавать ограниченный ресурс в конкурентной среде.
Оплата: вебхук как источник правды
Оплату мы подключили через онлайн-эквайринг банка. И здесь есть тонкость, которую часто недооценивают. Когда гость оплачивает картой, платёжный шлюз возвращает его обратно на сайт по так называемому редиректу — переходу на страницу «спасибо, оплата прошла». Соблазн велик: раз пользователь вернулся на страницу успеха, значит, можно считать билет оплаченным. Это ошибка.
Редирект — ненадёжный сигнал. Пользователь может закрыть вкладку сразу после оплаты и не вернуться. Может, наоборот, вручную открыть адрес страницы успеха, вообще не заплатив. Соединение может оборваться. Полагаться на возврат браузера как на подтверждение оплаты — значит строить финансовую логику на песке.
Поэтому единственным источником правды об оплате мы сделали серверный вебхук — запрос, который платёжный шлюз отправляет напрямую нашему серверу, минуя браузер гостя. Именно этот запрос, а не редирект, переводит билет в статус «оплачен». У вебхука две обязательные защиты. Первая — проверка подписи. Каждое уведомление шлюз подписывает секретным ключом по алгоритму SHA-256, и наш сервер пересчитывает подпись самостоятельно. Если подпись не совпадает, запрос отвергается: значит, его прислал не шлюз, а кто-то, кто пытается выдать себя за него.
Вторая защита — идемпотентность. Платёжные шлюзы по своей природе могут прислать одно и то же уведомление несколько раз (например, если не получили от нас быстрый ответ и повторили попытку). Наша обработка устроена так, что повторный вебхук по уже оплаченному билету не делает ничего вредного: статус меняется на «оплачен» только один раз, а все последующие уведомления просто подтверждаются без побочных эффектов. Билет не может «оплатиться дважды», участник не может задвоиться в списке.
Commit-reveal: проверяемая честность
Это сердце проекта и главный ответ на боль недоверия. Схема commit-reveal (по-русски «зафиксировал — раскрыл») пришла из криптографии и позволяет доказать, что результат был предопределён случайностью, а не выбран вручную постфактум. Разберём её простым языком.
Перед стартом продаж система генерирует случайное секретное зерно — длинную непредсказуемую строку, назовём её seed. Из этого зерна вычисляется его хеш: commitHash = SHA-256(seed). Хеш — это как отпечаток пальца строки: по нему нельзя восстановить саму строку, но по строке всегда можно пересчитать и сверить хеш. Этот commitHash кафе публикует публично ещё до того, как продан хоть один билет. Это и есть «фиксация обязательства»: кафе как будто кладёт в запечатанный конверт заранее заготовленную инструкцию по выбору победителя и показывает всем печать на конверте.
Продажи идут своим чередом. Когда розыгрыш закрывается, фиксируется момент закрытия — closedAt. И вот теперь кафе раскрывает то самое зерно seed. Любой желающий первым делом проверяет: SHA-256 от раскрытого seed должен в точности совпасть с опубликованным заранее commitHash. Совпал — значит, зерно то самое, его не подменили после начала продаж.
Победитель вычисляется детерминированной формулой, одинаковой для всех: берётся хеш SHA-256(seed + ":" + closedAt), интерпретируется как большое число, и от него берётся остаток от деления на количество оплаченных билетов N. Полученный индекс указывает на билет-победитель в упорядоченном списке оплаченных билетов: winner = paidTickets[ parseInt(SHA-256(seed + ":" + closedAt), 16) mod N ]. Всё. Ни одного «ручного» шага.
Почему это невозможно подкрутить? Потому что commitHash опубликован до продаж, а значит, зерно seed зафиксировано ещё тогда, когда кафе не знало ни числа участников, ни того, кто какой билет купит. Чтобы «подогнать» победителя под нужного человека, кафе пришлось бы подобрать другое зерно — но тогда его хеш не совпал бы с опубликованным ранее commitHash, и подмена вскрылась бы мгновенно. Математически кафе загнало само себя в рамки: оно связано обязательством, которое дало публично и заранее. Любой гость с калькулятором хешей может независимо повторить всю цепочку и убедиться, что победитель именно тот, кого выдаёт формула.
Гость без регистрации
Отдельным требованием была предельная простота участия. Никаких аккаунтов, паролей и подтверждений почты — всё это отпугивает и снижает число участников. Гость идентифицируется только именем и номером телефона: имя, чтобы объявить победителя по-человечески, телефон, чтобы с ним связаться и вручить приз. Эти данные привязываются к билету в момент атомарного резервирования и хранятся ровно для одной цели — контакт с победителем.
Такой подход снимает и юридическую, и продуктовую нагрузку: мы не строим базу пользователей, не храним лишнего, не заводим личные кабинеты, которые нужно поддерживать и защищать. Порог входа — минимальный: увидел билет, выбрал номер, ввёл имя и телефон, оплатил. Меньше шагов — больше участников, а значит, и больше сбор.
Архитектура и стек
Систему мы сделали как полноценное веб-приложение с серверной логикой и базой данных — без конструкторов и без хрупких связок из сторонних сервисов. Именно потому, что в проекте есть деньги и есть требование к честности, критично контролировать каждый шаг самостоятельно. Билеты, их статусы, привязки к гостям, факты оплаты и параметры розыгрыша (commitHash, seed, closedAt) хранятся в реляционной базе, где атомарные операции и транзакции — родная возможность, а не костыль.
Серверная часть отвечает за три критичных контура: атомарное резервирование билета, приём и верификацию вебхуков эквайринга и вычисление победителя по commit-reveal. Клиентская часть — это витрина розыгрыша и интерактивная сетка выбора билета. Публичную страницу проверки честности мы сделали так, чтобы на ней были видны и заранее опубликованный commitHash, и раскрытый после розыгрыша seed, и момент closedAt, и итоговая формула — всё, что нужно любому гостю, чтобы пересчитать результат вручную.
Мы намеренно не выносили финансовую и «честностную» логику на сторону клиента: браузеру нельзя доверять определение оплаты или выбор победителя. Всё, что влияет на деньги и результат, живёт на сервере и проверяется на сервере. Клиент только показывает и отправляет запросы.
Что получилось
Кафе получило инструмент, который снял главную головную боль — споры о честности. Теперь на любую реплику «вы накрутили» есть простой ответ: вот хеш, опубликованный до продаж, вот раскрытое зерно, вот формула — проверьте сами. Розыгрыш перестал держаться на доверии к организатору и стал держаться на математике, а её оспорить нельзя.
Учёт билетов стал автоматическим и защищённым от двойных продаж, оплата — прозрачной и надёжной за счёт серверных вебхуков, участие — максимально простым. Ручные таблицы, сверки переводов и нервные объяснения в комментариях ушли в прошлое. Кафе занимается своим делом, а система сама продаёт билеты, принимает оплату и честно выбирает победителя.
Частые вопросы
Можно ли всё-таки подтасовать победителя, если очень захотеть? Нет — при условии, что commitHash опубликован публично до старта продаж. Чтобы сдвинуть результат на нужного человека, пришлось бы подобрать другое зерно, но его хеш не совпадёт с опубликованным заранее, и подмена сразу вскроется. В этом и смысл схемы: организатор связывает себя обязательством, данным до того, как узнал состав участников.
Зачем нужен серверный вебхук, если после оплаты пользователя и так возвращают на сайт? Потому что возврат в браузер (редирект) легко потерять или подделать: вкладку можно закрыть, соединение оборвать, а адрес страницы успеха открыть вручную без оплаты. Надёжное подтверждение оплаты приходит только серверным запросом от шлюза, подписанным ключом и проверяемым на нашей стороне.
Что произойдёт, если два человека кликнут на один билет в одну секунду? Билет достанется тому, чей запрос первым выполнит атомарное обновление строки с условием «статус свободен». Второй мгновенно получит отказ и увидит, что номер занят. Двойная продажа исключена на уровне базы данных.
Нужно ли гостю регистрироваться? Нет. Достаточно имени и телефона — они нужны только для связи с победителем. Ни аккаунтов, ни паролей, ни личных кабинетов.
Коротко о главном
Розыгрыш с реальными деньгами — это не столько про красивый интерфейс, сколько про доверие и корректность. Мы закрыли обе задачи инженерно: атомарное резервирование защищает от двойных продаж, серверный вебхук с проверкой подписи и идемпотентностью делает оплату надёжной, а схема commit-reveal превращает честность из обещания в математически проверяемый факт.
Если вы проводите розыгрыши, конкурсы или продаёте ограниченный ресурс онлайн и устали от споров о честности — эту проблему можно решить раз и навсегда. Мы делаем такие системы под ключ: от витрины выбора билета до публичной страницы проверки результата.
Что я делаю под ключ
- Системы розыгрышей и продажи билетов
- Онлайн-оплата и эквайринг с вебхуками
- Проверяемая честность на commit-reveal
- Надёжная серверная логика и БД
- Веб-приложения без конструкторов
Бесплатно: чек-лист «Готов ли ваш бизнес к 152-ФЗ»
12 пунктов, которые проверяют готовность за час: данные, согласия, уведомление в РКН, локализация, защита. Отметьте, что уже сделано, и увидите дыры, за которые сейчас штрафуют.
Готовы обсудить вашу задачу?
Бесплатная консультация — разберём, как внедрить это в вашем бизнесе под ключ. Без форм, пишите напрямую.


