[苍穹外卖]-11数据可视化接口开发

ECharts

Apache ECharts是一款基于JavaScript的数据可视化图表库, 提供直观, 生动,可交互, 可定制的数据可视化图表

入门案例: 使用Echarts, 前端关注图表的配置, 不同的配置影响展示的效果, 后端关注图表所需要的数据格式

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

营业额统计接口

需求分析

业务规则

  1. 营业额是指订单状态为已完成的订单金额总计
  2. 基于可视化报表展示营业数据, x轴是日期, y轴是营业额
  3. 根据时间选择区间, 展示每天的营业额数据

接口设计

设计VO: 根据接口的返回值, 设计TurnoverReportVO对象, 用于封装返回给前端的数据

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TurnoverReportVO implements Serializable {

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
    private String dateList;

    //营业额,以逗号分隔,例如:406.0,1520.0,75.0
    private String turnoverList;

}

Controller: 新建controller/admin/ReportController

/** * 数据统计相关接口
 */
@RestController
@RequestMapping("/admin/report")
@Api(tags = "数据统计相关接口")
@Slf4j
public class ReportController {

    @Autowired
    private ReportService reportService;
      /**
     * 营业额统计
     *
     * @param begin
     * @param end
     * @return
     */
    @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: 新建ReportService接口以及实现类

public interface ReportService {
    /**
     * 统计指定时间区间内的营业额数据
     * @param begin
     * @param end
     * @return
     */
    TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end);
}

@Service
public class ReportServiceImpl implements ReportService {

    @Autowired
    private OrderMapper orderMapper;

    /**
     * 统计指定时间区间内的营业额数据
     *
     * @param begin
     * @param end
     * @return
     */
        public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
        // 当前集合用于存放从begin到end范围内的每天日期
        List<LocalDate> dateList = new ArrayList<>();

        dateList.add(begin);

        while (!begin.equals(end)) {
            // 计算日期:  开始日期+1,就是后一天的数据
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
             // 当前集合用于存放日期范围内每天的营业额
        List<Double> turnoverList = new ArrayList<>();
        for (LocalDate date : dateList) {
            // 查询date日期对应的营业额数据
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); // 计算一天的开始时间
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); // 计算一天的结束时间

            // 查询语句
            // 查询当天的00:00和23.59之间的并且状态是已完成的营业额,然后求和
            // select sum(amount) from orders where order_time > beginTime and order_time < endTime and status = 5
            Map 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);
        }
            // 封装并返回数据
        return TurnoverReportVO.builder()
                .dateList(StringUtils.join(dateList, ","))
                .turnoverList(StringUtils.join(turnoverList, ","))
                .build();
    }
}
  1. 日期对象是可以计算的, 使用 plusDays(1) 方法在开始日期的基础上加一天, 得到后一天的日期
  2. 得到日期集合之后, 通过StringUtils工具类的join()方法, 把日期集合转成逗号分隔的字符串
  3. 查询数据时, 我们可以先根据需求把sql写出来, 然后再去拼凑需要的参数, 这样编码会比较顺畅

Mapper

@Mapper
public interface OrderMapper {
    /**
     * 根据动态条件统计营业额数据
     * @param map
     * @return
     */
    Double sumByMap(Map map);

}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">

      <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>                                
</mapper>

功能测试

用户统计接口

需求分析

业务规则

  1. 基于折线图展示用户数据, X轴为日期, Y轴为用户数
  2. 根据选择的时间区间, 展示每天的用户总量和新增的用户量

接口设计

设计VO: 根据接口的返回值, 设计VO实体类,用于封装返回给前端的数据

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserReportVO implements Serializable {

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
    private String dateList;

    //用户总量,以逗号分隔,例如:200,210,220
    private String totalUserList;

    //新增用户,以逗号分隔,例如:20,21,10
    private String newUserList;

}

controller

/** * 数据统计相关接口
 */
@RestController
@RequestMapping("/admin/report")
@Api(tags = "数据统计相关接口")
@Slf4j
public class ReportController {

    @Autowired
    private ReportService reportService;

     /**
     * 用户统计
     *
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/userStatistics")
    @ApiOperation("用户统计")
    public Result<UserReportVO> userStatictics(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
        log.info("用户数据统计:{},{}", begin, end);
        return Result.success(reportService.getUserStatistics(begin, end));
    }
 
}

service

public interface ReportService {
   /**
     * 统计指定时间区间内的用户数据
     * @param begin
     * @param end
     * @return
     */
    UserReportVO getUserStatistics(LocalDate begin, LocalDate end);
}

