yeg
2026-08-29 8ba28ba5fbee0582f742f6eb1e824544869abc05
修正共享字段快照模块边界

将测试字段快照规则和持久化收口到共享字段服务。
移除 MySQL 专有电池号聚合并补充批量查询与自动化测试。
5个文件已添加
5个文件已修改
420 ■■■■■ 已修改文件
ruoyi-admin/src/test/java/com/ruoyi/config/TestDeviceBindingMapperContractTest.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TSharedFieldSnapshotServiceImplTest.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestServiceImplTest.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TSharedFieldSnapshotMapper.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/ISharedFieldSnapshotService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TSharedFieldSnapshotServiceImpl.java 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TSharedFieldSnapshotMapper.xml 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/ruoyi/config/TestDeviceBindingMapperContractTest.java
@@ -1,6 +1,7 @@
package com.ruoyi.config;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertFalse;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
@@ -13,13 +14,16 @@
    void persistsAndReadsCollectorMappingsAndFieldSnapshots()
    {
        String mapper = resource("mapper/system/TTestMapper.xml");
        String snapshotMapper = resource("mapper/system/TSharedFieldSnapshotMapper.xml");
        assertTrue(mapper.contains("id=\"insertTestCollectorChannel\""));
        assertTrue(mapper.contains("id=\"selectTestCollectorChannels\""));
        assertTrue(mapper.contains("id=\"insertTestFieldSnapshot\""));
        assertTrue(mapper.contains("id=\"selectTestFieldSnapshots\""));
        assertTrue(snapshotMapper.contains("id=\"insert\""));
        assertTrue(snapshotMapper.contains("id=\"selectByTestId\""));
        assertTrue(mapper.contains("reception_status = 'NOT_STARTED'"));
        assertTrue(mapper.contains("channel_count channelCount"));
        assertTrue(mapper.contains("id=\"selectBatteryNosByTestIds\""));
        assertFalse(mapper.contains("group_concat"));
    }
    private static String resource(String path)
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TSharedFieldSnapshotServiceImplTest.java
New file
@@ -0,0 +1,92 @@
package com.ruoyi.system.service.impl;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.util.List;
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.system.domain.TBatteryGroup;
import com.ruoyi.system.domain.TBatteryGroupRelationField;
import com.ruoyi.system.domain.TTestFieldSnapshot;
import com.ruoyi.system.mapper.TSharedFieldSnapshotMapper;
import com.ruoyi.system.service.ITBatteryGroupService;
/** 共享业务字段快照规则测试。 */
@ExtendWith(MockitoExtension.class)
class TSharedFieldSnapshotServiceImplTest
{
    @Mock
    private ITBatteryGroupService batteryGroupService;
    @Mock
    private TSharedFieldSnapshotMapper snapshotMapper;
    @InjectMocks
    private TSharedFieldSnapshotServiceImpl snapshotService;
    @Test
    void createShouldCopyActiveBatteryGroupFields()
    {
        when(batteryGroupService.selectBatteryGroupById(1L)).thenReturn(group("72"));
        List<TTestFieldSnapshot> snapshots = snapshotService.createForBatteryGroup(1L);
        assertEquals(1, snapshots.size());
        assertEquals(11L, snapshots.get(0).getFieldDefinitionId());
        assertEquals("额定容量", snapshots.get(0).getFieldName());
        assertEquals("number", snapshots.get(0).getFieldType());
        assertEquals("72", snapshots.get(0).getFieldValue());
    }
    @Test
    void sameGroupUpdateShouldRetainOriginalSnapshotValue()
    {
        TTestFieldSnapshot original = snapshot("72");
        List<TTestFieldSnapshot> resolved = snapshotService.resolveForUpdate(1L, 1L, List.of(original));
        assertEquals("72", resolved.get(0).getFieldValue());
        assertNotSame(original, resolved.get(0));
        verify(batteryGroupService, never()).selectBatteryGroupById(1L);
    }
    @Test
    void changingGroupShouldCreateSnapshotFromNewGroup()
    {
        when(batteryGroupService.selectBatteryGroupById(2L)).thenReturn(group("99"));
        List<TTestFieldSnapshot> resolved = snapshotService.resolveForUpdate(1L, 2L, List.of(snapshot("72")));
        assertEquals("99", resolved.get(0).getFieldValue());
        verify(batteryGroupService).selectBatteryGroupById(2L);
    }
    private TBatteryGroup group(String value)
    {
        TBatteryGroupRelationField field = new TBatteryGroupRelationField();
        field.setBatteryGroupFieldId(11L);
        field.setFieldName("额定容量");
        field.setFieldType("number");
        field.setFieldValue(value);
        TBatteryGroup group = new TBatteryGroup();
        group.setTemplateFields(List.of(field));
        return group;
    }
    private TTestFieldSnapshot snapshot(String value)
    {
        TTestFieldSnapshot snapshot = new TTestFieldSnapshot();
        snapshot.setFieldDefinitionId(11L);
        snapshot.setFieldName("额定容量");
        snapshot.setFieldType("number");
        snapshot.setFieldValue(value);
        return snapshot;
    }
}
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestServiceImplTest.java
@@ -22,8 +22,6 @@
import org.mockito.junit.jupiter.MockitoExtension;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.domain.TTest;
import com.ruoyi.system.domain.TBatteryGroup;
import com.ruoyi.system.domain.TBatteryGroupRelationField;
import com.ruoyi.system.domain.TTestBattery;
import com.ruoyi.system.domain.TTestChannelBinding;
import com.ruoyi.system.domain.TTestCollectorChannel;
@@ -31,7 +29,7 @@
import com.ruoyi.system.domain.TTestPlanStep;
import com.ruoyi.system.domain.TTestWarningThreshold;
import com.ruoyi.system.mapper.TTestMapper;
import com.ruoyi.system.service.ITBatteryGroupService;
import com.ruoyi.system.service.ISharedFieldSnapshotService;
/** 测试配置服务的业务契约测试。 */
@ExtendWith(MockitoExtension.class)
@@ -41,7 +39,7 @@
    private TTestMapper testMapper;
    @Mock
    private ITBatteryGroupService batteryGroupService;
    private ISharedFieldSnapshotService sharedFieldSnapshotService;
    @InjectMocks
    private TTestServiceImpl testService;
