## 取消订单 ### 用户主动取消订单 我们来到`MyOrderController` ,这里就是统一用户操作订单的接口。 我们直接看一下取消订单接口核心代码: ```java @PutMapping("/cancel/{orderNumber}") @Operation(summary = "根据订单编号取消订单" , description = "根据订单编号取消订单") @Parameter(name = "orderNumber", description = "订单编号" , required = true) public ServerResponseEntity cancel(@PathVariable("orderNumber") String orderNumber) { String userId = SecurityUtils.getUser().getUserId(); Order order = orderService.getOne(new LambdaQueryWrapper().eq(Order::getOrderNumber, orderNumber).eq(Order::getUserId, userId)); if (Objects.isNull(order)) { eventPublisher.publishEvent(new SaveSeckillOrderEvent(orderNumber, userId)); eventPublisher.publishEvent(new RemoveSeckillOrderCacheEvent(orderNumber, userId)); order = orderService.getOrderByOrderNumberAndUserId(orderNumber, userId, true); } ... // 取消订单 orderService.cancelOrders(Collections.singletonList(order)); ... } ``` 可以看到取消订单前先检查了订单是否在数据库中,没有的话就从缓存中获取并入库到数据库,目前只有秒杀订单在极少数情况下会存在这种情况。 再看看`cancelOrders`里面的代码: ```java @Override @Transactional(rollbackFor = Exception.class) public void cancelOrders(List orders) { ... List allOrderItems = new ArrayList<>(); List orderNumberList = new ArrayList<>(); for (Order order : orders) { List orderItems = order.getOrderItems(); allOrderItems.addAll(orderItems); orderNumberList.add(order.getOrderNumber()); eventPublisher.publishEvent(new CancelOrderEvent(order)); } // 查询出微信支付并且为二维码支付的,失效掉对应支付二维码 invalidCode(orders); // 如果是秒杀订单也不必还原库存 if (CollectionUtil.isNotEmpty(allOrderItems) && !OrderType.SECKILL.value().equals(orders.get(0).getOrderType())) { // 判断处理关联卡券商品 List stockList = handVoucherItemOrder(orders, orderNumberList); // 解锁库存 skuStockLockService.unlockStock(orders); // 先解锁库存后在更新商品库存 if (CollectionUtils.isNotEmpty(stockList)) { stockManager.skuStock(stockList, LuaOperateEnum.SKU_SUB); } } ... } ``` 可以看到,在取消订单时,会失效掉对应的支付二维码,然后进行先解锁库存,最后更新商品库存。至于为什么不必还原秒杀订单的库存, 是因为这行会处理订单相关营销逻辑,如优惠券、团购、秒杀等等 ``` eventPublisher.publishEvent(new CancelOrderEvent(order)); ``` 处理到秒杀时: ``` @EventListener(CancelOrderEvent.class) @Order(CancelOrderOrder.SECKILL) public void seckillCancelOrderEventListener(CancelOrderEvent event) { String orderNumber = event.getOrder().getOrderNumber(); // 不是秒杀订单直接返回 if (!OrderType.SECKILL.value().equals(event.getOrder().getOrderType())) { return; } // 还原秒杀sku库存 seckillOrderService.cancelUnpayOrderByOrderNumber(orderNumber); } ``` 可以看到在秒杀事件监听时进行回退秒杀库存了,所以后续代码判断中我们不必在回退秒杀的库存了。 ### 定时任务自动取消订单 首先我们来到`OrderTask` ,这里就是统一处理数据库订单定时任务相关的类。 我们直接看一下定时任务取消订单的代码: ``` @XxlJob("cancelOrder") public void cancelOrder() { logger.info("解锁创建失败订单的库存。。。"); orderService.unLockFailOrderStock(); // 获取x分钟之前未支付的订单 SysOrderConfig sysOrderConfig = sysConfigService.getSysConfigObject(Constant.ORDER_TIME_CONFIG, SysOrderConfig.class); List orders = orderService.listUnRefundOrderAndOrderItems(OrderStatus.UNPAY.value(), null, DateUtil.offsetMinute(now, -sysOrderConfig.getOrderMaxTime())); if (CollectionUtil.isEmpty(orders)) { return; } List cancelOrderList = this.checkOrders(orders); orderService.cancelOrders(cancelOrderList); // 移除缓存 this.removeCache(cancelOrderList); } ``` 可以看到在`checkOrders`方法会检查订单是否已经在第三方支付过,如果已经支付则不取消订单进行支付成功操作。后续也是同用户主动取消订单用的同一个方法`cancelOrders`进行取消订单。 但是`listUnRefundOrderAndOrderItems`方法只查询了数据库中未支付的秒杀订单,有可能因为网络问题等导致redis中的秒杀订单还没有落库,此时我们用的是秒杀订单的定时任务来取消订单。 接着我们来到`SeckillOrderTask`,找到`cancelSeckillOrder`方法: ```java @XxlJob("cancelSeckillOrder") public void cancelSeckillOrder() { log.info("先从Redis中将订单数据存储到MySQL中。。。"); seckillCacheManager.saveSeckillOrderInfo(); log.info("取消超时未支付的秒杀订单..."); seckillOrderService.cancelOrderUnpayOrderByTime(); } ``` 首先在`saveSeckillOrderInfo`可以看到我们将Redis中的订单数据保存到MySQL中: ```java // 保存订单到MySQL eventPublisher.publishEvent(new SaveSeckillOrderEvent(multiDecrementBO.getOrderNumber(), multiDecrementBO.getUserId())); ``` 接着在`cancelOrderUnpayOrderByTime`方法: ```java public void cancelOrderUnpayOrderByTime() { SysOrderConfig sysOrderConfig = sysConfigService.getSysConfigObject(Constant.ORDER_TIME_CONFIG, SysOrderConfig.class); Date cancelTime = DateUtil.offsetMinute(new Date(), -sysOrderConfig.getOrderMaxTime()); // 查询出微信or支付宝支付并且为二维码支付的,失效掉对应支付二维码 List filterPayInfo = payInfoMapper.listByPayTypeAndTime(cancelTime); // 失效二维码 if (CollectionUtils.isNotEmpty(filterPayInfo)) { eventPublisher.publishEvent(new PayManagerEvent(filterPayInfo)); } List orderList = orderMapper.listUnPayOrder(cancelTime); if (CollUtil.isEmpty(orderList)) { log.info("没有查询到超时未的支付秒杀订单"); return; } orderList.removeIf(order -> CollUtil.isEmpty(order.getOrderItems())); List orderNumbers = orderList.stream().map(Order::getOrderNumber).toList(); // 修改订单状态 DataBatchHandleUtil.batchSplitInsert(orderNumbers, orderMapper::batchCancelSeckillOrder); // 还原库存 for (Order order : orderList) { OrderItem orderItem = order.getOrderItems().get(0); // 还原库存可以重复执行,lua有幂等处理,只会还原一次 stockManager.rollbackSecKillStock(orderItem.getActivityId(), orderItem.getSkuId(), orderItem.getProdCount(), orderItem.getOrderNumber(), orderItem.getUserId(), orderItem.getStockPointId()); } } ``` 我们可以看到逻辑跟取消订单定时任务一样,都是先进行失效第三方二维码和还原库存等操作,这里就不再赘述了。