From 9fed155adefdb73dd349ff99a897f2d2da5ea03e Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期五, 28 八月 2026 17:51:32 +0800
Subject: [PATCH] 修复:恢复 Redis 模拟器嵌套状态
---
ruoyi-admin/src/main/java/com/ruoyi/config/RedisBtsSimulatorStateStore.java | 77 +++++++++++++++++++++++++++++++++++++-
AGENTS.md | 10 ++--
ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java | 22 +++++++++++
3 files changed, 101 insertions(+), 8 deletions(-)
diff --git a/AGENTS.md b/AGENTS.md
index c57fbbe..7d475c2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -20,9 +20,9 @@
## 业务事务
-- 开始监测在一个事务内锁定全部绑定通道;任一通道不可用则整体失败。数据库唯一约束必须阻止并发占用。
-- 未观测运行前,负责人或管理员可以取消等待并释放通道;已观测运行后走异常处理或终止流程。
-- 所有通道正常结束后先进入内部“完成确认中”;只有补采完成且没有未解释数据缺口,才聚合为“检测完成”。
+- 开始接收在一个事务内校验全部检测柜和采集器在线可观测、必填映射完整及区间无冲突,并为各数据来源打开接收区间;任一条件失败则整体回滚。数据库约束或等价事务机制必须阻止并发区间重叠。
+- 检测柜通道允许运行中接入,人工结束接收后立即释放业务占用;同一次设备运行可依次分配给多个测试,但实际接收边界不得重叠。
+- 全部接收区间关闭后先进入内部“完成确认中”;只有所有来源补采完成且区间内没有未解释缺口、冲突或异常,才聚合为“检测完成”。
- “检测完成”和“已终止”是不可恢复终态。异常恢复、数据缺口处置、终止和负责人转交都追加审计。
- 相同“测试 + 通道 + 异常类型”在未恢复期间聚合首次时间、最近时间和次数;恢复后关闭,再次发生创建新事件。
@@ -37,7 +37,7 @@
## API 与实时流
- 普通提交和历史区间查询使用 REST;实时快照、状态变化和异常使用 `SseEmitter`。
-- SSE 只保留每测试每通道最新快照。状态变化和异常不可合并;发送失败关闭连接。重连先发当前快照,历史由 REST 查询。
+- SSE 只保留每测试每数据来源最新快照。设备状态、接收状态、测试状态和异常变化不可合并;发送失败关闭连接。重连先发当前事实,历史由 REST 查询。
- 权限由后端统一执行。创建人、负责人和管理员是独立字段和角色;不要依赖前端隐藏按钮实现授权。
- API 时间输出 ISO 8601 带时区,测量值同时返回明确单位。
@@ -47,4 +47,4 @@
- 运行设备模块测试:`mvn -pl ruoyi-device test`。
- 跨模块改动运行全量测试:`mvn test`。
- 验证可执行包:`mvn -pl ruoyi-admin -am package -DskipTests`。启动验证时使用可写的 `LOG_PATH`,完成后停止临时进程。
-
+- 浏览器主接缝由前端仓库 `npm run test:e2e:ticket04` 编排隔离 MySQL 8、Redis、完整可执行包和模拟器;不得连接共享开发库或复用已有服务端口。
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 fb14c00..f5fa5ae 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/config/RedisBtsSimulatorStateStore.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/config/RedisBtsSimulatorStateStore.java
@@ -1,12 +1,15 @@
package com.ruoyi.config;
+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
@@ -67,12 +70,19 @@
{
try
{
- String json = stored.toString();
- JSONObject root = JSON.parseObject(json);
+ // 通用 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);
- return JSON.parseObject(json, SimulatorState.class);
+ 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)
{
@@ -83,4 +93,65 @@
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);
+ return new CollectorState(requiredInteger(object, "collectorId"), object.getString("deviceCode"),
+ object.getString("ip"), object.getString("mac"), object.getString("name"),
+ object.getString("manufacturer"), object.getString("model"),
+ requiredInteger(object, "channelCount"), object.getBooleanValue("online"));
+ }
+
+ 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"));
+ }
}
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 b8884ef..94f9d19 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/config/RedisStateStoreTest.java
@@ -15,6 +15,8 @@
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.device.model.DeviceWorkStatus;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelState;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelTombstone;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.DeviceState;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState;
import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
@@ -40,6 +42,26 @@
}
@Test
+ void restoresNestedSimulatorStateFromRedisSerializer()
+ {
+ SimulatorState expected = new SimulatorState(1,
+ List.of(new DeviceState(2, "127.0.0.3", false,
+ List.of(new ChannelState(1, true, DeviceWorkStatus.WORKING)))),
+ List.of(new CollectorState(3, "dev-sim-collector-3", "127.0.1.3",
+ "02:43:00:00:00:03", "模拟采集器", "开发模拟器",
+ "Virtual Collector", 3, true)),
+ 3, 4, List.of(new ChannelTombstone(1, "127.0.0.2", 4, 2)));
+ FastJson2JsonRedisSerializer<Object> serializer = new FastJson2JsonRedisSerializer<>(Object.class);
+ AtomicReference<Object> stored = new AtomicReference<>(
+ serializer.deserialize(serializer.serialize(JSON.toJSONString(expected))));
+
+ SimulatorState restored = store(stored).loadOrInitialize(state(1, "127.0.0.2"));
+
+ assertEquals(expected, restored);
+ assertEquals(DeviceWorkStatus.WORKING, restored.devices().get(0).channels().get(0).workStatus());
+ }
+
+ @Test
void rejectsIncompatibleVersionWithoutOverwritingIt()
{
AtomicReference<Object> stored = new AtomicReference<>("{\"version\":2,\"devices\":[],"
--
Gitblit v1.7.1