From e647ad75c68c25ed10fede7548149e7a531201f0 Mon Sep 17 00:00:00 2001 From: Jerry Yan <792602257@qq.com> Date: Sat, 17 Jan 2026 16:58:23 +0800 Subject: [PATCH] =?UTF-8?q?feat(clickhouse):=20=E5=AE=9E=E7=8E=B0=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E6=95=B0=E6=8D=AE=E6=9F=A5=E8=AF=A2=E7=9A=84=E6=97=B6?= =?UTF-8?q?=E9=97=B4=E5=BA=8F=E5=88=97=E5=A1=AB=E5=85=85=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 将日期时间处理从旧的 Date 和 SimpleDateFormat 迁移到新的 Java 8 时间 API - 添加小时级别数据序列填充功能,确保每个小时都有数据记录 - 添加日期级别数据序列填充功能,确保每天都有数据记录 - 实现缺失时间段的数据自动补零机制 - 重构查询方法以支持连续时间序列数据返回 - 提高统计图表数据完整性和可视化效果 --- .../impl/ClickHouseStatsQueryServiceImpl.java | 95 +++++++++++++++++-- 1 file changed, 87 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/ycwl/basic/clickhouse/service/impl/ClickHouseStatsQueryServiceImpl.java b/src/main/java/com/ycwl/basic/clickhouse/service/impl/ClickHouseStatsQueryServiceImpl.java index 35dd1b27..5639babc 100644 --- a/src/main/java/com/ycwl/basic/clickhouse/service/impl/ClickHouseStatsQueryServiceImpl.java +++ b/src/main/java/com/ycwl/basic/clickhouse/service/impl/ClickHouseStatsQueryServiceImpl.java @@ -12,10 +12,13 @@ import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; import org.springframework.stereotype.Service; import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.TimeZone; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.*; +import java.util.stream.Collectors; /** * ClickHouse 统计数据查询服务实现 @@ -366,12 +369,14 @@ public class ClickHouseStatsQueryServiceImpl implements StatsQueryService { sql.append("GROUP BY toStartOfHour(s.create_time) "); sql.append("ORDER BY toStartOfHour(s.create_time)"); - return getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> { + List> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> { HashMap map = new HashMap<>(); map.put("t", rs.getString("t")); map.put("count", rs.getString("count")); return map; }); + + return fillHourSeries(rawData, query.getStartTime(), query.getEndTime()); } @Override @@ -389,12 +394,14 @@ public class ClickHouseStatsQueryServiceImpl implements StatsQueryService { sql.append("GROUP BY toStartOfDay(s.create_time) "); sql.append("ORDER BY toStartOfDay(s.create_time)"); - return getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> { + List> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> { HashMap map = new HashMap<>(); map.put("t", rs.getString("t")); map.put("count", rs.getString("count")); return map; }); + + return fillDateSeries(rawData, query.getStartTime(), query.getEndTime()); } @Override @@ -418,12 +425,14 @@ public class ClickHouseStatsQueryServiceImpl implements StatsQueryService { sql.append("GROUP BY toStartOfHour(s.create_time) "); sql.append("ORDER BY toStartOfHour(s.create_time)"); - return getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> { + List> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> { HashMap map = new HashMap<>(); map.put("t", rs.getString("t")); map.put("count", rs.getString("count")); return map; }); + + return fillHourSeries(rawData, query.getStartTime(), query.getEndTime()); } @Override @@ -447,11 +456,81 @@ public class ClickHouseStatsQueryServiceImpl implements StatsQueryService { sql.append("GROUP BY toStartOfDay(s.create_time) "); sql.append("ORDER BY toStartOfDay(s.create_time)"); - return getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> { + List> rawData = getJdbcTemplate().query(sql.toString(), (rs, rowNum) -> { HashMap map = new HashMap<>(); map.put("t", rs.getString("t")); map.put("count", rs.getString("count")); return map; }); + + return fillDateSeries(rawData, query.getStartTime(), query.getEndTime()); + } + + /** + * 填充小时序列,确保每个小时都有数据(缺失的填充为0) + */ + private List> fillHourSeries(List> rawData, Date startTime, Date endTime) { + if (startTime == null || endTime == null) { + return rawData; + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd HH"); + LocalDateTime start = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().truncatedTo(ChronoUnit.HOURS); + LocalDateTime end = endTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime().truncatedTo(ChronoUnit.HOURS); + + // 将原始数据转为 Map 以便快速查找 + Map dataMap = rawData.stream() + .collect(Collectors.toMap( + m -> m.get("t"), + m -> m.get("count"), + (existing, replacement) -> existing + )); + + List> result = new ArrayList<>(); + LocalDateTime current = start; + while (!current.isAfter(end)) { + String timeKey = current.format(formatter); + HashMap item = new HashMap<>(); + item.put("t", timeKey); + item.put("count", dataMap.getOrDefault(timeKey, "0")); + result.add(item); + current = current.plusHours(1); + } + + return result; + } + + /** + * 填充日期序列,确保每天都有数据(缺失的填充为0) + */ + private List> fillDateSeries(List> rawData, Date startTime, Date endTime) { + if (startTime == null || endTime == null) { + return rawData; + } + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd"); + LocalDate start = startTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + LocalDate end = endTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + + // 将原始数据转为 Map 以便快速查找 + Map dataMap = rawData.stream() + .collect(Collectors.toMap( + m -> m.get("t"), + m -> m.get("count"), + (existing, replacement) -> existing + )); + + List> result = new ArrayList<>(); + LocalDate current = start; + while (!current.isAfter(end)) { + String timeKey = current.format(formatter); + HashMap item = new HashMap<>(); + item.put("t", timeKey); + item.put("count", dataMap.getOrDefault(timeKey, "0")); + result.add(item); + current = current.plusDays(1); + } + + return result; } }