苍穹外卖 Spring Task 来单提醒 催单Apache ECharts day10~11

苍穹外卖-day10

课程内容

  • Spring Task
  • 订单状态定时处理
  • WebSocket
  • 来单提醒
  • 客户催单

功能实现:订单状态定时处理来单提醒客户催单

订单状态定时处理:

来单提醒:

客户催单:

1. Spring Task

1.1 介绍

Spring Task 是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑。

定位:定时任务框架

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

为什么要在Java程序中使用Spring Task?

应用场景:

1). 信用卡每月还款提醒

2). 银行贷款每月还款提醒

3). 火车票售票系统处理未支付订单

4). 入职纪念日为用户发送通知

强调:只要是需要定时处理的场景都可以使用Spring Task

1.2 cron表达式

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

构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义

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

举例:

2022年10月12日上午9点整 对应的cron表达式为:0 0 9 12 10 ? 2022

说明:一般的值不同时设置,其中一个设置,另一个用?表示。

比如:描述2月份的最后一天,最后一天具体是几号呢?可能是28号,也有可能是29号,所以就不能写具体数字。

为了描述这些信息,提供一些特殊的字符。这些具体的细节,我们就不用自己去手写,因为这个cron表达式,它其实有在线生成器。

cron表达式在线生成器:在线Cron表达式生成器

可以直接在这个网站上面,只要根据自己的要求去生成corn表达式即可。所以一般就不用自己去编写这个表达式。

通配符:

* 表示所有值;

? 表示未说明的值,即不关心它为何值

- 表示一个指定的范围

, 表示附加一个可能值

/ 符号前表示开始时间,符号后表示每次递增的值;

cron表达式案例:

*/5 * * * * ? 每隔5秒执行一次

0 */1 * * * ? 每隔1分钟执行一次

0 0 5-15 * * ? 每天5-15点整点触发

0 0/3 * * * ? 每三分钟触发一次

0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发

0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发

0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发

0 0/30 9-17 * * ? 朝九晚五工作时间内每半小时

0 0 10,14,16 * * ? 每天上午10点,下午2点,4点

1.3 入门案例

1.3.1 Spring Task使用步骤

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

2). 启动类添加注解 @EnableScheduling 开启任务调度

3). 自定义定时任务类

1.3.2 代码开发

编写定时任务类:

进入sky-server模块中

package com.sky.task;import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;import java.util.Date;/*** 自定义定时任务类*/
@Component
@Slf4j
public class MyTask {/*** 定时任务 每隔5秒触发一次*/@Scheduled(cron = "0/5 * * * * ?")public void executeTask(){log.info("定时任务开始执行:{}",new Date());}
}

开启任务调度:

启动类添加注解 @EnableScheduling

package com.sky;import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching
@EnableScheduling
public class SkyApplication {public static void main(String[] args) {SpringApplication.run(SkyApplication.class, args);log.info("server started");}
}

1.3.3 功能测试

启动服务,查看日志

每隔5秒执行一次。

2.订单状态定时处理

2.1 需求分析

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

  • 下单后未支付,订单一直处于**“待支付”**状态
  • 用户收货后管理端未点击完成按钮,订单一直处于**“派送中”**状态

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

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

2.2 代码开发

1). 自定义定时任务类OrderTask(待完善):

package com.sky.task;/*** 自定义定时任务,实现订单状态定时处理*/
@Component
@Slf4j
public class OrderTask {@Autowiredprivate OrderMapper orderMapper;/*** 处理支付超时订单*/@Scheduled(cron = "0 * * * * ?")public void processTimeoutOrder(){log.info("处理支付超时订单:{}", new Date());}/*** 处理“派送中”状态的订单*/@Scheduled(cron = "0 0 1 * * ?")public void processDeliveryOrder(){log.info("处理派送中订单:{}", new Date());}}

2). 在OrderMapper接口中扩展方法:

	/*** 根据状态和下单时间查询订单* @param status* @param orderTime*/@Select("select * from orders where status = #{status} and order_time < #{orderTime}")List<Orders> getByStatusAndOrdertimeLT(Integer status, LocalDateTime orderTime);

