
本文介绍如何在不使用 stripe customer portal 的前提下,安全、可靠地完成订阅计划升级:通过创建一次性发票并监听支付成功事件,确保客户完成付款后再更新订阅,避免服务提前切换或账务不一致问题。
本文介绍如何在不使用 stripe customer portal 的前提下,安全、可靠地完成订阅计划升级:通过创建一次性发票并监听支付成功事件,确保客户完成付款后再更新订阅,避免服务提前切换或账务不一致问题。
在 Stripe 中直接调用 Subscription.modify() 并设置 collection_method="send_invoice" 会立即生成待付发票,但不会阻塞订阅变更——即订阅状态可能在客户尚未付款时就已更新为新计划(尤其当 proration_behavior 或 cancel_at 等参数触发即时生效逻辑时),这与“先付款、后升级”的业务要求相悖。
正确的做法是采用 两阶段原子化流程(Two-Phase Upgrade):
✅ 第一阶段:生成并确认一次性发票(One-off Invoice)
不直接修改订阅,而是为差额(或全额新周期费用)创建一笔独立发票,并强制客户支付该发票:
import stripe
from datetime import timedelta
# 假设已获取 customer_id、subscription_id、new_price_id
customer_id = "cus_..."
subscription_id = "sub_..."
new_price_id = "price_..."
# 1. 创建 draft 发票(不含税项,便于手动控制)
invoice = stripe.Invoice.create(
customer=customer_id,
collection_method="send_invoice",
days_until_due=1, # 1天内需支付
auto_advance=True, # 自动进入 open 状态(非 draft)
)
# 2. 向该发票添加新价格对应的行项目(即升级差额)
stripe.InvoiceItem.create(
customer=customer_id,
invoice=invoice.id,
price=new_price_id,
quantity=1,
description=f"Plan upgrade to {new_price_id}",
)
# 3. 最终确认发票(触发邮件 & 支付链接生成)
confirmed_invoice = stripe.Invoice.finalize_invoice(invoice.id)
print("Invoice URL:", confirmed_invoice.hosted_invoice_url) # 可返回前端跳转
? 注意:hosted_invoice_url 是 Stripe 提供的安全支付页链接,支持卡片/SEPA/BACS 等多种方式,无需自行集成 PaymentIntent。
✅ 第二阶段:监听支付成功,再执行订阅变更
关键在于仅在发票成功支付后才更新订阅。推荐通过 Stripe Webhook 监听 invoice.payment_succeeded 事件:
# Webhook handler(需部署在可公网访问的 endpoint)
@app.route('/webhook', methods=['POST'])
def stripe_webhook():
payload = request.get_data()
sig_header = request.headers.get('Stripe-Signature')
event = None
try:
event = stripe.Webhook.construct_event(
payload, sig_header, endpoint_secret
)
except ValueError:
return 'Invalid payload', 400
except stripe.error.SignatureVerificationError:
return 'Invalid signature', 400
if event['type'] == 'invoice.payment_succeeded':
invoice = event['data']['object']
# 验证是否为本次升级产生的发票(建议用 metadata 标记来源)
if invoice.get('metadata', {}).get('upgrade_intent') == 'true':
subscription_id = invoice['subscription']
new_price_id = get_target_price_from_invoice(invoice) # 自定义逻辑
# ✅ 此刻才安全地升级订阅(无 proration 或延迟生效)
stripe.Subscription.modify(
subscription_id,
items=[{
'id': get_subscription_item_id(subscription_id), # 获取当前 item.id
'price': new_price_id,
}],
proration_behavior='create_prorations', # 或 'always_invoice'(如需开调整发票)
# 注意:移除 cancel_at / days_until_due 等可能干扰的参数
)
print(f"✅ Subscription {subscription_id} upgraded after payment.")
return 'OK', 200
⚠️ 重要注意事项
- 禁止在前端直接调用 Subscription.modify():易被绕过或重复提交,必须由服务端受控执行。
- 务必校验 Webhook 来源:使用 Stripe-Signature + endpoint_secret 防止伪造事件。
- 发票与订阅强关联:建议在创建 Invoice 时通过 metadata={"upgrade_intent": "true", "subscription_id": "sub_..."} 显式标记,便于 Webhook 中精准识别。
- 处理失败场景:若发票支付失败(invoice.payment_failed),应通知用户并保留原订阅;可选自动重试或发放优惠券补偿。
- 税务与合规:若涉及多国 VAT,启用 Stripe Tax 并确保 InvoiceItem 设置正确 tax_behavior。
✅ 总结
要实现“先付款、后升级”,核心不是调整 Subscription.modify() 参数,而是将付款动作与订阅变更解耦:
① 用 Invoice + InvoiceItem 构建明确的收费契约;
② 依赖 invoice.payment_succeeded Webhook 作为唯一可信信号;
③ 在 Webhook 处理器中执行最终的 Subscription.modify()。
该模式符合 PCI 合规要求,具备幂等性与可观测性,是 Stripe 生产环境推荐的最佳实践。










