苍穹外卖Day10 | 订单状态定时处理、来单提醒、客户催单、SpringTask、WebSocket、cron表达式
目录
SpringTask
1. 介绍
2. cron表达式
3. 入门案例
订单状态定时处理
1. 需求分析和设计
2. 代码开发
3. 功能测试
WebSocket
1. 介绍
2. HTTP协议和WebSocket协议对比
1. 通信模式:“单向请求 - 响应” vs “双向全双工”
2. 连接性质:“短连接(可复用)” vs “长连接(持续保持)”
3. 头部开销:“每次请求大开销” vs “仅握手一次开销”
4. 状态维护:“无状态” vs “有状态”
HTTP 适用场景:非实时、单向请求 - 响应
WebSocket 适用场景:实时、双向交互
3. 入门案例
编辑
编辑
来单提醒
1. 需求分析和设计
2. 代码开发
3. 功能测试
客户催单
1. 需求分析和设计
2. 代码开发
3. 功能测试
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 {@Autowiredprivate 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 contentMap 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();/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("sid") String sid) {System.out.println("客户端:" + sid + "建立连接");sessionMap.put(sid, session);}/*** 收到客户端消息后调用的方法* 类似于controller* @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, @PathParam("sid") String sid) {System.out.println("收到来自客户端:" + sid + "的信息:" + message);}/*** 连接关闭调用的方法** @param sid*/@OnClosepublic 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 {@Beanpublic 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 {@Autowiredprivate 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/paySuccessrefundNotifyUrl: 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. 功能测试
在用户端点击催单按钮可以收到语音播报催单提醒