订单自动取消实现方案:让超时订单自动消失!⏰

92 阅读10分钟

标题: 订单超时不处理?用户体验和库存都炸了!
副标题: 四种方案实现订单自动取消,总有一款适合你


🎬 开篇:一个被遗忘的订单

双11购物狂欢:

00:00 - 用户A下单抢购iPhone(库存扣减)
00:15 - 用户A去洗澡了,忘记支付...
06:00 - 运营查看后台:
        "怎么有1000个订单都是'待支付'状态?"
        "库存全被锁住了!"
        "其他用户买不了了!"
        
12:00 - 客服电话被打爆:
        "为什么显示有库存却买不了?"
        "我的订单怎么还是待支付?"
        
运营:这些超时订单为什么不自动取消?💀
开发:我...我忘了做这个功能...😰

损失:
- 库存占用:1000件
- 用户流失:500+
- 加班处理:通宵
- 老板暴怒:无价

教训:订单超时自动取消是电商系统的必备功能!

🤔 为什么需要自动取消?

想象你预订了餐厅座位:

  • ❌ 不限时: 你订了不去,别人也订不了(浪费资源)
  • ✅ 15分钟规则: 15分钟不到,自动释放座位(合理利用)

核心目的:释放锁定的库存,让其他用户能购买!


📚 知识地图

订单自动取消四大方案
├── ⏰ 方案1:定时任务扫描(简单但不精确)
├── 📦 方案2:延迟消息(推荐!)
├── ⏱️ 方案3:时间轮算法(高性能)
└── 🔔 方案4:Redis过期监听(轻量级)

⏰ 方案1:定时任务扫描

🌰 生活中的例子

保安巡逻:

  • 每小时巡查一次,发现超时的车就开罚单
  • 优点: 简单易实现
  • 缺点: 不够及时,有延迟

💻 技术实现

/**
 * 定时任务扫描方案
 */
@Component
public class OrderCancelScheduler {
    
    @Autowired
    private OrderService orderService;
    
    @Autowired
    private StockService stockService;
    
    /**
     * 定时扫描超时订单(每分钟执行一次)
     */
    @Scheduled(cron = "0 */1 * * * ?")
    public void scanAndCancelTimeoutOrders() {
        log.info("开始扫描超时订单");
        
        // 1. 查询超时的待支付订单
        // 超时时间:15分钟
        LocalDateTime expireTime = LocalDateTime.now().minusMinutes(15);
        
        List<Order> timeoutOrders = orderService.findTimeoutOrders(expireTime);
        
        log.info("查询到{}个超时订单", timeoutOrders.size());
        
        // 2. 批量取消订单
        for (Order order : timeoutOrders) {
            try {
                cancelOrder(order);
            } catch (Exception e) {
                log.error("取消订单失败:orderId=" + order.getId(), e);
            }
        }
        
        log.info("超时订单扫描完成");
    }
    
    /**
     * 取消订单
     */
    @Transactional(rollbackFor = Exception.class)
    public void cancelOrder(Order order) {
        // 1. 更新订单状态
        order.setStatus(OrderStatus.CANCELLED);
        order.setCancelTime(LocalDateTime.now());
        order.setCancelReason("超时未支付");
        orderService.updateById(order);
        
        // 2. 回滚库存
        stockService.rollbackStock(order.getProductId(), order.getQuantity());
        
        // 3. 如果有优惠券,回滚优惠券
        if (order.getCouponId() != null) {
            couponService.rollbackCoupon(order.getCouponId());
        }
        
        log.info("订单自动取消:orderId={}", order.getId());
    }
}

/**
 * Mapper实现
 */
@Mapper
public interface OrderMapper {
    
    /**
     * 查询超时订单
     */
    @Select("SELECT * FROM `order` " +
            "WHERE status = 'PENDING' " +  // 待支付
            "AND create_time < #{expireTime} " +  // 超时
            "LIMIT 1000")  // 限制查询数量
    List<Order> selectTimeoutOrders(@Param("expireTime") LocalDateTime expireTime);
}

