修正检测柜测试方案实时事实
让实时控制台从模拟器和后端状态读取检测柜测试方案,补充循环与结束统计行,并覆盖无采样记录时的方案快照回归。
| | |
| | | }).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) |
| | | { |
| | |
| | | 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; |
| | |
| | | } |
| | | |
| | | @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"); |
| | |
| | | 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) { } |
| | | } |
| | |
| | | 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 连接生命周期。 */ |
| | |
| | | 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) |
| | |
| | | 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()); |