yeg
2026-09-02 c6202c6b72613e714a7b32717259b2f8ad532596
按设备时间归属采集器工步观测

让实时查询为采集器样本补充工步上下文,供工步图表读取当前有效实时数据。
4个文件已修改
132 ■■■■■ 已修改文件
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TestDataQueryServiceEffectiveDataTest.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java 105 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
@@ -165,7 +165,7 @@
        return success(consoleDataService.selectOverrides(testId, dataType));
    }
    /** 只查询已归属并持久化到当前测试的检测柜 record。 */
    /** 查询当前测试成功接收的检测柜与采集器实时观测。 */
    @PreAuthorize("@ss.hasPermi('system:test:query')")
    @GetMapping("/{testId}/data/records")
    public AjaxResult persistedRecords(@PathVariable Long testId)
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TestDataQueryServiceEffectiveDataTest.java
@@ -3,6 +3,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -65,6 +66,28 @@
        assertTrue(service.cycles(8L).get(0).containsKey("cycleNo"));
    }
    @Test
    void assignsCollectorObservationToCabinetStepByDeviceTime()
    {
        when(tests.selectTestById(9L)).thenReturn(new TTest());
        Map<String, Object> cabinet = row("sourceType", "CABINET", "batteryNo", "B-01", "cycleNo", 1,
                "stepNo", 2, "stepName", "恒流充电", "deviceTime", Instant.parse("2026-09-02T00:00:00Z"),
                "adapterId", 1L, "devtype", 22, "deviceId", 22L, "subdevid", 0, "channelId", 8L, "sequenceId", 1L);
        Map<String, Object> collector = row("sourceType", "COLLECTOR", "batteryNo", "B-01", "metricCode", "temperature",
                "deviceTime", Instant.parse("2026-09-02T00:00:01Z"), "collectorId", 3L, "channelCode", "S-01", "sequenceId", 2L);
        when(mapper.selectPersistedRecords(9L)).thenReturn(List.of(cabinet));
        when(mapper.selectPersistedCollectorRecords(9L)).thenReturn(List.of(collector));
        when(mapper.selectDataOverrides(9L, "REALTIME")).thenReturn(List.of());
        List<Map<String, Object>> result = new TestDataQueryServiceImpl(mapper, tests).records(9L);
        assertEquals(2, result.size());
        Map<String, Object> assigned = result.stream().filter(item -> "COLLECTOR".equals(item.get("sourceType"))).findFirst().orElseThrow();
        assertEquals("1", assigned.get("cycleNo"));
        assertEquals("2", assigned.get("stepNo"));
        assertEquals("恒流充电", assigned.get("stepName"));
    }
    private static TTestDataOverride override(String identity, String values, boolean deleted)
    {
        TTestDataOverride result = new TTestDataOverride();
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.ZoneOffset;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.HashMap;
@@ -33,6 +40,7 @@
        List<Map<String, Object>> collector = mapper.selectPersistedCollectorRecords(testId);
        if (cabinet != null) source.addAll(cabinet);
        if (collector != null) source.addAll(collector);
        attachCollectorStepContext(source);
        return applyEffectiveData(testId, "REALTIME", source);
    }
@@ -116,6 +124,103 @@
        return "cycle:" + channelIdentity + ":" + cycleNo;
    }
    /** 按检测柜实际工步的设备时间边界给采集器样本补充筛选上下文,不配对采样点。 */
    private void attachCollectorStepContext(List<Map<String, Object>> rows)
    {
        List<StepWindow> windows = new ArrayList<>();
        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;
        }
        for (StepWindowBuilder builder : builders.values()) windows.add(builder.build());
        windows.sort(Comparator.comparing(StepWindow::start));
        for (int index = 0; index < windows.size(); index++)
        {
            StepWindow current = windows.get(index);
            Instant nextStart = windows.stream()
                    .filter(window -> window.batteryNo().equals(current.batteryNo()) && window.start().isAfter(current.start()))
                    .map(StepWindow::start).min(Comparator.naturalOrder()).orElse(null);
            windows.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;
            StepWindow match = windows.stream()
                    .filter(window -> text(row, "batteryNo").equals(window.batteryNo()))
                    .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).toInstant(ZoneOffset.UTC); }
        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);
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -321,7 +321,7 @@
    <select id="selectPersistedRecords" resultType="map">
        select sample.sample_id id,
               concat('RD-', channel.devid, '-', channel.chlid, '-', lpad(sample.seq_id, 6, '0')) recordNo,
               battery.battery_no batteryNo, sample.cycle_no cycleNo, sample.step_no stepNo,
               'CABINET' sourceType, battery.battery_no batteryNo, sample.cycle_no cycleNo, sample.step_no stepNo,
               coalesce(sample.step_name, sample.step_type) stepName, sample.step_type stepType, sample.seq_id sequenceId,
               sample.device_time_utc recordedAt, sample.device_time_utc absoluteTime,
               sample.device_time_utc deviceTime, sample.received_at_utc serverReceivedAt,