修正采集器历史与实时快照模型
保持模拟原始历史只追加并加强 Redis 恢复校验。分离实时快照状态,修复配置收缩残留和控制页编辑刷新竞态。
| | |
| | | requiredArray(object, "channels").forEach(item -> channels.add(collectorChannelState(item))); |
| | | List<CollectorMeasurementRecord> history = new ArrayList<>(); |
| | | requiredArray(object, "history").forEach(item -> history.add(collectorMeasurementRecord(item))); |
| | | List<CollectorRealtimeMeasurement> latestMeasurements = new ArrayList<>(); |
| | | JSONArray latest = object.getJSONArray("latestMeasurements"); |
| | | if (latest != null) |
| | | latest.forEach(item -> latestMeasurements.add(collectorRealtimeMeasurement(item))); |
| | | else |
| | | latestMeasurements.addAll(latestMeasurementsFromLegacyHistory(history, channels)); |
| | | return new CollectorState(collectorId, deviceCode, ip, mac, name, manufacturer, model, |
| | | channelCount, online, requiredInteger(object, "sampleIntervalMs"), |
| | | requiredLong(object, "nextSequence"), optionalInstant(object, "lastSampledAt"), |
| | | channels, history, collectorHistoryFaults(object.get("historyFaults"))); |
| | | channels, latestMeasurements, history, collectorHistoryFaults(object.get("historyFaults"))); |
| | | } |
| | | |
| | | private CollectorChannelState collectorChannelState(Object value) |
| | |
| | | measurement, object.getString("unit")); |
| | | } |
| | | |
| | | private CollectorRealtimeMeasurement collectorRealtimeMeasurement(Object value) |
| | | { |
| | | JSONObject object = jsonObject(value); |
| | | BigDecimal measurement = object.getBigDecimal("value"); |
| | | if (measurement == null) throw new IllegalStateException("Missing BTS simulator state field: value"); |
| | | return new CollectorRealtimeMeasurement(object.getString("adapterCode"), object.getString("collectorCode"), |
| | | object.getString("channelCode"), requiredLong(object, "sequenceId"), |
| | | requiredInstant(object, "deviceTime"), object.getString("deviceTimeRaw"), |
| | | requiredInstant(object, "serverReceivedAt"), object.getString("metricCode"), |
| | | measurement, object.getString("unit")); |
| | | } |
| | | |
| | | /** 兼容 Ticket06 开发期间已写入但尚未拆分快照模型的状态。 */ |
| | | private List<CollectorRealtimeMeasurement> latestMeasurementsFromLegacyHistory( |
| | | List<CollectorMeasurementRecord> history, List<CollectorChannelState> channels) |
| | | { |
| | | java.util.Set<String> configured = new java.util.HashSet<>(); |
| | | channels.forEach(channel -> channel.metrics().forEach(metric -> |
| | | configured.add(channel.channelCode() + "\u0000" + metric.metricCode()))); |
| | | java.util.Map<String, CollectorRealtimeMeasurement> latest = new java.util.LinkedHashMap<>(); |
| | | for (CollectorMeasurementRecord record : history) |
| | | { |
| | | String key = record.channelCode() + "\u0000" + record.metricCode(); |
| | | if (configured.contains(key)) |
| | | latest.put(key, new CollectorRealtimeMeasurement(record.adapterCode(), record.collectorCode(), |
| | | record.channelCode(), record.sequenceId(), record.deviceTime(), record.deviceTimeRaw(), |
| | | record.serverReceivedAt(), record.metricCode(), record.value(), record.unit())); |
| | | } |
| | | return List.copyOf(latest.values()); |
| | | } |
| | | |
| | | private CollectorHistoryFaults collectorHistoryFaults(Object value) |
| | | { |
| | | if (value == null) return CollectorHistoryFaults.none(); |
| | |
| | | } |
| | | |
| | | async function refresh() { |
| | | if (document.activeElement && ['INPUT', 'SELECT'].includes(document.activeElement.tagName)) return; |
| | | if (document.activeElement && ['INPUT', 'SELECT', 'TEXTAREA'].includes(document.activeElement.tagName)) return; |
| | | try { render(await request('/state')); } catch (error) { showError(error); } |
| | | } |
| | | |
| | |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMetricState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.DeviceState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.RecentRequest; |
| | |
| | | public record CollectorSnapshot(int collectorId, String deviceCode, String ip, String mac, String name, |
| | | String manufacturer, String model, int channelCount, boolean online, int sampleIntervalMs, |
| | | List<CollectorChannelState> channels, CollectorHistoryFaults historyFaults, int historyRecordCount, |
| | | List<CollectorMeasurementRecord> latestMeasurements) |
| | | List<CollectorRealtimeMeasurement> latestMeasurements) |
| | | { |
| | | public CollectorSnapshot |
| | | { |
| | |
| | | record CollectorState(int collectorId, String deviceCode, String ip, String mac, String name, |
| | | String manufacturer, String model, int channelCount, boolean online, int sampleIntervalMs, |
| | | long nextSequence, Instant lastSampledAt, List<CollectorChannelState> channels, |
| | | List<CollectorRealtimeMeasurement> latestMeasurements, |
| | | List<CollectorMeasurementRecord> history, CollectorHistoryFaults historyFaults) |
| | | { |
| | | public CollectorState |
| | | { |
| | | channels = List.copyOf(channels); |
| | | latestMeasurements = List.copyOf(latestMeasurements); |
| | | history = List.copyOf(history); |
| | | historyFaults = historyFaults == null ? CollectorHistoryFaults.none() : historyFaults; |
| | | } |
| | |
| | | { |
| | | this(collectorId, deviceCode, ip, mac, name, manufacturer, model, channelCount, online, |
| | | DevelopmentCollectorSimulation.DEFAULT_SAMPLE_INTERVAL_MS, 1L, null, |
| | | DevelopmentCollectorSimulation.defaultChannels(channelCount), List.of(), |
| | | DevelopmentCollectorSimulation.defaultChannels(channelCount), List.of(), List.of(), |
| | | CollectorHistoryFaults.none()); |
| | | } |
| | | } |
| | |
| | | |
| | | record CollectorMetricState(String metricCode, BigDecimal value, String unit) { } |
| | | |
| | | record CollectorRealtimeMeasurement(String adapterCode, String collectorCode, String channelCode, |
| | | long sequenceId, Instant deviceTime, String deviceTimeRaw, Instant serverReceivedAt, |
| | | String metricCode, BigDecimal value, String unit) { } |
| | | |
| | | record CollectorMeasurementRecord(String adapterCode, String collectorCode, String channelCode, |
| | | long sequenceId, Instant deviceTime, String deviceTimeRaw, Instant serverReceivedAt, |
| | | String metricCode, BigDecimal value, String unit) { } |
| | |
| | | package com.ruoyi.device.simulator; |
| | | |
| | | import java.math.BigDecimal; |
| | | import java.time.Duration; |
| | | 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.Comparator; |
| | | import java.util.LinkedHashMap; |
| | |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMetricState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState; |
| | | |
| | | /** 实现第一版开发采集器的独立采样与故障注入合同。 */ |
| | |
| | | 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 = 1000; |
| | | private static final int MAX_HISTORY_RECORDS = 50000; |
| | | 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 = |
| | |
| | | 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, current.history(), current.historyFaults()); |
| | | lastSampledAt, channels, latestMeasurements, current.history(), current.historyFaults()); |
| | | } |
| | | |
| | | /** 更新采样频率和历史下载故障;测量原始历史保持只追加。 */ |
| | |
| | | validateSampleInterval(sampleIntervalMs); |
| | | validateFaults(historyFaults); |
| | | return copy(current, current.ip(), current.channelCount(), current.online(), sampleIntervalMs, |
| | | current.nextSequence(), current.lastSampledAt(), current.channels(), current.history(), historyFaults); |
| | | current.nextSequence(), current.lastSampledAt(), current.channels(), current.latestMeasurements(), |
| | | current.history(), historyFaults); |
| | | } |
| | | |
| | | /** 替换一个演示通道的指标配置;动态添加项与固定指标共用指标编码合同。 */ |
| | |
| | | int channelIndex = 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, current.history(), current.historyFaults()); |
| | | current.nextSequence(), current.lastSampledAt(), channels, latestMeasurements, |
| | | current.history(), current.historyFaults()); |
| | | } |
| | | |
| | | /** 按采集器自身频率推进采样;离线期间不生成记录,恢复后从下一序号继续。 */ |
| | |
| | | if (sampleAt.isAfter(now)) return current; |
| | | |
| | | List<CollectorMeasurementRecord> history = new ArrayList<>(current.history()); |
| | | 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(); |
| | |
| | | { |
| | | for (CollectorMetricState metric : channel.metrics()) |
| | | { |
| | | history.add(new CollectorMeasurementRecord("dev-sim", current.deviceCode(), |
| | | 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())); |
| | | metric.metricCode(), metric.value(), metric.unit()); |
| | | history.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; |
| | |
| | | generated++; |
| | | sampleAt = sampleAt.plusMillis(current.sampleIntervalMs()); |
| | | } |
| | | if (history.size() > MAX_HISTORY_RECORDS) |
| | | history = new ArrayList<>(history.subList(history.size() - MAX_HISTORY_RECORDS, history.size())); |
| | | return copy(current, current.ip(), current.channelCount(), current.online(), current.sampleIntervalMs(), |
| | | sequence, lastSampledAt, current.channels(), history, current.historyFaults()); |
| | | sequence, lastSampledAt, current.channels(), List.copyOf(latest.values()), history, |
| | | current.historyFaults()); |
| | | } |
| | | |
| | | /** 返回每个通道和指标的最新实时快照,不将快照写入另一条历史路径。 */ |
| | | /** 返回独立保存的最新实时快照,不从原始历史反推当前状态。 */ |
| | | public static CollectorRealtimeSnapshot realtime(CollectorState current) |
| | | { |
| | | Map<String, CollectorMeasurementRecord> latest = new LinkedHashMap<>(); |
| | | for (CollectorMeasurementRecord record : current.history()) |
| | | latest.put(record.channelCode() + "\u0000" + record.metricCode(), record); |
| | | return new CollectorRealtimeSnapshot(current.deviceCode(), current.online(), current.sampleIntervalMs(), |
| | | List.copyOf(latest.values())); |
| | | current.latestMeasurements()); |
| | | } |
| | | |
| | | /** 对只追加原始模拟历史应用可重复的下载故障视图,再按偏移量分页。 */ |
| | |
| | | throw new IllegalStateException("Invalid collector channel identity in Redis"); |
| | | validateMetrics(channel.metrics()); |
| | | } |
| | | validateLatestMeasurements(state); |
| | | validateHistory(state); |
| | | validateFaults(state.historyFaults()); |
| | | } |
| | | |
| | | private static void validateLatestMeasurements(CollectorState state) |
| | | { |
| | | 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) |
| | | { |
| | | long maximumSequence = 0; |
| | | long previousSequence = 0; |
| | | Set<String> identities = new java.util.HashSet<>(); |
| | | for (CollectorMeasurementRecord record : state.history()) |
| | | { |
| | | String identity = record.sequenceId() + "\u0000" + 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) |
| | | || record.sequenceId() < previousSequence) |
| | | throw new IllegalStateException("Invalid collector history state in Redis"); |
| | | maximumSequence = Math.max(maximumSequence, record.sequenceId()); |
| | | previousSequence = record.sequenceId(); |
| | | } |
| | | if (state.nextSequence() <= maximumSequence) |
| | | throw new IllegalStateException("Invalid collector history sequence 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 List<CollectorMeasurementRecord> faultedHistory(CollectorState current) |
| | |
| | | |
| | | private static CollectorState copy(CollectorState current, String ip, int channelCount, boolean online, |
| | | int sampleIntervalMs, long nextSequence, Instant lastSampledAt, |
| | | List<CollectorChannelState> channels, List<CollectorMeasurementRecord> history, |
| | | List<CollectorChannelState> channels, List<CollectorRealtimeMeasurement> latestMeasurements, |
| | | List<CollectorMeasurementRecord> history, |
| | | 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, history, faults); |
| | | nextSequence, lastSampledAt, channels, latestMeasurements, history, faults); |
| | | } |
| | | |
| | | private static void validateSampleInterval(int sampleIntervalMs) |
| | |
| | | } |
| | | |
| | | public record CollectorRealtimeSnapshot(String collectorCode, boolean online, int sampleIntervalMs, |
| | | List<CollectorMeasurementRecord> measurements) |
| | | List<CollectorRealtimeMeasurement> measurements) |
| | | { |
| | | public CollectorRealtimeSnapshot { measurements = List.copyOf(measurements); } |
| | | } |
| | |
| | | import com.ruoyi.device.model.DeviceWorkStatus; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.ChannelState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorChannelState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorHistoryFaults; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMeasurementRecord; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorMetricState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.DeviceState; |
| | | import com.ruoyi.device.simulator.BtsSimulatorStateStore.SimulatorState; |
| | | import com.ruoyi.device.simulator.InMemoryBtsSimulatorStateStore; |
| | |
| | | assertEquals(List.of("S-01", "S-02", "S-03", "S-04"), |
| | | restored.snapshot().collectors().get(0).channels().stream() |
| | | .map(channel -> channel.channelCode()).toList()); |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | void collectorKeepsAppendOnlyHistoryBeyondPreviousDisplayLimit() throws Exception |
| | | { |
| | | Instant startedAt = Instant.parse("2026-08-29T03:00:00Z"); |
| | | try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0)) |
| | | { |
| | | simulator.configureCollectorSampling(1, 100, CollectorHistoryFaults.none()); |
| | | simulator.advanceCollectors(startedAt); |
| | | for (int batch = 1; batch <= 5; batch++) |
| | | simulator.advanceCollectors(startedAt.plusMillis(batch * 100_000L)); |
| | | |
| | | assertEquals(60_012, simulator.collectorHistory(1, 0, 1).total()); |
| | | assertEquals(1, simulator.collectorHistory(1, 0, 1).records().get(0).sequenceId()); |
| | | } |
| | | } |
| | | |
| | | @Test |
| | | void collectorRealtimeDropsMeasurementsRemovedFromCurrentConfiguration() throws Exception |
| | | { |
| | | Instant startedAt = Instant.parse("2026-08-29T04:00:00Z"); |
| | | InMemoryBtsSimulatorStateStore store = new InMemoryBtsSimulatorStateStore(); |
| | | try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0, store)) |
| | | { |
| | | simulator.advanceCollectors(startedAt); |
| | | assertEquals(12, simulator.snapshot().collectors().get(0).latestMeasurements().size()); |
| | | |
| | | simulator.configureCollectorChannel(1, "S-01", List.of( |
| | | new CollectorMetricState("temperature", new BigDecimal("27.0"), "℃"))); |
| | | simulator.updateCollector(1, "127.0.1.2", 2, true); |
| | | |
| | | var snapshot = simulator.snapshot().collectors().get(0); |
| | | assertEquals(4, snapshot.latestMeasurements().size()); |
| | | assertTrue(snapshot.latestMeasurements().stream() |
| | | .noneMatch(item -> item.channelCode().equals("S-03") |
| | | || item.channelCode().equals("S-04") |
| | | || item.channelCode().equals("S-01") |
| | | && item.metricCode().equals("positive_potential"))); |
| | | assertEquals(12, simulator.collectorHistory(1, 0, 100).total()); |
| | | } |
| | | |
| | | try (BtsDevelopmentSimulator restored = new BtsDevelopmentSimulator(0, store)) |
| | | { |
| | | assertEquals(4, restored.snapshot().collectors().get(0).latestMeasurements().size()); |
| | | assertEquals(12, restored.collectorHistory(1, 0, 100).total()); |
| | | } |
| | | } |
| | | |
| | |
| | | assertThrows(IllegalStateException.class, |
| | | () -> new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore(damaged))); |
| | | } |
| | | |
| | | @Test |
| | | void developmentSimulatorRejectsDamagedCollectorHistoryIdentity() |
| | | { |
| | | Instant observedAt = Instant.parse("2026-08-29T05:00:00Z"); |
| | | List<CollectorChannelState> channels = List.of(new CollectorChannelState("S-01", List.of( |
| | | new CollectorMetricState("temperature", new BigDecimal("25.0"), "℃")))); |
| | | CollectorState collector = new CollectorState(1, "dev-sim-collector-1", "127.0.1.2", |
| | | "02:43:00:00:00:01", "模拟采集器", "开发模拟器", "Virtual Collector", 1, true, |
| | | 1000, 2L, observedAt, channels, List.of(), List.of(new CollectorMeasurementRecord( |
| | | "dev-sim", "wrong-collector", "S-01", 1L, observedAt, |
| | | "2026-08-29 13:00:00.000", observedAt, "temperature", |
| | | new BigDecimal("25.0"), "℃")), CollectorHistoryFaults.none()); |
| | | SimulatorState damaged = new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION, |
| | | List.of(), List.of(collector), 1, 2, List.of()); |
| | | |
| | | assertThrows(IllegalStateException.class, |
| | | () -> new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore(damaged))); |
| | | } |
| | | |
| | | @Test |
| | | void developmentSimulatorRejectsCollectorSequenceBehindPersistedHistory() |
| | | { |
| | | Instant observedAt = Instant.parse("2026-08-29T06:00:00Z"); |
| | | List<CollectorChannelState> channels = List.of(new CollectorChannelState("S-01", List.of( |
| | | new CollectorMetricState("temperature", new BigDecimal("25.0"), "℃")))); |
| | | CollectorState collector = new CollectorState(1, "dev-sim-collector-1", "127.0.1.2", |
| | | "02:43:00:00:00:01", "模拟采集器", "开发模拟器", "Virtual Collector", 1, true, |
| | | 1000, 1L, observedAt, channels, List.of(), List.of(new CollectorMeasurementRecord( |
| | | "dev-sim", "dev-sim-collector-1", "S-01", 1L, observedAt, |
| | | "2026-08-29 14:00:00.000", observedAt, "temperature", |
| | | new BigDecimal("25.0"), "℃")), CollectorHistoryFaults.none()); |
| | | SimulatorState damaged = new SimulatorState(BtsSimulatorStateStore.CURRENT_VERSION, |
| | | List.of(), List.of(collector), 1, 2, List.of()); |
| | | |
| | | assertThrows(IllegalStateException.class, |
| | | () -> new BtsDevelopmentSimulator(0, new InMemoryBtsSimulatorStateStore(damaged))); |
| | | } |
| | | } |