苍穹外卖 day10:Spring Task:订单定时处理。Websocket:来单提醒和 客户催单

苍穹外卖day10

功能实现:订单状态定时处理

思考1:如果用户一直卡在支付界面怎么处理

思考2:如果用户已收到外卖,但是管理员却没有及时点击完成订单该怎么办

解决:需要使用一个可以定时处理任务的方法

1.Spring Task

1.1 介绍

Spring Task是Spring框架提供的任务调度工具,可以按照约定时间对任务进行执行

应用场景:

1)火车自动抢票

2)自动的短信通知

3)定时闹钟

总之,一切需要定时任务的执行都可以使用Spring Task

1.2 cron表达式

介绍:cron表达式实际上是一个字符串,可以自定义设定触发时间

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

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

举例:2025年6月8日14点28分56秒对应的cron表达式是56 28 14 8 6 ? 2025

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

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

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

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

可以直接使用该网站进行生成,一般不自己手写

通配符:

* 表示所有值;

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

- 表示一个指定的范围;

, 表示附加一个可能值;

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

常用cron表达式例子

(1)0/2 * * * * ?   表示每2秒 执行任务

(1)0 0/2 * * * ?    表示每2分钟 执行任务

(1)0 0 2 1 * ?   表示在每月的1日的凌晨2点调整任务

(2)0 15 10 ? * MON-FRI   表示周一到周五每天上午10:15执行作业

(3)0 15 10 ? 6L 2002-2006   表示2002-2006年的每个月的最后一个星期五上午10:15执行

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

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

1.3 入门案例

1.3.1 导入坐标

实际上由于该框架体量较小,该坐标spring - context 已经存在于 spring-boot-starter中 

1.3.2 注解 @EnableScheduling 

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

1.3.3 实例

在sky-serve模块中的com.sky包下新建一个task包,创建一个MyTask类

代码如下:

package com.sky.task;


import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.util.Date;

/**
 * 自定义定时任务类
 */
@Component
@Slf4j
public class Mytask {

    @Scheduled(cron = "0/5 * * * * ?")
    public void exe(){
        log.info("任务开始执行:{}",new Date());
    }
}

1.3.3:功能测试

启动服务,查看日志

每隔5s执行一次

2.订单状态定时处理

2.1 需求分析

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

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

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

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

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

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

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

2.2 代码开发(该需求可以使用redis来减轻数据库压力,详情见此处

(1)自定义定时任务类OrderTask

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;
    /**
     * 处理超时订单的方法
     */
    @Scheduled(cron = "0 * * * * ?")//每一分钟触发
    public void processTimeoutOrder(){
        log.info("处理超时订单,{}", LocalDateTime.now());
        LocalDateTime time = LocalDateTime.now().plusMinutes(-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 * * ?")//每日凌晨一点
    public void processDeliveryOrder(){
        log.info("定时处理处于派送中的订单,{}",LocalDateTime.now());
        LocalDateTime time = LocalDateTime.now().plusMinutes(-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);
            }
        }
    }
}

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

    /**
     * 查询状态和当前时间
     * @param status
     * @param orederTime
     * @return
     */
    @Select("select * from orders where status = #{status} and order_time < (#{orderTime})")
    List<Orders> getByStatusAndOrderTimeLT(@Param("status")Integer status,@Param("orderTime")LocalDateTime orderTime);
2.3 功能测试

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

  • 查看控制台sql

  • 查看数据库中数据变化

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

功能测试:来单提醒和客户催单

3.WebSocket

3.1 介绍

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

HTTP协议和WebSocket协议对比:

  • HTTP是短连接

  • WebSocket是长连接

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

  • WebSocket支持双向通信

  • HTTP和WebSocket底层都是TCP连接

思考:既然WebSocket这么好用,是否可以代替HTTP呢?

WebSocket的劣势包括:

  • 需要浏览器和服务器都支持: WebSocket是一种相对新的技术,需要浏览器和服务器都支持。一些旧的浏览器和服务器可能不支持WebSocket。

  • 需要额外的开销: WebSocket需要在服务器上维护长时间的连接,这需要额外的开销,包括内存和CPU。

  • 安全问题: 由于WebSocket允许服务器主动向客户端发送数据,可能会存在安全问题。服务器必须保证只向合法的客户端发送数据。