3). 完善定时任务类的processTimeoutOrder方法:

	/*** 处理支付超时订单*/@Scheduled(cron = "0 * * * * ?")public void processTimeoutOrder(){log.info("处理支付超时订单:{}", new Date());LocalDateTime time = LocalDateTime.now().plusMinutes(-15);// select * from orders where status = 1 and order_time < 当前时间-15分钟List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.PENDING_PAYMENT, time);if(ordersList != null && ordersList.size() > 0){ordersList.forEach(order -> {order.setStatus(Orders.CANCELLED);order.setCancelReason("支付超时,自动取消");order.setCancelTime(LocalDateTime.now());orderMapper.update(order);});}}

4). 完善定时任务类的processDeliveryOrder方法:

	/*** 处理“派送中”状态的订单*/@Scheduled(cron = "0 0 1 * * ?")public void processDeliveryOrder(){log.info("处理派送中订单:{}", new Date());// select * from orders where status = 4 and order_time < 当前时间-1小时LocalDateTime time = LocalDateTime.now().plusMinutes(-60);List<Orders> ordersList = orderMapper.getByStatusAndOrdertimeLT(Orders.DELIVERY_IN_PROGRESS, time);if(ordersList != null && ordersList.size() > 0){ordersList.forEach(order -> {order.setStatus(Orders.COMPLETED);orderMapper.update(order);});}}

2.3 功能测试

可以通过如下方式进行测试:

  • 查看控制台sql
  • 查看数据库中数据变化

支付超时的订单测试:

1). 查看订单表

有一条订单,状态为1。订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消

2). 开启定时任务

启动服务,观察控制台日志。处理支付超时订单任务每隔1分钟执行一次。

3). 再次查看订单表

状态已更改为6,已取消。

证明定时任务已生效。

处理“派送中”状态的订单任务测试自已完成,测试步骤和上述一致。可适当修改cron表达式,改变任务执行频率,方便测试。

3. WebSocket

3.1 介绍

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

HTTP协议和WebSocket协议对比

  • HTTP是短连接
  • WebSocket是长连接
  • HTTP通信是单向的,基于请求响应模式
  • WebSocket支持双向通信
  • HTTP和WebSocket底层都是TCP连接

思考:既然WebSocket支持双向通信,功能看似比HTTP强大,那么我们是不是可以基于WebSocket开发所有的业务功能?

WebSocket缺点:

服务器长期维护长连接需要一定的成本

各个浏览器支持程度不一

WebSocket 是长连接,受网络限制比较大,需要处理好重连

结论:WebSocket并不能完全取代HTTP,它只适合在特定的场景下使用

WebSocket应用场景:

1). 视频弹幕

2). 网页聊天

3). 体育实况更新

4). 股票基金报价实时更新

3.2 入门案例

3.2.1 案例分析

需求:实现浏览器与服务器全双工通信。浏览器既可以向服务器发送消息,服务器也可主动向浏览器推送消息。

效果展示:

实现步骤:

1). 直接使用websocket.html页面作为WebSocket客户端

2). 导入WebSocket的maven坐标

3). 导入WebSocket服务端组件WebSocketServer,用于和客户端通信

4). 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件

5). 导入定时任务类WebSocketTask,定时向客户端推送数据

3.2.2 代码开发

1). 定义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);//判断当前浏览器是否支持WebSocketif('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>

2). 导入maven坐标

在sky-server模块pom.xml中已定义

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId>
</dependency>

3). 定义WebSocket服务端组件(资料中已提供)

直接导入到sky-server模块即可

package com.sky.websocket;import java.util.HashMap;
import java.util.Map;/*** WebSocket服务*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {//存放会话对象private static Map<String, Session> sessionMap = new HashMap();/*** 连接建立成功调用的方法*/@OnOpenpublic void onOpen(Session session, @PathParam("sid") String sid) {System.out.println("客户端:" + sid + "建立连接");sessionMap.put(sid, session);}/*** 收到客户端消息后调用的方法** @param message 客户端发送过来的消息*/@OnMessagepublic void onMessage(String message, @PathParam("sid") String sid) {System.out.println("收到来自客户端:" + sid + "的信息:" + message);}/*** 连接关闭调用的方法** @param sid*/@OnClosepublic 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();}}}
}

4). 定义配置类,注册WebSocket的服务端组件(从资料中直接导入即可)

package com.sky.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;/*** WebSocket配置类,用于注册WebSocket的Bean*/
@Configuration
public class WebSocketConfiguration {@Beanpublic ServerEndpointExporter serverEndpointExporter() {return new ServerEndpointExporter();}
}

5). 定义定时任务类,定时向客户端推送数据(从资料中直接导入即可)

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

3.2.3 功能测试

启动服务,打开websocket.html页面

浏览器向服务器发送数据:

服务器向浏览器间隔5秒推送数据:

4. 来单提醒

4.1 需求分析和设计

用户下单并且支付成功后,需要第一时间通知外卖商家。通知的形式有如下两种:

语音播报

  • 弹出提示框

设计思路:

  • 通过WebSocket实现管理端页面和服务端保持长连接状态
  • 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
  • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
  • 约定服务端发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
    • type 为消息类型,1为来单提醒 2为客户催单
    • orderId 为订单id
    • content 为消息内容

这里的socket链接配置了反向代理P133的视频

4.2 代码开发

