Spring Task

  • 框架定位:Spring框架提供的任务调度工具
  • 核心特性:
    • 按照约定时间自动执行代码逻辑
    • 区别于请求响应模式,无需外部触发
    • 类比闹钟机制,到设定时间自动执行

cron表达式

  • 本质:cron表达式本质上是一个字符串,用于定义任务触发的时间
  • 作用:作为Spring Task任务调度框架的核心配置,通过表达式指定任务执行的时间点
  • 域的数量:由6或7个域组成,用空格分隔
  • 域的顺序:从左到右依次代表秒、分钟、小时、日、月、周、年(年可选)
  • 特殊规则:日和周两个域通常互斥,只能指定其中一个,另一个用问号表示
  • 示例解析:
    • 2022年10月12日上午9点整对应的表达式:0 0 9 12 10 ? 2022
    • 各域对应关系:秒(0) 分(0) 时(9) 日(12) 月(10) 周(?) 年(2022)
    • 周使用问号的原因:当指定具体日期时,周信息应设为不指定

cron表达式在线生成器:在线Cron表达式生成器

使用步骤

1.导入maven坐标spring-context

  • 框架特点: Spring Task是一个非常轻量级的框架,没有独立的jar包,其API集成在spring-context包中
  • 依赖关系: 项目中已通过spring-boot-starter传递引入了spring-context依赖,无需额外导入
  • 验证方式: 可在项目依赖树中查看org.springframework:spring-context:5.3.22是否存在

2.开启任务调度

  • 注解使用: 在启动类上添加@EnableScheduling注解即可开启任务调度功能
  • 类比说明: 类似@EnableTransactionManagement开启事务、@EnableCaching开启缓存的使用方式
  • 实现原理: 该注解会注册ScheduledAnnotationBeanPostProcessor处理定时任务

3.自定义定时任务类

  • 组件要求: 类需添加@Component注解纳入Spring容器管理
  • 方法规范:
    • 方法需无返回值(void)
    • 方法名可任意定义
    • 业务逻辑代码编写在方法体内
  • 定时配置: 使用@Scheduled(cron=“表达式”)注解指定触发时间
    • 表达式格式: 秒 分 时 日 月 周(年可选)
    • 示例: “0/5 * * * * ?”表示每5秒触发一次

案例

/**  
 * 自定义定时任务类  
 */  
  
@Component  
@Slf4j  
public class MyTask {  
  
    /**  
     * 定时任务,每5秒触发一次  
     */  
    @Scheduled(cron = "0/5 * * * * ?")  
    public void myTask() {  
        log.info("定时任务开始执行:{}", LocalDateTime.now());  
    }  
}

订单状态定时处理

需求分析

下单后未支付

  • 业务规则:用户需在15分钟内完成支付,超时订单应自动取消
  • 处理方案:通过定时任务每分钟检查一次超时订单
  • 执行频率:
    • 每分钟检查一次(时效性要求高)
    • 不建议每秒检查(系统压力过大)
  • 判定标准:下单时间超过15分钟且状态仍为”待支付”
  • 状态变更:自动将订单状态修改为”已取消”

用户收货后管理端未点击完成按钮

  • 问题现象:订单长期处于”派送中”状态,与实际收货情况不符
  • 处理方案:通过定时任务检查超时派送订单
  • 执行频率:
    • 每天凌晨1点检查一次(避免高峰期误判)
    • 不建议白天检查(可能误判正常派送订单)
  • 时间选择:选择店铺打烊后统一处理
  • 状态变更:将符合条件的订单状态修改为”已完成”

代码开发

/**  
 * 处理超时订单  
 */  
@Scheduled(cron = "0 * * * * ?") // 每分钟执行一次  
public void processTimeoutOrders() {  
    log.info("定时处理超时订单:{}", LocalDateTime.now());  
  
    LocalDateTime time = LocalDateTime.now().minusMinutes(15);  
    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);  
        }  
    }  
}  
  
/**  
 * 处理处于派送中的订单  
 */  
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点执行一次  
public void processDeliveryOrders() {  
    log.info("定时处理处于派送中的订单:{}", LocalDateTime.now());  
    LocalDateTime time = LocalDateTime.now().minusMinutes(60);  
    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);  
        }  
    }  
}
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")  
List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);

WebSocket

  • 握手过程: 客户端首先发送握手请求,服务器应答后建立持久连接
  • 连接特性: 建立的是长连接,连接建立后会一直保持
  • 通信方向: 支持全双工通信,浏览器和服务器可以同时双向传输数据
  • 应用类比: 类似电话通信,连接建立后双方可随时主动说话
  • 底层基础: 同样基于TCP协议实现数据传输

导入maven坐标

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

案例

<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Demo</title>
</head>
<body>
    <input id="text" type="text" />
    <button onclick="send()">发送消息</button>
    <button onclick="closeWebSocket()">关闭连接</button>
    <div id="message">
    </div>
</body>
<script type="text/javascript">
    var websocket = null;
    var clientId = Math.random().toString(36).substr(2);
 
    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        //连接WebSocket节点
        websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
    }
    else{
        alert('Not support websocket')
    }
 
    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };
 
    //连接成功建立的回调方法
    websocket.onopen = function(){
        setMessageInnerHTML("连接成功");
    }
 
    //接收到消息的回调方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }
 
    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }
 
    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }
 
    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }
 
    //发送消息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
	
	//关闭连接
    function closeWebSocket() {
        websocket.close();
    }
