目录

苍穹外卖Day10-订单状态定时处理来单提醒客户催单SpringTaskWebSocketcron表达式

苍穹外卖Day10 | 订单状态定时处理、来单提醒、客户催单、SpringTask、WebSocket、cron表达式


SpringTask

1. 介绍

https://i-blog.csdnimg.cn/direct/40c941dee2534654951992a8c86ee1c1.png

定时自动执行某段java代码

https://i-blog.csdnimg.cn/direct/4c83d12025de4751964a2109b8679f77.png

2. cron表达式

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

https://i-blog.csdnimg.cn/direct/5c4b5fbba3234be181a38397eb3a4142.png

日和周通常只能定义一个,另一个写成?

https://i-blog.csdnimg.cn/direct/efa0bf58946c41999548b0ecd512940d.png

https://i-blog.csdnimg.cn/direct/8962397454cd4b0aac0b077d3c057556.png

3. 入门案例

https://i-blog.csdnimg.cn/direct/e64f5515813a44cd9356c774e872232e.png

server下面新建一个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 executeTask(){
        log.info("定时任务开始执行:{}", new Date());
    }
}

https://i-blog.csdnimg.cn/direct/9db295897e5c456795394b05ec136acb.png

订单状态定时处理

https://i-blog.csdnimg.cn/direct/231b8e3e8f6949148ec2651809fa1df1.png

1. 需求分析和设计

https://i-blog.csdnimg.cn/direct/c52d2a38819c4d129d41c21f9390afeb.png

https://i-blog.csdnimg.cn/direct/d0f15a9e724e4a3c906f2bdcdbeb12a5.png

不需要接口,不需要前端发送什么请求

2. 代码开发

OrderTask

package com.sky.task;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.weaver.ast.Or;
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());

        // 获取减去15min之后的时间
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);

        // 查询超时订单--当前处于待付款状态 且 下单时间已经超过15分钟
        // select * from orders where status = ? and order_time < (当前时间 - 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 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);
            }
        }
    }
}

3. 功能测试

测试这种和其他功能不同,其他功能可以通过前后端联调或者接口文档测试,测试订单状态定时处理可以先临时改掉cron表达式,测试无误后再改回去

两个都修改成每5秒触发一次

https://i-blog.csdnimg.cn/direct/db8d4da965944e21b56d806998b117b6.png

WebSocket

1. 介绍

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

HTTP做不到这个效果,HTTP是请求-响应模式

https://i-blog.csdnimg.cn/direct/99f4745116dd4922b77687e64a8af1bc.png

https://i-blog.csdnimg.cn/direct/750cd311385e40559099314079cc5b9d.png

https://i-blog.csdnimg.cn/direct/8f35fb8138204375afea8ea9addb312d.png

2. HTTP协议和WebSocket协议对比

HTTP 协议和 WebSocket 协议都是基于 TCP 传输层的应用层协议,但其设计目标、通信模式、适用场景存在本质差异 ——HTTP 是 “单向请求 - 响应” 的短连接协议,WebSocket 是 “双向实时通信” 的长连接协议。

对比维度HTTP 协议(超文本传输协议)WebSocket 协议(WebSocket 协议)
1. 通信模式单向请求 - 响应(客户端发请求,服务端回响应)双向全双工(客户端 / 服务端可随时主动发消息)
2. 连接性质短连接(请求完成后断开,需复用则靠 Keep-Alive)长连接(建立后持续保持,直到主动关闭)
3. 发起方仅客户端可发起请求(服务端不能主动发消息给客户端)客户端发起握手建立连接,之后双方均可主动发消息
4. 头部开销每次请求 / 响应头部大(如 HTTP/1.1 头部通常几百字节)仅握手阶段用 HTTP 头部,后续通信头部极小(仅 2-10 字节)
5. 状态维护无状态(服务端不保存客户端上下文,需靠 Cookie/Session 维持状态)有状态(连接建立后,服务端可识别客户端身份,无需重复验证)
6. 实时性低(需客户端轮询 / 长轮询获取更新,有延迟)高(消息实时推送,延迟毫秒级)
7. 适用场景静态资源获取、普通接口请求(如网页加载、数据查询)实时交互场景(如聊天、直播、实时通知)
8. 协议标识URL 以 http:///https:// 开头URL 以 ws:///wss://(加密)开头
1. 通信模式:“单向请求 - 响应” vs “双向全双工”

