如何在Django REST Framework网站中集成PayPal订阅功能
Django REST Framework 订阅功能实现方案
针对你的需求——Manager用户每月支付20美元订阅,下属Resident用户在Manager未付费时无法登录,以下是完整的实现步骤:
一、扩展数据模型
首先需要添加订阅相关的模型,用来记录Manager的订阅状态、PayPal订阅ID、到期时间等信息:
from django.db import models from django.utils import timezone from django.db.models.signals import post_save from django.dispatch import receiver class Subscription(models.Model): # 订阅常量定义 MANAGER_SUBSCRIPTION_PRICE = 20.00 STATUS_ACTIVE = 'active' STATUS_CANCELLED = 'cancelled' STATUS_EXPIRED = 'expired' STATUS_CHOICES = [ (STATUS_ACTIVE, '正常'), (STATUS_CANCELLED, '已取消'), (STATUS_EXPIRED, '已过期'), ] manager = models.OneToOneField(Manager, on_delete=models.CASCADE, related_name='subscription') paypal_subscription_id = models.CharField(max_length=255, blank=True, null=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES, default=STATUS_EXPIRED) start_date = models.DateTimeField(null=True, blank=True) next_billing_date = models.DateTimeField(null=True, blank=True) expires_at = models.DateTimeField(null=True, blank=True) def is_active(self): # 判断订阅是否有效:状态正常且未过期 if self.status != self.STATUS_ACTIVE: return False return self.expires_at > timezone.now() def __str__(self): return f"{self.manager.user.username} 的订阅" # 新建Manager时自动创建订阅记录 @receiver(post_save, sender=Manager) def create_manager_subscription(sender, instance, created, **kwargs): if created: Subscription.objects.create(manager=instance)
二、DRF 订阅API开发
1. 序列化器
先写序列化器来处理订阅数据的序列化和反序列化:
from rest_framework import serializers from .models import Subscription, Manager class SubscriptionSerializer(serializers.ModelSerializer): manager_username = serializers.CharField(source='manager.user.username', read_only=True) is_active = serializers.BooleanField(source='is_active', read_only=True) class Meta: model = Subscription fields = ['id', 'manager_username', 'status', 'start_date', 'next_billing_date', 'expires_at', 'is_active'] class ManagerSubscriptionSerializer(serializers.ModelSerializer): subscription = SubscriptionSerializer(read_only=True) class Meta: model = Manager fields = ['id', 'user', 'village_name', 'subscription']
2. 生成PayPal订阅链接的视图
首先去PayPal开发者后台创建一个每月20美元的订阅计划,拿到计划ID后,编写视图生成订阅链接给前端:
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated import requests from django.conf import settings from django.utils import timezone class CreateSubscriptionView(APIView): permission_classes = [IsAuthenticated] def post(self, request): # 验证当前用户是Manager if not request.user.is_manager: return Response({"error": "仅管理员用户可创建订阅"}, status=403) manager = request.user.manager paypal_plan_id = settings.PAYPAL_MONTHLY_PLAN_ID # 替换为你的PayPal计划ID # 获取PayPal访问令牌 access_token = self._get_paypal_access_token() if not access_token: return Response({"error": "获取PayPal令牌失败"}, status=500) # 调用PayPal API创建订阅 paypal_api_url = "https://api-m.sandbox.paypal.com/v1/billing/subscriptions" if settings.DEBUG else "https://api-m.paypal.com/v1/billing/subscriptions" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {access_token}" } payload = { "plan_id": paypal_plan_id, "application_context": { "return_url": f"{settings.FRONTEND_URL}/subscription-success?manager_id={manager.id}", "cancel_url": f"{settings.FRONTEND_URL}/subscription-cancel" } } response = requests.post(paypal_api_url, json=payload, headers=headers) if response.status_code != 201: return Response(response.json(), status=response.status_code) paypal_sub_data = response.json() # 更新本地订阅记录 subscription = manager.subscription subscription.paypal_subscription_id = paypal_sub_data['id'] subscription.status = Subscription.STATUS_ACTIVE subscription.start_date = timezone.datetime.fromisoformat(paypal_sub_data['start_time'].replace('Z', '+00:00')) subscription.next_billing_date = timezone.datetime.fromisoformat(paypal_sub_data['billing_info']['next_billing_time'].replace('Z', '+00:00')) subscription.expires_at = timezone.datetime.fromisoformat(paypal_sub_data['billing_info']['next_billing_time'].replace('Z', '+00:00')) subscription.save() # 返回PayPal授权链接给前端 approval_url = next(link['href'] for link in paypal_sub_data['links'] if link['rel'] == 'approve') return Response({ "subscription_id": paypal_sub_data['id'], "approval_url": approval_url }) def _get_paypal_access_token(self): paypal_auth_url = "https://api-m.sandbox.paypal.com/v1/oauth2/token" if settings.DEBUG else "https://api-m.paypal.com/v1/oauth2/token" auth = (settings.PAYPAL_CLIENT_ID, settings.PAYPAL_CLIENT_SECRET) data = {"grant_type": "client_credentials"} response = requests.post(paypal_auth_url, auth=auth, data=data) if response.status_code != 200: return None return response.json()['access_token']
3. PayPal Webhook 处理视图
PayPal会在订阅状态变化时发送webhook(比如支付成功、订阅过期、取消),需要编写视图处理这些事件并更新本地订阅状态:
from rest_framework.views import APIView from rest_framework.response import Response import requests from django.conf import settings from django.utils import timezone class PayPalWebhookView(APIView): permission_classes = [] # 无需用户认证,但必须验证PayPal签名 def post(self, request): # 验证PayPal webhook签名(必须做,防止伪造请求) webhook_verified = self._verify_paypal_webhook(request) if not webhook_verified: return Response({"error": "无效的Webhook签名"}, status=403) event_data = request.data event_type = event_data['event_type'] subscription_id = event_data['resource']['id'] try: subscription = Subscription.objects.get(paypal_subscription_id=subscription_id) except Subscription.DoesNotExist: return Response({"error": "订阅记录不存在"}, status=404) # 根据事件类型更新订阅状态 if event_type == 'BILLING.SUBSCRIPTION.CREATED': subscription.status = Subscription.STATUS_ACTIVE subscription.start_date = timezone.datetime.fromisoformat(event_data['resource']['start_time'].replace('Z', '+00:00')) subscription.next_billing_date = timezone.datetime.fromisoformat(event_data['resource']['billing_info']['next_billing_time'].replace('Z', '+00:00')) subscription.expires_at = timezone.datetime.fromisoformat(event_data['resource']['billing_info']['next_billing_time'].replace('Z', '+00:00')) elif event_type == 'BILLING.SUBSCRIPTION.ACTIVATED': subscription.status = Subscription.STATUS_ACTIVE elif event_type == 'BILLING.SUBSCRIPTION.EXPIRED': subscription.status = Subscription.STATUS_EXPIRED elif event_type == 'BILLING.SUBSCRIPTION.CANCELLED': subscription.status = Subscription.STATUS_CANCELLED elif event_type == 'BILLING.SUBSCRIPTION.PAYMENT.FAILED': # 支付失败直接标记为过期 subscription.status = Subscription.STATUS_EXPIRED subscription.save() return Response({"status": "处理成功"}) def _verify_paypal_webhook(self, request): paypal_webhook_id = settings.PAYPAL_WEBHOOK_ID auth_algo = request.headers.get('Paypal-Auth-Algo') cert_url = request.headers.get('Paypal-Cert-Url') transmission_id = request.headers.get('Paypal-Transmission-Id') transmission_sig = request.headers.get('Paypal-Transmission-Sig') transmission_time = request.headers.get('Paypal-Transmission-Time') webhook_event = request.data access_token = self._get_paypal_access_token() if not access_token: return False verify_url = "https://api-m.sandbox.paypal.com/v1/notifications/verify-webhook-signature" if settings.DEBUG else "https://api-m.paypal.com/v1/notifications/verify-webhook-signature" headers = { "Content-Type": "application/json", "Authorization": f"Bearer {access_token}" } verify_payload = { "auth_algo": auth_algo, "cert_url": cert_url, "transmission_id": transmission_id, "transmission_sig": transmission_sig, "transmission_time": transmission_time, "webhook_id": paypal_webhook_id, "webhook_event": webhook_event } response = requests.post(verify_url, json=verify_payload, headers=headers) return response.json()['verification_status'] == 'SUCCESS' def _get_paypal_access_token(self): paypal_auth_url = "https://api-m.sandbox.paypal.com/v1/oauth2/token" if settings.DEBUG else "https://api-m.paypal.com/v1/oauth2/token" auth = (settings.PAYPAL_CLIENT_ID, settings.PAYPAL_CLIENT_SECRET) data = {"grant_type": "client_credentials"} response = requests.post(paypal_auth_url, auth=auth, data=data) if response.status_code != 200: return None return response.json()['access_token']
三、限制Resident用户登录
需要在Resident用户登录或认证时,检查其所属Manager的订阅状态:
1. 自定义Token认证类
如果你用的是DRF的Token认证,编写自定义认证类:
from rest_framework.authentication import TokenAuthentication from rest_framework.exceptions import AuthenticationFailed from .models import Resident, Subscription class ResidentTokenAuthentication(TokenAuthentication): def authenticate_credentials(self, key): user, token = super().authenticate_credentials(key) # 仅对Resident用户做订阅检查 if user.is_resident: try: resident = Resident.objects.get(user=user) # 假设你的Dwar模型有manager字段关联到Manager manager = resident.selected_village_name.manager if not manager.subscription.is_active(): raise AuthenticationFailed("你的管理员订阅已过期,请联系管理员续费后再登录。") except Resident.DoesNotExist: raise AuthenticationFailed("未找到居民账户信息。") except Exception: raise AuthenticationFailed("无法验证订阅状态,请联系管理员。") return user, token
然后在settings.py中配置:
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': [ 'your_app_name.authentication.ResidentTokenAuthentication', # 其他认证类... ], }
2. 自定义登录视图
如果是自定义登录接口,在返回Token前做检查:
from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.authtoken.models import Token from django.contrib.auth import authenticate from .models import Resident, Subscription class LoginView(APIView): def post(self, request): username = request.data.get('username') password = request.data.get('password') user = authenticate(username=username, password=password) if not user: return Response({"error": "用户名或密码错误"}, status=400) # 检查Resident用户的管理员订阅状态 if user.is_resident: try: resident = Resident.objects.get(user=user) manager = resident.selected_village_name.manager if not manager.subscription.is_active(): return Response({"error": "你的管理员订阅已过期,请联系管理员续费后再登录。"}, status=403) except (Resident.DoesNotExist, Exception): return Response({"error": "无法验证账户信息"}, status=400) # 生成或获取Token token, created = Token.objects.get_or_create(user=user) return Response({"token": token.key})
四、React前端集成要点
- 调用
CreateSubscriptionView接口获取PayPal授权链接,引导Manager用户跳转至PayPal完成支付。 - 支付成功后,PayPal会跳转回你配置的
return_url,前端可在此页面调用API获取最新订阅状态并更新UI。 - Resident用户登录时,若收到403错误,展示提示信息引导其联系Manager。
五、关键注意事项
- Webhook签名验证:必须验证PayPal的Webhook签名,避免恶意请求篡改订阅状态。
- 定时任务:可使用Celery或Django Cron定期检查订阅到期时间,手动标记过期订阅(防止Webhook未触发的情况)。
- 环境切换:测试用PayPal沙箱环境,上线前切换至生产环境。
- 订阅管理:给Manager提供查询订阅状态、取消订阅的API接口。
内容的提问来源于stack exchange,提问作者Something Else
相关产品推荐
相关产品推荐

