From 9bd8e752b5d7ba7177fa16d74565b741699f5c22 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期一, 31 八月 2026 00:43:06 +0800
Subject: [PATCH] 实现 Ticket14 区间完整性检查与检测完成

---
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestBackfillServiceImpl.java |  121 +++++++++++++++++++++++++++++++++++++++-
 1 files changed, 117 insertions(+), 4 deletions(-)

diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestBackfillServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestBackfillServiceImpl.java
index e0a5e17..b886001 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestBackfillServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestBackfillServiceImpl.java
@@ -27,6 +27,7 @@
 import com.ruoyi.system.domain.TTestChannelBinding;
 import com.ruoyi.system.domain.TTestCollectorChannel;
 import com.ruoyi.system.domain.TTestReceptionInterval;
+import com.ruoyi.system.domain.TTestIntegrityIssue;
 import com.ruoyi.system.mapper.TTestMapper;
 import com.ruoyi.system.service.ITestBackfillService;
 import com.ruoyi.system.service.ITTestService;
@@ -36,6 +37,7 @@
 public class TestBackfillServiceImpl implements ITestBackfillService
 {
     private static final int PAGE_SIZE = 1000;
+    private static final int MAX_INTEGRITY_RETRIES = 3;
     private final TTestMapper mapper;
     private final DeviceAdapterRegistry registry;
     private final BtsSimulatorStateStore collectorHistoryStore;
@@ -119,7 +121,20 @@
                     ? List.<TTestCollectorChannel>of() : battery.getCollectorChannels())
                 backfillCollector(test, battery, mapping);
         }
-        mapper.updateTestBackfillCompleted(test.getTestId(), new Date());
+        Date checkedAt = new Date();
+        boolean complete = checkIntegrity(test, checkedAt);
+        if (complete)
+        {
+            mapper.updateTestCompleted(test.getTestId(), checkedAt);
+            mapper.insertTestAudit(test.getTestId(), "TEST_COMPLETED", null, "system", null,
+                    JSON.toJSONString(java.util.Map.of("backfillStatus", "COMPLETED", "integrity", "PASSED")));
+        }
+        else
+        {
+            mapper.updateTestBackfillWaiting(test.getTestId(), checkedAt, "区间完整性检查未通过");
+            mapper.insertTestAudit(test.getTestId(), "INTEGRITY_CHECK_BLOCKED", null, "system", null,
+                    JSON.toJSONString(java.util.Map.of("backfillStatus", "WAITING", "integrity", "OPEN")));
+        }
     }
 
     private void backfillCabinet(TTest test, TTestBattery battery, TTestChannelBinding binding)
@@ -206,7 +221,7 @@
             for (CollectorMeasurementRecord record : page.records())
             {
                 if (!mapping.getMeasurementCode().equalsIgnoreCase(record.metricCode()) || !within(record.deviceTime(), intervals)) continue;
-                saveCollectorSample(test, battery, mapping, record);
+                saveCollectorSample(test, battery, mapping, intervals, record);
             }
             offset = page.nextOffset();
             cursor.setNextPosition(offset + 1L);
@@ -218,7 +233,7 @@
     }
 
     private void saveCollectorSample(TTest test, TTestBattery battery, TTestCollectorChannel mapping,
-            CollectorMeasurementRecord record)
+            List<TTestReceptionInterval> intervals, CollectorMeasurementRecord record)
     {
         byte[] hash = hash(record.sequenceId() + "|" + record.deviceTimeRaw() + "|" + record.metricCode() + "|"
                 + record.value() + "|" + record.unit());
@@ -230,11 +245,109 @@
                     "采集器=" + record.collectorCode() + ", seqid=" + record.sequenceId());
             return;
         }
-        mapper.insertCollectorSample(test.getTestId(), battery.getBinding().getBindingId(), battery.getCollectorId(),
+        mapper.insertCollectorSample(test.getTestId(), battery.getBinding().getBindingId(),
+                intervalId(intervals, record.deviceTime()), battery.getCollectorId(),
                 record.channelCode(), record.sequenceId(), Date.from(record.deviceTime()), record.deviceTimeRaw(),
                 Date.from(record.serverReceivedAt()), record.metricCode(), record.value(), record.unit(), hash);
     }
 
