From 7259e265b9237286996b09da03038ea5a133e1e0 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 05 九月 2026 17:45:07 +0800
Subject: [PATCH] 收敛模拟器为设备事实与实时观测
---
ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java | 404 ++-------------------------------------------------------
1 files changed, 19 insertions(+), 385 deletions(-)
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java
index f407c87..8eed74a 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/RedisBtsSimulatorStateStore.java
@@ -1,13 +1,9 @@
package com.battery.business.runtime;
import java.math.BigDecimal;
-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 org.springframework.context.annotation.Profile;
@@ -16,9 +12,6 @@
import com.alibaba.fastjson2.JSONObject;
import com.battery.device.simulator.BtsSimulatorStateStore;
import com.battery.device.simulator.BtsSimulatorStateStore.BtsDeviceProfile;
-import com.battery.device.simulator.BtsSimulatorStateStore.BtsHistoryBatch;
-import com.battery.device.simulator.BtsSimulatorStateStore.BtsHistoryRecord;
-import com.battery.device.simulator.BtsSimulatorStateStore.BtsPlaybackState;
import com.battery.device.api.model.DeviceWorkStatus;
/** 将开发模拟器完整状态写入 Redis,主状态不设置普通 TTL。 */
@@ -28,16 +21,9 @@
{
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:";
- // 历史与播放位置使用独立键版本,避免把旧的三循环演示历史恢复到当前时间轴。
- static final String BTS_HISTORY_KEYS_KEY = "dev:bts-simulator:bts-history-keys:v2";
- static final String BTS_HISTORY_KEY_PREFIX = "dev:bts-simulator:bts-history:v2:";
- static final String BTS_PLAYBACK_KEY_PREFIX = "dev:bts-simulator:bts-playback:v2:";
private static final long RECENT_REQUESTS_TTL_HOURS = 1;
private final RedisTemplate<Object, Object> redisTemplate;
- private List<CollectorHistoryAppend> pendingEmbeddedHistory = List.of();
public RedisBtsSimulatorStateStore(RedisTemplate<Object, Object> redisTemplate)
{
@@ -57,236 +43,9 @@
}
@Override
- public SimulatorState resetHistoryForDemoStartup(SimulatorState state)
- {
- if (pendingEmbeddedHistory.isEmpty()) return state;
- java.util.Map<String, Integer> embeddedCounts = new java.util.HashMap<>();
- pendingEmbeddedHistory.forEach(append -> embeddedCounts.put(append.collectorCode(), append.records().size()));
- List<CollectorState> collectors = state.collectors().stream().map(collector -> {
- int embeddedCount = embeddedCounts.getOrDefault(collector.deviceCode(), 0);
- if (embeddedCount == 0) return collector;
- long remaining = Math.max(0L, collector.historyRecordCount() - embeddedCount);
- return new CollectorState(collector.collectorId(), collector.deviceCode(), collector.ip(), collector.mac(),
- collector.name(), collector.manufacturer(), collector.model(), collector.channelCount(),
- collector.online(), collector.sampleIntervalMs(), collector.nextSequence(), collector.lastSampledAt(),
- collector.channels(), collector.latestMeasurements().stream()
- .filter(item -> collector.deviceCode().equals(item.collectorCode()))
- .toList(), remaining, collector.historyFaults());
- }).toList();
- // 不删除独立历史 Redis key;这里只阻止旧内嵌数据在本次启动时迁移回来。
- pendingEmbeddedHistory = List.of();
- SimulatorState cleaned = new SimulatorState(state.version(), state.devices(), collectors, state.nextDeviceId(),
- state.nextCollectorId(), state.tombstones());
- save(cleaned);
- return cleaned;
- }
-
- 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)
{
- 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.opsForList().trim(historyKey, -MAX_HISTORY_RECORDS, -1);
- 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 CollectorHistoryBatch loadCollectorHistoryPage(String collectorCode, long offset, int limit,
- boolean reverse)
- {
- if (offset < 0) throw new IllegalArgumentException("offset must not be negative");
- if (limit < 1 || limit > MAX_HISTORY_BATCH_SIZE)
- throw new IllegalArgumentException("limit must be between 1 and " + MAX_HISTORY_BATCH_SIZE);
- String key = historyKey(collectorCode);
- Long storedSize = redisTemplate.opsForList().size(key);
- long redisCount = storedSize == null ? 0 : storedSize;
- List<CollectorMeasurementRecord> embedded = pendingEmbeddedHistory.stream()
- .filter(append -> append.collectorCode().equals(collectorCode))
- .findFirst().map(CollectorHistoryAppend::records).orElse(List.of());
- long total = redisCount + embedded.size();
- long from = Math.min(offset, total);
- int count = (int) Math.min(limit, total - from);
- List<CollectorMeasurementRecord> records = new ArrayList<>(count);
- if (reverse)
- {
- long lower = total - from - count;
- long upper = total - from;
- long embeddedLower = Math.max(lower, redisCount);
- for (long index = upper - 1; index >= embeddedLower; index--)
- records.add(embedded.get((int) (index - redisCount)));
- long redisUpper = Math.min(upper, redisCount);
- if (lower < redisUpper)
- {
- List<Object> stored = redisTemplate.opsForList().range(key, lower, redisUpper - 1);
- if (stored != null)
- for (int index = stored.size() - 1; index >= 0; index--)
- records.add(collectorMeasurementRecord(stored.get(index)));
- }
- }
- else
- {
- long upper = from + count;
- long redisUpper = Math.min(upper, redisCount);
- if (from < redisUpper)
- {
- List<Object> stored = redisTemplate.opsForList().range(key, from, redisUpper - 1);
- if (stored != null) stored.forEach(item -> records.add(collectorMeasurementRecord(item)));
- }
- long embeddedFrom = Math.max(from, redisCount);
- if (embeddedFrom < upper)
- records.addAll(embedded.subList((int) (embeddedFrom - redisCount),
- (int) (upper - redisCount)));
- }
- if (records.size() != count)
- throw new IllegalStateException("Collector history page changed while reading Redis");
- long nextOffset = from + records.size();
- return new CollectorHistoryBatch(offset, nextOffset, nextOffset < total, total, records);
- }
-
- @Override
- public BtsHistoryBatch loadBtsHistoryPage(String historyKey, long offset, int limit)
- {
- if (offset < 0 || limit < 1 || limit > MAX_HISTORY_BATCH_SIZE)
- throw new IllegalArgumentException("invalid BTS history page");
- String key = BTS_HISTORY_KEY_PREFIX + historyKey;
- Long size = redisTemplate.opsForList().size(key);
- long total = size == null ? 0 : size;
- long from = Math.min(offset, total);
- int count = (int) Math.min(limit, total - from);
- List<Object> values = count == 0 ? List.of() : redisTemplate.opsForList().range(key, from, from + count - 1);
- List<BtsHistoryRecord> records = values == null ? List.of() : values.stream()
- .map(item -> JSON.parseObject(item.toString(), BtsHistoryRecord.class)).toList();
- long next = from + records.size();
- return new BtsHistoryBatch(offset, next, next < total, total, records);
- }
-
- @Override
- @SuppressWarnings({ "rawtypes", "unchecked" })
- public void appendBtsHistory(String historyKey, BtsHistoryRecord record)
- {
- String key = BTS_HISTORY_KEY_PREFIX + historyKey;
- RedisOperations<Object, Object> operations = redisTemplate;
- operations.opsForList().rightPush(key, JSON.toJSONString(record));
- operations.opsForList().trim(key, -MAX_HISTORY_RECORDS, -1);
- operations.opsForSet().add(BTS_HISTORY_KEYS_KEY, key);
- }
-
- @Override
- public BtsPlaybackState loadBtsPlaybackState(String historyKey)
- {
- Object value = redisTemplate.opsForValue().get(BTS_PLAYBACK_KEY_PREFIX + historyKey);
- return value == null ? null : JSON.parseObject(value.toString(), BtsPlaybackState.class);
- }
-
- @Override
- public void saveBtsPlaybackState(String historyKey, BtsPlaybackState playback)
- {
- redisTemplate.opsForValue().set(BTS_PLAYBACK_KEY_PREFIX + historyKey, JSON.toJSONString(playback));
- }
-
- @Override
- public void deleteBtsPlaybackState(String historyKey)
- {
- redisTemplate.delete(BTS_PLAYBACK_KEY_PREFIX + historyKey);
- }
-
- @Override
- public void deleteBtsHistory(String historyKey)
- {
- String key = BTS_HISTORY_KEY_PREFIX + historyKey;
- redisTemplate.delete(key);
- redisTemplate.opsForSet().remove(BTS_HISTORY_KEYS_KEY, key);
- }
-
- @Override
- @SuppressWarnings({ "rawtypes", "unchecked" })
- public void saveBtsPlaybackAndAppendHistory(String historyKey, BtsPlaybackState playback,
- List<BtsHistoryRecord> records)
- {
- 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();
- String key = BTS_HISTORY_KEY_PREFIX + historyKey;
- if (!records.isEmpty())
- {
- typed.opsForList().rightPushAll(key, new ArrayList<>(records.stream()
- .map(JSON::toJSONString).toList()));
- typed.opsForList().trim(key, -MAX_HISTORY_RECORDS, -1);
- typed.opsForSet().add(BTS_HISTORY_KEYS_KEY, key);
- }
- typed.opsForValue().set(BTS_PLAYBACK_KEY_PREFIX + historyKey, JSON.toJSONString(playback));
- return typed.exec();
- }
- });
- if (results == null) throw new IllegalStateException("BTS simulator playback transaction failed in Redis");
- }
-
- @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");
+ redisTemplate.opsForValue().set(STATE_KEY, JSON.toJSONString(state));
}
@Override
@@ -307,21 +66,11 @@
@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);
- Set<Object> btsHistoryKeys = redisTemplate.opsForSet().members(BTS_HISTORY_KEYS_KEY);
- if (btsHistoryKeys != null && !btsHistoryKeys.isEmpty()) redisTemplate.delete(btsHistoryKeys);
- redisTemplate.delete(BTS_HISTORY_KEYS_KEY);
- // 播放状态按通道独立保存,重置演示时必须一并清理。
- Set<Object> playbackKeys = redisTemplate.keys(BTS_PLAYBACK_KEY_PREFIX + "*");
- if (playbackKeys != null && !playbackKeys.isEmpty()) redisTemplate.delete(playbackKeys);
redisTemplate.delete(RECENT_REQUESTS_KEY);
save(defaultState);
}
- private ParsedSimulatorState parseState(Object stored)
+ private SimulatorState restoreState(Object stored)
{
try
{
@@ -333,20 +82,13 @@
List<DeviceState> devices = new ArrayList<>();
requiredArray(root, "devices").forEach(item -> devices.add(deviceState(item)));
List<CollectorState> collectors = new ArrayList<>();
- 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));
- });
+ requiredArray(root, "collectors").forEach(item -> collectors.add(collectorState(jsonObject(item))));
List<ChannelTombstone> tombstones = new ArrayList<>();
requiredArray(root, "tombstones").forEach(item -> tombstones.add(channelTombstone(item)));
SimulatorState state = new SimulatorState(version, devices, collectors,
- requiredInteger(root, "nextDeviceId"), requiredInteger(root, "nextCollectorId"), tombstones);
- return new ParsedSimulatorState(state, embeddedHistory);
+ requiredInteger(root, "nextDeviceId"), requiredInteger(root, "nextCollectorId"), tombstones,
+ root.getBoolean("softwareAccessible"), root.getLongValue("observationSequence"));
+ return state;
}
catch (IllegalStateException exception)
{
@@ -406,46 +148,18 @@
requiredDecimal(object, "voltageMaxV"), requiredInteger(object, "sampleIntervalMs"));
}
- private List<CollectorMeasurementRecord> embeddedCollectorHistory(JSONObject object)
+ private CollectorState collectorState(JSONObject object)
{
- 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");
- String mac = object.getString("mac");
- String name = object.getString("name");
- String manufacturer = object.getString("manufacturer");
- String model = object.getString("model");
- int channelCount = requiredInteger(object, "channelCount");
- boolean online = object.getBooleanValue("online");
- // 仅含发现配置的旧状态首次恢复时,以演示默认值补齐采样状态。
- if (!object.containsKey("sampleIntervalMs"))
- return new CollectorState(collectorId, deviceCode, ip, mac, name, manufacturer, model,
- channelCount, online);
-
+ int count = requiredInteger(object, "channelCount");
List<CollectorChannelState> channels = new ArrayList<>();
- requiredArray(object, "channels").forEach(item -> channels.add(collectorChannelState(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(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, historyRecordCount,
- collectorHistoryFaults(object.get("historyFaults")));
+ JSONArray configured = object.getJSONArray("channels");
+ if (configured == null) channels.addAll(com.battery.device.simulator.DevelopmentCollectorSimulation.defaultChannels(count));
+ else configured.forEach(item -> channels.add(collectorChannelState(item)));
+ // 重启只恢复配置,不把上次响应或已废止的历史作为新观测。
+ return new CollectorState(requiredInteger(object, "collectorId"), object.getString("deviceCode"),
+ object.getString("ip"), object.getString("mac"), object.getString("name"),
+ object.getString("manufacturer"), object.getString("model"), count, object.getBooleanValue("online"),
+ com.battery.device.simulator.DevelopmentCollectorSimulation.DEFAULT_SAMPLE_INTERVAL_MS, channels, List.of());
}
private CollectorChannelState collectorChannelState(Object value)
@@ -453,7 +167,9 @@
JSONObject object = jsonObject(value);
List<CollectorMetricState> metrics = new ArrayList<>();
requiredArray(object, "metrics").forEach(item -> metrics.add(collectorMetricState(item)));
- return new CollectorChannelState(object.getString("channelCode"), metrics);
+ return new CollectorChannelState(object.getString("channelCode"), metrics,
+ object.getString("workStatus") == null ? DeviceWorkStatus.WORKING
+ : DeviceWorkStatus.valueOf(object.getString("workStatus")));
}
private CollectorMetricState collectorMetricState(Object value)
@@ -462,60 +178,6 @@
BigDecimal measurement = object.getBigDecimal("value");
if (measurement == null) throw new IllegalStateException("Missing BTS simulator state field: value");
return new CollectorMetricState(object.getString("metricCode"), measurement, object.getString("unit"));
- }
-
- private CollectorMeasurementRecord collectorMeasurementRecord(Object value)
- {
- JSONObject object = jsonObject(value);
- BigDecimal measurement = object.getBigDecimal("value");
- if (measurement == null) throw new IllegalStateException("Missing BTS simulator state field: value");
- return new CollectorMeasurementRecord(object.getString("adapterCode"), object.getString("collectorCode"),
- object.getString("channelCode"), requiredLong(object, "sequenceId"),
- requiredInstant(object, "deviceTime"), object.getString("deviceTimeRaw"),
- requiredInstant(object, "serverReceivedAt"), object.getString("metricCode"),
- measurement, object.getString("unit"));
- }
-
- private CollectorRealtimeMeasurement collectorRealtimeMeasurement(Object value)
- {
- JSONObject object = jsonObject(value);
- BigDecimal measurement = object.getBigDecimal("value");
- if (measurement == null) throw new IllegalStateException("Missing BTS simulator state field: value");
- return new CollectorRealtimeMeasurement(object.getString("adapterCode"), object.getString("collectorCode"),
- object.getString("channelCode"), requiredLong(object, "sequenceId"),
- requiredInstant(object, "deviceTime"), object.getString("deviceTimeRaw"),
- requiredInstant(object, "serverReceivedAt"), object.getString("metricCode"),
- measurement, object.getString("unit"));
- }
-
- /** 从尚未拆分快照模型的旧状态中提取最新测量值。 */
- private List<CollectorRealtimeMeasurement> latestMeasurementsFromLegacyHistory(
- List<CollectorMeasurementRecord> history, List<CollectorChannelState> channels)
- {
- java.util.Set<String> configured = new java.util.HashSet<>();
- channels.forEach(channel -> channel.metrics().forEach(metric ->
- configured.add(channel.channelCode() + "\u0000" + metric.metricCode())));
- java.util.Map<String, CollectorRealtimeMeasurement> latest = new java.util.LinkedHashMap<>();
- for (CollectorMeasurementRecord record : history)
- {
- String key = record.channelCode() + "\u0000" + record.metricCode();
- if (configured.contains(key))
- latest.put(key, new CollectorRealtimeMeasurement(record.adapterCode(), record.collectorCode(),
- record.channelCode(), record.sequenceId(), record.deviceTime(), record.deviceTimeRaw(),
- record.serverReceivedAt(), record.metricCode(), record.value(), record.unit()));
- }
- return List.copyOf(latest.values());
- }
-
- private CollectorHistoryFaults collectorHistoryFaults(Object value)
- {
- if (value == null) return CollectorHistoryFaults.none();
- JSONObject object = jsonObject(value);
- List<Long> missing = new ArrayList<>();
- JSONArray values = object.getJSONArray("missingSequences");
- if (values != null) values.forEach(item -> missing.add(Long.valueOf(item.toString())));
- return new CollectorHistoryFaults(missing, object.getLong("duplicateSequence"),
- object.getLong("conflictingSequence"), object.getBooleanValue("outOfOrder"));
}
private ChannelState channelState(Object value)
@@ -534,13 +196,6 @@
requiredInteger(object, "channelId"), requiredInteger(object, "remainingDiscoveries"));
}
- private long requiredLong(JSONObject object, String field)
- {
- Long value = object.getLong(field);
- if (value == null) throw new IllegalStateException("Missing BTS simulator state field: " + field);
- return value;
- }
-
private BigDecimal requiredDecimal(JSONObject object, String field)
{
BigDecimal value = object.getBigDecimal(field);
@@ -548,26 +203,5 @@
return value;
}
- private Instant requiredInstant(JSONObject object, String field)
- {
- Instant value = optionalInstant(object, field);
- if (value == null) throw new IllegalStateException("Missing BTS simulator state field: " + field);
- return value;
- }
- private Instant optionalInstant(JSONObject object, String field)
- {
- 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); }
- }
}
--
Gitblit v1.7.1