苍穹外卖开发日志Day11——Spring Task、订单状态定时处理、WebSocket、来单提醒及客户催单


2025.07.29

一、Spring Task

1.1 Spring Task介绍

  • Spring Task是Spring框架提供的一个定时任务调度功能,可以用来执行周期性任务。
    • 定位:定时任务框架
    • 作用:定时自动执行某段Java代码
    • 应用场景
      • 信用卡每月还款提醒
      • 银行贷款每月还款提醒
      • 火车票售票系统处理未支付订单
      • 入职纪念日为用户发送通知

1.2 cron表达式

  • cron表达式是一个字符串,用于定义定时任务的执行时间。
  • 构成规则:分为6或7个域,由空格分开,每个域代表一个含义
    • 秒(0-59)
    • 分钟(0-59)
    • 小时(0-23)
    • 日(1-31)
    • 月(1-12或JAN-DEC)
    • 星期(0-6或SUN-SAT,0表示星期日)
    • 年(可选,1970-2099)
  • 示例:2022年10月12日上午9点整:0 0 9 12 10 ? 2022
  • cron表达式在线生成器:https://cron.qqe2.com/

1.3 入门案例

  • Spring Task使用步骤:
    1. 导入maven坐标spring_context(已存在)
    2. 启动类添加注解@EnableScheduling开启任务调度
    3. 自定义定时任务类
  • 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 自定义定时任务类
*/
@Component
@Slf4j
public class MyTask {

@Scheduled(cron = "0/5 * * * * ?") // 每5秒执行一次
public void executeTask() {
log.info("定时任务执行,当前时间:{}", LocalDateTime.now());
}
}

二、订单状态定时处理

2.1 订单状态定时处理——功能开发

  • 需求分析:用户下单后可能存在的情况:
    • 下单后未支付,订单一直处于“待支付”状态
    • 用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态
  • 对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:
    • 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
    • 通过定时任务每天凌晨1点检查一次是否存在“派送中”订单,如果存在则修改订单状态为“已完成”

2.2 订单状态定时处理——代码实现

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
39
40
41
42
43
44
45
46
47
48
49
50
/**
* 定时任务类,定时处理订单状态
*/
@Component
@Slf4j
public class OrderTask {

@Autowired
private OrderMapper orderMapper;

/**
* 处理超时订单:每分钟一次
*/
@Scheduled(cron = "0 * * * * ?") // 每分钟
public void processTimeoutOrder() {
log.info("定时处理超时订单:{}", LocalDateTime.now());

// 查询超时订单
// select * from orders where status = ? and order_time < (当前时间-15分钟)
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 processDeliveryOrder() {
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);
}
}
}
}

2.3 订单状态定时处理——功能测试

  • 订单状态定时处理

三、WebSocket

3.1 WebSocket介绍

  • WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

Http与WebSocket对比

  • 应用场景:
    • 视频弹幕
    • 网页聊天
    • 体育实况更新
    • 股票基金报价实时更新

3.2 入门案例

  • WebSocket使用步骤:
    1. 直接使用websocket.html页面作为WebSocket客户端
    2. 导入WebSocket的maven坐标
    3. 导入WebSocket服务端组件WebSocketServer,用于和客户端通信
    4. 导入配置类WebSocketConfig,注册WebSocket的服务端组件
    5. 导入定时任务类WebSocketTask,定时向客户端推送数据

3.2.1 WebSocket.html

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">发送消息</button>
<button onclick="closeWebSocket()">关闭连接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);

//判断当前浏览器是否支持WebSocket
if('WebSocket' in window){
//连接WebSocket节点
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}

//连接发生错误的回调方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};

//连接成功建立的回调方法
websocket.onopen = function(){
setMessageInnerHTML("连接成功");
}

//接收到消息的回调方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}

//连接关闭的回调方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}

//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
window.onbeforeunload = function(){
websocket.close();
}

//将消息显示在网页上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}

//发送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}

//关闭连接
function closeWebSocket() {
websocket.close();
}
</script>
</html>

3.2.2 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* 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) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}

}

3.2.2 WebSocketConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {

@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}

}

3.2.3 WebSocketTask

1
2
3
4
5
6
7
8
9
10
11
12
13
@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()));
}
}

3.3 WebSocket——功能测试

  • WebSocket功能测试

四、来单提醒

4.1 来单提醒——功能开发

  • 用户下单并且支付成功后,需要第一时间通知外卖商家,通知的形式有如下两种:
    • 语音播报
    • 弹出提示框
  • 设计
    • 通过WebSocket实现管理端页面和服务端保持长连接状态
    • 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
    • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
    • 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
      • type:消息类型,1为来单提醒 2为客户催单
      • orderId:订单id
      • content:消息内容

4.2 来单提醒——代码实现

1
2
3
4
5
6
7
//通过websocket向客户端浏览器推送消息 type orderId content
Map map = new HashMap();
map.put("type",1);
map.put("orderId",orders.getId());
map.put("content","订单号:"+ orderNumber);
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);

4.3 来单提醒——功能测试

  • 来单提醒功能测试

五、客户催单

5.1 客户催单——功能开发

  • 用户在小程序中点击催单按钮后,需要第一时间通知外卖商家,通知的形式有如下两种:
    • 语音播报
    • 弹出提示框
  • 设计
    • 通过WebSocket实现管理端页面和服务端保持长连接状态
    • 当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息
    • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
    • 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
      • type: 消息类型,1为来单提醒 2为客户催单
      • orderId: 订单id
      • content: 消息内容
  • 接口设计

5.2 客户催单——代码实现

5.2.1 OrderController

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 用户催单
* @param id
* @return
*/
@GetMapping("/reminder/{id}")
@ApiOperation("用户催单")
public Result reminder(@PathVariable Long id) {
log.info("用户催单:{}", id);
orderService.reminder(id);
return Result.success();
}

5.2.2 OrderService

1
2
3
4
5
/**
* 用户催单
* @param id
*/
void reminder(Long id);

5.2.3 OrderServiceImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 用户催单
* @param id
*/
@Override
public void reminder(Long id) {
Orders ordersDB = orderMapper.getById(id);
if (ordersDB == null) {
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}

Map map = new HashMap();
map.put("type", 2);
map.put("orderId", id);
map.put("content", "订单号" + ordersDB.getNumber());

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

5.3 客户催单——功能测试

  • 客户催单功能测试

六、开发中的问题——自定义nginx端口号导致websocket无法连接

6.1 问题描述

  • 由于自定义了nginx的端口号,导致WebSocket无法连接

6.2 问题解决

  • 修改前端网页代码
  • 两个文件地址:
    • app.d0aa4eb3.js:“nginx-1.20.2\html\sky\js\app.d0aa4eb3.js”
    • app.d0aa4eb3.js.map:“nginx-1.20.2\html\sky\js\app.d0aa4eb3.js.map”

6.2.1 app.d0aa4eb3.js

修改ws://localhost/ws/ws://localhost:8082/ws/
这里的8082是自定义的nginx端口号

6.2.2 app.d0aa4eb3.js.map

修改process.env.VUE_APP_SOCKET_URLws://localhost:8082/ws/

重新启动即可正常连接