苍穹外卖--day10

目录

[Day10-01-Spring Task_介绍](#Day10-01-Spring Task_介绍)

[Day10-02-Spring Task_cron 表达式](#Day10-02-Spring Task_cron 表达式)

问题:日和周为什么不能同时出现?

[Day10-03-Spring Task_入门案例](#Day10-03-Spring Task_入门案例)

[Day10-04 - 订单状态定时处理_需求分析](#Day10-04 - 订单状态定时处理_需求分析)

问题:当前时间减去15min怎么操作?

[Day10-05 - 订单状态定时处理_代码开发](#Day10-05 - 订单状态定时处理_代码开发)

[Day10-06 - 订单状态定时处理_功能测试](#Day10-06 - 订单状态定时处理_功能测试)

Day10-07-WebSocket_介绍

Day10-08-WebSocket_入门案例_1

关键

问题:配置类的作用?

Day10-09-WebSocket_入门案例_2

[Day10-10 - 来单提醒_需求分析和设计](#Day10-10 - 来单提醒_需求分析和设计)

[Day10-11 - 来单提醒_代码开发](#Day10-11 - 来单提醒_代码开发)

[Day10-12 - 来单提醒_功能测试](#Day10-12 - 来单提醒_功能测试)

[Day10-13 - 客户催单_需求分析和设计](#Day10-13 - 客户催单_需求分析和设计)

[Day10-14 - 客户催单_代码开发](#Day10-14 - 客户催单_代码开发)

[Day10-15 - 客户催单_功能测试](#Day10-15 - 客户催单_功能测试)

总结:


Day10-01-Spring Task_介绍

Day10-02-Spring Task_cron 表达式

问题:日和周为什么不能同时出现?

因为你要是都指定的话,2022年2月10号 周六 它可能不是周六,所以日和周只需要指定一个就可以,另一个设置为?

Day10-03-Spring Task_入门案例

Day10-04 - 订单状态定时处理_需求分析

问题:当前时间减去15min怎么操作?

复制代码
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);

Day10-05 - 订单状态定时处理_代码开发

Day10-06 - 订单状态定时处理_功能测试

Day10-07-WebSocket_介绍

Day10-08-WebSocket_入门案例_1

关键

重点是知道WebSocket的实现流程,代码是固定的

问题:配置类的作用?

Day10-09-WebSocket_入门案例_2

Day10-10 - 来单提醒_需求分析和设计

Day10-11 - 来单提醒_代码开发

问题:语音播报功能前后端是怎么实现的?

一般采用第二种形式

Day10-12 - 来单提醒_功能测试

Day10-13 - 客户催单_需求分析和设计

Day10-14 - 客户催单_代码开发

Day10-15 - 客户催单_功能测试

总结:

本文介绍了SpringTask定时任务和WebSocket技术的应用。在SpringTask部分,讲解了cron表达式的使用注意事项(日和周不能同时指定),并演示了订单状态定时处理功能,包括15分钟时间差计算。WebSocket部分重点介绍了实现流程和配置类作用,并展示了来单提醒和客户催单两个实际应用案例的开发与测试过程。课程内容涵盖定时任务和实时通信两大技术要点,通过具体案例帮助理解技术实现。

Day10 知识点总结:Spring Task、WebSocket 与订单状态管理

一、Spring Task 定时任务

  1. Spring Task 基础介绍

Spring Task 是 Spring 框架提供的轻量级定时任务工具,无需依赖第三方组件(如 Quartz),即可实现简单、高效的任务调度。它支持基于 cron 表达式、固定延迟、固定频率等多种触发方式,适合处理周期性业务场景(如订单超时清理、数据统计、消息推送等)。

核心特点:

轻量易用:无需额外部署,直接集成在 Spring 容器中,通过注解即可开启定时任务。

多种触发方式:支持 cron 表达式、fixedDelay(固定延迟)、fixedRate(固定频率)、initialDelay(初始延迟)等配置。

线程池管理:默认使用单线程执行任务,可通过配置自定义线程池,提升并发处理能力。

与 Spring 生态无缝集成:可直接注入 Spring Bean,访问业务服务、数据库等资源。

  1. cron 表达式详解

cron 表达式是 Spring Task 中最灵活的触发规则,由 6~7 个空格分隔的字段组成,按顺序为:秒 分 时 日 月 周 [年](年为可选字段)。

表格

字段 允许值 允许特殊字符

秒 0-59 , - * /

分 0-59 , - * /

时 0-23 , - * /

日 1-31 , - * / ? L W

月 1-12 或 JAN-DEC , - * /

周 1-7(1 = 周日)或 SUN-SAT , - * / ? L #

年(可选) 1970-2099 , - * /

特殊字符含义:

*:匹配该字段的所有可能值(如秒字段为 * 表示每秒触发)。

?:表示 "不指定",仅用于日和周字段,用于避免两者的语义冲突。

/:表示增量触发(如分字段为 0/5 表示从 0 分开始,每 5 分钟触发一次)。

,:表示枚举多个值(如周字段为 1,7 表示周日、周六触发)。

-:表示范围(如时字段为 9-17 表示 9 点到 17 点之间触发)。

L:表示 "最后"(如日字段为 L 表示当月最后一天,周字段为 7L 表示当月最后一个周六)。

W:表示 "最近的工作日"(如日字段为 15W 表示 15 号最近的工作日,若 15 号是周六则触发 14 号周五)。

#:表示 "第几个周几"(如周字段为 5#3 表示每月第三个周四)。

核心问题:日和周为什么不能同时指定?

cron 表达式中,日和周是互斥的语义维度:

若两者都指定具体值(如日为 15、周为 MON),会导致逻辑冲突:系统无法判断是 "每月 15 号且周一" 触发,还是 "每月 15 号或周一" 触发,语义模糊。

因此规范要求:日和周字段中必须有一个为 ?,表示不指定该维度,避免逻辑歧义。

例如:0 0 12 * * ? 表示每天中午 12 点触发(周字段为 ?,不限制周几);0 0 12 ? * MON 表示每周一中午 12 点触发(日字段为 ?,不限制日期)。

  1. Spring Task 入门案例

步骤 1:开启定时任务

在 Spring Boot 启动类上添加 @EnableScheduling 注解,启用 Spring Task 定时任务功能:

java

运行

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication

@EnableScheduling // 开启定时任务

public class DemoApplication {

public static void main(String[] args) {

SpringApplication.run(DemoApplication.class, args);

}

}

步骤 2:定义定时任务类

创建业务任务类,使用 @Component 注解将其纳入 Spring 容器,通过 @Scheduled 注解配置触发规则:

java

运行

import org.springframework.scheduling.annotation.Scheduled;

import org.springframework.stereotype.Component;

import java.time.LocalDateTime;

@Component

public class DemoTask {

// 固定延迟:上次任务结束后,延迟固定时间再执行(单位:毫秒)

@Scheduled(fixedDelay = 5000)

public void fixedDelayTask() {

System.out.println("固定延迟任务执行,当前时间:" + LocalDateTime.now());

}

// 固定频率:上次任务开始后,固定时间间隔执行(单位:毫秒)

@Scheduled(fixedRate = 5000)

public void fixedRateTask() {

System.out.println("固定频率任务执行,当前时间:" + LocalDateTime.now());

}

// cron 表达式:每天 12:00 执行

@Scheduled(cron = "0 0 12 * * ?")

public void cronTask() {

System.out.println("cron 任务执行,当前时间:" + LocalDateTime.now());

}

}

步骤 3:自定义线程池(可选)

默认情况下,Spring Task 使用单线程执行所有任务,若任务耗时较长会导致阻塞。可通过配置类自定义线程池:

java

运行

import org.springframework.context.annotation.Configuration;

import org.springframework.scheduling.annotation.SchedulingConfigurer;

import org.springframework.scheduling.config.ScheduledTaskRegistrar;

import java.util.concurrent.Executors;

@Configuration

public class SchedulingConfig implements SchedulingConfigurer {

@Override

public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {

// 配置 5 个核心线程的线程池

taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5));

}

}

  1. 订单状态定时处理:需求分析

在电商或外卖系统中,订单状态需要自动流转,避免资源占用和数据不一致。核心需求包括:

订单超时未支付:用户下单后 15 分钟内未支付,自动取消订单,释放库存。

订单已完成但未评价:订单完成后 7 天内未评价,自动生成默认评价。

订单退款超时:退款申请提交后 7 天未处理,自动完成退款流程。

本次案例聚焦订单超时未支付场景:

触发时机:定时任务每分钟执行一次。

处理逻辑:查询所有 "待支付" 且 "下单时间超过 15 分钟" 的订单,将其状态更新为 "已取消",并恢复对应商品库存。

核心问题:当前时间减去 15 分钟怎么操作?

在 Java 8 及以上版本中,推荐使用 LocalDateTime 类处理时间计算,简洁且线程安全:

java

运行

import java.time.LocalDateTime;

// 获取当前时间

LocalDateTime now = LocalDateTime.now();

// 当前时间减去 15 分钟

LocalDateTime before15Min = now.minusMinutes(15);

若使用旧版 Date 类,可通过 Calendar 实现:

java

运行

import java.util.Calendar;

import java.util.Date;

Calendar calendar = Calendar.getInstance();

calendar.setTime(new Date());

calendar.add(Calendar.MINUTE, -15);

Date before15Min = calendar.getTime();

在 SQL 中也可直接计算时间范围(以 MySQL 为例):

sql

-- 查询 15 分钟前创建的待支付订单

SELECT * FROM orders

WHERE status = 'WAIT_PAY'

AND create_time <= DATE_SUB(NOW(), INTERVAL 15 MINUTE);

  1. 订单状态定时处理:代码开发

步骤 1:定义订单实体与数据库表

订单表 orders 核心字段:

id:订单 ID(主键)

order_no:订单编号

status:订单状态(WAIT_PAY:待支付,PAID:已支付,CANCELLED:已取消,COMPLETED:已完成)

create_time:下单时间

pay_time:支付时间(可选)

步骤 2:编写 Mapper 层

使用 MyBatis 或 MyBatis-Plus 编写数据访问层,查询超时待支付订单并更新状态:

java

运行

// OrderMapper.java

import org.apache.ibatis.annotations.Mapper;

import org.apache.ibatis.annotations.Select;

import org.apache.ibatis.annotations.Update;

import java.time.LocalDateTime;

import java.util.List;

@Mapper

public interface OrderMapper {

// 查询超时待支付订单

@Select("SELECT id FROM orders WHERE status = 'WAIT_PAY' AND create_time <= #{before15Min}")

List<Long> selectTimeoutOrderIds(LocalDateTime before15Min);

// 更新订单状态为已取消

@Update("UPDATE orders SET status = 'CANCELLED' WHERE id = #{orderId}")

void updateStatusToCancelled(Long orderId);

}

步骤 3:编写 Service 层

封装业务逻辑,处理订单取消及库存恢复:

java

运行

// OrderService.java

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Service;

import java.time.LocalDateTime;

import java.util.List;

@Service

public class OrderService {

@Autowired

private OrderMapper orderMapper;

// 处理超时订单

public void processTimeoutOrders() {

LocalDateTime before15Min = LocalDateTime.now().minusMinutes(15);

List<Long> orderIds = orderMapper.selectTimeoutOrderIds(before15Min);

for (Long orderId : orderIds) {

// 1. 更新订单状态为已取消

orderMapper.updateStatusToCancelled(orderId);

// 2. 恢复商品库存(需调用库存服务)

// stockService.recoverStock(orderId);

System.out.println("订单 " + orderId + " 已超时取消,库存已恢复");

}

}

}

步骤 4:编写定时任务类

调用 Service 层方法,配置每分钟执行一次:

java

运行

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.scheduling.annotation.Scheduled;

import org.springframework.stereotype.Component;

@Component

public class OrderTimeoutTask {

@Autowired

private OrderService orderService;

// 每分钟执行一次:cron = "0 * * * * ?"

@Scheduled(cron = "0 * * * * ?")

public void execute() {

System.out.println("开始处理超时订单...");

orderService.processTimeoutOrders();

System.out.println("超时订单处理完成");

}

}

  1. 订单状态定时处理:功能测试

测试步骤:

向数据库插入一条待支付订单,create_time 设置为当前时间减去 20 分钟(模拟超时)。

启动 Spring Boot 应用,观察控制台输出。

等待定时任务触发(每分钟执行一次),检查数据库中订单状态是否更新为 CANCELLED。

验证库存是否恢复(若集成库存服务)。

测试注意事项:

可临时修改 cron 表达式为 0/10 * * * * ?(每 10 秒执行一次),加快测试验证。

测试完成后需恢复原 cron 表达式,避免生产环境频繁执行。

需处理并发问题:若多个实例同时执行任务,可通过分布式锁(如 Redis 锁)避免重复处理。

二、WebSocket 实时通信

  1. WebSocket 基础介绍

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,解决了传统 HTTP 协议 "请求 - 响应" 模式的局限性,实现了服务器主动向客户端推送消息的能力。

核心特点:

全双工通信:客户端和服务器可同时发送 / 接收数据,无需等待请求响应。

长连接:一次连接建立后保持持久连接,减少 HTTP 握手开销。

低延迟:适合实时性要求高的场景(如聊天、通知、直播弹幕等)。

跨平台:支持浏览器、移动端、桌面端等多种客户端。

与 HTTP 的对比:

表格

特性 HTTP WebSocket

通信模式 单向(客户端请求→服务器响应) 双向全双工

连接状态 短连接(每次请求重新建立连接) 长连接(一次建立,持久保持)

数据推送 服务器无法主动推送 服务器可主动推送消息

开销 每次请求携带完整头部信息 仅握手时携带头部,后续数据帧开销小

  1. WebSocket 入门案例 1:基础通信

步骤 1:引入依赖

在 Spring Boot 项目中添加 WebSocket 依赖:

xml

<dependency>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-starter-websocket</artifactId>

</dependency>

步骤 2:编写配置类(核心:配置类的作用)

配置类的作用是将 WebSocket 相关组件注册到 Spring 容器中,并自定义处理逻辑。核心作用包括:

启用 WebSocket 支持:通过 @EnableWebSocket 注解开启 WebSocket 功能。

注册 WebSocket 处理器:将自定义的 WebSocketHandler 与特定 URL 路径绑定。

配置拦截器:添加握手拦截器,实现连接建立前的身份验证、参数传递等逻辑。

自定义消息编解码:配置消息转换器,支持 JSON、Protobuf 等数据格式。

示例配置类:

java

运行

import org.springframework.context.annotation.Configuration;

import org.springframework.web.socket.config.annotation.EnableWebSocket;

import org.springframework.web.socket.config.annotation.WebSocketConfigurer;

import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

@Configuration

@EnableWebSocket // 启用 WebSocket

public class WebSocketConfig implements WebSocketConfigurer {

// 注入自定义处理器

private final MyWebSocketHandler myWebSocketHandler;

public WebSocketConfig(MyWebSocketHandler myWebSocketHandler) {

this.myWebSocketHandler = myWebSocketHandler;

}

@Override

public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {

// 注册处理器,绑定路径 /ws,允许跨域访问

registry.addHandler(myWebSocketHandler, "/ws")

.setAllowedOrigins("*"); // 生产环境需限制具体域名

}

}

步骤 3:编写 WebSocket 处理器

自定义 WebSocketHandler 处理连接、消息、断开等事件:

java

运行

import org.springframework.web.socket.TextMessage;

import org.springframework.web.socket.WebSocketSession;

import org.springframework.web.socket.handler.TextWebSocketHandler;

import org.springframework.stereotype.Component;

import java.io.IOException;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

@Component

public class MyWebSocketHandler extends TextWebSocketHandler {

// 存储所有连接的客户端会话(线程安全)

private static final Map<String, WebSocketSession> SESSIONS = new ConcurrentHashMap<>();

// 连接建立时触发

@Override

public void afterConnectionEstablished(WebSocketSession session) throws Exception {

String sessionId = session.getId();

SESSIONS.put(sessionId, session);

System.out.println("客户端连接成功,sessionId:" + sessionId);

}

// 收到客户端消息时触发

@Override

protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {

String payload = message.getPayload();

String sessionId = session.getId();

System.out.println("收到客户端 " + sessionId + " 消息:" + payload);

// 向所有客户端广播消息

broadcastMessage("客户端 " + sessionId + " 说:" + payload);

}

// 连接关闭时触发

@Override

public void afterConnectionClosed(WebSocketSession session, org.springframework.web.socket.CloseStatus status) throws Exception {

String sessionId = session.getId();

SESSIONS.remove(sessionId);

System.out.println("客户端连接关闭,sessionId:" + sessionId);

}

// 广播消息给所有客户端

public void broadcastMessage(String message) throws IOException {

for (WebSocketSession session : SESSIONS.values()) {

if (session.isOpen()) {

session.sendMessage(new TextMessage(message));

}

}

}

}

步骤 4:前端页面测试

创建 HTML 页面,使用浏览器原生 WebSocket API 连接服务器:

html

预览

<!DOCTYPE html>

<html>

<head>

<title>WebSocket 测试</title>

</head>

<body>

<input type="text" id="messageInput" placeholder="输入消息">

<button onclick="sendMessage()">发送</button>

<div id="messageContainer"></div>

<script>

// 连接 WebSocket 服务器

const ws = new WebSocket('ws://localhost:8080/ws');

// 连接成功回调

ws.onopen = function() {

console.log('连接成功');

};

// 收到消息回调

ws.onmessage = function(event) {

const container = document.getElementById('messageContainer');

container.innerHTML += `<p>${event.data}</p>`;

};

// 连接关闭回调

ws.onclose = function() {

console.log('连接关闭');

};

// 发送消息

function sendMessage() {

const input = document.getElementById('messageInput');

const message = input.value;

if (message) {

ws.send(message);

input.value = '';

}

}

</script>

</body>

</html>

相关推荐
dreamread2 小时前
完美解决phpstudy安装后mysql无法启动
数据库·mysql
小江的记录本2 小时前
【SQL】多表关系与冷热数据(全维度知识体系)
数据库·sql·mysql·数据库开发·数据库架构
sjmaysee2 小时前
Windows操作系统部署Tomcat详细讲解
java·windows·tomcat
数据知道2 小时前
MongoDB慢查询分析:详细讲述如何使用profile集合识别性能瓶颈
数据库·mongodb
zjjsctcdl3 小时前
【prometheus】监控MySQL并实现可视化
数据库·mysql·prometheus
阿波罗尼亚3 小时前
MySQL 存储引擎 FEDERATED
数据库·mysql
lym5400508893 小时前
MySQL篇(管理工具)
数据库·mysql
1.14(java)3 小时前
Spring-boot快速上手
java·开发语言·javaee
Darkdreams3 小时前
SpringBoot项目集成ONLYOFFICE
java·spring boot·后端