|
| 1 | +# 00-如何防止订单二次重复支付? |
| 2 | + |
| 3 | +## 1 背景 |
| 4 | + |
| 5 | +用户第一次点击下单操作时,会弹出支付页面待支付。但可能存在用户在支付时发现账户金额不够,后续选择: |
| 6 | + |
| 7 | +- 其他渠道支付(如微信支付转为支付宝支付) |
| 8 | +- 或采用不同终端来支付(如由电脑端支付转为app端支付) |
| 9 | + |
| 10 | +这时就面临二次支付场景。 |
| 11 | + |
| 12 | +## 2 方案1 |
| 13 | + |
| 14 | +由于用户支付的时候的支付页面是html文件或是一个支付二维码,可将支付页面先存储一份在数据库中,用户二次支付时通过查询数据库来重新返回用户原来的支付页面。 |
| 15 | + |
| 16 | +### 2.1 缺点 |
| 17 | + |
| 18 | +需注意支付页面是否过期,若支付页面过期,需二次调用第三方支付 |
| 19 | + |
| 20 | +- 后台需维护用户第一次调用时的支付页面,增加开发成本 |
| 21 | + |
| 22 | +- 需要注意幂等性,即能唯一标识用户的多次请求 |
| 23 | + |
| 24 | +### 2.2 优点 |
| 25 | + |
| 26 | +规定时间内,不论用户多少次调用,后台只需要调用一次第三方支付。 |
| 27 | + |
| 28 | +### 2.3 流程图 |
| 29 | + |
| 30 | + |
| 31 | + |
| 32 | + |
| 33 | + |
| 34 | +## 3 方案2 |
| 35 | + |
| 36 | +用户第二次支付的时,继续调用第三方支付,让第三方根据是否超时等情况判断是: |
| 37 | + |
| 38 | +- 返回原来的支付页面 |
| 39 | +- or生成一个新的支付页面返回 |
| 40 | + |
| 41 | +### 3.1 优点 |
| 42 | + |
| 43 | +便于实现,减轻自己后台下单的维护成本。【推荐】 |
| 44 | + |
| 45 | + |
| 46 | + |
| 47 | +用户二次支付时,订单微服务中存储了用户第一次下单支付的基本信息。因此第二次支付时,可通过查询第一次支付的一些基本信息来调用第三方支付。这样设计可告诉第三方支付平台这是一个订单,尤其是该订单的【剩余过期时间】。 |
| 48 | + |
| 49 | +### 剩余过期时间 |
| 50 | + |
| 51 | +后台调用第三方支付,第三方支付从收到请求信息->处理请求信息->响应请求信息是存在一定的时延的,因此一定不能死死卡住过期时间来调用第三方支付。需要预留一些时间给第三方支付处理。如支付过期时间是30分钟,当用户二次支付到达我们下单服务的时候是29分钟那么就拒绝支付。 |
| 52 | + |
| 53 | +### 用户超时支付的拒绝策略 |
| 54 | + |
| 55 | +#### 策略一 |
| 56 | + |
| 57 | +前端显示订单30分钟内需要支付,后端中对第三方支付实际上是31分钟内不能支付 【预留时间给后端和第三方支付交互】 |
| 58 | + |
| 59 | +#### 策略二 |
| 60 | + |
| 61 | +前端显示订单30分钟内需要支付,后端对第三方的支付实际上是当用户支付请求在地29分钟到后端就不给支付了。 |
| 62 | + |
| 63 | + |
| 64 | + |
| 65 | +```java |
| 66 | +@Override |
| 67 | +@Transactional |
| 68 | +public JsonData repay(RepayOrderRequest repayOrderRequest) { |
| 69 | + LoginUser loginUser = LoginInterceptor.threadLocal.get(); |
| 70 | + // 根据订单流水号查询第一次支付的订单信息 |
| 71 | + ProductOrderDO productOrderDO = productOrderMapper.selectOne(new QueryWrapper<ProductOrderDO>().eq("out_trade_no",repayOrderRequest.getOutTradeNo()).eq("user_id",loginUser.getId())); |
| 72 | + |
| 73 | + log.info("订单状态:{}",productOrderDO); |
| 74 | + |
| 75 | + if(productOrderDO==null){ |
| 76 | + return JsonData.buildResult(BizCodeEnum.PAY_ORDER_NOT_EXIST); |
| 77 | + } |
| 78 | + |
| 79 | + // 订单状态不对,不是NEW状态 |
| 80 | + if(!productOrderDO.getState().equalsIgnoreCase(ProductOrderStateEnum.NEW.name())){ |
| 81 | + return JsonData.buildResult(BizCodeEnum.PAY_ORDER_STATE_ERROR); |
| 82 | + }else { |
| 83 | + //订单创建到现在的存活时间 |
| 84 | + long orderLiveTime = CommonUtil.getCurrentTimestamp() - productOrderDO.getCreateTime().getTime(); |
| 85 | + //创建订单是临界点在预留一分钟时间,比如订单实际已经存活了28分钟了,我们就对外说订单已经存活了29分钟。 |
| 86 | + orderLiveTime = orderLiveTime + 60*1000; |
| 87 | + |
| 88 | + //大于订单超时时间,则失效 |
| 89 | + if(orderLiveTime>TimeConstant.ORDER_PAY_TIMEOUT_MILLS){ |
| 90 | + return JsonData.buildResult(BizCodeEnum.PAY_ORDER_PAY_TIMEOUT); |
| 91 | + }else { |
| 92 | + |
| 93 | + //记得更新DB订单支付参数 payType,还可以增加订单支付信息日志 TODO |
| 94 | + //总时间-存活的时间 = 剩下的有效时间 |
| 95 | + long timeout = TimeConstant.ORDER_PAY_TIMEOUT_MILLS - orderLiveTime; |
| 96 | + //创建支付 |
| 97 | + PayInfoVO payInfoVO = new PayInfoVO(productOrderDO.getOutTradeNo(), |
| 98 | + productOrderDO.getPayAmount(),repayOrderRequest.getPayType(), |
| 99 | + repayOrderRequest.getClientType(), productOrderDO.getOutTradeNo(),"",timeout); |
| 100 | + |
| 101 | + log.info("payInfoVO={}",payInfoVO); |
| 102 | + //调用第三方支付 |
| 103 | + String payResult = payFactory.pay(payInfoVO); |
| 104 | + if(StringUtils.isNotBlank(payResult)){ |
| 105 | + log.info("创建二次支付订单成功:payInfoVO={},payResult={}",payInfoVO,payResult); |
| 106 | + return JsonData.buildSuccess(payResult); |
| 107 | + }else { |
| 108 | + log.error("创建二次支付订单失败:payInfoVO={},payResult={}",payInfoVO,payResult); |
| 109 | + return JsonData.buildResult(BizCodeEnum.PAY_ORDER_FAIL); |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | +} |
| 114 | +``` |
0 commit comments