在OrderServiceImpl中注入WebSocketServer对象,修改paySuccess方法,加入如下代码:

	@Autowiredprivate WebSocketServer webSocketServer;/*** 支付成功,修改订单状态** @param outTradeNo*/public void paySuccess(String outTradeNo) {// 当前登录用户idLong userId = BaseContext.getCurrentId();// 根据订单号查询当前用户的订单Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);// 根据订单id更新订单的状态、支付方式、支付状态、结账时间Orders orders = Orders.builder().id(ordersDB.getId()).status(Orders.TO_BE_CONFIRMED).payStatus(Orders.PAID).checkoutTime(LocalDateTime.now()).build();orderMapper.update(orders);//Map map = new HashMap();map.put("type", 1);//消息类型,1表示来单提醒map.put("orderId", orders.getId());map.put("content", "订单号:" + outTradeNo);//通过WebSocket实现来单提醒,向客户端浏览器推送消息webSocketServer.sendToAllClient(JSON.toJSONString(map));///}

4.3 功能测试

可以通过如下方式进行测试:

  • 查看浏览器调试工具数据交互过程
  • 前后端联调

1). 登录管理端后台

登录成功后,浏览器与服务器建立长连接

查看控制台日志

2). 小程序端下单支付

修改回调地址,利用内网穿透获取域名

下单支付

3). 查看来单提醒

支付成功后,后台收到来单提醒,并有语音播报

4.4 代码提交

后续步骤和其它功能代码提交一致,不再赘述。

5. 客户催单

5.1 需求分析和设计

用户在小程序中点击催单按钮后,需要第一时间通知外卖商家。通知的形式有如下两种:

语音播报

  • 弹出提示框

设计思路:

  • 通过WebSocket实现管理端页面和服务端保持长连接状态
  • 当用户点击催单按钮后,调用WebSocket的相关API实现服务端向客户端推送消息
  • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报

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

  • type 为消息类型,1为来单提醒 2为客户催单
  • orderId 为订单id
  • content 为消息内容

当用户点击催单按钮时,向服务端发送请求。

接口设计(催单):

5.2 代码开发

5.2.1 Controller层

根据用户催单的接口定义,在user/OrderController中创建催单方法:

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

5.2.2 Service层接口

在OrderService接口中声明reminder方法:

	/*** 用户催单* @param id*/void reminder(Long id);

5.2.3 Service层实现类

在OrderServiceImpl中实现reminder方法:

	/*** 用户催单** @param id*/public void reminder(Long id) {// 查询订单是否存在Orders orders = orderMapper.getById(id);if (orders == null) {throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);}//基于WebSocket实现催单Map map = new HashMap();map.put("type", 2);//2代表用户催单map.put("orderId", id);map.put("content", "订单号:" + orders.getNumber());webSocketServer.sendToAllClient(JSON.toJSONString(map));}

5.2.4 Mapper层

在OrderMapper中添加getById:

	/*** 根据id查询订单* @param id*/@Select("select * from orders where id=#{id}")Orders getById(Long id);

5.3 功能测试

可以通过如下方式进行测试:

  • 查看浏览器调试工具数据交互过程
  • 前后端联调

1). 登录管理端后台

登录成功后,浏览器与服务器建立长连接

查看控制台日志

2). 用户进行催单

用户可在订单列表或者订单详情,进行催单

3). 查看催单提醒

既有催单弹窗,同时语音播报

5.4 代码提交

后续步骤和其它功能代码提交一致,不再赘述。


苍穹外卖-day11

课程内容

  • Apache ECharts
  • 营业额统计
  • 用户统计
  • 订单统计
  • 销量排名Top10

功能实现:数据统计

数据统计效果图:

1. Apache ECharts

1.1 介绍

Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。

官网地址:Apache ECharts

常见效果展示:

1). 柱形图

2). 饼形图

3). 折线图

总结:不管是哪种形式的图形,最本质的东西实际上是数据,它其实是对数据的一种可视化展示。

1.2 入门案例

Apache Echarts官方提供的快速入门:快速上手 - Handbook - Apache ECharts

效果展示:

实现步骤:

1). 引入echarts.js 文件(当天资料已提供)

2). 为 ECharts 准备一个设置宽高的 DOM

3). 初始化echarts实例

4). 指定图表的配置项和数据

5). 使用指定的配置项和数据显示图表

代码开发:

<!DOCTYPE html>
<html><head><meta charset="utf-8" /><title>ECharts</title><!-- 引入刚刚下载的 ECharts 文件 --><script src="echarts.js"></script></head><body><!-- 为 ECharts 准备一个定义了宽高的 DOM --><div id="main" style="width: 600px;height:400px;"></div><script type="text/javascript">// 基于准备好的dom,初始化echarts实例var myChart = echarts.init(document.getElementById('main'));// 指定图表的配置项和数据var option = {title: {text: 'ECharts 入门示例'},tooltip: {},legend: {data: ['销量']},xAxis: {data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']},yAxis: {},series: [{name: '销量',type: 'bar',data: [5, 20, 36, 10, 10, 20]}]};// 使用刚指定的配置项和数据显示图表。myChart.setOption(option);</script></body>
</html>

