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

目录

SpringTask

[1. 介绍](#1. 介绍)

[2. cron表达式](#2. cron表达式)

[3. 入门案例](#3. 入门案例)

订单状态定时处理

[1. 需求分析和设计](#1. 需求分析和设计)

[2. 代码开发](#2. 代码开发)

[3. 功能测试](#3. 功能测试)

WebSocket

[1. 介绍](#1. 介绍)

[2. HTTP协议和WebSocket协议对比](#2. HTTP协议和WebSocket协议对比)

[1. 通信模式:"单向请求 - 响应" vs "双向全双工"](#1. 通信模式:“单向请求 - 响应” vs “双向全双工”)

[2. 连接性质:"短连接(可复用)" vs "长连接(持续保持)"](#2. 连接性质:“短连接(可复用)” vs “长连接(持续保持)”)

[3. 头部开销:"每次请求大开销" vs "仅握手一次开销"](#3. 头部开销:“每次请求大开销” vs “仅握手一次开销”)

[4. 状态维护:"无状态" vs "有状态"](#4. 状态维护:“无状态” vs “有状态”)

[HTTP 适用场景:非实时、单向请求 - 响应](#HTTP 适用场景:非实时、单向请求 - 响应)

[WebSocket 适用场景:实时、双向交互](#WebSocket 适用场景:实时、双向交互)

[3. 入门案例](#3. 入门案例)

​编辑

​编辑

来单提醒

[1. 需求分析和设计](#1. 需求分析和设计)

[2. 代码开发](#2. 代码开发)

[3. 功能测试](#3. 功能测试)

客户催单

[1. 需求分析和设计](#1. 需求分析和设计)

[2. 代码开发](#2. 代码开发)

[3. 功能测试](#3. 功能测试)


SpringTask

1. 介绍

定时自动执行某段java代码

2. cron表达式

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

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

3. 入门案例

server下面新建一个task包,新建MyTask类

java 复制代码
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());
    }
}

订单状态定时处理

1. 需求分析和设计

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

2. 代码开发

OrderTask

java 复制代码
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秒触发一次

WebSocket

1. 介绍

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

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

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. 入门案例

来单提醒

1. 需求分析和设计

2. 代码开发

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

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

java 复制代码
/**
     * 支付成功回调
     *
     * @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);
    }

实际追加代码

java 复制代码
    /**
     * 支付成功,修改订单状态
     *
     * @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

java 复制代码
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

java 复制代码
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

java 复制代码
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进行内网穿透

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

java 复制代码
 notifyUrl: http://1d4c81e9.r7.vip.cpolar.cn/notify/paySuccess
    refundNotifyUrl:  http://1d4c81e9.r7.vip.cpolar.cn/notify/refundSuccess

3. 功能测试

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

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

客户催单

1. 需求分析和设计

2. 代码开发

user/OrderController

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

OrderService

java 复制代码
   /**
     * 客户催单
     * @param id
     */
    void reminder(Long id);

OrderServiceImpl

java 复制代码
    /**
     * 客户催单
     * @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. 功能测试

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

相关推荐
tan180°5 小时前
Linux系统编程守护进程(36)
linux·服务器·网络
小马哥编程5 小时前
计算机网络:以太网中的数据传输
网络·网络协议·计算机网络
小马哥编程5 小时前
计算机网络:无线局域网加密与认证方式
网络·计算机网络·安全
zzc9216 小时前
Packet Radio Network,PRNET
网络·tcp/ip·互联网·arpanet·自组网·prnet
sdszoe49229 小时前
0904网络设备配置与管理第二次授课讲义
网络·华为交换机基础
努力的小帅9 小时前
CAN通信入门
网络·stm32·单片机·嵌入式硬件·stm32c8t6·can总线通信
久绊A10 小时前
指定端口-SSH连接的目标(告别 22 端口暴力破解)
linux·网络·ssh
bantinghy15 小时前
Linux系统TCP/IP网络参数优化
linux·网络·tcp/ip
wanhengidc16 小时前
云手机可以息屏挂手游吗?
运维·网络·安全·游戏·智能手机