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" 判断用户是否拥有权限。免费试用、宽限期、退款,以及取消后继续使用至当前周期结束等情况,都会让支付状态和产品权限出现差异。将权益单独建模,可以减少后续处理这些规则时的麻烦。
推荐的处理流程
一次典型购买可以按以下流程完成:
- Django 创建状态为
pending的本地订单。 - 服务端调用支付处理商 API,创建支付或结账会话。
- 将本地
order_id写入外部对象的 metadata。 - 用户在托管支付页面完成付款。
- 支付处理商向 Django 发送 Webhook。
- Django 验证签名,并根据事件编号进行幂等处理。
- 在数据库事务中更新支付、订单、订阅和权益。
- 根据外部发票信息提供收据或发票入口。
浏览器跳转到成功页面,只能说明用户完成了页面跳转,不能证明支付最终成功。订单入账和权益发放应以通过签名验证的 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 应用中。
备注:内容仅供参考。
