工作台

需求分析和设计

作用

  • 核心功能: 系统运营的数据看板,提供快捷操作入口
  • 商业价值: 有效提高商家工作效率,集中展示关键运营数据
  • 使用场景: 商家日常营业时查看当日营业数据、订单状态等核心信息

展示数据

  • 今日数据区:
    • 展示当天营业数据:营业额、有效订单数、订单完成率、平均客单价、新增用户数
  • 订单管理区:
    • 展示不同状态订单数量:待接单、待派送、已完成、已取消、全部订单数
  • 菜品/套餐总览区:
    • 菜品总览:已启售和已停售菜品数量
    • 套餐总览:已启售和已停售套餐数量
  • 订单信息区:
    • 重点展示待接单和待派送状态的订单详情列表
    • 商家最关心的两种订单状态,需要及时处理

名词解释

  • 营业额: 已完成订单的总金额
  • 有效订单: 已完成订单的数量(注意是数量而非订单本身)
  • 订单完成率: 计算公式为
  • 平均客单价: 计算公式为,反映每位用户平均消费金额
  • 新增用户: 当日新增注册用户的数量

接口设计

今日数据接口
  • 请求方式: GET
  • 请求路径: /admin/workspace/businessData
  • 请求参数: 无(日期由后端自动计算)
  • 返回数据:
    • turnover: 营业额(double类型)
    • validOrderCount: 有效订单数(int32类型)
    • orderCompletionRate: 订单完成率(double类型)
    • unitPrice: 平均客单价(double类型)
    • newUsers: 新增用户数(int32类型)
订单管理接口
  • -请求方式: GET
  • 请求路径: /admin/workspace/overviewOrders
  • 请求参数: 无
  • 返回数据:
    • allOrders: 全部订单数(int32类型)
    • completedOrders: 已完成订单数(int32类型)
    • cancelledOrders: 已取消订单数(int32类型)
    • deliveredOrders: 待派送订单数(int32类型)
    • waitingOrders: 待接单订单数(int32类型)
菜品总览接口
  • 请求方式: GET
  • 请求路径: /admin/workspace/overviewDishes
  • 请求参数: 无
  • 返回数据:
    • sold: 已启售菜品数量
    • discontinued: 已停售菜品数量
  • 套餐总览接口:
    • 路径:/admin/workspace/overviewSetmeals
    • 返回数据与菜品总览类似,反映套餐的启售/停售状态数量

代码开发

  
/**  
 * 工作台  
 */  
