From 2e75fb72c1ba5d3d055362b44e7b85c0ad3f5416 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 05 九月 2026 18:41:47 +0800
Subject: [PATCH] 按设备绑定关系查询关联测试与通道

---
 battery-business/src/main/resources/mapper/battery/DeviceRelatedTestMapper.xml                     |   30 +++++
 battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java                  |   19 +-
 battery-business/src/main/java/com/battery/business/mapper/DeviceRelatedTestMapper.java            |   14 ++
 battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCollectorVo.java           |   29 ----
 battery-business/src/main/java/com/battery/business/service/impl/TDetectionCabinetServiceImpl.java |   20 --
 ruoyi-admin/pom.xml                                                                                |    6 +
 ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java   |    2 
 battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java             |    6 
 battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java             |   22 +++
 battery-business/src/main/java/com/battery/business/domain/TCollector.java                         |   18 +-
 battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml                            |   10 -
 battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java        |    2 
 battery-business/src/main/resources/mapper/battery/TDetectionCabinetMapper.xml                     |    4 
 ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java             |  143 +++++++++++++++++++++++
 ruoyi-admin/src/test/java/com/battery/business/service/impl/TDetectionCabinetServiceImplTest.java  |   15 +-
 15 files changed, 254 insertions(+), 86 deletions(-)

diff --git a/battery-business/src/main/java/com/battery/business/domain/TCollector.java b/battery-business/src/main/java/com/battery/business/domain/TCollector.java
index 4ec573d..e610f70 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TCollector.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TCollector.java
@@ -6,6 +6,8 @@
 import jakarta.validation.constraints.NotNull;
 import jakarta.validation.constraints.Size;
 import java.time.Instant;
+import java.util.List;
+import com.battery.business.domain.vo.DeviceRelatedTestVo;
 import org.apache.commons.lang3.builder.ToStringBuilder;
 import org.apache.commons.lang3.builder.ToStringStyle;
 import com.ruoyi.common.core.domain.BaseEntity;
@@ -74,11 +76,11 @@
     /** 在线状态:0离线、1在线 */
     private String onlineStatus;
 
-    /** 测试状态展示值,由测试绑定事实派生,不写入采集器表。 */
+    /** 进行中测试关联标记,供启禁用和删除校验使用,不写入采集器表。 */
     private String testStatus;
 
-    /** 当前测试展示值,由测试绑定事实派生,不写入采集器表。 */
-    private String currentTest;
+    /** 关联测试与本采集器绑定通道,由现有绑定关系查询,不写入设备表。 */
+    private List<DeviceRelatedTestVo> relatedTests = List.of();
 
     /** 兼容旧页面的派生字段,不参与采集器登记持久化。 */
     private String currentSampleName;
@@ -227,14 +229,14 @@
         this.testStatus = testStatus;
     }
 
-    public String getCurrentTest()
+    public List<DeviceRelatedTestVo> getRelatedTests()
     {
-        return currentTest;
+        return relatedTests;
     }
 
-    public void setCurrentTest(String currentTest)
+    public void setRelatedTests(List<DeviceRelatedTestVo> relatedTests)
     {
-        this.currentTest = currentTest;
+        this.relatedTests = relatedTests;
     }
 
     public String getCurrentSampleName()
@@ -281,7 +283,7 @@
                 .append("channelCount", getChannelCount())
                 .append("onlineStatus", getOnlineStatus())
                 .append("testStatus", getTestStatus())
-                .append("currentTest", getCurrentTest())
+                .append("relatedTests", getRelatedTests())
                 .append("currentSampleName", getCurrentSampleName())
                 .append("status", getStatus())
                 .append("delFlag", getDelFlag())
diff --git a/battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java b/battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java
index dd6a3ae..52a462b 100644
--- a/battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java
+++ b/battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java
@@ -3,6 +3,8 @@
 import java.math.BigDecimal;
 import java.time.Instant;
 import java.util.Date;
