django订阅

如果您熟悉Stripe,就会知道他们在在线支付处理领域中有多大的球员。 他们的API不仅使程序员可以轻松地为诸如电子商务商店之类的网站创建一次性付款,而且还为每月订阅和路由付款提供了快速集成。 如果对Django和Stripe不熟悉,请查看我们最近的有关一次性付款集成的文章 。 否则,让我们开始使用Django和Stripe设置每月付款。

为什么要按月订阅?

每月订阅是在网络上常见的一种惯例,特别是在那些推广软件即服务(SAAS)作为其交付模型的公司中。 例如,诸如Hubspot(市场营销),Dropbox(数据存储)和Mailchimp(电子邮件市场营销)之类的SAAS公司都为其潜在客户提供了分层定价选项。 考虑到一旦计算出基本指标(客户获取成本,生命周期价值,客户流失率)就更容易预测收入,许多人认为此模型是有利的。 当然,可预测的收入可以创造稳定并产生更准确的财务预测。

Mailchimp定价页面


Django设定

首先设置虚拟环境并创建一个基本的Django项目。 我们将创建一个名为saas的新虚拟环境。

注意:在Mac上,对所有命令使用python3而不是py

C:\Users\Owner\Desktop\code>py -m venv saas

接下来,将目录更改为虚拟环境,安装Django,然后设置您的项目和应用程序。

C:\Users\Owner\Desktop\code>cd saas

C:\Users\Owner\Desktop\code\saas>Scripts\activate

(saas) C:\Users\Owner\Desktop\code\saas>pip install Django

(saas) C:\Users\Owner\Desktop\code\saas>django-admin startproject mysite

(saas) C:\Users\Owner\Desktop\code\saas\mysite>py manage.py startapp main

将主应用程序添加到mysite中settings.py中。

settings.py

INSTALLED_APPS = ['main.apps.MainConfig' , #add this
    'django.contrib.admin' ,
    'django.contrib.auth' ,
    'django.contrib.contenttypes' ,
    'django.contrib.sessions' ,
    'django.contrib.messages' ,
    'django.contrib.staticfiles' ,
]

文件夹中创建urls.py ,并将其包含在mysite> urls.py中

mysite> urls.py

from django.contrib import admin
from django.urls import path, include #add include

urlpatterns = [
	path( '' , include( 'main.urls' )),  #add path
    path( 'admin/' , admin.site.urls),
]

条纹整合

首先安装用于连接Stripe API的官方库。

pip install --upgrade stripe

接下来,创建一个Stripe帐户,并在其仪表板中创建产品。 尽管可以使用Stripe CLI来执行此操作,但由于我们已经在创建帐户页面上,因此我们将使用仪表板。 确保您处于测试模式,并且只能查看测试数据。 默认情况下,创建帐户后,您应该处于测试模式。 单击左侧导航菜单中的产品。 创建两个新产品:

  • 将产品命名为高级计划
  • 添加描述“高级功能的付费计划”
  • 使用标准定价
  • 输入$ 15.00并确保选择“重复”
  • 保持结算期为“每月”
  • 命名产品企业计划
  • 添加描述“高级功能的企业计划”
  • 使用标准定价
  • 输入$ 30.00并确保选择“重复”
  • 保持结算期为“每月”
  • 现在,让我们在Stripe仪表板中同步产品。 虽然我们可以创建模型并存储相关产品信息(例如产品ID)作为模型字段,但更简单的解决方案是仅安装dj-stripe程序包并使用sync命令。 我们还需要在settings.py中添加我们的API密钥。 请注意,您的活动密钥应始终受到保护,并且永远不会在设置中列出。 有关保护环境变量的更多信息,请查看Python保护。

    pip install dj-stripe

    mysite / settings.py

    STRIPE_TEST_PUBLIC_KEY ='pk_test_E52Nh0gTCRpJ7h4JhuEX7BIO006LVew6GG'
    STRIPE_TEST_SECRET_KEY = 'sk_test_L87kx7GQNbz9tajOluDts7da00mSbze3dW'
    STRIPE_LIVE_MODE = False  # Change to True in production
    DJSTRIPE_WEBHOOK_SECRET = "whsec_xxx"

    最后迁移数据库。 注意:如果数据库是sqlite,则迁移所需的时间比平时长。

    py manage.py migrate

    使用以下命令将产品自动添加到数据库:

    py manage.py djstripe_sync_plans_from_stripe

    查看管理页面以查看我们刚刚进行的更改。 首先创建一个管理员用户。 包括电子邮件,例如test@example,因为我们将其用作Stripe客户名,然后访问http://127.0.0.1:8000/admin/ 。 您应该看到各种新模型,包括我们新创建的产品以及产品计划。

    py manage.py createsuperuser

结帐页面

现在我们已经同步了数据,让我们创建一个结帐页面,用户可以在其中选择计划和结帐。 我们将从视图和签出模板开始。 注意:我们已经在home.html中导入了Bootstrap CDN:

views.py

from django.shortcuts import render, redirect
import stripe
import json
from django.http import JsonResponse
from djstripe.models import Product
from django.contrib.auth.decorators import login_required

# Create your views here.
def homepage (request) :
	return render(request, "home.html" )

@login_required
def checkout (request) :
	products = Product.objects.all()
	return render(request, "checkout.html" ,{ "products" : products})

checkout.html

{% extends "home.html" %}

{% block content %}< script src = "https://js.stripe/v3/" >  </ script >

< br > < br >

	
< div class = "container " >

	< div class = "row " >
		{% for p in products %}
		< div class = "col-6" >
			< div class = "card mx-5 shadow" style = "border-radius: 10px; border:none; " >
				< div class = "card-body" >
					< h5 class = "card-title font-weight-bold" > {{p.name}} </ h5 >
					< p class = "card-text text-muted" > < svg class = "bi bi-check" width = "1em" height = "1em" viewBox = "0 0 16 16" fill = "currentColor" xmlns = "http://www.w3/2000/svg" >
						< path fill-rule = "evenodd" d = "M10.97 4.97a.75.75 0 0 1 1.071 1.05l-3.992 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.236.236 0 0 1 .02-.022z" />
					</ svg > {{p.description}} </ p >

					{% for plan in p.plan_set.all %}
					< h5 > {{ plan.human_readable_price }} </ h5 >
					< div class = "text-right" >
						< input type = "checkbox" name = "{{p.name}}" value = "{{p.id}}" onclick = "planSelect('{{p.name}}' ,'{{plan.human_readable_price}}', '{{plan.id}}')" >
					{% endfor %}
					</ div >
				</ div >
			</ div >

		</ div >

		{% endfor %}
	</ div >
	< br > < br > < hr > < br > < br >
	< div >
		< div class = "row" >
			< div class = "col-12" >
				< div class = "card mx-5 shadow rounded" style = "border-radius:50px;border:none" >
					< div class = "card-body" >
						< h5 class = "card-title font-weight-bold" > Checkout </ h5 >
						< p class = "text-muted " > Enter card details.  Your subscription will start immediately </ p >
						< div class = "row" >
							< div class = "col-6 text-muted" >
								< p > Plan: </ p >
								< p > Total: </ p >
							</ div >
							< div class = "col-6 text-right" >
								< p id = "plan" > </ p >
								< p id = "price" > </ p >
								< p hidden id = "priceId" > </ p >
							</ div >

						</ div >
						< br >
						< form id = "subscription-form" >
							< div id = "card-element" class = "MyCardElement" >
								<!-- Elements will create input elements here -->
							</ div >
							
							<!-- We'll put the error messages in this element -->
							< div id = "card-errors" role = "alert" > </ div >
							< button id = "submit" type = "submit" >
								< div class = "spinner-border  spinner-border-sm text-light hidden" id = "spinner" role = "status" >
									< span class = "sr-only" > Loading... </ span >
								</ div >
								< span id = "button-text" > Subscribe </ span >
							</ button >
						</ form >
					</ div >
				</ div >

			</ div >
		</ div >
	</ div >

</ div >

{% endblock %}

现在,我们有了用于选择订阅计划的模板。 让我们添加一些样式,并使用Stripe Elements(预先构建的一组UI组件)设置信用卡表单。

<style >
body {


    .StripeElement {
      box-sizing : border-box;

      height : 40px ;

      padding : 10px 12px ;

      border : 1px solid transparent;
      border-radius : 4px ;
      background-color : white;

      box-shadow : 0 1px 3px 0 #e6ebf1 ;
      -webkit-transition : box-shadow 150ms ease;
      transition : box-shadow 150ms ease;
    }

    .StripeElement--focus {
      box-shadow : 0 1px 3px 0 #cfd7df ;
    }

    .StripeElement--invalid {
      border-color : #fa755a ;
    }

    .StripeElement--webkit-autofill {
      background-color : #fefde5 !important ;
    }
    .hidden {
        display : none;
    }


    #submit :hover {
      filter : contrast (120%);
    }

    #submit {
      font-feature-settings : "pnum" ;
      --body-color : #f7fafc ;
      --button-color : #556cd6 ;
      --accent-color : #556cd6 ;
      --gray-border : #e3e8ee ;
      --link-color : #fff ;
      --font-color : #697386 ;
      --body-font-family : -apple-system,BlinkMacSystemFont,sans-serif;
      --radius : 4px ;
      --form-width : 400px ;
      -webkit-box-direction : normal;
      word-wrap : break-word;
      box-sizing : border-box;
      font : inherit;
      overflow : visible;
      -webkit-appearance : button;
      -webkit-font-smoothing : antialiased;
      margin : 0 ;
      font-family : inherit;
      -webkit-tap-highlight-color : transparent;
      font-size : 16px ;
      padding : 0 12px ;
      line-height : 32px ;
      outline : none;
      text-decoration : none;
      text-transform : none;
      margin-right : 8px ;
      height : 36px ;
      border-radius : var (--radius);
      color : #fff ;
      border : 0 ;
      margin-top : 16px ;
      font-weight : 600 ;
      cursor : pointer;
      transition : all . 2s ease;
      display : block;
      box-shadow : 0 4px 5.5px 0 rgba (0,0,0,.07);
      width : 100% ;
      background : var (--button-color);
    }

</ style >
document .getElementById( "submit" ).disabled = true ;

stripeElements();

function stripeElements ()  {
  stripe = Stripe( 'pk_test_E52Nh0gTCRpJ7h4JhuEX7BIO006LVew6GG' );

  if ( document .getElementById( 'card-element' )) {
    let elements = stripe.elements();

    // Card Element styles
    let style = {
    	base : {
    		color : "#32325d" ,
    		fontFamily : '"Helvetica Neue", Helvetica, sans-serif' ,
    		fontSmoothing : "antialiased" ,
    		fontSize : "16px" ,
    		"::placeholder" : {
    			color : "#aab7c4"
    		}
    	},
    	invalid : {
    		color : "#fa755a" ,
    		iconColor : "#fa755a"
    	}
    };


    card = elements.create( 'card' , { style : style });

    card.mount( '#card-element' );

    card.on( 'focus' , function ()  {
      let el = document .getElementById( 'card-errors' );
      el.classList.add( 'focused' );
    });

    card.on( 'blur' , function ()  {
      let el = document .getElementById( 'card-errors' );
      el.classList.remove( 'focused' );
    });

    card.on( 'change' , function ( event )  {
      displayError(event);
    });
  }
  //we'll add payment form handling here
}

function displayError ( event )  {
 
  let displayError = document .getElementById( 'card-errors' );
  if (event.error) {
    displayError.textContent = event.error.message;
  } else {
    displayError.textContent = '' ;
  }
}

这应允许信用卡表格呈现。 首先,我们禁用了订阅按钮,然后调用stripeElements()创建然后挂载信用卡表格。 接下来,我们添加一个小脚本,以使用选定的订阅计划更新模板,并在选择计划后重新启用订阅按钮。

function planSelect ( name, price, priceId )  {
		var inputs = document .getElementsByTagName( 'input' );

		for ( var i = 0 ; i<inputs.length; i++){
			inputs[i].checked = false ;
			if (inputs[i].name== name){

				inputs[i].checked = true ;
			}
		}

		var n = document .getElementById( 'plan' );
		var p = document .getElementById( 'price' );
		var pid = document .getElementById( 'priceId' );
		n.innerHTML = name;
		p.innerHTML = price;
		pid.innerHTML = priceId;
        document .getElementById( "submit" ).disabled = false ;


	}

现在,我们需要处理付款表格提交。 我们将第一个代码块添加到stripeElements()函数的末尾。 首先通过表单的ID抓取表单,然后添加一个事件侦听器。 在阻止默认表单提交之后,我们更改了表单的加载状态,以防止与双击订阅按钮相关的任何问题。 最后,使用输入的卡信息,我们创建一种付款方式,并将此数据以及所选订阅的价格ID提交到我们的服务器。

如所示,我们使用Django的CSRF令牌来授权向我们服务器的提交。

注意:Stripe的文档包括一些其他验证,如果有兴趣,您应该检查一下。 另外,与文档不同,我们在一个请求中创建了一个客户并订阅了服务器,而不是发送单独的请求。

//we'll add payment form handling here
    let paymentForm = document .getElementById( 'subscription-form' );
	if (paymentForm) {

		paymentForm.addEventListener( 'submit' , function ( evt )  {
			evt.preventDefault();
			changeLoadingState( true );


	      // create new payment method & create subscription
	      createPaymentMethod({ card });
	  });
	}

}



function createPaymentMethod ( { card } )  {

  // Set up payment method for recurring usage
  let billingName = '{{user.username}}' ;

  stripe
    .createPaymentMethod({
      type : 'card' ,
      card : card,
      billing_details : {
        name : billingName,
      },
    })
    .then( ( result ) => {
      if (result.error) {
        displayError(result);
      } else {
       const paymentParams = {
          price_id : document .getElementById( "priceId" ).innerHTML,
          payment_method : result.paymentMethod.id,
      };
      fetch( "/create-sub" , {
        method : 'POST' ,
        headers : {
          'Content-Type' : 'application/json' ,
          'X-CSRFToken' : '{{ csrf_token }}' ,
        },
        credentials : 'same-origin' ,
        body : JSON .stringify(paymentParams),
      }).then( ( response ) => {
        return response.json(); 
      }).then( ( result ) => {
      	if (result.error) {
          // The card had an error when trying to attach it to a customer
          throw result;
        }
        return result;
      }).then( ( result ) => {
      	if (result && result.status === 'active' ) {

         window .location.href = '/complete' ;
      	};
      }).catch( function ( error )  {
          displayError(result.error.message);

      });
      }
    });
}


var changeLoadingState = function ( isLoading )  {
	if (isLoading) {
		document .getElementById( "submit" ).disabled = true ;
		document .querySelector( "#spinner" ).classList.remove( "hidden" );
		document .querySelector( "#button-text" ).classList.add( "hidden" );
	} else {
		document .getElementById( "submit" ).disabled = false ;
		document .querySelector( "#spinner" ).classList.add( "hidden" );
		document .querySelector( "#button-text" ).classList.remove( "hidden" );
	}
};

添加用于创建订阅/客户以及付款完成时的网址路径。

main / urls.py

from django.urls import path
from . import views

app_name = "main"   

urlpatterns = [
	path( "" , views.homepage, name= "homepage" ),
	path( "checkout" , views.checkout, name= "checkout" ),
	path( "logout" , views.logout_request, name= "logout_request" ),
	path( "login" , views.login_request, name= "logout_request" ),
	path( "register" , views.register, name= "register" ),
	path( "create-sub" , views.create_sub, name= "create sub" ), #add
	path( "complete" , viewsplete, name= "complete" ), #add

]

接下来,创建一个视图来处理创建Stripe客户和订阅。 客户和订阅也应存储在我们的本地数据库中。 幸运的是,我们可以使用dj-stripe轻松合成数据。

main / views.py

...import stripe
import json
from django.http import JsonResponse
from djstripe.models import Product
from django.contrib.auth.decorators import login_required
import djstripe
from django.http import HttpResponse

...

@login_required
def create_sub (request) :
	if request.method == 'POST' :
	    # Reads application/json and returns a response
	    data = json.loads(request.body)
	    payment_method = data[ 'payment_method' ]
	    stripe.api_key = djstripe.settings.STRIPE_SECRET_KEY

	    payment_method_obj = stripe.PaymentMethod.retrieve(payment_method)
	    djstripe.models.PaymentMethod.sync_from_stripe_data(payment_method_obj)


	    try :
	        # This creates a new Customer and attaches the PaymentMethod in one API call.
	        customer = stripe.Customer.create(
	            payment_method=payment_method,
	            email=request.user.email,
	            invoice_settings={
	                'default_payment_method' : payment_method
	            }
	        )

	        djstripe_customer = djstripe.models.Customer.sync_from_stripe_data(customer)
	        request.user.customer = djstripe_customer
	       

	        # At this point, associate the ID of the Customer object with your
	        # own internal representation of a customer, if you have one.
	        # print(customer)

	        # Subscribe the user to the subscription created
	        subscription = stripe.Subscription.create(
	            customer=customer.id,
	            items=[
	                {
	                    "price" : data[ "price_id" ],
	                },
	            ],
	            expand=[ "latest_invoice.payment_intent" ]
	        )

	        djstripe_subscription = djstripe.models.Subscription.sync_from_stripe_data(subscription)

	        request.user.subscription = djstripe_subscription
	        request.user.save()

	        return JsonResponse(subscription)
	    except Exception as e:
	        return JsonResponse({ 'error' : (e.args[ 0 ])}, status = 403 )
	else :
		return HTTPresponse( 'requet method not allowed' )

还为付款完成页面添加查看功能:

def complete (request) :
	return render(request, "complete.html" )

通过测试集成。 根据Stripe的建议,使用“ 4242 4242 4242 4242”作为测试信用卡,并在提交付款后查看结果。 单击Stripe仪表板中“客户”下的“订阅”,以查看新创建的订阅和客户。 您应该看到类似以下内容:

结论

感谢您阅读有关使用Django和Stripe创建月度订阅的信息。

希望这为创建您自己的SAAS项目奠定了基础。 我们计划在Django和Stripe上编写其他文章,以涵盖诸如使用Stripe Connect进行付款,Lyft,Postmates,Kickstarter等背后的基础付款技术的主题。

另外,如果您想了解其他有关Django集成的建议,请在下面留下评论。

先前发布在 https://www.ordinarycoders/blog/article/django-stripe-monthly-subscription

翻译自: https://hackernoon/setting-up-subscriptions-and-recurring-payments-using-django-and-stripe-lh2d3ujc

django订阅

更多推荐

django订阅_使用Django和Stripe设置订阅和定期付款