苍穹外卖 —— Spring Task和WebSocket的运用以及订单统一处理、订单的提醒和催单功能的实现

一、前言

这一节将介绍一个框架和一个新的协议,用于订单提醒和催单功能。

二、Spring Task

这是一个小框架,主要是用于定时功能,可以定时执行某些操作,比如这里我们创建一个测试类,我们希望每五秒在日志中打印当前的时间:

java 复制代码
/**
 * 自定义定时任务类
 */
@Component
@Slf4j
public class MyTask {

    /**
     * 定时任务 每隔5秒就触发一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void executeTask(){
        log.info("定时任务开始执行:{}",new Date());
    }
}

这里用到了**@Scheduled**注解,这个注解是用来填写cron表达式的,而cron表达式时用于计时的,我们不需要去背它的格式,直接使用网上的cron工具即可自动生成我们想要的表达式,这里的表达式意义就是每五秒触发一次。

这里需要添加**@Component**注解,将这个类实例化注入到容器,后续在启动类中才能识别这个bean,从而才能开启计时。

不要忘记在启动类中添加**@EnableScheduling**,添加了这个注解,才会开启任务调度。

java 复制代码
@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有很多方法,每个都有自己的功能:

html 复制代码
<!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);

    //判断当前浏览器是否支持WebSocket
    if('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>

页面如下:

同时后端服务器也具有这些相同的功能方法,需要通过注解来标明功能。

java 复制代码
/**
 * 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();
            }
        }
    }

}

我们希望客户端每五秒接收一次服务端的时间消息:

java 复制代码
@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
    }
}

这里需要一个配置类用于注册WebSocket的bean:

java 复制代码
/**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }

}

启动类:

java 复制代码
@SpringBootApplication
@EnableCaching//开启缓存注解
@EnableScheduling//开启任务调度
public class TestJ58WebSocketApplication {

    public static void main(String[] args) {
        SpringApplication.run(TestJ58WebSocketApplication.class, args);
    }

}

启动后效果如下:

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

服务器接收到消息:

四、订单状态统一处理

通过定时任务类来实现对超时订单和长时间未送达的订单状态进行处理,派送中的订单将在每天凌晨一点统一处理(将状态全部设置为完成),而支付超过15分钟,将自动取消订单。

java 复制代码
/**
 * 定时任务类
 */
@Component
@Slf4j
public class OrderTask {


    @Autowired
    private 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数据会自动转化,并且弹出提示。

java 复制代码
//通过webSocket向客户端浏览器推送消息 type orderId content
        Map 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

java 复制代码
 /**
     * 用户催单
     * @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层

接口:

java 复制代码
 /**
     * 用户催单
     * @param id
     */
    void reminder(Long id);

实现类:

这里还可以对订单判空,但是实际来讲,如果是空订单,前端是不会显示催单按钮的。

java 复制代码
    /**
     * 用户催单
     * @param id
     */
    @Override
    public void reminder(Long id) {

        //根据id查询订单
        Orders orders = orderMapper.getById(id);

        //通过webSocket向客户端浏览器推送消息 type orderId content
        Map 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);
    }
相关推荐
爱丽_29 分钟前
深入理解 Java Socket 编程与线程池:从阻塞 I/O 到高并发处理
java·开发语言
济南壹软网络科技有限公司32 分钟前
云脉IM的高性能消息路由与离线推送机制摘要:消息的“零丢失、低延迟”之道
java·即时通讯源码·开源im·企业im
Seven9738 分钟前
剑指offer-46、孩⼦们的游戏(圆圈中最后剩下的数)
java
serendipity_hky1 小时前
互联网大厂Java面试故事:核心技术栈与场景化业务问题实战解析
java·spring boot·redis·elasticsearch·微服务·消息队列·内容社区
我真不会起名字啊1 小时前
C、C++中的sprintf和stringstream的使用
java·c语言·c++
qq_328067811 小时前
springboot4 启动 Unable to find JSON tool
spring boot·json
十点摆码1 小时前
Spring Boot2 使用 Flyway 管理数据库版本
java·flyway·数据库脚本·springboo2·数据库脚本自动管理
望道同学1 小时前
PMP/信息系统项目管理师 9 张 思维导图【考试必备】
前端·后端·程序员
毕设源码-钟学长1 小时前
【开题答辩全过程】以 基于Javaweb的电动汽车充电桩管理系统为例,包含答辩的问题和答案
java·spring boot
码事漫谈1 小时前
C++11到C++23语法糖万字详解
后端