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