src/main/java/com/zy/asrs/controller/DeviceLogController.java
@@ -4,10 +4,14 @@ 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; @@ -24,6 +28,8 @@ 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 @@ -39,6 +45,9 @@ DEVICE_TYPE_LABELS.put("Devp", "输送设备"); } @Autowired private BasDevpService basDevpService; @Value("${deviceLogStorage.loggingPath}") private String loggingPath; @@ -53,6 +62,7 @@ private static class FileNameInfo { String type; String deviceNo; String stationId; String day; int index; } @@ -66,6 +76,8 @@ String type; String typeLabel; String deviceNo; String stationId; List<String> stationIds; int fileCount; Long firstTime; Long lastTime; @@ -140,31 +152,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,32 +199,29 @@ } 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()) { @@ -227,6 +234,7 @@ 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 +246,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 +259,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("未找到日志文件"); } @@ -300,6 +312,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 +329,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,39 +343,27 @@ 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 -> { @@ -370,7 +372,6 @@ DeviceDataLog logItem = JSON.parseObject(line, DeviceDataLog.class); resultLogs.add(logItem); } catch (Exception e) { // 忽略解析错误 } } }); @@ -378,9 +379,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 +393,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,72 +406,49 @@ 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("寻址失败"); @@ -522,12 +500,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 +546,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 +584,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("起始序号超出范围"); } @@ -732,9 +719,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 +735,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 +793,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,22 +870,71 @@ 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) { try { return Integer.parseInt(String.valueOf(deviceNo)); src/main/java/com/zy/asrs/entity/DeviceDataLog.java
@@ -1,16 +1,15 @@ package com.zy.asrs.entity; import com.core.common.Cools;import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import java.text.SimpleDateFormat; import java.util.Date; import org.springframework.format.annotation.DateTimeFormat; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.annotations.ApiModelProperty; import lombok.Data; import com.baomidou.mybatisplus.annotation.TableName; import org.springframework.format.annotation.DateTimeFormat; import java.io.Serializable; import java.util.Date; @Data @TableName("wcs_device_data_log") @@ -18,62 +17,60 @@ private static final long serialVersionUID = 1L; @ApiModelProperty(value= "") @ApiModelProperty(value = "") @TableId(value = "id", type = IdType.AUTO) private Long id; /** * 设备类型 */ @ApiModelProperty(value= "设备类型") @ApiModelProperty(value = "设备类型") private String type; /** * 设备号 */ @ApiModelProperty(value= "设备号") @ApiModelProperty(value = "设备号") @TableField("device_no") private Integer deviceNo; /** * 站点号 */ @ApiModelProperty(value = "站点号") @TableField("station_id") private Integer stationId; /** * 源数据 */ @ApiModelProperty(value= "源数据") @ApiModelProperty(value = "源数据") @TableField("origin_data") private String originData; /** * 源数据解析后得到的wcs数据 */ @ApiModelProperty(value= "源数据解析后得到的wcs数据") @ApiModelProperty(value = "源数据解析后得到的wcs数据") @TableField("wcs_data") private String wcsData; /** * 采集时间 */ @ApiModelProperty(value= "采集时间") @ApiModelProperty(value = "采集时间") @TableField("create_time") @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss") @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date createTime; public DeviceDataLog() {} public DeviceDataLog() { } public DeviceDataLog(String type,Integer deviceNo,String originData,String wcsData,Date createTime) { public DeviceDataLog(String type, Integer deviceNo, String originData, String wcsData, Date createTime) { this.type = type; this.deviceNo = deviceNo; this.originData = originData; this.wcsData = wcsData; this.createTime = createTime; } // DeviceDataLog deviceDataLog = new DeviceDataLog( // null, // 设备类型 // null, // 设备号 // null, // 源数据 // null, // 源数据解析后得到的wcs数据 // null // 采集时间 // ); } src/main/java/com/zy/common/utils/RedisUtil.java
@@ -215,6 +215,37 @@ } } public boolean multiSet(Map<String, Object> values, long time) { if (values == null || values.isEmpty()) { return true; } try { redisTemplate.executePipelined((RedisCallback<Object>) connection -> { for (Map.Entry<String, Object> entry : values.entrySet()) { if (entry == null || entry.getKey() == null) { continue; } byte[] keyBytes = redisTemplate.getStringSerializer().serialize(entry.getKey()); byte[] valueBytes = redisTemplate.getValueSerializer().serialize(entry.getValue()); if (keyBytes == null || valueBytes == null) { continue; } if (time > 0) { connection.stringCommands().setEx(keyBytes, time, valueBytes); } else { connection.stringCommands().set(keyBytes, valueBytes); } } return null; }); redisTemplate.execute((RedisCallback<Void>) connection -> null); return true; } catch (Exception e) { e.printStackTrace(); return false; } } /** * 递增 * src/main/java/com/zy/core/task/DeviceAsyncLogPublisher.java
New file @@ -0,0 +1,82 @@ package com.zy.core.task; import com.zy.asrs.entity.DeviceDataLog; import com.zy.common.utils.RedisUtil; import com.zy.core.utils.DeviceLogRedisKeyBuilder; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; @Slf4j @Component public class DeviceAsyncLogPublisher { private static final String LANE_PREFIX = "device-log-publish-"; private static final String TASK_NAME = "publish-device-log"; private static final long LOG_TTL_SECONDS = 60L * 60L * 24L; private static final long MIN_INTERVAL_MS = 0L; private final ConcurrentHashMap<String, AtomicReference<DeviceDataLog>> pendingByLane = new ConcurrentHashMap<>(); @Autowired private MainProcessTaskSubmitter mainProcessTaskSubmitter; @Autowired private RedisUtil redisUtil; public void publishLatest(DeviceDataLog deviceDataLog) { String laneKey = buildLaneKey(deviceDataLog); if (laneKey == null) { return; } pendingByLane .computeIfAbsent(laneKey, key -> new AtomicReference<>()) .set(deviceDataLog); boolean submitted = mainProcessTaskSubmitter.submitKeyedSerialTask( LANE_PREFIX, laneKey, TASK_NAME, MIN_INTERVAL_MS, () -> drain(laneKey) ); if (!submitted) { log.debug("Skip duplicate device async log publish submit, laneKey={}", laneKey); } } private void drain(String laneKey) { AtomicReference<DeviceDataLog> pending = pendingByLane.get(laneKey); if (pending == null) { return; } while (true) { DeviceDataLog deviceDataLog = pending.getAndSet(null); if (deviceDataLog == null) { return; } try { boolean success = redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, LOG_TTL_SECONDS); if (!success) { pending.compareAndSet(null, deviceDataLog); log.warn("Device async log publish failed, keep latest pending snapshot, type={}, deviceNo={}, stationId={}", deviceDataLog.getType(), deviceDataLog.getDeviceNo(), deviceDataLog.getStationId()); return; } } catch (Exception e) { pending.compareAndSet(null, deviceDataLog); log.error("Device async log publish error, type={}, deviceNo={}, stationId={}", deviceDataLog.getType(), deviceDataLog.getDeviceNo(), deviceDataLog.getStationId(), e); return; } } } private String buildLaneKey(DeviceDataLog deviceDataLog) { if (deviceDataLog == null || deviceDataLog.getType() == null || deviceDataLog.getDeviceNo() == null) { return null; } return deviceDataLog.getType() + ":" + deviceDataLog.getDeviceNo(); } } src/main/java/com/zy/core/task/DeviceLogScheduler.java
@@ -7,6 +7,7 @@ import com.zy.asrs.service.DeviceDataLogService; import com.zy.common.utils.RedisUtil; import com.zy.core.enums.RedisKeyType; import com.zy.core.enums.SlaveType; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -18,21 +19,21 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardOpenOption; import java.nio.file.attribute.BasicFileAttributes; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Set; import java.util.List; import java.util.ArrayList; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; import java.util.stream.Stream; import java.util.stream.Collectors; import java.util.stream.Stream; @Slf4j @Component @@ -59,7 +60,7 @@ public void delDeviceLog() { if ("mysql".equals(storageType)) { deviceDataLogService.clearLog(expireDays == null ? 1 : expireDays); }else if ("file".equals(storageType)) { } else if ("file".equals(storageType)) { if (!FILE_OP_LOCK.tryLock()) { return; } @@ -68,7 +69,7 @@ } finally { FILE_OP_LOCK.unlock(); } }else { } else { log.error("未定义的存储类型:{}", storageType); } } @@ -90,7 +91,7 @@ if (!list.isEmpty()) { if ("mysql".equals(storageType)) { mysqlSave(keys, list); }else if ("file".equals(storageType)) { } else if ("file".equals(storageType)) { if (!FILE_OP_LOCK.tryLock()) { return; } @@ -99,7 +100,7 @@ } finally { FILE_OP_LOCK.unlock(); } }else { } else { log.error("未定义的存储类型:{}", storageType); } } @@ -143,22 +144,40 @@ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); Map<String, Map<String, List<DeviceDataLog>>> group = new HashMap<>(); for (DeviceDataLog logItem : list) { String typeName = logItem.getType(); String datePart = sdf.format(logItem.getCreateTime() == null ? new Date() : logItem.getCreateTime()); String prefix = typeName + "_" + String.valueOf(logItem.getDeviceNo()) + "_" + datePart + "_"; String prefix = buildFilePrefix(logItem, datePart); if (prefix == null) { continue; } String deviceFolderKey = buildDeviceFolderKey(logItem); if (deviceFolderKey == null) { continue; } group.computeIfAbsent(datePart, k -> new HashMap<>()) .computeIfAbsent(prefix, k -> new ArrayList<>()) .computeIfAbsent(deviceFolderKey, k -> new ArrayList<>()) .add(logItem); } for (Map.Entry<String, Map<String, List<DeviceDataLog>>> dateEntry : group.entrySet()) { Path dayDir = baseDir.resolve(dateEntry.getKey()); Files.createDirectories(dayDir); for (Map.Entry<String, List<DeviceDataLog>> entry : dateEntry.getValue().entrySet()) { String prefix = entry.getKey(); List<DeviceDataLog> logs = entry.getValue(); if (logs == null || logs.isEmpty()) { continue; } DeviceDataLog firstLog = logs.get(0); Path deviceDir = resolveDeviceDir(dayDir, firstLog); if (deviceDir == null) { continue; } Files.createDirectories(deviceDir); String prefix = buildFilePrefix(firstLog, dateEntry.getKey()); if (prefix == null) { continue; } logs.sort(Comparator.comparing(DeviceDataLog::getCreateTime, Comparator.nullsLast(Date::compareTo))); int index = findStartIndex(dayDir, prefix); Path current = dayDir.resolve(prefix + index + ".log"); int index = findStartIndex(deviceDir, prefix); Path current = deviceDir.resolve(prefix + index + ".log"); if (!Files.exists(current)) { Files.createFile(current); } @@ -169,7 +188,7 @@ byte[] line = (json + System.lineSeparator()).getBytes(StandardCharsets.UTF_8); if (size + line.length > max) { index++; current = dayDir.resolve(prefix + index + ".log"); current = deviceDir.resolve(prefix + index + ".log"); if (!Files.exists(current)) { Files.createFile(current); } @@ -184,6 +203,34 @@ } catch (Exception e) { log.error("设备日志文件存储失败", e); } } private String buildFilePrefix(DeviceDataLog logItem, String datePart) { if (logItem == null || logItem.getType() == null || logItem.getDeviceNo() == null) { return null; } if (String.valueOf(SlaveType.Devp).equals(logItem.getType())) { if (logItem.getStationId() == null) { log.warn("跳过缺少站点号的输送设备日志, deviceNo={}, createTime={}", logItem.getDeviceNo(), logItem.getCreateTime()); return null; } return logItem.getType() + "_" + logItem.getDeviceNo() + "_station_" + logItem.getStationId() + "_" + datePart + "_"; } return logItem.getType() + "_" + logItem.getDeviceNo() + "_" + datePart + "_"; } private String buildDeviceFolderKey(DeviceDataLog logItem) { if (logItem == null || logItem.getType() == null || logItem.getDeviceNo() == null) { return null; } return logItem.getType() + ":" + logItem.getDeviceNo(); } private Path resolveDeviceDir(Path dayDir, DeviceDataLog logItem) { if (dayDir == null || logItem == null || logItem.getType() == null || logItem.getDeviceNo() == null) { return null; } return dayDir.resolve(logItem.getType()).resolve(String.valueOf(logItem.getDeviceNo())); } private int findStartIndex(Path baseDir, String prefix) throws Exception { @@ -206,7 +253,8 @@ if (val > maxIdx) { maxIdx = val; } } catch (NumberFormatException ignored) {} } catch (NumberFormatException ignored) { } } } int candidate = maxIdx == 0 ? 1 : maxIdx; @@ -242,7 +290,8 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { try { Files.deleteIfExists(file); } catch (Exception ignored) {} } catch (Exception ignored) { } return FileVisitResult.CONTINUE; } @@ -250,7 +299,8 @@ public FileVisitResult postVisitDirectory(Path dir, java.io.IOException exc) { try { Files.deleteIfExists(dir); } catch (Exception ignored) {} } catch (Exception ignored) { } return FileVisitResult.CONTINUE; } }); src/main/java/com/zy/core/thread/impl/ZyRgvThread.java
@@ -25,7 +25,7 @@ import com.zy.core.network.ZyRgvConnectDriver; import com.zy.core.network.entity.ZyRgvStatusEntity; import com.zy.core.thread.RgvThread; import com.zy.core.utils.DeviceLogRedisKeyBuilder; import com.zy.core.task.DeviceAsyncLogPublisher; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -43,10 +43,12 @@ private ZyRgvConnectDriver zyRgvConnectDriver; private RgvProtocol rgvProtocol; private int deviceLogCollectTime = 200; private final DeviceAsyncLogPublisher deviceAsyncLogPublisher; public ZyRgvThread(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; this.deviceAsyncLogPublisher = SpringUtils.getBean(DeviceAsyncLogPublisher.class); } @Override @@ -140,7 +142,7 @@ deviceDataLog.setType(String.valueOf(SlaveType.Rgv)); deviceDataLog.setDeviceNo(rgvProtocol.getRgvNo()); deviceDataLog.setCreateTime(new Date()); redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24); deviceAsyncLogPublisher.publishLatest(deviceDataLog); rgvProtocol.setDeviceDataLog(System.currentTimeMillis()); } src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java
@@ -26,7 +26,7 @@ import com.zy.core.network.entity.ZyCrnStatusEntity; import com.zy.core.service.WrkCommandRollbackService; import com.zy.core.thread.CrnThread; import com.zy.core.utils.DeviceLogRedisKeyBuilder; import com.zy.core.task.DeviceAsyncLogPublisher; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -47,10 +47,12 @@ private ZyCrnConnectDriver zyCrnConnectDriver; private CrnProtocol crnProtocol; private int deviceLogCollectTime = 200; private final DeviceAsyncLogPublisher deviceAsyncLogPublisher; public ZySiemensCrnThread(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; this.deviceAsyncLogPublisher = SpringUtils.getBean(DeviceAsyncLogPublisher.class); } @Override @@ -200,7 +202,7 @@ deviceDataLog.setDeviceNo(crnProtocol.getCrnNo()); deviceDataLog.setCreateTime(new Date()); redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24); deviceAsyncLogPublisher.publishLatest(deviceDataLog); //更新采集时间 crnProtocol.setDeviceDataLog(System.currentTimeMillis()); } src/main/java/com/zy/core/thread/impl/ZySiemensCrnV2Thread.java
@@ -25,7 +25,7 @@ import com.zy.core.network.entity.ZyCrnStatusEntity; import com.zy.core.service.WrkCommandRollbackService; import com.zy.core.thread.CrnThread; import com.zy.core.utils.DeviceLogRedisKeyBuilder; import com.zy.core.task.DeviceAsyncLogPublisher; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -44,10 +44,12 @@ private ZyCrnV2ConnectDriver zyCrnConnectDriver; private CrnProtocol crnProtocol; private int deviceLogCollectTime = 200; private final DeviceAsyncLogPublisher deviceAsyncLogPublisher; public ZySiemensCrnV2Thread(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; this.deviceAsyncLogPublisher = SpringUtils.getBean(DeviceAsyncLogPublisher.class); } @Override @@ -199,7 +201,7 @@ deviceDataLog.setDeviceNo(crnProtocol.getCrnNo()); deviceDataLog.setCreateTime(new Date()); redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24); deviceAsyncLogPublisher.publishLatest(deviceDataLog); //更新采集时间 crnProtocol.setDeviceDataLog(System.currentTimeMillis()); } src/main/java/com/zy/core/thread/impl/ZySiemensDualCrnThread.java
@@ -26,7 +26,7 @@ import com.zy.core.network.entity.ZyDualCrnStatusEntity; import com.zy.core.thread.DualCrnThread; import com.zy.core.thread.StationThread; import com.zy.core.utils.DeviceLogRedisKeyBuilder; import com.zy.core.task.DeviceAsyncLogPublisher; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -48,10 +48,12 @@ private ZyDualCrnConnectDriver zyDualCrnConnectDriver; private DualCrnProtocol crnProtocol; private int deviceLogCollectTime = 200; private final DeviceAsyncLogPublisher deviceAsyncLogPublisher; public ZySiemensDualCrnThread(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; this.deviceAsyncLogPublisher = SpringUtils.getBean(DeviceAsyncLogPublisher.class); } @Override @@ -372,7 +374,7 @@ deviceDataLog.setDeviceNo(crnProtocol.getCrnNo()); deviceDataLog.setCreateTime(new Date()); redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24); deviceAsyncLogPublisher.publishLatest(deviceDataLog); //更新采集时间 crnProtocol.setDeviceDataLog(System.currentTimeMillis()); } src/main/java/com/zy/core/thread/impl/ZyStationThread.java
@@ -28,11 +28,11 @@ import com.zy.core.model.command.StationCommand; import com.zy.core.model.protocol.StationProtocol; import com.zy.core.network.entity.ZyStationStatusEntity; import com.zy.core.task.DeviceAsyncLogPublisher; import java.text.MessageFormat; import java.util.*; import com.zy.core.utils.DeviceLogRedisKeyBuilder; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -51,11 +51,13 @@ private int deviceLogCollectTime = 200; private long deviceDataLogTime = System.currentTimeMillis(); private final RecentStationArrivalTracker recentArrivalTracker; private final DeviceAsyncLogPublisher devpAsyncLogPublisher; public ZyStationThread(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; this.recentArrivalTracker = new RecentStationArrivalTracker(redisUtil); this.devpAsyncLogPublisher = SpringUtils.getBean(DeviceAsyncLogPublisher.class); } @Override @@ -170,20 +172,41 @@ StationErrLogSupport.sync(deviceConfig, redisUtil, statusList); if (System.currentTimeMillis() - deviceDataLogTime > deviceLogCollectTime) { //保存数据记录 DeviceDataLog deviceDataLog = new DeviceDataLog(); deviceDataLog.setOriginData(JSON.toJSONString(zyStationStatusEntities)); deviceDataLog.setWcsData(JSON.toJSONString(statusList)); deviceDataLog.setType(String.valueOf(SlaveType.Devp)); deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo()); deviceDataLog.setCreateTime(new Date()); redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24); //更新采集时间 Date createTime = new Date(); HashMap<Integer, ZyStationStatusEntity> originDataMap = buildStationStatusMap(zyStationStatusEntities); for (int i = 0; i < statusList.size(); i++) { StationProtocol stationProtocol = statusList.get(i); if (stationProtocol == null || stationProtocol.getStationId() == null) { continue; } DeviceDataLog deviceDataLog = new DeviceDataLog(); ZyStationStatusEntity originEntity = originDataMap.get(stationProtocol.getStationId()); deviceDataLog.setOriginData(originEntity == null ? null : JSON.toJSONString(originEntity)); deviceDataLog.setWcsData(JSON.toJSONString(stationProtocol)); deviceDataLog.setType(String.valueOf(SlaveType.Devp)); deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo()); deviceDataLog.setStationId(stationProtocol.getStationId()); deviceDataLog.setCreateTime(createTime); devpAsyncLogPublisher.publishLatest(deviceDataLog); } deviceDataLogTime = System.currentTimeMillis(); } } private HashMap<Integer, ZyStationStatusEntity> buildStationStatusMap(List<ZyStationStatusEntity> zyStationStatusEntities) { HashMap<Integer, ZyStationStatusEntity> map = new HashMap<>(); if (zyStationStatusEntities == null) { return map; } for (ZyStationStatusEntity statusEntity : zyStationStatusEntities) { if (statusEntity == null || statusEntity.getStationId() == null) { continue; } map.put(statusEntity.getStationId(), statusEntity); } return map; } @Override public boolean connect() { zyStationConnectDriver = new ZyStationConnectDriver(deviceConfig, redisUtil); src/main/java/com/zy/core/thread/impl/ZyStationV3Thread.java
@@ -30,9 +30,9 @@ import com.zy.core.network.DeviceConnectPool; import com.zy.core.network.ZyStationConnectDriver; import com.zy.core.network.entity.ZyStationStatusEntity; import com.zy.core.task.DeviceAsyncLogPublisher; import com.zy.core.thread.support.RecentStationArrivalTracker; import com.zy.core.thread.support.StationErrLogSupport; import com.zy.core.utils.DeviceLogRedisKeyBuilder; import lombok.Data; import lombok.extern.slf4j.Slf4j; @@ -59,11 +59,13 @@ private long deviceDataLogTime = System.currentTimeMillis(); private ExecutorService executor = Executors.newFixedThreadPool(9999); private final RecentStationArrivalTracker recentArrivalTracker; private final DeviceAsyncLogPublisher devpAsyncLogPublisher; public ZyStationV3Thread(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; this.recentArrivalTracker = new RecentStationArrivalTracker(redisUtil); this.devpAsyncLogPublisher = SpringUtils.getBean(DeviceAsyncLogPublisher.class); } @Override @@ -176,18 +178,41 @@ StationErrLogSupport.sync(deviceConfig, redisUtil, statusList); if (System.currentTimeMillis() - deviceDataLogTime > deviceLogCollectTime) { DeviceDataLog deviceDataLog = new DeviceDataLog(); deviceDataLog.setOriginData(JSON.toJSONString(zyStationStatusEntities)); deviceDataLog.setWcsData(JSON.toJSONString(statusList)); deviceDataLog.setType(String.valueOf(SlaveType.Devp)); deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo()); deviceDataLog.setCreateTime(new Date()); redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24); Date createTime = new Date(); HashMap<Integer, ZyStationStatusEntity> originDataMap = buildStationStatusMap(zyStationStatusEntities); for (int i = 0; i < statusList.size(); i++) { StationProtocol stationProtocol = statusList.get(i); if (stationProtocol == null || stationProtocol.getStationId() == null) { continue; } DeviceDataLog deviceDataLog = new DeviceDataLog(); ZyStationStatusEntity originEntity = originDataMap.get(stationProtocol.getStationId()); deviceDataLog.setOriginData(originEntity == null ? null : JSON.toJSONString(originEntity)); deviceDataLog.setWcsData(JSON.toJSONString(stationProtocol)); deviceDataLog.setType(String.valueOf(SlaveType.Devp)); deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo()); deviceDataLog.setStationId(stationProtocol.getStationId()); deviceDataLog.setCreateTime(createTime); devpAsyncLogPublisher.publishLatest(deviceDataLog); } deviceDataLogTime = System.currentTimeMillis(); } } private HashMap<Integer, ZyStationStatusEntity> buildStationStatusMap(List<ZyStationStatusEntity> zyStationStatusEntities) { HashMap<Integer, ZyStationStatusEntity> map = new HashMap<>(); if (zyStationStatusEntities == null) { return map; } for (ZyStationStatusEntity statusEntity : zyStationStatusEntities) { if (statusEntity == null || statusEntity.getStationId() == null) { continue; } map.put(statusEntity.getStationId(), statusEntity); } return map; } @Override public boolean connect() { zyStationConnectDriver = new ZyStationConnectDriver(deviceConfig, redisUtil); src/main/java/com/zy/core/thread/impl/ZyStationV4Thread.java
@@ -30,10 +30,10 @@ import com.zy.core.network.DeviceConnectPool; import com.zy.core.network.ZyStationConnectDriver; import com.zy.core.network.entity.ZyStationStatusEntity; import com.zy.core.task.DeviceAsyncLogPublisher; import com.zy.core.thread.impl.v5.StationMoveSegmentExecutor; import com.zy.core.thread.support.RecentStationArrivalTracker; import com.zy.core.thread.support.StationErrLogSupport; import com.zy.core.utils.DeviceLogRedisKeyBuilder; import com.zy.system.entity.Config; import com.zy.system.service.ConfigService; import lombok.Data; @@ -61,12 +61,14 @@ private long deviceDataLogTime = System.currentTimeMillis(); private ExecutorService executor = Executors.newFixedThreadPool(9999); private final RecentStationArrivalTracker recentArrivalTracker; private final DeviceAsyncLogPublisher devpAsyncLogPublisher; public ZyStationV4Thread(DeviceConfig deviceConfig, RedisUtil redisUtil) { this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; this.recentArrivalTracker = new RecentStationArrivalTracker(redisUtil); this.segmentExecutor = new StationMoveSegmentExecutor(deviceConfig, redisUtil, this::sendCommand); this.devpAsyncLogPublisher = SpringUtils.getBean(DeviceAsyncLogPublisher.class); } @Override @@ -193,18 +195,41 @@ StationErrLogSupport.sync(deviceConfig, redisUtil, statusList); if (System.currentTimeMillis() - deviceDataLogTime > deviceLogCollectTime) { DeviceDataLog deviceDataLog = new DeviceDataLog(); deviceDataLog.setOriginData(JSON.toJSONString(zyStationStatusEntities)); deviceDataLog.setWcsData(JSON.toJSONString(statusList)); deviceDataLog.setType(String.valueOf(SlaveType.Devp)); deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo()); deviceDataLog.setCreateTime(new Date()); redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24); Date createTime = new Date(); HashMap<Integer, ZyStationStatusEntity> originDataMap = buildStationStatusMap(zyStationStatusEntities); for (int i = 0; i < statusList.size(); i++) { StationProtocol stationProtocol = statusList.get(i); if (stationProtocol == null || stationProtocol.getStationId() == null) { continue; } DeviceDataLog deviceDataLog = new DeviceDataLog(); ZyStationStatusEntity originEntity = originDataMap.get(stationProtocol.getStationId()); deviceDataLog.setOriginData(originEntity == null ? null : JSON.toJSONString(originEntity)); deviceDataLog.setWcsData(JSON.toJSONString(stationProtocol)); deviceDataLog.setType(String.valueOf(SlaveType.Devp)); deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo()); deviceDataLog.setStationId(stationProtocol.getStationId()); deviceDataLog.setCreateTime(createTime); devpAsyncLogPublisher.publishLatest(deviceDataLog); } deviceDataLogTime = System.currentTimeMillis(); } } private HashMap<Integer, ZyStationStatusEntity> buildStationStatusMap(List<ZyStationStatusEntity> zyStationStatusEntities) { HashMap<Integer, ZyStationStatusEntity> map = new HashMap<>(); if (zyStationStatusEntities == null) { return map; } for (ZyStationStatusEntity statusEntity : zyStationStatusEntities) { if (statusEntity == null || statusEntity.getStationId() == null) { continue; } map.put(statusEntity.getStationId(), statusEntity); } return map; } @Override public boolean connect() { zyStationConnectDriver = new ZyStationConnectDriver(deviceConfig, redisUtil); src/main/java/com/zy/core/thread/impl/v5/StationV5StatusReader.java
@@ -17,13 +17,14 @@ import com.zy.core.model.protocol.StationProtocol; import com.zy.core.network.ZyStationConnectDriver; import com.zy.core.network.entity.ZyStationStatusEntity; import com.zy.core.task.DeviceAsyncLogPublisher; import com.zy.core.thread.support.RecentStationArrivalTracker; import com.zy.core.thread.support.StationErrLogSupport; import com.zy.core.utils.DeviceLogRedisKeyBuilder; import java.text.MessageFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashSet; import java.util.List; @@ -32,6 +33,7 @@ private final DeviceConfig deviceConfig; private final RedisUtil redisUtil; private final RecentStationArrivalTracker recentArrivalTracker; private final DeviceAsyncLogPublisher devpAsyncLogPublisher; private final List<StationProtocol> statusList = new ArrayList<>(); private volatile List<Integer> taskNoList = new ArrayList<>(); private boolean initialized = false; @@ -43,6 +45,7 @@ this.deviceConfig = deviceConfig; this.redisUtil = redisUtil; this.recentArrivalTracker = recentArrivalTracker; this.devpAsyncLogPublisher = SpringUtils.getBean(DeviceAsyncLogPublisher.class); } public void readStatus(ZyStationConnectDriver zyStationConnectDriver) { @@ -128,18 +131,41 @@ StationErrLogSupport.sync(deviceConfig, redisUtil, statusList); if (System.currentTimeMillis() - deviceDataLogTime > deviceLogCollectTime) { DeviceDataLog deviceDataLog = new DeviceDataLog(); deviceDataLog.setOriginData(JSON.toJSONString(zyStationStatusEntities)); deviceDataLog.setWcsData(JSON.toJSONString(statusList)); deviceDataLog.setType(String.valueOf(SlaveType.Devp)); deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo()); deviceDataLog.setCreateTime(new Date()); redisUtil.set(DeviceLogRedisKeyBuilder.build(deviceDataLog), deviceDataLog, 60 * 60 * 24); Date createTime = new Date(); HashMap<Integer, ZyStationStatusEntity> originDataMap = buildStationStatusMap(zyStationStatusEntities); for (int i = 0; i < statusList.size(); i++) { StationProtocol stationProtocol = statusList.get(i); if (stationProtocol == null || stationProtocol.getStationId() == null) { continue; } DeviceDataLog deviceDataLog = new DeviceDataLog(); ZyStationStatusEntity originEntity = originDataMap.get(stationProtocol.getStationId()); deviceDataLog.setOriginData(originEntity == null ? null : JSON.toJSONString(originEntity)); deviceDataLog.setWcsData(JSON.toJSONString(stationProtocol)); deviceDataLog.setType(String.valueOf(SlaveType.Devp)); deviceDataLog.setDeviceNo(deviceConfig.getDeviceNo()); deviceDataLog.setStationId(stationProtocol.getStationId()); deviceDataLog.setCreateTime(createTime); devpAsyncLogPublisher.publishLatest(deviceDataLog); } deviceDataLogTime = System.currentTimeMillis(); } } private HashMap<Integer, ZyStationStatusEntity> buildStationStatusMap(List<ZyStationStatusEntity> zyStationStatusEntities) { HashMap<Integer, ZyStationStatusEntity> map = new HashMap<>(); if (zyStationStatusEntities == null) { return map; } for (ZyStationStatusEntity statusEntity : zyStationStatusEntities) { if (statusEntity == null || statusEntity.getStationId() == null) { continue; } map.put(statusEntity.getStationId(), statusEntity); } return map; } public List<StationProtocol> getStatusList() { return statusList; } src/main/java/com/zy/core/utils/DeviceLogRedisKeyBuilder.java
@@ -2,6 +2,7 @@ import com.zy.asrs.entity.DeviceDataLog; import com.zy.core.enums.RedisKeyType; import com.zy.core.enums.SlaveType; import java.util.UUID; @@ -14,10 +15,19 @@ String deviceNo = deviceDataLog == null || deviceDataLog.getDeviceNo() == null ? "unknown" : String.valueOf(deviceDataLog.getDeviceNo()); String stationPart = buildStationPart(deviceDataLog, type); long millis = deviceDataLog != null && deviceDataLog.getCreateTime() != null ? deviceDataLog.getCreateTime().getTime() : System.currentTimeMillis(); String uuid = UUID.randomUUID().toString().replace("-", ""); return RedisKeyType.DEVICE_LOG_KEY.key + type + ":" + deviceNo + ":" + millis + ":" + uuid; return RedisKeyType.DEVICE_LOG_KEY.key + type + ":" + deviceNo + stationPart + ":" + millis + ":" + uuid; } private static String buildStationPart(DeviceDataLog deviceDataLog, String type) { if (!String.valueOf(SlaveType.Devp).equals(type)) { return ""; } Integer stationId = deviceDataLog == null ? null : deviceDataLog.getStationId(); return stationId == null ? ":station:unknown" : ":station:" + stationId; } } src/main/webapp/static/js/deviceLogs/deviceLogs.js
@@ -16,6 +16,7 @@ }, selectedDay: '', searchDeviceNo: '', searchStationId: '', activeType: '', viewMode: 'picker', deviceSummary: { @@ -31,6 +32,8 @@ selectedType: '', selectedDeviceNo: '', selectedStationId: '', visualFocusStationId: '', activeDeviceKey: '', timelineMeta: { @@ -123,12 +126,38 @@ filteredDevices: function () { var group = this.activeGroup; var devices = group && group.devices ? group.devices.slice() : []; var keyword = String(this.searchDeviceNo || '').trim(); if (!keyword) { return devices; } return devices.filter(function (item) { return String(item.deviceNo).indexOf(keyword) >= 0; var deviceKeyword = String(this.searchDeviceNo || '').trim(); var stationKeyword = String(this.searchStationId || '').trim(); return devices.map(function (item) { var matchedStationId = ''; var matchesDeviceNo = !deviceKeyword || String(item.deviceNo).indexOf(deviceKeyword) >= 0; var matchesStation = true; if (stationKeyword) { matchesStation = false; if (item.type === 'Devp') { if (item.stationId && String(item.stationId).indexOf(stationKeyword) >= 0) { matchesStation = true; matchedStationId = String(item.stationId); } else { var stationIds = Array.isArray(item.stationIds) ? item.stationIds : []; for (var i = 0; i < stationIds.length; i++) { if (String(stationIds[i]).indexOf(stationKeyword) >= 0) { matchesStation = true; matchedStationId = String(stationIds[i]); break; } } } } } if (!matchesDeviceNo || !matchesStation) { return null; } return Object.assign({}, item, { matchedStationId: matchedStationId }); }).filter(function (item) { return !!item; }); }, selectedDeviceSummary: function () { @@ -136,7 +165,7 @@ var found = null; (this.deviceGroups || []).forEach(function (group) { (group.devices || []).forEach(function (device) { if (this.buildDeviceKey(device.type, device.deviceNo) === key) { if (this.buildDeviceKey(device.type, device.deviceNo, device.stationId) === key) { found = device; } }, this); @@ -228,6 +257,9 @@ return this.selectedLogRow ? this.selectedLogRow._visualItems : []; }, visualParam: function () { if (this.selectedType === 'Devp') { return this.buildVisualParam(this.selectedType, this.selectedDeviceNo, this.resolveVisualStationId(this.selectedLogRow)); } return this.selectedLogRow ? this.selectedLogRow._visualParam : {}; }, activeRawText: function () { @@ -420,6 +452,7 @@ } this.selectedDay = day; this.searchDeviceNo = ''; this.searchStationId = ''; this.pause(); this.summaryLoading = true; this.resetSelectionState(); @@ -471,6 +504,9 @@ type: device.type, typeLabel: device.typeLabel || self.typeLabels[device.type] || device.type, deviceNo: String(device.deviceNo), stationId: device.stationId == null ? '' : String(device.stationId), stationIds: (device.stationIds || []).map(function (stationId) { return String(stationId); }), matchedStationId: '', fileCount: device.fileCount || 0, firstTime: device.firstTime || 0, lastTime: device.lastTime || 0 @@ -536,6 +572,8 @@ this.viewMode = 'picker'; this.selectedType = ''; this.selectedDeviceNo = ''; this.selectedStationId = ''; this.visualFocusStationId = ''; this.activeDeviceKey = ''; this.detailTab = 'logs'; this.rawTab = 'wcs'; @@ -553,7 +591,7 @@ if (!device) { return; } var nextKey = this.buildDeviceKey(device.type, device.deviceNo); var nextKey = this.buildDeviceKey(device.type, device.deviceNo, device.stationId); if (this.activeDeviceKey === nextKey && this.logRows.length) { return; } @@ -562,6 +600,10 @@ this.activeType = device.type; this.selectedType = device.type; this.selectedDeviceNo = String(device.deviceNo); this.selectedStationId = device.stationId == null ? '' : String(device.stationId); this.visualFocusStationId = device.stationId == null || String(device.stationId) === '' ? (device.matchedStationId == null ? '' : String(device.matchedStationId)) : String(device.stationId); this.activeDeviceKey = nextKey; this.detailTab = 'raw'; this.rawTab = 'wcs'; @@ -572,6 +614,25 @@ this.selectedTimestamp = 0; this.logLoadError = ''; this.loadTimeline(); }, resolveVisualStationId: function (logItem) { if (this.selectedType !== 'Devp') { return ''; } if (logItem && logItem.stationId != null && String(logItem.stationId) !== '') { return String(logItem.stationId); } if (this.visualFocusStationId) { return String(this.visualFocusStationId); } var selected = this.selectedDeviceSummary; if (selected && selected.stationId) { return String(selected.stationId); } if (selected && selected.matchedStationId) { return String(selected.matchedStationId); } return ''; }, returnToSelector: function () { this.pause(); @@ -590,7 +651,8 @@ method: 'GET', data: { type: this.selectedType, deviceNo: this.selectedDeviceNo deviceNo: this.selectedDeviceNo, stationId: this.selectedStationId || undefined }, success: function (res) { self.timelineLoading = false; @@ -627,6 +689,7 @@ timeline.type = data.type || this.selectedType; timeline.typeLabel = data.typeLabel || this.typeLabels[timeline.type] || timeline.type; timeline.deviceNo = String(data.deviceNo || this.selectedDeviceNo || ''); timeline.stationId = data.stationId == null ? this.selectedStationId : String(data.stationId || ''); timeline.startTime = data.startTime || 0; timeline.endTime = data.endTime || 0; timeline.totalFiles = data.totalFiles || 0; @@ -685,6 +748,7 @@ data: { type: this.selectedType, deviceNo: this.selectedDeviceNo, stationId: this.selectedStationId || undefined, offset: offset, limit: batchSize }, @@ -741,12 +805,13 @@ var protocol = this.safeParse(logItem && logItem.wcsData); var visualItems = this.buildVisualItems(protocol, this.selectedType); return Object.assign({}, logItem, { stationId: logItem && logItem.stationId == null ? this.selectedStationId : logItem.stationId, _ts: this.parseTimestamp(logItem && logItem.createTime), _key: this.buildLogRowKey(logItem), _segmentOffset: segmentOffset, _protocol: protocol, _visualItems: visualItems, _visualParam: this.buildVisualParam(this.selectedType, this.selectedDeviceNo), _visualParam: this.buildVisualParam(this.selectedType, this.selectedDeviceNo, this.resolveVisualStationId(logItem)), _summary: this.buildLogSummary(this.selectedType, visualItems, protocol) }); }, @@ -795,36 +860,17 @@ }; } if (type === 'Devp') { var stations = visualItems || []; var autoCount = 0; var taskCount = 0; var loadingCount = 0; var errorStations = []; var canInCount = 0; for (var i = 0; i < stations.length; i++) { if (this.toBool(stations[i].autoing)) { autoCount += 1; } if (stations[i].taskNo != null && stations[i].taskNo !== '' && Number(stations[i].taskNo) !== 0) { taskCount += 1; } if (this.toBool(stations[i].loading)) { loadingCount += 1; } if (this.toBool(stations[i].inEnable)) { canInCount += 1; } if (stations[i].error || stations[i].errorMsg) { errorStations.push(stations[i].stationId); } } var statusLabel = errorStations.length ? '故障' : (autoCount === stations.length && stations.length ? '自动' : '手动'); var station = visualItems[0] || this.transformData(protocol, type) || {}; var hasError = !!(station.error || station.errorMsg); var autoing = this.toBool(station.autoing); var loading = this.toBool(station.loading); var statusLabel = hasError ? '故障' : (autoing ? '自动' : '手动'); return { statusLabel: statusLabel, tone: MonitorCardKit.statusTone(statusLabel), title: stations.length + ' 个站点 · 任务 ' + taskCount + ' · 有物 ' + loadingCount, detail: '自动 ' + autoCount + ' / 手动 ' + Math.max(0, stations.length - autoCount) + ' / 可入 ' + canInCount, hint: errorStations.length ? ('异常站点 ' + errorStations.slice(0, 6).join(', ')) : ('站点数组大小 ' + stations.length) title: '站点 ' + MonitorCardKit.orDash(station.stationId) + ' · 任务 ' + MonitorCardKit.orDash(station.taskNo), detail: '目标站点 ' + MonitorCardKit.orDash(station.targetStaNo) + ' / 载货 ' + (loading ? '有物' : '无物') + ' / 条码 ' + MonitorCardKit.orDash(station.barcode), hint: hasError ? (MonitorCardKit.orDash(station.error) + (station.errorMsg ? (' · ' + station.errorMsg) : '')) : ('自动 ' + (autoing ? '是' : '否') + ' / 允许入库 ' + (this.toBool(station.inEnable) ? '是' : '否')) }; } return fallback; @@ -833,25 +879,23 @@ if (!protocol) { return []; } if (type === 'Devp' && Array.isArray(protocol)) { var self = this; return protocol.map(function (item) { return self.transformData(item, type); }).sort(function (a, b) { return (a.stationId || 0) - (b.stationId || 0); }); if (type === 'Devp') { return [this.transformData(protocol, type)]; } if (type !== 'Devp') { return [this.transformData(protocol, type)]; } return []; }, buildVisualParam: function (type, deviceNo) { buildVisualParam: function (type, deviceNo, stationId) { if (type === 'Crn' || type === 'DualCrn') { return { crnNo: Number(deviceNo) }; } if (type === 'Rgv') { return { rgvNo: Number(deviceNo) }; } if (type === 'Devp' && stationId) { return { stationId: Number(stationId) }; } return {}; }, @@ -1013,6 +1057,7 @@ return [ this.selectedType, this.selectedDeviceNo, this.selectedStationId, logItem && logItem.createTime ? logItem.createTime : '', this.hashString(logItem && logItem.originData ? logItem.originData : ''), this.hashString(logItem && logItem.wcsData ? logItem.wcsData : '') @@ -1290,6 +1335,7 @@ data: { type: this.selectedType, deviceNo: this.selectedDeviceNo, stationId: this.selectedStationId || undefined, timestamp: timestamp }, success: function (res) { @@ -1331,9 +1377,9 @@ return this.formatTimestamp(this.timelineMeta.startTime + value, true); }, handleCurrentDeviceDownload: function () { this.doDownload(this.selectedDay, this.selectedType, this.selectedDeviceNo); this.doDownload(this.selectedDay, this.selectedType, this.selectedDeviceNo, this.selectedStationId); }, doDownload: function (day, type, deviceNo) { doDownload: function (day, type, deviceNo, stationId) { if (!day || !type || !deviceNo) { return; } @@ -1345,7 +1391,8 @@ data: JSON.stringify({ day: day, type: type, deviceNo: deviceNo deviceNo: deviceNo, stationId: stationId || undefined }), dataType: 'json', contentType: 'application/json;charset=UTF-8', @@ -1356,7 +1403,7 @@ } var pid = res.data.progressId; self.startDownloadProgress(pid); self.performDownloadRequest(day, type, deviceNo, pid); self.performDownloadRequest(day, type, deviceNo, stationId, pid); }, error: function () { self.$message.error('初始化失败'); @@ -1386,10 +1433,15 @@ }); }, 500); }, performDownloadRequest: function (day, type, deviceNo, pid) { performDownloadRequest: function (day, type, deviceNo, stationId, pid) { var self = this; var query = '?type=' + encodeURIComponent(type) + '&deviceNo=' + encodeURIComponent(deviceNo); if (stationId) { query += '&stationId=' + encodeURIComponent(stationId); } query += '&progressId=' + encodeURIComponent(pid); $.ajax({ url: baseUrl + '/deviceLog/day/' + day + '/download/auth?type=' + encodeURIComponent(type) + '&deviceNo=' + encodeURIComponent(deviceNo) + '&progressId=' + encodeURIComponent(pid), url: baseUrl + '/deviceLog/day/' + day + '/download/auth' + query, headers: { token: localStorage.getItem('token') }, method: 'GET', xhrFields: { responseType: 'blob' }, @@ -1438,8 +1490,12 @@ } }); }, buildDeviceKey: function (type, deviceNo) { return String(type || '') + ':' + String(deviceNo || ''); buildDeviceKey: function (type, deviceNo, stationId) { var key = String(type || '') + ':' + String(deviceNo || ''); if (String(type || '') === 'Devp') { key += ':' + String(stationId || ''); } return key; }, parseDeviceNo: function (deviceNo) { var n = parseInt(deviceNo, 10); src/main/webapp/views/deviceLogs/deviceLogs.html
@@ -1140,12 +1140,19 @@ 当前日期 <strong>{{ selectedDay ? formatDayText(selectedDay) : '未选择' }}</strong> </div> <div class="dl-device-search" style="margin-bottom: 0;"> <div class="dl-device-search" style="margin-bottom: 0; display: flex; flex-direction: column; gap: 8px;"> <el-input v-model.trim="searchDeviceNo" size="small" clearable placeholder="按设备编号筛选"> <i slot="prefix" class="el-input__icon el-icon-search"></i> </el-input> <el-input v-model.trim="searchStationId" size="small" clearable placeholder="按站点编号筛选"> <i slot="prefix" class="el-input__icon el-icon-search"></i> </el-input> </div> @@ -1160,17 +1167,19 @@ <button v-else v-for="device in filteredDevices" :key="buildDeviceKey(device.type, device.deviceNo)" :key="buildDeviceKey(device.type, device.deviceNo, device.stationId)" type="button" class="dl-device-item" @click="selectDevice(device)"> <div class="dl-device-name"> <span>{{ device.deviceNo }} 号{{ device.typeLabel }}</span> <span>{{ device.deviceNo }} 号{{ device.typeLabel }}<template v-if="device.type === 'Devp' && device.stationId"> · 站点 {{ device.stationId }}</template></span> <span class="dl-device-badge">{{ device.fileCount }} 文件</span> </div> <div class="dl-device-meta"> <span>首条: {{ formatTimestamp(device.firstTime, false) }}</span> <span>末条: {{ formatTimestamp(device.lastTime, false) }}</span> <span v-if="device.type === 'Devp' && device.stationIds && device.stationIds.length">站点数: {{ device.stationIds.length }}</span> <span v-if="device.type === 'Devp' && device.matchedStationId">命中站点: {{ device.matchedStationId }}</span> </div> </button> </div> @@ -1185,10 +1194,11 @@ <div class="dl-viewer-header-main"> <button type="button" class="dl-btn is-ghost" @click="returnToSelector">返回筛选</button> <div class="dl-viewer-copy"> <div class="dl-panel-title">{{ selectedDeviceSummary ? (selectedDeviceSummary.typeLabel + ' ' + selectedDeviceSummary.deviceNo + '号') : '设备状态查看' }}</div> <div class="dl-panel-title">{{ selectedDeviceSummary ? (selectedDeviceSummary.typeLabel + ' ' + selectedDeviceSummary.deviceNo + '号' + (selectedDeviceSummary.type === 'Devp' && selectedDeviceSummary.stationId ? (' · 站点 ' + selectedDeviceSummary.stationId) : '')) : '设备状态查看' }}</div> <div class="dl-panel-desc">{{ selectedDay ? ('日志日期 ' + formatDayText(selectedDay)) : '请选择日期和设备' }}</div> <div v-if="selectedDeviceSummary" class="dl-viewer-meta"> <span class="dl-viewer-meta-item">类型 <strong>{{ selectedDeviceSummary.typeLabel }}</strong></span> <span v-if="selectedDeviceSummary.type === 'Devp' && selectedDeviceSummary.stationId" class="dl-viewer-meta-item">站点 <strong>{{ selectedDeviceSummary.stationId }}</strong></span> <span class="dl-viewer-meta-item">文件 <strong>{{ selectedDeviceSummary.fileCount }}</strong></span> <span class="dl-viewer-meta-item">已载 <strong>{{ loadedSegmentCount }}</strong></span> <span class="dl-viewer-meta-item">范围 <strong>{{ timelineRangeText }}</strong></span>