苍穹外卖 —— 数据统计和使用Apache_POI库导出Excel报表

一、前言

这是苍穹外卖后端部分最后的一部分内容了,数据统计的难点在于持久层,对于图表的接口依旧看文档写即可,这部分主要还是前端的工作,而报表导出会采用到一个新的库: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 &gt; #{begin}
            </if>
            <if test="end != null">
                and order_time &lt; #{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),这里注释写得很详细,就不过多赘述了。

相关推荐
q***01653 小时前
【保姆级教程】apache-tomcat的安装配置教程
java·tomcat·apache
申阳3 小时前
Day 12:09. 基于Nuxt开发博客项目-使用NuxtContent构建博客模块
前端·后端·程序员
得物技术3 小时前
Golang HTTP请求超时与重试:构建高可靠网络请求|得物技术
java·后端·go
合作小小程序员小小店3 小时前
web网页开发,在线短视频管理系统,基于Idea,html,css,jQuery,java,springboot,mysql。
java·前端·spring boot·mysql·vue·intellij-idea
信码由缰3 小时前
Java 缓存精要
后端
朝新_4 小时前
Spring事务和事务传播机制
数据库·后端·sql·spring·javaee
通往曙光的路上4 小时前
SpringMVC基础
spring boot
培风图楠4 小时前
Java个人学习笔记
java·笔记·学习
梅梅绵绵冰4 小时前
SpringMVC的配置响应-页面跳转,回写数据
java
A***27954 小时前
后端服务限流配置,Spring Cloud Gateway
java·运维·数据库