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.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.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; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @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", "输送设备"); } @Autowired private BasDevpService basDevpService; @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 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 stationIds; 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 = listDayLogFiles(dayDir); Map> deviceMap = new HashMap<>(); for (Path p : files) { FileNameInfo info = parseFileName(p.getFileName().toString()); if (info == null || !day.equals(info.day)) { continue; } String deviceKey = buildDeviceKey(info.type, info.deviceNo, info.stationId); Map infoMap = deviceMap.computeIfAbsent(deviceKey, k -> { Map map = new HashMap<>(); map.put("deviceNo", info.deviceNo); map.put("stationId", info.stationId); map.put("types", new HashSet()); map.put("fileCount", 0); return map; }); ((Set) infoMap.get("types")).add(info.type); infoMap.put("fileCount", ((Integer) infoMap.get("fileCount")) + 1); } List> res = deviceMap.values().stream().map(m -> { Map x = new HashMap<>(); x.put("deviceNo", m.get("deviceNo")); x.put("stationId", m.get("stationId")); 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<>(); List 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 files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); 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), stationId); 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), 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 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 { 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("设备编号错误"); } 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<>()); } List files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); int from = offset == null || offset < 0 ? 0 : offset; 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 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); if (matchesRequestedStation(logItem, stationId)) { 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(value = "stationId", required = false) String stationId, @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("设备编号错误"); } 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 files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); if (files.isEmpty()) { return R.error("未找到日志文件"); } int low = 0; int high = files.size() - 1; int foundIndex = -1; while (low <= high) { int mid = (low + high) >>> 1; Path midFile = files.get(mid); Long midStart = getFileStartTime(midFile, stationId); if (midStart == null) { low = mid + 1; continue; } if (midStart <= timestamp) { foundIndex = mid; low = mid + 1; } else { high = mid - 1; } } if (foundIndex == -1) { foundIndex = 0; } 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, String stationId) { try { 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; } } @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; } 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 files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); 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 (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) { for (Path f : files) { ZipEntry entry = new 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("设备编号错误"); } 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 files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId); 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("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 result = new LinkedHashMap<>(); result.put("stats", stats); result.put("groups", groups); return result; } private void enrichDevpStationIds(Collection aggregates) { if (aggregates == null || aggregates.isEmpty() || basDevpService == null) { return; } List 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 devpNos = devpAggregates.stream() .map(item -> parseInteger(item.deviceNo)) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); if (devpNos.isEmpty()) { return; } Map> 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 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 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 files; try (Stream 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 listDayLogFiles(Path dayDir) throws Exception { if (dayDir == null || !Files.exists(dayDir) || !Files.isDirectory(dayDir)) { return Collections.emptyList(); } List files = new ArrayList<>(); try (Stream typeStream = Files.list(dayDir)) { List typeDirs = typeStream.filter(Files::isDirectory).collect(Collectors.toList()); for (Path typeDir : typeDirs) { try (Stream deviceStream = Files.list(typeDir)) { List deviceDirs = deviceStream.filter(Files::isDirectory).collect(Collectors.toList()); for (Path deviceDir : deviceDirs) { try (Stream 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 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 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 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 lines = Files.lines(file, StandardCharsets.UTF_8)) { List 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; } } }