From 359c89d726ff693e445d6aa432926c75e41904d1 Mon Sep 17 00:00:00 2001 From: "review512jwy@163.com" <“review512jwy@163.com”> Date: Mon, 26 Jan 2026 14:06:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9B=BE=E8=A1=A848=E5=B0=8F=E6=97=B6=E3=80=81?= =?UTF-8?q?=E7=B4=AF=E7=A7=AF=E5=A2=9E=E5=8A=A0=E5=89=8D=E5=B9=B4=E5=89=8D?= =?UTF-8?q?=E6=97=A5=E7=AD=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../back/vo/device/AccumulateLineData.java | 30 ++ .../dashboard/back/vo/device/LineData.java | 2 +- .../back/service/common/CommonOpt.java | 375 ++++++++++-------- .../service/common/LineDataAggregator.java | 333 ++++++++++++++++ .../common/LineDataHourAggregator.java | 224 ----------- .../impl/DeviceDataAccumulateServiceImpl.java | 12 +- .../impl/DeviceDataMeasureServiceImpl.java | 12 +- 7 files changed, 582 insertions(+), 406 deletions(-) create mode 100644 dongjian-dashboard-back-model/src/main/java/com/dongjian/dashboard/back/vo/device/AccumulateLineData.java create mode 100644 dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/LineDataAggregator.java delete mode 100644 dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/LineDataHourAggregator.java diff --git a/dongjian-dashboard-back-model/src/main/java/com/dongjian/dashboard/back/vo/device/AccumulateLineData.java b/dongjian-dashboard-back-model/src/main/java/com/dongjian/dashboard/back/vo/device/AccumulateLineData.java new file mode 100644 index 0000000..a2e250a --- /dev/null +++ b/dongjian-dashboard-back-model/src/main/java/com/dongjian/dashboard/back/vo/device/AccumulateLineData.java @@ -0,0 +1,30 @@ +package com.dongjian.dashboard.back.vo.device; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@Data +public class AccumulateLineData { + + @Schema(description = "属性编码, 默认:single,温湿度设备:temperature或者humidity") + private String attrCode; + + @Schema(description = "Y-axis data", example = "[]") + private Map subData = new HashMap<>(); + + + @Data + public static class childLineData { + @Schema(description = "X-axis data", example = "[]") + private List xData = new ArrayList<>(); + + @Schema(description = "Y-axis data", example = "[]") + private List yData = new ArrayList<>(); + } + +} diff --git a/dongjian-dashboard-back-model/src/main/java/com/dongjian/dashboard/back/vo/device/LineData.java b/dongjian-dashboard-back-model/src/main/java/com/dongjian/dashboard/back/vo/device/LineData.java index 6938200..ceac7f5 100644 --- a/dongjian-dashboard-back-model/src/main/java/com/dongjian/dashboard/back/vo/device/LineData.java +++ b/dongjian-dashboard-back-model/src/main/java/com/dongjian/dashboard/back/vo/device/LineData.java @@ -15,7 +15,7 @@ public class LineData { private String attrCode; @Schema(description = "X-axis data", example = "[]") - private List xData = new ArrayList<>(); + private List xData = new ArrayList<>(); @Schema(description = "Y-axis data", example = "[]") private Map> yData = new HashMap<>(); diff --git a/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/CommonOpt.java b/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/CommonOpt.java index e88bdd7..ef166fc 100644 --- a/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/CommonOpt.java +++ b/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/CommonOpt.java @@ -8,22 +8,24 @@ import java.time.LocalDate; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.*; +import java.util.function.BiConsumer; import java.util.function.Function; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; +import com.alibaba.fastjson2.JSON; import com.dongjian.dashboard.back.common.Constants; import com.dongjian.dashboard.back.common.language.msg.MsgLanguageChange; -import com.dongjian.dashboard.back.common.response.ResponseCode; -import com.dongjian.dashboard.back.common.response.SimpleDataResponse; import com.dongjian.dashboard.back.dao.ex.*; import com.dongjian.dashboard.back.dto.device.LineDataSearchParams; import com.dongjian.dashboard.back.model.*; import com.dongjian.dashboard.back.util.DESUtil; +import com.dongjian.dashboard.back.util.DateUtil; import com.dongjian.dashboard.back.util.DurationData; import com.dongjian.dashboard.back.vo.building.BindedBuildingVO; import com.dongjian.dashboard.back.vo.company.AuroraInfo; +import com.dongjian.dashboard.back.vo.device.AccumulateLineData; import com.dongjian.dashboard.back.vo.device.DeviceIncrement; import com.dongjian.dashboard.back.vo.device.LineData; import com.dongjian.dashboard.back.vo.record.RecordAccumulateDto; @@ -257,8 +259,10 @@ public class CommonOpt { } public List getPreDay(int days) { + // 日本时区 + ZoneId JST = ZoneId.of("Asia/Tokyo"); // 获取当前日期 - LocalDate currentDate = LocalDate.now(); + LocalDate currentDate = LocalDate.now(JST); // 设置日期格式化 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy_MM_dd"); // 用于存储日期的列表 @@ -272,94 +276,182 @@ public class CommonOpt { return dateList; } - public List getLineData(Long companyId, LineDataSearchParams lineDataSearchParams, int deviceType) { - List lineDataList = new ArrayList<>(); - for (String attrCode : lineDataSearchParams.getAttrCodeList()){ - DeviceInfo deviceInfo = getDeviceInfoByDeviceId(lineDataSearchParams.getDeviceId()); - if (null == deviceInfo){ - continue; - } + public List getAccumulateLineData(Long companyId, Integer searchType, LineDataSearchParams lineDataSearchParams) { + List result = new ArrayList<>(); + executeLineDataQuery( + result, + companyId, + searchType, + lineDataSearchParams, + (conn, lineData) -> { + + DeviceInfo deviceInfo = getDeviceInfoByDeviceId(lineDataSearchParams.getDeviceId()); + List dateList = getPreDay(1); + + for (String date : dateList) { + LocalDate current = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy_MM_dd")); + + LocalDate yesterday = current.minusDays(1); + LocalDate beforeYesterday = current.minusDays(2); + LocalDate lastYear = DateUtil.getLastYearSameIsoWeekDay(current); + LocalDate lastYearPrev = lastYear.minusDays(1); + + Double todayBase = getDayAccumulate(conn, yesterday, lineDataSearchParams.getDeviceId()); + Double yesterdayBase = getDayAccumulate(conn, beforeYesterday, lineDataSearchParams.getDeviceId()); + Double lastYearBase = getDayAccumulate(conn, lastYearPrev, lineDataSearchParams.getDeviceId()); + + AccumulateLineData acc = new AccumulateLineData(); + acc.setAttrCode(lineData.getAttrCode()); + + processAccumulateDay(conn, current, todayBase, "today", acc, deviceInfo); + if (searchType == 1) { + processAccumulateDay(conn, yesterday, yesterdayBase, "yesterday", acc, deviceInfo); + processAccumulateDay(conn, lastYear, lastYearBase, "lastYear", acc, deviceInfo); + LineDataAggregator.convertToLineDataHour(acc, lineData); + } else { + LineDataAggregator.convertToLineDataAll(acc, lineData); + } + } + }); + return result; + } + + private void executeLineDataQuery( + List result, + Long companyId, + Integer searchType, + LineDataSearchParams params, + BiConsumer dataProcessor) { + + DeviceInfo deviceInfo = getDeviceInfoByDeviceId(params.getDeviceId()); + if (deviceInfo == null) { + return; + } + + AuroraInfo auroraInfo = getAuroraInfoByApikey(Collections.singletonMap("companyId", companyId)); + if (auroraInfo == null || StringUtils.isBlank(auroraInfo.getAuroraUrl())) { + logger.error("AuroraInfo is not set for companyId: {}", companyId); + return; + } + + String jdbcUrl = null; + try { + jdbcUrl = buildAuroraJdbcUrl(auroraInfo); + } catch (ClassNotFoundException e) { + logger.error("Failed to build Aurora JDBC URL for companyId: {}", companyId, e); + return; + } + + for (String attrCode : params.getAttrCodeList()) { LineData lineData = new LineData(); lineData.setAttrCode(attrCode); - try { - Map apikeyParamMap = new HashMap<>(); - apikeyParamMap.put("companyId", companyId); - AuroraInfo apikeyInfo = getAuroraInfoByApikey(apikeyParamMap); - - if (null == apikeyInfo) { - logger.error("Failed to get AuroraInfo for companyId: {}", companyId); - lineDataList.add(lineData); - } - if (StringUtils.isNotBlank(apikeyInfo.getAuroraUrl())) { - Class.forName("com.mysql.cj.jdbc.Driver"); + try (Connection conn = DriverManager.getConnection( + jdbcUrl, + DESUtil.decrypt(auroraInfo.getAuroraUsername(), Constants.DES_SALT), + DESUtil.decrypt(auroraInfo.getAuroraPassword(), Constants.DES_SALT))) { - String regex = "(jdbc:mysql://)([^/]+)(/data_center_aeon_admin.*)"; - Pattern pattern = Pattern.compile(regex); - Matcher matcher = pattern.matcher(dbUrl); - String newJdbcUrl = ""; - if (matcher.find()) { - newJdbcUrl = matcher.replaceAll("$1" + apikeyInfo.getAuroraUrl() + "$3"); - } + dataProcessor.accept(conn, lineData); - try (Connection conn = DriverManager.getConnection( - newJdbcUrl.replace("data_center_aeon_admin", "aeon") + "&allowPublicKeyRetrieval=true", - DESUtil.decrypt(apikeyInfo.getAuroraUsername(), Constants.DES_SALT), - DESUtil.decrypt(apikeyInfo.getAuroraPassword(), Constants.DES_SALT))) { + } catch (Exception e) { + logger.error("executeLineDataQuery error", e); + } - List dateList = getPreDay(1); + result.add(lineData); + } + } - for (String date : dateList) { - //获取昨天的值,这里只针对累积设备 - Double lastDayValue = null; - if (deviceType == 2) { - lastDayValue = getLastDayValue(conn, date, lineDataSearchParams.getDeviceId()); - } + private String buildAuroraJdbcUrl(AuroraInfo auroraInfo) throws ClassNotFoundException { + Class.forName("com.mysql.cj.jdbc.Driver"); - // 提取年、月、日 - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy_MM_dd"); - LocalDate currentDate = LocalDate.parse(date, formatter); - int year = currentDate.getYear(); - int month = currentDate.getMonthValue(); - int day = currentDate.getDayOfMonth(); - - String sql = ""; - if (deviceType == 2){ - sql = "SELECT upload_value, upload_at FROM dashboard_record_accumulate " + - "WHERE device_id = ? AND attr_code = ? AND date_year = ? AND date_month = ? AND date_day = ? order by id "; - } else if (deviceType == 3){ - sql = "SELECT upload_value, upload_at FROM dashboard_record_measure " + - "WHERE device_id = ? AND attr_code = ? AND date_year = ? AND date_month = ? AND date_day = ? order by id "; - } - logger.info("getLineData sql: {}", sql); - try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { - preparedStatement.setString(1, lineDataSearchParams.getDeviceId()); - preparedStatement.setString(2, attrCode); - preparedStatement.setInt(3, year); // 设置年份 - preparedStatement.setInt(4, month); // 设置月份 - preparedStatement.setInt(5, day); // 设置日期 - try (ResultSet result = preparedStatement.executeQuery()) { - if (result.next()) { - if (deviceType == 2) { - processResult(result, lineData, deviceInfo, lastDayValue); - } else if (deviceType == 3) { - processResult(result, lineData, deviceInfo); - } - } - } - } - } - } catch (Exception e) { - logger.error("getLineData processing aurora error", e); - } + String regex = "(jdbc:mysql://)([^/]+)(/data_center_aeon_admin.*)"; + Matcher matcher = Pattern.compile(regex).matcher(dbUrl); + + if (!matcher.find()) { + throw new RuntimeException("Invalid dbUrl format"); + } + + return matcher.replaceAll("$1" + auroraInfo.getAuroraUrl() + "$3") + .replace("data_center_aeon_admin", "aeon") + + "&allowPublicKeyRetrieval=true"; + } + + private void processAccumulateDay( + Connection conn, + LocalDate date, + Double baseValue, + String yKey, + AccumulateLineData accumulateLineData, + DeviceInfo deviceInfo) { + + AccumulateLineData.childLineData childLineData = new AccumulateLineData.childLineData(); + + + String sql = "SELECT upload_value, upload_at FROM dashboard_record_accumulate " + + "WHERE device_id = ? AND attr_code = ? AND date_year = ? AND date_month = ? AND date_day = ? ORDER BY id"; + + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, deviceInfo.getDeviceId()); + ps.setString(2, accumulateLineData.getAttrCode()); + ps.setInt(3, date.getYear()); + ps.setInt(4, date.getMonthValue()); + ps.setInt(5, date.getDayOfMonth()); + + try (ResultSet rs = ps.executeQuery()) { + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + ZoneId JST = ZoneId.of("Asia/Tokyo"); + + while (rs.next()) { + long ts = rs.getLong("upload_at"); + String value = rs.getString("upload_value"); + + if (ts == 0) continue; + + String time = Instant.ofEpochMilli(ts) + .atZone(JST) + .toLocalDateTime() + .format(formatter); + + double current = StringUtils.isBlank(value) + ? 0 + : new BigDecimal(value).doubleValue(); + + double y = baseValue == null || current < baseValue + ? current + : current - baseValue; + + childLineData.getXData().add(time); + childLineData.getYData().add(CommonUtil.formatDecimal(y, deviceInfo.getDashboardDecimalPlaces())); } + } + } catch (Exception e) { + logger.error("processAccumulateDay error", e); + } - } catch (Exception e) { - logger.error("getLineData error", e); + accumulateLineData.getSubData().put(yKey, childLineData); + } + + private Double getDayAccumulate(Connection conn, LocalDate date, String deviceId) { + + String sql = "SELECT upload_value FROM dashboard_realtime_accumulate_day " + + "WHERE device_id = ? AND date_year = ? AND date_month = ? AND date_day = ?"; + + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, deviceId); + ps.setInt(2, date.getYear()); + ps.setInt(3, date.getMonthValue()); + ps.setInt(4, date.getDayOfMonth()); + + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + return new BigDecimal(rs.getString("upload_value")).doubleValue(); + } } - lineDataList.add(lineData); + } catch (Exception e) { + logger.error("getDayAccumulate error", e); } - return lineDataList; + return null; } private DeviceInfo getDeviceInfoByDeviceId(String deviceId) { @@ -371,47 +463,56 @@ public class CommonOpt { return list.isEmpty() ? null : list.get(0); } - private Double getLastDayValue(Connection conn, String date, String deviceId) { - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy_MM_dd"); - // 解析日期字符串为 LocalDate 对象 - LocalDate previousDate = LocalDate.parse(date, formatter); - // 获取前一天的日期 - LocalDate previousDay = previousDate.minusDays(1); - // 获取年、月、日 - int year = previousDay.getYear(); - int month = previousDay.getMonthValue(); - int day = previousDay.getDayOfMonth(); - - String sql = "SELECT upload_value FROM dashboard_realtime_accumulate_day " + - "WHERE device_id = ? AND date_year = ? AND date_month = ? AND date_day = ?"; - - logger.info("getUploadValue sql: {}", sql); - - try (PreparedStatement preparedStatement = conn.prepareStatement(sql)) { - preparedStatement.setString(1, deviceId); // 设置设备ID - preparedStatement.setInt(2, year); // 设置年份 - preparedStatement.setInt(3, month); // 设置月份 - preparedStatement.setInt(4, day); // 设置日期 + public List getMeasureLineData(Long companyId, Integer searchType, LineDataSearchParams lineDataSearchParams) { + List list = new ArrayList<>(); + executeLineDataQuery( + list, + companyId, + searchType, + lineDataSearchParams, + (conn, lineData) -> { + + DeviceInfo deviceInfo = getDeviceInfoByDeviceId(lineDataSearchParams.getDeviceId()); + List dateList = getPreDay(1); + + for (String date : dateList) { + LocalDate localDate = LocalDate.parse(date, DateTimeFormatter.ofPattern("yyyy_MM_dd")); + + String sql = "SELECT upload_value, upload_at FROM dashboard_record_measure " + + "WHERE device_id = ? AND attr_code = ? AND date_year = ? AND date_month = ? AND date_day = ? ORDER BY id"; + + try (PreparedStatement ps = conn.prepareStatement(sql)) { + ps.setString(1, lineDataSearchParams.getDeviceId()); + ps.setString(2, lineData.getAttrCode()); + ps.setInt(3, localDate.getYear()); + ps.setInt(4, localDate.getMonthValue()); + ps.setInt(5, localDate.getDayOfMonth()); + + try (ResultSet rs = ps.executeQuery()) { + if (rs.next()) { + processResult(rs, lineData, deviceInfo); + } + } + } catch (Exception e) { + logger.error("process measure data error", e); + } + } + }); - try (ResultSet result = preparedStatement.executeQuery()) { - if (result.next()) { - String uploadValue = result.getString("upload_value"); - logger.info("Found upload_value: {}", uploadValue); - return new BigDecimal(uploadValue).doubleValue(); - } else { - logger.warn("No data found for deviceId: {} on {}/{}/{}", deviceId, year, month, day); - } - } - } catch (SQLException e) { - logger.error("Error getLastDayValue query: ", e); + if (searchType == 1) { + list.forEach(ld -> { + LineDataAggregator.aggregateAverageByHalfHour(ld); + LineDataAggregator.fillTo48HalfHour(ld); + }); } - return null; + + return list; } private void processResult(ResultSet result, LineData lineData, DeviceInfo deviceInfo) { try { // 用于存储 xData 和 yData - List xDataList = new ArrayList<>(); + List xDataList = new ArrayList<>(); List yDataList = new ArrayList<>(); // 使用 DateTimeFormatter 来格式化时间 @@ -451,54 +552,6 @@ public class CommonOpt { } } - private void processResult(ResultSet result, LineData lineData, DeviceInfo deviceInfo, Double lastDayValue) { - try { - List xDataList = new ArrayList<>(); - List yDataList = new ArrayList<>(); - - DateTimeFormatter formatter = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - ZoneId JST = ZoneId.of("Asia/Tokyo"); - - do { - long receiveTs = result.getLong("upload_at"); - String value = result.getString("upload_value"); - - if (receiveTs == 0) { - continue; - } - - // 日本时区时间 - Instant instant = Instant.ofEpochMilli(receiveTs); - String formattedDate = instant - .atZone(JST) - .toLocalDateTime() - .format(formatter); - - double todayValue = StringUtils.isBlank(value) ? 0.0 : new BigDecimal(value).doubleValue(); - - // 默认今天值 - double yValue = todayValue; - - // 计算差值 - if (lastDayValue != null && todayValue >= lastDayValue) { - yValue = todayValue - lastDayValue; - } - - xDataList.add(formattedDate); - yDataList.add(CommonUtil.formatDecimal(yValue, deviceInfo.getDashboardDecimalPlaces())); - - } while (result.next()); - - lineData.getXData().addAll(xDataList); - lineData.getYData().put("common", yDataList); - - } catch (Exception e) { - logger.error("Error processing result set", e); - } - } - public Map buildDeviceId85To111Map(List deviceIds) { return deviceIds.stream() .filter(id -> id.endsWith(SUFFIX_85) || id.endsWith(SUFFIX_85_9003)) diff --git a/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/LineDataAggregator.java b/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/LineDataAggregator.java new file mode 100644 index 0000000..5aa2b2f --- /dev/null +++ b/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/LineDataAggregator.java @@ -0,0 +1,333 @@ +package com.dongjian.dashboard.back.service.common; + +import com.dongjian.dashboard.back.vo.device.AccumulateLineData; +import com.dongjian.dashboard.back.vo.device.LineData; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.format.DateTimeFormatter; +import java.util.*; + +/** + * 完整整合版:LineData 聚合与转换工具类(统一使用 LineDataAligner 对齐) + */ +public class LineDataAggregator { + + /* =============================== + * 时间工具 + * =============================== */ + public static final class TimeAxisUtil { + public static final DateTimeFormatter DATETIME_FMT = + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + public static String hourKey(LocalDateTime time) { + return time.withMinute(0).withSecond(0).withNano(0).format(DATETIME_FMT); + } + + public static String halfHourKey(LocalDateTime time) { + int minute = time.getMinute() < 30 ? 0 : 30; + return time.withMinute(minute).withSecond(0).withNano(0).format(DATETIME_FMT); + } + + public static String alignToHalfHour(LocalTime time) { + int minute = time.getMinute() < 30 ? 0 : 30; + return LocalTime.of(time.getHour(), minute).format(DateTimeFormatter.ofPattern("HH:mm")); + } + + public static String extractDate(String datetime) { + return LocalDateTime.parse(datetime, DATETIME_FMT).toLocalDate().toString(); + } + + public static List generate48HalfHourPoints(String date) { + List list = new ArrayList<>(48); + LocalDateTime t = LocalDate.parse(date).atStartOfDay(); + for (int i = 0; i < 48; i++) { + list.add(t.plusMinutes(i * 30).format(DATETIME_FMT)); + } + return list; + } + } + + /* =============================== + * Y 对齐工具 + * =============================== */ + public static final class LineDataAligner { + + /** + * 对齐 X 轴,补 null + */ + public static Map> alignY( + List xAxis, + Map> seriesValueMap + ) { + Map> result = new LinkedHashMap<>(); + for (Map.Entry> entry : seriesValueMap.entrySet()) { + List aligned = new ArrayList<>(xAxis.size()); + Map valueMap = entry.getValue(); + for (String x : xAxis) { + aligned.add(valueMap.get(x)); + } + result.put(entry.getKey(), aligned); + } + return result; + } + } + + /* =============================== + * 聚合策略接口(可扩展) + * =============================== */ + @FunctionalInterface + public interface HalfHourCollector { + void collect(Map bucket, String key, LocalDateTime time, Object value); + } + + /* 平均值聚合实现 */ + public static class AvgCollector implements HalfHourCollector> { + @Override + public void collect(Map> bucket, String key, LocalDateTime time, Object value) { + bucket.computeIfAbsent(key, k -> new ArrayList<>()).add(Double.parseDouble(value.toString())); + } + } + + /* 取最后一条实现 */ + public static class LastValueCollector implements HalfHourCollector { + @Override + public void collect(Map bucket, String key, LocalDateTime time, Object value) { + TimeValue old = bucket.get(key); + if (old == null || time.isAfter(old.time)) { + bucket.put(key, new TimeValue(time, value)); + } + } + } + + /* =============================== + * 核心数据模型 + * =============================== */ + public static class TimeValue { + LocalDateTime time; + Object value; + + TimeValue(LocalDateTime time, Object value) { + this.time = time; + this.value = value; + } + } + + public static class HalfHourSeries { + String date; + List xList; + List yList; + + HalfHourSeries(String date) { + this(date, new ArrayList<>(), new ArrayList<>()); + } + + HalfHourSeries(String date, List xList, List yList) { + this.date = date; + this.xList = xList; + this.yList = yList; + } + + Map toMap() { + Map map = new HashMap<>(); + for (int i = 0; i < xList.size(); i++) { + map.put(xList.get(i), yList.get(i)); + } + return map; + } + } + + /* =============================== + * 工具方法 + * =============================== */ + private static Object safeGet(List list, int index) { + return (list != null && index < list.size()) ? list.get(index) : null; + } + + private static LocalDateTime parseTime(Object x) { + return LocalDateTime.parse(String.valueOf(x), TimeAxisUtil.DATETIME_FMT); + } + + /* =============================== + * 按小时聚合(平均值) + * =============================== */ + public static void aggregateByHour(LineData lineData) { + aggregateByHourOrHalfHour(lineData, new AvgCollector(), true); + } + + /* =============================== + * 按半小时聚合(平均值) + * =============================== */ + public static void aggregateAverageByHalfHour(LineData lineData) { + aggregateByHourOrHalfHour(lineData, new AvgCollector(), false); + } + + /* 公共聚合方法 */ + private static void aggregateByHourOrHalfHour( + LineData lineData, + HalfHourCollector collector, + boolean byHour + ) { + if (lineData == null) return; + + List xData = lineData.getXData(); + Map> yDataMap = lineData.getYData(); + if (xData == null || xData.isEmpty() || yDataMap == null || yDataMap.isEmpty()) return; + + Set allSeriesKeys = yDataMap.keySet(); + + Map> bucket = new TreeMap<>(); + for (int i = 0; i < xData.size(); i++) { + LocalDateTime time = parseTime(xData.get(i)); + String key = byHour ? TimeAxisUtil.hourKey(time) : TimeAxisUtil.halfHourKey(time); + + for (String series : allSeriesKeys) { + Object yVal = safeGet(yDataMap.get(series), i); + if (yVal == null) continue; + + bucket.computeIfAbsent(key, k -> new HashMap<>()); + collector.collect(bucket.get(key), series, time, yVal); + } + } + + // 构建临时 Map 用于统一对齐 + Map> tmpMap = new HashMap<>(); + boolean isAvg = collector instanceof AvgCollector; + + for (Map.Entry> entry : bucket.entrySet()) { + String xKey = entry.getKey(); + Map seriesMap = entry.getValue(); + for (String series : allSeriesKeys) { + Object val; + T tVal = seriesMap.get(series); + if (isAvg && tVal instanceof List) { + List list = (List) tVal; + val = list.stream().mapToDouble(Double::doubleValue).average().orElse(0); + } else if (!isAvg && tVal instanceof TimeValue) { + val = ((TimeValue) tVal).value; + } else { + val = tVal; + } + tmpMap.computeIfAbsent(series, k -> new HashMap<>()).put(xKey, val); + } + } + + List newX = new ArrayList<>(bucket.keySet()); + lineData.setXData(newX); + lineData.setYData(LineDataAligner.alignY(newX, tmpMap)); + } + + /* =============================== + * AccumulateLineData -> LineData + * 半小时取最后一条 + * =============================== */ + public static void convertToLineDataHour(AccumulateLineData accumulate, LineData lineData) { + if (accumulate == null || accumulate.getSubData() == null) return; + + Map seriesMap = new LinkedHashMap<>(); + for (Map.Entry entry : accumulate.getSubData().entrySet()) { + seriesMap.put(entry.getKey(), aggregateLastByHalfHour(entry.getValue())); + } + + HalfHourSeries today = seriesMap.get("today"); + if (today == null) throw new IllegalStateException("subData 必须包含 today"); + + // 构建最终 X + List finalX = new ArrayList<>(); + for (String hm : today.xList) finalX.add(today.date + " " + hm + ":00"); + + // 构建临时 Map + Map> tmpMap = new HashMap<>(); + for (Map.Entry entry : seriesMap.entrySet()) { + tmpMap.put(entry.getKey(), entry.getValue().toMap()); + } + + lineData.setXData(finalX); + lineData.setYData(LineDataAligner.alignY(today.xList, tmpMap)); + lineData.setAttrCode(accumulate.getAttrCode()); + + fillTo48HalfHour(lineData); + } + + private static HalfHourSeries aggregateLastByHalfHour(AccumulateLineData.childLineData child) { + if (child == null || child.getXData() == null || child.getYData() == null) + return new HalfHourSeries(null); + + Map bucket = new HashMap<>(); + String date = null; + int size = Math.min(child.getXData().size(), child.getYData().size()); + + for (int i = 0; i < size; i++) { + LocalDateTime time = LocalDateTime.parse(child.getXData().get(i).toString(), TimeAxisUtil.DATETIME_FMT); + Object value = child.getYData().get(i); + if (value == null) continue; + + if (date == null) date = time.toLocalDate().toString(); + String hm = TimeAxisUtil.alignToHalfHour(time.toLocalTime()); + + TimeValue old = bucket.get(hm); + if (old == null || time.isAfter(old.time)) bucket.put(hm, new TimeValue(time, value)); + } + + List sortedHM = new ArrayList<>(bucket.keySet()); + Collections.sort(sortedHM); + + List values = new ArrayList<>(); + for (String hm : sortedHM) values.add(bucket.get(hm).value); + + return new HalfHourSeries(date, sortedHM, values); + } + + /* =============================== + * 补齐 48 半小时点 + * =============================== */ + public static void fillTo48HalfHour(LineData lineData) { + List xData = lineData.getXData(); + Map> yData = lineData.getYData(); + if (xData == null || xData.isEmpty() || yData == null) return; + + String date = TimeAxisUtil.extractDate(xData.get(0)); + List fixedX = TimeAxisUtil.generate48HalfHourPoints(date); + + // 构建临时 Map + Map> tmpMap = new HashMap<>(); + for (Map.Entry> entry : yData.entrySet()) { + Map map = new HashMap<>(); + for (int i = 0; i < xData.size(); i++) { + map.put(xData.get(i), entry.getValue().get(i)); + } + tmpMap.put(entry.getKey(), map); + } + + lineData.setXData(new ArrayList<>(fixedX)); + lineData.setYData(LineDataAligner.alignY(fixedX, tmpMap)); + } + + /* =============================== + * 全量对齐 + * =============================== */ + public static void convertToLineDataAll(AccumulateLineData accumulate, LineData lineData) { + if (accumulate == null || accumulate.getSubData() == null) return; + + LinkedHashSet xSet = new LinkedHashSet<>(); + for (AccumulateLineData.childLineData child : accumulate.getSubData().values()) { + if (child.getXData() != null) xSet.addAll(child.getXData()); + } + List mergedX = new ArrayList<>(xSet); + + Map> tmpMap = new LinkedHashMap<>(); + for (Map.Entry entry : accumulate.getSubData().entrySet()) { + Map map = new LinkedHashMap<>(); + AccumulateLineData.childLineData child = entry.getValue(); + if (child.getXData() != null && child.getYData() != null) { + int size = Math.min(child.getXData().size(), child.getYData().size()); + for (int i = 0; i < size; i++) map.put(child.getXData().get(i), child.getYData().get(i)); + } + tmpMap.put(entry.getKey(), map); + } + + lineData.setXData(mergedX); + lineData.setYData(LineDataAligner.alignY(mergedX, tmpMap)); + } +} diff --git a/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/LineDataHourAggregator.java b/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/LineDataHourAggregator.java deleted file mode 100644 index e191ee8..0000000 --- a/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/common/LineDataHourAggregator.java +++ /dev/null @@ -1,224 +0,0 @@ -package com.dongjian.dashboard.back.service.common; - -import com.dongjian.dashboard.back.vo.device.LineData; - -import java.time.LocalDate; -import java.time.LocalDateTime; -import java.time.ZoneId; -import java.time.format.DateTimeFormatter; -import java.util.*; - -/** - * 给计测设备,按小时聚合数据,求平均值 - */ -public class LineDataHourAggregator { - - private static final DateTimeFormatter INPUT_FMT = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); - - private static final DateTimeFormatter HOUR_FMT = - DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"); - - public static void aggregateByHour(LineData lineData) { - - List xData = lineData.getXData(); - Map> yDataMap = lineData.getYData(); - - if (xData == null || yDataMap == null || yDataMap.isEmpty()) { - return; - } - - // hour -> index - Map hourIndexMap = new TreeMap<>(); - Map>> hourValueMap = new TreeMap<>(); - - for (int i = 0; i < xData.size(); i++) { - String timeStr = String.valueOf(xData.get(i)); - LocalDateTime time = LocalDateTime.parse(timeStr, INPUT_FMT); - String hourKey = time.format(HOUR_FMT); - - hourIndexMap.putIfAbsent(hourKey, hourIndexMap.size()); - - for (Map.Entry> entry : yDataMap.entrySet()) { - Object yVal = entry.getValue().get(i); - if (yVal == null) { - continue; - } - - hourValueMap - .computeIfAbsent(hourKey, k -> new HashMap<>()) - .computeIfAbsent(entry.getKey(), k -> new ArrayList<>()) - .add(Double.parseDouble(String.valueOf(yVal))); - } - } - - List newX = new ArrayList<>(); - Map> newY = new HashMap<>(); - - for (String hourKey : hourIndexMap.keySet()) { - newX.add(hourKey); - - Map> seriesMap = hourValueMap.get(hourKey); - if (seriesMap == null) { - continue; - } - - for (Map.Entry> e : seriesMap.entrySet()) { - double avg = e.getValue().stream().mapToDouble(Double::doubleValue).average().orElse(0); - newY.computeIfAbsent(e.getKey(), k -> new ArrayList<>()).add(avg); - } - } - - lineData.setXData(newX); - lineData.setYData(newY); - } - - /** - * 将 LineData 补齐为【日本时区】当天 0:00 - 23:30 的 48 个半小时点 - * 没有数据的时间点,Y 值补 0 - */ - public static void fillTodayHalfHourPointsJST(LineData lineData) { - - List xData = lineData.getXData(); - Map> yDataMap = lineData.getYData(); - - if (xData == null || yDataMap == null) { - return; - } - - ZoneId JST = ZoneId.of("Asia/Tokyo"); - LocalDate todayJst = LocalDate.now(JST); - LocalDateTime start = todayJst.atStartOfDay(); - - // 原数据转 Map:time -> series -> value - Map> valueMap = new HashMap<>(); - - for (int i = 0; i < xData.size(); i++) { - String time = String.valueOf(xData.get(i)); - for (Map.Entry> e : yDataMap.entrySet()) { - valueMap - .computeIfAbsent(time, k -> new HashMap<>()) - .put(e.getKey(), e.getValue().get(i)); - } - } - - List newX = new ArrayList<>(48); - Map> newY = new HashMap<>(); - - for (int i = 0; i < 48; i++) { - LocalDateTime t = start.plusMinutes(i * 30); - String key = t.format(INPUT_FMT); - newX.add(key); - - Map seriesValues = valueMap.get(key); - for (String series : yDataMap.keySet()) { - newY - .computeIfAbsent(series, k -> new ArrayList<>()) - .add(seriesValues == null ? null : seriesValues.get(series)); - } - } - - lineData.setXData(newX); - lineData.setYData(newY); - } - - - /** - * 给计测设备,按半小时聚合数据,求平均值 - * 00~29 分 → HH:00:00 - * 30~59 分 → HH:30:00 - */ - public static void aggregateAverageByHalfHour(LineData lineData) { - - List xData = lineData.getXData(); - Map> yDataMap = lineData.getYData(); - - if (xData == null || yDataMap == null) { - return; - } - - Map>> halfHourMap = new TreeMap<>(); - - for (int i = 0; i < xData.size(); i++) { - LocalDateTime time = LocalDateTime.parse(String.valueOf(xData.get(i)), INPUT_FMT); - - int minute = time.getMinute() < 30 ? 0 : 30; - LocalDateTime halfHourTime = time.withMinute(minute).withSecond(0).withNano(0); - String key = halfHourTime.format(INPUT_FMT); - - for (Map.Entry> e : yDataMap.entrySet()) { - Object yVal = e.getValue().get(i); - if (yVal == null) continue; - - halfHourMap - .computeIfAbsent(key, k -> new HashMap<>()) - .computeIfAbsent(e.getKey(), k -> new ArrayList<>()) - .add(Double.parseDouble(String.valueOf(yVal))); - } - } - - List newX = new ArrayList<>(); - Map> newY = new HashMap<>(); - - for (Map.Entry>> entry : halfHourMap.entrySet()) { - newX.add(entry.getKey()); - - for (Map.Entry> e : entry.getValue().entrySet()) { - double avg = e.getValue().stream().mapToDouble(Double::doubleValue).average().orElse(0); - newY.computeIfAbsent(e.getKey(), k -> new ArrayList<>()).add(avg); - } - } - - lineData.setXData(newX); - lineData.setYData(newY); - } - - - /** - * 按半小时聚合,只保留每个半小时内的最后一条数据 - */ - public static void aggregateLastByHalfHour(LineData lineData) { - - List xData = lineData.getXData(); - Map> yDataMap = lineData.getYData(); - - if (xData == null || yDataMap == null) { - return; - } - - Map> halfHourLastMap = new TreeMap<>(); - - for (int i = 0; i < xData.size(); i++) { - LocalDateTime time = LocalDateTime.parse(String.valueOf(xData.get(i)), INPUT_FMT); - - int minute = time.getMinute() < 30 ? 0 : 30; - LocalDateTime halfHourTime = time.withMinute(minute).withSecond(0).withNano(0); - String key = halfHourTime.format(INPUT_FMT); - - for (Map.Entry> e : yDataMap.entrySet()) { - Object yVal = e.getValue().get(i); - if (yVal == null) continue; - - halfHourLastMap - .computeIfAbsent(key, k -> new HashMap<>()) - .put(e.getKey(), yVal); - } - } - - List newX = new ArrayList<>(); - Map> newY = new HashMap<>(); - - for (Map.Entry> entry : halfHourLastMap.entrySet()) { - newX.add(entry.getKey()); - - for (Map.Entry e : entry.getValue().entrySet()) { - newY.computeIfAbsent(e.getKey(), k -> new ArrayList<>()).add(e.getValue()); - } - } - - lineData.setXData(newX); - lineData.setYData(newY); - } - -} - diff --git a/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/impl/DeviceDataAccumulateServiceImpl.java b/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/impl/DeviceDataAccumulateServiceImpl.java index 0a2e501..58ce354 100644 --- a/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/impl/DeviceDataAccumulateServiceImpl.java +++ b/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/impl/DeviceDataAccumulateServiceImpl.java @@ -10,6 +10,7 @@ import java.util.*; import java.util.function.Function; import java.util.stream.Collectors; +import com.alibaba.fastjson2.JSON; import com.dongjian.dashboard.back.common.Constants; import com.dongjian.dashboard.back.common.language.msg.MsgLanguageChange; import com.dongjian.dashboard.back.common.response.PageInfo; @@ -23,7 +24,6 @@ import com.dongjian.dashboard.back.dto.device.LineDataSearchParams; import com.dongjian.dashboard.back.model.DeviceRawdataRealtime; import com.dongjian.dashboard.back.service.DeviceDataAccumulateService; import com.dongjian.dashboard.back.service.common.CommonOpt; -import com.dongjian.dashboard.back.service.common.LineDataHourAggregator; import com.dongjian.dashboard.back.util.CommonUtil; import com.dongjian.dashboard.back.util.DateUtil; import com.dongjian.dashboard.back.vo.data.DeviceAccumulateData; @@ -198,15 +198,7 @@ public class DeviceDataAccumulateServiceImpl implements DeviceDataAccumulateServ if (null == searchType) { searchType = 2;//默认全部数据 } - List lineDataList = commonOpt.getLineData(companyId, lineDataSearchParams, 2); - if (1 == searchType && CollectionUtils.isNotEmpty(lineDataList)) { - lineDataList.forEach(lineData -> { - //按半小时聚合 - LineDataHourAggregator.aggregateLastByHalfHour(lineData); - //补齐48个横坐标 - LineDataHourAggregator.fillTodayHalfHourPointsJST(lineData); - }); - } + List lineDataList = commonOpt.getAccumulateLineData(companyId, searchType, lineDataSearchParams); return SimpleDataResponse.success(lineDataList); } diff --git a/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/impl/DeviceDataMeasureServiceImpl.java b/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/impl/DeviceDataMeasureServiceImpl.java index 32f6b99..7e5e1ec 100644 --- a/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/impl/DeviceDataMeasureServiceImpl.java +++ b/dongjian-dashboard-back-service/src/main/java/com/dongjian/dashboard/back/service/impl/DeviceDataMeasureServiceImpl.java @@ -17,7 +17,7 @@ import com.dongjian.dashboard.back.model.DeviceRawdataRealtime; import com.dongjian.dashboard.back.model.DeviceRawdataRealtimeExample; import com.dongjian.dashboard.back.service.DeviceDataMeasureService; import com.dongjian.dashboard.back.service.common.CommonOpt; -import com.dongjian.dashboard.back.service.common.LineDataHourAggregator; +//import com.dongjian.dashboard.back.service.common.LineDataHourAggregator; import com.dongjian.dashboard.back.util.CommonUtil; import com.dongjian.dashboard.back.vo.data.DeviceAccumulateData; import com.dongjian.dashboard.back.vo.data.DeviceMeasureData; @@ -236,15 +236,7 @@ public class DeviceDataMeasureServiceImpl implements DeviceDataMeasureService { if (null == searchType) { searchType = 2;//默认全部数据 } - List lineDataList = commonOpt.getLineData(companyId, lineDataSearchParams, 3); - if (1 == searchType && CollectionUtils.isNotEmpty(lineDataList)) { - lineDataList.forEach(lineData -> { - //按半小时聚合 - LineDataHourAggregator.aggregateAverageByHalfHour(lineData); - //补齐48个横坐标 - LineDataHourAggregator.fillTodayHalfHourPointsJST(lineData); - }); - } + List lineDataList = commonOpt.getMeasureLineData(companyId, searchType, lineDataSearchParams); return SimpleDataResponse.success(lineDataList); }