yeg
2026-09-02 281c6afae7fbbb59a8932ae01db38eda38487fdc
修复测试实时快照字段

输出检测柜核心采样字段和单位,限制未开始接收的快照缓存,补充实时字段与暂停边界测试。
2个文件已修改
120 ■■■■■ 已修改文件
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestLiveServiceImplTest.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestLiveServiceImplTest.java
@@ -1,10 +1,15 @@
package com.ruoyi.system.service.impl;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.Duration;
import java.time.Instant;
import java.util.List;
@@ -14,12 +19,90 @@
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore;
import com.ruoyi.device.model.ChannelIdentity;
import com.ruoyi.device.model.DeviceWorkStatus;
import com.ruoyi.device.model.RealtimeSnapshot;
import com.ruoyi.system.domain.TTest;
import com.ruoyi.system.domain.TTestBattery;
import com.ruoyi.system.domain.TTestChannelBinding;
import com.ruoyi.system.service.ITTestService;
import com.ruoyi.system.service.ITestLiveService.LiveState;
/** 实时流客户端断连时应结束连接而不触发全局异常处理。 */
class TTestLiveServiceImplTest
{
    @Test
    void notStartedShouldNotExposePreTestDeviceSnapshot()
    {
        Instant now = Instant.parse("2026-09-02T06:00:00Z");
        TTest test = test("NOT_STARTED");
        TTestLiveServiceImpl service = liveService(test, now);
        LiveState state = service.current(10L);
        assertTrue(state.sources().stream().allMatch(source -> source.latestSnapshot().isEmpty()));
    }
    @Test
    void receivingShouldExposeBtsCoreMeasurements()
    {
        Instant now = Instant.parse("2026-09-02T06:00:00Z");
        TTest test = test("RECEIVING");
        TTestLiveServiceImpl service = liveService(test, now);
        LiveState state = service.current(10L);
        var snapshot = state.sources().stream().filter(source -> "CABINET_CHANNEL".equals(source.sourceType()))
                .findFirst().orElseThrow().latestSnapshot();
        assertEquals(new BigDecimal("3.700"), snapshot.get("voltageV"));
        assertEquals(new BigDecimal("1.200"), snapshot.get("currentA"));
        assertEquals(new BigDecimal("2.500"), snapshot.get("capacityAh"));
    }
    private static TTest test(String receptionStatus)
    {
        TTest test = new TTest();
        test.setTestId(10L);
        test.setTestStatus("RECEIVING".equals(receptionStatus) ? "RUNNING" : "WAITING");
        test.setReceptionStatus(receptionStatus);
        TTestBattery battery = new TTestBattery();
        battery.setBatteryNo("B-01");
        TTestChannelBinding binding = new TTestChannelBinding();
        binding.setChannelId(1L);
        binding.setAdapterCode("dev-sim");
        binding.setDevtype(1);
        binding.setDevid(1);
        binding.setSubdevid(0);
        binding.setChlid(1);
        battery.setBinding(binding);
        test.setBatteries(List.of(battery));
        return test;
    }
    private static TTestLiveServiceImpl liveService(TTest test, Instant now)
    {
        DeviceDiscoveryCandidateStore candidates = mock(DeviceDiscoveryCandidateStore.class);
        BtsDevelopmentSimulator simulator = mock(BtsDevelopmentSimulator.class);
        ITTestService tests = mock(ITTestService.class);
        when(tests.selectTestById(10L)).thenReturn(test);
        ChannelIdentity identity = new ChannelIdentity("dev-sim", 1, 1, 0, 1);
        RealtimeSnapshot snapshot = new RealtimeSnapshot(identity, DeviceWorkStatus.WORKING, 1, 1, "cc", null,
                new BigDecimal("1.200"), new BigDecimal("3.700"), new BigDecimal("2.500"),
                new BigDecimal("9.250"), 12L, 12L, "0", now,
                java.util.Map.of());
        DeviceDiscoveryCandidateStore.ChannelCandidate channel = new DeviceDiscoveryCandidateStore.ChannelCandidate(
                identity, true, DeviceWorkStatus.WORKING, now, snapshot);
        DeviceDiscoveryCandidateStore.CabinetCandidate cabinet = new DeviceDiscoveryCandidateStore.CabinetCandidate(
                "dev-sim", 1L, "dev-sim", 1, 1, "127.0.0.2", "mac", "cabinet", "vendor", "model",
                List.of(channel), now, now.plusSeconds(60));
        when(candidates.snapshot(any(Instant.class))).thenReturn(
                new DeviceDiscoveryCandidateStore.CandidateSnapshot(List.of(cabinet), List.of()));
        var plan = new BtsDevelopmentSimulator.BtsTestSnapshot(1, 1, 1, List.of());
        when(simulator.btsTest(anyInt(), anyInt())).thenReturn(plan);
        when(simulator.btsTest(anyInt(), anyInt(), anyInt())).thenReturn(plan);
        return new CurrentLiveService(tests, candidates, simulator);
    }
    @Test
    void initialWriteFailureShouldCompleteStreamWithoutErrorDispatch()
    {
@@ -82,6 +165,15 @@
        }
    }
    private static final class CurrentLiveService extends TTestLiveServiceImpl
    {
        private CurrentLiveService(ITTestService tests, DeviceDiscoveryCandidateStore candidates,
                BtsDevelopmentSimulator simulator)
        {
            super(tests, candidates, simulator);
        }
    }
    private static final class RecordingEmitter extends SseEmitter
    {
        private final int successfulWrites;
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestLiveServiceImpl.java
@@ -48,14 +48,15 @@
        TTest test = tests.selectTestById(testId);
        List<CabinetCandidate> cabinets = candidates.snapshot(Instant.now()).cabinets();
        List<LiveSource> sources = new ArrayList<>();
        boolean receiving = "RECEIVING".equals(test.getReceptionStatus());
        boolean freezeSnapshots = "PAUSED".equals(test.getReceptionStatus())
                || "ENDED".equals(test.getReceptionStatus());
        for (TTestBattery battery : test.getBatteries() == null ? List.<TTestBattery>of() : test.getBatteries())
        {
            if (battery.getBinding() != null)
                sources.add(snapshotFor(testId, cabinetSource(battery, cabinets), freezeSnapshots));
                sources.add(snapshotFor(testId, cabinetSource(battery, cabinets), receiving, freezeSnapshots));
            if (battery.getCollectorId() != null)
                sources.add(snapshotFor(testId, collectorSource(battery), freezeSnapshots));
                sources.add(snapshotFor(testId, collectorSource(battery), receiving, freezeSnapshots));
        }
        List<Map<String, Object>> exceptions = new ArrayList<>();
        exceptions.addAll(sources.stream().filter(source -> !"COLLECTOR".equals(source.sourceType())
@@ -93,12 +94,18 @@
        return null;
    }
    private LiveSource snapshotFor(Long testId, LiveSource fresh, boolean freeze)
    private LiveSource snapshotFor(Long testId, LiveSource fresh, boolean receiving, boolean freeze)
    {
        String key = testId + ":" + fresh.sourceType() + ":" + fresh.sourceId() + ":" + fresh.batteryNo();
        if (freeze) return lastSources.getOrDefault(key, fresh);
        if (!receiving)
            return freeze ? lastSources.getOrDefault(key, withoutSnapshot(fresh)) : withoutSnapshot(fresh);
        lastSources.put(key, fresh);
        return fresh;
    }
    private LiveSource withoutSnapshot(LiveSource source)
    {
        return new LiveSource(source.sourceType(), source.sourceId(), source.batteryNo(), source.deviceStatus(), Map.of());
    }
    @Override public SseEmitter stream(Long testId)
@@ -163,6 +170,11 @@
        if (channel.latestSnapshot() == null) return value;
        value.put("source", "检测柜");
        value.put("observedAt", channel.latestSnapshot().receivedAt());
        // BTS 核心采样字段是快照的一等事实,不能只依赖动态 measurements 展示。
        value.put("currentA", channel.latestSnapshot().currentA());
        value.put("voltageV", channel.latestSnapshot().voltageV());
        value.put("capacityAh", channel.latestSnapshot().capacityAh());
        value.put("energyWh", channel.latestSnapshot().energyWh());
        // 实时方案需要设备当前循环和实际工步;这些字段只用于快照展示,不形成历史采集事实。
        value.put("cycleNo", channel.latestSnapshot().cycleNo());
        value.put("stepNo", channel.latestSnapshot().stepNo());
@@ -172,7 +184,13 @@
            Measurement measurement = entry.getValue();
            return Map.of("metricCode", entry.getKey(), "value", measurement.value(), "unit", measurement.unit());
        }).toList());
        value.put("units", channel.latestSnapshot().measurements().values().stream().map(Measurement::unit).distinct().toList());
        List<String> units = new ArrayList<>();
        if (channel.latestSnapshot().currentA() != null) units.add("A");
        if (channel.latestSnapshot().voltageV() != null) units.add("V");
        if (channel.latestSnapshot().capacityAh() != null) units.add("Ah");
        if (channel.latestSnapshot().energyWh() != null) units.add("Wh");
        units.addAll(channel.latestSnapshot().measurements().values().stream().map(Measurement::unit).toList());
        value.put("units", units.stream().filter(item -> item != null && !item.isBlank()).distinct().toList());
        return value;
    }