/**
 * 优点:
 * ✅ 实现简单
 * ✅ 易于理解和维护
 * ✅ 无需额外组件
 * 
 * 缺点:
 * ❌ 时效性差(最多延迟1分钟)
 * ❌ 数据库压力大(频繁全表扫描)
 * ❌ 订单量大时性能差
 * 
 * 适用场景:
 * ✅ 订单量小(<10万/天)
 * ✅ 对时效性要求不高
 * ✅ 初期MVP版本
 */

优化版:分布式锁 + 分页查询

/**
 * 优化的定时任务方案
 */
@Component
public class OptimizedOrderCancelScheduler {
    
    @Autowired
    private RedissonClient redissonClient;
    
    @Autowired
    private OrderService orderService;
    
    @Scheduled(cron = "0 */1 * * * ?")
    public void scanAndCancelTimeoutOrders() {
        String lockKey = "order:cancel:lock";
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // ⚡ 获取分布式锁(避免多实例重复执行)
            boolean locked = lock.tryLock(0, 50, TimeUnit.SECONDS);
            
            if (!locked) {
                log.info("获取锁失败,跳过本次执行");
                return;
            }
            
            // 分页查询(避免一次加载过多数据)
            int pageSize = 100;
            int pageNum = 0;
            
            while (true) {
                List<Order> orders = orderService.findTimeoutOrdersPage(
                    pageNum++, pageSize);
                
                if (orders.isEmpty()) {
                    break;
                }
                
                // 批量取消
                orderService.batchCancelOrders(orders);
                
                log.info("取消第{}页,共{}个订单", pageNum, orders.size());
            }
            
        } catch (InterruptedException e) {
            log.error("获取锁被中断", e);
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
}

📦 方案2:延迟消息(推荐!)

🌰 生活中的例子

外卖订单:

  • 下单时设置15分钟闹钟
  • 15分钟后闹钟响起,检查是否支付
  • 未支付自动取消

💻 技术实现

实现1:RabbitMQ延迟队列

/**
 * RabbitMQ延迟队列配置
 */
@Configuration
public class RabbitMQConfig {
    
    /**
     * 延迟交换机
     */
    @Bean
    public CustomExchange delayExchange() {
        Map<String, Object> args = new HashMap<>();
        args.put("x-delayed-type", "direct");
        
        return new CustomExchange(
            "order.delay.exchange",
            "x-delayed-message",
            true,
            false,
            args
        );
    }
    
    /**
     * 延迟队列
     */
    @Bean
    public Queue delayQueue() {
        return new Queue("order.cancel.queue", true);
    }
    
    /**
     * 绑定
     */
    @Bean
    public Binding delayBinding(Queue delayQueue, CustomExchange delayExchange) {
        return BindingBuilder
            .bind(delayQueue)
            .to(delayExchange)
            .with("order.cancel")
            .noargs();
    }
}

/**
 * 订单服务
 */
@Service
public class OrderService {
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    /**
     * 创建订单
     */
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(OrderDTO dto) {
        // 1. 创建订单
        Order order = Order.builder()
            .orderNo(generateOrderNo())
            .userId(dto.getUserId())
            .productId(dto.getProductId())
            .quantity(dto.getQuantity())
            .totalAmount(dto.getTotalAmount())
            .status(OrderStatus.PENDING)
            .createTime(LocalDateTime.now())
            .build();
        
        orderMapper.insert(order);
        
        // 2. 扣减库存
        stockService.deductStock(dto.getProductId(), dto.getQuantity());
        
        // 3. ⚡ 发送延迟消息(15分钟后执行)
        OrderCancelMessage message = OrderCancelMessage.builder()
            .orderId(order.getId())
            .orderNo(order.getOrderNo())
            .build();
        
        rabbitTemplate.convertAndSend(
            "order.delay.exchange",
            "order.cancel",
            message,
            msg -> {
                // 设置延迟时间(毫秒)
                msg.getMessageProperties().setDelay(15 * 60 * 1000);  // 15分钟
                return msg;
            }
        );
        
        log.info("订单创建成功,已发送延迟取消消息:orderNo={}", order.getOrderNo());
        
        return order.getOrderNo();
    }
}

/**
 * MQ消费者
 */
@Component
public class OrderCancelConsumer {
    
    @Autowired
    private OrderService orderService;
    
    /**
     * 消费延迟消息
     */
    @RabbitListener(queues = "order.cancel.queue")
    public void handleOrderCancel(OrderCancelMessage message) {
        log.info("收到延迟消息:orderId={}", message.getOrderId());
        
        try {
            // 1. 查询订单
            Order order = orderService.getById(message.getOrderId());
            
            if (order == null) {
                log.warn("订单不存在:orderId={}", message.getOrderId());
                return;
            }
            
            // 2. 检查订单状态
            if (order.getStatus() != OrderStatus.PENDING) {
                log.info("订单已支付或已取消,无需处理:orderId={}, status={}", 
                    order.getId(), order.getStatus());
                return;
            }
            
            // 3. ⚡ 取消订单
            orderService.cancelOrder(order);
            
            log.info("订单自动取消成功:orderNo={}", order.getOrderNo());
            
        } catch (Exception e) {
            log.error("处理订单取消失败", e);
            // 可以选择重试或记录到死信队列
            throw new AmqpRejectAndDontRequeueException("处理失败", e);
        }
    }
}

/**
 * 优点:
 * ✅ 时效性好(精确到毫秒)
 * ✅ 无需轮询数据库
 * ✅ 性能高
 * ✅ 可靠性高(消息持久化)
 * 
 * 缺点:
 * ⚠️ 需要RabbitMQ延迟插件
 * ⚠️ 增加系统复杂度
 * 
 * 适用场景:
 * ✅ 订单量大(>10万/天)
 * ✅ 对时效性要求高
 * ✅ 生产环境推荐 ⭐⭐⭐⭐⭐
 */

实现2:RocketMQ延迟消息

/**
 * RocketMQ延迟消息方案
 */
@Service
public class RocketMQOrderService {
    
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    
    /**
     * 创建订单
     */
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(OrderDTO dto) {
        // 1. 创建订单
        Order order = createOrderEntity(dto);
        orderMapper.insert(order);
        
        // 2. 扣减库存
        stockService.deductStock(dto.getProductId(), dto.getQuantity());
        
        // 3. ⚡ 发送延迟消息
        OrderCancelMessage message = OrderCancelMessage.builder()
            .orderId(order.getId())
            .orderNo(order.getOrderNo())
            .build();
        
        // RocketMQ支持18个延迟级别:
        // 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
        // 15分钟 = 延迟级别14
        
        rocketMQTemplate.syncSend(
            "order-cancel-topic",
            MessageBuilder.withPayload(message).build(),
            3000,  // 发送超时
            14     // 延迟级别:15分钟
        );
        
        log.info("订单创建成功,已发送延迟消息:orderNo={}", order.getOrderNo());
        
        return order.getOrderNo();
    }
}

/**
 * RocketMQ消费者
 */
@Component
@RocketMQMessageListener(
    topic = "order-cancel-topic",
    consumerGroup = "order-cancel-consumer"
)
public class OrderCancelListener implements RocketMQListener<OrderCancelMessage> {
    
    @Autowired
    private OrderService orderService;
    
    @Override
    public void onMessage(OrderCancelMessage message) {
        log.info("收到延迟消息:orderId={}", message.getOrderId());
        
        // 查询订单并取消
        Order order = orderService.getById(message.getOrderId());
        
        if (order != null && order.getStatus() == OrderStatus.PENDING) {
            orderService.cancelOrder(order);
        }
    }
}

⏱️ 方案3:时间轮算法

🌰 生活中的例子

手表的秒针:

  • 秒针转一圈60秒
  • 每个刻度代表1秒
  • 到时间就执行任务

💻 技术实现

/**
 * 时间轮实现(Netty Hashedwheeltimer)
 */
@Component
public class TimeWheelOrderCancel {
    
    private final HashedWheelTimer timer;
    
    @Autowired
    private OrderService orderService;
    
