yeg
2026-09-29 b78295e8da96c3e5e76befeef9582ce5273dbb74
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
package com.battery.business.service.impl;
 
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Service;
import com.battery.device.api.TestDataObservationAdapter;
import com.battery.device.api.model.AdapterCapabilities;
import com.battery.device.api.model.ChannelIdentity;
import com.battery.device.api.model.DetectedChannel;
import com.battery.device.api.model.DeviceObservation;
import com.battery.device.api.registry.AdapterRegistryException;
import com.battery.device.api.registry.DeviceAdapterRegistry;
 
/**
 * 把取数请求按通道身份里的适配器编码送到它所属的来源实例。
 *
 * <p>这是持久化与实时展示路径上唯一的取数入口:同一个进程里可以注册多个来源实例,
 * 每个通道的请求只送到它自己的实例;实例缺失或未提供测试数据观测契约时明确失败,
 * 不回退到其他实例。发现不走这里,它按实例在 {@link DeviceDiscoveryPersistenceService} 中执行。
 *
 * <p>声明为 {@code @Primary}:来源实例可能自己也实现了观测契约(开发模拟器就是这样,
 * 它同时是 Spring Bean),业务侧注入的必须是这个按编码解析的入口,而不是某台设备的实例。
 */
@Service
@Primary
public class RoutingTestDataObservationAdapter implements TestDataObservationAdapter
{
    /** 聚合路由不是来源实例,这个编码只用于说明"按通道身份路由"。 */
    public static final String ADAPTER_CODE = "by-source";
 
    private final DeviceAdapterRegistry registry;
 
    public RoutingTestDataObservationAdapter(DeviceAdapterRegistry registry)
    {
        this.registry = registry;
    }
 
    /** 返回聚合路由的固定编码,仅用于说明"按通道身份路由",不是任何来源实例的编码。 */
    @Override
    public String adapterCode()
    {
        return ADAPTER_CODE;
    }
 
    /** 返回聚合适配器声明的能力:只观测,不提供控制能力。 */
    @Override
    public AdapterCapabilities capabilities()
    {
        return AdapterCapabilities.observeOnly();
    }
 
    /** 聚合适配器不承载发现;发现按来源实例执行,避免在这条路上猜一个设备。 */
    @Override
    public List<DetectedChannel> discoverChannels()
    {
        throw new UnsupportedOperationException("设备发现按来源实例执行,不在聚合适配器上执行");
    }
 
    /** 一次观测里的通道可能来自多个来源实例,逐个按各自的编码请求,不能按第一个通道决定全部。 */
    @Override
    public Map<ChannelIdentity, DeviceObservation> observe(Set<ChannelIdentity> channels)
    {
        if (channels == null || channels.isEmpty()) return Map.of();
        Map<ChannelIdentity, DeviceObservation> result = new java.util.LinkedHashMap<>();
        for (Map.Entry<String, List<ChannelIdentity>> group : groupBySource(channels).entrySet())
        {
            Set<ChannelIdentity> groupChannels = new java.util.LinkedHashSet<>(group.getValue());
            result.putAll(registry.executeTestData(group.getKey(), adapter -> adapter.observe(groupChannels)));
        }
        return Map.copyOf(result);
    }
 
    private static Map<String, List<ChannelIdentity>> groupBySource(Set<ChannelIdentity> channels)
    {
        Map<String, List<ChannelIdentity>> grouped = new java.util.LinkedHashMap<>();
        for (ChannelIdentity channel : channels)
            grouped.computeIfAbsent(channel.adapterCode(), ignored -> new java.util.ArrayList<>()).add(channel);
        return grouped;
    }
 
    /**
     * 按通道所属来源实例请求该通道最近一次实时观测。
     *
     * @param channel 检测柜通道身份,其中的适配器编码决定请求送到哪个实例
     * @return 最近一次实时观测;没有成功观测时为空
     * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
     */
    @Override
    public CabinetRealtime latestCabinet(ChannelIdentity channel)
    {
        return registry.executeTestData(channel.adapterCode(), adapter -> adapter.latestCabinet(channel));
    }
 
