思考订单状态相关处理1.超时订单用户下单后跳转到确认支付页面进行付款问题用户下单后要在15分钟内支付如果不支付判定为订单超时需要取消操作程序自动完成不需要人工操作2.派送中的订单一直不点击完成自动更新为已完成另外两个功能1.来单提醒用户支付完成商家那端要进行语音播报提醒来单并弹出来单提示框。2.客户催单用户支付完成商家不接单用户端点击催单商家那端进行语音播报提示弹出催单提示框1.Spring Task1.介绍是Spring框架提供的任务调度工具可以按照约定的时间自动执行某个代码逻辑以前开发的程序是基于请求响应的即他是一个定时任务框架作用定时自动执行某段java代码应用场景信用卡每月还款提醒银行贷款每月还款提醒火车票售票系统处理未支付订单入职纪念日为用户发送通知只要是需要定时处理的场景都可以使用Spring Task2.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容器管理】内容包含具体业务逻辑定时任务什么时候触发定义方法方法不要返回值方法名任意定义任务逻辑定时发短信、发邮件、做统计工作、处理订单状态...Scheduledcorn表达式该注解为方法执行触发点在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); ListOrders 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); ListOrders 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连接1.http:请求响应模式2.应用场景也使用到了定时任务•视频弹幕•网页聊天最典型的应用场景即通过服务器把消息主动推送到网页上•体育实况更新•股票基金报价实时更新入门案例重点案例主要了解执行流程代码不为重点实现步骤①直接使用websocket.html页面作为WebSocket客户端②导入WebSocket的maven坐标dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency③最重要导入WebSocket服务端组件WebSocketServer用于和客户端通信【类似于springMVC中的Controller用于响应客户端发出的http请求】这里的客户端是基于WebSocket的方式注意31.该组件需要加入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作为key3.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 MapString, 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) { CollectionSession 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字段包括typeorderIdcontent- 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而不是外部显示的订单号功能测试nginxwebsocket请求逻辑1.websocket请求不到的nginx端口自己修改过的要修改前端的端口。位置在nginx的安装目录/html/sky/js/app.js文件中ctrlF查找 ws: localhost/ws/ 然后修改成 ws: localhost:(自己设置的nginx端口号)/ws/然后重启生效重启后清理一下浏览器缓存或者重启电脑就不会显示临时标题了2.本质上第一次登录请求浏览器控制台状态码中显示该请求状态码为101idea显示某用户建立了连接实际上就是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字段包括typeorderIdcontent- 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)); }