课程内容

  • Apache ECharts

  • 营业额统计

  • 用户统计

  • 订单统计

  • 销量排名Top10

1. Apache ECharts

1.1 介绍

使用Echarts,重点在于研究当前图表所需的数据格式。通常是需要后端提供符合格式要求的动态数据,然后响应给前端来展示图表。

Apache ECharts 是一款基于 Javascript 的数据可视化图表库,提供直观,生动,可交互,可个性化定制的数据可视化图表。 官网地址:https://echarts.apache.org/zh/index.html

常见效果展示:

1). 柱形图

2). 饼形图

3). 折线图

总结:不管是哪种形式的图形,最本质的东西实际上是数据,它其实是对数据的一种可视化展示。

1.2 入门案例

Apache Echarts官方提供的快速入门:https://echarts.apache.org/handbook/zh/get-started/

效果展示:

实现步骤:

1). 引入echarts.js 文件(当天资料已提供)

2). 为 ECharts 准备一个设置宽高的 DOM

3). 初始化echarts实例

4). 指定图表的配置项和数据

5). 使用指定的配置项和数据显示图表

<!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>

2. 营业额统计

2.1 需求分析和设计

2.1.1 产品原型

营业额统计是基于折现图来展现,并且按照天来展示的。实际上,就是某一个时间范围之内的每一天的营业额。同时,不管光标放在哪个点上,那么它就会把具体的数值展示出来。并且还需要注意日期并不是固定写死的,是由上边时间选择器来决定。比如选择是近7天、或者是近30日,或者是本周,就会把相应这个时间段之内的每一天日期通过横坐标展示。

原型图:

业务规则:

  • 营业额指订单状态为已完成的订单金额合计

  • 基于可视化报表的折线图展示营业额数据,X轴为日期,Y轴为营业额

  • 根据时间选择区间,展示每天的营业额数据

通过上述原型图,设计出对应的接口。

注意:具体返回数据一般由前端来决定,前端展示图表,具体折现图对应数据是什么格式,是有固定的要求的。 所以说,后端需要去适应前端,它需要什么格式的数据,我们就给它返回什么格式的数据。

2.2 代码实现

实现过程:

  • 将开始时间、结束时间、订单状态作为查询条件发给Mapper层,Mapper返回一个sum(amount)

遇到的问题:

示例:

<select id="selectByTime" resultType="Order">
    SELECT * FROM orders
    WHERE 
        <!-- 正确使用小于等于和大于等于 -->
        create_time &lt;= #{endTime}
        AND create_time &gt;= #{beginTime}
        <!-- 不等于 -->
        AND status &lt;&gt; #{invalidStatus}
        <!-- 逻辑运算 -->
        AND (amount > 100 OR pay_type = 1)
</select>

