package com.zy.asrs.controller; import com.alibaba.fastjson.JSON; import com.core.annotations.ManagerAuth; import com.core.common.Cools; import com.core.common.R; import com.zy.asrs.entity.DeviceDataLog; import com.zy.common.web.BaseController; import com.zy.core.enums.SlaveType; import lombok.extern.slf4j.Slf4j; 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.ByteArrayOutputStream; import java.io.RandomAccessFile; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.*; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import java.util.stream.Stream; @Slf4j @RestController public class DeviceLogController extends BaseController { private static final List DEVICE_TYPE_ORDER = Arrays.asList("Crn", "DualCrn", "Rgv", "Devp"); private static final Map 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", "输送设备"); } @Value("${deviceLogStorage.loggingPath}") private String loggingPath; private static class ProgressInfo { long totalRaw; long processedRaw; int totalCount; int processedCount; boolean finished; } private static class FileNameInfo { String type; String deviceNo; String day; int index; } private static class FileTimeRange { Long startTime; Long endTime; } private static class DeviceAggregate { String type; String typeLabel; String deviceNo; int fileCount; Long firstTime; Long lastTime; Integer firstIndex; Integer lastIndex; Path firstFile; Path lastFile; } private static final Map DOWNLOAD_PROGRESS = new ConcurrentHashMap<>(); @RequestMapping(value = "/deviceLog/dates/auth") @ManagerAuth public R dates() { try { Path baseDir = Paths.get(loggingPath); if (!Files.exists(baseDir)) { return R.ok(new ArrayList<>()); } List days = Files.list(baseDir) .filter(Files::isDirectory) .map(p -> p.getFileName().toString()) .filter(name -> name.length() == 8 && name.chars().allMatch(Character::isDigit)) .sorted() .collect(Collectors.toList()); Map>> grouped = new LinkedHashMap<>(); for (String day : days) { String year = day.substring(0, 4); String month = day.substring(4, 6); grouped.computeIfAbsent(year, k -> new LinkedHashMap<>()) .computeIfAbsent(month, k -> new ArrayList<>()) .add(day); } List> tree = new ArrayList<>(); for (Map.Entry>> yEntry : grouped.entrySet()) { Map yNode = new HashMap<>(); yNode.put("title", yEntry.getKey()); yNode.put("id", yEntry.getKey()); List> mChildren = new ArrayList<>(); for (Map.Entry> mEntry : yEntry.getValue().entrySet()) { Map mNode = new HashMap<>(); mNode.put("title", mEntry.getKey()); mNode.put("id", yEntry.getKey() + "-" + mEntry.getKey()); List> dChildren = new ArrayList<>(); for (String d : mEntry.getValue()) { Map dNode = new HashMap<>(); dNode.put("title", d.substring(6, 8)); dNode.put("id", d); dNode.put("day", d); dChildren.add(dNode); } mNode.put("children", dChildren); mChildren.add(mNode); } yNode.put("children", mChildren); tree.add(yNode); } return R.ok(tree); } catch (Exception e) { return R.error("读取日期失败"); } } @RequestMapping(value = "/deviceLog/day/{day}/devices/auth") @ManagerAuth public R devices(@PathVariable("day") String day) { try { if (day == null || day.length() != 8 || !day.chars().allMatch(Character::isDigit)) { return R.error("日期格式错误"); } Path dayDir = Paths.get(loggingPath, day); if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { return R.ok(new ArrayList<>()); } List files = Files.list(dayDir) .filter(p -> !Files.isDirectory(p) && p.getFileName().toString().endsWith(".log")) .collect(Collectors.toList()); Map> deviceMap = new HashMap<>(); for (Path p : files) { String name = p.getFileName().toString(); String[] parts = name.split("_"); if (parts.length < 4) { continue; } String deviceNo = parts[1]; String type = parts[0]; Map info = deviceMap.computeIfAbsent(deviceNo, k -> { Map map = new HashMap<>(); map.put("deviceNo", deviceNo); map.put("types", new HashSet()); map.put("fileCount", 0); return map; }); ((Set) info.get("types")).add(type); info.put("fileCount", ((Integer) info.get("fileCount")) + 1); } List> res = deviceMap.values().stream().map(m -> { Map x = new HashMap<>(); x.put("deviceNo", m.get("deviceNo")); x.put("types", ((Set) m.get("types")).stream().collect(Collectors.toList())); x.put("fileCount", m.get("fileCount")); return x; }).collect(Collectors.toList()); return R.ok(res); } catch (Exception e) { return R.error("读取设备列表失败"); } } @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 aggregateMap = new LinkedHashMap<>(); try (Stream stream = Files.list(dayDir)) { List files = stream .filter(p -> !Files.isDirectory(p) && p.getFileName().toString().endsWith(".log")) .collect(Collectors.toList()); 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 = info.type + ":" + info.deviceNo; 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; 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.firstTime = firstRange.startTime != null ? firstRange.startTime : firstRange.endTime; } if (aggregate.lastFile != null) { FileTimeRange lastRange = readFileTimeRange(aggregate.lastFile); aggregate.lastTime = lastRange.endTime != null ? lastRange.endTime : lastRange.startTime; } } 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) { 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("设备编号错误"); } Path dayDir = Paths.get(loggingPath, dayClean); if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { return R.error("未找到日志文件"); } List files = findDeviceFiles(dayDir, dayClean, type, deviceNo); if (files.isEmpty()) { return R.error("未找到日志文件"); } List> segments = new ArrayList<>(); Long startTime = null; for (int i = 0; i < files.size(); i++) { Long segmentStart = getFileStartTime(files.get(i)); if (segmentStart != null && (startTime == null || segmentStart < startTime)) { startTime = segmentStart; } Map 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)); 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 result = new HashMap<>(); result.put("type", type); result.put("typeLabel", DEVICE_TYPE_LABELS.getOrDefault(type, type)); result.put("deviceNo", deviceNo); 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 = "offset", required = false) Integer offset, @RequestParam(value = "limit", required = false) Integer limit) { try { String dayClean = day == null ? null : day.replaceAll("\\D", ""); if (dayClean == null || dayClean.length() != 8 || !dayClean.chars().allMatch(Character::isDigit)) { return R.error("日期格式错误"); } if (type == null || SlaveType.findInstance(type) == null) { return R.error("设备类型错误"); } if (deviceNo == null || !deviceNo.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 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; } })); int from = offset == null || offset < 0 ? 0 : offset; int max = limit == null || limit <= 0 ? 5 : limit; // 默认读取5个文件 if (max > 10) max = 10; // 限制最大文件数,防止超时 int to = Math.min(files.size(), from + max); if (from >= files.size()) { return R.ok(new ArrayList<>()); } List targetFiles = files.subList(from, to); List resultLogs = new ArrayList<>(); for (Path f : targetFiles) { try (Stream 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); } catch (Exception e) { // 忽略解析错误 } } }); } 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); return R.error("预览日志失败"); } } @RequestMapping(value = "/deviceLog/day/{day}/seek/auth") @ManagerAuth public R seek(@PathVariable("day") String day, @RequestParam("type") String type, @RequestParam("deviceNo") String deviceNo, @RequestParam("timestamp") Long timestamp) { try { String dayClean = day == null ? null : day.replaceAll("\\D", ""); if (dayClean == null || dayClean.length() != 8 || !dayClean.chars().allMatch(Character::isDigit)) { return R.error("日期格式错误"); } if (type == null || SlaveType.findInstance(type) == null) { return R.error("设备类型错误"); } if (deviceNo == null || !deviceNo.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 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; } })); 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); 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 } 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 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) { try { String firstLine = readFirstNonBlankLine(file); 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) { try { String lastLine = readLastNonBlankLine(file); if (lastLine == null) return null; DeviceDataLog lastLog = JSON.parseObject(lastLine, DeviceDataLog.class); return lastLog.getCreateTime().getTime(); } catch (Exception e) { return null; } } @RequestMapping(value = "/deviceLog/day/{day}/download/auth") @ManagerAuth public void download(@PathVariable("day") String day, @RequestParam("type") String type, @RequestParam("deviceNo") String deviceNo, @RequestParam(value = "offset", required = false) Integer offset, @RequestParam(value = "limit", required = false) Integer limit, @RequestParam(value = "progressId", required = false) String progressId, HttpServletResponse response) { try { String dayClean = day == null ? null : day.replaceAll("\\D", ""); if (dayClean == null || dayClean.length() != 8 || !dayClean.chars().allMatch(Character::isDigit)) { response.setStatus(400); return; } if (type == null || SlaveType.findInstance(type) == null) { response.setStatus(400); return; } if (deviceNo == null || !deviceNo.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 files = findDeviceFiles(dayDir, dayClean, type, deviceNo); files = sliceDownloadFiles(files, offset, limit); if (files.isEmpty()) { response.setStatus(404); return; } ProgressInfo info; String id = progressId; if (Cools.isEmpty(id)) { id = UUID.randomUUID().toString(); } List finalFiles = files; info = DOWNLOAD_PROGRESS.computeIfAbsent(id, k -> { ProgressInfo x = new ProgressInfo(); x.totalCount = finalFiles.size(); long sum = 0L; for (Path f : finalFiles) { try { sum += Files.size(f); } catch (Exception ignored) {} } x.totalRaw = sum; x.processedRaw = 0L; x.processedCount = 0; x.finished = false; return x; }); response.reset(); response.setContentType("application/zip"); String filename = type + "_" + deviceNo + "_" + dayClean + ".zip"; response.setHeader("Content-Disposition", "attachment; filename=" + filename); long totalRawSize = 0L; for (Path f : files) { try { totalRawSize += Files.size(f); } catch (Exception ignored) {} } 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())) { for (Path f : files) { java.util.zip.ZipEntry entry = new java.util.zip.ZipEntry(f.getFileName().toString()); zos.putNextEntry(entry); Files.copy(f, zos); zos.closeEntry(); try { info.processedRaw += Files.size(f); } catch (Exception ignored) {} info.processedCount += 1; } zos.finish(); info.finished = true; } } catch (Exception e) { try { response.setStatus(500); } catch (Exception ignore) {} } } @RequestMapping(value = "/deviceLog/download/init/auth") @ManagerAuth public R init(@org.springframework.web.bind.annotation.RequestBody com.alibaba.fastjson.JSONObject param) { try { String day = param.getString("day"); String type = param.getString("type"); String deviceNo = param.getString("deviceNo"); Integer offset = param.getInteger("offset"); Integer limit = param.getInteger("limit"); String dayClean = Cools.isEmpty(day) ? null : day.replaceAll("\\D", ""); if (Cools.isEmpty(dayClean) || dayClean.length() != 8 || !dayClean.chars().allMatch(Character::isDigit)) { return R.error("日期格式错误"); } if (Cools.isEmpty(type) || SlaveType.findInstance(type) == null) { return R.error("设备类型错误"); } if (Cools.isEmpty(deviceNo) || !deviceNo.chars().allMatch(Character::isDigit)) { return R.error("设备编号错误"); } Path dayDir = Paths.get(loggingPath, dayClean); if (!Files.exists(dayDir) || !Files.isDirectory(dayDir)) { return R.error("当日目录不存在"); } List files = findDeviceFiles(dayDir, dayClean, type, deviceNo); if ((offset != null && offset >= files.size())) { return R.error("起始序号超出范围"); } files = sliceDownloadFiles(files, offset, limit); 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); Map res = new HashMap<>(); res.put("progressId", id); res.put("totalSize", info.totalRaw); res.put("fileCount", info.totalCount); return R.ok(res); } catch (Exception e) { return R.error("初始化失败"); } } @RequestMapping(value = "/deviceLog/download/progress/auth") @ManagerAuth public R progress(String id) { ProgressInfo info = DOWNLOAD_PROGRESS.get(id); if (info == null) { return R.error("无效进度"); } long total = info.totalRaw; long done = info.processedRaw; int percent; if (info.finished) { percent = 100; } else if (total > 0) { percent = (int) Math.min(99, (done * 100L) / total); } else if (info.totalCount > 0) { percent = (int) Math.min(99, (info.processedCount * 100L) / info.totalCount); } else { percent = 0; } Map res = new HashMap<>(); res.put("percent", percent); res.put("processedSize", done); res.put("totalSize", total); res.put("processedCount", info.processedCount); res.put("totalCount", info.totalCount); res.put("finished", info.finished); return R.ok(res); } @RequestMapping(value = "/deviceLog/enums/auth") @ManagerAuth public R getEnums() { Map> 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("DualCrnForkPosType", Arrays.stream(com.zy.core.enums.DualCrnForkPosType.values()) .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc))); enums.put("DualCrnLiftPosType", Arrays.stream(com.zy.core.enums.DualCrnLiftPosType.values()) .collect(Collectors.toMap(e -> String.valueOf(e.id), e -> e.desc))); enums.put("RgvModeType", Arrays.stream(com.zy.core.enums.RgvModeType.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 Map buildEmptySummary() { return buildSummaryResponse(Collections.emptyList()); } private Map buildSummaryResponse(Collection aggregates) { List aggregateList = new ArrayList<>(aggregates); Map stats = new LinkedHashMap<>(); Map 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> groups = new ArrayList<>(); for (String type : DEVICE_TYPE_ORDER) { List devices = aggregateList.stream() .filter(item -> type.equals(item.type)) .sorted(Comparator.comparingInt(item -> parseDeviceNo(item.deviceNo))) .collect(Collectors.toList()); Map 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 x = new LinkedHashMap<>(); x.put("type", item.type); x.put("typeLabel", item.typeLabel); x.put("deviceNo", item.deviceNo); x.put("fileCount", item.fileCount); x.put("firstTime", item.firstTime); x.put("lastTime", item.lastTime); return x; }).collect(Collectors.toList())); groups.add(group); } Map result = new LinkedHashMap<>(); result.put("stats", stats); result.put("groups", groups); return result; } 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 findDeviceFiles(Path dayDir, String dayClean, String type, String deviceNo) throws Exception { String prefix = type + "_" + deviceNo + "_" + dayClean + "_"; List files; try (Stream stream = Files.list(dayDir)) { files = stream .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; } })); return files; } private List sliceDownloadFiles(List 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[] parts = fileName.split("_", 4); if (parts.length < 4) { return null; } FileNameInfo info = new FileNameInfo(); info.type = parts[0]; info.deviceNo = parts[1]; info.day = parts[2]; try { info.index = Integer.parseInt(parts[3].replace(".log", "")); } catch (Exception e) { return null; } return info; } private int parseDeviceNo(String deviceNo) { try { return Integer.parseInt(String.valueOf(deviceNo)); } catch (Exception e) { return Integer.MAX_VALUE; } } private FileTimeRange readFileTimeRange(Path file) { FileTimeRange range = new FileTimeRange(); try { String firstLine = readFirstNonBlankLine(file); String lastLine = readLastNonBlankLine(file); range.startTime = parseLogTime(firstLine); range.endTime = parseLogTime(lastLine); return range; } catch (Exception e) { return range; } } private Long parseLogTime(String line) { try { if (line == null || line.trim().isEmpty()) { return null; } DeviceDataLog logItem = JSON.parseObject(line, DeviceDataLog.class); return logItem != null && logItem.getCreateTime() != null ? logItem.getCreateTime().getTime() : null; } catch (Exception e) { return null; } } private String readFirstNonBlankLine(Path file) { try (Stream lines = Files.lines(file, StandardCharsets.UTF_8)) { return lines.filter(line -> line != null && !line.trim().isEmpty()).findFirst().orElse(null); } catch (Exception e) { return null; } } private String readLastNonBlankLine(Path file) { try (RandomAccessFile randomAccessFile = new RandomAccessFile(file.toFile(), "r")) { long length = randomAccessFile.length(); if (length <= 0) { return null; } ByteArrayOutputStream baos = new ByteArrayOutputStream(); for (long pointer = length - 1; pointer >= 0; pointer--) { randomAccessFile.seek(pointer); int read = randomAccessFile.read(); if (read == '\n' || read == '\r') { if (baos.size() > 0) { break; } continue; } baos.write(read); } byte[] bytes = baos.toByteArray(); for (int i = 0, j = bytes.length - 1; i < j; i++, j--) { byte tmp = bytes[i]; bytes[i] = bytes[j]; bytes[j] = tmp; } String line = new String(bytes, StandardCharsets.UTF_8).trim(); return line.isEmpty() ? null : line; } catch (Exception e) { return null; } } }