| | |
| | | import java.util.concurrent.ScheduledExecutorService; |
| | | import java.util.concurrent.ScheduledFuture; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.ConcurrentHashMap; |
| | | import java.util.concurrent.ConcurrentMap; |
| | | import jakarta.annotation.PreDestroy; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; |
| | | import com.ruoyi.device.bts.BtsDevelopmentSimulator; |
| | | import com.ruoyi.device.bts.BtsDevelopmentSimulator.CollectorSnapshot; |
| | |
| | | import com.ruoyi.system.service.ITestLiveService.TestPlanRow; |
| | | import com.ruoyi.system.service.ITestLiveService.TestPlanSnapshot; |
| | | import com.ruoyi.system.service.ITTestService; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | |
| | | /** 将设备最新快照转换为测试视角,并托管 SSE 连接生命周期。 */ |
| | | @Service |
| | |
| | | private final ITTestService tests; |
| | | private final DeviceDiscoveryCandidateStore candidates; |
| | | private final BtsDevelopmentSimulator simulator; |
| | | private final TTestMapper mapper; |
| | | private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2); |
| | | private final ConcurrentMap<String, LiveSource> lastSources = new ConcurrentHashMap<>(); |
| | | |
| | | @Autowired |
| | | public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates, |
| | | BtsDevelopmentSimulator simulator, TTestMapper mapper) |
| | | { this.tests = tests; this.candidates = candidates; this.simulator = simulator; this.mapper = mapper; } |
| | | |
| | | public TTestLiveServiceImpl(ITTestService tests, DeviceDiscoveryCandidateStore candidates, |
| | | BtsDevelopmentSimulator simulator) |
| | | { this.tests = tests; this.candidates = candidates; this.simulator = simulator; } |
| | | { this(tests, candidates, simulator, 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 freezeSnapshots = "PAUSED".equals(test.getReceptionStatus()) |
| | | || "ENDED".equals(test.getReceptionStatus()); |
| | | boolean receiving = "RECEIVING".equals(test.getReceptionStatus()); |
| | | for (TTestBattery battery : test.getBatteries() == null ? List.<TTestBattery>of() : test.getBatteries()) |
| | | { |
| | | if (battery.getBinding() != null) |
| | | sources.add(snapshotFor(testId, cabinetSource(battery, cabinets), freezeSnapshots)); |
| | | sources.add(snapshotFor(cabinetSource(battery, cabinets), receiving)); |
| | | if (battery.getCollectorId() != null) |
| | | sources.add(snapshotFor(testId, collectorSource(battery), freezeSnapshots)); |
| | | sources.add(snapshotFor(collectorSource(battery), receiving)); |
| | | } |
| | | List<Map<String, Object>> exceptions = new ArrayList<>(); |
| | | exceptions.addAll(sources.stream().filter(source -> !"COLLECTOR".equals(source.sourceType()) |
| | |
| | | .map(source -> Map.<String, Object>of("sourceType", source.sourceType(), "sourceId", source.sourceId(), |
| | | "batteryNo", source.batteryNo(), "status", source.deviceStatus(), "message", "数据来源当前不可观测")) |
| | | .toList()); |
| | | if (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(), sources, exceptions, testPlan); |
| | |
| | | return null; |
| | | } |
| | | |
| | | private LiveSource snapshotFor(Long testId, LiveSource fresh, boolean freeze) |
| | | private LiveSource snapshotFor(LiveSource fresh, boolean receiving) |
| | | { |
| | | String key = testId + ":" + fresh.sourceType() + ":" + fresh.sourceId() + ":" + fresh.batteryNo(); |
| | | if (freeze) return lastSources.getOrDefault(key, fresh); |
| | | lastSources.put(key, fresh); |
| | | // 暂停或停止只停止数据请求;设备状态仍以本次查询的最新事实为准,采集快照不继续展示。 |
| | | 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()); |
| | | } |
| | | |
| | | @Override public SseEmitter stream(Long testId) |
| | |
| | | && 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() ? channel.workStatus().name() : "OFFLINE", snapshot(channel)); |
| | | channel.online() ? channel.workStatus().name() : "OFFLINE", |
| | | channel.online() ? snapshot(channel) : Map.of()); |
| | | return new LiveSource("CABINET_CHANNEL", binding.getChannelId(), battery.getBatteryNo(), "UNKNOWN", Map.of()); |
| | | } |
| | | |
| | |
| | | if (channel.latestSnapshot() == null) return value; |
| | | value.put("source", "检测柜"); |
| | | value.put("observedAt", channel.latestSnapshot().receivedAt()); |
| | | // 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()); |
| | |
| | | Measurement measurement = entry.getValue(); |
| | | return Map.of("metricCode", entry.getKey(), "value", measurement.value(), "unit", measurement.unit()); |
| | | }).toList()); |
| | | value.put("units", channel.latestSnapshot().measurements().values().stream().map(Measurement::unit).distinct().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; |
| | | } |
| | | |
| | |
| | | collector.latestMeasurements().stream().map(item -> item.serverReceivedAt()).max(Instant::compareTo) |
| | | .ifPresent(observedAt -> value.put("observedAt", observedAt)); |
| | | return new LiveSource("COLLECTOR", (long) collector.collectorId(), battery.getBatteryNo(), |
| | | collector.online() ? "ONLINE" : "OFFLINE", value); |
| | | collector.online() ? "ONLINE" : "OFFLINE", collector.online() ? value : Map.of()); |
| | | } |
| | | return new LiveSource("COLLECTOR", battery.getCollectorId(), battery.getBatteryNo(), "UNKNOWN", Map.of()); |
| | | } |