今日首先介绍前端技术Apache ECharts,说明后端需要准备的数据,然后讲解具体统计功能的实现,包括营业额统计、用户统计、订单统计、销量排名。
一、ECharts
是什么
ECharts是一款基于 Javascript 的数据可视化图表库。我们用它来展示图表数据。
入门案例
步骤
1). 引入echarts.js 文件
2). 为 ECharts 准备一个设置宽高的 DOM
3). 初始化echarts实例
4). 指定图表的配置项和数据
5). 使用指定的配置项和数据显示图表
代码
js文件在黑马对应项目自取。
测试用的html代码:
<!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: '班级出勤人数'},tooltip: {},legend: {data: ['人数']},xAxis: {type: 'category',data: ['星期1', '星期2', '星期3', '星期4', '星期5']},yAxis: {type: 'value'},series: [{name: '人数',type: 'line',data: [160, 71, 66, 73, 68],smooth: true}]};// 使用刚指定的配置项和数据显示图表。myChart.setOption(option);</script></body>
</html>
结果页面如下:
然后我们打开ECharts官网Apache ECharts 选择一个图案来试着改一下。
首先进入官网,点击所有示例。
然后点击一个自己喜欢的样式:
复制左边的代码到原代码的option位置:
复制后代码如下:
<!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: 'Stacked Area Chart'},tooltip: {trigger: 'axis',axisPointer: {type: 'cross',label: {backgroundColor: '#6a7985'}}},legend: {data: ['Email', 'Union Ads', 'Video Ads', 'Direct', 'Search Engine']},toolbox: {feature: {saveAsImage: {}}},grid: {left: '3%',right: '4%',bottom: '3%',containLabel: true},xAxis: [{type: 'category',boundaryGap: false,data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']}],yAxis: [{type: 'value'}],series: [{name: 'Email',type: 'line',stack: 'Total',areaStyle: {},emphasis: {focus: 'series'},data: [120, 132, 101, 134, 90, 230, 210]},{name: 'Union Ads',type: 'line',stack: 'Total',areaStyle: {},emphasis: {focus: 'series'},data: [220, 182, 191, 234, 290, 330, 310]},{name: 'Video Ads',type: 'line',stack: 'Total',areaStyle: {},emphasis: {focus: 'series'},data: [150, 232, 201, 154, 190, 330, 410]},{name: 'Direct',type: 'line',stack: 'Total',areaStyle: {},emphasis: {focus: 'series'},data: [320, 332, 301, 334, 390, 330, 320]},{name: 'Search Engine',type: 'line',stack: 'Total',label: {show: true,position: 'top'},areaStyle: {},emphasis: {focus: 'series'},data: [820, 932, 901, 934, 1290, 1330, 1320]}]
};// 使用刚指定的配置项和数据显示图表。myChart.setOption(option);</script></body>
</html>
页面展示结果如下:
总结
传输的两列数据,分别是下标和数据。在如下位置:
二、营业额统计
查看接口文档
分析
控制层
控制层只要接收数据传给业务层,返回VO(已经有设计好的TurnoverReportVO了)就可以。重点在业务层和持久层。
业务层
具体要处理得到两类,或者说两列数据,包括:
- 日期列表
- 营业额列表
所以步骤便是:
- 获取日期列表
- 获取日期对应的营业额的列表
- 封装返回
持久层
那么持久层需要的操作就在第2步,即:
- 根据日期查找当日营业额
之后几个案例都是大差不差的层次结构,除了数据的种类要求不同。
具体代码
控制层
@RestController
@Slf4j
@Api(tags = "统计相关")
@RequestMapping("/admin/report")
public class ReportController {@Autowiredprivate ReportService reportService;@GetMapping("/turnoverStatistics")public Result turnoverStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {TurnoverReportVO turnoverReportVO = reportService.turnoverStatistics(begin, end);return Result.success(turnoverReportVO);}
}
业务层
@Service
public class ReportServiceImpl implements ReportService {@Autowiredprivate OrdersMapper ordersMapper;@Overridepublic TurnoverReportVO turnoverStatistics(LocalDate begin, LocalDate end) {// 1. 获取日期列表List<LocalDate> list = getDateList(begin, end);// 2. 查询每日营业额List<Double> result = new ArrayList<>();Double turnover;LocalDateTime dayBegin;LocalDateTime dayEnd;if (list != null && list.size() > 0) {dayBegin = LocalDateTime.of(list.get(0), LocalTime.MIN); // 知识点2和3dayEnd = LocalDateTime.of(list.get(0), LocalTime.MAX); // 知识点2和3} else {return new TurnoverReportVO();}for (LocalDate localDate : list) {Map<String, Object> map = new HashMap<>();map.put("status", Orders.COMPLETED);map.put("begin", dayBegin);map.put("end", dayEnd);turnover = ordersMapper.sumByMap(map); // 知识点4result.add(turnover == null ? 0 : turnover);dayBegin = dayBegin.plusDays(1);dayEnd = dayEnd.plusDays(1);}// 3. 返回TurnoverReportVO turnoverReportVO = new TurnoverReportVO();turnoverReportVO.setDateList(StringUtils.join(list, ","));turnoverReportVO.setTurnoverList(StringUtils.join(result, ","));return turnoverReportVO;}private List<LocalDate> getDateList(LocalDate begin, LocalDate end) {List<LocalDate> list = new ArrayList<>();while (begin.compareTo(end) <= 0) { // 知识点1list.add(begin);begin = begin.plusDays(1);}return list;}
}
4个知识点
这里体现了4个知识点:
- 日期之间用compareTo比较
- LocalDate和LocalTime组合成LocalDateTime,用LocalDateTime的of方法
- LocalTime.MIN与LocalTime.MAX
- 用Map封装数据交给mapper查找。
持久层
直接上xml文件了:
elect id="sumByMap" resultType="java.lang.Double">select sum(amount)from orders<where><if test="status!=null and status!=''">status = #{status}</if><if test="begin!=null and end!=null">and order_time between #{begin} and #{end}</if></where>
</select>
三、用户统计
接口文档
分析所需数据
由于三层的架构都大差不差,所以直接介绍所需数据的不同。
- 日期列表
- 新增用户数列表
- 总用户数列表
具体代码
控制层
@GetMapping("/userStatistics")
public Result userStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {UserReportVO userReportVO = reportService.userStatistics(begin, end);return Result.success(userReportVO);
}
业务层
@Override
public UserReportVO userStatistics(LocalDate begin, LocalDate end) {// 1. 获取日期列表List<LocalDate> dateList = getDateList(begin, end);// 2. 获取用户数量列表List<Integer> newUserList = new ArrayList<>();List<Integer> totalUserList = new ArrayList<>();LocalDateTime dayBegin;LocalDateTime dayEnd;if (dateList != null && dateList.size() > 0) {dayBegin = LocalDateTime.of(dateList.get(0), LocalTime.MIN);dayEnd = LocalDateTime.of(dateList.get(0), LocalTime.MAX);} else {return new UserReportVO();}Integer totalUser;Integer newUser;for (LocalDate localDate : dateList) {Map<String, Object> map = new HashMap<>();map.put("end", dayEnd);totalUser = userMapper.countByMap(map);totalUserList.add(totalUser == null ? 0 : totalUser);map.put("begin", dayBegin);newUser = userMapper.countByMap(map);newUserList.add(newUser == null ? 0 : newUser);dayBegin = dayBegin.plusDays(1);dayEnd = dayEnd.plusDays(1);}// 3. 返回UserReportVO userReportVO = new UserReportVO();userReportVO.setDateList(StringUtils.join(dateList, ","));userReportVO.setNewUserList(StringUtils.join(newUserList, ","));userReportVO.setTotalUserList(StringUtils.join(totalUserList, ","));return userReportVO;
}
3个注意的点
第一点,获取日期列表可以抽取出来,供营业额统计、用户统计共同调用。
小tips,抽取函数的快捷键是 ctrl + alt + m 哦。
第二点,持久层的两次查找,可以巧妙的用一个函数来完成的。用动态sql的if判断,分为有begin时间的判断和没有begin时间的判断进行处理。具体看下面持久层代码。
第三点,两个统计都要判断持久层查到的结果是不是null,是的话要归为0哦。
持久层
<select id="countByMap" resultType="java.lang.Integer">select count(*) from user<where><if test="end!=null">and create_time <= #{end}</if><if test="begin!=null">and create_time >= #{begin}</if></where>
</select>
四、订单统计
接口文档
所需数据
- 日期列表
- 所有订单每日总数列表
- 所有订单总数
- 有效订单每日总数列表
- 有效订单总数
- 订单完成率
具体代码
控制层
@GetMapping("/ordersStatistics")
public Result ordersStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {OrderReportVO orderReportVO = reportService.ordersStatistics(begin, end);return Result.success(orderReportVO);
}
业务层
@Override
public OrderReportVO ordersStatistics(LocalDate begin, LocalDate end) {OrderReportVO orderReportVO = new OrderReportVO();// 1. 日期列表List<LocalDate> dateList = getDateList(begin, end);if (dateList == null) {return orderReportVO;}// 2. 订单数列表List<Integer> totalOrderList = new ArrayList<>();// 3. 有效订单数列表List<Integer> validOrderList = new ArrayList<>();// 4. 订单总数Integer totalOrderCount = 0;// 5. 有效订单总数Integer validOrderCount = 0;for (LocalDate localDate : dateList) {Map map = new HashMap();map.put("begin", LocalDateTime.of(localDate, LocalTime.MIN));map.put("end", LocalDateTime.of(localDate, LocalTime.MAX));Integer total = ordersMapper.countByMap(map);total = total == null ? 0 : total;map.put("status", Orders.COMPLETED);Integer valid = ordersMapper.countByMap(map);valid = valid == null ? 0 : valid;totalOrderList.add(total);validOrderList.add(valid);totalOrderCount += total;validOrderCount += valid;}// 6. 订单完成率Double completionR = 0.0;if (totalOrderCount != null) {completionR = validOrderCount * 1.0 / totalOrderCount;}orderReportVO.setDateList(StringUtils.join(dateList, ","));orderReportVO.setOrderCountList(StringUtils.join(totalOrderList, ","));orderReportVO.setValidOrderCountList(StringUtils.join(validOrderList, ","));orderReportVO.setTotalOrderCount(totalOrderCount);orderReportVO.setValidOrderCount(validOrderCount);orderReportVO.setOrderCompletionRate(completionR);return orderReportVO;
}
1个注意的巩固知识点
还是用动态sql来巧妙的满足一个函数查询两种不同的数据,即status的if判断是否查询。
持久层
<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>
没啥好说的,算一个巩固练习。
五、销量排名top10
接口文档
所需数据
- 商品名列表
- 销量列表
具体代码
控制层
@GetMapping("/top10")
public Result top10(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {SalesTop10ReportVO salesTop10ReportVO = reportService.top10(begin, end);return Result.success(salesTop10ReportVO);
}
业务层
@Override
public SalesTop10ReportVO top10(LocalDate begin, LocalDate end) {List<GoodsSalesDTO> goodsSalesDTOS = ordersMapper.countSaleTop10(LocalDateTime.of(begin, LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX));if (goodsSalesDTOS == null) {return new SalesTop10ReportVO();}List<String> nameList = new ArrayList<>();List<Integer> numberList = new ArrayList<>();for (GoodsSalesDTO goodsSalesDTO : goodsSalesDTOS) {nameList.add(goodsSalesDTO.getName());numberList.add(goodsSalesDTO.getNumber());} // 思考:这里可不可以简写?SalesTop10ReportVO salesTop10ReportVO = new SalesTop10ReportVO();salesTop10ReportVO.setNameList(StringUtils.join(nameList, ","));salesTop10ReportVO.setNumberList(StringUtils.join(numberList, ","));return salesTop10ReportVO;
}
2个注意的点
第一个,下面持久层的多表查询。
第二个,查询到DTO后,对象数据到两列数据的转换。
- 法一,普通方法,老老实实用两个List添加。
- 法二,流方法。值得练习,公司中可能会见到、用到很多,资深程序员必备。
练习:用流的写法完成查询数据到两个列表数据的转换
尝试用流的写法完成。
答案如下:
@Override
public SalesTop10ReportVO top10(LocalDate begin, LocalDate end) {List<GoodsSalesDTO> goodsSalesDTOS = ordersMapper.countSaleTop10(LocalDateTime.of(begin, LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX));if (goodsSalesDTOS == null) {return new SalesTop10ReportVO();}// List<String> nameList = new ArrayList<>();
// List<Integer> numberList = new ArrayList<>();
// for (GoodsSalesDTO goodsSalesDTO : goodsSalesDTOS) {
// nameList.add(goodsSalesDTO.getName());
// numberList.add(goodsSalesDTO.getNumber());
// }// ==========注意这里的写法==========List<String> nameList = goodsSalesDTOS.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());List<Integer> numberList = goodsSalesDTOS.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());// ==========注意上面的写法==========SalesTop10ReportVO salesTop10ReportVO = new SalesTop10ReportVO();salesTop10ReportVO.setNameList(StringUtils.join(nameList, ","));salesTop10ReportVO.setNumberList(StringUtils.join(numberList, ","));return salesTop10ReportVO;
}
持久层
<select id="countSaleTop10" resultType="com.sky.dto.GoodsSalesDTO">select t2.name, sum(t2.number) as numberfrom orders as t1inner join order_detail as t2on t1.id = t2.order_idwhere t1.status = 5and t1.order_time >= #{begin}and t1.order_time <= #{end}group by t2.nameorder by number desc limit 0, 10;
</select>
复习
1.ECharts最少需要准备几列数据?
2.LocalDateTime的比较,以及比较接口讲解的复习
3.日期时间的拼接、时间在一天的最大、最小值
4.Map封装数据进行查找的代码手法
5.统计中,持久层查询为null的归0化处理;
6.查找增量与总量时的简写mapper查询。