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

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

2.Controller
这里我们需要返回一个OrderReportVO,然后接收两个日期参数,所以我们这里使用 @DateTimeFormat(pattern = "yyyy-MM-dd")注解来规定参数格式,便于后续字符串拼接。
/*** 订单统计** @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层
接口:
/*** 订单统计* @param begin* @param end* @return*/OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end);实现类:这个就比较复杂了,我们慢慢来解析。
/*** 订单统计** @param begin* @param end* @return*/@Overridepublic 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 = 5Integer 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:
/*** 根据动态条件统计订单数量** @param map* @return*/Integer countByMap(Map map);映射文件:
<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对象,然后获取每一行的对象,最后获取每个单元个格的对象,我们在单元格中写入想写的数据即可。
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异常简单。
/*** 导出Excel报表** @param response*/@GetMapping("/export")@ApiOperation("导出Excel报表")public void export(HttpServletResponse response) {log.info("导出Excel报表");reportService.exportBusinessData(response);}2.Service层
接口:
/*** 导出Excel报表* @param response*/void exportBusinessData(HttpServletResponse response);实现类:
@Overridepublic 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),这里注释写得很详细,就不过多赘述了。
