苍穹外卖day11-StringUtils、Java 8 Stream API、LocalDateTime.of、group by
课程内容
-
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 <= #{endTime}
AND create_time >= #{beginTime}
<!-- 不等于 -->
AND status <> #{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显式命名。
- 单个参数:若为非 Map 类型,key 默认为参数类型(或通过
- 问:在MyBatis中是否不能直接使用>和<,而是需要使用<?还有哪些运算符号呢?
- 答:在 MyBatis 的 XML 映射文件中,直接使用
<(小于号)和>(大于号)会导致 XML 解析错误,因为这些符号是 XML 的保留字符(用于标记标签,如<select>、<if>等)。因此需要使用 XML 实体引用 来替代。 -
必须替换的特殊符号:
符号 含义 XML 实体引用 说明 <小于 <必须替换,否则 XML 解析报错 >大于 >理论上可直接使用,但建议统一替换以避免解析问题 &与运算 &必须替换,否则会被解析为实体引用的开始 "双引号 "属性值中使用双引号时需要替换 '单引号 '属性值中使用单引号时需要替换 其他常用运算符(可直接使用):
在 MyBatis 的 XML 中,除了上述必须替换的符号外,其他运算符可以直接使用,例如:
- 等于:
= - 不等于:
!=或<>(注意<>中的<需要替换为<,即<>) - 大于等于:
>=(建议写为>=) - 小于等于:
<=(建议写为<=) - 逻辑运算符:
and、or、not - 算术运算符:
+、-、*、/ - 模糊匹配:
like - 范围判断:
in、not in - 核心原则:XML 保留字符(
<、>、&、"、')必须用实体引用替换,否则会导致 XML 解析失败。 - 其他运算符(
=、!=、>=、<=等)可直接使用,但为了统一规范,建议对<、>相关的组合运算符(如>=写为>=)也使用实体引用,避免潜在问题。
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 > #{begin}</if>
<if test="end!=null">and order_time <= #{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 > #{begin} </if>
<if test="end!=null">and create_time < #{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 > #{begin}</if>
<if test="end!=null">and order_time <= #{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 > #{beginTime}</if>
<if test="endTime!=null">and order_time <= #{endTime}</if>
group by name
order by number desc
limit 0,10
</select>
更多推荐




所有评论(0)