营业额统计

接口设计

  • 请求方式: GET
  • 请求路径: /admin/report/turnoverStatistics
  • 请求参数:
    • begin: 开始日期(如2022-05-01)
    • end: 结束日期(如2022-05-31)

接口响应

  • 数据格式要求:
    • dateList: 日期列表字符串(日期间用逗号分隔,如”4-1,4-2,4-3”)
    • turnoverList: 营业额列表字符串(数值间用逗号分隔,如”120,140,135”)
  • 设计原则:
    • 返回格式由前端图表组件需求决定
    • 后端需严格按指定格式组织数据
    • 日期与营业额数值需保持顺序一致
@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);  
}
@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;  
  
}
  • 定义了 getTurnoverStatistics 方法接口
  • 实现类 ReportServiceImpl 负责具体业务逻辑:
    • 生成从 begin 到 end 的日期列表
    • 针对每一天,查询状态为”已完成”的订单总额
    • 将日期列表和营业额列表组装成 TurnoverReportVO 返回
  • 通过 Map 传递动态查询条件
  • 使用 StringUtils.join 将列表数据转换为逗号分隔的字符串
  • 只统计状态为”已完成”的订单金额
public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {  
    List<LocalDate> dateList = new ArrayList<>();  
    dateList.add(begin);  
  
    while (!begin.equals(end)) {  
        begin = begin.plusDays(1);  
        dateList.add(begin);  
    }  
  
    List<Double> turnoverList = 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("begin", beginTime);  
        map.put("end", endTime);  
        map.put("status", Orders.COMPLETED);  
        Double turnover = orderMapper.sumByMap(map);  
        turnoverList.add(turnover == null ? 0.0 : turnover);  
    }  
  
    // 组装结果数据  
    return TurnoverReportVO  
            .builder()  
            .dateList(StringUtils.join(dateList, ','))  
            .turnoverList(StringUtils.join(turnoverList, ','))  
            .build();  
}
  • 新增 sumByMap 方法,支持根据动态条件统计订单金额
  • 在 XML 中实现对应的 SQL 查询,支持按时间范围和订单状态筛选
<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>

用户统计

产品原型

  • 数据展示方式:通过折线图展示用户数据,包含两根折线分别表示用户总量和新增用户数量
  • 图表构成:
    • X轴:日期(如4月1号至4月14号)
    • Y轴:用户数量
    • 蓝色折线:用户总量(截止到当天的累计用户数)
    • 绿色折线:新增用户数量(当天新增的用户数)

业务规则

  • 动态时间区间:根据页面左上角的时间选择器动态展示选定区间内的数据
  • 数据联动:时间选择器会同时影响营业额统计、用户统计、订单统计等多个模块的数据展示
  • 具体统计内容:
    • 展示选定时间区间内每天的用户总量数据
    • 展示选定时间区间内每天的新增用户量数据

接口设计

  • 请求方式:GET
  • 请求路径:/admin/report/userStatistics
  • 请求参数:
    • begin:必填,开始日期(如2022-05-01)
    • end:必填,结束日期(如2022-05-31)
  • 返回数据结构:
    • dateList:日期列表,以逗号分隔的字符串
    • newUserList:新增用户数列表,以逗号分隔的字符串
    • totalUserList:总用户量列表,以逗号分隔的字符串
  • 数据格式说明:返回数据格式由前端折线图组件约定,需要严格遵循字符串格式要求

代码开发

public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {  
    List<LocalDate> dateList = new ArrayList<>();  
    dateList.add(begin);  
  
    while (!begin.equals(end)) {  
        begin = begin.plusDays(1);  
        dateList.add(begin);  
    }  
  
    // 存放每天的新增用户数量  
    List<Integer> newUserList = new ArrayList<>();  
    // 存放每天的总用户数量  
    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, ','))  
            .totalUserList(StringUtils.join(totalUserList, ','))  
            .newUserList(StringUtils.join(newUserList, ','))  
            .build();  
}
<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>

