PAYATHON 2026

Django 用户计费平台与 Middleware 推荐

支付小周

结论

Django 生态中没有一个公认的通用计费 Middleware,能够同时做到与支付渠道无关,并完整支持订阅、单次购买、优惠券、退款、发票和后台管理。

实际项目一般会这样分工:

  • 支付处理商负责收款、支付方式、退款执行、税务能力和支付合规。
  • Django 应用负责用户与客户账号的映射,以及订单、购买历史、订阅状态、权益和业务审计。
  • Webhook 将支付处理商的最终状态同步到本地。
  • 第三方 Django library 用于封装 API 或同步模型,不应成为业务事实的唯一来源。

如果只接入一家支付处理商,通常应先使用其官方 Python SDK,再按需要增加 Django 集成库。如果确实要支持多个支付处理商,可以使用 django-payments 这类统一接口,但订阅、优惠券、发票和税务等高级功能仍需针对各渠道单独适配。

可考虑的 Django 方案

dj-stripe

dj-stripe 面向 Stripe,可以把部分 Stripe 对象同步到 Django 数据库,并提供 Django Admin 集成。它适合需要在后台查看客户、订阅、发票和支付记录的项目。

主要优点有:

  • 集成 Django ORM 和 Admin;
  • 通过 Webhook 同步 Stripe 对象;
  • 减少自行设计支付镜像表的工作;
  • 适合以 Stripe Billing 为中心的订阅业务。

不过,它并不是与支付处理商无关的抽象层。一旦项目深度依赖其数据模型,将来迁移到其他渠道仍会有不少工作。

django-payments

django-payments 提供相对统一的支付接口,可以隔离不同支付后端之间的部分差异,适合单次订单支付或需要接入多个支付渠道的项目。

需要注意的是,这类“统一支付接口”通常只能覆盖创建付款、跳转支付、确认状态和退款等共同能力。各支付处理商在以下功能上的差异很大:

  • 订阅升级、降级和按比例计费;
  • 优惠券、促销码和使用限制;
  • 发票状态和税务处理;
  • 拒付和争议;
  • 异步支付方式;
  • 部分退款和多次退款。

因此,即使使用抽象库,项目中也要保留渠道专用代码。

支付处理商的官方 SDK

对新项目来说,官方 Python SDK 往往是比较稳妥的起点,通常也能更快支持新的 API、Webhook 事件和支付状态。

官方 SDK 一般不会替你设计 Django 业务模型,所以应用内仍需建立订单、订阅和权益模型。这样确实要多写一些代码,但业务边界更清楚,也不会受某个第三方 Django 包的数据模型限制。

较旧或维护状态不明确的项目

django-billing 这类项目可以作为设计参考,但正式采用前需要检查:

  • 最近一次发布和提交时间;
  • 是否支持项目当前使用的 Django 和 Python 版本;
  • Webhook 签名校验是否完整;
  • 是否支持幂等处理;
  • 是否覆盖退款、争议和异步支付;
  • 数据迁移是否稳定;
  • 文档和测试是否仍在持续维护。

如果维护状态不明确,就不适合把核心计费流程建立在这类项目上。

哪些数据应该保存在 Django 中

支付处理商可以保存交易和支付凭证,但 Django 应用仍要保留足够的本地业务记录。一个实用的最小模型通常包括:

  • CustomerAccount:本地用户与外部客户编号的映射;
  • Product 和 Price:应用销售的产品及价格;
  • Order 和 OrderItem:用户实际购买的内容;
  • Subscription:订阅周期和本地状态;
  • Payment:支付尝试及最终结果;
  • Refund:退款记录;
  • InvoiceRecord:外部发票编号、状态和下载地址;
  • WebhookEvent:收到的事件及处理结果;
  • Entitlement:用户当前可以使用的功能或额度。

不要仅凭 Subscription.status == "active" 判断用户是否拥有权限。免费试用、宽限期、退款,以及取消后继续使用至当前周期结束等情况,都会让支付状态和产品权限出现差异。将权益单独建模,可以减少后续处理这些规则时的麻烦。

推荐的处理流程

一次典型购买可以按以下流程完成:

  1. Django 创建状态为 pending 的本地订单。
  2. 服务端调用支付处理商 API,创建支付或结账会话。
  3. 将本地 order_id 写入外部对象的 metadata。
  4. 用户在托管支付页面完成付款。
  5. 支付处理商向 Django 发送 Webhook。
  6. Django 验证签名,并根据事件编号进行幂等处理。
  7. 在数据库事务中更新支付、订单、订阅和权益。
  8. 根据外部发票信息提供收据或发票入口。

浏览器跳转到成功页面,只能说明用户完成了页面跳转,不能证明支付最终成功。订单入账和权益发放应以通过签名验证的 Webhook,或服务端主动查询到的结果为准。

Django 模型示例

下面的模型有意保持简单。实际字段需要根据所接入的支付处理商和业务规则调整:

from django.conf import settings
from django.db import models