+import java.util.List;
+import com.battery.business.domain.vo.DeviceRelatedTestVo;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import jakarta.validation.constraints.Max;
 import jakarta.validation.constraints.Min;
@@ -74,12 +76,11 @@
     /** 在线状态:0正常,1异常。 */
     private String onlineStatus;
 
-    /** 测试状态展示值,由通道绑定事实派生,不写入检测柜表。 */
+    /** 进行中测试关联标记,供启禁用和删除校验使用,不写入检测柜表。 */
     private String testStatus;
 
-    /** 当前测试展示值,由通道绑定事实派生,不写入检测柜表。 */
-    @Size(max = 200, message = "当前测试内容长度不能超过200个字符")
-    private String currentTest;
+    /** 关联测试与本柜绑定通道,由现有绑定关系查询,不写入检测柜表。 */
+    private List<DeviceRelatedTestVo> relatedTests = List.of();
 
     /** 启用状态:0启用,1停用。 */
     private String status;
@@ -231,14 +232,14 @@
         this.testStatus = testStatus;
     }
 
-    public String getCurrentTest()
+    public List<DeviceRelatedTestVo> getRelatedTests()
     {
-        return currentTest;
+        return relatedTests;
     }
 
-    public void setCurrentTest(String currentTest)
+    public void setRelatedTests(List<DeviceRelatedTestVo> relatedTests)
     {
-        this.currentTest = currentTest;
+        this.relatedTests = relatedTests;
     }
 
     public String getStatus()
@@ -306,7 +307,7 @@
                 ", channelCount=" + channelCount +
                 ", onlineStatus='" + onlineStatus + '\'' +
                 ", testStatus='" + testStatus + '\'' +
-                ", currentTest='" + currentTest + '\'' +
+                ", relatedTests=" + relatedTests +
                 ", status='" + status + '\'' +
                 ", delFlag='" + delFlag + '\'' +
                 ", device='" + device + '\'' +
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java
new file mode 100644
index 0000000..7a3b30b
--- /dev/null
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java
@@ -0,0 +1,22 @@
+package com.battery.business.domain.vo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** 设备关联的测试及该测试绑定的本设备通道,不写入设备表。 */
+public class DeviceRelatedTestVo
+{
+    private Long testId;
+    private String testName;
+    private String testStatus;
+    private List<String> channels = new ArrayList<>();
+
+    public Long getTestId() { return testId; }
+    public void setTestId(Long testId) { this.testId = testId; }
+    public String getTestName() { return testName; }
+    public void setTestName(String testName) { this.testName = testName; }
+    public String getTestStatus() { return testStatus; }
+    public void setTestStatus(String testStatus) { this.testStatus = testStatus; }
+    public List<String> getChannels() { return channels; }
+    public void setChannels(List<String> channels) { this.channels = channels; }
+}
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java
index 5473b3b..96d84de 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java
@@ -9,9 +9,7 @@
         String manufacturer,
         String model,
         Integer channelCount,
-        String onlineStatus,
-        String testStatus,
-        String currentTest)
+        String onlineStatus)
 {
     public String getDeviceCode() { return deviceCode; }
     public String getIp() { return ip; }
@@ -21,6 +19,4 @@
     public String getModel() { return model; }
     public Integer getChannelCount() { return channelCount; }
     public String getOnlineStatus() { return onlineStatus; }
-    public String getTestStatus() { return testStatus; }
-    public String getCurrentTest() { return currentTest; }
 }
diff --git a/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCollectorVo.java b/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCollectorVo.java
index 338599b..0d4a9c9 100644
--- a/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCollectorVo.java
+++ b/battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCollectorVo.java
@@ -29,18 +29,12 @@
     /** 在线状态 */
     private String onlineStatus;
 