+    private boolean checkIntegrity(TTest test, Date checkedAt)
+    {
+        boolean complete = true;
+        for (TTestReceptionInterval interval : mapper.selectTestReceptionIntervals(test.getTestId()))
+        {
+            String sourceType = interval.getSourceType();
+            List<Long> sequenceIds = "CABINET_CHANNEL".equals(sourceType)
+                    ? mapper.selectBtsSequencesByInterval(interval.getIntervalId())
+                    : mapper.selectCollectorSequencesByInterval(interval.getIntervalId());
+            List<IntervalIntegrityChecker.Gap> gaps = IntervalIntegrityChecker.gaps(sequenceIds);
+            if (sequenceIds == null || sequenceIds.isEmpty())
+            {
+                complete = false;
+                TTestIntegrityIssue old = mapper.selectOpenIntegrityIssue(interval.getIntervalId(), sourceType,
+                        interval.getSourceId(), interval.getSourceChannelCode(), null, null);
+                if (old == null)
+                {
+                    TTestIntegrityIssue issue = new TTestIntegrityIssue();
+                    issue.setTestId(test.getTestId()); issue.setIntervalId(interval.getIntervalId());
+                    issue.setBindingId(interval.getBindingId()); issue.setSourceType(sourceType);
+                    issue.setSourceId(interval.getSourceId()); issue.setSourceChannelCode(interval.getSourceChannelCode());
+                    issue.setIssueType("NO_SAMPLES"); issue.setRetryCount(1);
+                    issue.setDetail("接收区间没有可确认的历史采样"); issue.setFirstDetectedAtUtc(checkedAt);
+                    issue.setLastDetectedAtUtc(checkedAt); mapper.insertIntegrityIssue(issue);
+                    mapper.insertTestAudit(test.getTestId(), "INTEGRITY_GAP_DETECTED", null, "system", null,
+                            JSON.toJSONString(java.util.Map.of("intervalId", interval.getIntervalId(), "sourceType", sourceType,
+                                    "issueType", "NO_SAMPLES")));
+                }
+                else
+                {
+                    int retry = old.getRetryCount() == null ? 1 : old.getRetryCount() + 1;
+                    int cappedRetry = Math.min(retry, MAX_INTEGRITY_RETRIES);
+                    mapper.touchIntegrityIssue(old.getIssueId(), cappedRetry,
+                            old.getDetail(), checkedAt);
+                    mapper.insertTestAudit(test.getTestId(), "INTEGRITY_GAP_RETRIED", null, "system", null,
+                            JSON.toJSONString(java.util.Map.of("issueId", old.getIssueId(), "retryCount", cappedRetry)));
+                    if (cappedRetry < MAX_INTEGRITY_RETRIES)
+                        mapper.rewindBackfillCursor(test.getTestId(), interval.getBindingId(), sourceType,
+                                interval.getSourceId(), interval.getSourceChannelCode(), 1L);
+                }
+                continue;
+            }
+            if (gaps.isEmpty())
+            {
+                int resolved = mapper.resolveIntegrityIssues(test.getTestId(), interval.getIntervalId(), sourceType,
+                        interval.getSourceId(), interval.getSourceChannelCode(), checkedAt);
+                if (resolved > 0) mapper.insertTestAudit(test.getTestId(), "INTEGRITY_GAP_RESOLVED", null, "system", null,
+                        JSON.toJSONString(java.util.Map.of("intervalId", interval.getIntervalId())));
+                continue;
+            }
+            complete = false;
+            for (TTestIntegrityIssue open : mapper.selectOpenIntegrityIssues(interval.getIntervalId(), sourceType,
+                    interval.getSourceId(), interval.getSourceChannelCode()))
+            {
+                boolean stillOpen = gaps.stream().anyMatch(gap -> Objects.equals(gap.from(), open.getMissingFromSequence())
+                        && Objects.equals(gap.to(), open.getMissingToSequence()));
+                if (!stillOpen && mapper.resolveIntegrityIssue(open.getIssueId(), checkedAt) > 0)
+                    mapper.insertTestAudit(test.getTestId(), "INTEGRITY_GAP_RESOLVED", null, "system", null,
+                            JSON.toJSONString(java.util.Map.of("issueId", open.getIssueId())));
+            }
+            for (IntervalIntegrityChecker.Gap gap : gaps)
+            {
+                TTestIntegrityIssue old = mapper.selectOpenIntegrityIssue(interval.getIntervalId(), sourceType,
+                        interval.getSourceId(), interval.getSourceChannelCode(), gap.from(), gap.to());
+                if (old == null)
+                {
+                    TTestIntegrityIssue issue = new TTestIntegrityIssue();
+                    issue.setTestId(test.getTestId()); issue.setIntervalId(interval.getIntervalId());
+                    issue.setBindingId(interval.getBindingId()); issue.setSourceType(sourceType);
+                    issue.setSourceId(interval.getSourceId()); issue.setSourceChannelCode(interval.getSourceChannelCode());
+                    issue.setIssueType("SEQUENCE_GAP"); issue.setMissingFromSequence(gap.from());
+                    issue.setMissingToSequence(gap.to()); issue.setRetryCount(1);
+                    issue.setDetail("接收区间内存在未解释序号缺口"); issue.setFirstDetectedAtUtc(checkedAt);
+                    issue.setLastDetectedAtUtc(checkedAt); mapper.insertIntegrityIssue(issue);
+                    mapper.insertTestAudit(test.getTestId(), "INTEGRITY_GAP_DETECTED", null, "system", null,
+                            JSON.toJSONString(java.util.Map.of("intervalId", interval.getIntervalId(), "sourceType", sourceType,
+                                    "missingFrom", gap.from(), "missingTo", gap.to())));
+                }
+                else
+                {
+                    int retry = old.getRetryCount() == null ? 1 : old.getRetryCount() + 1;
+                    int cappedRetry = Math.min(retry, MAX_INTEGRITY_RETRIES);
+                    mapper.touchIntegrityIssue(old.getIssueId(), cappedRetry,
+                            old.getDetail(), checkedAt);
+                    mapper.insertTestAudit(test.getTestId(), "INTEGRITY_GAP_RETRIED", null, "system", null,
+                            JSON.toJSONString(java.util.Map.of("issueId", old.getIssueId(), "retryCount", cappedRetry)));
+                    if (cappedRetry >= MAX_INTEGRITY_RETRIES) continue;
+                }
+                // next_position 是分页偏移而非 seqid;从安全起点重放,依靠幂等键避免覆盖原始事实。
+                mapper.rewindBackfillCursor(test.getTestId(), interval.getBindingId(), sourceType,
+                        interval.getSourceId(), interval.getSourceChannelCode(), 1L);
+            }
+        }
+        return complete && mapper.countOpenIntegrityIssues(test.getTestId()) == 0
+                && mapper.countOpenExceptions(test.getTestId()) == 0;
+    }
+
     private TTestBackfillCursor cursor(TTest test, Long bindingId, String sourceType, Long sourceId,
             String channelCode, String protocolTestId)
     {

--
Gitblit v1.7.1