做过税地系统或三方支付或对接过银行支付通道的朋友应该清楚,我们的支付系统在调用银行通道获取到付款单的终态后,涉及到记账、结算、通知下游商户等业务逻辑。这其中,有一项默认的操作是,更新付款单的状态。
并且,应该先变更状态,变更状态成功后,然后再去执行其他业务逻辑。
我们在参与一次代码评审时,就发现了不靠谱的事情。开发人员先发起异步记账,然后才是更新付款单状态。
/**
* 订单完成的业务逻辑封装
* @param paymentOrder 付款单对象
* @param payResult 付款结果
*/
void handlePaymentCompletion(PaymentOrder paymentOrder, PaymentResult payResult){
if (PaymentOrderStatusEnum.isFinalState(payResult.getStatus())){
return;
}
// 订单完成,异步记账
accounting(paymentOrder);
// 持久化更新订单付款状态
updateOrderPayResult(paymentOrder, payResult);
// 异步通知下游商户系统
notifyMerchant(paymentOrder);
}
那么,这会出现什么后果呢?
假如持久化记账完成了,商户的账户余额也变更了,但是,变更付款单状态时,由于字段超长等某些原因导致异常,程序中断,付款单状态未能持久化变更,也就是说,此时数据库里付款单的状态依然是“付款中”。那么,定时查单任务(或查单延迟消息队列)在下一次调度时还会读取到这一笔“付款中”的交易,然后继续查询银行通道,通道返回终态后,又开始执行记账逻辑,这为重复记账埋下了种子。
重复记账会产生什么后果呢?
①如果付款单是付款失败,商户账户可用余额会增加。重复记账,就会出现余额异常翻倍增加,意味着商户可以用增加的这笔“意外之财”继续付款。这是资金风险的原罪。
②如果付款单是付款成功,商户账户可用余额减少。重复记账,就会导致商户余额异常成倍减少,意味着商户无法继续付款。我(商户)明明给你充钱了,你却告诉我余额不足,不让我用,我投诉你!
因此,这个顺序颠倒的代码逻辑是不是很可怕?支付系统最怕这种资金风险。上面说的定时任务只是一方面,回调或并发场景下也同样会导致这个资金隐患。
借助这个案例,来强调一下,业务处理流程的先后顺序是很重要的,一定要摸索清楚。日常开发中养成严谨的好习惯,关键时刻才能彰显靠谱。
ref:§ 业务校验,注意各个校验的先后顺序。
【EOF】欢迎大家关注我的微信公众号「靠谱的程序员」,让我们一起做靠谱的程序员。