package com.ruoyi.system.service.impl; import java.util.List; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.time.Instant; import java.time.OffsetDateTime; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.LinkedHashMap; import java.util.Map; import java.util.HashMap; import com.alibaba.fastjson2.JSON; import org.springframework.stereotype.Service; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.system.domain.TTestDataOverride; import com.ruoyi.system.mapper.TTestMapper; import com.ruoyi.system.service.ITTestService; import com.ruoyi.system.service.ITestDataQueryService; /** 测试数据查询模块;实现不访问设备或模拟器历史。 */ @Service public class TestDataQueryServiceImpl implements ITestDataQueryService { private final TTestMapper mapper; private final ITTestService tests; public TestDataQueryServiceImpl(TTestMapper mapper, ITTestService tests) { this.mapper = mapper; this.tests = tests; } /** 查询测试当前有效的检测柜与采集器实时观测,并补充可筛选的工步上下文。 */ @Override public List> records(Long testId) { requireTest(testId); List> source = new java.util.ArrayList<>(); List> snapshots = mapper.selectPersistedFinalRealtimeRecords(testId); if (snapshots != null && !snapshots.isEmpty()) source.addAll(snapshots); else { // 迁移完成前兼容旧数据;新采集只从统一快照读取。 List> cabinet = mapper.selectPersistedRecords(testId); List> collector = mapper.selectPersistedCollectorRecords(testId); if (cabinet != null) source.addAll(cabinet); if (collector != null) source.addAll(collector); } List> effective = applyEffectiveData(testId, "REALTIME", source); attachCollectorStepContext(effective); return normalizeApiTimes(effective); } @Override public List> steps(Long testId) { requireTest(testId); List> source = mapper.selectPersistedFinalStepRecords(testId); if (source == null || source.isEmpty()) { List> legacy = mapper.selectPersistedSteps(testId); if (legacy != null && !legacy.isEmpty()) source = legacy; } return normalizeApiTimes(applyEffectiveData(testId, "STEP", source)); } @Override public List> cycles(Long testId) { requireTest(testId); List> source = mapper.selectPersistedFinalCycleRecords(testId); if (source == null || source.isEmpty()) { List> legacy = mapper.selectPersistedCycles(testId); if (legacy != null && !legacy.isEmpty()) source = legacy; } return normalizeApiTimes(applyEffectiveData(testId, "CYCLE", source)); } /** 自研业务接口统一以 Unix epoch 毫秒返回时间,避免前端依赖数据库日期格式。 */ private List> normalizeApiTimes(List> rows) { if (rows == null) return List.of(); for (Map row : rows) for (String key : List.of("deviceTime", "recordedAt", "absoluteTime", "serverReceivedAt")) { Object value = row.get(key); if (value instanceof Date date) row.put(key, date.getTime()); else if (value instanceof Instant instant) row.put(key, instant.toEpochMilli()); } return rows; } /** 将最新人工覆盖应用到设备事实,删除只影响当前有效视图,不抹除原始采样。 */ private List> applyEffectiveData(Long testId, String dataType, List> source) { List> effective = new java.util.ArrayList<>( source == null ? List.>of() : source); List> imports = mapper.selectConsoleImportRecords(testId, dataType); for (Map imported : imports == null ? List.>of() : imports) { try { @SuppressWarnings("unchecked") Map row = (Map) (Map) JSON.parseObject( String.valueOf(imported.get("rowValues")), Map.class); row.put("stableIdentity", imported.get("stableIdentity")); effective.add(row); } catch (RuntimeException ignored) { } } List overrides = mapper.selectDataOverrides(testId, dataType); if (effective.isEmpty() || overrides == null || overrides.isEmpty()) return effective; Map byIdentity = new HashMap<>(); for (TTestDataOverride override : overrides) if (override != null && override.getStableIdentity() != null) byIdentity.put(override.getStableIdentity(), override); List> filtered = new java.util.ArrayList<>(effective.size()); for (Map row : effective) { String identity = row.get("stableIdentity") == null ? stableIdentity(dataType, row) : String.valueOf(row.get("stableIdentity")); TTestDataOverride override = byIdentity.get(identity); if (override != null && Boolean.TRUE.equals(override.getDeleted())) continue; Map copy = new LinkedHashMap<>(row); if (override != null && override.getFieldValues() != null && !override.getFieldValues().isBlank()) { try { Map values = JSON.parseObject(override.getFieldValues(), Map.class); if (values != null) copy.putAll(values); } catch (RuntimeException ignored) { // 覆盖已由修订服务校验;解析异常时保留原始事实,避免查询整体失败。 } } filtered.add(copy); } return filtered; } private String stableIdentity(String dataType, Map row) { if ("REALTIME".equals(dataType) && "SNAPSHOT".equals(String.valueOf(row.get("sourceType")))) return String.valueOf(row.getOrDefault("stableIdentity", "snapshot:" + value(row, "snapshotId"))); if ("REALTIME".equals(dataType) && "COLLECTOR".equals(String.valueOf(row.get("sourceType")))) return String.join(":", "collector", identityPart(row, "collectorId"), identityPart(row, "channelCode"), value(row, "sequenceId"), identityPart(row, "metricCode")); String adapterId = identityPart(row, "adapterId"); String devtype = identityPart(row, "devtype"); String deviceId = identityPart(row, "deviceId"); String subdevid = identityPart(row, "subdevid"); String channelId = identityPart(row, "channelId"); String cycleNo = value(row, "cycleNo"); String channelIdentity = String.join(":", adapterId, devtype, deviceId, subdevid, channelId); if ("REALTIME".equals(dataType)) return channelIdentity + ":" + value(row, "sequenceId"); if ("STEP".equals(dataType)) return "step:" + channelIdentity + ":" + cycleNo + ":" + value(row, "stepNo"); return "cycle:" + channelIdentity + ":" + cycleNo; } /** 按检测柜实际工步的设备时间边界给采集器样本补充筛选上下文,不配对采样点。 */ private void attachCollectorStepContext(List> rows) { Map builders = new LinkedHashMap<>(); for (Map row : rows) { if ("COLLECTOR".equals(String.valueOf(row.get("sourceType")))) continue; Instant time = toInstant(row.get("deviceTime")); if (time == null || row.get("stepNo") == null) continue; String key = String.join("|", text(row, "batteryNo"), text(row, "cycleNo"), text(row, "stepNo")); StepWindowBuilder builder = builders.computeIfAbsent(key, ignored -> new StepWindowBuilder(row, time)); builder.start = builder.start.isAfter(time) ? time : builder.start; builder.end = builder.end == null || builder.end.isBefore(time) ? time : builder.end; } Map> windowsByBattery = new LinkedHashMap<>(); for (StepWindowBuilder builder : builders.values()) windowsByBattery.computeIfAbsent(builder.batteryNo, ignored -> new ArrayList<>()).add(builder.build()); for (List batteryWindows : windowsByBattery.values()) { batteryWindows.sort(Comparator.comparing(StepWindow::start)); for (int index = 0; index < batteryWindows.size(); index++) { StepWindow current = batteryWindows.get(index); Instant nextStart = index + 1 < batteryWindows.size() ? batteryWindows.get(index + 1).start() : null; batteryWindows.set(index, current.withNextStart(nextStart)); } } for (Map row : rows) { if (!"COLLECTOR".equals(String.valueOf(row.get("sourceType")))) continue; Instant time = toInstant(row.get("deviceTime")); if (time == null) continue; List batteryWindows = windowsByBattery.getOrDefault(text(row, "batteryNo"), List.of()); StepWindow match = batteryWindows.stream() .filter(window -> !time.isBefore(window.start()) && (window.nextStart() == null || time.isBefore(window.nextStart()))) .findFirst().orElse(null); if (match == null) continue; row.put("cycleNo", match.cycleNo()); row.put("stepNo", match.stepNo()); row.put("stepName", match.stepName()); row.put("actualStepLabel", match.stepName()); } } private String text(Map row, String key) { Object value = row.get(key); return value == null ? "" : String.valueOf(value); } private Instant toInstant(Object value) { if (value instanceof Instant instant) return instant; if (value instanceof Date date) return date.toInstant(); if (value == null) return null; String text = String.valueOf(value); try { return Instant.parse(text); } catch (RuntimeException ignored) { } try { return OffsetDateTime.parse(text).toInstant(); } catch (RuntimeException ignored) { } try { return LocalDateTime.parse(text).atZone(ZoneId.of("Asia/Hong_Kong")).toInstant(); } catch (RuntimeException ignored) { return null; } } private static final class StepWindowBuilder { private final String batteryNo; private final String cycleNo; private final String stepNo; private final String stepName; private Instant start; private Instant end; private StepWindowBuilder(Map row, Instant time) { batteryNo = row.get("batteryNo") == null ? "" : String.valueOf(row.get("batteryNo")); cycleNo = row.get("cycleNo") == null ? "" : String.valueOf(row.get("cycleNo")); stepNo = row.get("stepNo") == null ? "" : String.valueOf(row.get("stepNo")); stepName = row.get("stepName") == null ? "-" : String.valueOf(row.get("stepName")); start = time; end = time; } private StepWindow build() { return new StepWindow(batteryNo, cycleNo, stepNo, stepName, start, end, null); } } private record StepWindow(String batteryNo, String cycleNo, String stepNo, String stepName, Instant start, Instant end, Instant nextStart) { private StepWindow withNextStart(Instant value) { return new StepWindow(batteryNo, cycleNo, stepNo, stepName, start, end, value); } } private String identityPart(Map row, String key) { Object value = row == null ? null : row.get(key); return value == null ? "" : String.valueOf(value); } private String value(Map row, String key) { return identityPart(row, key); } private void requireTest(Long testId) { if (testId == null || tests.selectTestById(testId) == null) throw new ServiceException("测试不存在"); } }