🌈个人主页 :一条泥憨鱼(欢迎各位大佬莅临)

❄️《数据结构》 ❄️《AI与Agent那些事》
❄️《从0开始学计算机网络》 ❄️《后端开发》

用户统计
需求分析和设计


代码开发

整个模块涉及 4 层(Controller → Service → Mapper → VO),按数据流向逐层解读。
一、VO 层:数据的"快递盒"
UserReportVO.java
java
@Data // Lombok:自动生成 getter / setter / toString / equals / hashCode
@Builder // Lombok:自动生成建造者模式(UserReportVO.builder().xxx().build())
@NoArgsConstructor // 无参构造
@AllArgsConstructor // 全参构造
public class UserReportVO implements Serializable {
| 行 | 代码 | 解读 |
|---|---|---|
| 17 | private String dateList; |
日期列表,用逗号拼接。如 "2022-01-01,2022-01-02,2022-01-03" |
| 20 | private String totalUserList; |
每个日期对应的累计总用户数,逗号分隔 |
| 23 | private String newUserList; |
每个日期对应的当日新增用户数,逗号分隔 |
为什么用字符串而不是数组? 前端 ECharts 图表组件通常接收逗号分隔的字符串,前端
split(",")一下就能用。
二、Mapper 层:和数据库打交道
UserMapper.java
java
//根据动态条件统计用户数量
Integer countByMap(Map map);
传入一个 Map,根据里面有什么 key 来动态拼接 SQL。
UserMapper.xml
XML
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from user <!-- 统计 user 表的总行数 -->
<where>
<if test="begin !=null">
and create_time >#{begin} <!-- 如果传了 begin,只统计在 begin 之后创建的用户 -->
</if>
</where>
</select>
核心逻辑:MyBatis 动态 SQL。
| 传入的 Map 内容 | 生成 SQL | 含义 |
|---|---|---|
{end: "2022-01-01 23:59:59"} |
select count(id) from user |
无条件,统计全部用户(总用户数) |
{end: "2022-01-01 23:59:59", begin: "2022-01-01 00:00:00"} |
select count(id) from user where create_time > '2022-01-01 00:00:00' |
统计当天新注册的用户(新增用户数) |
三、Service 层:核心业务逻辑(重点)
ReportServiceImpl.java
第 67 行 --- 方法签名
java
public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
接收两个参数:统计的起始日期 和结束日期(只含日期,不含时间)。
第 68-74 行 --- 构建日期列表
java
//存放从begin到end之间的每天对应的日期
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)) {
begin = begin.plusDays(1); // begin 往后推一天
dateList.add(begin);
}
执行过程 (假设 begin=1月1日, end=1月3日):
| 循环次数 | begin 当前值 |
begin.plusDays(1) 后 |
dateList |
|---|---|---|---|
| 初始 | 1月1日 | --- | [1月1日] |
| 第1轮 | 1月1日 ≠ 1月3日 | 变成 1月2日 | [1月1日, 1月2日] |
| 第2轮 | 1月2日 ≠ 1月3日 | 变成 1月3日 | [1月1日, 1月2日, 1月3日] |
| 第3轮 | 1月3日 = 1月3日 → 退出 | --- | ✅ 最终 3 个日期 |
⚠️ 副作用 :循环结束后
begin被改成了end,好在此后不再需要begin。
第 77-79 行 --- 准备两个收集容器
java
//存放每天新增的用户数量
List<Integer> newUserList = new ArrayList<>();
//存放每天的总用户数量
List<Integer> totalUserList = new ArrayList<>();
两个列表和 dateList 一一对应(同一个索引位置代表同一天)。
第 81-96 行 --- 逐天统计(核心循环)
java
for (LocalDate date : dateList) {
// ↓ 把"日期"转成"日期+时间",用于 SQL 比较
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); // 如 2022-01-01 00:00:00
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); // 如 2022-01-01 23:59:59.999...
| 常量 | 值 | 含义 |
|---|---|---|
LocalTime.MIN |
00:00:00 |
一天开始 |
LocalTime.MAX |
23:59:59.999999999 |
一天结束 |
java
Map map = new HashMap();
map.put("end", endTime);
//总用户数量
Integer totalUser = userMapper.countByMap(map);
| 此时 map 内容 | SQL 条件 | 统计结果 |
|---|---|---|
{end: "2022-01-01 23:59:59"} |
无 begin,<if test="begin !=null"> 不成立 |
截至 1月1日之前的所有用户(总用户数) |
java
map.put("begin", beginTime);
//新增用户数量
Integer newUser = userMapper.countByMap(map);
| 此时 map 内容 | SQL 条件 | 统计结果 |
|---|---|---|
{end: "2022-01-01 23:59:59", begin: "2022-01-01 00:00:00"} |
create_time > '2022-01-01 00:00:00' |
1月1日当天新注册的用户 |
技巧 :复用同一个
Map,逐步加条件,避免创建多个 Map 对象。
java
totalUserList.add(totalUser);
newUserList.add(newUser);
}
把每天的结果分别存入对应列表。
第 98-104 行 --- 封装返回结果
java
//封装结果数据
return UserReportVO.
builder() // 启动建造者模式
.dateList(StringUtils.join(dateList, ",")) // [1月1日,1月2日] → "2022-01-01,2022-01-02"
.totalUserList(StringUtils.join(totalUserList, ",")) // [200, 210] → "200,210"
.newUserList(StringUtils.join(newUserList, ",")) // [20, 21] → "20,21"
.build(); // 建造完成,返回 UserReportVO 对象
StringUtils.join(list, ",") 是 Apache Commons Lang 的工具方法,把 List 用逗号拼成一个字符串。
四、Controller 层:对外暴露接口
ReportController.java
java
@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);
return Result.success(reportService.getUserStatistics(begin, end));
}
| 行 | 说明 |
|---|---|
@GetMapping("/userStatistics") |
响应 GET /admin/report/userStatistics?begin=2022-01-01&end=2022-01-31 |
@DateTimeFormat(pattern = "yyyy-MM-dd") |
把 URL 中的字符串 "2022-01-01" 自动转为 LocalDate |
Result.success(...) |
用统一响应格式 {"code":1, "msg":"success", "data":{...}} 包裹返回 |
五、完整数据流图
前端请求
│ GET /admin/report/userStatistics?begin=2022-01-01&end=2022-01-03
▼
ReportController.userStatistics()
│ 解析参数 → LocalDate begin=1月1日, end=1月3日
▼
ReportServiceImpl.getUserStatistics()
│
├─ ① 构建日期列表: [1月1日, 1月2日, 1月3日]
│
├─ ② 逐天循环:
│ ├─ 设 end=当天结尾 → countByMap({end}) → 总用户数(截止当天)
│ └─ 加 begin=当天开始 → countByMap({end,begin}) → 新增用户数(当天注册)
│ → user 表: WHERE create_time > begin
│
└─ ③ 建造 UserReportVO:
dateList = "2022-01-01,2022-01-02,2022-01-03"
totalUserList = "200,210,220"
newUserList = "20,21,10"
│
▼
Controller 包装为 Result.success() 返回 JSON
订单统计
需求分析和设计

