lsh
2026-04-21 720e0926fa1c94b952c26e111206c5d6e1ed5ba2
src/main/java/com/zy/asrs/controller/DeviceLogController.java
@@ -4,26 +4,39 @@
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.io.BufferedWriter;
import java.io.OutputStreamWriter;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@Slf4j
@RestController
@@ -39,8 +52,20 @@
        DEVICE_TYPE_LABELS.put("Devp", "输送设备");
    }
    @Autowired
    private BasDevpService basDevpService;
    @Value("${deviceLogStorage.loggingPath}")
    private String loggingPath;
    @Value("${logging.file.path:./stock/out/@pom.build.finalName@/logs}")
    private String systemLoggingPath;
    private static final DateTimeFormatter SYSTEM_LOG_DATE = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter SYSTEM_LOG_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final DateTimeFormatter SYSTEM_LOG_EXPORT_TIME = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss");
    private static final Pattern SYSTEM_LOG_TIMESTAMP = Pattern.compile("^(\\d{2}:\\d{2}:\\d{2}\\.\\d{3})");
    private static final Pattern SYSTEM_LOG_ROLLED_FILE = Pattern.compile("^(info|error)_(\\d{4}-\\d{2}-\\d{2})\\.(\\d+)\\.log$");
    private static class ProgressInfo {
        long totalRaw;
@@ -53,6 +78,7 @@
    private static class FileNameInfo {
        String type;
        String deviceNo;
        String stationId;
        String day;
        int index;
    }
@@ -66,6 +92,8 @@
        String type;
        String typeLabel;
        String deviceNo;
        String stationId;
        List<String> stationIds;
        int fileCount;
        Long firstTime;
        Long lastTime;
@@ -75,7 +103,27 @@
        Path lastFile;
    }
    private static class SystemLogFileInfo {
        String logType;
        LocalDate day;
        Integer index;
        boolean active;
        Path path;
    }
    private static class SystemLogDownloadRequest {
        String logType;
        LocalDateTime startTime;
        LocalDateTime endTime;
        List<Path> files;
    }
    private static final Map<String, ProgressInfo> DOWNLOAD_PROGRESS = new ConcurrentHashMap<>();
    private static final int SYSTEM_LOG_MAX_RANGE_DAYS = 10;
    private static final int SYSTEM_LOG_MATCH_BUFFER_LINES = 200000;
    private static final String SYSTEM_LOG_ENTRY_NAME = "system.log";
    private static final Map<String, SystemLogDownloadRequest> SYSTEM_LOG_DOWNLOAD_REQUESTS = new ConcurrentHashMap<>();
    @RequestMapping(value = "/deviceLog/dates/auth")
    @ManagerAuth
@@ -140,31 +188,29 @@
            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;
@@ -189,44 +235,42 @@
            }
            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;
                    }
            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);
                    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);
                    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);
@@ -238,7 +282,8 @@
    @ManagerAuth
    public R timeline(@PathVariable("day") String day,
                      @RequestParam("type") String type,
                      @RequestParam("deviceNo") String deviceNo) {
                      @RequestParam("deviceNo") String deviceNo,
                      @RequestParam(value = "stationId", required = false) String stationId) {
        try {
            String dayClean = normalizeDay(day);
            if (dayClean == null) {
@@ -250,11 +295,14 @@
            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);
            List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId);
            if (files.isEmpty()) {
                return R.error("未找到日志文件");
            }
@@ -262,7 +310,7 @@
            List<Map<String, Object>> segments = new ArrayList<>();
            Long startTime = null;
            for (int i = 0; i < files.size(); i++) {
                Long segmentStart = getFileStartTime(files.get(i));
                Long segmentStart = getFileStartTime(files.get(i), stationId);
                if (segmentStart != null && (startTime == null || segmentStart < startTime)) {
                    startTime = segmentStart;
                }
@@ -272,7 +320,7 @@
                segment.put("endTime", null);
                segments.add(segment);
            }
            Long endTime = getFileEndTime(files.get(files.size() - 1));
            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");
@@ -300,6 +348,7 @@
            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());
