← 产品发现与 AI 开发 · 第 4 篇

收费功能怎样与产品权限保持一致

把支付、订阅与产品权限分开建模,处理签名、重复、乱序和对账,并明确退款、取消与测试环境的边界。

学完这一篇,你能做到

设计可信收费状态转换,拒绝跳转授予权限,并验证事件去重和授权恢复规则。

用户完成订阅页面并返回产品,服务端还没收到支付结果。此时前端可以展示确认中,不能仅凭成功跳转就开放付费权限。收费系统需要把支付对象、订阅状态与产品授权连接起来,并处理通知重复、乱序和服务暂时不可用。

本课只使用合成事件 E-1 与 E-2,不接真实卡片、账号或资金。离线账本使用人为单调版本演示去重与旧状态拒绝,这个版本不是 Stripe 的事件顺序字段。

支付、订阅与授权分别存储

发起支付、授权支付、确认入账与产品可用是不同阶段,具体状态取决于支付方式与集成。订阅会关联账单、付款尝试与周期,产品 entitlement 决定用户可使用哪些功能。三者可以关联,但不能只用一个布尔值替代所有业务含义。

建立可信映射:产品用户对应支付客户,订阅对应套餐,套餐对应功能集合。映射来自受认证服务端流程,不接受浏览器随意指定他人客户 ID。金额、币种与商品也要由可信产品规则校验,不能以客户端展示价格为权威。

订阅 active 不一定等于所有账单都已支付,试用与延迟确认的支付方式也有不同条件。开放权限要依据明确策略与已核验对象,不能从单一状态名推导所有场景。实际状态与字段需核对使用的 Stripe API 版本及文档。

浏览器返回页只显示当前进度

支付后跳转是用户导航,可能被重复访问或伪造。返回页应向自己的服务端查询关联交易或授权结果,展示 pending、available 或需要处理等状态。服务端尚未确认时,不能把客户端参数写成已付款。

服务端创建结账请求也要处理重复提交。稳定的业务请求标识可以减少重复订单,外部接口的幂等机制有适用范围,需要按当前文档配置。创建请求超时之后,应查询已有对象,避免不确定结果直接触发另一份订阅。

用户体验要给等待、失败与重试明确入口。付款失败不应只显示空白;确认中也不应永久停住,应有查询与人工支持路径。支持排查依靠对象 ID 与内部记录,不要求用户把完整支付凭据发到日志或聊天里。

Webhook 验证先于业务处理

Stripe webhook 需要用原始请求 body、签名头与对应端点的签名密钥按官方 SDK 验证。框架提前解析再重新序列化可能改变字节,导致签名失败。传输使用 HTTPS 与签名验证分别解决不同问题。

时间容忍与重放检查按当前 SDK 规则实施,不能自行略过验证。测试与正式环境的密钥、签名配置与端点需要分开。本文不提供密钥示例或未经运行的框架实现,示例账本假定事件已经通过可信验证。

验证后将事件与待处理状态持久保存,再尽快返回成功响应,把复杂处理放到可恢复后台。若未持久保存就返回成功,进程故障可能丢掉通知。若内部操作很久才响应,则可能触发发送端重试,形成重复处理。

重复与乱序都需要容错

Stripe 文档明确事件交付不保证生成顺序。不同事件可能有相同秒级 created 时间,不能用它当作唯一去重或全局顺序。持久 event ID 识别重复,必要时读取权威订阅、账单等对象并对账。

下面假设本地系统已经生成严格递增 revision,用于表示同一份授权状态的新旧关系。它演示账本怎样忽略同事件与旧 revision,不模拟 Stripe 事件排序。

state = {"version": 0, "active": False}
seen = set()
def apply(event_id, version, active):
    if event_id in seen:
        return False
    seen.add(event_id)
    if version <= state["version"]:
        return False
    state.update(version=version, active=active)
    return True
assert apply("E-2", 2, True)
assert not apply("E-2", 2, True)
assert not apply("E-1", 1, False)
assert state["active"]
print(state)

实际输出是 {'version': 2, 'active': True}。E-2 先更新到版本二;重复 E-2 不再处理;版本一的 E-1 不覆盖新状态。这里的 active 是教学授权状态,不是直接复制 Stripe Subscription.status。

真实处理需要事务把事件处理记录、状态更新与派生任务保持一致,并管理并发与失败重试。多个相关对象也不一定共享一个单调版本。收到旧通知时应按当前对象与业务策略核验,不能凭示例里的数字就判断可以丢弃所有真实通知。

欠款、取消与退款按策略影响权限

付款失败时可以进入明确的宽限期,也可以暂停功能,取决于产品承诺与业务规则。周期末取消与立即取消影响不同;试用结束也需要单独处理。记录授权起止与原因,方便查询当前权限来自哪一条规则。

退款与取消订阅是不同操作,不能假定退款会自动取消所有后续账单或撤销所有权限。产品需决定全额或部分退款怎样影响已经使用与未来可用的功能,并分别核验支付对象与订阅对象。争议等情况也应有独立处理流程。

授权更新后使缓存按规则失效,关键付费操作在服务端检查当前权限。只在前端隐藏按钮不能控制使用;旧登录会话或旧缓存也不能无限保留已撤销能力。数据访问与收费授权有交叉,但仍需各自业务边界。

测试环境不能当作正式行为保证

Stripe 测试环境与沙箱帮助验证支付流程,可以使用文档提供的测试对象与事件条件。测试时钟与某些模拟能力有支持范围,不能据此宣称所有真实支付方式、银行延迟和通知时序已经验证。

准备支付成功、失败、需要额外操作、重复通知、乱序通知、通知暂时未送达与状态对账样本。测试与正式配置要区分环境标识,验证器不能拿一套密钥误处理另一套事件。不要在练习里使用真实卡或真实资金。

对账任务定期核对本地授权与权威业务对象,发现缺失事件和不一致。记录差异、修复来源与处理结果,避免自动把未知对象当作付费。计税、价格和经营规则需要实际业务信息,本文不替这些条件作结论。

通知处理失败时保留已验证事件与重试次数,不能每次重新接收都生成新的业务发放记录。修复后从失败步骤继续,并用权威对象核对状态。人工处理也要留下原因与来源,避免为了让用户尽快恢复而直接修改布尔权限,却无法解释它何时到期。

练习:交付一致的收费状态表

列出浏览器页面、内部支付记录、订阅与 entitlement 的状态及关联方式。模拟 E-2 先到、E-1 后到、相同事件重复、验证失败、处理后进程重启与退款。为每种情况写允许功能与恢复动作。

验收时,跳转不能直接授予权限;不合法签名不进入处理;重复不会重复发放;乱序不会盲目覆盖已核验状态;退款与取消分别有规则。解释 revision 的教学假设,明确真实 Stripe 对账依据。离线断言只能证明账本局部逻辑,真实 webhook 签名、事务与支付方式仍需隔离测试。

资料来源与延伸阅读

资料核验:2026-11-10。本文为原创工程讲解,教学示例不代表线上实测。