| | |
| | | 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; |
| | |
| | | @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", "输送设备"); |
| | | } |
| | | |
| | | @Value("${deviceLogStorage.loggingPath}") |
| | | private String loggingPath; |
| | | |
| | |
| | | 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<String, ProgressInfo> DOWNLOAD_PROGRESS = new ConcurrentHashMap<>(); |
| | |
| | | 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<String, DeviceAggregate> aggregateMap = new LinkedHashMap<>(); |
| | | try (Stream<Path> stream = Files.list(dayDir)) { |
| | | List<Path> 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<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo); |
| | | 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("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("读取设备日志时间轴失败"); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | 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(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); |
| | | files = sliceDownloadFiles(files, offset, limit); |
| | | if (files.isEmpty()) { |
| | | response.setStatus(404); |
| | | return; |
| | |
| | | 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); |
| | | 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("fileCount", item.fileCount); |
| | | x.put("firstTime", item.firstTime); |
| | | x.put("lastTime", item.lastTime); |
| | | 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 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) throws Exception { |
| | | String prefix = type + "_" + deviceNo + "_" + dayClean + "_"; |
| | | List<Path> files; |
| | | try (Stream<Path> 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<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[] 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<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; |
| | | } |
| | | } |
| | | } |