目录
[1. 需求分析和设计](#1. 需求分析和设计)
[2. 代码导入](#2. 代码导入)
[3. 功能测试](#3. 功能测试)
[Apache POI](#Apache POI)
[1. 介绍](#1. 介绍)
[2. 入门案例](#2. 入门案例)
[1. 需求分析和设计](#1. 需求分析和设计)
[2. 代码开发](#2. 代码开发)
[一、参数传递链路:从 Controller 到 Service](#一、参数传递链路:从 Controller 到 Service)
[二、HttpServletResponse 的核心作用(为何要传递它?)](#二、HttpServletResponse 的核心作用(为何要传递它?))
[1. 提供 "输出流":将 Excel 数据写入响应](#1. 提供 “输出流”:将 Excel 数据写入响应)
[3. 功能测试](#3. 功能测试)
先开前端nginx,再开redis,cpolar内网穿透(用于语音播报),最后springboot


工作台
1. 需求分析和设计







2. 代码导入
admin/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());
}
}
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();
}
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 = 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();
}
}
SetmealMapper
java
package com.sky.mapper;
import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Setmeal;
import com.sky.entity.SetmealDish;
import com.sky.enumeration.OperationType;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
import java.util.Map;
@Mapper
public interface SetmealMapper {
/**
* 新增套餐
* @param setmeal
*/
@AutoFill(OperationType.INSERT)
void insert(Setmeal setmeal);
/**
* 根据分类id查询套餐的数量
* @param id
* @return
*/
@Select("select count(id) from setmeal where category_id = #{categoryId}")
Integer countByCategoryId(Long id);
/**
* 分页查询
* @param setmealPageQueryDTO
* @return
*/
Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);
/**
* 根据id查询套餐
* @param id
* @return
*/
@Select("select * from setmeal where id = #{id}")
Setmeal getById(Long id);
/**
* 根据id删除套餐
* @param setmealId
*/
@Delete("delete from setmeal where id = #{id}")
void deleteById(Long setmealId);
/**
* 根据id查询套餐和套餐菜品关系
* @param id
* @return
*/
SetmealVO getByIdWithDish(Long id);
/**
* 根据id修改套餐
*
* @param setmeal
*/
@AutoFill(OperationType.UPDATE)
void update(Setmeal setmeal);
/**
* 动态条件查询套餐
* @param setmeal
* @return
*/
List<Setmeal> list(Setmeal setmeal);
/**
* 根据套餐id查询菜品选项
* @param setmealId
* @return
*/
@Select("select sd.name, sd.copies, d.image, d.description " +
"from setmeal_dish sd left join dish d on sd.dish_id = d.id " +
"where sd.setmeal_id = #{setmealId}")
List<DishItemVO> getDishItemBySetmealId(Long setmealId);
/**
* 根据条件统计套餐数量
* @param map
* @return
*/
Integer countByMap(Map map);
}
XML
<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
java
/**
* 根据条件统计菜品数量
* @param map
* @return
*/
Integer countByMap(Map map);
XML
<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>
3. 功能测试

Apache POI
1. 介绍
Apache POI 的全称是 Poor Obfuscation Implementation(意为 "简陋的混淆实现")。这个名称源于其最初的开发背景 ------ 早期它主要用于解析微软 Office 文件格式,而这些格式在当时并未完全公开,且存在一定的 "混淆" 特性,开发者通过逆向工程等方式逆向解析格式并实现兼容,因此得名 "简陋的混淆实现"。


2. 入门案例

java
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.*;
/**
* 使用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页中创建行对象,下标从0开始
XSSFRow row = sheet.createRow(1);
// 在这一行的第几个创建cell单元格、写入内容,结果可以不接收
row.createCell(0).setCellValue("姓名");
row.createCell(2).setCellValue("城市");
// 通过输出流将内存中的Excel文件写入到磁盘
FileOutputStream out = new FileOutputStream(new File("D:\\info.xlsx"));
excel.write(out);
//关闭资源
out.close();
excel.close();
}
/**
* 通过POI获取Excel文件中的内容
*/
public static void read() throws Exception {
InputStream in = new FileInputStream(new File("D:\\info.xlsx"));
// 读取磁盘上已经存在的Excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
// 读取excel文件中的第一个sheet页
XSSFSheet sheet = excel.getSheetAt(0);
// 获取sheet中最后一行的行号(有文字内容的)
int lastRowNum = sheet.getLastRowNum();
for (int i = 1; i <= lastRowNum; i++){
// 获取某一行
XSSFRow row = sheet.getRow(i);
// 获得单元格对象
String cellValue1 = row.getCell(0).getStringCellValue();
String cellValue2 = row.getCell(2).getStringCellValue();
System.out.println(cellValue1 + " " + cellValue2);
}
// 关闭资源
in.close();
excel.close();
}
public static void main(String[] args) throws Exception {
//write();
read();
}
}
效果如下


导出运营数据Excel报表
1. 需求分析和设计

点击导出按钮下载Excel文件

2. 代码开发
在sky-server/resources/template下面拷贝报表excel模版
ReportController
java
/**
* 导出运营数据报表
* @param response
*/
@GetMapping("/export")
@ApiOperation("导出运营数据报表")
public void export(HttpServletResponse response){
reportService.exportBusinessData(response);
}
结合 Controller
层的 export
方法和 Service
层的 exportBusinessData
方法来看,整个调用链路中传递的核心参数是 HttpServletResponse
对象,它是连接 "接口响应" 和 "Excel 下载" 的关键,具体传递逻辑和作用如下:
一、参数传递链路:从 Controller 到 Service
整个过程中只有一个核心参数 HttpServletResponse
,传递路径非常清晰:
-
Controller 层接收并转发参数
当客户端(如浏览器、Postman)访问
GET /export
接口时,Spring MVC 框架会自动创建 HttpServletResponse 对象 (无需开发者手动创建),并将其作为参数注入到export
方法中。随后,
export
方法直接将这个response
对象传递给Service
层的exportBusinessData
方法,没有新增或修改其他参数。 -
Service 层接收并使用参数
exportBusinessData
方法接收response
对象后,基于它完成 Excel 文件的下载响应逻辑(这是该参数的核心作用,也是整个报表导出功能的关键)。
二、HttpServletResponse
的核心作用(为何要传递它?)
response
对象的本质是 "服务器对客户端的 HTTP 响应载体",在 exportBusinessData
方法中,它的作用完全服务于 "Excel 下载",具体体现在 3 个关键步骤:
1. 提供 "输出流":将 Excel 数据写入响应
Service
层通过 POI 生成 Excel 工作簿(XSSFWorkbook
)后,需要将 Excel 的二进制数据传递到客户端。此时通过 response.getOutputStream()
可以获取 Servlet 输出流(ServletOutputStream),这个流是 "服务器 Excel 数据" 到 "客户端下载" 的桥梁:
java
// 从 response 中获取输出流
ServletOutputStream out = response.getOutputStream();
// 将 Excel 数据写入流,最终传递到客户端
excel.write(out);
ReportService
java
/**
* 导出运营数据报表
* @param response
*/
void exportBusinessData(HttpServletResponse response);
ReportServiceImpl
java
/**
* 导出运营数据报表
* @param 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);
// 第四行第三、五、七个单元格--营业额、订单完成率、新增用户数
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 = 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();
}
}
3. 功能测试