@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());  
    }  
}
/**  
 * 根据时间段统计营业数据  
 * @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 = orderMapper.countByMap(map);  
  
    map.put("status", Orders.COMPLETED);  
    //营业额  
    Double turnover = orderMapper.sumByMap(map);  
    turnover = turnover == null? 0.0 : turnover;  
  
    //有效订单数  
    Integer validOrderCount = orderMapper.countByMap(map);  
  
    Double unitPrice = 0.0;  
  
    Double orderCompletionRate = 0.0;  
    if(totalOrderCount != 0 && validOrderCount != 0){  
        //订单完成率  
        orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;  
        //平均客单价  
        unitPrice = turnover / validOrderCount;  
    }  
  
    //新增用户数  
    Integer newUsers = userMapper.countByMap(map);  
  
    return BusinessDataVO.builder()  
            .turnover(turnover)  
            .validOrderCount(validOrderCount)  
            .orderCompletionRate(orderCompletionRate)  
            .unitPrice(unitPrice)  
            .newUsers(newUsers)  
            .build();  
}  
  
  
/**  
 * 查询订单管理数据  
 *  
 * @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 = orderMapper.countByMap(map);  
  
    //待派送  
    map.put("status", Orders.CONFIRMED);  
    Integer deliveredOrders = orderMapper.countByMap(map);  
  
    //已完成  
    map.put("status", Orders.COMPLETED);  
    Integer completedOrders = orderMapper.countByMap(map);  
  
    //已取消  
    map.put("status", Orders.CANCELLED);  
    Integer cancelledOrders = orderMapper.countByMap(map);  
  
    //全部订单  
    map.put("status", null);  
    Integer allOrders = orderMapper.countByMap(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();  
}

Apache POI

  • 核心功能:Apache POI是一个处理Microsoft Office文件格式的开源项目,主要用于在Java程序中操作Excel文件,支持读写操作。
  • 读写能力:支持对Word/PPT/Excel等文档进行读写操作,但实际应用中90%场景聚焦Excel文件处理。
  • 典型场景:
    • 银行交易明细:如图中招商银行交易记录,包含交易日期、收支金额等字段的Excel导出
    • 业务报表导出:如图中订单统计表,支持按年份/地区统计金额并生成Excel
    • 批量数据导入:如图中学生信息表,可批量导入姓名、身份证号等结构化数据
<dependency>  
    <groupId>org.apache.poi</groupId>  
    <artifactId>poi</artifactId>  
</dependency>  
<dependency>  
    <groupId>org.apache.poi</groupId>  
    <artifactId>poi-ooxml</artifactId>  
</dependency>

写操作

public static void write() throws IOException {  
    // 在内存中创建一个Excel文件  
    XSSFWorkbook workbook = new XSSFWorkbook();  
    // 在Excel文件中创建一个Sheet  
    XSSFSheet sheet = workbook.createSheet("info");  
    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("北京");  
  
    FileOutputStream outputStream = new FileOutputStream("info.xlsx");  
    workbook.write(outputStream);  
  
    // 关闭资源  
    outputStream.close();  
    workbook.close();  
  
}

读操作

public static void read() throws IOException {  
    FileInputStream inputStream = new FileInputStream("info.xlsx");  
    XSSFWorkbook workbook = new XSSFWorkbook(inputStream);  
    // 获取第一个工作表  
    XSSFSheet sheet = workbook.getSheetAt(0);  
  
    // 获取最后一行行号  
    int lastRowNum = sheet.getLastRowNum();  
    for (int i = 1; i <= lastRowNum; i++) {  
        // 获取当前行  
        XSSFRow row = sheet.getRow(i);  
        // 获得单元格对象  
        int lastCellNum = row.getLastCellNum();  
        String cellValue1 = row.getCell(1).getStringCellValue();  
        String cellValue2 = row.getCell(2).getStringCellValue();  
        System.out.println(cellValue1 + " " + cellValue2);  
    }  
    // 关闭资源  
    workbook.close();  
    inputStream.close();  
}

导出运营数据Excel报表

需求分析和设计

报表样式

image-1

  • 报表结构: 分为两部分数据展示,上方为概览数据,下方为明细数据
  • 概览数据: 包含营业额、订单完成率、新增用户数、有效订单、平均客单价等关键指标
  • 明细数据: 按日期展示30天的详细运营数据,包含日期、营业额、有效订单、订单完成率、平均客单价、新增用户数等字段

业务规则

  • 报表类型: 固定格式的Excel表格报表,区别于之前实现的可视化图形报表(如ECharts折线图、柱形图)
  • 数据范围: 导出最近30天的运营数据,商家最关心的营业状况指标
  • 应用场景: 主要用于商家存档和分析餐厅经营状况,如营业额、利润等关键指标

接口设计

  • 请求方式: GET请求
  • 请求路径: /admin/report/export
  • 参数设计: 无需请求参数,后端自动计算最近30天数据
  • 返回数据
    • 特殊处理: 不同于常规JSON格式返回,该接口通过输出流直接返回Excel文件
    • 实现原理: 服务端使用输出流将Excel文件写入客户端浏览器,实现文件下载功能
    • 注意事项: 接口本身不返回结构化数据,而是直接输出文件流

代码开发

实现步骤

  • 模板文件设计:提前设计好包含合并单元格、背景色等复杂格式的Excel模板文件,避免通过POI编程方式创建复杂表格
  • 数据查询:需要查询近30天的运营数据,包括营业额、订单完成率、新增用户数等关键指标
  • 数据填充:通过POI API将查询到的数据写入模板文件对应位置
  • 文件下载:最后将填充好的文件下载到客户端浏览器

设计模版

  • 模板优势:相比POI编程创建表格,使用预设计模板可以避免繁琐的样式设置代码
  • 模板内容:包含概览数据(营业额、订单完成率等)和明细数据(按日期统计的各项指标)
  • 存放位置:模板文件应存放在项目resources目录下的template文件夹中

查询数据库获取营业数据

  • 实现步骤:
    • 查询数据库获取营业数据(最近30天)
    • 通过POI将数据写入Excel文件
    • 通过输出流下载Excel文件到客户端浏览器
  • 时间区间计算:
    • 使用LocalDate.now().minusDays(30)获取30天前日期
    • 使用LocalDate.now().minusDays(1)获取昨天日期
    • 转换为LocalDateTime时:开始时间用LocalTime.MIN(00:00:00),结束时间用LocalTime.MAX(23:59:59)
  • 数据来源:
    • 调用WorkspaceService.getBusinessData()方法获取概览数据
    • 数据项包括:营业额、订单完成率、新增用户数、有效订单数、平均客单价
@GetMapping("/export")  
@ApiOperation("导出运营数据报表")  
public void export(HttpServletResponse  response){  
    reportService.exportBusinessData(response);  
}
public void exportBusinessData(HttpServletResponse response) {  
    //1. 查询数据库,获取营业数据---查询最近30天的运营数据  
    LocalDate dateBegin = LocalDate.now().minusDays(30);  
    LocalDate dateEnd = LocalDate.now().minusDays(1);  
  
    //查询概览数据  
    BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN), LocalDateTime.of(dateEnd, LocalTime.MAX));  
  
    //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("时间:" + dateBegin + "至" + dateEnd);  
  
        //获得第4行  
        XSSFRow row = sheet.getRow(3);  
        row.getCell(2).setCellValue(businessDataVO.getTurnover());  
        row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());  
        row.getCell(6).setCellValue(businessDataVO.getNewUsers());  
  
        //获得第5行  
        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 = dateBegin.plusDays(i);  
            //查询某一天的营业数据  
            BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));  
  
            //获得某一行  
            row = sheet.getRow(7 + i);  
            row.getCell(1).setCellValue(date.toString());  
            row.getCell(2).setCellValue(businessData.getTurnover());  
            row.getCell(3).setCellValue(businessData.getValidOrderCount());  
            row.getCell(4).setCellValue(businessData.getOrderCompletionRate());  
            row.getCell(5).setCellValue(businessData.getUnitPrice());  
            row.getCell(6).setCellValue(businessData.getNewUsers());  
        }  
  
        //3. 通过输出流将Excel文件下载到客户端浏览器  
        ServletOutputStream out = response.getOutputStream();  
        excel.write(out);  
  
        //关闭资源  
        out.close();  
        excel.close();  
    } catch (IOException e) {  
        e.printStackTrace();  
    }  
}

Note

该导出功能采用了Excel模板+动态填充数据的方式,避免了在代码中动态创建复杂的Excel格式,通过预先设计好格式的模板文件,只需要填充数据即可,保证了导出文件的格式美观和一致性。同时使用Apache POI库操作Excel,将生成的Excel文件通过HttpServletResponse的输出流返回给客户端浏览器,实现文件下载功能

代码原理

虽然控制器方法没有返回值,但它通过HttpServletResponse对象直接操作HTTP响应流来发送数据:

  1. 控制器方法
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
    reportService.exportBusinessData(response);
}
  1. 服务层实现: 在ReportServiceImpl.exportBusinessData方法中,关键代码是:
//3. 通过输出流将Excel文件下载到客户端浏览器
ServletOutputStream out = response.getOutputStream();
excel.write(out);
 
//关闭资源
out.close();
excel.close();

具体流程

  1. Spring MVC调用控制器:当用户访问/admin/report/export时,Spring MVC调用export方法

  2. 传递response对象:控制器将HttpServletResponse对象传递给服务层方法

  3. 直接写入响应流

    • 通过response.getOutputStream()获取输出流
    • 使用excel.write(out)将生成的Excel文件内容写入到输出流中
    • 这会直接发送给客户端浏览器
  4. 设置响应头(虽然代码中没有明确设置)Spring Boot会自动设置相应的内容类型

与普通JSON响应的区别

普通的REST API控制器方法:

@GetMapping("/userStatistics")
@ApiOperation("用户统计")
public Result<UserReportVO> userStatistics(...) {
    // 返回对象,Spring MVC自动将其转换为JSON并写入响应
    return Result.success(reportService.getUserStatistics(begin, end));
}

而文件下载的控制器:

@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
    // 直接操作response对象写入二进制数据
    reportService.exportBusinessData(response);
}

关键点

  • 没有返回值void类型的方法不通过返回值传递数据
  • 直接操作响应:通过HttpServletResponse直接写入响应体
  • 二进制流:写入的是Excel文件的二进制数据,而不是JSON文本
  • 框架处理:Spring MVC仍然会完成整个HTTP响应过程,只是响应体由我们直接控制

这种方式是Java Web开发中标准的文件下载实现方式,允许将任意数据(如Excel、PDF、图片等)直接作为HTTP响应返回给客户端。