思考:
订单状态相关处理
1.超时订单
用户下单后 跳转到确认支付页面进行付款,问题:用户下单后要在15分钟内支付
如果不支付,判定为订单超时,需要取消操作(程序自动完成,不需要人工操作)
2.派送中的订单
一直不点击完成,自动更新为已完成
另外两个功能
1.来单提醒
用户支付完成,商家 那端要进行语音播报 (提醒来单),并弹出来单提示框。
2.客户催单
用户支付完成,商家不接单,用户端 点击催单 ,商家 那端进行语音播报,提示弹出催单提示框
1.Spring Task
1.介绍:
是Spring框架提供的任务调度工具,可以按照约定的时间 自动执行某个代码逻辑
(以前开发的程序是基于请求响应的);
即他是一个定时任务框架
作用:
定时自动执行某段java代码
应用场景:
信用卡每月还款提醒
银行贷款每月还款提醒
火车票售票系统处理未支付订单
入职纪念日为用户发送通知
只要是需要定时处理的场景都可以使用Spring Task
2.corn表达式(定义定时任务(触发时间))
介绍
cron表达式其实就是一个字符串 ,通过cron表达式可以定义任务触发的时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)
注意:日和周往往不能同时出现
(一个为具体值,另一个为?)
eg:
6月10号不一定是星期五
eg:
2022年10月12日上午9点整 对应的cron表达式为:
0 0 9 12 10 ? 2022
注意:表达式相关注意事项
*:每多少就发送
?:未知(不填)
但有些表达式难写,因为表达式除了数字,还有一些特殊的字符
比如2月最后一天,可能是28/29(用特殊的字符进行描述)
cron表达式在线生成器:
3.入门案例
Spring Task使用步骤:
①导入maven坐标 spring-context(已存在)

②启动类添加注解 @EnableScheduling 开启任务调度:(类似一个开关,加在启动类)
【事务控制、缓存处理的逻辑也一样要添加开启注解】
③自定义定时任务类【要记得类加上@Component注解(表示当前这个类需要实例化,并交给Spring容器管理)】
内容:
包含具体业务逻辑,定时任务什么时候触发
定义方法:
方法不要返回值,方法名任意,定义任务逻辑(定时发短信、发邮件、做统计工作、处理订单状态...)
@Scheduled(corn="表达式"):
该注解为:方法执行触发点
在spring-context包内
代码
@Component
@Slf4j
public class MyTask {
/**
* 定时任务: 每隔5秒执行一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void myTask() {
log.info("定时任务开始执行:{}",new Date());
}
}
4.使用注意事项(重点)
1.表达式的写法
2.具体业务逻辑
2.订单状态定时处理【超时订单,完成派送订单】
1.需求分析
用户下单后可能存在的情况:
•下单后未支付,订单一直处于"待支付"状态
•用户收货后管理端未点击完成按钮,订单一直处于"派送中"状态
对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:
•通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为"已取消"
•通过定时任务每天凌晨1点检查一次是否存在"派送中"的订单,如果存在则修改订单状态为"已完成"
由于这里不需要请求响应 ,所以没有接口的概念,不需要接口
2.代码开发
package com.sky.task;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.service.OrderService;
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;
/**
* 处理超时订单的方法,超过15分钟未支付则取消订单
*/
//每分钟执行一次
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder(){
log.info("定时处理超时订单:{}", LocalDateTime.now());
//具体业务逻辑相关的代码
//获取所有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);
}
}
}
/**
* 处理一直处于派送中的超时订单
*/
//每日1点执行
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder(){
log.info("定时处理派送中订单:{}", LocalDateTime.now());
//获取所有派送中的订单(集合),并且订单状态为派送中
LocalDateTime time = LocalDateTime.now().plusHours(-1);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
if (ordersList != null && ordersList.size() > 0){
for (Orders orders : ordersList) {
//更新状态,派送中订单改为已完成
orders.setStatus(Orders.COMPLETED);
orders.setDeliveryTime(LocalDateTime.now());
orderMapper.update(orders);
}
}
}
}
3.功能测试
改成5秒测试一次,方便测试
可以通过如下方式进行测试:
•查看控制台sql
•查看数据库中数据变化
3.WebSocket(新型协议:消息推送)【来单提醒、客户催单】
可以实现客户端浏览器和服务端进行双向数据传输,
基于该协议,就可以向客户端浏览器来**推送消息**
介绍
WebSocket 是基于 TCP 的一种新的网络协议 。它实现了浏览器与服务器全双工通信------浏览器和服务器只需要完成一次握手 ,两者之间就可以创建持久性 的连接, 并进行双向数据传输。
HTTP协议和WebSocket协议对比:
•HTTP是短连接
•WebSocket是长连接
•HTTP通信是单向的,基于请求响应模式
•WebSocket支持**双向通信**
•HTTP和WebSocket底层都是TCP连接
http:
请求响应模式


