Day10-苍穹外卖(Spring-task/WebSocket篇)

前言:

今天写Day10的内容,主要介绍下spring-task,WebSocket以及他们的应用案例。另外,在处理超时订单那,还会尝试用redis进行一个优化,减轻对数据库的查询压力

今日所学:

  • spring-task
  • 订单超时处理(优化)
  • WebSocket
  • 用户下单和催单提醒

ps:用户下单功能实现要用到微信支付功能,而微信支付未解决的可以看下我写的这篇博客(纯后端解决):苍穹外卖-微信支付功能解决-CSDN博客

目录

1. spring-task

Cron表达式

spring-task使用流程

2.订单超时处理(优化)

问题目标:

运行效果:

技术实现思路:

代码实现:

3.WebSocket

定义

与http的对比

应用场景:

4.WebSocket应用-用户催单/下单

接口功能设计:

代码实现:


1. spring-task

Spring Task是Spring框架提供的任务调度工具

定位:定时任务框架

作用:定时的自动执行某段Java代码

从上面三句话我们知道spring-task是定时工具。那么如何确定具体的定时呢?

这里我们就需要用到Cron表达式

Cron表达式

Cron表达式是一个字符串,通过cron表达式可以定义任务触发的时间

构成规则:分为6或者7个域,每个域代表着一个含义

每个域的含义分别为:秒,分钟,小时,日,月,周,年(可选)

当然这个表达式我们不需要自己写,登录以下网站:

Cron - 在线Cron表达式生成器

需要怎么设置定时,自己勾选即可,网站会自动生成相应的Cron表达式

spring-task使用流程

  1. 导入maven坐标(spring-context已存在)

spring-task没有单独的依赖,是包含在context里面的

2.启动类添加注解@EnableScheduling

3.自定义定时任务类

要作为一个bean交给IOC容器管理,@Component不要忘记添加

方法上加@Scheduled注解, 注解中写Cron表达式

2.订单超时处理(优化)

问题目标:

用户下单后可能存在的情况:

1.下单后未支付,订单一直处于“待支付”状态

2.用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态

对于上面两种情况需要通过定时任务来修改订单状态,具体要求为:

  • 通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”
  • 通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则改为完成

优化:

原本的代码是不断查询数据库,是否存在超时状态的订单。

我这边给进行了一个优化,使用了redis储存订单信息,只有当真正存在超时订单时,才会去修改数据库相应信息,以此减轻了对数据库的压力

这里我们看下运行效果

运行效果:

如图,只有当出现超时订单时,才会去查询数据库并修改相应的数据,否则每分钟的定时查询只会停留在redis层,这样大大减轻了数据库的压力

技术实现思路:

1.定义两种redis的数据结构:

  • Hash结构,用来储存订单状态,判断是否支付(具体结构为 "key" :{orderId :status}
  • Zset结构(有序集合) 用来储存自订单下单后15分钟的时间点(具体结构为"key": orderId-score(这边score表示下单后15分钟的时间点))

2. 在用户下单是创建这两种数据结构并相应的数据(orderId, status, Time)添加进去

3.如果支付完成(在这表现为调用payment方法),我们将Hash结构的status更新,并删除Zset结构中相应的订单,否则不变

4.如果一直没有支付到下单后15分钟,则查找相应的订单,根据订单号删除Hash结构和Zset结构中相应的数据

5.最后将订单号传给Task层,如果订单号不为空,也就是确实存在相应超时的订单,才会调用mapper层修改数据库相应的订单状态

代码实现:

1.先准备好相应的redis序列化结构

这里我在redis中定义了两个bean,第一个是用于处理JSON数据的,第二个是专门为我们这个接口设计的,用来处理long结构数据(一定要加,不然会有Integer和long数据不兼容的问题,这个问题我改了很久)

@Configuration
@Slf4j
public class RedisConfiguration {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        // 设置key的序列化方式
        StringRedisSerializer keySerializer = new StringRedisSerializer();
        template.setKeySerializer(keySerializer);

        // 设置value的序列化方式,这里以JSON为例
        Jackson2JsonRedisSerializer<Object> valueSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        template.setValueSerializer(valueSerializer);

        // 设置Hash的key和value的序列化方式
        template.setHashKeySerializer(keySerializer);
        template.setHashValueSerializer(valueSerializer);

        template.afterPropertiesSet();
        return template;
    }

// 下面这一个是为我们这个接口设计的,你原先的配置类不用动

        @Bean
        public RedisTemplate<String, Long> longRedisTemplate(RedisConnectionFactory factory) {
            RedisTemplate<String, Long> template = new RedisTemplate<>();
            template.setConnectionFactory(factory);

            // 使用StringRedisSerializer序列化key
            template.setKeySerializer(new StringRedisSerializer());

            // 使用GenericToStringSerializer序列化value为Long
            template.setValueSerializer(new GenericToStringSerializer<>(Long.class));

            // 对于Hash结构也做同样配置
            template.setHashKeySerializer(new GenericToStringSerializer<>(Long.class));
            template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));

            template.afterPropertiesSet();
            return template;
        }
    }

2. 给bean注入到相应的service层