</script>
</html>
 

com/sky/config/WebSocketConfiguration.java

/**  
 * WebSocket配置类,用于注册WebSocket的Bean  
 */@Configuration  
public class WebSocketConfiguration {  
  
    @Bean  
    public ServerEndpointExporter serverEndpointExporter() {  
        return new ServerEndpointExporter();  
    }  
  
}

com/sky/websocket/WebSocketServer.java

  
/**  
 * WebSocket服务  
 */  
@Component  
@ServerEndpoint("/ws/{sid}")  
public class WebSocketServer {  
  
    //存放会话对象  
    private static Map<String, Session> sessionMap = new HashMap();  
  
    /**  
     * 连接建立成功调用的方法  
     */  
    @OnOpen  
    public void onOpen(Session session, @PathParam("sid") String sid) {  
        System.out.println("客户端:" + sid + "建立连接");  
        sessionMap.put(sid, session);  
    }  
  
    /**  
     * 收到客户端消息后调用的方法  
     *  
     * @param message 客户端发送过来的消息  
     */  
    @OnMessage  
    public void onMessage(String message, @PathParam("sid") String sid) {  
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);  
    }  
  
    /**  
     * 连接关闭调用的方法  
     *  
     * @param sid  
     */  
    @OnClose  
    public void onClose(@PathParam("sid") String sid) {  
        System.out.println("连接断开:" + sid);  
        sessionMap.remove(sid);  
    }  
  
    /**  
     * 群发  
     *  
     * @param message  
     */  
    public void sendToAllClient(String message) {  
        Collection<Session> sessions = sessionMap.values();  
        for (Session session : sessions) {  
            try {  
                //服务器向客户端发送消息  
                session.getBasicRemote().sendText(message);  
            } catch (Exception e) {  
                e.printStackTrace();  
            }  
        }  
    }  
  
}

com/sky/task/WebSocketTask.java

@Component  
public class WebSocketTask {  
    @Autowired  
    private WebSocketServer webSocketServer;  
  
    /**  
     * 通过WebSocket每隔5秒向客户端发送消息  
     */  
    @Scheduled(cron = "0/5 * * * * ?")  
    public void sendMessageToClient() {  
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));  
    }  
}

来电提醒

需求分析与设计

  • 业务场景:当用户下单并支付成功后,系统需立即通知外卖商家进行备货
  • 通知形式:
    • 语音播报(示例:“您有新的订单,请注意查收”)
    • 弹出提示框(显示订单详情)
  • 实现目标:商家端实时接收订单通知,包含套餐名称、订单号等关键信息

  • 技术方案:采用WebSocket实现服务端到客户端的长连接推送
  • 实现流程:
    • 建立连接:商家管理端页面与服务端建立WebSocket长连接
    • 消息推送:支付成功后,服务端调用WebSocket API推送订单消息
    • 消息解析:客户端解析消息类型(1=来单提醒,2=客户催单)
    • 消息展示:根据消息类型执行语音播报和弹窗提示
  • 数据格式:
  • 关键字段说明:
    • type:数字标识(1为来单提醒,2为催单)
    • orderId:关联的具体订单编号
    • content:提示框显示的完整文本内容

代码开发

sky-server/src/main/java/com/sky/service/impl/OrderServiceImpl.java

public void paySuccess(String outTradeNo) {  
  
    // 根据订单号查询订单  
    Orders ordersDB = orderMapper.getByNumber(outTradeNo);  
  
    // 根据订单id更新订单的状态、支付方式、支付状态、结账时间  
    Orders orders = Orders.builder()  
            .id(ordersDB.getId())  
            .status(Orders.TO_BE_CONFIRMED)  
            .payStatus(Orders.PAID)  
            .checkoutTime(LocalDateTime.now())  
            .build();  
    orderMapper.update(orders);  
  
    // 通过websocket 向客户端推送消息  
    Map map = new HashMap();  
    map.put("type", 1); // 1表示来电提醒 2表示客户催单  
    map.put("orderId", ordersDB.getId());  
    map.put("content", "订单号:" + outTradeNo);  
    String jsonString = JSONObject.toJSONString(map);  
    webSocketServer.sendToAllClient(jsonString);  
}

客户催单

需求分析与设计

  • 触发条件:用户在小程序支付成功后且订单处于待接单状态时,可点击催单按钮
  • 通知形式:商家端会收到两种形式的通知
    • 语音播报提示”有用户催单了”
    • 弹出提示框显示催单信息
  • 业务场景:主要用于客户催促商家尽快接单的场景

  • 实现流程:
    • 通过WebSocket建立管理端页面与服务端的长连接
    • 用户点击催单按钮后调用WebSocket API向服务端推送消息
    • 客户端解析消息类型并做出相应提示
  • 数据格式:
    • 采用JSON格式包含三个字段:
      • type:消息类型(1为来单提醒,2为客户催单)
      • orderId:订单ID
      • content:消息内容
  • 与来单提醒的区别:
    • 消息类型值不同(催单为2)
    • 触发方式不同(催单由客户主动触发)
public void reminder(Long id) {  
    Orders orderDB = orderMapper.getById(id);  
    if (orderDB == null) {  
        throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);  
    }  
  
    Map map = new HashMap();  
    map.put("type", 2);  
    map.put("orderId", orderDB.getId());  
    map.put("content", "订单号:" + orderDB.getNumber());  
    webSocketServer.sendToAllClient(JSON.toJSONString(map));  
}