    public TimeWheelOrderCancel() {
        // 创建时间轮
        // tickDuration: 每格时间
        // ticksPerWheel: 总格数
        this.timer = new HashedWheelTimer(
            new ThreadFactoryBuilder()
                .setNameFormat("order-cancel-timer-%d")
                .build(),
            1,                    // 每格1秒
            TimeUnit.SECONDS,
            60                    // 60格
        );
        
        timer.start();
    }
    
    /**
     * 添加取消任务
     */
    public void addCancelTask(Long orderId, long delaySeconds) {
        timer.newTimeout(timeout -> {
            // 延迟任务执行
            try {
                Order order = orderService.getById(orderId);
                
                if (order != null && order.getStatus() == OrderStatus.PENDING) {
                    orderService.cancelOrder(order);
                    log.info("订单自动取消:orderId={}", orderId);
                }
            } catch (Exception e) {
                log.error("取消订单失败:orderId=" + orderId, e);
            }
        }, delaySeconds, TimeUnit.SECONDS);
        
        log.info("添加延迟取消任务:orderId={}, delay={}s", orderId, delaySeconds);
    }
    
    /**
     * 关闭时间轮
     */
    @PreDestroy
    public void shutdown() {
        if (timer != null) {
            timer.stop();
        }
    }
}

/**
 * 订单服务
 */
@Service
public class OrderService {
    
    @Autowired
    private TimeWheelOrderCancel timeWheelOrderCancel;
    
    /**
     * 创建订单
     */
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(OrderDTO dto) {
        // 1. 创建订单
        Order order = createOrderEntity(dto);
        orderMapper.insert(order);
        
        // 2. 扣减库存
        stockService.deductStock(dto.getProductId(), dto.getQuantity());
        
        // 3. ⚡ 添加时间轮任务(15分钟后执行)
        timeWheelOrderCancel.addCancelTask(order.getId(), 15 * 60);
        
        return order.getOrderNo();
    }
}

/**
 * 优点:
 * ✅ 性能极高(内存操作)
 * ✅ 时效性好
 * ✅ 适合大量短时任务
 * 
 * 缺点:
 * ❌ 任务存在内存中(重启丢失)
 * ❌ 不支持持久化
 * ❌ 单机方案(不支持分布式)
 * 
 * 适用场景:
 * ✅ 超高并发场景
 * ✅ 短时延迟任务(<1小时)
 * ✅ 可容忍任务丢失
 */

🔔 方案4:Redis过期监听

💻 技术实现

/**
 * Redis过期监听配置
 */
@Configuration
public class RedisConfig {
    
    @Bean
    public RedisMessageListenerContainer redisMessageListenerContainer(
            RedisConnectionFactory connectionFactory,
            OrderExpireListener orderExpireListener) {
        
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        
        // 监听过期事件
        container.addMessageListener(
            orderExpireListener,
            new PatternTopic("__keyevent@0__:expired")
        );
        
        return container;
    }
}

/**
 * 订单过期监听器
 */
@Component
public class OrderExpireListener implements MessageListener {
    
    @Autowired
    private OrderService orderService;
    
    @Override
    public void onMessage(Message message, byte[] pattern) {
        String expiredKey = message.toString();
        
        log.info("Redis key过期:{}", expiredKey);
        
        // 解析key:order:cancel:123456
        if (expiredKey.startsWith("order:cancel:")) {
            String orderId = expiredKey.substring("order:cancel:".length());
            
            try {
                // 取消订单
                Order order = orderService.getById(Long.parseLong(orderId));
                
                if (order != null && order.getStatus() == OrderStatus.PENDING) {
                    orderService.cancelOrder(order);
                    log.info("订单自动取消:orderId={}", orderId);
                }
            } catch (Exception e) {
                log.error("取消订单失败:orderId=" + orderId, e);
            }
        }
    }
}

/**
 * 订单服务
 */
@Service
public class OrderService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    /**
     * 创建订单
     */
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(OrderDTO dto) {
        // 1. 创建订单
        Order order = createOrderEntity(dto);
        orderMapper.insert(order);
        
        // 2. 扣减库存
        stockService.deductStock(dto.getProductId(), dto.getQuantity());
        
        // 3. ⚡ 设置Redis key(15分钟后过期)
        String key = "order:cancel:" + order.getId();
        redisTemplate.opsForValue().set(
            key,
            order.getOrderNo(),
            Duration.ofMinutes(15)
        );
        
        log.info("订单创建成功,已设置Redis过期监听:orderNo={}", order.getOrderNo());
        
        return order.getOrderNo();
    }
}

