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,保证坐标数组长度一致。

相关推荐
云安全助手1 小时前
自建接入VS聚合平台:企业 AI 调用的选型思路与迁移成本拆解
java·大数据·数据库·人工智能·ai大模型
霸道流氓气质1 小时前
Dify 可视化 LLM 应用开发平台完全指南:从Workflow编排到Java生产级集成实战
java·开发语言
玩AI的奶茶1 小时前
配一次环境像装修一次房:哪些云 GPU 平台能把它留下来?
人工智能·ai·gpu算力·token·算力租赁
愛芳芳1 小时前
element-plus+vite+vue3+pinpa+mock+typeScript搭建经典的电商管理后台
前端·javascript·typescript
m0_587383001 小时前
24小时自助健身房软硬件解决方案实战:从架构设计到部署指南
java·spring·小程序·架构·需求分析
LucianaiB1 小时前
用 HarmonyOS 做一张会写诗的月夜明信片:追月的完整开发复盘
华为·ai·harmonyos·skill
CallFay云起未来2 小时前
AI客服上线后多久才能回本?从TCO到ROI的完整测算方法
java·大数据·人工智能·架构·文心一言
Sam_Deep_Thinking2 小时前
什么是CountDownLatch?
java·后端·面试·程序员
小蒜学长2 小时前
基于SpringBoot和Vue的低卡食品销售系统的设计与实现(代码+数据库+LW)
java·后端·springboot·健康管理·低卡食品销售系统