package com.zy.asrs.service; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.serializer.SerializerFeature; import com.zy.asrs.domain.DevicePingSample; import com.zy.asrs.entity.DeviceConfig; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.io.BufferedWriter; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.stream.Stream; @Service public class DevicePingFileStorageService { private static final ZoneId ZONE_ID = ZoneId.systemDefault(); private static final DateTimeFormatter DAY_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd"); private static final DateTimeFormatter HOUR_FORMAT = DateTimeFormatter.ofPattern("HH"); private static final DateTimeFormatter TIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); @Value("${devicePingStorage.loggingPath}") private String loggingPath; @Value("${devicePingStorage.expireDays:7}") private Integer expireDays; @Value("${devicePingStorage.packetSize:-1}") private Integer packetSize; public void appendSamples(List samples) { if (samples == null || samples.isEmpty()) { return; } Map> grouped = new LinkedHashMap<>(); for (DevicePingSample sample : samples) { if (sample == null || sample.getCreateTime() == null) { continue; } grouped.computeIfAbsent(resolveFilePath(sample), k -> new ArrayList<>()).add(sample); } for (Map.Entry> entry : grouped.entrySet()) { Path path = entry.getKey(); List fileSamples = entry.getValue(); fileSamples.sort(Comparator.comparing(DevicePingSample::getCreateTime, Comparator.nullsLast(Date::compareTo))); try { Files.createDirectories(path.getParent()); try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8, StandardOpenOption.CREATE, StandardOpenOption.APPEND)) { for (DevicePingSample sample : fileSamples) { writer.write(JSON.toJSONStringWithDateFormat(sample, "yyyy-MM-dd HH:mm:ss", SerializerFeature.WriteDateUseDateFormat)); writer.newLine(); } } } catch (Exception ignored) { } } } public List listDays() { Path baseDir = Paths.get(loggingPath); if (!Files.exists(baseDir)) { return new ArrayList<>(); } try { List days = new ArrayList<>(); try (Stream stream = Files.list(baseDir)) { stream .filter(Files::isDirectory) .forEach(path -> { String day = path.getFileName().toString(); if (day.length() == 8 && day.chars().allMatch(Character::isDigit)) { days.add(day); } }); } days.sort(Comparator.reverseOrder()); return days; } catch (Exception ignored) { return new ArrayList<>(); } } public Map queryTrend(DeviceConfig deviceConfig, long startTime, long endTime, Integer bucketSec) { if (deviceConfig == null || startTime <= 0 || endTime <= 0 || endTime < startTime) { return Collections.emptyMap(); } TrendAccumulator summary = new TrendAccumulator(); List> series = new ArrayList<>(); List> alerts = new ArrayList<>(); for (Path file : resolveRangeFiles(deviceConfig.getDeviceType(), deviceConfig.getDeviceNo(), startTime, endTime)) { if (!Files.exists(file)) { continue; } try { try (Stream lines = Files.lines(file, StandardCharsets.UTF_8)) { lines.forEach(line -> { if (line == null || line.trim().isEmpty()) { return; } DevicePingSample sample; try { sample = JSON.parseObject(line, DevicePingSample.class); } catch (Exception ex) { return; } if (sample == null || sample.getCreateTime() == null) { return; } long ts = sample.getCreateTime().getTime(); if (ts < startTime || ts > endTime) { return; } summary.add(sample); Map point = new LinkedHashMap<>(); point.put("time", ts); point.put("timeLabel", formatDateTime(ts)); point.put("reachable", Boolean.TRUE.equals(sample.getReachable())); point.put("status", sample.getStatus()); point.put("message", sample.getMessage()); point.put("latencyMs", sample.getLatencyMs()); point.put("avgLatencyMs", sample.getAvgLatencyMs()); point.put("minLatencyMs", sample.getMinLatencyMs()); point.put("maxLatencyMs", sample.getMaxLatencyMs()); point.put("packetSize", resolvePacketSize(sample.getPacketSize())); point.put("probeCount", sample.getProbeCount()); point.put("successProbeCount", sample.getSuccessProbeCount()); point.put("successRate", round2(summaryRate(sample.getSuccessProbeCount(), sample.getProbeCount()))); point.put("failProbeCount", Math.max(0, safeInt(sample.getProbeCount()) - safeInt(sample.getSuccessProbeCount()))); series.add(point); if (!"OK".equalsIgnoreCase(safeText(sample.getStatus(), "")) && alerts.size() < 120) { Map alert = new LinkedHashMap<>(); alert.put("time", ts); alert.put("timeLabel", formatDateTime(ts)); alert.put("status", sample.getStatus()); alert.put("message", safeText(sample.getMessage(), "探测失败")); alert.put("ip", sample.getIp()); alerts.add(alert); } }); } } catch (Exception ignored) { } } series.sort(Comparator.comparingLong(item -> Long.parseLong(String.valueOf(item.get("time"))))); Map device = new LinkedHashMap<>(); device.put("deviceType", deviceConfig.getDeviceType()); device.put("deviceNo", deviceConfig.getDeviceNo()); device.put("ip", deviceConfig.getIp()); device.put("port", deviceConfig.getPort()); device.put("label", buildDeviceLabel(deviceConfig)); device.put("packetSize", resolvePacketSize(summary.latestPacketSize)); Map summaryMap = new LinkedHashMap<>(); summaryMap.put("totalSamples", summary.totalCount); summaryMap.put("successSamples", summary.successCount); summaryMap.put("failSamples", summary.failCount); summaryMap.put("successRate", round2(summary.successRate())); summaryMap.put("avgLatencyMs", summary.latestAvgLatency); summaryMap.put("minLatencyMs", summary.latestMinLatency); summaryMap.put("maxLatencyMs", summary.latestMaxLatency); summaryMap.put("latestStatus", summary.latestStatus); summaryMap.put("latestTime", summary.latestTime); summaryMap.put("latestTimeLabel", summary.latestTime <= 0 ? "" : formatDateTime(summary.latestTime)); summaryMap.put("packetSize", resolvePacketSize(summary.latestPacketSize)); summaryMap.put("bucketSec", 1); summaryMap.put("startTime", startTime); summaryMap.put("endTime", endTime); summaryMap.put("startTimeLabel", formatDateTime(startTime)); summaryMap.put("endTimeLabel", formatDateTime(endTime)); Map result = new LinkedHashMap<>(); result.put("device", device); result.put("summary", summaryMap); result.put("series", series); result.put("alerts", alerts); return result; } public Map queryOverview(List deviceConfigs) { List> devices = new ArrayList<>(); if (deviceConfigs == null || deviceConfigs.isEmpty()) { Map result = new LinkedHashMap<>(); result.put("summary", buildOverviewSummary(devices)); result.put("devices", devices); return result; } for (DeviceConfig config : deviceConfigs) { DevicePingSample latestSample = findLatestSample(config); Map item = new LinkedHashMap<>(); item.put("deviceType", config.getDeviceType()); item.put("deviceNo", config.getDeviceNo()); item.put("ip", config.getIp()); item.put("port", config.getPort()); item.put("label", buildDeviceLabel(config)); item.put("packetSize", resolvePacketSize(latestSample == null ? null : latestSample.getPacketSize())); if (latestSample == null) { item.put("status", "NO_DATA"); item.put("statusText", "暂无数据"); item.put("statusLevel", 3); item.put("reachable", false); item.put("successRate", null); item.put("avgLatencyMs", null); item.put("minLatencyMs", null); item.put("maxLatencyMs", null); item.put("latestTime", null); item.put("latestTimeLabel", "--"); item.put("message", ""); } else { item.put("status", safeText(latestSample.getStatus(), "UNKNOWN")); item.put("statusText", resolveStatusText(latestSample.getStatus())); item.put("statusLevel", resolveStatusLevel(latestSample.getStatus())); item.put("reachable", Boolean.TRUE.equals(latestSample.getReachable())); item.put("successRate", round2(summaryRate(latestSample.getSuccessProbeCount(), latestSample.getProbeCount()))); item.put("avgLatencyMs", latestSample.getAvgLatencyMs()); item.put("minLatencyMs", latestSample.getMinLatencyMs()); item.put("maxLatencyMs", latestSample.getMaxLatencyMs()); item.put("latestTime", latestSample.getCreateTime() == null ? null : latestSample.getCreateTime().getTime()); item.put("latestTimeLabel", latestSample.getCreateTime() == null ? "--" : formatDateTime(latestSample.getCreateTime().getTime())); item.put("message", safeText(latestSample.getMessage(), "")); } devices.add(item); } devices.sort((a, b) -> { int levelDiff = toInt(a.get("statusLevel")) - toInt(b.get("statusLevel")); if (levelDiff != 0) { return levelDiff; } int typeDiff = safeText(String.valueOf(a.get("deviceType")), "").compareTo(safeText(String.valueOf(b.get("deviceType")), "")); if (typeDiff != 0) { return typeDiff; } return Integer.compare(toInt(a.get("deviceNo")), toInt(b.get("deviceNo"))); }); Map result = new LinkedHashMap<>(); result.put("summary", buildOverviewSummary(devices)); result.put("devices", devices); return result; } public void cleanupExpired() { Path baseDir = Paths.get(loggingPath); if (!Files.exists(baseDir) || expireDays == null || expireDays <= 0) { return; } long cutoff = System.currentTimeMillis() - expireDays * 24L * 60L * 60L * 1000L; try { try (Stream stream = Files.list(baseDir)) { stream .filter(Files::isDirectory) .forEach(path -> { String day = path.getFileName().toString(); if (day.length() != 8 || !day.chars().allMatch(Character::isDigit)) { return; } try { LocalDate date = LocalDate.parse(day, DAY_FORMAT); long startOfDay = date.atStartOfDay(ZONE_ID).toInstant().toEpochMilli(); if (startOfDay >= cutoff) { return; } try (Stream walk = Files.walk(path)) { walk.sorted(Comparator.reverseOrder()) .forEach(p -> { try { Files.deleteIfExists(p); } catch (Exception ignored) { } }); } } catch (Exception ignored) { } }); } } catch (Exception ignored) { } } private Path resolveFilePath(DevicePingSample sample) { LocalDateTime dateTime = LocalDateTime.ofInstant(sample.getCreateTime().toInstant(), ZONE_ID); String day = DAY_FORMAT.format(dateTime); String hour = HOUR_FORMAT.format(dateTime); String fileName = sample.getDeviceType() + "_" + sample.getDeviceNo() + "_" + day + "_" + hour + ".log"; return Paths.get(loggingPath, day, fileName); } private List resolveRangeFiles(String deviceType, Integer deviceNo, long startTime, long endTime) { List paths = new ArrayList<>(); LocalDateTime current = LocalDateTime.ofInstant(Instant.ofEpochMilli(startTime), ZONE_ID) .withMinute(0).withSecond(0).withNano(0); LocalDateTime end = LocalDateTime.ofInstant(Instant.ofEpochMilli(endTime), ZONE_ID) .withMinute(0).withSecond(0).withNano(0); while (!current.isAfter(end)) { String day = DAY_FORMAT.format(current); String hour = HOUR_FORMAT.format(current); String fileName = deviceType + "_" + deviceNo + "_" + day + "_" + hour + ".log"; paths.add(Paths.get(loggingPath, day, fileName)); current = current.plusHours(1); } return paths; } private String formatDateTime(long timestamp) { return TIME_FORMAT.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZONE_ID)); } private String buildDeviceLabel(DeviceConfig config) { return String.format(Locale.ROOT, "%s-%s (%s)", safeText(config.getDeviceType(), "-"), config.getDeviceNo() == null ? "-" : String.valueOf(config.getDeviceNo()), safeText(config.getIp(), "-")); } private DevicePingSample findLatestSample(DeviceConfig config) { if (config == null || config.getDeviceNo() == null || safeText(config.getDeviceType(), "").isEmpty()) { return null; } List days = listDays(); for (String day : days) { Path dayDir = Paths.get(loggingPath, day); if (!Files.exists(dayDir)) { continue; } String prefix = config.getDeviceType() + "_" + config.getDeviceNo() + "_" + day + "_"; List candidates = new ArrayList<>(); try (Stream stream = Files.list(dayDir)) { stream.filter(path -> !Files.isDirectory(path)) .filter(path -> { String name = path.getFileName().toString(); return name.startsWith(prefix) && name.endsWith(".log"); }) .forEach(candidates::add); } catch (Exception ignored) { } candidates.sort((a, b) -> b.getFileName().toString().compareTo(a.getFileName().toString())); for (Path candidate : candidates) { DevicePingSample sample = readLastSample(candidate); if (sample != null) { return sample; } } } return null; } private DevicePingSample readLastSample(Path file) { try { List lines = Files.readAllLines(file, StandardCharsets.UTF_8); for (int i = lines.size() - 1; i >= 0; i--) { String line = lines.get(i); if (line == null || line.trim().isEmpty()) { continue; } try { DevicePingSample sample = JSON.parseObject(line, DevicePingSample.class); if (sample != null && sample.getCreateTime() != null) { return sample; } } catch (Exception ignored) { } } } catch (Exception ignored) { } return null; } private Map buildOverviewSummary(List> devices) { Map summary = new LinkedHashMap<>(); long total = devices == null ? 0L : devices.size(); long okCount = 0L; long unstableCount = 0L; long offlineCount = 0L; long noDataCount = 0L; long latencyCount = 0L; long latencySum = 0L; Long maxLatency = null; if (devices != null) { for (Map item : devices) { String status = String.valueOf(item.get("status")); if ("OK".equals(status)) { okCount++; } else if ("UNSTABLE".equals(status)) { unstableCount++; } else if ("NO_DATA".equals(status)) { noDataCount++; } else { offlineCount++; } Object avgLatency = item.get("avgLatencyMs"); if (avgLatency instanceof Number) { latencySum += ((Number) avgLatency).longValue(); latencyCount++; } Object peakLatency = item.get("maxLatencyMs"); if (peakLatency instanceof Number) { long candidate = ((Number) peakLatency).longValue(); if (maxLatency == null || candidate > maxLatency) { maxLatency = candidate; } } } } summary.put("totalDevices", total); summary.put("okDevices", okCount); summary.put("unstableDevices", unstableCount); summary.put("offlineDevices", offlineCount); summary.put("noDataDevices", noDataCount); summary.put("avgLatencyMs", latencyCount <= 0 ? null : Math.round(latencySum * 100D / latencyCount) / 100D); summary.put("maxLatencyMs", maxLatency); return summary; } private String safeText(String value, String defaultValue) { if (value == null || value.trim().isEmpty()) { return defaultValue; } return value.trim(); } private Double round2(double value) { return Math.round(value * 100D) / 100D; } private Integer resolvePacketSize(Integer samplePacketSize) { if (samplePacketSize != null) { return Math.max(-1, samplePacketSize); } return packetSize == null ? -1 : Math.max(-1, packetSize); } private double summaryRate(Integer successCount, Integer probeCount) { int total = safeInt(probeCount); if (total <= 0) { return 0D; } return safeInt(successCount) * 100D / total; } private int safeInt(Integer value) { return value == null ? 0 : value; } private int toInt(Object value) { if (value == null) { return 0; } if (value instanceof Number) { return ((Number) value).intValue(); } try { return Integer.parseInt(String.valueOf(value)); } catch (Exception ignored) { return 0; } } private String resolveStatusText(String status) { if ("OK".equalsIgnoreCase(status)) { return "正常"; } if ("UNSTABLE".equalsIgnoreCase(status)) { return "波动"; } if ("TIMEOUT".equalsIgnoreCase(status)) { return "超时"; } if ("ERROR".equalsIgnoreCase(status)) { return "异常"; } if ("NO_DATA".equalsIgnoreCase(status)) { return "暂无数据"; } return safeText(status, "未知"); } private int resolveStatusLevel(String status) { if ("TIMEOUT".equalsIgnoreCase(status) || "ERROR".equalsIgnoreCase(status)) { return 0; } if ("UNSTABLE".equalsIgnoreCase(status)) { return 1; } if ("OK".equalsIgnoreCase(status)) { return 2; } return 3; } private static class TrendAccumulator { private long totalCount; private long successCount; private long failCount; private Long latestAvgLatency; private Long latestMinLatency; private Long latestMaxLatency; private Integer latestPacketSize; private String latestStatus; private long latestTime; private void add(DevicePingSample sample) { totalCount++; if (Boolean.TRUE.equals(sample.getReachable())) { successCount++; } else { failCount++; } if (sample.getCreateTime() != null) { long currentTime = sample.getCreateTime().getTime(); if (currentTime >= latestTime) { latestTime = currentTime; latestAvgLatency = sample.getAvgLatencyMs(); latestMinLatency = sample.getMinLatencyMs(); latestMaxLatency = sample.getMaxLatencyMs(); latestPacketSize = sample.getPacketSize(); latestStatus = sample.getStatus(); } } } private Double successRate() { if (totalCount <= 0) { return 0D; } return (successCount * 100D) / totalCount; } } }