yeg
2026-09-05 3a8c49426eb95f50bc3223098e9c4a1ddea90b06
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java
@@ -1,6 +1,13 @@
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;
@@ -25,30 +32,62 @@
        this.tests = tests;
    }
    /** 查询测试当前有效的检测柜与采集器实时观测,并补充可筛选的工步上下文。 */
    @Override public List<Map<String, Object>> records(Long testId)
    {
        requireTest(testId);
        List<Map<String, Object>> source = new java.util.ArrayList<>();
        List<Map<String, Object>> cabinet = mapper.selectPersistedRecords(testId);
        List<Map<String, Object>> collector = mapper.selectPersistedCollectorRecords(testId);
        if (cabinet != null) source.addAll(cabinet);
        if (collector != null) source.addAll(collector);
        return applyEffectiveData(testId, "REALTIME", source);
        List<Map<String, Object>> snapshots = mapper.selectPersistedFinalRealtimeRecords(testId);
        if (snapshots != null && !snapshots.isEmpty()) source.addAll(snapshots);
        else {
            // 迁移完成前兼容旧数据;新采集只从统一快照读取。
            List<Map<String, Object>> cabinet = mapper.selectPersistedRecords(testId);
            List<Map<String, Object>> collector = mapper.selectPersistedCollectorRecords(testId);
            if (cabinet != null) source.addAll(cabinet);
            if (collector != null) source.addAll(collector);
        }
        List<Map<String, Object>> effective = applyEffectiveData(testId, "REALTIME", source);
        attachCollectorStepContext(effective);
        return normalizeApiTimes(effective);
    }
    @Override public List<Map<String, Object>> steps(Long testId)
    {
        requireTest(testId);
        return applyEffectiveData(testId, "STEP", mapper.selectPersistedSteps(testId));
        List<Map<String, Object>> source = mapper.selectPersistedFinalStepRecords(testId);
        if (source == null || source.isEmpty()) {
            List<Map<String, Object>> legacy = mapper.selectPersistedSteps(testId);
            if (legacy != null && !legacy.isEmpty()) source = legacy;
        }
        return normalizeApiTimes(applyEffectiveData(testId, "STEP", source));
    }
    @Override public List<Map<String, Object>> cycles(Long testId)
    {
        requireTest(testId);
        return applyEffectiveData(testId, "CYCLE", mapper.selectPersistedCycles(testId));
        List<Map<String, Object>> source = mapper.selectPersistedFinalCycleRecords(testId);
        if (source == null || source.isEmpty()) {
            List<Map<String, Object>> legacy = mapper.selectPersistedCycles(testId);
            if (legacy != null && !legacy.isEmpty()) source = legacy;
        }
        return normalizeApiTimes(applyEffectiveData(testId, "CYCLE", source));
    }
    /** 将最新人工覆盖应用到设备事实,删除只影响当前有效视图,不抹除原始采样。 */
    /** 自研业务接口统一以 Unix epoch 毫秒返回时间,避免前端依赖数据库日期格式。 */
    private List<Map<String, Object>> normalizeApiTimes(List<Map<String, Object>> rows)
    {
        if (rows == null) return List.of();
        for (Map<String, Object> row : rows)
            for (String key : List.of("deviceTime", "collectorDeviceTime", "captureSlot", "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<Map<String, Object>> applyEffectiveData(Long testId, String dataType,
            List<Map<String, Object>> source)
    {
@@ -101,6 +140,8 @@
    private String stableIdentity(String dataType, Map<String, Object> 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"));
@@ -116,6 +157,105 @@
        return "cycle:" + channelIdentity + ":" + cycleNo;
    }
    /** 按检测柜实际工步的设备时间边界给采集器样本补充筛选上下文,不配对采样点。 */
    private void attachCollectorStepContext(List<Map<String, Object>> rows)
    {
        Map<String, StepWindowBuilder> builders = new LinkedHashMap<>();
        for (Map<String, Object> 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<String, List<StepWindow>> windowsByBattery = new LinkedHashMap<>();
        for (StepWindowBuilder builder : builders.values())
            windowsByBattery.computeIfAbsent(builder.batteryNo, ignored -> new ArrayList<>()).add(builder.build());
        for (List<StepWindow> 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<String, Object> row : rows)
        {
            if (!"COLLECTOR".equals(String.valueOf(row.get("sourceType")))) continue;
            Instant time = toInstant(row.get("deviceTime"));
            if (time == null) continue;
            List<StepWindow> 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<String, Object> 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<String, Object> 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<String, Object> row, String key)
    {
        Object value = row == null ? null : row.get(key);