苍穹外卖 —— Spring Task和WebSocket的运用以及订单统一处理、订单的提醒和催单功能的实现
一、前言
这一节将介绍一个框架和一个新的协议,用于订单提醒和催单功能。
二、Spring Task
这是一个小框架,主要是用于定时功能,可以定时执行某些操作,比如这里我们创建一个测试类,我们希望每五秒在日志中打印当前的时间:
/*** 自定义定时任务类*/
@Component
@Slf4j
public class MyTask {/*** 定时任务 每隔5秒就触发一次*/@Scheduled(cron = "0/5 * * * * ?")public void executeTask(){log.info("定时任务开始执行:{}",new Date());}
}这里用到了@Scheduled注解,这个注解是用来填写cron表达式的,而cron表达式时用于计时的,我们不需要去背它的格式,直接使用网上的cron工具即可自动生成我们想要的表达式,这里的表达式意义就是每五秒触发一次。
这里需要添加@Component注解,将这个类实例化注入到容器,后续在启动类中才能识别这个bean,从而才能开启计时。
不要忘记在启动类中添加@EnableScheduling,添加了这个注解,才会开启任务调度。
@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@EnableCaching//开启缓存注解
@Slf4j
@EnableScheduling//开启任务调度
public class SkyApplication {public static void main(String[] args) {SpringApplication.run(SkyApplication.class, args);log.info("server started");}
}最终的效果就是:

三、WebSocket
1.介绍
这是一个新的协议,以往我们使用http协议时会有一个无法解决的问题,就是服务端无法主动向客户端发请求,只能被动接收客户端请求然后返回响应,而且这个流程不是实时的,我们必须刷新页面才能更新数据,这个就会导致一个问题,假如用户催单,那商家还得刷新页面才能看到别人在催单,这显然是不合理的,所以在这个功能上我们必须找到一个协议来代替http,所以我们使用到了WebSocket。
WebSocket是基于TCP的一个网络协议,其实我认为他的功能和tcp非常相似,当时学tcp的时候我做了一个聊天软件,用的就是tcp/ip协议,因为聊天也是需要实时的,是需要两端互通的,当时聊天的原理是先将客户端和服务器建立连接,然后就可以互相发消息,当然,当时是通过向Socket传输入输出流和多线程来解决的传输数据的问题,其实这里的webSocket很类似,不同点是它建立连接不是通过传入ip,而是通过客户端用http发出请求,然后获取服务器的响应来建立的连接,一旦建立了连接,就可以通过WebSocket来进行双向实时信息传输了。
2.快速入门
我们先给用户端建立一个前端页面,通过JS来将websocket处理的事件和按钮文本框联系,这里可以看到websocket有很多方法,每个都有自己的功能:
<!DOCTYPE HTML>
<html>
<head><meta charset="UTF-8"><title>WebSocket Demo</title>
</head>
<body><input id="text" type="text" /><button onclick="send()">发送消息</button><button onclick="closeWebSocket()">关闭连接</button><div id="message"></div>
</body>
<script type="text/javascript">var websocket = null;var clientId = Math.random().toString(36).substr(2);//判断当前浏览器是否支持WebSocketif('WebSocket' in window){//连接WebSocket节点websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);}else{alert('Not support websocket')}//连接发生错误的回调方法websocket.onerror = function(){setMessageInnerHTML("error");};//连接成功建立的回调方法websocket.onopen = function(){setMessageInnerHTML("连接成功");}//接收到消息的回调方法websocket.onmessage = function(event){setMessageInnerHTML(event.data);}//连接关闭的回调方法websocket.onclose = function(){setMessageInnerHTML("close");}//监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。window.onbeforeunload = function(){websocket.close();}//将消息显示在网页上function setMessageInnerHTML(innerHTML){document.getElementById('message').innerHTML += innerHTML + '<br/>';}//发送消息function send(){var message = document.getElementById('text').value;websocket.send(message);}//关闭连接function closeWebSocket() {websocket.close();}
</script>
</html>
页面如下:

