- PVSM.RU - https://www.pvsm.ru -

При этом я по-максимуму старался вести разъяснительную работу с нарушителями. Например, нанес [1] на карту все стоянки в городе, предварительно их обзвонив и убедившись в том, что 29 из 30 стоянок в городе заполнены меньше чем на половину. Также пытался донести до автомобилистов простую истину: во дворах сложившейся застройки нет места [2] для их автомобиля.
О результатах эксперимента и некоторых выводах я подробно написал в своеобразном постмортеме [3] первой версии ХХК. Опять же, если кратко, то выводы просты - чтобы был результат, нужно в десяток раз больше штрафов: необходимо исключать наказание в виде предупреждения, увеличивать размер штрафов и повышать продуктивность работы комиссий, чтобы с момента фиксации нарушения до момента привлечения к ответственности проходило не более пары недель (сейчас этот показатель - 1,5-2 месяца).
В процессе, я пришел к выводу что для решения данной проблемы нам в Балаково нужен ПАК "Помощник Москвы" [4], однако внедрять столь остро-социальное решение наш регион пока не готов, по понятным причинам. В общем оставалось только ждать и терпеть.
Терпел я до весны, пока не увидел в своем, только что благоустроенном дворе, вот это:
А еще вот такое попалось [5] (в 9 минутах ходьбы от полупустой стоянки):
В общем я осознал что все эти "ребята" меня дико триггерят и с этим надо что-то делать уже сейчас.
Просто так запустить ХХК на этот раз не получилось по двум основаниям:
1) Госдума решила запретить [6] использование электронной почты для подачи обращений граждан. В первой версии ХХК обращения направлялись как раз по ЭП.
2) Одним из узких мест в модели работы ХХК было решение, по которому я (или модератор), проверяли все материалы и подписывались под каждым заявлением. Мы многое там автоматизировали, но все-же это одно из самых узких мест в системе: мне приходилось тратить по часу в день, чтобы разобрать сотню фотографий и направить обращения в комиссию и ГИБДД.
Мне давно хотелось научиться пилить ботов в телеграме и хотелось сделать какой-нибудь проект на Го с нуля, поэтому было принято решение сделать абсолютно новую версию ХХК, которая будет представлять собой телеграм-бота.
Пользователь делится с ботом местоположением и присылает ему снимок автомобиля на зелёной зоне;
Бот распознает автомобильные номера, производит обратное геокодирование;
Пункт 1-2 повторяется столько раз, сколько требуется.
Пользователь переходит в режим утверждения снимков, где может поправить номера, адрес или точку, либо вовсе удалить снимок.
Когда все или часть снимков утверждены, пользователь переходит в секцию подачи обращения.
Бот формирует одно обращение на все утвержденные снимки пользователя, сделанные в одном городе. По результатам бот выдает текст обращения и PDF-файл с материалами фотофиксации нарушений. Также бот выдает ссылку на ПОС Госуслуг административной комиссии города (вот [7], например, ПОС для Балаково).
Пользователь сохраняет PDF, копирует текст обращения и переходит на ПОС. Там вставляет текст обращения, прикрепляет PDF и, авторизовавшись через ЕПГУ, отправляет обращение.
В результате на этот раз я ничего не модерирую, при этом никаких чувствительных данных не получаю и не храню (раньше я хранил данные модераторов, которые подписывались под обращениями). Пользователи просто получают удобный инструмент и работают с ним тогда, когда им требуется.
В результате, за 2,5 недели удалось написать законченный проект, который я опубликовал под MIT в своем репозитории. [8] Он готов к запуску как локально на слабой машине, так и в продуктовой среде под нагрузкой.
В продакшене на данный момент обеспечивают работу следующие сервисы/системы:
Один инстанс MySQL8
Один инстанс Redis для Pub/Sub
Два инстанса Django для админ-панели, миграций и генерации PDF [9].
Один инстанс Nomeroff за Flask [10] для распознавания номеров (дает около 30-и снимков в секунду, чего мне более чем достаточно)
Три инстанса сервера бота [11], написанного на Go
Traefik - балансирует запросы [12]телеграма на серверы бота. Получает SSL-сертификаты, проксирует мои запросы к админ-панели. Роутит запросы к внутреннему эндпоинту [13] генерации PDF (и балансирует запросы к нему), также прикрывает за базовой авторизацией (как доп.мера) служебные эндпоинты.
S3 Яндекс-облака хранит все снимки и PDF-файлы обращений
Бота можно достаточно быстро запустить локально на своей машине. Процесс запуска я описал в Readme [8].
При запуске в dev-окружении, бот запустится [14] в режиме Long-polling, что позволит вам работать локально, не имея облачной инфраструктуры (кроме S3) и публично доступного домена.
Тем не менее, следует понимать, что для продуктовой среды такой режим не подойдет, поэтому для staging/prod бот запускается [15] в режиме вебхуков, что позволяет горизонтально масштабировать сервис и балансировать нагрузку.
Для работы с данными я реализовал [16] систему сторов. Все чтение данных производится [17] через стор-кэш, при этом если происходит промах, мы получаем [18] данные из базы, сохраняем в кэш и публикуем [19] апдейт через редис, чтобы другие инстансы тоже положили [20] к себе эти данные.
Данные живут в кэше в соответствии с выставленными TTL и периодически вычищаются [21] из кэша в случаях, если их никто давно не запрашивал. Для возможности последующей оптимизации я во всех сторах использую метрики [22], что позволит в последующем подобрать и выставить правильные TTL для каждого стора [23].
Для изменения данных, запись предварительно необходимо получить в транзакции через специальные методы [24], чтобы обеспечить атомарность изменений и возможность отката транзакции средствами БД. Ну и сохранять [25]/удалять [26] объекты необходимо в той же транзакции.
Есть данные [27], которые я захотел хранить только в ОЗУ серверов, как из соображений безопасности, так и по соображениям производительности. Например, при фиксации нарушений, пользователь может начать делиться [28] своим местоположением, которое телеграм будет присылать мне каждые 30 секунд и которое мне необходимо хранить временно, пока не прилетят обновленные координаты или пользователь не завершит фиксацию нарушений. Также есть некритичная информация, например - текущая секция бота, в которой находится юзер. Все это сохранять в базу не хотелось, при этом было желание хранить это в структурах Go.
По результату, я реализовал схожий набор сторов, где все данные хранятся только в кэше [29], а redis помогает временно блокировать [30] данные по идентификатору на короткое время для изменений. После внесения изменений, их автор, владея токеном блокировки, производит сохранение [31], сообщая об обновлении другим инстансам и снимая блокировку.
Для реализации сервера бота [32]я использовал github.com/mymmrac/telego [33]. API оказался достаточно прост для понимания.
По сути, для реализации бота необходимо реализовать:
Набор команд [34] (предупреждаю: текст у меня хардкодом, т.к. не требуется i18n и правки "на лету");
Набор сообщений [35];
Набор клавиатур [36];
Обработчики сообщений [37] и обратных вызовов [38];
Набор промежуточных слоев [39].
Каждый обработчик и промежуточный слой, помимо самой функции, содержит еще и предикат [40], который должен возвращать булево значение. Это значение будет определять - запускать обработчик или нет. В предикате не следует делать какие-либо запросы к БД, иначе эти запросы будут выполняться каждый раз, когда сервер будет обходить хендлеры поочередно и определять какой из них следует выполнять, а какой - нет.
Инлайн-клавиатуры оказались достаточно удобны тем, что каждой кнопке можно назначить [41] callback-данные, которые при нажатии на нее, прилетят в наш обработчик. Однако Telegram не готов брать на себя хранение наших данных в объеме, большем чем 64 байта на callback-данные одной кнопки, поэтому имейте это в виду, используя UUID в качестве идентификаторов, ведь название команды и один идентификатор в 64 байта вы еще сможете уместить, а вот два - уже нет...
Для удобства был реализован следующий набор [42] внутренних пакетов:
config [43] - для управления конфигурациями. Использовал github.com/kelseyhightower/envconfig [44];
encryptor [45] - для простого детерминированного шифрования tg-идентификаторов пользователей, чтобы не хранить их в открытом виде;
geocoder [46] - для обратного геокодирования (получение адреса по координатам). Используется сервис Nominatim от OSM;
logger [47] - журналирование данных в структурированном виде. Спасибо @JustSkiv [48] за код.
pdf [49] - клиент генератора приложений к обращению в формате PDF. Пример приложения [50].
recogniser [51] - клиент сервиса распознавания номеров.
storage [52] - хранение данных в S3. Реализованы варианты для публичных бакетов и приватных (с подписью в ссылке).
transactions [53] - удобная обертка для создания транзакций с несколькими ретраями в случае retryable-ошибок. Пример [54] использования.
Для сборки и управления зависимостями используется bazelisk. В dev-окружении используется iBazel, который сам следит за изменениями go-файлов и быстро пересобирает проект если есть изменения.
Репозиторий проекта [8];
Первая (старая) версия [55] проекта от 2024 года (Django+Vue);
Бот проекта [56] (работает по крупнейшим городам Саратовской области, но могу оперативно добавить любой город РФ, пишите на почту [57]).
Канал проекта [58].
Спасибо за уделенное время! Надеюсь что мой опыт поможет кому-нибудь запустить миграцию автомобилей с зеленых зон своего двора на стоянки и в гаражи.
Если будут какие-либо вопросы или замечания по коду - пишите. Код далек от идеала и, конечно, требует внимания и доработки.
Ну и хотел сказать спасибо @JustSkiv [48] за его публикации. Пользуясь случаем, рекомендую его TG-канал [59]для тех кто также как и я влюбился в Go. Там теплая, ламповая атмосфера и классные тематические стикеры:

