PAYATHON 2026

使用 Django 最容易实现的 Payment Gateway 是什么?

支付阿杰

结论

对于只面向美国收款,同时需要支持一次性付款和按月订阅的 Django 网站,通常最省事的方案是使用 Stripe Checkout 和 Stripe Billing。

主要原因有:

  • 支付页面由 Stripe 托管,无需自行处理银行卡输入。
  • 一套 API 就能覆盖一次性付款和 recurring billing。
  • 可以在 Dashboard 中查看交易、退款、订阅和失败付款。
  • Django 后端只需创建 Checkout Session、接收 webhook,并维护账户流水。
  • PCI 合规范围会小很多。

如果你已经熟悉 Django,而且业务规则比较简单,基础测试版本有可能在 10 小时内完成。生产环境还要处理 webhook 重试、幂等、退款同步、账务核对和安全测试,因此不能保证完整上线也能控制在 10 小时内。

Amazon FPS 已不适合新项目。PayPal 也能实现这些功能,但以减少集成代码为目标时,Stripe Checkout 通常更直接。

推荐的整体流程

一次性付款:

用户选择金额
    → Django 创建 Checkout Session
    → 跳转到 Stripe 托管支付页
    → Stripe 发送 checkout.session.completed webhook
    → Django 验证签名并增加用户余额

按月订阅:

用户选择月付金额
    → Django 创建 subscription 模式的 Checkout Session
    → Stripe 创建订阅并完成首期付款
    → Stripe 发送 invoice.paid webhook
    → Django 根据实际到账结果更新账户

不要根据浏览器是否跳转到 success 页面来确认付款。用户可能关闭页面、伪造请求,也可能在付款完成后没有成功跳转。只有经过签名验证的 Stripe webhook 才能作为入账依据。

安装与配置

安装 Stripe 的 Python SDK:

pip install stripe

将密钥放入环境变量,不要写进源码:

# settings.py
import os

STRIPE_SECRET_KEY = os.environ["STRIPE_SECRET_KEY"]
STRIPE_WEBHOOK_SECRET = os.environ["STRIPE_WEBHOOK_SECRET"]

测试密钥和正式密钥必须分开管理。开发阶段应先使用 Stripe test mode。

创建一次性付款

下面的示例允许用户选择付款金额。实际项目必须在服务端验证金额,不能直接信任浏览器提交的数据。

# payments/views.py
from decimal import Decimal, InvalidOperation

import stripe
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseBadRequest
from django.shortcuts import redirect
from django.urls import reverse
from django.views.decorators.http import require_POST

stripe.api_key = settings.STRIPE_SECRET_KEY


@login_required
@require_POST
def create_deposit_checkout(request):
    try:
        amount = Decimal(request.POST["amount"])
    except (KeyError, InvalidOperation):
        return HttpResponseBadRequest("Invalid amount")

    if amount < Decimal("1.00") or amount > Decimal("5000.00"):
        return HttpResponseBadRequest("Amount is outside the allowed range")

    amount_cents = int(amount * 100)

    session = stripe.checkout.Session.create(
        mode="payment",
        payment_method_types=["card"],
        line_items=[
            {
                "price_data": {
                    "currency": "usd",
                    "product_data": {
                        "name": "Account deposit",
                    },
                    "unit_amount": amount_cents,
                },
                "quantity": 1,
            }
        ],
        client_reference_id=str(request.user.pk),
        metadata={
            "user_id": str(request.user.pk),
            "purpose": "account_deposit",
        },
        success_url=request.build_absolute_uri(
            reverse("payments:success")
        ) + "?session_id={CHECKOUT_SESSION_ID}",
        cancel_url=request.build_absolute_uri(
            reverse("payments:cancel")
        ),
    )

    return redirect(session.url, permanent=False)

金额必须以整数美分传递。例如,$19.99 应转换为 1999。不要用 float 计算货币金额。

创建按月订阅

如果所有用户支付相同金额,最好提前在 Stripe Dashboard 中创建 Product 和 recurring Price,再在 Django 中保存对应的 price_... ID:

@login_required
@require_POST
def create_subscription_checkout(request):
    price_id = settings.STRIPE_MONTHLY_PRICE_ID

    session = stripe.checkout.Session.create(
        mode="subscription",
        payment_method_types=["card"],
        line_items=[
            {
                "price": price_id,
                "quantity": 1,
            }
        ],
        client_reference_id=str(request.user.pk),
        metadata={
            "user_id": str(request.user.pk),
        },
        subscription_data={
            "metadata": {
                "user_id": str(request.user.pk),
            }
        },
        success_url=request.build_absolute_uri(
            reverse("payments:success")
        ) + "?session_id={CHECKOUT_SESSION_ID}",
        cancel_url=request.build_absolute_uri(
            reverse("payments:cancel")
        ),
    )

    return redirect(session.url, permanent=False)

固定 Price 比每次动态创建价格更方便管理,也便于在 Dashboard 中统计收入。如果不同用户的月付金额不同,可以使用动态 price_data,但要先确定价格变更、退款、取消和续费规则。

接收并验证 webhook

Webhook 请求来自 Stripe,因此端点通常不使用 Django 的 CSRF 校验,但必须通过 webhook signing secret 验证签名:

# payments/views.py
import stripe

from django.conf import settings
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST


@csrf_exempt
@require_POST
def stripe_webhook(request):
    payload = request.body
    signature = request.headers.get("Stripe-Signature", "")

    try:
        event = stripe.Webhook.construct_event(
            payload,
            signature,
            settings.STRIPE_WEBHOOK_SECRET,
        )
    except ValueError:
        return HttpResponse(status=400)
    except stripe.error.SignatureVerificationError:
        return HttpResponse(status=400)

    event_type = event["type"]
    obj = event["data"]["object"]

    if event_type == "checkout.session.completed":
        handle_checkout_completed(
            event_id=event["id"],
            session=obj,
        )

    elif event_type == "invoice.paid":
        handle_invoice_paid(
            event_id=event["id"],
            invoice=obj,
        )

    elif event_type == "invoice.payment_failed":
        handle_invoice_payment_failed(
            event_id=event["id"],
            invoice=obj,
        )

    return HttpResponse(status=200)

URL 配置:

# payments/urls.py
from django.urls import path

from . import views

app_name = "payments"

urlpatterns = [
    path(
        "checkout/deposit/",
        views.create_deposit_checkout,
        name="deposit",
    ),
    path(
        "checkout/subscription/",
        views.create_subscription_checkout,
        name="subscription",
    ),
    path(
        "webhooks/stripe/",
        views.stripe_webhook,
        name="stripe-webhook",
    ),
]

Webhook 必须具备幂等性

Stripe 可能在超时或网络异常时重复发送同一个事件。如果每次收到 webhook 都直接增加余额,同一笔付款就可能被记账两次。

至少应保存以下信息:

class StripeEvent(models.Model):
    event_id = models.CharField(max_length=255, unique=True)
    event_type = models.CharField(max_length=100)
    processed_at = models.DateTimeField(auto_now_add=True)

处理事件时,应在数据库事务中先创建事件记录,再修改余额:

from django.db import IntegrityError, transaction


def handle_checkout_completed(event_id, session):
    if session.get("mode") != "payment":
        return

    if session.get("payment_status") != "paid":
        return

    user_id = session.get("metadata", {}).get("user_id")
    amount_total = session.get("amount_total")

    if not user_id or amount_total is None:
        return

    try:
        with transaction.atomic():
            StripeEvent.objects.create(
                event_id=event_id,
                event_type="checkout.session.completed",
            )

            add_account_credit(
                user_id=user_id,
                amount_cents=amount_total,
                external_reference=session["id"],
            )
    except IntegrityError:
        # 该事件已经处理过
        return

add_account_credit() 应创建不可变的账务流水,而不是只执行:

user.balance += amount

更可靠的做法是保存每一笔 credit、debit、refund 和 adjustment,再根据流水计算或校验余额。

订阅不能只处理创建事件

订阅创建成功,不代表以后每个月都能付款成功。至少需要处理以下事件:

  • invoice.paid:某一期账单付款成功。
  • invoice.payment_failed:付款失败。
  • 订阅取消或状态变化事件。
  • 如果业务需要同步退款和争议状态,还要处理对应事件。

首次订阅可能同时产生 Checkout 和 invoice 相关事件,因此不能在两个事件中重复记入同一笔款项。可以采用下面这套简单规则:

  • checkout.session.completed 只处理 mode="payment" 的一次性付款。
  • 所有订阅周期的到账都以 invoice.paid 为准。
  • 保存 Stripe Customer、Subscription 和 Invoice ID,用于关联本地用户并防止重复记账。

退款怎么做

简单场景可以直接在 Stripe Dashboard 中发起退款,也可以调用 API:

refund = stripe.Refund.create(
    payment_intent=payment_intent_id,
    amount=amount_cents,
)

调用成功只表示 Stripe 接受了退款请求。本地系统仍需记录退款流水,并通过相应 webhook 同步最终状态。不要删除原付款记录,也不要覆盖原交易金额。

“存入账户”需要特别注意

如果”账户余额”只能用于购买你自己提供的商品或服务,通常可以将其实现为站内 credit ledger。

如果用户可以进行以下操作:

  • 提现;
  • 向其他用户转账;
  • 长期储存资金;
  • 将余额兑换成现金;
  • 代第三方收付款;

业务可能就不再是普通的 Payment Gateway 集成,而会涉及 stored value、money transmission、KYC、AML 等合规问题。Stripe Checkout 不会自动解决这些问题。此时应先确认业务模式和法律要求,再判断是否需要 Stripe Connect、专门的钱包基础设施或持牌支付合作方。

能否在 10 小时内完成

满足以下条件时,基础版本有机会在 10 小时内完成:

  • 只收 USD;
  • 只接受美国客户;
  • 使用 Stripe 托管的 Checkout;
  • 订阅价格固定;
  • 不在网站中保存银行卡信息;
  • 退款暂时由 Dashboard 操作;
  • 已有用户、数据库和 HTTPS 环境;
  • 只要求测试环境可用。

生产上线通常需要更多时间,还要完成:

  • webhook 幂等和重试测试;
  • 订阅取消与付款失败处理;
  • 退款、争议和账务核对;
  • 日志与告警;
  • 密钥管理;
  • 税务处理;
  • 服务条款和合规审查。

实际选择时,与其寻找一个包办支付页面的 Django 项目,不如直接使用 Stripe Checkout。支付界面交给 Stripe,Django 负责创建会话、验证 webhook 和维护账务状态。

备注:内容仅供参考。