@@ -68,9 +66,32 @@
        verify(testMapper).insertTestPlanStep(any(TTestPlanStep.class));
        verify(testMapper).insertTestChannelBinding(any(TTestChannelBinding.class));
        verify(testMapper, times(3)).insertTestCollectorChannel(any(TTestCollectorChannel.class));
        verify(testMapper).insertTestFieldSnapshot(any());
        verify(sharedFieldSnapshotService).saveForTest(eq(10L), any());
        verify(testMapper).insertTestThreshold(any(TTestWarningThreshold.class));
        verify(testMapper).insertTestAudit(anyLong(), any(), anyLong(), any(), isNull(), any());
    }
    @Test
    void listShouldAggregateBatteryNumbersOutsideDatabaseQuery()
    {
        TTest first = new TTest();
        first.setTestId(10L);
        TTest second = new TTest();
        second.setTestId(11L);
        TTestBattery firstBattery = new TTestBattery();
        firstBattery.setTestId(10L);
        firstBattery.setBatteryNo("BAT-001");
        TTestBattery secondBattery = new TTestBattery();
        secondBattery.setTestId(10L);
        secondBattery.setBatteryNo("BAT-002");
        when(testMapper.selectTestList(any(TTest.class))).thenReturn(java.util.List.of(first, second));
        when(testMapper.selectBatteryNosByTestIds(java.util.List.of(10L, 11L)))
                .thenReturn(java.util.List.of(firstBattery, secondBattery));
        java.util.List<TTest> result = testService.selectTestList(new TTest());
        assertEquals("BAT-001、BAT-002", result.get(0).getBatteryNo());
        assertEquals("", result.get(1).getBatteryNo());
    }
    @Test