-    /** 测试状态 */
-    private String testStatus;
-
-    /** 当前测试 */
-    private String currentTest;
-
     public DiscoveredCollectorVo()
     {
     }
 
     public DiscoveredCollectorVo(String deviceCode, String ip, String mac, String name, String manufacturer,
-            String model, Integer channelCount, String onlineStatus, String testStatus, String currentTest)
+            String model, Integer channelCount, String onlineStatus)
     {
         this.deviceCode = deviceCode;
         this.ip = ip;
@@ -50,8 +44,6 @@
         this.model = model;
         this.channelCount = channelCount;
         this.onlineStatus = onlineStatus;
-        this.testStatus = testStatus;
-        this.currentTest = currentTest;
     }
 
     public String getDeviceCode()
@@ -134,23 +126,4 @@
         this.onlineStatus = onlineStatus;
     }
 
-    public String getTestStatus()
-    {
-        return testStatus;
-    }
-
-    public void setTestStatus(String testStatus)
-    {
-        this.testStatus = testStatus;
-    }
-
-    public String getCurrentTest()
-    {
-        return currentTest;
-    }
-
-    public void setCurrentTest(String currentTest)
-    {
-        this.currentTest = currentTest;
-    }
 }
diff --git a/battery-business/src/main/java/com/battery/business/mapper/DeviceRelatedTestMapper.java b/battery-business/src/main/java/com/battery/business/mapper/DeviceRelatedTestMapper.java
new file mode 100644
index 0000000..fca199c
--- /dev/null
+++ b/battery-business/src/main/java/com/battery/business/mapper/DeviceRelatedTestMapper.java
@@ -0,0 +1,14 @@
+package com.battery.business.mapper;
+
+import java.util.List;
+import com.battery.business.domain.vo.DeviceRelatedTestVo;
+
+/** 从现有绑定关系读取设备关联测试。 */
+public interface DeviceRelatedTestMapper
+{
+    /** 查询检测柜关联的未删除测试,按测试去重并汇总本柜通道。 */
+    List<DeviceRelatedTestVo> selectCabinetRelatedTests(Long cabinetId);
+
+    /** 查询采集器关联的未删除测试,按测试去重并汇总本采集器通道。 */
+    List<DeviceRelatedTestVo> selectCollectorRelatedTests(Long collectorId);
+}
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java
index 95fd84a..8cf4d0b 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java
@@ -106,7 +106,7 @@
         collector.setOnlineStatus(OFFLINE);
         // 测试占用由 t_test_collector_channel + t_test 派生,采集器登记表不写入这些字段。
         collector.setTestStatus(IDLE);
-        collector.setCurrentTest(null);
+        collector.setRelatedTests(List.of());
         collector.setCurrentSampleName(null);
         if (StringUtils.isEmpty(collector.getStatus()))
         {
diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TDetectionCabinetServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/TDetectionCabinetServiceImpl.java
index 7e8ce2b..de81e9c 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TDetectionCabinetServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TDetectionCabinetServiceImpl.java
@@ -36,10 +36,6 @@
     private static final String TESTING = "1";
     private static final String ONLINE = "0";
     private static final String OFFLINE = "1";
-    private static final Set<DeviceWorkStatus> ACTIVE_WORK_STATUSES = Set.of(DeviceWorkStatus.WORKING,
-            DeviceWorkStatus.PAUSED, DeviceWorkStatus.PROTECTED, DeviceWorkStatus.SYNCHRONIZING,
-            DeviceWorkStatus.LIT, DeviceWorkStatus.WAITING_TIMEOUT, DeviceWorkStatus.WAITING_START,
-            DeviceWorkStatus.LOCKED);
 
     private final TDetectionCabinetMapper detectionCabinetMapper;
     private final DeviceDiscoveryCandidateStore candidateStore;
@@ -102,7 +98,7 @@
 
     /**
      * 读取未过期且未登记的候选,离线设备仍保留身份并可选择。
-     * 测试占用状态由实时通道工作状态聚合;测试归属留待后续绑定闭环确认。
+     * 发现只返回设备事实,不从通道工作状态推断系统测试关系。
      *
      * @return 可供选择的检测柜设备摘要
      */
@@ -112,14 +108,9 @@
         Set<String> registeredCodes = Set.copyOf(detectionCabinetMapper.selectRegisteredCabinetCodes());
         return candidateStore.snapshot(Instant.now()).cabinets().stream()
                 .filter(candidate -> !registeredCodes.contains(candidate.deviceCode()))
-                .map(candidate -> {
-                    boolean active = candidate.online() && isActive(candidate);
-                    return new DiscoveredCabinetVo(
+                .map(candidate -> new DiscoveredCabinetVo(
                             candidate.deviceCode(), candidate.ip(), candidate.mac(), candidate.name(),
-                            candidate.manufacturer(), candidate.model(), candidate.channels().size(), candidate.online() ? "在线" : "离线",
-                            active ? "测试中" : "空闲",
-                            active ? "待确认" : "-");
-                })
+                            candidate.manufacturer(), candidate.model(), candidate.channels().size(), candidate.online() ? "在线" : "离线"))
                 .toList();
     }
 
