Day26 - Spring Task定时任务与WebSocket双向通信 一、Spring Task 定时任务 1.1 概述 Spring Task 是 Spring 框架提供的任务调度工具 ,可以按照约定的时间自动触发 执行某段代码逻辑。
核心用途:凡是需要定时处理的场景都可以使用,例如信用卡还款提醒、未支付订单超时处理等。
1.2 cron 表达式 cron 表达式是一个字符串,用于定义任务触发的时间 。
常用通配符速查:
通配符
含义
*
所有值
?
未说明的值,不关心
-
指定范围
,
附加一个可能值
/
前为开始时间,后为每次递增值
常用示例:
需求
cron 表达式
每 5 秒执行一次
0/5 * * * * ?
每分钟执行一次
0 * * * * ?
每天凌晨 1 点执行
0 0 1 * * ?
每天 5-15 点整点触发
0 0 5-15 * * ?
1.3 使用步骤
导入 Maven 坐标 :spring-context(Spring Boot 项目一般已包含)
启动类开启调度 :添加 @EnableScheduling 注解
自定义定时任务类 :
入门示例:
1 2 3 4 5 6 7 8 9 @Component @Slf4j public class MyTask { @Scheduled(cron = "0/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 ); 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 ); 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 实现流程
导入 Maven 坐标 :
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency >
添加配置类 WebSocketConfiguration,注册 WebSocket 服务端组件:
1 2 3 4 5 6 7 8 @Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter (); } }
定义服务端组件 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 { 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) { 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); Map map = new HashMap (); map.put("type" , 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); } Map map = new HashMap (); map.put("type" , 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 区分提醒和催单,触发弹窗 + 语音播报