From c2dd019c1fddaeae412e2b6f44fb86ff0dc635d4 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 02 九月 2026 22:08:16 +0800
Subject: [PATCH] 实现主动轮询完整实时快照

---
 ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java |   74 +++++++++++++++++++++++++++----------
 1 files changed, 54 insertions(+), 20 deletions(-)

diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
index cded5ba..281a531 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
@@ -70,7 +70,7 @@
     {
         this.stateStore = stateStore;
         state = stateStore.loadOrInitialize(defaultState());
-        // 仅清理 Ticket13 之前嵌在主状态中的旧演示历史;独立历史由存储实现保留并继续补采。
+        // 仅清理 Ticket13 之前嵌在主状态中的旧演示历史;独立历史由模拟器存储保留。
         state = stateStore.resetHistoryForDemoStartup(state);
         restoreBtsHistory();
         validateRestoredState();
@@ -228,11 +228,23 @@
     }
 
     /**
-     * 模拟现场人工统一开关机:关机停止采样但保留历史,开机后检测柜通道保持待机。
+     * 模拟现场人工统一开关机:关机停止采样,开机重启检测柜夹具生命周期并保持通道待机。
+     * 采集器历史继续保留,检测柜夹具历史在关机后的开机上升沿清理。
      * 该操作只改变模拟设备事实,不代表系统具备真实设备控制能力。
      */
     public synchronized void setAllDevicesPoweredOn(boolean poweredOn)
     {
+        boolean wasPoweredOff = (!state.devices().isEmpty() || !state.collectors().isEmpty())
+                && state.devices().stream().noneMatch(DeviceState::online)
+                && state.collectors().stream().noneMatch(CollectorState::online);
+        Instant changedAt = Instant.now();
+        if (poweredOn && wasPoweredOff)
+        {
+            // 一键开机代表新一轮演示;清理检测柜夹具,避免序号从 1 重放并与旧生命周期冲突。
+            for (DeviceState device : state.devices())
+                for (ChannelState channel : device.channels())
+                    resetBtsFixtureLifecycle(btsKey(device, channel.channelId()), changedAt);
+        }
         List<DeviceState> devices = state.devices().stream()
                 .map(device -> new DeviceState(device.deviceId(), device.ip(), poweredOn,
                         device.channels().stream()
@@ -240,13 +252,21 @@
                                         DeviceWorkStatus.STOPPED))
                                 .toList(), device.profile()))
                 .toList();
-        Instant changedAt = Instant.now();
         List<CollectorState> collectors = state.collectors().stream()
                 .map(collector -> DevelopmentCollectorSimulation.resize(collector, collector.ip(),
                         collector.channelCount(), poweredOn, changedAt))
                 .toList();
         persist(new SimulatorState(state.version(), devices, collectors, state.nextDeviceId(),
                 state.nextCollectorId(), state.tombstones()));
+    }
+
+    /** 清理检测柜夹具的历史和播放指针;采集器历史由总电源开关独立保留。 */
+    private void resetBtsFixtureLifecycle(String key, Instant now)
+    {
+        btsHistory.remove(key);
+        stateStore.deleteBtsHistory(key);
+        stateStore.deleteBtsPlaybackState(key);
+        savePlayback(key, defaultPlayback(now));
     }
 
     /** 按每台采集器自己的频率推进实时和历史采样。 */
@@ -371,10 +391,7 @@
         DeviceState device = requireDevice(deviceId);
         requireChannel(device, channelId);
         String key = btsKey(device, channelId);
-        btsHistory.remove(key);
-        stateStore.deleteBtsHistory(key);
-        stateStore.deleteBtsPlaybackState(key);
-        savePlayback(key, defaultPlayback(Instant.now()));
+        resetBtsFixtureLifecycle(key, Instant.now());
     }
 
     /** 返回播放状态及当前通道的 record、step、cycle 数据。 */
@@ -396,6 +413,15 @@
         return List.copyOf(btsHistoryFor(btsKey(device, channelId)));
     }
 
