PAYATHON 2026

如何通过 Paystack API 验证交易并保存 model?

支付小周

结论

报错是因为 Paystack 类中只有 verify_payment(),并没有 process_payment()。因此,下面这行代码会触发 AttributeError:

status = paystack.process_payment(self.reference)

调用时还需要修正两处:

  • 改用已有的 paystack.verify_payment(transaction_id)。
  • 当前函数中没有 self,交易 reference 已保存在 transaction_id 中。

Paystack 验证接口顶层的 status 只表示 API 请求是否成功,交易本身的状态通常位于 data["status"]。保存成功记录前,这两项都要检查。

修正 Paystack 验证方法

可以让验证方法始终返回统一的数据结构,同时处理超时、非 JSON 响应和字段缺失:

import requests
from django.conf import settings


class Paystack:
    base_url = "https://api.paystack.co"

    def __init__(self):
        self.secret_key = settings.PAYSTACK_SECRET_KEY

    def verify_payment(self, reference):
        url = f"{self.base_url}/transaction/verify/{reference}"
        headers = {
            "Authorization": f"Bearer {self.secret_key}",
            "Content-Type": "application/json",
        }

        try:
            response = requests.get(url, headers=headers, timeout=15)
            response_data = response.json()
        except requests.RequestException as exc:
            return False, {"message": str(exc)}
        except ValueError:
            return False, {"message": "Paystack returned an invalid JSON response"}

        if response.status_code != 200:
            return False, {
                "message": response_data.get(
                    "message",
                    "Unable to verify transaction",
                )
            }

        return response_data.get("status", False), response_data.get("data", {})

不要把密钥直接写进源代码。可以通过 Django 配置读取环境变量:

# settings.py
import os

PAYSTACK_SECRET_KEY = os.environ["PAYSTACK_SECRET_KEY"]

问题中展示的密钥已经暴露。如果该密钥仍然有效,即使只是测试密钥,也应在 Paystack 后台撤销或重新生成。

在视图中调用 verify_payment()

支付处理逻辑可以改成:

from decimal import Decimal

from django.db import transaction
from django.db.models import F
from django.shortcuts import redirect, render


def process_payment(request, slug, amount, award, votes):
    reference = request.GET.get("reference")

    if not reference:
        return render(
            request,
            "payment_error.html",
            {"error": "Missing transaction reference"},
        )

    paystack = Paystack()
    api_status, payment_data = paystack.verify_payment(reference)

    payment_status = payment_data.get("status")
    verified_reference = payment_data.get("reference")

    if (
        api_status is True
        and payment_status == "success"
        and verified_reference == reference
    ):
        amount_in_major_unit = Decimal(str(payment_data["amount"])) / Decimal("100")

        with transaction.atomic():
            payment, created = SuccessfulTransactionHistory.objects.get_or_create(
                transaction_id=reference,
                defaults={
                    "nominee_name": slug,
                    "amount": amount_in_major_unit,
                    "award": award,
                },
            )

            if created:
                Nomination.objects.filter(slug=slug).update(
                    votes=F("votes") + votes
                )
                Award.objects.filter(slug=award).update(
                    amount=F("amount") + amount_in_major_unit
                )

        return redirect("vote:paymentsuccess", slug=slug)

    error_message = payment_data.get(
        "message",
        f"Transaction status: {payment_status or 'unknown'}",
    )

    FailedTransactionHistory.objects.get_or_create(
        transaction_id=reference,
        defaults={
            "nominee_name": slug,
            "amount": Decimal(str(amount)) / Decimal("100"),
            "award": award,
        },
    )

    return render(
        request,
        "payment_error.html",
        {"error": error_message},
    )

主要变化是调用 verify_payment(),并分别检查 API 状态和交易状态:

api_status, payment_data = paystack.verify_payment(reference)

if api_status and payment_data.get("status") == "success":
    # 保存成功交易

不能这样写:

status = paystack.verify_payment(reference)

if status == "success":

因为 verify_payment() 返回的是二元组,例如:

(True, {"status": "success", ...})

它不会单独返回 "success" 字符串。

保存前核对交易信息

浏览器回调中的 reference、amount、award 和 votes 都可能被修改,因此不能只根据 URL 参数更新业务数据。至少要用 Paystack 返回的数据核对金额:

expected_amount = int(amount)
verified_amount = payment_data.get("amount")
verified_currency = payment_data.get("currency")

if verified_amount != expected_amount:
    # 金额不一致,拒绝入账
    ...

还需要检查:

  • data["status"] == "success"
  • 返回的 reference 与提交验证的 reference 一致
  • data["amount"] 与服务端保存的应付金额一致
  • data["currency"] 是预期币种
  • 该交易此前没有处理过
  • 必要时核对 customer、metadata 或订单编号

应付金额和票数最好从服务端的订单记录中读取,不要直接信任 URL 参数。

防止重复保存同一笔交易

用户可能刷新支付回调页面,Paystack 也可能重复发送通知。如果每次验证成功都增加票数,同一笔交易就可能被重复计入。

可以为 transaction_id 添加唯一约束:

class SuccessfulTransactionHistory(models.Model):
    transaction_id = models.CharField(max_length=100, unique=True)

再通过 get_or_create() 判断是否为首次处理。成功交易记录、票数增加和金额增加应放在同一个 transaction.atomic() 中,防止操作只完成一部分。

函数命名

视图函数仍然可以命名为 process_payment(),但它与 Paystack 类的方法没有关系:

def process_payment(request, ...):
    paystack = Paystack()
    api_status, payment_data = paystack.verify_payment(reference)

如果要使用下面这种调用方式:

paystack.process_payment(reference)

就需要在 Paystack 类中定义 process_payment()。不过,这个类当前负责调用 Paystack 的验证接口,直接使用 verify_payment() 更清楚,也没有必要再加一层包装。

备注:内容仅供参考。