返回学院
OPERATE经营手册

订阅不是一个 isPro:续费失败、取消和恢复都会让状态变复杂

订阅系统如果只存“是否会员”,很快会遇到现实:用户已取消但本周期仍有效、扣款失败进入宽限期、恢复订阅、退款和 Provider 回调重复。

第一次做订阅,数据库里很容易出现一个字段:is_pro。

付款成功就设成 true,用户取消就改成 false。这个模型在 Demo 里够用,接到真实账单以后很快会崩。

因为“取消”本身就有至少两种含义:立即终止,或本周期结束后不再续费。第二种情况下,用户已经点了取消,但权益还应该继续到 paid-through date。

订阅至少同时存在几个事实

Provider 有自己的 subscription id 和状态;本地有当前套餐;有当前计费周期开始和结束;有是否计划在周期末取消;还有实际权益什么时候失效。

这些事实相关,但不是同一个字段。

Stripe 的订阅文档本身就有 trialing、active、past_due、canceled 等生命周期状态。支付失败以后还可能根据重试和业务策略继续一段时间,而不是瞬间踢出产品。

Webhook 才能补齐异步变化

用户可能不在你的网站里取消,而是在支付 Provider 的客户门户操作;续费失败发生在凌晨;银行几小时后更新支付结果。

这些都不会经过你的“取消按钮”。本地状态必须能够根据签名 webhook 或服务端同步更新。

同一个 webhook 还可能被重试,因此订阅状态更新也需要幂等,不能每收到一次 invoice.paid 就重复发一份月度权益。

取消和恢复要定义清楚

如果用户选择“周期末取消”,本地可以记录 cancel_at_period_end,但当前权益仍然 active。用户第二天后悔,又点恢复,则需要撤销未来取消计划,而不是新建第二个订阅。

如果用户已经进入真正 canceled 的终态,再回来购买,才可能是新订阅。

这就是为什么一个 isPro 无法表达生命周期。

续费失败需要业务策略

支付失败以后立刻停服务最简单,但不一定合理;给三天或七天宽限又会增加状态复杂度。

无论选哪种,都要把规则写死:宽限从哪个时间开始,重复失败会不会不断延长,成功补款以后怎样恢复,最终什么时候关闭权益。

这些规则应该由本地持久化,而不是临时根据“当前时间 - webhook 时间”猜。

退款也不是简单反转

一个用户已经使用了半个月、消费了附带积分,再发生部分退款,权益如何变化需要产品政策。Provider 只知道退多少钱,不知道你的功能应该撤掉多少。

不要让财务事件直接修改一个布尔值,要经过业务规则层。

订阅上线前至少画出:创建、试用、激活、续费成功、续费失败、宽限、周期末取消、恢复、立即取消、退款、终止。再给每条箭头写触发来源:用户操作、后台操作、Provider webhook、定时任务。

订阅的难点从来不是“每月扣一次钱”,而是用户和支付系统都可能在任何一天改变下一步。

KEEP READING

继续看这个方向