总结:

  • 问:响应返回类型是String字符串,怎么样拼接比较方便易懂?
  • 答:先使用List<LocalDate/其他泛型> 获取一个list集合,再通过lang3包下的工具类
    StringUtils.join(localDateList, ",");  将集合转换为字符串类型。
  • 问:前端请求时间参数,只包含了年月日,没有时分秒,sql没法比较
  • 答:LocalDateTime beginTime = LocalDateTime.of(localDate, LocalTime.MIN);
    LocalDateTime endTime = LocalDateTime.of(localDate, LocalTime.MAX);
  • 问:向Mapper层传Map类型的参数,可以直接在sql语句中使用key值,还有什么类型的参数可以直接使用key值而不需要前缀?
  • 答:无需额外前缀的核心场景:基本类型 / 包装类JavaBean 属性List(用 list 引用)数组(用 array 引用)@Param 注解指定的参数名
  • 本质上,MyBatis 会将方法参数统一封装为 Map:
    • 单个参数:若为非 Map 类型,key 默认为参数类型(或通过 @Param 指定);
    • 多个参数:默认以 param1、param2... 为 key,推荐用 @Param 显式命名。
  • 问:在MyBatis中是否不能直接使用>和<,而是需要使用&lt;?还有哪些运算符号呢?
  • 答:在 MyBatis 的 XML 映射文件中,直接使用 <(小于号)和 >(大于号)会导致 XML 解析错误,因为这些符号是 XML 的保留字符(用于标记标签,如 <select><if> 等)。因此需要使用 XML 实体引用 来替代。
  • 必须替换的特殊符号:

    符号 含义 XML 实体引用 说明
    < 小于 &lt; 必须替换,否则 XML 解析报错
    > 大于 &gt; 理论上可直接使用,但建议统一替换以避免解析问题
    & 与运算 &amp; 必须替换,否则会被解析为实体引用的开始
    " 双引号 &quot; 属性值中使用双引号时需要替换
    ' 单引号 &apos; 属性值中使用单引号时需要替换

    其他常用运算符(可直接使用):

    在 MyBatis 的 XML 中,除了上述必须替换的符号外,其他运算符可以直接使用,例如:

  • 等于:=
  • 不等于:!= 或 <>(注意 <> 中的 < 需要替换为 &lt;,即 &lt;>
  • 大于等于:>=(建议写为 &gt;=
  • 小于等于:<=(建议写为 &lt;=
  • 逻辑运算符:andornot
  • 算术运算符:+-*/
  • 模糊匹配:like
  • 范围判断:innot in
  • 核心原则:XML 保留字符(<>&"')必须用实体引用替换,否则会导致 XML 解析失败。
  • 其他运算符(=!=>=<= 等)可直接使用,但为了统一规范,建议对 <> 相关的组合运算符(如 >= 写为 &gt;=)也使用实体引用,避免潜在问题。

Service层:

    /**
     * 统计指定时间范围内的营业额
     * @param begin
     * @param end
     * @return
     */
    @Override
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
        //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
        List<LocalDate> localDateList = new ArrayList<>();
        localDateList.add(begin);
        while (!begin.equals(end)){
            //日期计算
            begin=begin.plusDays(1);
            localDateList.add(begin);
        }
        //将List集合使用工具类组成字符串
        //lang3包中的工具类
        String dateList = StringUtils.join(localDateList, ",");

        //营业额,以逗号分隔,例如:406.0,1520.0,75.0
        List<Double>  doublesTurnoverList= new ArrayList<>();//存放每天的营业额
        for (LocalDate localDate : localDateList) {
            //营业额:当天状态为已完成订单金额合计
            //当天开始和结束时间点
            LocalDateTime beginTime = LocalDateTime.of(localDate, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(localDate, LocalTime.MAX);
            Map map=new HashMap<>();
            map.put("begin",beginTime);
            map.put("end",endTime);
            map.put("status", Orders.COMPLETED);
            Double turnover = orderMapper.sumByMap(map);
            //如果当天没有营业,营业额应该为0,但是数据库返回的是null,如果拼接成字符串会显示错误,所以要手动定义一下
            turnover = turnover == null ? 0.0 : turnover;
            doublesTurnoverList.add(turnover);
        }
        String turnoverList = StringUtils.join(doublesTurnoverList,",");

        //select sum(amount) from orders where 当天凌晨<order_time<当天午夜 and status=订单完成状态

        return TurnoverReportVO.builder()
                .dateList(dateList)
                .turnoverList(turnoverList)
                .build();
    }

Mapper层:

<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. 用户统计

3.1 需求分析和设计

3.1.1 产品原型

所谓用户统计,实际上统计的是用户的数量。通过折线图来展示,上面这根蓝色线代表的是用户总量,下边这根绿色线代表的是新增用户数量,是具体到每一天。所以说用户统计主要统计两个数据,一个是总的用户数量,另外一个是新增用户数量

原型图:

业务规则:

  • 基于可视化报表的折线图展示用户数据,X轴为日期,Y轴为用户数

  • 根据时间选择区间,展示每天的用户总量和新增用户量数据

3.1.2 接口设计

根据上述原型图设计接口。

3.2 代码开发

实现过程:

  • 用户统计功能和营业额统计功能实现过程基本一致,先通过前端请求参数获取日期集合,再遍历日期集合,获取创建时间截止于当天的全部用户,再向map中put当天开始时间,获取当天开始时间<创建时间<当天结束时间,获取到当天创建的新用户。最后将3个集合通过StringUtils工具转换为用","隔开的字符串。

Service层;

    /**
     * 根据时间区间统计用户数量
     * @param begin
     * @param end
     * @return
     */
    @Override
    public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
        //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
        List<LocalDate> localDateList = new ArrayList<>();
        localDateList.add(begin);
        while (!begin.equals(end)){
            begin=begin.plusDays(1);
            localDateList.add(begin);
        }
        String dateList = StringUtils.join(localDateList, ",");

        //存放每天的新增用户数据
        //select count(id) from user where creat_time<? and create_time>?,?表示当天
        List<Integer> integerNewUserList = new ArrayList<>();

        //存放每天的总用户数据
        //select count(id) from user where creat_time<? ,?表示当天
        List<Integer> integerTotalUserList = new ArrayList<>();
        for (LocalDate localDate : localDateList) {
            LocalDateTime beginTime = LocalDateTime.of(localDate,LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(localDate,LocalTime.MAX);

            Map map = new HashMap<>();
            map.put("end",endTime);

            Integer totalUser = userMapper.countByMap(map);
            integerTotalUserList.add(totalUser);

            map.put("begin",beginTime);
            Integer newUser = userMapper.countByMap(map);
            integerNewUserList.add(newUser);
        }
        String totalUserList = StringUtils.join(integerTotalUserList,",");
        String newUserList = StringUtils.join(integerNewUserList,",");

        return UserReportVO.builder()
                .dateList(dateList)
                .totalUserList(totalUserList)
                .newUserList(newUserList)
                .build();
    }

Mapper层:

    <select id="countByMap" resultType="java.lang.Integer">
        select count(id) from user
        <where>
            <if test="begin!=null">and create_time &gt; #{begin} </if>
            <if test="end!=null">and create_time &lt; #{end} </if>
        </where>
    </select>

4. 订单统计

4.1 需求分析和设计

4.1.1 产品原型

订单统计通过一个折现图来展现,折线图上有两根线,这根蓝色的线代表的是订单总数,而下边这根绿色的线代表的是有效订单数,指的就是状态是已完成的订单就属于有效订单,分别反映的是每一天的数据。上面还有3个数字,分别是订单总数、有效订单、订单完成率,它指的是整个时间区间之内总的数据。

原型图:

业务规则:

  • 有效订单指状态为 “已完成” 的订单

  • 基于可视化报表的折线图展示订单数据,X轴为日期,Y轴为订单数量

  • 根据时间选择区间,展示每天的订单总数和有效订单数

  • 展示所选时间区间内的有效订单数、总订单数、订单完成率,订单完成率 = 有效订单数 / 总订单数 * 100%

4.1.2 接口设计

根据上述原型图设计接口。

4.2 代码开发

实现过程:

  • 订单统计和用户统计、营业额统计实现思路完全一致,只是多了3个额外需要返回的字段。

遇到的问题

  • 问:List<Integer> list集合,如何获取每个元素相加之和?
  • 答:
            Integer totalOrderCount = integerDayTotalList.stream()
                    .reduce(Integer::sum)  // 累加器:将元素依次相加
                    .get();  // 获取计算结果

Service层:

    /**
     * 根据时间区间统计订单信息
     * @param begin
     * @param end
     * @return
     */
    @Override
    public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
        //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
        List<LocalDate> localDateList = new ArrayList<>();
        localDateList.add(begin);
        while (!begin.equals(end)){
            begin=begin.plusDays(1);
            localDateList.add(begin);
        }
        String dateList = StringUtils.join(localDateList, ",");
        //每日订单数,以逗号分隔,例如:260,210,215
        List<Integer> integerDayTotalList = new ArrayList<>();
        //每日有效订单数,以逗号分隔,例如:20,21,10
        List<Integer> integerDayCompletedList = new ArrayList<>();

        for (LocalDate localDate : localDateList) {
            LocalDateTime beginTime = LocalDateTime.of(localDate,LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(localDate,LocalTime.MAX);
            
            Map map=new HashMap<>();
            map.put("begin",beginTime);
            map.put("end",endTime);
            //每天订单数
            Integer dayTotal = orderMapper.countByMap(map);
            integerDayTotalList.add(dayTotal);
            
            map.put("status",Orders.COMPLETED);
            //每天有效订单数
            Integer dayCompleted = orderMapper.countByMap(map);
            integerDayCompletedList.add(dayCompleted);

        }

        //每日订单数,以逗号分隔,例如:260,210,215
        String orderCountList = StringUtils.join(integerDayTotalList,",");
        //每日有效订单数,以逗号分隔,例如:20,21,10
        String validOrderCountList = StringUtils.join(integerDayCompletedList,",");
        //订单总数
        Integer totalOrderCount = integerDayTotalList.stream().reduce(Integer::sum).get();
        //有效订单数
        Integer validOrderCount = integerDayCompletedList.stream().reduce(Integer::sum).get();
        //订单完成率
        Double orderCompletionRate = 0.0;
        if(totalOrderCount!=0){
            orderCompletionRate = validOrderCount.doubleValue()/totalOrderCount;
        }

        return OrderReportVO.builder()
                .dateList(dateList)
                .orderCountList(orderCountList)
                .validOrderCountList(validOrderCountList)
                .totalOrderCount(totalOrderCount)
                .validOrderCount(validOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .build();
    }

Mapper层:

    <select id="countByMap" resultType="java.lang.Integer">
        select count(id) 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>

5. 销量排名Top10

5.1 需求分析和设计

5.1.1 产品原型

所谓销量排名,销量指的是商品销售的数量。项目当中的商品主要包含两类:一个是套餐,一个是菜品,所以销量排名其实指的就是菜品和套餐销售的数量排名。通过柱形图来展示销量排名,这些销量是按照降序来排列,并且只需要统计销量排名前十的商品。

原型图:

业务规则:

  • 根据时间选择区间,展示销量前10的商品(包括菜品和套餐)

  • 基于可视化报表的柱状图降序展示商品销量

  • 此处的销量为商品销售的份数

5.1.2 接口设计

根据上述原型图设计接口。

5.2 代码开发

遇到的问题:

  • 问:sql部分,这里涉及了order和order_detail两个表查询数据,为什么不能使用左外链接?而使用了内连接?
  • 答:是否使用左外连接,取决于是否需要保留左表中不满足连接条件的记录
    • 内连接:只留 "双方都匹配" 的记录 → 适合 "必须双方都有效" 的场景(如有效订单的销量统计)。
    • 左外连接:保留 "左表所有记录,不管右表是否匹配" → 适合 "必须保留左表全部数据" 的场景(如展示所有菜品,即使它没有分类)。
    • 而这里我们的目的是优先满足 “双方匹配” 这个点,所以要使用内连接。

Service层:

    /**
     * 查询指定时间区间内的销量排名top10
     * @param begin
     * @param end
     * @return
     */
    @Override
    public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
        LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);

        List<GoodsSalesDTO> list = orderMapper.getSalesTop10(beginTime,endTime);

        List<String> NameList = list.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
        String nameList = StringUtils.join(NameList,",");

        List<Integer> NumberList = list.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
        String numberList = StringUtils.join(NumberList, ",");
        
        return SalesTop10ReportVO.builder()
                .nameList(nameList)
                .numberList(numberList)
                .build();
    }

Mapper层:

    <select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
        select od.name 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 order_time &gt; #{beginTime}</if>
            <if test="endTime!=null">and order_time &lt;= #{endTime}</if>
        group by name
        order by number desc
        limit 0,10
    </select>

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