From 370ef4f5e663fdfc1d19d2774dfc4ec1ed0af60e Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 29 八月 2026 14:57:48 +0800
Subject: [PATCH] 修复采集器历史状态持续序列化错误

---
 ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java |  122 ++++++++++++++++++++++++++++++++++++++++
 1 files changed, 121 insertions(+), 1 deletions(-)

diff --git a/ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java b/ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java
index 6ccc48d..d9fad0c 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java
@@ -3,17 +3,26 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.Mockito.doAnswer;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.when;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicReference;
 import org.junit.jupiter.api.Test;
 import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ListOperations;
+import org.springframework.data.redis.core.SessionCallback;
+import org.springframework.data.redis.core.SetOperations;
 import org.springframework.data.redis.core.ValueOperations;
 import com.alibaba.fastjson2.JSON;
 import com.alibaba.fastjson2.JSONObject;
 import com.ruoyi.device.model.DeviceWorkStatus;
+import com.ruoyi.device.bts.BtsDevelopmentSimulator;
 import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelState;
 import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelTombstone;
 import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState;
@@ -78,7 +87,65 @@
         assertEquals(1L, collector.nextSequence());
         assertEquals(List.of("S-01", "S-02"),
                 collector.channels().stream().map(channel -> channel.channelCode()).toList());
-        assertEquals(List.of(), collector.history());
+        assertEquals(0L, collector.historyRecordCount());
+    }
+
+    @Test
+    void migratesEmbeddedCollectorHistoryToAppendOnlyRedisList()
+    {
+        AtomicReference<Object> stored = new AtomicReference<>("{\"version\":1,\"devices\":[],"
+                + "\"collectors\":[{\"collectorId\":1,\"deviceCode\":\"dev-sim-collector-1\","
+                + "\"ip\":\"127.0.1.2\",\"mac\":\"02:43:00:00:00:01\","
+                + "\"name\":\"模拟采集器\",\"manufacturer\":\"开发模拟器\","
+                + "\"model\":\"Virtual Collector\",\"channelCount\":1,\"online\":true,"
+                + "\"sampleIntervalMs\":1000,\"nextSequence\":2,"
+                + "\"lastSampledAt\":\"2026-08-29T05:00:00Z\","
+                + "\"channels\":[{\"channelCode\":\"S-01\",\"metrics\":[{"
+                + "\"metricCode\":\"temperature\",\"value\":25.1,\"unit\":\"℃\"}]}],"
+                + "\"history\":[{\"adapterCode\":\"dev-sim\","
+                + "\"collectorCode\":\"dev-sim-collector-1\",\"channelCode\":\"S-01\","
+                + "\"sequenceId\":1,\"deviceTime\":\"2026-08-29T05:00:00Z\","
+                + "\"deviceTimeRaw\":\"2026-08-29 13:00:00.000\","
+                + "\"serverReceivedAt\":\"2026-08-29T05:00:00Z\","
+                + "\"metricCode\":\"temperature\",\"value\":25.1,\"unit\":\"℃\"}],"
+                + "\"historyFaults\":{\"missingSequences\":[],\"outOfOrder\":false}}],"
+                + "\"nextDeviceId\":1,\"nextCollectorId\":2,\"tombstones\":[]}");
+        RedisBtsSimulatorStateStore store = store(stored);
+
+        SimulatorState restored = store.loadOrInitialize(state(1, "127.0.0.2"));
+
+        assertEquals(1L, restored.collectors().get(0).historyRecordCount());
+        assertEquals(1, store.loadCollectorHistory("dev-sim-collector-1").size());
+        assertEquals(true, stored.get().toString().contains("\"history\":[{"));
+        store.completeValidatedRestore(restored);
+        assertEquals(false, stored.get().toString().contains("\"history\":[{"));
+    }
+
+    @Test
+    void doesNotOverwriteDamagedEmbeddedHistoryBeforeValidation()
+    {
+        AtomicReference<Object> stored = new AtomicReference<>("{\"version\":1,\"devices\":[],"
+                + "\"collectors\":[{\"collectorId\":1,\"deviceCode\":\"dev-sim-collector-1\","
+                + "\"ip\":\"127.0.1.2\",\"mac\":\"02:43:00:00:00:01\","
+                + "\"name\":\"模拟采集器\",\"manufacturer\":\"开发模拟器\","
+                + "\"model\":\"Virtual Collector\",\"channelCount\":1,\"online\":true,"
+                + "\"sampleIntervalMs\":1000,\"nextSequence\":2,"
+                + "\"lastSampledAt\":\"2026-08-29T05:00:00Z\","
+                + "\"channels\":[{\"channelCode\":\"S-01\",\"metrics\":[{"
+                + "\"metricCode\":\"temperature\",\"value\":25.1,\"unit\":\"℃\"}]}],"
+                + "\"history\":[{\"adapterCode\":\"dev-sim\","
+                + "\"collectorCode\":\"wrong-collector\",\"channelCode\":\"S-01\","
+                + "\"sequenceId\":1,\"deviceTime\":\"2026-08-29T05:00:00Z\","
+                + "\"deviceTimeRaw\":\"2026-08-29 13:00:00.000\","
+                + "\"serverReceivedAt\":\"2026-08-29T05:00:00Z\","
+                + "\"metricCode\":\"temperature\",\"value\":25.1,\"unit\":\"℃\"}],"
+                + "\"historyFaults\":{\"missingSequences\":[],\"outOfOrder\":false}}],"
+                + "\"nextDeviceId\":1,\"nextCollectorId\":2,\"tombstones\":[]}");
+
+        assertThrows(IllegalStateException.class,
+                () -> new BtsDevelopmentSimulator(0, store(stored)));
+
+        assertEquals(true, stored.get().toString().contains("\"history\":[{"));
     }
 
     @Test
