From 210663040bf65ee00b5f86545fdc31bd0356fddf Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 27 八月 2026 16:31:53 +0800
Subject: [PATCH] feat(device): persist simulator discovery in redis

---
 ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TDetectionCabinetServiceImplTest.java |  137 ++++++++++++++++++++++++++-------------------
 1 files changed, 80 insertions(+), 57 deletions(-)

diff --git a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TDetectionCabinetServiceImplTest.java b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TDetectionCabinetServiceImplTest.java
index 2305e44..9207f29 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TDetectionCabinetServiceImplTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TDetectionCabinetServiceImplTest.java
@@ -28,6 +28,11 @@
 import com.ruoyi.system.domain.TDetectionCabinet;
 import com.ruoyi.system.domain.vo.DiscoveredCabinetVo;
 import com.ruoyi.system.mapper.TDetectionCabinetMapper;
+import com.ruoyi.device.discovery.InMemoryDeviceDiscoveryCandidateStore;
+import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
+import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate;
+import com.ruoyi.device.model.ChannelIdentity;
+import com.ruoyi.device.model.DeviceWorkStatus;
 
 /**
  * 检测柜业务层单元测试,验证字段规范化、状态限制及唯一性校验等关键规则。
@@ -38,8 +43,8 @@
     @Mock
     private TDetectionCabinetMapper detectionCabinetMapper;
 
-    @InjectMocks
     private TDetectionCabinetServiceImpl detectionCabinetService;
+    private InMemoryDeviceDiscoveryCandidateStore candidateStore;
 
     private TDetectionCabinet detectionCabinet;
 
@@ -49,6 +54,9 @@
     @BeforeEach
     void setUp()
     {
+        candidateStore = new InMemoryDeviceDiscoveryCandidateStore();
+        detectionCabinetService = new TDetectionCabinetServiceImpl(detectionCabinetMapper, candidateStore);
+        candidateStore.recordCabinets(List.of(candidate("CABINET-001", true, DeviceWorkStatus.WORKING)));
         detectionCabinet = new TDetectionCabinet();
         detectionCabinet.setCabinetCode(" CABINET-001 ");
         detectionCabinet.setCabinetName(" 一号检测柜 ");
@@ -67,19 +75,15 @@
     @Test
     void insertDetectionCabinetShouldPromoteDiscoveredCandidate()
     {
-        TDetectionCabinet candidate = new TDetectionCabinet();
-        candidate.setCabinetId(7L);
-        candidate.setCabinetCode("CABINET-001");
-        candidate.setMacAddress("AA:BB:CC:DD:EE:FF");
-        candidate.setRegistrationStatus("DISCOVERED");
-        when(detectionCabinetMapper.selectByCabinetCode("CABINET-001")).thenReturn(candidate);
-        when(detectionCabinetMapper.selectByMacAddress("AA:BB:CC:DD:EE:FF")).thenReturn(candidate);
-        when(detectionCabinetMapper.registerDiscoveredCabinet(any(TDetectionCabinet.class))).thenReturn(1);
+        when(detectionCabinetMapper.insertDetectionCabinet(any(TDetectionCabinet.class))).thenAnswer(invocation -> {
+            invocation.getArgument(0, TDetectionCabinet.class).setCabinetId(7L);
+            return 1;
+        });
 
         int rows = detectionCabinetService.insertDetectionCabinet(detectionCabinet);
 
         ArgumentCaptor<TDetectionCabinet> captor = ArgumentCaptor.forClass(TDetectionCabinet.class);
-        verify(detectionCabinetMapper).registerDiscoveredCabinet(captor.capture());
+        verify(detectionCabinetMapper).insertDetectionCabinet(captor.capture());
         TDetectionCabinet savedCabinet = captor.getValue();
         assertEquals(1, rows);
         assertEquals(7L, savedCabinet.getCabinetId());
@@ -88,13 +92,13 @@
         assertEquals("192.168.1.10", savedCabinet.getIpAddress());
         assertEquals("AA:BB:CC:DD:EE:FF", savedCabinet.getMacAddress());
         assertEquals(new BigDecimal("120.50"), savedCabinet.getCapacity());
-        assertNull(savedCabinet.getOnlineStatus());
+        assertEquals("OFFLINE", savedCabinet.getOnlineStatus());
         assertEquals("0", savedCabinet.getStatus());
         assertNull(savedCabinet.getCurrentTest());
     }
 
     /**
-     * 验证设备选择查询不读取旧检测柜测试字段,并改由持久化通道状态聚合。
+     * 验证设备选择查询不再从 MySQL 读取发现候选。
      */
     @Test
     void discoveredCabinetSqlShouldNotReadLegacyTestColumns() throws IOException
