
Привет хабр!
Почти год назад я писал про ХрюХрюКар. Если коротко: в 2024 году мы запилили экспериментальный проект, который проработал 7 месяцев в городе Балаково Саратовской области. За это время мы «поймали» около тысячи автомобилистов, разместивших свои авто на зеленых зонах, детских/спортивных площадках и тротуарах (в нарушение ПДД). Большинство из них было привлечено к административной ответственности.
При этом я по-максимуму старался вести разъяснительную работу с нарушителями. Например, нанес на карту все стоянки в городе, предварительно их обзвонив и убедившись в том, что 29 из 30 стоянок в городе заполнены меньше чем на половину. Также пытался донести до автомобилистов простую истину: во дворах сложившейся застройки нет места для их автомобиля.
О результатах эксперимента и некоторых выводах я подробно написал в своеобразном постмортеме первой версии ХХК. Опять же, если кратко, то выводы просты - чтобы был результат, нужно в десяток раз больше штрафов: необходимо исключать наказание в виде предупреждения, увеличивать размер штрафов и повышать продуктивность работы комиссий, чтобы с момента фиксации нарушения до момента привлечения к ответственности проходило не более пары недель (сейчас этот показатель — 1,5–2 месяца).
В процессе, я пришел к выводу что для решения данной проблемы нам в Балаково нужен ПАК «Помощник Москвы», однако внедрять столь остро-социальное решение наш регион пока не готов, по понятным причинам. В общем оставалось только ждать и терпеть.
Терпел я до весны, пока не увидел в своем, только что благоустроенном дворе, вот это:

А еще вот такое попалось (в 9 минутах ходьбы от полупустой стоянки):