/**
 * ⚠️ 注意:Redis过期监听的坑
 * 
 * 1. 需要在redis.conf中开启:
 *    notify-keyspace-events Ex
 * 
 * 2. 过期事件不保证立即触发
 *    - Redis采用惰性删除+定期删除
 *    - 可能延迟几秒
 * 
 * 3. 集群模式下可能丢失
 *    - 主从切换时可能丢失
 *    - 需要配合消息队列使用
 * 
 * 4. 性能问题
 *    - 大量key过期时性能下降
 * 
 * 优点:
 * ✅ 实现简单
 * ✅ 轻量级
 * 
 * 缺点:
 * ❌ 不可靠(可能丢失)
 * ❌ 不精确(可能延迟)
 * ❌ 集群模式问题多
 * 
 * 适用场景:
 * ⚠️ 不推荐生产环境使用
 * ✅ 可用于辅助方案
 */

📊 方案对比

方案时效性可靠性性能复杂度推荐度
定时任务扫描低(延迟1分钟)⭐⭐
延迟消息(RabbitMQ)高(毫秒级)⭐⭐⭐⭐⭐
延迟消息(RocketMQ)⭐⭐⭐⭐⭐
时间轮极高⭐⭐⭐
Redis过期监听⭐⭐

✅ 最佳实践

生产环境推荐方案:延迟消息(RabbitMQ/RocketMQ)

设计要点:
□ 订单创建时立即发送延迟消息
□ 消息体包含订单ID和订单号
□ 消费端先检查订单状态再取消
□ 幂等性设计(防止重复取消)
□ 记录取消日志(可追溯)

容错处理:
□ 消息消费失败重试3次
□ 最终失败记录死信队列
□ 人工介入处理死信消息
□ 定时任务作为兜底方案

性能优化:
□ 批量查询订单信息
□ 异步回滚库存
□ Redis缓存订单状态
□ 监控消息堆积情况

用户体验:
□ 支付前倒计时提示
□ 取消后push/短信通知
□ 提供一键恢复订单功能
□ 清晰的取消原因说明

🎯 完整实现示例

/**
 * 完整的订单服务实现
 */
