- 工作台
- Apache POI
- 导出运营数据Excel报表
产品原型
工作台是系统运营的数据看板,并提供快捷操作入口,可以有效提高商家的工作效率。
工作台展示的数据:
- 今日数据
- 订单管理
- 菜品总览
- 套餐总览
- 订单信息
名词解释:
- 营业额:已完成订单的总金额
- 有效订单:已完成订单的数量
- 订单完成率:有效订单数 / 总订单数 * 100%
- 平均客单价:营业额 / 有效订单数
- 新增用户:新增用户的数量
接口设计:
- 今日数据接口
- 订单管理接口
- 菜品总览接口
- 套餐总览接口
- 订单搜索(已完成)
- 各个状态的订单数量统计(已完成)
Controller层
WorkSpaceController.java
package com.sky.controller.admin; import com.sky.result.Result; import com.sky.service.WorkspaceService; import com.sky.vo.BusinessDataVO; import com.sky.vo.DishOverViewVO; import com.sky.vo.OrderOverViewVO; import com.sky.vo.SetmealOverViewVO; 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.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime; import java.time.LocalTime; /** * 工作台 */ @RestController @RequestMapping("/admin/workspace") @Slf4j @Api(tags = "工作台相关接口") public class WorkSpaceController { @Autowired private WorkspaceService workspaceService; /** * 工作台今日数据查询 * @return */ @GetMapping("/businessData") @ApiOperation("工作台今日数据查询") public Result<BusinessDataVO> businessData(){ //获得当天的开始时间 LocalDateTime begin = LocalDateTime.now().with(LocalTime.MIN); //获得当天的结束时间 LocalDateTime end = LocalDateTime.now().with(LocalTime.MAX); BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin, end); return Result.success(businessDataVO); } /** * 查询订单管理数据 * @return */ @GetMapping("/overviewOrders") @ApiOperation("查询订单管理数据") public Result<OrderOverViewVO> orderOverView(){ return Result.success(workspaceService.getOrderOverView()); } /** * 查询菜品总览 * @return */ @GetMapping("/overviewDishes") @ApiOperation("查询菜品总览") public Result<DishOverViewVO> dishOverView(){ return Result.success(workspaceService.getDishOverView()); } /** * 查询套餐总览 * @return */ @GetMapping("/overviewSetmeals") @ApiOperation("查询套餐总览") public Result<SetmealOverViewVO> setmealOverView(){ return Result.success(workspaceService.getSetmealOverView()); } }
Service层接口
WorkspaceService.java
package com.sky.service; import com.sky.vo.BusinessDataVO; import com.sky.vo.DishOverViewVO; import com.sky.vo.OrderOverViewVO; import com.sky.vo.SetmealOverViewVO; import java.time.LocalDateTime; public interface WorkspaceService { /** * 根据时间段统计营业数据 * @param begin * @param end * @return */ BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end); /** * 查询订单管理数据 * @return */ OrderOverViewVO getOrderOverView(); /** * 查询菜品总览 * @return */ DishOverViewVO getDishOverView(); /** * 查询套餐总览 * @return */ SetmealOverViewVO getSetmealOverView(); }
Service层实现类
WorkspaceServiceImpl.java
package com.sky.service.impl; import com.sky.constant.StatusConstant; import com.sky.entity.Orders; import com.sky.mapper.DishMapper; import com.sky.mapper.OrderMapper; import com.sky.mapper.SetmealMapper; import com.sky.mapper.UserMapper; import com.sky.service.WorkspaceService; import com.sky.vo.BusinessDataVO; import com.sky.vo.DishOverViewVO; import com.sky.vo.OrderOverViewVO; import com.sky.vo.SetmealOverViewVO; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.HashMap; import java.util.Map; @Service @Slf4j public class WorkspaceServiceImpl implements WorkspaceService { @Autowired private OrderMapper orderMapper; @Autowired private UserMapper userMapper; @Autowired private DishMapper dishMapper; @Autowired private SetmealMapper setmealMapper; /** * 根据时间段统计营业数据 * @param begin * @param end * @return */ public BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end) { /** * 营业额:当日已完成订单的总金额 * 有效订单:当日已完成订单的数量 * 订单完成率:有效订单数 / 总订单数 * 平均客单价:营业额 / 有效订单数 * 新增用户:当日新增用户的数量 */ Map map = new HashMap(); map.put("begin",begin); map.put("end",end); //查询总订单数 Integer totalOrderCount = processData(map); map.put("status", Orders.COMPLETED); //营业额 Double turnover = 0.0; Optional<Double> optional = orderMapper.sumByMap(map).stream().map(TurnoverDTO::getTurnover).reduce(Double::sum); if (optional.isPresent()){//查询出来有值,给营业额重新置值 turnover = optional.get(); } //有效订单数 Integer validOrderCount = processData(map); //平均客单价 Double unitPrice = 0.0; //订单完成率 Double orderCompletionRate = 0.0; if(totalOrderCount != 0 && validOrderCount != 0){ //订单完成率 orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount; //平均客单价 unitPrice = turnover / validOrderCount; } //新增用户数 Integer count = 0; List<UserCountDTO> userCountDTOList = userMapper.countNewUserByMap(map); if (!CollectionUtils.isEmpty(userCountDTOList)){ //查询的userCountDTOList不为空,则optional有值可以直接调用get count = userCountDTOList.stream().map(UserCountDTO::getUserCount).reduce(Integer::sum).get(); } Integer newUsers = count; return BusinessDataVO.builder() .turnover(turnover) .validOrderCount(validOrderCount) .orderCompletionRate(orderCompletionRate) .unitPrice(unitPrice) .newUsers(newUsers) .build(); } /** * 查询日期范围内的订单数 * @param map * @return */ public Integer processData(Map map){ Integer count = 0; List<OrderCountDTO> orderCountDTOList = orderMapper.countByMap(map); if (!CollectionUtils.isEmpty(orderCountDTOList)){ //查询的有值optional可以直接调用get count = orderCountDTOList.stream().map(OrderCountDTO::getOrderCount).reduce(Integer::sum).get(); } return count; } /** * 查询订单管理数据 * * @return */ public OrderOverViewVO getOrderOverView() { Map map = new HashMap(); map.put("begin", LocalDateTime.now().with(LocalTime.MIN)); map.put("status", Orders.TO_BE_CONFIRMED); //待接单 Integer waitingOrders = processData(map); //待派送 map.put("status", Orders.CONFIRMED); Integer deliveredOrders = processData(map); //已完成 map.put("status", Orders.COMPLETED); Integer completedOrders = processData(map); //已取消 map.put("status", Orders.CANCELLED); Integer cancelledOrders = processData(map); //全部订单 map.put("status", null); Integer allOrders = processData(map); return OrderOverViewVO.builder() .waitingOrders(waitingOrders) .deliveredOrders(deliveredOrders) .completedOrders(completedOrders) .cancelledOrders(cancelledOrders) .allOrders(allOrders) .build(); } /** * 查询菜品总览 * * @return */ public DishOverViewVO getDishOverView() { Map map = new HashMap(); map.put("status", StatusConstant.ENABLE); Integer sold = dishMapper.countByMap(map); map.put("status", StatusConstant.DISABLE); Integer discontinued = dishMapper.countByMap(map); return DishOverViewVO.builder() .sold(sold) .discontinued(discontinued) .build(); } /** * 查询套餐总览 * * @return */ public SetmealOverViewVO getSetmealOverView() { Map map = new HashMap(); map.put("status", StatusConstant.ENABLE); Integer sold = setmealMapper.countByMap(map); map.put("status", StatusConstant.DISABLE); Integer discontinued = setmealMapper.countByMap(map); return SetmealOverViewVO.builder() .sold(sold) .discontinued(discontinued) .build(); } }
Mapper层
在SetmealMapper中添加countByMap方法定义
/** * 根据条件统计套餐数量 * @param map * @return */ Integer countByMap(Map map);
在SetmealMapper.xml中添加对应SQL实现
<select id="countByMap" resultType="java.lang.Integer"> select count(id) from setmeal <where> <if test="status != null"> and status = #{status} if> <if test="categoryId != null"> and category_id = #{categoryId} if> where> select>
在DishMapper中添加countByMap方法定义
/** * 根据条件统计菜品数量 * @param map * @return */ Integer countByMap(Map map);
在DishMapper.xml中添加对应SQL实现
<select id="countByMap" resultType="java.lang.Integer"> select count(id) from dish <where> <if test="status != null"> and status = #{status} if> <if test="categoryId != null"> and category_id = #{categoryId} if> where> select>
可以通过如下方式进行测试:
- 通过接口文档测试
- 前后端联调测试
接下来我们使用上述两种方式分别测试。
接口文档测试
启动服务,访问http://localhost:8080/doc.html,进入工作台相关接口
**注意:**使用admin用户登录重新获取token,在全局参数设置中添加,防止token失效。
前后端联调测试
commit—>describe—>push
Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对Miscrosoft Office各种文件进行读写操作。
一般情况下,POI 都是用于操作 Excel 文件。
Apache POI 的应用场景:
Apache POI既可以将数据写入Excel文件,也可以读取Excel文件中的数据,接下来分别进行实现。
Apache POI的maven坐标:(项目中已导入)
<dependency> <groupId>org.apache.poigroupId> <artifactId>poiartifactId> <version>3.16version> dependency> <dependency> <groupId>org.apache.poigroupId> <artifactId>poi-ooxmlartifactId> <version>3.16version> dependency>
1). 代码开发
package com.sky.test; import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; public class POITest { /** * 基于POI向Excel文件写入数据 * @throws Exception */ public static void testWrite() throws Exception{ //在内存中创建一个Excel文件对象 XSSFWorkbook excel = new XSSFWorkbook(); //创建Sheet页 XSSFSheet sheet = excel.createSheet("yishooo"); //在Sheet页中创建行,0表示第1行 XSSFRow row1 = sheet.createRow(0); //创建单元格并在单元格中设置值,单元格编号也是从0开始,1表示第2个单元格 row1.createCell(1).setCellValue("姓名"); row1.createCell(2).setCellValue("城市"); XSSFRow row2 = sheet.createRow(1); row2.createCell(1).setCellValue("张三"); row2.createCell(2).setCellValue("北京"); XSSFRow row3 = sheet.createRow(2); row3.createCell(1).setCellValue("李四"); row3.createCell(2).setCellValue("上海"); FileOutputStream out = new FileOutputStream(new File("E:\yishooo.xlsx")); //通过输出流将内存中的Excel文件写入到磁盘上 excel.write(out); //关闭资源 out.flush(); out.close(); excel.close(); } public static void main(String[] args) throws Exception { write(); } }
2). 实现效果
1). 代码开发
package com.sky.test; import org.apache.poi.xssf.usermodel.XSSFCell; import org.apache.poi.xssf.usermodel.XSSFRow; import org.apache.poi.xssf.usermodel.XSSFSheet; import org.apache.poi.xssf.usermodel.XSSFWorkbook; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; public class POITest { /** * 基于POI读取Excel文件 * @throws Exception */ @Test public void testRead() throws Exception{ FileInputStream fis = new FileInputStream(new File("E:\\yishooo.xlsx")); //通过输入流读取指定的excel文件 XSSFWorkbook excel = new XSSFWorkbook(fis); //获取excel文件的第1个Sheet页 XSSFSheet sheet = excel.getSheetAt(0); //获取sheet页中的第一行的行号 int firstRowNum = sheet.getFirstRowNum(); //获取sheet页中的最后一行的行号 int lastRowNum = sheet.getLastRowNum(); for (int i = 0; i <= lastRowNum; i++) { //获取sheet页中的行 XSSFRow row = sheet.getRow(i); //获取有数据的第一个单元格的索引 short firstCellNum = row.getFirstCellNum(); //获取有数据的最后一个单元格的索引 short lastCellNum = row.getLastCellNum(); for (int j = firstCellNum;j < lastCellNum;j++){ XSSFCell cell = row.getCell(j); CellType cellType = cell.getCellTypeEnum(); if (cellType == CellType.STRING){ String value = cell.getStringCellValue(); System.out.println(value); } } /*//获取行的第2个单元格 XSSFCell cell1 = row.getCell(1); //获取单元格中的文本内容 String cellValue1 = cell1.getStringCellValue(); //获取行的第3个单元格和文本内容 XSSFCell cell2 = row.getCell(2); String cellValue2 = cell2.getStringCellValue(); System.out.println(cellValue1 + " " +cellValue2);*/ } //关闭资源 fis.close(); excel.close(); } }
2). 实现效果
产品原型
在数据统计页面,有一个数据导出的按钮,点击该按钮时,其实就会下载一个文件。这个文件实际上是一个Excel形式的文件,文件中主要包含最近30日运营相关的数据。表格的形式已经固定,主要由概览数据和明细数据两部分组成。真正导出这个报表之后,相对应的数字就会填充在表格中,就可以进行存档。
业务规则:
- 导出Excel形式的报表文件
- 导出最近30天的运营数据
注意:
- 当前接口没有传递参数,因为导出的是最近30天的运营数据,后端计算即可,所以不需要任何参数
- 当前接口没有返回数据,因为报表导出功能本质上是文件下载,服务端会通过输出流将Excel文件下载到客户端浏览器
实现步骤
1). 设计Excel模板文件
2). 查询近30天的运营数据
3). 将查询到的运营数据写入模板文件
在sky-pojo定义BusinessDataVO类
@Data @Builder @NoArgsConstructor @AllArgsConstructor public class BusinessDataVO implements Serializable { private LocalDate date; //日期 private Double turnover;//营业额 private Integer validOrderCount;//有效订单数 private Double orderCompletionRate;//订单完成率 private Double unitPrice;//平均客单价 private Integer newUsers;//新增用户数 }
Controller层
根据接口定义,在ReportController中创建export方法:
/** * 导出运营数据报表 * @param response */ @GetMapping("/export") @ApiOperation("导出运营数据报表") public void export(HttpServletResponse response){ reportService.exportBusinessData(response); }
Service层接口
在ReportService接口中声明导出运营数据报表的方法:
/** * 导出近30天的运营数据报表 * @param response **/ void exportBusinessData(HttpServletResponse response);
Service层实现类
在ReportServiceImpl实现类中实现导出运营数据报表的方法:
提前将运营数据报表模板.xlsx拷贝到项目的resources/template目录中
/**导出近30天的运营数据报表 * @param response **/ public void exportBusinessData(HttpServletResponse response) { //准备开始时间和结束时间 LocalDate begin = LocalDate.now().minusDays(30); LocalDate end = LocalDate.now().minusDays(1); //调用WorkSpaceService中的getBusinessData方法,得到过去30天运营数据汇总 BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(begin, LocalTime.MIN), LocalDateTime.of(end, LocalTime.MAX)); try { //读取template下的excel模板文件 InputStream is = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx"); //基于提供好的模板文件创建一个新的Excel表格对象 XSSFWorkbook excel = new XSSFWorkbook(is); //得到工作表 XSSFSheet sheet = excel.getSheetAt(0); //设置第2行的起始截止日期 sheet.getRow(1).getCell(1).setCellValue(begin + "至" + end); //获得第4行并设置单元格数据 XSSFRow row = sheet.getRow(3); row.getCell(2).setCellValue(businessData.getTurnover()); row.getCell(4).setCellValue(businessData.getOrderCompletionRate()); row.getCell(6).setCellValue(businessData.getNewUsers()); //获取第5行并设置单元格数据 row = sheet.getRow(4); row.getCell(2).setCellValue(businessData.getValidOrderCount()); row.getCell(4).setCellValue(businessData.getUnitPrice()); //组装查询日期 Map map = new HashMap(); map.put("begin",LocalDateTime.of(begin,LocalTime.MIN)); map.put("end",LocalDateTime.of(end,LocalTime.MAX)); //按照日期范围统计每一天的营业数据 List<BusinessDataVO> businessDataVOList = orderMapper.getBusinessDataVO(map); //按照日期范围统计每一天的订单总数量 List<OrderCountDTO> orderCountDTOList = orderMapper.countByMap(map); //按照日期范围统计每一天的新增用户数量 List<UserCountDTO> userCountDTOList = userMapper.countNewUserByMap(map); //循环写入近30日每一天的营业数据 for (int i = 0; i < 30; i++) { LocalDate date = begin.plusDays(i); row = sheet.getRow(7 + i); row.getCell(1).setCellValue(date.toString());//设置日期列 //判断businessDataVOList数据中是否有营业额 Optional<BusinessDataVO> optional = businessDataVOList.stream().filter(businessDataVO -> { return businessDataVO.getDate().isEqual(date); }).findFirst(); if (optional.isPresent()){ //有营业额,取出来放到excel表格中 Double turnover = optional.get().getTurnover(); Integer validOrderCount = optional.get().getValidOrderCount(); Double unitPrice = optional.get().getUnitPrice(); row.getCell(2).setCellValue(turnover);//设置营业额 row.getCell(3).setCellValue(validOrderCount);//设置有效订单 Integer orderCount = orderCountDTOList.stream().filter(orderCountDTO -> { return orderCountDTO.getOrderTime().isEqual(date); }).findFirst().get().getOrderCount(); row.getCell(4).setCellValue(validOrderCount.doubleValue() / orderCount);//设置订单完成率 row.getCell(5).setCellValue(unitPrice);//设置平均客单价 }else { row.getCell(2).setCellValue(0.0); row.getCell(3).setCellValue(0); row.getCell(4).setCellValue(0.0); row.getCell(5).setCellValue(0.0); } Optional<UserCountDTO> optional1 = userCountDTOList.stream().filter(userCountDTO -> { return userCountDTO.getCreateTime().isEqual(date); }).findFirst(); //设置新增用户数 if (optional1.isPresent()){ row.getCell(6).setCellValue(optional1.get().getUserCount()); }else { row.getCell(6).setCellValue(0); } } //通过输出流将文件下载到客户端浏览器中 excel.write(response.getOutputStream());//response调用的流由response自己关闭资源 //关闭资源 excel.close(); } catch (IOException e) { e.printStackTrace(); } }
Mapper层
OrderMapper.java
/** * 根据日期范围统计营业数据 * @param map * @return */ List<BusinessDataVO> getBusinessDataVO(Map map);
OrderMapper.xml
<select id="getBusinessDataVO" resultType="com.sky.vo.BusinessDataVO"> select date_format(order_time,'%Y-%m-%d') date,sum(amount) turnover, count(*) validOrderCount, sum(amount)/count(*) unitPrice from orders where status = 5 <if test="beginTime != null">and order_time >= #{beginTime}if> <if test="endTime != null">and order_time <= #{endTime}if> group by date select>
直接使用前后端联调测试。
commit—>describe—>push