我终于开始鼓起勇气写支付下单接口,一直不敢触碰,因为这个接口算的上是整个系统里面,最复杂最需要严谨的那个,毕竟和金钱相关,没有足够的知识准备,没有能力去写,写出来也是经不起高并发、异常的考验的,可能平时状态下没什么问题罢了,我是对自己这样要求的。
所以我不断的去找资源学习,发现每个老师都有自己的处理方案,其实我一直想寻求一套统一的公式。废话不多说,进入正文。
Native支付举例:
- 下单接口第一步校验幂等性,防止重复下单(这一步可以用lua脚本或者其他token机制,我只知道lua脚本搭配redis)。
- 第二步校验通过,创建订单数据(根据前端传过来的商品id去数据库查询对应的金额、商品描述、利用雪花算法生成订单号、订单状态设置为‘待支付’、设置回调通知地址等等)。
- 第三步请求官方下单接口,如果请求成功则返回二维码,至此下单这一步完成。
- 接下来是notify_url回调的处理,这一步给的感觉就是很严谨,比下单还严谨,我说一下。首先获取返回的响应体,解析,验签,校验金额。验签失败则抛出异常,否则继续,接下来响应码不是Success则抛出异常,否则继续。好,都无误,先加锁, 然后查询订单状态是否被处理,没有改为‘已支付’则修改订单状态(先查一下,再做处理其实是考虑到了微信可能会发多次通知),然后响应给微信SUCCESS字符串或者XML。
至此!一套支付完成。当然,后续还要看情况关单。
接下来,我提几点疑问,其实上面所说的,也只不过是我照着老师的代码翻译的意思罢了(老师的代码在最后)。
- 幂等性,为什么要幂等性,为什么防止重复请求支付接口,难道说我手速极快,一下子点支付按钮点了两次或者更多?难道说我手机卡顿,我还是点按钮点了好几次?当我点击第一次的时候,前端ui已经将按钮置为disable或者弹出提示性的遮罩mask,我又怎么能点击多次呢?难道说防止有人不走页面,走postman,不过那样,也是有先后的呀?
- 雪花算法,为什么不用uuid,难道是怕高并发的时候,即使uuid也会重复,我和你同时下单,uuid重复了,你和我之间,只有一人会下单成功?
- 校验金额,为什么要校验金额,已经验过签了还怕有人心怀不轨篡改响应,形成‘假通知’,签名你都不信了?
- 加锁以及校验订单状态保证幂等性,微信说回调需要加锁,避免函数重入,函数就是方法吧,应该就是notify_url地址对应的那个方法被调用多次吧,为什么会出现此场景?这是一个问题,其次如果函数重入,如果我后续只是修改订单状态从‘未支付’到‘已支付’,那即使重入几次也没关系吧,你永远都是更新为‘已支付’,多几次数据库的操作,结果也还是一样。但是如果我后续还需要加订单日志到数据库,那是不是就需要考虑函数重入,避免写多条日志?其实我一直想不通为什么会多次调用,官方文档说
这又不是并发级别的,第一次15s,第二次15s...真的搞不懂,难道还有其他情况会重复通知,而且还是并发级别的?
- 先本地生成订单、调用下单接口还是先调用下单接口,再生成本地订单,听有些博客说,如果是后者,那么如果数据库写入慢一点的话,但是notify已经回来了,然后查看订单是否支付的时候,会查询不到,你可以想象一下这个过程,是否存在这种可能?
- 关单这种操作是一定有必要的吗,如果不是为了释放库存,是否有必要关单。我再重新生成一个订单号而不关闭之前的订单会怎样?
好了,差不多就这么多了,对了还有个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;
}