当前位置: 首页 > article >正文

定时任务Spring Task双向数据传输WebSocket

        之前的代码已完成基础功能,但仍有一些逻辑未完善:

定时任务:

  • 用户下单15分钟后仍未支付,订单判定为超时。status应修改为6(已取消)。
  • 管理端忘记点击"完成",使订单长时间处于"派送中"状态。系统需每天定时清理该类订单,status修改为5(已完成)。

        可使用Sping框架提供的工具Spring Task来定时处理一些任务。

弹窗任务:

  • 来单提醒:用户下单并支付后,管理端需弹出窗口提示有新订单。
  • 客户催单:用户下单并支付后,可点击"催单",管理端会弹出窗口提示接单。

        可以通过WebSocket协议来实现客户端浏览器和服务器之间进行双向的数据传输。

Spring Task

        Spring Task是Spring框架提供的一个任务调度工具,可以按照约定的时间自动执行某个代码逻辑,属于定时任务框架。 其作用类似于手机上的闹钟,即定时自动执行指定的java代码。

        指定代码要在哪个时间点触发可以通过cron表达式来确定。

cron表达式

        Cron表达式是一个强大的时间表达式,用于定义定时任务的执行时间。它由六个或七个字段(域)组成,中间以空格分隔,每个字段代表一个时间单位,分别为秒、分、时、日、月、周、年(其中年可选)。其本质上就是一个字符串,通过cron表达式可以定义任务触发的时间

        Cron表达式中使用了一些特殊字符来表示复杂的时间规则:

  • 星号(*):表示所有可能的值。例如,在“小时”字段中,*表示每小时。
  • 问号(?):表示不指定的值,只能用于日和周字段。例如在周字段中使用?,表示不指定具体是周几。
  • 连字符(-):表示一个范围。例如,在“小时”字段中,1-5表示从1点到5点。
  • 逗号(,):表示列表值。例如,在“周”字段中,Mon,Wed,Fri表示周一、周三和周五。
  • 斜杠(/):用于指定增量。例如,在“分”字段中,0/15表示从0分钟开始,每15分钟执行一次。

        一些特殊的字符:

  • L:表示最后。只能用于日和月字段。在月字段中表示一个月的最后一天;在周字段中表示一个星期的最后一天(即星期六)。如果L前有具体的内容,如“6L”,则表示该月的倒数第6天。
  • W:表示有效工作日(周一到周五)。只能用于日字段,系统将在离指定日期的最近的有效工作日触发事件。例如在日字段使用5W,如果5日是休息日,则将在最近的工作日(即星期五,4日)触发;如果是工作日,则就在5日触发。
  • LW:表示某个月最后一个工作日,即最后一个星期五。
  • #:用于确定每个月第几个星期几。它只能出现在DayofMonth字段中。例如,“4#2”表示某月的第二个星期三。

        一般日和周字段必须有一个为"?",例如每月1号早上9点执行一次:0 0 9 1 * ?,因为我们无法确定每月1号是周几,所以只能用"?"代替。

        这些只是些常用的字符,还有些特殊的字符我们并不需要去手写,我们可以通过在线生成器来生成cron表达式:

Cron表达式生成器https://cron.ciding.cc/https://cron.ciding.cc/https://cron.ciding.cc/https://cron.ciding.cc/https://cron.ciding.cc/https://cron.ciding.cc/

            以下是一些Cron表达式的示例及其含义:

    • 0 0 12 * * ?:每天中午12点执行一次。
    • 0 15 10 ? * *:每天上午10点15分执行一次。
    • 0 0/30 9-17 * * ?:每天上午9点到下午5点之间,每30分钟执行一次。
    • 0 0 0 1 * ?:每月1号凌晨0点执行一次。
    • 0 0 0 ? * Sun:每周日凌晨0点执行一次。

            接下来我们通过一个案例来了解如何使用Spring Task。

    入门案例

    一、导入maven坐标spring-context(已存在)

            因为Spring Task是一个非常小的框架,以至于其没有单独的jar包,其相关的api都封装在spring-context包下,而在原代码中已经导入了相关的maven坐标。

    二、在启动类添加注解@EnableScheduling开启任务调度

    三、自定义定时任务类

            在server模块下新建task包,在该包下新建定时任务类并添加相关注解。然后定义方法,返回值为void,并在方法上添加注解@Scheduled,在该注解内部添加cron属性:

    @Component//实例化该类
    @Slf4j
    public class toDoTask {
        @Scheduled(cron = "*/10 * * * * *")//每十秒执行一次该方法
        public void task() {
            log.info("task");
        }
    }

    订单状态定时处理

            了解完如何使用Spring Task后,我们回过头来完成相关的业务功能。因为定时任务都是自动执行的,并不需要前端发起请求,因此也不需要做接口设计。

    订单超时未付款

            检查订单是否超时的频率过高会增加数据库压力,频率过低又会难以实现目标,每分钟检查一次订单正合适。如果超时则将订单状态更新为6(已取消),未超时则不做处理。

            我们可以通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为"已取消"。

    @Component//实例化该类
    @Slf4j
    public class OrderTask {
    
        @Autowired
        private OrderMapper orderMapper;
    
        @Scheduled(cron = "0 * * * * *")//每分钟触发一次
        public void orderTimeOut() {
            // 记录当前时间并打印日志
            log.info("定时处理超时订单{}", LocalDateTime.now());
            // 计算出15分钟前的时间
            LocalDateTime time = LocalDateTime.now().minusMinutes(15);
            // 从数据库中查询所有状态为"待支付"且下单时间小于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);
                }
            }
        }
    }

    订单长时间未完成

            每天0点检查一次订单,如果仍在派送中则更新为5(已完成)。其思路与"订单超时未付款"类似,不再赘述。

        @Scheduled(cron = "0 0 0 * * *")
        public void orderComplete() {
            log.info("处理一直处于派送中的订单:{}", LocalDateTime.now());
    
            // 从数据库中查询所有状态为"派送中"的订单
            List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, LocalDateTime.now());
    
            // 如果查询到派送中的订单,则遍历列表并更新每个订单的状态
            if (ordersList != null && ordersList.size() > 0) {
                for (Orders orders : ordersList) {
                    // 设置订单状态为"已完成"
                    orders.setStatus(Orders.COMPLETED);
                    // 更新订单信息到数据库
                    orderMapper.update(orders);
                }
            }
        }

            这样代码便已编写完毕,但测试两功能与之前不同,因为无需发起请求,系统自动执行,我们只需要查看控制台是否输出对应的日志语句即可,"订单长时间未完成"功能则可以将时间暂时改为当前时间来测试。

    WebSocket

            Web Socket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

            简而言之就是浏览器和服务器之间都可以主动的向对方传输数据。
            与之相似的http协议是由Client(客户端)发起Request,Server响应Response,其必须由浏览器先发送请求,服务器无法主动发送请求,且在服务器响应后连接便不存在,如需再次对话则需建立新的连接,因此http连接我们也称为短连接。

            WebSocket的工作原理可以分为三个阶段:握手、数据传输和断开连接。

    • 一、握手:
              客户端发起WebSocket连接时,通过向服务器发送一个特殊的HTTP请求头(Handshake)来建立连接。服务器检查请求头中的特定字段,确认支持WebSocket协议后,发送特殊的HTTP响应头(Acknowledgement)进行握手确认。
    • 二、数据传输:
              握手成功后,双方建立了WebSocket连接,可以进行后续的数据传输。客户端和服务器可以通过该连接进行双向的实时数据传输,消息以帧的形式进行传输。
    • 三、断开连接:
              当连接不再需要时,客户端或服务器可以发起关闭连接的请求。双方会交换特殊的关闭帧,以协商关闭连接,并确保双方都接收到了关闭请求。
    连接长度通信模式前缀底层
    Http短连接单向(请求-响应)httpTCP连接
    Web Socket长连接双向(全双工通信)ws

            视频网站的弹幕、网页间的聊天、体育数据实时更新都可以通过WebSocket实现。

    入门案例

    一、编写html页面作为WebSocket客户端

    <!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>
    

    二、导入WebSocket的maven坐标

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

    三、导入WebSocket服务端组件WebSocketServer,用于和客户端通信

            在server模块下新建WebSocket包,并创建WebSocketServer类:

    @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();
                }
            }
        }
    }

            注解@ServerEndpoint("/ws/{sid}")定义了WebSocket的连接地址,"/ws/{sid}"允许客户端在连接时传递一个会话ID(sid)。对应html页面代码中连接WebSocket节点:websocket = new WebSocket("ws://localhost:8080/ws/"+clientId),也就是说其也是根据路径进行匹配的。

            首先定义的Map内部存放的是Session(会话),其属于WebSocket包,客户端和服务端建立连接本质上就是一个会话,建立会话之后双方就可以开始通信了,而该Map就是用来存放这些会话对象的。

            而其他方法上添加的@OnOpen、@OnMessage、@OnClose注解使这些方法变成回调方法,其分别对应连接建立时、连接中、连接关闭时执行的方法。

            最后一个sendToAllClient()方法并未添加注解,也就意为着我们需要手动去调用该方法。同一时间可能有多个客户端与该服务器建立会话,这个方法会将所以的session遍历出来。

    四、导入配置类WebSocketConfiguration

            在server模块的config包下导入配置类WebSocketConfiguration,用于注册WebSocket的Bean。

    @Configuration
    public class WebSocketConfiguration {
        @Bean
        public ServerEndpointExporter serverEndpointExporter() {
            return new ServerEndpointExporter();
        }
    }

    五、测试

            在server模块下WebSocket包中新建WebSocketTask类用于测试。

    @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()));
        }
    }

            启动项目,然后点击之前编辑的html页面,该页面启动/刷新时就会与服务器建立连接,之后便可开始双向通信:

    来单提醒

            先来分析业务需求:用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式包括语音播报、弹出提示框两种。

    一、通过WebSocket实现管理端页面和服务端保持长连接状态

            刚刚导入的代码已包含相关代码,我们在登陆时前端和后端就会建立连接。同样该请求也就由nginx转发。

    二、当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息

            在OrderServiceImpl类中负责"支付成功,修改订单状态"的paySuccess()方法中调用WebSocket向客户端浏览器推送消息。要推送消息就要用到WebSocketServer的bean,因此要先注入。

            推送的消息应为json数据,同时包含的三个字段包括: type, orderId, content。其中type 为消息类型,1为来单提醒2为客户催单,orderId 为订单id,content 为消息内容。

            我们可以先将其封装到Map中,然后再转化为json格式的数据并推送。

    @Service
    @Slf4j
    public class OrderServiceImpl implements OrderService {
        @Autowired
        private WebSocketServer webSocketServer;
    
        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<String, Object> map = new HashMap<>();
            map.put("type", 1); // 消息类型,1表示来单提醒
            map.put("orderId", ordersDB.getId()); // 订单ID
            map.put("content", "订单号:" + outTradeNo); // 消息内容,包含订单号
    
            // 将消息内容转换为JSON字符串
            String json = JSON.toJSONString(map);
    
            // 通过WebSocket服务器向所有客户端发送消息
            webSocketServer.sendToAllClient(json);
    //新代码—————————————————————————————————————————
        }
    }

            重新启动项目,并在小程序端下单测试,客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报:

     

    客户催单

            同理,用户在小程序点击催单按钮后,需要第一时间通知外卖商家。通知的形式包括语音播报、弹出提示框两种。调用的方法、提交的消息都与上文相同,在此不再赘述。

            不同的是来单提醒是集成在paySuccess方法内部的,而客户催单则需要回应小程序端发起的请求。请求路径为/user/order/reminder/{id},请求方法为Get,以路径参数提交id,后端使

            回到user包下的OrderController编写方法:

    // Controller———————————————————
        @GetMapping("/reminder/{id}")
        @ApiOperation("用户催单")
        public Result reminder(@PathVariable Long id){
            orderService.remionder(id);
            return Result.success();
        }
    // Service———————————————————————
        void reminder(Long id);
    // ServiceImpl———————————————————
        @Override
        public void reminder(Long id) {
            Orders orderDB = orderMapper.getById(id);
            // 校验订单是否存在
            if (orderDB == null) {
                //抛出异常:订单状态错误
                throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
            }
            // 创建消息内容,通过WebSocket推送给客户端
            Map<String, Object> map = new HashMap<>();
            map.put("type", 2); // 消息类型,1表示来单提醒,2表示客户催单
            map.put("orderId", id); // 订单ID
            map.put("content", "订单号:" + orderDB.getNumber()); // 消息内容,包含订单号
    
            // 通过WebSocket服务器向所有客户端发送消息
            webSocketServer.sendToAllClient(JSON.toJSONString(map));
        }

    http://www.kler.cn/a/522083.html

    相关文章:

  • 58.界面参数传递给Command C#例子 WPF例子
  • A7. Jenkins Pipeline自动化构建过程,可灵活配置多项目、多模块服务实战
  • Qt文件操作
  • MATLAB算法实战应用案例精讲-【数模应用】方向梯度直方图(HOG)(附python代码实现)
  • 初始JavaEE篇 —— Spring Web MVC入门(上)
  • 选择困难?直接生成pynput快捷键字符串
  • 第05章 14 绘制人脸部的PolyData并使用小圆锥体来展现法线
  • Go反射指南
  • 爱的魔力转圈圈,基于carsim与simulink模拟仰望u8原地调头
  • DeepSeek-R1 是否才是 “Open” AI?
  • YOLOv11改进,YOLOv11检测头融合DynamicHead,并添加小目标检测层(四头检测),适合目标检测、分割等任务
  • 航空客户价值的数据挖掘与分析(numpy+pandas+matplotlib+scikit-learn)
  • 基于STM32的循迹小车设计与实现
  • torch.tile 手动实现 kron+矩阵乘法
  • MongoDB中常用的几种高可用技术方案及优缺点
  • 基础项目实战——3D赛车(c++)
  • 把本地搭建的hexo博客部署到自己的服务器上
  • 网络工程师 (4)存储系统
  • 21款炫酷烟花合集
  • python selenium 用法教程
  • Warm-Flow新春版:网关直连和流程图重构, 新增Ruoyi-Vue-Plus优秀开源集成案例
  • Python中容器类型的数据(下)
  • Linux 常用命令 - sort 【对文件内容进行排序】
  • (1)SpringBoot入门+彩蛋
  • JavaSE第十一天——集合框架Collection
  • java —— 面向对象(下)