@Service
public class CompleteOrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private StockService stockService;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 创建订单
     */
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(OrderDTO dto) {
        // 1. 参数校验
        validateOrderDTO(dto);
        
        // 2. 检查库存
        if (!stockService.checkStock(dto.getProductId(), dto.getQuantity())) {
            throw new BusinessException("库存不足");
        }
        
        // 3. 创建订单
        Order order = Order.builder()
            .orderNo(generateOrderNo())
            .userId(dto.getUserId())
            .productId(dto.getProductId())
            .quantity(dto.getQuantity())
            .totalAmount(dto.getTotalAmount())
            .status(OrderStatus.PENDING)
            .createTime(LocalDateTime.now())
            .expireTime(LocalDateTime.now().plusMinutes(15))  // 15分钟后过期
            .build();
        
        orderMapper.insert(order);
        
        // 4. 扣减库存
        boolean deductSuccess = stockService.deductStock(
            dto.getProductId(), 
            dto.getQuantity()
        );
        
        if (!deductSuccess) {
            throw new BusinessException("扣减库存失败");
        }
        
        // 5. 发送延迟消息(主方案)
        sendDelayMessage(order.getId(), order.getOrderNo());
        
        // 6. 设置Redis key(备用方案)
        setRedisExpire(order.getId(), order.getOrderNo());
        
        log.info("订单创建成功:orderNo={}", order.getOrderNo());
        
        return order.getOrderNo();
    }
    
    /**
     * 取消订单
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean cancelOrder(Long orderId) {
        // 1. 查询订单
        Order order = orderMapper.selectById(orderId);
        if (order == null) {
            log.warn("订单不存在:orderId={}", orderId);
            return false;
        }
        
        // 2. 幂等性检查
        if (order.getStatus() != OrderStatus.PENDING) {
            log.info("订单状态不是待支付,无需取消:orderId={}, status={}", 
                orderId, order.getStatus());
            return false;
        }
        
        // 3. 更新订单状态
        order.setStatus(OrderStatus.CANCELLED);
        order.setCancelTime(LocalDateTime.now());
        order.setCancelReason("超时未支付");
        
        int updated = orderMapper.updateById(order);
        if (updated == 0) {
            log.error("更新订单状态失败:orderId={}", orderId);
            return false;
        }
        
        // 4. 回滚库存
        stockService.rollbackStock(order.getProductId(), order.getQuantity());
        
        // 5. 回滚优惠券(如果有)
        if (order.getCouponId() != null) {
            couponService.rollbackCoupon(order.getCouponId());
        }
        
        // 6. 删除Redis key
        String redisKey = "order:cancel:" + orderId;
        redisTemplate.delete(redisKey);
        
        // 7. 发送取消通知
        sendCancelNotification(order);
        
        log.info("订单取消成功:orderNo={}", order.getOrderNo());
        
        return true;
    }
    
    /**
     * 支付成功(取消定时任务)
     */
    @Transactional(rollbackFor = Exception.class)
    public void paySuccess(String orderNo) {
        // 1. 查询订单
        Order order = orderMapper.selectByOrderNo(orderNo);
        if (order == null) {
            throw new BusinessException("订单不存在");
        }
        
        // 2. 更新订单状态
        order.setStatus(OrderStatus.PAID);
        order.setPayTime(LocalDateTime.now());
        orderMapper.updateById(order);
        
        // 3. ⚡ 删除Redis key(阻止自动取消)
        String redisKey = "order:cancel:" + order.getId();
        redisTemplate.delete(redisKey);
        
        // 注意:延迟消息已发送无法撤回
        // 消费端会检查订单状态,已支付的订单不会被取消
        
        log.info("订单支付成功:orderNo={}", orderNo);
    }
    
    /**
     * 发送延迟消息
     */
    private void sendDelayMessage(Long orderId, String orderNo) {
        OrderCancelMessage message = OrderCancelMessage.builder()
            .orderId(orderId)
            .orderNo(orderNo)
            .build();
        
        rabbitTemplate.convertAndSend(
            "order.delay.exchange",
            "order.cancel",
            message,
            msg -> {
                msg.getMessageProperties().setDelay(15 * 60 * 1000);  // 15分钟
                return msg;
            }
        );
    }
    
    /**
     * 设置Redis过期key
     */
    private void setRedisExpire(Long orderId, String orderNo) {
        String key = "order:cancel:" + orderId;
        redisTemplate.opsForValue().set(
            key,
            orderNo,
            Duration.ofMinutes(15)
        );
    }
    
    /**
     * 发送取消通知
     */
    private void sendCancelNotification(Order order) {
        // 发送push通知
        pushService.sendPush(
            order.getUserId(),
            "订单已取消",
            "您的订单" + order.getOrderNo() + "已超时未支付,已自动取消"
        );
        
        // 发送短信(可选)
        // smsService.sendSms(user.getPhone(), "订单取消通知", ...);
    }
}

🎉 总结

核心要点

订单自动取消实现方案:

1️⃣ 生产环境首选:延迟消息
   - RabbitMQ延迟插件
   - RocketMQ延迟消息
   - 时效性好,可靠性高

2️⃣ 备用方案:定时任务
   - 作为兜底方案
   - 处理消息丢失的情况

3️⃣ 关键设计点:
   - 订单创建时发送延迟消息
   - 消费端检查订单状态
   - 幂等性设计
   - 回滚库存和优惠券

4️⃣ 用户体验:
   - 支付前倒计时
   - 取消后通知
   - 提供恢复功能

记住:订单自动取消是"延迟消息+定时任务"双保险!


文档编写时间:2025年10月24日
作者:热爱电商的订单工程师
版本:v1.0
愿你的订单管理井井有条! ⏰✨