订单统计

需求分析和设计

image-2

  • 有效订单定义:状态为”已完成”的订单才被视为有效订单
  • 图表规范:
    • X轴:表示日期(如4-1到4-14)
    • Y轴:表示订单数量(范围0-100)
  • 时间区间计算:顶部三个数字反映的是所选时间区间内的汇总数据
  • 完成率计算:订单完成率=有效订单数/总订单数∗100

接口设计

  • 请求方式:GET
  • 请求路径:/admin/report/ordersStatistics
  • 请求参数:
    • begin:开始日期(示例:2022-05-01)
    • end:结束日期(示例:2022-05-31)
  • 核心数据结构:包含6个关键数据项
    • 汇总数据:
      • orderCompletionRate:订单完成率(double类型)
      • totalOrderCount:订单总数(integer类型)
      • validOrderCount:有效订单数(integer类型)
    • 图表数据:
      • dateList:日期列表(逗号分隔的字符串)
      • orderCountList:每日订单数列表(逗号分隔的字符串)
      • validOrderCountList:每日有效订单数列表(逗号分隔的字符串)

代码实现

实现思路

  • 日期处理:从开始日期到结束日期,逐日生成日期列表
  • 数据统计:对每一天分别统计总订单数和有效订单数
  • 聚合计算:汇总时间区间内的总订单数、有效订单数,并计算完成率
  • 数据封装:将统计结果封装到 OrderReportVO 对象中返回
@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;  
  
}

在 ReportServiceImpl 中实现了具体的订单统计逻辑:

  • 遍历日期区间内的每一天
  • 统计每天的订单总数和有效订单数(已完成的订单)
  • 计算整个时间区间的总订单数、有效订单数和订单完成率
  • 使用 OrderReportVO 封装返回结果
/**  
 * 订单统计  
 * @param begin  
 * @param end  
 * @return  
 */
@Override  
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);  
    }  
  
    //存放每天的订单总数  
    List<Integer> orderCountList = new ArrayList<>();  
    //存放每天的有效订单数  
    List<Integer> validOrderCountList = new ArrayList<>();  
  
    //遍历dateList集合,查询每天的有效订单数和订单总数  
    for (LocalDate date : dateList) {  
        //查询每天的订单总数 select count(id) from orders where order_time > ? and order_time < ?        LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);  
        LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);  
        Integer orderCount = getOrderCount(beginTime, endTime, null);  
  
        //查询每天的有效订单数 select count(id) from orders where order_time > ? and order_time < ? and status = 5        Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);  
  
        orderCountList.add(orderCount);  
        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, ","))  
            .totalOrderCount(totalOrderCount)  
            .validOrderCount(validOrderCount)  
            .orderCompletionRate(orderCompletionRate)  
            .build();  
}  
  
/**  
 * 根据条件统计订单数量  
 *  
 * @param begin  
 * @param end  
 * @param status  
 * @return  
 */private Integer getOrderCount(LocalDateTime begin, LocalDateTime end, Integer status) {  
    Map map = new HashMap();  
    map.put("begin", begin);  
    map.put("end", end);  
    map.put("status", status);  
  
    return orderMapper.countByMap(map);  
}

Stream详解

代码中使用了 Stream 的 map()collect() 方法,可参考集合流的聚合操作

List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());

代码的执行过程如下:

  1. 创建 Stream
salesTop10.stream()

这行代码创建了一个 Stream 流,数据源是 salesTop10 列表。

  1. map 操作
.map(GoodsSalesDTO::getName)

map() 是一个中间操作,它将一个函数应用于流中的每个元素,并将结果作为新的流元素。

  • GoodsSalesDTO::getName 是方法引用,等价于 x -> x.getName()

  • 对于 salesTop10 中的每个 GoodsSalesDTO 对象,调用其 getName() 方法

  • 返回一个新的流,其中包含所有商品名称

  1. collect 操作