@@ -93,6 +160,29 @@
 
         assertEquals("Unsupported BTS simulator state version: 2", exception.getMessage());
         assertEquals(true, stored.get().toString().contains("\"version\":2"));
+    }
+
+    @Test
+    void atomicallySavesCollectorDeletionAndRemovesIndependentHistory()
+    {
+        AtomicReference<Object> stored = new AtomicReference<>();
+        RedisBtsSimulatorStateStore store = store(stored);
+        SimulatorState current = state(1, "127.0.0.2");
+        store.loadOrInitialize(current);
+        var record = new com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord(
+                "dev-sim", "dev-sim-collector-1", "S-01", 1L,
+                java.time.Instant.parse("2026-08-29T05:00:00Z"), "2026-08-29 13:00:00.000",
+                java.time.Instant.parse("2026-08-29T05:00:00Z"), "temperature",
+                new java.math.BigDecimal("25.1"), "℃");
+        store.save(current, List.of(
+                new com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryAppend(
+                        "dev-sim-collector-1", List.of(record))));
+        SimulatorState next = state(7, "127.0.0.9");
+
+        store.saveAndDeleteCollectorHistory(next, "dev-sim-collector-1");
+
+        assertEquals(next, store.loadOrInitialize(current));
+        assertEquals(List.of(), store.loadCollectorHistory("dev-sim-collector-1"));
     }
 
     @Test
@@ -175,7 +265,37 @@
     {
         RedisTemplate<Object, Object> redis = mock(RedisTemplate.class);
         ValueOperations<Object, Object> values = mock(ValueOperations.class);
+        ListOperations<Object, Object> lists = mock(ListOperations.class);
+        SetOperations<Object, Object> sets = mock(SetOperations.class);
+        Map<Object, List<Object>> listValues = new LinkedHashMap<>();
         when(redis.opsForValue()).thenReturn(values);
+        when(redis.opsForList()).thenReturn(lists);
+        when(redis.opsForSet()).thenReturn(sets);
+        when(sets.add(any(), any())).thenReturn(1L);
+        when(sets.members(any())).thenReturn(Set.of());
+        when(redis.exec()).thenReturn(List.of(Boolean.TRUE));
+        when(redis.execute(any(SessionCallback.class))).thenAnswer(invocation -> {
+            SessionCallback<?> callback = invocation.getArgument(0);
+            return callback.execute(redis);
+        });
+        when(lists.rightPushAll(any(), org.mockito.ArgumentMatchers.<Object>anyCollection()))
+                .thenAnswer(invocation -> {
+                    Object key = invocation.getArgument(0);
+                    List<Object> records = new ArrayList<>(listValues.getOrDefault(key, List.of()));
+                    FastJson2JsonRedisSerializer<Object> serializer =
+                            new FastJson2JsonRedisSerializer<>(Object.class);
+                    List<Object> appended = invocation.getArgument(1);
+                    appended.forEach(item -> records.add(
+                            serializer.deserialize(serializer.serialize(item))));
+                    listValues.put(key, records);
+                    return (long) records.size();
+                });
+        when(lists.range(any(), anyLong(), anyLong())).thenAnswer(invocation ->
+                List.copyOf(listValues.getOrDefault(invocation.getArgument(0), List.of())));
+        when(lists.size(any())).thenAnswer(invocation ->
+                (long) listValues.getOrDefault(invocation.getArgument(0), List.of()).size());
+        when(redis.delete(org.mockito.ArgumentMatchers.<Object>any()))
+                .thenAnswer(invocation -> listValues.remove(invocation.getArgument(0)) != null);
         when(values.get(any())).thenAnswer(invocation -> stored.get());
         when(values.setIfAbsent(any(), any())).thenAnswer(invocation -> {
             if (stored.get() != null) return false;

--
Gitblit v1.7.1