@@ -316,6 +365,7 @@
    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 {
@@ -329,48 +379,37 @@
            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 -> {
                        if (line != null && !line.trim().isEmpty()) {
                            try {
                                DeviceDataLog logItem = JSON.parseObject(line, DeviceDataLog.class);
                                resultLogs.add(logItem);
                                if (matchesRequestedStation(logItem, stationId)) {
                                    resultLogs.add(logItem);
                                }
                            } catch (Exception e) {
                                // 忽略解析错误
                            }
                        }
                    });
@@ -378,9 +417,8 @@
                    log.error("读取日志文件失败: " + f, e);
                }
            }
            // 按时间排序
            resultLogs.sort(Comparator.comparing(DeviceDataLog::getCreateTime, Comparator.nullsLast(Date::compareTo)));
            return R.ok(resultLogs);
        } catch (Exception e) {
            log.error("预览日志失败", e);
@@ -393,6 +431,7 @@
    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", "");
@@ -405,81 +444,58 @@
            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);
                Long midStart = getFileStartTime(midFile, stationId);
                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) {
    private Long getFileStartTime(Path file, String stationId) {
        try {
            String firstLine = readFirstNonBlankLine(file);
            String firstLine = readFirstMatchingLine(file, stationId);
            if (firstLine == null) return null;
            DeviceDataLog firstLog = JSON.parseObject(firstLine, DeviceDataLog.class);
            return firstLog.getCreateTime().getTime();
@@ -488,9 +504,9 @@
        }
    }
    private Long getFileEndTime(Path file) {
    private Long getFileEndTime(Path file, String stationId) {
        try {
            String lastLine = readLastNonBlankLine(file);
            String lastLine = readLastMatchingLine(file, stationId);
            if (lastLine == null) return null;
            DeviceDataLog lastLog = JSON.parseObject(lastLine, DeviceDataLog.class);
            return lastLog.getCreateTime().getTime();
@@ -522,12 +538,17 @@
                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 = findDeviceFiles(dayDir, dayClean, type, deviceNo);
            List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId);
            files = sliceDownloadFiles(files, offset, limit);
            if (files.isEmpty()) {
                response.setStatus(404);
@@ -563,9 +584,9 @@
            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();
@@ -601,11 +622,15 @@
            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 = findDeviceFiles(dayDir, dayClean, type, deviceNo);
            List<Path> files = findDeviceFiles(dayDir, dayClean, type, deviceNo, stationId);
            if ((offset != null && offset >= files.size())) {
                return R.error("起始序号超出范围");
            }
@@ -664,20 +689,145 @@
        return R.ok(res);
    }
    @RequestMapping(value = "/deviceLog/system/download/init/auth")
    @ManagerAuth
    public R initSystemDownload(@org.springframework.web.bind.annotation.RequestBody com.alibaba.fastjson.JSONObject param) {
        try {
            String logType = normalizeSystemLogType(param.getString("logType"));
            if (logType == null) {
                return R.error("日志类型错误");
            }
            LocalDateTime startTime = parseSystemLogDateTime(param.getString("startTime"));
            LocalDateTime endTime = parseSystemLogDateTime(param.getString("endTime"));
            if (startTime == null || endTime == null) {
                return R.error("时间格式错误");
            }
            if (startTime.isAfter(endTime)) {
                return R.error("开始时间不能晚于结束时间");
            }
            long daySpan = java.time.temporal.ChronoUnit.DAYS.between(startTime.toLocalDate(), endTime.toLocalDate());
            if (daySpan > SYSTEM_LOG_MAX_RANGE_DAYS) {
                return R.error("时间范围不能超过" + SYSTEM_LOG_MAX_RANGE_DAYS + "天");
            }
            List<Path> files = findSystemLogFiles(logType, startTime, endTime);
            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);
            SystemLogDownloadRequest request = new SystemLogDownloadRequest();
            request.logType = logType;
            request.startTime = startTime;
            request.endTime = endTime;
            request.files = files;
            SYSTEM_LOG_DOWNLOAD_REQUESTS.put(id, request);
            Map<String, Object> res = new HashMap<>();
            res.put("progressId", id);
            res.put("totalSize", info.totalRaw);
            res.put("fileCount", info.totalCount);
            return R.ok(res);
        } catch (Exception e) {
            log.error("初始化系统日志下载失败", e);
            return R.error("初始化失败");
        }
    }
    @RequestMapping(value = "/deviceLog/system/download/auth")
    @ManagerAuth
    public void downloadSystemLog(@RequestParam("logType") String logTypeParam,
                                  @RequestParam("startTime") String startTimeParam,
                                  @RequestParam("endTime") String endTimeParam,
                                  @RequestParam(value = "progressId", required = false) String progressId,
                                  HttpServletResponse response) {
        String progressKey = null;
        try {
            String logType = normalizeSystemLogType(logTypeParam);
            LocalDateTime startTime = parseSystemLogDateTime(startTimeParam);
            LocalDateTime endTime = parseSystemLogDateTime(endTimeParam);
            if (logType == null || startTime == null || endTime == null || startTime.isAfter(endTime)) {
                response.setStatus(400);
                return;
            }
            SystemLogDownloadRequest requestInfo = null;
            if (!Cools.isEmpty(progressId)) {
                requestInfo = SYSTEM_LOG_DOWNLOAD_REQUESTS.get(progressId);
                progressKey = progressId;
            }
            List<Path> files;
            if (requestInfo != null
                    && Objects.equals(requestInfo.logType, logType)
                    && Objects.equals(requestInfo.startTime, startTime)
                    && Objects.equals(requestInfo.endTime, endTime)) {
                files = requestInfo.files == null ? Collections.emptyList() : requestInfo.files;
            } else {
                files = findSystemLogFiles(logType, startTime, endTime);
            }
            if (files.isEmpty()) {
                response.setStatus(404);
                return;
            }
            if (Cools.isEmpty(progressKey)) {
                progressKey = UUID.randomUUID().toString();
            }
            ProgressInfo info = prepareProgress(progressKey, files);
            response.reset();
            response.setContentType("application/zip");
            String filename = logType + "_" + formatSystemExportTime(startTime) + "_" + formatSystemExportTime(endTime) + ".zip";
            response.setHeader("Content-Disposition", "attachment; filename=" + filename);
            response.setHeader("X-Progress-Id", progressKey);
            try (ZipOutputStream zos = new ZipOutputStream(response.getOutputStream())) {
                zos.putNextEntry(new ZipEntry(SYSTEM_LOG_ENTRY_NAME));
                try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(zos, StandardCharsets.UTF_8))) {
                    boolean written = writeSystemLogContent(files, logType, startTime, endTime, writer, info);
                    writer.flush();
                    if (!written) {
                        response.reset();
                        response.setStatus(404);
                        return;
                    }
                }
                zos.closeEntry();
                zos.finish();
                info.finished = true;
            }
        } catch (Exception e) {
            log.error("下载系统日志失败", e);
            try {
                response.reset();
                response.setStatus(500);
            } catch (Exception ignore) {
            }
        } finally {
            if (!Cools.isEmpty(progressId)) {
                SYSTEM_LOG_DOWNLOAD_REQUESTS.remove(progressId);
            }
        }
    }
    @RequestMapping(value = "/deviceLog/enums/auth")
    @ManagerAuth
    public R getEnums() {
        Map<String, Map<String, String>> 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)));
