From 0fcd0642efdff8d4e0584fa991aaea2178bc932b Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期二, 01 九月 2026 22:48:35 +0800
Subject: [PATCH] 测试控制台持续更新
---
ruoyi-admin/src/main/resources/db/migration/V27__persist_bts_step_name.sql | 2
ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java | 28 ++++
ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentSimulatorDiscoveryScheduler.java | 7
ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java | 9 +
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataQueryService.java | 12 +
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestBackfillServiceImpl.java | 24 +++
ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataReceptionService.java | 8 +
ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml | 70 +++++++++
ruoyi-admin/src/test/java/com/ruoyi/config/TestPersistedDataQueryContractTest.java | 56 ++++++++
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TTestMapper.java | 10 +
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java | 47 ++++++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java | 110 +++++++++++++++
12 files changed, 378 insertions(+), 5 deletions(-)
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 2c067bf..de5cf4b 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentSimulatorDiscoveryScheduler.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentSimulatorDiscoveryScheduler.java
@@ -6,6 +6,7 @@
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
@@ -13,12 +14,14 @@
{
private final DeviceAdapterRegistry registry;
private final DeviceDiscoveryPersistenceService persistence;
+ private final ITestDataReceptionService reception;
public DevelopmentSimulatorDiscoveryScheduler(DeviceAdapterRegistry registry,
- DeviceDiscoveryPersistenceService persistence)
+ DeviceDiscoveryPersistenceService persistence, ITestDataReceptionService reception)
{
this.registry = registry;
this.persistence = persistence;
+ this.reception = reception;
}
/** 注册表已包含固定开发适配器时,将一轮检测柜观测写入 Redis 候选。 */
@@ -28,5 +31,7 @@
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();
}
}
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
index e111730..5594c72 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/system/TTestController.java
@@ -25,6 +25,7 @@
import com.ruoyi.system.domain.TTestDataOverride;
import com.ruoyi.system.service.ITTestService;
import com.ruoyi.system.service.ITestConsoleDataService;
+import com.ruoyi.system.service.ITestDataQueryService;
/** 测试管理 Controller。 */
@RestController
@@ -36,6 +37,9 @@
@Autowired
private ITestConsoleDataService consoleDataService;
+
+ @Autowired
+ private ITestDataQueryService testDataQueryService;
@PreAuthorize("@ss.hasAnyPermi('system:test:add,system:test:edit')")
@GetMapping("/options")
@@ -161,6 +165,30 @@
return success(consoleDataService.selectOverrides(testId, dataType));
}
+ /** 只查询已归属并持久化到当前测试的检测柜 record。 */
+ @PreAuthorize("@ss.hasPermi('system:test:query')")
+ @GetMapping("/{testId}/data/records")
+ public AjaxResult persistedRecords(@PathVariable Long testId)
+ {
+ return success(testDataQueryService.records(testId));
+ }
+
+ /** 只查询当前测试持久化采样汇总出的工步事实。 */
+ @PreAuthorize("@ss.hasPermi('system:test:query')")
+ @GetMapping("/{testId}/data/steps")
+ public AjaxResult persistedSteps(@PathVariable Long testId)
+ {
+ return success(testDataQueryService.steps(testId));
+ }
+
+ /** 只查询当前测试持久化采样汇总出的循环事实。 */
+ @PreAuthorize("@ss.hasPermi('system:test:query')")
+ @GetMapping("/{testId}/data/cycles")
+ public AjaxResult persistedCycles(@PathVariable Long testId)
+ {
+ return success(testDataQueryService.cycles(testId));
+ }
+
/** 一次提交编辑、导入或删除;服务端只追加一条修订行为。 */
@PreAuthorize("@ss.hasPermi('system:test:edit')")
@Log(title = "测试控制台数据修订", businessType = BusinessType.UPDATE)
diff --git a/ruoyi-admin/src/main/resources/db/migration/V27__persist_bts_step_name.sql b/ruoyi-admin/src/main/resources/db/migration/V27__persist_bts_step_name.sql
new file mode 100644
index 0000000..1de7c6f
--- /dev/null
+++ b/ruoyi-admin/src/main/resources/db/migration/V27__persist_bts_step_name.sql
@@ -0,0 +1,2 @@
+ALTER TABLE t_test_sample
+ ADD COLUMN step_name VARCHAR(128) NULL COMMENT 'BTS 实际工步名称;旧数据为空时回退工步类型' AFTER step_type;
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/config/TestPersistedDataQueryContractTest.java b/ruoyi-admin/src/test/java/com/ruoyi/config/TestPersistedDataQueryContractTest.java
new file mode 100644
index 0000000..42b1ad9
--- /dev/null
+++ b/ruoyi-admin/src/test/java/com/ruoyi/config/TestPersistedDataQueryContractTest.java
@@ -0,0 +1,56 @@
+package com.ruoyi.config;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import javax.xml.parsers.DocumentBuilderFactory;
+import org.junit.jupiter.api.Test;
+
+/** 测试数据列表必须通过测试级持久化查询,不得回读模拟器历史。 */
+class TestPersistedDataQueryContractTest
+{
+ @Test
+ void exposesTestScopedPersistedDataQueries()
+ {
+ String mapper = resource("mapper/system/TTestMapper.xml");
+ assertTrue(mapper.contains("id=\"selectPersistedRecords\""));
+ assertTrue(mapper.contains("id=\"selectPersistedSteps\""));
+ assertTrue(mapper.contains("id=\"selectPersistedCycles\""));
+ assertTrue(mapper.contains("id=\"selectTestSampleContentHash\""));
+ assertTrue(mapper.contains("sample.step_name"));
+ assertTrue(mapper.contains("where sample.test_id = #{testId}"));
+ assertTrue(mapper.contains("from t_test_sample sample"));
+ }
+
+ @Test
+ void mapperXmlIsWellFormed()
+ {
+ try (InputStream stream = TestPersistedDataQueryContractTest.class.getClassLoader()
+ .getResourceAsStream("mapper/system/TTestMapper.xml"))
+ {
+ if (stream == null) throw new AssertionError("缺少资源 mapper/system/TTestMapper.xml");
+ DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
+ factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ factory.newDocumentBuilder().parse(stream);
+ }
+ catch (Exception exception)
+ {
+ throw new AssertionError("TTestMapper.xml 必须是可解析的 XML", exception);
+ }
+ }
+
+ private static String resource(String path)
+ {
+ try (InputStream stream = TestPersistedDataQueryContractTest.class.getClassLoader().getResourceAsStream(path))
+ {
+ if (stream == null) throw new AssertionError("缺少资源 " + path);
+ return new String(stream.readAllBytes(), StandardCharsets.UTF_8);
+ }
+ catch (Exception exception)
+ {
+ throw new AssertionError("无法读取资源 " + path, exception);
+ }
+ }
+}
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java
index f91bc15..8ac19ca 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/web/controller/system/TTestControllerContractTest.java
@@ -30,6 +30,15 @@
assertPermission("endReception", "@ss.hasPermi('system:test:stop')", Long.class);
assertEquals("/{testId}/reception/end", TTestController.class.getMethod("endReception", Long.class)
.getAnnotation(PostMapping.class).value()[0]);
+ assertPermission("persistedRecords", "@ss.hasPermi('system:test:query')", Long.class);
+ assertEquals("/{testId}/data/records", TTestController.class.getMethod("persistedRecords", Long.class)
+ .getAnnotation(org.springframework.web.bind.annotation.GetMapping.class).value()[0]);
+ assertPermission("persistedSteps", "@ss.hasPermi('system:test:query')", Long.class);
+ assertEquals("/{testId}/data/steps", TTestController.class.getMethod("persistedSteps", Long.class)
+ .getAnnotation(org.springframework.web.bind.annotation.GetMapping.class).value()[0]);
+ assertPermission("persistedCycles", "@ss.hasPermi('system:test:query')", Long.class);
+ assertEquals("/{testId}/data/cycles", TTestController.class.getMethod("persistedCycles", Long.class)
+ .getAnnotation(org.springframework.web.bind.annotation.GetMapping.class).value()[0]);
}
private void assertPermission(String methodName, String permission, Class<?>... parameterTypes) throws Exception
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 c000e5c..25139a7 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
@@ -58,6 +58,11 @@
List<TTestWarningThreshold> selectTestThresholds(Long testId);
/** 按来源和起始边界顺序返回测试接收区间。 */
List<TTestReceptionInterval> selectTestReceptionIntervals(Long testId);
+ /** 查询当前正在接收的测试,供接收调度绑定测试事实。 */
+ List<Long> selectReceivingTestIds();
+ List<java.util.Map<String, Object>> selectPersistedRecords(@org.apache.ibatis.annotations.Param("testId") Long testId);
+ List<java.util.Map<String, Object>> selectPersistedSteps(@org.apache.ibatis.annotations.Param("testId") Long testId);
+ List<java.util.Map<String, Object>> selectPersistedCycles(@org.apache.ibatis.annotations.Param("testId") Long testId);
List<TTestResult> selectTestResults(TTestResult result);
String selectLogicStepMappingsJson(Long testId);
int updateLogicStepMappings(@Param("testId") Long testId, @Param("mappingsJson") String mappingsJson,
@@ -139,12 +144,15 @@
int updateTestNaturallyEnded(@Param("testId") Long testId, @Param("endedAtUtc") Date endedAtUtc);
String selectSampleContentHash(@Param("channelId") Long channelId, @Param("btsTestId") String btsTestId,
@Param("sequenceId") Long sequenceId);
+ /** 按测试事实查询通道序号,避免补采重复写入已即时接收的采样。 */
+ String selectTestSampleContentHash(@Param("testId") Long testId, @Param("channelId") Long channelId,
+ @Param("sequenceId") Long sequenceId);
int insertBtsSample(@Param("testId") Long testId, @Param("bindingId") Long bindingId,
@Param("channelId") Long channelId, @Param("intervalId") Long intervalId,
@Param("btsTestId") String btsTestId, @Param("sequenceId") Long sequenceId,
@Param("deviceTimeUtc") Date deviceTimeUtc, @Param("deviceTimeRaw") String deviceTimeRaw,
@Param("receivedAtUtc") Date receivedAtUtc, @Param("cycleNo") Integer cycleNo,
- @Param("stepNo") Integer stepNo, @Param("stepType") String stepType,
+ @Param("stepNo") Integer stepNo, @Param("stepType") String stepType, @Param("stepName") String stepName,
@Param("testTimeMs") Long testTimeMs, @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("contentSha256") byte[] contentSha256);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataQueryService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataQueryService.java
new file mode 100644
index 0000000..ec2e5a7
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataQueryService.java
@@ -0,0 +1,12 @@
+package com.ruoyi.system.service;
+
+import java.util.List;
+import java.util.Map;
+
+/** 只读取已经归属测试的持久化数据。 */
+public interface ITestDataQueryService
+{
+ List<Map<String, Object>> records(Long testId);
+ List<Map<String, Object>> steps(Long testId);
+ List<Map<String, Object>> cycles(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
new file mode 100644
index 0000000..4b29eea
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/ITestDataReceptionService.java
@@ -0,0 +1,8 @@
+package com.ruoyi.system.service;
+
+/** 将设备已接收测量追加到当前测试事实。 */
+public interface ITestDataReceptionService
+{
+ /** 扫描当前接收中的开发检测柜,并把接收窗口内的新记录持久化。 */
+ void captureDevelopmentCabinetRecords();
+}
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 8e725bf..7f85409 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
@@ -190,6 +190,15 @@
byte[] hash = hash(sample.sequenceId() + "|" + sample.stepNo() + "|" + sample.cycleNo() + "|"
+ sample.stepType() + "|" + sample.deviceTimeRaw() + "|" + sample.voltageV() + "|"
+ sample.currentA() + "|" + sample.capacityAh() + "|" + sample.energyWh());
+ String testScoped = mapper.selectTestSampleContentHash(test.getTestId(), binding.getChannelId(),
+ sample.sequenceId());
+ if (testScoped != null)
+ {
+ if (!testScoped.equalsIgnoreCase(HexFormat.of().formatHex(hash)))
+ mapper.insertBackfillConflict(test.getTestId(), binding.getChannelId(),
+ "测试内采样内容冲突 seqid=" + sample.sequenceId());
+ return;
+ }
String old = mapper.selectSampleContentHash(binding.getChannelId(), btsTestId, sample.sequenceId());
if (old != null && !old.equalsIgnoreCase(HexFormat.of().formatHex(hash)))
{
@@ -199,7 +208,8 @@
}
mapper.insertBtsSample(test.getTestId(), binding.getBindingId(), binding.getChannelId(),
intervalId(intervals, sample.deviceTime()), btsTestId, sample.sequenceId(), Date.from(sample.deviceTime()),
- sample.deviceTimeRaw(), new Date(), sample.cycleNo(), sample.stepNo(), sample.stepType(), sample.testTimeMs(),
+ sample.deviceTimeRaw(), new Date(), sample.cycleNo(), sample.stepNo(), sample.stepType(),
+ stepNameForType(sample.stepType()), sample.testTimeMs(),
sample.voltageV(), sample.currentA(), sample.capacityAh(), sample.energyWh(), hash);
Long sampleId = mapper.selectBtsSampleId(binding.getChannelId(), btsTestId, sample.sequenceId());
if (sampleId != null)
@@ -255,6 +265,18 @@
Date.from(record.serverReceivedAt()), record.metricCode(), record.value(), record.unit(), hash);
}
+ /** BTS download 协议只提供类型码;演示协议按已确认的类型语义补出名称。 */
+ private String stepNameForType(String stepType)
+ {
+ return switch (stepType == null ? "" : stepType.toLowerCase(java.util.Locale.ROOT))
+ {
+ case "cc" -> "恒流充电";
+ case "dc" -> "恒流放电";
+ case "rest" -> "搁置";
+ default -> stepType;
+ };
+ }
+
private boolean checkIntegrity(TTest test, Date checkedAt)
{
// 序号只在同一来源、同一接收区间内比较;区间边界和暂停间隔不构成缺口。
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
new file mode 100644
index 0000000..10226fb
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataQueryServiceImpl.java
@@ -0,0 +1,47 @@
+package com.ruoyi.system.service.impl;
+
+import java.util.List;
+import java.util.Map;
+import org.springframework.stereotype.Service;
+import com.ruoyi.common.exception.ServiceException;
+import com.ruoyi.system.mapper.TTestMapper;
+import com.ruoyi.system.service.ITTestService;
+import com.ruoyi.system.service.ITestDataQueryService;
+
+/** 测试数据查询模块;实现不访问设备或模拟器历史。 */
+@Service
+public class TestDataQueryServiceImpl implements ITestDataQueryService
+{
+ private final TTestMapper mapper;
+ private final ITTestService tests;
+
+ public TestDataQueryServiceImpl(TTestMapper mapper, ITTestService tests)
+ {
+ this.mapper = mapper;
+ this.tests = tests;
+ }
+
+ @Override public List<Map<String, Object>> records(Long testId)
+ {
+ requireTest(testId);
+ return mapper.selectPersistedRecords(testId);
+ }
+
+ @Override public List<Map<String, Object>> steps(Long testId)
+ {
+ requireTest(testId);
+ return mapper.selectPersistedSteps(testId);
+ }
+
+ @Override public List<Map<String, Object>> cycles(Long testId)
+ {
+ requireTest(testId);
+ return mapper.selectPersistedCycles(testId);
+ }
+
+ private void requireTest(Long testId)
+ {
+ if (testId == null || tests.selectTestById(testId) == null)
+ throw new ServiceException("测试不存在");
+ }
+}
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
new file mode 100644
index 0000000..025c217
--- /dev/null
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TestDataReceptionServiceImpl.java
@@ -0,0 +1,110 @@
+package com.ruoyi.system.service.impl;
+
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.Date;
+import java.util.HexFormat;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+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.system.domain.TTest;
+import com.ruoyi.system.domain.TTestBattery;
+import com.ruoyi.system.domain.TTestChannelBinding;
+import com.ruoyi.system.domain.TTestReceptionInterval;
+import com.ruoyi.system.mapper.TTestMapper;
+import com.ruoyi.system.service.ITTestService;
+import com.ruoyi.system.service.ITestDataReceptionService;
+
+/** 接收时确定测试归属,并以现有采样表追加保存检测柜事实。 */
+@Service
+public class TestDataReceptionServiceImpl implements ITestDataReceptionService
+{
+ private final TTestMapper mapper;
+ private final ITTestService tests;
+ private final BtsDevelopmentSimulator simulator;
+
+ public TestDataReceptionServiceImpl(TTestMapper mapper, ITTestService tests, BtsDevelopmentSimulator simulator)
+ {
+ this.mapper = mapper;
+ this.tests = tests;
+ this.simulator = simulator;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void captureDevelopmentCabinetRecords()
+ {
+ List<Long> testIds = mapper.selectReceivingTestIds();
+ if (testIds == null) return;
+ for (Long testId : testIds)
+ {
+ TTest test = tests.selectTestById(testId);
+ if (test == null || !"RECEIVING".equals(test.getReceptionStatus())) continue;
+ captureTest(test);
+ }
+ }
+
+ private void captureTest(TTest test)
+ {
+ for (TTestBattery battery : test.getBatteries() == null ? List.<TTestBattery>of() : test.getBatteries())
+ {
+ TTestChannelBinding binding = battery.getBinding();
+ if (binding == null || binding.getDevid() == null || binding.getChlid() == null) continue;
+ Optional<TTestReceptionInterval> open = mapper.selectTestReceptionIntervals(test.getTestId()).stream()
+ .filter(item -> Objects.equals(item.getBindingId(), binding.getBindingId())
+ && "CABINET_CHANNEL".equals(item.getSourceType())
+ && Objects.equals(item.getSourceId(), binding.getChannelId())
+ && "OPEN".equals(item.getIntervalStatus()))
+ .findFirst();
+ if (open.isEmpty()) continue;
+ Date start = open.get().getStartBoundaryAtUtc();
+ List<BtsHistoryRecord> records;
+ try { records = simulator.btsRecords(binding.getDevid(), binding.getChlid()); }
+ catch (IllegalArgumentException ignored) { continue; }
+ for (BtsHistoryRecord record : records)
+ {
+ if (start != null && record.deviceTime().isBefore(start.toInstant())) continue;
+ save(test, battery, binding, open.get(), record);
+ }
+ }
+ }
+
+ private void save(TTest test, TTestBattery battery, TTestChannelBinding binding,
+ TTestReceptionInterval interval, BtsHistoryRecord record)
+ {
+ // 模拟器历史 testid 按通道固定;接收事实必须再带测试作用域,避免顺序测试共用全局幂等键。
+ String btsTestId = "SIM-" + binding.getDevid() + "-" + binding.getChlid() + "-T" + test.getTestId();
+ byte[] hash = hash(record);
+ String old = mapper.selectSampleContentHash(binding.getChannelId(), btsTestId, record.sequenceId());
+ if (old != null)
+ {
+ if (!old.equalsIgnoreCase(HexFormat.of().formatHex(hash)))
+ mapper.insertBackfillConflict(test.getTestId(), binding.getChannelId(),
+ "实时接收内容冲突 testid=" + btsTestId + ", seqid=" + record.sequenceId());
+ return;
+ }
+ mapper.insertBtsSample(test.getTestId(), binding.getBindingId(), binding.getChannelId(), interval.getIntervalId(),
+ btsTestId, record.sequenceId(), Date.from(record.deviceTime()),
+ record.deviceTime().toString(), new Date(), record.cycleNo(), record.stepNo(), record.stepType(),
+ record.stepName(), null,
+ BigDecimal.valueOf(record.voltageV()), BigDecimal.valueOf(record.currentA()),
+ BigDecimal.valueOf(record.capacityAh()), BigDecimal.valueOf(record.energyWh()), hash);
+ }
+
+ private byte[] hash(BtsHistoryRecord record)
+ {
+ try
+ {
+ String value = record.sequenceId() + "|" + record.deviceTime() + "|" + record.currentA() + "|"
+ + record.voltageV() + "|" + record.capacityAh() + "|" + record.energyWh() + "|"
+ + record.cycleNo() + "|" + record.stepNo() + "|" + record.stepType();
+ return MessageDigest.getInstance("SHA-256").digest(value.getBytes(StandardCharsets.UTF_8));
+ }
+ catch (Exception exception) { throw new IllegalStateException("无法计算采样内容摘要", exception); }
+ }
+}
diff --git a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
index 1f0e210..4ba5d0a 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TTestMapper.xml
@@ -367,6 +367,67 @@
where test_id = #{testId}
order by source_type, source_id, source_channel_code, start_boundary_at_utc, interval_id
</select>
+ <select id="selectReceivingTestIds" resultType="long">
+ select test_id from t_test where del_flag = 0 and reception_status = 'RECEIVING' order by test_id
+ </select>
+ <select id="selectPersistedRecords" resultType="map">
+ select sample.sample_id id,
+ concat('RD-', channel.devid, '-', channel.chlid, '-', lpad(sample.seq_id, 6, '0')) recordNo,
+ battery.battery_no batteryNo, sample.cycle_no cycleNo, sample.step_no stepNo,
+ coalesce(sample.step_name, sample.step_type) stepName, sample.step_type stepType, sample.seq_id sequenceId,
+ sample.device_time_utc recordedAt, sample.device_time_utc absoluteTime,
+ sample.voltage_v voltage, sample.current_a current, sample.capacity_ah capacity,
+ sample.energy_wh energy, channel.devid deviceId, channel.chlid channelId
+ from t_test_sample sample
+ join t_test_battery battery on battery.relation_id = sample.binding_id and battery.del_flag = 0
+ join t_detection_channel channel on channel.channel_id = sample.channel_id and channel.del_flag = 0
+ where sample.test_id = #{testId}
+ order by sample.device_time_utc desc, sample.seq_id desc, sample.sample_id desc
+ </select>
+ <select id="selectPersistedSteps" resultType="map">
+ select concat('step:', channel.devid, ':', channel.chlid, ':', sample.cycle_no, ':', sample.step_no) id,
+ concat('STEP-', channel.devid, '-', channel.chlid, '-', sample.cycle_no, '-', sample.step_no) recordNo,
+ battery.battery_no batteryNo, sample.cycle_no cycleNo, sample.step_no stepNo,
+ coalesce(sample.step_name, sample.step_type) stepName,
+ sample.step_type stepType, coalesce(sample.step_name, sample.step_type) actualStepLabel,
+ count(*) recordCount,
+ min(sample.device_time_utc) recordedAt, min(sample.device_time_utc) absoluteTime,
+ timestampdiff(second, min(sample.device_time_utc), max(sample.device_time_utc)) stepDuration,
+ max(sample.capacity_ah) capacity, max(sample.energy_wh) energy,
+ min(sample.voltage_v) startVoltage,
+ max(sample.voltage_v) endVoltage,
+ max(case when sample.current_a >= 0 then sample.voltage_v end) chargePeakVoltage,
+ min(case when sample.current_a < 0 then sample.voltage_v end) dischargeValleyVoltage,
+ channel.devid deviceId, channel.chlid channelId
+ from t_test_sample sample
+ join t_test_battery battery on battery.relation_id = sample.binding_id and battery.del_flag = 0
+ join t_detection_channel channel on channel.channel_id = sample.channel_id and channel.del_flag = 0
+ where sample.test_id = #{testId}
+ group by sample.binding_id, battery.battery_no, channel.devid, channel.chlid, sample.cycle_no, sample.step_no,
+ sample.step_type, sample.step_name
+ order by sample.cycle_no, sample.step_no, battery.battery_no
+ </select>
+ <select id="selectPersistedCycles" resultType="map">
+ select concat('cycle:', channel.devid, ':', channel.chlid, ':', sample.cycle_no) id,
+ concat('CYCLE-', channel.devid, '-', channel.chlid, '-', sample.cycle_no) recordNo,
+ battery.battery_no batteryNo, sample.cycle_no cycleNo, count(*) recordCount,
+ min(sample.device_time_utc) recordedAt, min(sample.device_time_utc) absoluteTime,
+ greatest(max(case when sample.current_a >= 0 then sample.capacity_ah end), 0) chargeCapacity,
+ greatest(max(case when sample.current_a < 0 then sample.capacity_ah end), 0) dischargeCapacity,
+ greatest(max(case when sample.current_a >= 0 then sample.energy_wh end), 0) chargeEnergy,
+ greatest(max(case when sample.current_a < 0 then abs(sample.energy_wh) end), 0) dischargeEnergy,
+ case when max(case when sample.current_a >= 0 then sample.capacity_ah end) > 0
+ then greatest(max(case when sample.current_a < 0 then sample.capacity_ah end), 0)
+ / max(case when sample.current_a >= 0 then sample.capacity_ah end) * 100
+ else 0 end efficiency,
+ channel.devid deviceId, channel.chlid channelId
+ from t_test_sample sample
+ join t_test_battery battery on battery.relation_id = sample.binding_id and battery.del_flag = 0
+ join t_detection_channel channel on channel.channel_id = sample.channel_id and channel.del_flag = 0
+ where sample.test_id = #{testId}
+ group by sample.binding_id, battery.battery_no, channel.devid, channel.chlid, sample.cycle_no
+ order by sample.cycle_no, battery.battery_no
+ </select>
<select id="selectTestResults" parameterType="TTestResult" resultMap="TTestResultResultMap">
select result.*, battery.test_id from t_test_result result
join t_test_battery battery on battery.relation_id = result.relation_id
@@ -559,13 +620,18 @@
select hex(content_sha256) from t_test_sample
where channel_id = #{channelId} and bts_test_id = #{btsTestId} and seq_id = #{sequenceId}
</select>
+ <select id="selectTestSampleContentHash" resultType="string">
+ select hex(content_sha256) from t_test_sample
+ where test_id = #{testId} and channel_id = #{channelId} and seq_id = #{sequenceId}
+ order by sample_id limit 1
+ </select>
<insert id="insertBtsSample">
insert ignore into t_test_sample
(test_id, binding_id, channel_id, reception_interval_id, bts_test_id, seq_id, device_time_utc,
- device_time_raw, received_at_utc, cycle_no, step_no, step_type, test_time_ms, voltage_v,
+ device_time_raw, received_at_utc, cycle_no, step_no, step_type, step_name, test_time_ms, voltage_v,
current_a, capacity_ah, energy_wh, content_sha256)
values (#{testId}, #{bindingId}, #{channelId}, #{intervalId}, #{btsTestId}, #{sequenceId}, #{deviceTimeUtc},
- #{deviceTimeRaw}, #{receivedAtUtc}, #{cycleNo}, #{stepNo}, #{stepType}, #{testTimeMs}, #{voltageV},
+ #{deviceTimeRaw}, #{receivedAtUtc}, #{cycleNo}, #{stepNo}, #{stepType}, #{stepName}, #{testTimeMs}, #{voltageV},
#{currentA}, #{capacityAh}, #{energyWh}, #{contentSha256})
</insert>
<insert id="insertBtsMeasurement">
--
Gitblit v1.7.1