苍穹外卖 数据可视化
将营业额、用户数据、订单数据、商品销量top10数据全部使用Apache Echarts可视化,展现在前端,后端只需要按照需要的格式,为前端提供数据即可。
ReportController
package com.sky.controller.admin;
import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.OrderReportVO;
import com.sky.vo.SalesTop10ReportVO;
import com.sky.vo.TurnoverReportVO;
import com.sky.vo.UserReportVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
@RestController
@RequestMapping("/admin/report")
@Slf4j
@Api(tags = "统计报表相关接口")
public class ReportController {
// Apache Echarts
// Apache Echarts是一个基于JavaScript的数据可视化图标库,提供直观、生动、可交互的数据可视化图表
// 无论是什么形式的图形,其本质上是数据,ApacheEcharts就是对数据的可视化展示
// 但是Apache Echarts是前端需要使用的东西,后端只需要按照和前端的约定,为其提供数据即可
@Autowired
private ReportService reportService;
/**
* 营业额数据统计
*
* @param begin
* @param end
* @return
*/
// 查询一段时间内的营业额,前端给后端传递开始时间和结束时间,后端需要查询这段时间内的营业额,并封装到VO中、
// 约定好VO中封装两个字符串,时间和对应的营业额,用","分隔
@GetMapping("/turnoverStatistics")
@ApiOperation("营业额数据统计")
// 使用@DateTimeFormat限定前端传递的时间的格式
public Result<TurnoverReportVO> turnoverStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate end) {
return Result.success(reportService.getTurnoverStatistics(begin, end));
}
/**
* 用户数据统计
*
* @param begin
* @param end
* @return
*/
// 用户数据统计分为两个部分:用户总量和新增用户;用户总量很好理解————
// 而新增用户可以理解为:假如是今天是11.11日,
// 那么在11.11日最小时间(00:00:00)————11.11日最大时间(23:59:59)创建的用户都是这一天的新用户
@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));
}
/**
* 订单数据统计
*
* @param begin
* @param end
* @return
*/
// 订单数据统计需要查询当天的所有订单和完成了的有效订单,并根据这两个数据计算出订单总数、有效订单总数、订单完成率
@GetMapping("/ordersStatistics")
@ApiOperation("订单数据统计")
public Result<OrderReportVO> ordersStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate end) {
return Result.success(reportService.getOrdersStatistics(begin, end));
}
/**
* top10畅销商品统计
*
* @param begin
* @param end
* @return
*/
@GetMapping("top10")
@ApiOperation("top10畅销商品统计")
public Result<SalesTop10ReportVO> top10Statistics(@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd")
LocalDate end) {
return Result.success(reportService.getSalesTop10Statistics(begin, end));
}
}
ReportService
package com.sky.service;
import com.sky.vo.OrderReportVO;
import com.sky.vo.SalesTop10ReportVO;
import com.sky.vo.TurnoverReportVO;
import com.sky.vo.UserReportVO;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
@Service
public interface ReportService {
/**
* 根据时间区间统计营业额数据
*
* @param begin
* @param end
* @return
*/
TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end);
/**
* 根据时间区间统计用户数量
*
* @param begin
* @param end
* @return
*/
UserReportVO getUserStatistics(LocalDate begin, LocalDate end);
/**
* 根据时间区间统计订单数据
*
* @param begin
* @param end
* @return
*/
OrderReportVO getOrdersStatistics(LocalDate begin, LocalDate end);
/**
* 根据时间区间统计畅销top10商品
*
* @param begin
* @param end
* @return
*/
SalesTop10ReportVO getSalesTop10Statistics(LocalDate begin, LocalDate end);
}
实现类
package com.sky.service.impl;
import com.sky.dto.GoodsSalesDTO;
import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.ReportService;
import com.sky.vo.OrderReportVO;
import com.sky.vo.SalesTop10ReportVO;
import com.sky.vo.TurnoverReportVO;
import com.sky.vo.UserReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
/**
* 抽取方法:根据begin————end时间区间创建日期集合
*
* @param begin
* @param end
* @return
*/
private List<LocalDate> createTimeList (LocalDate begin, LocalDate end) {
// 创建一个集合,用于存储从begin-end时间内的所有日期,用于查找对应的营业额
List<LocalDate> dateList = new ArrayList<>();
// 先加入起始日期
dateList.add(begin);
// 若还没有加入到最后一个日期,就一直加入
while (!begin.equals(end)) {
// 每次都将begin日期后延1天,直到begin = end
begin = begin.plusDays(1);
// 加入集合
dateList.add(begin);
}
return dateList;
}
/**
* 根据时间区间查询营业额数据
*
* @param begin
* @param end
* @return
*/
@Override
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
// 通过抽取的方法创建时间区间的日期集合
List<LocalDate> dateList = createTimeList(begin, end);
// 创建营业额集合,用于存储每天的营业额
List<Double> turnoverList = new ArrayList<>();
// 遍历日期集合,按照每一天进行逻辑处理
for (LocalDate date : dateList) {
// 因为表中的订单的时间是LocalDateTime类型的,所以说要将日期集合中的LocalDate封装为LocalDateTime
// 当天的营业额是大于当天的最小时间(00:00:00),小于当天的最大时间的(23:59:59),所以说可以将日期集合中的元素对应的
// 那天的beginTime设置为当天最小时间;endTime设置为当天的最大时间
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
// 用Map来封装查询的条件
Map<Object, Object> map = new HashMap<>();
// 要统计当天的营业额,只统计已经完成了的订单
map.put("status", Orders.COMPLETED);
// 封装查询的时间(时间为当天)
map.put("begin", beginTime);
map.put("end", endTime);
Double turnover = orderMapper.sumAmount(map);
// 判断当天是否有营业额,若没有营业额则turnover为空,但是这不符合前端展示的逻辑,需要对其检查,若没有营业额,那么营业额是0.0
turnover = turnover == null ? 0.0 : turnover;
// 将当前date对应的营业额加入turnover集合
turnoverList.add(turnover);
}
// 处理数据返回
// 使用StringUtils进行数据封装,封装为前端需要的格式返回。StringUtils是Apache的
return TurnoverReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.turnoverList(StringUtils.join(turnoverList, ","))
.build();
}
/**
* 根据时间区间查询用户数据
*
* @param begin
* @param end
* @return
*/
@Override
public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
// 通过抽取的方法创建时间区间的日期集合
List<LocalDate> dateList = createTimeList(begin, end);
// 新增用户集合
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);
// 总用户,只要在目标时间之前创建的用户都算是当前时间的总用户
// 建议先查询总用户数,因为查询条件更加简单
Integer totalUser = getUserCount(null, endTime);
// 新增用户可以理解为:假如是今天是11.11日,
// 那么在11.11日最小时间(00:00:00)————11.11日最大时间(23:59:59)创建的用户都是这一天的新用户
Integer newUser = getUserCount(beginTime, endTime);
// 进行前端逻辑处理,将null变为0
totalUser = totalUser == null ? 0 : totalUser;
newUser = newUser == null ? 0 : newUser;
// 加入对应集合
totalUserList.add(totalUser);
newUserList.add(newUser);
}
// 封装数据返回
return UserReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.newUserList(StringUtils.join(newUserList, ","))
.totalUserList(StringUtils.join(totalUserList, ","))
.build();
}
/**
* 根据时间区间统计用户数量
*
* @param beginTime
* @param endTime
* @return
*/
private Integer getUserCount(LocalDateTime beginTime, LocalDateTime endTime) {
// 将开始时间和结束时间封装为map再在数据库中进行查询
Map<Object, Object> map = new HashMap<>();
map.put("begin", beginTime);
map.put("end", endTime);
return userMapper.countUsersByTime(map);
}
/**
* 根据时间区间查询订单数据
*
* @param begin
* @param end
* @return
*/
@Override
public OrderReportVO getOrdersStatistics(LocalDate begin, LocalDate end) {
// 通过抽取的方法创建时间区间的日期集合
List<LocalDate> dateList = createTimeList(begin, end);
// 总订单集合
List<Integer> totalOrdersList = new ArrayList<>();
// 有效订单集合 valid adj.有效的
List<Integer> validOrdersList = new ArrayList<>();
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
// 查询总订单
Integer totalOrders = getOrdersCount(beginTime, endTime, null);
// 查询有效订单
Integer validOrders = getOrdersCount(beginTime, endTime, Orders.COMPLETED);
// 进行前端逻辑处理,将null变为0
totalOrders = totalOrders == null ? 0 : totalOrders;
// TODO 细心!细心!细心!不要再犯这种傻逼错误
validOrders = validOrders == null ? 0 : validOrders;
// 将其加入对应的集合
totalOrdersList.add(totalOrders);
validOrdersList.add(validOrders);
}
// 计算总订单数量
Integer totalOrdersCount = 0;
for (Integer order : totalOrdersList) {
totalOrdersCount += order;
}
// 计算总有效订单数量
Integer validOrdersCount = 0;
for (Integer order : validOrdersList) {
validOrdersCount += order;
}
// 计算完单率
// 如果没有订单,完单率就是0
Double orderCompletionRate = 0.0;
if (totalOrdersCount != 0) {
// 只有存在订单,才计算完单率
orderCompletionRate = validOrdersCount.doubleValue() / totalOrdersCount;
}
return OrderReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.orderCountList(StringUtils.join(totalOrdersList, ","))
.validOrderCountList(StringUtils.join(validOrdersList, ","))
.totalOrderCount(totalOrdersCount)
.validOrderCount(validOrdersCount)
.orderCompletionRate(orderCompletionRate)
.build();
}
/**
* 根据时间区间和订单状态查询订单数据
*
* @param beginTime
* @param endTime
* @param status
* @return
*/
private Integer getOrdersCount(LocalDateTime beginTime, LocalDateTime endTime, Integer status) {
// 将时间和状态封装为map进行查询
// 在SQL中先根据status查询,若status不对,那么就可以不用比对后面的属性,提高效率
Map<Object, Object> map = new HashMap<>();
map.put("status", status);
map.put("begin", beginTime);
map.put("end", endTime);
return orderMapper.statisticsOrders(map);
}
/**
* 根据时间区间统计畅销top10商品
*
* @param begin
* @param end
* @return
*/
@Override
public SalesTop10ReportVO getSalesTop10Statistics(LocalDate begin, LocalDate end) {
// 因为不需要统计每一天的销量,只需要统计在这段时间之内的销量,所以说不需要遍历每一天的销量,可以直接使用begin和end
LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);
// 查询销量前10的商品,并封装在GoodsSalesDTO中(商品名和销量)
List<GoodsSalesDTO> goodsSalesDTOList = orderMapper.getSalesTop10(beginTime, endTime);
// 处理goodsSalesDTOList中数据
String nameList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getName)
.collect(Collectors.toList()), ",");
String numberList = StringUtils.join(goodsSalesDTOList.stream().map(GoodsSalesDTO::getNumber)
.collect(Collectors.toList()), ",");
// 封装成对应的VO返回
return SalesTop10ReportVO.builder()
.nameList(nameList)
.numberList(numberList)
.build();
}
}
Mapper相对简单,这里不过多赘述。