关于自己开发微信支付下单接口的几点关于严谨性方面的疑问?

67 阅读6分钟

我终于开始鼓起勇气写支付下单接口,一直不敢触碰,因为这个接口算的上是整个系统里面,最复杂最需要严谨的那个,毕竟和金钱相关,没有足够的知识准备,没有能力去写,写出来也是经不起高并发、异常的考验的,可能平时状态下没什么问题罢了,我是对自己这样要求的。

所以我不断的去找资源学习,发现每个老师都有自己的处理方案,其实我一直想寻求一套统一的公式。废话不多说,进入正文。

Native支付举例:

  1. 下单接口第一步校验幂等性,防止重复下单(这一步可以用lua脚本或者其他token机制,我只知道lua脚本搭配redis)。
  2. 第二步校验通过,创建订单数据(根据前端传过来的商品id去数据库查询对应的金额、商品描述、利用雪花算法生成订单号、订单状态设置为‘待支付’、设置回调通知地址等等)。
  3. 第三步请求官方下单接口,如果请求成功则返回二维码,至此下单这一步完成。
  4. 接下来是notify_url回调的处理,这一步给的感觉就是很严谨,比下单还严谨,我说一下。首先获取返回的响应体,解析,验签,校验金额。验签失败则抛出异常,否则继续,接下来响应码不是Success则抛出异常,否则继续。好,都无误,先加锁, 然后查询订单状态是否被处理,没有改为‘已支付’则修改订单状态(先查一下,再做处理其实是考虑到了微信可能会发多次通知),然后响应给微信SUCCESS字符串或者XML。

至此!一套支付完成。当然,后续还要看情况关单。

接下来,我提几点疑问,其实上面所说的,也只不过是我照着老师的代码翻译的意思罢了(老师的代码在最后)。

  1. 幂等性,为什么要幂等性,为什么防止重复请求支付接口,难道说我手速极快,一下子点支付按钮点了两次或者更多?难道说我手机卡顿,我还是点按钮点了好几次?当我点击第一次的时候,前端ui已经将按钮置为disable或者弹出提示性的遮罩mask,我又怎么能点击多次呢?难道说防止有人不走页面,走postman,不过那样,也是有先后的呀?
  2. 雪花算法,为什么不用uuid,难道是怕高并发的时候,即使uuid也会重复,我和你同时下单,uuid重复了,你和我之间,只有一人会下单成功?
  3. 校验金额,为什么要校验金额,已经验过签了还怕有人心怀不轨篡改响应,形成‘假通知’,签名你都不信了?
  4. 加锁以及校验订单状态保证幂等性,微信说回调需要加锁,避免函数重入,函数就是方法吧,应该就是notify_url地址对应的那个方法被调用多次吧,为什么会出现此场景?这是一个问题,其次如果函数重入,如果我后续只是修改订单状态从‘未支付’到‘已支付’,那即使重入几次也没关系吧,你永远都是更新为‘已支付’,多几次数据库的操作,结果也还是一样。但是如果我后续还需要加订单日志到数据库,那是不是就需要考虑函数重入,避免写多条日志?其实我一直想不通为什么会多次调用,官方文档说

0.png

0 (1).png

这又不是并发级别的,第一次15s,第二次15s...真的搞不懂,难道还有其他情况会重复通知,而且还是并发级别的?

  1. 先本地生成订单、调用下单接口还是先调用下单接口,再生成本地订单,听有些博客说,如果是后者,那么如果数据库写入慢一点的话,但是notify已经回来了,然后查看订单是否支付的时候,会查询不到,你可以想象一下这个过程,是否存在这种可能?
  2. 关单这种操作是一定有必要的吗,如果不是为了释放库存,是否有必要关单。我再重新生成一个订单号而不关闭之前的订单会怎样?

好了,差不多就这么多了,对了还有个JsApi的支付,听说即使下单之后回调的结果是‘requestPayment:ok’也可能会失败,严谨的是再查单一次,是吗?

提了很多问题, 这些问题都是日思夜想的问题,无奈身边没有大神,希望有懂的兄弟可以回答一下,感激不尽,谢谢!

附上,学习尚硅谷微信支付的代码,说白了也是这些代码所产生的疑问:

/**
 * Native下单
 * @param productId
 * @return
 * @throws Exception
 */
@ApiOperation("调用统一下单API,生成支付二维码")
@PostMapping("/native/{productId}")
public R nativePay(@PathVariable Long productId) throws Exception {

    log.info("发起支付请求 v3");

    //返回支付二维码连接和订单号
    Map<String, Object> map = wxPayService.nativePay(productId);

    return R.ok().setData(map);
}


/**
 * 创建订单,调用Native支付接口
 * @param productId
 * @return code_url 和 订单号
 * @throws Exception
 */
