工作台
需求分析和设计
作用
- 核心功能: 系统运营的数据看板,提供快捷操作入口
- 商业价值: 有效提高商家工作效率,集中展示关键运营数据
- 使用场景: 商家日常营业时查看当日营业数据、订单状态等核心信息
展示数据
- 今日数据区:
- 展示当天营业数据:营业额、有效订单数、订单完成率、平均客单价、新增用户数
- 订单管理区:
- 展示不同状态订单数量:待接单、待派送、已完成、已取消、全部订单数
- 菜品/套餐总览区:
- 菜品总览:已启售和已停售菜品数量
- 套餐总览:已启售和已停售套餐数量
- 订单信息区:
- 重点展示待接单和待派送状态的订单详情列表
- 商家最关心的两种订单状态,需要及时处理
名词解释
- 营业额: 已完成订单的总金额
- 有效订单: 已完成订单的数量(注意是数量而非订单本身)
- 订单完成率: 计算公式为
- 平均客单价: 计算公式为,反映每位用户平均消费金额
- 新增用户: 当日新增注册用户的数量
接口设计
今日数据接口
- 请求方式: 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报表
需求分析和设计
报表样式

- 报表结构: 分为两部分数据展示,上方为概览数据,下方为明细数据
- 概览数据: 包含营业额、订单完成率、新增用户数、有效订单、平均客单价等关键指标
- 明细数据: 按日期展示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响应流来发送数据:
- 控制器方法:
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
reportService.exportBusinessData(response);
}- 服务层实现: 在ReportServiceImpl.exportBusinessData方法中,关键代码是:
//3. 通过输出流将Excel文件下载到客户端浏览器
ServletOutputStream out = response.getOutputStream();
excel.write(out);
//关闭资源
out.close();
excel.close();具体流程
-
Spring MVC调用控制器:当用户访问
/admin/report/export时,Spring MVC调用export方法 -
传递response对象:控制器将
HttpServletResponse对象传递给服务层方法 -
直接写入响应流:
- 通过
response.getOutputStream()获取输出流 - 使用
excel.write(out)将生成的Excel文件内容写入到输出流中 - 这会直接发送给客户端浏览器
- 通过
-
设置响应头(虽然代码中没有明确设置)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响应返回给客户端。