В общем я осознал что все эти «ребята» меня дико триггерят и с этим надо что‑то делать уже сейчас.
Почему v.2?
Просто так запустить ХХК на этот раз не получилось по двум основаниям:
Госдума решила запретить использование электронной почты для подачи обращений граждан. В первой версии ХХК обращения направлялись как раз по ЭП.
Одним из узких мест в модели работы ХХК было решение, по которому я (или модератор), проверяли все материалы и подписывались под каждым заявлением. Мы многое там автоматизировали, но все‑же это одно из самых узких мест в системе: мне приходилось тратить по часу в день, чтобы разобрать сотню фотографий и направить обращения в комиссию и ГИБДД.
Мне давно хотелось научиться пилить ботов в телеграме и хотелось сделать какой‑нибудь проект на Го с нуля, поэтому было принято решение сделать абсолютно новую версию ХХК, которая будет представлять собой телеграм-бота.
Кейс на этот раз такой:
Пользователь делится с ботом местоположением и присылает ему снимок автомобиля на зелёной зоне;
Бот распознает автомобильные номера, производит обратное геокодирование;
Пункт 1-2 повторяется столько раз, сколько требуется.
Пользователь переходит в режим утверждения снимков, где может поправить номера, адрес или точку, либо вовсе удалить снимок.
Когда все или часть снимков утверждены, пользователь переходит в секцию подачи обращения.
Бот формирует одно обращение на все утвержденные снимки пользователя, сделанные в одном городе. По результатам бот выдает текст обращения и PDF-файл с материалами фотофиксации нарушений. Также бот выдает ссылку на ПОС Госуслуг административной комиссии города (вот, например, ПОС для Балаково).
Пользователь сохраняет PDF, копирует текст обращения и переходит на ПОС. Там вставляет текст обращения, прикрепляет PDF и, авторизовавшись через ЕПГУ, отправляет обращение.
В результате на этот раз я ничего не модерирую, при этом никаких чувствительных данных не получаю и не храню (раньше я хранил данные модераторов, которые подписывались под обращениями). Пользователи просто получают удобный инструмент и работают с ним тогда, когда им требуется.
Что получилось
Исходники
В результате, за 2,5 недели удалось написать законченный проект, который я опубликовал под MIT в своем репозитории. Он готов к запуску как локально на слабой машине, так и в продуктовой среде под нагрузкой.
Как выглядит бот
Фиксация нарушений
Утверждение снимков
Подготовка и отправка обращения
Что под капотом?
В продакшене на данный момент обеспечивают работу следующие сервисы/системы:
Один инстанс MySQL8
Один инстанс Redis для Pub/Sub
Два инстанса Django для админ‑панели, миграций и генерации PDF.
Один инстанс Nomeroff за Flask для распознавания номеров (дает около 30-и снимков в секунду, чего мне более чем достаточно)
Три инстанса сервера бота, написанного на Go
Traefik — балансирует запросы телеграма на серверы бота. Получает SSL‑сертификаты, проксирует мои запросы к админ‑панели. Роутит запросы к внутреннему эндпоинту генерации PDF (и балансирует запросы к нему), также прикрывает за базовой авторизацией (как доп.мера) служебные эндпоинты.
S3 Яндекс‑облака хранит все снимки и PDF‑файлы обращений
Бота можно достаточно быстро запустить локально на своей машине. Процесс запуска я описал в Readme.
При запуске в dev-окружении, бот запустится в режиме Long-polling, что позволит вам работать локально, не имея облачной инфраструктуры (кроме S3) и публично доступного домена.
Тем не менее, следует понимать, что для продуктовой среды такой режим не подойдет, поэтому для staging/prod бот запускается в режиме вебхуков, что позволяет горизонтально масштабировать сервис и балансировать нагрузку.
Работа с данными
Для работы с данными я реализовал систему сторов. Все чтение данных производится через стор-кэш, при этом если происходит промах, мы получаем данные из базы, сохраняем в кэш и публикуем апдейт через редис, чтобы другие инстансы тоже положили к себе эти данные.
Данные живут в кэше в соответствии с выставленными TTL и периодически вычищаются из кэша в случаях, если их никто давно не запрашивал. Для возможности последующей оптимизации я во всех сторах использую метрики, что позволит в последующем подобрать и выставить правильные TTL для каждого стора.
Для изменения данных, запись предварительно необходимо получить в транзакции через специальные методы, чтобы обеспечить атомарность изменений и возможность отката транзакции средствами БД. Ну и сохранять/удалять объекты необходимо в той же транзакции.
Есть данные, которые я захотел хранить только в ОЗУ серверов, как из соображений безопасности, так и по соображениям производительности. Например, при фиксации нарушений, пользователь может начать делиться своим местоположением, которое телеграм будет присылать мне каждые 30 секунд и которое мне необходимо хранить временно, пока не прилетят обновленные координаты или пользователь не завершит фиксацию нарушений. Также есть некритичная информация, например - текущая секция бота, в которой находится юзер. Все это сохранять в базу не хотелось, при этом было желание хранить это в структурах Go.
По результату, я реализовал схожий набор сторов, где все данные хранятся только в кэше, а redis помогает временно блокировать данные по идентификатору на короткое время для изменений. После внесения изменений, их автор, владея токеном блокировки, производит сохранение, сообщая об обновлении другим инстансам и снимая блокировку.
Бот
Для реализации сервера бота я использовал github.com/mymmrac/telego
. API оказался достаточно прост для понимания.
По сути, для реализации бота необходимо реализовать:
Набор команд (предупреждаю: текст у меня хардкодом, т.к. не требуется i18n и правки «на лету»);
Набор сообщений;
Набор клавиатур;
Обработчики сообщений и обратных вызовов;
Набор промежуточных слоев.
Каждый обработчик и промежуточный слой, помимо самой функции, содержит еще и предикат, который должен возвращать булево значение. Это значение будет определять — запускать обработчик или нет. В предикате не следует делать какие‑либо запросы к БД, иначе эти запросы будут выполняться каждый раз, когда сервер будет обходить хендлеры поочередно и определять какой из них следует выполнять, а какой — нет.
Инлайн‑клавиатуры оказались достаточно удобны тем, что каждой кнопке можно назначить callback‑данные, которые при нажатии на нее, прилетят в наш обработчик. Однако Telegram не готов брать на себя хранение наших данных в объеме, большем чем 64 байта на callback‑данные одной кнопки, поэтому имейте это в виду, используя UUID в качестве идентификаторов, ведь название команды и один идентификатор в 64 байта вы еще сможете уместить, а вот два — уже нет...
Иное
Для удобства был реализован следующий набор внутренних пакетов:
config — для управления конфигурациями. Использовал
github.com/kelseyhightower/envconfig
;encryptor — для простого детерминированного шифрования tg‑идентификаторов пользователей, чтобы не хранить их в открытом виде;
geocoder — для обратного геокодирования (получение адреса по координатам). Используется сервис Nominatim от OSM;
logger — журналирование данных в структурированном виде. Спасибо @JustSkiv за код.
pdf — клиент генератора приложений к обращению в формате PDF. Пример приложения.
recogniser — клиент сервиса распознавания номеров.
storage — хранение данных в S3. Реализованы варианты для публичных бакетов и приватных (с подписью в ссылке).
transactions — удобная обертка для создания транзакций с несколькими ретраями в случае retryable‑ошибок. Пример использования.
Для сборки и управления зависимостями используется bazelisk
. В dev‑окружении используется iBazel
, который сам следит за изменениями go-файлов и быстро пересобирает проект если есть изменения.
Ссылки
Первая (старая) версия проекта от 2024 года (Django+Vue);
Бот проекта (работает по крупнейшим городам Саратовской области, но могу оперативно добавить любой город РФ, пишите через форму обратной связи).
UPD 17:11 16.04.2025: добавлены города Екатеринбург
, Нижний Тагил
, Каменск-Уральский
, Первоуральск
, Новосибирск
, Самара
, Пенза
, Ульяновск
, Нижний Новгород
, Уфа
, Киров
UPD 22.04.2025: добавлены почти все областные центры и крупнейшие города России (135 городов в общем).
P.S.
Спасибо за уделенное время! Надеюсь что мой опыт поможет кому‑нибудь запустить миграцию автомобилей с зеленых зон своего двора на стоянки и в гаражи.
Если будут какие‑либо вопросы или замечания по коду — пишите. Код далек от идеала и, конечно, требует внимания и доработки.
Ну и хотел сказать спасибо @JustSkiv за его публикации. Пользуясь случаем, рекомендую его TG‑канал для тех кто также как и я влюбился в Go. Там теплая, ламповая атмосфера и классные тематические стикеры:
