You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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前端集成要点

  1. 调用CreateSubscriptionView接口获取PayPal授权链接,引导Manager用户跳转至PayPal完成支付。
  2. 支付成功后,PayPal会跳转回你配置的return_url,前端可在此页面调用API获取最新订阅状态并更新UI。
  3. Resident用户登录时,若收到403错误,展示提示信息引导其联系Manager。

五、关键注意事项

  • Webhook签名验证:必须验证PayPal的Webhook签名,避免恶意请求篡改订阅状态。
  • 定时任务:可使用Celery或Django Cron定期检查订阅到期时间,手动标记过期订阅(防止Webhook未触发的情况)。
  • 环境切换:测试用PayPal沙箱环境,上线前切换至生产环境。
  • 订阅管理:给Manager提供查询订阅状态、取消订阅的API接口。

内容的提问来源于stack exchange,提问作者Something Else

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.29 03:45:55