应用场景:也使用到了定时任务
•视频弹幕
•网页聊天(最典型的应用场景,即通过服务器把消息主动推送到网页上)
•体育实况更新
•股票基金报价实时更新
入门案例:
重点:
案例主要了解执行流程,代码不为重点
实现步骤:
①直接使用websocket.html页面作为WebSocket客户端
②导入WebSocket的maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
③**最重要** :导入WebSocket服务端组件 WebSocketServer,用于和客户端通信
【类似于springMVC中的Controller:用于响应客户端发出的http请求】
这里的客户端是基于WebSocket的方式
注意3:
1.该组件需要加入Component注解,表示要将这个类交给Spring容器进行管理
2.@ServerEndpoint("/ws/{sid}"):(WebSocket包下的)
括号里面和websocket.html中连接webSocket节点的路径对应:即根据路径进行匹配,这样就能请求到我们当前这个服务端组件,类似Controller
④导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
作用:
通过配置类,让组件生效
⑤导入定时任务类WebSocketTask,定时向客户端推送数据
回调方法和普通方法用途场景区别
回调方法将一个方法(或函数)作为一个参数传递给另一个方法,并在特定事件发生时或某个操作完成后被调用
这里回调方法在webSocket.html里面
-
普通方法:主要用于完成一些同步的、可以立即得到结果的操作,比如简单的数学计算、字符串处理等。
-
回调方法 :常用于处理异步操作(如网络请求、文件读写等)、事件驱动的场景(如用户点击事件、WebSocket 连接关闭事件等)。像图中
websocket.onclose就是一个回调函数,当 WebSocket 连接关闭这个事件发生时,才会执行该回调函数;window.onbeforeunload也是如此,当窗口即将关闭这个事件触发时,才会调用对应的回调函数去关闭 WebSocket 连接。
对于大部分浏览器,都是支持web Socket协议的,所以直接new一个webSocket,就能把他创建出来
代码:WebSocket服务端组件WebSocketServer
阅读代码
1.存放会话对象到map集合
放入session(WebSocket包下的),也是代表会话的意思
即客户端和服务端要建立一个连接,实际上就是一个会话,建立好会话后,他们间就可以双向通信
建立这个map 是用来存储客户端的会话对象
2.OnOpen方法
使用OnOpen注解(WebSocket包下的)):使当前方法变成一个回调方法
【类似客户端websocket.html中的回调方法,服务端 是通过注解表示回调方法,客户端是通过回调函数】
作用:建立连接会调用
握手成功后,这个连接就建立好了,服务端就会调用这个**OnOpen**方法
(过程不用管,该调用是由webSocket这个小框架调用的)
传入参数:
该方法传入session,代表客户端和服务端建立的一个会话 ,具体是哪个客户端:由另一个被注解@PathParam标记的参数sid表示,也就是注解**@ServerEndpoint("/ws/{sid}")** 里面的sid进行动态表示
方法体:
将会话对象session放到map里面,并使用sid作为key
3.OnMessage
作用:
收到客户端消息后会调用该方法
其类似于controller方法:客户端发送请求到服务端,服务端就需要执行一个方法
4.OnClose
当客户端和服务端连接关闭后会调用
方法体:
清理map【用remove清理会话】
5.sendToAllClient
该方法时需要主动调用的方法,没有注解
方法体:
将map取出,遍历map(将里面的session都拿出来,去调用一个固定的方法:向客户端来发送消息)
【前面强调为双向通信,那服务端向客户端发送消息就是通过调用这个固定的方法】
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();
}
}
}
}
注意:这个组件代码是固定的,然后需要一个配置类去注册WebSocket的Bean(注册代码也是固定的)
思考
既然WebSocket支持双向通信,功能看似比HTTP强大,那么我们是不是可以基于WebSocket开发所有的业务功能?
WebSocket缺点:
•服务器长期维护长连接需要一定的成本
•各个浏览器支持程度不一
•WebSocket 是长连接,受网络限制比较大,需要处理好重连
结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用
4.来单提醒
需求分析和设计
用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:
•语音播报
•弹出提示框
实现思路
主题流程为3个
1.商家管理端页面和后端服务建立 一个WebSocket的长连接,从而保持两端长连接状态
2.当用户下单且支付成功后,服务端 调用WebSocket的相关API实现服务端向客户端推送消息
3.推送消息后,客户端 浏览器解析 服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
判断消息形式
前后端需要定义规范
【即前后端约定:推送来的消息具体的数据格式(什么表示来单消息,什么表示催单消息)】
4.约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
-
type 为消息类型,1为来单提醒 2为客户催单
-
orderId 为订单id
-
content 为消息内容
代码开发
1.长连接
【这里之前已经导入WebSocketServer组件,前端代码已经提供好客户端WebSocketServer相关的的js代码】
2.服务端推送消息
用户下单且支付成功后,需要推消息(基于WebSocketServer,里面有一个群发的方法,可以实现将消息推送到客户端)
该代码位置:
用户下单且支付成功后,所以在微信支付回调 相关接口这个类的paySuccessNotify方法调用OrderServiceImpl的paySuccess方法里 添加推送消息方法【因为该方法是修改订单状态为已支付,所以进行来单提醒】
推送消息的形式是已经做好的约定(json格式:包含3个字段):所以可以将这3个字段封装到map里面,然后再转成json格式(JSON.toJSONString)
代码:修改前的代码位置
OrderServiceImpl.java
该代码行不通
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 centext
HashMap map = new HashMap();
// 1:来单提醒;2:客户催单
map.put("type", 1);
map.put("orderId", ordersDB.getId());
map.put("context", "订单号:" + ordersDB.getNumber());
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
}
代码:修改后的代码位置
/**
* 导入的代码:订单支付
*
* @param ordersPaymentDTO
* @return
*/
public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
User user = userMapper.getById(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", "OPDERPAID");
OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class);
vo.setPackageStr(jsonObject.getString("package"));
//订单状态:待接单
Integer OrderStatus = Orders.TO_BE_CONFIRMED;
//支付状态:已支付
Integer OrderPaidStatus = Orders.PAID;
//更新支付时间
LocalDateTime check_out_time = LocalDateTime.now();
orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, this.orders.getId());
//通过WebSocket推送消息给前端浏览器 type orderId centext
Map map = new HashMap();
// 1:来单提醒;2:客户催单
map.put("type", 1);
map.put("orderId", this.orders.getId());
map.put("context", "订单号:" + this.orders.getNumber());
String json = JSON.toJSONString(map);
webSocketServer.sendToAllClient(json);
return vo;
}
我的疑问:关于订单号是orders表的getId(主键)还是orders表的number
前端收到 WebSocket 消息后,通常需要根据 orderId 进行页面跳转或数据查询 虽然通知内容显示的是订单号(number),但系统内部操作使用的是 id 因此,这里使用 ordersDB.getId() 是正确的,因为 WebSocket 通知需要传递的是系统内部使用的订单 ID,而不是外部显示的订单号
功能测试(nginx,websocket请求逻辑)
1.websocket请求不到的,nginx端口自己修改过的,要修改前端的端口。位置在nginx的安装目录/html/sky/js/app.js文件中,ctrl+F查找 ws: localhost/ws/ ,然后修改成 ws: localhost:(自己设置的nginx端口号)/ws/,然后重启生效
重启后清理一下浏览器缓存,或者重启电脑就不会显示临时标题了
2.本质上第一次登录请求,浏览器控制台状态码中显示该请求状态码为101,idea显示某用户建立了连接
实际上就是websocket的握手过程,然后建立好长连接了
当用户下单并支付之后,就可以基于这个连接给我们这个客户端推送消息
3.接下来看请求地址,并不是端口8080,那么是怎么请求到后端的tomcat服务器(其端口为8080)?并且还连接成功了?
我们看:
现在这个请求,是先请求到nginx,然后由它的反向代理,实际上转发到了后端【websocket这个请求,实际上是转发到了后端,即websocket这次请求是通过nginx进行了一次转发,才发送到后端】
前提是:在nginx里面配置好路径(前端是基于nginx启动的)
nginx的服务器的conf文件也配置了websocket的路径
html文件里面也配置了路径
4.然后如果我们想回调成功的话,需要回调地址改对,这个地址(后端)是通过内网穿透获得的【要启动内网穿透工具】
由于没使用该工具
我将代码修改到下单时调用
如下图(在用户下单后点击支付就立即提示接单,因为在前面设置支付的时候,默认都是直接支付成功,所以跳过了paySuccess方法):

