package com.ruoyi.config;
|
|
import java.math.BigDecimal;
|
import java.time.Instant;
|
import java.util.ArrayList;
|
import java.util.List;
|
import java.util.concurrent.TimeUnit;
|
import org.springframework.data.redis.core.RedisTemplate;
|
import org.springframework.stereotype.Component;
|
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONObject;
|
import com.ruoyi.device.simulator.BtsSimulatorStateStore;
|
import com.ruoyi.device.model.DeviceWorkStatus;
|
|
/** 将开发模拟器完整状态写入 Redis,主状态不设置普通 TTL。 */
|
@Component
|
public class RedisBtsSimulatorStateStore implements BtsSimulatorStateStore
|
{
|
static final String STATE_KEY = "dev:bts-simulator:state:v1";
|
static final String RECENT_REQUESTS_KEY = "dev:bts-simulator:recent-requests:v1";
|
private static final long RECENT_REQUESTS_TTL_HOURS = 1;
|
|
private final RedisTemplate<Object, Object> redisTemplate;
|
|
public RedisBtsSimulatorStateStore(RedisTemplate<Object, Object> redisTemplate)
|
{
|
this.redisTemplate = redisTemplate;
|
}
|
|
@Override
|
public SimulatorState loadOrInitialize(SimulatorState defaultState)
|
{
|
Object stored = redisTemplate.opsForValue().get(STATE_KEY);
|
if (stored != null) return parseState(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);
|
}
|
|
@Override
|
public void save(SimulatorState state)
|
{
|
redisTemplate.opsForValue().set(STATE_KEY, JSON.toJSONString(state));
|
}
|
|
@Override
|
public List<RecentRequest> loadRecentRequests()
|
{
|
Object stored = redisTemplate.opsForValue().get(RECENT_REQUESTS_KEY);
|
if (stored == null) return List.of();
|
return List.copyOf(JSON.parseArray(stored.toString(), RecentRequest.class));
|
}
|
|
@Override
|
public void saveRecentRequests(List<RecentRequest> requests)
|
{
|
redisTemplate.opsForValue().set(RECENT_REQUESTS_KEY, JSON.toJSONString(requests),
|
RECENT_REQUESTS_TTL_HOURS, TimeUnit.HOURS);
|
}
|
|
@Override
|
public void reset(SimulatorState defaultState)
|
{
|
redisTemplate.delete(RECENT_REQUESTS_KEY);
|
save(defaultState);
|
}
|
|
private SimulatorState parseState(Object stored)
|
{
|
try
|
{
|
// 通用 RedisTemplate 在可执行包中可能将泛型数组恢复为 JSONObject,必须在存储边界逐层重建。
|
JSONObject root = jsonObject(stored);
|
Integer version = root.getInteger("version");
|
if (version == null || version != CURRENT_VERSION)
|
throw new IllegalStateException("Unsupported BTS simulator state version: " + version);
|
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<ChannelTombstone> tombstones = new ArrayList<>();
|
requiredArray(root, "tombstones").forEach(item -> tombstones.add(channelTombstone(item)));
|
return new SimulatorState(version, devices, collectors,
|
requiredInteger(root, "nextDeviceId"), requiredInteger(root, "nextCollectorId"), tombstones);
|
}
|
catch (IllegalStateException exception)
|
{
|
throw exception;
|
}
|
catch (Exception exception)
|
{
|
throw new IllegalStateException("Invalid BTS simulator state in Redis", exception);
|
}
|
}
|
|
private JSONObject jsonObject(Object value)
|
{
|
if (value instanceof JSONObject object) return object;
|
if (value instanceof String text)
|
{
|
Object parsed = JSON.parse(text);
|
if (parsed instanceof String nested) parsed = JSON.parse(nested);
|
if (parsed instanceof JSONObject object) return object;
|
return JSON.parseObject(JSON.toJSONString(parsed));
|
}
|
return JSON.parseObject(JSON.toJSONString(value));
|
}
|
|
private JSONArray requiredArray(JSONObject object, String field)
|
{
|
JSONArray array = object.getJSONArray(field);
|
if (array == null) throw new IllegalStateException("Missing BTS simulator state field: " + field);
|
return array;
|
}
|
|
private int requiredInteger(JSONObject object, String field)
|
{
|
Integer value = object.getInteger(field);
|
if (value == null) throw new IllegalStateException("Missing BTS simulator state field: " + field);
|
return value;
|
}
|
|
private DeviceState deviceState(Object value)
|
{
|
JSONObject object = jsonObject(value);
|
List<ChannelState> channels = new ArrayList<>();
|
requiredArray(object, "channels").forEach(item -> channels.add(channelState(item)));
|
return new DeviceState(requiredInteger(object, "deviceId"), object.getString("ip"),
|
object.getBooleanValue("online"), channels);
|
}
|
|
private CollectorState collectorState(Object value)
|
{
|
JSONObject object = jsonObject(value);
|
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");
|
// Ticket04 状态只包含发现配置;首次恢复时以演示默认值补齐采样状态。
|
if (!object.containsKey("sampleIntervalMs"))
|
return new CollectorState(collectorId, deviceCode, ip, mac, name, manufacturer, model,
|
channelCount, online);
|
|
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));
|
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")));
|
}
|
|
private CollectorChannelState collectorChannelState(Object value)
|
{
|
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);
|
}
|
|
private CollectorMetricState collectorMetricState(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 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"));
|
}
|
|
/** 兼容 Ticket06 开发期间已写入但尚未拆分快照模型的状态。 */
|
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)
|
{
|
JSONObject object = jsonObject(value);
|
String workStatus = object.getString("workStatus");
|
if (workStatus == null) throw new IllegalStateException("Missing BTS simulator state field: workStatus");
|
return new ChannelState(requiredInteger(object, "channelId"), object.getBooleanValue("online"),
|
DeviceWorkStatus.valueOf(workStatus));
|
}
|
|
private ChannelTombstone channelTombstone(Object value)
|
{
|
JSONObject object = jsonObject(value);
|
return new ChannelTombstone(requiredInteger(object, "deviceId"), object.getString("ip"),
|
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 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);
|
}
|
}
|