day26 Spring Task定时任务与WebSocket双向通信

Day26 - Spring Task定时任务与WebSocket双向通信

一、Spring Task 定时任务

1.1 概述

Spring Task 是 Spring 框架提供的任务调度工具,可以按照约定的时间自动触发执行某段代码逻辑。

核心用途:凡是需要定时处理的场景都可以使用,例如信用卡还款提醒、未支付订单超时处理等。

1.2 cron 表达式

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

  • 构成规则:分为 6 或 7 个域,由空格分隔,含义依次为:秒、分、时、日、月、周、年(可选)

  • 日和周一般不同时设置具体值,其中一个用 ? 表示不关心

  • 实际开发中通常使用在线生成器自动生成,无需手写

常用通配符速查:

通配符 含义
* 所有值
? 未说明的值,不关心
- 指定范围
, 附加一个可能值
/ 前为开始时间,后为每次递增值

常用示例:

需求 cron 表达式
每 5 秒执行一次 0/5 * * * * ?
每分钟执行一次 0 * * * * ?
每天凌晨 1 点执行 0 0 1 * * ?
每天 5-15 点整点触发 0 0 5-15 * * ?

1.3 使用步骤

  1. 导入 Maven 坐标spring-context(Spring Boot 项目一般已包含)

  2. 启动类开启调度:添加 @EnableScheduling 注解

  3. 自定义定时任务类

    • 类上标注 @Component 加入 IOC 容器

    • 方法上标注 @Scheduled(cron = "...") 定义触发时间

入门示例:

1
2
3
4
5
6
7
8
9
@Component
@Slf4j
public class MyTask {

@Scheduled(cron = "0/5 * * * * ?") // 每5秒执行一次
public void executeTask() {
log.info("定时任务开始执行:{}", new Date());
}
}

注意:@EnableScheduling 必须加在启动类或配置类上,否则定时任务不会生效。

1.4 订单状态定时处理

根据业务需求,使用 Spring Task 处理两类异常订单状态:

场景 触发频率 业务逻辑
未支付超时 每分钟检查一次 下单超过 15 分钟仍未支付,自动取消订单
派送中未完成 每天凌晨 1 点检查 派送超过 60 分钟未点击完成,自动置为已完成

Mapper 查询方法:按状态和下单时间(小于指定时间)查询订单:

1
2
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrdertimeLT(Integer status, LocalDateTime orderTime);

支付超时处理(每分钟执行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder() {
log.info("处理支付超时订单:{}", new Date());

LocalDateTime time = LocalDateTime.now().plusMinutes(-15);

// 查询状态为待支付、且下单时间早于15分钟前的订单
List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time);
if (ordersList != null && ordersList.size() > 0) {
ordersList.forEach(order -> {
order.setStatus(Orders.CANCELLED);
order.setCancelReason("支付超时,自动取消");
order.setCancelTime(LocalDateTime.now());
orderMapper.update(order);
});
}
}

派送中订单处理(每天凌晨 1 点执行):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder() {
log.info("处理派送中订单:{}", new Date());

LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
// 查询状态为派送中、且下单时间早于60分钟前的订单
List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time);

if (ordersList != null && ordersList.size() > 0) {
ordersList.forEach(order -> {
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
});
}
}

二、WebSocket 双向通信

2.1 与 HTTP 的对比

WebSocket 是基于 TCP 的网络协议,实现了浏览器与服务器的全双工通信。一次握手后即可创建持久性连接,并持续双向传输数据。

对比项 HTTP WebSocket
通信方向 单向,请求-响应模式 双向,双方均可主动发送
连接模式 短连接,一次请求对应一次连接 长连接,建立后持续保持
服务端推送 不支持,服务端无法主动发消息 支持,服务端可随时推送
底层协议 TCP TCP

WebSocket 不能完全取代 HTTP:维护长连接有成本,浏览器支持程度不一,且受网络波动影响需要处理重连,只适合特定实时场景。