这是两者最核心的差异,直接决定了 “能否实时通信”:

  • HTTP 单向请求 - 响应
    通信必须由客户端先发起 “请求”(如 GET/POST 请求),服务端才能返回 “响应”;服务端无法主动向客户端推送消息—— 若客户端需获取实时更新(如实时聊天消息),只能通过 “轮询”(每隔几秒发一次请求)或 “长轮询”(客户端发请求后,服务端 hold 住连接直到有更新)实现,本质仍是 “客户端主动问、服务端被动答”。
    例:打开网页时,浏览器(客户端)发 HTTP GET 请求获取 HTML/CSS/JS,服务器返回资源后,HTTP 连接通常断开;若要获取网页的实时数据(如股票行情),需浏览器每隔 10 秒发一次 GET 请求查询最新行情。
  • WebSocket 双向全双工
    连接建立后,客户端和服务端处于 “平等地位”,双方均可随时主动向对方发送消息,无需等待对方先请求 —— 类似 “打电话”,接通后双方可随时说话,无需一方 “先提问”。
    例:微信网页版的聊天功能,通过 WebSocket 连接,当好友发消息时,微信服务器可直接将消息 “推” 给你的浏览器(无需你的浏览器主动查询),实现实时聊天。
2. 连接性质:“短连接(可复用)” vs “长连接(持续保持)”
  • HTTP 短连接与 Keep-Alive
    标准 HTTP 是 “短连接”—— 每次请求完成后,TCP 连接会断开;为减少连接建立 / 断开的开销,HTTP/1.1 引入 Connection: Keep-Alive 机制,让 TCP 连接在一定时间内(如 30 秒)复用(后续请求可复用同一连接),但本质仍是 “请求触发式” 连接,无请求时连接可能被回收,且服务端仍不能主动发消息。
    例:浏览一个包含 10 张图片的网页,浏览器会用 1-6 个复用的 TCP 连接(HTTP 连接池)依次请求图片,所有图片加载完成后,连接会在闲置一段时间后断开。
  • WebSocket 长连接
    连接通过 “HTTP 握手” 建立后,TCP 连接会持续保持(直到客户端 / 服务端主动调用 close() 关闭),期间即使没有消息传输,连接也不会被随意断开(可通过 “心跳包” 维持连接,避免被防火墙 / 路由器判定为闲置连接而断开)。
    例:直播平台的 “实时弹幕” 功能,用户打开直播间后,浏览器与直播服务器建立 WebSocket 长连接,后续所有弹幕消息(用户发送、服务器推送)都通过这个连接实时传输,连接会持续到用户关闭直播间。
3. 头部开销:“每次请求大开销” vs “仅握手一次开销”

HTTP 的性能瓶颈之一是 “头部开销”,而 WebSocket 完美解决了这一问题:

  • HTTP 头部开销大
    每次 HTTP 请求 / 响应都需要携带完整的头部(如请求行、Host、Cookie、User-Agent、Accept 等),头部大小通常几百字节,甚至超过实际传输的 “业务数据”(如一个查询用户信息的请求,数据仅 50 字节,头部却有 300 字节);即使复用连接,每次请求仍需携带头部。
  • WebSocket 头部开销极小
    仅在 “建立连接的握手阶段” 使用 HTTP 头部(格式类似 HTTP 请求,用于告诉服务器 “要升级为 WebSocket 连接”),握手成功后,后续传输的 “WebSocket 帧” 头部仅 2-10 字节(包含帧类型、数据长度等核心信息),业务数据占比极高,传输效率远高于 HTTP。
    例:实时推送温度数据(每秒 1 次,每次数据 20 字节),用 HTTP 每次需额外携带 300 字节头部,总开销 320 字节 / 次;用 WebSocket 仅需 2+20=22 字节 / 次,开销仅为 HTTP 的 1/14。
4. 状态维护:“无状态” vs “有状态”
  • HTTP 无状态
    服务端不保存客户端的 “连接状态”—— 每次 HTTP 请求都是独立的,服务端无法通过连接识别客户端身份,需通过 Cookie、Session ID、Token 等额外机制让服务端记住客户端(如登录状态),增加了开发复杂度和请求开销。
  • WebSocket 有状态
    连接建立后,服务端会为每个 WebSocket 连接分配唯一标识(如 sessionId),并保存连接上下文(如客户端用户 ID、连接状态);后续双方通信时,服务端可直接通过连接标识识别客户端,无需重复传递身份信息(如 Token),简化开发且减少开销
HTTP 适用场景:非实时、单向请求 - 响应
  • 静态资源获取:网页(HTML/CSS/JS)、图片、视频、文件下载;
  • 普通接口请求:数据查询(如用户信息、商品列表)、表单提交(如登录、注册)、数据上传(如上传图片);
  • 无实时需求的业务:博客浏览、电商商品详情页、新闻阅读。
