【苍穹外卖Day10】定时任务与来单提醒
自动化处理与实时消息交互。如何处理那些用户下单后迟迟不支付的“幽灵订单”?如何让商家第一时间听到“您有新的外卖订单”的语音播报?本篇博客将通过和WebSocket技术来解决这些问题。Spring Task 是 Spring 框架提供的轻量级任务调度工具。简单来说,它能让你的代码按照约定的时间,自动执行某段逻辑,而无需人工干预。
前言
在“苍穹外卖”开发的第十天,我们迎来了两个非常实用的功能模块:自动化处理与实时消息交互。
-
如何处理那些用户下单后迟迟不支付的“幽灵订单”?
-
如何让商家第一时间听到“您有新的外卖订单”的语音播报?
本篇博客将通过 Spring Task 和 WebSocket 技术来解决这些问题。
一、Spring Task 定时任务
1.1 什么是 Spring Task?
Spring Task 是 Spring 框架提供的轻量级任务调度工具。简单来说,它能让你的代码按照约定的时间,自动执行某段逻辑,而无需人工干预。
1.2 Cron 表达式
定时任务的核心在于“什么时候执行”,这需要通过 Cron 表达式 来定义。 它本质上是一个字符串,由 6 或 7 个域组成,通过空格分隔:
秒 分 时 日 月 周 年(可选)
常用通配符:
-
*:表示所有值。 -
?:表示不关心该值(常用于“周”或“日”,避免冲突)。 -
-:表示范围。 -
/:表示递增(例如0/5表示从0开始每5个单位执行一次)。
经典案例:
-
*/5 * * * * ?:每隔5秒执行一次 -
0 0/3 * * * ?:每3分钟触发一次 -
0 0 5-15 * * ?:每天5-15点整点触发
1.3 业务场景:订单状态定时处理
在本项目中,我们需要通过定时任务解决以下两个数据一致性问题:
-
支付超时处理:
-
需求:用户下单后可能未支付,如果一直占用资源不好。
-
逻辑:每分钟检查一次,判定
order_time < now - 15min且状态为未支付的订单,将其自动取消。
-
-
派送中订单兜底:
-
需求:防止骑士送完货后,后台管理员忘记点击“完成”,导致订单一直处于“派送中”。
-
逻辑:每天凌晨 1 点 (
0 0 1 * * ?),查找所有处于派送中的订单,自动将其状态修改为已完成。
-
在使用 Spring Task 之前,别忘了在启动类上添加注解 @EnableScheduling 来开启定时任务功能。
核心代码
package com.sky.task;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
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;
/**
* 处理支付超时订单
* 需求:每分钟触发一次,判定 order_time < now - 15min 则超时,自动取消
*/
@Scheduled(cron = "0 * * * * ?") // 每分钟触发一次
public void processTimeoutOrder() {
log.info("定时处理超时订单:{}", LocalDateTime.now());
// 计算时间点:当前时间 - 15分钟
LocalDateTime time = LocalDateTime.now().minusMinutes(15);
// 查询“未支付”且“下单时间小于time”的订单
// select * from orders where status = ? and order_time < ?
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);
}
}
}
/**
* 处理一直处于派送中的订单
* 需求:每天凌晨1点触发,防止订单一直处于派送中
*/
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨1点触发一次
public void processDeliveryOrder() {
log.info("定时处理处于派送中的订单:{}", LocalDateTime.now());
// 为了保险起见,查找的是“上一工作日”还在派送中的订单
// 这里逻辑通常是查找 60分钟前(即前一天晚上12点之前)还在派送的订单
LocalDateTime time = LocalDateTime.now().minusMinutes(60);
// 查询“派送中”且“下单时间小于time”的订单
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);
}
}
}
}
Mapper 层需要实现根据状态和时间范围查询订单的 SQL 语句。
二、WebSocket 双向通信
2.1 为什么需要 WebSocket?
传统的 HTTP 协议是单向的(客户端请求 -> 服务端响应)。但在“来单提醒”这种场景下,服务端需要主动告诉浏览器:“嘿,有新订单了!”。如果用 HTTP 轮询(每隔几秒问一次服务器),效率低且浪费资源。
"WebSocket 主要是为了解决 HTTP 协议无法实现服务端主动推送的问题。它基于 TCP,通过 HTTP 协议进行一次握手后升级为长连接。相比于传统的轮询方式,它具有低延迟、低带宽的优势,非常适合做类似苍穹外卖里的来单提醒、或者网页聊天室这种实时性要求高的功能。"
WebSocket 的优势:
-
全双工通信:浏览器和服务器只需要一次握手,就能建立持久连接,双方都能主动发送数据。
-
轻量级:相比 HTTP 轮询,网络开销更小。

| 特性 | HTTP | WebSocket |
| 连接方式 | 短连接 | 长连接 |
| 通信方向 | 单向(Req -> Resp) | 双向(全双工) |
| 适用场景 | 常规数据交互 | 弹幕、股票、聊天、实时提醒 |
2.2 使用步骤
-
客户端:前端使用 HTML5 提供的 WebSocket API。
-
服务端:
-
导入
WebSocketServer组件。 -
配置
WebSocketConfiguration注册服务端点。 -
通过
sendToAllClient等方法推送消息。
-
业务功能实战代码
3.1 来单提醒 (New Order Notification)
场景:用户支付成功后,第一时间通知管理端语音播报。 触发点:在 OrderService 的 paySuccess 方法中。
public void paySuccess(String outTradeNo) {
// ... (省略前面的查询与状态更新代码) ...
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED) // 状态变更为待接单
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
// --- WebSocket 消息推送 ---
Map<String,Object> map = new HashMap<>();
map.put("type", 1); // 1表示来单提醒
map.put("orderId", ordersDB.getId());
map.put("content", "订单号" + outTradeNo);
String json = JSON.toJSONString(map);
// 向客户端浏览器推送消息
webSocketServer.sendToAllClient(json);
}
3.2 用户催单 (Order Reminder)
场景:用户在小程序点击“催单”,管理端弹出提示。 触发点:Controller 接收请求 -> Service 处理推送。
@Override
public void reminder(Long id) {
Orders orders = orderMapper.getByOrderId(id);
if(orders == null){
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}
// --- WebSocket 消息推送 ---
Map<String,Object> map = new HashMap<>();
map.put("type", 2); // 2表示催单
map.put("orderId", orders.getId());
map.put("content", "订单号" + orders.getNumber());
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
}
更多推荐
所有评论(0)