处理超时订单(定时查询、核实微信支付平台的订单、调用微信支付平台查单接口、更新本地订单状态、记录支付日志)
【* * * * * * 】 每一秒执行一次
【0/3 * * * * * 】 从第0秒开始,每隔3秒执行一次
【1-3 * * * * * 】 从第1秒开始执行,到第3秒结束执行
【1,2,3 * * * * * 】 在指定的第1,2,3秒执行
每隔30秒执行一次定时查询方法,先在【本地数据库】查询创建超过5分钟,并且未支付的订单。
然后再根据商品订单号调用【微信支付端】的【查单接口】进行查询,核实订单状态
如果订单在微信支付端那边已支付,则更新商户端(就是本地数据库)订单状态为已支付(本地数据库修改商户端的订单状态)
如果订单在微信支付端那边未支付,则调用微信支付平台的关单接口关闭订单,并更新商户端订单状态(本地数据库修改商户端的订单状态)
作用:把那些超时未支付的订单给关了
注意:调用微信支付端的【商户订单号查询订单】的接口,查询出来的数据是明文的,不要老是想着查出来的都是密文。
而且该接口查询出来的数据,和微信支付平台自动发给商户端的支付通知里面携带的通知参数的 resource 属性里面的 ciphertext 这个密文数据解密出来后的数据是一样的。
这里的分钟是1,只是为了方便测试
根据订单号查询微信支付查单接口,核实订单状态
queryOrder 查单接口方法,查出来的数据是明文的。
这个是调用微信支付端的【商户订单号查询订单】的接口,查询出来的数据是明文的,不要老是想着查出来的都是密文。
商户订单号查询订单
因为queryOrder 查单接口方法,查出来的数据是明文的。跟支付通知的 resource 里面的 **ciphertext(密文)**进行解密后的数据是一样的,所以也可以作为参数传给这个方法。
支付通知
关闭订单
先创建一个测试环境:
Java课程:点击了【确认支付】,弹出了支付二维码后就直接关掉了,没有进行支付,所以只添加了一条未支付的订单。
大数据课程:扫码支付了,但是我把ngrok内网穿透关掉了,那么隧道就失效了,微信支付平台发送的支付通知,商户端这边也就接收不到了,所以虽然微信支付平台那边,这个订单已经支付了,但商户端本地这边,因没有接收到支付通知,所以这个订单也是未支付的状态。
内网穿透地址注释掉用于演示商户端接收不到微信支付平台发来的支付通知,从而无法修改订单支付状态的情况。
演示环境创建好了,现在启动处理超时订单的方法。
查出创建订单超过1分钟且未支付的订单,然后到微信支付平台调用查询订单的接口,核实这个超时的订单是否真的没支付。
如果在微信支付平台那边已经支付了,那么获取该接口返回的结果里面的支付状态,修改到本地数据库的订单状态里面。
如果没有支付,直接调用微信支付端那边的关闭订单的接口,然后修改本地数据库的那条订单的支付状态为超时未支付。
期望结果应该是:
**Java课程:**是超时1分钟且没有支付的,所以调用定时任务后,本地数据库的该订单的支付状态应该是【超时已关闭】
**大数据库课程:**是超时1分钟,但是已经支付了,只是没收到支付通知,所以调用定时任务后,本地数据库的该订单的支付状态应该【支付成功】,且为该订单生成一条【支付日志记录】
实际上:跟预想的一样,成功。
一开始查询未支付且超时的订单:
大数据课程的:
成功。
@Slf4j
@Component //组件,项目启动的时候就会加载这个组件类
public class WxPayTask
{
@Resource
private OrderInfoService orderInfoService;
@Resource
private 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);
}
}
}
/**
* 从0秒开始,每隔30秒执行一次这个方法,查询创建超过5分钟,并且未支付的订单
* @param minutes 5 分钟
* @return 未支付的订单集合
*/
@Override
public 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;
}
}
/**
* 根据订单号查询微信支付查单接口,核实订单状态
* 如果订单已支付,则更新商户端订单状态
* 如果订单未支付,则调用微信支付平台的关单接口关闭订单,并更新商户端订单状态
* @param orderNo 订单id
*/
@Override
public 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);
}
}
/**
* 根据商品订单号查询订单
* @param orderNo 商品订单号
* @return 订单
*
* 注意:这个查单接口查出来的数据是明文不是密文,不要想成是密文
* 而且查出来的数据 跟支付通知里面的通知参数的密文ciphertext解密出来的数据是一样的
*
*/
@Override
public 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)
{ //处理成功,无返回Body
System.out.println("成功");
} else
{
System.out.println("下单失败, 响应码 = " + statusCode + ", 返回结果 = " + bodyAsString);
throw new IOException("请求失败 request failed");
}
return bodyAsString;
} finally
{
response.close();
}
}
/**
* 根据订单号更新订单状态
* @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 = orderNo
queryWrapper.eq("order_no",orderNo);
//要修改的字段,存到这个 orderInfo 对象里面
OrderInfo orderInfo = new OrderInfo();
orderInfo.setOrderStatus(orderStatus.getType());
//调用sql
orderInfoMapper.update(orderInfo,queryWrapper);
}
@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_total
Map<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);
}
/**
* 关单接口
* @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.com
url = 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)
{ // 处理成功,无返回Body
log.info("成功");
} else
{
log.info("关闭订单API失败, 响应码 = " + statusCode + "");
throw new IOException("请求失败 request failed");
}
} finally
{
response.close();
}
}