代码开发
一、VO 层:返回数据模型
OrderReportVO.java
| 行 | 字段 | 类型 | 含义 |
|---|---|---|---|
| 16 | dateList |
String |
日期列表,逗号拼接,如 "2022-01-01,2022-01-02" |
| 19 | orderCountList |
String |
每日订单总数 ,逗号拼接,如 "260,215" |
| 22 | validOrderCountList |
String |
每日有效订单数 ,逗号拼接,如 "200,180" |
| 25 | totalOrderCount |
Integer |
整个时间区间内的订单总数(一个数字) |
| 28 | validOrderCount |
Integer |
整个时间区间内的有效订单数(一个数字) |
| 31 | orderCompletionRate |
Double |
订单完成率 = 有效订单 / 订单总数 |
六字段分为两组:前三个存每日明细 (给折线图用),后三个存汇总数据(给饼图/指标卡用)。
二、Mapper 层:countByMap SQL
OrderMapper.xml
XML
<select id="countByMap" resultType="java.lang.Integer">
select count(id) from orders -- 统计 orders 表的行数
<where>
<if test="begin != null">
and order_time > #{begin} -- 下单时间 > begin
</if>
<if test="end != null">
and order_time < #{end} -- 下单时间 < end
</if>
<if test="status != null">
and status = #{status} -- 订单状态筛选
</if>
</where>
</select>
和用户统计的关键区别 :这里多了 status 条件。传入不同 status 参数,统计不同含义:
| 传入参数 | SQL 等价 | 含义 |
|---|---|---|
status=null |
count(id) WHERE order_time BETWEEN begin AND end |
当天全部订单 |
status=5 |
count(id) WHERE order_time BETWEEN begin AND end AND status=5 |
当天已完成订单(有效订单) |
三、Service 层:核心业务逻辑
3.1 主方法:getOrderStatistics
ReportServiceImpl.java
第 107 行 --- 方法签名
java
public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
第 109-114 行 --- 构建日期列表
java
//存放从begin到end之间的每天对应的日期
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while (!begin.equals(end)) {
begin = begin.plusDays(1); // begin 自身后移一天
dateList.add(begin);
}
这和用户统计中完全一样的逻辑,生成 [1号, 2号, 3号, ..., N号]。
第 117-118 行 --- 准备两个容器
java
//存放每天的订单总数
List<Integer> orderCountList = new ArrayList<>();
//存放每天的有效订单数
List<Integer> validOrderCountList = new ArrayList<>();
两个列表与 dateList 索引一一对应。
第 121-130 行 --- 逐天查询(核心循环)
java
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); // 当天 00:00:00
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); // 当天 23:59:59
Integer orderCount = getOrderCount(beginTime, endTime, null); // ← 全部订单
Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED); // ← 仅已完成订单
orderCountList.add(orderCount);
validOrderCountList.add(validOrderCount);
}
关键技巧 :两次调用 getOrderCount,唯一区别是第三个参数:
| 调用 | status 参数 |
统计对象 |
|---|---|---|
getOrderCount(begin, end, null) |
null |
不管状态,当天所有订单 |
getOrderCount(begin, end, Orders.COMPLETED) |
5 |
只统计已完成的订单 |
第 132-136 行 --- 汇总计算
java
Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();
Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();
| 代码 | 含义 | 示例输入 | 结果 |
|---|---|---|---|
stream().reduce(Integer::sum) |
对列表所有元素求和 | [100, 120, 80] |
300 |
.get() |
从 Optional 中取出值 |
--- | --- |
第 138-141 行 --- 计算完成率
java
Double orderCompletionRate = 0.0; // 默认 0,避免除零
if (totalOrderCount != 0) {
orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
}
| 场景 | 计算 | 结果 |
|---|---|---|
| 总订单 300,有效 240 | 240.0 / 300 |
0.8 |
| 总订单 0 | 不进入 if | 0.0(安全默认值) |
第 143-153 行 --- 封装返回
java
return OrderReportVO.builder()
.dateList(StringUtils.join(dateList, ",")) // [1号,2号] → "2022-01-01,2022-01-02"
.orderCountList(StringUtils.join(orderCountList, ",")) // [100,120] → "100,120"
.validOrderCountList(StringUtils.join(validOrderCountList, ",")) // [80,90] → "80,90"
.totalOrderCount(totalOrderCount) // 300
.validOrderCount(validOrderCount) // 240
.orderCompletionRate(orderCompletionRate) // 0.8
.build();
3.2 辅助方法:getOrderCount
ReportServiceImpl.java#L158-L165
java
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);
Integer count = orderMapper.countByMap(map);
return count;
}
| 行 | 代码 | 解读 |
|---|---|---|
| 158 | private Integer getOrderCount(...) |
private:仅供本类内部调用;第三个参数 status 可为 null |
| 159 | Map map = new HashMap(); |
用 Map 传参,MyBatis 的 parameterType="map" 风格 |
| 160-162 | map.put("begin/end/status", ...) |
把三个参数放入 Map |
| 164 | orderMapper.countByMap(map) |
调用 MyBatis,触发动态 SQL,返回 int |
| 165 | return count; |
返回给调用方 |
四、动态 SQL 行为对照表
以 2022-01-01 这天为例(begin=2022-01-01 00:00, end=2022-01-01 23:59):
| 调用 | Map 内容 | 生成 SQL | 结果 |
|---|---|---|---|
getOrderCount(begin, end, null) |
{begin, end, status=null} |
count(id) FROM orders WHERE order_time > begin AND order_time < end |
当天全部订单数 |
getOrderCount(begin, end, 5) |
{begin, end, status=5} |
count(id) FROM orders WHERE order_time > begin AND order_time < end AND status=5 |
当天有效订单数 |
五、完整数据流图
GET /admin/report/ordersStatistics?begin=2022-01-01&end=2022-01-03
│
▼
getOrderStatistics(1月1日, 1月3日)
│
├─ ① dateList = [1月1日, 1月2日, 1月3日]
│
├─ ② 逐天循环 ──┬── getOrderCount(当天, null) → orderCountList [100, 120, 80]
│ └── getOrderCount(当天, COMPLETED) → validOrderCountList [ 80, 90, 60]
│
├─ ③ totalOrderCount = 100+120+80 = 300
│ validOrderCount = 80+90+60 = 230
│ completionRate = 230.0 / 300 = 0.767
│
└─ ④ OrderReportVO {
dateList = "2022-01-01,2022-01-02,2022-01-03"
orderCountList = "100,120,80"
validOrderCountList= "80,90,60"
totalOrderCount = 300
validOrderCount = 230
orderCompletionRate= 0.767
}
六、与用户统计的对比
| 维度 | 用户统计 | 订单统计 |
|---|---|---|
| 复用 Map 策略 | 同一个 Map 逐步加条件(先只放 end,再加 begin) | 同一天调两次 Mapper,传不同 status |
| Mapper 调用次数 | 每天 1 次(复用 Map) | 每天 2 次(订单总数 + 有效订单) |
| 有无辅助方法 | 无,直接在循环中写 | 有 getOrderCount 封装复用 |
| 汇总计算 | 无(VO 里只存每日明细) | 有(totalOrderCount / completionRate) |
销量排名TOP10
需求分析和设计


