Skip to content
Все статьи
Кейсы8 мин

250 000 регистраций через Telegram-бот на национальном конкурсе

Бот для «Yoshlar ovozi» занимает меньше 300 строк кода. Через него прошло больше 250 тысяч регистраций. Рассказываю, что в нём выдержало нагрузку, что сломалось в данных и как мы переделали всё это в следующих конкурсах.

Mirodil OrtiqovБэкенд-разработчик

Когда Агентство по делам молодёжи пришло с задачей, она звучала просто: нужен бот, через который молодые исполнители со всей страны подадут заявку на вокальный конкурс «Yoshlar ovozi». Имя, адрес, возрастная категория, направление, телефон. Всё.

Простые задачи в масштабе страны перестают быть простыми. Вот цифры, которые накопились в базе бота за все сезоны:

250 330
завершённых регистраций
~44 800
разных пользователей Telegram
4
направления: эстрада, традиционное исполнение, бахшичилик, опера
~5,6
участника в среднем на один аккаунт

Последняя цифра — первое, что удивило нас самих. Об этом ниже.

Почему бот, а не сайт

Аудитория конкурса — молодёжь от 10 до 30 лет, в том числе из районов, где интернет медленный, а сайты открываются через раз. Telegram есть у всех. Его не надо устанавливать, в нём не надо придумывать пароль, а номер телефона можно отправить одной кнопкой.

Сайт с формой здесь проиграл бы по трём пунктам: его надо найти, он долго грузится на слабом 3G, и форму на 7 полей на телефоне заполнять неудобно. В боте каждый вопрос приходит отдельным сообщением, и человек отвечает на него, как в обычной переписке.

Как был устроен бот

Первая версия была очень прямолинейной: aiogram 2, SQLite в одном файле, один процесс на long polling, Docker. Никакого Redis, никакой админки. Состояние регистрации хранилось прямо в таблице заявок, в колонке state.

state = db.get_user_state(user_id)  # последняя заявка этого пользователя
if state == "region":
    if message.text in regions:
        db.update_user_region(user_id, message.text)
        await message.answer("Tuman (shahar)ni tanlang", reply_markup=districts_kb)
elif state == "district":
    db.update_user_district(user_id, message.text)  # без проверки — это ошибка

Каждое нажатие «Boshlash» создавало новую строку. Это сделали специально: один человек мог зарегистрировать нескольких участников подряд. Бот даже подсказывал после успешной заявки: «Если есть другие участники, нажмите Boshlash ещё раз».

Отсюда и среднее 5,6 участника на аккаунт. Мы закладывали это для случаев, когда у ребёнка нет своего Telegram и заявку подаёт родитель или руководитель кружка. Но масштаб такой «групповой» подачи мы не предсказывали.

Y
Yoshlar ovozi
bot
Ishtirok etish va ro‘yxatdan o‘tish uchun Boshlash tugmasini bosing.
Boshlash
F.I.Sh. kiriting
Karimova Madina
Doimiy yashash manzilingiz (hududingizni tanlang)
Toshkent shahri
Samarqand viloyati
Farg‘ona viloyati
Samarqand viloyati
Tuman (shahar)ni tanlang
Samarqand shahri
Urgut tumani
Kattaqo‘rg‘on tumani

Нагрузку этот простой стек выдержал. Каждый шаг регистрации — одна короткая запись в базу, а SQLite с одним писателем справляется с таким потоком без проблем. Узким местом оказался не сервер, а мы сами в одном месте: проверка подписки на каналы.

Сначала бот перед каждым сообщением проверял, подписан ли человек на два канала организаторов. Это два лишних запроса к Telegram API на каждое нажатие, а за все сезоны таких нажатий набрались миллионы. В итоге проверку отключили одной строкой: return True в начале функции. Некрасиво, но честно.

Где сломались данные

Сервер жил, а вот данные — не очень. Когда мы закрыли сезон и посмотрели на выгрузку целиком, картина была такая:

  • в поле «район» оказалось 3 385 разных значений, хотя в Узбекистане около двухсот районов и городов;
  • в поле «махалля» — почти 45 тысяч разных написаний;
  • в поле «направление» кроме четырёх правильных вариантов нашлись «Estrada» с большой буквы, «16-22 yosh» и даже «Sport».

Причина одна. На шаге с районом бот принимал любой текст, а махаллю вообще просили ввести вручную. Клавиатура с кнопками у нас была one_time_keyboard: после одного нажатия она прячется, и часть людей просто печатала ответ сама. Бот послушно сохранял.

