| | |
| | | import com.core.annotations.ManagerAuth; |
| | | import com.core.common.Cools; |
| | | import com.core.common.R; |
| | | import com.zy.asrs.entity.BasDevp; |
| | | import com.zy.asrs.entity.DeviceDataLog; |
| | | import com.zy.asrs.service.BasDevpService; |
| | | import com.zy.common.web.BaseController; |
| | | import com.zy.core.enums.SlaveType; |
| | | import com.zy.core.model.StationObjModel; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.beans.factory.annotation.Value; |
| | | import org.springframework.web.bind.annotation.PathVariable; |
| | | import org.springframework.web.bind.annotation.RequestMapping; |
| | | import org.springframework.web.bind.annotation.RequestParam; |
| | | import org.springframework.web.bind.annotation.RestController; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | import java.io.BufferedWriter; |
| | | import java.io.OutputStreamWriter; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | | import java.nio.file.Paths; |
| | | import java.time.LocalDate; |
| | | import java.time.LocalDateTime; |
| | | import java.time.LocalTime; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.time.format.DateTimeParseException; |
| | | import java.util.*; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | import java.util.stream.Collectors; |
| | | import java.util.stream.Stream; |
| | | import java.util.zip.ZipEntry; |
| | | import java.util.zip.ZipOutputStream; |
| | | |
| | | @Slf4j |
| | | @RestController |
| | | public class DeviceLogController extends BaseController { |
| | | |
| | | private static final List<String> DEVICE_TYPE_ORDER = Arrays.asList("Crn", "DualCrn", "Rgv", "Devp"); |
| | | private static final Map<String, String> DEVICE_TYPE_LABELS = new LinkedHashMap<>(); |
| | | |
| | | static { |
| | | DEVICE_TYPE_LABELS.put("Crn", "堆垛机"); |
| | | DEVICE_TYPE_LABELS.put("DualCrn", "双工位堆垛机"); |
| | | DEVICE_TYPE_LABELS.put("Rgv", "RGV"); |
| | | DEVICE_TYPE_LABELS.put("Devp", "输送设备"); |
| | | } |
| | | |
| | | @Autowired |
| | | private BasDevpService basDevpService; |
| | | |
| | | @Value("${deviceLogStorage.loggingPath}") |
| | | private String loggingPath; |
| | | |
| | | @Value("${logging.file.path:./stock/out/@pom.build.finalName@/logs}") |
| | | private String systemLoggingPath; |
| | | |
| | | private static final DateTimeFormatter SYSTEM_LOG_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | private static final DateTimeFormatter SYSTEM_LOG_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); |
| | | private static final DateTimeFormatter SYSTEM_LOG_EXPORT_TIME = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"); |
| | | private static final Pattern SYSTEM_LOG_TIMESTAMP = Pattern.compile("^(\\d{2}:\\d{2}:\\d{2}\\.\\d{3})"); |
| | | private static final Pattern SYSTEM_LOG_ROLLED_FILE = Pattern.compile("^(info|error)_(\\d{4}-\\d{2}-\\d{2})\\.(\\d+)\\.log$"); |
| | | |
| | | private static class ProgressInfo { |
| | | long totalRaw; |
| | |
| | | boolean finished; |
| | | } |
| | | |
| | | private static class FileNameInfo { |
| | | String type; |
| | | String deviceNo; |
| | | String stationId; |
| | | String day; |
| | | int index; |
| | | } |
| | | |
| | | private static class FileTimeRange { |
| | | Long startTime; |
| | | Long endTime; |
| | | } |
| | | |
| | | private static class DeviceAggregate { |
| | | String type; |
| | | String typeLabel; |
| | | String deviceNo; |
| | | String stationId; |
| | | List<String> stationIds; |
| | | int fileCount; |
| | | Long firstTime; |
| | | Long lastTime; |
| | | Integer firstIndex; |
| | | Integer lastIndex; |
| | | Path firstFile; |
| | | Path lastFile; |
| | | } |
| | | |
| | | private static class SystemLogFileInfo { |
| | | String logType; |
| | | LocalDate day; |
| | | Integer index; |
| | | boolean active; |
| | | Path path; |
| | | } |
| | | |
| | | private static class SystemLogDownloadRequest { |
| | | String logType; |
| | | LocalDateTime startTime; |
| | | LocalDateTime endTime; |
| | | List<Path> files; |
| | | } |
| | | |
| | | private static final Map<String, ProgressInfo> DOWNLOAD_PROGRESS = new ConcurrentHashMap<>(); |
| | | private static final int SYSTEM_LOG_MAX_RANGE_DAYS = 10; |
| | | private static final int SYSTEM_LOG_MATCH_BUFFER_LINES = 200000; |
| | | private static final String SYSTEM_LOG_ENTRY_NAME = "system.log"; |
| | | |
| | | private static final Map<String, SystemLogDownloadRequest> SYSTEM_LOG_DOWNLOAD_REQUESTS = new ConcurrentHashMap<>(); |
| | | |
| | | @RequestMapping(value = "/deviceLog/dates/auth") |
| | | @ManagerAuth |
| | |
| | | if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { |
| | | return R.ok(new ArrayList<>()); |
| | | } |
| | | List<Path> files = Files.list(dayDir) |
| | | .filter(p -> !Files.isDirectory(p) && p.getFileName().toString().endsWith(".log")) |
| | | .collect(Collectors.toList()); |
| | | List<Path> files = listDayLogFiles(dayDir); |
| | | Map<String, Map<String, Object>> deviceMap = new HashMap<>(); |
| | | for (Path p : files) { |
| | | String name = p.getFileName().toString(); |
| | | String[] parts = name.split("_"); |
| | | if (parts.length < 4) { |
| | | FileNameInfo info = parseFileName(p.getFileName().toString()); |
| | | if (info == null || !day.equals(info.day)) { |
| | | continue; |
| | | } |
| | | String deviceNo = parts[1]; |
| | | String type = parts[0]; |
| | | Map<String, Object> info = deviceMap.computeIfAbsent(deviceNo, k -> { |
| | | String deviceKey = buildDeviceKey(info.type, info.deviceNo, info.stationId); |
| | | Map<String, Object> infoMap = deviceMap.computeIfAbsent(deviceKey, k -> { |
| | | Map<String, Object> map = new HashMap<>(); |
| | | map.put("deviceNo", deviceNo); |
| | | map.put("deviceNo", info.deviceNo); |
| | | map.put("stationId", info.stationId); |
| | | map.put("types", new HashSet<String>()); |
| | | map.put("fileCount", 0); |
| | | return map; |
| | | }); |
| | | ((Set<String>) info.get("types")).add(type); |
| | | info.put("fileCount", ((Integer) info.get("fileCount")) + 1); |
| | | ((Set<String>) infoMap.get("types")).add(info.type); |
| | | infoMap.put("fileCount", ((Integer) infoMap.get("fileCount")) + 1); |
| | | } |
| | | List<Map<String, Object>> res = deviceMap.values().stream().map(m -> { |
| | | Map<String, Object> x = new HashMap<>(); |
| | | x.put("deviceNo", m.get("deviceNo")); |
| | | x.put("stationId", m.get("stationId")); |
| | | x.put("types", ((Set<String>) m.get("types")).stream().collect(Collectors.toList())); |
| | | x.put("fileCount", m.get("fileCount")); |
| | | return x; |
| | |
| | | } |
| | | } |
| | | |
| | | @RequestMapping(value = "/deviceLog/day/{day}/summary/auth") |
| | | @ManagerAuth |
| | | public R summary(@PathVariable("day") String day) { |
| | | try { |
| | | String dayClean = normalizeDay(day); |
| | | if (dayClean == null) { |
| | | return R.error("日期格式错误"); |
| | | } |
| | | Path dayDir = Paths.get(loggingPath, dayClean); |
| | | if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { |
| | | return R.ok(buildEmptySummary()); |
| | | } |
| | | |
| | | Map<String, DeviceAggregate> aggregateMap = new LinkedHashMap<>(); |
| | | List<Path> files = listDayLogFiles(dayDir); |
| | | for (Path file : files) { |
| | | FileNameInfo info = parseFileName(file.getFileName().toString()); |
| | | if (info == null || !dayClean.equals(info.day) || !DEVICE_TYPE_LABELS.containsKey(info.type)) { |
| | | continue; |
| | | } |
| | | String key = buildDeviceKey(info.type, info.deviceNo, info.stationId); |
| | | DeviceAggregate aggregate = aggregateMap.computeIfAbsent(key, k -> { |
| | | DeviceAggregate x = new DeviceAggregate(); |
| | | x.type = info.type; |
| | | x.typeLabel = DEVICE_TYPE_LABELS.get(info.type); |
| | | x.deviceNo = info.deviceNo; |
| | | x.stationId = info.stationId; |
| | | return x; |
| | | }); |
| | | aggregate.fileCount += 1; |
| | | if (aggregate.firstIndex == null || info.index < aggregate.firstIndex) { |
| | | aggregate.firstIndex = info.index; |
| | | aggregate.firstFile = file; |
| | | } |
| | | if (aggregate.lastIndex == null || info.index > aggregate.lastIndex) { |
| | | aggregate.lastIndex = info.index; |
| | | aggregate.lastFile = file; |
| | | } |
| | | } |
| | | for (DeviceAggregate aggregate : aggregateMap.values()) { |
| | | if (aggregate.firstFile != null) { |
| | | FileTimeRange firstRange = readFileTimeRange(aggregate.firstFile, aggregate.stationId); |
| | | aggregate.firstTime = firstRange.startTime != null ? firstRange.startTime : firstRange.endTime; |
| | | } |
| | | if (aggregate.lastFile != null) { |
| | | FileTimeRange lastRange = readFileTimeRange(aggregate.lastFile, aggregate.stationId); |
| | | aggregate.lastTime = lastRange.endTime != null ? lastRange.endTime : lastRange.startTime; |
| | | } |
| | | } |
| | | enrichDevpStationIds(aggregateMap.values()); |
| | | return R.ok(buildSummaryResponse(aggregateMap.values())); |
| | | } catch (Exception e) { |
| | | log.error("读取设备日志摘要失败", e); |
| | | return R.error("读取设备日志摘要失败"); |
| | | } |
| | | } |
| | | |
| | | @RequestMapping(value = "/deviceLog/day/{day}/timeline/auth") |
| | | @ManagerAuth |
| | | public R timeline(@PathVariable("day") String day, |
| | | @RequestParam("type") String type, |
| | | @RequestParam("deviceNo") String deviceNo, |
| | | @RequestParam(value = "stationId", required = false) String stationId) { |
| | | try { |
| | | String dayClean = normalizeDay(day); |
| | | if (dayClean == null) { |
| | | return R.error("日期格式错误"); |
| | | } |
| | | if (type == null || SlaveType.findInstance(type) == null) { |
| | | return R.error("设备类型错误"); |
| | | } |
| | | if (deviceNo == null || !deviceNo.chars().allMatch(Character::isDigit)) { |
| | | return R.error("设备编号错误"); |
| | | } |
| | | if (isDevpType(type) && (stationId == null || !stationId.chars().allMatch(Character::isDigit))) { |
| | | return R.error("站点编号错误"); |
| | | } |
| | | Path dayDir = Paths.get(loggingPath, dayClean); |
| | | if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { |
| | | return R.error("未找到日志文件"); |
| | | } |
| | | List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); |
| | | if (files.isEmpty()) { |
| | | return R.error("未找到日志文件"); |
| | | } |
| | | |
| | | List<Map<String, Object>> segments = new ArrayList<>(); |
| | | Long startTime = null; |
| | | for (int i = 0; i < files.size(); i++) { |
| | | Long segmentStart = getFileStartTime(files.get(i), stationId); |
| | | if (segmentStart != null && (startTime == null || segmentStart < startTime)) { |
| | | startTime = segmentStart; |
| | | } |
| | | Map<String, Object> segment = new LinkedHashMap<>(); |
| | | segment.put("offset", i); |
| | | segment.put("startTime", segmentStart); |
| | | segment.put("endTime", null); |
| | | segments.add(segment); |
| | | } |
| | | Long endTime = getFileEndTime(files.get(files.size() - 1), stationId); |
| | | if (endTime == null) { |
| | | for (int i = segments.size() - 1; i >= 0; i--) { |
| | | Long segmentStart = (Long) segments.get(i).get("startTime"); |
| | | if (segmentStart != null) { |
| | | endTime = segmentStart; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | | for (int i = 0; i < segments.size(); i++) { |
| | | Long segmentEnd = null; |
| | | if (i < segments.size() - 1) { |
| | | segmentEnd = (Long) segments.get(i + 1).get("startTime"); |
| | | } |
| | | if (segmentEnd == null && i == segments.size() - 1) { |
| | | segmentEnd = endTime; |
| | | } |
| | | if (segmentEnd == null) { |
| | | segmentEnd = (Long) segments.get(i).get("startTime"); |
| | | } |
| | | segments.get(i).put("endTime", segmentEnd); |
| | | } |
| | | |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("type", type); |
| | | result.put("typeLabel", DEVICE_TYPE_LABELS.getOrDefault(type, type)); |
| | | result.put("deviceNo", deviceNo); |
| | | result.put("stationId", stationId); |
| | | result.put("startTime", startTime); |
| | | result.put("endTime", endTime); |
| | | result.put("totalFiles", files.size()); |
| | | result.put("segments", segments); |
| | | return R.ok(result); |
| | | } catch (Exception e) { |
| | | log.error("读取设备日志时间轴失败", e); |
| | | return R.error("读取设备日志时间轴失败"); |
| | | } |
| | | } |
| | | |
| | | @RequestMapping(value = "/deviceLog/day/{day}/preview/auth") |
| | | @ManagerAuth |
| | | public R preview(@PathVariable("day") String day, |
| | | @RequestParam("type") String type, |
| | | @RequestParam("deviceNo") String deviceNo, |
| | | @RequestParam(value = "stationId", required = false) String stationId, |
| | | @RequestParam(value = "offset", required = false) Integer offset, |
| | | @RequestParam(value = "limit", required = false) Integer limit) { |
| | | try { |
| | |
| | | if (deviceNo == null || !deviceNo.chars().allMatch(Character::isDigit)) { |
| | | return R.error("设备编号错误"); |
| | | } |
| | | if (isDevpType(type) && (stationId == null || !stationId.chars().allMatch(Character::isDigit))) { |
| | | return R.error("站点编号错误"); |
| | | } |
| | | Path dayDir = Paths.get(loggingPath, dayClean); |
| | | if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { |
| | | return R.ok(new ArrayList<>()); |
| | | } |
| | | String prefix = type + "_" + deviceNo + "_" + dayClean + "_"; |
| | | List<Path> files = Files.list(dayDir) |
| | | .filter(p -> { |
| | | String name = p.getFileName().toString(); |
| | | return name.endsWith(".log") && name.startsWith(prefix); |
| | | }).collect(Collectors.toList()); |
| | | |
| | | files.sort(Comparator.comparingInt(p -> { |
| | | String n = p.getFileName().toString(); |
| | | try { |
| | | String suf = n.substring(prefix.length(), n.length() - 4); |
| | | return Integer.parseInt(suf); |
| | | } catch (Exception e) { |
| | | return Integer.MAX_VALUE; |
| | | } |
| | | })); |
| | | |
| | | List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); |
| | | |
| | | int from = offset == null || offset < 0 ? 0 : offset; |
| | | int max = limit == null || limit <= 0 ? 5 : limit; // 默认读取5个文件 |
| | | if (max > 10) max = 10; // 限制最大文件数,防止超时 |
| | | int max = limit == null || limit <= 0 ? 5 : limit; |
| | | if (max > 10) max = 10; |
| | | int to = Math.min(files.size(), from + max); |
| | | |
| | | |
| | | if (from >= files.size()) { |
| | | return R.ok(new ArrayList<>()); |
| | | } |
| | | |
| | | |
| | | List<Path> targetFiles = files.subList(from, to); |
| | | List<DeviceDataLog> resultLogs = new ArrayList<>(); |
| | | |
| | | |
| | | for (Path f : targetFiles) { |
| | | try (Stream<String> lines = Files.lines(f, StandardCharsets.UTF_8)) { |
| | | lines.forEach(line -> { |
| | | if (line != null && !line.trim().isEmpty()) { |
| | | try { |
| | | DeviceDataLog logItem = JSON.parseObject(line, DeviceDataLog.class); |
| | | resultLogs.add(logItem); |
| | | if (matchesRequestedStation(logItem, stationId)) { |
| | | resultLogs.add(logItem); |
| | | } |
| | | } catch (Exception e) { |
| | | // 忽略解析错误 |
| | | } |
| | | } |
| | | }); |
| | |
| | | log.error("读取日志文件失败: " + f, e); |
| | | } |
| | | } |
| | | // 按时间排序 |
| | | resultLogs.sort(Comparator.comparing(DeviceDataLog::getCreateTime, Comparator.nullsLast(Date::compareTo))); |
| | | |
| | | |
| | | return R.ok(resultLogs); |
| | | } catch (Exception e) { |
| | | log.error("预览日志失败", e); |
| | |
| | | public R seek(@PathVariable("day") String day, |
| | | @RequestParam("type") String type, |
| | | @RequestParam("deviceNo") String deviceNo, |
| | | @RequestParam(value = "stationId", required = false) String stationId, |
| | | @RequestParam("timestamp") Long timestamp) { |
| | | try { |
| | | String dayClean = day == null ? null : day.replaceAll("\\D", ""); |
| | |
| | | if (deviceNo == null || !deviceNo.chars().allMatch(Character::isDigit)) { |
| | | return R.error("设备编号错误"); |
| | | } |
| | | if (isDevpType(type) && (stationId == null || !stationId.chars().allMatch(Character::isDigit))) { |
| | | return R.error("站点编号错误"); |
| | | } |
| | | Path dayDir = Paths.get(loggingPath, dayClean); |
| | | if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { |
| | | return R.error("未找到日志文件"); |
| | | } |
| | | |
| | | String prefix = type + "_" + deviceNo + "_" + dayClean + "_"; |
| | | List<Path> files = Files.list(dayDir) |
| | | .filter(p -> { |
| | | String name = p.getFileName().toString(); |
| | | return name.endsWith(".log") && name.startsWith(prefix); |
| | | }).collect(Collectors.toList()); |
| | | |
| | | files.sort(Comparator.comparingInt(p -> { |
| | | String n = p.getFileName().toString(); |
| | | try { |
| | | String suf = n.substring(prefix.length(), n.length() - 4); |
| | | return Integer.parseInt(suf); |
| | | } catch (Exception e) { |
| | | return Integer.MAX_VALUE; |
| | | } |
| | | })); |
| | | |
| | | |
| | | List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); |
| | | |
| | | if (files.isEmpty()) { |
| | | return R.error("未找到日志文件"); |
| | | } |
| | | |
| | | // Binary search for the file containing the timestamp |
| | | // We want to find the LAST file that has startTime <= targetTime. |
| | | // Because files are sequential: [t0, t1), [t1, t2), ... |
| | | // If we find file[i].startTime <= target < file[i+1].startTime, then target is in file[i]. |
| | | |
| | | |
| | | int low = 0; |
| | | int high = files.size() - 1; |
| | | int foundIndex = -1; |
| | | |
| | | |
| | | while (low <= high) { |
| | | int mid = (low + high) >>> 1; |
| | | Path midFile = files.get(mid); |
| | | |
| | | // Read start time of this file |
| | | Long midStart = getFileStartTime(midFile); |
| | | Long midStart = getFileStartTime(midFile, stationId); |
| | | if (midStart == null) { |
| | | low = mid + 1; |
| | | continue; |
| | | } |
| | | |
| | | |
| | | if (midStart <= timestamp) { |
| | | // This file starts before or at target. It COULD be the one. |
| | | // But maybe a later file also starts before target? |
| | | foundIndex = mid; |
| | | low = mid + 1; // Try to find a later start time |
| | | low = mid + 1; |
| | | } else { |
| | | // This file starts AFTER target. So target must be in an earlier file. |
| | | high = mid - 1; |
| | | } |
| | | } |
| | | |
| | | |
| | | if (foundIndex == -1) { |
| | | foundIndex = 0; |
| | | } |
| | | |
| | | // Return the file index (offset) |
| | | |
| | | Map<String, Object> result = new HashMap<>(); |
| | | result.put("offset", foundIndex); |
| | | return R.ok(result); |
| | | |
| | | |
| | | } catch (Exception e) { |
| | | log.error("寻址失败", e); |
| | | return R.error("寻址失败"); |
| | | } |
| | | } |
| | | |
| | | private Long getFileStartTime(Path file) { |
| | | private Long getFileStartTime(Path file, String stationId) { |
| | | try { |
| | | String firstLine = null; |
| | | try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) { |
| | | firstLine = lines.findFirst().orElse(null); |
| | | } |
| | | String firstLine = readFirstMatchingLine(file, stationId); |
| | | if (firstLine == null) return null; |
| | | DeviceDataLog firstLog = JSON.parseObject(firstLine, DeviceDataLog.class); |
| | | return firstLog.getCreateTime().getTime(); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private Long getFileEndTime(Path file, String stationId) { |
| | | try { |
| | | String lastLine = readLastMatchingLine(file, stationId); |
| | | if (lastLine == null) return null; |
| | | DeviceDataLog lastLog = JSON.parseObject(lastLine, DeviceDataLog.class); |
| | | return lastLog.getCreateTime().getTime(); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | |
| | | response.setStatus(400); |
| | | return; |
| | | } |
| | | String stationId = request.getParameter("stationId"); |
| | | if (isDevpType(type) && (stationId == null || !stationId.chars().allMatch(Character::isDigit))) { |
| | | response.setStatus(400); |
| | | return; |
| | | } |
| | | Path dayDir = Paths.get(loggingPath, dayClean); |
| | | if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { |
| | | response.setStatus(404); |
| | | return; |
| | | } |
| | | List<Path> files = Files.list(dayDir) |
| | | .filter(p -> { |
| | | String name = p.getFileName().toString(); |
| | | String prefix = type + "_" + deviceNo + "_" + dayClean + "_"; |
| | | return name.endsWith(".log") && name.startsWith(prefix); |
| | | }).collect(Collectors.toList()); |
| | | // 排序(按文件中的索引号递增) |
| | | String prefix = type + "_" + deviceNo + "_" + dayClean + "_"; |
| | | files.sort(Comparator.comparingInt(p -> { |
| | | String n = p.getFileName().toString(); |
| | | try { |
| | | String suf = n.substring(prefix.length(), n.length() - 4); |
| | | return Integer.parseInt(suf); |
| | | } catch (Exception e) { |
| | | return Integer.MAX_VALUE; |
| | | } |
| | | })); |
| | | int from = offset == null || offset < 0 ? 0 : offset; |
| | | int max = limit == null || limit <= 0 ? 200 : limit; |
| | | int to = Math.min(files.size(), from + max); |
| | | if (from >= files.size()) { |
| | | response.setStatus(404); |
| | | return; |
| | | } |
| | | files = files.subList(from, to); |
| | | List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); |
| | | files = sliceDownloadFiles(files, offset, limit); |
| | | if (files.isEmpty()) { |
| | | response.setStatus(404); |
| | | return; |
| | |
| | | response.setHeader("X-Total-Size", String.valueOf(totalRawSize)); |
| | | response.setHeader("X-File-Count", String.valueOf(files.size())); |
| | | response.setHeader("X-Progress-Id", id); |
| | | try (java.util.zip.ZipOutputStream zos = new java.util.zip.ZipOutputStream(response.getOutputStream())) { |
| | | try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { |
| | | for (Path f : files) { |
| | | java.util.zip.ZipEntry entry = new java.util.zip.ZipEntry(f.getFileName().toString()); |
| | | ZipEntry entry = new ZipEntry(f.getFileName().toString()); |
| | | zos.putNextEntry(entry); |
| | | Files.copy(f, zos); |
| | | zos.closeEntry(); |
| | |
| | | if (Cools.isEmpty(deviceNo) || !deviceNo.chars().allMatch(Character::isDigit)) { |
| | | return R.error("设备编号错误"); |
| | | } |
| | | String stationId = param.getString("stationId"); |
| | | if (isDevpType(type) && (Cools.isEmpty(stationId) || !stationId.chars().allMatch(Character::isDigit))) { |
| | | return R.error("站点编号错误"); |
| | | } |
| | | Path dayDir = Paths.get(loggingPath, dayClean); |
| | | if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { |
| | | return R.error("当日目录不存在"); |
| | | } |
| | | List<Path> files = Files.list(dayDir) |
| | | .filter(p -> { |
| | | String name = p.getFileName().toString(); |
| | | String prefix = type + "_" + deviceNo + "_" + dayClean + "_"; |
| | | return name.endsWith(".log") && name.startsWith(prefix); |
| | | }).collect(Collectors.toList()); |
| | | String prefix = type + "_" + deviceNo + "_" + dayClean + "_"; |
| | | files.sort(Comparator.comparingInt(p -> { |
| | | String n = p.getFileName().toString(); |
| | | try { |
| | | String suf = n.substring(prefix.length(), n.length() - 4); |
| | | return Integer.parseInt(suf); |
| | | } catch (Exception e) { |
| | | return Integer.MAX_VALUE; |
| | | } |
| | | })); |
| | | int from = offset == null || offset < 0 ? 0 : offset; |
| | | int max = limit == null || limit <= 0 ? 200 : limit; |
| | | int to = Math.min(files.size(), from + max); |
| | | if (from >= files.size()) { |
| | | List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); |
| | | if ((offset != null && offset >= files.size())) { |
| | | return R.error("起始序号超出范围"); |
| | | } |
| | | files = files.subList(from, to); |
| | | files = sliceDownloadFiles(files, offset, limit); |
| | | if (files.isEmpty()) { |
| | | return R.error("未找到日志文件"); |
| | | } |
| | | String id = UUID.randomUUID().toString(); |
| | | ProgressInfo info = new ProgressInfo(); |
| | | info.totalCount = files.size(); |
| | |
| | | return R.ok(res); |
| | | } |
| | | |
| | | @RequestMapping(value = "/deviceLog/system/download/init/auth") |
| | | @ManagerAuth |
| | | public R initSystemDownload(@org.springframework.web.bind.annotation.RequestBody com.alibaba.fastjson.JSONObject param) { |
| | | try { |
| | | String logType = normalizeSystemLogType(param.getString("logType")); |
| | | if (logType == null) { |
| | | return R.error("日志类型错误"); |
| | | } |
| | | LocalDateTime startTime = parseSystemLogDateTime(param.getString("startTime")); |
| | | LocalDateTime endTime = parseSystemLogDateTime(param.getString("endTime")); |
| | | if (startTime == null || endTime == null) { |
| | | return R.error("时间格式错误"); |
| | | } |
| | | if (startTime.isAfter(endTime)) { |
| | | return R.error("开始时间不能晚于结束时间"); |
| | | } |
| | | long daySpan = java.time.temporal.ChronoUnit.DAYS.between(startTime.toLocalDate(), endTime.toLocalDate()); |
| | | if (daySpan > SYSTEM_LOG_MAX_RANGE_DAYS) { |
| | | return R.error("时间范围不能超过" + SYSTEM_LOG_MAX_RANGE_DAYS + "天"); |
| | | } |
| | | List<Path> files = findSystemLogFiles(logType, startTime, endTime); |
| | | if (files.isEmpty()) { |
| | | return R.error("未找到日志文件"); |
| | | } |
| | | String id = UUID.randomUUID().toString(); |
| | | ProgressInfo info = new ProgressInfo(); |
| | | info.totalCount = files.size(); |
| | | long sum = 0L; |
| | | for (Path f : files) { |
| | | try { sum += Files.size(f); } catch (Exception ignored) {} |
| | | } |
| | | info.totalRaw = sum; |
| | | info.processedRaw = 0L; |
| | | info.processedCount = 0; |
| | | info.finished = false; |
| | | DOWNLOAD_PROGRESS.put(id, info); |
| | | SystemLogDownloadRequest request = new SystemLogDownloadRequest(); |
| | | request.logType = logType; |
| | | request.startTime = startTime; |
| | | request.endTime = endTime; |
| | | request.files = files; |
| | | SYSTEM_LOG_DOWNLOAD_REQUESTS.put(id, request); |
| | | Map<String, Object> res = new HashMap<>(); |
| | | res.put("progressId", id); |
| | | res.put("totalSize", info.totalRaw); |
| | | res.put("fileCount", info.totalCount); |
| | | return R.ok(res); |
| | | } catch (Exception e) { |
| | | log.error("初始化系统日志下载失败", e); |
| | | return R.error("初始化失败"); |
| | | } |
| | | } |
| | | |
| | | @RequestMapping(value = "/deviceLog/system/download/auth") |
| | | @ManagerAuth |
| | | public void downloadSystemLog(@RequestParam("logType") String logTypeParam, |
| | | @RequestParam("startTime") String startTimeParam, |
| | | @RequestParam("endTime") String endTimeParam, |
| | | @RequestParam(value = "progressId", required = false) String progressId, |
| | | HttpServletResponse response) { |
| | | String progressKey = null; |
| | | try { |
| | | String logType = normalizeSystemLogType(logTypeParam); |
| | | LocalDateTime startTime = parseSystemLogDateTime(startTimeParam); |
| | | LocalDateTime endTime = parseSystemLogDateTime(endTimeParam); |
| | | if (logType == null || startTime == null || endTime == null || startTime.isAfter(endTime)) { |
| | | response.setStatus(400); |
| | | return; |
| | | } |
| | | SystemLogDownloadRequest requestInfo = null; |
| | | if (!Cools.isEmpty(progressId)) { |
| | | requestInfo = SYSTEM_LOG_DOWNLOAD_REQUESTS.get(progressId); |
| | | progressKey = progressId; |
| | | } |
| | | List<Path> files; |
| | | if (requestInfo != null |
| | | && Objects.equals(requestInfo.logType, logType) |
| | | && Objects.equals(requestInfo.startTime, startTime) |
| | | && Objects.equals(requestInfo.endTime, endTime)) { |
| | | files = requestInfo.files == null ? Collections.emptyList() : requestInfo.files; |
| | | } else { |
| | | files = findSystemLogFiles(logType, startTime, endTime); |
| | | } |
| | | if (files.isEmpty()) { |
| | | response.setStatus(404); |
| | | return; |
| | | } |
| | | if (Cools.isEmpty(progressKey)) { |
| | | progressKey = UUID.randomUUID().toString(); |
| | | } |
| | | ProgressInfo info = prepareProgress(progressKey, files); |
| | | response.reset(); |
| | | response.setContentType("application/zip"); |
| | | String filename = logType + "_" + formatSystemExportTime(startTime) + "_" + formatSystemExportTime(endTime) + ".zip"; |
| | | response.setHeader("Content-Disposition", "attachment; filename=" + filename); |
| | | response.setHeader("X-Progress-Id", progressKey); |
| | | try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { |
| | | zos.putNextEntry(new ZipEntry(SYSTEM_LOG_ENTRY_NAME)); |
| | | try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zos, StandardCharsets.UTF_8))) { |
| | | boolean written = writeSystemLogContent(files, logType, startTime, endTime, writer, info); |
| | | writer.flush(); |
| | | if (!written) { |
| | | response.reset(); |
| | | response.setStatus(404); |
| | | return; |
| | | } |
| | | } |
| | | zos.closeEntry(); |
| | | zos.finish(); |
| | | info.finished = true; |
| | | } |
| | | } catch (Exception e) { |
| | | log.error("下载系统日志失败", e); |
| | | try { |
| | | response.reset(); |
| | | response.setStatus(500); |
| | | } catch (Exception ignore) { |
| | | } |
| | | } finally { |
| | | if (!Cools.isEmpty(progressId)) { |
| | | SYSTEM_LOG_DOWNLOAD_REQUESTS.remove(progressId); |
| | | } |
| | | } |
| | | } |
| | | |
| | | @RequestMapping(value = "/deviceLog/enums/auth") |
| | | @ManagerAuth |
| | | public R getEnums() { |
| | | Map<String, Map<String, String>> enums = new HashMap<>(); |
| | | |
| | | |
| | | enums.put("CrnModeType", Arrays.stream(com.zy.core.enums.CrnModeType.values()) |
| | | .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc))); |
| | | |
| | | |
| | | enums.put("CrnStatusType", Arrays.stream(com.zy.core.enums.CrnStatusType.values()) |
| | | .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc))); |
| | | |
| | | |
| | | enums.put("CrnForkPosType", Arrays.stream(com.zy.core.enums.CrnForkPosType.values()) |
| | | .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc))); |
| | | |
| | | |
| | | enums.put("CrnLiftPosType", Arrays.stream(com.zy.core.enums.CrnLiftPosType.values()) |
| | | .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc))); |
| | | |
| | |
| | | |
| | | enums.put("RgvStatusType", Arrays.stream(com.zy.core.enums.RgvStatusType.values()) |
| | | .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc))); |
| | | |
| | | |
| | | return R.ok(enums); |
| | | } |
| | | |
| | | private ProgressInfo prepareProgress(String progressId, List<Path> files) { |
| | | return DOWNLOAD_PROGRESS.compute(progressId, (key, existing) -> { |
| | | ProgressInfo next = existing == null ? new ProgressInfo() : existing; |
| | | next.totalCount = files == null ? 0 : files.size(); |
| | | long total = 0L; |
| | | if (files != null) { |
| | | for (Path f : files) { |
| | | try { total += Files.size(f); } catch (Exception ignored) {} |
| | | } |
| | | } |
| | | next.totalRaw = total; |
| | | next.processedRaw = 0L; |
| | | next.processedCount = 0; |
| | | next.finished = false; |
| | | return next; |
| | | }); |
| | | } |
| | | |
| | | private boolean writeSystemLogContent(List<Path> files, |
| | | String logType, |
| | | LocalDateTime startTime, |
| | | LocalDateTime endTime, |
| | | BufferedWriter writer, |
| | | ProgressInfo progressInfo) throws Exception { |
| | | boolean wroteAny = false; |
| | | for (Path file : files) { |
| | | SystemLogFileInfo fileInfo = parseSystemLogFile(file); |
| | | if (fileInfo == null) { |
| | | updateProgress(progressInfo, file); |
| | | continue; |
| | | } |
| | | LocalDate baseDate = fileInfo.day != null ? fileInfo.day : startTime.toLocalDate(); |
| | | try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) { |
| | | List<String> pending = new ArrayList<>(); |
| | | boolean pendingMatched = false; |
| | | for (Iterator<String> iterator = lines.iterator(); iterator.hasNext(); ) { |
| | | String line = iterator.next(); |
| | | Matcher matcher = SYSTEM_LOG_TIMESTAMP.matcher(line == null ? "" : line); |
| | | if (matcher.find()) { |
| | | if (pendingMatched && !pending.isEmpty()) { |
| | | for (String pendingLine : pending) { |
| | | writer.write(pendingLine == null ? "" : pendingLine); |
| | | writer.newLine(); |
| | | } |
| | | wroteAny = true; |
| | | } |
| | | pending.clear(); |
| | | pendingMatched = isSystemLogLineInRange(baseDate, matcher.group(1), startTime, endTime); |
| | | } |
| | | if (pendingMatched) { |
| | | pending.add(line); |
| | | if (pending.size() > SYSTEM_LOG_MATCH_BUFFER_LINES) { |
| | | for (String pendingLine : pending) { |
| | | writer.write(pendingLine == null ? "" : pendingLine); |
| | | writer.newLine(); |
| | | } |
| | | wroteAny = true; |
| | | pending.clear(); |
| | | } |
| | | } else if (!pending.isEmpty()) { |
| | | pending.clear(); |
| | | } |
| | | } |
| | | if (pendingMatched && !pending.isEmpty()) { |
| | | for (String pendingLine : pending) { |
| | | writer.write(pendingLine == null ? "" : pendingLine); |
| | | writer.newLine(); |
| | | } |
| | | wroteAny = true; |
| | | } |
| | | } |
| | | updateProgress(progressInfo, file); |
| | | } |
| | | if (progressInfo != null) { |
| | | progressInfo.finished = true; |
| | | } |
| | | return wroteAny; |
| | | } |
| | | |
| | | private void updateProgress(ProgressInfo progressInfo, Path file) { |
| | | if (progressInfo == null) { |
| | | return; |
| | | } |
| | | try { |
| | | progressInfo.processedRaw += Files.size(file); |
| | | } catch (Exception ignored) { |
| | | } |
| | | progressInfo.processedCount += 1; |
| | | } |
| | | |
| | | private List<Path> findSystemLogFiles(String logType, LocalDateTime startTime, LocalDateTime endTime) throws Exception { |
| | | Path baseDir = Paths.get(systemLoggingPath); |
| | | if (!Files.exists(baseDir) || !Files.isDirectory(baseDir)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | LocalDate startDate = startTime.toLocalDate(); |
| | | LocalDate endDate = endTime.toLocalDate(); |
| | | List<SystemLogFileInfo> matched = new ArrayList<>(); |
| | | try (Stream<Path> stream = Files.list(baseDir)) { |
| | | stream.filter(path -> !Files.isDirectory(path)).forEach(path -> { |
| | | SystemLogFileInfo info = parseSystemLogFile(path); |
| | | if (info == null || !Objects.equals(info.logType, logType)) { |
| | | return; |
| | | } |
| | | if (info.active || info.day == null || (!info.day.isBefore(startDate) && !info.day.isAfter(endDate))) { |
| | | matched.add(info); |
| | | } |
| | | }); |
| | | } |
| | | matched.sort((left, right) -> { |
| | | LocalDate leftDay = left.day == null ? LocalDate.MAX : left.day; |
| | | LocalDate rightDay = right.day == null ? LocalDate.MAX : right.day; |
| | | int cmp = leftDay.compareTo(rightDay); |
| | | if (cmp != 0) { |
| | | return cmp; |
| | | } |
| | | int leftIndex = left.index == null ? Integer.MAX_VALUE : left.index; |
| | | int rightIndex = right.index == null ? Integer.MAX_VALUE : right.index; |
| | | if (left.active != right.active) { |
| | | return left.active ? 1 : -1; |
| | | } |
| | | return Integer.compare(leftIndex, rightIndex); |
| | | }); |
| | | return matched.stream().map(item -> item.path).collect(Collectors.toList()); |
| | | } |
| | | |
| | | private SystemLogFileInfo parseSystemLogFile(Path path) { |
| | | if (path == null) { |
| | | return null; |
| | | } |
| | | String name = path.getFileName().toString(); |
| | | if ("info.log".equals(name) || "error.log".equals(name)) { |
| | | SystemLogFileInfo info = new SystemLogFileInfo(); |
| | | info.logType = name.startsWith("info") ? "info" : "error"; |
| | | info.active = true; |
| | | info.path = path; |
| | | return info; |
| | | } |
| | | Matcher matcher = SYSTEM_LOG_ROLLED_FILE.matcher(name); |
| | | if (!matcher.matches()) { |
| | | return null; |
| | | } |
| | | try { |
| | | SystemLogFileInfo info = new SystemLogFileInfo(); |
| | | info.logType = matcher.group(1); |
| | | info.day = LocalDate.parse(matcher.group(2), SYSTEM_LOG_DATE); |
| | | info.index = Integer.parseInt(matcher.group(3)); |
| | | info.active = false; |
| | | info.path = path; |
| | | return info; |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String normalizeSystemLogType(String logType) { |
| | | if (Cools.isEmpty(logType)) { |
| | | return null; |
| | | } |
| | | String value = logType.trim().toLowerCase(Locale.ROOT); |
| | | if ("info".equals(value) || "error".equals(value)) { |
| | | return value; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private LocalDateTime parseSystemLogDateTime(String value) { |
| | | if (Cools.isEmpty(value)) { |
| | | return null; |
| | | } |
| | | try { |
| | | return LocalDateTime.parse(value.trim(), SYSTEM_LOG_DATE_TIME); |
| | | } catch (DateTimeParseException e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private boolean isSystemLogLineInRange(LocalDate baseDate, |
| | | String timePart, |
| | | LocalDateTime startTime, |
| | | LocalDateTime endTime) { |
| | | if (baseDate == null || Cools.isEmpty(timePart) || startTime == null || endTime == null) { |
| | | return false; |
| | | } |
| | | try { |
| | | LocalTime time = LocalTime.parse(timePart); |
| | | LocalDateTime timestamp = LocalDateTime.of(baseDate, time); |
| | | return !timestamp.isBefore(startTime) && !timestamp.isAfter(endTime); |
| | | } catch (DateTimeParseException e) { |
| | | return false; |
| | | } |
| | | } |
| | | |
| | | private String formatSystemExportTime(LocalDateTime dateTime) { |
| | | if (dateTime == null) { |
| | | return "unknown"; |
| | | } |
| | | return SYSTEM_LOG_EXPORT_TIME.format(dateTime); |
| | | } |
| | | |
| | | |
| | | private Map<String, Object> buildEmptySummary() { |
| | | return buildSummaryResponse(Collections.emptyList()); |
| | | } |
| | | |
| | | private Map<String, Object> buildSummaryResponse(Collection<DeviceAggregate> aggregates) { |
| | | List<DeviceAggregate> aggregateList = new ArrayList<>(aggregates); |
| | | Map<String, Object> stats = new LinkedHashMap<>(); |
| | | Map<String, Object> typeCounts = new LinkedHashMap<>(); |
| | | int totalFiles = 0; |
| | | for (String type : DEVICE_TYPE_ORDER) { |
| | | int count = (int) aggregateList.stream().filter(item -> type.equals(item.type)).count(); |
| | | typeCounts.put(type, count); |
| | | } |
| | | for (DeviceAggregate aggregate : aggregateList) { |
| | | totalFiles += aggregate.fileCount; |
| | | } |
| | | stats.put("totalDevices", aggregateList.size()); |
| | | stats.put("totalFiles", totalFiles); |
| | | stats.put("typeCounts", typeCounts); |
| | | |
| | | List<Map<String, Object>> groups = new ArrayList<>(); |
| | | for (String type : DEVICE_TYPE_ORDER) { |
| | | List<DeviceAggregate> devices = aggregateList.stream() |
| | | .filter(item -> type.equals(item.type)) |
| | | .sorted(Comparator.comparingInt(item -> parseDeviceNo(item.deviceNo))) |
| | | .collect(Collectors.toList()); |
| | | Map<String, Object> group = new LinkedHashMap<>(); |
| | | group.put("type", type); |
| | | group.put("typeLabel", DEVICE_TYPE_LABELS.get(type)); |
| | | group.put("deviceCount", devices.size()); |
| | | group.put("totalFiles", devices.stream().mapToInt(item -> item.fileCount).sum()); |
| | | group.put("devices", devices.stream().map(item -> { |
| | | Map<String, Object> x = new LinkedHashMap<>(); |
| | | x.put("type", item.type); |
| | | x.put("typeLabel", item.typeLabel); |
| | | x.put("deviceNo", item.deviceNo); |
| | | x.put("stationId", item.stationId); |
| | | x.put("fileCount", item.fileCount); |
| | | x.put("firstTime", item.firstTime); |
| | | x.put("lastTime", item.lastTime); |
| | | x.put("stationIds", item.stationIds == null ? Collections.emptyList() : item.stationIds); |
| | | return x; |
| | | }).collect(Collectors.toList())); |
| | | groups.add(group); |
| | | } |
| | | |
| | | Map<String, Object> result = new LinkedHashMap<>(); |
| | | result.put("stats", stats); |
| | | result.put("groups", groups); |
| | | return result; |
| | | } |
| | | |
| | | private void enrichDevpStationIds(Collection<DeviceAggregate> aggregates) { |
| | | if (aggregates == null || aggregates.isEmpty() || basDevpService == null) { |
| | | return; |
| | | } |
| | | List<DeviceAggregate> devpAggregates = aggregates.stream() |
| | | .filter(item -> item != null && isDevpType(item.type) && Cools.isEmpty(item.stationId) && !Cools.isEmpty(item.deviceNo)) |
| | | .collect(Collectors.toList()); |
| | | if (devpAggregates.isEmpty()) { |
| | | return; |
| | | } |
| | | List<Integer> devpNos = devpAggregates.stream() |
| | | .map(item -> parseInteger(item.deviceNo)) |
| | | .filter(Objects::nonNull) |
| | | .distinct() |
| | | .collect(Collectors.toList()); |
| | | if (devpNos.isEmpty()) { |
| | | return; |
| | | } |
| | | Map<Integer, List<String>> stationIdsByDevpNo = basDevpService.listByIds(devpNos).stream() |
| | | .filter(Objects::nonNull) |
| | | .collect(Collectors.toMap(BasDevp::getDevpNo, this::extractStationIds, (left, right) -> left)); |
| | | for (DeviceAggregate aggregate : devpAggregates) { |
| | | Integer devpNo = parseInteger(aggregate.deviceNo); |
| | | if (devpNo != null) { |
| | | aggregate.stationIds = stationIdsByDevpNo.getOrDefault(devpNo, Collections.emptyList()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | private List<String> extractStationIds(BasDevp basDevp) { |
| | | if (basDevp == null) { |
| | | return Collections.emptyList(); |
| | | } |
| | | return basDevp.getStationList$().stream() |
| | | .map(StationObjModel::getStationId) |
| | | .filter(Objects::nonNull) |
| | | .map(String::valueOf) |
| | | .distinct() |
| | | .sorted(Comparator.comparingInt(this::parseDeviceNo)) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | private Integer parseInteger(String value) { |
| | | try { |
| | | return Integer.parseInt(String.valueOf(value)); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String normalizeDay(String day) { |
| | | String dayClean = day == null ? null : day.replaceAll("\\D", ""); |
| | | if (dayClean == null || dayClean.length() != 8 || !dayClean.chars().allMatch(Character::isDigit)) { |
| | | return null; |
| | | } |
| | | return dayClean; |
| | | } |
| | | |
| | | private List<Path> findDeviceFiles(Path dayDir, String dayClean, String type, String deviceNo, String stationId) throws Exception { |
| | | FileNameInfo target = new FileNameInfo(); |
| | | target.type = type; |
| | | target.deviceNo = deviceNo; |
| | | target.stationId = stationId; |
| | | target.day = dayClean; |
| | | Path deviceDir = resolveDeviceDir(dayDir, type, deviceNo); |
| | | if (deviceDir == null || !Files.exists(deviceDir) || !Files.isDirectory(deviceDir)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<Path> files; |
| | | try (Stream<Path> stream = Files.list(deviceDir)) { |
| | | files = stream |
| | | .filter(p -> !Files.isDirectory(p) && matchesFileInfo(parseFileName(p.getFileName().toString()), target)) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | files.sort(Comparator.comparingInt(p -> { |
| | | FileNameInfo info = parseFileName(p.getFileName().toString()); |
| | | return info == null ? Integer.MAX_VALUE : info.index; |
| | | })); |
| | | return files; |
| | | } |
| | | |
| | | private List<Path> listDayLogFiles(Path dayDir) throws Exception { |
| | | if (dayDir == null || !Files.exists(dayDir) || !Files.isDirectory(dayDir)) { |
| | | return Collections.emptyList(); |
| | | } |
| | | List<Path> files = new ArrayList<>(); |
| | | try (Stream<Path> typeStream = Files.list(dayDir)) { |
| | | List<Path> typeDirs = typeStream.filter(Files::isDirectory).collect(Collectors.toList()); |
| | | for (Path typeDir : typeDirs) { |
| | | try (Stream<Path> deviceStream = Files.list(typeDir)) { |
| | | List<Path> deviceDirs = deviceStream.filter(Files::isDirectory).collect(Collectors.toList()); |
| | | for (Path deviceDir : deviceDirs) { |
| | | try (Stream<Path> fileStream = Files.list(deviceDir)) { |
| | | fileStream |
| | | .filter(p -> !Files.isDirectory(p) && p.getFileName().toString().endsWith(".log")) |
| | | .forEach(files::add); |
| | | } |
| | | } |
| | | } |
| | | } |
| | | } |
| | | return files; |
| | | } |
| | | |
| | | private Path resolveDeviceDir(Path dayDir, String type, String deviceNo) { |
| | | if (dayDir == null || Cools.isEmpty(type) || Cools.isEmpty(deviceNo)) { |
| | | return null; |
| | | } |
| | | return dayDir.resolve(type).resolve(deviceNo); |
| | | } |
| | | |
| | | private List<Path> sliceDownloadFiles(List<Path> files, Integer offset, Integer limit) { |
| | | if (files == null || files.isEmpty()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | int from = offset == null || offset < 0 ? 0 : offset; |
| | | if (from >= files.size()) { |
| | | return Collections.emptyList(); |
| | | } |
| | | if (offset == null && limit == null) { |
| | | return new ArrayList<>(files); |
| | | } |
| | | int to; |
| | | if (limit == null || limit <= 0) { |
| | | to = files.size(); |
| | | } else { |
| | | to = Math.min(files.size(), from + limit); |
| | | } |
| | | return new ArrayList<>(files.subList(from, to)); |
| | | } |
| | | |
| | | private FileNameInfo parseFileName(String fileName) { |
| | | if (fileName == null || !fileName.endsWith(".log")) { |
| | | return null; |
| | | } |
| | | String fileNameNoExt = fileName.substring(0, fileName.length() - 4); |
| | | String[] parts = fileNameNoExt.split("_"); |
| | | if (parts.length < 4) { |
| | | return null; |
| | | } |
| | | FileNameInfo info = new FileNameInfo(); |
| | | info.type = parts[0]; |
| | | info.deviceNo = parts[1]; |
| | | if (isDevpType(info.type)) { |
| | | if (parts.length != 6 || !"station".equals(parts[2])) { |
| | | return null; |
| | | } |
| | | info.stationId = parts[3]; |
| | | info.day = parts[4]; |
| | | try { |
| | | info.index = Integer.parseInt(parts[5]); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | return info; |
| | | } |
| | | if (parts.length != 4) { |
| | | return null; |
| | | } |
| | | info.day = parts[2]; |
| | | try { |
| | | info.index = Integer.parseInt(parts[3]); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | return info; |
| | | } |
| | | |
| | | private String buildDeviceKey(String type, String deviceNo, String stationId) { |
| | | StringBuilder builder = new StringBuilder(); |
| | | builder.append(String.valueOf(type)).append(":").append(String.valueOf(deviceNo)); |
| | | if (isDevpType(type)) { |
| | | builder.append(":").append(String.valueOf(stationId)); |
| | | } |
| | | return builder.toString(); |
| | | } |
| | | |
| | | private boolean matchesFileInfo(FileNameInfo actual, FileNameInfo target) { |
| | | if (actual == null || target == null) { |
| | | return false; |
| | | } |
| | | if (!Objects.equals(actual.type, target.type)) { |
| | | return false; |
| | | } |
| | | if (!Objects.equals(actual.deviceNo, target.deviceNo)) { |
| | | return false; |
| | | } |
| | | if (!Objects.equals(actual.day, target.day)) { |
| | | return false; |
| | | } |
| | | if (isDevpType(actual.type)) { |
| | | return Objects.equals(actual.stationId, target.stationId); |
| | | } |
| | | return true; |
| | | } |
| | | |
| | | private boolean isDevpType(String type) { |
| | | return SlaveType.Devp.name().equals(type); |
| | | } |
| | | |
| | | private int parseDeviceNo(String deviceNo) { |
| | | try { |
| | | return Integer.parseInt(String.valueOf(deviceNo)); |
| | | } catch (Exception e) { |
| | | return Integer.MAX_VALUE; |
| | | } |
| | | } |
| | | |
| | | private FileTimeRange readFileTimeRange(Path file, String stationId) { |
| | | FileTimeRange range = new FileTimeRange(); |
| | | try { |
| | | String firstLine = readFirstMatchingLine(file, stationId); |
| | | String lastLine = readLastMatchingLine(file, stationId); |
| | | range.startTime = parseLogTime(firstLine, stationId); |
| | | range.endTime = parseLogTime(lastLine, stationId); |
| | | return range; |
| | | } catch (Exception e) { |
| | | return range; |
| | | } |
| | | } |
| | | |
| | | private Long parseLogTime(String line, String stationId) { |
| | | try { |
| | | if (line == null || line.trim().isEmpty()) { |
| | | return null; |
| | | } |
| | | DeviceDataLog logItem = JSON.parseObject(line, DeviceDataLog.class); |
| | | if (!matchesRequestedStation(logItem, stationId)) { |
| | | return null; |
| | | } |
| | | return logItem != null && logItem.getCreateTime() != null ? logItem.getCreateTime().getTime() : null; |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private boolean matchesRequestedStation(DeviceDataLog logItem, String stationId) { |
| | | if (Cools.isEmpty(stationId)) { |
| | | return true; |
| | | } |
| | | if (logItem == null || logItem.getStationId() == null) { |
| | | return false; |
| | | } |
| | | return Objects.equals(String.valueOf(logItem.getStationId()), stationId); |
| | | } |
| | | |
| | | private String readFirstMatchingLine(Path file, String stationId) { |
| | | try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) { |
| | | return lines |
| | | .filter(line -> line != null && !line.trim().isEmpty()) |
| | | .filter(line -> matchesRequestedStation(parseLogLine(line), stationId)) |
| | | .findFirst() |
| | | .orElse(null); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String readLastMatchingLine(Path file, String stationId) { |
| | | try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) { |
| | | List<String> matched = lines |
| | | .filter(line -> line != null && !line.trim().isEmpty()) |
| | | .filter(line -> matchesRequestedStation(parseLogLine(line), stationId)) |
| | | .collect(Collectors.toList()); |
| | | if (matched.isEmpty()) { |
| | | return null; |
| | | } |
| | | return matched.get(matched.size() - 1); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private DeviceDataLog parseLogLine(String line) { |
| | | try { |
| | | if (line == null || line.trim().isEmpty()) { |
| | | return null; |
| | | } |
| | | return JSON.parseObject(line, DeviceDataLog.class); |
| | | } catch (Exception e) { |
| | | return null; |
| | | } |
| | | } |
| | | } |