Автор: lepekhovs
Источник [60]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/telegram/416822
Ссылки в тексте:
[1] нанес: https://t.me/oinkoinkcar/120
[2] нет места : https://t.me/oinkoinkcar/142
[3] постмортеме: https://vk.com/@xxkap_blk-ps
[4] ПАК "Помощник Москвы": https://t.me/oinkoinkcar/137
[5] вот такое попалось: https://t.me/oinkoinkcar/207
[6] решила запретить: https://t.me/oinkoinkcar/204
[7] вот: https://pos.gosuslugi.ru/form/?opaId=165&fz59=false
[8] в своем репозитории.: https://gitlab.com/theansweris42/xxk
[9] генерации PDF: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/django_backend/pdf_api/views.py
[10] Nomeroff за Flask: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/nomeroff/main.py
[11] сервера бота: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/bin/server/server.go#L83
[12] балансирует запросы : https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/traefik/conf-prod/traefik-conf-prod-000.yml#L90
[13] внутреннему эндпоинту: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/traefik/conf-prod/traefik-conf-prod-000.yml#L48
[14] запустится: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/bot.go#L224
[15] запускается: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/bot.go#L215
[16] реализовал: https://gitlab.com/theansweris42/xxk/-/tree/main/go_backend/internal/store?ref_type=heads
[17] производится: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L145
[18] получаем: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L364
[19] публикуем: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L379
[20] положили: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L588
[21] вычищаются: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L546
[22] метрики: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/photo.go#L49
[23] TTL для каждого стора: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/cache.go
[24] методы: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/store.go#L26
[25] сохранять: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/store.go#L23
[26] удалять: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/store.go#L25
[27] данные: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/models/telegram_user_state.go
[28] начать делиться: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/middlewares.go#L390
[29] в кэше: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/telegram_user_state.go
[30] блокировать: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/redis_broker/telegram_user_state.go#L53
[31] сохранение: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/store/cache/telegram_user_state.go#L189
[32] сервера бота : https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/bot.go
[33] github.com/mymmrac/telego: http://github.com/mymmrac/telego
[34] команд: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/commands.go
[35] сообщений: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/messages.go
[36] клавиатур: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/keyboards.go
[37] сообщений: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/capturing.go
[38] обратных вызовов: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/capturing.go#L21
[39] промежуточных слоев: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/middlewares.go
[40] предикат: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/capturing.go#L490
[41] назначить: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/keyboards.go#L37
[42] набор: https://gitlab.com/theansweris42/xxk/-/tree/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib
[43] config: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/config/config.go
[44] github.com/kelseyhightower/envconfig: http://github.com/kelseyhightower/envconfig
[45] encryptor: https://gitlab.com/theansweris42/xxk/-/tree/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/encryptor
[46] geocoder: https://gitlab.com/theansweris42/xxk/-/tree/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/geocoder
[47] logger: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/logger/logger.go
[48] @JustSkiv: https://www.pvsm.ru/users/justskiv
[49] pdf: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/pdf/pdf.go
[50] Пример приложения: https://storage.yandexcloud.net/oinkoinkcar/example-app.pdf
[51] recogniser: https://gitlab.com/theansweris42/xxk/-/tree/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/recogniser
[52] storage: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/storage/storage.go
[53] transactions: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/internal/lib/transactions/transactions.go
[54] Пример: https://gitlab.com/theansweris42/xxk/-/blob/54a99b4f55812697e0a0f4b07c8d19c702cafaaa/go_backend/pkg/bot/capturing.go#L536
[55] Первая (старая) версия: https://gitlab.com/theansweris42/oinkoinkcar
[56] Бот проекта: https://t.me/XXKapBot
[57] на почту: mailto:lepehovsv@gmail.com
[58] Канал проекта: https://t.me/oinkoinkcar
[59] TG-канал : https://t.me/ntuzov
[60] Источник: https://habr.com/ru/articles/901082/?utm_source=habrahabr&utm_medium=rss&utm_campaign=901082
Нажмите здесь для печати.