yeg
2026-09-05 2e75fb72c1ba5d3d055362b44e7b85c0ad3f5416
按设备绑定关系查询关联测试与通道

以关联测试集合替换当前测试占位,按测试去重并保留各状态和本设备通道。发现候选只返回设备事实,不新增表,保留设备操作校验。

后端全量测试与打包通过,补充内存数据库映射及分页插件验证,规范与需求审查通过。
4个文件已添加
11个文件已修改
340 ■■■■■ 已修改文件
battery-business/src/main/java/com/battery/business/domain/TCollector.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/domain/TDetectionCabinet.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCabinetVo.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/domain/vo/DiscoveredCollectorVo.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/mapper/DeviceRelatedTestMapper.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/service/impl/TDetectionCabinetServiceImpl.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/resources/mapper/battery/DeviceRelatedTestMapper.xml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/resources/mapper/battery/TCollectorMapper.xml 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/resources/mapper/battery/TDetectionCabinetMapper.xml 4 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/pom.xml 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java 143 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/service/impl/TDetectionCabinetServiceImplTest.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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())
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 + '\'' +
battery-business/src/main/java/com/battery/business/domain/vo/DeviceRelatedTestVo.java
New file
@@ -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; }
}
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; }
}
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;
    }
}
battery-business/src/main/java/com/battery/business/mapper/DeviceRelatedTestMapper.java
New file
@@ -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);
}
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()))
        {
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()));
    }
    /** 将设备观测状态转换为最终数据库允许的通道事实状态。 */
battery-business/src/main/resources/mapper/battery/DeviceRelatedTestMapper.xml
New file
@@ -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>
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
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>
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>
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();
    }
}
ruoyi-admin/src/test/java/com/battery/business/mapper/DeviceRelatedTestMapperTest.java
New file
@@ -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());
    }
}
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());