1. Spring Task
1.1 介紹
Spring Task 是Spring框架提供的任務(wù)調(diào)度工具,可以按照約定的時(shí)間自動(dòng)執(zhí)行某個(gè)代碼邏輯。
定位:定時(shí)任務(wù)框架
作用:定時(shí)自動(dòng)執(zhí)行某段Java代碼
應(yīng)用場(chǎng)景:
1). 信用卡每月還款提醒
2). 銀行貸款每月還款提醒
3). 火車票售票系統(tǒng)處理未支付訂單
4). 入職紀(jì)念日為用戶發(fā)送通知
強(qiáng)調(diào):只要是需要定時(shí)處理的場(chǎng)景都可以使用Spring Task
1.2 cron表達(dá)式
cron表達(dá)式其實(shí)就是一個(gè)字符串,通過cron表達(dá)式可以定義任務(wù)觸發(fā)的時(shí)間
構(gòu)成規(guī)則:分為6或7個(gè)域,由空格分隔開,每個(gè)域代表一個(gè)含義
每個(gè)域的含義分別為:秒、分鐘、小時(shí)、日、月、周、年(可選)
舉例:
2022年10月12日上午9點(diǎn)整 對(duì)應(yīng)的cron表達(dá)式為:0 0 9 12 10 ? 2022
說明:一般日和周的值不同時(shí)設(shè)置,其中一個(gè)設(shè)置,另一個(gè)用?表示。
比如:描述2月份的最后一天,最后一天具體是幾號(hào)呢?可能是28號(hào),也有可能是29號(hào),所以就不能寫具體數(shù)字。
為了描述這些信息,提供一些特殊的字符。這些具體的細(xì)節(jié),我們就不用自己去手寫,因?yàn)檫@個(gè)cron表達(dá)式,它其實(shí)有在線生成器。
cron表達(dá)式在線生成器
可以直接在這個(gè)網(wǎng)站上面,只要根據(jù)自己的要求去生成corn表達(dá)式即可。所以一般就不用自己去編寫這個(gè)表達(dá)式。
通配符:
* 表示所有值;
? 表示未說明的值,即不關(guān)心它為何值;
- 表示一個(gè)指定的范圍;
, 表示附加一個(gè)可能值;
/ 符號(hào)前表示開始時(shí)間,符號(hào)后表示每次遞增的值;
cron表達(dá)式案例:
*/5 * * * * ? 每隔5秒執(zhí)行一次
0 */1 * * * ? 每隔1分鐘執(zhí)行一次
0 0 5-15 * * ? 每天5-15點(diǎn)整點(diǎn)觸發(fā)
0 0/3 * * * ? 每三分鐘觸發(fā)一次
0 0-5 14 * * ? 在每天下午2點(diǎn)到下午2:05期間的每1分鐘觸發(fā)
0 0/5 14 * * ? 在每天下午2點(diǎn)到下午2:55期間的每5分鐘觸發(fā)
0 0/5 14,18 * * ? 在每天下午2點(diǎn)到2:55期間和下午6點(diǎn)到6:55期間的每5分鐘觸發(fā)
0 0/30 9-17 * * ? 朝九晚五工作時(shí)間內(nèi)每半小時(shí)
0 0 10,14,16 * * ? 每天上午10點(diǎn),下午2點(diǎn),4點(diǎn)
1.3 入門案例
1.3.1 Spring Task使用步驟
1). 導(dǎo)入maven坐標(biāo) spring-context
2). 啟動(dòng)類添加注解 @EnableScheduling 開啟任務(wù)調(diào)度
3). 自定義定時(shí)任務(wù)類
1.3.2 代碼開發(fā)
編寫定時(shí)任務(wù)類:
進(jìn)入sky-server模塊中
package com.sky.task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
* 自定義定時(shí)任務(wù)類
*/
@Component
@Slf4j
public class MyTask {
/**
* 定時(shí)任務(wù) 每隔5秒觸發(fā)一次
*/
@Scheduled(cron = "0/5 * * * * ?")
public void executeTask(){
log.info("定時(shí)任務(wù)開始執(zhí)行:{}",new Date());
}
}
開啟任務(wù)調(diào)度:
啟動(dòng)類添加注解 @EnableScheduling
package com.sky;
@SpringBootApplication
@EnableTransactionManagement //開啟注解方式的事務(wù)管理
@Slf4j
@EnableCaching
@EnableScheduling
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("server started");
}
}
1.3.3 功能測(cè)試
啟動(dòng)服務(wù),查看日志
每隔5秒執(zhí)行一次。
2.訂單狀態(tài)定時(shí)處理
2.1 需求分析
用戶下單后可能存在的情況:
- 下單后未支付,訂單一直處于待支付狀態(tài)
- 用戶收貨后管理端未點(diǎn)擊完成按鈕,訂單一直處于派送中狀態(tài)
支付超時(shí)的訂單如何處理? 派送中的訂單一直不點(diǎn)擊完成如何處理?
對(duì)于上面兩種情況需要通過定時(shí)任務(wù)來修改訂單狀態(tài),具體邏輯為:
- 通過定時(shí)任務(wù)每分鐘檢查一次是否存在支付超時(shí)訂單(下單后超過15分鐘仍未支付則判定為支付超時(shí)訂單),如果存在則修改訂單狀態(tài)為“已取消”
- 通過定時(shí)任務(wù)每天凌晨1點(diǎn)檢查一次是否存在“派送中”的訂單,如果存在則修改訂單狀態(tài)為“已完成”
2.2 代碼開發(fā)
1). 自定義定時(shí)任務(wù)類OrderTask(待完善):
package com.sky.task;
/**
* 自定義定時(shí)任務(wù),實(shí)現(xiàn)訂單狀態(tài)定時(shí)處理
*/
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderMapper orderMapper;
/**
* 處理支付超時(shí)訂單
*/
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder(){
log.info("處理支付超時(shí)訂單:{}", new Date());
}
/**
* 處理“派送中”狀態(tài)的訂單
*/
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder(){
log.info("處理派送中訂單:{}", new Date());
}
}
2). 在OrderMapper接口中擴(kuò)展方法:
/**
* 根據(jù)狀態(tài)和下單時(shí)間查詢訂單
* @param status
* @param orderTime
*/
@Select("select * from orders where status = #{status} and order_time < #{orderTime}")
List<Orders> getByStatusAndOrdertimeLT(Integer status, LocalDateTime orderTime);
3). 完善定時(shí)任務(wù)類的processTimeoutOrder方法:
/**
* 處理支付超時(shí)訂單
*/
@Scheduled(cron = "0 * * * * ?")
public void processTimeoutOrder(){
log.info("處理支付超時(shí)訂單:{}", new Date());
LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
// select * from orders where status = 1 and order_time < 當(dāng)前時(shí)間-15分鐘
List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time);
if(ordersList != null && ordersList.size() > 0){
ordersList.forEach(order -> {
order.setStatus(Orders.CANCELLED);
order.setCancelReason("支付超時(shí),自動(dòng)取消");
order.setCancelTime(LocalDateTime.now());
orderMapper.update(order);
});
}
}
4). 完善定時(shí)任務(wù)類的processDeliveryOrder方法:
/**
* 處理“派送中”狀態(tài)的訂單
*/
@Scheduled(cron = "0 0 1 * * ?")
public void processDeliveryOrder(){
log.info("處理派送中訂單:{}", new Date());
// select * from orders where status = 4 and order_time < 當(dāng)前時(shí)間-1小時(shí)
LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time);
if(ordersList != null && ordersList.size() > 0){
ordersList.forEach(order -> {
order.setStatus(Orders.COMPLETED);
orderMapper.update(order);
});
}
}
2.3 功能測(cè)試
開啟定時(shí)任務(wù)
啟動(dòng)服務(wù),觀察控制臺(tái)日志。處理支付超時(shí)訂單任務(wù)每隔1分鐘執(zhí)行一次。
3. WebSocket
3.1 介紹
WebSocket 是基于 TCP 的一種新的網(wǎng)絡(luò)協(xié)議。它實(shí)現(xiàn)了瀏覽器與服務(wù)器全雙工通信——瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就可以創(chuàng)建持久性的連接, 并進(jìn)行雙向數(shù)據(jù)傳輸。
HTTP協(xié)議和WebSocket協(xié)議對(duì)比:
- HTTP是短連接
- WebSocket是長連接
- HTTP通信是單向的,基于請(qǐng)求響應(yīng)模式
- WebSocket支持雙向通信
- HTTP和WebSocket底層都是TCP連接
思考:既然WebSocket支持雙向通信,功能看似比HTTP強(qiáng)大,那么我們是不是可以基于WebSocket開發(fā)所有的業(yè)務(wù)功能?
WebSocket缺點(diǎn):
服務(wù)器長期維護(hù)長連接需要一定的成本
各個(gè)瀏覽器支持程度不一
WebSocket 是長連接,受網(wǎng)絡(luò)限制比較大,需要處理好重連
結(jié)論:WebSocket并不能完全取代HTTP,它只適合在特定的場(chǎng)景下使用
WebSocket應(yīng)用場(chǎng)景:
1). 視頻彈幕
2). 網(wǎng)頁聊天
3). 體育實(shí)況更新
4). 股票基金報(bào)價(jià)實(shí)時(shí)更新
3.2 入門案例
3.2.1 案例分析
需求:實(shí)現(xiàn)瀏覽器與服務(wù)器全雙工通信。瀏覽器既可以向服務(wù)器發(fā)送消息,服務(wù)器也可主動(dòng)向?yàn)g覽器推送消息。
效果展示:
實(shí)現(xiàn)步驟:
1). 直接使用websocket.html頁面作為WebSocket客戶端
2). 導(dǎo)入WebSocket的maven坐標(biāo)
3). 導(dǎo)入WebSocket服務(wù)端組件WebSocketServer,用于和客戶端通信
4). 導(dǎo)入配置類WebSocketConfiguration,注冊(cè)WebSocket的服務(wù)端組件
5). 導(dǎo)入定時(shí)任務(wù)類WebSocketTask,定時(shí)向客戶端推送數(shù)據(jù)
3.2.2 代碼開發(fā)
1). 定義websocket.html頁面
<!DOCTYPE HTML>
<html>
<head>
<meta charset="UTF-8">
<title>WebSocket Demo</title>
</head>
<body>
<input id="text" type="text" />
<button onclick="send()">發(fā)送消息</button>
<button onclick="closeWebSocket()">關(guān)閉連接</button>
<div id="message">
</div>
</body>
<script type="text/javascript">
var websocket = null;
var clientId = Math.random().toString(36).substr(2);
//判斷當(dāng)前瀏覽器是否支持WebSocket
if('WebSocket' in window){
//連接WebSocket節(jié)點(diǎn)
websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
}
else{
alert('Not support websocket')
}
//連接發(fā)生錯(cuò)誤的回調(diào)方法
websocket.onerror = function(){
setMessageInnerHTML("error");
};
//連接成功建立的回調(diào)方法
websocket.onopen = function(){
setMessageInnerHTML("連接成功");
}
//接收到消息的回調(diào)方法
websocket.onmessage = function(event){
setMessageInnerHTML(event.data);
}
//連接關(guān)閉的回調(diào)方法
websocket.onclose = function(){
setMessageInnerHTML("close");
}
//監(jiān)聽窗口關(guān)閉事件,當(dāng)窗口關(guān)閉時(shí),主動(dòng)去關(guān)閉websocket連接,防止連接還沒斷開就關(guān)閉窗口,server端會(huì)拋異常。
window.onbeforeunload = function(){
websocket.close();
}
//將消息顯示在網(wǎng)頁上
function setMessageInnerHTML(innerHTML){
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//發(fā)送消息
function send(){
var message = document.getElementById('text').value;
websocket.send(message);
}
//關(guān)閉連接
function closeWebSocket() {
websocket.close();
}
</script>
</html>
2). 導(dǎo)入maven坐標(biāo)
在sky-server模塊pom.xml中已定義
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
3). 定義WebSocket服務(wù)端組件
直接導(dǎo)入到sky-server模塊即可
package com.sky.websocket;
/**
* WebSocket服務(wù)
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
//存放會(huì)話對(duì)象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 連接建立成功調(diào)用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客戶端:" + sid + "建立連接");
sessionMap.put(sid, session);
}
/**
* 收到客戶端消息后調(diào)用的方法
*
* @param message 客戶端發(fā)送過來的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到來自客戶端:" + sid + "的信息:" + message);
}
/**
* 連接關(guān)閉調(diào)用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("連接斷開:" + sid);
sessionMap.remove(sid);
}
/**
* 群發(fā)
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服務(wù)器向客戶端發(fā)送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
4). 定義配置類,注冊(cè)WebSocket的服務(wù)端組件(從資料中直接導(dǎo)入即可)
package com.sky.config;
/**
* WebSocket配置類,用于注冊(cè)WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
5). 定義定時(shí)任務(wù)類,定時(shí)向客戶端推送數(shù)據(jù)(從資料中直接導(dǎo)入即可)
package com.sky.task;
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
/**
* 通過WebSocket每隔5秒向客戶端發(fā)送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("這是來自服務(wù)端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}
3.2.3 功能測(cè)試
啟動(dòng)服務(wù),打開websocket.html頁面
瀏覽器向服務(wù)器發(fā)送數(shù)據(jù):
服務(wù)器向?yàn)g覽器間隔5秒推送數(shù)據(jù):
4. 來單提醒
4.1 需求分析和設(shè)計(jì)
用戶下單并且支付成功后,需要第一時(shí)間通知外賣商家。通知的形式有如下兩種:
- 語音播報(bào)
- 彈出提示框
設(shè)計(jì)思路:
- 通過WebSocket實(shí)現(xiàn)管理端頁面和服務(wù)端保持長連接狀態(tài)
- 當(dāng)客戶支付后,調(diào)用WebSocket的相關(guān)API實(shí)現(xiàn)服務(wù)端向客戶端推送消息
- 客戶端瀏覽器解析服務(wù)端推送的消息,判斷是來單提醒還是客戶催單,進(jìn)行相應(yīng)的消息提示和語音播報(bào)
- 約定服務(wù)端發(fā)送給客戶端瀏覽器的數(shù)據(jù)格式為JSON,字段包括:type,orderId,content
- type 為消息類型,1為來單提醒 2為客戶催單
- orderId 為訂單id
- content 為消息內(nèi)容
4.2 代碼開發(fā)
在OrderServiceImpl中注入WebSocketServer對(duì)象,修改paySuccess方法,加入如下代碼:
@Autowired
private WebSocketServer webSocketServer;
/**
* 支付成功,修改訂單狀態(tài)
*
* @param outTradeNo
*/
public void paySuccess(String outTradeNo) {
// 當(dāng)前登錄用戶id
Long userId = BaseContext.getCurrentId();
// 根據(jù)訂單號(hào)查詢當(dāng)前用戶的訂單
Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);
// 根據(jù)訂單id更新訂單的狀態(tài)、支付方式、支付狀態(tài)、結(jié)賬時(shí)間
Orders orders = Orders.builder()
.id(ordersDB.getId())
.status(Orders.TO_BE_CONFIRMED)
.payStatus(Orders.PAID)
.checkoutTime(LocalDateTime.now())
.build();
orderMapper.update(orders);
//
Map map = new HashMap();
map.put("type", 1);//消息類型,1表示來單提醒
map.put("orderId", orders.getId());
map.put("content", "訂單號(hào):" + outTradeNo);
//通過WebSocket實(shí)現(xiàn)來單提醒,向客戶端瀏覽器推送消息
webSocketServer.sendToAllClient(JSON.toJSONString(map));
///
}
4.3 功能測(cè)試
登錄管理端后臺(tái)
登錄成功后,瀏覽器與服務(wù)器建立長連接
5. 客戶催單
5.1 需求分析和設(shè)計(jì)
用戶在小程序中點(diǎn)擊催單按鈕后,需要第一時(shí)間通知外賣商家。通知的形式有如下兩種:
- 語音播報(bào)
- 彈出提示框
設(shè)計(jì)思路:
- 通過WebSocket實(shí)現(xiàn)管理端頁面和服務(wù)端保持長連接狀態(tài);
- 當(dāng)用戶點(diǎn)擊催單按鈕后,調(diào)用WebSocket的相關(guān)API實(shí)現(xiàn)服務(wù)端向客戶端推送消息;
- 客戶端瀏覽器解析服務(wù)端推送的消息,判斷是來單提醒還是客戶催單,進(jìn)行相應(yīng)的消息提示和語音播報(bào);
約定服務(wù)端發(fā)送給客戶端瀏覽器的數(shù)據(jù)格式為JSON,字段包括:type,orderId,content- type 為消息類型,1為來單提醒 2為客戶催單;
- orderId 為訂單id;
- content 為消息內(nèi)容;
當(dāng)用戶點(diǎn)擊催單按鈕時(shí),向服務(wù)端發(fā)送請(qǐng)求。
接口設(shè)計(jì)(催單):
5.2 代碼開發(fā)
5.2.1 Controller層
根據(jù)用戶催單的接口定義,在user/OrderController中創(chuàng)建催單方法:
/**
* 用戶催單
*
* @param id
* @return
*/
@GetMapping("/reminder/{id}")
@ApiOperation("用戶催單")
public Result reminder(@PathVariable("id") Long id) {
orderService.reminder(id);
return Result.success();
}
5.2.2 Service層接口
在OrderService接口中聲明reminder方法:
/**
* 用戶催單
* @param id
*/
void reminder(Long id);
5.2.3 Service層實(shí)現(xiàn)類
在OrderServiceImpl中實(shí)現(xiàn)reminder方法:
/**
* 用戶催單
*
* @param id
*/
public void reminder(Long id) {
// 查詢訂單是否存在
Orders orders = orderMapper.getById(id);
if (orders == null) {
throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
}
//基于WebSocket實(shí)現(xiàn)催單
Map map = new HashMap();
map.put("type", 2);//2代表用戶催單
map.put("orderId", id);
map.put("content", "訂單號(hào):" + orders.getNumber());
webSocketServer.sendToAllClient(JSON.toJSONString(map));
}
5.2.4 Mapper層
在OrderMapper中添加getById:文章來源:http://www.zghlxwxcb.cn/news/detail-832200.html
/**
* 根據(jù)id查詢訂單
* @param id
*/
@Select("select * from orders where id=#{id}")
Orders getById(Long id);
后記
????????美好的一天,到此結(jié)束,下次繼續(xù)努力!欲知后續(xù),請(qǐng)看下回分解,寫作不易,感謝大家的支持?。???????文章來源地址http://www.zghlxwxcb.cn/news/detail-832200.html
到了這里,關(guān)于基于SpringBoot+WebSocket+Spring Task的前后端分離外賣項(xiàng)目-訂單管理(十七)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!