一、前言
这是苍穹外卖后端部分最后的一部分内容了,数据统计的难点在于持久层,对于图表的接口依旧看文档写即可,这部分主要还是前端的工作,而报表导出会采用到一个新的库:Apache_POI。
二、数据统计
数据统计我们分为四个表来统计,前面三个表都是以日期作为横坐标,具体值作为纵坐标,所以具体步骤非常相似,我们这里选择订单统计来作为示例详细讲解,因为他最复杂。

1.文档
可以看到请求参数是开始和结束的日期,用的Query参数,要求我们返回一个OrderReportVO即可,所以我们的主要任务还是在持久层查询响应的属性,值得注意的是这几个属性都是String类型的,需要我们用逗号分隔拼接成一个长字符串。

2.Controller
这里我们需要返回一个OrderReportVO ,然后接收两个日期参数,所以我们这里使用 **@DateTimeFormat(pattern = "yyyy-MM-dd")**注解来规定参数格式,便于后续字符串拼接。
java
/**
* 订单统计
*
* @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) {
log.info("订单统计:{},{}", begin, end);
OrderReportVO orderReportVO = reportService.getOrderStatistics(begin, end);
return Result.success(orderReportVO);
}
3.Service层
接口:
java
/**
* 订单统计
* @param begin
* @param end
* @return
*/
OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end);
**实现类:**这个就比较复杂了,我们慢慢来解析。
java
/**
* 订单统计
*
* @param begin
* @param end
* @return
*/
@Override
public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
//当前集合用于存放从begin到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<>();
//遍历dateList集合,查询每天的有效订单数和订单总数
for (LocalDate date : dateList) {
//查询每天的订单总数 select count(id) from orders where order_time and order_time < ?
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
Integer orderCount = getOrderCount(beginTime, endTime, null);
//查询每天的有效订单数 select count(id) from orders where order_time and order_time < ? and status = 5
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();
}
/**
* 根据条件统计订单数量
*
* @param begin
* @param end
* @param status
* @return
*/
private Integer getOrderCount(LocalDateTime begin, LocalDateTime end, Integer status) {
Map map = new HashMap();
map.put("begin", begin);
map.put("end", end);
map.put("status", status);
return orderMapper.countByMap(map);
}
首先看第一个属性dateList,我们通过日期的加减,可以获得每天的日期,尽管最后是需要传回一个字符串,但是我们这里还是选择先存到一个日期列表中去(后面可以用工具类一次性转化为长字符串)。
**第二个属性orderCountList(每天的订单数)**我们将通过从持久层查询得到,我们这里是通过一个Map来向持久层查找数据的,相当于查询的参数是:1.开始日期 2.结束日期 3.订单状态。
传入的是开始日期的刚开始的时间,比如我想传入11月16日,那么我在这里就会传入11月16日0时0分000000001秒 ,儿结束日期就是最后快结束的时间,比如11月16日23时59分59.9999999秒。我们将这个步骤单独封装成了一个private方法便于后续复用。
而对于第三个属性validOrderCountList(每天有效的订单数),我们当然只统计完成了的订单,派送中的未接单的我们不会统计,所以要求订单的状态是Orders.COMPLETED。
第四个属性orderCompletionRate 就很简单了,订单完成率 = 有效订单数 / 总订单数。
至于总订单数 和总有效订单数 ,我们就用流处理。
orderCountList:存储了每天订单数量的列表(如每天的订单数分别为 10、20、30 等)。stream():将列表转换为流,以便进行函数式操作。reduce(Integer::sum):reduce是流的归约操作,用于将流中的元素合并为一个结果。Integer::sum是方法引用,等价于 **(a, b) -> a + b,**表示对两个整数进行求和。- 该操作会依次将流中的元素累加,最终得到所有天的订单数量总和。
get(): 因为reduce返回的是Optional<Integer>(防止流为空时出现异常),这里通过get()获取最终的整数值。
最后我们创建一个**OrderReportVO,**用工具类将集合转化为长字符串
4.持久层
Mapper:
java
/**
* 根据动态条件统计订单数量
*
* @param map
* @return
*/
Integer countByMap(Map map);
映射文件:
XML
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from orders
<where>
<if test="begin != null">
and order_time > #{begin}
</if>
<if test="end != null">
and order_time < #{end}
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>
三、Apache_POI快速入门
POI总的来说就是获取一个Excel对象,然后获取每一行的对象,最后获取每个单元个格的对象,我们在单元格中写入想写的数据即可。
java
public class POITest {
/**
* 通过POI创建Excel文件并且写入文件内容
*/
public static void write() throws Exception {
//在内存中创建一个Excel文件
XSSFWorkbook excel = new XSSFWorkbook();
//在Excel文件中创建一个Sheet页
XSSFSheet sheet = excel.createSheet("info");
//在sheet页中创建行对象,rownumber是从0开始
XSSFRow row = sheet.createRow(1);
//创建单元格并写入内容
row.createCell(1).setCellValue("姓名");
row.createCell(2).setCellValue("城市");
//创建一个新行
row = sheet.createRow(2);
//创建单元格并写入内容
row.createCell(1).setCellValue("印东升");
row.createCell(2).setCellValue("成都");
//创建一个新行
row = sheet.createRow(3);
//创建单元格并写入内容
row.createCell(1).setCellValue("张宇");
row.createCell(2).setCellValue("万州");
//通过输出流将内存中的Excel文件写入到磁盘
FileOutputStream out = new FileOutputStream(new File("D:\\info.xlsx"));
excel.write(out);
excel.close();
out.close();
}
public static void main(String[] args) throws Exception {
//write();
read();
}
/**
* 通过POI读取Excel文件
*
* @throws Exception
*/
public static void read() throws Exception {
FileInputStream in = new FileInputStream(new File("D:\\info.xlsx"));
//读取磁盘中已经存在的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
//读取Excel文件中的第一个Sheet页
XSSFSheet sheet = excel.getSheet("info");
//获取sheet页中最后一行的行号
int lastRowNum = sheet.getLastRowNum();
for (int i = 1; i <= lastRowNum; i++) {
//获得某一行
XSSFRow row = sheet.getRow(i);
//获得单元格对象
String cellValue1 = row.getCell(1).getStringCellValue();
String cellValue2 = row.getCell(2).getStringCellValue();
System.out.println(cellValue1 + " " + cellValue2);
}
//关闭资源
excel.close();
in.close();
}
}
四、导出Excel报表
这里一个用于将数据导出为Excel的库。我们这里就不写快速入门了,直接边写功能边讲。
1.文档
这里是不需要参数的,也不需要返回响应。

