yeg
2026-09-05 7259e265b9237286996b09da03038ea5a133e1e0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
package com.battery.business.service.impl;
 
import java.time.Instant;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.lang.Nullable;
import com.battery.device.api.TestDataObservationAdapter;
import com.battery.device.api.model.ChannelIdentity;
import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore;
import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
import com.battery.device.api.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate;
import com.battery.device.api.model.Measurement;
import com.battery.business.domain.TTest;
import com.battery.business.domain.TTestBattery;
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.TTestMapper;
 
/** 将设备最新快照转换为测试视角,按测试接收状态决定是否展示采集参数。 */
@Service
public class TTestLiveServiceImpl implements ITestLiveService
{
    private final ITTestService tests;
    private final DeviceDiscoveryCandidateStore candidates;
    private final TestDataObservationAdapter adapter;
    private final TTestMapper mapper;
 
    @Autowired
    public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates,
            @Nullable TestDataObservationAdapter adapter, TTestMapper mapper)
    { this.tests = tests; this.candidates = candidates; this.adapter = adapter; this.mapper = mapper; }
 
    public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates,
            TestDataObservationAdapter adapter)
    { this(tests, candidates, adapter, null); }
 
    @Override public LiveState current(Long testId)
    {
        TTest test = tests.selectTestById(testId);
        List<CabinetCandidate> cabinets = candidates.snapshot(Instant.now()).cabinets();
        List<LiveSource> sources = new ArrayList<>();
        boolean receiving = "RECEIVING".equals(test.getReceptionStatus());
        for (TTestBattery battery : test.getBatteries() == null ? List.<TTestBattery>of() : test.getBatteries())
        {
            if (battery.getBinding() != null)
                sources.add(snapshotFor(cabinetSource(battery, cabinets), receiving));
            if (battery.getCollectorId() != null)
                sources.add(collectorSource(battery, receiving));
        }
        List<Map<String, Object>> exceptions = new ArrayList<>();
        if (receiving)
            exceptions.addAll(sources.stream().filter(source -> !"COLLECTOR".equals(source.sourceType())
                    && "OFFLINE".equals(source.deviceStatus()))
                    .map(source -> Map.<String, Object>of("sourceType", source.sourceType(), "sourceId", source.sourceId(),
                            "batteryNo", source.batteryNo(), "status", source.deviceStatus(), "message", "数据来源当前不可观测"))
                    .toList());
        // 完成或暂停后只保留历史异常事实,实时控制台不再展示开放预警。
        if (receiving && mapper != null) {
            mapper.selectWarningExceptions(testId, "OPEN").forEach(item -> exceptions.add(Map.of(
                    "exceptionId", item.getExceptionId(), "sourceType", item.getSourceType(),
                    "metricCode", item.getMetricCode(), "value", item.getAbnormalValue(), "unit", item.getUnit(),
                    "status", item.getExceptionStatus(), "message", "测试预警异常")));
        }
        String testStatus = test.getTestStatus();
        TestPlanSnapshot testPlan = detectorTestPlan(test);
        return new LiveState(testId, testStatus, test.getReceptionStatus(), Instant.now().toEpochMilli(), sources, exceptions, testPlan);
    }
 
    private TestPlanSnapshot detectorTestPlan(TTest test)
    {
        if (adapter == null) return null;
        if (test.getBatteries() == null) return null;
        for (TTestBattery battery : test.getBatteries())
        {
            if (battery.getBinding() == null || battery.getBinding().getDevid() == null
                    || battery.getBinding().getChlid() == null) continue;
            try
            {
                var binding = battery.getBinding();
                var source = adapter.testPlan(new ChannelIdentity(binding.getAdapterCode(), binding.getDevtype(),
                        binding.getDevid(), binding.getSubdevid(), binding.getChlid()));
                if (source == null) continue;
                List<TestPlanRow> rows = source.rows().stream()
                        .map(row -> new TestPlanRow(row.rowType(), row.stepNo(), row.cyclePosition(), row.stepName(), row.durationSeconds(),
                                row.voltageV(), row.executionStatus(), row.cycleCount(), row.recordCount()))
                        .toList();
                return new TestPlanSnapshot(source.currentCycleNo(), source.currentStepNo(),
                        source.plannedCycleCount(), rows);
            }
            catch (IllegalArgumentException ignored) { /* 来源未提供方案时保持为空。 */ }
        }
        return null;
    }
 
    private LiveSource snapshotFor(LiveSource fresh, boolean receiving)
    {
        // 暂停或停止只停止数据请求;设备状态仍以本次查询的最新事实为准,采集快照不继续展示。
        if (!receiving)
            return withoutSnapshot(fresh);
        return fresh;
    }
 
    private LiveSource withoutSnapshot(LiveSource source)
    {
        return new LiveSource(source.sourceType(), source.sourceId(), source.batteryNo(), source.deviceStatus(), Map.of());
    }
 
    private LiveSource cabinetSource(TTestBattery battery, List<CabinetCandidate> cabinets)
    {
        var binding = battery.getBinding();
        for (CabinetCandidate cabinet : cabinets) for (ChannelCandidate channel : cabinet.channels())
            if (Objects.equals(channel.identity().adapterCode(), binding.getAdapterCode())
                    && Objects.equals(channel.identity().devtype(), binding.getDevtype())
                    && Objects.equals(channel.identity().devid(), binding.getDevid())
                    && Objects.equals(channel.identity().subdevid(), binding.getSubdevid())
                    && Objects.equals(channel.identity().chlid(), binding.getChlid()))
                return new LiveSource("CABINET_CHANNEL", binding.getChannelId(), battery.getBatteryNo(),
                        channel.online() ? "ONLINE" : "OFFLINE",
                        channel.online() ? snapshot(channel) : Map.of());
        return new LiveSource("CABINET_CHANNEL", binding.getChannelId(), battery.getBatteryNo(), "OFFLINE", Map.of());
    }
 
    private Map<String, Object> snapshot(ChannelCandidate channel)
    {
        Map<String, Object> value = new LinkedHashMap<>();
        if (channel.latestSnapshot() == null) return value;
        value.put("source", "检测柜");
        value.put("observedAt", channel.latestSnapshot().receivedAt().toEpochMilli());
        // BTS 核心采样字段是快照的一等事实,不能只依赖动态 measurements 展示。
        value.put("currentA", channel.latestSnapshot().currentA());
        value.put("voltageV", channel.latestSnapshot().voltageV());
        value.put("capacityAh", channel.latestSnapshot().capacityAh());
        value.put("energyWh", channel.latestSnapshot().energyWh());
        // 实时方案需要设备当前循环和实际工步;这些字段只用于快照展示,不形成历史采集事实。
        value.put("cycleNo", channel.latestSnapshot().cycleNo());
        value.put("stepNo", channel.latestSnapshot().stepNo());
        value.put("stepType", channel.latestSnapshot().stepType());
        value.put("totalTimeSeconds", channel.latestSnapshot().totalTimeSeconds());
        value.put("measurements", channel.latestSnapshot().measurements().entrySet().stream().map(entry -> {
            Measurement measurement = entry.getValue();
            return Map.of("metricCode", entry.getKey(), "value", measurement.value(), "unit", measurement.unit());
        }).toList());
        List<String> units = new ArrayList<>();
        if (channel.latestSnapshot().currentA() != null) units.add("A");
        if (channel.latestSnapshot().voltageV() != null) units.add("V");
        if (channel.latestSnapshot().capacityAh() != null) units.add("Ah");
        if (channel.latestSnapshot().energyWh() != null) units.add("Wh");
        units.addAll(channel.latestSnapshot().measurements().values().stream().map(Measurement::unit).toList());
        value.put("units", units.stream().filter(item -> item != null && !item.isBlank()).distinct().toList());
        return value;
    }
 
    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());
        // 非接收状态只读设备发现事实,不能先请求测量再隐藏,否则仍会产生新观测。
        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());
        }
        var collector = adapter.latestCollector(collectorCode);
        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()))).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()));
            return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(),
                    collector.online() ? "ONLINE" : "OFFLINE", collector.online() ? value : Map.of());
        }
        return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(), "OFFLINE", Map.of());
    }
 
}