@Transactional(rollbackFor = Exception.class)
@Override
public Map<String, Object> nativePay(Long productId) throws Exception {

    log.info("生成订单");

    //生成订单
    OrderInfo orderInfo = orderInfoService.createOrderByProductId(productId, PayType.WXPAY.getType());
    String codeUrl = orderInfo.getCodeUrl();
    if(orderInfo != null && !StringUtils.isEmpty(codeUrl)){
        log.info("订单已存在,二维码已保存");
        //返回二维码
        Map<String, Object> map = new HashMap<>();
        map.put("codeUrl", codeUrl);
        map.put("orderNo", orderInfo.getOrderNo());
        return map;
    }


    log.info("调用统一下单API");

    //调用统一下单API
    HttpPost httpPost = new HttpPost(wxPayConfig.getDomain().concat(WxApiType.NATIVE_PAY.getType()));

    // 请求body参数
    Gson gson = new Gson();
    Map paramsMap = new HashMap();
    paramsMap.put("appid", wxPayConfig.getAppid());
    paramsMap.put("mchid", wxPayConfig.getMchId());
    paramsMap.put("description", orderInfo.getTitle());
    paramsMap.put("out_trade_no", orderInfo.getOrderNo());
    paramsMap.put("notify_url", wxPayConfig.getNotifyDomain().concat(WxNotifyType.NATIVE_NOTIFY.getType()));

    Map amountMap = new HashMap();
    amountMap.put("total", orderInfo.getTotalFee());
    amountMap.put("currency", "CNY");

    paramsMap.put("amount", amountMap);

    //将参数转换成json字符串
    String jsonParams = gson.toJson(paramsMap);
    log.info("请求参数 ===> {}" + jsonParams);

    StringEntity entity = new StringEntity(jsonParams,"utf-8");
    entity.setContentType("application/json");
    httpPost.setEntity(entity);
    httpPost.setHeader("Accept", "application/json");

    //完成签名并执行请求
    CloseableHttpResponse response = wxPayClient.execute(httpPost);

    try {
        String bodyAsString = EntityUtils.toString(response.getEntity());//响应体
        int statusCode = response.getStatusLine().getStatusCode();//响应状态码
        if (statusCode == 200) { //处理成功
            log.info("成功, 返回结果 = " + bodyAsString);
        } else if (statusCode == 204) { //处理成功,无返回Body
            log.info("成功");
        } else {
            log.info("Native下单失败,响应码 = " + statusCode+ ",返回结果 = " + bodyAsString);
            throw new IOException("request failed");
        }

        //响应结果
        Map<String, String> resultMap = gson.fromJson(bodyAsString, HashMap.class);
        //二维码
        codeUrl = resultMap.get("code_url");

        //保存二维码
        String orderNo = orderInfo.getOrderNo();
        orderInfoService.saveCodeUrl(orderNo, codeUrl);

        //返回二维码
        Map<String, Object> map = new HashMap<>();
        map.put("codeUrl", codeUrl);
        map.put("orderNo", orderInfo.getOrderNo());

        return map;

    } finally {
        response.close();
    }
}

接下来是回调接口

/**
 * 支付通知
 * 微信支付通过支付通知接口将用户支付成功消息通知给商户
 */
@PostMapping("/native/notify")
public String wxNotify(HttpServletRequest request) throws Exception {

    System.out.println("微信发送的回调");
    Map<String, String> returnMap = new HashMap<>();//应答对象

    //处理通知参数
    String body = HttpUtils.readData(request);

    //验签
    if(!WXPayUtil.isSignatureValid(body, wxPayConfig.getPartnerKey())) {
        log.error("通知验签失败");
        //失败应答
        returnMap.put("return_code", "FAIL");
        returnMap.put("return_msg", "验签失败");
        String returnXml = WXPayUtil.mapToXml(returnMap);
        return returnXml;
    }

    //解析xml数据
    Map<String, String> notifyMap = WXPayUtil.xmlToMap(body);
    //判断通信和业务是否成功
    if(!"SUCCESS".equals(notifyMap.get("return_code")) || !"SUCCESS".equals(notifyMap.get("result_code"))) {
        log.error("失败");
        //失败应答
        returnMap.put("return_code", "FAIL");
        returnMap.put("return_msg", "失败");
        String returnXml = WXPayUtil.mapToXml(returnMap);
        return returnXml;
    }

    //获取商户订单号
    String orderNo = notifyMap.get("out_trade_no");
    OrderInfo orderInfo = orderInfoService.getOrderByOrderNo(orderNo);
    //并校验返回的订单金额是否与商户侧的订单金额一致
    if (orderInfo != null && orderInfo.getTotalFee() != Long.parseLong(notifyMap.get("total_fee"))) {
        log.error("金额校验失败");
        //失败应答
        returnMap.put("return_code", "FAIL");
        returnMap.put("return_msg", "金额校验失败");
        String returnXml = WXPayUtil.mapToXml(returnMap);
        return returnXml;
    }

    //处理订单
    if(lock.tryLock()){
        try {
            //处理重复的通知
            //接口调用的幂等性:无论接口被调用多少次,产生的结果是一致的。
            String orderStatus = orderInfoService.getOrderStatus(orderNo);
            if(OrderStatus.NOTPAY.getType().equals(orderStatus)){
                //更新订单状态
                orderInfoService.updateStatusByOrderNo(orderNo, OrderStatus.SUCCESS);

                //记录支付日志
                paymentInfoService.createPaymentInfo(body);
            }
        } finally {
            //要主动释放锁
            lock.unlock();
        }
    }

    returnMap.put("return_code", "SUCCESS");
    returnMap.put("return_msg", "OK");
    String returnXml = WXPayUtil.mapToXml(returnMap);
    log.info("支付成功,已应答");
    return returnXml;
}