苍穹外卖day10
功能实现:订单状态定时处理
思考1:如果用户一直卡在支付界面怎么处理

思考2:如果用户已收到外卖,但是管理员却没有及时点击完成订单该怎么办

解决:需要使用一个可以定时处理任务的方法
1.Spring Task
1.1 介绍
Spring Task是Spring框架提供的任务调度工具,可以按照约定时间对任务进行执行
应用场景:
1)火车自动抢票
2)自动的短信通知
3)定时闹钟
总之,一切需要定时任务的执行都可以使用Spring Task
1.2 cron表达式
介绍:cron表达式实际上是一个字符串,可以自定义设定触发时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
举例:2025年6月8日14点28分56秒对应的cron表达式是56 28 14 8 6 ? 2025
说明:一般日和周的值不同时设置,其中一个设置,另一个用?表示。
比如:描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。
为了描述这些信息,提供一些特殊的字符。这些具体的细节,我们就不用自己去手写,因为这个cron表达式,它其实有在线生成器。
cron表达式在线生成器:在线Cron表达式生成器

可以直接使用该网站进行生成,一般不自己手写
通配符:
* 表示所有值;
? 表示未说明的值,即不关心它为何值;
- 表示一个指定的范围;
, 表示附加一个可能值;
/ 符号前表示开始时间,符号后表示每次递增的值;
常用cron表达式例子
(1)0/2 * * * * ? 表示每2秒 执行任务
(1)0 0/2 * * * ? 表示每2分钟 执行任务
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2点调整任务
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15执行作业
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每个月的最后一个星期五上午10:15执行
(4)0 0 10,14,16 * * ? 每天上午10点,下午2点,4点
(5)0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时
1.3 入门案例
1.3.1 导入坐标
实际上由于该框架体量较小,该坐标spring - context 已经存在于 spring-boot-starter中

1.3.2 注解 @EnableScheduling
在启动类里添加注解 @EnableScheduling 开启任务调度

1.3.3 实例
在sky-serve模块中的com.sky包下新建一个task包,创建一个MyTask类

代码如下:
package com.sky.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 自定义定时任务类
*/
@Component
@Slf4j
public class Mytask {
@Scheduled(cron = "0/5 * * * * ?")
public void exe(){
log.info("任务开始执行:{}",new Date());
}
}
1.3.3:功能测试
启动服务,查看日志

每隔5s执行一次
2.订单状态定时处理
2.1 需求分析
用户下单后可能存在的情况:
下单后未支付,订单一直处于“待支付”状态
用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态


支付超时的订单如何处理? 派送中的订单一直不点击完成如何处理?
对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:
通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”
2.2 代码开发(该需求可以使用redis来减轻数据库压力,详情见此处)
(1)自定义定时任务类OrderTask
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;
/**
* 处理超时订单的方法
*/
@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 * * ?")//每日凌晨一点
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) 在OrderMapper接口中扩展方法:
/**
* 查询状态和当前时间
* @param status
* @param orederTime
* @return
*/
@Select("select * from orders where status = #{status} and order_time < (#{orderTime})")
List<Orders> getByStatusAndOrderTimeLT(@Param("status")Integer status,@Param("orderTime")LocalDateTime orderTime);
2.3 功能测试
可以通过如下方式进行测试:
-
查看控制台sql
-
查看数据库中数据变化
可以先将每个任务间隔改成几秒触发一次,方便测试 , 然后可以手动改一下数据库中订单状态,方便测试 ;
功能测试:来单提醒和客户催单
3.WebSocket
3.1 介绍
WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
HTTP协议和WebSocket协议对比:
-
HTTP是短连接
-
WebSocket是长连接
-
HTTP通信是单向的,基于请求响应模式
-
WebSocket支持双向通信
-
HTTP和WebSocket底层都是TCP连接

思考:既然WebSocket这么好用,是否可以代替HTTP呢?
WebSocket的劣势包括:
-
需要浏览器和服务器都支持: WebSocket是一种相对新的技术,需要浏览器和服务器都支持。一些旧的浏览器和服务器可能不支持WebSocket。
-
需要额外的开销: WebSocket需要在服务器上维护长时间的连接,这需要额外的开销,包括内存和CPU。
-
安全问题: 由于WebSocket允许服务器主动向客户端发送数据,可能会存在安全问题。服务器必须保证只向合法的客户端发送数据。
应用场景:
1)视频弹幕
2)网页聊天
3)体育实况更新
总之,WebSocket用于不用刷新页面,可以实时展示数据的情况
3.2 入门实例
3.2.1 案例
实现浏览器与服务器全双工通信。浏览器既可以向服务器发送消息,服务器也可主动向浏览器推送消息。
页面展示:

实现步骤:
1). 直接使用websocket.html页面作为WebSocket客户端
2). 导入WebSocket的maven坐标
3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信
4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
5). 导入定时任务类WebSocketTask,定时向客户端推送数据
3.2.2 代码开发
1)导入maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2)定义WebSocketServer组件(资料以提供)
package com.sky.websocket;
import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
* 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)定义WebSocketConfiguration配置类
package com.sky.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
4)定义WebSocketTask任务类
package com.sky.task;
import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
@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()));
}
}
5)定义HTML页面(资料中已提供)
<!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>
WebSocketServe开头的这段代码
@ServerEndpoint("/ws/{sid}")
对应着html的这段代码,这样就可以连接客户端与服务端了
ws://localhost:8080/ws/"+clientId
3.2.3 功能测试
启动服务
建立连接:

![]()
浏览器向服务端发送数据:


点击关闭连接:

![]()
4.来单提醒
4.1 需求设计与分析
用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:
-
语音播报
-
弹出提示框

设计思路:
● 通过WebSocket实现管理端页面和服务端保持长连接状态
● 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
● 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
● 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
● type 为消息类型,1为来单提醒 2为客户催单
● orderId 为订单id
● content 为消息内容
注意 :
-
通过WebSocket实现管理端页面和服务端保持长连接状态 : 这个通过WebSocketServer.java实现与客户端进行连接 ;而前端已经通过一些js代码实现与服务端进行连接 ;
-
通过检查发现

前端请求的地址是红线标注的那一段,而后端是8080端口。原因 : 前端先请求到nginx服务器,然后在nigix服务器中做了反向代理 ,将请求转发到后端的tomcat服务器;
可以在nigix.conf(配置文件中查看) :


前端发送请求后:

4.2 代码开发
将WebSocketServer对象注入OrderServiceImpl中。
本该修改的是paySucess方法,但因为跳过了支付功能(详情见此处)所以在payMent方法处补充,将订单状态设置成待接单,并转换为JSON格式,代码如下:
/**
* 订单支付
*
* @param ordersPaymentDTO
* @return
*/
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getByOpenId(String.valueOf(userId));
//调用微信支付接口,生成预支付交易单
/*JSONObject jsonObject = weChatPayUtil.pay(
ordersPaymentDTO.getOrderNumber(), //商户订单号
new BigDecimal(0.01), //支付金额,单位 元
"苍穹外卖订单", //商品描述
user.getOpenid() //微信用户的openid
);
if (jsonObject.getString("code") != null && jsonObject.getString("code").equals("ORDERPAID")) {
throw new OrderBusinessException("该订单已支付");
}*/
JSONObject jsonObject = new JSONObject();
jsonObject.put("code","ORDERPAID");
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));
//为替代微信支付成功后的数据库订单状态更新,多定义一个方法进行修改
Integer OrderPaidstatus =Orders.PAID;
//支付状态,已支付
Integer Orderstatus = Orders.TO_BE_CONFIRMED; //订单状态,待接单
//发现没有将支付时间 check out属性赋值,所以在这里更新
LocalDateTime check_out_time = LocalDateTime.now();
//获取订单号码
String orderNumber =ordersPaymentDTO.getOrderNumber();
log.info("调用updatestatus,用于替换微信支付更新数据库状态的问题");
orderMapper.updatestatus(Orderstatus, OrderPaidstatus, check_out_time, orderNumber);
//根据订单号和用户id查询
Orders ordersDB = orderMapper.getByNumberAndUserId(orderNumber,userId);
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
//通过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);
return vo;
}
4.3 功能测试
客户端与服务端建立长连接 :


用户提交订单:

前端提醒:

注意:
1)因为音频是在前端设定好的,所以可以直接使用
2)如果 音频一直响的话,将WebSocketTask中设置的定时任务给注释了

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

5.2 代码开发
5.2.1 Controller层
/**
* 催单
* @param id
* @return
*/
@GetMapping("/reminder/{id}")
@ApiOperation("催单")
public Result reminder(@PathVariable Long id){
log.info("催单的id:{}",id);
orderService.reminder(id);
return Result.success();
}
5.2.2 Srevice层
/**
* 客户催单
* @param id
*/
void reminder(Long id);
5.2.3 ServiceImpl层
/**
* 客户催单
* @param id
*/
@Override
public void reminder(Long id) {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(Math.toIntExact(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());
//通过websocket向客户端浏览器推送消息
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}
5.2.4 Mapper层
/**
* 查询id的全部数据
* @param id
* @return
*/
@Select("select * from orders where id = #{id}")
Orders getById(Integer id);
5.3 功能测试
用户点击催单:

管理端响起催单请求:

625






