From c2dd019c1fddaeae412e2b6f44fb86ff0dc635d4 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 02 九月 2026 22:08:16 +0800
Subject: [PATCH] 实现主动轮询完整实时快照
---
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java | 9 +
ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorSamplingScheduler.java | 2
ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentRealtimeSnapshotScheduler.java | 19 +++
ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentSimulatorDiscoveryScheduler.java | 12 -
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataReceptionService.java | 3
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml | 41 ++++++++
ruoyi-admin/src/main/resources/db/migration/V31__unified_realtime_snapshots.sql | 36 +++++++
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java | 14 ++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java | 15 ++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java | 11 +
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java | 87 +++++++++++++++++
11 files changed, 234 insertions(+), 15 deletions(-)
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorSamplingScheduler.java b/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorSamplingScheduler.java
index 56f3215..ded4bad 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorSamplingScheduler.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorSamplingScheduler.java
@@ -3,10 +3,12 @@
import java.time.Instant;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
+import org.springframework.context.annotation.Profile;
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
/** 按各采集器配置频率推进开发模拟测量,不参与正式测试数据持久化。 */
@Component
+@Profile("dev-simulator")
public class DevelopmentCollectorSamplingScheduler
{
private final BtsDevelopmentSimulator simulator;
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentRealtimeSnapshotScheduler.java b/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentRealtimeSnapshotScheduler.java
new file mode 100644
index 0000000..305ba48
--- /dev/null
+++ b/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentRealtimeSnapshotScheduler.java
@@ -0,0 +1,19 @@
+package com.ruoyi.config;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.context.annotation.Profile;
+import com.ruoyi.system.service.ITestDataReceptionService;
+
+/** 开发环境按模拟器槽位主动形成完整实时采集快照;生产轮询周期由真实设备调度器决定。 */
+@Component
+@Profile("dev-simulator")
+public class DevelopmentRealtimeSnapshotScheduler
+{
+ private final ITestDataReceptionService reception;
+
+ public DevelopmentRealtimeSnapshotScheduler(ITestDataReceptionService reception) { this.reception = reception; }
+
+ @Scheduled(fixedDelay = 100)
+ public void capture() { reception.captureDevelopmentRealtimeSnapshots(); }
+}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentSimulatorDiscoveryScheduler.java b/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentSimulatorDiscoveryScheduler.java
index ad7c8ad..d0ab590 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentSimulatorDiscoveryScheduler.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentSimulatorDiscoveryScheduler.java
@@ -2,26 +2,24 @@
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
+import org.springframework.context.annotation.Profile;
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
import com.ruoyi.device.registry.DeviceAdapterRegistry;
import com.ruoyi.system.service.impl.DeviceDiscoveryPersistenceService;
import com.ruoyi.system.service.impl.DeviceDiscoveryPersistenceService.DiscoverySource;
-import com.ruoyi.system.service.ITestDataReceptionService;
/** 轮询代码固定装配的 dev-sim;该运行时不依赖数据库适配器记录。 */
@Component
+@Profile("dev-simulator")
public class DevelopmentSimulatorDiscoveryScheduler
{
private final DeviceAdapterRegistry registry;
private final DeviceDiscoveryPersistenceService persistence;
- private final ITestDataReceptionService reception;
-
public DevelopmentSimulatorDiscoveryScheduler(DeviceAdapterRegistry registry,
- DeviceDiscoveryPersistenceService persistence, ITestDataReceptionService reception)
+ DeviceDiscoveryPersistenceService persistence)
{
this.registry = registry;
this.persistence = persistence;
- this.reception = reception;
}
/** 注册表已包含固定开发适配器时,将一轮检测柜观测写入 Redis 候选。 */
@@ -31,9 +29,5 @@
if (!registry.contains(BtsDevelopmentSimulator.ADAPTER_CODE)) return;
persistence.discoverForReception(registry, new DiscoverySource(BtsDevelopmentSimulator.ADAPTER_CODE, null,
BtsDevelopmentSimulator.ADAPTER_NAME, BtsDevelopmentSimulator.POLL_INTERVAL_MS));
- // 发现轮询成功后立即把接收窗口内的模拟 record 写入测试事实;页面不再读取模拟器历史。
- reception.captureDevelopmentCabinetRecords();
- // 采集器拥有独立采样频率;只接收本轮实时快照,不读取历史下载接口。
- reception.captureDevelopmentCollectorMeasurements();
}
}
diff --git a/ruoyi-admin/src/main/resources/db/migration/V31__unified_realtime_snapshots.sql b/ruoyi-admin/src/main/resources/db/migration/V31__unified_realtime_snapshots.sql
new file mode 100644
index 0000000..0e5f683
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration/V31__unified_realtime_snapshots.sql
@@ -0,0 +1,36 @@
+-- 一次主动轮询形成的完整实时采集快照;原始 record/collector_sample 继续保留用于追溯。
+CREATE TABLE t_test_realtime_snapshot (
+ snapshot_id BIGINT NOT NULL AUTO_INCREMENT,
+ test_id BIGINT NOT NULL,
+ binding_id BIGINT NOT NULL,
+ channel_id BIGINT NOT NULL,
+ collector_id BIGINT NOT NULL,
+ capture_slot_utc DATETIME(3) NOT NULL,
+ cabinet_sequence_id BIGINT NULL,
+ cabinet_device_time_utc DATETIME(3) NULL,
+ cabinet_device_time_raw VARCHAR(64) NULL,
+ collector_sequence_id BIGINT NULL,
+ collector_device_time_utc DATETIME(3) NULL,
+ collector_device_time_raw VARCHAR(64) NULL,
+ server_received_at_utc DATETIME(3) NOT NULL,
+ cycle_no INT NULL,
+ step_no INT NULL,
+ step_type VARCHAR(64) NULL,
+ step_name VARCHAR(191) NULL,
+ voltage_v DECIMAL(30,12) NULL,
+ current_a DECIMAL(30,12) NULL,
+ capacity_ah DECIMAL(30,12) NULL,
+ energy_wh DECIMAL(30,12) NULL,
+ temperature DECIMAL(30,12) NULL,
+ positive_potential DECIMAL(30,12) NULL,
+ negative_potential DECIMAL(30,12) NULL,
+ extension_measurements_json JSON NULL,
+ content_sha256 BINARY(32) NOT NULL,
+ PRIMARY KEY (snapshot_id),
+ UNIQUE KEY uk_test_snapshot_slot (test_id, binding_id, capture_slot_utc),
+ KEY idx_snapshot_test_time (test_id, capture_slot_utc),
+ CONSTRAINT fk_snapshot_test FOREIGN KEY (test_id) REFERENCES t_test (test_id),
+ CONSTRAINT fk_snapshot_binding FOREIGN KEY (binding_id) REFERENCES t_test_channel_binding (binding_id),
+ CONSTRAINT fk_snapshot_channel FOREIGN KEY (channel_id) REFERENCES t_detection_channel (channel_id),
+ CONSTRAINT fk_snapshot_collector FOREIGN KEY (collector_id) REFERENCES t_collector (collector_id)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='主动轮询形成的完整实时采集快照';
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
index cf28b98..281a531 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsDevelopmentSimulator.java
@@ -413,6 +413,15 @@
return List.copyOf(btsHistoryFor(btsKey(device, channelId)));
}
+ /** 返回开发模拟器当前通道的持久化槽位间隔;仅供开发调度层使用。 */
+ public synchronized long persistenceIntervalMillis(int deviceId, int channelId)
+ {
+ DeviceState device = requireDevice(deviceId);
+ requireChannel(device, channelId);
+ BtsPlaybackState playback = playbackState(btsKey(device, channelId), Instant.now());
+ return Math.max(100L, 60000L / Math.max(1, playback.speedMultiplier()));
+ }
+
/** 按实际出现的循环和工步汇总 record,不补齐缺失行。 */
public synchronized List<BtsStepRecord> btsSteps(int deviceId, int channelId)
{
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 ff75420..6374fd2 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
@@ -53,6 +53,20 @@
List<TTestWarningThreshold> selectTestThresholds(Long testId);
/** 查询当前正在接收的测试,供接收调度绑定测试事实。 */
List<Long> selectReceivingTestIds();
+ int insertRealtimeSnapshot(@Param("testId") Long testId, @Param("bindingId") Long bindingId,
+ @Param("channelId") Long channelId, @Param("collectorId") Long collectorId,
+ @Param("captureSlotUtc") Date captureSlotUtc, @Param("cabinetSequenceId") Long cabinetSequenceId,
+ @Param("cabinetDeviceTimeUtc") Date cabinetDeviceTimeUtc, @Param("cabinetDeviceTimeRaw") String cabinetDeviceTimeRaw,
+ @Param("collectorSequenceId") Long collectorSequenceId, @Param("collectorDeviceTimeUtc") Date collectorDeviceTimeUtc,
+ @Param("collectorDeviceTimeRaw") String collectorDeviceTimeRaw, @Param("serverReceivedAtUtc") Date serverReceivedAtUtc,
+ @Param("cycleNo") Integer cycleNo, @Param("stepNo") Integer stepNo, @Param("stepType") String stepType,
+ @Param("stepName") String stepName, @Param("voltageV") java.math.BigDecimal voltageV,
+ @Param("currentA") java.math.BigDecimal currentA, @Param("capacityAh") java.math.BigDecimal capacityAh,
+ @Param("energyWh") java.math.BigDecimal energyWh, @Param("temperature") java.math.BigDecimal temperature,
+ @Param("positivePotential") java.math.BigDecimal positivePotential,
+ @Param("negativePotential") java.math.BigDecimal negativePotential,
+ @Param("extensionMeasurementsJson") String extensionMeasurementsJson, @Param("contentSha256") byte[] contentSha256);
+ List<java.util.Map<String, Object>> selectPersistedRealtimeSnapshots(@Param("testId") Long testId);
List<java.util.Map<String, Object>> selectPersistedRecords(@org.apache.ibatis.annotations.Param("testId") Long testId);
List<java.util.Map<String, Object>> selectPersistedCollectorRecords(@org.apache.ibatis.annotations.Param("testId") Long testId);
List<java.util.Map<String, Object>> selectPersistedSteps(@org.apache.ibatis.annotations.Param("testId") Long testId);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataReceptionService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataReceptionService.java
index f88f924..350cce4 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataReceptionService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataReceptionService.java
@@ -3,6 +3,9 @@
/** 将设备已接收测量追加到当前测试事实。 */
public interface ITestDataReceptionService
{
+ /** 按开发模拟器持久化槽位主动查询并保存完整实时采集快照。 */
+ void captureDevelopmentRealtimeSnapshots();
+
/** 扫描当前接收中的开发检测柜,并把接收窗口内的新记录持久化。 */
void captureDevelopmentCabinetRecords();
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java
index 4d8a52c..78ee125 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestConsoleDataServiceImpl.java
@@ -134,7 +134,8 @@
List<Map<String, Object>> changes = (List<Map<String, Object>>) (List<?>) JSON.parseArray(action.getChangesSummary(), Map.class);
Set<String> editable = switch (action.getDataType())
{
- case "REALTIME" -> Set.of("voltage", "current", "capacity", "energy", "metricValue");
+ case "REALTIME" -> Set.of("voltage", "current", "capacity", "energy", "metricValue",
+ "temperature", "positivePotential", "negativePotential");
case "STEP" -> Set.of("stepDuration", "capacity", "energy", "startVoltage", "endVoltage",
"chargePeakVoltage", "dischargeValleyVoltage");
case "CYCLE" -> Set.of("chargeCapacity", "dischargeCapacity", "efficiency", "chargeEnergy",
@@ -215,7 +216,8 @@
{
return switch (dataType)
{
- case "REALTIME" -> Set.of("voltage", "current", "capacity", "energy", "metricValue");
+ case "REALTIME" -> Set.of("voltage", "current", "capacity", "energy", "metricValue",
+ "temperature", "positivePotential", "negativePotential");
case "STEP" -> Set.of("stepDuration", "capacity", "energy", "startVoltage", "endVoltage",
"chargePeakVoltage", "dischargeValleyVoltage");
case "CYCLE" -> Set.of("chargeCapacity", "dischargeCapacity", "efficiency", "chargeEnergy",
@@ -233,6 +235,11 @@
try { return new Long[] { Long.valueOf(parts[1]), Long.valueOf(parts[3]) }; }
catch (NumberFormatException exception) { return null; }
}
+ if ("REALTIME".equals(dataType) && parts.length == 2 && "snapshot".equals(parts[0]))
+ {
+ try { return new Long[] { Long.valueOf(parts[1]) }; }
+ catch (NumberFormatException exception) { return null; }
+ }
int expectedLength = "STEP".equals(dataType) ? 8 : "CYCLE".equals(dataType) ? 7 : 6;
int legacyLength = "STEP".equals(dataType) ? 5 : "CYCLE".equals(dataType) ? 4 : 3;
if (parts.length != expectedLength && parts.length != legacyLength) return null;
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java
index 910777b..06ee3c8 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java
@@ -37,10 +37,15 @@
{
requireTest(testId);
List<Map<String, Object>> source = new java.util.ArrayList<>();
- List<Map<String, Object>> cabinet = mapper.selectPersistedRecords(testId);
- List<Map<String, Object>> collector = mapper.selectPersistedCollectorRecords(testId);
- if (cabinet != null) source.addAll(cabinet);
- if (collector != null) source.addAll(collector);
+ List<Map<String, Object>> snapshots = mapper.selectPersistedRealtimeSnapshots(testId);
+ if (snapshots != null && !snapshots.isEmpty()) source.addAll(snapshots);
+ else {
+ // 迁移完成前兼容旧数据;新采集只从统一快照读取。
+ List<Map<String, Object>> cabinet = mapper.selectPersistedRecords(testId);
+ List<Map<String, Object>> collector = mapper.selectPersistedCollectorRecords(testId);
+ if (cabinet != null) source.addAll(cabinet);
+ if (collector != null) source.addAll(collector);
+ }
List<Map<String, Object>> effective = applyEffectiveData(testId, "REALTIME", source);
attachCollectorStepContext(effective);
return effective;
@@ -111,6 +116,8 @@
private String stableIdentity(String dataType, Map<String, Object> row)
{
+ if ("REALTIME".equals(dataType) && "SNAPSHOT".equals(String.valueOf(row.get("sourceType"))))
+ return String.valueOf(row.getOrDefault("stableIdentity", "snapshot:" + value(row, "snapshotId")));
if ("REALTIME".equals(dataType) && "COLLECTOR".equals(String.valueOf(row.get("sourceType"))))
return String.join(":", "collector", identityPart(row, "collectorId"),
identityPart(row, "channelCode"), value(row, "sequenceId"), identityPart(row, "metricCode"));
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 22f195f..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,8 +4,12 @@
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;
@@ -26,6 +30,8 @@
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)
{
@@ -36,6 +42,87 @@
@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
+ @Transactional(rollbackFor = Exception.class)
public void captureDevelopmentCabinetRecords()
{
List<Long> testIds = mapper.selectReceivingTestIds();
diff --git a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
index 56b7312..ebd26ac 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -318,6 +318,47 @@
<select id="selectReceivingTestIds" resultType="long">
select test_id from t_test where del_flag = 0 and reception_status = 'RECEIVING' order by test_id
</select>
+ <insert id="insertRealtimeSnapshot">
+ insert ignore into t_test_realtime_snapshot
+ (test_id, binding_id, channel_id, collector_id, capture_slot_utc,
+ cabinet_sequence_id, cabinet_device_time_utc, cabinet_device_time_raw,
+ collector_sequence_id, collector_device_time_utc, collector_device_time_raw,
+ server_received_at_utc, cycle_no, step_no, step_type, step_name,
+ voltage_v, current_a, capacity_ah, energy_wh, temperature, positive_potential,
+ negative_potential, extension_measurements_json, content_sha256)
+ values (#{testId}, #{bindingId}, #{channelId}, #{collectorId}, #{captureSlotUtc},
+ #{cabinetSequenceId}, #{cabinetDeviceTimeUtc}, #{cabinetDeviceTimeRaw},
+ #{collectorSequenceId}, #{collectorDeviceTimeUtc}, #{collectorDeviceTimeRaw},
+ #{serverReceivedAtUtc}, #{cycleNo}, #{stepNo}, #{stepType}, #{stepName},
+ #{voltageV}, #{currentA}, #{capacityAh}, #{energyWh}, #{temperature}, #{positivePotential},
+ #{negativePotential}, #{extensionMeasurementsJson}, #{contentSha256})
+ </insert>
+ <select id="selectPersistedRealtimeSnapshots" resultType="map">
+ select concat('snapshot:', snapshot.snapshot_id) id,
+ concat('snapshot:', snapshot.snapshot_id) stableIdentity,
+ concat('RS-', snapshot.snapshot_id) recordNo,
+ 'SNAPSHOT' sourceType, battery.battery_no batteryNo,
+ snapshot.cycle_no cycleNo, snapshot.step_no stepNo, snapshot.step_name stepName,
+ snapshot.cabinet_sequence_id sequenceId,
+ snapshot.cabinet_device_time_utc recordedAt, snapshot.cabinet_device_time_utc absoluteTime,
+ snapshot.cabinet_device_time_utc deviceTime, snapshot.server_received_at_utc serverReceivedAt,
+ snapshot.snapshot_id snapshotId,
+ snapshot.temperature temperature, snapshot.positive_potential positivePotential,
+ snapshot.negative_potential negativePotential,
+ snapshot.voltage_v voltage, snapshot.current_a current,
+ snapshot.capacity_ah capacity, snapshot.energy_wh energy,
+ channel.adapter_id adapterId, channel.adapter_code adapterCode,
+ channel.devtype devtype, channel.devid deviceId, channel.subdevid subdevid, channel.chlid channelId,
+ collector.collector_id collectorId, collector.collector_code collectorCode,
+ snapshot.extension_measurements_json extensionMeasurementsJson
+ from t_test_realtime_snapshot snapshot
+ join t_test_channel_binding binding on binding.binding_id = snapshot.binding_id
+ join t_test_battery battery on battery.relation_id = binding.battery_relation_id and battery.del_flag = 0
+ join t_detection_channel channel on channel.channel_id = snapshot.channel_id and channel.del_flag = 0
+ join t_collector collector on collector.collector_id = snapshot.collector_id
+ where snapshot.test_id = #{testId}
+ order by snapshot.capture_slot_utc desc, snapshot.snapshot_id desc
+ </select>
<select id="selectPersistedRecords" resultType="map">
select sample.sample_id id,
concat('RD-', channel.devid, '-', channel.chlid, '-', lpad(sample.seq_id, 6, '0')) recordNo,
--
Gitblit v1.7.1