这边在提醒下第二个bean写好后一定要记得注入,再下面定义的两个常量是用来做key值的

3. 在用户下单接口创建Hash结构和Zset结构,并添加相应的订单数据

下面是monitorPaymentTimeout方法的具体方法体

可以看到定义了Zset和Hash两种结构,并添加了相应数据

这里为了方便测试,我设定的是订单创建后超过30秒未支付就为超时订单

/**
 * 给订单设置时间
 * 监控超时订单
 * @param orderId
 */
public void monitorPaymentTimeout(Long orderId){
    // 计算下单后15分钟后面的时间戳(score)
    double expireTime = System.currentTimeMillis() + 0.5 * 60 * 1000;
    longRedisTemplate.opsForZSet().add(ORDER_PAYMENT_TIMEOUT_ZSET, (Long)orderId, expireTime);
    longRedisTemplate.opsForHash().put(ORDER_STATUS,orderId,Orders.PENDING_PAYMENT);
}

4.如果用户支付了(这里表现为执行payment方法),更新redis相应的数据

这里我贴心的给代码粘贴放在这儿了。

具体代码逻辑就是在Hash结构中把status设置为已支付,并且删除Zset结构相应的订单信息

       // 2. 更新 Redis 状态
        longRedisTemplate.opsForHash().put(ORDER_STATUS, orders.getId(), Orders.PAID);
        // 3. 从 ZSET 移除(避免被超时任务处理)
        longRedisTemplate.opsForZSet().remove(ORDER_PAYMENT_TIMEOUT_ZSET, orders.getId());

5,订单超时为支付,处理超时订单

具体代码逻辑:

1.计算当前时间

2.获得时间上小于当前时间的订单(订单添加时添加的都是创建后15分钟的时间)

3.遍历获得的每个订单,查看获取到的订单的状态(status)

4.如果状态是未支付,则删除Zset结构和Hash结构相应的订单,并将相应的订单号加到result中(表示需要更新数据库的订单号)

5.返回result

/**
 * 处理超时订单
 * @return
 */
@Override
public List<Long> checkExpiredOrders(){
    // 存放需要更改数据库状态的订单号
    List<Long> result = new ArrayList<>();
    //计算当前时间
    long now = System.currentTimeMillis();

    // 获取所有下单时间超过15分钟的订单
    Set<Long> expiredOrders = longRedisTemplate.opsForZSet().rangeByScore(ORDER_PAYMENT_TIMEOUT_ZSET, 0, now);

    if (expiredOrders != null && !expiredOrders.isEmpty()) {
        expiredOrders.forEach(orderId -> {
            Integer status = (Integer) longRedisTemplate.opsForHash().get(ORDER_STATUS, orderId);
            if(status == Orders.PENDING_PAYMENT){
                // Zset移除已移除的订单
                longRedisTemplate.opsForHash().delete(ORDER_STATUS, orderId);
                longRedisTemplate.opsForZSet().remove(ORDER_PAYMENT_TIMEOUT_ZSET, expiredOrders);
                result.add( orderId);
            }
        }
        );
    }
    return result;
}

6.最后来到OrderTask层,如果checkExpiredOrders()方法传来的result不为空,也就是存在需要更改数据库的订单,我们再调用mapper执行相应的数据库逻辑

@Autowired
private OrderService orderService;
/**
 * 处理超时订单
 */
@Scheduled(cron = "1/5 * * * * *")
public void processTimeoutOrder() {
    log.info("定时处理超时订单:{}", LocalDateTime.now());
    
     List<Long> orderIdLists = orderService.checkExpiredOrders();
// 出现超时订单
     if(!orderIdLists.isEmpty() && orderIdLists != null){
        for(Long orderId : orderIdLists){
            Orders order = orderMapper.getByIdL(orderId);
            order.setStatus(Orders.CANCELLED);
            order.setCancelReason("订单超时");
            order.setCancelTime(LocalDateTime.now());
            orderMapper.update(order);
        }

     }

}

3.WebSocket

定义

WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信-----浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输

与http的对比

Http协议和WebSocket协议的对比:

不同点:

http是短连接,即每次要进行交互都要重新发送请求,建立连接

WEbSocket是长链接,即链接后可以进行多次交互

http通信是单向的,基于请求响应模式

webSocket支持双向通信

相同点:

底层都是TCP连接的

一句话总结,虽然http和websocket底层都是TCP连接的,但是http是单向的,每次需要交互时只能由客户端重新发起请求给客户端(这种也叫短连接)。而websocket是双向的,只要建立起连接,客户端可以请求服务端,服务端也可以请求客户端(这种也叫长连接)。

应用场景:

  • 视频弹幕
  • 网页聊天
  • 体育实况更新

还有股市等众多应用场景,这些都有一个共同的特点,即:页面没有刷新却能自动更新, 服务端主动向客户端发送消息

4.WebSocket应用-用户催单/下单

接口功能设计:

通过WebSocket实现管理端和服务端保持长连接

用户点击催单后,调用WebSccket的相关API实现服务端向客户端推送信息

客户端解服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语言播报。

约定服务端发送客户端浏览器的数据格式为JSON,字段包括: type,orderId, content