@Service
public class ReportServiceImpl implements ReportService {
   /**
     * 统计指定时间区间内的用户数据
     *
     * @param begin
     * @param end
     * @return
     */
    public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
        // 存放从begin到end之间的每天对应的日期
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);

        while (!begin.equals(end)) {
            begin = begin.plusDays(1);
            dateList.add(begin);
        }
        // 存放每天的新增用户数量
        // select count(id) from user where create_time < ? and create_time > ?
        List<Integer> newUserList = new ArrayList<>();
        // 存放每天的总用户数量
         // select count(id) from user where create_time < ?
        List<Integer> totalUserList = new ArrayList<>();
        for (LocalDate date : dateList) {
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); // 得到一天的开始时间
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); // 得到一天的结束时间
            Map map = new HashMap<>();
            map.put("end", endTime);
            // 根据结束时间查询用户总数
            Integer totalUser = userMapper.countByMap(map);
            // 根据开始和结束时间查询新增用户数
            map.put("begin", beginTime);
            Integer newUser = userMapper.countByMap(map);
             totalUserList.add(totalUser);
            newUserList.add(newUser);
        }

        // 封装返回结果
        return UserReportVO
                .builder()
                .dateList(StringUtils.join(dateList, ','))
                .newUserList(StringUtils.join(newUserList, ','))
                .totalUserList(StringUtils.join(totalUserList, ','))
                .build();
    }
}
  1. 先把数据查出来放在集合中, 然后再把集合处理成逗号分隔的字符串

mapper

@Mapper
public interface OrderMapper {
    /**
     * 根据动态条件统计用户数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">
    <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>              
</mapper>

功能测试

订单统计接口

需求分析

业务规则

  1. 有效订单是"已完成"的订单
  2. 基于可视化图表展示数据, x轴为日期, y轴为订单数量
  3. 根据选择的时间区间, 展示每天的订单总数和有效订单数
  4. 展示所选区间内的有效订单数, 总订单数,订单完成率, 订单完成率 =(有效订单数/总订单数 )*100%

接口设计

设计VO: 根据接口返回的结果设计VO实体类

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderReportVO implements Serializable {

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
    private String dateList;

    //每日订单数,以逗号分隔,例如:260,210,215
    private String orderCountList;
    //每日有效订单数,以逗号分隔,例如:20,21,10
    private String validOrderCountList;

    //订单总数
    private Integer totalOrderCount;

    //有效订单数
    private Integer validOrderCount;

    //订单完成率
    private Double orderCompletionRate;

}

controller

/** * 数据统计相关接口
 */
@RestController
@RequestMapping("/admin/report")
@Api(tags = "数据统计相关接口")
@Slf4j
public class ReportController {

    @Autowired
    private ReportService reportService;

    /**
     * 订单统计
     *
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/ordersStatistics")
    @ApiOperation("订单统计统计")
    public Result<OrderReportVO> ordersStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
        log.info("用户数据统计:{},{}", begin, end);
        return Result.success(reportService.getOrderStatistics(begin, end));
    }
 
}

service

public interface ReportService {
   /**
     * 统计指定时间区间内的订单数据
     * @param begin
     * @param end
     * @return
     */
    OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end);
}

@Service
public class ReportServiceImpl implements ReportService {
   /**
     * 统计指定时间区间内的订单数据
     *
     * @param begin
     * @param end
     * @return
     */
    public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
        // 存放从begin到end之间的每天对应的日期
        List<LocalDate> dateList = new ArrayList<>();

        dateList.add(begin);
              while (!begin.equals(end)) {
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        // 存放每天的订单总数
        // select count(id) from order where order_time > ? and order_time < ?
        List<Integer> orderCountList = new ArrayList<>();
        // 存放每天的有效订单数
        // select count(id) from order where order_time > ? and order_time < ? and ststus = 5
        List<Integer> validOrderCountList = new ArrayList<>();
        // 遍历时间集合, 查询每天的有效订单数和订单总数
        for (LocalDate date : dateList) {
            // 查询每天的订单总数
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);  // 计算一天的开始使劲
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);  // 计算一天的结束时间
            Integer orderCount = getOrderCount(beginTime, endTime, null);
            orderCountList.add(orderCount);
             // 查询每天的有效订单数
            Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);
            validOrderCountList.add(validOrderCount);
        }
        // 计算时间区间内的订单总数量
        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, ","))
                .validOrderCount(validOrderCount)
                .totalOrderCount(totalOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .build();
    }
      /**
     * 根据条件统计订单数量
     *
     * @param begin
     * @param end
     * @return
     */
    private Integer getOrderCount(LocalDateTime begin, LocalDateTime end, Integer status) {
        HashMap map = new HashMap();
        map.put("begin", begin);
        map.put("end", end);
        map.put("status", status);

        return orderMapper.countByMap(map);
    }
}
  1. 每日订单总数和每日有效订单数, 我们可以通过查询数据库得到
  2. 这里我们通过stream流遍历集合中的数据, 进行求和, 得的订单总数和有效订单总数

