
Собрали онлайн-мероприятие с нуля. Веб-приложение в виде чат-бота, в формате игры. За сутки через него прошло 1904 человека, все на одном инстансе за $5 с 1 GB памяти. На подготовку было 2 недели.
Привет, Хабр!
Кванториум — это федеральный проект, цель которого обучать детей по техническим направлениям, в том числе и IT. Там проходит офлайн мероприятие «Ночь в Технопарке», детей знакомят с направлениями, часть потом записывается на занятия. Обычно офлайн, но из-за ковида так было нельзя. Отменять не хотелось, это живые заявки на обучение.
Оставался онлайн. Первой мыслью было собрать чат-бота на готовом конструкторе — быстро и понятно, все сидят в мессенджерах. Но конструктор не давал главного — игровой механики с баллами, профиля, своей админки. Поэтому взялся писать свое веб-приложение с интерфейсом мессенджера.
Задача и ограничения
- Сроки: 2 недели на разработку и запуск
- Бюджет: $5 в месяц на хостинг
- Неизвестная нагрузка: открытое мероприятие, аудитория разная — дети, родители, случайные люди. Заранее не спрогнозируешь, сколько придет и когда
- UX для нетехнической аудитории: многие видят продукт впервые, интерфейс должен быть понятен без инструкции
Почему свое приложение, а не готовый бот
Главный довод — мультиканальность. Люди приходили из разных мессенджеров, а бота под каждый пришлось бы вести отдельно и с урезанными под платформу фичами. Одна веб-платформа открывается по одной ссылке из любого канала — один код, один интерфейс. Сверху — своя игровая механика, баллы, профиль со ссылкой на запись, админка, выгрузка данных. Минус — писал сам и в те же 2 недели.
Как устроена игра
Игра из трех блоков. Сначала выбираешь одно из 8 направлений и отвечаешь на вопросы по нему. Потом угадываешь, какие проекты сделаны в технопарке — подвох в том, что все. В конце блок рефлексии, оставляешь отзыв. За правильные ответы капают баллы, на них в финале берешь подарок.

Архитектура
- Фронтенд: Vue.js, Vue Apollo, Vuex
- Бэкенд: Node.js, Express, Apollo Server (apollo-server-express), MongoDB Atlas, Redis
- Аутентификация: JWT — access на 15 минут, refresh на 30 дней
- Транспорт: Full WebSocket Transport (без REST-поверх-HTTP)
- Хостинг: DigitalOcean, $5 — 1 GB RAM / 25 GB SSD / Ubuntu 18.04.3 LTS x64
Mongo Atlas взял ради скорости старта. Управляемая база без DevOps, бесплатный тариф, админка из коробки. Из минусов — документная модель тянет денормализацию, данные дублируются и база растет быстрее, чем на Postgres. Для двух недель нормальный размен.
Все приложение realtime — клиент и сервер держат постоянный канал по WebSocket и синхронизируют состояние на лету. Лучше всего это видно на каталоге подарков. Позиции конечные, разбираются параллельно, забрали последний — и он сразу пропадает у всех, у кого открыт список. Сделано на GraphQL-подписках (Apollo) поверх Redis PubSub — подписки готовы жить на нескольких инстансах, хотя ивент отработал на одном.
Аутентификация на сокете — один раз, при установке. Access-токен уходит в connectionParams и проверяется в onConnect, дальше соединение живет авторизованным. Протух access — клиент берет новый по refresh и переподключается.
Верстка
Большая часть трафика идет с телефонов, поэтому верстал mobile-first. Базовый лейаут под мобильный, десктоп как расширение. Стили на Spectre.css. Проверял на живых телефонах, не только в эмуляции DevTools. Трафик в итоге разделился 64/36 в пользу мобилки.
Экономика баллов и подарков: где была гонка
Подарки выросли из мелочи. В профиле лежала ссылка на запись, и мысль пошла дальше. Раз можно вставить ссылку, почему не призы от партнеров? Партнер получает клиентов, игрок — мотивацию доиграть. Так набралось 1400 призов от 40 партнеров — среди них Ростелеком, ПРИО, «Мой бизнес», РГУ. Призы были настоящие и разные — от сета роллов и корзины сладостей до похода к стоматологу на 3000 ₽ и сертификатов на обучение. По условию партнеров после регистрации пользователю принудительно показывался их список, а логотипы шли и в самих подарках.