Где люди бросали регистрацию

Начатых регистраций было около 274 тысяч, завершённых — 250 тысяч. Около 24 тысяч человек ушли на полпути. Мы посмотрели, на каком шаге они застряли:

Незавершённые регистрации по шагу, на котором человек остановилсязаявок
Ввод имени
9175
Отправка телефона
5155
Выбор направления
5123
Ввод махалли
2043
Выбор области
1077
Выбор района
932
Возрастная категория
685

Самый большой провал — на первом же вопросе. Человек нажал «Boshlash», увидел «F.I.Sh. kiriting» и ушёл. Думаю, часть просто хотела посмотреть, что внутри. Второе место — телефон: кто-то не доверяет кнопке «Отправить номер», кто-то не понимает, что её надо нажать, а не набирать номер вручную.

Махалля — на четвёртом месте, хотя это самый неприятный шаг: надо вспомнить и напечатать официальное название. В следующих ботах мы его переделали первым.

Что на самом деле нужно организаторам

Организаторам не нужна была красивая админка. Им нужны были две вещи: цифры на сегодня и таблица.

Для цифр была команда /getall: бот присылал сводку на текущий момент по ташкентскому времени — сколько всего заявок, по областям, по направлениям, по возрастам. Её отправляли руководству прямо из чата.

Для таблицы была команда /get: pandas собирал заявки в Excel и бот отправлял файл. Никаких логинов и паролей, только проверка, что Telegram ID есть в списке админов.

По распределению всё вышло ожидаемо: эстраду выбрали больше половины участников, следом шло традиционное исполнение (макомы, катта ашула), затем бахшичилик и опера. Самая большая возрастная группа — 16–22 года.

Kitobxon: вторая попытка, уже с базой махаллей

Когда мы делали боты для конкурсов чтения «Yosh Kitobxon» и «Kitobxon Oila», ошибки «Yoshlar ovozi» лежали у нас перед глазами. Поэтому второй заход был совсем другим:

  • Django 4.2 + PostgreSQL вместо SQLite и админка, где организаторы сами меняют тексты, каналы и сроки конкурсов.
  • Справочник из 9 442 махаллей и сёл, загруженный из Excel. Область → район → махалля выбираются только кнопками, и бот отвечает «выберите из предложенных», если пришёл текст.
  • FSM aiogram вместо колонки state в таблице заявок: профиль пользователя отдельно, участие в конкретном конкурсе отдельно.
  • Возраст по дате рождения, а не по выбранной категории. В «Yosh Kitobxon» участвуют от 10 до 30 лет, и бот сам отсекает тех, кто не подходит.
  • Несколько конкурсов в одном боте, у каждого свои даты начала и конца регистрации.
  • Excel по каждому конкурсу отдельно, а не одна общая таблица.

Каскад из 9 442 вариантов в виде кнопок звучит страшно, но в одном районе обычно несколько десятков махаллей, так что клавиатура остаётся разумной. После этого вопросы «что за махалля „Navbaxor MFY 2-uy“?» у организаторов закончились.

Что я бы проверил перед следующим национальным конкурсом

Если к вам придёт похожая задача, вот список, который я теперь держу в голове с первого дня:

  • Поля, по которым будет статистика, принимают только значения из справочника
  • Справочник регионов, районов и махаллей загружен до запуска, а не «потом допишем»
  • Вопрос с самым большим отвалом (обычно первый) сделан максимально коротким и понятным
  • Телефон принимается только кнопкой request_contact, с понятным текстом рядом
  • Проверка подписки на каналы — один раз при старте, а не на каждое сообщение
  • Отдельная команда или админка с цифрами «на сейчас» для организаторов
  • Выгрузка в Excel, которую можно открыть без обработки
  • Один пользователь может подать несколько заявок, и это видно в отчёте

Самое неожиданное в этом проекте: бот на 300 строк не упал под четвертью миллиона заявок. Проблемы были не в коде, который обрабатывал нагрузку, а в коде, который принимал ответы. Если вы тоже собираете заявки на конкурс или мероприятие, посмотрите, как мы делаем Telegram-боты, и кейсы Yoshlar ovozi и Yosh Kitobxon. А если вы ещё выбираете между ботом и сайтом, у нас есть отдельный текст о том, что делать первым.

Есть задача? Напишите — обсудим.

Пишите в Telegram напрямую основателю студии. Расскажите в двух словах, что нужно — ответим с вопросами и примерной оценкой.