2.Controller
由于参数和响应都没有,所以Controller异常简单。
java
/**
* 导出Excel报表
*
* @param response
*/
@GetMapping("/export")
@ApiOperation("导出Excel报表")
public void export(HttpServletResponse response) {
log.info("导出Excel报表");
reportService.exportBusinessData(response);
}
2.Service层
接口:
java
/**
* 导出Excel报表
* @param response
*/
void exportBusinessData(HttpServletResponse response);
实现类:
java
@Override
public void exportBusinessData(HttpServletResponse response) {
//1.查询数据库,获取营业数据---30天
LocalDate dataBegin = LocalDate.now().minusDays(30);
LocalDate dataEnd = LocalDate.now().minusDays(1);
LocalDateTime begin = LocalDateTime.of(dataBegin, LocalTime.MIN);
LocalDateTime end = LocalDateTime.of(dataEnd, LocalTime.MIN);
BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin, end);
//2.通过POI将数据写入到excel文件中
InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
try {
//基于模板文件创建一个新的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
//获取表格文件Sheet页
XSSFSheet sheet = excel.getSheet("Sheet1");
//填充数据--时间
sheet.getRow(1).getCell(1).setCellValue("时间:" + dataBegin + " 至 " + dataEnd);
XSSFRow row = sheet.getRow(3);
row.getCell(2).setCellValue(businessDataVO.getTurnover());
row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
row.getCell(6).setCellValue(businessDataVO.getNewUsers());
row = sheet.getRow(4);
row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
row.getCell(4).setCellValue(businessDataVO.getUnitPrice());
//填充明细数据
for (int i = 0; i < 30; i++) {
//查询某一天的营业数据
LocalDate date = dataBegin.plusDays(i);
LocalDateTime beginDate = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endDate = LocalDateTime.of(date, LocalTime.MAX);
BusinessDataVO businessDataVO2 = workspaceService.getBusinessData(beginDate, endDate);
row = sheet.getRow(i+7);
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(businessDataVO2.getTurnover());
row.getCell(3).setCellValue(businessDataVO2.getValidOrderCount());
row.getCell(4).setCellValue(businessDataVO2.getOrderCompletionRate());
row.getCell(5).setCellValue(businessDataVO2.getUnitPrice());
row.getCell(6).setCellValue(businessDataVO2.getNewUsers());
}
//3.通过输出流将Excel文件下载到客户端浏览器
ServletOutputStream out = response.getOutputStream();
excel.write(out);
//关闭资源
out.close();
excel.close();
in.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
这里需要传回一个响应参数(类似Servlet),让浏览器下载报表。
如果想导入一个报表模板就需要InputStream,当然,想下载下来就用一个OutputStream就行了(ServletOutputStream),这里注释写得很详细,就不过多赘述了。