package com.battery.business.runtime;
|
|
import java.math.BigDecimal;
|
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 org.springframework.context.annotation.Profile;
|
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSONArray;
|
import com.alibaba.fastjson2.JSONObject;
|
import com.battery.device.simulator.BtsSimulatorStateStore;
|
import com.battery.device.simulator.BtsSimulatorStateStore.BtsDeviceProfile;
|
import com.battery.device.simulator.BtsSimulatorStateStore.ChannelConfiguration;
|
import com.battery.device.simulator.BtsSimulatorStateStore.SimulatorPlanStep;
|
import com.battery.device.api.model.DeviceWorkStatus;
|
|
/** 将开发模拟器完整状态写入 Redis,主状态不设置普通 TTL。 */
|
@Component
|
@Profile("dev-simulator")
|
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;
|
}
|
|
/** 读取模拟器主状态,不存在时原子写入默认拓扑;并发初始化时以 Redis 中先写入的那份为准。 */
|
@Override
|
public SimulatorState loadOrInitialize(SimulatorState defaultState)
|
{
|
Object stored = redisTemplate.opsForValue().get(STATE_KEY);
|
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 restoreState(stored);
|
}
|
|
/** 整体覆盖模拟器主状态;该键不设普通 TTL。 */
|
@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));
|
}
|
|
/** 覆盖保存最近请求,整键按小时级 TTL 自动过期。 */
|
@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 restoreState(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(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,
|
root.getBoolean("softwareAccessible"), root.getLongValue("observationSequence"));
|
return state;
|
}
|
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)));
|
BtsDeviceProfile profile = object.containsKey("profile") ? deviceProfile(object.getJSONObject("profile")) : null;
|
return new DeviceState(requiredInteger(object, "deviceId"), object.getString("ip"),
|
object.getBooleanValue("online"), channels, profile);
|
}
|
|
private BtsDeviceProfile deviceProfile(JSONObject object)
|
{
|
if (object == null) throw new IllegalStateException("Invalid BTS simulator device profile");
|
return new BtsDeviceProfile(requiredInteger(object, "devtype"), requiredInteger(object, "subDeviceId"),
|
object.getString("name"), object.getString("manufacturer"), object.getString("model"),
|
object.getString("serialNumber"), object.getString("firmwareVersion"),
|
requiredInteger(object, "channelCount"), requiredDecimal(object, "currentMinA"),
|
requiredDecimal(object, "currentMaxA"), requiredDecimal(object, "voltageMinV"),
|
requiredDecimal(object, "voltageMaxV"), requiredInteger(object, "sampleIntervalMs"));
|
}
|
|
private CollectorState collectorState(JSONObject object)
|
{
|
int count = requiredInteger(object, "channelCount");
|
List<CollectorChannelState> channels = new ArrayList<>();
|
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)
|
{
|
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,
|
object.getString("workStatus") == null ? DeviceWorkStatus.WORKING
|
: DeviceWorkStatus.valueOf(object.getString("workStatus")));
|
}
|
|
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 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");
|
JSONObject configuration = object.getJSONObject("configuration");
|
return new ChannelState(requiredInteger(object, "channelId"), object.getBooleanValue("online"),
|
DeviceWorkStatus.valueOf(workStatus), channelConfiguration(configuration));
|
}
|
|
/** Redis 通用反序列化会把泛型 steps 恢复为 JSONObject,不能让 record 构造器直接强转。 */
|
private ChannelConfiguration channelConfiguration(JSONObject object)
|
{
|
if (object == null) return null;
|
JSONArray configuredSteps = requiredArray(object, "steps");
|
List<SimulatorPlanStep> steps = new ArrayList<>();
|
configuredSteps.forEach(item -> {
|
JSONObject step = jsonObject(item);
|
steps.add(new SimulatorPlanStep(step.getString("name"), step.getString("stepType"),
|
step.getDoubleValue("durationSeconds"), step.getDoubleValue("voltageV")));
|
});
|
return new ChannelConfiguration(requiredDecimal(object, "currentA"), requiredDecimal(object, "voltageV"),
|
requiredDecimal(object, "capacityAh"), requiredDecimal(object, "energyWh"),
|
requiredInteger(object, "cycleNo"), requiredInteger(object, "stepNo"),
|
requiredInteger(object, "plannedCycleCount"), object.getLongValue("totalTimeSeconds"),
|
object.getLongValue("relativeTimeSeconds"), steps);
|
}
|
|
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 BigDecimal requiredDecimal(JSONObject object, String field)
|
{
|
BigDecimal value = object.getBigDecimal(field);
|
if (value == null) throw new IllegalStateException("Missing BTS simulator state field: " + field);
|
return value;
|
}
|
|
|
}
|