@@ -394,11 +385,6 @@
     private String stableCandidateKey(String adapterCode, Integer devtype, Integer devid)
     {
         return adapterCode + "-" + devtype + "-" + devid;
-    }
-
-    private boolean isActive(CabinetCandidate candidate)
-    {
-        return candidate.channels().stream().anyMatch(channel -> ACTIVE_WORK_STATUSES.contains(channel.workStatus()));
     }
 
     /** 将设备观测状态转换为最终数据库允许的通道事实状态。 */
diff --git a/battery-business/src/main/resources/mapper/battery/DeviceRelatedTestMapper.xml b/battery-business/src/main/resources/mapper/battery/DeviceRelatedTestMapper.xml
new file mode 100644
index 0000000..c5d2ec2
--- /dev/null
+++ b/battery-business/src/main/resources/mapper/battery/DeviceRelatedTestMapper.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.battery.business.mapper.DeviceRelatedTestMapper">
+    <resultMap id="RelatedTestResult" type="com.battery.business.domain.vo.DeviceRelatedTestVo">
+        <id property="testId" column="test_id" />
+        <result property="testName" column="test_name" />
+        <result property="testStatus" column="test_status" />
+        <collection property="channels" ofType="java.lang.String">
+            <result column="channel_label" />
+        </collection>
+    </resultMap>
+
+    <select id="selectCabinetRelatedTests" parameterType="Long" resultMap="RelatedTestResult">
+        select distinct t.test_id, t.test_name, t.test_status,
+               concat(c.subdevid, '-', c.chlid) as channel_label
+        from t_test_channel_binding b
+        join t_detection_channel c on c.channel_id = b.channel_id
+        join t_test t on t.test_id = b.test_id
+        where c.cabinet_id = #{cabinetId} and t.del_flag = 0
+        order by t.test_id desc, channel_label
+    </select>
+
+    <select id="selectCollectorRelatedTests" parameterType="Long" resultMap="RelatedTestResult">
+        select distinct t.test_id, t.test_name, t.test_status, b.channel_code as channel_label
+        from t_test_collector_channel b
+        join t_test t on t.test_id = b.test_id
+        where b.collector_id = #{collectorId} and t.del_flag = 0
+        order by t.test_id desc, channel_label
+    </select>
+</mapper>
diff --git a/battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml b/battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml
index 747a2ae..dce3675 100644
--- a/battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml
+++ b/battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml
@@ -16,7 +16,6 @@
         <result property="channelCount" column="channel_count" />
         <result property="onlineStatus" column="online_status" />
         <result property="testStatus" column="test_status" />
-        <result property="currentTest" column="current_test" />
         <result property="lastSeenAtUtc" column="last_seen_at_utc" />
         <result property="status" column="status" />
         <result property="delFlag" column="del_flag" />
