Stripe创建Subscription时重复扣费问题求助
问题分析
用户订阅产品时出现重复扣费,核心原因有两个:
- 每次订阅都重复创建Stripe的Product和Price:这会生成大量冗余资源,还可能触发重复计费逻辑。
- 订阅支付行为未明确管控:默认情况下,Stripe创建订阅会自动生成首张发票并扣款,如果存在接口重复调用、前置支付操作,就会导致两次扣费。
修复方案
1. 复用Product和Price,避免重复创建
Product和Price是Stripe里的静态套餐配置,无需每次订阅都重新创建。可以提前在Stripe后台建好对应套餐的配置,也可以在代码中先查询是否已有现成的资源,仅在不存在时才创建:
// 查询同名Product,存在则复用,不存在则创建 const existingProducts = await stripe.products.list({ name: req.body.package_name }); let product = existingProducts.data.length > 0 ? existingProducts.data[0] : await stripe.products.create({ name: req.body.package_name }); // 查询对应规格的Price,存在则复用,不存在则创建 const existingPrices = await stripe.prices.list({ product: product.id, unit_amount: req.body.amount * 100, currency: 'gbp', recurring: { interval: 'month' } }); let price = existingPrices.data.length > 0 ? existingPrices.data[0] : await stripe.prices.create({ unit_amount: req.body.amount * 100, currency: 'gbp', recurring: { interval: 'month' }, product: product.id, });
2. 明确控制订阅的支付逻辑
创建订阅时,通过payment_behavior参数精准控制支付行为,避免自动重复扣款:
- 正常订阅场景(需立即扣首月费用):使用
payment_behavior: 'default_incomplete'(Stripe当前默认值),支付失败时订阅会进入待完成状态,不会自动重试扣款,避免重复扣费。 - 若用户已通过其他渠道完成支付:设置
payment_behavior: 'skip',跳过首张发票的自动支付。
修改后的订阅创建代码:
const subscription = await stripe.subscriptions.create({ customer: user.stripe_customer_id, items: [{ price: price.id }], expand: ['latest_invoice.payment_intent'], payment_behavior: 'default_incomplete', // 明确支付行为 });
3. 添加幂等性处理,防止重复提交
前端用户可能重复点击订阅按钮导致接口多次调用,给Stripe接口添加幂等键,确保同一请求只会执行一次:
// 前端每次请求生成唯一幂等键,放在请求头或参数中传递 const idempotencyKey = req.headers['idempotency-key'] || req.body.idempotency_key; // 调用Stripe接口时传入幂等键 const subscription = await stripe.subscriptions.create({ customer: user.stripe_customer_id, items: [{ price: price.id }], expand: ['latest_invoice.payment_intent'], payment_behavior: 'default_incomplete' }, { idempotencyKey });
4. 重构代码,提升可读性与可维护性
原代码嵌套过深,换成async/await替代链式then,更易维护且方便统一处理错误:
exports.createSubscription = async function(req, res, next) { try { const { payment_method_id, package_name, amount } = req.body; // 参数校验 if (!payment_method_id) return Response.failure(req, res, '', 'payment_method_id is required'); if (!package_name) return Response.failure(req, res, '', 'package_name is required'); if (!amount) return Response.failure(req, res, '', 'amount is required'); // 查询用户 const user = await User.findById(req.userId); if (!user?.stripe_customer_id) return Response.failure(req, res, '', '用户不存在或未绑定Stripe客户ID'); // 绑定并设置默认支付方式 await stripe.paymentMethods.attach(payment_method_id, { customer: user.stripe_customer_id }); await stripe.customers.update(user.stripe_customer_id, { invoice_settings: { default_payment_method: payment_method_id } }); // 查询/创建Product和Price const existingProducts = await stripe.products.list({ name: package_name }); let product = existingProducts.data.length > 0 ? existingProducts.data[0] : await stripe.products.create({ name: package_name }); const existingPrices = await stripe.prices.list({ product: product.id, unit_amount: amount * 100, currency: 'gbp', recurring: { interval: 'month' } }); let price = existingPrices.data.length > 0 ? existingPrices.data[0] : await stripe.prices.create({ unit_amount: amount * 100, currency: 'gbp', recurring: { interval: 'month' }, product: product.id, }); // 创建订阅 const subscription = await stripe.subscriptions.create({ customer: user.stripe_customer_id, items: [{ price: price.id }], expand: ['latest_invoice.payment_intent'], payment_behavior: 'default_incomplete', }); Response.success(req, res, subscription, '订阅创建成功'); } catch (error) { console.error('订阅创建失败:', error); Response.failure(req, res, {}, error.raw?.message || error.message); } };
额外排查点
- 登录Stripe后台查看订阅和发票记录,确认是否生成了重复订阅或多次支付的发票。
- 前端添加按钮防抖逻辑,比如点击后禁用按钮,防止用户重复触发订阅请求。
内容的提问来源于stack exchange,提问作者Expert Dev Designer
相关产品推荐
相关产品推荐

