You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义时长自动取消的Stripe订阅排期问题排查与实现咨询

问题分析与解决方案

代码中的核心Bug

  1. Price计费周期配置错误
    当前代码创建的是每日计费的Price(recurring: { interval: "day" },默认interval_count=1),但你的需求是每30天一个计费周期,完全不匹配业务逻辑,这会导致订阅每天生成发票,直接引发后续的支付失败问题。

  2. 未设置客户默认支付方式
    你使用了Setup Mode的Checkout Session,仅完成了支付方式收集,但没有将该支付方式设为客户的默认支付方式。创建Subscription Schedule时,Stripe会自动生成首个发票,若客户无默认支付方式,会直接触发invoice.payment_failed事件,最终导致订阅被自动取消。

  3. 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 05:55:01