@@ -692,9 +842,210 @@
        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 ProgressInfo prepareProgress(String progressId, List<Path> files) {
        return DOWNLOAD_PROGRESS.compute(progressId, (key, existing) -> {
            ProgressInfo next = existing == null ? new ProgressInfo() : existing;
            next.totalCount = files == null ? 0 : files.size();
            long total = 0L;
            if (files != null) {
                for (Path f : files) {
                    try { total += Files.size(f); } catch (Exception ignored) {}
                }
            }
            next.totalRaw = total;
            next.processedRaw = 0L;
            next.processedCount = 0;
            next.finished = false;
            return next;
        });
    }
    private boolean writeSystemLogContent(List<Path> files,
                                          String logType,
                                          LocalDateTime startTime,
                                          LocalDateTime endTime,
                                          BufferedWriter writer,
                                          ProgressInfo progressInfo) throws Exception {
        boolean wroteAny = false;
        for (Path file : files) {
            SystemLogFileInfo fileInfo = parseSystemLogFile(file);
            if (fileInfo == null) {
                updateProgress(progressInfo, file);
                continue;
            }
            LocalDate baseDate = fileInfo.day != null ? fileInfo.day : startTime.toLocalDate();
            try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
                List<String> pending = new ArrayList<>();
                boolean pendingMatched = false;
                for (Iterator<String> iterator = lines.iterator(); iterator.hasNext(); ) {
                    String line = iterator.next();
                    Matcher matcher = SYSTEM_LOG_TIMESTAMP.matcher(line == null ? "" : line);
                    if (matcher.find()) {
                        if (pendingMatched && !pending.isEmpty()) {
                            for (String pendingLine : pending) {
                                writer.write(pendingLine == null ? "" : pendingLine);
                                writer.newLine();
                            }
                            wroteAny = true;
                        }
                        pending.clear();
                        pendingMatched = isSystemLogLineInRange(baseDate, matcher.group(1), startTime, endTime);
                    }
                    if (pendingMatched) {
                        pending.add(line);
                        if (pending.size() > SYSTEM_LOG_MATCH_BUFFER_LINES) {
                            for (String pendingLine : pending) {
                                writer.write(pendingLine == null ? "" : pendingLine);
                                writer.newLine();
                            }
                            wroteAny = true;
                            pending.clear();
                        }
                    } else if (!pending.isEmpty()) {
                        pending.clear();
                    }
                }
                if (pendingMatched && !pending.isEmpty()) {
                    for (String pendingLine : pending) {
                        writer.write(pendingLine == null ? "" : pendingLine);
                        writer.newLine();
                    }
                    wroteAny = true;
                }
            }
            updateProgress(progressInfo, file);
        }
        if (progressInfo != null) {
            progressInfo.finished = true;
        }
        return wroteAny;
    }
    private void updateProgress(ProgressInfo progressInfo, Path file) {
        if (progressInfo == null) {
            return;
        }
        try {
            progressInfo.processedRaw += Files.size(file);
        } catch (Exception ignored) {
        }
        progressInfo.processedCount += 1;
    }
    private List<Path> findSystemLogFiles(String logType, LocalDateTime startTime, LocalDateTime endTime) throws Exception {
        Path baseDir = Paths.get(systemLoggingPath);
        if (!Files.exists(baseDir) || !Files.isDirectory(baseDir)) {
            return Collections.emptyList();
        }
        LocalDate startDate = startTime.toLocalDate();
        LocalDate endDate = endTime.toLocalDate();
        List<SystemLogFileInfo> matched = new ArrayList<>();
        try (Stream<Path> stream = Files.list(baseDir)) {
            stream.filter(path -> !Files.isDirectory(path)).forEach(path -> {
                SystemLogFileInfo info = parseSystemLogFile(path);
                if (info == null || !Objects.equals(info.logType, logType)) {
                    return;
                }
                if (info.active || info.day == null || (!info.day.isBefore(startDate) && !info.day.isAfter(endDate))) {
                    matched.add(info);
                }
            });
        }
        matched.sort((left, right) -> {
            LocalDate leftDay = left.day == null ? LocalDate.MAX : left.day;
            LocalDate rightDay = right.day == null ? LocalDate.MAX : right.day;
            int cmp = leftDay.compareTo(rightDay);
            if (cmp != 0) {
                return cmp;
            }
            int leftIndex = left.index == null ? Integer.MAX_VALUE : left.index;
            int rightIndex = right.index == null ? Integer.MAX_VALUE : right.index;
            if (left.active != right.active) {
                return left.active ? 1 : -1;
            }
            return Integer.compare(leftIndex, rightIndex);
        });
        return matched.stream().map(item -> item.path).collect(Collectors.toList());
    }
    private SystemLogFileInfo parseSystemLogFile(Path path) {
        if (path == null) {
            return null;
        }
        String name = path.getFileName().toString();
        if ("info.log".equals(name) || "error.log".equals(name)) {
            SystemLogFileInfo info = new SystemLogFileInfo();
            info.logType = name.startsWith("info") ? "info" : "error";
            info.active = true;
            info.path = path;
            return info;
        }
        Matcher matcher = SYSTEM_LOG_ROLLED_FILE.matcher(name);
        if (!matcher.matches()) {
            return null;
        }
        try {
            SystemLogFileInfo info = new SystemLogFileInfo();
            info.logType = matcher.group(1);
            info.day = LocalDate.parse(matcher.group(2), SYSTEM_LOG_DATE);
            info.index = Integer.parseInt(matcher.group(3));
            info.active = false;
            info.path = path;
            return info;
        } catch (Exception e) {
            return null;
        }
    }
    private String normalizeSystemLogType(String logType) {
        if (Cools.isEmpty(logType)) {
            return null;
        }
        String value = logType.trim().toLowerCase(Locale.ROOT);
        if ("info".equals(value) || "error".equals(value)) {
            return value;
        }
        return null;
    }
    private LocalDateTime parseSystemLogDateTime(String value) {
        if (Cools.isEmpty(value)) {
            return null;
        }
        try {
            return LocalDateTime.parse(value.trim(), SYSTEM_LOG_DATE_TIME);
        } catch (DateTimeParseException e) {
            return null;
        }
    }
    private boolean isSystemLogLineInRange(LocalDate baseDate,
                                           String timePart,
                                           LocalDateTime startTime,
                                           LocalDateTime endTime) {
        if (baseDate == null || Cools.isEmpty(timePart) || startTime == null || endTime == null) {
            return false;
        }
        try {
            LocalTime time = LocalTime.parse(timePart);
            LocalDateTime timestamp = LocalDateTime.of(baseDate, time);
            return !timestamp.isBefore(startTime) && !timestamp.isAfter(endTime);
        } catch (DateTimeParseException e) {
            return false;
        }
    }
    private String formatSystemExportTime(LocalDateTime dateTime) {
        if (dateTime == null) {
            return "unknown";
        }
        return SYSTEM_LOG_EXPORT_TIME.format(dateTime);
    }
    private Map<String, Object> buildEmptySummary() {
        return buildSummaryResponse(Collections.emptyList());
@@ -732,9 +1083,11 @@
                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);
