From e38aca8134570513435c003c25dc05f1ae51c9c9 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 08 十月 2026 13:56:28 +0800
Subject: [PATCH] 实现 Ticket04:采集器辅助测量与登记入口,拆除开发模拟器

---
 ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java |  172 +++++++++++++++++++++++++++++++++++++++------------------
 1 files changed, 118 insertions(+), 54 deletions(-)

diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java
index 3aef6e8..f132f1f 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java
@@ -3,6 +3,7 @@
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
 import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
@@ -24,12 +25,15 @@
 import org.mockito.junit.jupiter.MockitoExtension;
 import com.ruoyi.common.exception.ServiceException;
 import com.battery.business.domain.TCollector;
+import com.battery.business.domain.TDeviceAdapter;
 import com.battery.business.mapper.TCollectorMapper;
-import com.battery.device.api.discovery.InMemoryDeviceDiscoveryCandidateStore;
-import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate;
+import com.battery.business.mapper.TDeviceAdapterMapper;
 
 /**
  * 采集器 Service 单元测试。
+ *
+ * <p>采集器按 ADR-0064 不建独立设备模型:登记不再依赖发现候选(真实来源不产出采集器候选),
+ * 由操作员手工登记来源身份,在线状态取所链接来源实例的连接结果。
  */
 @ExtendWith(MockitoExtension.class)
 class TCollectorServiceImplTest
@@ -37,20 +41,17 @@
     @Mock
     private TCollectorMapper collectorMapper;
 
+    @Mock
+    private TDeviceAdapterMapper adapterMapper;
+
     private TCollectorServiceImpl collectorService;
-    private InMemoryDeviceDiscoveryCandidateStore candidateStore;
 
     private TCollector collector;
 
     @BeforeEach
     void setUp()
     {
-        candidateStore = new InMemoryDeviceDiscoveryCandidateStore();
-        collectorService = new TCollectorServiceImpl(collectorMapper, candidateStore);
-        java.time.Instant now = java.time.Instant.now();
-        candidateStore.replaceCollectors("dev-sim", java.util.List.of(new CollectorCandidate(
-                "dev-sim", "COL-001", "192.168.1.10", "AA:BB:CC:DD:EE:FF", "一号采集器",
-                "开发模拟器", "Virtual Collector", 8, true, now, now.plusSeconds(60))));
+        collectorService = new TCollectorServiceImpl(collectorMapper, adapterMapper);
         collector = new TCollector();
         collector.setCollectorId(1L);
         collector.setCollectorCode("COL-001");
@@ -80,21 +81,52 @@
         }
     }
 
+    /** 在线事实只有一个来源:所链接来源实例的连接结果;登记表里的存储列不再充当这个事实。 */
     @Test
-    void registeredCollectorCodeSqlShouldIgnoreDeletedRows() throws IOException
+    void onlineStatusSqlShouldDeriveFromLinkedAdapter() throws IOException
     {
         try (InputStream input = getClass().getResourceAsStream(
                 "/mapper/battery/TCollectorMapper.xml"))
         {
             assertNotNull(input);
             String mapperXml = new String(input.readAllBytes(), StandardCharsets.UTF_8);
-            int selectStart = mapperXml.indexOf("<select id=\"selectRegisteredCollectorCodes\"");
-            assertTrue(selectStart >= 0);
-            int selectEnd = mapperXml.indexOf("</select>", selectStart);
-            String selectSql = mapperXml.substring(selectStart, selectEnd);
-
-            assertTrue(selectSql.contains("where del_flag = '0'"));
+            int fragmentStart = mapperXml.indexOf("<sql id=\"derivedOnlineStatus\"");
+            assertTrue(fragmentStart >= 0, "在线状态的派生表达式应当是一个共用片段");
+            String fragment = mapperXml.substring(fragmentStart, mapperXml.indexOf("</sql>", fragmentStart));
+            assertTrue(fragment.contains("from t_device_adapter adapter_fact"), fragment);
+            // 存储列不再参与:只有派生的那个表达式。
+            assertFalse(mapperXml.contains("upper(c.online_status)"));
+            // SELECT 与 WHERE 各用一次(WHERE 里不能用别名,所以走同一个片段)。
+            assertEquals(2, mapperXml.split("<include refid=\"derivedOnlineStatus\" />", -1).length - 1);
         }
+    }
+
+    /** 登记不再要求发现候选:手工填写的来源身份即可落库。 */
+    @Test
+    void insertCollectorShouldNotRequireADiscoveryCandidate()
+    {
+        collector.setCollectorId(null);
+        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
+
+        assertEquals(1, collectorService.insertCollector(collector));
+
+        verify(collectorMapper).insertCollector(any(TCollector.class));
+    }
+
+    @Test
+    void insertCollectorShouldAcceptCollectorWithoutIpAndMac()
+    {
+        collector.setCollectorId(null);
+        collector.setIpAddress(null);
+        collector.setMacAddress(null);
+        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
+
+        assertEquals(1, collectorService.insertCollector(collector));
+
+        ArgumentCaptor<TCollector> captor = ArgumentCaptor.forClass(TCollector.class);
+        verify(collectorMapper).insertCollector(captor.capture());
+        assertNull(captor.getValue().getIpAddress());
+        assertNull(captor.getValue().getMacAddress());
     }
 
     @Test
@@ -117,6 +149,55 @@
         assertEquals("0", saved.getTestStatus());
         assertEquals("0", saved.getStatus());
         assertEquals("0", saved.getDelFlag());