WebSocket 适用场景:实时、双向交互
  • 实时聊天:网页版微信、企业 IM(如钉钉网页版)、在线客服;
  • 实时通知:订单状态更新(如 “订单已发货” 推送)、消息提醒(如 “收到新评论”);
  • 实时数据展示:股票行情、实时监控(如设备温度 / 湿度)、直播弹幕;
  • 实时协作:在线文档协作(如腾讯文档)、多人在线游戏(如网页版小游戏)。

3. 入门案例

https://i-blog.csdnimg.cn/direct/b6fc1980c1684c1bace1767d2d17d1fb.png

https://i-blog.csdnimg.cn/direct/a7c165f2728f4be5b5a7fcbbb453830b.png

https://i-blog.csdnimg.cn/direct/18fe711f71c940768e27d0c9f6af9d7b.png

来单提醒

https://i-blog.csdnimg.cn/direct/946329c45aa24d48972cb9db37b68961.png

1. 需求分析和设计

https://i-blog.csdnimg.cn/direct/ae6f5755a5af4f8895a6783266031cf1.png

https://i-blog.csdnimg.cn/direct/3245e3251ddd41d48f0286beed5ba454.png

2. 代码开发

在notify/PayNotifyController的paySuccessNotify函数中,最后通过orderService.paySuccess(outTradeNo); 进行修改订单状态、来电提醒

所以通过OrderServiceImpl的paySuccess方法,通过websocket 向用户端推送消息

/**
     * 支付成功回调
     *
     * @param request
     */
    @RequestMapping("/paySuccess")
    public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //读取数据
        String body = readData(request);
        log.info("支付成功回调:{}", body);

        //数据解密
        String plainText = decryptData(body);
        log.info("解密后的文本:{}", plainText);

        JSONObject jsonObject = JSON.parseObject(plainText);
        String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
        String transactionId = jsonObject.getString("transaction_id");//微信支付交易号

        log.info("商户平台订单号:{}", outTradeNo);
        log.info("微信支付交易号:{}", transactionId);

        //业务处理,修改订单状态、来单提醒
        orderService.paySuccess(outTradeNo);

        //给微信响应
        responseToWeixin(response);
    }

实际追加代码

    /**
     * 支付成功,修改订单状态
     *
     * @param outTradeNo
     */
    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向客户端浏览器推送消息 type orderId content
        Map map = new HashMap();
        map.put("type", 1); // 1表示来单提醒 2表示用户催单
        map.put("orderId", ordersDB.getId());
        map.put("content", "订单号" + outTradeNo);

        String json = JSON.toJSONString(map);
        webSocketServer.sendToAllClient(json);
    }

导入代码: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);
    }

    /**
     * 收到客户端消息后调用的方法
     * 类似于controller
     * @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();
            }
        }
    }

}

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

}

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

使用cpolar进行内网穿透

https://i-blog.csdnimg.cn/direct/bcf2c834c0cf4a33b78e0a35cd14333b.png

使用穿透后的地址配置application-dev,将内网穿透后的地址配置到dev文件

 notifyUrl: http://1d4c81e9.r7.vip.cpolar.cn/notify/paySuccess
    refundNotifyUrl:  http://1d4c81e9.r7.vip.cpolar.cn/notify/refundSuccess

3. 功能测试

https://i-blog.csdnimg.cn/direct/69dd545528e241d0b13bb4c03d827e7b.png

前端页面先请求到nginx,由nginx反向代理请求、转发后端服务器,需要提前在nginx配置好相关路径

https://i-blog.csdnimg.cn/direct/95ea8b23c9344f25a004a981cc844611.png

https://i-blog.csdnimg.cn/direct/336bc2aed8d74201bf1e1ee7b26fc682.png

将schedual五秒定时提醒的注释给注释掉,重新登陆管理员身份,可以正常在用户下单后播报语音提醒

客户催单

https://i-blog.csdnimg.cn/direct/50d13a915a5644b6b676519a66765b95.png

1. 需求分析和设计

https://i-blog.csdnimg.cn/direct/6d9552ee463d42b8ac6e39746429bfa3.png

https://i-blog.csdnimg.cn/direct/7b2c606a63784d1e9f8d77524c5def5c.png

https://i-blog.csdnimg.cn/direct/9d161e2088f345d8aa5ed2c967f87b48.png

2.  代码开发

user/OrderController

   /**
     * 客户催单
     * @param id
     * @return
     */
    @GetMapping("/reminder/{id}")
    @ApiOperation("客户催单")
    public Result reminder(@PathVariable("id") Long id){
        orderService.reminder(id);
        return Result.success();
    }

OrderService

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

OrderServiceImpl

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

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

        Map map = new HashMap();
        map.put("type", 2); //1表示来单提醒 2表示客户催单
        map.put("orderId", id);
        map.put("content", "订单号:" + ordersDB.getNumber());

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

3. 功能测试

在用户端点击催单按钮可以收到语音播报催单提醒