#
Junjie
17 小时以前 fd82105a3dfe347c4c9acb0410c117d8d67c9339
src/main/java/com/zy/asrs/controller/DeviceLogController.java
@@ -14,6 +14,8 @@
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;
@@ -27,6 +29,16 @@
@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;
@@ -36,6 +48,31 @@
        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<>();
@@ -135,6 +172,142 @@
            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("读取设备日志时间轴失败");
        }
    }
@@ -306,13 +479,21 @@
    
    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;
        }
@@ -346,31 +527,8 @@
                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;
@@ -447,29 +605,14 @@
            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();
@@ -552,4 +695,197 @@
        
        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;
        }
    }
}