Дальше механика простая. 10 баллов за правильный ответ, максимум 110 за игру. На баллы берешь один подарок. Число призов конечное. Остаток по позиции падает до нуля — позиция пропадает из списка. Сертификаты без лимита.
Списание подарка — это read-modify-write. Читаешь остаток, проверяешь что больше нуля, уменьшаешь, пишешь обратно. Если держать проверку в коде между чтением и записью, влезет второй запрос — и последний подарок уйдет сразу двоим. Плюс правило «один в руки, остаток баллов сгорает» — это уже стейт юзера, его тоже надо защитить от дабл-клика.
Остаток подарка закрыл атомарным findOneAndUpdate с условием stock > 0. Mongo проверяет и декрементит за одну операцию, второй запрос видит ноль и уходит ни с чем, в минус не уйдет. Тонкость в том, что findOneAndUpdate атомарен только в пределах одного документа, а тут их два — подарок и стейт юзера (флаг «выдано» плюс баллы). Чтобы оба менялись согласованно, это правильно заворачивать в транзакцию — Atlas работает как replica set, транзакции доступны. Сегодня я бы сразу взял транзакцию.
При 1904 юзерах за сутки шанс реально влететь в саму гонку мал, но закладываться на нее надо.
UX-деталь: эмодзи
Когда собрал фронтенд, бот читался холодно и безлично. Прошелся по репликам, где цепляло эмоцию, добавил эмодзи и аватарку. В отзывах «дружелюбный бот» стал ответом номер один, 35.7%.

Аналитика
Отчет нужен был партнерам и нам. Выгружали в Excel через vue-json-excel — имя, почта, регион, баллы, подарок, партнер, направление, отзыв.
География. Подарки давали только жителям региона, при регистрации стоял чекбокс.
- Регион — 76.2%
- Остальная Россия — 23.8%
Устройства
- Мобайл — 64%
- Десктоп — 36%
Что выбирали из направлений
- Математика — 25.6%
- Робоквантум — 16.3%
- Английский — 14.7%
- IT-квантум — 11%
- Наноквантум — 10.7%
- Промдизайн — 9.3%
- Аэроквантум — 6.8%
- Шахматы — 5.5%
Отзывы. В конце игры блок рефлексии: можно выбрать готовый вариант или написать свое.
- Дружелюбный бот — 35.7%
- Задания — 27%
- Подарки — 14.8%
- Интерфейс — 9.8%
- Видео-приветствие — 9.3%
- Свои комменты — 0.1%, из них 3 негативных
Грабли
- Письма о подарке доходили не всегда: После выбора подарка письмо уходило само, но часть не доезжала. Кнопки «отправить повторно» не было, человек оставался без подтверждения
- Баллы сгорали полностью: Ждали много народу, поэтому решили: один подарок в руки, остаток сгорает. Перевести или доложить нельзя, часть людей это бесило
- Нормальной админки не было: CRUD по пользователям, компаниям и подаркам не успели, правили вручную
Что бы переделал
- Админ-панель с CRUD пользователей, компаний, подарков
- Идемпотентная отправка писем + кнопка «отправить повторно» в профиле
- Перевод баллов между пользователями и/или несколько подарков на остаток
- Смена региона в профиле (меняется только имя)
Итог
За сутки через бота прошло 1904 человека — дети, родители, случайные люди, для многих технопарк был в новинку. Собрали их через группы в разных мессенджерах, там висели посты про партнеров и подарки. На выходе 163 лида, все на одном инстансе за $5.
Самое сложное оказалось не там, где ждали. Не чат, не игра и не нагрузка, а консистентная выдача конечных подарков, когда за последний экземпляр борются сразу несколько человек.

