[苍穹外卖]-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

测试

相关推荐
侠客行03176 小时前
Mybatis连接池实现及池化模式
java·mybatis·源码阅读
蛇皮划水怪6 小时前
深入浅出LangChain4J
java·langchain·llm
老毛肚8 小时前
MyBatis体系结构与工作原理 上篇
java·mybatis
风流倜傥唐伯虎8 小时前
Spring Boot Jar包生产级启停脚本
java·运维·spring boot
Yvonne爱编码8 小时前
JAVA数据结构 DAY6-栈和队列
java·开发语言·数据结构·python
Re.不晚8 小时前
JAVA进阶之路——无奖问答挑战1
java·开发语言
你这个代码我看不懂8 小时前
@ConditionalOnProperty不直接使用松绑定规则
java·开发语言
fuquxiaoguang9 小时前
深入浅出:使用MDC构建SpringBoot全链路请求追踪系统
java·spring boot·后端·调用链分析
琹箐9 小时前
最大堆和最小堆 实现思路
java·开发语言·算法
__WanG9 小时前
JavaTuples 库分析
java