+    /** 返回开发模拟器当前通道的持久化槽位间隔;仅供开发调度层使用。 */
+    public synchronized long persistenceIntervalMillis(int deviceId, int channelId)
+    {
+        DeviceState device = requireDevice(deviceId);
+        requireChannel(device, channelId);
+        BtsPlaybackState playback = playbackState(btsKey(device, channelId), Instant.now());
+        return Math.max(100L, 60000L / Math.max(1, playback.speedMultiplier()));
+    }
+
     /** 按实际出现的循环和工步汇总 record,不补齐缺失行。 */
     public synchronized List<BtsStepRecord> btsSteps(int deviceId, int channelId)
     {
@@ -403,7 +429,10 @@
         for (BtsHistoryRecord item : btsRecords(deviceId, channelId))
         {
             String key = item.cycleNo() + ":" + item.stepNo();
-            grouped.computeIfAbsent(key, ignored -> new BtsStepAccumulator(item)).add(item);
+            int position = BtsExcelFixture.records().stream()
+                    .filter(record -> record.cycleNo() == item.cycleNo())
+                    .map(BtsExcelFixture.FixtureRecord::stepNo).distinct().toList().indexOf(item.stepNo()) + 1;
+            grouped.computeIfAbsent(key, ignored -> new BtsStepAccumulator(item, position)).add(item);
         }
         return grouped.values().stream().map(BtsStepAccumulator::finish).toList();
     }
@@ -430,11 +459,15 @@
 
     /** 返回检测柜测试方案事实;方案行来自 Excel 夹具,当前状态来自该通道播放状态。 */
     public synchronized BtsTestSnapshot btsTest(int deviceId, int channelId)
