有道无术,术尚可求,有术无道,止于术。
前言
在上篇文档中,我们简单实现了Native支付下单
功能,根据订单生成二维码,用户扫码支付。
但是用户支付是不经过商家的,商家需要通过以下两种方式获取订单状态:
- 支付结果通知:用户支付成功后,微信支付会将支付成功的结果以回调通知的形式同步给商户,商户的回调地址需要在调用下单API时传入
notify_url
参数(10)。 - 主动调用:当因网络抖动或本身
notify_url
存在问题等原因,导致无法接收到回调通知时,商户也可主动调用查询订单API来获取订单状态(11)。
主动调用
在微信官方API文档可以查看接口详情。
商户订单号查询
紧接上文,在WechatPayNativeApiEnum
添加接口地址:
QUERY_BY_MERCHANT_ORDER_NO("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s?mchid=%s", "商户订单号查询订单"),
在WechatPayService
接口声明根据商户订单号查询订单方法:
/**
* 根据商户订单号查询订单
*
* @param outTradeNo 商户订单号:商户系统内部订单号,只能是数字、大小写字母_-*且在同一个商户号下唯一。特殊规则:最小字符长度为6
* @param merchantId 商户ID:直连商户的商户号
* @throws Exception 异常
*/
String selectPayOrderByOutTradeNo(String outTradeNo, String merchantId) throws Exception;
需要注意请求参数的长度和规则:
实现上述方法:
@Override
public String selectPayOrderByOutTradeNo(String outTradeNo, String merchantId) throws Exception {
// 1. 构建请求对象
String apiPath = String.format(WechatPayNativeApiEnum.QUERY_BY_MERCHANT_ORDER_NO.getAddress(), outTradeNo, merchantId);
URIBuilder uriBuilder = new URIBuilder(apiPath);
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.addHeader("Accept", "application/json");
// 2. 执行
CloseableHttpResponse response = httpClient.execute(httpGet);
// 3. 获取响应
String result = EntityUtils.toString(response.getEntity());
log.info("查询微信交易订单返回结果:" + result);
return result;
}
服务类添加查询商户订单支付状态,商户的订单还是未支付,则去查询微信订单API。
@Override
public Boolean selectPayOrderStatus(String orderId) throws Exception {
// 1. 首先查询商户自己的订单是否是已支付成功
OrderEntity order = orderService.getById(orderId);
if (OrderStatusEnum.SUCCESS.getCode() == order.getStatus()) {
return true;
}
// 2. 商户的订单还是未支付,则去查询微信API。
String response = selectPayOrderByOutTradeNo(order.getOutTradeNo(), wechatPayProperties.getMerchantId()); // 微信查询该订单状态
Map<String, String> resultMap = objectMapper.readValue(response, Map.class);
String tradeState = resultMap.get("trade_state"); // 订单状态
// 3. 已支付时,修改订单状态
if ("SUCCESS".equals(tradeState)) {
// 修改订单为已支付
order.setStatus(OrderStatusEnum.SUCCESS.getCode());
String transactionId = resultMap.get("transaction_id");
order.setTransactionId(transactionId);
orderService.updateById(order);
return true;
}
return false;
}
添加访问接口,用户下订单生成支付二维码后,前端需要开启定时器,每三秒查询一次该订单支付状态,如果返回已支付,则关闭二维码,弹出下单成功,即将发货。如果超过规定的时间还未支付,则弹出订单支付超时已关闭。
@Operation(summary = "查询商户订单是否已支付,前端轮询")
@GetMapping("/orderPayStatus")
public R<Boolean> orderPayStatus(String orderId) throws Exception {
Boolean status=wechatPayService.selectPayOrderStatus(orderId);
return R.success(status);
}
回调通知
在微信官方API文档可以查看接口详情。
用户支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理该消息,并返回应答。
对后台通知交互时,如果微信收到商户的应答不符合规范或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。
通知频率
为【15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h 】- 总计 24h4m。
集成支付通知大致分为以下几个步骤:
- 下单接口中的添加请求参数
notify_url
- 添加回调访问接口,收到微信请求后,进行验签、查询订单、修改订单支付状态、应答、错误处理等
1. 添加通知回调地址
微信调用商户,肯定需要提供一个回调地址,而且是有要求的,否则可能导致商户无法接收到微信的回调通知信息。
要求如下:
- 必须为
https
地址 - 确保回调
URL
是外部可正常访问的 - 不能携带后缀参数
开发本地的地址,微信无法访问,而且我们也没有Https
证书,所以需要使用内网穿透工具。可以使用ngrok
(本篇使用)或者花生壳。
进入ngrok官网下载页面,下载对应安装文件。
注册,获取认证令牌。
注册完成后,会收到验证邮件,点击后在跳转页面查看令牌。
双击运行,复制上面的输入命令添加令牌。
输入ngrok http 服务端口
命令,启动成功。可以在窗口中查看到当前内网地址对应的外网访问地址(重新启动会变化)。
修改之前的下单接口,将通知地址修改为外网地址(/pay/wechat/notify
为回调接口路径,后面添加)。
2. 通知处理
支付结果通知是以POST
方法访问商户设置的通知地址,通知的数据以JSON
格式通过请求主体(BODY)
传输。通知的数据包括了加密的支付结果详情。
官方SDK
提供了一个NotificationHandler
通知处理器,它可以校验参数、验签、解密请求体。我们只需要获取一些消息头,传入处理器,调用其解析处理方法即可。
public Notification notifyVerifier(HttpServletRequest request) throws ValidationException, ParseException {
// 1. 获取回调请求头中的相关信息
String nonce = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_NONCE); // 随机字符串
String timestamp = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_TIMESTAMP); // 时间戳
// 加密不能保证通知请求来自微信。微信会对发送给商户的通知进行签名,并将签名值放在通知的HTTP头Wechatpay-Signature。
// 商户应当验证签名,以确认请求来自微信,而不是其他的第三方
String signature = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_SIGNATURE); // 签名数据
String serial = request.getHeader(WechatPayHttpHeaders.WECHAT_PAY_SERIAL); // 平台证书序列号
// 2. 构建通知请求对象
NotificationRequest notificationRequest = new NotificationRequest.Builder()
.withSerialNumber(serial)
.withNonce(nonce)
.withTimestamp(timestamp)
.withSignature(signature)
.withBody(getBody(request))
.build();
// 3. 创建通知处理器,传入验签器、APIv3密钥(解密回调通知)
NotificationHandler handler = new NotificationHandler(verifier, wechatPayProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
// 4. 处理器处理请求,校验参数、验签、AES解密请求体
// 抛出ValidationException时,请先检查传入参数是否与回调通知参数一致。若一致,说明参数可能被恶意篡改导致验签失败。
// 抛出ParseException时,请先检查传入包体是否与回调通知包体一致。若一致,请检查AES密钥是否正确设置。若正确,说明包体可能被恶意篡改导致解析失败。
Notification notification = handler.parse(notificationRequest);
log.info("收到微信通知数据:" + notification.toString());
return notification;
}
/**
* 将通知参数转化为字符串
*/
private String getBody(HttpServletRequest request) {
BufferedReader br = null;
try {
StringBuilder result = new StringBuilder();
br = request.getReader();
for (String line; (line = br.readLine()) != null; ) {
if (result.length() > 0) {
result.append("\n");
}
result.append(line);
}
return result.toString();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (br != null) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
3. 通知接口
在服务类中,添加通知回调处理逻辑,根据回调参数返回的订单状态,如果回调显示已支付,则商户订单状态也修改为已支付,最后返回应答消息给微信。
@Override
public String handleNativeNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
// 1. 微信通知校验、验签、解密请求体
Notification notification = notifyVerifier(request);
// 2. 获取回调信息
String decryptData = notification.getDecryptData();
Map<String, String> resultMap = objectMapper.readValue(decryptData, HashMap.class); // json字符串转map
String outTradeNo = resultMap.get("out_trade_no"); // 商户订单
String tradeState = resultMap.get("trade_state"); // 支付状态
String transactionId = resultMap.get("transaction_id"); // 支付订单ID
// 3. 查询商户订单
OrderEntity order = orderService.getOneByOutTradeNo(outTradeNo);
// 4. 订单不是支付成功更新订单状态
if (OrderStatusEnum.SUCCESS.getCode() != order.getStatus()) {
if ("SUCCESS".equals(tradeState)) {
// 修改订单为已支付
order.setStatus(OrderStatusEnum.SUCCESS.getCode());
order.setTransactionId(transactionId);
orderService.updateById(order);
}
}
// 5. 应答
// 接收成功:HTTP应答状态码需返回200或204,无需返回应答报文。
// 接收失败:HTTP应答状态码需返回5XX或4XX
Map<String, String> map = new HashMap<>(); // 应答对象
response.setStatus(200); // 收到200后,判断成功,就不会再发了
map.put("code", "SUCCESS"); // 错误码,SUCCESS为清算机构接收成功,其他错误码为失败。
map.put("message", "成功"); // 返回信息,如非空,为错误原因。
return JSONUtil.toJsonPrettyStr(map);
}
添加回调访问接口:
@Operation(summary = "支付回调通知")
@PostMapping("/notify")
public String notify(HttpServletRequest request, HttpServletResponse response) throws Exception {
return wechatPayService.handleNativeNotify(request,response);
}
4. 测试
启动想问,断点定在handleNativeNotify
方法中,访问微信Native下单
接口,生成二维码,在库中生成了一条未支付的订单。
扫描二维码进行支付,马上就进入了断点,首先在请求头中获取到了很多信息。
经过通知处理器处理后的数据:
获取到了支付成功的回调参数:
最终订单状态修改为了已支付,且记录了微信支付订单号。