From 20b346ef9d4fcbbbe05abfe08e4f4f0303b0812e Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期日, 30 八月 2026 20:37:45 +0800
Subject: [PATCH] 实现 Ticket12 人工结束与顺序接入

---
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java |  198 ++++++++++---------------------------------------
 1 files changed, 40 insertions(+), 158 deletions(-)

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 a60fe2f..329d523 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
@@ -5,6 +5,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import com.alibaba.fastjson2.JSON;
@@ -25,10 +26,7 @@
 import com.ruoyi.system.domain.TTestFieldSnapshot;
 import com.ruoyi.system.domain.TTestWarningThreshold;
 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;
@@ -51,9 +49,6 @@
 
     @Autowired
     private ISharedFieldSnapshotService sharedFieldSnapshotService;
-
-    @Autowired(required = false)
-    private BtsDevelopmentSimulator collectorSimulator;
 
     @Override
     public TTest selectTestById(Long testId)
@@ -172,7 +167,6 @@
         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<>();
         boolean runtimeAttach = false;
@@ -186,10 +180,8 @@
             TDetectionChannel channel = testMapper.selectReceptionChannelForUpdate(binding.getChannelId());
             if (channel == null || !isEnabled(channel.getStatus()) || !isOnline(channel.getOnlineStatus()))
                 throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 当前离线或不可观测");
-            if (channel.getLastSeenAtUtc() == null)
-                throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 尚未取得成功观测边界");
-            runtimeAttach |= "WORKING".equalsIgnoreCase(channel.getWorkStatus())
-                    || "PAUSED".equalsIgnoreCase(channel.getWorkStatus());
+            // 只有设备事实为运行中时才标记“运行中接入”;设备暂停属于独立设备状态。
+            runtimeAttach |= "WORKING".equalsIgnoreCase(channel.getWorkStatus());
             if (testMapper.selectChannelLeaseForUpdate(binding.getChannelId()) != null)
                 throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 已被其他测试占用");
             if (battery.getCollectorId() == null || testMapper.countCollectorEnabled(battery.getCollectorId()) == 0)
@@ -200,14 +192,6 @@
             validateCollectorChannels(battery.getCollectorId(), collectorChannelCount == null ? 0 : collectorChannelCount,
                     battery.getCollectorChannels(), collectorChannels);
 
-            intervals.add(interval(current, binding.getBindingId(), "CABINET_CHANNEL", binding.getChannelId(), null,
-                    actionAt, Date.from(channel.getLastSeenAtUtc()), runtimeAttach
-                            ? "运行中接入:采用检测柜最近成功观测边界" : "采用检测柜最近成功观测边界"));
-            for (TTestCollectorChannel mapping : mutable(battery.getCollectorChannels()))
-            {
-                intervals.add(interval(current, binding.getBindingId(), "COLLECTOR_CHANNEL", battery.getCollectorId(),
-                        mapping.getChannelCode(), actionAt, actionAt, "采集器开始接收确认边界"));
-            }
         }
 
         try
@@ -216,16 +200,20 @@
             {
                 TTestChannelBinding binding = battery.getBinding();
                 testMapper.insertChannelLease(binding.getChannelId(), testId, binding.getBindingId(), operatorId, actionAt);
+                testMapper.insertReceptionInterval(testId, binding.getBindingId(), "CABINET_CHANNEL",
+                        binding.getChannelId(), null, actionAt, "RECEPTION_STARTED");
                 for (TTestCollectorChannel mapping : mutable(battery.getCollectorChannels()))
+                {
                     testMapper.insertCollectorChannelLease(battery.getCollectorId(), mapping.getChannelCode(), testId,
                             binding.getBindingId(), operatorId, actionAt);
+                    testMapper.insertReceptionInterval(testId, binding.getBindingId(), "COLLECTOR_CHANNEL",
+                            battery.getCollectorId(), mapping.getChannelCode(), actionAt, "RECEPTION_STARTED");
+                }
             }
-            for (TTestReceptionInterval interval : intervals) testMapper.insertReceptionInterval(interval);
-            if (testMapper.updateReceptionStarted(testId, actionAt, updateBy) != 1)
+            if (testMapper.updateReceptionStarted(testId, actionAt, runtimeAttach, updateBy) != 1)
                 throw new ServiceException("测试状态已变更,请刷新后重试");
             testMapper.insertTestAudit(testId, "RECEPTION_STARTED", operatorId, updateBy, null,
-                    JSON.toJSONString(Map.of("receptionStatus", "RECEIVING", "intervalCount", intervals.size(),
-                            "runtimeAttach", runtimeAttach, "intervals", intervals)));
+                    JSON.toJSONString(Map.of("receptionStatus", "RECEIVING", "runtimeAttach", runtimeAttach)));
             return 1;
         }
         catch (DuplicateKeyException exception)
