目录
- Native 支付
- 处理超时订单
- 定时的讲解
- 需求分析
- 代码
- 定时任务:WxPayTask
- 定时查询的方法:
- 核实订单状态等操作 :WxPayServiceImpl
- 查单接口方法:queryOrder
- 更新本地订单状态:updateStatusByOrderNo
- 记录支付日志:createPaymentInfo
- 关闭订单接口:closeOrder
- 测试:
- 创建测试环境:
- 期望结果:
- 实际结果:成功
- 完整代码:
- WxPayTask
- getNoPayOrderByDuration 方法
- checkOrderStatus 方法
- queryOrder 方法
- updateStatusByOrderNo 方法
- PaymentInfoServiceImpl 方法
- closeOrder 方法
Native 支付
处理超时订单
处理超时订单(定时查询、核实微信支付平台的订单、调用微信支付平台查单接口、更新本地订单状态、记录支付日志)
定时的讲解
【* * * * * * 】 每一秒执行一次
【0/3 * * * * * 】 从第0秒开始,每隔3秒执行一次
【1-3 * * * * * 】 从第1秒开始执行,到第3秒结束执行
【1,2,3 * * * * * 】 在指定的第1,2,3秒执行
需求分析
每隔30秒执行一次定时查询方法,先在【本地数据库】查询创建超过5分钟,并且未支付的订单。
然后再根据商品订单号调用【微信支付端】的【查单接口】进行查询,核实订单状态
如果订单在微信支付端那边已支付,则更新商户端(就是本地数据库)订单状态为已支付(本地数据库修改商户端的订单状态)
如果订单在微信支付端那边未支付,则调用微信支付平台的关单接口关闭订单,并更新商户端订单状态(本地数据库修改商户端的订单状态)
作用:把那些超时未支付的订单给关了
注意:调用微信支付端的【商户订单号查询订单】的接口,查询出来的数据是明文的,不要老是想着查出来的都是密文。
而且该接口查询出来的数据,和微信支付平台自动发给商户端的支付通知里面携带的通知参数的 resource 属性里面的 ciphertext 这个密文数据解密出来后的数据是一样的。
代码
定时任务:WxPayTask
这里的分钟是1,只是为了方便测试
定时查询的方法:
核实订单状态等操作 :WxPayServiceImpl
根据订单号查询微信支付查单接口,核实订单状态
queryOrder 查单接口方法,查出来的数据是明文的。
查单接口方法:queryOrder
这个是调用微信支付端的【商户订单号查询订单】的接口,查询出来的数据是明文的,不要老是想着查出来的都是密文。
商户订单号查询订单
更新本地订单状态:updateStatusByOrderNo
记录支付日志:createPaymentInfo
因为queryOrder 查单接口方法,查出来的数据是明文的。跟支付通知的 resource 里面的 **ciphertext(密文)**进行解密后的数据是一样的,所以也可以作为参数传给这个方法。
支付通知
关闭订单接口:closeOrder
关闭订单
测试:
创建测试环境:
先创建一个测试环境:
Java课程:点击了【确认支付】,弹出了支付二维码后就直接关掉了,没有进行支付,所以只添加了一条未支付的订单。
大数据课程:扫码支付了,但是我把ngrok内网穿透关掉了,那么隧道就失效了,微信支付平台发送的支付通知,商户端这边也就接收不到了,所以虽然微信支付平台那边,这个订单已经支付了,但商户端本地这边,因没有接收到支付通知,所以这个订单也是未支付的状态。
内网穿透地址注释掉用于演示商户端接收不到微信支付平台发来的支付通知,从而无法修改订单支付状态的情况。
演示环境创建好了,现在启动处理超时订单的方法。
查出创建订单超过1分钟且未支付的订单,然后到微信支付平台调用查询订单的接口,核实这个超时的订单是否真的没支付。
如果在微信支付平台那边已经支付了,那么获取该接口返回的结果里面的支付状态,修改到本地数据库的订单状态里面。
如果没有支付,直接调用微信支付端那边的关闭订单的接口,然后修改本地数据库的那条订单的支付状态为超时未支付。
期望结果:
期望结果应该是:
**Java课程:**是超时1分钟且没有支付的,所以调用定时任务后,本地数据库的该订单的支付状态应该是【超时已关闭】
**大数据库课程:**是超时1分钟,但是已经支付了,只是没收到支付通知,所以调用定时任务后,本地数据库的该订单的支付状态应该【支付成功】,且为该订单生成一条【支付日志记录】
实际结果:成功
实际上:跟预想的一样,成功。
一开始查询未支付且超时的订单:
Java课程的:
大数据课程的:
成功。
完整代码:
WxPayTask
@Slf4j
@Component //组件,项目启动的时候就会加载这个组件类
public class WxPayTask
{@Resourceprivate OrderInfoService orderInfoService;@Resourceprivate WxPayService wxPayService;//从0秒开始,每隔30秒执行一次这个方法,查询创建超过5分钟,并且未支付的订单@Scheduled(cron = "0/30 * * * * *")public void orderConfirm() throws Exception{//从0秒开始,每隔30秒执行一次这个方法,查询创建超过5分钟,并且未支付的订单List<OrderInfo> orderInfoList = orderInfoService.getNoPayOrderByDuration(1);for (OrderInfo orderInfo : orderInfoList){String orderNo = orderInfo.getOrderNo();log.warn("超时订单 ===> {}" , orderNo);//核实订单状态:调用微信支付查单接口wxPayService.checkOrderStatus(orderNo);}}
}
getNoPayOrderByDuration 方法
/*** 从0秒开始,每隔30秒执行一次这个方法,查询创建超过5分钟,并且未支付的订单* @param minutes 5 分钟* @return 未支付的订单集合*/@Overridepublic List<OrderInfo> getNoPayOrderByDuration(int minutes){// 使用Java的 Instant 类和 Duration 类来计算一个时间点。// 获取当前时间,并减去指定的分钟数。Instant instant = Instant.now().minus(Duration.ofMinutes(minutes));//QueryWrapper 是 MyBatis-Plus 提供的一个用于构建查询条件的工具类QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();//构建查询条件//条件1:订单的状态为未支付queryWrapper.eq("order_status",OrderStatus.NOTPAY.getType());//条件2:该订单创建时间超过5分钟--le 是“less than or equal to”的缩写:小于或等于queryWrapper.le("create_time",instant);List<OrderInfo> orderInfoList = orderInfoMapper.selectList(queryWrapper);return orderInfoList;}
}
checkOrderStatus 方法
/*** 根据订单号查询微信支付查单接口,核实订单状态* 如果订单已支付,则更新商户端订单状态* 如果订单未支付,则调用微信支付平台的关单接口关闭订单,并更新商户端订单状态* @param orderNo 订单id*/@Overridepublic void checkOrderStatus(String orderNo) throws Exception{log.warn("根据订单号核实订单状态 ===> {}", orderNo);//调用微信支付的查单接口---这个方法查出来的是明文String result = this.queryOrder(orderNo);System.err.println("订单号:"+orderNo+",调用微信支付查单接口:" + result);Gson gson = new Gson();//将结果转成map类型Map resultMap = gson.fromJson(result, HashMap.class);//获取微信支付端的订单的状态Object tradeState = resultMap.get("trade_state");//判断订单状态if (WxTradeState.SUCCESS.getType().equals(tradeState)){log.warn("核定该订单已经支付 ===> {}", orderNo);//如果确认该订单已支付,则更新本地订单状态orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.SUCCESS);//记录支付日志paymentInfoService.createPaymentInfo(result);}if (WxTradeState.NOTPAY.getType().equals(tradeState)){log.warn("核实订单未支付 ===> {}", orderNo);//如果订单未支付,则调用关单接口this.closeOrder(orderNo);//更新本地订单状态orderInfoService.updateStatusByOrderNo(orderNo,OrderStatus.CLOSED);}}
queryOrder 方法
/*** 根据商品订单号查询订单* @param orderNo 商品订单号* @return 订单** 注意:这个查单接口查出来的数据是明文不是密文,不要想成是密文* 而且查出来的数据 跟支付通知里面的通知参数的密文ciphertext解密出来的数据是一样的**/@Overridepublic String queryOrder(String orderNo) throws Exception{log.info("查单接口调用 ====> {}", orderNo);//组装url---主机地址 + 访问接口地址String url = String.format(WxApiType.ORDER_QUERY_BY_NO.getType(), orderNo);//还要拼接上商户号url = wxPayConfig.getDomain().concat(url).concat("?mchid=").concat(wxPayConfig.getMchId());//创建远程请求对象HttpGet httpGet = new HttpGet(url);//get请求只需要设置请求头就可以了--作用:希望接收json类型的响应httpGet.setHeader("Accept","application/json");// 完成签名并执行请求CloseableHttpResponse response = wxPayClient.execute(httpGet);try{//字符串形式的响应体String bodyAsString = EntityUtils.toString(response.getEntity());//响应状态码int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200){ //处理成功System.out.println("成功, 返回结果 = " + bodyAsString);} else if (statusCode == 204){ //处理成功,无返回BodySystem.out.println("成功");} else{System.out.println("下单失败, 响应码 = " + statusCode + ", 返回结果 = " + bodyAsString);throw new IOException("请求失败 request failed");}return bodyAsString;} finally{response.close();}}
updateStatusByOrderNo 方法
/*** 根据订单号更新订单状态* @param orderNo 商品订单号* @param orderStatus 订单状态*/
@Override
public void updateStatusByOrderNo(String orderNo, OrderStatus orderStatus)
{log.info("修改订单状态为: ===> {}" , orderStatus);//QueryWrapper 是 MyBatis-Plus 提供的一个用于构建查询条件的工具类QueryWrapper<OrderInfo> queryWrapper = new QueryWrapper<>();//查询条件---相当于 update t_order_info set xxx = xxx where order_no = orderNoqueryWrapper.eq("order_no",orderNo);//要修改的字段,存到这个 orderInfo 对象里面OrderInfo orderInfo = new OrderInfo();orderInfo.setOrderStatus(orderStatus.getType());//调用sqlorderInfoMapper.update(orderInfo,queryWrapper);}
PaymentInfoServiceImpl 方法
@Resource
private PaymentInfoMapper paymentInfoMapper;/*** 记录支付日志* @param plainText 解密后的参数明文*/
@Override
public void createPaymentInfo(String plainText)
{log.info("记录支付日志");Gson gson = new Gson();// 将字符串的plainText转成 hashMap 对象Map plainTextMap = gson.fromJson(plainText, HashMap.class);// 创建一个记录支付日志的对象PaymentInfo paymentInfo = new PaymentInfo();// 从 plainTextMap 对象中获取要存到记录支付日志(PaymentInfo)的 属性字段// 商品订单编号String orderNo = (String) plainTextMap.get("out_trade_no");// 支付系统交易编号String transactionId = (String)plainTextMap.get("transaction_id");// 交易类型-- NATIVE:扫码支付String tradeType = (String)plainTextMap.get("trade_type");// 交易状态--SUCCESS:支付成功String tradeState = (String)plainTextMap.get("trade_state");// 用户支付金额,单位为分 amount.payer_totalMap<String, Object> amount = (Map)plainTextMap.get("amount");// 官网指定返回的金额是int类型,但是直接把object转成int会报错// 弄个中转站:隐式类型转换(小转大)将int类型转换成Double,// 然后再用intValue 把double类型的值转成Integer整形int payerTotal = ((Double) amount.get("payer_total")).intValue();paymentInfo.setOrderNo(orderNo); //商品订单编号paymentInfo.setTransactionId(transactionId);//支付系统交易编号paymentInfo.setTradeType(tradeType);//交易类型paymentInfo.setTradeState(tradeState);//交易状态paymentInfo.setPayerTotal(payerTotal);//用户支付金额,单位为分paymentInfo.setPaymentType(PayType.WXPAY.getType()); //支付类型// 通知参数 -- 因为可能会有各种各样的参数,所以直接把整个参数存到一个字段里面,// 方便后续遇到问题可以查看paymentInfo.setContent(plainText);//插入一个订单支付日志记录paymentInfoMapper.insert(paymentInfo);
}
closeOrder 方法
/*** 关单接口* @param orderNo 订单编号*/
private void closeOrder(String orderNo) throws IOException
{log.info("关单接口的调用,订单编号 ===> {}", orderNo);// 构建url地址:// 关单接口的url地址--/v3/pay/transactions/out-trade-no/{out_trade_no}/close,// {out_trade_no}这个是个占位符,用format方法,把orderNo传进去String url = String.format(WxApiType.CLOSE_ORDER_BY_NO.getType(), orderNo);//微信端的主机地址: 【主域名】https://api.mch.weixin.qq.comurl = wxPayConfig.getDomain().concat(url);// 创建远程请求对象HttpPost httpPost = new HttpPost(url);// 组装json请求体Gson gson = new Gson();HashMap<String, String> paramsMap = new HashMap<>();paramsMap.put("mchid", wxPayConfig.getMchId()); // 直连商户号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{// 响应状态码int statusCode = response.getStatusLine().getStatusCode();if (statusCode == 200){ // 处理成功log.info("成功");} else if (statusCode == 204){ // 处理成功,无返回Bodylog.info("成功");} else{log.info("关闭订单API失败, 响应码 = " + statusCode + "");throw new IOException("请求失败 request failed");}} finally{response.close();}
}