目录
[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 - 订单状态定时处理_需求分析)
[Day10-05 - 订单状态定时处理_代码开发](#Day10-05 - 订单状态定时处理_代码开发)
[Day10-06 - 订单状态定时处理_功能测试](#Day10-06 - 订单状态定时处理_功能测试)
[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 定时任务
- Spring Task 基础介绍
Spring Task 是 Spring 框架提供的轻量级定时任务工具,无需依赖第三方组件(如 Quartz),即可实现简单、高效的任务调度。它支持基于 cron 表达式、固定延迟、固定频率等多种触发方式,适合处理周期性业务场景(如订单超时清理、数据统计、消息推送等)。
核心特点:
轻量易用:无需额外部署,直接集成在 Spring 容器中,通过注解即可开启定时任务。
多种触发方式:支持 cron 表达式、fixedDelay(固定延迟)、fixedRate(固定频率)、initialDelay(初始延迟)等配置。
线程池管理:默认使用单线程执行任务,可通过配置自定义线程池,提升并发处理能力。
与 Spring 生态无缝集成:可直接注入 Spring Bean,访问业务服务、数据库等资源。
- 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 点触发(日字段为 ?,不限制日期)。
- 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));
}
}
- 订单状态定时处理:需求分析
在电商或外卖系统中,订单状态需要自动流转,避免资源占用和数据不一致。核心需求包括:
订单超时未支付:用户下单后 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:定义订单实体与数据库表
订单表 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("超时订单处理完成");
}
}
- 订单状态定时处理:功能测试
测试步骤:
向数据库插入一条待支付订单,create_time 设置为当前时间减去 20 分钟(模拟超时)。
启动 Spring Boot 应用,观察控制台输出。
等待定时任务触发(每分钟执行一次),检查数据库中订单状态是否更新为 CANCELLED。
验证库存是否恢复(若集成库存服务)。
测试注意事项:
可临时修改 cron 表达式为 0/10 * * * * ?(每 10 秒执行一次),加快测试验证。
测试完成后需恢复原 cron 表达式,避免生产环境频繁执行。
需处理并发问题:若多个实例同时执行任务,可通过分布式锁(如 Redis 锁)避免重复处理。
二、WebSocket 实时通信
- WebSocket 基础介绍
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议,解决了传统 HTTP 协议 "请求 - 响应" 模式的局限性,实现了服务器主动向客户端推送消息的能力。
核心特点:
全双工通信:客户端和服务器可同时发送 / 接收数据,无需等待请求响应。
长连接:一次连接建立后保持持久连接,减少 HTTP 握手开销。
低延迟:适合实时性要求高的场景(如聊天、通知、直播弹幕等)。
跨平台:支持浏览器、移动端、桌面端等多种客户端。
与 HTTP 的对比:
表格
特性 HTTP WebSocket
通信模式 单向(客户端请求→服务器响应) 双向全双工
连接状态 短连接(每次请求重新建立连接) 长连接(一次建立,持久保持)
数据推送 服务器无法主动推送 服务器可主动推送消息
开销 每次请求携带完整头部信息 仅握手时携带头部,后续数据帧开销小
- 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>