测试:(当天资料中已提供)

使用浏览器方式打开即可。

总结:使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。

2. 营业额统计

2.1 需求分析和设计

2.1.1 产品原型

营业额统计是基于折现图来展现,并且按照天来展示的。实际上,就是某一个时间范围之内的每一天的营业额。同时,不管光标放在哪个点上,那么它就会把具体的数值展示来。并且还需要注意日期并不是固定写死的,是由上边时间选择器来决定。比如选择是近7天、或者是近30日,或者是本周,就会把相应这个时间段之内的每一天日期通过横坐标展示。

原型图:

业务规则:

  • 营业额指订单状态为已完成的订单金额合计
  • 基于可视化报表的折线图展示营业额数据,X轴为日期,Y轴为营业额
  • 根据时间选择区间,展示每天的营业额数据

2.1.2 接口设计

通过上述原型图,设计出对应的接口。

注意:具体返回数据一般由前端来决定,前端展示图表,具体折现图对应数据是什么格式,是有固定的要求的。

所以说,后端需要去适应前端,它需要什么格式的数据,我们就给它返回什么格式的数据。

2.2 代码开发

2.2.1 VO设计

根据接口定义设计对应的VO:

在sky-pojo模块,TurnoverReportVO.java已定义

package com.sky.vo;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;import java.io.Serializable;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TurnoverReportVO implements Serializable {//日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03private String dateList;//营业额,以逗号分隔,例如:406.0,1520.0,75.0private String turnoverList;}

2.2.2 Controller层

根据接口定义创建ReportController:

package com.sky.controller.admin;import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;/*** 报表*/
@RestController
@RequestMapping("/admin/report")
@Slf4j
@Api(tags = "统计报表相关接口")
public class ReportController {@Autowiredprivate ReportService reportService;/*** 营业额数据统计** @param begin* @param end* @return*/@GetMapping("/turnoverStatistics")@ApiOperation("营业额数据统计")public Result<TurnoverReportVO> turnoverStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate end) {return Result.success(reportService.getTurnover(begin, end));}}

2.2.3 Service层接口

创建ReportService接口,声明getTurnover方法:

package com.sky.service;import com.sky.vo.TurnoverReportVO;
import java.time.LocalDate;public interface ReportService {/*** 根据时间区间统计营业额* @param beginTime* @param endTime* @return*/TurnoverReportVO getTurnover(LocalDate beginTime, LocalDate endTime);
}

2.2.4 Service层实现类

创建ReportServiceImpl实现类,实现getTurnover方法:

package com.sky.service.impl;import java.util.List;
import java.util.Map;@Service
@Slf4j
public class ReportServiceImpl implements ReportService {@Autowiredprivate OrderMapper orderMapper;/*** 根据时间区间统计营业额* @param begin* @param end* @return*/public TurnoverReportVO getTurnover(LocalDate begin, LocalDate end) {List<LocalDate> dateList = new ArrayList<>();dateList.add(begin);while (!begin.equals(end)){begin = begin.plusDays(1);//日期计算,获得指定日期后1天的日期dateList.add(begin);}List<Double> turnoverList = new ArrayList<>();for (LocalDate date : dateList) {LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);//0:0LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);//23:59999999999Map map = new HashMap();map.put("status", Orders.COMPLETED);map.put("begin",beginTime);map.put("end", endTime);Double turnover = orderMapper.sumByMap(map); turnover = turnover == null ? 0.0 : turnover;//为空就返回0.0turnoverList.add(turnover);}//数据封装return TurnoverReportVO.builder().dateList(StringUtils.join(dateList,",")).turnoverList(StringUtils.join(turnoverList,",")).build();}
}

2.2.5 Mapper层

在OrderMapper接口声明sumByMap方法:

	/*** 根据动态条件统计营业额* @param map*/Double sumByMap(Map map);

在OrderMapper.xml文件中编写动态SQL:

<select id="sumByMap" resultType="java.lang.Double">select sum(amount) from orders<where><if test="status != null">and status = #{status}</if><if test="begin != null">and order_time >= #{begin}</if><if test="end != null">and order_time <= #{end}</if></where>
</select>

2.3 功能测试

可以通过如下方式进行测试:

  • 接口文档测试
  • 前后端联调测试

启动服务器,启动nginx,直接采用前后端联调测试。

进入数据统计模块

1). 查看近7日营业额统计

进入开发者模式,查看返回数据

2). 查看近30日营业额统计

进入开发者模式,查看返回数据

也可通过断点方式启动,查看每步执行情况。

2.4 代码提交

后续步骤和其它功能代码提交一致,不再赘述。

3. 用户统计

3.1 需求分析和设计

3.1.1 产品原型