@@ -104,17 +108,28 @@
         {
             assertNotNull(input);
             String mapperXml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
-            int selectStart = mapperXml.indexOf("<select id=\"selectDiscoveredCabinetList\"");
-            int selectEnd = mapperXml.indexOf("</select>", selectStart);
-            String selectSql = mapperXml.substring(selectStart, selectEnd);
-            String activeChannelSql = sqlFragment(mapperXml, "activeCabinetChannelExists");
+            assertFalse(mapperXml.contains("selectDiscoveredCabinetList"));
+            assertFalse(mapperXml.contains("registration_status = 'DISCOVERED'"));
+        }
+    }
 
-            assertFalse(selectSql.contains("cabinet.test_status"));
-            assertFalse(selectSql.contains("cabinet.current_test"));
-            assertTrue(selectSql.contains("activeCabinetChannelExists"));
-            assertTrue(activeChannelSql.contains("t_detection_channel"));
-            assertTrue(activeChannelSql.contains("work_status"));
-            assertTrue(selectSql.contains("registration_status = 'DISCOVERED'"));
+    /**
+     * 验证删除正式检测柜只取消人工登记,不删除适配器持续维护的设备事实。
+     */
+    @Test
+    void deleteDetectionCabinetSqlShouldReturnOnlineDeviceToDiscoveryPool() throws IOException
+    {
+        try (InputStream input = getClass().getResourceAsStream(
+                "/mapper/system/TDetectionCabinetMapper.xml"))
+        {
+            assertNotNull(input);
+            String mapperXml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
+            int deleteStart = mapperXml.indexOf("<update id=\"deleteDetectionCabinetById\"");
+            int deleteEnd = mapperXml.indexOf("</update>", deleteStart);
+            String deleteSql = mapperXml.substring(deleteStart, deleteEnd);
+
+            assertTrue(deleteSql.contains("set del_flag = cabinet_id"));
+            assertTrue(deleteSql.contains("registration_status = 'REGISTERED'"));
         }
     }
 
@@ -132,23 +147,24 @@
             int selectStart = mapperXml.indexOf("<sql id=\"selectDetectionCabinetVo\"");
             int selectEnd = mapperXml.indexOf("</sql>", selectStart);
             String selectSql = mapperXml.substring(selectStart, selectEnd);
-            String activeChannelSql = sqlFragment(mapperXml, "activeCabinetChannelExists");
-
             assertFalse(selectSql.contains("online_status, test_status"));
             assertFalse(selectSql.contains("current_test, status"));
-            assertTrue(selectSql.contains("activeCabinetChannelExists"));
-            assertTrue(activeChannelSql.contains("t_detection_channel"));
-            assertTrue(activeChannelSql.contains("work_status"));
             assertTrue(selectSql.contains("as test_status"));
             assertTrue(selectSql.contains("as current_test"));
+            String activeTestSql = sqlFragment(mapperXml, "activeCabinetTestExists");
+            assertTrue(activeTestSql.contains("t_test_channel_binding"));
+            assertTrue(activeTestSql.contains("t_detection_channel"));
+            assertTrue(activeTestSql.contains("test_fact.test_status"));
+            assertFalse(activeTestSql.contains("t_test_battery"));
+            assertFalse(activeTestSql.contains("battery_fact.cabinet_id"));
             int listStart = mapperXml.indexOf("<select id=\"selectDetectionCabinetList\"");
             int listEnd = mapperXml.indexOf("</select>", listStart);
             assertTrue(mapperXml.substring(listStart, listEnd).contains("registration_status = 'REGISTERED'"));
-            int registerStart = mapperXml.indexOf("<update id=\"registerDiscoveredCabinet\"");
-            int registerEnd = mapperXml.indexOf("</update>", registerStart);
+            int registerStart = mapperXml.indexOf("<insert id=\"insertDetectionCabinet\"");
+            int registerEnd = mapperXml.indexOf("</insert>", registerStart);
             String registerSql = mapperXml.substring(registerStart, registerEnd);
-            assertTrue(registerSql.contains("registration_status = 'REGISTERED'"));
-            assertTrue(registerSql.contains("create_by = #{createBy}"));
+            assertTrue(registerSql.contains("'REGISTERED'"));
+            assertTrue(registerSql.contains("#{createBy}"));
         }
     }
 
@@ -159,24 +175,25 @@
         return mapperXml.substring(start, end);
     }
 
