自定义时长自动取消的Stripe订阅排期问题排查与实现咨询
问题分析与解决方案
代码中的核心Bug
Price计费周期配置错误
当前代码创建的是每日计费的Price(recurring: { interval: "day" },默认interval_count=1),但你的需求是每30天一个计费周期,完全不匹配业务逻辑,这会导致订阅每天生成发票,直接引发后续的支付失败问题。未设置客户默认支付方式
你使用了Setup Mode的Checkout Session,仅完成了支付方式收集,但没有将该支付方式设为客户的默认支付方式。创建Subscription Schedule时,Stripe会自动生成首个发票,若客户无默认支付方式,会直接触发invoice.payment_failed事件,最终导致订阅被自动取消。Subscription Schedule逻辑缺失
未明确绑定Checkout Session中收集的支付方式,也没有确保订阅生成时能使用该支付方式完成自动扣费。
修复后的代码实现
1. 修正订阅控制器中的Price创建逻辑
创建每30天计费一次的Price,匹配你的周期需求:
const metadata = { userId: userId.toString(), planType: type, totalMonths: totalMonths.toString(), totalSessions: totalSessions.toString(), sessionTime: sessionTime.toString(), action: type === "custom" ? "create_subscription" : "create_payment", }; const price = await stripe.prices.create({ currency: "usd", unit_amount: priceAmount * 100, recurring: { interval: "day", interval_count: 30, // 明确设置每30天一个计费周期 }, product_data: { name: plan.data.name, }, metadata: metadata, }); const customMetadata = { ...metadata, priceId: price.id, }; session = await stripe.checkout.sessions.create({ customer: customer.id, payment_method_types: ["card"], mode: "setup", success_url: `${config.frontendURL}/payment/success?session_id={CHECKOUT_SESSION_ID}`, cancel_url: `${config.frontendURL}/payment/cancel`, metadata: customMetadata, }); return res.status(200).json({ status: true, message: "Checkout session created successfully", data: { url: session.url }, });
2. 修正Webhook中的订阅计划创建逻辑
添加设置默认支付方式的步骤,确保订阅能自动扣费:
case "checkout.session.completed": { const session = event.data.object as Stripe.Checkout.Session; // 提取元数据 const userId = session.metadata?.userId; const planType = session.metadata?.planType; const totalSessions = session.metadata?.totalSessions; const sessionTime = session.metadata?.sessionTime; const totalMonths = session.metadata?.totalMonths || "1"; const action = session.metadata?.action; if (!userId || !action) { console.error("Missing required metadata in checkout session", session.id); break; } if (action === "create_subscription") { const customerId = session.customer as string; const priceId = session.metadata?.priceId as string; const paymentMethodId = session.payment_method as string; // 关键步骤:将收集到的支付方式设为客户默认支付方式 await stripe.customers.update(customerId, { invoice_settings: { default_payment_method: paymentMethodId, }, }); // 创建订阅计划,运行指定数量的30天周期 const subscriptionSchedule = await stripe.subscriptionSchedules.create({ customer: customerId, start_date: "now", end_behavior: "cancel", phases: [ { items: [ { price: priceId, quantity: 1, }, ], iterations: parseInt(totalMonths), }, ], }); } break; }
额外优化:自然月周期替代方案
如果你的需求是按自然月调整账单日(比如4月29日→5月29日→6月29日,若当月无该日期则自动调整到月末),而非固定30天,可将Price改为按月计费,更符合常规订阅逻辑:
const price = await stripe.prices.create({ currency: "usd", unit_amount: priceAmount * 100, recurring: { interval: "month", // 自然月计费 }, product_data: { name: plan.data.name, }, metadata: metadata, });
这种方式下,Stripe会自动处理月份天数差异的账单日调整,无需手动计算30天间隔。
失败原因总结
- 每日计费的Price导致订阅逻辑完全偏离需求,生成大量无效发票;
- 未设置默认支付方式,导致订阅生成的首个发票无支付渠道,触发支付失败;
- 之前使用
cancel_at的方案,若用自然月订阅,cancel_at设置的总时长若不是刚好在周期结束点,会产生按比例计费的部分周期账单;若用固定30天订阅,需确保cancel_at严格等于订阅起始时间 + 总周期天数,否则也会产生 prorated费用。
内容的提问来源于stack exchange,提问作者MSourav99
相关产品推荐
相关产品推荐

