3订单设计-取消订单.md 7.1 KB

取消订单

用户主动取消订单

我们来到MyOrderController ,这里就是统一用户操作订单的接口。

我们直接看一下取消订单接口核心代码:

@PutMapping("/cancel/{orderNumber}")
@Operation(summary = "根据订单编号取消订单" , description = "根据订单编号取消订单")
@Parameter(name = "orderNumber", description = "订单编号" , required = true)
public ServerResponseEntity<String> cancel(@PathVariable("orderNumber") String orderNumber) {
    String userId = SecurityUtils.getUser().getUserId();
    Order order = orderService.getOne(new LambdaQueryWrapper<Order>().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里面的代码:

@Override
@Transactional(rollbackFor = Exception.class)
public void cancelOrders(List<Order> orders) {
    ...
    List<OrderItem> allOrderItems = new ArrayList<>();
    List<String> orderNumberList = new ArrayList<>();
    for (Order order : orders) {
        List<OrderItem> 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<SkuStockVO> 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<Order> orders = orderService.listUnRefundOrderAndOrderItems(OrderStatus.UNPAY.value(), null, DateUtil.offsetMinute(now, -sysOrderConfig.getOrderMaxTime()));
    if (CollectionUtil.isEmpty(orders)) {
        return;
    }
    List<Order> cancelOrderList = this.checkOrders(orders);
    orderService.cancelOrders(cancelOrderList);
    // 移除缓存
    this.removeCache(cancelOrderList);

}

可以看到在checkOrders方法会检查订单是否已经在第三方支付过,如果已经支付则不取消订单进行支付成功操作。后续也是同用户主动取消订单用的同一个方法cancelOrders进行取消订单。

但是listUnRefundOrderAndOrderItems方法只查询了数据库中未支付的秒杀订单,有可能因为网络问题等导致redis中的秒杀订单还没有落库,此时我们用的是秒杀订单的定时任务来取消订单。

接着我们来到SeckillOrderTask,找到cancelSeckillOrder方法:

@XxlJob("cancelSeckillOrder")
public void cancelSeckillOrder() {
    log.info("先从Redis中将订单数据存储到MySQL中。。。");
    seckillCacheManager.saveSeckillOrderInfo();

    log.info("取消超时未支付的秒杀订单...");
    seckillOrderService.cancelOrderUnpayOrderByTime();
}

首先在saveSeckillOrderInfo可以看到我们将Redis中的订单数据保存到MySQL中:

// 保存订单到MySQL
eventPublisher.publishEvent(new SaveSeckillOrderEvent(multiDecrementBO.getOrderNumber(), multiDecrementBO.getUserId()));

接着在cancelOrderUnpayOrderByTime方法:

public void cancelOrderUnpayOrderByTime() {
    SysOrderConfig sysOrderConfig = sysConfigService.getSysConfigObject(Constant.ORDER_TIME_CONFIG, SysOrderConfig.class);
    Date cancelTime = DateUtil.offsetMinute(new Date(), -sysOrderConfig.getOrderMaxTime());
    // 查询出微信or支付宝支付并且为二维码支付的,失效掉对应支付二维码
    List<PayInfo> filterPayInfo = payInfoMapper.listByPayTypeAndTime(cancelTime);
    // 失效二维码
    if (CollectionUtils.isNotEmpty(filterPayInfo)) {
        eventPublisher.publishEvent(new PayManagerEvent(filterPayInfo));
    }
    List<Order> orderList = orderMapper.listUnPayOrder(cancelTime);
    if (CollUtil.isEmpty(orderList)) {
        log.info("没有查询到超时未的支付秒杀订单");
        return;
    }

    orderList.removeIf(order -> CollUtil.isEmpty(order.getOrderItems()));
    List<String> 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());
    }
}

我们可以看到逻辑跟取消订单定时任务一样,都是先进行失效第三方二维码和还原库存等操作,这里就不再赘述了。