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 redisTemplate; public RedisBtsSimulatorStateStore(RedisTemplate 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 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 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 devices = new ArrayList<>(); requiredArray(root, "devices").forEach(item -> devices.add(deviceState(item))); List collectors = new ArrayList<>(); requiredArray(root, "collectors").forEach(item -> collectors.add(collectorState(item))); List 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 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 channels = new ArrayList<>(); requiredArray(object, "channels").forEach(item -> channels.add(collectorChannelState(item))); List history = new ArrayList<>(); requiredArray(object, "history").forEach(item -> history.add(collectorMeasurementRecord(item))); return new CollectorState(collectorId, deviceCode, ip, mac, name, manufacturer, model, channelCount, online, requiredInteger(object, "sampleIntervalMs"), requiredLong(object, "nextSequence"), optionalInstant(object, "lastSampledAt"), channels, history, collectorHistoryFaults(object.get("historyFaults"))); } private CollectorChannelState collectorChannelState(Object value) { JSONObject object = jsonObject(value); List 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 CollectorHistoryFaults collectorHistoryFaults(Object value) { if (value == null) return CollectorHistoryFaults.none(); JSONObject object = jsonObject(value); List 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); } }