SpringBoot + ECharts 后台数据统计报表模块实战

前言

在后台管理系统中,数据可视化报表是运营人员的核心工具。前端使用 ECharts 绘制折线图、柱状图,后端的核心工作就是按照 ECharts 要求的格式,封装时间区间内的统计数据。 本文基于一套 B 端管理后台,实现 4 大类统计报表:营业额统计、用户统计、订单统计、商品销量 TOP10,完整讲解需求、接口设计、代码实现、踩坑点。

一、核心思路

ECharts 绘图,需要后端返回两组核心数据:

  1. X 轴:日期列表(datelist),作为图表横坐标;
  2. 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 &gt;= #{begin}
        </if>
        <if test="end != null">
            and order_time &lt;= #{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。

六、开发踩坑总结

  1. 日期补 0:如果跳过无数据日期,X 轴和 Y 轴数据长度不一致,ECharts 图表渲染错乱,必须循环生成完整日期序列;
  2. LocalDateTime 时间边界 :LocalTime.MIN是 00:00:00,LocalTime.MAX是 23:59:59.999999,防止跨天数据丢失;
  3. null 值判断:数据库查询 sum 聚合函数,无数据返回 null,不做判断直接赋值会空指针;
  4. 前端 ECharts 格式约定:后端统一返回逗号分隔字符串,前端 split 切割成数组,前后端约定好数据格式;
  5. Map 传参动态 SQL :使用 MyBatis 动态标签<if>,灵活拼接查询条件,复用 Mapper。

七、拓展与面试考点

  1. 如果数据量很大,每次查询数据库压力高,可以定时任务预统计,存入 Redis,报表直接读缓存;
  2. 可以增加导出 Excel 功能,把报表数据导出给运营;
  3. 面试常问:时间区间统计,某天没有数据怎么处理?

答:先预生成起止时间内全部日期,遍历日期逐个查询,查询结果为空则填充 0,保证坐标数组长度一致。

相关推荐
Pniubi6 分钟前
HashMap的扩容和树化边界
java·开发语言
Mikko721 分钟前
用 Java 复刻了高中时在塞班上玩的「熔岩矿车」:纯 Swing、零依赖、没有一张图片,一个 jar 双击就能玩
java·游戏开发·swing
知了学历孙老师23 分钟前
小自考的校考和统考,出题方和难度差在哪
java·大数据·前端
Liaiyang6626 分钟前
空圈容错:从 (0, 1) 夹缝本体论到 11 次跨域盲测
ai·大模型·量子力学·科学哲学·宇宙学·认识论·认知边界
Java爱好狂.41 分钟前
Java初学者如何一周速通SpringBoot!
java·spring boot·后端·spring·高并发·架构师·java面试
俊哥V1 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
秋饼2 小时前
LLM API密钥动态轮换实战:Spring Vault + Spring AI + 多Provider路由
java·ai·技术分享·后端开发
ShineWinsu2 小时前
对于JavaScript:操作元素的解析
开发语言·前端·javascript·面试·web·dom·元素
福兮说3 小时前
JSON 格式正确,数据就可靠吗?我用一份自编配置检查类型与空值
java·前端·json
高频因子挖掘机3 小时前
A股量化从日线升级到5分钟线:数据量增加后最先暴露哪些工程问题?
后端·github·api