RUEN
Обсудить проект

Инженерный кейс

Обработка заказов на торговых площадках

Два бота для продавцов на разных площадках: приём заказов и сообщений, автовыдача, возврат товара в продажу, удержание сессии. Один движок обработки, два принципиально разных транспорта.

повторов отправки платежа
0
перевода в одной транзакции
4
транспорта к площадке
2

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

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

Библиотека кошелька вынесена в отдельный процесс, потому что падает неперехватываемо

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

Очевидное решение — обернуть перевод в перехват исключений и повторить. Не работает: у библиотеки кошелька нативная часть, и она завершает процесс сигналом, который перехватить нельзя. Один сбой кошелька убивал бы весь бот целиком — вместе с автовыдачей, автоответчиком и панелью, то есть продавец терял бы всё из-за одной неудачной операции.

«Деньги ушли» и «ответ получен» — два разных факта, и приоритет у первого

Рабочий процесс сообщает о фазах отдельно. Если пришло подтверждение, что транзакция ушла в сеть, результат считается успешным даже если потом прилетел таймаут — идентификатор транзакции добирается позже отдельным запросом.

Очевидная логика «нет успешного ответа — верни деньги» здесь означает двойную оплату: перевод уже необратим, а возврат на площадке отдаёт покупателю ещё и деньги за заказ. Исключение ровно одно и обработано отдельно: транзакция вошла в блок, комиссия списана, но само отправление не состоялось — тогда возврат делается несмотря на подтверждение.

Отправку платежа нельзя повторять — и это записано в коде прямым запретом

Все запросы на чтение обёрнуты в повторы с нарастающей паузой: узлы сети часто отвечают отказом. А сама отправка не повторяется никогда, и рядом стоит комментарий, объясняющий почему.

Повторный вызов подписывает новый пакет с тем же порядковым номером. Если первый уже дошёл до сети, оба варианта могут пройти — это двойное списание. Разница между безопасным повтором и опасным здесь не интуитивна, поэтому она и зафиксирована текстом, а не оставлена на память.

Перезапуск рабочего процесса откладывается, пока есть незавершённые переводы

Процесс положено обновлять после определённого числа операций или по времени — нативная часть деградирует. Но если в этот момент есть незавершённые запросы, обновление переносится.

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

Успех сохранения объявления определяется по наличию ссылки, а не по отсутствию ошибок

Площадка на сохранение отвечает так, что в ответе могут быть одновременно ссылка на созданный лот и список замечаний вроде «название обрезано». Операция считается успешной, если ссылка есть.

Раньше исключение бросалось на любых замечаниях, и бот писал «ошибка, пропускаю» по лоту, который уже создан. На следующем проходе тот же лот создавался снова — то есть строгая проверка ошибок порождала дубликаты.

Отказы площадки про лимиты разведены на четыре разных класса

Текст отказа разбирается: общий лимит на аккаунт, лимит конкретного раздела, временное ограничение частоты, требование подтвердить личность. У каждого своя реакция.

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

Порядок прочитанного синтезируется из времени, потому что у второй площадки нет порядковых номеров

Идентификаторы сообщений там — случайные строки. Сортируемый ключ собирается из времени сообщения и устойчивого хеша идентификатора, и по нему ведётся отметка «досюда прочитано».

Без числового порядка нельзя сказать «это сообщение новее обработанного». Хранить множество виденных идентификаторов — значит растить его бесконечно и всё равно не уметь сортировать.

Отметка прочитанного только растёт — это лечение бесконечного цикла

Новое значение берётся как максимум из найденного и уже известного, а не присваивается напрямую.

Площадка периодически отдаёт историю переписки урезанной. Максимум такой страницы оказывается старше уже обработанного, и при прямом присвоении бот заново рассылал уведомления о старых сообщениях — и так по кругу.

Смена статуса сделки сообщается только при движении вперёд

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

Первые минуты после смены статуса площадка «мерцает» из-за отложенной согласованности: список сделок отдаёт то новое состояние, то снова старое. Наивное сравнение «статус изменился» превращает это в поток ложных уведомлений продавцу.

Сначала пробуем вернуть оригинал, копию — только если площадка отказала

После продажи бот сперва пытается опубликовать тот же лот. Копия создаётся, только когда площадка отвечает отказом.

Копия проще и срабатывает всегда, поэтому соблазнительна. Но она теряет просмотры, позицию в выдаче и историю лота, а профиль продавца засоряется дублями. Более сложный путь выбран потому, что дешёвый портит то, ради чего всё делается.

У прохода есть потолок трат

Отдельный проход подбирает истёкшие лоты, у которых бесплатной публикации обычно нет вовсе. На один проход задан потолок расходов.

У магазина может лежать несколько сотен истёкших лотов. Проход без потолка списал бы весь баланс продавца за один заход — молча, ночью, по расписанию.

Python · SQLite · GraphQL · BeautifulSoup · Telegram Bot API