在商家管理端的左侧,有一个名为"数据统计"的菜单,该页面负责展示各个维度的数据统计,分别是营业额统计、用户统计、订单统计、销量排名top10。统计的数据是借助一些图形化的报表技术来生成并展示的。在左上角还可选择时间段,如昨日、近7日等等。
在实现这些功能之前,我们需要先了解前端技术Apache ECharts,上文的图形报表就是基于该技术实现的。但因为我们侧重于后端开发,因对于前端技术Apache ECharts我们简单了解即可。
Apache ECharts
Apache ECharts 是一个基于 JavaScript 的开源数据可视化图表库,ECharts 提供了直观、生动、可交互和可个性化定制的数据可视化图表,广泛应用于各行业的Web应用开发中。
在SQL基础篇的文章末尾我们就提到了Apache ECharts,当时就已介绍过:后端获取数据后需与前端的组件相结合,如果需要以图形报表的方式渲染出来,可前往开源组件网站Apache ECharts寻找所需组件。
文末提及了Apache EChartshttps://blog.csdn.net/qq_65501197/article/details/143835463?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522a876cb851713ef546e5e0739b5d4760e%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=a876cb851713ef546e5e0739b5d4760e&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-143835463-null-null.nonecase&utm_term=%E5%9B%BE%E8%A1%A8&spm=1018.2226.3001.4450https://blog.csdn.net/qq_65501197/article/details/143835463?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522a876cb851713ef546e5e0739b5d4760e%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=a876cb851713ef546e5e0739b5d4760e&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-143835463-null-null.nonecase&utm_term=%E5%9B%BE%E8%A1%A8&spm=1018.2226.3001.4450https://blog.csdn.net/qq_65501197/article/details/143835463?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522a876cb851713ef546e5e0739b5d4760e%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=a876cb851713ef546e5e0739b5d4760e&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-143835463-null-null.nonecase&utm_term=%E5%9B%BE%E8%A1%A8&spm=1018.2226.3001.4450https://blog.csdn.net/qq_65501197/article/details/143835463?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522a876cb851713ef546e5e0739b5d4760e%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=a876cb851713ef546e5e0739b5d4760e&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-143835463-null-null.nonecase&utm_term=%E5%9B%BE%E8%A1%A8&spm=1018.2226.3001.4450 常见的图表有柱状图、饼状图、折线图等等。无论是哪种图形,其本质都是对数据进行加工处理,最后通过直观的图表来展示数据。
接下来我们通过一个入门案例来了解如何使用Apache ECharts。
快速入门
前往官网,点击快速入门根据提示编写代码(了解即可)
Apache EChartsApache ECharts,一款基于JavaScript的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。https://echarts.apache.org/zh/index.htmlhttps://echarts.apache.org/zh/index.htmlhttps://echarts.apache.org/zh/index.htmlhttps://echarts.apache.org/zh/index.html
Apache ECharts 支持多种下载方式,这里我们以从 jsDelivr CDN 上获取为例,介绍如何快速安装。
在 echarts CDN by jsDelivr - A CDN for npm and GitHub 选择 dist/echarts.js 点击并保存为 echarts.js 文件。
引入 Apache ECharts
在刚才保存 echarts.js 的目录新建一个 index.html 文件,内容如下:
<!DOCTYPE html>
<html><head><meta charset="utf-8" /><!-- 引入刚刚下载的 ECharts 文件 --><script src="echarts.js"></script></head>
</html>
打开这个 index.html ,你会看到一片空白。但是不要担心,打开控制台确认没有报错信息,就可以进行下一步。
绘制一个简单的图表
在绘图前我们需要为 ECharts 准备一个定义了高宽的 DOM 容器。在刚才的例子 </head> 之后,添加:
<body><!-- 为 ECharts 准备一个定义了宽高的 DOM --><div id="main" style="width: 600px;height:400px;"></div>
</body>
然后就可以通过 echarts.init 方法初始化一个 echarts 实例并通过 setOption 方法生成一个简单的柱状图,下面是完整代码。
<!DOCTYPE html>
<html><head><meta charset="utf-8" /><title>ECharts</title><!-- 引入刚刚下载的 ECharts 文件 --><script src="echarts.js"></script></head><body><!-- 为 ECharts 准备一个定义了宽高的 DOM --><div id="main" style="width: 600px;height:400px;"></div><script type="text/javascript">// 基于准备好的dom,初始化echarts实例var myChart = echarts.init(document.getElementById('main'));// 指定图表的配置项和数据var option = {title: {text: 'ECharts 入门示例'},tooltip: {},legend: {data: ['销量']},xAxis: {//横坐标data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子']},yAxis: {},series: [//纵坐标{name: '销量',type: 'bar',data: [5, 20, 36, 10, 10, 20]}]};// 使用刚指定的配置项和数据显示图表。myChart.setOption(option);</script></body>
</html>
这样你的第一个图表就诞生了!
后端则侧重于研究当前图表所需的数据格式,通常是需要后端提供符合格式的动态数据,然后响应该给前端来展示图表。
营业额统计
营业额统计采用折线图,横坐标为日期、纵坐标为营业额。只有订单状态为已完成的订单金额合计才是营业额总计。需根据时间选择区间来展示每天的营业额数据。
请求路径为/admin/report/turnoverStatistics,其本质是个查询数据库的操作,因此请求方法为get,前端通过Query传入String类型的begin和end,意为开始日期和结束日期,在文章"前后端请求响应"日期参数部分介绍过,接收日期参数需要使用@DateTimeFormat注解。
返回的类型为TurnoverReportVO,包括日期列表dateList,日期之间以逗号分隔,营业额列表turnoverList,营业额之间以逗号分隔。
有一点需要注意:如果当日无符合要求的订单,那么查询结果为null,如果直接返回null会导致程序出错,因此我们需要使用三元表达式判断营业额是否为空,如果为空则替换为0.0。
在server模块controller包admin包下新建ReportController类:
// Controller———————————————————
@RestController
@RequestMapping("/admin/report")
@Api(tags = "数据统计相关接口")
@Slf4j
public class ReportController {@Autowiredprivate ReportService reportService;//统计指定时间区间内的营业额@GetMapping("/turnoverStatistics")@ApiOperation("营业额统计")public Result<TurnoverReportVO> turnoverStatistics(@RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {//省略了默认的@RequestParam注解,效果相同log.info("统计从{}至{}的营业额", begin, end);return Result.success(reportService.getTurnoverStatistics(begin, end));}}
// Service———————————————————————
public interface ReportService {//统计指定时间区间内的营业额TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end);
}
// ServiceImpl———————————————————
@Service
@Slf4j
public class ReportServiceImpl implements ReportService {@Autowiredprivate OrderMapper orderMapper;// 统计指定时间区间内的营业额@Overridepublic TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {// 创建日期列表,用于存储从开始日期到结束日期的每一天List<LocalDate> dateList = new ArrayList<>();dateList.add(begin);// 循环计算从开始日期到结束日期的每一天,并将其添加到日期列表中while (!begin.equals(end)) {// 日期计算,计算指定日期的后一天begin = begin.plusDays(1);dateList.add(begin);}// 创建营业额列表,用于存储每个日期对应的营业额List<Double> turnoverList = new ArrayList<>();for (LocalDate date : dateList) {// 查询date日期对应的营业额数据(状态为5(已完成)的订单总金额)LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); // 当天的开始时间LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); // 当天的结束时间Map<String, Object> map = new HashMap<>();map.put("begin", beginTime);map.put("end", endTime);map.put("status", Orders.COMPLETED); // 订单状态为已完成Double turnover = orderMapper.sumByTime(map); // 根据时间范围和订单状态查询营业额turnover = turnover == null ? 0.0 : turnover; // 如果营业额为null,则设置为0.0turnoverList.add(turnover); // 将营业额添加到列表中}// 封装返回结果,将日期列表和营业额列表转换为逗号分隔的字符串return TurnoverReportVO.builder().dateList(StringUtils.join(dateList, ",")) // 日期列表,日期之间以逗号分隔.turnoverList(StringUtils.join(turnoverList, ",")) // 营业额列表,营业额之间以逗号分隔.build();}
}
// Mapper———————————————————————
@Mapper
public interface OrderMapper {......//统计指定时间区间内的营业额Double sumByTime(Map <String,Object>map);
}
<!--OrderMapper--><select id="sumByTime" resultType="java.lang.Double">select sum(amount)from orders<where><if test="begin != null">and order_time >= #{begin}</if><if test="end != null">and order_time <= #{end}</if><if test="status != null">and status = #{status}</if></where></select>
用户统计
用户统计同样采用折线图,横坐标为日期、纵坐标为用户量。不同的是用户统计有两条折线,一条为总用户量,一条为当日新增用户量。
请求路径为/admin/report/turnoverStatistics,请求方法为get,前端通过Query传入String类型的begin和end,意为开始日期和结束日期。返回的类型为UserReportVO,包括dateList、newUserList、totalUserList,分别为日期列表、新增用户数列表、总用户量列表。
// Controller———————————————————@GetMapping("/userStatistics")@ApiOperation("用户统计")public Result<UserReportVO> userStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {log.info("用户数据统计:{},{}", begin, end);return Result.success(reportService.getUserStatistics(begin, end));}
// Service———————————————————————UserReportVO getUserStatistics(LocalDate begin, LocalDate end);
// ServiceImpl———————————————————@Autowiredprivate UserMapper userMapper;......@Overridepublic UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {// 初始化日期列表,用于存储从开始日期到结束日期的每一天List<LocalDate> dateList = new ArrayList<>();dateList.add(begin);// 循环计算从开始日期到结束日期的每一天,并将其添加到日期列表中while (!begin.equals(end)) {dateList.add(begin);begin = begin.plusDays(1); // 计算指定日期的后一天}// 初始化总用户数列表List<Integer> totalUserList = new ArrayList<>();// 初始化每日新增用户数列表List<Integer> newUserList = new ArrayList<>();// 遍历日期列表,计算每天的总用户数和新增用户数for (LocalDate date : dateList) {LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); // 当天的开始时间LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); // 当天的结束时间// 设置结束时间参数,查询总用户数Map<String, Object> map = new HashMap<>();map.put("end", endTime);// 查询总用户数并添加到总用户数列表 select count(*) from user where create_time <= '2025-01-23'Integer totalUser = userMapper.countUserByMap(map);totalUserList.add(totalUser);// 设置开始时间参数,查询新增用户数 select count(*) from user where create_time <= '2025-01-23' and create_time>='2025-01-16'map.put("begin", beginTime);Integer newUser = userMapper.countUserByMap(map);newUserList.add(newUser);}// 封装结果数据并返回return UserReportVO.builder().dateList(StringUtils.join(dateList, ",")) // 将日期列表转换为逗号分隔的字符串.totalUserList(StringUtils.join(totalUserList, ",")) // 将总用户数列表转换为逗号分隔的字符串.newUserList(StringUtils.join(newUserList, ",")) // 将每日新增用户数列表转换为逗号分隔的字符串.build();}
// Mapper———————————————————————
//UserMapperInteger countUserByMap(Map map);
<!-- UserMapper--><select id="countUserByMap" resultType="java.lang.Integer">select count(*) from user<where><if test="begin != null">and create_time >= #{begin}</if><if test="end != null">and create_time <= #{end}</if></where></select>
订单统计
订单统计同理也采用折线图,横坐标为日期、纵坐标为订单数。两条折线一条为当日总订单数,一条为有效订单数(状态为已完成的订单)。同时不止折线图,还有总订单数、总有效订单数、订单完成率(总有效订单数/总订单数*100%)。
请求路径为/admin/report/ordersStatistics,请求方法为get,前端通过Query传入String类型的begin和end,意为开始日期和结束日期。
返回的类型为OrderReportVO,参数较多,包括日期列表dateList、订单完成率orderCompletionRate、订单数列表orderCountList、订单总数totalOrderCount、有效订单数validOrderCount、有效订单数列表validOrderCountList。
有两点需要注意,首先是计算订单完成率时,有可能出现订单数为0的清空,为避免计算订单完成率时除数为0,需要先进行判断再计算,且因为两数为Integer类型,完成率为Double类型,所以应先调用.doubleValue() 对其类型进行转化。
然后是计算总订单数和总有效订单数时,使用循环累加过于麻烦,我们直接使用stream进行累加计算。
// Controller———————————————————@GetMapping("/ordersStatistics")@ApiOperation("订单统计")public Result<OrderReportVO> orderStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {log.info("订单数据统计:{},{}", begin, end);return Result.success(reportService.getOrderStatistics(begin, end));}
// Service———————————————————————OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end);
// ServiceImpl———————————————————@Overridepublic OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {// 初始化日期列表,用于存储从开始日期到结束日期的每一天List<LocalDate> dateList = new ArrayList<>();// 循环计算从开始日期到结束日期的每一天,并将其添加到日期列表中while (!begin.equals(end)) {dateList.add(begin);begin = begin.plusDays(1); // 计算指定日期的后一天}// 添加结束日期到日期列表中,以确保包含完整的日期范围dateList.add(end);// 初始化每天订单总数列表List<Integer> orderCountList = new ArrayList<>();// 初始化每日有效订单数列表(订单状态为已完成,即status为5)List<Integer> validOrderList = new ArrayList<>();// 遍历日期列表,计算每天的总订单数和有效订单数for (LocalDate date : dateList) {LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); // 当天的开始时间LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); // 当天的结束时间// 查询每天的订单总数 select count(*) from orders where order_time <= '2025-01-23' and order_time >='2025-01-18';Integer orderCount = getOrderCount(beginTime, endTime, null);orderCountList.add(orderCount);// 查询每天的有效订单数 select count(*) from orders where order_time <= '2025-01-23' and order_time >='2025-01-18' and status = 5;Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);validOrderList.add(validOrderCount);}// 计算指定时间内的订单总数Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();// 计算指定时间内的有效订单总数Integer validOrderCount = validOrderList.stream().reduce(Integer::sum).get();// 计算订单完成率(有效订单数除以总订单数)Double orderCompletionRate = 0.0; 初始化订单完成率为0.0,用于存储计算后的完成率比例// 检查总订单数是否不为零,以避免除以零的错误if (totalOrderCount != 0) {// 计算订单完成率:有效订单数转换为Double类型后除以总订单数,得到完成比例orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;}// 封装结果数据并返回return OrderReportVO.builder().dateList(StringUtils.join(dateList, ",")) // 将日期列表转换为以逗号分隔的字符串.orderCountList(StringUtils.join(orderCountList, ",")) // 将每天订单总数列表转换为以逗号分隔的字符串.validOrderCountList(StringUtils.join(validOrderList, ",")) // 将每日有效订单数列表转换为以逗号分隔的字符串.totalOrderCount(totalOrderCount) // 设置指定时间内的订单总数.validOrderCount(validOrderCount) // 设置指定时间内的有效订单总数.orderCompletionRate(orderCompletionRate) // 设置订单完成率.build();}//根据给定的时间范围和订单状态查询订单数量。private Integer getOrderCount(LocalDateTime begin, LocalDateTime end, Integer status) {Map<String, Object> map = new HashMap<>();map.put("begin", begin);map.put("end", end);map.put("status", status); // 订单状态,null表示查询所有状态的订单,否则查询指定状态的订单return orderMapper.countOrderByMap(map);}
// Mapper———————————————————————
//OrderMapperInteger countOrderByMap(Map<String, Object> map);
<!--OrderMapper--><select id="countOrderByMap" resultType="java.lang.Integer">select count(*) from orders<where><if test="begin != null">and order_time >= #{begin}</if><if test="end != null">and order_time <= #{end}</if><if test="status != null">and status = #{status}</if></where></select>
销量排名Top10
销量排名采用柱状图,横坐标为商品、纵坐标为销量(商品份数),降序排列。根据指定的时间区间,展示销量前十的商品(包括菜品和套餐)。
请求路径为/admin/report/top10,请求方法为get,前端通过Query传入String类型的begin和end,意为开始日期和结束日期。
返回的类型为SalesTop10ReportVO,包括nameList、numberList分别为商品名称列表和销量列表,以逗号分隔。
我们要结合orders和order_detail两张表来查询,先在orders中查询status为5的订单,然后再查询对应的order_detail表中餐品的数量,名称重复时数量累加。逻辑较复杂,我们先写出对应的sql语句:
-- 选择订单详情中的商品名称和数量总和,并将数量总和命名为num
select od.name, sum(od.number) as num
from order_detail od, -- 订单详情表,别名为odorders o -- 订单表,别名为o
where od.order_id = o.id -- 通过订单ID关联订单详情表和订单表and o.status = 5 -- 筛选状态为5的订单,通常表示已完成and o.order_time <= '2025-01-24' -- 筛选订单时间小于或等于2025年1月24日的记录and o.order_time >= '2025-01-10' -- 筛选订单时间大于或等于2025年1月10日的记录
group by od.name -- 按商品名称分组,以便对每个商品的数量进行汇总
order by num desc -- 按数量总和降序排序,以便查看销量最高的商品
limit 0,10; -- 限制结果只显示前10条记录,用于显示销量最高的10个商品
// Controller———————————————————@GetMapping("/top10")@ApiOperation("销量排名")public Result<SalesTop10ReportVO> top10(@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {log.info("销量排名Top10:{},{}", begin, end);return Result.success(reportService.getSalesTop10(begin, end));}
// Service———————————————————————SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end);
// ServiceImpl———————————————————
@Override
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {// 将开始日期转换为当天的开始时间(00:00:00)LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);// 将结束日期转换为当天的结束时间(23:59:59)LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);// 调用Mapper层方法获取销量前10的商品信息List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);// 提取商品名称列表List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());// 将商品名称列表转换为逗号分隔的字符串String nameList = StringUtils.join(names, ",");// 提取商品销量列表List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());// 将商品销量列表转换为逗号分隔的字符串(注意:这里应该使用numbers而不是name)String numList = StringUtils.join(numbers, ",");// 构建并返回销量前10的报告VO对象return SalesTop10ReportVO.builder().nameList(nameList).numberList(numList).build();
}
// Mapper———————————————————————
//OrderMapperList<GoodsSalesDTO> getSalesTop10(LocalDateTime begin, LocalDateTime end);
<!--OrderMapper--><select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO"><!-- 统计销量排名,仅考虑订单状态已完成的记录 -->select od.name, sum(od.number) numberfrom order_detail od, orders owhere od.order_id = o.id and o.status = 5<!-- 如果开始时间不为空,则添加订单时间大于等于开始时间的条件 --><if test="begin != null">and o.order_time >= #{begin}</if><!-- 如果结束时间不为空,则添加订单时间小于等于结束时间的条件 --><if test="end != null">and o.order_time <= #{end}</if><!-- 按商品名称分组,以便统计每个商品的销量 -->group by od.name<!-- 按销量降序排序,以获取销量最高的商品 -->order by number desc<!-- 限制结果为前10条记录,即销量最高的10个商品 -->limit 0, 10</select>
Excel报表
工作台
工作台是系统运营的数据看板,并提供快捷操作按钮,可以有效提高商家的工作效率。
点击商家管理端左侧菜单的工作台,即可跳转到工作台页面。该页面会展示项目中的一些业务数据,因为业务数据较多,因此我们将其分为几大部分:
- 今日数据:当日已完成订单总金额、有效订单数、订单完成率、平均客单价、新增用户。
- 订单管理:待接单数、待派送数、已完成订单数、已取消订单数、全部订单数。
- 菜品总览:已启售商品数、已停售商品数。
- 套餐总览:已启售套餐数、已停售套餐数。
- 订单信息:待接单订单、待派送订单。
因为未涉及新知识,所以直接导入代码即可:
// Controller———————————————————
@RestController
@RequestMapping("/admin/workspace")
@Slf4j
@Api(tags = "工作台相关接口")
public class WorkSpaceController {@Autowiredprivate WorkspaceService workspaceService;// 工作台今日数据查询@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);}// 查询订单管理数据@GetMapping("/overviewOrders")@ApiOperation("查询订单管理数据")public Result<OrderOverViewVO> orderOverView(){return Result.success(workspaceService.getOrderOverView());}// 查询菜品总览@GetMapping("/overviewDishes")@ApiOperation("查询菜品总览")public Result<DishOverViewVO> dishOverView(){return Result.success(workspaceService.getDishOverView());}// 查询套餐总览@GetMapping("/overviewSetmeals")@ApiOperation("查询套餐总览")public Result<SetmealOverViewVO> setmealOverView(){return Result.success(workspaceService.getSetmealOverView());}
}
// Service———————————————————————
public interface WorkspaceService {// 根据时间段统计营业数据BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end);// 查询订单管理数据OrderOverViewVO getOrderOverView();// 查询菜品总览DishOverViewVO getDishOverView();// 查询套餐总览SetmealOverViewVO getSetmealOverView();}
// ServiceImpl———————————————————
@Service
@Slf4j
public class WorkspaceServiceImpl implements WorkspaceService {@Autowiredprivate OrderMapper orderMapper;@Autowiredprivate UserMapper userMapper;@Autowiredprivate DishMapper dishMapper;@Autowiredprivate SetmealMapper setmealMapper;// 根据时间段统计营业数据public BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end) {// 营业额:当日已完成订单的总金额// 有效订单:当日已完成订单的数量// 订单完成率:有效订单数 / 总订单数// 平均客单价:营业额 / 有效订单数// 新增用户:当日新增用户的数量Map map = new HashMap();map.put("begin",begin);map.put("end",end);// 查询总订单数Integer totalOrderCount = orderMapper.countOrderByMap(map);map.put("status", Orders.COMPLETED);// 营业额Double turnover = orderMapper.sumByTime(map);turnover = turnover == null? 0.0 : turnover;// 有效订单数Integer validOrderCount = orderMapper.countOrderByMap(map);Double unitPrice = 0.0;Double orderCompletionRate = 0.0;if(totalOrderCount != 0 && validOrderCount != 0){// 订单完成率orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;// 平均客单价unitPrice = turnover / validOrderCount;}// 新增用户数Integer newUsers = userMapper.countUserByMap(map);return BusinessDataVO.builder().turnover(turnover).validOrderCount(validOrderCount).orderCompletionRate(orderCompletionRate).unitPrice(unitPrice).newUsers(newUsers).build();}// 查询订单管理数据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.countOrderByMap(map);// 待派送map.put("status", Orders.CONFIRMED);Integer deliveredOrders = orderMapper.countOrderByMap(map);// 已完成map.put("status", Orders.COMPLETED);Integer completedOrders = orderMapper.countOrderByMap(map);// 已取消map.put("status", Orders.CANCELLED);Integer cancelledOrders = orderMapper.countOrderByMap(map);//全部订单map.put("status", null);Integer allOrders = orderMapper.countOrderByMap(map);return OrderOverViewVO.builder().waitingOrders(waitingOrders).deliveredOrders(deliveredOrders).completedOrders(completedOrders).cancelledOrders(cancelledOrders).allOrders(allOrders).build();}//查询菜品总览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();}//查询套餐总览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———————————————————————
public interface SetmealMapper {......Integer countByMap(Map map);
}
public interface DishMapper {......Integer countByMap(Map map);
}
<!--SetMealMapper--><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--><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>
Apache POI
Apache POI 是一个处理 Microsoft Office 各种文件格式的开源项目。简单来说就是,我们可以使用 POI 在 Java 程序中对 Microsoft Office 各种文件进行读写操作。
通过POI写入文件
一般情况下,POI 都是用于操作 Excel 文件。想要使用它,需两步:
一、Apache POI的maven坐标:(项目中已导入)
<dependency><groupId>org.apache.poi</groupId><artifactId>poi</artifactId><version>3.16</version>
</dependency>
<dependency><groupId>org.apache.poi</groupId><artifactId>poi-ooxml</artifactId><version>3.16</version>
</dependency>
二、编写相关代码
我们平时创建的Excel分为页、行、列、值。使用java代码创建Excel也不例外:
public class POITest {public static void write() throws Exception {// 在内存中创建一个XSSFWorkbook对象,代表一个Excel文件XSSFWorkbook excel = new XSSFWorkbook();// 在Excel文件中创建一个名为"test"的Sheet页XSSFSheet sheet = excel.createSheet("test");// 在Sheet页中创建第一行,从第一行(序号为0)开始XSSFRow row = sheet.createRow(0);// 在第一行中创建单元格,并设置单元格的值row.createCell(0).setCellValue("姓名");row.createCell(1).setCellValue("性别");row.createCell(2).setCellValue("年龄");// 创建第二行,并填充数据row = sheet.createRow(1);row.createCell(0).setCellValue("张三");row.createCell(1).setCellValue("男");row.createCell(2).setCellValue("21");// 创建第三行,并填充数据row = sheet.createRow(2);row.createCell(0).setCellValue("李四");row.createCell(1).setCellValue("女");row.createCell(2).setCellValue("12");// 指定文件路径,创建文件输出流FileOutputStream out = new FileOutputStream(new File("C:\\Users\\chn\\Desktop\\test.xlsx"));// 将Excel文件内容写入到输出流中,即保存到磁盘excel.write(out);// 关闭文件输出流,释放资源out.close();// 关闭XSSFWorkbook对象,释放资源excel.close();}public static void main(String[] args) throws Exception {// 调用write方法,执行Excel文件写入操作write();}
}
通过POI读取文件
public static void read() throws Exception {// 创建文件输入流,用于读取指定路径的Excel文件FileInputStream inputStream = new FileInputStream(new File("C:\\Users\\chn\\Desktop\\test.xlsx"));// 创建XSSFWorkbook对象,通过输入流读取磁盘上已存在的Excel文件XSSFWorkbook excel = new XSSFWorkbook(inputStream);// 读取Excel文件中的第一个Sheet页XSSFSheet sheet = excel.getSheetAt(0);// 获取Sheet中存在值的最后一行的行号int lastRowNum = sheet.getLastRowNum();// 遍历每一行,从第二行开始(假设第一行是标题行)for (int i = 1; i <= lastRowNum; i++) {// 获取当前行的Row对象XSSFRow row = sheet.getRow(i);// 获取当前行中每个单元格的值String value1 = row.getCell(0).getStringCellValue(); // 获取第一个单元格的值String value2 = row.getCell(1).getStringCellValue(); // 获取第二个单元格的值String value3 = row.getCell(2).getStringCellValue(); // 获取第三个单元格的值// 打印出每个单元格的值System.out.println(value1 + " " + value2 + " " + value3);}// 关闭XSSFWorkbook对象,释放资源excel.close();// 关闭文件输入流,释放资源inputStream.close();
}
导出Excel
接下来到项目中实现该功能。
在数据统计界面的右上角有一数据导出按钮,点击即可将近30天的运营数据导出为Excel表格中,表格样式:
请求路径为/admin/report/export,请求方法为get。因为报表导出的本质是文件下载,服务端会通过输出流将Excel文件下载到客户端浏览器,所以该接口无请求参数和返回数据。
可以看到表格样式中涉及到了不同的背景色、字号、合并单元格等等,这一系列操作如果通过POI来完成会比较麻烦。我们一般选择先创建一个模板文件,完成除填充数据外的一切操作,系统只需在对应处填充数据即可。
此时即可得到完成该功能所需的四步:1、设计Excel模板文件。2、查询近30天的运营数据,3、将查询到的运营数据写入模板文件。4、通过输入流将Excel文件下载到客户端浏览器。
先来完成第一步,因为该文件一般是在项目内部的,所以我们在rsources中新建template并将运营数据报表模板放入其中。
然后是第二步:在ReportController中编写相关方法:
// Controller———————————————————
@GetMapping("/export")
public void export(HttpServletResponse response) throws IOException {reportService.exportData(response);
}
// Service———————————————————————
void exportData(HttpServletResponse response) throws IOException;
// ServiceImpl———————————————————
@Autowired
private WorkspaceService workspaceService;
......
@Override
public void exportData(HttpServletResponse response) throws IOException {// 计算查询日期范围,从当前日期向前推30天LocalDate begin = LocalDate.now().minusDays(30);LocalDate end = LocalDate.now().minusDays(1);// 设置查询的开始时间和结束时间LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);// 查询概览数据BusinessDataVO businessData = workspaceService.getBusinessData(beginTime, endTime);// 获取模板文件输入流InputStream input = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");// 基于提供的模板文件创建一个新的Excel工作簿对象XSSFWorkbook excel = new XSSFWorkbook(input);// 获取Excel工作簿中的"Sheet1"工作表XSSFSheet sheet = excel.getSheet("Sheet1");// 设置报表的时间范围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());// 继续填写其他概览数据row = sheet.getRow(4); row.getCell(2).setCellValue(businessData.getValidOrderCount());row.getCell(4).setCellValue(businessData.getUnitPrice());// 遍历最近30天,填写每日明细数据for (int i = 0; i < 30; i++) {LocalDate date = begin.plusDays(i);// 查询每日的营业数据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());}// 设置响应头,提示浏览器下载文件response.setHeader("Content-Disposition", "attachment; filename=\"business_data.xlsx\"");// 获取响应的输出流ServletOutputStream out = response.getOutputStream();// 将Excel工作簿写入到输出流,实现文件下载excel.write(out);// 刷新输出流,确保数据完全输出out.flush();// 关闭资源out.close();excel.close();
}