Entwicklung eines Zahlungssystems, bei dem keine Transaktion verloren geht
Bei der Zahlungsabwicklung darf es sich keine Fehler leisten. Lernen Sie alles über Idempotenzschlüssel, Webhook-Abstimmung, Zustandsmaschinen und die Muster, die Doppelbuchungen und verlorene Zahlungen verhindern.
Warum Zahlungen schwieriger sind, als man denkt
Stellen Sie sich folgendes Szenario vor: Ein Nutzer klickt auf „Jetzt bezahlen“, die Anfrage erreicht Stripe, Stripe belastet die Karte, doch Ihr Server läuft ab, bevor er die Antwort erhält. War die Zahlung erfolgreich? Ihre Datenbank sagt nein. Stripe sagt ja. Der Nutzer klickt erneut auf „Jetzt bezahlen“. Nun wird ihm der Betrag doppelt berechnet.
Das ist keine hypothetische Situation. So etwas passiert jeden Tag in Produktionssystemen. Hier erfahren Sie, wie Sie das verhindern können.
Muster 1: Idempotenzschlüssel
Jede Zahlungsanforderung muss einen eindeutigen Idempotenzschlüssel enthalten. Wird derselbe Schlüssel zweimal gesendet, gibt das Zahlungsgateway das ursprüngliche Ergebnis zurück, anstatt die Verarbeitung erneut durchzuführen.
import
uuidfrom django.db import modelsclass
Payment(models.Model):
class Status(models.TextChoices):
PENDING = 'pending'
PROCESSING = 'processing'
COMPLETED = 'completed'
FAILED = 'failed'
idempotency_key = models.UUIDField(
default=uuid.uuid4, unique=True, db_index=True
)
status = models.CharField(
max_length=20, choices=Status.choices, default=Status.PENDING
)
amount = models.DecimalField(max_digits=10, decimal_places=2)
stripe_payment_intent_id = models.CharField(
max_length=255, null=True, blank=True
)
attempts = models.IntegerField(default=0)
class Meta:
constraints = [
models.UniqueConstraint(
fields=['idempotency_key'],
name='unique_payment_idempotency'
)
]Muster 2: Zahlungs-Zustandsmaschine
Verwende niemals boolesche Flags wie „is_paid“. Nutze stattdessen eine Zustandsmaschine mit expliziten Übergängen. Dies verhindert ungültige Zustände und vereinfacht die Fehlersuche erheblich.
VALID_TRANSITIONS = {
'pending': ['processing', 'failed'],
'processing': ['completed', 'failed', 'pending'], # pending = erneuter Versuch
'completed': ['refunded'],
'failed': ['pending'], # Wiederholung
'refunded': [], # Endzustand
}
def transition_payment(payment, new_status):
allowed = VALID_TRANSITIONS.get(payment.status, [])
if new_status not in allowed:
raise InvalidTransition(
f"Cannot go from {payment.status} to {new_status}"
)
old_status = payment.status
payment.status = new_status
payment.save()
# Jeden Übergang
protokollieren PaymentAuditLog.objects.create(
payment=payment,
from_status=old_status,
to_status=new_status,
)Muster 3: Webhook-Abgleich
Verlassen Sie sich bei der Zahlungsbestätigung niemals auf Ihre eigene API-Antwort. Die Webhooks von Stripe/Razorpay sind die verlässliche Quelle. Ihre API-Antwort kann zwar aufgrund einer Zeitüberschreitung ausfallen, aber der Webhook kommt immer an (dank Wiederholungsversuchen).
# Webhook-Handler: die verlässliche
Quelle@csrf_exemptdef stripe_webhook(request):
payload = request.body
sig = request.headers.get('Stripe-Signature')
try:
event = stripe.Webhook.construct_event(
payload, sig, settings.STRIPE_WEBHOOK_SECRET
)
except (ValueError, stripe.error.SignatureVerificationError):
return HttpResponse(status=400)
if event['type'] == 'payment_intent.succeeded':
intent = event['data']['object']
payment = Payment.objects.get(
stripe_payment_intent_id=intent['id']
)
transition_payment(payment, 'completed')
# Die Bestellung HIER ausführen, nicht in der API-Antwort
fulfill_order(payment.order)
elif event['type'] == 'payment_intent.payment_failed':
intent = event['data']['object']
payment = Payment.objects.get(
stripe_payment_intent_id=intent['id']
)
transition_payment(payment, 'failed')
return HttpResponse(status=200)Die goldenen Regeln der Zahlungsabwicklung
1. Jeder Zahlungsvorgang muss idempotent sein. Wiederholungsversuche sollten sicher sein.
2. Webhooks sind die verlässliche Quelle, nicht die API-Antworten.
3. Protokollieren Sie alles. Jeden Statuswechsel, jeden API-Aufruf, jeden Webhook. Wenn ein Kunde eine Belastung beanstandet, benötigen Sie einen lückenlosen Prüfpfad.
4. Testen Sie nicht nur den Erfolg, sondern auch Fehlerfälle. Simulieren Sie Zeitüberschreitungen, doppelte Übermittlungen, Verzögerungen bei Webhooks und Teilausfälle.
5. Führen Sie täglich einen Abgleich durch. Vergleichen Sie Ihre Datenbank mit den Daten von Stripe. Erkennen Sie Unstimmigkeiten, bevor es Ihre Kunden tun.
In Zahlungssystemen ist Optimismus ein Fehler. Gehen Sie davon aus, dass jeder Netzwerkaufruf fehlschlägt, jeder Webhook verzögert wird und jeder Nutzer zweimal auf die Schaltfläche klickt.
— Stripe-Entwickler-Blog
