第一次做订阅,数据库里很容易出现一个字段: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、定时任务。
订阅的难点从来不是“每月扣一次钱”,而是用户和支付系统都可能在任何一天改变下一步。