前言
后台运营页面需要把订单、用户和商品数据转换成可以直接阅读的趋势与排行。苍穹外卖的统计模块将日期范围作为统一输入,在服务层按天汇总数据,再把日期序列和数值序列返回给前端;前端拿到这些数据后,可使用 Apache ECharts 绘制折线图、柱状图等图表。
本文只梳理四类统计:营业额、用户、订单和销量 Top10,并介绍 ECharts 的基本使用方式。
一、统计模块的接口入口
统计接口集中在 ReportController,统一的请求路径前缀是 /admin/report。四个接口都接收 begin 与 end 两个日期参数,并通过 @DateTimeFormat(pattern = "yyyy-MM-dd") 转换为 LocalDate。
java
@GetMapping("/turnoverStatistics")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
return Result.success(reportService.getTurnoverStatistica(begin, end));
}
其余三个接口与它的结构一致:
| 统计内容 | 接口 | 返回对象 |
|---|---|---|
| 营业额 | /admin/report/turnoverStatistics |
TurnoverReportVO |
| 用户 | /admin/report/userStatistics |
UserReportVO |
| 订单 | /admin/report/ordersStatistics |
OrderReportVO |
| 销量 Top10 | /admin/report/top10 |
SalesTop10ReportVO |
日期参数在 Controller 层完成转换,业务统计放到 ReportServiceImpl。这样接口只负责接收参数和返回结果,按日循环、查询数据库、封装图表数据等工作都在服务层完成。
二、为什么返回逗号分隔的数据
趋势图需要两组一一对应的数据:横坐标日期和纵坐标数值。项目中的 VO 使用 String 保存逗号分隔的列表,例如营业额统计的返回对象:
java
public class TurnoverReportVO implements Serializable {
private String dateList;
private String turnoverList;
}
当日期列表为 2026-07-01,2026-07-02,2026-07-03,营业额列表为 406.0,1520.0,75.0 时,前端按逗号拆分后,就能让每个日期与同下标的营业额对应。这也是后续配置 ECharts xAxis 和 series.data 的数据基础。
三、营业额统计:只统计已完成订单
营业额统计先生成 begin 到 end 之间的完整日期列表,然后逐日查询。每一天都会被转换为当天的起止时间:00:00:00 到 23:59:59.999999999。
java
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
Map map = new HashMap<>();
map.put("begin", beginTime);
map.put("end", endTime);
map.put("status", Orders.COMPLETED);
Double turnover = orderMapper.sumByMap(map);
if (turnover == null) {
turnover = 0.0;
}
turnoverList.add(turnover);
}
Orders.COMPLETED 在实体中对应状态值 5。统计营业额时带上这个状态条件,避免待支付、已取消等订单金额被计入运营数据。
Mapper 中的聚合查询如下:
xml
<select id="sumByMap" 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>
sum(amount) 在某天没有匹配订单时会得到 null。服务层把它转为 0.0,保证日期列表与营业额列表长度始终一致,图表不会因为中间某天无订单而缺少一个数据点。
四、用户统计:新增用户与累计用户同时计算
用户统计同样按天循环,但一次循环会查询两组数据:
java
map.put("endTime", endTime);
Integer totalUser = userMapper.countByMap(map);
map.put("beginTime", beginTime);
Integer newUser = userMapper.countByMap(map);
totalUserList.add(totalUser);
newUserList.add(newUser);
第一次查询只传 endTime,SQL 会统计截至当天结束时的用户总数;第二次再补上 beginTime,SQL 就限定在当天创建时间范围内,得到当日新增用户数。
xml
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from user
<where>
<if test="beginTime!=null">
create_time >= #{beginTime}
</if>
<if test="endTime!=null">
and create_time < #{endTime}
</if>
</where>
</select>
UserReportVO 返回 dateList、totalUserList、newUserList 三项数据。页面可以用新增用户绘制每日增长趋势,用用户总量绘制累计增长趋势。
五、订单统计:订单数、有效订单数和完成率
订单统计把每天的订单数和有效订单数分别保存起来:
java
Integer orderCount = getOrderCount(beginTime, endTime, null);
Integer validOrderCount = getOrderCount(
beginTime, endTime, Orders.COMPLETED);
orderCountList.add(orderCount);
validOrderCountList.add(validOrderCount);
传入 null 时,Mapper 不会拼接状态条件,得到当天全部订单数;传入 Orders.COMPLETED 时,得到当天有效订单数。随后将两个列表求和,并计算整个统计区间的订单完成率:
java
Double orderCompletionRate = 0.0;
if (totalOrderCount != 0) {
orderCompletionRate = validOrderCount.doubleValue()
/ totalOrderCount.doubleValue();
}
这里先判断总订单数是否为 0。没有订单的时间范围直接返回 0.0,避免除以 0。OrderReportVO 除了两条按日曲线,还会返回 totalOrderCount、validOrderCount 和 orderCompletionRate,适合页面顶部的运营指标卡片。
六、销量 Top10:从订单明细中汇总商品销量
营业额和订单数来自 orders 表,商品销量要从订单明细表中计算。getSalesTop10 查询把 order_detail 与 orders 关联,只保留已完成订单,按商品名称分组后汇总数量:
xml
<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
select od.name, sum(od.number) number
from order_detail od, orders o
where od.order_id = o.id and o.status = 5
<if test="beginTime != null">
and o.order_time >= #{beginTime}
</if>
<if test="endTime != null">
and o.order_time <= #{endTime}
</if>
group by od.name
order by number desc
limit 0, 10
</select>
查询结果是 GoodsSalesDTO 列表。服务层分别提取 name 和 number,再通过逗号拼接成 SalesTop10ReportVO:
java
List<String> names = salesTop10.stream()
.map(GoodsSalesDTO::getName)
.collect(Collectors.toList());
List<Integer> numbers = salesTop10.stream()
.map(GoodsSalesDTO::getNumber)
.collect(Collectors.toList());
这组数据适合用横向柱状图展示:商品名称放在类目轴,销量放在数值轴。名称较长时,横向柱状图也比竖向柱状图更容易阅读。
七、Apache ECharts:把统计数据画成图表
Apache ECharts 是基于 JavaScript 的数据可视化图表库,可用于柱状图、折线图、饼图等交互式图表。对于统计页面,后端只负责提供日期和数值;图表标题、坐标轴、提示框、折线或柱体则由前端的 option 配置决定。
一个最小的 ECharts 图表需要四步。
1. 准备带宽高的 DOM 容器
html
<div id="turnoverChart" style="width: 600px; height: 400px;"></div>
容器必须有明确宽高,否则图表没有可绘制区域。
2. 初始化图表实例
javascript
var chart = echarts.init(document.getElementById('turnoverChart'));
这一步把 DOM 元素交给 ECharts,得到后续配置和更新使用的图表实例。
3. 组装统计接口返回的数据
javascript
var dates = result.data.dateList.split(',');
var turnover = result.data.turnoverList.split(',').map(Number);
dateList 拆分后用作横坐标;turnoverList 拆分并转为 Number 后,用作图表数值。两个数组下标必须对应,这也是服务端按天补 0 的原因。
4. 配置 option 并渲染
javascript
chart.setOption({
title: { text: '营业额趋势' },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: dates },
yAxis: { type: 'value' },
series: [{
name: '营业额',
type: 'line',
data: turnover
}]
});
xAxis.data 对应日期数组,series.data 对应营业额数组。将 type 改成 bar 就能以柱状图呈现同一份统计数据。ECharts 的 setOption 也可在拿到新的时间范围数据后再次调用,用于刷新图表。
八、接口自测与排查入口
可以先调用营业额接口验证返回结构:
text
GET /admin/report/turnoverStatistics?begin=2026-07-01&end=2026-07-07
检查时重点关注三点:
- dateList 中的日期数量应与查询区间天数一致。
- turnoverList、newUserList、orderCountList 等序列长度应与 dateList 一致。
- 销量 Top10 只应包含已完成订单中的商品;如果排行异常,先检查订单状态与订单明细关联条件。
图表没有显示时,先检查 DOM 是否已设置宽高,再检查接口返回的字符串是否成功拆分为数组,最后确认 xAxis.data 与 series.data 长度是否相同。
总结
Day11 的统计代码以日期范围为共同入口:服务层逐日汇总订单和用户数据,Mapper 使用聚合、分组与排序完成数据库统计,VO 再把结果整理为前端图表需要的数据序列。ECharts 负责把这些序列渲染为趋势图和排行图,后台运营数据也就从表记录变成了可直接观察的图表。