@@ -25,6 +24,8 @@
         <result property="updateBy" column="update_by" />
         <result property="updateTime" column="update_time" />
         <result property="remark" column="remark" />
+        <collection property="relatedTests" column="collector_id"
+                    select="com.battery.business.mapper.DeviceRelatedTestMapper.selectCollectorRelatedTests" />
     </resultMap>
 
     <sql id="selectCollectorVo">
@@ -39,13 +40,6 @@
                      and test_fact.del_flag = 0
                      and test_fact.test_status in ('RUNNING', 'PAUSED')
                ) then '1' else '0' end as test_status,
-               case when exists (
-                   select 1 from t_test_collector_channel mapping
-                   join t_test test_fact on test_fact.test_id = mapping.test_id
-                   where mapping.collector_id = c.collector_id
-                     and test_fact.del_flag = 0
-                     and test_fact.test_status in ('RUNNING', 'PAUSED')
-               ) then '待确认' else null end as current_test,
                c.status, c.del_flag, c.create_by,
                c.create_time, c.update_by, c.update_time, c.remark
         from t_collector c
diff --git a/battery-business/src/main/resources/mapper/battery/TDetectionCabinetMapper.xml b/battery-business/src/main/resources/mapper/battery/TDetectionCabinetMapper.xml
index 14a9fb3..37a69d6 100644
--- a/battery-business/src/main/resources/mapper/battery/TDetectionCabinetMapper.xml
+++ b/battery-business/src/main/resources/mapper/battery/TDetectionCabinetMapper.xml
@@ -17,7 +17,6 @@
         <result property="channelCount" column="channel_count" />
         <result property="onlineStatus" column="online_status" />
         <result property="testStatus" column="test_status" />
-        <result property="currentTest" column="current_test" />
         <result property="status" column="status" />
         <result property="createBy" column="create_by" />
         <result property="createTime" column="create_time" />
@@ -26,6 +25,8 @@
         <result property="remark" column="remark" />
         <result property="delFlag" column="del_flag" />
         <result property="registrationStatus" column="registration_status" />
+        <collection property="relatedTests" column="cabinet_id"
+                    select="com.battery.business.mapper.DeviceRelatedTestMapper.selectCabinetRelatedTests" />
     </resultMap>
 
     <sql id="activeCabinetTestExists">
@@ -44,7 +45,6 @@
                manufacturer, model, capacity, channel_count,
                case when upper(online_status) = 'ONLINE' or online_status = '0' then '0' else '1' end as online_status,
                case when <include refid="activeCabinetTestExists"/> then '1' else '0' end as test_status,
-               case when <include refid="activeCabinetTestExists"/> then '待确认' else null end as current_test,
                status, create_by, create_time, update_by, update_time, remark, del_flag, registration_status
         from t_detection_cabinet
     </sql>
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 6fc33ac..5209293 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -57,6 +57,12 @@
 
         <!-- 核心模块-->
         <dependency>
+            <groupId>com.h2database</groupId>
+            <artifactId>h2</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
             <groupId>com.ruoyi</groupId>
             <artifactId>ruoyi-framework</artifactId>
         </dependency>
diff --git a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java b/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java
index 6741cf1..de8fe36 100644
--- a/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java
+++ b/ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java
@@ -32,7 +32,7 @@
                 .filter(collector -> !registeredCodes.contains(collector.deviceCode()))
                 .map(collector -> new DiscoveredCollectorVo(collector.deviceCode(), collector.ip(), collector.mac(),
                         collector.name(), collector.manufacturer(), collector.model(), collector.channelCount(),
-                        collector.online() ? "在线" : "离线", "空闲", "-"))
+                        collector.online() ? "在线" : "离线"))
                 .toList();
     }
 }
