一.定时任务
实现定时任务可以使用spring家族中的sprinig-task;
1.1 spring-task
spring-task是Spring框架的任务调度工具,可以按照约定的时间自动执行某个代码逻辑;
应用场景
- 信用卡每月归还贷款提醒,定时任务检查,每天查看有没有提醒的人,有的话自动提醒;
- 车票处理未支付订单(xx分钟之内付款,超时自动取消);
- 入职纪念日为用户发送通知;
cron表达式的使用
- 本质上是一个字符串,可以用来定义任务触发的时间;
- 构成规则:分为6/7个域,由空格隔开,每个域代表一个含义,每个域可以分为:秒,分钟,小时,日,月,周,年(可选),
- 日和周通常只写一个,另一个写?例如:2023年10月12日上午9点整对应的cron表达式为:0 0 9 12 10 ? 2023
- cron表达式在线生成器:https://cron.qqe2.com/
入门案例
- 导入坐标:实际上是spring-context依赖,但是由于spring-boot-starter已经包含了spring-context依赖,因此直接导入springboot的依赖即可,会将context传递过来;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
- 在启动类上加入@EnableScheduling注解开启任务调度
- 自定义定时任务类(写业务逻辑)
@Component
@Slf4j
/**
* 自定义定时任务类
*/
public class MyTask {
//从第0秒开始,每隔5秒触发一次;
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask(){
log.info("定时任务开始执行......"+ LocalDateTime.now());
}
}
用户下单之后可能存在
- 下单之后未支付,订单一直处于“待支付”状态;超时后订单需要自动取消;
- 用户收获之后未点击“完成”按钮,订单一直处于“派送中”状态;超时后定时任务应该自动完成;
1.2 超时订单的处理
- 通过定时任务设置每分钟检查一次是否存在支付超时订单(下单完成后超过15分钟仍未支付则判定为超时订单),如果存在则修改订单为“已取消”;
/**
* 处理超时订单,每分钟触发一次
*/
// @Scheduled(cron = "0 * * * * ?")
@Scheduled(cron = "0 0/10 * * * ?") //每10分钟触发一次
public void processTimeoutOrder() {
log.info("定时处理超市订单...{}", LocalDateTime.now());
//查询订单表,时候有处于待付款的订单;
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTime(Orders.PENDING_PAYMENT, time);
if (ordersList != null && ordersList.size() != 0) {
for (Orders order : ordersList) {
order.setStatus(Orders.CANCELLED);
order.setCancelReason("订单超时,自动取消");
order.setCancelTime(LocalDateTime.now());
orderMapper.update(order);
}
}
}
1.3 用户已收到货物但未点击“完成”的仍处于派送中的订单处理
- 对于派送中的订单,一天只需要检查一次即可(可以设置为每天凌晨4点检查一次),是否存在“派送中”的订单,如果存在,则修改为“已完成”;否则如果一分钟检查一次,可能出现用户还未收到货,但是已经完成了。
/**
* 处理派送中订单,每天触发一次
*/
@Scheduled(cron = " 0 0 4 * * ? ") //每月每天凌晨四点
public void processDeliveryOrder() {
log.info("定时处理派送中订单...{}", LocalDateTime.now());
//每天凌晨四点清算上一天的派送中订单(小于上一天24:00的订单都完成)
LocalDateTime time = LocalDateTime.now().plusMinutes(-240);
List<Orders> ordersList = orderMapper.getByStatusAndOrderTime(Orders.DELIVERY_IN_PROGRESS, time);
if (ordersList != null && ordersList.size() != 0) {
for (Orders order : ordersList) {
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
}
}
}
二.WebSocket
- 基于TCP新的网络协议;实现了浏览器与服务器的全双工通信;浏览器可以同浏览器进行双向数据传输,浏览器与服务器只需要完车一次握手,二者之间就会创建持久性连接,并进行双向数据传输;类似打电话。
- HTTP协议是一定是客户端先请求服务器,HTTP是短连接单向通信,请求-响应模式,
- WebSocket与HTTP底层都是基于TCP协议;
应用场景
适用于页面并没有刷新,但是数据在实时更新的场景,即用户;
- 视频弹幕;
- 网页聊天;
- 体育实况更新;
- 股票基金实时报价;
websocket使用流程
- 导入maven坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
- 导入websocket配置类
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
- 导入websocket服务端代码
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}") //websocket客户端请求路径;
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();
}
}
}
}
可能出现的问题
- 1.导入代码之后,如果出现了websocket客户端连接失败的问题,可能原因是代码导入之后未重新编译,手动点击“重新构建代码”即可
- 2.由于前边在配置nginx反向代理时,可能会由于IIS占用80端口,导致Nginx无法监听在80端口,因此有2种解决方案:1.关闭IIS服务,2.将监听端口修改为其他端口比如8888端口;由于第一种方案可能带来一些影响并且比较麻烦,因此通常使用第2种方案,但是如果使用第2种方案,现在就会引入这一新的问题:由于NGINX反向代理的存在,请求会被先转发到nginx服务器,再由nginx服务器转发到我的服务器,然而最开始由客户端发送的请求ws://localhost/ws/k3ql4hupss默认是80端口,而我的nginx服务器运行在8888端口,所以就连nginx服务器都无法收到这个请求,更别说我的服务器了,因此会造成服务器无法接收到建立websocket连接的请求;
- 此时如果最开始配置nginx时你使用了第一种方案,那么恭喜你,你不用修改任何东西,即可继续正常运行;如果你使用了第二种方案(我把nginx监听端口设置为了8888端口),此时要么改回80端口,但是需要关闭IIS服务,要么修改前端代码,我直接修改前端代码:找到nginx目录下的html目录,里边部署了sky-take-out的前端工程;
- 进入sky目录下:
- 进入js目录:
- 使用vscode或其他编辑器打开app.d0aa4eb3.js文件,ctrl+F查找"ws://localhost",如图所示,原本为"ws://localhost/ws/“改为"ws://localhost:你的nginx监听端口/ws/”
- 修改之后重启nginx;并记得清除浏览器缓存(我因为没请缓存所以改过之后还是旧的请求想了半天不知道哪出问题…)修改成功之后的效果:
三.来单提醒与用户催单
3.1 来单提醒
- 商家管理端页面与服务端建立长连接;
- 当支付成功之后,调用websocket的相关API实现服务端向客户端推送消息;
- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报;
- 约定服务端发送给客户端浏览器数据的数据格式为JSON,字段包括:type,orderId,content;
- type 消息类型1来单提醒,2客户催单;
- orderId 订单ID;
- content消息内容;
内网穿透工具cpolar
- 访问网址cpoloar.com
- 注册账号并登陆
- 下载windows版
- 进行傻瓜式安装
- 回到cpolar页面,点击“验证”
会得到你的令牌,复制之
进入到带有cpolar.exe的目录,打开命令行
windows下使用命令:
cpolar.exe authtoken 你的authtoken
会生成一个内网穿透的配置文件,文件的地址会给出:
然后可以临时启动一个服务指定一个IP地址:
cpolar.exe http 8080
此时生成了一个域名并被映射到localhost:8080,使用临时域名访问swagger接口文档(注意复制时如果cmd窗口无法复制,可以右键点击“标记”,在此之后便可以复制)
修改配置文件
由于前边使用的模拟微信支付,直接调用了支付成功的接口,所以并没有微信支付的相关配置;
wechat:
appid: 你的appid
secret: 你的appsecret
notify-url: https://21e88914.r24.cpolar.top/notify/paySuccess
refund-notify-url: https://21e88914.r24.cpolar.top/notify/refundSuccess
修改支付成功业务层代码OrderServiceImpl.paySuccess,这里代码中所指出的客户端是商家管理端(由于商家可能打开了多个管理端页面,所以向所有与websocket-server建立了连接的管理端都推送了订单通知)
/**
* 支付成功,修改订单状态
*
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 当前登录用户id
Long userId = BaseContext.getCurrentId();
// 根据订单号查询当前用户的订单
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
// 根据订单id更新订单的状态、支付方式、支付状态、结账时间
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
//通过websocket向客户端浏览器推送消息(新增消息推送代码)
Map map=new HashMap<>();
map.put("type",1);
map.put("orderId",ordersDB.getId());
map.put("content","订单号"+outTradeNo);
String json= JSON.toJSONString(map);
//推送到所有用户;
webSocketServer.sendToAllClient(json);
}
3.2 客户催单
- 商家管理端页面与服务端建立长连接;
- 当用户催单之后,调用websocket的相关API实现服务端向客户端推送消息;
- 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报;
- 约定服务端发送给客户端浏览器数据的数据格式为JSON,字段包括:type,orderId,content;
- type 消息类型1来单提醒,2客户催单;
- orderId 订单ID;
- content消息内容;
催单业务代码与来单提醒类似:
/**
* 用户催单
* @param id
*/
@Override
public void reminder(Long id) {
Orders orders=orderMapper.getById(id);
if (orders==null){
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}
Map map=new HashMap<>();
map.put("type",2);
map.put("orderId",id);
map.put("content","订单号"+orders.getNumber());
String json= JSON.toJSONString(map);
log.info("推送消息为:{}",json);
//推送到所有的管理端
webSocketServer.sendToAllClient(json);
}