| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | ? 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) |
| | |
| | | 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); |
| | |
| | | } |
| | | |
| | | 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()); |
| | |
| | | "采集器=" + 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) |
| | | { |