返回文章列表

订阅系统最难的不是付款:是重复通知、退款和恢复购买

2026年10月12日


用户确认购买、系统返回成功、App 显示会员,这条路径很容易被当成订阅系统的终点。

后端真正需要长期维护的,是购买之后的事情:通知重复投递、处理到一半进程退出、退款晚到、用户重装 App,以及匿名购买后再登录。

这次整理几个 App 的服务端时,我花了不少时间补这些边界。

每个边界单独看都不复杂。放在同一个权益系统里,处理顺序和记录方式就变得很重要。

解出一段数据,不等于验证了交易

Apple 的签名数据需要验证。能读到其中的交易号、产品和到期时间,不代表这些字段已经可信。

服务端这次补强了 Apple JWS 的证书链校验,并核对签名对应的应用和环境。验单时保留可信交易链证据,避免后续归属判断只依赖客户端提交的字符串。

我把这一层放在权益写入之前。只有先确认来源,后面的去重和恢复才有意义。

同时,临时无法访问 Apple 或数据库,并不等于用户永久没有权益。不能用一次查询失败,覆盖已经存在的合法记录。

重复通知,不只是查一次 Redis

处理重复通知时,最直接的办法是在缓存里记一个“已处理”标记。

但有两个问题要回答:标记写了,业务更新失败怎么办?业务已经更新了,进程在写标记之前退出怎么办?

如果这两个动作不能一起完成,就可能出现永远不重试,或者重试时重复产生业务效果。

这次的处理重点,是把通知状态、事务内的权益更新,以及后续需要执行的工作放进可恢复的持久记录。缓存可以帮助协调,但不作为唯一的完成凭证。

可以把顺序理解成:

验证签名与应用、环境
    ↓
记录并认领通知处理任务
    ↓
定位对应交易与订阅
    ↓
事务提交权益变化和后续任务
    ↓
处理缓存失效等后续工作

进程在哪一段退出,下一次处理就应知道已经完成到哪里,而不是只得到一个模糊的“处理过了”。

如果签名已验证,但本地暂时找不到对应订阅,也需要保留待匹配事件。后续验单建立记录后,还有机会继续匹配,不应该直接要求用户再买一次。

最晚收到的,不一定是最新状态

通知到达顺序和业务发生顺序不能简单画等号。

假设新购买已经生效,随后收到一条更早交易的退款通知。如果只是看到“退款”就把整个用户会员关掉,就会影响另一笔仍然有效的购买。

反过来,如果为了保护新状态而忽略所有旧事件,又可能漏掉确实需要处理的退款。

这次把通知顺序和交易范围分别处理:更新状态时参考可信事件时间与已有快照版本;退款和撤销则定位到对应交易,避免跨交易误撤销权益。

终身购买也是需要单独考虑的语义。它没有订阅到期时间,不能因为到期字段为空就被判成失效。

同一笔交易,也可能带着不同字符串回来

匿名购买、恢复购买和登录后的认领,会让同一条交易链从不同请求入口回来。

不能只把收据字符串当作唯一身份,也不能因为两个请求接近同时到达,就允许交易链被不同账号分别认领。

这次补了交易链归属的串行化和真实数据库约束检查。同一所有者的合法重试应幂等;存在归属冲突时,要保留证据并明确处理,不能自动迁走已有购买。

流水记录也需要保存经过验证的交易标识。这样之后排查通知、退款和恢复时,能够沿着同一笔交易回看,而不是只能比较几段客户端输入。

测试要覆盖“做到一半”的状态

这轮不仅测试正常购买,还覆盖了重复和并发通知、旧快照更新、恢复处理,以及事务或后续工作发生故障的情况。

部分验证使用隔离的真实 MySQL、Redis,检查唯一约束和跨实例并发。只用 Repository mock,很难证明实际数据库竞争时的行为。

我也检查了失败后能否重试,以及重试是否会重复生成流水或影响错误的交易。这里的结果需要具体到场景,不能用一个总通过数替代。

沙盒购买和 TEST 通知,各自证明什么

这次在真机上完成了 GBalance 的月度沙盒购买。使用的是指向隔离服务的 QA 包,能够验证这条测试购买路径,但它不是 App Store 二进制上的真实生产扣款。

上线后,还对三个应用分别发起了 Production 和 Sandbox 的 Apple TEST 通知,六次投递状态均为 SUCCESS。

这证明了测试通知的投递链路,并结合服务端记录验证了签名及应用、环境匹配。TEST 通知没有交易标识,不能据此开通权益,也不等于购买、续期和退款业务全部通过。

沙盒购买、签名投递和生产配置检查,是不同层次的证据。写验收结果时,我把它们分别记录。

修复完成之后,仍然需要观察

本次服务端已经部署到生产,关键在线检查通过。但实际上架客户端的完整恢复路径、退款和真实生产购买仍未全部验收,也没有做生产压力测试。

如果发现历史交易归属异常,也应先核实可信交易证据,再单独处理。不能为了让页面上的数字看起来正常,直接全量重放旧通知或重新分配购买。

我现在更重视的是:一条权益从哪里来,经历过哪些交易和通知,失败之后能不能继续处理。

把这些记录补齐,用户下一次恢复购买时,服务端才有足够的依据判断结果。关于这类修复怎样保持旧客户端接口可用,我另外写了后端兼容发布的复盘。


查看所有文章

本站展示个人独立开发的产品与技术实现过程。技术交流或对产品有反馈,请邮件联系。

© 2026 GunLabs · 刘罡 · 桂ICP备2025072478号-3 · 隐私政策