class Order(models.Model):
    class Status(models.TextChoices):
        PENDING = "pending", "Pending"
        PAID = "paid", "Paid"
        REFUNDED = "refunded", "Refunded"
        CANCELED = "canceled", "Canceled"

    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.PROTECT,
    )
    status = models.CharField(
        max_length=20,
        choices=Status.choices,
        default=Status.PENDING,
    )
    currency = models.CharField(max_length=3)
    total_minor = models.PositiveBigIntegerField()
    provider = models.CharField(max_length=32)
    provider_payment_id = models.CharField(
        max_length=255,
        blank=True,
        db_index=True,
    )
    created_at = models.DateTimeField(auto_now_add=True)


class WebhookEvent(models.Model):
    provider = models.CharField(max_length=32)
    event_id = models.CharField(max_length=255)
    event_type = models.CharField(max_length=255)
    payload = models.JSONField()
    processed_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        constraints = [
            models.UniqueConstraint(
                fields=["provider", "event_id"],
                name="unique_provider_webhook_event",
            ),
        ]

金额建议使用最小货币单位存储。例如,19.99 美元应保存为 1999,以免受到二进制浮点数精度问题的影响。还要同时保存货币代码,因为并非所有货币都使用两位小数。

Webhook 的幂等处理示例

签名验证方法取决于支付处理商。下面只展示签名验证完成后的通用处理结构,不对应任何特定平台的 API:

from django.db import IntegrityError, transaction
from django.http import HttpResponse
from django.utils import timezone

from .models import Order, WebhookEvent


def payment_webhook(request):
    event = verify_and_parse_webhook(
        body=request.body,
        headers=request.headers,
    )

    try:
        with transaction.atomic():
            webhook_event = WebhookEvent.objects.create(
                provider=event.provider,
                event_id=event.id,
                event_type=event.type,
                payload=event.payload,
            )

            if event.type == "payment.succeeded":
                order_id = event.metadata["order_id"]

                order = Order.objects.select_for_update().get(
                    id=order_id,
                )

                if (
                    event.currency != order.currency
                    or event.amount_minor != order.total_minor
                ):
                    raise ValueError("Payment amount does not match order")

                order.status = Order.Status.PAID
                order.provider_payment_id = event.payment_id
                order.save(
                    update_fields=[
                        "status",
                        "provider_payment_id",
                    ]
                )

                grant_entitlements(order)

            webhook_event.processed_at = timezone.now()
            webhook_event.save(update_fields=["processed_at"])

    except IntegrityError:
        # 相同事件已经成功写入或正在处理
        return HttpResponse(status=200)

    return HttpResponse(status=200)

生产代码还要处理失败重试、异常告警、退款、争议、乱序事件和事件补偿。对于耗时操作,可以先可靠地保存事件,再交给任务队列处理。

发票和收据如何处理

发票不只是 PDF 文件,还可能涉及连续编号、商家信息、税率、税号、贷项通知单和当地规定的保存期限。具体规则取决于经营主体和客户所在地区。

如果支付处理商提供的发票功能符合业务及合规要求,通常可以直接使用,并在 Django 中保存:

  • 外部发票编号;
  • 发票状态;
  • 货币和金额;
  • 托管页面或 PDF 地址;
  • 对应的订单和订阅;
  • 必要的税务快照。

如果必须自行生成正式发票,应先确认当地财税要求。不能只用订单模板生成一份 PDF,就默认它是合规发票。

优惠券、退款和订阅的边界

如果优惠券只影响结账金额,可以交给支付处理商计算,但 Django 仍应保存订单成交时的折扣快照。否则,优惠规则日后发生变化时,历史订单可能无法准确还原。

退款至少要区分以下状态和情况:

  • 退款请求已创建;
  • 支付处理商正在处理;
  • 退款成功;
  • 退款失败;
  • 部分退款;
  • 支付争议或拒付。

订阅则需要明确:

  • 取消是立即生效,还是在当前周期结束后生效;
  • 升级或降级是否按比例计费;
  • 扣款失败后是否进入宽限期;
  • 退款后是否立即收回权益;
  • 套餐价格变化是否影响已有订阅。

这些属于产品规则,不应完全交由第三方 library 隐式决定。

实际选型建议

如果项目只使用一家支付处理商,可以采用“官方 SDK + 本地订单和权益模型”的方案。需要更完整的 Django Admin 和数据同步功能时,再引入该支付处理商对应的成熟 Django 集成库。

如果项目以 Stripe 订阅为核心,可以评估 dj-stripe。如果主要目标是统一多个渠道的基础支付流程,可以评估 django-payments。无论选择哪一种,都应先做最小验证,确认它是否支持项目当前使用的 Django 和 Python 版本,以及所需的 Webhook、订阅变更、部分退款和后台操作。

不建议一开始就自行实现完整的通用计费平台。更可靠的做法是把敏感支付处理和渠道计费功能交给支付处理商,把订单事实、用户权益、业务审计及渠道映射保留在 Django 应用中。

备注:内容仅供参考。