From 56cddf53e7e71afad29885d7fcf773a222b95f6c Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 30 八月 2026 19:38:18 +0800
Subject: [PATCH] 修复采集器断线区间重复关闭

---
 ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java                             |    8 ++++++++
 ruoyi-admin/src/main/resources/db/migration/V19__collector_disconnect_open_dedupe.sql                   |   11 +++++++++++
 .gitignore                                                                                              |    1 +
 ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/CollectorDisconnectRecoveryServiceImplTest.java |   28 ++++++++++++++++++++++++++++
 4 files changed, 48 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore
index f18146e..47809a8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -13,5 +13,6 @@
 !/ruoyi-admin/src/main/resources/db/migration/V7__complete_device_registration_boundary.sql
 !/ruoyi-admin/src/main/resources/db/migration/V8__complete_collector_page_contract.sql
 !/ruoyi-admin/src/main/resources/db/migration/V9__move_discovery_candidates_to_redis.sql
+!/ruoyi-admin/src/main/resources/db/migration/V19__collector_disconnect_open_dedupe.sql
 /.mvn-review-repository/
 /ruoyi-admin/logs/
diff --git a/ruoyi-admin/src/main/resources/db/migration/V19__collector_disconnect_open_dedupe.sql b/ruoyi-admin/src/main/resources/db/migration/V19__collector_disconnect_open_dedupe.sql
new file mode 100644
index 0000000..f138caa
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration/V19__collector_disconnect_open_dedupe.sql
@@ -0,0 +1,11 @@
+-- Ticket11:唯一约束只覆盖开放中的采集器断线区间,关闭区间保留为历史追加记录。
+ALTER TABLE t_test_collector_exception_interval
+    DROP INDEX uk_collector_exception_open,
+    ADD COLUMN open_dedupe_key VARCHAR(320)
+        GENERATED ALWAYS AS (
+            CASE WHEN interval_status = 'OPEN' THEN
+                CONCAT(test_id, ':', battery_relation_id, ':', collector_id, ':',
+                       collector_channel_code, ':', exception_type)
+            ELSE NULL END
+        ) STORED AFTER interval_status,
+    ADD UNIQUE KEY uk_collector_exception_open (open_dedupe_key);
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java b/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java
index 5106b4b..0473fd3 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java
@@ -135,6 +135,14 @@
         assertTrue(v18.contains("uk_collector_exception_open"));
         assertFalse(v18.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
 
+        String v19 = resource("db/migration/V19__collector_disconnect_open_dedupe.sql");
+        assertTrue(v19.contains("DROP INDEX uk_collector_exception_open"));
+        assertTrue(v19.contains("open_dedupe_key VARCHAR(320)"));
+        assertTrue(v19.contains("interval_status = 'OPEN'"));
+        assertTrue(v19.contains("ELSE NULL"));
+        assertTrue(v19.contains("ADD UNIQUE KEY uk_collector_exception_open (open_dedupe_key)"));
+        assertFalse(v19.matches("(?s).*\\bDROP\\s+TABLE\\b.*"));
+
         String testMapper = resource("mapper/system/TTestMapper.xml");
         assertTrue(testMapper.contains("selectReceivingChannelsForAdapter"));
         assertTrue(testMapper.contains("RECEPTION_RESUMED"));
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/CollectorDisconnectRecoveryServiceImplTest.java b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/CollectorDisconnectRecoveryServiceImplTest.java
index dd11996..1c2c636 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/CollectorDisconnectRecoveryServiceImplTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/CollectorDisconnectRecoveryServiceImplTest.java
@@ -56,6 +56,34 @@
         verify(mapper, never()).insertCollectorExceptionInterval(any());
     }
 
+    @Test
+    void allowsAnotherDisconnectCycleAfterPreviousIntervalWasClosed()
+    {
+        TTestCollectorSource source = source(10L, 20L, 8L, "COL-1", "S-01");
+        TTestCollectorExceptionInterval first = openInterval(99L);
+        TTestCollectorExceptionInterval second = openInterval(100L);
+        when(mapper.selectReceivingCollectorSources()).thenReturn(List.of(source));
+        when(mapper.selectOpenCollectorException(10L, 20L, 8L, "S-01"))
+                .thenReturn(null, first, null, second);
+
+        service.reconcile(Set.of(), Instant.parse("2026-08-30T09:00:00Z"));
+        service.reconcile(Set.of("COL-1"), Instant.parse("2026-08-30T09:01:00Z"));
+        service.reconcile(Set.of(), Instant.parse("2026-08-30T09:02:00Z"));
+        service.reconcile(Set.of("COL-1"), Instant.parse("2026-08-30T09:03:00Z"));
+
+        verify(mapper, org.mockito.Mockito.times(2)).insertCollectorExceptionInterval(any());
+        verify(mapper).closeCollectorExceptionInterval(eq(99L), any(), any());
+        verify(mapper).closeCollectorExceptionInterval(eq(100L), any(), any());
+    }
+
+    private TTestCollectorExceptionInterval openInterval(Long intervalId)
+    {
+        TTestCollectorExceptionInterval interval = new TTestCollectorExceptionInterval();
+        interval.setIntervalId(intervalId);
+        interval.setIntervalStatus("OPEN");
+        return interval;
+    }
+
     private TTestCollectorSource source(Long testId, Long relationId, Long collectorId,
             String collectorCode, String channelCode)
     {

--
Gitblit v1.7.1