yeg
2026-09-01 3b0d51d628c772c47845d9fd0b95ebde8a969d07
实现数据修订行为模型

新增修订行为分页与详情查询,支持三类数据、测试、操作类型和时间筛选。
补充身份摘要校验、三类数据物理删除和测试控制台上下文返回。
4个文件已添加
5个文件已修改
379 ■■■■■ 已修改文件
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestDataRevisionController.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/resources/db/migration/V26__data_revision_action_query_contract.sql 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/config/Ticket26MigrationContractTest.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TestConsoleDataRevisionQueryServiceTest.java 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestConsoleDataService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml 60 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestDataRevisionController.java
New file
@@ -0,0 +1,40 @@
package com.ruoyi.web.controller.system;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.system.domain.TTestDataRevisionAction;
import com.ruoyi.system.service.ITestConsoleDataService;
/** 数据修订行为查询接口;只读返回一次用户行为及其摘要。 */
@RestController
@RequestMapping("/system/data-revision")
public class TTestDataRevisionController extends BaseController
{
    @Autowired
    private ITestConsoleDataService service;
    @PreAuthorize("@ss.hasPermi('system:dataRevision:list')")
    @GetMapping("/list")
    public TableDataInfo list(TTestDataRevisionAction query)
    {
        startPage();
        List<TTestDataRevisionAction> rows = service.selectActions(query);
        return getDataTable(rows);
    }
    @PreAuthorize("@ss.hasPermi('system:dataRevision:query')")
    @GetMapping("/{actionId}")
    public AjaxResult detail(@PathVariable Long actionId)
    {
        TTestDataRevisionAction action = service.selectActionById(actionId);
        return action == null ? error("未找到数据修订行为") : success(action);
    }
}
ruoyi-admin/src/main/resources/db/migration/V26__data_revision_action_query_contract.sql
New file
@@ -0,0 +1,3 @@
-- Ticket26:数据修订行为查询使用现有一次行为表,不复制按单元格记录。
CREATE INDEX idx_data_revision_query_time
    ON t_test_data_revision_action (data_type, operation_type, created_at_utc, action_id);
