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<DevicePingSample> samples) {
|
if (samples == null || samples.isEmpty()) {
|
return;
|
}
|
Map<Path, List<DevicePingSample>> 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<Path, List<DevicePingSample>> entry : grouped.entrySet()) {
|
Path path = entry.getKey();
|
List<DevicePingSample> 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<String> listDays() {
|
Path baseDir = Paths.get(loggingPath);
|
if (!Files.exists(baseDir)) {
|
return new ArrayList<>();
|
}
|
try {
|
List<String> days = new ArrayList<>();
|
try (Stream<Path> 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<String, Object> 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<Map<String, Object>> series = new ArrayList<>();
|
List<Map<String, Object>> alerts = new ArrayList<>();
|
|
for (Path file : resolveRangeFiles(deviceConfig.getDeviceType(), deviceConfig.getDeviceNo(), startTime, endTime)) {
|
if (!Files.exists(file)) {
|
continue;
|
}
|
try {
|
try (Stream<String> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> result = new LinkedHashMap<>();
|
result.put("device", device);
|
result.put("summary", summaryMap);
|
result.put("series", series);
|
result.put("alerts", alerts);
|
return result;
|
}
|
|
public Map<String, Object> queryOverview(List<DeviceConfig> deviceConfigs) {
|
List<Map<String, Object>> devices = new ArrayList<>();
|
if (deviceConfigs == null || deviceConfigs.isEmpty()) {
|
Map<String, Object> result = new LinkedHashMap<>();
|
result.put("summary", buildOverviewSummary(devices));
|
result.put("devices", devices);
|
return result;
|
}
|
|
for (DeviceConfig config : deviceConfigs) {
|
DevicePingSample latestSample = findLatestSample(config);
|
Map<String, Object> 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<String, Object> 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<Path> 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<Path> 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<Path> resolveRangeFiles(String deviceType, Integer deviceNo, long startTime, long endTime) {
|
List<Path> 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<String> 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<Path> candidates = new ArrayList<>();
|
try (Stream<Path> 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<String> 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<String, Object> buildOverviewSummary(List<Map<String, Object>> devices) {
|
Map<String, Object> 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<String, Object> 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;
|
}
|
}
|
}
|