
本教程详细介绍了如何在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。因为用户可能在支付成功页面加载前关闭浏览器,或者支付本身是异步完成的。









