业务介绍
支付订单系统是商业体系里的常见一环, 其承载的主要功能是与用户交互完成收银及收银后的订单流转相应的动作。
探探的应用场景是典型的虚拟会员及虚拟货币的购买。与电商体系相比, 主要有两点不同:
- 除常见的支付类渠道(如国内的微信、支付宝,海外的checkout等)外, 还需要接入一些APP内购渠道(如苹果、Appstore、华为的 IAP支付渠道等) 这个主要原因是对于苹果等设备有虚拟商品必须走内购这样的限制。
- 为了进一步提升收入, 会员类的商品需要支持到期自动续费。 这块主要依赖渠道的两种能力 第一微信支付宝等支付渠道提供了免密支付的能力, 第二对于内购渠支持了自动续费类的商品。
针对于上述的差异点, 探探的支付订单体系主要涉及三个层面的业务诉求
- 通过支付渠道提供的支付能力进行单次购买
- 通过支付渠道提供的免密扣款能力进行自动续费
- 通过三方的内购渠道进行单次购买及续费
整体来讲, 这三个层面的业务诉求带来了较大的业务复杂度, 因此我们构建了探探的支付订单体系。
技术实现
整体架构
我们将系统拆分成了两层
第一层是支付体系: 负责与第三方应用商店交互, 屏蔽三方的复杂性与差异性提供统一的业务视角, 支付体系主要涉及两个功能。
- 支付单管理: 最终用户每一笔付费都会形成一笔支付单。
- 续费计划管理: 用户的自动续费会生成一个续费计划。
支付体系构建了一个业务逻辑独立, 可以跨平台的基础服务, 提供探探接入的能力之外也提供了其他内购应用接入的能力。
第二层是订单体系: 订单体系负责处理探探的业务逻辑, 主要包括两部分
- 与商品、促销体系交互计算单价及优惠信息
- 与钱包、会员体系交互进行发货
订单体系
统一的发货服务
用户购买的虚拟商品多种多样(包括多种类型的会员, 每种会员可能拥有不同的特权; 以及虚拟货币), 当购买成功后订单系统需要与多种个系统交互完成发货行为,为此我们设计了一套保障最终一致性的事务处理体系来应对这个挑战。
为了保证最终一致性, 我们采用了上游重试+下游幂等的方案。 其架构如下
其任务流程如下
- 订单系统根据商品信息里的发货描述信息 调用任务管理模块,生成对应的发货任务及相应的子任务。 任务管理模块依赖订单id来保证幂等性。 在调用失败时候订单系统会发起重试并且也会依赖第三方的回调进行重试。
- 发货子任务模块会根据任务描述找到相应的任务执行器 调用下游进行发货, 下游需要根据子任务系统提供的子任务ID来保障业务的幂等性。
- 在任务执行失败的场景下 会自动重试。
幂等性保证的实现机制
-
数据库事务: 对于操作数据库的业务 采用数据库事务的方案保证任务exactly once的执行一次。一般会才有 开始事务 -> (SELECT ... FOR UPDATE| INSERT ON COLICT DO NOTHING)的方式。
-
涉及Redis场景的一致性: 在Redis场景下使用的是Lua脚本的方式做SetNX,但需要注意的是Redis并不是严格的事务, 由于它没有回滚机制在执行过程中Crash会存在问题, 但这种场景比较少见。
支付及签约流程
对于微信和支付宝这种支付渠道, 其支付流程与电商体系相差不大
- 客户端发起支付或签约请求
- 服务端校验商品和券的有效性, 并调用券服务获取优惠策略
- 通过支付服务调到第三方获取相应的签名 返回客户端
- 客户端唤起三方支付
- 三方支付成功后回调服务端完成发货流程
对于苹果来讲, 具有一定的特殊性, 苹果在支付完成之前其交互都是用户通过app和苹果交互的, 交互完成之后苹果将对应的发票返回给客户端, 客户端再回传给服务端。 服务端解析发票完成相应的动作。
在支付和签约流程中,订单体系做的相对较薄, 主要做了几个工作
- 获取商品信息,解析出对应的金额及特权信息
- 获取优惠券信息 计算出订单及合约对应的优惠
支付体系
系统架构
支付模块主要包含两部分功能, 第一将三方的复杂度屏蔽在系统之外提供统一的视角, 第二提供合约管理的功能。 系统设计如下
模块拆解
- 三方服务Provider及IAP: 与对应的三方支付渠道和IAP商店进行交互 获取订单信息
- Pay模块: 负责支付单的管理
- Renew模块 负责续费管理
- 对于微信和支付宝签约的续费计划, 依赖于Credential模块获取免密合约
- 对于IAP模块直接调IAP相关的接口刷新付费信息
续费机制
扣款服务会面临的问题是用户当前可能账户里没钱, 对此我们采用了分级的重试机制去尽可能多的扣款 完成用户的续费。
系统的主要难点及解决方案
一致性保障机制
交易系统有一个关键问题, 保障一致性, 避免资损问题发生。 在保障一致性方面我们整体采用的是幂等+重试的思路。被调用方通过调用方提供的唯一键来保证幂等性, 上游通过重试的机制来保证最终任务成功或者到期后进入补偿流程。
幂等性的实现机制
下游业务会接收来自上游业务传过来的唯一ID,对于DB的场景, 下游业务会将上游业务传来的ID作为唯一索引。 对于非DB索引, 通常采用Check-Lock-Check的模式, 其中Lock阶段采用分布式锁。
重试的实现机制
目前重试机制我们主要有两种实现方式
- 状态机+扫表 扫出符合特定状态的记录进行重试
- 利用延迟任务队列进行重试, 分发一个到期执行的任务给延迟队列,执行失败后再补发一个重试任务。
对于支付订单体系 一致性问题可以拆分成以下三个点
- 支付单和第三方平台订单一致 , 订单要一一对应 金额状态一致。
- 支付单和业务订单一致:两边的订单要一一对应、金额、状态一致;
- 业务单要和会员及虚拟币系统的履约状态一致;钱货一致。
支付单和三方平台订单的一致性保证
机制层面
每一笔支付单都有一个唯一的ID, 会用这个ID去调用第三方,并将第三方回调的三方ID作为唯一键存储在支付单系统里面。
在这块我们踩过一些坑
- 识别三方的唯一ID方面,接入apple的IAP支付的时候遇到了TransactionID变更的问题, 在接入苹果的IAP支付的时候一开始我们用的发票里的TransactionID作为唯一键, 后来发现对于续费的商品同一笔支付会出现不同的tranaction_id的情况 这个发生于用户换设备点恢复购买的场景。 对于续费的产品可以用web_order_id,但对于一次性购买的商品的没有web_order_id 。之后我们进行了优化 使用时间+orignal_transaction_id的方式去解决这个问题, 同一个续费订阅下丢弃掉续费时间一致的订单。 但这种解法会存在并发的问题, 同一个苹果用户在多台设备上同时请求可能会触发。 这块后续的解法是抽象一层中间层, 对于一次性购买的产品用transaction_id做唯一键, 对于非自动续费的产品用orignal_transaction_id+时间戳做唯一键, 从而根本上解决这个问题。
- 三方的退款回调未能正确接入, Gooleplay平台回调接入不正确的问题, Googpleplay的续费产品和一次性产品的退费回调是两个接口, 我们在接入退费的过程中一开始只接入了续费产品的退费回调, 并未接入一次性购买的退费回调, 导致用户在google退费后未能及时通知我们,造成系统异常。
踩这些坑的原因主要是因为对于一些三方支付平台在最初接入的阶段掌控力还不足。 这个一方面需要我们不断积累经验, 做到高的掌控性, 另一方面也需要我们建立健全兜底机制, 及时发现问题, 及时止损。 我们主要是通过建立对账机制来解决的这个问题
观测层面
- 天级对账机制: 每天从三方平台拉取账单, 与支付单一一对账, 及时发现问题。 拉取天级的三方平台数据, 同时从离线表里导出天级的支付单数据 然后一一校验。 特殊场景 针对与apple并未提供订单的明细, 只提供了汇总信息, 这块我们的对账方式是用两天的差额对账,
- 小时级监控每个渠道的成交量, 防止突发问题
业务订单和支付单的一致性保证
-
对于发起支付的流程, 每一笔业务订单都会生成一个唯一的ID, 业务方会用这个ID来调用支付业务。 支付业务会将业务订单的唯一ID作为唯一索引的方式存储下来, 以保证不重复创建支付单。
-
对于续费及三方商店的场景, 其流程是反的, 会先创建支付单,再通知业务系统创建业务订单。
业务订单与会员虚拟币体系的一致性
业务订单成功后会分发发货任务到发货系统, 发货系统会解析商品里的特权描述, 生成多个发货任务, 多个发货任务顺序执行, 在遇到中间失败的情况下会停下来重试。
三方商店的特殊性
对于虚拟商品的售卖来讲, 其特殊的点在于需要接入三方商店,而非传统的支付渠道。 三方商店提供了能力的封装,但各家封装的方式不一致, 实现的水平参差不齐。 这一块带来了较大的系统复杂度。
对此在领域建模阶段, 一开始我们会尝试用一个统一的视角,将支付渠道与三方商店合并到一起, 发现总会有很多不兼容的Case, 于是最终我们在支付系统拆分成了两块, 支付渠道和三方商店, 各自分别去处理各自的业务逻辑, 最后形成统一的业务订单和支付订单。
在实现过程中 支付渠道我们走的是正向流程 先创建业务订单 再创建支付订单 再调用第三方; 三方商店我们会走逆向流程, 先调用第三方解析发票找出未同步的订单 -> 再创建支付单 -》再调用业务创建业务订单。
讨论
系统的优势
在一定程度上屏蔽了支付渠道多样性带来的业务复杂度, 新增加一个支付渠道的成本相对可控。 提升了系统的可维护性及易扩展性。
系统的不足
因为天然支付渠道和APP内购存在较大的差异性, 将两套体系统一的操作会带来较大的系统复杂度, 早期我们在试图合并方向走了挺多的弯路, 到最终是拆开的状态。