saas-starter 订阅暂停/恢复实战:从用户点击到 Webhook 同步的完整指南
【免费下载链接】saas-starterGet started quickly with Next.js, Postgres, Stripe, and shadcn/ui.项目地址: https://gitcode.com/GitHub_Trending/sa/saas-starter
上个月,有客户说团队预算被砍,想先停三个月、下季度再续——这个场景正是 saas-starter 订阅暂停/恢复功能要解决的:让用户不取消订阅就能停掉计费,需要时再一键续上。项目基于 Next.js、Postgres(Drizzle ORM)、Stripe 和 shadcn/ui,本文直接拆给你看要改哪 4 个地方、怎么自测。
一张图看懂:订阅状态到底怎么流转
动代码之前,先把状态机定下来。"暂停"不是你在数据库里加的一个额外字段,而是 Stripe 官方认可的订阅状态,你的系统只做同步。
| 状态 | 含义 | 常见触发 |
|---|---|---|
| active | 正常计费 | 试用结束扣款成功,或暂停后恢复 |
| paused | 停止扣费,工作区和数据保留 | 用户在 Billing Portal 选择暂停 |
| canceled | 订阅终止 | 用户取消,或扣款永久失败 |
动手:3 步暂停你的订阅
站在用户视角,暂停就是三次点击:点仪表盘上的"管理订阅"进 Stripe Billing Portal → 选暂停模式 → 确认返回。
- 选模式:
immediate立即停止计费,本期不再扣款;at_period_end本期服务用到头,下期起暂停。 - 确认:Portal 落库变更,Stripe 同时把
customer.subscription.paused事件推给后端 webhook。 - 回仪表盘:状态徽标从绿色"活跃"变琥珀色"已暂停",用户一眼明白——号没丢,只是停了扣费。
之所以能这么顺,是因为 Portal 是 Stripe 提供的现成界面,项目只在配置里打开对应能力,不用自己画暂停按钮。入口处的createCustomerPortalSession负责换一张带正确配置的 Portal 链接,这就是整个功能"看起来大、实际很小"的原因。
恢复订阅
恢复是暂停的反向操作:进同一个 Portal,点"恢复",确认卡片仍有效(卡过期的先换卡),完事。Stripe 推customer.subscription.resumed回来,库里状态变回active,徽标重新变绿。全程不需要重新注册或重建工作区。
幕后:改动到底发生在哪 4 个地方
把仓库翻一遍,真正要动的只有 4 处,全部可肉眼验证,且都不碰主结账链路。
1. teams 表的subscriptionStatus列
lib/db/schema.ts 里 teams 表本来就留了三个 Stripe 同步列,不用加字段、不用跑迁移:
| 列 | 作用 |
|---|---|
stripeCustomerId | 从 Stripe Customer 反查本地团队 |
stripeSubscriptionId | 关联具体的 Stripe 订阅 |
subscriptionStatus | 同步 active/paused/canceled 等状态 |
2. Portal 配置里加 pause
lib/payments/stripe.ts 的 Portal 配置features里已有subscription_update、subscription_cancel,补一项即可:
subscription_pause: { enabled: true, mode: 'immediate' // 或 at_period_end }3. webhook 新增两个事件分支
app/api/stripe/webhook/route.ts 的 switch 现在只接updated/deleted,加上暂停/恢复:
case 'customer.subscription.paused': case 'customer.subscription.resumed': { const sub = event.data.object as Stripe.Subscription; const team = await getTeamByStripeCustomerId(sub.customer as string); await updateTeamSubscription(team.id, { subscriptionStatus: sub.status === 'paused' ? 'paused' : 'active' }); break; }4. 前端状态徽标
仪表盘页面读team.subscriptionStatus做分支:paused渲染琥珀色 Badge"已暂停,将于 x 日恢复",active渲染绿色。想再严格些,就在 app/(dashboard)/layout.tsx/layout.tsx) 里对暂停态灰掉部分功能入口。
自测:5 步验证暂停/恢复真的生效
✅ 测试卡4242 4242 4242 4242,五步走完闭环:
- 本地跑起来,用测试卡过一次结账,确认
teams表里subscriptionStatus是active。 - 调暂停接口(或在 Portal 里点暂停):
# 暂停 curl -X POST https://api.stripe.com/v1/subscriptions/sub_xxx/pause \ -u sk_test_xxx: -d "pause[behavior]=immediate" # 恢复 curl -X POST https://api.stripe.com/v1/subscriptions/sub_xxx/resume -u sk_test_xxx:- 查库,
subscriptionStatus应变为paused。 - 发恢复命令,状态回到
active。 - 看仪表盘:徽标从琥珀变绿。若没变,先查 webhook 签名校验有没有过。
避坑清单
下面四条,都是上线后会被真实扎到的地方。
- webhook 重试:Stripe 投递失败会自动重发,处理逻辑必须幂等——同一状态写两次等价于写一次。
- 邮件通知:暂停/恢复要通知到用户(Stripe Billing 自带邮件,或自己发),否则用户根本不知道扣费状态变了。
- 权限控制:只有团队管理员能进账单管理入口,入口处做角色校验。
- 暂停期功能限制:状态为
paused时灰掉邀请成员、数据导出等功能,避免"停了计费却还在消耗资源"的尴尬。
【免费下载链接】saas-starterGet started quickly with Next.js, Postgres, Stripe, and shadcn/ui.项目地址: https://gitcode.com/GitHub_Trending/sa/saas-starter
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考