@@ -263,8 +284,11 @@
        current.setReceptionStatus("NOT_STARTED");
        current.setMonitorTimeoutMinutes(30);
        when(testMapper.selectTestByIdForUpdate(10L)).thenReturn(current);
        when(testMapper.selectTestBatteries(10L)).thenReturn(current.getBatteries());
        when(sharedFieldSnapshotService.selectByTestId(10L)).thenReturn(java.util.List.of());
        when(sharedFieldSnapshotService.resolveForUpdate(1L, 1L, java.util.List.of()))
                .thenReturn(java.util.List.of());
        mockEnabledOptions();
        mockGroupSnapshot();
        when(testMapper.updateTest(request)).thenReturn(1);
        doAnswer(invocation -> {
            invocation.<TTestBattery>getArgument(0).setRelationId(21L);
@@ -295,7 +319,9 @@
        when(testMapper.selectTestByIdForUpdate(10L)).thenReturn(current);
        when(testMapper.selectTestBatteries(10L)).thenReturn(current.getBatteries());
        when(testMapper.selectTestFieldSnapshots(10L)).thenReturn(java.util.List.of(originalSnapshot));
        when(sharedFieldSnapshotService.selectByTestId(10L)).thenReturn(java.util.List.of(originalSnapshot));
        when(sharedFieldSnapshotService.resolveForUpdate(1L, 1L, java.util.List.of(originalSnapshot)))
                .thenReturn(java.util.List.of(originalSnapshot));
        mockEnabledOptions();
        when(testMapper.updateTest(request)).thenReturn(1);
        doAnswer(invocation -> {
@@ -305,12 +331,9 @@
        assertEquals(1, testService.updateTest(request));
        verify(testMapper).insertTestFieldSnapshot(org.mockito.ArgumentMatchers.argThat(snapshot ->
                Long.valueOf(11L).equals(snapshot.getFieldDefinitionId())
                        && "额定容量".equals(snapshot.getFieldName())
                        && "number".equals(snapshot.getFieldType())
                        && "72".equals(snapshot.getFieldValue())));
        verify(batteryGroupService, never()).selectBatteryGroupById(anyLong());
        verify(sharedFieldSnapshotService).resolveForUpdate(1L, 1L, java.util.List.of(originalSnapshot));
        verify(sharedFieldSnapshotService).saveForTest(eq(10L), org.mockito.ArgumentMatchers.argThat(snapshots ->
                snapshots.size() == 1 && "72".equals(snapshots.get(0).getFieldValue())));
    }
    @Test
@@ -345,15 +368,12 @@
    private void mockGroupSnapshot()
    {
        TBatteryGroupRelationField field = new TBatteryGroupRelationField();
        field.setBatteryGroupFieldId(11L);
        field.setFieldName("额定容量");
        field.setFieldType("number");
        field.setFieldValue("72");
        TBatteryGroup group = new TBatteryGroup();
        group.setGroupId(1L);
        group.setTemplateFields(java.util.List.of(field));
        when(batteryGroupService.selectBatteryGroupById(1L)).thenReturn(group);
        TTestFieldSnapshot snapshot = new TTestFieldSnapshot();
        snapshot.setFieldDefinitionId(11L);
        snapshot.setFieldName("额定容量");
        snapshot.setFieldType("number");
        snapshot.setFieldValue("72");
        when(sharedFieldSnapshotService.createForBatteryGroup(1L)).thenReturn(java.util.List.of(snapshot));
    }
    private TTest validTest()
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TSharedFieldSnapshotMapper.java
New file
@@ -0,0 +1,12 @@
package com.ruoyi.system.mapper;
import java.util.List;
import com.ruoyi.system.domain.TTestFieldSnapshot;
/** 共享业务字段快照持久层。 */
public interface TSharedFieldSnapshotMapper
{
    List<TTestFieldSnapshot> selectByTestId(Long testId);
    int insert(TTestFieldSnapshot snapshot);
    int deleteByTestId(Long testId);
}
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
@@ -7,7 +7,6 @@
import com.ruoyi.system.domain.TTestBatteryField;
import com.ruoyi.system.domain.TTestChannelBinding;
import com.ruoyi.system.domain.TTestCollectorChannel;
import com.ruoyi.system.domain.TTestFieldSnapshot;
import com.ruoyi.system.domain.TTestPlanStep;
import com.ruoyi.system.domain.TTestResult;
import com.ruoyi.system.domain.TTestWarningThreshold;
@@ -23,13 +22,13 @@
    TTest selectTestById(Long testId);
    TTest selectTestByIdForUpdate(Long testId);
    List<TTest> selectTestList(TTest test);
    List<TTestBattery> selectBatteryNosByTestIds(@Param("testIds") List<Long> testIds);
    TTest selectTestByName(String testName);
    int insertTest(TTest test);
    int updateTest(TTest test);
    int deleteTestById(@Param("testId") Long testId, @Param("updateBy") String updateBy);
    int deleteTestBindingsByTestId(Long testId);
    int deleteTestCollectorChannelsByTestId(Long testId);
    int deleteTestFieldSnapshotsByTestId(Long testId);
    int deleteTestPlanStepsByTestId(Long testId);
    int deleteTestBatteriesByTestId(Long testId);
    int deleteTestFieldsByTestId(Long testId);
@@ -39,14 +38,12 @@
    int insertTestBatteryField(TTestBatteryField field);
    int insertTestChannelBinding(TTestChannelBinding binding);
    int insertTestCollectorChannel(TTestCollectorChannel mapping);
    int insertTestFieldSnapshot(TTestFieldSnapshot snapshot);
    int insertTestThreshold(TTestWarningThreshold threshold);
    List<TTestPlanStep> selectTestPlanSteps(Long testId);
    List<TTestBattery> selectTestBatteries(Long testId);
    List<TTestBatteryField> selectTestBatteryFields(Long testId);
    List<TTestChannelBinding> selectTestChannelBindings(Long testId);
    List<TTestCollectorChannel> selectTestCollectorChannels(Long testId);
    List<TTestFieldSnapshot> selectTestFieldSnapshots(Long testId);
    List<TTestWarningThreshold> selectTestThresholds(Long testId);
    List<TTestResult> selectTestResults(TTestResult result);
    int countTestTypeEnabled(Long testTypeId);
ruoyi-system/src/main/java/com/ruoyi/system/service/ISharedFieldSnapshotService.java
New file
@@ -0,0 +1,24 @@
package com.ruoyi.system.service;
import java.util.List;
import com.ruoyi.system.domain.TTestFieldSnapshot;
/** 共享业务字段快照服务,隔离字段来源、快照规则和存储实现。 */
public interface ISharedFieldSnapshotService
{
    /** 从电池组当前共享字段创建一份测试快照。 */
    List<TTestFieldSnapshot> createForBatteryGroup(Long groupId);
    /** 同组编辑保留原快照,切换电池组时按新组创建快照。 */
    List<TTestFieldSnapshot> resolveForUpdate(Long currentGroupId, Long nextGroupId,
            List<TTestFieldSnapshot> currentSnapshots);
    /** 返回测试保存的不可变共享字段快照。 */
    List<TTestFieldSnapshot> selectByTestId(Long testId);
    /** 保存测试的共享字段快照。 */
    void saveForTest(Long testId, List<TTestFieldSnapshot> snapshots);
    /** 删除尚未开始接收的测试快照,以便在同一事务内替换。 */
    void deleteByTestId(Long testId);
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TSharedFieldSnapshotServiceImpl.java
New file
@@ -0,0 +1,88 @@
package com.ruoyi.system.service.impl;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.ruoyi.system.domain.TBatteryGroup;
import com.ruoyi.system.domain.TBatteryGroupRelationField;
import com.ruoyi.system.domain.TTestFieldSnapshot;
import com.ruoyi.system.mapper.TSharedFieldSnapshotMapper;
import com.ruoyi.system.service.ITBatteryGroupService;
import com.ruoyi.system.service.ISharedFieldSnapshotService;
/** 共享业务字段快照服务实现。 */
@Service
public class TSharedFieldSnapshotServiceImpl implements ISharedFieldSnapshotService
{
    @Autowired
    private ITBatteryGroupService batteryGroupService;
    @Autowired
    private TSharedFieldSnapshotMapper snapshotMapper;
    @Override
    public List<TTestFieldSnapshot> createForBatteryGroup(Long groupId)
    {
        TBatteryGroup group = batteryGroupService.selectBatteryGroupById(groupId);
        List<TTestFieldSnapshot> snapshots = new ArrayList<>();
        for (TBatteryGroupRelationField field : mutable(group.getTemplateFields()))
        {
            snapshots.add(snapshot(field.getBatteryGroupFieldId(), field.getFieldName(), field.getFieldType(),
                    field.getFieldValue()));
        }
        return snapshots;
    }
    @Override
    public List<TTestFieldSnapshot> resolveForUpdate(Long currentGroupId, Long nextGroupId,
            List<TTestFieldSnapshot> currentSnapshots)
    {
        // 同组编辑不能重新读取活动值,否则会静默改写测试创建时保存的业务事实。
        if (nextGroupId.equals(currentGroupId))
        {
            return mutable(currentSnapshots).stream()
                    .map(source -> snapshot(source.getFieldDefinitionId(), source.getFieldName(),
                            source.getFieldType(), source.getFieldValue()))
                    .toList();
        }
        return createForBatteryGroup(nextGroupId);
    }
    @Override
    public List<TTestFieldSnapshot> selectByTestId(Long testId)
    {
        return mutable(snapshotMapper.selectByTestId(testId));
    }
    @Override
    public void saveForTest(Long testId, List<TTestFieldSnapshot> snapshots)
    {
        for (TTestFieldSnapshot snapshot : mutable(snapshots))
        {
            snapshot.setTestId(testId);
            snapshotMapper.insert(snapshot);
        }
    }
    @Override
    public void deleteByTestId(Long testId)
    {
        snapshotMapper.deleteByTestId(testId);
    }
    private TTestFieldSnapshot snapshot(Long definitionId, String name, String type, String value)
    {
        TTestFieldSnapshot snapshot = new TTestFieldSnapshot();
        snapshot.setFieldDefinitionId(definitionId);
        snapshot.setFieldName(name);
        snapshot.setFieldType(type);
        snapshot.setFieldValue(value);
        return snapshot;
    }
    private <T> List<T> mutable(List<T> values)
    {
        return values == null ? new ArrayList<>() : new ArrayList<>(values);
    }
}
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
@@ -15,20 +15,17 @@
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.TTest;
import com.ruoyi.system.domain.TBatteryGroup;
import com.ruoyi.system.domain.TBatteryGroupRelationField;
import com.ruoyi.system.domain.TTestBattery;
import com.ruoyi.system.domain.TTestBatteryField;
import com.ruoyi.system.domain.TTestChannelBinding;
import com.ruoyi.system.domain.TTestCollectorChannel;
import com.ruoyi.system.domain.TTestFieldSnapshot;
import com.ruoyi.system.domain.TTestPlanStep;
import com.ruoyi.system.domain.TTestResult;
import com.ruoyi.system.domain.TTestWarningThreshold;
import com.ruoyi.system.domain.vo.TestConfigurationOptionsVo;
import com.ruoyi.system.mapper.TTestMapper;
import com.ruoyi.system.service.ITTestService;
import com.ruoyi.system.service.ITBatteryGroupService;
import com.ruoyi.system.service.ISharedFieldSnapshotService;
/** 测试配置服务,负责状态边界、关系校验、事务写入和只追加审计。 */
@Service
@@ -47,7 +44,7 @@
    private TTestMapper testMapper;
    @Autowired
    private ITBatteryGroupService batteryGroupService;
    private ISharedFieldSnapshotService sharedFieldSnapshotService;
    @Override
    public TTest selectTestById(Long testId)
@@ -67,7 +64,9 @@
        test.setBatteryNo(trimToNull(test.getBatteryNo()));
        validateQueryStatus(test.getTestStatus(), TEST_STATUSES, "测试状态无效");
        validateQueryStatus(test.getReceptionStatus(), RECEPTION_STATUSES, "接收状态无效");
        return testMapper.selectTestList(test);
        List<TTest> tests = mutable(testMapper.selectTestList(test));
        populateBatteryNos(tests);
        return tests;
    }
    @Override
@@ -93,7 +92,8 @@
        validateConfiguration(test);
        test.setTestStatus(WAITING);
        test.setReceptionStatus(NOT_STARTED);
        test.setFieldSnapshots(snapshotGroupFields(test.getBatteries().get(0).getGroupId()));
        test.setFieldSnapshots(sharedFieldSnapshotService.createForBatteryGroup(
                test.getBatteries().get(0).getGroupId()));
        test.setDelFlag("0");
        test.setCreateTime(new Date());
        testMapper.insertTest(test);
@@ -121,7 +121,10 @@
            test.setMonitorTimeoutMinutes(current.getMonitorTimeoutMinutes());
        validateBase(test, true);
        validateConfiguration(test);
        test.setFieldSnapshots(resolveUpdatedFieldSnapshots(test, current));
        Long currentGroupId = current.getBatteries().isEmpty() ? null : current.getBatteries().get(0).getGroupId();
        Long nextGroupId = test.getBatteries().get(0).getGroupId();
        test.setFieldSnapshots(sharedFieldSnapshotService.resolveForUpdate(
                currentGroupId, nextGroupId, current.getFieldSnapshots()));
        test.setUpdateTime(new Date());
        int rows = testMapper.updateTest(test);
        if (rows != 1) throw new ServiceException("测试配置已变更,请刷新后重试");
@@ -133,7 +136,7 @@
        testMapper.deleteTestFieldsByTestId(test.getTestId());
        testMapper.deleteTestBatteriesByTestId(test.getTestId());
        testMapper.deleteTestThresholdsByTestId(test.getTestId());
        testMapper.deleteTestFieldSnapshotsByTestId(test.getTestId());
        sharedFieldSnapshotService.deleteByTestId(test.getTestId());
        saveConfiguration(test);
        testMapper.insertTestAudit(test.getTestId(), "TEST_CONFIGURATION_UPDATED", test.getOperatorId(),
                test.getUpdateBy(), JSON.toJSONString(current), JSON.toJSONString(test));
@@ -209,8 +212,9 @@
        }
        test.setPlanSteps(planSteps);
        test.setBatteries(batteries);
        test.setBatteryNo(joinBatteryNos(batteries));
        test.setThresholds(mutable(testMapper.selectTestThresholds(testId)));
        test.setFieldSnapshots(mutable(testMapper.selectTestFieldSnapshots(testId)));
        test.setFieldSnapshots(sharedFieldSnapshotService.selectByTestId(testId));
    }
    private void validateBase(TTest test, boolean update)
@@ -326,39 +330,20 @@
        }
    }
    private List<TTestFieldSnapshot> resolveUpdatedFieldSnapshots(TTest test, TTest current)
    private void populateBatteryNos(List<TTest> tests)
    {
        Long nextGroupId = test.getBatteries().get(0).getGroupId();
        Long currentGroupId = current.getBatteries().isEmpty() ? null : current.getBatteries().get(0).getGroupId();
        if (nextGroupId.equals(currentGroupId))
            return mutable(current.getFieldSnapshots()).stream().map(this::copySnapshot).toList();
        return snapshotGroupFields(nextGroupId);
        if (tests.isEmpty()) return;
        Map<Long, List<TTestBattery>> batteriesByTest = new HashMap<>();
        List<Long> testIds = tests.stream().map(TTest::getTestId).toList();
        for (TTestBattery battery : mutable(testMapper.selectBatteryNosByTestIds(testIds)))
            batteriesByTest.computeIfAbsent(battery.getTestId(), ignored -> new ArrayList<>()).add(battery);
        for (TTest item : tests)
            item.setBatteryNo(joinBatteryNos(batteriesByTest.get(item.getTestId())));
    }
    private List<TTestFieldSnapshot> snapshotGroupFields(Long groupId)
    private String joinBatteryNos(List<TTestBattery> batteries)
    {
        TBatteryGroup group = batteryGroupService.selectBatteryGroupById(groupId);
        List<TTestFieldSnapshot> snapshots = new ArrayList<>();
        for (TBatteryGroupRelationField field : mutable(group.getTemplateFields()))
        {
            TTestFieldSnapshot snapshot = new TTestFieldSnapshot();
            snapshot.setFieldDefinitionId(field.getBatteryGroupFieldId());
            snapshot.setFieldName(field.getFieldName());
            snapshot.setFieldType(field.getFieldType());
            snapshot.setFieldValue(field.getFieldValue());
            snapshots.add(snapshot);
        }
        return snapshots;
    }
    private TTestFieldSnapshot copySnapshot(TTestFieldSnapshot source)
    {
        TTestFieldSnapshot copy = new TTestFieldSnapshot();
        copy.setFieldDefinitionId(source.getFieldDefinitionId());
        copy.setFieldName(source.getFieldName());
        copy.setFieldType(source.getFieldType());
        copy.setFieldValue(source.getFieldValue());
        return copy;
        return mutable(batteries).stream().map(TTestBattery::getBatteryNo).collect(java.util.stream.Collectors.joining("、"));
    }
    private void validatePlanSteps(List<TTestPlanStep> planSteps)
