From e38aca8134570513435c003c25dc05f1ae51c9c9 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 08 十月 2026 13:56:28 +0800
Subject: [PATCH] 实现 Ticket04:采集器辅助测量与登记入口,拆除开发模拟器

---
 battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java |   88 +++++++++++++++++++++++++++++--------------
 1 files changed, 59 insertions(+), 29 deletions(-)

diff --git a/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java b/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java
index 1b38027..3ef92c6 100644
--- a/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java
+++ b/battery-business/src/main/java/com/battery/business/service/impl/TTestLiveServiceImpl.java
@@ -18,10 +18,12 @@
 import com.battery.device.api.model.Measurement;
 import com.battery.business.domain.TTest;
 import com.battery.business.domain.TTestBattery;
+import com.battery.business.domain.TDeviceAdapter;
 import com.battery.business.service.ITestLiveService;
 import com.battery.business.service.ITestLiveService.TestPlanRow;
 import com.battery.business.service.ITestLiveService.TestPlanSnapshot;
 import com.battery.business.service.ITTestService;
+import com.battery.business.mapper.TDeviceAdapterMapper;
 import com.battery.business.mapper.TTestMapper;
 
 /** 将设备最新快照转换为测试视角,按测试接收状态决定是否展示采集参数。 */
@@ -32,16 +34,18 @@
     private final DeviceDiscoveryCandidateStore candidates;
     private final TestDataObservationAdapter adapter;
     private final TTestMapper mapper;
+    private final TDeviceAdapterMapper adapters;
 
     @Autowired
     public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates,
-            @Nullable TestDataObservationAdapter adapter, TTestMapper mapper)
-    { this.tests = tests; this.candidates = candidates; this.adapter = adapter; this.mapper = mapper; }
+            @Nullable TestDataObservationAdapter adapter, TTestMapper mapper, TDeviceAdapterMapper adapters)
+    { this.tests = tests; this.candidates = candidates; this.adapter = adapter; this.mapper = mapper; this.adapters = adapters; }
 
     public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates,
             TestDataObservationAdapter adapter)
-    { this(tests, candidates, adapter, null); }
+    { this(tests, candidates, adapter, null, null); }
 
+    /** 返回测试当前设备事实;接收中展示测量和实时提醒,历史异常处理状态不参与判定。 */
     @Override public LiveState current(Long testId)
     {
         TTest test = tests.selectTestById(testId);
@@ -190,6 +194,10 @@
     private Map<String, Object> snapshot(ChannelCandidate channel)
     {
         Map<String, Object> value = new LinkedHashMap<>();
+        // 通道工作状态是独立设备事实:读库来源下不可观测,界面把它当正常取值展示而不是异常。
+        value.put("workStatus", channel.workStatus() == null ? null : channel.workStatus().name());
+        // 最近数据时间取来源里最近一次采样行的设备时间,与展示轮询节奏无关。
+        value.put("lastDataAt", channel.lastDataAt() == null ? null : channel.lastDataAt().toEpochMilli());
         if (channel.latestSnapshot() == null) return value;
         value.put("source", "检测柜");
         value.put("observedAt", channel.latestSnapshot().receivedAt().toEpochMilli());
@@ -218,44 +226,66 @@
         return value;
     }
 
+    /**
+     * 采集器来源:测量按 ADR-0064 由同一来源实例作为辅助测量提供,所以它的在线事实就是该来源的可达性
+     * (ADR-0063:在线只表示来源可达),不再依赖只有开发模拟器会产出的发现候选。
+     *
+     * <p>来源不提供辅助测量、或来源可达但这一刻还没有任何辅助测量值时,明确标注为「无辅助数据」:
+     * 这是正常取值而不是故障,主测量接收不受影响,也不因此生成离线异常。来源不可达时不标这个标记——
+     * 那种情况是来源离线,由既有的离线异常路径表达。
+     */
     private LiveSource collectorSource(TTestBattery battery, boolean receiving)
     {
         if (adapter == null || mapper == null) return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(),
                 "OFFLINE", Map.of());
         String collectorCode = mapper.selectCollectorCode(battery.getCollectorId());
-        // 非接收状态只读设备发现事实,不能先请求测量再隐藏,否则仍会产生新观测。
+        String adapterCode = battery.getBinding() == null ? null : battery.getBinding().getAdapterCode();
+        boolean sourceOnline = sourceOnline(adapterCode);
+        // 非接收状态只读来源可达性,不能先请求测量再隐藏,否则仍会产生新观测。
         if (!receiving)
         {
-            boolean online = candidates.findCollector(collectorCode, Instant.now())
-                    .map(DeviceDiscoveryCandidateStore.CollectorCandidate::online).orElse(false);
             return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(),
-                    online ? "ONLINE" : "OFFLINE", Map.of());
+                    sourceOnline ? "ONLINE" : "OFFLINE", Map.of());
         }
