前言
在后台管理系统中,数据可视化报表是运营人员的核心工具。前端使用 ECharts 绘制折线图、柱状图,后端的核心工作就是按照 ECharts 要求的格式,封装时间区间内的统计数据。 本文基于一套 B 端管理后台,实现 4 大类统计报表:营业额统计、用户统计、订单统计、商品销量 TOP10,完整讲解需求、接口设计、代码实现、踩坑点。
一、核心思路
ECharts 绘图,需要后端返回两组核心数据:
- X 轴:日期列表(
datelist),作为图表横坐标; - Y 轴:对应指标数值列表,和日期一一对应。
⚠️重点难点:时间区间内没有数据的日期,也要填充 0,保证日期数组和数值数组长度完全一致,否则 ECharts 图表会错位。 解决方案:循环遍历起止日期,生成完整日期序列,再去数据库查询有数据的记录,最后合并补 0。
二、营业额统计(折线图)
2.1 需求分析
业务规则:
- 统计已完成订单的总金额作为当日营业额;
- 支持自定义时间区间查询;
- 前端 ECharts 折线图展示,X 轴为日期,Y 轴为营业额。
2.2 接口设计
| 项目 | 内容 |
|---|---|
| 请求路径 | /admin/report/turnoverStatistics |
| 请求方式 | GET |
| 请求参数 | begin:开始日期;end:结束日期 |
| 返回 VO | 日期字符串数组、营业额数组 |
2.3 后端代码实现
VO 实体
java
@Data
@Builder
public class TurnoverReportVO {
//日期列表,逗号分隔
private String dateList;
//营业额列表,逗号分隔
private String turnoverList;
}
Controller 层
java
@RestController
@RequestMapping("/admin/report")
@Slf4j
public class ReportController {
@Autowired
private ReportService reportService;
@GetMapping("/turnoverStatistics")
@ApiOperation("营业额统计")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
log.info("营业额统计,开始时间:{},结束时间:{}",begin,end);
return Result.success(reportService.getTurnoverStatistics(begin,end));
}
}
Service 核心逻辑
java
@Override
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
//1. 生成begin ~ end之间完整的日期集合
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)){
begin = begin.plusDays(1);
dateList.add(begin);
}
//2. 遍历每一天,查询当日营业额,无数据则为0
List<Double> turnoverList = 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("begin",beginTime);
map.put("end",endTime);
map.put("status", Orders.COMPLETED);
Double turnover = orderMapper.sumByMap(map);
turnover = turnover == null ? 0.0 : turnover;
turnoverList.add(turnover);
}
//3. 拼接成逗号分隔字符串,返回前端
return TurnoverReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.turnoverList(StringUtils.join(turnoverList, ","))
.build();
}
Mapper & XML
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>
三、用户统计(折线图)
3.1 需求分析
- 统计时间区间内:总用户量、新增用户量;
- X 轴为日期,两条折线:累计总用户、当日新增用户。
3.2 接口设计
路径:/admin/report/userStatistics,GET 请求,参数:begin、end
3.3 核心代码
java
@Override
public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
//生成完整日期序列
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)){
begin = begin.plusDays(1);
dateList.add(begin);
}
List<Integer> newUserList = new ArrayList<>();
List<Integer> totalUserList = 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);
//总用户数:截止到当天结束
Integer totalUser = userMapper.countByMap(map);
map.put("begin",beginTime);
//新增用户:当天新增
Integer newUser = userMapper.countByMap(map);
newUserList.add(newUser);
totalUserList.add(totalUser);
}
return UserReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.totalUserList(StringUtils.join(totalUserList, ","))
.newUserList(StringUtils.join(newUserList, ","))
.build();
}
要点:Mapper 使用 Map 传参,动态拼接 SQL 条件,统计用户创建时间。
四、订单统计(双折线图)
4.1 需求
统计区间内:订单总数、有效订单数,计算订单完成率 = 有效订单 / 总订单。
核心逻辑
java
@Override
public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = getDateList(begin, end);
List<Integer> orderCountList = new ArrayList<>();
List<Integer> validOrderCountList = 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("begin",beginTime);
map.put("end",endTime);
//当日全部订单
Integer total = orderMapper.countByMap(map);
map.put("status",Orders.COMPLETED);
//当日有效完成订单
Integer valid = orderMapper.countByMap(map);
orderCountList.add(total);
validOrderCountList.add(valid);
}
//计算区间内总订单、有效订单、完成率
Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();
Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();
Double orderCompletionRate = 0.0;
if(totalOrderCount != 0){
orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
}
return OrderReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.orderCountList(StringUtils.join(orderCountList, ","))
.validOrderCountList(StringUtils.join(validOrderCountList, ","))
.totalOrderCount(totalOrderCount)
.validOrderCount(validOrderCount)
.orderCompletionRate(orderCompletionRate)
.build();
}
知识点:Stream 流reduce累加集合,快速求和。
五、商品销量 TOP10(柱状图)
需求
查询时间段内,销量排名前 10 的商品,ECharts 柱状图展示。返回商品名称列表、销量列表。
java
@Override
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);
List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
//拆分名称、销量两个集合
List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
List<Integer> values = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
return SalesTop10ReportVO.builder()
.nameList(StringUtils.join(names, ","))
.numberList(StringUtils.join(values, ","))
.build();
}
Mapper SQL 核心:关联订单明细表,按商品分组,统计销量,排序取前 10。
六、开发踩坑总结
- 日期补 0:如果跳过无数据日期,X 轴和 Y 轴数据长度不一致,ECharts 图表渲染错乱,必须循环生成完整日期序列;
- LocalDateTime 时间边界 :
LocalTime.MIN是 00:00:00,LocalTime.MAX是 23:59:59.999999,防止跨天数据丢失; - null 值判断:数据库查询 sum 聚合函数,无数据返回 null,不做判断直接赋值会空指针;
- 前端 ECharts 格式约定:后端统一返回逗号分隔字符串,前端 split 切割成数组,前后端约定好数据格式;
- Map 传参动态 SQL :使用 MyBatis 动态标签
<if>,灵活拼接查询条件,复用 Mapper。
七、拓展与面试考点
- 如果数据量很大,每次查询数据库压力高,可以定时任务预统计,存入 Redis,报表直接读缓存;
- 可以增加导出 Excel 功能,把报表数据导出给运营;
- 面试常问:时间区间统计,某天没有数据怎么处理?
答:先预生成起止时间内全部日期,遍历日期逐个查询,查询结果为空则填充 0,保证坐标数组长度一致。