yeg
2026-09-01 d61c013599a1a4e09cadfe80cdf872f263737c9c
修正检测柜测试方案实时事实

让实时控制台从模拟器和后端状态读取检测柜测试方案,补充循环与结束统计行,并覆盖无采样记录时的方案快照回归。
4个文件已修改
100 ■■■■■ 已修改文件
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsDevelopmentSimulatorRecordStepCycleTest.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
@@ -428,6 +428,40 @@
        }).toList();
    }
    /** 返回检测柜测试方案事实;方案行来自 Excel 夹具,当前状态来自该通道播放状态。 */
    public synchronized BtsTestSnapshot btsTest(int deviceId, int channelId)
    {
        DeviceState device = requireDevice(deviceId);
        requireChannel(device, channelId);
        BtsPlaybackState playback = playbackState(btsKey(device, channelId), Instant.now());
        java.util.Map<Integer, BtsExcelFixture.FixtureRecord> firstByStep = new java.util.LinkedHashMap<>();
        for (BtsExcelFixture.FixtureRecord item : BtsExcelFixture.records())
            firstByStep.putIfAbsent(item.stepNo(), item);
        List<BtsTestPlanRow> rows = new ArrayList<>();
        for (BtsExcelFixture.FixtureRecord first : firstByStep.values())
        {
            List<BtsExcelFixture.FixtureRecord> sameCycle = BtsExcelFixture.records().stream()
                    .filter(item -> item.cycleNo() == first.cycleNo() && item.stepNo() == first.stepNo()).toList();
            BtsExcelFixture.FixtureRecord last = sameCycle.get(sameCycle.size() - 1);
            String status = playback.naturallyFinished() || playback.cycleNo() > first.cycleNo()
                    || (playback.cycleNo() == first.cycleNo() && playback.stepNo() > first.stepNo())
                            ? "已完成"
                            : playback.cycleNo() == first.cycleNo() && playback.stepNo() == first.stepNo()
                                    ? "进行中" : "待执行";
            rows.add(new BtsTestPlanRow("STEP", first.stepNo(), first.stepName(),
                    Math.max(0d, last.offsetSeconds() - first.offsetSeconds()), first.voltageV(), status,
                    null, null));
        }
        int firstStepNo = firstByStep.values().stream().findFirst().map(BtsExcelFixture.FixtureRecord::stepNo).orElse(0);
        rows.add(new BtsTestPlanRow("CYCLE", firstStepNo, "循环", 0d, null,
                playback.naturallyFinished() ? "已完成" : "待执行", BtsExcelFixture.cycleCount(),
                (long) BtsExcelFixture.recordCount()));
        rows.add(new BtsTestPlanRow("END", 0, "结束", 0d, null,
                playback.naturallyFinished() ? "已完成" : "待执行", null, (long) BtsExcelFixture.recordCount()));
        return new BtsTestSnapshot(playback.cycleNo(), playback.stepNo(), BtsExcelFixture.cycleCount(), rows);
    }
    /** 返回采集器最新实时快照;快照与分页历史保持不同读取路径。 */
    public synchronized CollectorRealtimeSnapshot collectorRealtime(int collectorId)
    {
@@ -856,6 +890,12 @@
            double chargeCapacityAh, double dischargeCapacityAh, double chargeEnergyWh, double dischargeEnergyWh,
            double efficiency) { }
    public record BtsTestPlanRow(String rowType, int stepNo, String stepName, double durationSeconds,
            Double voltageV, String executionStatus, Integer cycleCount, Long recordCount) { }
    public record BtsTestSnapshot(int currentCycleNo, int currentStepNo, int plannedCycleCount,
            List<BtsTestPlanRow> rows) { }
    private static double efficiency(double chargeCapacityAh, double dischargeCapacityAh)
    {
        return chargeCapacityAh > 0d ? dischargeCapacityAh / chargeCapacityAh * 100d : 0d;
ruoyi-device/src/test/java/com/ruoyi/device/bts/BtsDevelopmentSimulatorRecordStepCycleTest.java
@@ -38,6 +38,21 @@
    }
    @Test
    void testSnapshotExposesPlanRowsEvenBeforeRecordsAreProduced() throws Exception
    {
        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0,
                new InMemoryBtsSimulatorStateStore()))
        {
            var snapshot = simulator.btsTest(1, 1);
            assertTrue(snapshot.plannedCycleCount() > 0);
            assertTrue(snapshot.rows().stream().anyMatch(row -> "STEP".equals(row.rowType())));
            assertTrue(snapshot.rows().stream().anyMatch(row -> "CYCLE".equals(row.rowType())));
            assertTrue(snapshot.rows().stream().anyMatch(row -> "END".equals(row.rowType())));
            assertEquals(0, simulator.btsRecords(1, 1).size());
        }
    }
    @Test
    void playbackSpeedPauseResumeAndRestartAreIndependentOfDeviceStatus() throws Exception
    {
        Instant start = Instant.parse("2026-08-31T00:00:00Z");
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestLiveService.java
@@ -16,12 +16,21 @@
    SseEmitter stream(Long testId);
    record LiveState(Long testId, String testStatus, String receptionStatus, Instant updatedAt,
            List<LiveSource> sources, List<Map<String, Object>> exceptions,
            List<TTestIntegrityIssue> integrityIssues)
            List<TTestIntegrityIssue> integrityIssues, TestPlanSnapshot testPlan)
    {
        public LiveState(Long testId, String testStatus, String receptionStatus, Instant updatedAt,
                List<LiveSource> sources, List<Map<String, Object>> exceptions)
        { this(testId, testStatus, receptionStatus, updatedAt, sources, exceptions, List.of()); }
        { this(testId, testStatus, receptionStatus, updatedAt, sources, exceptions, List.of(), null); }
        public LiveState(Long testId, String testStatus, String receptionStatus, Instant updatedAt,
                List<LiveSource> sources, List<Map<String, Object>> exceptions,
                List<TTestIntegrityIssue> integrityIssues)
        { this(testId, testStatus, receptionStatus, updatedAt, sources, exceptions, integrityIssues, null); }
    }
    record TestPlanSnapshot(int currentCycleNo, int currentStepNo, int plannedCycleCount,
            List<TestPlanRow> rows) { }
    record TestPlanRow(String rowType, int stepNo, String stepName, double durationSeconds,
            Double voltageV, String executionStatus, Integer cycleCount, Long recordCount) { }
    record LiveSource(String sourceType, Long sourceId, String batteryNo, String deviceStatus,
            Map<String, Object> latestSnapshot) { }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java
