golang

ХрюХрюКар v.2 или как я использую Go для защиты своего двора

  • среда, 16 апреля 2025 г. в 00:00:10
https://habr.com/ru/articles/901082/

Привет хабр!

Почти год назад я писал про ХрюХрюКар. Если коротко: в 2024 году мы запилили экспериментальный проект, который проработал 7 месяцев в городе Балаково Саратовской области. За это время мы "поймали" около тысячи автомобилистов, разместивших свои авто на зеленых зонах, детских/спортивных площадках и тротуарах (в нарушение ПДД). Большинство из них было привлечено к административной ответственности.

При этом я по-максимуму старался вести разъяснительную работу с нарушителями. Например, нанес на карту все стоянки в городе, предварительно их обзвонив и убедившись в том, что 29 из 30 стоянок в городе заполнены меньше чем на половину. Также пытался донести до автомобилистов простую истину: во дворах сложившейся застройки нет места для их автомобиля.

О результатах эксперимента и некоторых выводах я подробно написал в своеобразном постмортеме первой версии ХХК. Опять же, если кратко, то выводы просты - чтобы был результат, нужно в десяток раз больше штрафов: необходимо исключать наказание в виде предупреждения, увеличивать размер штрафов и повышать продуктивность работы комиссий, чтобы с момента фиксации нарушения до момента привлечения к ответственности проходило не более пары недель (сейчас этот показатель - 1,5-2 месяца).

В процессе, я пришел к выводу что для решения данной проблемы нам в Балаково нужен ПАК "Помощник Москвы", однако внедрять столь остро-социальное решение наш регион пока не готов, по понятным причинам. В общем оставалось только ждать и терпеть.

Терпел я до весны, пока не увидел в своем, только что благоустроенном дворе, вот это:

Там стояло три автомобиля. Собственники живут в этом доме и их не смущает тот факт, что своей ленью они превращают свой же дом в свинарник.
Там стояло три автомобиля. Собственники живут в этом доме и их не смущает тот факт, что своей ленью они превращают свой же дом в свинарник.

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

Все они оправдывают себя тем, что мест на стоянках нет, хотя это не так. В 9-и минутах ходьбы от этой локации есть стоянка, заполненная лишь на 40%.
Все они оправдывают себя тем, что мест на стоянках нет, хотя это не так. В 9-и минутах ходьбы от этой локации есть стоянка, заполненная лишь на 40%.

В общем я осознал что все эти "ребята" меня дико триггерят и с этим надо что-то делать уже сейчас.

Почему v.2?

Просто так запустить ХХК на этот раз не получилось по двум основаниям:

1) Госдума решила запретить использование электронной почты для подачи обращений граждан. В первой версии ХХК обращения направлялись как раз по ЭП.

2) Одним из узких мест в модели работы ХХК было решение, по которому я (или модератор), проверяли все материалы и подписывались под каждым заявлением. Мы многое там автоматизировали, но все-же это одно из самых узких мест в системе: мне приходилось тратить по часу в день, чтобы разобрать сотню фотографий и направить обращения в комиссию и ГИБДД.

Мне давно хотелось научиться пилить ботов в телеграме и хотелось сделать какой-нибудь проект на Го с нуля, поэтому было принято решение сделать абсолютно новую версию ХХК, которая будет представлять собой телеграм-бота.

Кейс на этот раз такой:

  1. Пользователь делится с ботом местоположением и присылает ему снимок автомобиля на зелёной зоне;

  2. Бот распознает автомобильные номера, производит обратное геокодирование;

  3. Пункт 1-2 повторяется столько раз, сколько требуется.

  4. Пользователь переходит в режим утверждения снимков, где может поправить номера, адрес или точку, либо вовсе удалить снимок.

  5. Когда все или часть снимков утверждены, пользователь переходит в секцию подачи обращения.

  6. Бот формирует одно обращение на все утвержденные снимки пользователя, сделанные в одном городе. По результатам бот выдает текст обращения и PDF-файл с материалами фотофиксации нарушений. Также бот выдает ссылку на ПОС Госуслуг административной комиссии города (вот, например, ПОС для Балаково).

  7. Пользователь сохраняет 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 оказался достаточно прост для понимания.

По сути, для реализации бота необходимо реализовать:

Каждый обработчик и промежуточный слой, помимо самой функции, содержит еще и предикат, который должен возвращать булево значение. Это значение будет определять - запускать обработчик или нет. В предикате не следует делать какие-либо запросы к БД, иначе эти запросы будут выполнять каждый раз, когда сервер будет обходить хендлеры поочередно и определять какой из них следует выполнять, а какой - нет.

Инлайн-клавиатуры оказались достаточно удобны тем, что каждой кнопке можно назначить 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);

Бот проекта (работает по крупнейшим городам Саратовской области, но могу оперативно добавить любой город РФ, пишите на почту.

Канал проекта.

P.S.

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

Если будут какие-либо вопросы или замечания по коду - пишите. Код далек от идеала и, конечно, требует внимания и доработки.

Ну и хотел сказать спасибо @JustSkiv за его публикации. Пользуясь случаем, рекомендую его TG-канал для тех кто также как и я влюбился в Go. Там теплая, ламповая атмосфера и классные тематические стикеры: