0

0

Django与Stripe集成:实现单品与订单支付的教程

聖光之護

聖光之護

发布时间:2025-11-28 13:56:51

|

538人浏览过

|

来源于php中文网

原创

Django与Stripe集成:实现单品与订单支付的教程

本教程详细介绍了如何在django项目中集成stripe支付,以支持单品购买和多商品订单支付。内容涵盖了stripe checkout会话的创建、django模型与视图的实现、url路由配置以及前端javascript集成。同时,文章还提供了针对常见404错误和代码逻辑问题的排查与优化建议,旨在帮助开发者构建稳定可靠的电商支付系统。

在现代Web应用中,集成第三方支付平台是实现电子商务功能的关键一环。Stripe以其强大的API和灵活的集成方式,成为许多开发者的首选。本教程将以一个Django项目为例,展示如何利用Stripe Checkout实现商品和订单的支付流程。

1. 项目概览与模型设计

为了处理商品和订单,我们需要定义相应的Django模型。

models.py

from django.db import models

class Item(models.Model):
    """
    商品模型,包含名称、描述和价格。
    """
    name = models.CharField(max_length=100)
    description = models.TextField()
    price = models.IntegerField(default=0)  # 价格以美分存储,方便Stripe处理

    def __str__(self):
        return self.name

    def display_price(self):
        """格式化显示价格为美元"""
        return "{0:.2f}".format(self.price / 100) # 注意:这里假设price是美分,显示时除以100

class Order(models.Model):
    """
    订单模型,包含客户信息、创建时间及关联的商品。
    """
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)
    email = models.EmailField()
    address = models.CharField(max_length=250)
    postal_code = models.CharField(max_length=20)
    city = models.CharField(max_length=100)
    created_at = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    paid = models.BooleanField(default=False)
    items = models.ManyToManyField(Item, through='OrderItem') # 通过中间模型关联商品

    class Meta:
        ordering = ('-created_at',)

    def __str__(self):
        return f'Order {self.id} | {self.created_at.strftime("%d.%m.%Y %H:%M")}'

    def get_total_cost(self):
        """计算订单总价"""
        # 注意:这里需要遍历OrderItem来获取实际的购买价格和数量
        # 如果OrderItem的price字段代表的是购买时的单价,则应使用OrderItem的price
        # 否则,如果OrderItem只关联Item,且Item的price是实时价格,则使用Item.price
        # 假设OrderItem的price和quantity是准确的
        return sum(item.get_cost() for item in self.order_items.all()) # 访问related_name 'order_items'

class OrderItem(models.Model):
    """
    订单项模型,用于连接订单和商品,并记录购买时的价格和数量。
    """
    order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='order_items')
    item = models.ForeignKey(Item, related_name='order_items', on_delete=models.CASCADE)
    price = models.DecimalField(max_digits=10, decimal_places=2) # 购买时的单价
    quantity = models.PositiveIntegerField(default=1)

    def __str__(self):
        return f'{self.id}'

    def get_cost(self):
        """计算单个订单项的总价"""
        return self.price * self.quantity

模型说明:

  • Item:代表可单独购买的商品。price字段以整数形式存储价格(例如,1000代表10.00美元),这是Stripe推荐的做法,避免浮点数精度问题。
  • Order:代表一个订单,包含客户信息和订单创建时间。它通过 OrderItem 中间模型与 Item 建立多对多关系。get_total_cost 方法用于计算订单的总金额。
  • OrderItem:作为 Order 和 Item 之间的桥梁,记录了特定商品在特定订单中的购买价格和数量,这对于处理价格变动至关重要。

2. Stripe Checkout会话创建视图

Stripe Checkout通过创建会话(Session)来引导用户完成支付。我们需要为单品购买和订单购买分别创建视图。

views.py

import stripe
from django.conf import settings
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views import View
from django.views.generic import TemplateView

from .models import Item, Order, OrderItem

# 初始化Stripe API密钥
stripe.api_key = settings.STRIPE_SECRET_KEY

class ProductLandingPageView(TemplateView):
    """
    单个商品详情页视图。
    """
    template_name = 'landing.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        item_id = self.kwargs["item_id"]
        item = get_object_or_404(Item, id=item_id)
        context['item'] = item
        context.update({
            "STRIPE_PUBLIC_KEY": settings.STRIPE_PUBLISHABLE_KEY
        })
        return context

class CreateCheckoutSessionView(View):
    """
    创建单个商品Stripe Checkout会话的视图。
    """
    def get(self, request, *args, **kwargs):
        item_id = self.kwargs["item_id"]
        # 确保域名与Stripe配置的Webhooks一致,生产环境应使用HTTPS
        DOMAIN: str = 'http://127.0.0.1:8000' 
        item = get_object_or_404(Item, id=item_id) # 使用get_object_or_404更健壮

        session = stripe.checkout.Session.create(
            payment_method_types=['card'],
            line_items=[
                {
                    'price_data': {
                        'currency': 'usd',
                        'unit_amount': item.price,  # 确保这里是美分
                        'product_data': {
                            'name': item.name,
                        },
                    },
                    'quantity': 1,
                },
            ],
            payment_intent_data={
                'metadata': {
                    'item_id': item.id,
                },
            },
            mode='payment',
            success_url=DOMAIN + '/success/',
            cancel_url=DOMAIN + '/cancel/',
        )
        return JsonResponse({'id': session.id})

class CreateCheckoutSessionOrderView(View):
    """
    创建订单Stripe Checkout会话的视图。
    """
    def get(self, request, *args, **kwargs):
        order_id = self.kwargs["order_id"]
        DOMAIN: str = 'http://127.0.0.1:8000'
        order = get_object_or_404(Order, id=order_id) # 使用get_object_or_404更健壮

        # 针对订单,Stripe Checkout通常有两种处理方式:
        # 1. 将整个订单作为一个“产品”,价格为订单总价。
        # 2. 将订单中的每个商品作为单独的line_item。
        # 这里采用第一种方式,将订单总价作为单个line_item。

        # 优化:确保订单总价计算正确,且Stripe产品名称使用str(order)获取
        total_cost_cents = int(order.get_total_cost() * 100) # 将Decimal转换为整数美分

        session = stripe.checkout.Session.create(
            payment_method_types=['card'],
            line_items=[
                {
                    'price_data': {
                        'currency': 'usd',
                        'unit_amount': total_cost_cents, # 订单总价(美分)
                        'product_data': {
                            'name': str(order), # 正确调用__str__方法
                        },
                    },
                    'quantity': 1, # 订单整体数量为1
                },
            ],
            payment_intent_data={
                'metadata': {
                    'order_id': order.id,
                },
            },
            mode='payment',
            success_url=DOMAIN + '/success/',
            cancel_url=DOMAIN + '/cancel/',
        )
        return JsonResponse({'id': session.id})

class OrderView(TemplateView):
    """
    订单详情页视图。
    """
    template_name = 'order.html'

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        order_id = self.kwargs["order_id"]
        order = get_object_or_404(Order, id=order_id)
        context['order'] = order
        context.update({
            "STRIPE_PUBLIC_KEY": settings.STRIPE_PUBLISHABLE_KEY,
            'name': str(order), # 确保这里也正确调用__str__
            'items': order.order_items.all(), # 访问OrderItem而不是Item
            'total': order.get_total_cost()
        })
        return context

class SuccessView(TemplateView):
    """
    支付成功页面视图。
    """
    template_name = "success.html"

class CancelView(TemplateView):
    """
    支付取消页面视图。
    """
    template_name = "cancel.html"

视图说明与优化:

  • ProductLandingPageView 和 OrderView:用于展示商品和订单详情,并将Stripe公钥传递给前端。
  • CreateCheckoutSessionView:处理单个商品的支付。line_items中包含一个商品的信息,unit_amount直接使用item.price(假设已是美分)。
  • CreateCheckoutSessionOrderView:处理订单的支付。
    • 关键优化1:product_data['name'] 应使用 str(order) 或 order.__str__() 来正确获取订单的字符串表示,而不是 order.__str__ (方法引用)。
    • 关键优化2:unit_amount 需将 order.get_total_cost() 返回的Decimal类型转换为整数美分,即 int(order.get_total_cost() * 100)。
    • 这里选择将整个订单作为一个line_item,其价格为订单总价。如果需要Stripe Checkout页面显示订单中的所有商品列表,则需要遍历order.order_items.all(),为每个OrderItem创建一个line_item。
  • SuccessView 和 CancelView:简单的模板视图,用于在支付成功或取消后重定向用户。

3. URL路由配置

URL配置将HTTP请求映射到相应的Django视图。

小邮包 包月订购包年程序
小邮包 包月订购包年程序

小邮包-包月订购包年服务网,该程序由好买卖商城开发,程序采用PHP+MYSQL架设,程序商业模式为目前最为火爆的包月订制包年服务模式,这种包年订购在国外网站已经热火很多年了,并且已经发展到一定规模,像英国的男士用品网站BlackSocks,一年的袜子购买量更是达到了1000万双。功能:1、实现多产品上线,2、不用注册也可以直接下单购买,3、集成目前主流支付接口,4、下单发货均有邮件提醒。

下载

urls.py

from api.views import (
    CancelView, SuccessView, CreateCheckoutSessionView, 
    ProductLandingPageView, OrderView, CreateCheckoutSessionOrderView
)
from django.contrib import admin
from django.urls import path

urlpatterns = [
    path('admin/', admin.site.urls),
    # 订单支付Checkout会话创建,注意添加了尾部斜杠
    path('order_checkout//', CreateCheckoutSessionOrderView.as_view(), name='order_checkout'),
    # 订单详情页
    path('order//', OrderView.as_view(), name='order'),
    # 单个商品详情页
    path('item//', ProductLandingPageView.as_view(), name='item'),
    # 单个商品支付Checkout会话创建
    path('buy//', CreateCheckoutSessionView.as_view(), name='buy'),
    # 支付取消页
    path('cancel/', CancelView.as_view(), name='cancel'),
    # 支付成功页
    path('success/', SuccessView.as_view(), name='success'),
]

URL配置说明与404排查:

  • 404错误分析:原始问题中 /order_checkout/1/ 路径出现404错误,而URL配置是 path('order_checkout/', ...)。Django的URL解析器对尾部斜杠非常敏感。如果 APPEND_SLASH 设置为 False (非默认),那么 /order_checkout/1/ 将不会匹配 order_checkout/
  • 解决方案:为了确保URL匹配的健壮性,建议在URL模式的末尾也添加斜杠,使其与前端请求的URL保持一致。例如,将 path('order_checkout/', ...) 修改为 path('order_checkout//', ...)。这使得无论 APPEND_SLASH 设置如何,URL匹配都更加明确。

4. 前端集成与支付流程

前端负责触发Stripe Checkout流程。

order.html (示例,landing.html 类似)

{% extends 'base.html' %}

{% block title %}
    订单支付
{% endblock %}

{% block content %}
    

{{ name }}

总成本 {{ total }} USD.
{% endblock %}

前端说明:

  • 引入Stripe.js库:
  • 通过 data-order-id 获取订单ID。
  • 点击“购买”按钮时,JavaScript会向Django后端发送一个 fetch 请求到 /order_checkout// 路径。
  • 后端返回Stripe Checkout Session ID后,使用 stripe.redirectToCheckout({ sessionId: session.id }) 将用户重定向到Stripe的支付页面。
  • 注意:fetch 请求的URL '/order_checkout/' + orderId + '/' 必须与 urls.py 中定义的模式(包括尾部斜杠)精确匹配。

5. 支付结果处理

用户完成Stripe Checkout后,Stripe会将用户重定向到 success_url 或 cancel_url。

  • success.html:显示支付成功的消息。
  • cancel.html:显示支付取消的消息。

在实际生产环境中,支付成功后,您需要通过Stripe Webhooks来异步更新订单状态,而不是仅仅依赖 success_url。因为用户可能在支付成功页面加载前关闭浏览器,或者支付本身是异步完成的。

6. 总结与注意事项

相关专题

更多
js获取数组长度的方法
js获取数组长度的方法

在js中,可以利用array对象的length属性来获取数组长度,该属性可设置或返回数组中元素的数目,只需要使用“array.length”语句即可返回表示数组对象的元素个数的数值,也就是长度值。php中文网还提供JavaScript数组的相关下载、相关课程等内容,供大家免费下载使用。

554

2023.06.20

js刷新当前页面
js刷新当前页面

js刷新当前页面的方法:1、reload方法,该方法强迫浏览器刷新当前页面,语法为“location.reload([bForceGet]) ”;2、replace方法,该方法通过指定URL替换当前缓存在历史里(客户端)的项目,因此当使用replace方法之后,不能通过“前进”和“后退”来访问已经被替换的URL,语法为“location.replace(URL) ”。php中文网为大家带来了js刷新当前页面的相关知识、以及相关文章等内容

374

2023.07.04

js四舍五入
js四舍五入

js四舍五入的方法:1、tofixed方法,可把 Number 四舍五入为指定小数位数的数字;2、round() 方法,可把一个数字舍入为最接近的整数。php中文网为大家带来了js四舍五入的相关知识、以及相关文章等内容

731

2023.07.04

js删除节点的方法
js删除节点的方法

js删除节点的方法有:1、removeChild()方法,用于从父节点中移除指定的子节点,它需要两个参数,第一个参数是要删除的子节点,第二个参数是父节点;2、parentNode.removeChild()方法,可以直接通过父节点调用来删除子节点;3、remove()方法,可以直接删除节点,而无需指定父节点;4、innerHTML属性,用于删除节点的内容。

477

2023.09.01

JavaScript转义字符
JavaScript转义字符

JavaScript中的转义字符是反斜杠和引号,可以在字符串中表示特殊字符或改变字符的含义。本专题为大家提供转义字符相关的文章、下载、课程内容,供大家免费下载体验。

394

2023.09.04

js生成随机数的方法
js生成随机数的方法

js生成随机数的方法有:1、使用random函数生成0-1之间的随机数;2、使用random函数和特定范围来生成随机整数;3、使用random函数和round函数生成0-99之间的随机整数;4、使用random函数和其他函数生成更复杂的随机数;5、使用random函数和其他函数生成范围内的随机小数;6、使用random函数和其他函数生成范围内的随机整数或小数。

991

2023.09.04

如何启用JavaScript
如何启用JavaScript

JavaScript启用方法有内联脚本、内部脚本、外部脚本和异步加载。详细介绍:1、内联脚本是将JavaScript代码直接嵌入到HTML标签中;2、内部脚本是将JavaScript代码放置在HTML文件的`<script>`标签中;3、外部脚本是将JavaScript代码放置在一个独立的文件;4、外部脚本是将JavaScript代码放置在一个独立的文件。

657

2023.09.12

Js中Symbol类详解
Js中Symbol类详解

javascript中的Symbol数据类型是一种基本数据类型,用于表示独一无二的值。Symbol的特点:1、独一无二,每个Symbol值都是唯一的,不会与其他任何值相等;2、不可变性,Symbol值一旦创建,就不能修改或者重新赋值;3、隐藏性,Symbol值不会被隐式转换为其他类型;4、无法枚举,Symbol值作为对象的属性名时,默认是不可枚举的。

551

2023.09.20

高德地图升级方法汇总
高德地图升级方法汇总

本专题整合了高德地图升级相关教程,阅读专题下面的文章了解更多详细内容。

9

2026.01.16

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
React 教程
React 教程

共58课时 | 3.7万人学习

TypeScript 教程
TypeScript 教程

共19课时 | 2.2万人学习

Bootstrap 5教程
Bootstrap 5教程

共46课时 | 2.9万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号