所谓用户统计,实际上统计的是用户的数量。通过折线图来展示,上面这根蓝色线代表的是用户总量,下边这根绿色线代表的是新增用户数量,是具体到每一天。所以说用户统计主要统计两个数据,一个是总的用户数量,另外一个是新增用户数量

原型图:

业务规则:

  • 基于可视化报表的折线图展示用户数据,X轴为日期,Y轴为用户数
  • 根据时间选择区间,展示每天的用户总量和新增用户量数据

3.1.2 接口设计

根据上述原型图设计接口。

3.2 代码开发

3.2.1 VO设计

根据用户统计接口的返回结果设计VO:

在sky-pojo模块,UserReportVO.java已定义

package com.sky.vo;import lombok.NoArgsConstructor;
import java.io.Serializable;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserReportVO implements Serializable {//日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03private String dateList;//用户总量,以逗号分隔,例如:200,210,220private String totalUserList;//新增用户,以逗号分隔,例如:20,21,10private String newUserList;}

3.2.2 Controller层

根据接口定义,在ReportController中创建userStatistics方法:

/*** 用户数据统计* @param begin* @param end* @return*/@GetMapping("/userStatistics")@ApiOperation("用户数据统计")public Result<UserReportVO> userStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){return Result.success(reportService.getUserStatistics(begin,end));            
}

3.2.3 Service层接口

在ReportService接口中声明getUserStatistics方法:

	/*** 根据时间区间统计用户数量* @param begin* @param end* @return*/UserReportVO getUserStatistics(LocalDate begin, LocalDate end);

3.2.4 Service层实现类

在ReportServiceImpl实现类中实现getUserStatistics方法:

	@Overridepublic UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {List<LocalDate> dateList = new ArrayList<>();dateList.add(begin);while (!begin.equals(end)){begin = begin.plusDays(1);dateList.add(begin);}List<Integer> newUserList = new ArrayList<>(); //新增用户数List<Integer> totalUserList = new ArrayList<>(); //总用户数for (LocalDate date : dateList) {LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);//新增用户数量 select count(id) from user where create_time > ? and create_time < ?Integer newUser = getUserCount(beginTime, endTime);//总用户数量 select count(id) from user where  create_time < ?Integer totalUser = getUserCount(null, endTime);newUserList.add(newUser);totalUserList.add(totalUser);}return UserReportVO.builder().dateList(StringUtils.join(dateList,",")).newUserList(StringUtils.join(newUserList,",")).totalUserList(StringUtils.join(totalUserList,",")).build();}

在ReportServiceImpl实现类中创建私有方法getUserCount:

	/*** 根据时间区间统计用户数量* @param beginTime* @param endTime* @return*/private Integer getUserCount(LocalDateTime beginTime, LocalDateTime endTime) {Map map = new HashMap();map.put("begin",beginTime);map.put("end", endTime);return userMapper.countByMap(map);}

3.2.5 Mapper层

在UserMapper接口中声明countByMap方法:

	/*** 根据动态条件统计用户数量* @param map* @return*/Integer countByMap(Map map);

在UserMapper.xml文件中编写动态SQL:

<select id="countByMap" resultType="java.lang.Integer">select count(id) from user<where><if test="begin != null">and create_time >= #{begin}</if><if test="end != null">and create_time <= #{end}</if></where>
</select>

3.3 功能测试

可以通过如下方式进行测试:

  • 接口文档测试
  • 前后端联调测试

进入数据统计模块

1). 查看近7日用户统计

进入开发者模式,查看返回数据

2). 查看近30日用户统计

进入开发者模式,查看返回数据

也可通过断点方式启动,查看每步执行情况。

3.4 代码提交

后续步骤和其它功能代码提交一致,不再赘述。

4. 订单统计

4.1 需求分析和设计

4.1.1 产品原型

订单统计通过一个折现图来展现,折线图上有两根线,这根蓝色的线代表的是订单总数,而下边这根绿色的线代表的是有效订单数,指的就是状态是已完成的订单就属于有效订单,分别反映的是每一天的数据。上面还有3个数字,分别是订单总数、有效订单、订单完成率,它指的是整个时间区间之内总的数据。

原型图:

业务规则:

  • 有效订单指状态为 “已完成” 的订单
  • 基于可视化报表的折线图展示订单数据,X轴为日期,Y轴为订单数量
  • 根据时间选择区间,展示每天的订单总数和有效订单数
  • 展示所选时间区间内的有效订单数、总订单数、订单完成率,订单完成率 = 有效订单数 / 总订单数 * 100%

4.1.2 接口设计

根据上述原型图设计接口。

4.2 代码开发

4.2.1 VO设计

根据订单统计接口的返回结果设计VO:

在sky-pojo模块,OrderReportVO.java已定义

package com.sky.vo;import lombok.NoArgsConstructor;
import java.io.Serializable;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderReportVO implements Serializable {//日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03private String dateList;//每日订单数,以逗号分隔,例如:260,210,215private String orderCountList;//每日有效订单数,以逗号分隔,例如:20,21,10private String validOrderCountList;//订单总数private Integer totalOrderCount;//有效订单数private Integer validOrderCount;//订单完成率private Double orderCompletionRate;
}

