一、功能概述
将从第三方接口获取的储罐(温度 / 高度)原始数据,处理成按整点标准化的折线图数据格式,
解决原始数据时间不规整、存在缺失值、重复数据等问题,最终返回连续的整点数据序列给前端,
确保折线图展示连续、无断点。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、核心功能拆解与代码详解
2.1 数据预处理阶段
List<TankQueryResponse.TankInfo> items = result.getData().getItems();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
list = items.stream()
// 过滤掉更新时间为空的数据(防御性处理)
.filter(tankInfo -> tankInfo.getUpdatedAt() !=null)
.map(source -> {
try {
WarehouseTankThirdLogNewDTO tankThirdLog = new WarehouseTankThirdLogNewDTO();
// 字符串转BigDecimal(温度/高度)
tankThirdLog.setTemperature(parseBigDecimal(source.getTemperature()));
tankThirdLog.setHeight(parseBigDecimal(source.getHeight()));
// 时间字符串转Date类型
Date date = format.parse(source.getUpdatedAt());
tankThirdLog.setCreateTime(date);
return tankThirdLog;
} catch (Exception e) {
// 转换异常则返回null,后续过滤
return null;
}
})
// 过滤掉转换异常的null数据
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 按时间排序,为后续处理做准备
list.sort(Comparator.comparing(WarehouseTankThirdLogNewDTO::getCreateTime));
功能说明:
过滤空时间数据:避免后续时间转换出现 NPE(空指针异常);
数据类型转换:将原始字符串类型的温度、高度转为 BigDecimal,时间字符串转为 Date;
异常处理:捕获转换异常并标记为 null,最终过滤掉异常数据;
时间排序:保证数据按时间顺序排列,为后续整点匹配打下基础。
2.2 整点时间序列生成
// 确定时间范围(取最早/最晚数据的整点)
if (list.isEmpty()) {
return Collections.emptyList();
}
List<Date> hourPoints = generateHourPoints(
roundToHour(list.get(0).getCreateTime(), false),
roundToHour(list.get(list.size()-1).getCreateTime(), true)
);
核心方法说明:
roundToHour(Date date, boolean ceiling):时间取整到整点,ceiling=false向下取整(如 10:23→10:00),
ceiling=true向上取整(如 10:23→11:00);
generateHourPoints(Date start, Date end):生成从 start 到 end 的所有整点时间列表(如 10:00、11:00、12:00)。
2.3 数据去重与整点归并
// 分钟级精度去重:同一分钟内只保留最新数据
Map<Date, WarehouseTankThirdLogNewDTO> minutePrecisionMap = new HashMap<>();
list.forEach(item -> {
Date minuteKey = roundToMinutePrecision(item.getCreateTime());
minutePrecisionMap.merge(minuteKey, item,
(oldVal, newVal) -> oldVal.getCreateTime().after(newVal.getCreateTime()) ? oldVal : newVal);
});
// 严格整点归并:每个整点保留离该整点最近的数据
Map<Date, WarehouseTankThirdLogNewDTO> hourPointMap = new TreeMap<>();
minutePrecisionMap.values().forEach(item -> {
Date hourKey = roundToStrictHour(item.getCreateTime(), false);
hourPointMap.merge(hourKey, item, (oldVal, newVal) ->
Math.abs(oldVal.getCreateTime().getTime() - hourKey.getTime()) <
Math.abs(newVal.getCreateTime().getTime() - hourKey.getTime())
? oldVal : newVal
);
});
核心方法说明:
roundToMinutePrecision(Date date):时间取整到分钟(秒 / 毫秒置 0,如 10:23:45→10:23:00),
解决同一分钟内多条重复数据问题;
roundToStrictHour(Date date, boolean isEndOfHour):严格整点处理(如 23:59→23:59:59,避免跨天问题);
归并逻辑:同一整点下,对比数据时间与整点的差值,保留最近的一条数据。
2.4 缺失整点数据智能补全
// 智能填充(带有效性检查)
for (Date hourPoint : hourPoints) {
if (!hourPointMap.containsKey(hourPoint)) {
// 查找最近3小时内的有效数据(避免跨日污染)
WarehouseTankThirdLogNewDTO nearest = findNearestValidWithinRange(
hourPointMap, hourPoint, 3 * 60 * 60 * 1000);
if (nearest != null && isValidData(nearest)) {
hourPointMap.put(hourPoint, copyWithNewTime(nearest, hourPoint));
}
}
}
核心方法说明:
isValidData(WarehouseTankThirdLogNewDTO dto):校验数据有效性(温度 / 高度非空);
findNearestValidWithinRange(...):在目标整点 ±3 小时范围内,查找最近的有效数据;
copyWithNewTime(...):复制有效数据,仅替换时间为目标整点(保证数据值不变,时间标准化)。
三、数据流转示例
3.1 原始输入数据
假设从接口获取的原始items数据如下(简化格式):
3.2 预处理后数据
过滤重复、转换类型并排序后:
3.3 整点时间序列生成
最早时间 10:15:23 向下取整→10:00,最晚时间 13:05:30 向上取整→14:00,生成整点列表:
3.4 整点归并结果
3.5 缺失数据补全
12:00 整点无数据,查找 ±3 小时内的有效数据(11:00/13:00),取最近的 11:40:10(26.1,12.5),复制数据并将时间改为 12:00:
3.6 最终返回数据
按时间排序后返回:
四、完整代码
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
/**
* 储罐折线图整点数据处理工具类
* 功能:标准化原始数据为整点序列,处理缺失/重复数据,保证前端折线图连续展示
*/
public class TankChartDataProcessor {
// 核心处理方法(简化外层逻辑,保留核心)
public List<WarehouseTankThirdLogNewDTO> processTankChartData(TankQueryResponse result) {
List<WarehouseTankThirdLogNewDTO> list = new ArrayList<>();
if (result != null && result.getData() != null && result.getData().getItems() != null) {
List<TankQueryResponse.TankInfo> items = result.getData().getItems();
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
list = items.stream()
// 过滤空时间数据(防御性处理)
.filter(tankInfo -> tankInfo.getUpdatedAt() != null)
.map(source -> {
try {
WarehouseTankThirdLogNewDTO tankThirdLog = new WarehouseTankThirdLogNewDTO();
tankThirdLog.setTemperature(parseBigDecimal(source.getTemperature()));
tankThirdLog.setHeight(parseBigDecimal(source.getHeight()));
// 字符串转换Date
Date date = format.parse(source.getUpdatedAt());
tankThirdLog.setCreateTime(date);
return tankThirdLog;
} catch (Exception e) {
// 转换异常标记为null
return null;
}
})
// 过滤掉异常null数据
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 按时间排序
list.sort(Comparator.comparing(WarehouseTankThirdLogNewDTO::getCreateTime));
// 确定时间范围
if (list.isEmpty()) {
return Collections.emptyList();
}
// 生成整点时间序列
List<Date> hourPoints = generateHourPoints(
roundToHour(list.get(0).getCreateTime(), false),
roundToHour(list.get(list.size() - 1).getCreateTime(), true)
);
// 为每个整点找到最近的数据
// 构建时间到数据的映射(解决重复问题)
Map<Date, WarehouseTankThirdLogNewDTO> hourPointMap = new TreeMap<>();
Map<Date, WarehouseTankThirdLogNewDTO> minutePrecisionMap = new HashMap<>();
// 分钟级精度去重
list.forEach(item -> {
Date minuteKey = roundToMinutePrecision(item.getCreateTime());
minutePrecisionMap.merge(minuteKey, item,
(oldVal, newVal) -> oldVal.getCreateTime().after(newVal.getCreateTime()) ? oldVal : newVal);
});
// 严格整点归并
minutePrecisionMap.values().forEach(item -> {
Date hourKey = roundToStrictHour(item.getCreateTime(), false);
hourPointMap.merge(hourKey, item, (oldVal, newVal) ->
Math.abs(oldVal.getCreateTime().getTime() - hourKey.getTime()) <
Math.abs(newVal.getCreateTime().getTime() - hourKey.getTime())
? oldVal : newVal
);
});
// 智能填充(带有效性检查)
for (Date hourPoint : hourPoints) {
if (!hourPointMap.containsKey(hourPoint)) {
// 查找最近3小时内的有效数据(避免跨日污染)
WarehouseTankThirdLogNewDTO nearest = findNearestValidWithinRange(
hourPointMap, hourPoint, 3 * 60 * 60 * 1000);
if (nearest != null && isValidData(nearest)) {
hourPointMap.put(hourPoint, copyWithNewTime(nearest, hourPoint));
}
}
}
// 按时间排序后返回
return new ArrayList<>(hourPointMap.values());
}
// 查不到数据返回空列表
return Collections.emptyList();
}
// 严格小时取整(解决23:59→00:00问题)
private Date roundToStrictHour(Date date, boolean isEndOfHour) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.set(Calendar.MINUTE, isEndOfHour ? 59 : 0);
cal.set(Calendar.SECOND, isEndOfHour ? 59 : 0);
cal.set(Calendar.MILLISECOND, 0);
return cal.getTime();
}
// 分钟级精度处理
private Date roundToMinutePrecision(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return cal.getTime();
}
// 有效性检查
private boolean isValidData(WarehouseTankThirdLogNewDTO dto) {
return dto.getTemperature() != null && dto.getHeight() != null;
}
// 范围内查找有效数据
private WarehouseTankThirdLogNewDTO findNearestValidWithinRange(
Map<Date, WarehouseTankThirdLogNewDTO> map, Date target, long rangeMs) {
return map.entrySet().stream()
.filter(e -> Math.abs(e.getKey().getTime() - target.getTime()) <= rangeMs)
.min(Comparator.comparingLong(e ->
Math.abs(e.getKey().getTime() - target.getTime())))
.map(Map.Entry::getValue)
.orElse(null);
}
/**
* 时间取整(解决边界问题)
* @param date 原始时间
* @param ceiling 是否向上取整(false为向下取整)
*/
private Date roundToHour(Date date, boolean ceiling) {
Calendar calendar = Calendar.getInstance();
calendar.setTime(date);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
if (ceiling && (date.getTime() > calendar.getTimeInMillis())) {
calendar.add(Calendar.HOUR_OF_DAY, 1);
}
return calendar.getTime();
}
/**
* 生成从start到end的所有整点时间
*/
private List<Date> generateHourPoints(Date start, Date end) {
List<Date> hourPoints = new ArrayList<>();
Calendar calendar = Calendar.getInstance();
calendar.setTime(start);
// 设置为最近的整点
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
Calendar endCalendar = Calendar.getInstance();
endCalendar.setTime(end);
endCalendar.set(Calendar.MINUTE, 0);
endCalendar.set(Calendar.SECOND, 0);
endCalendar.set(Calendar.MILLISECOND, 0);
while (!calendar.after(endCalendar)) {
hourPoints.add(calendar.getTime());
calendar.add(Calendar.HOUR_OF_DAY, 1);
}
return hourPoints;
}
/**
* 复制数据对象并设置新的时间
*/
private WarehouseTankThirdLogNewDTO copyWithNewTime(
WarehouseTankThirdLogNewDTO source,
Date newTime) {
WarehouseTankThirdLogNewDTO copy = new WarehouseTankThirdLogNewDTO();
copy.setTemperature(source.getTemperature());
copy.setHeight(source.getHeight());
copy.setCreateTime(newTime);
return copy;
}
private static BigDecimal parseBigDecimal(String value) {
return (value != null && !value.isEmpty()) ?
new BigDecimal(value) :
new BigDecimal("0");
}
/**
* 备用补点方法:遍历时间范围补充缺失整点
*/
public static List<WarehouseTankThirdLogNewDTO> fillMissingRecords(LocalDateTime startTime, LocalDateTime endTime, List<WarehouseTankThirdLogNewDTO> recordList) {
// 遍历从开始时间到结束时间的每个小时
for (LocalDateTime time = startTime.truncatedTo(ChronoUnit.HOURS); time.isBefore(endTime); time = time.plusHours(1)) {
// 检查记录中是否有该小时的记录
boolean exists = false;
for (WarehouseTankThirdLogNewDTO record : recordList) {
if (record.getCreateTime().toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime().truncatedTo(ChronoUnit.HOURS).equals(time)) {
exists = true;
break;
}
}
// 如果没有该记录,补充一个
if (!exists) {
WarehouseTankThirdLogNewDTO record = new WarehouseTankThirdLogNewDTO();
record.setCreateTime(Date.from(time.atZone(ZoneId.systemDefault()).toInstant()));
recordList.add(record);
}
}
recordList.sort((r1, r2) -> r1.getCreateTime().compareTo(r2.getCreateTime()));
return recordList;
}
// 内部DTO类(简化定义,仅保留核心字段)
public static class WarehouseTankThirdLogNewDTO {
private BigDecimal temperature;
private BigDecimal height;
private Date createTime;
// Getter & Setter
public BigDecimal getTemperature() {
return temperature;
}
public void setTemperature(BigDecimal temperature) {
this.temperature = temperature;
}
public BigDecimal getHeight() {
return height;
}
public void setHeight(BigDecimal height) {
this.height = height;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
// 模拟第三方返回的响应类
public static class TankQueryResponse {
private TankData data;
public TankData getData() {
return data;
}
public void setData(TankData data) {
this.data = data;
}
public static class TankData {
private List<TankInfo> items;
public List<TankInfo> getItems() {
return items;
}
public void setItems(List<TankInfo> items) {
this.items = items;
}
}
public static class TankInfo {
private String updatedAt;
private String temperature;
private String height;
public String getUpdatedAt() {
return updatedAt;
}
public void setUpdatedAt(String updatedAt) {
this.updatedAt = updatedAt;
}
public String getTemperature() {
return temperature;
}
public void setTemperature(String temperature) {
this.temperature = temperature;
}
public String getHeight() {
return height;
}
public void setHeight(String height) {
this.height = height;
}
}
}
}




