Собрали онлайн-мероприятие с нуля. Веб-приложение в виде чат-бота, в формате игры. За сутки через него прошло 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.


Самое сложное оказалось не там, где ждали. Не чат, не игра и не нагрузка, а консистентная выдача конечных подарков, когда за последний экземпляр борются сразу несколько человек.