diff --git a/ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java b/ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java
new file mode 100644
index 0000000..a6188d6
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java
@@ -0,0 +1,143 @@
+package com.battery.business.mapper;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.UUID;
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import com.github.pagehelper.PageInterceptor;
+import org.apache.ibatis.builder.xml.XMLMapperBuilder;
+import org.apache.ibatis.datasource.unpooled.UnpooledDataSource;
+import org.apache.ibatis.mapping.Environment;
+import org.apache.ibatis.session.Configuration;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactoryBuilder;
+import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import com.battery.business.domain.TCollector;
+import com.battery.business.domain.TDetectionCabinet;
+import com.battery.business.domain.vo.DeviceRelatedTestVo;
+
+/** 使用真实映射和内存数据库验证绑定查询,不连接共享开发库。 */
+class DeviceRelatedTestMapperTest
+{
+    private SqlSession session;
+
+    @BeforeEach
+    void setUp() throws Exception
+    {
+        var source = new UnpooledDataSource("org.h2.Driver", "jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL", "sa", "");
+        var configuration = new Configuration(new Environment("test", new JdbcTransactionFactory(), source));
+        var pagination = new PageInterceptor();
+        var properties = new java.util.Properties();
+        properties.setProperty("helperDialect", "mysql");
+        pagination.setProperties(properties);
+        configuration.addInterceptor(pagination);
+        configuration.getTypeAliasRegistry().registerAliases("com.battery.business.domain");
+        for (String name : List.of("DeviceRelatedTestMapper", "TDetectionCabinetMapper", "TCollectorMapper"))
+        {
+            String resource = "mapper/battery/" + name + ".xml";
+            try (var input = getClass().getClassLoader().getResourceAsStream(resource))
+            {
+                new XMLMapperBuilder(input, configuration, resource, configuration.getSqlFragments()).parse();
+            }
+        }
+        session = new SqlSessionFactoryBuilder().build(configuration).openSession();
+        try (var sql = session.getConnection().createStatement())
+        {
+            sql.execute("create table t_test(test_id bigint primary key, test_name varchar(100), test_status varchar(30), del_flag int)");
+            sql.execute("insert into t_test values (1,'Waiting','WAITING',0),(2,'Running','RUNNING',0),(3,'Paused','PAUSED',0),(4,'Completed','COMPLETED',0),(5,'Deleted','RUNNING',1),(6,'Other device','WAITING',0)");
+            sql.execute("create table t_detection_channel(channel_id bigint primary key, cabinet_id bigint, subdevid int, chlid int, del_flag int)");
+            sql.execute("insert into t_detection_channel values (11,1,1,1,0),(12,1,1,2,0),(13,1,2,1,0),(21,2,1,1,0)");
+            sql.execute("create table t_test_channel_binding(test_id bigint, channel_id bigint)");
+            sql.execute("insert into t_test_channel_binding values (1,11),(1,12),(1,13),(2,11),(3,12),(4,11),(5,11),(6,21),(1,21)");
+            sql.execute("create table t_test_collector_channel(test_id bigint, collector_id bigint, channel_code varchar(100))");
+            sql.execute("insert into t_test_collector_channel values (1,1,'S-01'),(1,1,'S-02'),(2,1,'S-01'),(3,1,'S-02'),(4,1,'S-01'),(5,1,'S-01'),(6,2,'S-03'),(1,2,'S-03')");
+            String common = "adapter_id bigint, ip_address varchar(45), mac_address varchar(20), manufacturer varchar(100), model varchar(100), channel_count int, online_status varchar(10), status varchar(1), create_by varchar(64), create_time timestamp, update_by varchar(64), update_time timestamp, remark varchar(200), del_flag int default 0";
+            sql.execute("create table t_detection_cabinet(cabinet_id bigint primary key, adapter_code varchar(64), cabinet_code varchar(64), cabinet_name varchar(100), capacity decimal, registration_status varchar(20) default 'REGISTERED', " + common + ")");
+            sql.execute("insert into t_detection_cabinet(cabinet_id,cabinet_name,online_status) values (1,'Cabinet','OFFLINE'),(3,'Unused','ONLINE')");
+            sql.execute("create table t_collector(collector_id bigint primary key, collector_code varchar(64), collector_name varchar(100), last_seen_at_utc timestamp, " + common + ")");
+            sql.execute("insert into t_collector(collector_id,collector_name,online_status) values (1,'Collector','ONLINE'),(3,'Unused','OFFLINE')");
+        }
+    }
+
+    @AfterEach
+    void tearDown()
+    {
+        PageHelper.clearPage();
+        if (session != null) session.close();
+    }
+
+    @Test
+    void cabinetIncludesAllTestStatesOnceAndOnlyItsOwnChannels()
+    {
+        var tests = session.getMapper(DeviceRelatedTestMapper.class).selectCabinetRelatedTests(1L);
+        assertStates(tests);
+        assertEquals(List.of("1-1", "1-2", "2-1"), tests.get(3).getChannels());
+        assertEquals("Waiting", tests.get(3).getTestName());
+        assertTrue(session.getMapper(DeviceRelatedTestMapper.class).selectCabinetRelatedTests(3L).isEmpty());
+    }
+
+    @Test
+    void collectorIncludesAllTestStatesOnceAndOnlyItsOwnChannels()
+    {
+        var tests = session.getMapper(DeviceRelatedTestMapper.class).selectCollectorRelatedTests(1L);
+        assertStates(tests);
+        assertEquals(List.of("S-01", "S-02"), tests.get(3).getChannels());
+        assertTrue(session.getMapper(DeviceRelatedTestMapper.class).selectCollectorRelatedTests(3L).isEmpty());
+    }
+
+    @Test
+    void deviceListAndDetailAttachTheSameRelationsWithoutMultiplyingDeviceRows()
+    {
+        var cabinets = session.getMapper(TDetectionCabinetMapper.class);
+        assertEquals(2, cabinets.selectDetectionCabinetList(new TDetectionCabinet()).size());
+        assertStates(cabinets.selectDetectionCabinetById(1L).getRelatedTests());
+        assertTrue(cabinets.selectDetectionCabinetById(3L).getRelatedTests().isEmpty());
+        var collectors = session.getMapper(TCollectorMapper.class);
+        assertEquals(2, collectors.selectCollectorList(new TCollector()).size());
+        assertStates(collectors.selectCollectorById(1L).getRelatedTests());
+        assertTrue(collectors.selectCollectorById(3L).getRelatedTests().isEmpty());
+    }
+
+    @Test
+    void devicePaginationKeepsTotalsAndCompleteRelatedTestCollections()
+    {
+        PageHelper.startPage(2, 1);
+        var cabinets = session.getMapper(TDetectionCabinetMapper.class).selectDetectionCabinetList(new TDetectionCabinet());
+        assertEquals(2, new PageInfo<>(cabinets).getTotal());
+        assertEquals(1, cabinets.size());
+        assertEquals(1L, cabinets.get(0).getCabinetId());
+        assertStates(cabinets.get(0).getRelatedTests());
+        assertEquals(List.of("1-1", "1-2", "2-1"), cabinets.get(0).getRelatedTests().get(3).getChannels());
+
+        PageHelper.startPage(2, 1);
+        var collectors = session.getMapper(TCollectorMapper.class).selectCollectorList(new TCollector());
+        assertEquals(2, new PageInfo<>(collectors).getTotal());
+        assertEquals(1, collectors.size());
+        assertEquals(1L, collectors.get(0).getCollectorId());
+        assertStates(collectors.get(0).getRelatedTests());
+        assertEquals(List.of("S-01", "S-02"), collectors.get(0).getRelatedTests().get(3).getChannels());
+    }
+
+    @Test
+    void erasedTestDisappearsFromBothDevices() throws Exception
+    {
+        try (var sql = session.getConnection().createStatement())
+        {
+            sql.execute("delete from t_test where test_id = 1");
+        }
+        var mapper = session.getMapper(DeviceRelatedTestMapper.class);
+        assertEquals(List.of(4L, 3L, 2L), mapper.selectCabinetRelatedTests(1L).stream().map(DeviceRelatedTestVo::getTestId).toList());
+        assertEquals(List.of(4L, 3L, 2L), mapper.selectCollectorRelatedTests(1L).stream().map(DeviceRelatedTestVo::getTestId).toList());
+    }
+
+    private void assertStates(List<DeviceRelatedTestVo> tests)
+    {
+        assertEquals(List.of(4L, 3L, 2L, 1L), tests.stream().map(DeviceRelatedTestVo::getTestId).toList());
+        assertEquals(List.of("COMPLETED", "PAUSED", "RUNNING", "WAITING"), tests.stream().map(DeviceRelatedTestVo::getTestStatus).toList());
+    }
+}
diff --git a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TDetectionCabinetServiceImplTest.java b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TDetectionCabinetServiceImplTest.java
index 6a464b1..209a3dc 100644
--- a/ruoyi-admin/src/test/java/com/battery/business/service/impl/TDetectionCabinetServiceImplTest.java
+++ b/ruoyi-admin/src/test/java/com/battery/business/service/impl/TDetectionCabinetServiceImplTest.java
@@ -94,7 +94,7 @@
         assertEquals(new BigDecimal("120.50"), savedCabinet.getCapacity());
         assertEquals("OFFLINE", savedCabinet.getOnlineStatus());
         assertEquals("0", savedCabinet.getStatus());