代码开发
一、涉及的文件速览
| 文件 | 角色 |
|---|---|
| SalesTop10ReportVO.java | 返回给前端的数据模型 |
| GoodsSalesDTO.java | Mapper 返回的中间传输对象 |
| OrderMapper.java | 定义 getSalesTop10 方法签名 |
| OrderMapper.xml | 销量 Top10 的 SQL 查询 |
| ReportServiceImpl.java | 核心业务逻辑 |
二、VO 层 + DTO 层
SalesTop10ReportVO
java
private String nameList; // "鱼香肉丝,宫保鸡丁,水煮鱼"
private String numberList; // "260,215,200"
只有两个字段,分别存商品名称和对应销量,都用逗号拼接。
GoodsSalesDTO --- 中间传输对象
java
private String name; // 单个商品名称
private Integer number; // 单个商品销量
SQL 查询结果中每一行映射为一个 GoodsSalesDTO,再在 Service 层转换为 SalesTop10ReportVO。
为什么分两层? DTO 在 Mapper→Service 间传输(一条记录一个对象),VO 在 Service→Controller→前端传输(整个结果一个对象)。关注点分离。
三、Mapper 层:核心 SQL
OrderMapper.java
java
List<GoodsSalesDTO> getSalesTop10(LocalDateTime begin, LocalDateTime end);
返回一个 List,每条 GoodsSalesDTO 代表一个商品的名称 + 销量。
OrderMapper.xml
sql
select od.name, sum(od.number) number -- ① 查商品名 + 销量求和
from order_detail od, orders o -- ② 订单明细表 + 订单主表,隐式内连接
where od.order_id = o.id -- ③ 关联条件:明细属于哪个订单
and o.status = 5 -- ④ 只看已完成的订单
and o.order_time > #{begin} -- ⑤ 时间范围开始
and o.order_time < #{end} -- ⑥ 时间范围结束
group by od.name -- ⑦ 按商品名分组
order by number desc -- ⑧ 按销量降序
limit 0,10 -- ⑨ 只取前 10 条
逐句解读:
| SQL 片段 | 作用 |
|---|---|
order_detail od, orders o |
旧式逗号连接写法,等价于 INNER JOIN orders o ON ... |
od.order_id = o.id |
通过 order_detail 的外键 order_id 关联 orders 的主键 id |
o.status = 5 |
只统计已完成订单,排除取消/退款等无效订单 |
sum(od.number) |
number 是购买数量,按商品名分组后求和 |
group by od.name |
把相同菜名的购买数量累加在一起 |
order by number desc |
销量从高到低排序 |
limit 0,10 |
MySQL 分页语法,0 是 offset,10 是取的行数 |
为什么是
o.status = 5? 回顾 Orders.java:COMPLETED = 5,只有已完成的订单销量才有意义。
四、Service 层:逐行解读
ReportServiceImpl.java#L170-L185
第 170 行 --- 方法签名
java
public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
和前面的用户统计、订单统计一样,接收日期范围。
第 171-172 行 --- 日期转时间
java
LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN); // start: 00:00:00
LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX); // end: 23:59:59
第 174 行 --- 调用 Mapper
java
List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
一次 SQL 查询,返回 List<GoodsSalesDTO>,最多 10 条:
[
GoodsSalesDTO{name="鱼香肉丝", number=260},
GoodsSalesDTO{name="宫保鸡丁", number=215},
GoodsSalesDTO{name="水煮鱼", number=200},
...
]
第 175-176 行 --- 提取商品名称列表
java
List<String> names = salesTop10.stream() // 开启流
.map(GoodsSalesDTO::getName) // 每个 DTO → 取出 name
.collect(Collectors.toList()); // 收集为 List<String>
String nameList = StringUtils.join(names, ","); // ["鱼香肉丝","宫保鸡丁"] → "鱼香肉丝,宫保鸡丁"
| 步骤 | 输入 | 输出 |
|---|---|---|
stream() |
List<GoodsSalesDTO> |
Stream<GoodsSalesDTO> |
map(GoodsSalesDTO::getName) |
每个 DTO | 提取 .name 字段 |
collect(toList()) |
Stream<String> |
List<String> = ["鱼香肉丝", "宫保鸡丁", "水煮鱼"] |
StringUtils.join(..., ",") |
List<String> |
"鱼香肉丝,宫保鸡丁,水煮鱼" |
第 178-179 行 --- 提取销量列表
java
List<Integer> numbers = salesTop10.stream() // 开启流
.map(GoodsSalesDTO::getNumber) // 每个 DTO → 取出 number
.collect(Collectors.toList()); // 收集为 List<Integer>
String numberList = StringUtils.join(numbers, ","); // [260,215,200] → "260,215,200"
与上面完全对称的逻辑,只是提取的是 number 字段。
第 181-184 行 --- 封装返回
java
return SalesTop10ReportVO
.builder()
.nameList(nameList) // "鱼香肉丝,宫保鸡丁,水煮鱼"
.numberList(numberList) // "260,215,200"
.build();
五、与前两个统计的对比
| 维度 | 用户统计 | 订单统计 | 销量排名 Top10 |
|---|---|---|---|
| 循环 | ✅ 逐天循环 | ✅ 逐天循环 | ❌ 一次 SQL 搞定 |
| Mapper 调用次数 | 每天 1 次 | 每天 2 次 | 总共 1 次 |
| SQL 复杂度 | 简单 count |
简单 count |
多表联查 + GROUP BY + ORDER BY + LIMIT |
| 数据转换 | 直接拼字符串 | 直接拼字符串 | Stream → map → collect → join |
| 有汇总计算 | ❌ | ✅(total/rate) | ❌ |
销量排名之所以不需要循环,是因为
GROUP BY+LIMIT 10本身就是聚合操作,MySQL 一次性算完。
六、完整数据流图
GET /admin/report/top10?begin=2022-01-01&end=2022-01-31
│
▼
getSalesTop10(1月1日, 1月31日)
│
├─ ① beginTime = 1月1日 00:00:00, endTime = 1月31日 23:59:59
│
├─ ② orderMapper.getSalesTop10(beginTime, endTime)
│ │
│ └─ SQL:
│ SELECT od.name, SUM(od.number)
│ FROM order_detail od, orders o
│ WHERE od.order_id = o.id
│ AND o.status = 5
│ AND o.order_time > '2022-01-01 00:00'
│ AND o.order_time < '2022-01-31 23:59'
│ GROUP BY od.name
│ ORDER BY number DESC
│ LIMIT 10
│ │
│ └─ 返回: [ {name:鱼香肉丝,number:260}, {name:宫保鸡丁,number:215}, ... ]
│
├─ ③ Stream 提取 names → "鱼香肉丝,宫保鸡丁,水煮鱼,..."
│ Stream 提取 numbers → "260,215,200,..."
│
└─ ④ SalesTop10ReportVO { nameList: "...", numberList: "..." }
至此,day11的代码开发全部实现完毕。