4.2.2 Controller层

在ReportController中根据订单统计接口创建orderStatistics方法:

/*** 订单数据统计* @param begin* @param end* @return*/@GetMapping("/ordersStatistics")@ApiOperation("用户数据统计")public Result<OrderReportVO> orderStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate end){return Result.success(reportService.getOrderStatistics(begin,end));}

4.2.3 Service层接口

在ReportService接口中声明getOrderStatistics方法:

/**
* 根据时间区间统计订单数量
* @param begin 
* @param end
* @return 
*/
OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end);

4.2.4 Service层实现类

在ReportServiceImpl实现类中实现getOrderStatistics方法:

/**
* 根据时间区间统计订单数量
* @param begin 
* @param end
* @return 
*/
public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end){List<LocalDate> dateList = new ArrayList<>();dateList.add(begin);while (!begin.equals(end)){begin = begin.plusDays(1);dateList.add(begin);}//每天订单总数集合List<Integer> orderCountList = new ArrayList<>();//每天有效订单数集合List<Integer> validOrderCountList = new ArrayList<>();for (LocalDate date : dateList) {LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);//查询每天的总订单数 select count(id) from orders where order_time > ? and order_time < ?Integer orderCount = getOrderCount(beginTime, endTime, null);//查询每天的有效订单数 select count(id) from orders where order_time > ? and order_time < ? and status = ?Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);orderCountList.add(orderCount);validOrderCountList.add(validOrderCount);}//时间区间内的总订单数Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();//时间区间内的总有效订单数Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();//订单完成率Double orderCompletionRate = 0.0;if(totalOrderCount != 0){orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;}return OrderReportVO.builder().dateList(StringUtils.join(dateList, ",")).orderCountList(StringUtils.join(orderCountList, ",")).validOrderCountList(StringUtils.join(validOrderCountList, ",")).totalOrderCount(totalOrderCount).validOrderCount(validOrderCount).orderCompletionRate(orderCompletionRate).build();}

在ReportServiceImpl实现类中提供私有方法getOrderCount:

/**
* 根据时间区间统计指定状态的订单数量
* @param beginTime
* @param endTime
* @param status
* @return
*/
private Integer getOrderCount(LocalDateTime beginTime, LocalDateTime endTime, Integer status) {Map map = new HashMap();map.put("status", status);map.put("begin",beginTime);map.put("end", endTime);return orderMapper.countByMap(map);
}

4.2.5 Mapper层

在OrderMapper接口中声明countByMap方法:

/**
*根据动态条件统计订单数量
* @param map
*/
Integer countByMap(Map map);

在OrderMapper.xml文件中编写动态SQL:

<select id="countByMap" resultType="java.lang.Integer">select count(id) from orders<where><if test="status != null">and status = #{status}</if><if test="begin != null">and order_time >= #{begin}</if><if test="end != null">and order_time <= #{end}</if></where>
</select>

4.3 功能测试

可以通过如下方式进行测试:

  • 接口文档测试
  • 前后端联调

重启服务,直接采用前后端联调测试。

进入数据统计模块

1). 查看近7日订单统计

进入开发者模式,查看返回数据

2). 查看近30日订单统计

进入开发者模式,查看返回数据

也可通过断点方式启动,查看每步执行情况。

4.4 代码提交

后续步骤和其它功能代码提交一致,不再赘述。

5. 销量排名Top10

5.1 需求分析和设计

5.1.1 产品原型

所谓销量排名,销量指的是商品销售的数量。项目当中的商品主要包含两类:一个是套餐,一个是菜品,所以销量排名其实指的就是菜品和套餐销售的数量排名。通过柱形图来展示销量排名,这些销量是按照降序来排列,并且只需要统计销量排名前十的商品。

原型图:

业务规则:

  • 根据时间选择区间,展示销量前10的商品(包括菜品和套餐)
  • 基于可视化报表的柱状图降序展示商品销量
  • 此处的销量为商品销售的份数

5.1.2 接口设计

根据上述原型图设计接口。

5.2 代码开发

5.2.1 VO设计

根据销量排名接口的返回结果设计VO:

在sky-pojo模块,SalesTop10ReportVO.java已定义

package com.sky.vo;import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SalesTop10ReportVO implements Serializable {//商品名称列表,以逗号分隔,例如:鱼香肉丝,宫保鸡丁,水煮鱼private String nameList;//销量列表,以逗号分隔,例如:260,215,200private String numberList;
}

5.2.2 Controller层

在ReportController中根据销量排名接口创建top10方法:

/**
* 销量排名统计
* @param begin
* @param end
* @return
*/
@GetMapping("/top10")
@ApiOperation("销量排名统计")
public Result<SalesTop10ReportVO> top10(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){return Result.success(reportService.getSalesTop10(begin,end));
}