-        assertNull(savedCabinet.getCurrentTest());
+        assertTrue(savedCabinet.getRelatedTests().isEmpty());
     }
 
     /**
@@ -134,10 +134,10 @@
     }
 
     /**
-     * 验证检测柜主列表从通道事实派生旧页面展示字段,不读取数据库不存在的列。
+     * 验证检测柜主列表保留操作校验所需派生状态,不读取数据库不存在的列。
      */
     @Test
-    void detectionCabinetListSqlShouldDeriveLegacyPresentationFields() throws IOException
+    void detectionCabinetListSqlShouldDeriveOperationStateWithoutCurrentTest() throws IOException
     {
         try (InputStream input = getClass().getResourceAsStream(
                 "/mapper/battery/TDetectionCabinetMapper.xml"))
@@ -150,7 +150,7 @@
             assertFalse(selectSql.contains("online_status, test_status"));
             assertFalse(selectSql.contains("current_test, status"));
             assertTrue(selectSql.contains("as test_status"));
-            assertTrue(selectSql.contains("as current_test"));
+            assertFalse(selectSql.contains("current_test"));
             String activeTestSql = sqlFragment(mapperXml, "activeCabinetTestExists");
             assertTrue(activeTestSql.contains("t_test_channel_binding"));
             assertTrue(activeTestSql.contains("t_detection_channel"));
@@ -206,7 +206,7 @@
     }
 
     /**
-     * 验证设备选择列表读取 Redis 候选,并保持原前端字段和状态文本。
+     * 验证设备选择列表读取候选的设备事实,不推断系统测试关系。
      */
     @Test
     void selectDiscoveredCabinetListShouldMapPersistedCabinetFacts()
@@ -220,8 +220,9 @@
         assertEquals("192.168.1.10", devices.get(0).getIp());
         assertEquals("AA:BB:CC:DD:EE:FF", devices.get(0).getMac());
         assertEquals("在线", devices.get(0).getOnlineStatus());
-        assertEquals("测试中", devices.get(0).getTestStatus());
-        assertEquals("待确认", devices.get(0).getCurrentTest());
+        var fields = new com.fasterxml.jackson.databind.ObjectMapper().valueToTree(devices.get(0));
+        assertFalse(fields.has("testStatus"));
+        assertFalse(fields.has("currentTest"));
         assertEquals("一号模拟柜", devices.get(0).getName());
         assertEquals("开发模拟器", devices.get(0).getManufacturer());
         assertEquals("BTS Virtual Cabinet", devices.get(0).getModel());

--
Gitblit v1.7.1