前言

在“苍穹外卖”开发的第十天,我们迎来了两个非常实用的功能模块:自动化处理实时消息交互

  • 如何处理那些用户下单后迟迟不支付的“幽灵订单”?

  • 如何让商家第一时间听到“您有新的外卖订单”的语音播报?

本篇博客将通过 Spring TaskWebSocket 技术来解决这些问题。

一、Spring Task 定时任务

1.1 什么是 Spring Task?

Spring Task 是 Spring 框架提供的轻量级任务调度工具。简单来说,它能让你的代码按照约定的时间,自动执行某段逻辑,而无需人工干预。

1.2 Cron 表达式

定时任务的核心在于“什么时候执行”,这需要通过 Cron 表达式 来定义。 它本质上是一个字符串,由 6 或 7 个域组成,通过空格分隔:

秒 分 时 日 月 周 年(可选)

常用通配符:

  • *:表示所有值。

  • ?:表示不关心该值(常用于“周”或“日”,避免冲突)。

  • -:表示范围。

  • /:表示递增(例如 0/5 表示从0开始每5个单位执行一次)。

经典案例:

  • */5 * * * * ? :每隔5秒执行一次

  • 0 0/3 * * * ? :每3分钟触发一次

  • 0 0 5-15 * * ? :每天5-15点整点触发

Corn表达式在线生成网站

1.3 业务场景:订单状态定时处理

在本项目中,我们需要通过定时任务解决以下两个数据一致性问题:

  1. 支付超时处理

    • 需求:用户下单后可能未支付,如果一直占用资源不好。

    • 逻辑:每分钟检查一次,判定 order_time < now - 15min 且状态为 未支付 的订单,将其自动取消。

  2. 派送中订单兜底

    • 需求:防止骑士送完货后,后台管理员忘记点击“完成”,导致订单一直处于“派送中”。

    • 逻辑:每天凌晨 1 点 (0 0 1 * * ?),查找所有处于 派送中 的订单,自动将其状态修改为 已完成

在使用 Spring Task 之前,别忘了在启动类上添加注解 @EnableScheduling 来开启定时任务功能。

核心代码

package com.sky.task;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 定时任务类,定时处理订单状态
 */
@Component
@Slf4j
public class OrderTask {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 处理支付超时订单
     * 需求:每分钟触发一次,判定 order_time < now - 15min 则超时,自动取消
     */
    @Scheduled(cron = "0 * * * * ?") // 每分钟触发一次
    public void processTimeoutOrder() {
        log.info("定时处理超时订单:{}", LocalDateTime.now());

        // 计算时间点:当前时间 - 15分钟
        LocalDateTime time = LocalDateTime.now().minusMinutes(15);

        // 查询“未支付”且“下单时间小于time”的订单
        // select * from orders where status = ? and order_time < ?
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);

        if (ordersList != null && ordersList.size() > 0) {
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.CANCELLED); // 更改状态为已取消
                orders.setCancelReason("订单超时,自动取消");
                orders.setCancelTime(LocalDateTime.now());
                orderMapper.update(orders);
            }
        }
    }

    /**
     * 处理一直处于派送中的订单
     * 需求:每天凌晨1点触发,防止订单一直处于派送中
     */
    @Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点触发一次
    public void processDeliveryOrder() {
        log.info("定时处理处于派送中的订单:{}", LocalDateTime.now());

        // 为了保险起见,查找的是“上一工作日”还在派送中的订单
        // 这里逻辑通常是查找 60分钟前(即前一天晚上12点之前)还在派送的订单
        LocalDateTime time = LocalDateTime.now().minusMinutes(60);

        // 查询“派送中”且“下单时间小于time”的订单
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);

        if (ordersList != null && ordersList.size() > 0) {
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED); // 自动完成
                orderMapper.update(orders);
            }
        }
    }
}

Mapper 层需要实现根据状态和时间范围查询订单的 SQL 语句

二、WebSocket 双向通信

2.1 为什么需要 WebSocket

传统的 HTTP 协议是单向的(客户端请求 -> 服务端响应)。但在“来单提醒”这种场景下,服务端需要主动告诉浏览器:“嘿,有新订单了!”。如果用 HTTP 轮询(每隔几秒问一次服务器),效率低且浪费资源。

"WebSocket 主要是为了解决 HTTP 协议无法实现服务端主动推送的问题。它基于 TCP,通过 HTTP 协议进行一次握手后升级为长连接。相比于传统的轮询方式,它具有低延迟低带宽的优势,非常适合做类似苍穹外卖里的来单提醒、或者网页聊天室这种实时性要求高的功能。"

WebSocket 的优势:

  • 全双工通信:浏览器和服务器只需要一次握手,就能建立持久连接,双方都能主动发送数据。

  • 轻量级:相比 HTTP 轮询,网络开销更小。

动态展示http协议与websocket

特性 HTTP WebSocket
连接方式 短连接 长连接
通信方向 单向(Req -> Resp) 双向(全双工)
适用场景 常规数据交互 弹幕、股票、聊天、实时提醒

2.2 使用步骤

  1. 客户端:前端使用 HTML5 提供的 WebSocket API。

  2. 服务端

    • 导入 WebSocketServer 组件。

    • 配置 WebSocketConfiguration 注册服务端点。

    • 通过 sendToAllClient 等方法推送消息。

业务功能实战代码

3.1 来单提醒 (New Order Notification)

场景:用户支付成功后,第一时间通知管理端语音播报。 触发点:在 OrderServicepaySuccess 方法中。

public void paySuccess(String outTradeNo) {
    // ... (省略前面的查询与状态更新代码) ...

    Orders orders = Orders.builder()
            .id(ordersDB.getId())
            .status(Orders.TO_BE_CONFIRMED) // 状态变更为待接单
            .payStatus(Orders.PAID)
            .checkoutTime(LocalDateTime.now())
            .build();
    orderMapper.update(orders);

    // --- WebSocket 消息推送 ---
    Map<String,Object> map = new HashMap<>();
    map.put("type", 1); // 1表示来单提醒
    map.put("orderId", ordersDB.getId());
    map.put("content", "订单号" + outTradeNo);
    
    String json = JSON.toJSONString(map);
    // 向客户端浏览器推送消息
    webSocketServer.sendToAllClient(json);
}

3.2 用户催单 (Order Reminder)

场景:用户在小程序点击“催单”,管理端弹出提示。 触发点:Controller 接收请求 -> Service 处理推送。

@Override
public void reminder(Long id) {
    Orders orders = orderMapper.getByOrderId(id);
    if(orders == null){
        throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
    }
    
    // --- WebSocket 消息推送 ---
    Map<String,Object> map = new HashMap<>();
    map.put("type", 2); // 2表示催单
    map.put("orderId", orders.getId());
    map.put("content", "订单号" + orders.getNumber());
    
    String json = JSON.toJSONString(map);
    webSocketServer.sendToAllClient(json);
}

Logo

中国智能体开发者社区,聚焦智能体与大模型开发,提供前沿资讯、实用工具链、开源项目及行业案例。通过技术沙龙、开发者大赛等活动,促进经验交流与协作,助力开发者快速构建创新智能应用。

更多推荐