苍穹外卖--day10(订单状态定时处理、来单提醒和客户催单)

思考:

订单状态相关处理

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表达式在线生成器:

https://cron.qqe2.com/

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));
}
相关推荐
invicinble16 小时前
springboot出现的原因二---作为web的后端服务一站式整合器
前端·spring boot·后端
zhz521416 小时前
Spring Boot + 腾讯 Kona 实现 TLCP 8443 国密 HTTPS 排障实录(奇安信浏览器已通)
spring boot·后端·https·国密·gmssl·kona
ch.ju16 小时前
Java Programming Chapter 4——Construction method
java·开发语言
小龙报16 小时前
【优选算法】双指针专项:1.移动零 2. 复写零 3.快乐数
java·c语言·数据结构·c++·python·算法·面试
AI行业学习16 小时前
CC-Switch Windows + macOS 下载安装配置全流程
java·开发语言·人工智能·python
Niliuershangba16 小时前
ChestnutCMS 栗子内容管理系统:从入门到模板开发实战
java·git·开源·gitlab·github·开源软件·gitcode
2601_9577867716 小时前
多平台矩阵运营的底层逻辑:当账号管理、内容生产与线索转化被一条链路串起来
java·数据库·矩阵·多平台管理
代码中介商16 小时前
排序算法完全指南(六):希尔排序深度详解
java·算法·排序算法
布吉岛的石头16 小时前
Java 程序员第 22 阶段:Function Call 工具调用实战,Java 封装大模型外部能力
java·人工智能·python