同时后端服务器也具有这些相同的功能方法,需要通过注解来标明功能。
/*** 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);}/*** 收到客户端消息后调用的方法** @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();}}}}
我们希望客户端每五秒接收一次服务端的时间消息:
@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()));}
}这里需要一个配置类用于注册WebSocket的bean:
/*** WebSocket配置类,用于注册WebSocket的Bean*/
@Configuration
public class WebSocketConfiguration {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}}
启动类:
@SpringBootApplication
@EnableCaching//开启缓存注解
@EnableScheduling//开启任务调度
public class TestJ58WebSocketApplication {public static void main(String[] args) {SpringApplication.run(TestJ58WebSocketApplication.class, args);}}
启动后效果如下:

我们尝试从客户端发消息到服务器:

服务器接收到消息:

四、订单状态统一处理
通过定时任务类来实现对超时订单和长时间未送达的订单状态进行处理,派送中的订单将在每天凌晨一点统一处理(将状态全部设置为完成),而支付超过15分钟,将自动取消订单。
/*** 定时任务类*/
@Component
@Slf4j
public class OrderTask {@Autowiredprivate OrderMapper orderMapper;/*** 处理超时订单*/@Scheduled(cron = "0 * * * * ?")public void processTimeOutOrder() {log.info("定时处理超时订单,{}", LocalDateTime.now());LocalDateTime time = LocalDateTime.now().plusMinutes(-15);// select * from orders where status = ? and order_time < (当前时间 - 15分钟)List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);if(ordersList != null &&!ordersList.isEmpty()){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.isEmpty()){for (Orders orders : ordersList) {orders.setStatus(Orders.COMPLETED);orderMapper.update(orders);}}}
}
五、来单提醒
这里需要注意一点:无论是User端还是Admin端,全部都是客户端!只有SpringBoot才是服务器。
对于催单和来单提醒,都是在admin端中提示,所以user端相当于只是起到了一个启动事件的作用。有所不同的是,催单是通过按钮来启动事件的,而这个来单提醒,是通过返回支付成功时启动的事件。
这个就是WebSocet的典型应用了,服务器需要通过这个协议才能将来单提醒实时传给客户端(admin端),处理时机是在支付成功后,由于我们跳过了支付成功,所以以下代码都是写在payment方法中的,如果不跳过应该写在paySuccess中。
这里用来一个map来封装各种数据键值对,最终通过JSON格式统一传给所有客户端(admin和user),admin端接收到这个json数据会自动转化,并且弹出提示。
//通过webSocket向客户端浏览器推送消息 type orderId contentMap map = new HashMap();map.put("type",1);//1表示来单提醒map.put("orderId",orders.getId());map.put("content","订单号:"+ orders.getNumber());String json = JSONObject.toJSONString(map);webSocketServer.sendToAllClient(json);六、用户催单
催单是通过按钮启动的事件,所以是有接口的,那么就按照老步骤来:
1.文档

2.Controller
/*** 用户催单* @param id* @return*/@GetMapping("/reminder/{id}")@ApiOperation("用户催单")public Result reminder(@PathVariable("id") Long id){log.info("用户催单:{}",id);orderService.reminder(id);return Result.success();}3.Service层
接口:
/*** 用户催单* @param id*/void reminder(Long id);实现类:
这里还可以对订单判空,但是实际来讲,如果是空订单,前端是不会显示催单按钮的。
/*** 用户催单* @param id*/@Overridepublic void reminder(Long id) {//根据id查询订单Orders orders = orderMapper.getById(id);//通过webSocket向客户端浏览器推送消息 type orderId contentMap map = new HashMap();map.put("type",2);//2表示客户催单map.put("orderId",orders.getId());map.put("content","订单号:"+ orders.getNumber());String json = JSONObject.toJSONString(map);webSocketServer.sendToAllClient(json);}