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

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

Apache ECharts介绍



需求分析


代码开发
营业额统计功能完整详解
这是一个典型的分层架构数据统计功能,从 Controller → Service → Mapper → 数据库,逐层追溯。
一、整体调用链路
前端 GET /admin/report/turnoverStatistics?begin=2026-09-01&end=2026-09-07
│
▼
ReportController.turnoverStatistics(begin, end) ← 接收参数,参数转换
│
▼
ReportServiceImpl.getTurnoverStatistics(begin, end) ← 核心业务逻辑
│
│ for 每一天:
│ 构建 Map(begin, end, status=5)
│ OrderMapper.sumByMap(map) ← 查数据库
│
▼
TurnoverReportVO ← 返回两个逗号分隔的字符串
二、逐层拆解
第1层:Controller — 接收请求
@GetMapping("/turnoverStatistics")
public Result<TurnoverReportVO> turnoverStatistics(
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end) {
return Result.success(reportService.getTurnoverStatistics(begin, end));
}
关键注解 @DateTimeFormat:前端传来的 ?begin=2026-09-01&end=2026-09-07 是字符串,Spring 需要知道怎么转成 LocalDate。@DateTimeFormat(pattern = "yyyy-MM-dd") 告诉 Spring:「按年-月-日格式解析」。
如果不加这个注解,Spring 默认解析规则可能不兼容,请求会 400。
第2层:ServiceImpl — 核心逻辑
这是整个功能最核心的部分,分三步走:
步骤①:生成日期列表
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin); // 先把起始日期放进去
while (!begin.equals(end)) {
begin = begin.plusDays(1); // 日期 +1 天
dateList.add(begin); // 加入列表
}
假设 begin=9月1日, end=9月3日,循环过程:
| 初始 | 9/1 | false | [9/1] |
| 第1次 | 9/2 | false | [9/1, 9/2] |
| 第2次 | 9/3 | true → 退出 | [9/1, 9/2, 9/3] |
注意:这里直接修改了 begin 变量,由于 Java 的 LocalDate 是不可变对象,每次 plusDays(1) 返回新对象,不影响调用方。
步骤②:逐天查询营业额
List<Double> turnoverList = new ArrayList<>();
for (LocalDate date : dateList) {
// 将日期转为当天的起始时刻和结束时刻
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN); // 2026-09-01 00:00:00
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX); // 2026-09-01 23:59:59.999999999
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; // 当天没订单时返回 0,不是 null
turnoverList.add(turnover);
}
设计要点:
| LocalTime.MIN = 00:00:00 | 一天的开始 |
| LocalTime.MAX = 23:59:59.999999999 | 一天的结束 |
| status = COMPLETED(5) | 只统计已完成的订单,退款的、取消的不算 |
| turnover == null ? 0.0 | SQL SUM() 在没有匹配行时返回 null,必须兜底 |
步骤③:组装返回
return TurnoverReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.turnoverList(StringUtils.join(turnoverList, ","))
.build();
为什么不返回 JSON 数组而用逗号分隔的字符串?
ECharts 折线图需要的 xAxis.data 和 series.data 本身就是数组,但很多老旧前端框架或简单模板引擎拼接字符串更方便。这里的设计选择是用逗号分隔两个字符串,让前端自己 split(",") 还原为数组。
返回示例:
{
"code": 1,
"data": {
"dateList": "2026-09-01,2026-09-02,2026-09-03",
"turnoverList": "1520.0,0.0,890.5"
}
}
前端拿到后用 dateList.split(",") 和 turnoverList.split(",") 得到同长度的数组,一一对应渲染折线图。
第3层:Mapper — SQL 查询
<select id="sumByMap" resultType="java.lang.Double">
select sum(amount) from orders
<where>
<if test="begin != null">
and order_time > #{begin} <!– order_time > 当天0点 –>
</if>
<if test="end != null">
and order_time < #{end} <!– order_time < 当天24点 –>
</if>
<if test="status != null">
and status = #{status}
</if>
</where>
</select>
传入参数 {begin: 9月1日 00:00, end: 9月1日 23:59:59.9…, status: 5},实际执行:
SELECT SUM(amount) FROM orders
WHERE order_time > '2026-09-01 00:00:00'
AND order_time < '2026-09-01 23:59:59.999999999'
AND status = 5
为什么用 > 和 < 而不是 >= 和 <=?
因为 endTime 是 23:59:59.999999999,用 < 等于 <= 的效果。但严格来说这里的 > 不应该排除零点整的订单,这是一个小瑕疵,更严谨的写法是 >=。
第4层:VO — 返回载体
@Data
@Builder
public class TurnoverReportVO implements Serializable {
private String dateList; // "2026-09-01,2026-09-02,2026-09-03"
private String turnoverList; // "1520.0,0.0,890.5"
}
两个字段长度严格相等,按索引一一对应。前端拿到后:
const dates = data.dateList.split(",");
const amounts = data.turnoverList.split(",");
// dates[0] = "2026-09-01" → amounts[0] = "1520.0"
// dates[1] = "2026-09-02" → amounts[1] = "0.0"
// dates[2] = "2026-09-03" → amounts[2] = "890.5"
三、存在的问题和优化方向
问题①:N+1 查询
当前实现是循环内逐天查数据库,7 天就执行 7 次 SQL:
for (LocalDate date : dateList) {
orderMapper.sumByMap(map); // 每循环一次一次 SQL
}
优化方案:用一条 SQL 的 GROUP BY 一次性查完:
SELECT DATE(order_time) AS orderDate, SUM(amount) AS turnover
FROM orders
WHERE order_time >= #{begin} AND order_time <= #{end}
AND status = #{status}
GROUP BY DATE(order_time)
问题②:begin 变量被修改
while (!begin.equals(end)) {
begin = begin.plusDays(1); // 原始 begin 被改写了
}
虽然 Java 里是值传递不影响调用方,但不优雅。更干净的做法:
LocalDate current = begin;
while (!current.isAfter(end)) {
dateList.add(current);
current = current.plusDays(1);
}
问题③:StringUtils.join 类型不匹配
dateList 是 List<LocalDate>,commons-lang 2.6 的 StringUtils.join 接受 Collection 但内部调 toString(),输出格式可能是 2026-09-01(正确)也可能不是。保险起见用 Java 8 自带的:
dateList.stream().map(LocalDate::toString).collect(Collectors.joining
至此,营业额统计功能的代码开发完毕。