还有一个问题:
一直响的是之前webSocket案例定时任务造成的, 把每5秒执行那个注解注掉
还有:
我修改了OrderServiceImpl.java的代码
注释掉拒单和取消订单功能时调用微信支付的代码,否则出现空指针异常
然后只剩下一个问题:
来单提醒没有提示订单号(但查看订单可以看到)以及浏览器控制台没有显示3个属性( type orderId centext),和查看订单中没有打包费【查看菜品,待派送那些栏将菜品打包费算进去菜品费用了】
5.客户催单
需求分析和设计
用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。通知的形式有如下两种:
•语音播报
•弹出提示框
设计:与上面一样
•通过WebSocket实现管理端页面和服务端保持长连接状态
•当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息
【涉及一个接口,提交订单id】
•客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
•约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
-
type 为消息类型,1为来单提醒 2为客户催单
-
orderId 为订单id
-
content 为消息内容
代码开发
OrderController.java
/**
* 客户催单
*
* @param id
* @return
*/
@GetMapping("/reminder/{id}")
@ApiOperation("客户催单")
public Result reminder(@PathVariable("id") Long id) {
orderService.reminder(id);
return Result.success();
}
OrderService.java
/**
* 客户催单
*
* @param id
*/
void reminder(Long id);
OrderServiceImpl.java
/**
* 客户催单
*
* @param id
*/
@Override
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();
//1:表示来单提醒,2:表示客户催单
map.put("type", 2);
map.put("orderId", id);
map.put("content", "订单号:"+ordersDB.getNumber());
//通过websocket推送催单提醒
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}