type 为消息类型,1为来单提醒 2为客户催单

- orderId 为订单id

- content 为消息内容

代码实现:

1.WebSocket环境准备

pom文件中导入相应的依赖项(项目已经导入好了)

在config层配置WebSocketConfiguration

/**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

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

}

创建一个websocket软件包,配置好WebSocketServer类。

这里代码太多,我就不粘贴了,黑马资料中有

2.实现用户下单提醒功能

具体代码逻辑:

1.先是收到小程序的支付成功通知,将相应的订单号添加到一个map键值对中。

2.将其转换成json格式的数据

3. 有websocketserver调用sendtoallclient方法将数据由服务端发送到商家端

4.最后商家端接收到相应的数据发出下单提醒并进行语言播报

这里要注意的是,如果不想语言播报一直响,记得给下面代码注释掉

3.实现催单功能

具体逻辑是一样的,这里我就不多赘述了

user端的OrderController下写好相应的Controller层

/**
 * 催单
 * @param id
 * @return
 */
@GetMapping("/reminder/{id}")
@ApiOperation("催单")
public Result reminder(@PathVariable Long id) {
    orderService.reminder(id);
    return Result.success();
}

然后是service层

/**
 * 催单
 * @param id
 */
@Override
public void reminder(Long id) {
    Orders orders = orderMapper.getByIdL(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);
    webSocketServer.sendToAllClient(json);
}

最后:

今天的分享就到这里。如果我的内容对你有帮助,请点赞评论收藏。创作不易,大家的支持就是我坚持下去的动力!(๑`・ᴗ・´๑)

<think>嗯,用户想了解苍穹外卖项目中第10天关于WebSocket的实现教程或细节。我需要先回顾提供的三个引用内容,看看里面有没有相关的信息。 引用[1]提到课程内容包括Spring Task、订单状态定时处理、WebSocket的来单提醒和客户催单。这说明第10天的课程确实涉及WebSocket的应用。引用[2]和[3]都提到了在OrderServiceImpl中注入WebSocketServer对象,并在支付成功后发送提醒。看来用户需要的是具体的实现步骤,比如如何配置WebSocket、服务端和客户端的代码,以及业务逻辑的处理。 用户的问题可能包括如何建立WebSocket连接,服务端如何接收和处理消息,以及如何在前端展示提醒。需要分步骤解释,可能需要包括代码示例,如配置类、服务端端点、消息发送的时机等。同时,要注意引用中的代码片段,比如在paySuccess方法中加入发送消息的代码,这部分需要详细说明。 另外,用户可能还想知道WebSocket在订单提醒和客户催单中的具体应用场景,以及可能的优化方法。比如,如何确保消息的可靠传递,或者如何处理大量并发连接。不过根据引用内容,可能主要聚焦在基础实现上,所以先回答核心实现步骤,再生成相关问题。</think>### 苍穹外卖项目Day10 WebSocket实现详解 根据引用内容,苍穹外卖项目第10天通过WebSocket实现了**来单提醒**和**客户催单**功能,以下是具体实现步骤与逻辑: --- ### 一、WebSocket核心实现步骤 #### 1. **WebSocket服务端配置** ```java @ServerEndpoint("/websocket/{sid}") @Component public class WebSocketServer { // 存储会话对象和商户ID的映射 private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>(); @OnOpen public void onOpen(Session session, @PathParam("sid") String sid) { sessionMap.put(sid, session); // 商户连接时绑定会话 } @OnMessage public void onMessage(String message) { // 处理客户端发送的催单消息 } @OnClose public void onClose(@PathParam("sid") String sid) { sessionMap.remove(sid); // 断开连接时移除会话 } } ``` [^3] #### 2. **订单支付成功后触发提醒** 在支付成功逻辑中注入WebSocket服务,向指定商户推送消息: ```java // OrderServiceImpl.java @Autowired private WebSocketServer webSocketServer; public void paySuccess(String orderId) { // 更新订单状态... // 通过WebSocket推送新订单提醒 webSocketServer.sendToAllClient("新订单:" + orderId + " 已支付"); } ``` [^2] #### 3. **前端接收消息(示例)** ```javascript let socket = new WebSocket('ws://localhost:8080/websocket/商户ID'); socket.onmessage = function(event) { alert('系统提醒:' + event.data); // 弹窗显示催单或新订单 }; ``` --- ### 二、关键业务场景实现 #### 1. **来单提醒流程 $$流程图:商户端WebSocket连接 \rightarrow 支付成功触发推送 \rightarrow 前端弹窗/声音提醒$$ [^1] #### 2. **客户催单处理** 客户发起催单时,系统通过WebSocket实时通知对应商户: ```java // 在催单服务方法中 webSocketServer.sendToClient(商户ID, "订单" + orderId + "客户催单"); ``` --- ### 三、技术要点总结 | 技术点 | 实现方式 | 引用来源 | |--------------|-----------------------------------|------------| | 连接绑定 | 商户ID与会话映射存储 | | | 消息广播 | `sendToAllClient()`方法遍历会话 | | | 状态同步 | 支付成功后立即触发WebSocket推送 | | ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值