电商课题VII:支付交易一般性准则
@郑昀汇总 创建于2012/11
发布版本号:v1.3
概念:
退款期限,交易,交易关闭,交易结束,掉单,幂等性,数据一致性
关键词:
历史记录不得直接篡改原则,
交易关闭通知处理,退款处理结束通知,
掉单被动处理,掉单主动处理,
多个渠道的重复支付处理,
支付成功时商品不可售卖的处理,
订单金额变化交易流水号变化规则,
推送订单不得包含违禁词,
支付通知并发到达的处理,
支付子系统的独立性和可靠性,
补录数据的时间准则
一. 通用规则
1.1. 历史记录不得直接篡改
电商核心服务基本都是分布式应用,分布式事务如处理不妥善,容易产生数据不一致。一旦出现数据不一致,一定要有旁证来修正。
所以数据库中以下关键资源的记录,郑昀提醒您注意,原则上不允许直接修改历史数据:
- 下单;
- 支付购买;
- 生码/验码/物流信息记录;
- 退款(含部分退款);
- 结算;
- 用户注册;
- 与第三方数据同步;
这里的“直接修改”特指,没有把变更行为记录到日志表里,而是直接在原始记录上 update 甚至 delete ,这种“篡改”和“毁尸灭迹”是明文禁止的,即使留下了文件类型日志也是不允许的。
第一,要修改这些记录的关键字段时,必须在相关日志表里保留变更日志,并记录操作人和发起人,一定要确保历史可回溯。
第二,严禁对记录做物理删除,只能是软删除。
实例:
对于××团收到第三方支付的通知,我们有第三方交易流水记录表;
对于××团发起的到第三方支付的交易请求。我们有 jxxxe_pay_log 记录;
订单操作变更记录,我们有 jxxxe_order_action日志表记录。
Q:什么叫历史可回溯?
A:系统可能对关键记录做了一系列修改,甚至有程序在某个时间段内误写引入了脏数据,但郑昀提示您,我们依然要能从各种操作日志表中随时倒推回历史某一个时刻的快照,一是确保随时能安全地把数据还原回去,二是管理平台可以清晰地展示出由谁引发、怎么变化的历史,三是便于排查问题。
譬如,对于记录了订单信息的 order_info 表,会员如果点击使用账户余额支付了订单的应付金额,那么该订单操作日志表就会做如下记录,原订单记录的重要字段(what)在什么时候(when)从什么变为了什么(how),都会详细记录。
1.2. 对关键资源的操作,当接口保证不了幂等性时,必须能防并发
如果你的接口不具备幂等性,那么请保证整个(分布式)系统内对一个重要事物(订单,账户的资金变动等)的有效操作线程,同一时间内有且只有一个。
比如交易中心有N台服务器负载均衡,订单中心则有M台服务器,如何保证一个订单的同一笔支付处理,一个账户的同一笔资金变动操作是原子性的。
原因也很简单:
- 第三方支付平台可能同一时刻给你的 pay.5xxxxn.com 交易中心集群服务推送过来两个一模一样的支付成功通知。
- 用户浏览器可能安装了某种插件(如早期的迅雷插件),插件本身为了探测 一个URL 是否是BT资源,会同时模拟发起一个 HTTP GET 请求。
- 上游服务不可控,不可预知地向发起下游服务发起并发请求。
1.3. 支付子系统的独立性
电商业务容易出现以下问题:
- 受到DDoS攻击,带宽被打满;
- 某一个业务突然响应变慢,如从20ms激增为1s,业务请求被大量阻塞;
所以不同业务之间必须严格隔离,防止一个业务超负荷或宕机连累其他业务。
因此,我们至少要做到:
- 支付子系统是一个独立工程,独立部署,有单独的二级域名。
最好能做到:
- 独立带宽,
- 独立的存储介质。
1.4. 支付子系统接收第三方支付通知的可靠性
电商的支付子系统提供一个 Web Service,来接收各家第三方支付的各种异步通知。
接收对方通知之后,你可能会遭遇各种异常,如:
- 解析时发生异常;
- 调用支付中心时发生异常,如网络故障,如支付中心宕机,如调用超时;
- 写日志时发生异常;
即使如此,你也不应该丢弃该通知,并且没有返回“success”字符给第三方支付,
因为这样的话,相当于你的业务完全依赖于第三方支付下一次重试了。
所以,郑昀提醒您,你应该主动地、积极地先把对方的(支付成功、退款处理等)通知存入一个存储介质,如消息队列;
一旦同步处理失败,那么能以某种策略重播这个消息,直到业务系统恢复正常、处理完毕为止。
1.5. 补录数据的时间准则
电商结算和对账,由于帐期定义(如日清日结,如T+2结算,如销售佣金计算,如CPS联盟结算),非常依赖于数据记录的时间。
所以,当由于以下原因补录或同步数据时,请慎重考虑数据的时间字段到底如何采信:
- 掉单后主动处理(注:主动查询第三方支付网关,获得订单支付状态);
- 不同系统之间同步失败后手动触发重新同步;
- 不同公司的平台之间交换退款等数据。
下面举两个小例子:
例一:
××商城对供应商的结算标准是,仅仅以订单进入他们的ERP系统的时间为准,而不是以该订单的下单时间、支付时间等数据自身时间记录为准。
即,一个12月1日23:58支付成功的订单,数据一层一层传递到ERP时,同步时间是12月2日00:01,那么此订单就被判定在12月2日的应结算明细中,而不是12月1日的。
咋听上去好像不合理,仔细想想,供应商有很多,IT系统也就很多,彼此之间的服务器时间肯定不同步,更别提会有很多种类型的脏数据,所以××商城只有选择用ERP系统自身的时间作为唯一结算凭据,而不采信第三方系统的 add_time、update_time、pay_time 五花八门的时间,这样才不会重复结算或漏结算。
例二:
支付系统宕机,一段时间后才恢复,此时客服主动处理顾客投诉掉单的订单,从第三方支付查到已支付后,将订单置为已付款。那么,该订单的支付时间怎么记呢?
一是,采信第三方支付系统传递的真实支付时间。二是,记录为手动重置的当前时间。
郑昀的答案是,后者更安全。
因为,有可能补录数据已跨日或跨月,前一日的结算清单可能已计算完毕,如果按前者的逻辑,突然又补录一条记录,结果前一日(上一个月)也不结算它,后一日(下一个月)也不结算,那这个订单就漏结算了。
当然,真实支付时间也还是要记录到日志表的。
二. 易被忽略的逻辑处理
2.1. 交易关闭通知的处理
支付宝是这么定义“交易关闭”的:
枚举名称
|
枚举说明
|
TRADE_CLOSED
|
|
交易关闭通知默认是不发送的,如下表格所示:
触发条件名
|
触发条件描述
|
触发条件默认值
|
TRADE_CLOSED
|
交易关闭
|
false(不触发通知)
|
但如果商户(也就是你的网站)向支付宝申请打开了该配置,那么请注意接收 TRADE_CLOSED 通知,它会对你的核心购买逻辑产生影响。
如何主动指定交易关闭时间呢?
即时到帐交易接口中有这么一个参数:
参数
|
参数名称
|
类型
|
参数说明
|
是否可为空
|
样例
|
it_b_pay
|
超时时间
|
String(3)
|
设置未付款交易的超时时间,一旦超时,该笔交易就会自动被关闭。
取值范围:1m~15d。
m:分钟、h:小时、d:天、1c:当天(无论交易何时创建,都在0点关闭)。
该功能需要联系技术支持来配置关闭时间。
|
可空
|
1h
|
支付宝收到这个参数后,界面会有如下展示:
此时提示几点:
1)如果交易已经关闭,但商户的网站上仍保留了订单的“付款”按钮,那么点击跳转到支付宝后,会看到如下警告信息:
或
2)当交易状态为交易关闭时,就算用户能通过第三方网银对支付宝账单进行付款(用户可能已经跳转至银行交易页面,并且未关闭页面),第三方网银能将支付成功信息通知支付宝,支付宝也不会通知商户,而是会自动退还至支付宝余额中。
3)网银直连的订单是关闭不了的,因为它没有跟支付宝账户绑定。
4)商户如发现交易已关闭的订单被用户支付后,那么必须进入异常支付流程(能原路退返就退,如无法退返则返还至账户余额)。
2.2. 退款通知的处理
支付宝对此的定义是:
(1) 交易成功之后,商户(高级即时到账或机票平台商)可调用批量退款接口,系统会发送退款通知给商户。(2) 当商户使用站内退款时,系统会发送包含 refund_status(退款状态)和 gmt_refund(退款时间)字段的通知给商户。
其中退款状态有两种:
枚举名称
|
枚举说明
|
REFUND_SUCCESS
|
退款成功:
|
REFUND_CLOSED | 退款关闭 |
第三方支付在退款处理完毕后,会发送异步通知给商户,如下表格所示:
触发条件名
|
触发条件默认值
|
退款处理结束
|
true(触发通知)
|
这个所谓退款处理结束通知,实际上仍是一个“trade_status_sync(交易状态同步)”通知,特殊性在于携带的 refund_stauts =REFUND_SUCCESS 参数,实际例子如下所示:
注意几个要点:
- 当交易状态为 TRADE_FINISHED(交易完成) ,那么不可退款!
- 此处有一个“退款期限”概念,交易关闭(TRADE_CLOSED)后3个月(或6个月)内可以退款,超过此期限后,该笔交易成功且结束,从此不可退款!对于业务逻辑,意味着此时只能退还金额到账户余额,无法原路退返。支付宝、快钱、财付通等均有此设定。
- 手机支付退款如返回 D23190 错误码,含义是退款日期超过最大有效期(有效期是半年)。
- 退款处理结束的通知到达时,支付宝会先发送一个支付成功通知,防止你的系统不知道有此交易。请正确处理这个支付成功通知,不要误认为这是“重复支付”(因为对应的订单可能已确认+已支付),以至于误判给原路退返了。
三. 异常处理类
3.1. 掉单的被动处理
支付宝的文档说的很清楚:
服务器异步通知页面(由参数 notify_url 指定页面文件)获取支付宝返回的结果数据,(商户的)程序执行完后必须打印输出“success”(不包含引号)。如果商户反馈给支付宝的字符不是 success 这7个字符,支付宝服务器会不断重发通知,直到超过24小时22分钟。一般情况下,25小时以内完成8次通知(通知的间隔频率一般是:2m,10m,10m,1h,2h,6h,15h);
所以,如果你用来接收支付宝异步通知的服务阻塞了(hang/stuck)或挂了(shutdown/crash),就无法给支付宝返回 success 响应,所以它会不断地发起重试,直到25小时内你恢复服务返回 success 。
所以如果掉单(用户已付款/已扣款,但你的数据库里这个订单还是未付款状态),你还有机会补救。
3.2. 掉单的主动处理
掉单后,等待支付宝补发通知给你,商户可能来不及应对汹涌而来的顾客投诉。
此时,服务器端应该主动提交此订单对应的(一个或多个)唯一订单号(out_trade_no,支付宝合作商户网站唯一订单号),调用支付宝的单笔交易查询接口 single_trade_query,从而获得交易状态、支付宝交易号、付款时间、交易总金额等明细。
(具体细节请看支付宝的《单笔交易查询接口(single_trade_query).pdf》)
一旦查询到了支付成功的细节,而且付款金额也等于商户记录的应付金额,那么就可以给操作人员展示一个画面,使得他能手工置这个订单为已确认+已付款。
3.3. 来自于多个支付渠道的重复支付
什么情况下会产生重复支付呢?
看一个真实的顾客投诉案例:
『顾客购买××团的商品后,第一次付款时,由于付款故障(如系统掉单),使得顾客认为付款未成功,所以,顾客换用了其他支付渠道(如选择支付宝的网银直连,或者选择网银在线)进行了第二次付款,于是××团帐号收到两笔付款,且两笔付款均付款成功。』
这不是偶然现象。
处理办法还是:按时间顺序,稍晚一些的付款被认为是重复支付,进入异常支付流程(能原路退返就退,如无法退返则返还至账户余额)。
3.4. 支付成功时系统发现商品已不可售卖
商品不可售卖有两个原因:1)库存不足;2)商品已下线。
如果是库存不足:
团购商户为了避免超卖,应该
- 将订单关闭,
- 将交易关闭,
- 将实付金额原路退返(如无法退返,退至账户余额),
- 记录支付失败日志,
- 记录资金变动日志,标记退返原因是“库存不足”,
- 顾客可以在前台账户余额变更历史中看到有过付款以及被退款的明细。
如果是实物类电商商户,因为可以事后干预,补足库存,所以可以接受这次付款行为。
如果是商品已下线,处理方式同上,只不过要标记退返原因是“商品已下线”。
3.5. 订单名称中不能包含敏感词
支付宝对商品标题核查得非常严格,所以郑昀郑重提醒您,为了避免顾客发起支付时看到支付宝如下警告:
请提前调用 支付宝交易信息敏感词分析接口(fast_text_trade) ,在录入信息时就阻止保存。
注意,此敏感词分析接口需要联系支付宝开通权限。
四. 正常支付流程的两个要点
4.1.正常支付的处理流程
对于一个团购商品来说,顾客的正常支付成功通知到达服务器端时,业务规则简述为:
- 商品活动结束后,所有未付款订单,一律不允许支付。
- 必须符合库存管理规则。
- 本次支付金额应该小于等于该订单的待支付金额。
- 订单状态正常(不能处于“已取消”、“已付款”等状态)。
一旦发现违背业务规则的支付成功通知到达,则:
- 并不修改订单状态;
- 将此笔支付款项自动返还到该会员的账户余额里;
- 支付中心记录支付失败日志;并记录资金变动日志;
- 会员在前台“个人中心”下的“账户余额”里能看到这个余额变更历史以及对应的说明。
支付流程图如下图4.1所示:
图4.1 团购支付中心判断的简单流程
4.2. 交易流水号变化规则
商户发给第三方支付的 out-trade-no 标识了一次交易的商户唯一订单号。
该参数的定义为:
参数
|
参数名称
|
类型
|
参数说明
|
是否可为空
|
样例
|
out_trade_no
|
商户网站唯一订单号
|
String(64)
|
支付宝合作商户网站唯一订单号,并非支付宝交易流水号
(确保在合作伙伴系统中唯一)。
|
不可空
|
58942120-tuan-001
|
即使对应同一个订单,也可以构造出不同的 out_trade_no 。
只要当支付宝的交易通知把这个参数原样返回时,你的程序能知道这是哪一个订单的哪一笔交易,它的应付金额是多少,这个应付金额被支付后订单产生什么变化,这样就行。
下面举几个例子。
例一:修改订单,订单应付金额或支付方式发生变化
背景:订单在没有支付成功之前,顾客都是可以修改的。做了以下修改后,可能会引起订单应付金额或支付方式发生变化:
- 余额支付的金额变化
- 购买份数的调整
- 优惠券/代金券的使用
而支付宝等第三方支付,对于一个用商户唯一订单号标识的交易,禁止变更 total_fee(交易总金额)字段!
所以,我们的同一个订单,发起不同应付金额的支付请求时,必须更换 out_trade_no ,流程如下图4.2所示:
图4.2 订单应付金额变化,out_trade_no 必须变化
例二:修改订单,订单支付方式发生变化
背景:订单未成功支付前,用户也是可以调整支付方式的:
- 支付方式的调整(不仅仅指从支付宝变为快钱这种第三方支付之间的变更,而且包括从支付宝之网银直连变为支付宝这种第三方支付内部变更)
此时,建议更换 out_trade_no 。
例三:订单已付款,但追加一部分商品,需要补支付
背景:选择了菜品1、2、3、4的订单已支付成功,顾客追加菜品5,不需要创建新订单,可以在原订单基础上补充支付。
做法:如下图4.3所示
图4.3 订单补支付
4.3. 对账拉单
无论系统是否可靠,商户终归还是要对账的。
对的就是数据库里记录的当天应收帐款,与第三方支付商户帐号里收到的钱是否吻合。
如果你数据库记录的顾客用支付宝支付的款项是10001元,而你的支付宝帐号里只收到了10000元,那一定有问题,必须要深究下去。
核对的办法就是,
每天零点,从数据库里查询出前一日用支付宝支付的所有交易,得到支付宝交易流水号和支付金额的集合;
遍历这个集合,拿交易流水号去支付宝的单笔交易查询接口(single_trade_query),这样查出交易金额,对比一下,看你数据库里记的支付金额和实际收到的交易金额是否一致。
@郑昀汇总于2012/11
2)电商课题V:分布式锁 (2012-11-17 22:16)
3)电商课题:cookie防篡改 (2012-11-17 22:24)
4)电商课题VI:分布式Session (2012-11-17 22:30)
5)电商课题:RBAC权限控制 (2012-11-17 22:47)
6)电商课题:幂等性 (2012-11-22 23:52)
7)电商课题:客户端的IP地址伪造、CDN、反向代理、获取的那些事儿 (2012-09-19 01:17)
8)电商课题:对付秒杀器等恶意访问行为的简单梳理 (2012-09-18 03:51)
9)电商课题VII:支付交易一般性准则赠图几枚: