From 7259e265b9237286996b09da03038ea5a133e1e0 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 05 九月 2026 17:45:07 +0800
Subject: [PATCH] 收敛模拟器为设备事实与实时观测

---
 battery-device-simulator/src/main/java/com/battery/device/simulator/DevelopmentCollectorSimulation.java |  374 ++++++----------------------------------------------
 1 files changed, 46 insertions(+), 328 deletions(-)

diff --git a/battery-device-simulator/src/main/java/com/battery/device/simulator/DevelopmentCollectorSimulation.java b/battery-device-simulator/src/main/java/com/battery/device/simulator/DevelopmentCollectorSimulation.java
index 0dc231b..b0975f8 100644
--- a/battery-device-simulator/src/main/java/com/battery/device/simulator/DevelopmentCollectorSimulation.java
+++ b/battery-device-simulator/src/main/java/com/battery/device/simulator/DevelopmentCollectorSimulation.java
@@ -2,34 +2,19 @@
 
 import java.math.BigDecimal;
 import java.time.Instant;
-import java.time.LocalDateTime;
 import java.time.ZoneId;
 import java.time.format.DateTimeFormatter;
-import java.time.format.DateTimeParseException;
-import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorChannelState;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorMetricState;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement;
-import com.battery.device.simulator.BtsSimulatorStateStore.CollectorState;
+import com.battery.device.api.model.DeviceWorkStatus;
+import com.battery.device.simulator.BtsSimulatorStateStore.*;
 
-/** 实现第一版开发采集器的独立采样与故障注入合同。 */
+/** 提供请求时的采集器当前值;S 通道仍是待现场验证的开发合同。 */
 public final class DevelopmentCollectorSimulation
 {
     public static final int DEFAULT_SAMPLE_INTERVAL_MS = 1000;
-    public static final int MIN_SAMPLE_INTERVAL_MS = 100;
-    public static final int MAX_SAMPLE_INTERVAL_MS = 60000;
-    public static final int MAX_HISTORY_PAGE_SIZE = BtsSimulatorStateStore.MAX_HISTORY_BATCH_SIZE;
-    private static final int MAX_SAMPLES_PER_ADVANCE = 1000;
     private static final ZoneId DEVICE_ZONE = ZoneId.of("Asia/Hong_Kong");
-    private static final DateTimeFormatter DEVICE_TIME_FORMATTER =
-            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
+    private static final DateTimeFormatter DEVICE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS");
 
     private DevelopmentCollectorSimulation() { }
 
@@ -48,320 +33,72 @@
         return List.copyOf(channels);
     }
 
-    /** 调整通道数时保留已有通道配置,并为新增通道生成稳定默认配置。 */
-    public static CollectorState resize(CollectorState current, String ip, int channelCount, boolean online,
-            Instant changedAt)
+
+    /** 调整设备拓扑,保留已有通道的配置。 */
+    public static CollectorState resize(CollectorState current, String ip, int channelCount, boolean online, Instant changedAt)
     {
-        List<CollectorChannelState> defaults = defaultChannels(channelCount);
-        List<CollectorChannelState> channels = new ArrayList<>(channelCount);
+        var defaults = defaultChannels(channelCount);
+        List<CollectorChannelState> channels = new ArrayList<>();
         for (int index = 0; index < channelCount; index++)
             channels.add(index < current.channels().size() ? current.channels().get(index) : defaults.get(index));
-        Instant lastSampledAt = online == current.online() ? current.lastSampledAt() : changedAt;
-        List<CollectorRealtimeMeasurement> latestMeasurements = retainConfiguredMeasurements(
-                current.latestMeasurements(), channels);
-        return copy(current, ip, channelCount, online, current.sampleIntervalMs(), current.nextSequence(),
-                lastSampledAt, channels, latestMeasurements, current.historyRecordCount(), current.historyFaults());
+        return new CollectorState(current.collectorId(), current.deviceCode(), ip.trim(), current.mac(),
+                current.name(), current.manufacturer(), current.model(), channelCount, online,
+                current.sampleIntervalMs(), channels, List.of());
     }
 
-    /** 更新采样频率和历史下载故障;测量原始历史保持只追加。 */
-    public static CollectorState configureSampling(CollectorState current, int sampleIntervalMs,
-            CollectorHistoryFaults historyFaults)
-    {
-        validateSampleInterval(sampleIntervalMs);
-        validateFaults(historyFaults);
-        return copy(current, current.ip(), current.channelCount(), current.online(), sampleIntervalMs,
-                current.nextSequence(), current.lastSampledAt(), current.channels(), current.latestMeasurements(),
-                current.historyRecordCount(), historyFaults);
-    }
-
-    /** 替换一个演示通道的指标配置;动态添加项与固定指标共用指标编码合同。 */
-    public static CollectorState configureChannel(CollectorState current, String channelCode,
-            List<CollectorMetricState> metrics)
+    /** 保存当前测量配置,等待下一次实时请求。 */
+    public static CollectorState configureChannel(CollectorState current, String channelCode, List<CollectorMetricState> metrics)
     {
         validateMetrics(metrics);
-        int channelIndex = parseChannelIndex(channelCode, current.channelCount());
+        int index = parseChannelIndex(channelCode, current.channelCount());
         List<CollectorChannelState> channels = new ArrayList<>(current.channels());
-        channels.set(channelIndex, new CollectorChannelState(channelCode(channelIndex + 1), metrics));
-        List<CollectorRealtimeMeasurement> latestMeasurements = retainConfiguredMeasurements(
-                current.latestMeasurements(), channels);
-        return copy(current, current.ip(), current.channelCount(), current.online(), current.sampleIntervalMs(),
-                current.nextSequence(), current.lastSampledAt(), channels, latestMeasurements,
-                current.historyRecordCount(), current.historyFaults());
+        channels.set(index, new CollectorChannelState(channelCode, metrics, channels.get(index).workStatus()));
+        return copy(current, channels, List.of());
     }
 
-    /** 按采集器自身频率推进采样;离线期间不生成记录,恢复后从下一序号继续。 */
-    public static CollectorAdvance advance(CollectorState current, Instant now)
+    /** 保存通道工作状态,等待下一次实时请求。 */
+    public static CollectorState configureStatus(CollectorState current, String channelCode, DeviceWorkStatus status)
     {
-        if (!current.online()) return new CollectorAdvance(current, List.of());
-        Instant sampleAt = current.lastSampledAt() == null
-                ? now
-                : current.lastSampledAt().plusMillis(current.sampleIntervalMs());
-        if (sampleAt.isAfter(now)) return new CollectorAdvance(current, List.of());
+        int index = parseChannelIndex(channelCode, current.channelCount());
+        List<CollectorChannelState> channels = new ArrayList<>(current.channels());
+        channels.set(index, new CollectorChannelState(channelCode, channels.get(index).metrics(), status));
+        return copy(current, channels, List.of());
+    }
 
