• 苍穹外卖 -- day10- Spring Task- 订单状态定时处理- WebSocket- 来单提醒- 客户催单


    苍穹外卖-day10

    功能实现:订单状态定时处理来单提醒客户催单

    订单状态定时处理:

    来单提醒:

    客户催单:

    1. Spring Task

    1.1 介绍

    Spring TaskSpring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

    定位:定时任务框架

    作用:定时自动执行某段Java代码

    应用场景:

    1). 信用卡每月还款提醒

    2). 银行贷款每月还款提醒

    3). 火车票售票系统处理未支付订单

    4). 入职纪念日为用户发送通知

    强调:只要是需要定时处理的场景都可以使用Spring Task

    1.2 cron表达式

    cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间

    构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义

    每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

    举例:

    2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022

    说明:一般的值不同时设置,其中一个设置,另一个用?表示。

    比如:描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。

    为了描述这些信息,提供一些特殊的字符。这些具体的细节,我们就不用自己去手写,因为这个cron表达式,它其实有在线生成器。

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

    可以直接在这个网站上面,只要根据自己的要求去生成corn表达式即可。所以一般就不用自己去编写这个表达式。

    通配符:

    * 表示所有值;

    ? 表示未说明的值,即不关心它为何值;

    - 表示一个指定的范围;

    , 表示附加一个可能值;

    / 符号前表示开始时间,符号后表示每次递增的值;

    cron表达式案例:

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

    0 */1 * * * ? 每隔1分钟执行一次

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

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

    0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发

    0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发

    0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

    0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

    0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

    1.3 入门案例

    1.3.1 Spring Task使用步骤

    1). 导入maven坐标 spring-context(已存在)

    spring - context 在 spring-boot-starter中 ; 

    2). 启动类添加注解 @EnableScheduling 开启任务调度

    在苍穹外卖项目中,就是加在SkyApplication上 :

    3). 自定义定时任务类

    1.3.2 代码开发

    编写定时任务类:

    进入sky-server模块中 : 

    代码如下 : 

    1. package com.sky.task;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.scheduling.annotation.Scheduled;
    4. import org.springframework.stereotype.Component;
    5. import java.util.Date;
    6. /**
    7. * 自定义定时任务类
    8. */
    9. @Component
    10. @Slf4j
    11. public class MyTask {
    12.    /**
    13.     * 定时任务 每隔5秒触发一次
    14.     */
    15.    @Scheduled(cron = "0/5 * * * * ?")
    16.    public void executeTask(){
    17.        log.info("定时任务开始执行:{}",new Date());
    18.   }
    19. }

    开启任务调度:

    启动类添加注解 @EnableScheduling

    1. package com.sky;
    2. import lombok.extern.slf4j.Slf4j;
    3. import org.springframework.boot.SpringApplication;
    4. import org.springframework.boot.autoconfigure.SpringBootApplication;
    5. import org.springframework.cache.annotation.EnableCaching;
    6. import org.springframework.scheduling.annotation.EnableScheduling;
    7. import org.springframework.transaction.annotation.EnableTransactionManagement;
    8. @SpringBootApplication
    9. @EnableTransactionManagement //开启注解方式的事务管理
    10. @Slf4j
    11. @EnableCaching
    12. @EnableScheduling
    13. public class SkyApplication {
    14.    public static void main(String[] args) {
    15.        SpringApplication.run(SkyApplication.class, args);
    16.        log.info("server started");
    17.   }
    18. }

    1.3.3 功能测试

    启动服务,查看日志

    每隔5秒执行一次。

    2.订单状态定时处理

    2.1 需求分析

    用户下单后可能存在的情况:

    • 下单后未支付,订单一直处于“待支付”状态

    • 用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态

    支付超时的订单如何处理? 派送中的订单一直不点击完成如何处理?

    对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:

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

    • 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”

    2.2 代码开发

    1). 自定义定时任务类OrderTask(待完善):

    1. package com.sky.task;
    2. /**
    3. * 自定义定时任务,实现订单状态定时处理
    4. */
    5. @Component
    6. @Slf4j
    7. public class OrderTask {
    8.    @Autowired
    9.    private OrderMapper orderMapper;
    10.    /**
    11.     * 处理支付超时订单
    12.     */
    13.    @Scheduled(cron = "0 * * * * ?")
    14.    public void processTimeoutOrder(){
    15.        log.info("处理支付超时订单:{}", new Date());
    16.   }
    17.    /**
    18.     * 处理“派送中”状态的订单
    19.     */
    20.    @Scheduled(cron = "0 0 1 * * ?")
    21.    public void processDeliveryOrder(){
    22.        log.info("处理派送中订单:{}", new Date());
    23.   }
    24. }

    2). 在OrderMapper接口中扩展方法:

    1. /**
    2.     * 根据状态和下单时间查询订单
    3.     * @param status
    4.     * @param orderTime
    5.     */
    6.    @Select("select * from orders where status = #{status} and order_time < #{orderTime}")
    7.    List<Orders> getByStatusAndOrdertimeLT(Integer status, LocalDateTime orderTime);
     
    

    3). 完善定时任务类的processTimeoutOrder方法:

    1. /**
    2.     * 处理支付超时订单
    3.     */
    4.    @Scheduled(cron = "0 * * * * ?")
    5.    public void processTimeoutOrder(){
    6.        log.info("处理支付超时订单:{}", new Date());
    7.        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
    8.        // select * from orders where status = 1 and order_time < 当前时间-15分钟
    9.        List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time);
    10.        if(ordersList != null && ordersList.size() > 0){
    11.            ordersList.forEach(order -> {
    12.                order.setStatus(Orders.CANCELLED);
    13.                order.setCancelReason("支付超时,自动取消");
    14.                order.setCancelTime(LocalDateTime.now());
    15.                orderMapper.update(order);
    16.           });
    17.       }
    18.   }

    4). 完善定时任务类的processDeliveryOrder方法:

    1. /**
    2.     * 处理“派送中”状态的订单
    3.     */
    4.    @Scheduled(cron = "0 0 1 * * ?")
    5.    public void processDeliveryOrder(){
    6.        log.info("处理派送中订单:{}", new Date());
    7.        // select * from orders where status = 4 and order_time < 当前时间-1小时
    8.        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
    9.        List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time);
    10.        if(ordersList != null && ordersList.size() > 0){
    11.            ordersList.forEach(order -> {
    12.                order.setStatus(Orders.COMPLETED);
    13.                orderMapper.update(order);
    14.           });
    15.       }
    16.   }

    2.3 功能测试

    可以通过如下方式进行测试:

    • 查看控制台sql

    • 查看数据库中数据变化

    可以先将每个任务间隔改成几秒触发一次,方便测试 , 然后可以手动改一下数据库中订单状态,方便测试 ;

    3. WebSocket

    3.1 介绍

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

    HTTP协议和WebSocket协议对比:

    • HTTP是短连接

    • WebSocket是长连接

    • HTTP通信是单向的,基于请求响应模式

    • WebSocket支持双向通信

    • HTTP和WebSocket底层都是TCP连接

    思考:既然WebSocket支持双向通信,功能看似比HTTP强大,那么我们是不是可以基于WebSocket开发所有的业务功能?

    WebSocket缺点:

    服务器长期维护长连接需要一定的成本 各个浏览器支持程度不一 WebSocket 是长连接,受网络限制比较大,需要处理好重连

    结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用

    WebSocket应用场景:

    1). 视频弹幕

    2). 网页聊天

    3). 体育实况更新

    4). 股票基金报价实时更新

    3.2 入门案例

    3.2.1 案例分析

    需求:实现浏览器与服务器全双工通信。浏览器既可以向服务器发送消息,服务器也可主动向浏览器推送消息。

    效果展示:

    实现步骤:

    1). 直接使用websocket.html页面作为WebSocket客户端

    2). 导入WebSocket的maven坐标

    3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信

    4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件

    5). 导入定时任务类WebSocketTask,定时向客户端推送数据

    3.2.2 代码开发

    1). 定义websocket.html页面(资料中已提供)

    1. <!DOCTYPE HTML>
    2. <html>
    3. <head>
    4.    <meta charset="UTF-8">
    5.    <title>WebSocket Demo</title>
    6. </head>
    7. <body>
    8.    <input id="text" type="text" />
    9.    <button onclick="send()">发送消息</button>
    10.    <button onclick="closeWebSocket()">关闭连接</button>
    11.    <div id="message">
    12.    </div>
    13. </body>
    14. <script type="text/javascript">
    15.    var websocket = null;
    16.    var clientId = Math.random().toString(36).substr(2);
    17.    //判断当前浏览器是否支持WebSocket
    18.    if('WebSocket' in window){
    19.        //连接WebSocket节点
    20.        websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
    21.   }
    22.    else{
    23.        alert('Not support websocket')
    24.   }
    25.    //连接发生错误的回调方法
    26.    websocket.onerror = function(){
    27.        setMessageInnerHTML("error");
    28.   };
    29.    //连接成功建立的回调方法
    30.    websocket.onopen = function(){
    31.        setMessageInnerHTML("连接成功");
    32.   }
    33.    //接收到消息的回调方法
    34.    websocket.onmessage = function(event){
    35.        setMessageInnerHTML(event.data);
    36.   }
    37.    //连接关闭的回调方法
    38.    websocket.onclose = function(){
    39.        setMessageInnerHTML("close");
    40.   }
    41.    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    42.    window.onbeforeunload = function(){
    43.        websocket.close();
    44.   }
    45.    //将消息显示在网页上
    46.    function setMessageInnerHTML(innerHTML){
    47.        document.getElementById('message').innerHTML += innerHTML + '
      '
      ;
    48.   }
    49.    //发送消息
    50.    function send(){
    51.        var message = document.getElementById('text').value;
    52.        websocket.send(message);
    53.   }
    54. //关闭连接
    55.    function closeWebSocket() {
    56.        websocket.close();
    57.   }
    58. </script>
    59. </html>

    2). 导入maven坐标

    在sky-server模块pom.xml中已定义

    1. <dependency>
    2. <groupId>org.springframework.boot</groupId>
    3. <artifactId>spring-boot-starter-websocket</artifactId>
    4. </dependency>

    3). 定义WebSocket服务端组件(资料中已提供)

    直接导入到sky-server模块即可

    1. package com.sky.websocket;
    2. import org.springframework.stereotype.Component;
    3. import javax.websocket.OnClose;
    4. import javax.websocket.OnMessage;
    5. import javax.websocket.OnOpen;
    6. import javax.websocket.Session;
    7. import javax.websocket.server.PathParam;
    8. import javax.websocket.server.ServerEndpoint;
    9. import java.util.Collection;
    10. import java.util.HashMap;
    11. import java.util.Map;
    12. /**
    13. * WebSocket服务
    14. */
    15. @Component
    16. @ServerEndpoint("/ws/{sid}")
    17. public class WebSocketServer {
    18. //存放会话对象
    19. private static Map<String, Session> sessionMap = new HashMap();
    20. /**
    21. * 连接建立成功调用的方法
    22. */
    23. @OnOpen
    24. public void onOpen(Session session, @PathParam("sid") String sid) {
    25. System.out.println("客户端:" + sid + "建立连接");
    26. sessionMap.put(sid, session);
    27. }
    28. /**
    29. * 收到客户端消息后调用的方法
    30. *
    31. * @param message 客户端发送过来的消息
    32. */
    33. @OnMessage
    34. public void onMessage(String message, @PathParam("sid") String sid) {
    35. System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    36. }
    37. /**
    38. * 连接关闭调用的方法
    39. *
    40. * @param sid
    41. */
    42. @OnClose
    43. public void onClose(@PathParam("sid") String sid) {
    44. System.out.println("连接断开:" + sid);
    45. sessionMap.remove(sid);
    46. }
    47. /**
    48. * 群发
    49. *
    50. * @param message
    51. */
    52. public void sendToAllClient(String message) {
    53. Collection<Session> sessions = sessionMap.values();
    54. for (Session session : sessions) {
    55. try {
    56. //服务器向客户端发送消息
    57. session.getBasicRemote().sendText(message);
    58. } catch (Exception e) {
    59. e.printStackTrace();
    60. }
    61. }
    62. }
    63. }

    4). 定义配置类,注册WebSocket的服务端组件(从资料中直接导入即可)

    1. package com.sky.config;
    2. import org.springframework.context.annotation.Bean;
    3. import org.springframework.context.annotation.Configuration;
    4. import org.springframework.web.socket.server.standard.ServerEndpointExporter;
    5. /**
    6. * WebSocket配置类,用于注册WebSocket的Bean
    7. */
    8. @Configuration
    9. public class WebSocketConfiguration {
    10.    @Bean
    11.    public ServerEndpointExporter serverEndpointExporter() {
    12.        return new ServerEndpointExporter();
    13.   }
    14. }

    5). 定义定时任务类,定时向客户端推送数据(从资料中直接导入即可)

    1. package com.sky.task;
    2. import com.sky.websocket.WebSocketServer;
    3. import org.springframework.beans.factory.annotation.Autowired;
    4. import org.springframework.scheduling.annotation.Scheduled;
    5. import org.springframework.stereotype.Component;
    6. import java.time.LocalDateTime;
    7. import java.time.format.DateTimeFormatter;
    8. @Component
    9. public class WebSocketTask {
    10.    @Autowired
    11.    private WebSocketServer webSocketServer;
    12.    /**
    13.     * 通过WebSocket每隔5秒向客户端发送消息
    14.     */
    15.    @Scheduled(cron = "0/5 * * * * ?")
    16.    public void sendMessageToClient() {
    17.        webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
    18.   }
    19. }

    3.2.3 功能测试

    启动服务,打开websocket.html页面

    建立连接 :

    浏览器向服务器发送数据:

    服务器向浏览器间隔5秒推送数据:

    点击关闭连接 : 

    建立新的连接 :

    刷新一下页面就好了 ;

    4. 来单提醒

    4.1 需求分析和设计

    用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:

    • 语音播报

    • 弹出提示框

    设计思路:

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

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

    • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报

    • 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content

      • type 为消息类型,1为来单提醒 2为客户催单

      • orderId 为订单id

      • content 为消息内容

    注意 : 

    • 通过WebSocket实现管理端页面和服务端保持长连接状态 : 这个通过WebSocketServer.java实现与客户端进行连接 ;而前端已近通过一些js代码实现与服务端进行连接 ;

    • 通过检查发现 : 

           前端请求的地址是红线标注的那一段,而后端是8080端口。原因  : 前端先请求到nginx服务器,然后在nigix服务器中做了反向代理 ,将请求转发到后端的tomcat服务器; 

    可以在nigix.conf(配置文件中查看) : 

     

    前端发送请求后 : 

     

    表示前后端已经进行了长连接,握好了手 ;

     

    4.2 代码开发

    在OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:

    1. @Autowired
    2.    private WebSocketServer webSocketServer;
    3. /**
    4.     * 支付成功,修改订单状态
    5.     *
    6.     * @param outTradeNo
    7.     */
    8.    public void paySuccess(String outTradeNo) {
    9.        // 当前登录用户id
    10.        Long userId = BaseContext.getCurrentId();
    11.        // 根据订单号查询当前用户的订单
    12.        Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
    13.        // 根据订单id更新订单的状态、支付方式、支付状态、结账时间
    14.        Orders orders = Orders.builder()
    15.               .id(ordersDB.getId())
    16.               .status(Orders.TO_BE_CONFIRMED)
    17.               .payStatus(Orders.PAID)
    18.               .checkoutTime(LocalDateTime.now())
    19.               .build();
    20.        orderMapper.update(orders);
    21. //
    22.        Map map = new HashMap();
    23.        map.put("type", 1);//消息类型,1表示来单提醒
    24.        map.put("orderId", orders.getId());
    25.        map.put("content", "订单号:" + outTradeNo);
    26.        //通过WebSocket实现来单提醒,向客户端浏览器推送消息
    27.        webSocketServer.sendToAllClient(JSON.toJSONString(map));
    28.        ///
    29.   }

    然后这是老师给出的代码, 因为我们个人不能够使用微信支付功能,我们的paysuccess方法根本就调用不到,那么我们可以直接将只要顾客点击支付,那么就直接将订单状态设置成待接单,具体怎么改可以看我上一天的博客 (其实也就是直接将paySuccess中的代码移到payment中,只要1前端点击支付,那么直接修改订单状态为"待接单");

    然后我们将这一段代码进行改写加入到payment方法中 :

    1. //
    2. Map map = new HashMap();
    3. map.put("type", 1);//消息类型,1表示来单提醒
    4. map.put("orderId", orders.getId());
    5. map.put("content", "订单号:" + outTradeNo);
    6. //通过WebSocket实现来单提醒,向客户端浏览器推送消息
    7. webSocketServer.sendToAllClient(JSON.toJSONString(map));
    8. ///

    4.3 功能测试

    客户端与服务端建立长连接 : 

    小程序下单 : 

    数据库修改 

     

    前端提醒 : 

    那么这个功能就算完成了 ;

    关于提示音一直响个不停,原因是在WebSocketTask中设置了每隔x秒,重复发送,我们给那个注释起来就ok了;

    5. 客户催单

    5.1 需求分析和设计

    用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。通知的形式有如下两种:

    • 语音播报

    • 弹出提示框

    设计思路:

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

    • 当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息

    • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content

      • type 为消息类型,1为来单提醒 2为客户催单

      • orderId 为订单id

      • content 为消息内容

    当用户点击催单按钮时,向服务端发送请求。

    接口设计(催单):

    5.2 代码开发

    5.2.1 Controller层

    根据用户催单的接口定义,在user/OrderController中创建催单方法:

    1. /**
    2.     * 用户催单
    3.     *
    4.     * @param id
    5.     * @return
    6.     */
    7.    @GetMapping("/reminder/{id}")
    8.    @ApiOperation("用户催单")
    9.    public Result reminder(@PathVariable("id") Long id) {
    10.        orderService.reminder(id);
    11.        return Result.success();
    12.   }

    5.2.2 Service层接口

    在OrderService接口中声明reminder方法:

    1. /**
    2.     * 用户催单
    3.     * @param id
    4.     */
    5.    void reminder(Long id);

    5.2.3 Service层实现类

    在OrderServiceImpl中实现reminder方法:

    1. /**
    2.     * 用户催单
    3.     *
    4.     * @param id
    5.     */
    6.    public void reminder(Long id) {
    7.        // 查询订单是否存在
    8.        Orders orders = orderMapper.getById(id);
    9.        if (orders == null) {
    10.            throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
    11.       }
    12.        //基于WebSocket实现催单
    13.        Map map = new HashMap();
    14.        map.put("type", 2);//2代表用户催单
    15.        map.put("orderId", id);
    16.        map.put("content", "订单号:" + orders.getNumber());
    17.        webSocketServer.sendToAllClient(JSON.toJSONString(map));
    18.   }

    5.2.4 Mapper层

    在OrderMapper中添加getById:(这个之前就已经实现过了)

    1. /**
    2.     * 根据id查询订单
    3.     * @param id
    4.     */
    5.    @Select("select * from orders where id=#{id}")
    6.    Orders getById(Long id);

    5.3 功能测试

    可以通过如下方式进行测试:

    • 查看浏览器调试工具数据交互过程

    • 前后端联调

    1). 登录管理端后台

    登录成功后,浏览器与服务器建立长连接

    查看控制台日志

    2). 用户进行催单

    用户可在订单列表或者订单详情,进行催单

    3). 查看催单提醒

    既有催单弹窗,同时语音播报

  • 相关阅读:
    46.<list链表的举列>
    hadoop 日志聚集功能配置 hadoop(十一)
    多方通信许可证有何难
    80-Java的Map集合:概述、API、遍历方式
    带着问题去分析:Spring Bean 生命周期
    再见,Ubuntu,你好,Manjaro
    【华为OD机试高分必刷题目】洗衣服(Java&Python&C++贪心算法实现)
    uni-app微信小程序使用ECharts
    接口设计规范
    重庆自考本科一般多久能拿证?
  • 原文地址:https://blog.csdn.net/ros275229/article/details/136303737