@@ -27,6 +27,8 @@
import com.ruoyi.system.domain.TTestCollectorExceptionInterval;
import com.ruoyi.system.mapper.TTestMapper;
import com.ruoyi.system.service.ITestLiveService;
import com.ruoyi.system.service.ITestLiveService.TestPlanRow;
import com.ruoyi.system.service.ITestLiveService.TestPlanSnapshot;
import com.ruoyi.system.service.ITTestService;
/** 将设备最新快照转换为测试视角,并托管 SSE 连接生命周期。 */
@@ -90,8 +92,31 @@
        if (persistedIntegrity != null) integrity.addAll(persistedIntegrity);
        List<com.ruoyi.system.domain.TTestIntegrityIssue> persistedExceptions = testMapper.selectTestExceptionFacts(testId);
        if (persistedExceptions != null) integrity.addAll(persistedExceptions);
        TestPlanSnapshot testPlan = detectorTestPlan(test);
        return new LiveState(testId, testStatus, test.getReceptionStatus(), Instant.now(), sources, exceptions,
                integrity == null ? List.of() : integrity);
                integrity == null ? List.of() : integrity, testPlan);
    }
    private TestPlanSnapshot detectorTestPlan(TTest test)
    {
        if (test.getBatteries() == null) return null;
        for (TTestBattery battery : test.getBatteries())
        {
            if (battery.getBinding() == null || battery.getBinding().getDevid() == null
                    || battery.getBinding().getChlid() == null) continue;
            try
            {
                var source = simulator.btsTest(battery.getBinding().getDevid(), battery.getBinding().getChlid());
                List<TestPlanRow> rows = source.rows().stream()
                        .map(row -> new TestPlanRow(row.rowType(), row.stepNo(), row.stepName(), row.durationSeconds(),
                                row.voltageV(), row.executionStatus(), row.cycleCount(), row.recordCount()))
                        .toList();
                return new TestPlanSnapshot(source.currentCycleNo(), source.currentStepNo(),
                        source.plannedCycleCount(), rows);
            }
            catch (IllegalArgumentException ignored) { /* 登记设备尚未被开发模拟器提供时保持无方案事实。 */ }
        }
        return null;
    }
    private LiveSource snapshotFor(Long testId, LiveSource fresh, boolean freeze)
@@ -165,6 +190,11 @@
        if (channel.latestSnapshot() == null) return value;
        value.put("source", "检测柜");
        value.put("observedAt", channel.latestSnapshot().receivedAt());
        // 实时方案需要设备当前循环和实际工步;这些字段只用于快照展示,不形成历史采集事实。
        value.put("cycleNo", channel.latestSnapshot().cycleNo());
        value.put("stepNo", channel.latestSnapshot().stepNo());
        value.put("stepType", channel.latestSnapshot().stepType());
        value.put("totalTimeSeconds", channel.latestSnapshot().totalTimeSeconds());
        value.put("measurements", channel.latestSnapshot().measurements().entrySet().stream().map(entry -> {
            Measurement measurement = entry.getValue();
            return Map.of("metricCode", entry.getKey(), "value", measurement.value(), "unit", measurement.unit());