如何通过 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() 更清楚,也没有必要再加一层包装。
备注:内容仅供参考。
