From 370ef4f5e663fdfc1d19d2774dfc4ec1ed0af60e Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 29 八月 2026 14:57:48 +0800
Subject: [PATCH] 修复采集器历史状态持续序列化错误
---
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java | 45 +++-
ruoyi-device/src/main/java/com/ruoyi/device/simulator/InMemoryBtsSimulatorStateStore.java | 33 +++
ruoyi-admin/src/main/java/com/ruoyi/config/RedisBtsSimulatorStateStore.java | 151 +++++++++++++++-
ruoyi-device/src/main/java/com/ruoyi/device/simulator/BtsSimulatorStateStore.java | 33 +++
ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java | 122 +++++++++++++
ruoyi-device/src/main/java/com/ruoyi/device/simulator/DevelopmentCollectorSimulation.java | 56 +++--
ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java | 79 +++++++-
7 files changed, 460 insertions(+), 59 deletions(-)
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/config/RedisBtsSimulatorStateStore.java b/ruoyi-admin/src/main/java/com/ruoyi/config/RedisBtsSimulatorStateStore.java
index 0b9bb63..6c9d1eb 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/config/RedisBtsSimulatorStateStore.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/config/RedisBtsSimulatorStateStore.java
@@ -4,7 +4,10 @@
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
+import java.util.Set;
import java.util.concurrent.TimeUnit;
+import org.springframework.data.redis.core.RedisOperations;
+import org.springframework.data.redis.core.SessionCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson2.JSON;
@@ -19,9 +22,13 @@
{
static final String STATE_KEY = "dev:bts-simulator:state:v1";
static final String RECENT_REQUESTS_KEY = "dev:bts-simulator:recent-requests:v1";
+ static final String HISTORY_KEYS_KEY = "dev:bts-simulator:collector-history-keys:v1";
+ static final String HISTORY_KEY_PREFIX = "dev:bts-simulator:collector-history:v1:";
private static final long RECENT_REQUESTS_TTL_HOURS = 1;
+ private static final int HISTORY_READ_BATCH_SIZE = 1000;
private final RedisTemplate<Object, Object> redisTemplate;
+ private List<CollectorHistoryAppend> pendingEmbeddedHistory = List.of();
public RedisBtsSimulatorStateStore(RedisTemplate<Object, Object> redisTemplate)
{
@@ -32,18 +39,106 @@
public SimulatorState loadOrInitialize(SimulatorState defaultState)
{
Object stored = redisTemplate.opsForValue().get(STATE_KEY);
- if (stored != null) return parseState(stored);
+ if (stored != null) return restoreState(stored);
String initial = JSON.toJSONString(defaultState);
if (Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(STATE_KEY, initial))) return defaultState;
stored = redisTemplate.opsForValue().get(STATE_KEY);
if (stored == null) throw new IllegalStateException("BTS simulator state initialization lost in Redis");
- return parseState(stored);
+ return restoreState(stored);
+ }
+
+ private SimulatorState restoreState(Object stored)
+ {
+ ParsedSimulatorState parsed = parseState(stored);
+ // 旧格式历史先只供恢复校验读取,损坏状态不得被迁移写回覆盖。
+ pendingEmbeddedHistory = parsed.embeddedHistory();
+ return parsed.state();
+ }
+
+ @Override
+ public void completeValidatedRestore(SimulatorState state)
+ {
+ if (pendingEmbeddedHistory.isEmpty()) return;
+ save(state, pendingEmbeddedHistory);
+ pendingEmbeddedHistory = List.of();
}
@Override
public void save(SimulatorState state)
{
- redisTemplate.opsForValue().set(STATE_KEY, JSON.toJSONString(state));
+ save(state, List.of());
+ }
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public void save(SimulatorState state, List<CollectorHistoryAppend> historyAppends)
+ {
+ List<Object> results = redisTemplate.execute(new SessionCallback<List<Object>>()
+ {
+ @Override
+ public <K, V> List<Object> execute(RedisOperations<K, V> operations)
+ {
+ RedisOperations<Object, Object> typed = (RedisOperations) operations;
+ typed.multi();
+ for (CollectorHistoryAppend append : historyAppends)
+ {
+ String historyKey = historyKey(append.collectorCode());
+ if (!append.records().isEmpty())
+ {
+ List<Object> records = new ArrayList<>(
+ append.records().stream().map(JSON::toJSONString).toList());
+ typed.opsForList().rightPushAll(historyKey, records);
+ typed.opsForSet().add(HISTORY_KEYS_KEY, historyKey);
+ }
+ }
+ typed.opsForValue().set(STATE_KEY, JSON.toJSONString(state));
+ return typed.exec();
+ }
+ });
+ if (results == null) throw new IllegalStateException("BTS simulator state transaction failed in Redis");
+ }
+
+ @Override
+ public List<CollectorMeasurementRecord> loadCollectorHistory(String collectorCode)
+ {
+ String key = historyKey(collectorCode);
+ Long storedSize = redisTemplate.opsForList().size(key);
+ List<CollectorMeasurementRecord> records = new ArrayList<>();
+ if (storedSize != null)
+ {
+ for (long start = 0; start < storedSize; start += HISTORY_READ_BATCH_SIZE)
+ {
+ long end = Math.min(start + HISTORY_READ_BATCH_SIZE, storedSize) - 1;
+ List<Object> stored = redisTemplate.opsForList().range(key, start, end);
+ if (stored != null)
+ stored.forEach(item -> records.add(collectorMeasurementRecord(item)));
+ }
+ }
+ pendingEmbeddedHistory.stream()
+ .filter(append -> append.collectorCode().equals(collectorCode))
+ .forEach(append -> records.addAll(append.records()));
+ return List.copyOf(records);
+ }
+
+ @Override
+ @SuppressWarnings({ "rawtypes", "unchecked" })
+ public void saveAndDeleteCollectorHistory(SimulatorState state, String collectorCode)
+ {
+ String historyKey = historyKey(collectorCode);
+ List<Object> results = redisTemplate.execute(new SessionCallback<List<Object>>()
+ {
+ @Override
+ public <K, V> List<Object> execute(RedisOperations<K, V> operations)
+ {
+ RedisOperations<Object, Object> typed = (RedisOperations) operations;
+ typed.multi();
+ typed.delete(historyKey);
+ typed.opsForSet().remove(HISTORY_KEYS_KEY, historyKey);
+ typed.opsForValue().set(STATE_KEY, JSON.toJSONString(state));
+ return typed.exec();
+ }
+ });
+ if (results == null) throw new IllegalStateException("BTS simulator collector deletion failed in Redis");
}
@Override
@@ -64,11 +159,15 @@
@Override
public void reset(SimulatorState defaultState)
{
+ pendingEmbeddedHistory = List.of();
+ Set<Object> historyKeys = redisTemplate.opsForSet().members(HISTORY_KEYS_KEY);
+ if (historyKeys != null && !historyKeys.isEmpty()) redisTemplate.delete(historyKeys);
+ redisTemplate.delete(HISTORY_KEYS_KEY);
redisTemplate.delete(RECENT_REQUESTS_KEY);
save(defaultState);
}
- private SimulatorState parseState(Object stored)
+ private ParsedSimulatorState parseState(Object stored)
{
try
{
@@ -80,11 +179,20 @@
List<DeviceState> devices = new ArrayList<>();
requiredArray(root, "devices").forEach(item -> devices.add(deviceState(item)));
List<CollectorState> collectors = new ArrayList<>();
- requiredArray(root, "collectors").forEach(item -> collectors.add(collectorState(item)));
+ List<CollectorHistoryAppend> embeddedHistory = new ArrayList<>();
+ requiredArray(root, "collectors").forEach(item -> {
+ JSONObject collectorObject = jsonObject(item);
+ List<CollectorMeasurementRecord> history = embeddedCollectorHistory(collectorObject);
+ CollectorState collector = collectorState(collectorObject, history);
+ collectors.add(collector);
+ if (!history.isEmpty())
+ embeddedHistory.add(new CollectorHistoryAppend(collector.deviceCode(), history));
+ });
List<ChannelTombstone> tombstones = new ArrayList<>();
requiredArray(root, "tombstones").forEach(item -> tombstones.add(channelTombstone(item)));
- return new SimulatorState(version, devices, collectors,
+ SimulatorState state = new SimulatorState(version, devices, collectors,
requiredInteger(root, "nextDeviceId"), requiredInteger(root, "nextCollectorId"), tombstones);
+ return new ParsedSimulatorState(state, embeddedHistory);
}
catch (IllegalStateException exception)
{
@@ -132,9 +240,17 @@
object.getBooleanValue("online"), channels);
}
- private CollectorState collectorState(Object value)
+ private List<CollectorMeasurementRecord> embeddedCollectorHistory(JSONObject object)
{
- JSONObject object = jsonObject(value);
+ JSONArray values = object.getJSONArray("history");
+ if (values == null) return List.of();
+ List<CollectorMeasurementRecord> history = new ArrayList<>(values.size());
+ values.forEach(item -> history.add(collectorMeasurementRecord(item)));
+ return List.copyOf(history);
+ }
+
+ private CollectorState collectorState(JSONObject object, List<CollectorMeasurementRecord> embeddedHistory)
+ {
int collectorId = requiredInteger(object, "collectorId");
String deviceCode = object.getString("deviceCode");
String ip = object.getString("ip");
@@ -151,18 +267,19 @@
List<CollectorChannelState> channels = new ArrayList<>();
requiredArray(object, "channels").forEach(item -> channels.add(collectorChannelState(item)));
- List<CollectorMeasurementRecord> history = new ArrayList<>();
- requiredArray(object, "history").forEach(item -> history.add(collectorMeasurementRecord(item)));
List<CollectorRealtimeMeasurement> latestMeasurements = new ArrayList<>();
JSONArray latest = object.getJSONArray("latestMeasurements");
if (latest != null)
latest.forEach(item -> latestMeasurements.add(collectorRealtimeMeasurement(item)));
else
- latestMeasurements.addAll(latestMeasurementsFromLegacyHistory(history, channels));
+ latestMeasurements.addAll(latestMeasurementsFromLegacyHistory(embeddedHistory, channels));
+ long historyRecordCount = object.containsKey("historyRecordCount")
+ ? requiredLong(object, "historyRecordCount") : embeddedHistory.size();
return new CollectorState(collectorId, deviceCode, ip, mac, name, manufacturer, model,
channelCount, online, requiredInteger(object, "sampleIntervalMs"),
requiredLong(object, "nextSequence"), optionalInstant(object, "lastSampledAt"),
- channels, latestMeasurements, history, collectorHistoryFaults(object.get("historyFaults")));
+ channels, latestMeasurements, historyRecordCount,
+ collectorHistoryFaults(object.get("historyFaults")));
}
private CollectorChannelState collectorChannelState(Object value)
@@ -270,4 +387,14 @@
String value = object.getString(field);
return value == null ? null : Instant.parse(value);
}
+
+ private String historyKey(String collectorCode)
+ {
+ return HISTORY_KEY_PREFIX + collectorCode;
+ }
+
+ private record ParsedSimulatorState(SimulatorState state, List<CollectorHistoryAppend> embeddedHistory)
+ {
+ private ParsedSimulatorState { embeddedHistory = List.copyOf(embeddedHistory); }
+ }
}
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;
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 1663a40..5548804 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
@@ -13,6 +13,7 @@
import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelState;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelTombstone;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorChannelState;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryAppend;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMetricState;
@@ -23,6 +24,7 @@
import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState;
import com.ruoyi.device.simulator.InMemoryBtsSimulatorStateStore;
import com.ruoyi.device.simulator.DevelopmentCollectorSimulation;
+import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorAdvance;
import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorHistoryPage;
import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot;
@@ -58,6 +60,7 @@
this.stateStore = stateStore;
state = stateStore.loadOrInitialize(defaultState());
validateRestoredState();
+ stateStore.completeValidatedRestore(state);
simulator = ScriptedBtsTcpSimulator.responding(port, this::respond);
}
@@ -155,11 +158,13 @@
/** 删除指定开发采集器,不影响检测柜 TCP 发现结果。 */
public synchronized void deleteCollector(int collectorId)
{
- requireCollector(collectorId);
+ CollectorState collector = requireCollector(collectorId);
List<CollectorState> collectors = new ArrayList<>(state.collectors());
collectors.removeIf(item -> item.collectorId() == collectorId);
- persist(new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
- state.nextCollectorId(), state.tombstones()));
+ SimulatorState next = new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
+ state.nextCollectorId(), state.tombstones());
+ stateStore.saveAndDeleteCollectorHistory(next, collector.deviceCode());
+ this.state = next;
}
/** 配置采集器独立采样频率和历史下载故障场景。 */
@@ -181,12 +186,18 @@
/** 按每台采集器自己的频率推进实时和历史采样。 */
public synchronized void advanceCollectors(Instant now)
{
- List<CollectorState> collectors = state.collectors().stream()
- .map(collector -> DevelopmentCollectorSimulation.advance(collector, now))
- .toList();
- if (!collectors.equals(state.collectors()))
+ List<CollectorState> collectors = new ArrayList<>(state.collectors().size());
+ List<CollectorHistoryAppend> historyAppends = new ArrayList<>();
+ for (CollectorState collector : state.collectors())
+ {
+ CollectorAdvance advance = DevelopmentCollectorSimulation.advance(collector, now);
+ collectors.add(advance.state());
+ if (!advance.appendedRecords().isEmpty())
+ historyAppends.add(new CollectorHistoryAppend(collector.deviceCode(), advance.appendedRecords()));
+ }
+ if (!historyAppends.isEmpty())
persist(new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
- state.nextCollectorId(), state.tombstones()));
+ state.nextCollectorId(), state.tombstones()), historyAppends);
}
/** 返回采集器最新实时快照;快照与分页历史保持不同读取路径。 */
@@ -198,7 +209,9 @@
/** 分页下载采集器模拟历史,并应用已配置的重复、冲突、乱序或缺失场景。 */
public synchronized CollectorHistoryPage collectorHistory(int collectorId, long offset, int limit)
{
- return DevelopmentCollectorSimulation.history(requireCollector(collectorId), offset, limit);
+ CollectorState collector = requireCollector(collectorId);
+ return DevelopmentCollectorSimulation.history(collector,
+ stateStore.loadCollectorHistory(collector.deviceCode()), offset, limit);
}
/** 返回控制页需要的运行状态快照。 */
@@ -215,7 +228,7 @@
.map(collector -> new CollectorSnapshot(collector.collectorId(), collector.deviceCode(),
collector.ip(), collector.mac(), collector.name(), collector.manufacturer(),
collector.model(), collector.channelCount(), collector.online(), collector.sampleIntervalMs(),
- collector.channels(), collector.historyFaults(), collector.history().size(),
+ collector.channels(), collector.historyFaults(), collector.historyRecordCount(),
DevelopmentCollectorSimulation.realtime(collector).measurements()))
.toList();
return new StateSnapshot(running, HOST + ":" + port(), deviceSnapshots, collectorSnapshots,
@@ -452,6 +465,13 @@
state = nextState;
}
+ private void persist(SimulatorState nextState, List<CollectorHistoryAppend> historyAppends)
+ {
+ // 主状态与原始历史批次必须一起成功,内存序号才能向前推进。
+ stateStore.save(nextState, historyAppends);
+ state = nextState;
+ }
+
private String simulatorMac(int devtype, int devid)
{
return String.format("02:44:%02X:%02X:%02X:%02X",
@@ -498,7 +518,8 @@
|| !isIpv4(collector.ip()) || !ips.add(collector.ip())
|| collector.channelCount() < 1 || collector.channelCount() > MAX_CHANNEL_COUNT)
throw new IllegalStateException("Invalid BTS simulator collector state in Redis");
- DevelopmentCollectorSimulation.validate(collector);
+ DevelopmentCollectorSimulation.validate(collector,
+ stateStore.loadCollectorHistory(collector.deviceCode()));
maximumCollectorId = Math.max(maximumCollectorId, collector.collectorId());
}
java.util.Set<String> tombstoneIds = new java.util.HashSet<>();
@@ -529,7 +550,7 @@
public record CollectorSnapshot(int collectorId, String deviceCode, String ip, String mac, String name,
String manufacturer, String model, int channelCount, boolean online, int sampleIntervalMs,
- List<CollectorChannelState> channels, CollectorHistoryFaults historyFaults, int historyRecordCount,
+ List<CollectorChannelState> channels, CollectorHistoryFaults historyFaults, long historyRecordCount,
List<CollectorRealtimeMeasurement> latestMeasurements)
{
public CollectorSnapshot
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/simulator/BtsSimulatorStateStore.java b/ruoyi-device/src/main/java/com/ruoyi/device/simulator/BtsSimulatorStateStore.java
index 7f70ae5..e417b5a 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/simulator/BtsSimulatorStateStore.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/simulator/BtsSimulatorStateStore.java
@@ -16,6 +16,29 @@
/** 完整保存下一版模拟器状态。 */
void save(SimulatorState state);
+ /** 状态和独立历史全部校验通过后,提交兼容旧格式所需的迁移。 */
+ default void completeValidatedRestore(SimulatorState state) { }
+
+ /** 原子保存主状态并追加本轮新产生的原始历史。 */
+ default void save(SimulatorState state, List<CollectorHistoryAppend> historyAppends)
+ {
+ if (!historyAppends.isEmpty())
+ throw new UnsupportedOperationException("Collector history append is not supported");
+ save(state);
+ }
+
+ /** 读取指定采集器的只追加原始历史。 */
+ default List<CollectorMeasurementRecord> loadCollectorHistory(String collectorCode)
+ {
+ return List.of();
+ }
+
+ /** 原子保存删除采集器后的主状态,并删除该采集器的独立历史。 */
+ default void saveAndDeleteCollectorHistory(SimulatorState state, String collectorCode)
+ {
+ throw new UnsupportedOperationException("Atomic collector deletion is not supported");
+ }
+
/** 读取短期保留的最近请求。 */
List<RecentRequest> loadRecentRequests();
@@ -47,13 +70,12 @@
String manufacturer, String model, int channelCount, boolean online, int sampleIntervalMs,
long nextSequence, Instant lastSampledAt, List<CollectorChannelState> channels,
List<CollectorRealtimeMeasurement> latestMeasurements,
- List<CollectorMeasurementRecord> history, CollectorHistoryFaults historyFaults)
+ long historyRecordCount, CollectorHistoryFaults historyFaults)
{
public CollectorState
{
channels = List.copyOf(channels);
latestMeasurements = List.copyOf(latestMeasurements);
- history = List.copyOf(history);
historyFaults = historyFaults == null ? CollectorHistoryFaults.none() : historyFaults;
}
@@ -63,7 +85,7 @@
{
this(collectorId, deviceCode, ip, mac, name, manufacturer, model, channelCount, online,
DevelopmentCollectorSimulation.DEFAULT_SAMPLE_INTERVAL_MS, 1L, null,
- DevelopmentCollectorSimulation.defaultChannels(channelCount), List.of(), List.of(),
+ DevelopmentCollectorSimulation.defaultChannels(channelCount), List.of(), 0L,
CollectorHistoryFaults.none());
}
}
@@ -83,6 +105,11 @@
long sequenceId, Instant deviceTime, String deviceTimeRaw, Instant serverReceivedAt,
String metricCode, BigDecimal value, String unit) { }
+ record CollectorHistoryAppend(String collectorCode, List<CollectorMeasurementRecord> records)
+ {
+ public CollectorHistoryAppend { records = List.copyOf(records); }
+ }
+
record CollectorHistoryFaults(List<Long> missingSequences, Long duplicateSequence,
Long conflictingSequence, boolean outOfOrder)
{
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/simulator/DevelopmentCollectorSimulation.java b/ruoyi-device/src/main/java/com/ruoyi/device/simulator/DevelopmentCollectorSimulation.java
index 369172a..c07d403 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/simulator/DevelopmentCollectorSimulation.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/simulator/DevelopmentCollectorSimulation.java
@@ -61,7 +61,7 @@
List<CollectorRealtimeMeasurement> latestMeasurements = retainConfiguredMeasurements(
current.latestMeasurements(), channels);
return copy(current, ip, channelCount, online, current.sampleIntervalMs(), current.nextSequence(),
- lastSampledAt, channels, latestMeasurements, current.history(), current.historyFaults());
+ lastSampledAt, channels, latestMeasurements, current.historyRecordCount(), current.historyFaults());
}
/** 更新采样频率和历史下载故障;测量原始历史保持只追加。 */
@@ -72,7 +72,7 @@
validateFaults(historyFaults);
return copy(current, current.ip(), current.channelCount(), current.online(), sampleIntervalMs,
current.nextSequence(), current.lastSampledAt(), current.channels(), current.latestMeasurements(),
- current.history(), historyFaults);
+ current.historyRecordCount(), historyFaults);
}
/** 替换一个演示通道的指标配置;动态添加项与固定指标共用指标编码合同。 */
@@ -87,19 +87,19 @@
current.latestMeasurements(), channels);
return copy(current, current.ip(), current.channelCount(), current.online(), current.sampleIntervalMs(),
current.nextSequence(), current.lastSampledAt(), channels, latestMeasurements,
- current.history(), current.historyFaults());
+ current.historyRecordCount(), current.historyFaults());
}
/** 按采集器自身频率推进采样;离线期间不生成记录,恢复后从下一序号继续。 */
- public static CollectorState advance(CollectorState current, Instant now)
+ public static CollectorAdvance advance(CollectorState current, Instant now)
{
- if (!current.online()) return current;
+ if (!current.online()) return new CollectorAdvance(current, List.of());
Instant sampleAt = current.lastSampledAt() == null
? now
: current.lastSampledAt().plusMillis(current.sampleIntervalMs());
- if (sampleAt.isAfter(now)) return current;
+ if (sampleAt.isAfter(now)) return new CollectorAdvance(current, List.of());
- List<CollectorMeasurementRecord> history = new ArrayList<>(current.history());
+ List<CollectorMeasurementRecord> appendedRecords = new ArrayList<>();
Map<String, CollectorRealtimeMeasurement> latest = new LinkedHashMap<>();
for (CollectorRealtimeMeasurement measurement : current.latestMeasurements())
latest.put(measurementKey(measurement.channelCode(), measurement.metricCode()), measurement);
@@ -116,7 +116,7 @@
channel.channelCode(), sequence, sampleAt,
DEVICE_TIME_FORMATTER.format(sampleAt.atZone(DEVICE_ZONE)), now,
metric.metricCode(), metric.value(), metric.unit());
- history.add(record);
+ appendedRecords.add(record);
latest.put(measurementKey(record.channelCode(), record.metricCode()),
new CollectorRealtimeMeasurement(record.adapterCode(), record.collectorCode(),
record.channelCode(), record.sequenceId(), record.deviceTime(),
@@ -129,9 +129,12 @@
generated++;
sampleAt = sampleAt.plusMillis(current.sampleIntervalMs());
}
- return copy(current, current.ip(), current.channelCount(), current.online(), current.sampleIntervalMs(),
- sequence, lastSampledAt, current.channels(), List.copyOf(latest.values()), history,
+ CollectorState next = copy(current, current.ip(), current.channelCount(), current.online(),
+ current.sampleIntervalMs(),
+ sequence, lastSampledAt, current.channels(), List.copyOf(latest.values()),
+ current.historyRecordCount() + appendedRecords.size(),
current.historyFaults());
+ return new CollectorAdvance(next, appendedRecords);
}
/** 返回独立保存的最新实时快照,不从原始历史反推当前状态。 */
@@ -142,13 +145,14 @@
}
/** 对只追加原始模拟历史应用可重复的下载故障视图,再按偏移量分页。 */
- public static CollectorHistoryPage history(CollectorState current, long offset, int limit)
+ public static CollectorHistoryPage history(CollectorState current, List<CollectorMeasurementRecord> rawHistory,
+ long offset, int limit)
{
if (offset < 0) throw new IllegalArgumentException("offset must not be negative");
if (limit < 1 || limit > MAX_HISTORY_PAGE_SIZE)
throw new IllegalArgumentException("limit must be between 1 and " + MAX_HISTORY_PAGE_SIZE);
- List<CollectorMeasurementRecord> download = faultedHistory(current);
+ List<CollectorMeasurementRecord> download = faultedHistory(current, rawHistory);
int from = (int) Math.min(offset, download.size());
int to = Math.min(from + limit, download.size());
return new CollectorHistoryPage(current.deviceCode(), offset, to, to < download.size(),
@@ -156,7 +160,7 @@
}
/** 校验 Redis 恢复的采集器采样状态,拒绝损坏状态被静默替换。 */
- public static void validate(CollectorState state)
+ public static void validate(CollectorState state, List<CollectorMeasurementRecord> rawHistory)
{
validateSampleInterval(state.sampleIntervalMs());
if (state.nextSequence() < 1 || state.channels().size() != state.channelCount())
@@ -169,7 +173,7 @@
validateMetrics(channel.metrics());
}
validateLatestMeasurements(state);
- validateHistory(state);
+ validateHistory(state, rawHistory);
validateFaults(state.historyFaults());
}
@@ -189,12 +193,12 @@
}
}
- private static void validateHistory(CollectorState state)
+ private static void validateHistory(CollectorState state, List<CollectorMeasurementRecord> rawHistory)
{
long maximumSequence = 0;
long previousSequence = 0;
Set<String> identities = new java.util.HashSet<>();
- for (CollectorMeasurementRecord record : state.history())
+ for (CollectorMeasurementRecord record : rawHistory)
{
String identity = record.sequenceId() + "\u0000" + record.channelCode() + "\u0000" + record.metricCode();
if (!validMeasurement(state, record.adapterCode(), record.collectorCode(), record.channelCode(),
@@ -207,6 +211,8 @@
}
if (state.nextSequence() <= maximumSequence)
throw new IllegalStateException("Invalid collector history sequence in Redis");
+ if (state.historyRecordCount() != rawHistory.size())
+ throw new IllegalStateException("Invalid collector history count in Redis");
}
private static boolean validMeasurement(CollectorState state, String adapterCode, String collectorCode,
@@ -266,12 +272,13 @@
return channelCode + "\u0000" + metricCode;
}
- private static List<CollectorMeasurementRecord> faultedHistory(CollectorState current)
+ private static List<CollectorMeasurementRecord> faultedHistory(CollectorState current,
+ List<CollectorMeasurementRecord> rawHistory)
{
CollectorHistoryFaults faults = current.historyFaults();
Set<Long> missing = Set.copyOf(faults.missingSequences());
List<CollectorMeasurementRecord> records = new ArrayList<>();
- for (CollectorMeasurementRecord record : current.history())
+ for (CollectorMeasurementRecord record : rawHistory)
{
if (missing.contains(record.sequenceId())) continue;
records.add(record);
@@ -292,12 +299,12 @@
private static CollectorState copy(CollectorState current, String ip, int channelCount, boolean online,
int sampleIntervalMs, long nextSequence, Instant lastSampledAt,
List<CollectorChannelState> channels, List<CollectorRealtimeMeasurement> latestMeasurements,
- List<CollectorMeasurementRecord> history,
+ long historyRecordCount,
CollectorHistoryFaults faults)
{
return new CollectorState(current.collectorId(), current.deviceCode(), ip.trim(), current.mac(),
current.name(), current.manufacturer(), current.model(), channelCount, online, sampleIntervalMs,
- nextSequence, lastSampledAt, channels, latestMeasurements, history, faults);
+ nextSequence, lastSampledAt, channels, latestMeasurements, historyRecordCount, faults);
}
private static void validateSampleInterval(int sampleIntervalMs)
@@ -315,7 +322,9 @@
for (CollectorMetricState metric : metrics)
{
if (metric.metricCode() == null || metric.metricCode().isBlank() || metric.value() == null
- || metric.unit() == null || metric.unit().isBlank() || !codes.add(metric.metricCode().trim()))
+ || !metric.metricCode().equals(metric.metricCode().trim())
+ || metric.unit() == null || metric.unit().isBlank()
+ || !metric.unit().equals(metric.unit().trim()) || !codes.add(metric.metricCode()))
throw new IllegalArgumentException("Collector metric code, value and unit must be valid and unique");
}
}
@@ -350,6 +359,11 @@
public CollectorRealtimeSnapshot { measurements = List.copyOf(measurements); }
}
+ public record CollectorAdvance(CollectorState state, List<CollectorMeasurementRecord> appendedRecords)
+ {
+ public CollectorAdvance { appendedRecords = List.copyOf(appendedRecords); }
+ }
+
public record CollectorHistoryPage(String collectorCode, long offset, long nextOffset, boolean hasMore,
long total, List<CollectorMeasurementRecord> records)
{
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/simulator/InMemoryBtsSimulatorStateStore.java b/ruoyi-device/src/main/java/com/ruoyi/device/simulator/InMemoryBtsSimulatorStateStore.java
index 780ffe3..1c6db82 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/simulator/InMemoryBtsSimulatorStateStore.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/simulator/InMemoryBtsSimulatorStateStore.java
@@ -1,6 +1,11 @@
package com.ruoyi.device.simulator;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryAppend;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.RecentRequest;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState;
@@ -9,6 +14,7 @@
{
private SimulatorState state;
private List<RecentRequest> recentRequests = List.of();
+ private final Map<String, List<CollectorMeasurementRecord>> collectorHistory = new LinkedHashMap<>();
public InMemoryBtsSimulatorStateStore() { }
@@ -31,6 +37,32 @@
}
@Override
+ public synchronized void save(SimulatorState state, List<CollectorHistoryAppend> historyAppends)
+ {
+ for (CollectorHistoryAppend append : historyAppends)
+ {
+ List<CollectorMeasurementRecord> records = new ArrayList<>(
+ collectorHistory.getOrDefault(append.collectorCode(), List.of()));
+ records.addAll(append.records());
+ collectorHistory.put(append.collectorCode(), List.copyOf(records));
+ }
+ this.state = state;
+ }
+
+ @Override
+ public synchronized List<CollectorMeasurementRecord> loadCollectorHistory(String collectorCode)
+ {
+ return collectorHistory.getOrDefault(collectorCode, List.of());
+ }
+
+ @Override
+ public synchronized void saveAndDeleteCollectorHistory(SimulatorState state, String collectorCode)
+ {
+ collectorHistory.remove(collectorCode);
+ this.state = state;
+ }
+
+ @Override
public synchronized List<RecentRequest> loadRecentRequests()
{
return recentRequests;
@@ -47,5 +79,6 @@
{
state = defaultState;
recentRequests = List.of();
+ collectorHistory.clear();
}
}
diff --git a/ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java b/ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java
index ba19503..7a55294 100644
--- a/ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java
+++ b/ruoyi-device/src/test/java/com/ruoyi/device/contract/SimulatorPublicContractTest.java
@@ -11,6 +11,7 @@
import java.util.List;
import java.util.Set;
import org.junit.jupiter.api.Test;
+import com.alibaba.fastjson2.JSON;
import com.ruoyi.device.api.DeviceAdapter;
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
import com.ruoyi.device.bts.BtsDeviceAdapter;
@@ -22,6 +23,7 @@
import com.ruoyi.device.simulator.BtsSimulatorStateStore;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelState;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorChannelState;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryAppend;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMetricState;
@@ -132,6 +134,20 @@
}
@Test
+ void collectorRawHistoryIsNotEmbeddedInMainSimulatorState() throws Exception
+ {
+ InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
+ try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store))
+ {
+ simulator.advanceCollectors(Instant.parse("2026-08-29T03:30:00Z"));
+
+ String mainState = JSON.toJSONString(store.loadOrInitialize(null));
+ assertFalse(mainState.contains("\"history\":[{"));
+ assertEquals(12, simulator.collectorHistory(1, 0, 100).total());
+ }
+ }
+
+ @Test
void collectorRealtimeDropsMeasurementsRemovedFromCurrentConfiguration() throws Exception
{
Instant startedAt = Instant.parse("2026-08-29T04:00:00Z");
@@ -159,6 +175,18 @@
{
assertEquals(4, restored.snapshot().collectors().get(0).latestMeasurements().size());
assertEquals(12, restored.collectorHistory(1, 0, 100).total());
+ }
+ }
+
+ @Test
+ void collectorRejectsMetricWhitespaceThatWouldBreakRestore() throws Exception
+ {
+ try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
+ {
+ assertThrows(IllegalArgumentException.class, () -> simulator.configureCollectorChannel(1, "S-01",
+ List.of(new CollectorMetricState("temperature ", new BigDecimal("25.0"), "℃"))));
+ assertThrows(IllegalArgumentException.class, () -> simulator.configureCollectorChannel(1, "S-01",
+ List.of(new CollectorMetricState("temperature", new BigDecimal("25.0"), " ℃"))));
}
}
@@ -347,6 +375,31 @@
}
@Test
+ void collectorDeletionDoesNotReplaceMemoryWhenAtomicPersistenceFails() throws Exception
+ {
+ InMemoryBtsSimulatorStateStore delegate = new InMemoryBtsSimulatorStateStore();
+ BtsSimulatorStateStore failing = new BtsSimulatorStateStore()
+ {
+ @Override public SimulatorState loadOrInitialize(SimulatorState value)
+ { return delegate.loadOrInitialize(value); }
+ @Override public void save(SimulatorState value) { delegate.save(value); }
+ @Override public void saveAndDeleteCollectorHistory(SimulatorState value, String collectorCode)
+ { throw new IllegalStateException("redis unavailable"); }
+ @Override public java.util.List<RecentRequest> loadRecentRequests()
+ { return delegate.loadRecentRequests(); }
+ @Override public void saveRecentRequests(java.util.List<RecentRequest> requests)
+ { delegate.saveRecentRequests(requests); }
+ @Override public void reset(SimulatorState value) { delegate.reset(value); }
+ };
+
+ try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, failing))
+ {
+ assertThrows(IllegalStateException.class, () -> simulator.deleteCollector(1));
+ assertEquals(1, simulator.snapshot().collectors().size());
+ }
+ }
+
+ @Test
void developmentSimulatorRestoresUnfinishedTombstones() throws Exception
{
InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore();
@@ -390,17 +443,20 @@
Instant observedAt = Instant.parse("2026-08-29T05:00:00Z");
List<CollectorChannelState> channels = List.of(new CollectorChannelState("S-01", List.of(
new CollectorMetricState("temperature", new BigDecimal("25.0"), "℃"))));
+ CollectorMeasurementRecord record = new CollectorMeasurementRecord(
+ "dev-sim", "wrong-collector", "S-01", 1L, observedAt,
+ "2026-08-29 13:00:00.000", observedAt, "temperature",
+ new BigDecimal("25.0"), "℃");
CollectorState collector = new CollectorState(1, "dev-sim-collector-1", "127.0.1.2",
"02:43:00:00:00:01", "模拟采集器", "开发模拟器", "Virtual Collector", 1, true,
- 1000, 2L, observedAt, channels, List.of(), List.of(new CollectorMeasurementRecord(
- "dev-sim", "wrong-collector", "S-01", 1L, observedAt,
- "2026-08-29 13:00:00.000", observedAt, "temperature",
- new BigDecimal("25.0"), "℃")), CollectorHistoryFaults.none());
+ 1000, 2L, observedAt, channels, List.of(), 1L, CollectorHistoryFaults.none());
SimulatorState damaged = new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION,
List.of(), List.of(collector), 1, 2, List.of());
+ InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(damaged);
+ store.save(damaged, List.of(new CollectorHistoryAppend(collector.deviceCode(), List.of(record))));
assertThrows(IllegalStateException.class,
- () -> new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore(damaged)));
+ () -> new BtsDevelopmentSimulator(0, store));
}
@Test
@@ -409,16 +465,19 @@
Instant observedAt = Instant.parse("2026-08-29T06:00:00Z");
List<CollectorChannelState> channels = List.of(new CollectorChannelState("S-01", List.of(
new CollectorMetricState("temperature", new BigDecimal("25.0"), "℃"))));
+ CollectorMeasurementRecord record = new CollectorMeasurementRecord(
+ "dev-sim", "dev-sim-collector-1", "S-01", 1L, observedAt,
+ "2026-08-29 14:00:00.000", observedAt, "temperature",
+ new BigDecimal("25.0"), "℃");
CollectorState collector = new CollectorState(1, "dev-sim-collector-1", "127.0.1.2",
"02:43:00:00:00:01", "模拟采集器", "开发模拟器", "Virtual Collector", 1, true,
- 1000, 1L, observedAt, channels, List.of(), List.of(new CollectorMeasurementRecord(
- "dev-sim", "dev-sim-collector-1", "S-01", 1L, observedAt,
- "2026-08-29 14:00:00.000", observedAt, "temperature",
- new BigDecimal("25.0"), "℃")), CollectorHistoryFaults.none());
+ 1000, 1L, observedAt, channels, List.of(), 1L, CollectorHistoryFaults.none());
SimulatorState damaged = new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION,
List.of(), List.of(collector), 1, 2, List.of());
+ InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(damaged);
+ store.save(damaged, List.of(new CollectorHistoryAppend(collector.deviceCode(), List.of(record))));
assertThrows(IllegalStateException.class,
- () -> new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore(damaged)));
+ () -> new BtsDevelopmentSimulator(0, store));
}
}
--
Gitblit v1.7.1