-        var collector = adapter.latestCollector(collectorCode);
-        if (collector != null)
+        var collector = adapter.latestCollector(adapterCode, collectorCode);
+        Map<String, Object> value = new LinkedHashMap<>();
+        value.put("source", "采集器");
+        if (collector == null)
         {
-            Map<String, Object> value = new LinkedHashMap<>();
-            value.put("source", "采集器"); value.put("sampleIntervalMs", collector.sampleIntervalMs());
-            // 在业务响应边界转换毫秒时间,设备时间与服务器接收时间分别保留。
-            value.put("measurements", collector.measurements().stream().filter(item -> battery.getCollectorChannels() != null
-                    && battery.getCollectorChannels().stream().anyMatch(binding -> binding.getChannelCode().equals(item.channelCode())
-                            && TestWarningExceptionService.matchesCollectorMetric(binding.getMeasurementCode(), item.metricCode()))).map(item -> {
-                Map<String, Object> measurement = new LinkedHashMap<>();
-                measurement.put("metricCode", item.metricCode());
-                measurement.put("channelCode", item.channelCode());
-                measurement.put("value", item.value());
-                measurement.put("unit", item.unit());
-                measurement.put("deviceTime", item.deviceTime().toEpochMilli());
-                measurement.put("serverReceivedAt", item.serverReceivedAt().toEpochMilli());
-                return measurement;
-            }).toList());
-            value.put("units", collector.measurements().stream().map(item -> item.unit()).distinct().toList());
-            collector.measurements().stream().map(item -> item.serverReceivedAt()).max(Instant::compareTo)
-                    .ifPresent(observedAt -> value.put("observedAt", observedAt.toEpochMilli()));
+            // 该来源根本不提供辅助测量(新威实测无辅助数据;推式来源的列语义确认前同样如此)。
+            if (sourceOnline) value.put("auxiliaryDataAvailable", false);
             return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(),
-                    collector.online() ? "ONLINE" : "OFFLINE", collector.online() ? value : Map.of());
+                    sourceOnline ? "ONLINE" : "OFFLINE", value);
         }
-        return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(), "OFFLINE", Map.of());
+        value.put("sampleIntervalMs", collector.sampleIntervalMs());
+        // 在业务响应边界转换毫秒时间,设备时间与服务器接收时间分别保留。
+        value.put("measurements", collector.measurements().stream().filter(item -> battery.getCollectorChannels() != null
+                && battery.getCollectorChannels().stream().anyMatch(binding -> binding.getChannelCode().equals(item.channelCode())
+                        && TestWarningExceptionService.matchesCollectorMetric(binding.getMeasurementCode(), item.metricCode()))).map(item -> {
+            Map<String, Object> measurement = new LinkedHashMap<>();
+            measurement.put("metricCode", item.metricCode());
+            measurement.put("channelCode", item.channelCode());
+            measurement.put("value", item.value());
+            measurement.put("unit", item.unit());
+            measurement.put("deviceTime", item.deviceTime().toEpochMilli());
+            measurement.put("serverReceivedAt", item.serverReceivedAt().toEpochMilli());
+            return measurement;
+        }).toList());
+        value.put("units", collector.measurements().stream().map(item -> item.unit()).distinct().toList());
+        collector.measurements().stream().map(item -> item.serverReceivedAt()).max(Instant::compareTo)
+                .ifPresent(observedAt -> value.put("observedAt", observedAt.toEpochMilli()));
+        // 来源可达但这一刻还没有任何辅助测量值:同样是"无辅助数据",按空值展示而不是占位。
+        if (collector.online() && collector.measurements().isEmpty()) value.put("auxiliaryDataAvailable", false);
+        return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(),
+                collector.online() ? "ONLINE" : "OFFLINE", collector.online() ? value : Map.of());
+    }
+
+    /** 来源实例是否可达;来源编码缺失或查询不到时按不可达处理,不猜也不回退到其他实例。 */
+    private boolean sourceOnline(String adapterCode)
+    {
+        if (adapters == null || adapterCode == null || adapterCode.isBlank()) return false;
+        TDeviceAdapter adapter = adapters.selectDeviceAdapterByCode(adapterCode);
+        return adapter != null && "ONLINE".equalsIgnoreCase(adapter.getConnectionStatus());
     }
 
 }

--
Gitblit v1.7.1