.collect(Collectors.toList())

collect() 是一个终端操作,它将流中的元素收集到一个集合中。

  • Collectors.toList() 是一个收集器,将元素收集到一个 List 中
  • 这个操作触发了整个流水线的执行

在 OrderMapper 接口中添加了 countByMap 方法,用于根据动态条件统计订单数量,支持按时间范围和订单状态进行统计。

在 OrderMapper.xml 中实现了 countByMap 的 SQL 查询,支持动态条件查询:

  • 可按时间范围筛选订单
  • 可按订单状态筛选
  • 使用了 MyBatis 的动态 SQL 标签
<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>

销量排名top10

需求分析和设计

产品原型

image-1

  • 展示形式: 通过柱形图展示商品销量排名,按照降序排列
  • 统计范围: 仅统计销量排名前十的商品,包括菜品和套餐两类商品
  • 时间维度: 支持选择不同时间区间(如近7日、近30日等)进行动态统计

业务规则

  • 统计规则:
    • 根据选择的时间区间展示销量前10的商品
    • 商品范围包括菜品和套餐两类
  • 展示规则:
    • 使用柱状图进行可视化展示
    • 按销量降序排列商品
  • 计算规则:
    • 销量指商品销售的份数
    • 统计时间段内的累计销量

接口设计

  • 请求方式: GET请求
  • 请求路径: /admin/report/top10
  • 请求参数:
    • begin: 开始日期(如2022-05-01)
    • end: 结束日期(如2022-05-31)
  • 返回数据:
    • nameList: 商品名称列表,以逗号分隔的字符串(如”宫保鸡丁,龙井虾仁,…”)
    • numberList: 销量列表,以逗号分隔的数字字符串(如”160,120,…”)
  • 数据格式:
    • 商品名称和销量按顺序一一对应
    • 使用逗号作为分隔符保证前后端数据一致性

代码开发

  • GoodsSalesDTO: 用于存储商品名称和销量的数据传输对象
  • SalesTop10ReportVO: 用于返回销量排名前10的结果对象,包含商品名称列表和销量列表,均以逗号分隔的字符串形式存储
@Data  
@Builder  
@NoArgsConstructor  
@AllArgsConstructor  
public class GoodsSalesDTO implements Serializable {  
    //商品名称  
    private String name;  
  
    //销量  
    private Integer number;  
}
@Data  
@Builder  
@NoArgsConstructor  
@AllArgsConstructor  
public class SalesTop10ReportVO implements Serializable {  
  
    //商品名称列表,以逗号分隔,例如:鱼香肉丝,宫保鸡丁,水煮鱼  
    private String nameList;  
  
    //销量列表,以逗号分隔,例如:260,215,200  
    private String numberList;  
  
}
  • 实现getSalesTop10方法,将传入的LocalDate转换为LocalDateTime(开始时间为当天0点,结束时间为当天23:59:59)
  • 调用OrderMapper的getSalesTop10方法查询数据库
  • 使用Java 8 Stream将查询结果中的商品名称和销量分别提取并转换为逗号分隔的字符串
  • 构建并返回SalesTop10ReportVO对象
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {  
    LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);  
    LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);  
  
    List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);  
    List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());  
    String nameList = StringUtils.join(names, ",");  
  
    List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());  
    String numberList = StringUtils.join(numbers, ",");  
  
    //封装返回结果数据  
    return SalesTop10ReportVO  
            .builder()  
            .nameList(nameList)  
            .numberList(numberList)  
            .build();  
}
  • 在OrderMapper接口中新增getSalesTop10方法声明
  • 在OrderMapper.xml中编写对应的SQL查询语句:
  • 联合查询订单明细表(order_detail)和订单表(orders)
  • 筛选已完成状态(status=5)的订单
  • 按商品名称分组,统计销量总和
  • 按销量降序排列,取前10条记录
<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>