如何在Django REST中区分基础/付费用户以实现付费专属功能?
你的初始思路存在几个关键缺陷:依赖前端跳转更新状态不可靠、单一布尔字段无法处理订阅到期/续费等复杂场景、没有和Stripe的客户体系绑定。下面是行业通用的实现方案,直接解决你提出的问题:
一、模型设计:不要用单一布尔字段,关联Stripe订阅信息
修改你的User模型,添加以下字段来和Stripe的订阅体系对齐:
from django.contrib.auth.models import AbstractUser from django.db import models from datetime import datetime class User(AbstractUser): # 关联Stripe客户ID stripe_customer_id = models.CharField(max_length=255, blank=True, null=True) # 关联Stripe订阅ID stripe_subscription_id = models.CharField(max_length=255, blank=True, null=True) # 标记是否为付费用户(结合到期时间判断) is_premium = models.BooleanField(default=False) # 订阅到期时间,用于校验是否过期 subscription_end_date = models.DateTimeField(blank=True, null=True)
二、核心:用Stripe Webhook处理所有订阅状态变更
绝对不要依赖前端success_url更新用户状态——用户可能在支付成功后关闭页面,或者跳转过程中出错,导致状态不同步。正确的方式是通过Stripe Webhook接收事件,主动更新后端状态。
1. 配置Webhook视图
在Django中创建一个无CSRF保护的视图,用于接收Stripe的事件,并验证签名(防止伪造请求):
import stripe from django.http import HttpResponse from django.views.decorators.http import require_POST from django.views.decorators.csrf import csrf_exempt from django.conf import settings from .models import User from datetime import datetime stripe.api_key = settings.STRIPE_SECRET_KEY @csrf_exempt @require_POST def stripe_webhook(request): payload = request.body sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') event = None # 验证Stripe事件签名 try: event = stripe.Webhook.construct_event( payload, sig_header, settings.STRIPE_WEBHOOK_SECRET ) except (ValueError, stripe.error.SignatureVerificationError): return HttpResponse(status=400) # 处理不同类型的事件 if event['type'] == 'checkout.session.completed': # 用户首次完成订阅支付 session = event['data']['object'] # 从session的metadata中获取你的用户ID(创建checkout session时要传入) user_id = session['metadata']['user_id'] user = User.objects.get(id=user_id) # 获取订阅详情 subscription = stripe.Subscription.retrieve(session['subscription']) user.stripe_customer_id = session['customer'] user.stripe_subscription_id = subscription.id user.is_premium = True user.subscription_end_date = datetime.fromtimestamp(subscription.current_period_end) user.save() elif event['type'] == 'invoice.paid': # 用户续费成功 invoice = event['data']['object'] subscription_id = invoice['subscription'] if subscription_id: subscription = stripe.Subscription.retrieve(subscription_id) user = User.objects.get(stripe_subscription_id=subscription_id) user.subscription_end_date = datetime.fromtimestamp(subscription.current_period_end) user.is_premium = True user.save() elif event['type'] == 'customer.subscription.deleted': # 用户主动取消订阅 subscription = event['data']['object'] user = User.objects.get(stripe_subscription_id=subscription.id) user.is_premium = False user.subscription_end_date = None user.stripe_subscription_id = None user.save() elif event['type'] == 'invoice.payment_failed': # 用户续费失败(比如信用卡过期) invoice = event['data']['object'] subscription_id = invoice['subscription'] if subscription_id: user = User.objects.get(stripe_subscription_id=subscription_id) user.is_premium = False user.save() return HttpResponse(status=200)
2. 在Stripe后台配置Webhook
在Stripe控制台中添加你的webhook URL(比如https://your-domain.com/api/stripe/webhook/),并订阅以下事件:
checkout.session.completedinvoice.paidcustomer.subscription.deletedinvoice.payment_failed
三、权限校验:结合订阅状态和到期时间
写一个自定义权限类,确保只有有效订阅的用户才能访问付费功能:
from rest_framework.permissions import BasePermission from datetime import datetime class IsPremiumUser(BasePermission): def has_permission(self, request, view): if not request.user.is_authenticated: return False # 校验是否为付费用户,且订阅未到期 return request.user.is_premium and ( request.user.subscription_end_date is None or request.user.subscription_end_date > datetime.now() )
在付费专属API视图中使用这个权限:
from rest_framework.views import APIView from rest_framework.response import Response from .permissions import IsPremiumUser class PremiumWritingToolAPI(APIView): permission_classes = [IsPremiumUser] def get(self, request): # 处理付费专属逻辑 return Response({"data": "Premium writing features activated"})
四、你的问题逐一解答
用户停止付费后如何更新状态?
通过Webhook监听customer.subscription.deleted(主动取消)和invoice.payment_failed(续费失败)事件,在处理函数中将is_premium设为False,清空订阅相关字段。如何检测支付成功事件?
依赖Stripe Webhook的checkout.session.completed(首次订阅)和invoice.paid(续费)事件,这些事件由Stripe主动推送到你的后端,可靠性远高于前端跳转。是否应该用Stripe客户对象?
必须用。将stripe_customer_id绑定到你的User模型,能让你将平台用户和Stripe的客户体系一一对应,方便后续查询订阅状态、管理发票、处理用户的订阅变更请求,避免重复创建Stripe客户。
额外提示
- 创建Stripe Checkout Session时,一定要在
metadata中传入你的用户ID,这样Webhook才能找到对应的用户。 - 定期同步订阅状态:可以写一个定时任务(比如用Celery),每天拉取Stripe的订阅数据,和本地数据库对比,避免Webhook漏发导致的状态不一致。
内容的提问来源于stack exchange,提问作者Tyler Kim