-        List<CollectorMeasurementRecord> appendedRecords = new ArrayList<>();
-        Map<String, CollectorRealtimeMeasurement> latest = new LinkedHashMap<>();
-        for (CollectorRealtimeMeasurement measurement : current.latestMeasurements())
-            latest.put(measurementKey(measurement.channelCode(), measurement.metricCode()), measurement);
-        long sequence = current.nextSequence();
-        int generated = 0;
-        Instant lastSampledAt = current.lastSampledAt();
-        while (!sampleAt.isAfter(now) && generated < MAX_SAMPLES_PER_ADVANCE)
+    /** 仅生成本次请求的当前值,不补算上一次请求与本次之间的空档。 */
+    public static CollectorState sample(CollectorState current, Instant now, long sequence)
+    {
+        Instant observedAt = now.truncatedTo(java.time.temporal.ChronoUnit.MILLIS);
+        List<CollectorRealtimeMeasurement> measurements = new ArrayList<>();
+        for (CollectorChannelState channel : current.channels())
         {
-            for (CollectorChannelState channel : current.channels())
-            {
-                for (CollectorMetricState metric : channel.metrics())
-                {
-                    CollectorMeasurementRecord record = new CollectorMeasurementRecord("dev-sim", current.deviceCode(),
-                            channel.channelCode(), sequence, sampleAt,
-                            DEVICE_TIME_FORMATTER.format(sampleAt.atZone(DEVICE_ZONE)), now,
-                            metric.metricCode(), metric.value(), metric.unit());
-                    appendedRecords.add(record);
-                    latest.put(measurementKey(record.channelCode(), record.metricCode()),
-                            new CollectorRealtimeMeasurement(record.adapterCode(), record.collectorCode(),
-                                    record.channelCode(), record.sequenceId(), record.deviceTime(),
-                                    record.deviceTimeRaw(), record.serverReceivedAt(), record.metricCode(),
-                                    record.value(), record.unit()));
-                }
-            }
-            lastSampledAt = sampleAt;
-            sequence++;
-            generated++;
-            sampleAt = sampleAt.plusMillis(current.sampleIntervalMs());
+            if (channel.workStatus() == DeviceWorkStatus.UNOBSERVABLE) continue;
+            for (CollectorMetricState metric : channel.metrics())
+                measurements.add(new CollectorRealtimeMeasurement("dev-sim", current.deviceCode(), channel.channelCode(),
+                        sequence, observedAt, DEVICE_TIME_FORMATTER.format(observedAt.atZone(DEVICE_ZONE)), observedAt,
+                        metric.metricCode(), metric.value(), metric.unit()));
         }
-        CollectorState next = copy(current, current.ip(), current.channelCount(), current.online(),
-                current.sampleIntervalMs(),
-                sequence, lastSampledAt, current.channels(), List.copyOf(latest.values()),
-                Math.min(BtsSimulatorStateStore.MAX_HISTORY_RECORDS,
-                        current.historyRecordCount() + appendedRecords.size()),
-                current.historyFaults());
-        return new CollectorAdvance(next, appendedRecords);
+        return copy(current, current.channels(), measurements);
     }
 
-    /** 返回独立保存的最新实时快照,不从原始历史反推当前状态。 */
-    public static CollectorRealtimeSnapshot realtime(CollectorState current)
+    /** 校验恢复后的稳定通道和测量配置。 */
+    public static void validate(CollectorState state)
     {
-        return new CollectorRealtimeSnapshot(current.deviceCode(), current.online(), current.sampleIntervalMs(),
-                current.latestMeasurements());
-    }
-
-    /** 对只追加原始模拟历史应用可重复的下载故障视图,再按偏移量分页。 */
-    public static CollectorHistoryPage history(CollectorState current, BtsSimulatorStateStore stateStore,
-            long offset, int limit)
-    {
-        if (offset < 0) throw new IllegalArgumentException("offset must not be negative");
-        if (limit < 1 || limit > MAX_HISTORY_PAGE_SIZE)
-            throw new IllegalArgumentException("limit must be between 1 and " + MAX_HISTORY_PAGE_SIZE);
-        CollectorHistoryFaults faults = current.historyFaults();
-        if (faults.missingSequences().isEmpty() && faults.duplicateSequence() == null
-                && faults.conflictingSequence() == null && !faults.outOfOrder())
-        {
-            BtsSimulatorStateStore.CollectorHistoryBatch batch = stateStore.loadCollectorHistoryPage(
-                    current.deviceCode(), offset, limit, false);
-            return new CollectorHistoryPage(current.deviceCode(), offset, batch.nextOffset(), batch.hasMore(),
-                    batch.total(), batch.records());
-        }
-        return faultedHistoryPage(current, stateStore, offset, limit);
-    }
-
-    /** 校验 Redis 恢复的采集器采样状态,拒绝损坏状态被静默替换。 */
-    public static void validate(CollectorState state, BtsSimulatorStateStore stateStore)
-    {
-        validateSampleInterval(state.sampleIntervalMs());
-        if (state.nextSequence() < 1 || state.channels().size() != state.channelCount())
-            throw new IllegalStateException("Invalid collector sampling state in Redis");
+        if (state.channels().size() != state.channelCount()) throw new IllegalStateException("Invalid collector topology");
         for (int index = 0; index < state.channels().size(); index++)
         {
-            CollectorChannelState channel = state.channels().get(index);
-            if (!channelCode(index + 1).equals(channel.channelCode()))
-                throw new IllegalStateException("Invalid collector channel identity in Redis");
+            var channel = state.channels().get(index);
+            if (!channelCode(index + 1).equals(channel.channelCode())) throw new IllegalStateException("Invalid collector channel identity");
             validateMetrics(channel.metrics());
         }
-        validateLatestMeasurements(state);
-        validateHistory(state, stateStore);
-        validateFaults(state.historyFaults());
     }
 
