From c2dd019c1fddaeae412e2b6f44fb86ff0dc635d4 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 02 九月 2026 22:08:16 +0800
Subject: [PATCH] 实现主动轮询完整实时快照
---
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java | 190 +++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 189 insertions(+), 1 deletions(-)
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java
index 381adfb..4ba1b52 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java
@@ -4,12 +4,18 @@
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.Date;
+import java.time.Instant;
+import java.util.Map;
+import java.util.LinkedHashMap;
import java.util.HexFormat;
import java.util.List;
+import com.alibaba.fastjson2.JSON;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
import com.ruoyi.device.simulator.BtsSimulatorStateStore.BtsHistoryRecord;
+import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement;
+import com.ruoyi.device.simulator.DevelopmentCollectorSimulation.CollectorRealtimeSnapshot;
import com.ruoyi.system.domain.TTest;
import com.ruoyi.system.domain.TTestBattery;
import com.ruoyi.system.domain.TTestChannelBinding;
@@ -24,12 +30,95 @@
private final TTestMapper mapper;
private final ITTestService tests;
private final BtsDevelopmentSimulator simulator;
+ private final Map<String, Instant> lastSnapshotAt = new java.util.concurrent.ConcurrentHashMap<>();
+ private final Map<Long, Instant> lastRawCaptureAt = new java.util.concurrent.ConcurrentHashMap<>();
public TestDataReceptionServiceImpl(TTestMapper mapper, ITTestService tests, BtsDevelopmentSimulator simulator)
{
this.mapper = mapper;
this.tests = tests;
this.simulator = simulator;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void captureDevelopmentRealtimeSnapshots()
+ {
+ List<Long> testIds = mapper.selectReceivingTestIds();
+ if (testIds == null) return;
+ Date now = new Date();
+ for (Long testId : testIds)
+ {
+ TTest locked = mapper.selectTestByIdForUpdate(testId);
+ if (locked == null || !"RECEIVING".equals(locked.getReceptionStatus())) continue;
+ TTest test = tests.selectTestById(testId);
+ if (test == null || !"RECEIVING".equals(test.getReceptionStatus())) continue;
+ // 完整快照与原始事实在同一接收事务内写入;原始事实按发现频率限流,避免 100ms 开发调度重复扫描历史。
+ Instant lastRaw = lastRawCaptureAt.get(testId);
+ if (lastRaw == null || !now.toInstant().isBefore(lastRaw.plusSeconds(5)))
+ {
+ captureTest(test);
+ for (TTestBattery battery : test.getBatteries() == null ? List.<TTestBattery>of() : test.getBatteries())
+ captureCollectorMeasurements(test, battery);
+ lastRawCaptureAt.put(testId, now.toInstant());
+ }
+ for (TTestBattery battery : test.getBatteries() == null ? List.<TTestBattery>of() : test.getBatteries())
+ captureSnapshot(test, battery, now);
+ }
+ }
+
+ private void captureSnapshot(TTest test, TTestBattery battery, Date now)
+ {
+ TTestChannelBinding binding = battery.getBinding();
+ if (binding == null || binding.getDevid() == null || binding.getChlid() == null || battery.getCollectorId() == null) return;
+ final long interval;
+ try { interval = simulator.persistenceIntervalMillis(binding.getDevid(), binding.getChlid()); }
+ catch (IllegalArgumentException ignored) { return; }
+ String key = test.getTestId() + ":" + binding.getBindingId();
+ Instant last = lastSnapshotAt.get(key);
+ if (last != null && now.toInstant().isBefore(last.plusMillis(interval))) return;
+ List<BtsHistoryRecord> records;
+ try { records = simulator.btsRecords(binding.getDevid(), binding.getChlid()); } catch (IllegalArgumentException ignored) { return; }
+ BtsHistoryRecord cabinet = records.stream().max(java.util.Comparator.comparing(BtsHistoryRecord::deviceTime)).orElse(null);
+ CollectorRealtimeSnapshot collector;
+ try { collector = simulator.collectorRealtime(battery.getCollectorId().intValue()); } catch (IllegalArgumentException ignored) { return; }
+ if (cabinet == null || !collector.online()) return;
+ if (test.getStartedAtUtc() != null && cabinet.deviceTime().isBefore(test.getStartedAtUtc().toInstant())) return;
+ if (test.getUpdateTime() != null && cabinet.deviceTime().isBefore(test.getUpdateTime().toInstant())) return;
+ Map<String, CollectorRealtimeMeasurement> metrics = new LinkedHashMap<>();
+ for (CollectorRealtimeMeasurement item : collector.measurements())
+ if (battery.getCollectorChannels() != null && battery.getCollectorChannels().stream().anyMatch(mapping ->
+ java.util.Objects.equals(mapping.getChannelCode(), item.channelCode())
+ && metricMatches(mapping.getMeasurementCode(), item.metricCode())))
+ metrics.put(item.metricCode().toUpperCase(java.util.Locale.ROOT), item);
+ CollectorRealtimeMeasurement temp = metrics.get("TEMPERATURE");
+ CollectorRealtimeMeasurement pos = metrics.get("POSITIVE_POTENTIAL");
+ CollectorRealtimeMeasurement neg = metrics.get("NEGATIVE_POTENTIAL");
+ if (temp == null || pos == null || neg == null) return;
+ if (test.getStartedAtUtc() != null && (temp.deviceTime().isBefore(test.getStartedAtUtc().toInstant())
+ || pos.deviceTime().isBefore(test.getStartedAtUtc().toInstant())
+ || neg.deviceTime().isBefore(test.getStartedAtUtc().toInstant()))) return;
+ if (test.getUpdateTime() != null && (temp.serverReceivedAt().isBefore(test.getUpdateTime().toInstant())
+ || pos.serverReceivedAt().isBefore(test.getUpdateTime().toInstant())
+ || neg.serverReceivedAt().isBefore(test.getUpdateTime().toInstant()))) return;
+ Date slot = new Date((now.getTime() / interval) * interval);
+ byte[] hash = snapshotHash(cabinet, temp, pos, neg, slot.toInstant());
+ mapper.insertRealtimeSnapshot(test.getTestId(), binding.getBindingId(), binding.getChannelId(), battery.getCollectorId(), slot,
+ cabinet.sequenceId(), Date.from(cabinet.deviceTime()), cabinet.deviceTime().toString(),
+ temp.sequenceId(), Date.from(temp.deviceTime()), temp.deviceTimeRaw(), now,
+ cabinet.cycleNo(), cabinet.stepNo(), cabinet.stepType(), cabinet.stepName(),
+ BigDecimal.valueOf(cabinet.voltageV()), BigDecimal.valueOf(cabinet.currentA()), BigDecimal.valueOf(cabinet.capacityAh()),
+ BigDecimal.valueOf(cabinet.energyWh()), temp.value(), pos.value(), neg.value(), JSON.toJSONString(metrics), hash);
+ lastSnapshotAt.put(key, now.toInstant());
+ }
+
+ private byte[] snapshotHash(BtsHistoryRecord cabinet, CollectorRealtimeMeasurement temp,
+ CollectorRealtimeMeasurement pos, CollectorRealtimeMeasurement neg, Instant slot)
+ {
+ try { return MessageDigest.getInstance("SHA-256").digest((slot + "|" + cabinet.sequenceId() + "|" + cabinet.deviceTime()
+ + "|" + cabinet.voltageV() + "|" + cabinet.currentA() + "|" + temp.value() + "|" + pos.value() + "|" + neg.value())
+ .getBytes(StandardCharsets.UTF_8)); }
+ catch (Exception exception) { throw new IllegalStateException("无法计算实时快照内容摘要", exception); }
}
@Override
@@ -49,8 +138,26 @@
}
}
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void captureDevelopmentCollectorMeasurements()
+ {
+ List<Long> testIds = mapper.selectReceivingTestIds();
+ if (testIds == null) return;
+ for (Long testId : testIds)
+ {
+ TTest locked = mapper.selectTestByIdForUpdate(testId);
+ if (locked == null || !"RECEIVING".equals(locked.getReceptionStatus())) continue;
+ TTest test = tests.selectTestById(testId);
+ if (test == null || !"RECEIVING".equals(test.getReceptionStatus())) continue;
+ for (TTestBattery battery : test.getBatteries() == null ? List.<TTestBattery>of() : test.getBatteries())
+ captureCollectorMeasurements(test, battery);
+ }
+ }
+
private void captureTest(TTest test)
{
+ Date startedAt = test.getStartedAtUtc();
for (TTestBattery battery : test.getBatteries() == null ? List.<TTestBattery>of() : test.getBatteries())
{
TTestChannelBinding binding = battery.getBinding();
@@ -60,8 +167,66 @@
catch (IllegalArgumentException ignored) { continue; }
for (BtsHistoryRecord record : records)
{
+ // 模拟器保留通道历史;只接收本次测试开始后产生的设备时间记录,避免顺序测试串入旧事实。
+ if (startedAt != null && record.deviceTime().isBefore(startedAt.toInstant())) continue;
save(test, binding, record);
}
+ }
+ }
+
+ private void captureCollectorMeasurements(TTest test, TTestBattery battery)
+ {
+ if (battery.getCollectorId() == null || battery.getCollectorChannels() == null
+ || battery.getCollectorChannels().isEmpty()) return;
+ final CollectorRealtimeSnapshot realtime;
+ try { realtime = simulator.collectorRealtime(battery.getCollectorId().intValue()); }
+ catch (IllegalArgumentException ignored) { return; }
+ if (!realtime.online()) return;
+ Date startedAt = test.getStartedAtUtc();
+ for (CollectorRealtimeMeasurement measurement : realtime.measurements())
+ {
+ if (startedAt != null && measurement.deviceTime().isBefore(startedAt.toInstant())) continue;
+ // 继续接收只接受恢复动作之后查询到的快照;暂停期间采集器自行产生的历史不回补。
+ if (test.getUpdateTime() != null && measurement.serverReceivedAt().isBefore(test.getUpdateTime().toInstant())) continue;
+ boolean mapped = battery.getCollectorChannels().stream().anyMatch(mapping ->
+ java.util.Objects.equals(mapping.getChannelCode(), measurement.channelCode())
+ && metricMatches(mapping.getMeasurementCode(), measurement.metricCode()));
+ if (!mapped) continue;
+ saveCollector(test, battery, measurement);
+ }
+ }
+
+ private void saveCollector(TTest test, TTestBattery battery, CollectorRealtimeMeasurement measurement)
+ {
+ byte[] hash = hash(measurement);
+ String old = mapper.selectCollectorSampleContentHash(battery.getCollectorId(), measurement.channelCode(),
+ measurement.sequenceId(), measurement.metricCode());
+ String expected = HexFormat.of().formatHex(hash);
+ if (old != null)
+ {
+ if (!old.equalsIgnoreCase(expected))
+ mapper.insertSampleConflict(test.getTestId(), battery.getBinding().getChannelId(),
+ "采集器实时接收内容冲突 collector=" + battery.getCollectorId() + ", channel="
+ + measurement.channelCode() + ", metric=" + measurement.metricCode()
+ + ", seqid=" + measurement.sequenceId());
+ return;
+ }
+ Date receivedAt = new Date();
+ int inserted = mapper.insertCollectorSample(test.getTestId(), battery.getBinding().getBindingId(), battery.getCollectorId(),
+ measurement.adapterCode(),
+ measurement.channelCode(), measurement.sequenceId(), Date.from(measurement.deviceTime()),
+ measurement.deviceTimeRaw(), receivedAt, measurement.metricCode(),
+ measurement.value(), measurement.unit(), hash);
+ // 唯一键负责并发幂等;内容冲突保留首次事实,不覆盖原始记录。
+ if (inserted == 0)
+ {
+ String actual = mapper.selectCollectorSampleContentHash(battery.getCollectorId(), measurement.channelCode(),
+ measurement.sequenceId(), measurement.metricCode());
+ if (actual != null && !actual.equalsIgnoreCase(expected))
+ mapper.insertSampleConflict(test.getTestId(), battery.getBinding().getChannelId(),
+ "采集器实时并发内容冲突 collector=" + battery.getCollectorId() + ", channel="
+ + measurement.channelCode() + ", metric=" + measurement.metricCode()
+ + ", seqid=" + measurement.sequenceId());
}
}
@@ -79,7 +244,7 @@
"实时接收内容冲突 testid=" + btsTestId + ", seqid=" + record.sequenceId());
return;
}
- int inserted = mapper.insertBtsSample(test.getTestId(), binding.getBindingId(), binding.getChannelId(), null,
+ int inserted = mapper.insertBtsSample(test.getTestId(), binding.getBindingId(), binding.getChannelId(),
btsTestId, record.sequenceId(), Date.from(record.deviceTime()),
record.deviceTime().toString(), new Date(), record.cycleNo(), record.stepNo(), record.stepType(),
record.stepName(), null,
@@ -105,4 +270,27 @@
}
catch (Exception exception) { throw new IllegalStateException("无法计算采样内容摘要", exception); }
}
+
+ private byte[] hash(CollectorRealtimeMeasurement measurement)
+ {
+ try
+ {
+ String value = measurement.adapterCode() + "|" + measurement.collectorCode() + "|"
+ + measurement.channelCode() + "|" + measurement.sequenceId() + "|"
+ + measurement.deviceTimeRaw() + "|" + measurement.metricCode() + "|"
+ + measurement.value() + "|" + measurement.unit();
+ return MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
+ }
+ catch (Exception exception) { throw new IllegalStateException("无法计算采集器采样内容摘要", exception); }
+ }
+
+ private boolean metricMatches(String configured, String actual)
+ {
+ if (configured == null || actual == null) return false;
+ if (configured.equals(actual)) return true;
+ String normalized = configured.toUpperCase(java.util.Locale.ROOT);
+ return (normalized.contains("TEMPERATURE") && "TEMPERATURE".equals(actual.toUpperCase(java.util.Locale.ROOT)))
+ || (normalized.contains("POSITIVE_POTENTIAL") && "POSITIVE_POTENTIAL".equals(actual.toUpperCase(java.util.Locale.ROOT)))
+ || (normalized.contains("NEGATIVE_POTENTIAL") && "NEGATIVE_POTENTIAL".equals(actual.toUpperCase(java.util.Locale.ROOT)));
+ }
}
--
Gitblit v1.7.1