如何在Stripe结账会话完成后保存Django预订实例?
问题分析与解决方案
你的核心问题是误解了Stripe Checkout Session的状态逻辑:刚创建的会话状态默认就是open(因为用户还未完成支付流程),所以用status == 'completed'作为会话创建成功的判断条件完全错误,直接导致了错误提示;而直接跳过校验保存实例,又会出现未支付就生成预订的问题。
正确的流程应该是:先保存未确认的预订实例→创建会话并跳转支付→通过Stripe Webhook监听支付完成事件,再更新预订状态,具体实现如下:
1. 修正视图函数逻辑
移除错误的状态校验,改为捕获Stripe API异常,确保会话创建成功后跳转,同时先保存未确认的预订实例(必须先保存才能获取ID关联支付):
@transaction.atomic def booking_form(request, id): franchise_id = id form = BookingForm(franchise_id=franchise_id) if request.method == 'POST': form = BookingForm(request.POST, franchise_id=franchise_id) if 'paynow-btn' in request.POST: if form.is_valid(): instance = form.save(commit=False) instance.payment_status = 'due' instance.booking_status = 'unconfirmed' instance.franchise_id = franchise_id # 先保存实例,获取ID用于Stripe元数据关联 instance.save() try: # Stripe Connect 标准账户直接扣费 connected_account_id = create_or_get_connected_account(franchise_id) success_urlpattern = reverse('booking:booking_successful', kwargs={'id': franchise_id}) success_urlpath = request.build_absolute_uri(success_urlpattern) checkout_session = stripe.checkout.Session.create( payment_method_types=['card'], line_items=[{ "price_data": { "currency": "gbp", "unit_amount": int(instance.total_amount * 100), "product_data": { "name": f'Booking for {instance.full_name}', }, }, "quantity": 1, }], payment_intent_data={ 'metadata': { 'booking_id': instance.id, # 已保存实例,ID可用 }, 'application_fee_amount': int(instance.total_amount * (franchise.payout_percentage / 100) * 100), }, mode='payment', success_url=success_urlpath, cancel_url=settings.PAYMENT_CANCEL_URL, stripe_account=connected_account_id, ) # 会话创建成功,直接跳转至Stripe支付页面 return redirect(checkout_session.url) except stripe.error.StripeError as e: # 捕获Stripe API调用异常,清理未支付的预订实例 messages.error(request, f'支付会话创建失败:{str(e)}') instance.delete() return redirect(reverse('booking:booking_form', kwargs={'id': franchise_id}))
2. 添加Stripe Webhook处理函数
创建视图接收Stripe的支付完成事件,更新对应预订的状态:
import stripe from django.http import HttpResponse from django.views.decorators.csrf import csrf_exempt from .models import Booking @csrf_exempt def stripe_webhook(request): payload = request.body sig_header = request.META.get('HTTP_STRIPE_SIGNATURE') event = None try: # 验证Webhook签名,确保请求来自Stripe event = stripe.Webhook.construct_event( payload, sig_header, settings.STRIPE_WEBHOOK_SECRET ) except ValueError: # 无效请求体 return HttpResponse(status=400) except stripe.error.SignatureVerificationError: # 无效签名 return HttpResponse(status=400) # 处理支付完成事件 if event['type'] == 'checkout.session.completed': session = event['data']['object'] # 从支付意图元数据中获取预订ID booking_id = session['payment_intent']['metadata']['booking_id'] try: booking = Booking.objects.get(id=booking_id) # 更新支付状态与预订状态 booking.payment_status = 'paid' booking.booking_status = 'confirmed' booking.save() except Booking.DoesNotExist: # 找不到对应预订,可添加日志记录 pass return HttpResponse(status=200)
3. 配置Webhook路由
在项目的urls.py中添加Webhook的路由:
from django.urls import path from . import views urlpatterns = [ # 其他路由... path('stripe-webhook/', views.stripe_webhook, name='stripe_webhook'), ]
关键说明
- 先保存实例的必要性:未保存的实例没有ID,无法与Stripe支付流程关联,所以必须先保存并标记为未确认/待支付状态。
- Webhook是唯一可靠的支付完成通知:用户可能在支付页面关闭窗口、取消支付,只有Stripe主动发送的
checkout.session.completed事件能确保支付真的完成。 - 异常清理:Stripe API调用失败时,要删除已保存的未支付实例,避免产生无效的脏数据。
内容的提问来源于stack exchange,提问作者betty_
相关产品推荐
相关产品推荐