2.2 应用场景

  • 网页聊天(即时通讯)

  • 视频弹幕

  • 体育比赛 / 股票行情实时更新

  • 外卖系统的来单提醒客户催单

2.3 实现流程

  1. 导入 Maven 坐标
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
  1. 添加配置类 WebSocketConfiguration,注册 WebSocket 服务端组件:
1
2
3
4
5
6
7
8
@Configuration
public class WebSocketConfiguration {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
  1. 定义服务端组件 WebSocketServer,管理连接和消息:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

// 存放会话对象,key 为客户端标识 sid
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);
}

@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}

@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}

/**
* 向所有已连接的客户端群发消息
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

核心要点:

  • @ServerEndpoint("/ws/{sid}") 声明 WebSocket 端点路径,sid 用于区分不同客户端

  • sessionMap 维护所有在线会话,是实现群发的关键

  • sendToAllClient() 方法遍历所有会话发送消息


三、来单提醒与客户催单

来单提醒和客户催单的技术基础相同:管理端页面通过 WebSocket 与服务端保持长连接,服务端在特定事件发生时主动推送消息,浏览器解析消息后弹窗 + 语音播报。

3.1 消息格式约定

服务端推送的消息统一采用 JSON 格式,通过 type 字段区分业务类型:

字段 类型 含义 取值
type int 消息类型 1 = 来单提醒2 = 客户催单
orderId Long 订单 ID 订单主键
content String 消息内容 “订单号:xxx” 等展示文本

3.2 来单提醒

触发时机:用户支付成功后。

实现位置:在业务层 paySuccess(String outTradeNo) 方法中,更新订单状态后追加 WebSocket 推送逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Autowired
private WebSocketServer webSocketServer;

public void paySuccess(String outTradeNo) {
// 1. 查询并更新订单状态(原有逻辑)
Long userId = BaseContext.getCurrentId();
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);

// 2. 通过 WebSocket 推送来单提醒
Map map = new HashMap();
map.put("type", 1); // 1 表示来单提醒
map.put("orderId", orders.getId());
map.put("content", "订单号:" + outTradeNo);

webSocketServer.sendToAllClient(JSON.toJSONString(map));
}

3.3 客户催单

触发时机:用户在小程序端点击”催单”按钮。

调用链路

1
2
3
4
5
6
7
8
9
小程序点击催单

GET /order/reminder/{id}

OrderController.reminder(id)

OrderServiceImpl.reminder(id)

校验订单存在 → 推送 type=2 的 WebSocket 消息

Controller 层

1
2
3
4
5
6
@GetMapping("/reminder/{id}")
@ApiOperation("用户催单")
public Result reminder(@PathVariable("id") Long id) {
orderService.reminder(id);
return Result.success();
}

Service 层实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void reminder(Long id) {
// 查询订单是否存在
Orders orders = orderMapper.getById(id);
if (orders == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}

// 基于 WebSocket 推送催单消息
Map map = new HashMap();
map.put("type", 2); // 2 表示客户催单
map.put("orderId", id);
map.put("content", "订单号:" + orders.getNumber());
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}

注意:催单前必须校验订单是否存在,不存在时抛出业务异常。

今日总结

模块 核心要点
Spring Task 定时任务框架;@EnableScheduling 开启调度,@Scheduled(cron=...) 标记方法;cron 表达式用在线生成器
订单超时处理 每分钟执行:超时 15 分钟未支付的订单自动取消;每天凌晨 1 点:派送超 60 分钟未完成自动置完成
WebSocket 双向长连接协议,区别于 HTTP 的单向短连接;底层仍为 TCP;@ServerEndpoint 声明端点,维护 sessionMap 实现群发
来单提醒 支付成功后,在 paySuccess 中推送 type=1 的 JSON 消息到管理端
客户催单 用户端调用 /reminder/{id},校验订单后推送 type=2 的 JSON 消息
消息格式 统一 JSON:{type, orderId, content},管理端根据 type 区分提醒和催单,触发弹窗 + 语音播报