+    private CabinetCandidate candidate(String deviceCode, boolean online, DeviceWorkStatus workStatus)
+    {
+        java.time.Instant observedAt = java.time.Instant.now();
+        List<ChannelCandidate> channels = java.util.stream.IntStream.rangeClosed(1, 8)
+                .mapToObj(channelId -> new ChannelCandidate(
+                        new ChannelIdentity("dev-sim", 22, 1, 1, channelId), online, workStatus, observedAt))
+                .toList();
+        return new CabinetCandidate("dev-sim", null, deviceCode, 22, 1, "192.168.1.10",
+                "AA:BB:CC:DD:EE:FF", "一号模拟柜", "开发模拟器", "BTS Virtual Cabinet",
+                channels, observedAt, observedAt.plusSeconds(60));
+    }
+
     /**
-     * 验证设备选择列表复用已持久化检测柜,并保持原前端字段和状态文本。
+     * 验证设备选择列表读取 Redis 候选,并保持原前端字段和状态文本。
      */
     @Test
     void selectDiscoveredCabinetListShouldMapPersistedCabinetFacts()
     {
-        TDetectionCabinet persisted = new TDetectionCabinet();
-        persisted.setCabinetCode("CABINET-001");
-        persisted.setIpAddress("192.168.1.10");
-        persisted.setMacAddress("AA:BB:CC:DD:EE:FF");
-        persisted.setCabinetName("一号模拟柜");
-        persisted.setManufacturer("开发模拟器");
-        persisted.setModel("BTS Virtual Cabinet");
-        persisted.setChannelCount(6);
-        persisted.setOnlineStatus("ONLINE");
-        persisted.setObservedWorkStatus("ACTIVE");
-        when(detectionCabinetMapper.selectDiscoveredCabinetList())
-                .thenReturn(List.of(persisted));
+        when(detectionCabinetMapper.selectRegisteredCabinetCodes()).thenReturn(List.of());
 
         List<DiscoveredCabinetVo> devices = detectionCabinetService.selectDiscoveredCabinetList();
 
@@ -190,7 +207,7 @@
         assertEquals("一号模拟柜", devices.get(0).getName());
         assertEquals("开发模拟器", devices.get(0).getManufacturer());
         assertEquals("BTS Virtual Cabinet", devices.get(0).getModel());
-        assertEquals(6, devices.get(0).getChannelCount());
+        assertEquals(8, devices.get(0).getChannelCount());
     }
 
     /**
@@ -208,34 +225,40 @@
                 () -> detectionCabinetService.insertDetectionCabinet(detectionCabinet));
 
         assertEquals("设备编码已存在", exception.getMessage());
-        verify(detectionCabinetMapper, never()).registerDiscoveredCabinet(any(TDetectionCabinet.class));
+        verify(detectionCabinetMapper, never()).insertDetectionCabinet(any(TDetectionCabinet.class));
     }
 
     @Test
     void insertDetectionCabinetShouldRequireDiscoveredCandidate()
     {
-        when(detectionCabinetMapper.selectByCabinetCode("CABINET-001")).thenReturn(null);
+        detectionCabinetService = new TDetectionCabinetServiceImpl(detectionCabinetMapper,
+                new InMemoryDeviceDiscoveryCandidateStore());
 
         ServiceException exception = assertThrows(ServiceException.class,
                 () -> detectionCabinetService.insertDetectionCabinet(detectionCabinet));
 
-        assertEquals("请先选择发现候选设备", exception.getMessage());
-        verify(detectionCabinetMapper, never()).registerDiscoveredCabinet(any());
+        assertEquals("请先选择在线的发现候选设备", exception.getMessage());
+        verify(detectionCabinetMapper, never()).insertDetectionCabinet(any());
     }
 
-    /**
-     * 验证非法 IP 地址会阻止新增。
-     */
     @Test
-    void insertDetectionCabinetShouldRejectInvalidIpAddress()
+    void insertDetectionCabinetShouldValidateAuthoritativeCandidateTopology()
     {
         detectionCabinet.setIpAddress("invalid-ip");
+        detectionCabinet.setMacAddress("invalid-mac");
+        detectionCabinet.setChannelCount(99);
+        when(detectionCabinetMapper.insertDetectionCabinet(any(TDetectionCabinet.class))).thenAnswer(invocation -> {
+            invocation.getArgument(0, TDetectionCabinet.class).setCabinetId(7L);
+            return 1;
+        });
 
-        ServiceException exception = assertThrows(ServiceException.class,
-                () -> detectionCabinetService.insertDetectionCabinet(detectionCabinet));
+        assertEquals(1, detectionCabinetService.insertDetectionCabinet(detectionCabinet));
 
-        assertEquals("IP地址格式不正确", exception.getMessage());
-        verify(detectionCabinetMapper, never()).registerDiscoveredCabinet(any(TDetectionCabinet.class));
+        ArgumentCaptor<TDetectionCabinet> captor = ArgumentCaptor.forClass(TDetectionCabinet.class);
+        verify(detectionCabinetMapper).insertDetectionCabinet(captor.capture());
+        assertEquals("192.168.1.10", captor.getValue().getIpAddress());
+        assertEquals("AA:BB:CC:DD:EE:FF", captor.getValue().getMacAddress());
+        assertEquals(8, captor.getValue().getChannelCount());
     }
 
     /**

--
Gitblit v1.7.1