有道无术,术尚可求,有术无道,止于术。
关闭订单
以下情况需要调用关单接口:
- 商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付;
- 系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口。
注意:关单没有时间限制,建议在订单生成后间隔几分钟(最短5分钟)再调用关单接口,避免出现订单状态同步不及时导致关单失败。
1. 集成微信关闭订单API
官方关闭订单API文档
枚举类WechatPayNativeApiEnum
添加关闭订单API地址:
CLOSE_MERCHANT_ORDER_NO("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/%s/close", "商户订单号关闭订单"),
声明服务接口根据商户订单号关闭订单方法。
/**
* 根据商户订单号查询订单 关闭订单
*
* @param outTradeNo 商户订单号
* @param merchantId 商户ID
*/
void closePayOrder(String outTradeNo, String merchantId) throws Exception;
集成微信关闭订单API,实现上述方法。
@Override
public void closePayOrder(String outTradeNo, String merchantId) throws Exception {
log.info("关闭订单开始,订单号:{}", outTradeNo);
// 1. 创建POST请求
String url = String.format(WechatPayNativeApiEnum.CLOSE_MERCHANT_ORDER_NO.getAddress(), outTradeNo);
HttpPost httpPost = new HttpPost(url);
// 2. 使用JSON库,构建请求参数对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectNode rootNode = objectMapper.createObjectNode();
rootNode.put("mchid", merchantId);// 直连商户号
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
// 3. 执行请求,是没有数据返回的,只有(Http状态码为204)
CloseableHttpResponse response = httpClient.execute(httpPost);
// 4. 响应解析
int statusCode = response.getStatusLine().getStatusCode();
if (statusCode == 200 || statusCode == 204) {
log.info("成功关闭订单");
} else {
// 403 OUT_TRADE_NO_USED 商户订单号重复 请核实商户订单号是否重复提交
// 404 ORDERNOTEXIST 订单不存在 请检查订单是否发起过交易
// 400 ORDER_CLOSED 订单已关闭 当前订单已关闭,请重新下单
throw new RuntimeException("关闭订单失败,状态码:%s ,需要自己根据状态码获取到错误信息。。。。" + statusCode);
}
}
2. 用户关闭订单
用户操作时,有以下情况,需要调用关闭订单接口:
- 不想要该商品了,用户点击取消订单按钮
- 后台问题,导致支付失败,再次支付时,需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付
订单服务类添加关闭订单功能。
public void closeOrder(String orderId) throws Exception {
// 1. 查询订单
OrderEntity order = orderService.getById(orderId);
if (ObjectUtil.isNull(order)){
throw new IllegalStateException("未查询到当前订单");
}
// 2. 关闭订单
closePayOrder(order.getOutTradeNo(), wechatPayProperties.getMerchantId());
}
添加访问接口。
@Operation(summary = "用户关闭订单")
@PostMapping("/closeOrder")
public R<?> closeOrder(String orderId) throws Exception {
wechatPayService.closeOrder(orderId);
return R.success();
}
订单失效或支付失败,重新支付逻辑也不复杂,这里就不具体实现了。
@Operation(summary = "订单失效或支付失败,重新支付")
@PostMapping("/repayOrder")
public R<?> repayOrder(String orderId) throws Exception {
// 1. 查询订单
// 2. 检查订单状态支付失败 或超时
// 3. 重新调用微信下单
// 4. 将原支付交易订单关闭
// 5. 支付成功,修改订单信息
wechatPayService.repayOrder(orderId);
return R.success();
}
3. 支付超时关闭订单
下单时,微信返回的二维码链接都是有有效期的,一般订单都会设置有效期,比如30分钟还未支付,订单状态变为支付超时。
一般有一下几种实现方案:
1、定时任务
每隔30秒启动一次,找出最近30分钟内创建并且未支付的订单,调用微信查单接口核实订单状态。未支付成功调用关单接口关闭订单。
开发起来比较简单,缺点:
- 对数据库的压力很大,定时任务造成人为的波峰,执行的时刻数据库的压力会陡增
- 计时不准,定时任务做不到非常精确的时间控制,比如半小时订单过期,但是定时任务很难卡准这个点
2、被动取消
- 用户留在收银台的时候,客户端倒计时+主动查询订单状态,服务端每次都去检查一下订单是否超时、剩余时间
- 用户每次进入订单相关的页面,查询订单的时候,服务端也检查一下订单是否超时
这种方式实现起来也比较简单,缺点:
- 依赖客户端,如果客户端不发起请求,订单可能永远没法过期,一直占用库存
- 可以被动取消+定时任务,通过定时任务去做兜底的操作。
3、延时队列
可以使用RocketMQ
、RabbitMQ
、Kafka
的延时消息,下单消息发送后,发送延迟消息,延迟一到,消费者接收到订单消息,去微信查询订单支付情况,查询若还是未返回支付成功状态,调用微信关闭订单。
这里使用简单的定时任务演示:
@Component
public class CloseOrderTask {
@Resource
WechatPayService wechatPayService;
@Resource
OrderService orderService;
// 半小时:0 0/30 * * * ?
@Scheduled(cron = "*/30 * * * * ?") // 30秒钟执行一次
public void test() throws Exception {
// 1. 查询未支付订单
List<OrderEntity> orderEntities = orderService.selectListNoPayOrder();
for (OrderEntity order : orderEntities) {
// 2. 微信API查询该订单
Boolean orderStatus = wechatPayService.selectPayOrderStatus(order.getGoodId());
if (orderStatus) {
// 已支付,更新订单状态
order.setStatus(OrderStatusEnum.SUCCESS.getCode());
orderService.updateById(order);
} else {
// 未支付,关闭订单
order.setStatus(OrderStatusEnum.CLOSED.getCode());
wechatPayService.closeOrder(order.getGoodId());
}
}
}
}