← 返回文章列表
开发笔记

使用 Stripe 管理订阅:升降级、取消与 Supabase Edge Function Webhook

本文用分步方式说明如何用 Stripe 处理 订阅(Subscription),包括:套餐升级/降级取消订阅,以及用 Supabase Edge Function 作为后端接收 Webhook、同步业务库(例如用户权益)。适合已能部署 Edge Function、熟悉 REST/环境变量的开发者。

你将完成什么

  • 在 Stripe 中配置产品与价格(按月/按年等)
  • 客户端或自建结账页创建 Checkout Session / Customer Portal,完成首次订阅
  • 通过 Stripe API 或 Customer Portal 实现升级、降级、取消
  • 部署 Supabase Edge Function,校验 Webhook 签名并处理 customer.subscription.* 等事件
  • (建议)在数据库中维护订阅状态,与 Supabase Auth 用户关联

核心概念(先对齐名词)

概念说明
Product逻辑上的「产品」,如「Pro 会员」
Price具体计费单元,绑定币种、周期(recurring
CustomerStripe 侧的客户,建议与你的业务用户一一对应(可用 metadata 存 user_id
Subscription客户对某个 Price 的周期性合约;升级/降级本质是换 subscription.items 里的 Price
Checkout托管结账页,适合首次开通
Customer PortalStripe 托管的「管理订阅」页面,用户自助换套餐、取消、更新支付方式
WebhookStripe 向你的服务器推送事件(支付成功、订阅更新等),必须以服务端验签

前置条件

  • Stripe 账号(先用测试模式
  • Supabase 项目,已安装 Supabase CLI
  • 本地已登录 CLI:supabase login,项目已 supabase link

第一步:在 Stripe 中创建产品与价格

  1. 登录 Stripe Dashboard,右上角切换到 Test mode
  2. 进入 Product catalogAdd product
  3. 填写产品名称(如「专业版」),添加 Pricing
    • Recurring:选择计费周期(月付 / 年付等)。
    • 可创建多个 Price(例如月付价、年付价),它们都属于同一或不同 Product,按你的套餐设计即可。
  4. 记下每个 Price ID(形如 price_xxx),后续 API 与 Webhook 里会用到。

升降级设计建议:每个「档位」对应一个 Price。升级/降级 = 把订阅行项目从 Price A 换成 Price B(可在换档时选择按比例计费 proration_behavior,见下文)。


第二步:把 Stripe 客户与业务用户关联

常见做法:

  1. 用户在你的 App 登录后(例如 Supabase Auth 的 user.id)。
  2. 首次需要付款时,在服务端(Edge Function 或已有后端)调用 Stripe Customers API 创建客户,并在 metadata 写入 supabase_user_id(或你的主键)。
  3. 把返回的 customer.idcus_xxx)存到你自己的表(如 profiles.stripe_customer_id),下次直接复用。

不要在浏览器暴露 Secret Key;创建 Customer、创建 Checkout Session 等应放在 Edge Function 或受信任后端。


第三步:首次订阅 — 使用 Checkout Session(推荐)

在 Edge Function 中(示例逻辑,非完整可运行文件):

  1. 接收已认证用户的请求(例如校验 Supabase JWT)。
  2. 读取该用户的 stripe_customer_id;若无则先创建 Customer 并落库。
  3. 调用 stripe.checkout.sessions.create
    • mode: 'subscription'
    • customercustomer_email
    • line_items: [{ price: 'price_xxx', quantity: 1 }]
    • success_url / cancel_url 指向你的前端页面
    • client_reference_idmetadata 可带 user_id,便于 Webhook 关联。
  4. 把返回的 session.url 返回给客户端,重定向用户到该 URL 完成支付。

用户支付成功后,Stripe 会创建 Subscription,后续变更都针对该订阅对象进行。


第四步:套餐升级与降级

有两种主流实现方式:

方式 A:Customer Portal(最少代码)

  1. 在 Stripe Dashboard → SettingsBillingCustomer portal 中启用门户,并配置允许的操作:切换方案(Switch plans)取消 等。
  2. 在 Edge Function 中创建 Billing Portal Sessionstripe.billingPortal.sessions.create,传入 customerreturn_url
  3. 前端拿到 URL 后跳转,用户在 Stripe 页面选择新套餐即完成换档。

优点:省开发量;比例计费、发票展示由 Stripe 处理。缺点:定制 UI 空间有限。

方式 B:服务端直接改 Subscription(完全可控)

对已有 subscription_id 调用 Update Subscription API,更新 items 为新的 price,并可设置:

  • proration_behaviorcreate_prorations(按日比例补差价或抵扣)或 none(下个周期再按新价,依业务选择)。
  • billing_cycle_anchor 等高级字段按需使用。

升级降级在 Stripe 侧并无不同 API,都是换绑 Price;区别在你的定价策略与是否立即生效。

无论哪种方式,以 Webhook 收到的事件为准更新数据库,不要只信前端跳转回来的页面。


第五步:取消订阅

取消也有常见两类:

  1. 立即取消:将订阅设为 cancel_at_period_end: false 并执行取消 API,或 Portal 里用户选择立即结束(若你允许)。
  2. 周期末取消(更常见)cancel_at_period_end: true,用户付完当前周期,到期不再续费。

Customer Portal 里可让用户自助取消;若自建 UI,则调用 Subscriptions API 更新 cancel_at_period_endcancel()

同样:最终状态以 Webhook(如 customer.subscription.deletedcustomer.subscription.updated)同步到你的库。


第六步:Supabase Edge Function 接收 Webhook

1. 准备密钥与环境变量

在 Stripe Dashboard → DevelopersWebhooksAdd endpoint

  • Endpoint URL:部署后的函数公网地址,例如:
    https://<project-ref>.supabase.co/functions/v1/stripe-webhook
  • 选择要监听的事件,订阅业务至少包括:
    • checkout.session.completed(首次结账成功,可用来初次写入订阅)
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • (可选)invoice.paidinvoice.payment_failed用于宽限期与欠费处理

创建后复制 Signing secretwhsec_...)。

在 Supabase 中为该函数配置 Secrets(CLI 示例):

supabase secrets set STRIPE_WEBHOOK_SECRET=whsec_xxx STRIPE_SECRET_KEY=sk_test_xxx

2. 函数职责概要

  1. 只接受 POST;读取原始 body(字符串),用于验签。
  2. 使用 Stripe 官方库提供的 constructEvent(rawBody, signature, webhookSecret)(或等价方法)校验 Stripe-Signature
  3. 验签失败返回 400,不要处理业务。
  4. 根据 event.type 分支,用 event.data.object 更新数据库(例如 subscriptions 表:statuscurrent_period_endprice_idcancel_at_period_end 等)。
  5. Customer metadata 或 Checkout metadata 里的 user_id 关联到 Supabase 用户表。
  6. 尽快返回 200;耗时操作可写入队列或由另一函数异步处理(避免 Stripe 反复重试)。

3. Deno / Edge 注意点

  • 使用 Stripe 的 REST API 或兼容 Deno 的 SDK版本;以 Supabase Edge Functions 文档 与 Stripe 当前推荐方式为准。
  • 务必使用未解析的原始请求体验签;若框架提前 JSON.parse 过 body,验签会失败。
  • STRIPE_SECRET_KEY 仅用于必要时回查 Stripe(例如根据 customer 拉取最新订阅);Webhook 处理本身主要依赖事件 payload。

4. 本地调试 Webhook

使用 Stripe CLI:

stripe listen --forward-to localhost:54321/functions/v1/stripe-webhook

CLI 会打印临时 whsec_...,本地测试时把该 secret 配进函数环境即可。


第七步:数据库建议(与 Supabase Auth 对齐)

可建一张 subscriptions(或扩展 profiles),至少包含:

  • user_id(UUID,对齐 auth.users
  • stripe_customer_id
  • stripe_subscription_id
  • statusactivepast_duecanceled 等)
  • price_id / product_id(当前档位)
  • current_period_end(时间戳)
  • cancel_at_period_end(布尔)

权限:仅 service role 或 Edge Function 用 Supabase Admin 客户端写入;普通 anon 不得直接改订阅表。


第八步:上线前检查清单

  • 生产环境使用 Live 密钥与 Live Webhook endpoint,whsecsk_live 存在 Secrets中。
  • Dashboard 中配置 Customer Portal 域名与 Checkout 允许的返回 URL。
  • Webhook 日志中确认无长期 4xx/5xx;对失败事件可用 Stripe 控制台 Resend
  • 测试:新订、升级、降级、周期末取消、支付失败(测试卡)是否都把库表更新正确。

常见问题

现象建议
Webhook 验签失败是否用了原始 body;URL 是否指向正确环境;secret 是否与该 endpoint 一致
用户已付款但库没更新查 Stripe Webhook 投递记录;函数是否超时;是否在验签前就 parse 了 body
升降级后权益不对customer.subscription.updated 为准;检查是否更新了正确的 items[].price.id
测试与生产混乱Test/Live 的 Key、price_whsec 严格分开,Secrets 分环境管理

参考链接