-    private static void validateLatestMeasurements(CollectorState state)
+    private static CollectorState copy(CollectorState current, List<CollectorChannelState> channels,
+            List<CollectorRealtimeMeasurement> measurements)
     {
-        Set<String> configured = configuredMeasurementKeys(state.channels());
-        Set<String> identities = new java.util.HashSet<>();
-        for (CollectorRealtimeMeasurement measurement : state.latestMeasurements())
-        {
-            String key = measurementKey(measurement.channelCode(), measurement.metricCode());
-            if (!validMeasurement(state, measurement.adapterCode(), measurement.collectorCode(),
-                    measurement.channelCode(), measurement.sequenceId(), measurement.deviceTime(),
-                    measurement.deviceTimeRaw(), measurement.serverReceivedAt(), measurement.metricCode(),
-                    measurement.value(), measurement.unit()) || !configured.contains(key) || !identities.add(key)
-                    || measurement.sequenceId() >= state.nextSequence())
-                throw new IllegalStateException("Invalid collector realtime state in Redis");
-        }
-    }
-
-    private static void validateHistory(CollectorState state, BtsSimulatorStateStore stateStore)
-    {
-        long maximumSequence = 0;
-        long previousSequence = 0;
-        long recordCount = 0;
-        long offset = 0;
-        Set<String> identities = new java.util.HashSet<>();
-        while (true)
-        {
-            BtsSimulatorStateStore.CollectorHistoryBatch batch = stateStore.loadCollectorHistoryPage(
-                    state.deviceCode(), offset, MAX_HISTORY_PAGE_SIZE, false);
-            if (batch.offset() != offset || batch.records().size() > MAX_HISTORY_PAGE_SIZE)
-                throw new IllegalStateException("Invalid collector history page in Redis");
-            for (CollectorMeasurementRecord record : batch.records())
-            {
-                if (record.sequenceId() < previousSequence)
-                    throw new IllegalStateException("Invalid collector history state in Redis");
-                if (record.sequenceId() != previousSequence)
-                {
-                    identities.clear();
-                    previousSequence = record.sequenceId();
-                }
-                String identity = record.channelCode() + "\u0000" + record.metricCode();
-                if (!validMeasurement(state, record.adapterCode(), record.collectorCode(), record.channelCode(),
-                        record.sequenceId(), record.deviceTime(), record.deviceTimeRaw(), record.serverReceivedAt(),
-                        record.metricCode(), record.value(), record.unit()) || !identities.add(identity))
-                    throw new IllegalStateException("Invalid collector history state in Redis");
-                maximumSequence = Math.max(maximumSequence, record.sequenceId());
-                recordCount++;
-            }
-            if (!batch.hasMore())
-            {
-                if (batch.total() != recordCount)
-                    throw new IllegalStateException("Invalid collector history count in Redis");
-                break;
-            }
-            if (batch.nextOffset() <= offset)
-                throw new IllegalStateException("Invalid collector history page in Redis");
-            offset = batch.nextOffset();
-        }
-        if (state.nextSequence() <= maximumSequence)
-            throw new IllegalStateException("Invalid collector history sequence in Redis");
-        if (state.historyRecordCount() != recordCount)
-            throw new IllegalStateException("Invalid collector history count in Redis");
-    }
-
-    private static boolean validMeasurement(CollectorState state, String adapterCode, String collectorCode,
-            String channelCode, long sequenceId, Instant deviceTime, String deviceTimeRaw,
-            Instant serverReceivedAt, String metricCode, BigDecimal value, String unit)
-    {
-        return "dev-sim".equals(adapterCode) && state.deviceCode().equals(collectorCode)
-                && validHistoricalChannelCode(channelCode)
-                && sequenceId > 0 && deviceTime != null && deviceTimeRaw != null && !deviceTimeRaw.isBlank()
-                && validDeviceTime(deviceTime, deviceTimeRaw) && serverReceivedAt != null
-                && !serverReceivedAt.isBefore(deviceTime) && metricCode != null && !metricCode.isBlank()
-                && metricCode.equals(metricCode.trim()) && value != null && unit != null && !unit.isBlank()
-                && unit.equals(unit.trim());
-    }
-
-    private static boolean validHistoricalChannelCode(String channelCode)
-    {
-        if (channelCode == null || !channelCode.matches("S-\\d{2}")) return false;
-        int index = Integer.parseInt(channelCode.substring(2));
-        return index >= 1 && channelCode(index).equals(channelCode);
-    }
-
-    private static boolean validDeviceTime(Instant deviceTime, String deviceTimeRaw)
-    {
-        try
-        {
-            Instant parsed = LocalDateTime.parse(deviceTimeRaw, DEVICE_TIME_FORMATTER)
-                    .atZone(DEVICE_ZONE).toInstant();
-            return parsed.equals(deviceTime.truncatedTo(ChronoUnit.MILLIS));
-        }
-        catch (DateTimeParseException exception)
-        {
-            return false;
-        }
-    }
-
-    private static List<CollectorRealtimeMeasurement> retainConfiguredMeasurements(
-            List<CollectorRealtimeMeasurement> measurements, List<CollectorChannelState> channels)
-    {
-        Set<String> configured = configuredMeasurementKeys(channels);
-        return measurements.stream()
-                .filter(item -> configured.contains(measurementKey(item.channelCode(), item.metricCode())))
-                .toList();
-    }
-
-    private static Set<String> configuredMeasurementKeys(List<CollectorChannelState> channels)
-    {
-        Set<String> configured = new java.util.HashSet<>();
-        for (CollectorChannelState channel : channels)
-            for (CollectorMetricState metric : channel.metrics())
-                configured.add(measurementKey(channel.channelCode(), metric.metricCode()));
-        return configured;
-    }
-
-    private static String measurementKey(String channelCode, String metricCode)
-    {
-        return channelCode + "\u0000" + metricCode;
-    }
-
-    private static CollectorHistoryPage faultedHistoryPage(CollectorState current,
-            BtsSimulatorStateStore stateStore, long offset, int limit)
-    {
-        // 故障视图需返回精确 total,因此逐批扫描原始历史;正常下载会直接读取目标页。
-        CollectorHistoryFaults faults = current.historyFaults();
-        Set<Long> missing = Set.copyOf(faults.missingSequences());
-        List<CollectorMeasurementRecord> page = new ArrayList<>(limit);
-        long rawOffset = 0;
-        long total = 0;
-        while (true)
-        {
-            BtsSimulatorStateStore.CollectorHistoryBatch batch = stateStore.loadCollectorHistoryPage(
-                    current.deviceCode(), rawOffset, MAX_HISTORY_PAGE_SIZE, faults.outOfOrder());
-            for (CollectorMeasurementRecord record : batch.records())
-            {
-                if (missing.contains(record.sequenceId())) continue;
-                total = appendFaultedRecord(page, record, total, offset, limit);
-                if (Long.valueOf(record.sequenceId()).equals(faults.duplicateSequence()))
-                    total = appendFaultedRecord(page, record, total, offset, limit);
-                if (Long.valueOf(record.sequenceId()).equals(faults.conflictingSequence()))
-                    total = appendFaultedRecord(page, conflictingRecord(record), total, offset, limit);
-            }
-            if (!batch.hasMore()) break;
-            if (batch.nextOffset() <= rawOffset)
-                throw new IllegalStateException("Invalid collector history page in Redis");
-            rawOffset = batch.nextOffset();
-        }
-        long from = Math.min(offset, total);
-        long nextOffset = from + page.size();
-        return new CollectorHistoryPage(current.deviceCode(), offset, nextOffset, nextOffset < total, total, page);
-    }
-
-    private static long appendFaultedRecord(List<CollectorMeasurementRecord> page,
-            CollectorMeasurementRecord record, long index, long offset, int limit)
-    {
-        if (index >= offset && page.size() < limit) page.add(record);
-        return index + 1;
-    }
-
-    private static CollectorMeasurementRecord conflictingRecord(CollectorMeasurementRecord record)
-    {
-        return new CollectorMeasurementRecord(record.adapterCode(), record.collectorCode(), record.channelCode(),
-                record.sequenceId(), record.deviceTime(), record.deviceTimeRaw(), record.serverReceivedAt(),
-                record.metricCode(), record.value().add(new BigDecimal("0.001")), record.unit());
-    }
-
-    private static CollectorState copy(CollectorState current, String ip, int channelCount, boolean online,
-            int sampleIntervalMs, long nextSequence, Instant lastSampledAt,
-            List<CollectorChannelState> channels, List<CollectorRealtimeMeasurement> latestMeasurements,
-            long historyRecordCount,
-            CollectorHistoryFaults faults)
-    {
-        return new CollectorState(current.collectorId(), current.deviceCode(), ip.trim(), current.mac(),
-                current.name(), current.manufacturer(), current.model(), channelCount, online, sampleIntervalMs,
-                nextSequence, lastSampledAt, channels, latestMeasurements, historyRecordCount, faults);
-    }
-
-    private static void validateSampleInterval(int sampleIntervalMs)
-    {
-        if (sampleIntervalMs < MIN_SAMPLE_INTERVAL_MS || sampleIntervalMs > MAX_SAMPLE_INTERVAL_MS)
-            throw new IllegalArgumentException("sampleIntervalMs must be between " + MIN_SAMPLE_INTERVAL_MS
-                    + " and " + MAX_SAMPLE_INTERVAL_MS);
+        return new CollectorState(current.collectorId(), current.deviceCode(), current.ip(), current.mac(),
+                current.name(), current.manufacturer(), current.model(), current.channelCount(), current.online(),
+                current.sampleIntervalMs(), channels, measurements);
     }
 
     private static void validateMetrics(List<CollectorMetricState> metrics)
