
推送支付是每个移动货币客户都熟悉到不假思索的流程。商户请求付款,客户手机上弹出提示,客户输入 PIN,交易完成。在较早普及移动货币的市场,这是使用最广泛的支付方式,而且感觉毫不费力。这种轻松感背后,是大量为了"隐身"而努力工作的工程设计。任何一环出错,便利就会变成重复扣款、卡住的付款,或者商户为一笔从未到账的钱交付了货物。
下面介绍提示背后实际发生的事情,以及如何让它即时、安全、绝不重复扣款。
提示到达的两种方式
"弹出提示"背后有两种机制,它们的覆盖范围和故障模式不同,因此很重要。
经典机制是 SIM 应用工具包(STK):运行在 SIM 卡本身上的代码,因此能在没有安装任何应用的普通功能机上推送安全的 PIN 输入提示。最早的移动货币系统正是借助 STK 覆盖了所有人:菜单和 PIN 输入都由 SIM 卡通过网络驱动,运行在别无他物的硬件上。现代机制是应用推送:向智能手机应用发送通知,打开确认界面。两者殊途同归:客户授权后,扣款进入其生命周期。值得构建的平台会同时支持两者,因为客户群体两种手机都有。
扣款是一个状态机
推送支付中最重要的理念是:扣款不是一个事件,而是一个状态机,具有明确、定义清晰的状态和转换。把它当作"发出即忘"的调用,是大多数生产事故的根源。状态大致如下:
- 处理中:商户已发起扣款;提示已推送;系统正在等待客户。
- 已授权:客户已输入 PIN 并确认;系统已检查余额和限额,正在提交资金变动。
- 已完成:账本已记录平衡的分录;资金已转移;商户可以交货。
- 已拒绝 / 已过期 / 失败:客户拒绝、提示超时或某项检查失败;没有资金变动,扣款被干净地关闭。
每笔扣款在任何时刻都恰好处于其中一个状态,并且只能沿着定义好的转换移动。这不是官僚主义,而是让流程可推理、可恢复的关键。出问题时,要问的永远是"这笔扣款处于什么状态,哪个转换卡住了?",这是一个有答案的问题,而不是一声无奈的叹息。
商户如何得知结果
商户系统需要知道扣款是否完成:以便打印小票、放行货物、更新订单。有两种获知方式,它们之间的差别,就是笨拙集成与利落集成之间的差别。
- 轮询。商户反复询问状态接口:"好了吗?"这能用,但浪费且滞后:轮询太频繁会压垮 API;太稀疏则客户只能站在收银台前干等。
- Webhook。扣款状态一变化,平台立即向商户推送一个已签名的回调。商户在瞬间得知结果,没有浪费的调用,并获得一个可直接处理的清晰事件。这才是正确的基础机制,构建良好的平台会将其设为默认方式,并以轮询作为后备。
是什么让它值得信赖
有三项特性能把一个能跑通的演示,变成商户可以赖以经营的系统。
幂等性:重复扣款的克星
网络会重试。如果商户的"创建扣款"请求超时后被重新发送,就绝不能创建第二笔扣款。每个创建扣款的请求都带有一个幂等键;平台记录该键,使用相同键的重复请求会返回原有扣款,而不是新建一笔。同样的规则也保护确认步骤。客户感受到的保证——"我只被扣了一次"——正是这项特性,它在资金变动的那一刻强制执行,而不是应用里一次侥幸的检查。
签名的 Webhook
Webhook 是一个不请自来的 HTTP 调用,到达商户服务器并声称某笔付款已完成。商户凭什么相信它?因为每个回调都有签名:使用只有平台和商户知道的共享密钥,对内容计算出的 HMAC-SHA256 签名。商户重新计算签名,拒绝任何不匹配的回调,这让伪造的"您已收款"回调对攻击者毫无用处。并且由于网络会出故障,Webhook 会以递增间隔重试,直到被确认接收,因此商户端的短暂中断不会丢失事件:它会被反复投递,直到商户服务器返回成功。
过期
未确认的扣款不能无限期挂着。如果客户没有输入 PIN 就离开了,扣款必须干净地过期,而不是作为一笔未结的负债留着,可能在一小时后莫名其妙地完成。一个有界且可预测的过期窗口,才能让商户看到的情况与客户保持一致。
区分真实系统与演示的边界情况
顺利路径(推送、确认、完成)只是工作中容易的那四分之一。有意思的故障都藏在缝隙里:
- 客户已确认,但网络丢失了确认回执。扣款在平台上已是
completed,商户却没收到通知。Webhook 重试正是为此而设。商户的处理程序本身也必须是幂等的,这样重复投递的"已完成"事件不会导致货物发出两次。 - 两次确认同时到达。重试的确认不能重复记账。幂等键和状态机共同保证第二次确认看到的是"已完成",而不是再次转移资金。
- 扣款恰好在客户确认的同一时刻过期。状态机必须规定唯一的胜者:转换是原子的,因此扣款要么已授权,要么已过期,绝不会两者兼有,并且会明确告诉客户是哪一种。
- 对账。日终时,每一笔
completed扣款都必须对应一组平衡的账本分录和一条结算记录。由于扣款生命周期与账本绑定在一起——完成就是记账——对账是由结构保证的,而不是靠调查。
一个实例时间线
以下是商户向客户收取 1,250 杂货款时逐秒发生的事情。时间为典型值,并非保证。
| 时间 | 发生了什么 | 扣款状态 |
|---|---|---|
| 0.0 秒 | 收银员输入客户号码和金额;收银台发送带幂等键的创建扣款请求 | 处理中 |
| 0.3 秒 | 平台验证商户和客户,并推送提示 | 处理中 |
| 2–20 秒 | 客户查看商户名称和金额,并输入 PIN | 处理中 |
| +0.2 秒 | 核心系统检查 PIN、余额和限额,并记录平衡分录 | 已完成 |
| +0.5 秒 | 签名的 Webhook 到达商户;客户和商户收到确认 | 已完成 |
| 稍后 | 商户余额按其结算周期划入银行 | 已完成,已结算 |
客户阅读和输入占据了大部分时间。平台所做的一切都远不到一秒,这就是为什么运作正常时体验感觉是即时的,而偶尔出现的慢步骤会立刻被察觉。
正确构建商户端
可靠的推送支付集成,有一半在商户系统里。一个好的 Webhook 处理程序应当:
- 在信任内容中的任何信息之前,先验证签名。
- 具备幂等性:同一事件的第二次投递不会改变任何东西。
- 快速响应,把繁重工作放到之后处理,以免平台超时并进行不必要的重试。
- 按扣款参考号匹配,绝不只凭金额和电话号码。
- 如果在预期窗口内没有收到事件,回退到状态接口查询,而不是假定失败。
- 记录收到的每一个事件,以便用记录回应争议。
选择过期窗口
过期时间是一种权衡。太短,正在找手机的客户还没确认扣款就已失效。太长,商户只能守着柜台上的货物等待,过时的提示还会在客户离开后才到达。大多数运营方为当面支付设置一到两分钟,远程请求(例如发给在家客户的账单)则更长。无论窗口多长,都要显示出来:知道提示持续一分钟的收银员,就知道何时该重试。
客户会注意到的细节
提示界面上的小选择,决定了客户是否信任它:
- 显示商户的注册名称,而不是内部代码,让客户知道是谁在请求。
- 在 PIN 输入框之前清楚显示金额和参考号。
- 提供明确的拒绝选项,让没有预料到提示的客户可以拒绝,而不是置之不理。
- 用文字和消息确认:包含商户、金额和参考号的回执,在屏幕上和短信中都提供。
- 提醒注意意外的提示。欺诈者会发送提示,指望客户条件反射地批准;一句简短的"仅在您向该商户付款时才批准"即可降低这种风险。
一个生命周期,覆盖所有渠道
做对这件事的最后一环是:推送支付不应是一个在应用、收银台和 API 中分别接线的特例。它应该是一个由所有渠道驱动的统一扣款生命周期:客户扫描商户二维码、商户从 POS 终端发起扣款、合作伙伴调用开发者 API,创建的都是同一种扣款,经过相同的状态,记入同一个账本,发出相同的签名 Webhook。一次构建、正确构建,每个渠道都能继承这些保证。
推送支付、二维码还是收款号?
推送支付是向客户收款的三种常见方式之一。每种方式适合不同的场景:
| 方式 | 由谁发起 | 功能机可用 | 最适合 |
|---|---|---|---|
| 推送支付 | 商户 | 可以,通过 SIM 卡提示 | 商户知道金额的收银台和结账环节 |
| 二维码 | 客户扫描 | 不可以,需要智能手机应用 | 智能手机客户较多的商店;小商贩使用静态码 |
| 收款号或缴费号 | 客户输入 | 可以,通过 USSD | 任何商户;通用的后备方式 |
商户能同时接受这三种方式时效果最好,而平台把它们视为通过不同途径到达的同一种付款。
围绕提示的欺诈
由于提示要求客户批准付款,它会吸引社会工程攻击。常见模式是:欺诈者触发提示,然后冒充服务提供商给客户打电话,诱导其批准。防御需要在多个层面进行:醒目显示请求商户经过验证的名称;限制商户在短时间内向同一号码发送提示的次数;监控提示大多被拒绝或被举报的商户;并反复告诉客户,服务提供商绝不会在电话中要求他们批准付款。商户入网审核同样重要,因为身份可追溯的已验证商户很难被用于欺诈。
衡量推送支付
| 指标 | 为何重要 |
|---|---|
| 批准率 | 客户批准的提示占比;突然下降意味着投递问题或客户困惑 |
| 批准耗时 | 客户所用时间;耗时长说明界面不清晰或投递缓慢 |
| 过期率 | 无人响应而失效的提示;据此调整过期窗口和对收银员的指导 |
| Webhook 首次投递成功率 | 商户无需重试即可可靠收到结果的程度 |
| 被举报的意外提示 | 欺诈企图的早期信号 |
柜台之外:远程请求
当客户不在收银台前时,同样的机制也适用。学校可以向家长发送学费请求,公用事业公司可以请求缴纳账单,送货员可以在送达时请求付款。远程请求需要几项调整:更长的过期时间、对付款内容的清晰描述、请求方经过验证的名称,以及客户能从账单或消息中认出的参考号。状态机、签名和幂等性完全不变,这就是为什么一个统一的扣款生命周期可以同时服务收银台、账单和 API。
测试推送支付集成
在商户正式上线之前,请在沙箱中运行以下用例:
- 分别批准、拒绝提示以及让提示过期,检查商户系统对每种情况显示的结果是否正确;
- 用相同的幂等键发送两次相同的创建扣款请求,确认只存在一笔扣款;
- 让商户的 Webhook 接口故障几分钟,确认其恢复后事件能够到达;
- 发送一个签名错误的 Webhook,确认商户会拒绝它;
- 尝试一笔超过客户限额的扣款,确认它被干净地拒绝;
- 检查退款是否关联到原始扣款。
通过这些测试的集成,在最繁忙的日子里也会表现良好。
常见问题
如果客户没有数据连接怎么办?
基于 SIM 卡的提示通过移动网络工作,不需要数据。对于应用提示,没有数据的客户仍然可以通过 USSD 输入商户的收款号来付款。
商户能否在客户未批准的情况下扣款?
不能。只有客户用 PIN 授权后,扣款才会完成。商户可以发起请求;只有客户可以批准。
退款如何处理?
退款是一笔从商户返还给客户的新交易,并与原始扣款关联。原始扣款保留在历史记录中,双方都能看到支付了什么、退回了什么。
如果客户说已经批准,但收银台什么都没显示,收银员该怎么办?
等待结果,而不是再收一次款。收银台应使用参考号查询扣款状态。如果扣款已完成,结果会在片刻内显示;如果已过期,收银员可以放心发送新的提示。切勿把确认界面的截图当作付款凭证。
推送支付适合大额付款吗?
客户等级限额与其他任何付款完全一样适用,PIN 步骤也相同。对于大额商业付款,商户通常会把提示与账单参考号结合使用,以便双方能将付款与单据对应起来。
这样做,推送支付感觉是即时的,而且不可能重复扣款——不是因为从不出故障,而是因为每一种故障都有明确的状态、重试机制和回到一致性的路径。这正是 PesaBridge 在应用和开发者 API 中基于统一扣款生命周期所提供的能力。