@@ -245,29 +233,11 @@
         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)));
+                JSON.toJSONString(Map.of("receptionStatus", "PAUSED", "polling", "STOPPED")));
         return 1;
     }
 
@@ -281,131 +251,34 @@
         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)));
+                JSON.toJSONString(Map.of("receptionStatus", "RECEIVING", "polling", "RESUMED")));
         return 1;
     }
 
     @Override
-    public List<TTestReceptionInterval> selectReceptionIntervals(Long testId)
+    @Transactional(rollbackFor = Exception.class)
+    public int stopTest(Long testId, Long operatorId, String updateBy)
     {
         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)
-    {
-        TTestReceptionInterval interval = new TTestReceptionInterval();
-        interval.setTestId(test.getTestId());
-        interval.setBindingId(bindingId);
-        interval.setSourceType(sourceType);
-        interval.setSourceId(sourceId);
-        interval.setSourceChannelCode(sourceChannelCode);
-        interval.setActionAtUtc(actionAt);
-        interval.setStartBoundaryAtUtc(boundaryAt);
-        interval.setBoundaryNote(note);
-        interval.setIntervalStatus("OPEN");
-        return interval;
+        if (operatorId == null) throw new ServiceException("操作人不能为空");
+        TTest current = testMapper.selectTestByIdForUpdate(testId);
+        if (current == null) throw new ServiceException("测试不存在");
+        if (NOT_STARTED.equals(current.getReceptionStatus()) || "ENDED".equals(current.getReceptionStatus()))
+            throw new ServiceException("当前测试不能停止");
+        Date actionAt = new Date();
+        if (testMapper.updateReceptionStopped(testId, updateBy, actionAt) != 1)
+            throw new ServiceException("测试状态已变更,请刷新后重试");
+        int closedIntervals = testMapper.closeOpenReceptionIntervals(testId, actionAt, "MANUAL_END");
+        testMapper.deleteChannelLeasesByTestId(testId);
+        testMapper.deleteCollectorChannelLeasesByTestId(testId);
+        testMapper.insertTestAudit(testId, "TEST_STOPPED", operatorId, updateBy, null,
+                JSON.toJSONString(Map.of("testStatus", "COMPLETED", "receptionStatus", "ENDED",
+                        "polling", "STOPPED", "closedIntervals", closedIntervals, "release", "IMMEDIATE")));
+        return 1;
     }
 
     private boolean isOnline(String status)
@@ -495,6 +368,8 @@
         test.setBatteryNo(joinBatteryNos(batteries));
         List<TTestWarningThreshold> loadedThresholds = testMapper.selectTestThresholds(testId);
         test.setThresholds(loadedThresholds == null ? mutable(test.getThresholds()) : mutable(loadedThresholds));
+        List<com.ruoyi.system.domain.TTestReceptionInterval> intervals = testMapper.selectTestReceptionIntervals(testId);
+        test.setReceptionIntervals(intervals == null ? List.of() : intervals);
         List<TTestFieldSnapshot> loadedSnapshots = sharedFieldSnapshotService.selectByTestId(testId);
         test.setFieldSnapshots(loadedSnapshots == null ? mutable(test.getFieldSnapshots()) : loadedSnapshots);
     }
@@ -579,7 +454,7 @@
         for (TTestCollectorChannel mapping : mappings)
         {
             if (mapping == null) throw new ServiceException("采集器通道映射不完整");
-            mapping.setMeasurementCode(trimToNull(mapping.getMeasurementCode()));
+            mapping.setMeasurementCode(normalizeMeasurementCode(mapping.getMeasurementCode()));
             mapping.setMeasurementName(trimToNull(mapping.getMeasurementName()));
             mapping.setChannelCode(normalizeCollectorChannel(mapping.getChannelCode()));
             if (mapping.getMeasurementCode() == null || mapping.getMeasurementName() == null
@@ -738,6 +613,13 @@
         }
     }
 
+    /** 测量编码与数据库不区分大小写排序保持一致,避免校验通过后插入冲突。 */
+    private String normalizeMeasurementCode(String value)
+    {
+        String normalized = trimToNull(value);
+        return normalized == null ? null : normalized.toUpperCase(Locale.ROOT);
+    }
+
     private <T> List<T> mutable(List<T> values)
     {
         return values == null ? new ArrayList<>() : new ArrayList<>(values);

--
Gitblit v1.7.1