应用场景:

1)视频弹幕

2)网页聊天

3)体育实况更新

总之,WebSocket用于不用刷新页面,可以实时展示数据的情况

3.2 入门实例

3.2.1 案例

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

页面展示:

实现步骤:

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

2). 导入WebSocket的maven坐标

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

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

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

3.2.2 代码开发

1)导入maven坐标

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

2)定义WebSocketServer组件(资料以提供)

package com.sky.websocket;

import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

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

}

3)定义WebSocketConfiguration配置类

package com.sky.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

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

}

4)定义WebSocketTask任务类

package com.sky.task;

import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

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

5)定义HTML页面(资料中已提供)

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

WebSocketServe开头的这段代码

@ServerEndpoint("/ws/{sid}")

对应着html的这段代码,这样就可以连接客户端与服务端了

ws://localhost:8080/ws/"+clientId

3.2.3 功能测试

启动服务

建立连接:

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

点击关闭连接:

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 代码开发

将WebSocketServer对象注入OrderServiceImpl中。

本该修改的是paySucess方法,但因为跳过了支付功能(详情见此处)所以在payMent方法处补充,将订单状态设置成待接单,并转换为JSON格式,代码如下:

    /**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();
        User user = userMapper.getByOpenId(String.valueOf(userId));

        //调用微信支付接口,生成预支付交易单
        /*JSONObject jsonObject = weChatPayUtil.pay(
                ordersPaymentDTO.getOrderNumber(), //商户订单号
                new BigDecimal(0.01), //支付金额,单位 元
                "苍穹外卖订单", //商品描述
                user.getOpenid() //微信用户的openid
        );

        if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
            throw new OrderBusinessException("该订单已支付");
        }*/

        JSONObject jsonObject = new JSONObject();
        jsonObject.put("code","ORDERPAID");
        OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
        vo.setPackageStr(jsonObject.getString("package"));

        //为替代微信支付成功后的数据库订单状态更新,多定义一个方法进行修改
        Integer OrderPaidstatus =Orders.PAID;
        //支付状态,已支付
        Integer Orderstatus = Orders.TO_BE_CONFIRMED; //订单状态,待接单
        //发现没有将支付时间 check out属性赋值,所以在这里更新
        LocalDateTime check_out_time = LocalDateTime.now();
        //获取订单号码
        String orderNumber =ordersPaymentDTO.getOrderNumber();
        log.info("调用updatestatus,用于替换微信支付更新数据库状态的问题");
        orderMapper.updatestatus(Orderstatus, OrderPaidstatus, check_out_time, orderNumber);



        //根据订单号和用户id查询
        Orders ordersDB = orderMapper.getByNumberAndUserId(orderNumber,userId);
        Orders orders = Orders.builder()
                .id(ordersDB.getId())
                .status(Orders.TO_BE_CONFIRMED)
                .payStatus(Orders.PAID)
                .checkoutTime(LocalDateTime.now())
                .build();
        orderMapper.update(orders);
        //通过websocket向客户端浏览器推送消息 type orderId content
        Map map = new HashMap();
        map.put("type",1);
        map.put("orderId",orders.getId());
        map.put("content","订单号:"+ orderNumber);
        String json = JSON.toJSONString(map);
        webSocketServer.sendToAllClient(json);


        return vo;
    }
4.3 功能测试

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

用户提交订单:

前端提醒:

注意:

1)因为音频是在前端设定好的,所以可以直接使用

2)如果 音频一直响的话,将WebSocketTask中设置的定时任务给注释了

