当你的接口在向外部付钱时,千万别这么写,否则就算是高启强都不敢用这段代码!

小编在目前所在公司参与过很多系统的研发、维护等工作。其中印象最深的那段代码,就是『 花钱 』,公司账户上的钱,你可以随便花,如果你这段代码没有防重,说实话,就算是高启强都不敢用这段代码。

我们一般在用支付功能对外付钱的时候,大多都是调用第三方支付接口或者银企支付,小编在公司参与开发过的佣金发放系统,相当于是调用了公司支付部门对内部提供的接口,支付部门是将银企接口进行了一次包装。

不是你的钱也不能一直花

就拿小编曾经参与的佣金发放系统为例(以下称为佣金系统),这个系统主要功能是定时计算每个订单需要发放多少佣金,然后通过调用支付系统,将钱实时付出去。听起来很简单吧,但是小编告诉你,涉及到钱的事,都不简单,写代码的时候,要时刻提醒自己,你刑。

小编这里就直接从支付这块讲起。下面有一段伪代码,小编公司实际的佣金系统要比这个复杂的多,这里只做曾经想到的一些基本的问题演示

@Transactional(rollbackFor = Exception.class)
public void pay(String orderNo) {
        //加锁
        lock(orderNo);
        //获取订单
        getOrder(orderNo);
        //判断佣金支付状态,是否为已发放佣金,如果已发放,直接return
        if(alreadyPay){
          return;
        }
         //生成转账ID,银行一笔转账的唯一标识
        String uuid = UUID.randomUUID()
        //计算佣金
        BigDecimal commission = calCommission(orderNo);
        //调用第三方接口进行佣金发放,一笔转账对应一个唯一的编号
        pay(commission,uuid);
        //更新佣金发放状态并绑定唯一标识
        if(paySuccess){
          updateState(orderNo,uuid);
        }
        //解锁,这里只做示例,就不写try catch finally了。
        unLock(orderno);
    }

『 这是一段最简单基础的支付业务代码,大家看这段代码有什么问题 ?』

搭眼一看,可能也没有什么大问题,但是大家不妨想一想我们支付的时候是调用的第三方系统,如果第三方系统支付成功了,我们这边等返回结果的时候,超时了。那么就会导致,下一次定时任务还会调用到这个订单,导致佣金重复发放的问题

那我们应该怎么办呢?可能很多同学都想到了,我们可以在调支付接口之前将支付唯一标识保存下来,查一下第三方系统是否有过支付成功的记录。

那好,接下来我们把代码改一下。

@Transactional(rollbackFor = Exception.class)
public void pay(String orderNo) {
        //加锁
        lock(orderNo);
        //获取订单
        getOrder(orderNo);
        //判断佣金支付状态,是否为已发放佣金,如果已发放,直接return
        if(alreadyPay){
          return;
        }
        //如果该订单的支付唯一编号不等于空,就去第三方系统查有没有支付成功过
        if(uuid != null){
           queryPayResult(uuid);
           if(paySuccess){
             updateState(orderNo);
             return;
           }
        }
         //生成转账ID,银行一笔转账的唯一编号
        String uuid = UUID.randomUUID()
        //保存唯一标识
        saveOrderUUID(orderNo,uuid);
        //计算佣金
        BigDecimal commission = calCommission(orderNo);
        //调用第三方接口进行佣金发放,一笔转账对应一个唯一的编号
        pay(commission,uuid);
        //更新佣金发放状态
        if(paySuccess){
          updateState(orderNo);
        }
        //解锁,这里只做示例,就不写try catch finally了。
        unLock(orderno);
    }

『 这样看是不是就好多了呢?』

但是你的领导正在你的后面摩拳擦掌!!!所以你又看了看代码

你再一看,突然发现两个问题,

  1. 你整个方法都在一个事物里。如果第一次调用第三方支付超时了,但其实佣金已经发放成功了,那么前面的保存唯一编号就会发生回滚操作。这一样也什么都没解决。。。。
  2. 如果调用第三方支付成功了,但是更新佣金发放状态失败了,也会导致整体代码回滚。再调用到这个订单的时候,发现没有支付唯一标识,也会重新生成,重新调用。

所以我们这里可以引入一个中间态->支付中,就相当于是我已经跑过这个订单了,但是不知道现在是否支付成功了,而我们支付佣金的定时任务,只查待支付佣金的订单。
同时呢,我们根据业务需求,也要将事物去掉(事物还是要根据业务需求去用,不能盲目用)。

//能传到这个方法里的一定是待支付订单,所以就不用判断支付状态了
public void commissionPay(String orderNo){
 	//加锁
    lock(orderNo);
    //生成转账ID,银行一笔转账的唯一编号
    String uuid = UUID.randomUUID()
    //保存唯一标识并且将佣金状态改为支付中
    saveOrderUUIDAndState(orderNo,uuid);
    //计算佣金
    BigDecimal commission = calCommission(orderNo);
    //调用第三方接口进行佣金发放,一笔转账对应一个唯一的编号
    pay(commission,uuid);
    //更新佣金发放状态为已发放
    updateState(orderNo);
    //解锁,这里只做示例,就不写try catch finally了。
    unLock(orderno);
}

此外我们还差一个定时任务,需要我们对一直处于支付中中的订单进行补偿。这个定时任务会拿支付中的订单去第三方系统查,如果是支付成功,就将其状态改为支付成功,否则改为待支付。

//能传到这个方法里的一定是支付中的订单
public void compensate(String orderNo){
	
	//获取锁,如果没有获取到,直接return
	lock(orderNo);
    //如果该订单的支付唯一编号不等于空,就去第三方系统查有没有支付成功过
    if(uuid != null){
      queryPayResult(uuid);
      if(paySuccess){
         //更新佣金发放状态
         updateState(orderNo);
         return;
      }
       //非支付成功或者没查到数据,就将状态更改为 待支付
       updateState(orderNo);
     }
	
}

到这里,我们这个引起重复支付的其中一个问题就解决了。

同学们可以思考一下。

  1. 我们是否一定要按照订单维度进行发放,如果订单数量特别大怎么优化。
  2. 是否需要对每一次调用进行记录。
  3. 有没有什么更好的方法防止重复支付。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

云下牧羊人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值