@@ -746,6 +1099,56 @@
        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)) {
@@ -754,27 +1157,57 @@
        return dayClean;
    }
    private List<Path> findDeviceFiles(Path dayDir, String dayClean, String type, String deviceNo) throws Exception {
        String prefix = type + "_" + deviceNo + "_" + 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(dayDir)) {
        try (Stream<Path> stream = Files.list(deviceDir)) {
            files = stream
                    .filter(p -> {
                        String name = p.getFileName().toString();
                        return name.endsWith(".log") && name.startsWith(prefix);
                    })
                    .filter(p -> !Files.isDirectory(p) && matchesFileInfo(parseFileName(p.getFileName().toString()), target))
                    .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;
            }
            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) {
@@ -801,20 +1234,69 @@
        if (fileName == null || !fileName.endsWith(".log")) {
            return null;
        }
        String[] parts = fileName.split("_", 4);
        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].replace(".log", ""));
            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) {
@@ -825,65 +1307,77 @@
        }
    }
    private FileTimeRange readFileTimeRange(Path file) {
    private FileTimeRange readFileTimeRange(Path file, String stationId) {
        FileTimeRange range = new FileTimeRange();
        try {
            String firstLine = readFirstNonBlankLine(file);
            String lastLine = readLastNonBlankLine(file);
            range.startTime = parseLogTime(firstLine);
            range.endTime = parseLogTime(lastLine);
            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) {
    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 String readFirstNonBlankLine(Path file) {
    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<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
            return lines.filter(line -> line != null && !line.trim().isEmpty()).findFirst().orElse(null);
            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 readLastNonBlankLine(Path file) {
        try (RandomAccessFile randomAccessFile = new RandomAccessFile(file.toFile(), "r")) {
            long length = randomAccessFile.length();
            if (length <= 0) {
    private String readLastMatchingLine(Path file, String stationId) {
        try (Stream<String> lines = Files.lines(file, StandardCharsets.UTF_8)) {
            List<String> matched = lines
                    .filter(line -> line != null && !line.trim().isEmpty())
                    .filter(line -> matchesRequestedStation(parseLogLine(line), stationId))
                    .collect(Collectors.toList());
            if (matched.isEmpty()) {
                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);
            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;
            }
            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;
            return JSON.parseObject(line, DeviceDataLog.class);
        } catch (Exception e) {
            return null;
        }