苍穹外卖项目实战:Spring Task 定时任务与 WebSocket 消息推送
在苍穹外卖项目中如何使用 Spring Task 实现订单状态的定时处理,包括超时取消和派送完成逻辑。同时讲解了 WebSocket 协议的基础概念及入门案例,并应用于来单语音播报和客户催单提醒功能,实现了服务端向客户端的实时消息推送。

在苍穹外卖项目中如何使用 Spring Task 实现订单状态的定时处理,包括超时取消和派送完成逻辑。同时讲解了 WebSocket 协议的基础概念及入门案例,并应用于来单语音播报和客户催单提醒功能,实现了服务端向客户端的实时消息推送。

Spring Task 是 Spring 框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定位:定时任务框架
作用:定时自动执行某段 Java 代码
Cron 表达式本质是一个字符串,通过 cron 表达式可以定义任务触发的时间。
构成规则:分为 6 或 7 个域,由空格分隔开,每个域代表一个含义。每个域的含义分别为:秒、分钟、小时、日、月份、星期。
常用的符号:
* (星号): 表示任何值。例如,* 在分钟字段中表示'每一分钟'。, (逗号): 用来指定多个值。例如,1,2,3 表示'1、2、3 分钟'。- (连字符): 用来指定一个范围。例如,1-5 表示'1 到 5'。/ (斜杠): 用于指定步进。例如,*/5 在分钟字段中表示'每 5 分钟执行一次'。? (问号): 表示不指定值,通常用于'日'和'星期几'字段中。当两个字段互相冲突时,使用 ? 来避免冲突。L (最后): 用于'日'和'星期几'字段,表示'最后一天'或'最后一个星期几'。W (工作日): 用于'日'字段,表示离指定日期最近的工作日。# (星期几的第 N 个): 用于'星期几'字段,表示一个月中某个星期几的第 N 个。Spring Task 使用步骤:
/**
* 自定义任务类
*/
@Component
@Slf4j
public class MyTask {
/**
* 定时任务,每五秒触发一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask() {
log.info("定时任务开始执行:{}", new Date());
}
}

在 Task 包下创建一个 OrderTask。
/**
* 定时任务类,定时处理订单状态
*/
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 定时处理超时订单
*/
@Scheduled(cron = "0 * * * * ?") // 每分钟触发一次
public void processTimeoutOrder() {
log.info("定时处理超时订单:{}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
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);
}
}
}
/**
* 处理一直处于派送中状态中的订单
*/
@Scheduled(cron = "0 0 1 * * ?") // 每天凌晨 1 点执行一次
public void processDeliverOrder() {
log.info("定时处理处于派送中的订单:{}", LocalDateTime.now());
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
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);
}
}
}
}
在 OrderMapper 类下新增查询方法:
/**
* 根据订单状态和下单时间查询订单
* @param status
* @param orderTime
* @return
*/
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);
WebSocket 是基于 TCP 的一种新的网络协议,它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

应用场景:
步骤:
/**
* WebSocket 服务
*/
@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);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
{
session.getBasicRemote().sendText(message);
} (Exception e) {
e.printStackTrace();
}
}
}
}
/**
* WebSocket 配置类,用于注册 WebSocket 的 Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
/**
* 通过 WebSocket 每隔 5 秒向客户端发送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}
用户下单且支付成功后,需要第一时间通知外卖商家:

修改 OrderServiceImpl 中的 paySuccess 方法。
/**
* 支付成功,修改订单状态
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 根据订单号查询订单
Orders ordersDB = orderMapper.getByNumber(outTradeNo);
// 根据订单 id 更新订单的状态、支付方式、支付状态、结账时间
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);
}
支付成功后就通过 WebSocket 向客户端推送消息。
用户在小程序中点击催单按钮后,需要第一时间通知外卖商家:

用户催单接口设计:

在 User 包下的 OrderController 类中增加接口。
/**
* 客户催单
* @param id
* @return
*/
@GetMapping("/reminder/{id}")
public Result reminder(@PathVariable Long id) {
log.info("客户催单:{}", id);
orderService.reminder(id);
return Result.success();
}
在 OrderServiceImp 中增加客户催单方法。
/**
* 客户催单
* @param id
*/
@Override
public void reminder(Long id) {
// 根据 id 查询订单
Orders ordersDB = orderMapper.getById(id);
// 校验订单是否存在,并且状态为 4
if (ordersDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
// 通过 WebSocket 向客户端浏览器推送消息
Map<String, Object> map = new HashMap<>();
map.put("type", 2); // 2 表示客户催单
map.put("orderId", id);
map.put("content", "订单号:" + ordersDB.getId());
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online