From c2eefadb8239df552ef43d7ba55d5735bb603ed1 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 30 八月 2026 14:50:28 +0800
Subject: [PATCH] 实现测试暂停继续接收区间
---
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java | 163 +++++++++++++++++++++++
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java | 30 ++++
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestServiceImplTest.java | 93 +++++++++++++
ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java | 9 +
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml | 48 ++++++
ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java | 9 +
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java | 11 +
ruoyi-admin/src/main/resources/db/migration/V15__pause_resume_reception_actions.sql | 27 +++
8 files changed, 390 insertions(+), 0 deletions(-)
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
index c1cd2e5..db97598 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
@@ -20,6 +20,7 @@
import com.ruoyi.common.enums.BusinessType;
import com.ruoyi.system.domain.TTest;
import com.ruoyi.system.domain.TTestResult;
+import com.ruoyi.system.domain.TTestReceptionInterval;
import com.ruoyi.system.service.ITTestService;
/** 测试管理 Controller。 */
@@ -95,6 +96,35 @@
return success(testService.selectTestById(testId));
}
+ /** 独立暂停接收动作;只关闭系统接收区间,不向设备发送控制命令。 */
+ @PreAuthorize("@ss.hasPermi('system:test:pause')")
+ @Log(title = "测试数据接收", businessType = BusinessType.UPDATE)
+ @PostMapping("/{testId}/reception/pause")
+ public AjaxResult pauseReception(@PathVariable Long testId)
+ {
+ testService.pauseReception(testId, getUserId(), getUsername());
+ return success(testService.selectTestById(testId));
+ }
+
+ /** 独立继续接收动作;从新的成功观测边界打开区间,不向设备发送控制命令。 */
+ @PreAuthorize("@ss.hasPermi('system:test:resume')")
+ @Log(title = "测试数据接收", businessType = BusinessType.UPDATE)
+ @PostMapping("/{testId}/reception/resume")
+ public AjaxResult resumeReception(@PathVariable Long testId)
+ {
+ testService.resumeReception(testId, getUserId(), getUsername());
+ return success(testService.selectTestById(testId));
+ }
+
+ /** 返回测试各来源的接收区间、动作时间和实际采样边界。 */
+ @PreAuthorize("@ss.hasPermi('system:test:query')")
+ @GetMapping("/{testId}/reception/intervals")
+ public AjaxResult receptionIntervals(@PathVariable Long testId)
+ {
+ List<TTestReceptionInterval> intervals = testService.selectReceptionIntervals(testId);
+ return success(intervals);
+ }
+
@PreAuthorize("@ss.hasPermi('system:test:remove')")
@Log(title = "测试管理", businessType = BusinessType.DELETE)
@DeleteMapping("/{testIds}")
diff --git a/ruoyi-admin/src/main/resources/db/migration/V15__pause_resume_reception_actions.sql b/ruoyi-admin/src/main/resources/db/migration/V15__pause_resume_reception_actions.sql
new file mode 100644
index 0000000..7379b1a
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration/V15__pause_resume_reception_actions.sql
@@ -0,0 +1,27 @@
+-- Ticket10:暂停/继续接收权限与区间查询索引;不改变已应用迁移。
+ALTER TABLE t_test_reception_interval
+ ADD KEY idx_reception_interval_status (test_id, interval_status, interval_id);
+
+SET @pause_menu_id := (SELECT COALESCE(MAX(menu_id), 0) + 1 FROM sys_menu);
+INSERT INTO sys_menu
+ (menu_id, menu_name, parent_id, order_num, path, component, query, route_name,
+ is_frame, is_cache, menu_type, visible, status, perms, icon,
+ create_by, create_time, update_by, update_time, remark)
+SELECT @pause_menu_id, '暂停接收', menu_id, 7, '', NULL, NULL, '', 1, 0, 'F', '0', '0',
+ 'system:test:pause', '#', 'admin', NOW(), '', NULL, '测试暂停接收权限'
+FROM sys_menu
+WHERE perms = 'system:test:list'
+ AND NOT EXISTS (SELECT 1 FROM sys_menu existing WHERE existing.perms = 'system:test:pause')
+LIMIT 1;
+
+SET @resume_menu_id := (SELECT COALESCE(MAX(menu_id), 0) + 1 FROM sys_menu);
+INSERT INTO sys_menu
+ (menu_id, menu_name, parent_id, order_num, path, component, query, route_name,
+ is_frame, is_cache, menu_type, visible, status, perms, icon,
+ create_by, create_time, update_by, update_time, remark)
+SELECT @resume_menu_id, '继续接收', menu_id, 8, '', NULL, NULL, '', 1, 0, 'F', '0', '0',
+ 'system:test:resume', '#', 'admin', NOW(), '', NULL, '测试继续接收权限'
+FROM sys_menu
+WHERE perms = 'system:test:list'
+ AND NOT EXISTS (SELECT 1 FROM sys_menu existing WHERE existing.perms = 'system:test:resume')
+LIMIT 1;
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestServiceImplTest.java b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestServiceImplTest.java
index 04ec96a..c54c680 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestServiceImplTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/TTestServiceImplTest.java
@@ -15,12 +15,16 @@
import java.math.BigDecimal;
import java.util.Collections;
import java.util.LinkedHashMap;
+import java.util.List;
import java.time.Instant;
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.device.bts.BtsDevelopmentSimulator;
+import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement;
import org.springframework.dao.DuplicateKeyException;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.system.domain.TTest;
@@ -31,6 +35,7 @@
import com.ruoyi.system.domain.TTestPlanStep;
import com.ruoyi.system.domain.TTestWarningThreshold;
import com.ruoyi.system.domain.TDetectionChannel;
+import com.ruoyi.system.domain.TTestReceptionInterval;
import com.ruoyi.system.mapper.TTestMapper;
import com.ruoyi.system.service.ISharedFieldSnapshotService;
@@ -43,6 +48,9 @@
@Mock
private ISharedFieldSnapshotService sharedFieldSnapshotService;
+
+ @Mock
+ private BtsDevelopmentSimulator collectorSimulator;
@InjectMocks
private TTestServiceImpl testService;
@@ -488,6 +496,91 @@
verify(testMapper, never()).selectTestByIdForUpdate(anyLong());
}
+ @Test
+ void pauseReceptionShouldCloseAllOpenIntervalsAndAppendAudit()
+ {
+ TTest current = validTest();
+ current.setTestId(10L);
+ current.setReceptionStatus("RECEIVING");
+ when(testMapper.selectTestByIdForUpdate(10L)).thenReturn(current);
+ TTestReceptionInterval cabinet = receptionInterval(1L, "CABINET_CHANNEL", 100L, null);
+ TTestReceptionInterval collector = receptionInterval(2L, "COLLECTOR_CHANNEL", 8L, "S-01");
+ when(testMapper.selectOpenReceptionIntervalsForUpdate(10L)).thenReturn(List.of(cabinet, collector));
+ TDetectionChannel channel = new TDetectionChannel();
+ channel.setChannelId(100L); channel.setOnlineStatus("ONLINE"); channel.setLastSeenAtUtc(Instant.parse("2026-08-29T12:00:00Z"));
+ when(testMapper.selectReceptionChannelForUpdate(100L)).thenReturn(channel);
+ when(testMapper.collectorOnline(8L)).thenReturn(true);
+ mockCollectorBoundary();
+ when(testMapper.closeReceptionInterval(anyLong(), any(), any())).thenReturn(1);
+ when(testMapper.updateReceptionPaused(eq(10L), eq("operator"), any())).thenReturn(1);
+
+ assertEquals(1, testService.pauseReception(10L, 7L, "operator"));
+ verify(testMapper, times(2)).closeReceptionInterval(anyLong(), any(), any());
+ verify(testMapper).updateReceptionPaused(eq(10L), eq("operator"), any());
+ verify(testMapper).insertTestAudit(eq(10L), eq("RECEPTION_PAUSED"), eq(7L), eq("operator"), isNull(), any());
+ }
+
+ @Test
+ void pauseReceptionShouldFailBeforeWritesWhenBoundaryUnavailable()
+ {
+ TTest current = validTest();
+ current.setTestId(10L); current.setReceptionStatus("RECEIVING");
+ when(testMapper.selectTestByIdForUpdate(10L)).thenReturn(current);
+ TTestReceptionInterval interval = receptionInterval(1L, "CABINET_CHANNEL", 100L, null);
+ when(testMapper.selectOpenReceptionIntervalsForUpdate(10L)).thenReturn(List.of(interval));
+ when(testMapper.selectReceptionChannelForUpdate(100L)).thenReturn(null);
+
+ ServiceException exception = assertThrows(ServiceException.class,
+ () -> testService.pauseReception(10L, 7L, "operator"));
+ assertEquals("检测柜通道 100 当前无法确认暂停接收边界", exception.getMessage());
+ verify(testMapper, never()).closeReceptionInterval(anyLong(), any(), any());
+ verify(testMapper, never()).updateReceptionPaused(anyLong(), any(), any());
+ }
+
+ @Test
+ void resumeReceptionShouldOpenNewIntervalsAndAppendAudit()
+ {
+ TTest current = validTest();
+ current.setTestId(10L); current.setReceptionStatus("PAUSED");
+ current.getBatteries().get(0).getBinding().setBindingId(30L);
+ when(testMapper.selectTestByIdForUpdate(10L)).thenReturn(current);
+ TDetectionChannel channel = new TDetectionChannel();
+ channel.setChannelId(100L); channel.setOnlineStatus("ONLINE"); channel.setLastSeenAtUtc(Instant.parse("2026-08-29T12:00:00Z"));
+ when(testMapper.selectReceptionChannelForUpdate(100L)).thenReturn(channel);
+ when(testMapper.countCollectorEnabled(8L)).thenReturn(1);
+ when(testMapper.collectorOnline(8L)).thenReturn(true);
+ when(testMapper.selectCollectorChannelCount(8L)).thenReturn(4);
+ mockCollectorBoundary();
+ when(testMapper.updateReceptionResumed(eq(10L), eq("operator"), any())).thenReturn(1);
+
+ assertEquals(1, testService.resumeReception(10L, 7L, "operator"));
+ verify(testMapper, times(4)).insertReceptionInterval(any());
+ verify(testMapper).updateReceptionResumed(eq(10L), eq("operator"), any());
+ verify(testMapper).insertTestAudit(eq(10L), eq("RECEPTION_RESUMED"), eq(7L), eq("operator"), isNull(), any());
+ }
+
+ private TTestReceptionInterval receptionInterval(Long id, String sourceType, Long sourceId, String channelCode)
+ {
+ TTestReceptionInterval interval = new TTestReceptionInterval();
+ interval.setIntervalId(id); interval.setTestId(10L); interval.setSourceType(sourceType);
+ interval.setSourceId(sourceId); interval.setSourceChannelCode(channelCode); interval.setIntervalStatus("OPEN");
+ return interval;
+ }
+
+ private void mockCollectorBoundary()
+ {
+ Instant deviceTime = Instant.parse("2026-08-29T12:00:00Z");
+ CollectorRealtimeMeasurement measurement = new CollectorRealtimeMeasurement("dev-sim", "collector-8", "S-01",
+ 1L, deviceTime, "2026-08-29 20:00:00", deviceTime, "PRIMARY_TEMPERATURE", new BigDecimal("25"), "℃");
+ CollectorRealtimeMeasurement positive = new CollectorRealtimeMeasurement("dev-sim", "collector-8", "S-02",
+ 1L, deviceTime, "2026-08-29 20:00:00", deviceTime, "PRIMARY_POSITIVE_POTENTIAL", new BigDecimal("1"), "V");
+ CollectorRealtimeMeasurement negative = new CollectorRealtimeMeasurement("dev-sim", "collector-8", "S-03",
+ 1L, deviceTime, "2026-08-29 20:00:00", deviceTime, "PRIMARY_NEGATIVE_POTENTIAL", new BigDecimal("1"), "V");
+ when(collectorSimulator.collectorRealtime(8)).thenReturn(
+ new CollectorRealtimeSnapshot("collector-8", true, 1000,
+ List.of(measurement, positive, negative)));
+ }
+
private void mockEnabledOptions()
{
mockEnabledPeopleAndType();
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java
index 8db451e..0c6a21b 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java
@@ -4,6 +4,7 @@
import java.lang.reflect.Method;
import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.security.access.prepost.PreAuthorize;
class TTestControllerContractTest
@@ -18,6 +19,14 @@
assertPermission("startReception", "@ss.hasPermi('system:test:start')", Long.class);
Method startReception = TTestController.class.getMethod("startReception", Long.class);
assertEquals("/{testId}/reception/start", startReception.getAnnotation(PostMapping.class).value()[0]);
+ assertPermission("pauseReception", "@ss.hasPermi('system:test:pause')", Long.class);
+ assertEquals("/{testId}/reception/pause", TTestController.class.getMethod("pauseReception", Long.class)
+ .getAnnotation(PostMapping.class).value()[0]);
+ assertPermission("resumeReception", "@ss.hasPermi('system:test:resume')", Long.class);
+ assertEquals("/{testId}/reception/resume", TTestController.class.getMethod("resumeReception", Long.class)
+ .getAnnotation(PostMapping.class).value()[0]);
+ assertEquals("/{testId}/reception/intervals", TTestController.class.getMethod("receptionIntervals", Long.class)
+ .getAnnotation(GetMapping.class).value()[0]);
}
private void assertPermission(String methodName, String permission, Class<?>... parameterTypes) throws Exception
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
index 47cd067..1f18933 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java
@@ -78,4 +78,15 @@
int updateReceptionStarted(@Param("testId") Long testId, @Param("startedAtUtc") Date startedAtUtc,
@Param("updateBy") String updateBy);
int insertReceptionInterval(TTestReceptionInterval interval);
+ List<TTestReceptionInterval> selectOpenReceptionIntervalsForUpdate(Long testId);
+ List<TTestReceptionInterval> selectReceptionIntervals(Long testId);
+ TTestReceptionInterval selectLatestReceptionIntervalForUpdate(@Param("testId") Long testId,
+ @Param("sourceType") String sourceType, @Param("sourceId") Long sourceId,
+ @Param("sourceChannelCode") String sourceChannelCode);
+ int closeReceptionInterval(@Param("intervalId") Long intervalId, @Param("endBoundaryAtUtc") Date endBoundaryAtUtc,
+ @Param("boundaryNote") String boundaryNote);
+ int updateReceptionPaused(@Param("testId") Long testId, @Param("updateBy") String updateBy,
+ @Param("updatedAtUtc") Date updatedAtUtc);
+ int updateReceptionResumed(@Param("testId") Long testId, @Param("updateBy") String updateBy,
+ @Param("updatedAtUtc") Date updatedAtUtc);
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java
index e09428a..bb3082a 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITTestService.java
@@ -29,6 +29,15 @@
/** 原子开始接收:校验全部来源并从实际观测边界打开区间。 */
int startReception(Long testId, Long operatorId, String updateBy);
+ /** 原子暂停接收:关闭全部开放区间并保留设备实时观察。 */
+ int pauseReception(Long testId, Long operatorId, String updateBy);
+
+ /** 原子继续接收:从新的成功观测边界打开全部来源区间。 */
+ int resumeReception(Long testId, Long operatorId, String updateBy);
+
+ /** 查询测试各数据来源的接收区间和实际边界。 */
+ List<com.ruoyi.system.domain.TTestReceptionInterval> selectReceptionIntervals(Long testId);
+
/** 仅删除未开始接收的测试并追加审计记录。 */
int deleteTestByIds(Long[] testIds, Long operatorId, String updateBy);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
index e2fedad..a60fe2f 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
@@ -27,6 +27,8 @@
import com.ruoyi.system.domain.TDetectionChannel;
import com.ruoyi.system.domain.TTestReceptionInterval;
import com.ruoyi.system.domain.vo.TestConfigurationOptionsVo;
+import com.ruoyi.device.bts.BtsDevelopmentSimulator;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement;
import com.ruoyi.system.mapper.TTestMapper;
import com.ruoyi.system.service.ITTestService;
import com.ruoyi.system.service.ISharedFieldSnapshotService;
@@ -49,6 +51,9 @@
@Autowired
private ISharedFieldSnapshotService sharedFieldSnapshotService;
+
+ @Autowired(required = false)
+ private BtsDevelopmentSimulator collectorSimulator;
@Override
public TTest selectTestById(Long testId)
@@ -229,6 +234,164 @@
}
}
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int pauseReception(Long testId, Long operatorId, String updateBy)
+ {
+ if (testId == null) throw new ServiceException("测试ID不能为空");
+ if (operatorId == null) throw new ServiceException("操作人不能为空");
+ TTest current = testMapper.selectTestByIdForUpdate(testId);
+ if (current == null) throw new ServiceException("测试不存在");
+ if (!"RECEIVING".equals(current.getReceptionStatus()))
+ throw new ServiceException("当前测试不是接收中状态");
+
+ List<TTestReceptionInterval> openIntervals = mutable(testMapper.selectOpenReceptionIntervalsForUpdate(testId));
+ if (openIntervals.isEmpty()) throw new ServiceException("当前测试没有开放的接收区间");
+ Date actionAt = new Date();
+ Map<Long, Date> boundaries = new HashMap<>();
+ // 先确认所有来源都能取得边界,再写入任何一条关闭记录,保证整项动作原子失败。
+ for (TTestReceptionInterval open : openIntervals)
+ {
+ Date boundary = confirmBoundary(open, actionAt);
+ boundaries.put(open.getIntervalId(), boundary);
+ open.setEndBoundaryAtUtc(boundary);
+ open.setIntervalStatus("CLOSED");
+ }
+ for (TTestReceptionInterval open : openIntervals)
+ {
+ if (testMapper.closeReceptionInterval(open.getIntervalId(), boundaries.get(open.getIntervalId()),
+ "暂停接收:采用来源最近成功观测边界") != 1)
+ throw new ServiceException("接收区间已变更,请刷新后重试");
+ }
+ if (testMapper.updateReceptionPaused(testId, updateBy, actionAt) != 1)
+ throw new ServiceException("测试状态已变更,请刷新后重试");
+ testMapper.insertTestAudit(testId, "RECEPTION_PAUSED", operatorId, updateBy, null,
+ JSON.toJSONString(Map.of("receptionStatus", "PAUSED", "intervalCount", openIntervals.size(),
+ "intervals", openIntervals)));
+ return 1;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public int resumeReception(Long testId, Long operatorId, String updateBy)
+ {
+ if (testId == null) throw new ServiceException("测试ID不能为空");
+ if (operatorId == null) throw new ServiceException("操作人不能为空");
+ TTest current = testMapper.selectTestByIdForUpdate(testId);
+ if (current == null) throw new ServiceException("测试不存在");
+ if (!"PAUSED".equals(current.getReceptionStatus()))
+ throw new ServiceException("当前测试不是暂停接收状态");
+ loadConfiguration(current);
+ if (current.getBatteries().isEmpty()) throw new ServiceException("测试未配置电池号");
+
+ Date actionAt = new Date();
+ List<TTestReceptionInterval> intervals = new ArrayList<>();
+ Set<Long> channelIds = new HashSet<>();
+ Set<String> collectorChannels = new HashSet<>();
+ for (TTestBattery battery : current.getBatteries())
+ {
+ TTestChannelBinding binding = battery.getBinding();
+ if (binding == null || binding.getChannelId() == null)
+ throw new ServiceException("电池号 " + battery.getBatteryNo() + " 未绑定检测柜通道");
+ if (!channelIds.add(binding.getChannelId())) throw new ServiceException("检测柜通道重复绑定,无法继续接收");
+ TDetectionChannel channel = testMapper.selectReceptionChannelForUpdate(binding.getChannelId());
+ if (channel == null || !isEnabled(channel.getStatus()) || !isOnline(channel.getOnlineStatus())
+ || channel.getLastSeenAtUtc() == null)
+ throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 当前无法确认继续接收边界");
+ if (isEndedWorkStatus(channel.getWorkStatus()))
+ throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 已结束,不能继续接收");
+ if (battery.getCollectorId() == null || testMapper.countCollectorEnabled(battery.getCollectorId()) == 0
+ || !testMapper.collectorOnline(battery.getCollectorId()))
+ throw new ServiceException("采集器 " + battery.getCollectorId() + " 当前无法确认继续接收边界");
+ Integer count = testMapper.selectCollectorChannelCount(battery.getCollectorId());
+ validateCollectorChannels(battery.getCollectorId(), count == null ? 0 : count,
+ battery.getCollectorChannels(), collectorChannels);
+ Date cabinetBoundary = Date.from(channel.getLastSeenAtUtc());
+ ensureAfterPreviousBoundary(current.getTestId(), "CABINET_CHANNEL", binding.getChannelId(), null,
+ cabinetBoundary);
+ intervals.add(interval(current, binding.getBindingId(), "CABINET_CHANNEL", binding.getChannelId(), null,
+ actionAt, cabinetBoundary, "继续接收:采用检测柜最近成功观测边界"));
+ for (TTestCollectorChannel mapping : mutable(battery.getCollectorChannels()))
+ {
+ Date collectorBoundary = collectorBoundary(battery.getCollectorId(), mapping.getChannelCode());
+ ensureAfterPreviousBoundary(current.getTestId(), "COLLECTOR_CHANNEL", battery.getCollectorId(),
+ mapping.getChannelCode(), collectorBoundary);
+ intervals.add(interval(current, binding.getBindingId(), "COLLECTOR_CHANNEL", battery.getCollectorId(),
+ mapping.getChannelCode(), actionAt, collectorBoundary, "继续接收:采用采集器最近成功观测边界"));
+ }
+ }
+ for (TTestReceptionInterval interval : intervals) testMapper.insertReceptionInterval(interval);
+ if (testMapper.updateReceptionResumed(testId, updateBy, actionAt) != 1)
+ throw new ServiceException("测试状态已变更,请刷新后重试");
+ testMapper.insertTestAudit(testId, "RECEPTION_RESUMED", operatorId, updateBy, null,
+ JSON.toJSONString(Map.of("receptionStatus", "RECEIVING", "intervalCount", intervals.size(),
+ "intervals", intervals)));
+ return 1;
+ }
+
+ @Override
+ public List<TTestReceptionInterval> selectReceptionIntervals(Long testId)
+ {
+ if (testId == null) throw new ServiceException("测试ID不能为空");
+ selectTestById(testId);
+ return mutable(testMapper.selectReceptionIntervals(testId));
+ }
+
+ private Date confirmBoundary(TTestReceptionInterval interval, Date actionAt)
+ {
+ if ("CABINET_CHANNEL".equals(interval.getSourceType()))
+ {
+ TDetectionChannel channel = testMapper.selectReceptionChannelForUpdate(interval.getSourceId());
+ if (channel == null || !isEnabled(channel.getStatus()) || !isOnline(channel.getOnlineStatus())
+ || channel.getLastSeenAtUtc() == null)
+ throw new ServiceException("检测柜通道 " + interval.getSourceId() + " 当前无法确认暂停接收边界");
+ Date boundary = Date.from(channel.getLastSeenAtUtc());
+ if (interval.getStartBoundaryAtUtc() != null && boundary.before(interval.getStartBoundaryAtUtc()))
+ throw new ServiceException("检测柜通道 " + interval.getSourceId() + " 的暂停边界早于区间开始");
+ return boundary;
+ }
+ if (!testMapper.collectorOnline(interval.getSourceId()))
+ throw new ServiceException("采集器 " + interval.getSourceId() + " 当前无法确认暂停接收边界");
+ Date boundary = collectorBoundary(interval.getSourceId(), interval.getSourceChannelCode());
+ if (interval.getStartBoundaryAtUtc() != null && boundary.before(interval.getStartBoundaryAtUtc()))
+ throw new ServiceException("采集器 " + interval.getSourceId() + " 的暂停边界早于区间开始");
+ return boundary;
+ }
+
+ private Date collectorBoundary(Long collectorId, String channelCode)
+ {
+ if (collectorSimulator == null) throw new ServiceException("采集器 " + collectorId + " 当前无法取得成功观测边界");
+ try
+ {
+ return collectorSimulator.collectorRealtime(collectorId.intValue()).measurements().stream()
+ .filter(item -> channelCode == null || channelCode.equals(item.channelCode()))
+ .map(CollectorRealtimeMeasurement::deviceTime).filter(java.util.Objects::nonNull)
+ .max(java.time.Instant::compareTo).map(Date::from)
+ .orElseThrow(() -> new ServiceException("采集器 " + collectorId + " 当前无法取得成功观测边界"));
+ }
+ catch (RuntimeException exception)
+ {
+ if (exception instanceof ServiceException serviceException) throw serviceException;
+ throw new ServiceException("采集器 " + collectorId + " 当前无法取得成功观测边界");
+ }
+ }
+
+ private void ensureAfterPreviousBoundary(Long testId, String sourceType, Long sourceId,
+ String sourceChannelCode, Date boundary)
+ {
+ TTestReceptionInterval previous = testMapper.selectLatestReceptionIntervalForUpdate(testId, sourceType,
+ sourceId, sourceChannelCode);
+ if (previous != null && previous.getEndBoundaryAtUtc() != null
+ && !boundary.after(previous.getEndBoundaryAtUtc()))
+ throw new ServiceException("数据来源尚未取得新的接收边界,请稍后重试");
+ }
+
+ private boolean isEndedWorkStatus(String workStatus)
+ {
+ return "STOPPED".equalsIgnoreCase(workStatus) || "FINISHED".equalsIgnoreCase(workStatus)
+ || "ENDED".equalsIgnoreCase(workStatus);
+ }
+
private TTestReceptionInterval interval(TTest test, Long bindingId, String sourceType, Long sourceId,
String sourceChannelCode, Date actionAt, Date boundaryAt, String note)
{
diff --git a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
index d432575..fd242ba 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -399,4 +399,52 @@
values(#{testId}, #{bindingId}, #{sourceType}, #{sourceId}, #{sourceChannelCode}, #{actionAtUtc},
#{startBoundaryAtUtc}, #{boundaryNote}, #{intervalStatus})
</insert>
+ <select id="selectOpenReceptionIntervalsForUpdate" parameterType="Long"
+ resultType="com.ruoyi.system.domain.TTestReceptionInterval">
+ select interval_id intervalId, test_id testId, binding_id bindingId, source_type sourceType,
+ source_id sourceId, source_channel_code sourceChannelCode, action_at_utc actionAtUtc,
+ start_boundary_at_utc startBoundaryAtUtc, end_boundary_at_utc endBoundaryAtUtc,
+ boundary_note boundaryNote, interval_status intervalStatus
+ from t_test_reception_interval
+ where test_id = #{testId} and interval_status = 'OPEN'
+ order by interval_id
+ for update
+ </select>
+ <select id="selectReceptionIntervals" parameterType="Long"
+ resultType="com.ruoyi.system.domain.TTestReceptionInterval">
+ select interval_id intervalId, test_id testId, binding_id bindingId, source_type sourceType,
+ source_id sourceId, source_channel_code sourceChannelCode, action_at_utc actionAtUtc,
+ start_boundary_at_utc startBoundaryAtUtc, end_boundary_at_utc endBoundaryAtUtc,
+ boundary_note boundaryNote, interval_status intervalStatus
+ from t_test_reception_interval
+ where test_id = #{testId}
+ order by interval_id
+ </select>
+ <select id="selectLatestReceptionIntervalForUpdate" resultType="com.ruoyi.system.domain.TTestReceptionInterval">
+ select interval_id intervalId, test_id testId, binding_id bindingId, source_type sourceType,
+ source_id sourceId, source_channel_code sourceChannelCode, action_at_utc actionAtUtc,
+ start_boundary_at_utc startBoundaryAtUtc, end_boundary_at_utc endBoundaryAtUtc,
+ boundary_note boundaryNote, interval_status intervalStatus
+ from t_test_reception_interval
+ where test_id = #{testId} and source_type = #{sourceType} and source_id = #{sourceId}
+ and (source_channel_code = #{sourceChannelCode} or (source_channel_code is null and #{sourceChannelCode} is null))
+ order by interval_id desc
+ limit 1
+ for update
+ </select>
+ <update id="closeReceptionInterval">
+ update t_test_reception_interval
+ set end_boundary_at_utc = #{endBoundaryAtUtc}, boundary_note = #{boundaryNote}, interval_status = 'CLOSED'
+ where interval_id = #{intervalId} and interval_status = 'OPEN'
+ </update>
+ <update id="updateReceptionPaused">
+ update t_test
+ set test_status = 'PAUSED', reception_status = 'PAUSED', update_by = #{updateBy}, update_time = #{updatedAtUtc}
+ where test_id = #{testId} and reception_status = 'RECEIVING' and del_flag = 0
+ </update>
+ <update id="updateReceptionResumed">
+ update t_test
+ set test_status = 'RUNNING', reception_status = 'RECEIVING', update_by = #{updateBy}, update_time = #{updatedAtUtc}
+ where test_id = #{testId} and reception_status = 'PAUSED' and del_flag = 0
+ </update>
</mapper>
--
Gitblit v1.7.1