3)如果没声音的话,可能是浏览器的音频设置问题(详情见此处

5.客户催单

5.1 需求分析和设计

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

  • 语音播报

  • 弹出提示框

设计思路:

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

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

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

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

    • orderId 为订单id

    • content 为消息内容

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

接口设计(催单):

5.2 代码开发

5.2.1 Controller层

    /**
     * 催单
     * @param id
     * @return
     */
    @GetMapping("/reminder/{id}")
    @ApiOperation("催单")
    public Result reminder(@PathVariable Long id){
        log.info("催单的id:{}",id);
        orderService.reminder(id);
        return Result.success();
    }

5.2.2 Srevice层

    /**
     * 客户催单
     * @param id
     */
    void reminder(Long id);

5.2.3 ServiceImpl层

    /**
     * 客户催单
     * @param id
     */
    @Override
    public void reminder(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(Math.toIntExact(id));

        // 校验订单是否存在
        if (ordersDB == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Map map = new HashMap();
        map.put("type",2);
        map.put("orderId",id);
        map.put("content","订单号:"+ordersDB.getNumber());

        //通过websocket向客户端浏览器推送消息
        webSocketServer.sendToAllClient(JSON.toJSONString(map));

    }

5.2.4 Mapper层

    /**
     * 查询id的全部数据
     * @param id
     * @return
     */
    @Select("select * from orders where id = #{id}")
    Orders getById(Integer id);
5.3 功能测试

用户点击催单:

管理端响起催单请求:

<think>嗯,用户想了解苍穹外卖项目中第10天关于WebSocket的实现教程或细节。我需要先回顾提供的三个引用内容,看看里面有没有相关的信息。 引用[1]提到课程内容包括Spring Task订单状态定时处理WebSocket的来提醒客户。这说明第10天的课程确实涉及WebSocket的应用。引用[2][3]都提到了在OrderServiceImpl中注入WebSocketServer对象,并在支付成功后发送提醒。看来用户需要的是具体的实现步骤,比如如何配置WebSocket、服务端客户端的代码,以及业务逻辑的处理。 用户的问题可能包括如何建立WebSocket连接,服务端如何接收处理消息,以及如何在前端展示提醒。需要分步骤解释,可能需要包括代码示例,如配置类、服务端端点、消息发送的时机等。同时,要注意引用中的代码片段,比如在paySuccess方法中加入发送消息的代码,这部分需要详细说明。 另外,用户可能还想知道WebSocket订单提醒客户中的具体应用场景,以及可能的优化方法。比如,如何确保消息的可靠传递,或者如何处理大量并发连接。不过根据引用内容,可能主要聚焦在基础实现上,所以先回答核心实现步骤,再生成相关问题。</think>### 苍穹外卖项目Day10 WebSocket实现详解 根据引用内容,苍穹外卖项目第10天通过WebSocket实现了**来提醒****客户**功能,以下是具体实现步骤与逻辑: --- ### 一、WebSocket核心实现步骤 #### 1. **WebSocket服务端配置** ```java @ServerEndpoint("/websocket/{sid}") @Component public class WebSocketServer { // 存储会话对象商户ID的映射 private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { sessionMap.put(sid, session); // 商户连接时绑定会话 } @OnMessage public void onMessage(String message) { // 处理客户端发送的消息 } @OnClose public void onClose(@PathParam("sid") String sid) { sessionMap.remove(sid); // 断开连接时移除会话 } } ``` [^3] #### 2. **订单支付成功后触发提醒** 在支付成功逻辑中注入WebSocket服务,向指定商户推送消息: ```java // OrderServiceImpl.java @Autowired private WebSocketServer webSocketServer; public void paySuccess(String orderId) { // 更新订单状态... // 通过WebSocket推送新订单提醒 webSocketServer.sendToAllClient("新订单:" + orderId + " 已支付"); } ``` [^2] #### 3. **前端接收消息(示例)** ```javascript let socket = new WebSocket('ws://localhost:8080/websocket/商户ID'); socket.onmessage = function(event) { alert('系统提醒:' + event.data); // 弹窗显示或新订单 }; ``` --- ### 二、关键业务场景实现 #### 1. **来提醒流程 $$流程图:商户端WebSocket连接 \rightarrow 支付成功触发推送 \rightarrow 前端弹窗/声音提醒$$ [^1] #### 2. **客户处理** 客户发起时,系统通过WebSocket实时通知对应商户: ```java // 在服务方法中 webSocketServer.sendToClient(商户ID, "订单" + orderId + "客户"); ``` --- ### 三、技术要点总结 | 技术点 | 实现方式 | 引用来源 | |--------------|-----------------------------------|------------| | 连接绑定 | 商户ID与会话映射存储 | | | 消息广播 | `sendToAllClient()`方法遍历会话 | | | 状态同步 | 支付成功后立即触发WebSocket推送 | | ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值