+    { return btsTest(deviceId, channelId, null); }
+
+    /** 返回指定循环的检测柜测试方案;未开始接收的测试固定使用第一个循环作为核对上下文。 */
+    public synchronized BtsTestSnapshot btsTest(int deviceId, int channelId, Integer forcedCycleNo)
     {
         DeviceState device = requireDevice(deviceId);
         requireChannel(device, channelId);
         BtsPlaybackState playback = playbackState(btsKey(device, channelId), Instant.now());
-        int requestedCycleNo = playback.cycleNo();
+        int requestedCycleNo = forcedCycleNo == null ? playback.cycleNo() : forcedCycleNo;
         int currentCycleNo = requestedCycleNo;
         List<BtsExcelFixture.FixtureRecord> currentCycleRecords = BtsExcelFixture.records().stream()
                 .filter(item -> item.cycleNo() == requestedCycleNo).toList();
@@ -450,24 +483,25 @@
             firstByStep.putIfAbsent(item.stepNo(), item);
 
         List<BtsTestPlanRow> rows = new ArrayList<>();
+        int cyclePosition = 1;
         for (BtsExcelFixture.FixtureRecord first : firstByStep.values())
         {
             List<BtsExcelFixture.FixtureRecord> sameCycle = currentCycleRecords.stream()
                     .filter(item -> item.stepNo() == first.stepNo()).toList();
             BtsExcelFixture.FixtureRecord last = sameCycle.get(sameCycle.size() - 1);
             // step 号在每个循环内重复,跨循环时必须按当前循环的工步位置判断,不能把上一循环的同号工步当成当前状态。
-            String status = playback.naturallyFinished() ? "已完成"
+            String status = forcedCycleNo != null ? "待执行" : playback.naturallyFinished() ? "已完成"
                     : playback.stepNo() > first.stepNo() ? "已完成"
                             : playback.stepNo() == first.stepNo() ? "进行中" : "待执行";
-            rows.add(new BtsTestPlanRow("STEP", first.stepNo(), first.stepName(),
+            rows.add(new BtsTestPlanRow("STEP", first.stepNo(), cyclePosition++, first.stepName(),
                     Math.max(0d, last.offsetSeconds() - first.offsetSeconds()), first.voltageV(), status,
                     null, null));
         }
-        rows.add(new BtsTestPlanRow("CYCLE", BtsExcelFixture.startStepNo(), "循环", 0d, null,
-                playback.naturallyFinished() ? "已完成" : "进行中", currentCycleNo,
+        rows.add(new BtsTestPlanRow("CYCLE", BtsExcelFixture.startStepNo(), null, "循环", 0d, null,
+                forcedCycleNo != null ? "待执行" : playback.naturallyFinished() ? "已完成" : "进行中", currentCycleNo,
                 (long) currentCycleRecords.size()));
-        rows.add(new BtsTestPlanRow("END", 0, "结束", 0d, null,
-                playback.naturallyFinished() ? "已完成" : "待执行", null, (long) BtsExcelFixture.recordCount()));
+        rows.add(new BtsTestPlanRow("END", 0, null, "结束", 0d, null,
+                forcedCycleNo != null ? "待执行" : playback.naturallyFinished() ? "已完成" : "待执行", null, (long) BtsExcelFixture.recordCount()));
         return new BtsTestSnapshot(currentCycleNo, playback.stepNo(), BtsExcelFixture.cycleCount(), rows);
     }
 
@@ -891,7 +925,7 @@
         stateStore.saveBtsPlaybackState(key, playback);
     }
 
-    public record BtsStepRecord(int cycleNo, int stepNo, String stepName, String stepType, long recordCount,
+    public record BtsStepRecord(int cycleNo, int stepNo, Integer cyclePosition, String stepName, String stepType, long recordCount,
             Instant startedAt, Instant endedAt, double durationSeconds, double capacityAh, double energyWh,
             double startVoltageV, double endVoltageV, double chargePeakVoltageV, double dischargeValleyVoltageV) { }
 
@@ -899,7 +933,7 @@
             double chargeCapacityAh, double dischargeCapacityAh, double chargeEnergyWh, double dischargeEnergyWh,
             double efficiency) { }
 
-    public record BtsTestPlanRow(String rowType, int stepNo, String stepName, double durationSeconds,
+    public record BtsTestPlanRow(String rowType, int stepNo, Integer cyclePosition, String stepName, double durationSeconds,
             Double voltageV, String executionStatus, Integer cycleCount, Long recordCount) { }
 
     public record BtsTestSnapshot(int currentCycleNo, int currentStepNo, int plannedCycleCount,
@@ -915,12 +949,12 @@
 
     private static final class BtsStepAccumulator
     {
-        private final int cycle; private final int step; private final String name; private final String type;
+        private final int cycle; private final int step; private final int position; private final String name; private final String type;
         private long count; private Instant first; private Instant last; private double duration; private double capacity; private double energy;
         private double startVoltage; private double endVoltage; private double chargePeak = Double.NEGATIVE_INFINITY; private double dischargeValley = Double.POSITIVE_INFINITY;
-        BtsStepAccumulator(BtsHistoryRecord first) { cycle = first.cycleNo(); step = first.stepNo(); name = first.stepName(); type = first.stepType(); }
+        BtsStepAccumulator(BtsHistoryRecord first, int position) { cycle = first.cycleNo(); step = first.stepNo(); this.position = position; name = first.stepName(); type = first.stepType(); }
         void add(BtsHistoryRecord item) { count++; if (first == null) { first = item.deviceTime(); startVoltage = item.voltageV(); } last = item.deviceTime(); endVoltage = item.voltageV(); duration = first == null ? 0 : Math.max(0, java.time.Duration.between(first, last).toMillis() / 1000d); capacity = item.capacityAh(); energy = item.energyWh(); if (item.currentA() >= 0) chargePeak = Math.max(chargePeak, item.voltageV()); else dischargeValley = Math.min(dischargeValley, item.voltageV()); }
-        BtsStepRecord finish() { return new BtsStepRecord(cycle, step, name, type, count, first, last, duration, capacity, energy, startVoltage, endVoltage, chargePeak == Double.NEGATIVE_INFINITY ? 0 : chargePeak, dischargeValley == Double.POSITIVE_INFINITY ? 0 : dischargeValley); }
+        BtsStepRecord finish() { return new BtsStepRecord(cycle, step, position, name, type, count, first, last, duration, capacity, energy, startVoltage, endVoltage, chargePeak == Double.NEGATIVE_INFINITY ? 0 : chargePeak, dischargeValley == Double.POSITIVE_INFINITY ? 0 : dischargeValley); }
     }
 
     private static final class BtsCycleAccumulator

--
Gitblit v1.7.1