@@ -377,15 +114,6 @@
                     || !metric.unit().equals(metric.unit().trim()) || !codes.add(metric.metricCode()))
                 throw new IllegalArgumentException("Collector metric code, value and unit must be valid and unique");
         }
-    }
-
-    private static void validateFaults(CollectorHistoryFaults faults)
-    {
-        if (faults == null) throw new IllegalArgumentException("historyFaults must not be null");
-        if (faults.missingSequences().stream().anyMatch(sequence -> sequence == null || sequence < 1)
-                || faults.duplicateSequence() != null && faults.duplicateSequence() < 1
-                || faults.conflictingSequence() != null && faults.conflictingSequence() < 1)
-            throw new IllegalArgumentException("History fault sequences must be positive");
     }
 
     private static int parseChannelIndex(String value, int channelCount)
@@ -409,14 +137,4 @@
         public CollectorRealtimeSnapshot { measurements = List.copyOf(measurements); }
     }
 
-    public record CollectorAdvance(CollectorState state, List<CollectorMeasurementRecord> appendedRecords)
-    {
-        public CollectorAdvance { appendedRecords = List.copyOf(appendedRecords); }
-    }
-
-    public record CollectorHistoryPage(String collectorCode, long offset, long nextOffset, boolean hasMore,
-            long total, List<CollectorMeasurementRecord> records)
-    {
-        public CollectorHistoryPage { records = List.copyOf(records); }
-    }
 }

--
Gitblit v1.7.1