苍穹外卖Day10-订单状态定时处理来单提醒客户催单SpringTaskWebSocketcron表达式
苍穹外卖Day10 | 订单状态定时处理、来单提醒、客户催单、SpringTask、WebSocket、cron表达式
SpringTask
1. 介绍
定时自动执行某段java代码
2. cron表达式
cron表达式是一个字符串,可以定义任务触发的时间
日和周通常只能定义一个,另一个写成?
3. 入门案例
server下面新建一个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 executeTask(){
log.info("定时任务开始执行:{}", new Date());
}
}
订单状态定时处理
1. 需求分析和设计
不需要接口,不需要前端发送什么请求
2. 代码开发
OrderTask
package com.sky.task;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.weaver.ast.Or;
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());
// 获取减去15min之后的时间
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
// 查询超时订单--当前处于待付款状态 且 下单时间已经超过15分钟
// select * from orders where status = ? and order_time < (当前时间 - 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);
}
}
}
}
3. 功能测试
测试这种和其他功能不同,其他功能可以通过前后端联调或者接口文档测试,测试订单状态定时处理可以先临时改掉cron表达式,测试无误后再改回去
两个都修改成每5秒触发一次
WebSocket
1. 介绍
websocket是基于TCP的一种新的网络协议,它实现了浏览器和服务器全双工通信–浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性连接,并进行双向数据传输。
HTTP做不到这个效果,HTTP是请求-响应模式
2. HTTP协议和WebSocket协议对比
HTTP 协议和 WebSocket 协议都是基于 TCP 传输层的应用层协议,但其设计目标、通信模式、适用场景存在本质差异 ——HTTP 是 “单向请求 - 响应” 的短连接协议,WebSocket 是 “双向实时通信” 的长连接协议。
对比维度 | HTTP 协议(超文本传输协议) | WebSocket 协议(WebSocket 协议) |
---|---|---|
1. 通信模式 | 单向请求 - 响应(客户端发请求,服务端回响应) | 双向全双工(客户端 / 服务端可随时主动发消息) |
2. 连接性质 | 短连接(请求完成后断开,需复用则靠 Keep-Alive) | 长连接(建立后持续保持,直到主动关闭) |
3. 发起方 | 仅客户端可发起请求(服务端不能主动发消息给客户端) | 客户端发起握手建立连接,之后双方均可主动发消息 |
4. 头部开销 | 每次请求 / 响应头部大(如 HTTP/1.1 头部通常几百字节) | 仅握手阶段用 HTTP 头部,后续通信头部极小(仅 2-10 字节) |
5. 状态维护 | 无状态(服务端不保存客户端上下文,需靠 Cookie/Session 维持状态) | 有状态(连接建立后,服务端可识别客户端身份,无需重复验证) |
6. 实时性 | 低(需客户端轮询 / 长轮询获取更新,有延迟) | 高(消息实时推送,延迟毫秒级) |
7. 适用场景 | 静态资源获取、普通接口请求(如网页加载、数据查询) | 实时交互场景(如聊天、直播、实时通知) |
8. 协议标识 | URL 以 http:// /https:// 开头 | URL 以 ws:// /wss:// (加密)开头 |
1. 通信模式:“单向请求 - 响应” vs “双向全双工”
这是两者最核心的差异,直接决定了 “能否实时通信”:
- HTTP 单向请求 - 响应:
通信必须由客户端先发起 “请求”(如 GET/POST 请求),服务端才能返回 “响应”;服务端无法主动向客户端推送消息—— 若客户端需获取实时更新(如实时聊天消息),只能通过 “轮询”(每隔几秒发一次请求)或 “长轮询”(客户端发请求后,服务端 hold 住连接直到有更新)实现,本质仍是 “客户端主动问、服务端被动答”。
例:打开网页时,浏览器(客户端)发 HTTP GET 请求获取 HTML/CSS/JS,服务器返回资源后,HTTP 连接通常断开;若要获取网页的实时数据(如股票行情),需浏览器每隔 10 秒发一次 GET 请求查询最新行情。 - WebSocket 双向全双工:
连接建立后,客户端和服务端处于 “平等地位”,双方均可随时主动向对方发送消息,无需等待对方先请求 —— 类似 “打电话”,接通后双方可随时说话,无需一方 “先提问”。
例:微信网页版的聊天功能,通过 WebSocket 连接,当好友发消息时,微信服务器可直接将消息 “推” 给你的浏览器(无需你的浏览器主动查询),实现实时聊天。
2. 连接性质:“短连接(可复用)” vs “长连接(持续保持)”
- HTTP 短连接与 Keep-Alive:
标准 HTTP 是 “短连接”—— 每次请求完成后,TCP 连接会断开;为减少连接建立 / 断开的开销,HTTP/1.1 引入Connection: Keep-Alive
机制,让 TCP 连接在一定时间内(如 30 秒)复用(后续请求可复用同一连接),但本质仍是 “请求触发式” 连接,无请求时连接可能被回收,且服务端仍不能主动发消息。
例:浏览一个包含 10 张图片的网页,浏览器会用 1-6 个复用的 TCP 连接(HTTP 连接池)依次请求图片,所有图片加载完成后,连接会在闲置一段时间后断开。 - WebSocket 长连接:
连接通过 “HTTP 握手” 建立后,TCP 连接会持续保持(直到客户端 / 服务端主动调用close()
关闭),期间即使没有消息传输,连接也不会被随意断开(可通过 “心跳包” 维持连接,避免被防火墙 / 路由器判定为闲置连接而断开)。
例:直播平台的 “实时弹幕” 功能,用户打开直播间后,浏览器与直播服务器建立 WebSocket 长连接,后续所有弹幕消息(用户发送、服务器推送)都通过这个连接实时传输,连接会持续到用户关闭直播间。
3. 头部开销:“每次请求大开销” vs “仅握手一次开销”
HTTP 的性能瓶颈之一是 “头部开销”,而 WebSocket 完美解决了这一问题:
- HTTP 头部开销大:
每次 HTTP 请求 / 响应都需要携带完整的头部(如请求行、Host、Cookie、User-Agent、Accept 等),头部大小通常几百字节,甚至超过实际传输的 “业务数据”(如一个查询用户信息的请求,数据仅 50 字节,头部却有 300 字节);即使复用连接,每次请求仍需携带头部。 - WebSocket 头部开销极小:
仅在 “建立连接的握手阶段” 使用 HTTP 头部(格式类似 HTTP 请求,用于告诉服务器 “要升级为 WebSocket 连接”),握手成功后,后续传输的 “WebSocket 帧” 头部仅 2-10 字节(包含帧类型、数据长度等核心信息),业务数据占比极高,传输效率远高于 HTTP。
例:实时推送温度数据(每秒 1 次,每次数据 20 字节),用 HTTP 每次需额外携带 300 字节头部,总开销 320 字节 / 次;用 WebSocket 仅需 2+20=22 字节 / 次,开销仅为 HTTP 的 1/14。
4. 状态维护:“无状态” vs “有状态”
- HTTP 无状态:
服务端不保存客户端的 “连接状态”—— 每次 HTTP 请求都是独立的,服务端无法通过连接识别客户端身份,需通过 Cookie、Session ID、Token 等额外机制让服务端记住客户端(如登录状态),增加了开发复杂度和请求开销。 - WebSocket 有状态:
连接建立后,服务端会为每个 WebSocket 连接分配唯一标识(如sessionId
),并保存连接上下文(如客户端用户 ID、连接状态);后续双方通信时,服务端可直接通过连接标识识别客户端,无需重复传递身份信息(如 Token),简化开发且减少开销
HTTP 适用场景:非实时、单向请求 - 响应
- 静态资源获取:网页(HTML/CSS/JS)、图片、视频、文件下载;
- 普通接口请求:数据查询(如用户信息、商品列表)、表单提交(如登录、注册)、数据上传(如上传图片);
- 无实时需求的业务:博客浏览、电商商品详情页、新闻阅读。
WebSocket 适用场景:实时、双向交互
- 实时聊天:网页版微信、企业 IM(如钉钉网页版)、在线客服;
- 实时通知:订单状态更新(如 “订单已发货” 推送)、消息提醒(如 “收到新评论”);
- 实时数据展示:股票行情、实时监控(如设备温度 / 湿度)、直播弹幕;
- 实时协作:在线文档协作(如腾讯文档)、多人在线游戏(如网页版小游戏)。
3. 入门案例
来单提醒
1. 需求分析和设计
2. 代码开发
在notify/PayNotifyController的paySuccessNotify函数中,最后通过orderService.paySuccess(outTradeNo); 进行修改订单状态、来电提醒
所以通过OrderServiceImpl的paySuccess方法,通过websocket 向用户端推送消息
/**
* 支付成功回调
*
* @param request
*/
@RequestMapping("/paySuccess")
public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
//读取数据
String body = readData(request);
log.info("支付成功回调:{}", body);
//数据解密
String plainText = decryptData(body);
log.info("解密后的文本:{}", plainText);
JSONObject jsonObject = JSON.parseObject(plainText);
String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
String transactionId = jsonObject.getString("transaction_id");//微信支付交易号
log.info("商户平台订单号:{}", outTradeNo);
log.info("微信支付交易号:{}", transactionId);
//业务处理,修改订单状态、来单提醒
orderService.paySuccess(outTradeNo);
//给微信响应
responseToWeixin(response);
}
实际追加代码
/**
* 支付成功,修改订单状态
*
* @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向客户端浏览器推送消息 type orderId content
Map map = new HashMap();
map.put("type", 1); // 1表示来单提醒 2表示用户催单
map.put("orderId", ordersDB.getId());
map.put("content", "订单号" + outTradeNo);
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
}
导入代码: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);
}
/**
* 收到客户端消息后调用的方法
* 类似于controller
* @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();
}
}
}
}
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();
}
}
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()));
}
}
使用cpolar进行内网穿透
使用穿透后的地址配置application-dev,将内网穿透后的地址配置到dev文件
notifyUrl: http://1d4c81e9.r7.vip.cpolar.cn/notify/paySuccess
refundNotifyUrl: http://1d4c81e9.r7.vip.cpolar.cn/notify/refundSuccess
3. 功能测试
前端页面先请求到nginx,由nginx反向代理请求、转发后端服务器,需要提前在nginx配置好相关路径
将schedual五秒定时提醒的注释给注释掉,重新登陆管理员身份,可以正常在用户下单后播报语音提醒
客户催单
1. 需求分析和设计
2. 代码开发
user/OrderController
/**
* 客户催单
* @param id
* @return
*/
@GetMapping("/reminder/{id}")
@ApiOperation("客户催单")
public Result reminder(@PathVariable("id") Long id){
orderService.reminder(id);
return Result.success();
}
OrderService
/**
* 客户催单
* @param id
*/
void reminder(Long id);
OrderServiceImpl
/**
* 客户催单
* @param id
*/
public void reminder(Long id) {
// 根据id查询订单
Orders ordersDB = orderMapper.getById(id);
// 校验订单是否存在
if (ordersDB== null){
throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
}
Map map = new HashMap();
map.put("type", 2); //1表示来单提醒 2表示客户催单
map.put("orderId", id);
map.put("content", "订单号:" + ordersDB.getNumber());
//通过websocket向客户端浏览器推送消息
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}
3. 功能测试
在用户端点击催单按钮可以收到语音播报催单提醒