mapper

@Mapper
public interface OrderMapper {
    /*** 根据动态条件统计用户数量
     * @param map
     * @return
     */
    Integer countByMap(Map map); 
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">
    <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>                
</mapper>                          

测试

销量排名Top10

需求分析

业务规则

  1. 根据时间选择区间, 展示销量前10的商品(包括菜品和套餐)
  2. 基于可视化图表的柱状图展示商品销量
  3. 销量是指商品销售的份数

接口设计

设计VO: 根据接口的返回结果设计VO实体类

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class SalesTop10ReportVO implements Serializable {

    //商品名称列表,以逗号分隔,例如:鱼香肉丝,宫保鸡丁,水煮鱼
    private String nameList;

    //销量列表,以逗号分隔,例如:260,215,200
    private String numberList;

}

comtroller

/** * 数据统计相关接口
 */
@RestController
@RequestMapping("/admin/report")
@Api(tags = "数据统计相关接口")
@Slf4j
public class ReportController {

    @Autowired
    private ReportService reportService;
    /**
     * 销量Top10统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/top10")
    @ApiOperation("销量Top10统计")
    public Result<SalesTop10ReportVO> top10(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
        log.info("销量Top10统计:{},{}", begin, end);
        return Result.success(reportService.getSalesTop10(begin, end));
    }
 
}

设计DTO: 设计GoodsSalesDTO用来封装单条的查询结果

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class GoodsSalesDTO implements Serializable {
    //商品名称, 例如"可乐鸡翅"
    private String name;

    //销量, 例如"100"
    private Integer number;
}

service

public interface ReportService {
   /**
     * 统计指定时间区间内的销量排名前10商品
     * @param begin
     * @param end
     * @return
     */
    SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end);
}

@Service
public class ReportServiceImpl implements ReportService {
   /**
     * 统计指定时间区间内的销量排名前10商品
     *
     * @param begin
     * @param end
     * @return
     */
    public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
        LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN); //计算一天的开始时间
        LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX); //计算一天的结束时间

        // 查询数据, 例如[{可乐鸡翅: 100}]
        List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
        // 把所有name封装到List集合中
        List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
        // 把集合中的name使用,分割转成String
        String nameList = StringUtils.join(names, ",");
        // 把所有number封装到List集合中
        List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
        String numberList = StringUtils.join(numbers, ",");

        return SalesTop10ReportVO
                        .builder()
                .nameList(nameList)
                .numberList(numberList)
                .build();
        }
}
  1. 数据库查询出来的数据被封装到数组对象中, 和前端需要的数据不符, 所有我们我们要转换一下
  2. 使用遍历的方式转换是比较麻烦, 所以这里使用stream流的形式操作集合
  3. 最终把数组中每个对象的name值封装成一个逗号分隔的字符串, 例如"可乐鸡翅,红烧肉,烤鸭"
  4. 把数组中每个对象的number值封装成一个逗号分隔的字符串, 例如"100,50,88"
  5. 最终把这两个字符串封装到VO对象中,返回给前端使用

mapper

@Mapper
public interface OrderMapper {
    /**
     * 统计指定区间内的销量排名前10
     * @param begin
     * @param end
     * @return
     */
    List<GoodsSalesDTO> getSalesTop10(LocalDateTime begin, LocalDateTime end);
}

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.sky.mapper.OrderMapper">
     <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="begin != null">
            and o.order_time &gt; #{begin}
        </if>
        <if test="end != null">
            and o.order_time &lt; #{end}
        </if>
        group by od.name
        order by number desc
        limit 0,10
    </select>                  
</mapper>
  1. 数据可视化的核心还是数据, 所以重点就是sql的编写
  2. 对于复杂的查询, 我们可以现在sql工具中写好查询语句, 测试好之后在改造成动态sql
  3. selset od.name, sum(od.number) number from order_detail od, orders o where od.order_id = o.id and o.order_time > '2020-01-01 00:00:00' and o.order_time < '2021-01-01 00:00:00'

group by od.name

order by number desc

limit 0,10

测试

相关推荐
儿时可乖了21 分钟前
使用 Java 操作 SQLite 数据库
java·数据库·sqlite
ruleslol22 分钟前
java基础概念37:正则表达式2-爬虫
java
xmh-sxh-131439 分钟前
jdk各个版本介绍
java
天天扭码1 小时前
五天SpringCloud计划——DAY2之单体架构和微服务架构的选择和转换原则
java·spring cloud·微服务·架构
程序猿进阶1 小时前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺1 小时前
Spring Boot框架Starter组件整理
java·spring boot·后端
小曲程序1 小时前
vue3 封装request请求
java·前端·typescript·vue
陈王卜1 小时前
django+boostrap实现发布博客权限控制
java·前端·django