Интеграция платежей в Telegram-боте: проблема идемпотентности и её решение
Автор: Javohir Abdullayev · · Telegram-боты

Введение: неожиданная проблема в реальном проекте
Продажа товаров и услуг через Telegram-ботов сегодня стала очень популярной на рынке Узбекистана. Пользователь оформляет заказ внутри бота, оплачивает через Click или Payme и автоматически получает доступ к услуге. Однако в недавно запущенном боте по продаже электронных билетов возникла серьезная проблема: со счетов некоторых пользователей деньги списывались один раз, но бот генерировал два одинаковых билета, а в отдельных случаях средства списывались с баланса пользователя несколько раз.
В этой статье мы подробно разберем первопричину этой проблемы и на реальном примере кода рассмотрим, как сделать платежи на 100% безопасными и надежными в архитектуре бэкенда с помощью принципа идемпотентности (idempotency).
В чем была причина проблемы? (Root Cause Analysis)
Многие разработчики при работе с платежными системами представляют процесс слишком просто: от провайдера приходит вебхук, статус заказа в базе данных меняется на 'оплачен', и пользователю отправляется сообщение. Но в реальных сетевых условиях нормальный ход событий нарушают два фактора:
- Повторные попытки провайдера (Retries): если ваш сервер не успевает вернуть успешный ответ (HTTP 200) на запрос платежной системы (например, Click или Payme) в течение 2–3 секунд или происходит сетевой сбой, сервер платежной системы отправляет точно такой же запрос транзакции повторно.
- Действия пользователя: при медленном интернет-соединении пользователь нажимает кнопку оплаты или подтверждения несколько раз подряд, что приводит к параллельным запросам.
Если ваш обработчик вебхуков принимает один и тот же запрос дважды и обрабатывает их одновременно параллельно, возникает состояние гонки (race condition). В результате пользователю предоставляются две услуги либо дублируется транзакция, что нарушает финансовую отчетность.
Решение: идемпотентность и распределенная блокировка (Distributed Lock)
Идемпотентность — это принцип, согласно которому независимо от того, сколько раз отправляется запрос в систему с одинаковыми параметрами, результат изменяется только один раз и всегда сохраняется одно и то же конечное состояние. Для обеспечения этого в платежных системах мы внедрили два уровня защиты:
- Проверка статуса транзакции: для каждого запроса на оплату существует уникальный
transaction_id. Если в базе эта транзакция уже была успешно выполнена, повторный запрос немедленно прерывается. - Блокировка на базе Redis (Distributed Mutex): чтобы два параллельных запроса не прошли проверку одновременно, с помощью Redis устанавливается блокировка на несколько секунд.
Пример реализации на практике: Python и Redis
Ниже представлена логика установки блокировки с помощью Redis в обработчике вебхуков на FastAPI или aiohttp:
import redis
from fastapi import FastAPI, HTTPException, Request
app = FastAPI()
r = redis.Redis(host='localhost', port=6379, db=0)
@app.post('/webhook/payme/')
async def payme_webhook(request: Request):
data = await request.json()
transaction_id = data.get('params', {}).get('id')
order_id = data.get('params', {}).get('account', {}).get('order_id')
if not transaction_id:
raise HTTPException(status_code=400, detail='Отсутствует ID транзакции')
# Устанавливаем блокировку на 10 секунд через Redis (Distributed Lock)
lock_key = f'lock:payment:{transaction_id}'
is_acquired = r.set(lock_key, 'locked', nx=True, ex=10)
if not is_acquired:
# Если эта транзакция уже обрабатывается другим потоком
return {'error': {'code': -31008, 'message': 'Транзакция уже обрабатывается'}}
try:
# 1. Проверяем заказ в базе данных
# 2. Если заказ уже оплачен, повторная оплата не производится
# 3. Обновляем статус заказа и отправляем уведомление в Telegram
return {'result': {'state': 2, 'transaction': transaction_id}}
finally:
# Освобождаем блокировку после завершения
r.delete(lock_key)
Безопасность транзакций на уровне базы данных
Одного Redis недостаточно. Система должна стабильно работать, даже если кэш-сервер Redis временно недоступен. Для этого в реляционных базах данных, таких как PostgreSQL, необходимо использовать команду SELECT ... FOR UPDATE. Она блокирует строку заказа для других запросов до завершения текущей транзакции.
В Django ORM это реализуется следующим образом:
from django.db import transaction
@transaction.atomic
def process_payment(order_id, transaction_id):
# select_for_update() удерживает другие запросы в очереди
order = Order.objects.select_for_update().get(id=order_id)
if order.status == Order.Status.PAID:
return False, 'Заказ уже оплачен'
order.status = Order.Status.PAID
order.transaction_id = transaction_id
order.save()
return True, 'Успешно завершено'
Заключение и реальные результаты
Какие результаты мы получили после внедрения данной архитектуры в проект?
- Случаи повторных платежей и дублирования услуг полностью свелись к нулю.
- Сервер стал абсолютно устойчив к сетевым сбоям и агрессивным повторным запросам от платежных провайдеров.
- Жалобы пользователей в службу поддержки по поводу ошибок оплаты прекратились.
Если вы работаете с финансами в Telegram-боте, всегда следуйте правилам идемпотентности. При интеграции платежных систем на первом месте должно стоять не 'быстрое решение', а прежде всего 'безопасная и надежная архитектура'.
Теги: #Telegram Bot #To'lov Tizimlari #Python #Redis #Backend