| | |
| | | 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.ByteArrayOutputStream; |
| | | import java.io.RandomAccessFile; |
| | | import java.nio.charset.StandardCharsets; |
| | | import java.nio.file.Files; |
| | | import java.nio.file.Path; |
| | |
| | | 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<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; |
| | |
| | | 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<String> stationIds; |
| | | int fileCount; |
| | | Long firstTime; |
| | | Long lastTime; |
| | | Integer firstIndex; |
| | | Integer lastIndex; |
| | | Path firstFile; |
| | | Path lastFile; |
| | | } |
| | | |
| | | private static final Map<String, ProgressInfo> DOWNLOAD_PROGRESS = new ConcurrentHashMap<>(); |
| | |
| | | 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.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; |
| | | } |
| | | } |
| | | 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)); |
| | | 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)); |
| | | 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 -> { |
| | |
| | | DeviceDataLog logItem = JSON.parseObject(line, DeviceDataLog.class); |
| | | 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); |
| | | 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) { |
| | | try { |
| | | String firstLine = null; |
| | | try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) { |
| | | firstLine = lines.findFirst().orElse(null); |
| | | } |
| | | 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; |
| | | } |
| | |
| | | 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(enums); |
| | | } |
| | | |
| | | 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) { |
| | | 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<String> 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; |
| | | } |
| | | } |
| | | } |