ruoyi-admin/src/test/java/com/ruoyi/config/Ticket26MigrationContractTest.java
New file
@@ -0,0 +1,25 @@
package com.ruoyi.config;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Test;
/** Ticket26 迁移只增加行为查询索引,不建立按行或按格修订表。 */
class Ticket26MigrationContractTest
{
    @Test
    void addsActionQueryIndexWithoutReplacingAuditFacts() throws Exception
    {
        try (InputStream stream = getClass().getClassLoader().getResourceAsStream(
                "db/migration/V26__data_revision_action_query_contract.sql"))
        {
            if (stream == null) throw new AssertionError("缺少 Ticket26 迁移");
            String sql = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
            assertTrue(sql.contains("idx_data_revision_query_time"));
            assertTrue(sql.contains("t_test_data_revision_action"));
            assertFalse(sql.matches("(?s).*\\b(DROP|DELETE|UPDATE)\\b.*"));
        }
    }
}
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TestConsoleDataRevisionQueryServiceTest.java
New file
@@ -0,0 +1,116 @@
package com.ruoyi.system.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.domain.TTestDataRevisionAction;
import com.ruoyi.system.domain.TTestDataOverride;
import com.ruoyi.system.mapper.TTestMapper;
/** Ticket26:修订行为按一次提交查询,且三类数据与三类操作均受服务端约束。 */
@ExtendWith(MockitoExtension.class)
class TestConsoleDataRevisionQueryServiceTest
{
    @Mock
    private TTestMapper mapper;
    @InjectMocks
    private TestConsoleDataServiceImpl service;
    private TTestDataRevisionAction query;
    @BeforeEach
    void setUp()
    {
        query = new TTestDataRevisionAction();
        query.setTestId(26L);
        query.setDataType("STEP");
        query.setOperationType("EDIT");
    }
    @Test
    void delegatesOneActionQueryWithFilters()
    {
        TTestDataRevisionAction action = new TTestDataRevisionAction();
        action.setActionId(101L);
        when(mapper.selectDataRevisionActions(query)).thenReturn(List.of(action));
        assertEquals(101L, service.selectActions(query).get(0).getActionId());
        verify(mapper).selectDataRevisionActions(query);
    }
    @Test
    void rejectsUnknownDataTypeAndOperation()
    {
        query.setDataType("CELL");
        assertThrows(ServiceException.class, () -> service.selectActions(query));
        query.setDataType("CYCLE");
        query.setOperationType("RESTORE");
        assertThrows(ServiceException.class, () -> service.selectActions(query));
    }
    @Test
    void returnsReadOnlyActionDetail()
    {
        TTestDataRevisionAction action = new TTestDataRevisionAction();
        action.setActionId(99L);
        action.setOperationType("DELETE");
        when(mapper.selectDataRevisionActionById(99L)).thenReturn(action);
        assertEquals("DELETE", service.selectActionById(99L).getOperationType());
        assertThrows(ServiceException.class, () -> service.selectActionById(0L));
    }
    @Test
    void rejectsIdentitySummaryThatDoesNotMatchOverrides()
    {
        TTestDataRevisionAction action = new TTestDataRevisionAction();
        action.setTestId(26L); action.setDataType("REALTIME"); action.setOperationType("DELETE");
        action.setScopeSummary("删除一条实时数据"); action.setIdentitySummary("[\"record-1\"]");
        action.setReason("异常点"); action.setOperatorName("tester");
        TTestDataOverride override = new TTestDataOverride();
        override.setStableIdentity("record-2"); override.setDeleted(true);
        assertThrows(ServiceException.class,
                () -> service.submitAction(action, List.of(override), List.of(), List.of()));
    }
    @Test
    void rejectsIdentitySummaryWithExtraIdentity()
    {
        TTestDataRevisionAction action = new TTestDataRevisionAction();
        action.setTestId(26L); action.setDataType("STEP"); action.setOperationType("DELETE");
        action.setScopeSummary("删除一条工步数据"); action.setIdentitySummary("[\"step:1:2:3:4\",\"step:1:2:3:5\"]");
        action.setReason("异常工步"); action.setOperatorName("tester");
        TTestDataOverride override = new TTestDataOverride();
        override.setStableIdentity("step:1:2:3:4"); override.setDeleted(true);
        assertThrows(ServiceException.class,
                () -> service.submitAction(action, List.of(override), List.of(), List.of()));
    }
    @Test
    void physicallyDeletesSamplesBehindStepAggregate()
    {
        TTestDataRevisionAction action = new TTestDataRevisionAction();
        action.setTestId(26L); action.setDataType("STEP"); action.setOperationType("DELETE");
        action.setScopeSummary("删除一条工步数据"); action.setIdentitySummary("[\"step:1:2:3:4\"]");
        action.setReason("异常工步"); action.setOperatorName("tester");
        TTestDataOverride override = new TTestDataOverride();
        override.setStableIdentity("step:1:2:3:4"); override.setDeleted(true);
        when(mapper.selectBtsSampleIdsByDeviceChannelStep(26L, 1L, 2L, 3, 4))
                .thenReturn(List.of(101L, 102L));
        service.submitAction(action, List.of(override), List.of(), List.of());
        verify(mapper).deleteBtsMeasurementsBySampleIds(List.of(101L, 102L));
        verify(mapper).deleteBtsSamplesByIds(26L, List.of(101L, 102L));
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/domain/TTestDataRevisionAction.java
@@ -2,6 +2,9 @@
import java.io.Serializable;
import java.util.Date;
import org.springframework.format.annotation.DateTimeFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonFormat;
/** 一次控制台数据编辑、导入或删除行为。 */
public class TTestDataRevisionAction implements Serializable
@@ -17,7 +20,18 @@
    private String reason;
    private Long operatorId;
    private String operatorName;
    @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", timezone = "UTC")
    private Date createdAtUtc;
    /** 查询展示字段:来源测试名称。 */
    private String testName;
    /** 返回测试控制台时使用的后端接收状态。 */
    private String testReceptionStatus;
    @JsonIgnore
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private Date beginCreatedAt;
    @JsonIgnore
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    private Date endCreatedAt;
    public Long getActionId() { return actionId; }
    public void setActionId(Long value) { actionId = value; }
@@ -41,4 +55,12 @@
    public void setOperatorName(String value) { operatorName = value; }
    public Date getCreatedAtUtc() { return createdAtUtc; }
    public void setCreatedAtUtc(Date value) { createdAtUtc = value; }
    public String getTestName() { return testName; }
    public void setTestName(String value) { testName = value; }
    public String getTestReceptionStatus() { return testReceptionStatus; }
    public void setTestReceptionStatus(String value) { testReceptionStatus = value; }
    public Date getBeginCreatedAt() { return beginCreatedAt; }
    public void setBeginCreatedAt(Date value) { beginCreatedAt = value; }
    public Date getEndCreatedAt() { return endCreatedAt; }
    public void setEndCreatedAt(Date value) { endCreatedAt = value; }
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
@@ -192,14 +192,23 @@
    int countOpenExceptions(Long testId);
    int insertDataRevisionAction(TTestDataRevisionAction action);
    List<TTestDataRevisionAction> selectDataRevisionActions(TTestDataRevisionAction query);
    TTestDataRevisionAction selectDataRevisionActionById(Long actionId);
    int upsertDataOverride(TTestDataOverride override);
    List<TTestDataOverride> selectDataOverrides(@Param("testId") Long testId,
            @Param("dataType") String dataType);
    int deleteBtsSamplesByIds(@Param("testId") Long testId, @Param("sampleIds") List<Long> sampleIds);
    int deleteBtsMeasurementsBySampleIds(@Param("sampleIds") List<Long> sampleIds);
    int deleteCollectorSamplesByIds(@Param("testId") Long testId, @Param("sampleIds") List<Long> sampleIds);
    Long selectBtsSampleIdByChannelSequence(@Param("testId") Long testId,
            @Param("channelId") Long channelId, @Param("sequenceId") Long sequenceId);
    Long selectBtsSampleIdByDeviceChannelSequence(@Param("testId") Long testId,
            @Param("deviceId") Long deviceId, @Param("channelNo") Long channelNo,
            @Param("sequenceId") Long sequenceId);
    List<Long> selectBtsSampleIdsByDeviceChannelStep(@Param("testId") Long testId,
            @Param("deviceId") Long deviceId, @Param("channelNo") Long channelNo,
            @Param("cycleNo") Integer cycleNo, @Param("stepNo") Integer stepNo);
    List<Long> selectBtsSampleIdsByDeviceChannelCycle(@Param("testId") Long testId,
            @Param("deviceId") Long deviceId, @Param("channelNo") Long channelNo,
            @Param("cycleNo") Integer cycleNo);
    int insertConsoleImportRecord(TTestConsoleImportRecord record);
    int countTestBatteryNo(@Param("testId") Long testId, @Param("batteryNo") String batteryNo);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestConsoleDataService.java
@@ -7,6 +7,12 @@
/** 控制台三类数据的修订、导入和物理删除服务。 */
public interface ITestConsoleDataService
{
    /** 按测试、数据类型、操作类型和时间查询一次用户行为。 */
    List<TTestDataRevisionAction> selectActions(TTestDataRevisionAction query);
    /** 返回一条只读修订行为,不恢复被删除的数据。 */
    TTestDataRevisionAction selectActionById(Long actionId);
    List<TTestDataOverride> selectOverrides(Long testId, String dataType);
    /** 在一个事务中写入一次行为、覆盖状态并执行请求的物理删除。 */
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java
@@ -28,6 +28,24 @@
    private TTestMapper mapper;
    @Override
    public List<TTestDataRevisionAction> selectActions(TTestDataRevisionAction query)
    {
        if (query == null) query = new TTestDataRevisionAction();
        if (query.getDataType() != null && !query.getDataType().isBlank()
                && !DATA_TYPES.contains(query.getDataType())) throw new ServiceException("控制台数据类型无效");
        if (query.getOperationType() != null && !query.getOperationType().isBlank()
                && !OPERATIONS.contains(query.getOperationType())) throw new ServiceException("数据操作类型无效");
        return mapper.selectDataRevisionActions(query);
    }
    @Override
    public TTestDataRevisionAction selectActionById(Long actionId)
    {
        if (actionId == null || actionId < 1) throw new ServiceException("修订行为ID无效");
        return mapper.selectDataRevisionActionById(actionId);
    }
    @Override
    public List<TTestDataOverride> selectOverrides(Long testId, String dataType)
    {
        validateType(testId, dataType);
@@ -50,6 +68,7 @@
        }
        if ("EDIT".equals(action.getOperationType())) validateEditableFields(action);
        if (action.getReason() == null || action.getReason().trim().isEmpty()) throw new ServiceException("数据操作原因不能为空");
        validateActionSummaries(action, overrides);
        if (action.getOperatorName() == null || action.getOperatorName().trim().isEmpty()) action.setOperatorName("未知用户");
        action.setReason(action.getReason().trim());
        action.setCreatedAtUtc(new Date());
@@ -95,18 +114,31 @@
                mapper.upsertDataOverride(item);
            }
        }
        if ("DELETE".equals(action.getOperationType()) && "REALTIME".equals(action.getDataType()) && overrides != null)
        if ("DELETE".equals(action.getOperationType()) && overrides != null)
        {
            List<Long> resolvedIds = new ArrayList<>();
            for (TTestDataOverride item : overrides)
            {
                Long[] identity = parseRealtimeIdentity(item.getStableIdentity());
                if (identity == null) continue;
                Long sampleId = mapper.selectBtsSampleIdByChannelSequence(action.getTestId(), identity[0], identity[1]);
                if (sampleId != null) resolvedIds.add(sampleId);
                Long[] identity = parseDeleteIdentity(item.getStableIdentity(), action.getDataType());
                if (identity == null) throw new ServiceException("删除数据稳定身份无效");
                switch (action.getDataType())
                {
                    case "REALTIME" -> {
                        Long sampleId = mapper.selectBtsSampleIdByDeviceChannelSequence(action.getTestId(),
                                identity[0], identity[1], identity[2]);
                        if (sampleId != null) resolvedIds.add(sampleId);
                    }
                    case "STEP" -> resolvedIds.addAll(mapper.selectBtsSampleIdsByDeviceChannelStep(action.getTestId(),
                            identity[0], identity[1], identity[2].intValue(), identity[3].intValue()));
                    case "CYCLE" -> resolvedIds.addAll(mapper.selectBtsSampleIdsByDeviceChannelCycle(action.getTestId(),
                            identity[0], identity[1], identity[2].intValue()));
                    default -> throw new ServiceException("控制台数据类型无效");
                }
            }
            if (!resolvedIds.isEmpty())
            {
                resolvedIds = new ArrayList<>(new HashSet<>(resolvedIds));
                // 工步和循环是采样聚合视图,物理删除需在同一事务内清理对应底层采样。
                mapper.deleteBtsMeasurementsBySampleIds(resolvedIds);
                mapper.deleteBtsSamplesByIds(action.getTestId(), resolvedIds);
            }
@@ -175,6 +207,43 @@
        catch (Exception exception) { throw new ServiceException("修改字段格式无效"); }
    }
    /** 防止客户端提交与实际覆盖身份不一致的伪行为摘要。 */
    private void validateActionSummaries(TTestDataRevisionAction action, List<TTestDataOverride> overrides)
    {
        if (action.getScopeSummary() == null || action.getScopeSummary().trim().isEmpty())
            throw new ServiceException("数据操作范围不能为空");
        Set<String> summaryIdentities = new HashSet<>();
        try
        {
            Object parsed = JSON.parse(action.getIdentitySummary());
            if (!(parsed instanceof List<?> values)) throw new ServiceException("身份摘要格式无效");
            for (Object value : values)
                if (value == null || !summaryIdentities.add(String.valueOf(value)))
                    throw new ServiceException("身份摘要存在重复或空值");
        }
        catch (ServiceException exception) { throw exception; }
        catch (Exception exception) { throw new ServiceException("身份摘要格式无效"); }
        if (overrides == null) return;
        Set<String> overrideIdentities = new HashSet<>();
        for (TTestDataOverride item : overrides)
            if (item == null || !overrideIdentities.add(item.getStableIdentity())
                    || !summaryIdentities.contains(item.getStableIdentity()))
                throw new ServiceException("身份摘要与数据覆盖明细不一致");
        if (!summaryIdentities.equals(overrideIdentities))
            throw new ServiceException("身份摘要与数据覆盖明细不一致");
        if ("EDIT".equals(action.getOperationType()))
        {
            try
            {
                Object parsed = JSON.parse(action.getChangesSummary());
                if (!(parsed instanceof List<?> changes) || changes.size() < overrides.size())
                    throw new ServiceException("字段变化摘要与数据覆盖明细不一致");
            }
            catch (ServiceException exception) { throw exception; }
            catch (Exception exception) { throw new ServiceException("字段变化摘要格式无效"); }
        }
    }
    private Set<String> editableFields(String dataType)
    {
        return switch (dataType)
@@ -188,11 +257,20 @@
        };
    }
    private Long[] parseRealtimeIdentity(String value)
    private Long[] parseDeleteIdentity(String value, String dataType)
    {
        String[] parts = value.split(":", -1);
        if (parts.length != 3) return null;
        try { return new Long[] { Long.valueOf(parts[0]), Long.valueOf(parts[2]) }; }
        int expectedLength = "STEP".equals(dataType) ? 5 : "CYCLE".equals(dataType) ? 4 : 3;
        if (parts.length != expectedLength) return null;
        int offset = "REALTIME".equals(dataType) ? 0 : 1;
        try
        {
            if ("STEP".equals(dataType))
                return new Long[] { Long.valueOf(parts[offset]), Long.valueOf(parts[offset + 1]),
                        Long.valueOf(parts[offset + 2]), Long.valueOf(parts[offset + 3]) };
            return new Long[] { Long.valueOf(parts[offset]), Long.valueOf(parts[offset + 1]),
                    Long.valueOf(parts[offset + 2]) };
        }
        catch (NumberFormatException exception) { return null; }
    }
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -227,6 +227,42 @@
        (#{testName}, #{creatorId}, #{principalId}, #{testTypeId}, #{testPurpose}, #{testStatus}, #{receptionStatus},
         #{monitorTimeoutMinutes}, 0, #{createBy}, #{createTime}, #{remark})
    </insert>
    <resultMap id="TTestDataRevisionActionResultMap" type="com.ruoyi.system.domain.TTestDataRevisionAction">
        <id property="actionId" column="action_id"/>
        <result property="testId" column="test_id"/><result property="testName" column="test_name"/>
        <result property="testReceptionStatus" column="reception_status"/>
        <result property="dataType" column="data_type"/><result property="operationType" column="operation_type"/>
        <result property="scopeSummary" column="scope_summary"/><result property="identitySummary" column="identity_summary"/>
        <result property="changesSummary" column="changes_summary"/><result property="reason" column="reason"/>
        <result property="operatorId" column="operator_id"/><result property="operatorName" column="operator_name"/>
        <result property="createdAtUtc" column="created_at_utc"/>
    </resultMap>
    <select id="selectDataRevisionActions" parameterType="com.ruoyi.system.domain.TTestDataRevisionAction" resultMap="TTestDataRevisionActionResultMap">
        select a.action_id, a.test_id, t.test_name, t.reception_status,
               a.data_type, a.operation_type, a.scope_summary,
               cast(a.identity_summary as char) identity_summary,
               cast(a.changes_summary as char) changes_summary, a.reason,
               a.operator_id, a.operator_name, a.created_at_utc
        from t_test_data_revision_action a join t_test t on t.test_id = a.test_id
        <where>
            <if test="testId != null">and a.test_id = #{testId}</if>
            <if test="testName != null and testName != ''">and t.test_name like concat('%', #{testName}, '%')</if>
            <if test="dataType != null and dataType != ''">and a.data_type = #{dataType}</if>
            <if test="operationType != null and operationType != ''">and a.operation_type = #{operationType}</if>
            <if test="beginCreatedAt != null">and a.created_at_utc &gt;= #{beginCreatedAt}</if>
            <if test="endCreatedAt != null">and a.created_at_utc &lt; #{endCreatedAt}</if>
        </where>
        order by a.created_at_utc desc, a.action_id desc
    </select>
    <select id="selectDataRevisionActionById" parameterType="long" resultMap="TTestDataRevisionActionResultMap">
        select a.action_id, a.test_id, t.test_name, t.reception_status,
               a.data_type, a.operation_type, a.scope_summary,
               cast(a.identity_summary as char) identity_summary,
               cast(a.changes_summary as char) changes_summary, a.reason,
               a.operator_id, a.operator_name, a.created_at_utc
        from t_test_data_revision_action a join t_test t on t.test_id = a.test_id
        where a.action_id = #{actionId}
    </select>
    <update id="updateTest" parameterType="TTest">
        update t_test
@@ -822,11 +858,29 @@
        delete from t_test_collector_sample where test_id = #{testId} and sample_id in
        <foreach collection="sampleIds" item="id" open="(" separator="," close=")">#{id}</foreach>
    </delete>
    <select id="selectBtsSampleIdByChannelSequence" resultType="long">
        select sample_id from t_test_sample
        where test_id = #{testId} and channel_id = #{channelId} and seq_id = #{sequenceId}
    <select id="selectBtsSampleIdByDeviceChannelSequence" resultType="long">
        select sample.sample_id
        from t_test_sample sample
        join t_detection_channel channel_fact on channel_fact.channel_id = sample.channel_id
        where sample.test_id = #{testId} and channel_fact.devid = #{deviceId}
          and channel_fact.chlid = #{channelNo} and sample.seq_id = #{sequenceId}
        limit 1
    </select>
    <select id="selectBtsSampleIdsByDeviceChannelStep" resultType="long">
        select sample.sample_id
        from t_test_sample sample
        join t_detection_channel channel_fact on channel_fact.channel_id = sample.channel_id
        where sample.test_id = #{testId} and channel_fact.devid = #{deviceId}
          and channel_fact.chlid = #{channelNo} and sample.cycle_no = #{cycleNo}
          and sample.step_no = #{stepNo}
    </select>
    <select id="selectBtsSampleIdsByDeviceChannelCycle" resultType="long">
        select sample.sample_id
        from t_test_sample sample
        join t_detection_channel channel_fact on channel_fact.channel_id = sample.channel_id
        where sample.test_id = #{testId} and channel_fact.devid = #{deviceId}
          and channel_fact.chlid = #{channelNo} and sample.cycle_no = #{cycleNo}
    </select>
    <insert id="insertConsoleImportRecord" parameterType="com.ruoyi.system.domain.TTestConsoleImportRecord"
            useGeneratedKeys="true" keyProperty="importRecordId">
        insert into t_test_console_import_record