    /**
     * 按来源实例请求采集器的辅助测量快照。
     *
     * @param adapterCode   来源实例编码;为空表示来源未知,直接返回空值而不猜也不回退
     * @param collectorCode 采集器编码
     * @return 采集器实时快照;来源未知时为空
     * @throws AdapterRegistryException 来源实例未启用,或实例未提供测试数据观测契约
     */
    @Override
    public CollectorRealtime latestCollector(String adapterCode, String collectorCode)
    {
        // 采集器测量属于某个来源实例的辅助测量,按该实例请求;实例缺失时明确失败。
        // 来源编码为空说明测试电池连检测柜绑定都没有,此时来源未知:不猜也不回退,报无法请求。
        if (adapterCode == null || adapterCode.isBlank()) return null;
        return registry.executeTestData(adapterCode, adapter -> adapter.latestCollector(adapterCode, collectorCode));
    }
 
    /**
     * 按通道所属来源实例请求设备直接提供的工步统计。
     *
     * @param channel 检测柜通道身份
     * @return 该通道的设备工步统计;设备未提供时为空列表
     * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
     */
    @Override
    public List<StepRecord> stepRecords(ChannelIdentity channel)
    {
        return registry.executeTestData(channel.adapterCode(), adapter -> adapter.stepRecords(channel));
    }
 
    /**
     * 按通道所属来源实例请求循环统计,来源类别由实例写在记录上。
     *
     * @param channel 检测柜通道身份
     * @return 该通道的循环统计;设备未提供且无法聚合时为空列表
     * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
     */
    @Override
    public List<CycleRecord> cycleRecords(ChannelIdentity channel)
    {
        return registry.executeTestData(channel.adapterCode(), adapter -> adapter.cycleRecords(channel));
    }
 
    /**
     * 按通道所属来源实例取该来源定义的实时持久化槽位间隔。
     *
     * @param channel 检测柜通道身份
     * @return 持久化槽位间隔,单位毫秒
     * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
     */
    @Override
    public long persistenceIntervalMillis(ChannelIdentity channel)
    {
        return registry.executeTestData(channel.adapterCode(), adapter -> adapter.persistenceIntervalMillis(channel));
    }
 
    /**
     * 按通道所属来源实例取设备计划方案,只用于实时展示。
     *
     * @param channel 检测柜通道身份
     * @return 设备计划方案;设备未提供时为空
     * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
     */
    @Override
    public TestPlan testPlan(ChannelIdentity channel)
    {
        return registry.executeTestData(channel.adapterCode(), adapter -> adapter.testPlan(channel));
    }
 
    /**
     * 按通道所属来源实例读取接收水位,用于把开始接收与继续接收切在这一时刻之后。
     *
     * @param channel 检测柜通道身份
     * @return 通道当前接收水位;来源不按设备数据行接收时为空
     * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
     */
    @Override
    public SampleWatermark currentWatermark(ChannelIdentity channel)
    {
        return registry.executeTestData(channel.adapterCode(), adapter -> adapter.currentWatermark(channel));
    }
 
    /**
     * 按通道所属来源实例读取严格晚于水位的数据行,同秒多行各自成立、不按秒合并。
     *
     * @param channel   检测柜通道身份
     * @param watermark 上次取得的水位;为空表示尚未取水位,此时返回空列表而不并入设备历史
     * @return 水位之后的设备数据行,按设备时间与协议序号升序
     * @throws AdapterRegistryException 该通道所属的实例未启用,或实例未提供测试数据观测契约
     */
    @Override
    public List<CabinetRealtime> samplesSince(ChannelIdentity channel, SampleWatermark watermark)
    {
        return registry.executeTestData(channel.adapterCode(), adapter -> adapter.samplesSince(channel, watermark));
    }
}