用户端历史订单 查询历史订单 产品原型
业务规则
分页查询历史订单
可以根据订单状态查询
展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)
接口设计
代码实现 1.user/OrderController
1 2 3 4 5 6 7 @GetMapping("/historyOrders") @ApiOperation("历史订单查询") public Result<PageResult> page (int page, int pageSize, Integer status) { PageResult pageResult = orderService.pageQuery4User(page, pageSize, status); return Result.success(pageResult); }
2.OrderService
1 2 PageResult pageQuery4User (int page, int pageSize, Integer status) ;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public PageResult pageQuery4User (int pageNum, int pageSize, Integer status) { PageHelper.startPage(pageNum, pageSize); OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO (); ordersPageQueryDTO.setUserId(BaseContext.getCurrentId()); ordersPageQueryDTO.setStatus(status); Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO); List<OrderVO> list = new ArrayList (); if (page != null && page.getTotal() > 0 ) { for (Orders orders : page) { Long orderId = orders.getId(); List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId); OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDetailList(orderDetails); list.add(orderVO); } } return new PageResult (page.getTotal(), list); }
4.OrderMapper
1 2 Page<Orders> pageQuery (OrdersPageQueryDTO ordersPageQueryDTO) ;
5.OrderMapper.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <select id ="pageQuery" resultType ="Orders" > select * from orders <where > <if test ="number != null and number!=''" > and number like concat('%',#{number},'%') </if > <if test ="phone != null and phone!=''" > and phone like concat('%',#{phone},'%') </if > <if test ="userId != null" > and user_id = #{userId} </if > <if test ="status != null" > and status = #{status} </if > <if test ="beginTime != null" > and order_time > = #{beginTime} </if > <if test ="endTime != null" > and order_time < = #{endTime} </if > </where > order by order_time desc</select >
6.OrderDetailMapper
1 2 3 @Select("select * from order_detail where order_id = #{orderId}") List<OrderDetail> getByOrderId (Long orderId) ;
查询订单详情 产品原型
接口设计
代码实现 1.user/OrderController
1 2 3 4 5 6 7 @GetMapping("/orderDetail/{id}") @ApiOperation("查询订单详情") public Result<OrderVO> details (@PathVariable("id") Long id) { OrderVO orderVO = orderService.details(id); return Result.success(orderVO); }
2.OrderService
1 2 OrderVO details (Long id) ;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public OrderVO details (Long id) { Orders orders = orderMapper.getById(id); List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId()); OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); orderVO.setOrderDetailList(orderDetailList); return orderVO; }
4.OrderMapper
1 2 3 @Select("select * from orders where id = #{id}") Orders getById (Long id) ;
问题 【解决配送地址为null】 参考链接:
解决黑马苍穹外卖订单详情页面缺少配送地址(address)、订单备注(remark)的问题,以及如何隐藏顾客名字和电话号码-CSDN博客
苍穹外卖:解决订单没有地址(address)的问题_mars3d的getaddress方法返回未查询到相关结果!-CSDN博客
取消订单 产品原型
业务规则:
待支付和待接单状态下,用户可直接取消订单
商家已接单状态下,用户取消订单需电话沟通商家
派送中状态下,用户取消订单需电话沟通商家
如果在待接单状态下取消订单,需要给用户退款
取消订单后需要将订单状态修改为“已取消”
接口设计
代码实现 1.user/OrderController
1 2 3 4 5 6 7 @PutMapping("/cancel/{id}") @ApiOperation("取消订单") public Result cancel (@PathVariable("id") Long id) throws Exception { orderService.userCancelById(id); return Result.success(); }
2.OrderService
1 2 void userCancelById (Long id) throws Exception;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public void userCancelById (Long id) throws Exception { Orders ordersDB = orderMapper.getById(id); if (ordersDB == null ) { throw new OrderBusinessException (MessageConstant.ORDER_NOT_FOUND); } if (ordersDB.getStatus() > 2 ) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders (); orders.setId(ordersDB.getId()); if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) { weChatPayUtil.refund( ordersDB.getNumber(), ordersDB.getNumber(), new BigDecimal (0.01 ), new BigDecimal (0.01 )); orders.setPayStatus(Orders.REFUND); } orders.setStatus(Orders.CANCELLED); orders.setCancelReason("用户取消" ); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
再来一单 产品原型
接口设计
业务规则:
代码实现 1.user/OrderController
1 2 3 4 5 6 7 @PostMapping("/repetition/{id}") @ApiOperation("再来一单") public Result repetition (@PathVariable Long id) { orderService.repetition(id); return Result.success(); }
2.OrderService
1 2 void repetition (Long id) ;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void repetition (Long id) { Long userId = BaseContext.getCurrentId(); List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id); List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> { ShoppingCart shoppingCart = new ShoppingCart (); BeanUtils.copyProperties(x, shoppingCart, "id" ); shoppingCart.setUserId(userId); shoppingCart.setCreateTime(LocalDateTime.now()); return shoppingCart; }).collect(Collectors.toList()); shoppingCartMapper.insertBatch(shoppingCartList); }
4.ShoppingCartMapper
1 2 3 4 5 6 void insertBatch (List<ShoppingCart> shoppingCartList) ;
5.ShoppingCartMapper.xml
1 2 3 4 5 6 7 8 <insert id ="insertBatch" parameterType ="list" > insert into shopping_cart (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time) values <foreach collection ="shoppingCartList" item ="sc" separator ="," > (#{sc.name},#{sc.image},#{sc.userId},#{sc.dishId},#{sc.setmealId},#{sc.dishFlavor},#{sc.number},#{sc.amount},#{sc.createTime}) </foreach > </insert >
商家端订单管理模块 订单搜索 产品原型
业务规则:
输入订单号/手机号进行搜索,支持模糊搜索
根据订单状态进行筛选
下单时间进行时间筛选
搜索内容为空,提示未找到相关订单
搜索结果页,展示包含搜索关键词的内容
分页展示搜索到的订单数据
接口设计
代码实现 1.admin/OrderController
在admin包下创建OrderController
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @RestController("adminOrderController") @RequestMapping("/admin/order") @Slf4j @Api(tags = "订单管理接口") public class OrderController { @Autowired private OrderService orderService; @GetMapping("/conditionSearch") @ApiOperation("订单搜索") public Result<PageResult> conditionSearch (OrdersPageQueryDTO ordersPageQueryDTO) { PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO); return Result.success(pageResult); } }
2.OrderService
1 2 PageResult conditionSearch (OrdersPageQueryDTO ordersPageQueryDTO) ;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 public PageResult conditionSearch (OrdersPageQueryDTO ordersPageQueryDTO) { PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize()); Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO); List<OrderVO> orderVOList = getOrderVOList(page); return new PageResult (page.getTotal(), orderVOList); }private List<OrderVO> getOrderVOList (Page<Orders> page) { List<OrderVO> orderVOList = new ArrayList <>(); List<Orders> ordersList = page.getResult(); if (!CollectionUtils.isEmpty(ordersList)) { for (Orders orders : ordersList) { OrderVO orderVO = new OrderVO (); BeanUtils.copyProperties(orders, orderVO); String orderDishes = getOrderDishesStr(orders); orderVO.setOrderDishes(orderDishes); orderVOList.add(orderVO); } } return orderVOList; }private String getOrderDishesStr (Orders orders) { List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId()); List<String> orderDishList = orderDetailList.stream().map(x -> { String orderDish = x.getName() + "*" + x.getNumber() + ";" ; return orderDish; }).collect(Collectors.toList()); return String.join("" , orderDishList); }
各个状态的订单数量统计 产品原型
接口设计
代码实现 1.admin/OrderController
1 2 3 4 5 6 7 @GetMapping("/statistics") @ApiOperation("各个状态的订单数量统计") public Result<OrderStatisticsVO> statistics () { OrderStatisticsVO orderStatisticsVO = orderService.statistics(); return Result.success(orderStatisticsVO); }
2.OrderService
1 2 OrderStatisticsVO statistics () ;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public OrderStatisticsVO statistics () { Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED); Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED); Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS); OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO (); orderStatisticsVO.setToBeConfirmed(toBeConfirmed); orderStatisticsVO.setConfirmed(confirmed); orderStatisticsVO.setDeliveryInProgress(deliveryInProgress); return orderStatisticsVO; }
4.OrderMapper
1 2 3 @Select("select count(id) from orders where status = #{status}") Integer countStatus (Integer status) ;
查询订单详情 产品原型
业务规则:
订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
订单详情页面需要展示订单明细数据(商品名称、数量、单价)
接口设计
代码实现 admin/OrderController
1 2 3 4 5 6 7 @GetMapping("/details/{id}") @ApiOperation("查询订单详情") public Result<OrderVO> details (@PathVariable("id") Long id) { OrderVO orderVO = orderService.details(id); return Result.success(orderVO); }
接单 产品原型
业务规则:
接口设计
代码实现 1.admin/OrderController
1 2 3 4 5 6 7 @PutMapping("/confirm") @ApiOperation("接单") public Result confirm (@RequestBody OrdersConfirmDTO ordersConfirmDTO) { orderService.confirm(ordersConfirmDTO); return Result.success(); }
2.OrderService
1 2 void confirm (OrdersConfirmDTO ordersConfirmDTO) ;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 public void confirm (OrdersConfirmDTO ordersConfirmDTO) { Orders orders = Orders.builder() .id(ordersConfirmDTO.getId()) .status(Orders.CONFIRMED) .build(); orderMapper.update(orders); }
拒单 产品原型
业务规则:
商家拒单其实就是将订单状态修改为“已取消”
只有订单处于“待接单”状态时可以执行拒单操作
商家拒单时需要指定拒单原因
商家拒单时,如果用户已经完成了支付,需要为用户退款
接口设计
代码实现 1.admin/OrderController
1 2 3 4 5 6 7 @PutMapping("/rejection") @ApiOperation("拒单") public Result rejection (@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception { orderService.rejection(ordersRejectionDTO); return Result.success(); }
2.OrderService
1 2 void rejection (OrdersRejectionDTO ordersRejectionDTO) throws Exception;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public void rejection (OrdersRejectionDTO ordersRejectionDTO) throws Exception { Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId()); if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } Integer payStatus = ordersDB.getPayStatus(); if (payStatus == Orders.PAID) { String refund = weChatPayUtil.refund( ordersDB.getNumber(), ordersDB.getNumber(), new BigDecimal (0.01 ), new BigDecimal (0.01 )); log.info("申请退款:{}" , refund); } Orders orders = new Orders (); orders.setId(ordersDB.getId()); orders.setStatus(Orders.CANCELLED); orders.setRejectionReason(ordersRejectionDTO.getRejectionReason()); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
取消订单 产品原型
业务规则:
取消订单其实就是将订单状态修改为“已取消”
商家取消订单时需要指定取消原因
商家取消订单时,如果用户已经完成了支付,需要为用户退款
接口设计
代码实现 1.admin/OrderController
1 2 3 4 5 6 7 @PutMapping("/cancel") @ApiOperation("取消订单") public Result cancel (@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception { orderService.cancel(ordersCancelDTO); return Result.success(); }
2.OrderService
1 2 void cancel (OrdersCancelDTO ordersCancelDTO) throws Exception;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 public void cancel (OrdersCancelDTO ordersCancelDTO) throws Exception { Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId()); Integer payStatus = ordersDB.getPayStatus(); if (payStatus == 1 ) { String refund = weChatPayUtil.refund( ordersDB.getNumber(), ordersDB.getNumber(), new BigDecimal (0.01 ), new BigDecimal (0.01 )); log.info("申请退款:{}" , refund); } Orders orders = new Orders (); orders.setId(ordersCancelDTO.getId()); orders.setStatus(Orders.CANCELLED); orders.setCancelReason(ordersCancelDTO.getCancelReason()); orders.setCancelTime(LocalDateTime.now()); orderMapper.update(orders); }
派送订单 产品原型
业务规则:
派送订单其实就是将订单状态修改为“派送中”
只有状态为“待派送”的订单可以执行派送订单操作
接口设计
代码实现 1.admin/OrderController
1 2 3 4 5 6 7 @PutMapping("/delivery/{id}") @ApiOperation("派送订单") public Result delivery (@PathVariable("id") Long id) { orderService.delivery(id); return Result.success(); }
2.OrderService
1 2 void delivery (Long id) ;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public void delivery (Long id) { Orders ordersDB = orderMapper.getById(id); if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders (); orders.setId(ordersDB.getId()); orders.setStatus(Orders.DELIVERY_IN_PROGRESS); orderMapper.update(orders); }
完成订单 产品原型
业务规则:
完成订单其实就是将订单状态修改为“已完成”
只有状态为“派送中”的订单可以执行订单完成操作
接口设计
代码实现 1.admin/OrderController
1 2 3 4 5 6 7 @PutMapping("/complete/{id}") @ApiOperation("完成订单") public Result complete (@PathVariable("id") Long id) { orderService.complete(id); return Result.success(); }
2.OrderService
1 2 void complete (Long id) ;
3.OrderServiceImpl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void complete (Long id) { Orders ordersDB = orderMapper.getById(id); if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) { throw new OrderBusinessException (MessageConstant.ORDER_STATUS_ERROR); } Orders orders = new Orders (); orders.setId(ordersDB.getId()); orders.setStatus(Orders.COMPLETED); orders.setDeliveryTime(LocalDateTime.now()); orderMapper.update(orders); }
校验收货地址是否超出配送范围 环境准备 注册账号:https://passport.baidu.com/v2/?reg&tt=1671699340600&overseas=&gid=CF954C2-A3D2-417F-9FE6-B0F249ED7E33&tpl=pp&u=https%3A%2F%2Flbsyun.baidu.com%2Findex.php%3Ftitle%3D%E9%A6%96%E9%A1%B5
登录百度地图开放平台:https://lbsyun.baidu.com/
进入控制台,创建应用,获取AK:
相关接口:
https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding
https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1
代码开发 1.application.yml
配置外卖商家店铺地址和百度地图的AK:
2.OrderServiceImpl
改造OrderServiceImpl,注入上面的配置项:
1 2 3 4 5 @Value("${sky.shop.address}") private String shopAddress;@Value("${sky.baidu.ak}") private String ak;
在OrderServiceImpl中提供校验方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 private void checkOutOfRange (String address) { Map map = new HashMap (); map.put("address" ,shopAddress); map.put("output" ,"json" ); map.put("ak" ,ak); String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3" , map); JSONObject jsonObject = JSON.parseObject(shopCoordinate); if (!jsonObject.getString("status" ).equals("0" )){ throw new OrderBusinessException ("店铺地址解析失败" ); } JSONObject location = jsonObject.getJSONObject("result" ).getJSONObject("location" ); String lat = location.getString("lat" ); String lng = location.getString("lng" ); String shopLngLat = lat + "," + lng; map.put("address" ,address); String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3" , map); jsonObject = JSON.parseObject(userCoordinate); if (!jsonObject.getString("status" ).equals("0" )){ throw new OrderBusinessException ("收货地址解析失败" ); } location = jsonObject.getJSONObject("result" ).getJSONObject("location" ); lat = location.getString("lat" ); lng = location.getString("lng" ); String userLngLat = lat + "," + lng; map.put("origin" ,shopLngLat); map.put("destination" ,userLngLat); map.put("steps_info" ,"0" ); String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving" , map); jsonObject = JSON.parseObject(json); if (!jsonObject.getString("status" ).equals("0" )){ throw new OrderBusinessException ("配送路线规划失败" ); } JSONObject result = jsonObject.getJSONObject("result" ); JSONArray jsonArray = (JSONArray) result.get("routes" ); Integer distance = (Integer) ((JSONObject) jsonArray.get(0 )).get("distance" ); if (distance > 5000 ){ throw new OrderBusinessException ("超出配送范围" ); } }
在OrderServiceImpl的submitOrder方法中调用上面的校验方法:
订单状态定时处理 Spring Task 介绍 Spring Task是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。
定位:定时任务框架。
作用:定时自动执行某段Java代码。
应用场景:
信用卡每月还款提醒。
银行贷款每月还款提醒。
火车票售票系统处理未支付订单。
入职纪念日为用户发送通知。
只要是需要定时处理的场景都可以使用Spring Task。
cron表达式 cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发的时间。
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义。
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)。
cron表达式在线生成器:https://cron.qqe2.com/
入门案例 Spring Task使用步骤:
导入maven坐标spring-context(已存在)。
2.启动类添加注解@EnableScheduling开启任务调度。
1 2 @EnableScheduling public class SkyApplication {}
3.自定义定时任务类。
1 2 3 4 5 6 7 8 9 10 11 12 package com.sky.task;@Component @Slf4j public class MyTask { @Scheduled(cron = "0/5 * * * * ?") public void executeTask () { log.info("定时任务开始执行:{}" , new Date ()); } }
订单状态定时处理 需求分析 用户下单后可能存在的情况:
下单后未支付,订单一直处于“待支付”状态。
用户收货后管理端未点击完成按钮,订单一直处于“派送中”状态。
对于上面两种情况需要通过定时任务来修改订单状态,具体逻辑为:
通过定时任务每分钟检查一次是否存在支付超时订单(下单后超过15分钟仍未支付则判定为支付超时订单),如果存在则修改订单状态为“已取消”。
通过定时任务每天凌晨1点检查一次是否存在“派送中”的订单,如果存在则修改订单状态为“已完成”。
代码开发 在OrderMapper接口中扩展方法。
1 2 3 @Select("select * from orders where status = #{status} and order_time < #{orderTime}") List<Orders> getByStatusAndOrderTimeLT (Integer status, LocalDateTime orderTime) ;
自定义定时任务类OrderTask。完善定时任务类的processTimeoutOrder方法。完善定时任务类的processDeliveryOrder方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package com.sky.task;@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 ); 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); } } } @Scheduled(cron = "0 0 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.size() > 0 ){ for (Orders orders : ordersList){ orders.setStatus(Orders.CONFIRMED); orderMapper.update(orders); } } } }
WebSocket 介绍 WebSocket是基于TCP的一种新的网络协议。它实现了浏览器与服务器全双工通信:浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接, 并进行双向数据传输。
HTTP协议和WebSocket协议对比:
HTTP是短连接,WebSocket是长连接。
HTTP通信是单向的,基于请求响应模式。WebSocket支持双向通信。
HTTP和WebSocket底层都是TCP连接。
WebSocket应用场景:
视频弹幕
网页聊天
体育实况更新
股票基金报价实时更新
WebSocket缺点:
服务器长期维护长连接需要一定的成本。
各个浏览器支持程度不一。
WebSocket是长连接,受网络限制比较大,需要处理好重连。
结论:WebSocket支持双向通信,功能看似比HTTP强大,但是也不可以基于WebSocket开发所有的业务功能。WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用。
入门案例 实现步骤:
1.直接使用websocket.html页面作为WebSocket客户端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 <!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 ); if ('WebSocket' in window ){ 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" ); } 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 >
2.导入WebSocket的maven坐标。
1 2 3 4 <dependency > <groupId > org.springframework.boot</groupId > <artifactId > spring-boot-starter-websocket</artifactId > </dependency >
3.导入WebSocket服务端组件WebSocketServer,用于和客户端通信。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 package com.sky.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); } @OnMessage public void onMessage (String message, @PathParam("sid") String sid) { System.out.println("收到来自客户端:" + sid + "的信息:" + message); } @OnClose public void onClose (@PathParam("sid") String sid) { System.out.println("连接断开:" + sid); sessionMap.remove(sid); } public void sendToAllClient (String message) { Collection<Session> sessions = sessionMap.values(); for (Session session : sessions) { try { session.getBasicRemote().sendText(message); } catch (Exception e) { e.printStackTrace(); } } } }
4.导入配置类WebSocketConfiguration,注册WebSocket的服务端组件。
1 2 3 4 5 6 7 8 9 10 package com.sky.config;@Configuration public class WebSocketConfiguration { @Bean public ServerEndpointExporter serverEndpointExporter () { return new ServerEndpointExporter (); } }
5.导入定时任务类WebSocketTask,定时向客户端推送数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 package com.sky.task;@Component public class WebSocketTask { @Autowired private WebSocketServer webSocketServer; @Scheduled(cron = "0/5 * * * * ?") public void sendMessageToClient () { webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss" ).format(LocalDateTime.now())); } }
6.运行结果。
来单提醒 需求分析 用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:
设计 1.通过WebSocket实现管理端页面和服务端保持长连接状态。
2.当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息。
3.客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报。
4.约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content。
type为消息类型:1为来单提醒,2为客户催单。
orderId为订单id。
content 为消息内容。
代码开发 在OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:(这是使用了微信支付的)
1 2 3 4 5 6 Map map = new HashMap (); map.put("type" , 1 ); map.put("orderId" , orders.getId()); map.put("content" ,"订单号:" + outTradeNo); webSocketServer.sendToAllClient(JSON.toJSONString(map));
如果跳过微信支付,需要改为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 @Autowired private WebSocketServer webSocketServer;@Override public OrderPaymentVO payment (OrdersPaymentDTO ordersPaymentDTO) throws Exception{ Long userId = BaseContext.getCurrentId(); User user = userMapper.getById(userId); JSONObject jsonObject = new JSONObject (); jsonObject.put("code" , "ORDERPAID" ); OrderPaymentVO vo = jsonObject.toJavaObject(OrderPaymentVO.class); vo.setPackageStr(jsonObject.getString("package" )); Integer OrderPaidStatus = Orders.PAID; Integer OrderStatus = Orders.TO_BE_CONFIRMED; LocalDateTime check_out_time = LocalDateTime.now(); String orderNumber = ordersPaymentDTO.getOrderNumber(); log.info("调用updateStatus,用于替换微信支付更新数据库状态的问题" ); orderMapper.updateStatus(OrderStatus, OrderPaidStatus, check_out_time, orderNumber); Orders orders = orderMapper.getByNumber(orderNumber); Map map = new HashMap (); map.put("type" , 1 ); map.put("orderId" , orders.getId()); map.put("content" , "订单号:" + orderNumber); String json = JSON.toJSONString(map); webSocketServer.sendToAllClient(json); return vo; }
注意 浏览器发起的请求为:
1 2 3 请求 URL: ws: Request Method : GET 状态代码: 101 Switching Protocols
请求url中未指定端口,但是nginx中配置了反向代理指定了8080端口,所有能够顺利访问管理端。
问题 【没有语音播报】 解决:浏览器退出登录,重新登录账号。
【语音播报一直循环】 解决:注释掉WebSocketTask中的sendMessageToClient方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 package com.sky.task;@Component public class WebSocketTask { @Autowired private WebSocketServer webSocketServer; }
客户催单 需求分析 用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。通知的形式有如下两种:
设计 1.通过WebSocket实现管理端页面和服务端保持长连接状态。
2.当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息。
3.客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报。
4.约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content。
type为消息类型:1为来单提醒,2为客户催单。
orderId为订单id。
content 为消息内容。
接口设计
代码开发 1.根据用户催单的接口定义,在user/OrderController中创建催单方法。
1 2 3 4 5 6 7 @GetMapping("/reminder/{id}") @ApiOperation("客户催单") public Result reminder (@PathVariable("id") Long id) { orderService.reminder(id); return Result.success(); }
2.在OrderService接口中声明reminder方法。
1 2 void reminder (Long id) ;
3.在OrderServiceImpl中实现reminder方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Override public void reminder (Long id) { Orders ordersDB = orderMapper.getById(id); if (ordersDB == null ){ throw new OrderBusinessException (MessageConstant.ORDER_NOT_FOUND); } Map map = new HashMap (); map.put("type" , 2 ); map.put("orderId" , id); map.put("content" , "订单号:" + ordersDB.getNumber()); String json = JSON.toJSONString(map); webSocketServer.sendToAllClient(json); }