@@ -436,11 +421,7 @@
                testMapper.insertTestCollectorChannel(mapping);
            }
        }
        for (TTestFieldSnapshot snapshot : mutable(test.getFieldSnapshots()))
        {
            snapshot.setTestId(test.getTestId());
            testMapper.insertTestFieldSnapshot(snapshot);
        }
        sharedFieldSnapshotService.saveForTest(test.getTestId(), test.getFieldSnapshots());
        for (TTestWarningThreshold threshold : test.getThresholds())
        {
            threshold.setTestId(test.getTestId());
ruoyi-system/src/main/resources/mapper/system/TSharedFieldSnapshotMapper.xml
New file
@@ -0,0 +1,27 @@
<?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.ruoyi.system.mapper.TSharedFieldSnapshotMapper">
    <resultMap id="TTestFieldSnapshotResultMap" type="TTestFieldSnapshot">
        <id property="snapshotId" column="snapshot_id" />
        <result property="testId" column="test_id" />
        <result property="fieldDefinitionId" column="field_definition_id" />
        <result property="fieldName" column="field_name" />
        <result property="fieldType" column="field_type" />
        <result property="fieldValue" column="field_value" />
    </resultMap>
    <select id="selectByTestId" parameterType="Long" resultMap="TTestFieldSnapshotResultMap">
        select snapshot_id, test_id, field_definition_id, field_name, field_type, field_value
        from t_test_field_snapshot where test_id = #{testId} order by snapshot_id
    </select>
    <insert id="insert" parameterType="TTestFieldSnapshot" useGeneratedKeys="true" keyProperty="snapshotId">
        insert into t_test_field_snapshot
        (test_id, field_definition_id, field_name, field_type, field_value)
        values(#{testId}, #{fieldDefinitionId}, #{fieldName}, #{fieldType}, #{fieldValue})
    </insert>
    <delete id="deleteByTestId">delete from t_test_field_snapshot where test_id = #{testId}</delete>
</mapper>
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -75,15 +75,6 @@
        <result property="channelCode" column="channel_code" />
    </resultMap>
    <resultMap id="TTestFieldSnapshotResultMap" type="TTestFieldSnapshot">
        <id property="snapshotId" column="snapshot_id" />
        <result property="testId" column="test_id" />
        <result property="fieldDefinitionId" column="field_definition_id" />
        <result property="fieldName" column="field_name" />
        <result property="fieldType" column="field_type" />
        <result property="fieldValue" column="field_value" />
    </resultMap>
    <resultMap id="TTestPlanStepResultMap" type="TTestPlanStep">
        <id property="planStepId" column="plan_step_id" />
        <result property="testId" column="test_id" />
@@ -124,9 +115,7 @@
               t.test_status, t.reception_status, t.monitor_timeout_minutes,
               t.started_at_utc, t.ended_at_utc, t.del_flag,
               t.create_by, t.create_time, t.update_by, t.update_time, t.remark,
               (select count(1) from t_test_battery b where b.test_id = t.test_id and b.del_flag = 0) battery_count,
               (select group_concat(b.battery_no order by b.relation_id separator '、')
                  from t_test_battery b where b.test_id = t.test_id and b.del_flag = 0) battery_no
               (select count(1) from t_test_battery b where b.test_id = t.test_id and b.del_flag = 0) battery_count
        from t_test t
        left join sys_user creator on creator.user_id = t.creator_id and creator.del_flag = '0'
        left join sys_user principal on principal.user_id = t.principal_id and principal.del_flag = '0'
@@ -147,6 +136,14 @@
            <if test="endCreateTime != null">and t.create_time &lt;= #{endCreateTime}</if>
        </where>
        order by t.create_time desc, t.test_id desc
    </select>
    <select id="selectBatteryNosByTestIds" resultMap="TTestBatteryResultMap">
        select test_id, battery_no
        from t_test_battery
        where del_flag = 0 and test_id in
        <foreach collection="testIds" item="testId" open="(" separator="," close=")">#{testId}</foreach>
        order by test_id, relation_id
    </select>
    <select id="selectTestById" parameterType="Long" resultMap="TTestResultMap">
@@ -189,7 +186,6 @@
    <delete id="deleteTestBindingsByTestId">delete from t_test_channel_binding where test_id = #{testId}</delete>
    <delete id="deleteTestCollectorChannelsByTestId">delete from t_test_collector_channel where test_id = #{testId}</delete>
    <delete id="deleteTestFieldSnapshotsByTestId">delete from t_test_field_snapshot where test_id = #{testId}</delete>
    <delete id="deleteTestPlanStepsByTestId">delete from t_test_plan_step where test_id = #{testId}</delete>
    <update id="deleteTestFieldsByTestId">
        update t_test_battery_field f join t_test_battery b on b.relation_id = f.relation_id
@@ -222,11 +218,6 @@
        insert into t_test_collector_channel
        (test_id, battery_relation_id, collector_id, measurement_code, measurement_name, channel_code)
        values(#{testId}, #{batteryRelationId}, #{collectorId}, #{measurementCode}, #{measurementName}, #{channelCode})
    </insert>
    <insert id="insertTestFieldSnapshot" parameterType="TTestFieldSnapshot" useGeneratedKeys="true" keyProperty="snapshotId">
        insert into t_test_field_snapshot
        (test_id, field_definition_id, field_name, field_type, field_value)
        values(#{testId}, #{fieldDefinitionId}, #{fieldName}, #{fieldType}, #{fieldValue})
    </insert>
    <insert id="insertTestThreshold" parameterType="TTestWarningThreshold" useGeneratedKeys="true" keyProperty="thresholdId">
        insert into t_test_warning_threshold(test_id, metric_code, min_value, max_value, unit, severity, del_flag)
@@ -268,10 +259,6 @@
               measurement_code, measurement_name, channel_code
        from t_test_collector_channel where test_id = #{testId}
        order by battery_relation_id, mapping_id
    </select>
    <select id="selectTestFieldSnapshots" parameterType="Long" resultMap="TTestFieldSnapshotResultMap">
        select snapshot_id, test_id, field_definition_id, field_name, field_type, field_value
        from t_test_field_snapshot where test_id = #{testId} order by snapshot_id
    </select>
    <select id="selectTestThresholds" parameterType="Long" resultMap="TTestThresholdResultMap">
        select threshold_id, test_id, metric_code, min_value, max_value, unit, severity, del_flag