苍穹外卖 Day11 报表:优化数据库连接,多维度统计功能实现
个人水平有限,可能有错,欢迎大家在评论区指正和讨论。
同时,感谢参考文献中的作者。
文章目录
1. 引言
为减少苍穹外卖day11报表部分数据库连接,本文在上一篇文章[1]基础上给出多个功能实现,提取出公共函数。核心思想是不在Service层使用循环多次查询,而是修改sql只做一次连接。
注意存在命名可以优化。本文在mapper中类的命名可以优化,比如DailyTurnover,可以命名DailyTurnoverDTO,同时还有一个bug修复过程(方法中已改,思考过程见2.4部分)。
2. 方法
2.1 Controller
ReportController
package com.sky.controller.admin;
import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDate;
/**
* 报表
*/
@RestController
@RequestMapping("/admin/report")
@Slf4j
@Api(tags = "数据统计相关接口")
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);
TurnoverReportVO turnoverReportVO = reportService.getTurnoverStatistics(begin, end);
return Result.success(turnoverReportVO);
}
/**
* 用户统计接口
* @param begin
* @param end
* @return
*/
@GetMapping("/userStatistics")
@ApiOperation("用户统计接口")
public Result<UserReportVO> userStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end
){
log.info("用户统计接口:{},{}", begin, end);
UserReportVO userStatistics = reportService.getUserStatistics(begin, end);
return Result.success(userStatistics);
}
/**
* 订单统计接口
* @param begin 开始日期
* @param end 结束日期
* @return Result
*/
@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);
OrderReportVO orderReportVO = reportService.getOrdersStatistics(begin, end);
return Result.success(orderReportVO);
}
/**
* 销量排名统计
* @param begin 开始日期
* @param end 结束日期
* @return Result
*/
@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);
SalesTop10ReportVO salesTop10ReportVO = reportService.getTop10(begin, end);
return Result.success(salesTop10ReportVO);
}
}
2.2 Service
ReportService
package com.sky.service;
import com.sky.vo.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.time.LocalDate;
public interface ReportService {
/**
* 根据时间区间统计营业额
* @param begin
* @param end
* @return
*/
TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end);
/**
* 得到总用户量和新增用户数
* @param begin
* @param end
* @return
*/
UserReportVO getUserStatistics(LocalDate begin, LocalDate end);
/**
* 根据时间区间统计订单数量
* @param begin 开始日期
* @param end 结束日期
* @return OrderStatisticsVO
*/
OrderReportVO getOrdersStatistics(LocalDate begin, LocalDate end);
/**
* 销量排名统计
* @param begin 开始日期
* @param end 结束日期
* @return SalesTop10ReportVO
*/
SalesTop10ReportVO getTop10(LocalDate begin, LocalDate end);
}
ReportServiceImpl
package com.sky.service.impl;
import com.sky.dto.GoodsSalesDTO;
import com.sky.entity.Orders;
import com.sky.exception.BaseException;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.UserMapper;
import com.sky.mapperResult.DailyNewUsers;
import com.sky.mapperResult.DailyOrderCount;
import com.sky.mapperResult.DailyTurnover;
import com.sky.mapperResult.TotalUser;
import com.sky.service.ReportService;
import com.sky.service.WorkspaceService;
import com.sky.vo.*;
import org.apache.commons.lang.StringUtils;
import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
public class ReportServiceImpl implements ReportService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserMapper userMapper;
/**
* 根据时间区间统计营业额
* @param begin
* @param end
* @return
*/
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
//1.完成 dateList
List<LocalDate> dateList = getDatelist(begin, end);
//2.完成 turnoverList
LocalDateTime beginTime = LocalDateTime.of(dateList.get(0), LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(dateList.get(dateList.size() - 1), LocalTime.MAX);
Map<String, Object> map = new HashMap<>();
map.put("status", Orders.COMPLETED);
map.put("begin", beginTime);
map.put("end", endTime);
// 获取按日期分组的营业额数据
List<DailyTurnover> turnoverSource= orderMapper.sumByDateRange(map);
// 使用通用方法填充营业额数据
List<Double> turnoverList = convertToOrderedList(turnoverSource,
DailyTurnover::getOrderDate,
DailyTurnover::getDailyAmount,
dateList,
0.0);
//3.返回封装好的数据TurnoverReportVO
return TurnoverReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.turnoverList(StringUtils.join(turnoverList, ","))
.build();
}
/**
* 得到总用户量和新增用户数
* @param begin
* @param end
* @return
*/
public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
//1. 完成 dateList
List<LocalDate> dateList = getDatelist(begin, end);
//2. 完成 newUserList
LocalDateTime beginTime = LocalDateTime.of(dateList.get(0), LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(end, LocalTime.of(23, 59, 59));
Map<String, Object> map = new HashMap<>();
map.put("begin", beginTime);
map.put("end", endTime);
// 获取按日期分组的新增用户数数据
List<DailyNewUsers> newUsersSource = userMapper.countNewUsersByDateRange(map);
// 使用通用方法填充数据
List<Integer> newUserList = convertToOrderedList(newUsersSource,
DailyNewUsers::getUserDate,
DailyNewUsers::getDailyNewUsers,
dateList,
0);
//3. 完成 totalUserList
// 获取截止某日的总用户数, endTime 使用 LocalTime.MAX ,选择7日时,这里sql会返回8条记录,因为 endTime 精度太高,某些数据库会将其四舍五入到第二天,从而导致多返回一条记录。
List<TotalUser> totalUserSource = userMapper.countTotalUsersByDateRange(map);
// 使用通用方法填充数据
List<Integer> totalUserList = convertToOrderedList(totalUserSource,
TotalUser::getUserDate,
TotalUser::getTotalUsers,
dateList,
0);
//4.返回封装好的数据TurnoverReportVO
return UserReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.newUserList(StringUtils.join(newUserList, ","))
.totalUserList(StringUtils.join(totalUserList, ","))
.build();
}
/**
* 根据时间区间统计订单数量
* @param begin 开始日期
* @param end 结束日期
* @return OrderStatisticsVO
*/
public OrderReportVO getOrdersStatistics(LocalDate begin, LocalDate end) {
//1. 完成 dateList
List<LocalDate> dateList = getDatelist(begin, end);
//2. 完成 orderCountList
LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);
Map<String, Object> map = new HashMap<>();
map.put("begin", beginTime);
map.put("end", endTime);
// 获取按日期分组的订单数量
List<DailyOrderCount> orderCountSource = orderMapper.countOrderByDateRange(map);
// 使用通用方法填充数据
List<Integer> orderCountList = convertToOrderedList(orderCountSource,
DailyOrderCount::getOrderDate,
DailyOrderCount::getDailyOrderCount,
dateList,
0);
//3. 完成 validOrderCountList
map.put("status", Orders.COMPLETED);
// 获取按日期分组的订单数量
List<DailyOrderCount> validOrderCountSource = orderMapper.countOrderByDateRange(map);
// 使用通用方法填充数据
List<Integer> validOrderCountList = convertToOrderedList(validOrderCountSource,
DailyOrderCount::getOrderDate,
DailyOrderCount::getDailyOrderCount,
dateList,
0);
//4. 时间区间内的总订单数
Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();
//5. 时间区间内的总有效订单数
Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();
//6. 订单完成率
Double orderCompletionRate = 0.0;
if(totalOrderCount != 0){
orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
}
//7. 返回封装好的数据TurnoverReportVO
return OrderReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.orderCountList(StringUtils.join(orderCountList, ","))
.validOrderCountList(StringUtils.join(validOrderCountList, ","))
.totalOrderCount(totalOrderCount)
.validOrderCount(validOrderCount)
.orderCompletionRate(orderCompletionRate)
.build();
}
/**
* 销量排名统计
* @param begin 开始日期
* @param end 结束日期
* @return SalesTop10ReportVO
*/
public SalesTop10ReportVO getTop10(LocalDate begin, LocalDate end) {
//1. 完成 dateList
List<LocalDate> dateList = getDatelist(begin, end);
//2. 完成 goodsSalesDTOList
LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);
Map<String, Object> map = new HashMap<>();
map.put("begin", beginTime);
map.put("end", endTime);
map.put("status", Orders.COMPLETED);
// 获取按日期查询的top10
List<GoodsSalesDTO> goodsSalesDTOList = orderMapper.getSalesTop10(map);
//得到 nameList numberList
List<String> names = goodsSalesDTOList.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
String nameList = StringUtils.join(names, ",");
List<Integer> numbers = goodsSalesDTOList.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
String numberList = StringUtils.join(numbers, ",");
//3. 返回封装好的数据 SalesTop10ReportVO
return SalesTop10ReportVO.builder()
.nameList(nameList)
.numberList(numberList)
.build();
}
/**
* 得到begin-end的日期列表
* @param begin
* @param end
* @return
*/
private static List<LocalDate> getDatelist(LocalDate begin, LocalDate end) {
//1.完成 dateList
List<LocalDate> dateList = new ArrayList<>();
//begin end合法性判断
if (begin == null || end == null || begin.isAfter(end)) {
throw new BaseException("begin or end is error!");
}
while (!begin.isAfter(end)){
dateList.add(begin);
begin = begin.plusDays(1);
}
return dateList;
}
/**
* 将源列表按日期顺序转换为数值列表(支持Integer或Double)
*
* @param sourceList 源数据列表
* @param dateExtractor 从源数据提取日期的方法
* @param valueExtractor 从源数据提取数值的方法
* @param dateList 目标日期顺序列表
* @param defaultValue 默认填充值(需与T一致)
* @return 按日期顺序排列的数值列表
*/
public static <T, R extends Number> List<R> convertToOrderedList(
List<T> sourceList,
Function<T, LocalDate> dateExtractor,
Function<T, R> valueExtractor,
List<LocalDate> dateList,
R defaultValue) {
// 将查询结果转为日期到对应数值的映射
Map<LocalDate, R> dateToValue = new HashMap<>();
for (T item : sourceList) {
LocalDate date = dateExtractor.apply(item);
R value = valueExtractor.apply(item);
dateToValue.put(date, value);
}
// 按照日期列表顺序填充数值数据
List<R> resultList = new ArrayList<>();
for (LocalDate date : dateList) {
R value = dateToValue.getOrDefault(date, defaultValue);
resultList.add(value);
}
return resultList;
}
}
2.3 Mapper
2.3.1 OrderMapper
DailyTurnover:类的命名可以优化,比如DailyTurnover,可以命名DailyTurnoverDTO。其他类同理。
package com.sky.mapperResult;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDate;
/**
* 每天营业额
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DailyTurnover implements Serializable {
//订单日期(天为单位)
private LocalDate orderDate;
//每天营业额
private Double dailyAmount;
}
DailyOrderCount
package com.sky.mapperResult;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDate;
/**
* 每天订单数
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DailyOrderCount implements Serializable {
//订单日期(天为单位)
private LocalDate orderDate;
//每天订单数
private Integer dailyOrderCount;
}
GoodsSalesDTO
package com.sky.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class GoodsSalesDTO implements Serializable {
//商品名称
private String name;
//销量
private Integer number;
}
OrderMapper.java
@Mapper
public interface OrderMapper {
/**
* 根据动态条件统计营业额
* @param map
* @return
*/
List<DailyTurnover> sumByDateRange(Map<String, Object> map);
/**
* 根据动态条件统计订单数
* @param map
* @return
*/
List<DailyOrderCount> countOrderByDateRange(Map<String, Object> map);
/**
* 查询商品销量排名
* @param map
* @return
*/
List<GoodsSalesDTO> getSalesTop10(Map<String, Object> map);
}
OrderMapper.xml
<select id="sumByDateRange" resultType="com.sky.mapperResult.DailyTurnover">
select
cast(order_time as DATE ) as orderDate,
sum(amount) as dailyAmount
from orders
<where>
<if test="status != null"> and status = #{status}</if>
<if test="begin != null"> and order_time > #{begin}</if>
<if test="end != null"> and order_time < #{end}</if>
</where>
group by cast(order_time as Date)
</select>
<select id="countOrderByDateRange" resultType="com.sky.mapperResult.DailyOrderCount">
select cast(order_time as DATE) as orderDate,
count(*) as dailyOrderCount
from orders
<where>
<if test="status != null"> and status = #{status}</if>
<if test="begin != null"> and order_time > #{begin}</if>
<if test="end != null"> and order_time < #{end}</if>
</where>
group by cast(order_time as DATE )
order by orderDate;
</select>
<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
select name, sum(od.number) as number
from orders as o, order_detail as od
where o.id = od.order_id
<if test="status != null"> and status = #{status}</if>
<if test="begin != null"> and order_time >= #{begin}</if>
<if test="end != null"> and order_time <= #{end}</if>
group by name
order by number desc
limit 0, 10;
</select>
2.3.2 UserMapper
DailyNewUsers
package com.sky.mapperResult;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDate;
/**
* 每日新增用户数
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DailyNewUsers implements Serializable {
//用户创建日期(天为单位)
private LocalDate userDate;
//每天新增用户数
private Integer dailyNewUsers;
}
TotalUser
package com.sky.mapperResult;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDate;
/**
* 总用户数
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TotalUser implements Serializable {
//用户创建日期(天为单位)
private LocalDate userDate;
//截止某日总用户数
private Integer totalUsers;
}
UserMapper.java
public interface UserMapper {
/**
* 根据日期查询每日新建用户数
* @param map
* @return
*/
List<DailyNewUsers> countNewUsersByDateRange(Map<String, Object> map);
/**
* 根据日期查询截止end的总用户数
* @param map
* @return
*/
List<TotalUser> countTotalUsersByDateRange(Map<String, Object> map);
}
UserMapper.xml
<select id="countNewUsersByDateRange" resultType="com.sky.mapperResult.DailyNewUsers">
select
cast(create_time as DATE) as userDate,
count(*) as dailyNewUsers
from user
<where>
<if test="begin != null"> and create_time > #{begin}</if>
<if test="end != null"> and create_time < #{end}</if>
</where>
group by cast(create_time as Date)
</select>
<select id="countTotalUsersByDateRange" resultType="com.sky.mapperResult.TotalUser">
WITH RECURSIVE date_range AS (
SELECT CAST(#{begin} AS DATE) AS date
UNION ALL
SELECT date + INTERVAL 1 DAY
FROM date_range
WHERE date < CAST(#{end} AS DATE)
)
SELECT
dr.date as userDate,
(SELECT COUNT(*) FROM user WHERE create_time < DATE_ADD(dr.date, INTERVAL 1 DAY)) AS totalUsers
FROM
date_range dr
ORDER BY
dr.date
</select>
2.4 LocalDateTime 数据精度问题(一个bug的思考与学习)




通过这4条sql可以看出mysql中日期格式不影响递归CTE生成日期范围,关键是日期精度。日期精度过高mysql应该会四舍五入,导致出现问题[3]。所以service层中的getUserStatistics的endTime用LocalTime.MAX,查出的数据多一条记录,故endTime降低精度, 用LocalTime.of(23, 59, 59)。测试过程如下图。


关于这个问题,推荐看一下[3]的文章。
3. 结论
(1)DTO:数据传输对象,用于在应用程序的不同层之间传输数据。应用场景:通常用于表示从数据库查询出来的数据或通过网络发送的数据[2]。所以本文中从数据库中查询出来的数据封装的类可以命名为DTO。
(2)Mysql 由于 Java 日期 LocalDateTime 数据精度引发四舍五入的问题[3]。
参考文献
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐



所有评论(0)