+    }
+
+    @Test
+    void insertCollectorShouldRejectMalformedIpAndMacInsteadOfGuessing()
+    {
+        collector.setCollectorId(null);
+        collector.setIpAddress("invalid-host");
+        collector.setMacAddress("AA:BB:CC:DD:EE:FF");
+
+        ServiceException ip = assertThrows(ServiceException.class,
+                () -> collectorService.insertCollector(collector));
+        assertEquals("IP地址格式不正确", ip.getMessage());
+
+        collector.setIpAddress("192.168.1.10");
+        collector.setMacAddress("invalid-mac");
+        ServiceException mac = assertThrows(ServiceException.class,
+                () -> collectorService.insertCollector(collector));
+        assertEquals("MAC地址格式不正确", mac.getMessage());
+        verify(collectorMapper, never()).insertCollector(any(TCollector.class));
+    }
+
+    @Test
+    void insertCollectorShouldRejectUnknownLinkedSource()
+    {
+        collector.setCollectorId(null);
+        collector.setAdapterId(9L);
+        when(adapterMapper.selectDeviceAdapterById(9L)).thenReturn(null);
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> collectorService.insertCollector(collector));
+
+        assertEquals("所链接的数据来源不存在", exception.getMessage());
+        verify(collectorMapper, never()).insertCollector(any(TCollector.class));
+    }
+
+    /** 链接到来源实例后,登记保存该链接:采集器的辅助测量由它提供。 */
+    @Test
+    void insertCollectorShouldPersistLinkedSource()
+    {
+        collector.setCollectorId(null);
+        collector.setAdapterId(3L);
+        when(adapterMapper.selectDeviceAdapterById(3L)).thenReturn(new TDeviceAdapter());
+        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
+
+        assertEquals(1, collectorService.insertCollector(collector));
+
+        ArgumentCaptor<TCollector> captor = ArgumentCaptor.forClass(TCollector.class);
+        verify(collectorMapper).insertCollector(captor.capture());
+        assertEquals(Long.valueOf(3L), captor.getValue().getAdapterId());
     }
 
     @Test
@@ -148,36 +229,6 @@
     }
 
     @Test
-    void insertCollectorShouldValidateAuthoritativeCandidateTopology()
-    {
-        collector.setCollectorId(null);
-        collector.setIpAddress("invalid-host");
-        collector.setMacAddress("invalid-mac");
-        collector.setChannelCount(99);
-        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
-
-        assertEquals(1, collectorService.insertCollector(collector));
-
-        ArgumentCaptor<TCollector> captor = ArgumentCaptor.forClass(TCollector.class);
-        verify(collectorMapper).insertCollector(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());
-    }
-
-    @Test
-    void offlineCollectorCanBeRegisteredWithoutChangingOnlineFact()
-    {
-        java.time.Instant now = java.time.Instant.now();
-        candidateStore.replaceCollectors("dev-sim", java.util.List.of(new CollectorCandidate(
-                "dev-sim", "COL-001", "192.168.1.10", "AA:BB:CC:DD:EE:FF", "一号采集器",
-                "开发模拟器", "Virtual Collector", 8, false, now, now.plusSeconds(60))));
-        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
-        assertEquals(1, collectorService.insertCollector(collector));
-        verify(collectorMapper).updateRegisteredObservation("COL-001", "OFFLINE");
-    }
-
-    @Test
     void insertCollectorShouldRejectDuplicateCode()
     {
         collector.setCollectorId(null);
@@ -203,6 +254,27 @@
         assertEquals(1, collectorService.updateCollector(collector));
 
         verify(collectorMapper).updateCollector(collector);
+    }
+
+    /** 编辑只提交可改字段:未提交的 IP/MAC 不能被当成"清空"。 */
+    @Test
+    void updateCollectorShouldKeepUnsubmittedIpAndMac()
+    {
+        when(collectorMapper.selectCollectorById(1L)).thenReturn(collector);
+        when(collectorMapper.selectCollectorByCode("COL-001")).thenReturn(collector);
+        when(collectorMapper.selectCollectorByMacAddress("AA:BB:CC:DD:EE:FF")).thenReturn(collector);
+        when(collectorMapper.updateCollector(any(TCollector.class))).thenReturn(1);
+        TCollector edit = new TCollector();
+        edit.setCollectorId(1L);
+        edit.setCollectorName("改名后");
+
+        assertEquals(1, collectorService.updateCollector(edit));
+
+        ArgumentCaptor<TCollector> captor = ArgumentCaptor.forClass(TCollector.class);
+        verify(collectorMapper).updateCollector(captor.capture());
+        assertEquals("192.168.1.10", captor.getValue().getIpAddress());
+        assertEquals("AA:BB:CC:DD:EE:FF", captor.getValue().getMacAddress());
+        assertEquals("改名后", captor.getValue().getCollectorName());
     }
 
     @Test
@@ -256,14 +328,6 @@
         assertEquals(1, collectorService.updateCollectorStatus(command));
 
         verify(collectorMapper, never()).updateCollectorStatus(any(TCollector.class));
-    }
-
-    @Test
-    void selectRegisteredCollectorCodesShouldReturnUndeletedCodes()
-    {
-        when(collectorMapper.selectRegisteredCollectorCodes()).thenReturn(java.util.List.of("COL-001"));
-
-        assertEquals(java.util.Set.of("COL-001"), collectorService.selectRegisteredCollectorCodes());
     }
 
     @Test

--
Gitblit v1.7.1