5.2.3 Service层接口

在ReportService接口中声明getSalesTop10方法:

/**
* 查询指定时间区间内的销量排名top10 
* @param begin
* @param end
* @return
*/
SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end);

5.2.4 Service层实现类

在ReportServiceImpl实现类中实现getSalesTop10方法:

/*** 查询指定时间区间内的销量排名top10* @param begin* @param end* @return* */public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end){LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);List<GoodsSalesDTO> goodsSalesDTOList = orderMapper.getSalesTop10(beginTime, endTime);String nameList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList()),",");String numberList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList()),",");return SalesTop10ReportVO.builder().nameList(nameList).numberList(numberList).build();}

5.2.5 Mapper层

在OrderMapper接口中声明getSalesTop10方法:

/**
* 查询商品销量排名
* @param begin
* @param end
*/
List<GoodsSalesDTO> getSalesTop10(LocalDateTime begin, LocalDateTime end);

在OrderMapper.xml文件中编写动态SQL:

<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">select od.name name,sum(od.number) number from order_detail od ,orders owhere od.order_id = o.idand o.status = 5<if test="begin != null">and order_time >= #{begin}</if><if test="end != null">and order_time <= #{end}</if>group by nameorder by number desclimit 0, 10
</select>

5.3 功能测试

可以通过如下方式进行测试:

  • 接口文档测试
  • 前后端联调

重启服务,直接采用前后端联调测试。

查看近30日销量排名Top10统计

若查询的某一段时间没有销量数据,则显示不出效果。

进入开发者模式,查看返回数据

也可通过断点方式启动,查看每步执行情况。

5.4 代码提交

后续步骤和其它功能代码提交一致,不再赘述。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/9629.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【chatGpt】关于websocket连接中对未授权的捕捉问题

目录 问题 有效提问 有效的细节提问 问题 一路上&#xff0c;通过简单的error进行判断弹出授权&#xff0c;会有很多乱弹的现象&#xff1a; &#xff08;1&#xff09;链路正常切换会断 &#xff08;2&#xff09;服务器没有启动会连接不上 &#xff08;3&#xff09;没…

Python pygame(GUI编程)模块最完整教程(7)

上一篇文章&#xff1a; Python pygame(GUI编程)模块最完整教程&#xff08;6&#xff09;_Python-ZZY的博客-CSDN博客 总目录&#xff1a; README.md Python-ZZY/Python-Pygame最完整教程 - Gitee.com 21 OpenGL与Pygame 不会OpenGL的读者可以跳过本章节。 21.1 OpenGL简…

CRM系统化整合从N-1做减法实践 | 京东物流技术团队

1 背景 京销易系统已经接入大网、KA以及云仓三个条线商机&#xff0c;每个条线商机规则差异比较大&#xff0c;当前现状是独立实现三套系统分别做支撑。 2 目标 2022年下半年CRM目标是完成9个新条线业务接入&#xff0c;完成销售过程线上化&#xff0c;实现销售规则统一。 …

js版计算比亚迪行驶里程连续12个月计算不超3万公里改进版带echar

<!DOCTYPE html> <html lang"zh-CN" style"height: 100%"> <head> <meta charset"utf-8" /> <title>连续12个月不超3万公里计算LIGUANGHUA</title> <style> .clocks { …

计算机组成原理问答5

CPU CPU两大核心部件&#xff1a;运算器控制器 运算器的核心&#xff1a;ALU&#xff08;算数逻辑单元&#xff09;&#xff0c;功能&#xff1a;对数据进行加工。基本结构&#xff1a;ALU、暂存寄存器、通用寄存器组、累加寄存器ACC、程序状态寄存器PSW、移位器、计数器。 …

第一次编程测试(分频器)

一&#xff0c;分频器 定义 分频器&#xff08;Divider&#xff09;是一种电子电路或设备&#xff0c;用于将输入信号的频率降低到较低的频率。它常用于数字系统、通信系统和计时应用中。原理 整数分频器使用计数器来实现频率的降低。计数器根据输入信号的边沿触发进行计数&am…

Mybatis学习笔记教程

Mybatis-9.28 环境&#xff1a; JDK1.8Mysql 5.7maven 3.6.1IDEA 回顾&#xff1a; JDBCMysqlJava基础MavenJunit SSM框架&#xff1a;配置文件的。 最好的方式&#xff1a;看官网文档&#xff1b; 1、简介 1.1、什么是Mybatis MyBatis 是一款优秀的持久层框架它支持定制…

基于Vue+Element Plus实现表格组件

目录 前言分析实现例子效果图前言 表格对于管理类项目是很重要的,可以只管的展示和比比较数据。使用Element Plus能解决一部分问题,但是还存在一些缺点和不足。 分析 浏览器上表格数据展示空间不足。列显示太多不够直观。完全依赖官方表格组件代码过于臃肿不利于管理和优化…

通过VIOOVI,了解联合作业分析的意义和目标!

现如今企业的主流生产模式就是流水线生产&#xff0c;一道工序结束后&#xff0c;紧接着开展下一项工序&#xff0c;这种作业模式可以以一种比较高效的方式缩减生产时间。尽管流水作业的效率已经够高的了&#xff0c;但是各个工序之间如果衔接不到位的话&#xff0c;会造成生产…

K8S初级入门系列之九-共享存储

一、前言 Pod里面的容器都有自己独立的文件系统&#xff0c;来自容器镜像&#xff0c;用于保存容器运行的数据&#xff0c;但容器的文件存储有两个弊端&#xff0c;一个是无法持久化&#xff0c;其生命周期与容器一致&#xff0c;一旦容器销毁&#xff0c;相关的数据也就随之一…

Hive内部表和外部表

表类型详解 表分类 在Hive中,表类型主要分为两种 第一种&#xff1a;内部表 也叫管理表表目录会创建在集群上的{hive.metastore.warehouse.dir}下的相应的库对应的目录中。默认创建的表就是内部表 第二种&#xff1a;外部表 外部表需要使用关键字"external"&#xff…

SpringBoot第8讲:SpringBoot添加Logback日志

SpringBoot第8讲&#xff1a;SpringBoot添加Logback日志 本文是SpringBoot第8讲&#xff0c;对SpringBoot添加Logback日志。SpringBoot开发中如何选用日志框架呢&#xff1f; 出于性能等原因&#xff0c;Logback 目前是springboot应用日志的标配&#xff1b; 当然有时候在生产环…

264. 丑数 II

题目描述&#xff1a; 主要思路&#xff1a; 利用动态规划的思想&#xff0c;记录2 3 5分别乘到了哪里&#xff0c;然后取最小作为新的数字。 class Solution { public:int nthUglyNumber(int n) {int dp[n1];dp[1]1;int p21,p31,p51;for(int i2;i<n;i){int num2 dp[p2]*…

说一说java中的自定义注解之设计及实现

一、需求背景 比如我们需要对系统的部分接口进行token验证&#xff0c;防止对外的接口裸奔。所以&#xff0c;在调用这类接口前&#xff0c;先校验token的合法性&#xff0c;进而得到登录用户的userId/role/authority/tenantId等信息&#xff1b;再进一步对比当前用户是否有权…

【ESP32】Espressif-IDE及ESP-IDF安装

一、下载Espressif-IDE 2.10.0 with ESP-IDF v5.0.2 1.打开ESP-IDF 编程指南 2.点击快速入门–>安装–>手动安装–>Windows Installer–>Windows Installer Download 3.点击下载Espressif-IDE 2.10.0 with ESP-IDF v5.0.2 二、安装Espressif-IDE 2.10.0 wit…

分布式I/O,IT和OT融合少不了它

长期以来信息技术IT和操作运营技术OT是相互隔离的&#xff0c;随着大数据分析和边缘计算业务的对现场级实时数据的采集需求&#xff0c;IT和OT有了逐渐融合的趋势。IT与OT融合&#xff0c;它赋予工厂的管理者监控运行和过程的能力大为增强&#xff0c;甚至可以预测到可能发生的…

TypeScript -- 类

文章目录 TypeScript -- 类TS -- 类的概念创建一个简单的ts类继承 public / private / protected-- 公共/私有/受保护的public -- 公共private -- 私有的protected -- 受保护的 其他特性readonly -- 只读属性静态属性 -- static修饰ts的getter /setter抽象类abstract TypeScrip…

KnowStreaming系列教程第三篇——调度任务模块

前一篇文章KnowStreaming系列教程第二篇——项目整体架构分析_诸葛子房_的博客-CSDN博客 讲述了KS的整体项目目录&#xff0c;这边文章来讲述下KS在调度模块里面对于指标采集和元数据同步 一、调度模块代码主要在km-task里面 public class TaskClusterAddedListener impleme…

mybatisPlus入门篇

文章目录 初窥门径1.1 初识MybatisPlus1.2 MybatisPlus的特性1.3 MybatisPlus的架构模型 入门案例2.1 准备相关开发环境2.2 搭建springboot工程2.3 创建数据库2.4 引入相关依赖2.5 创建实体类2.6 集成MybatisPlus2.7 单元测试2.8 springboot日志优化 初窥门径 1.1 初识Mybatis…

ChatGPT把python 的import和from讲明白了

文章目录 1、import&#xff1a;import关键字用于导入整个模块&#xff0c;您可以使用该模块中的所有对象。语法如下&#xff1a;2、from ... import ...&#xff1a;from ... import ... 语法用于从模块中导入特定的对象&#xff0c;而不是导入整个模块。您可以通过这种方式选…