From 210663040bf65ee00b5f86545fdc31bd0356fddf Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 27 八月 2026 16:31:53 +0800
Subject: [PATCH] feat(device): persist simulator discovery in redis

---
 ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceDiscoveryPersistenceService.java |  149 ++++++++++++++++++++++---------------------------
 1 files changed, 66 insertions(+), 83 deletions(-)

diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceDiscoveryPersistenceService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceDiscoveryPersistenceService.java
index b9e9cdf..08582ef 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceDiscoveryPersistenceService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceDiscoveryPersistenceService.java
@@ -1,114 +1,97 @@
 package com.ruoyi.system.service.impl;
 
-import java.math.BigDecimal;
+import java.time.Duration;
 import java.time.Instant;
+import java.util.LinkedHashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
-import org.springframework.dao.DuplicateKeyException;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import com.ruoyi.device.model.DetectedChannel;
+import java.util.Set;
+import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore;
+import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.CabinetCandidate;
+import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.ChannelCandidate;
 import com.ruoyi.device.model.ChannelIdentity;
+import com.ruoyi.device.model.DetectedChannel;
 import com.ruoyi.device.model.DetectedDeviceMetadata;
+import com.ruoyi.device.model.DeviceWorkStatus;
 import com.ruoyi.device.model.RealtimeSnapshot;
-import com.ruoyi.system.domain.TDetectionCabinet;
-import com.ruoyi.system.domain.TDetectionChannel;
-import com.ruoyi.system.domain.TDeviceAdapter;
+import com.ruoyi.device.registry.DeviceAdapterRegistry;
 import com.ruoyi.system.domain.vo.AdapterDiscoveryResult;
-import com.ruoyi.system.mapper.TDeviceAdapterMapper;
+import org.springframework.stereotype.Service;
 
+/** 将适配器观测写入 Redis 候选存储,不创建正式 MySQL 设备或通道。 */
 @Service
 public class DeviceDiscoveryPersistenceService
 {
-    private final TDeviceAdapterMapper mapper;
+    private static final Duration MINIMUM_CANDIDATE_LIFETIME = Duration.ofSeconds(30);
+    private final DeviceDiscoveryCandidateStore candidateStore;
 
-    public DeviceDiscoveryPersistenceService(TDeviceAdapterMapper mapper)
+    public DeviceDiscoveryPersistenceService(DeviceDiscoveryCandidateStore candidateStore)
     {
-        this.mapper = mapper;
+        this.candidateStore = candidateStore;
     }
 
-    /** 在事务内 upsert 本轮发现和实时观测;未返回通道保持原记录不变。 */
-    @Transactional(rollbackFor = Exception.class)
-    public AdapterDiscoveryResult persist(TDeviceAdapter adapter, List<DetectedChannel> detected,
+    /** 执行设备发现和可用通道查询,并将本轮观测写入 Redis 候选。 */
+    public AdapterDiscoveryResult discover(DeviceAdapterRegistry registry, DiscoverySource source)
+    {
+        DiscoveryObservation observation = registry.execute(source.adapterCode(), runtime -> {
+            List<DetectedChannel> channels = runtime.discoverChannels();
+            Set<ChannelIdentity> available = channels.stream()
+                    .filter(DetectedChannel::available)
+                    .map(DetectedChannel::identity)
+                    .collect(java.util.stream.Collectors.toCollection(LinkedHashSet::new));
+            Map<ChannelIdentity, RealtimeSnapshot> snapshots = available.isEmpty()
+                    ? Map.of() : runtime.inquire(available);
+            return new DiscoveryObservation(channels, snapshots);
+        });
+        Instant observedAt = Instant.now();
+        return persist(source, observation.channels(), observation.snapshots(), observedAt);
+    }
+
+    /** upsert 本轮发现和实时状态;未返回设备由 expiresAt 控制淘汰。 */
+    private AdapterDiscoveryResult persist(DiscoverySource source, List<DetectedChannel> detected,
             Map<ChannelIdentity, RealtimeSnapshot> snapshots, Instant observedAt)
     {
-        Map<CabinetKey, List<DetectedChannel>> cabinets = detected.stream().collect(
+        Map<CabinetKey, List<DetectedChannel>> grouped = detected.stream().collect(
                 java.util.stream.Collectors.groupingBy(
                         item -> new CabinetKey(item.identity().devtype(), item.identity().devid()),
                         LinkedHashMap::new, java.util.stream.Collectors.toList()));
-
-        for (Map.Entry<CabinetKey, List<DetectedChannel>> entry : cabinets.entrySet())
-        {
-            Long cabinetId = ensureCabinet(adapter, entry.getKey(), entry.getValue(), observedAt);
-            for (DetectedChannel item : entry.getValue())
-                mapper.upsertDetectedChannel(toChannel(adapter.getAdapterId(), cabinetId, item,
-                        snapshots.get(item.identity()), observedAt));
-        }
-        return new AdapterDiscoveryResult(cabinets.size(), detected.size(), observedAt);
+        Duration lifetime = Duration.ofMillis(Math.max(MINIMUM_CANDIDATE_LIFETIME.toMillis(),
+                (long) source.pollIntervalMs() * 3));
+        List<CabinetCandidate> candidates = grouped.entrySet().stream()
+                .map(entry -> toCandidate(source, entry.getKey(), entry.getValue(), snapshots,
+                        observedAt, observedAt.plus(lifetime)))
+                .toList();
+        candidateStore.recordCabinets(candidates);
+        return new AdapterDiscoveryResult(grouped.size(), detected.size(), observedAt);
     }
 
-    private Long ensureCabinet(TDeviceAdapter adapter, CabinetKey key, List<DetectedChannel> channels,
-            Instant observedAt)
+    private CabinetCandidate toCandidate(DiscoverySource source, CabinetKey key, List<DetectedChannel> channels,
+            Map<ChannelIdentity, RealtimeSnapshot> snapshots, Instant observedAt, Instant expiresAt)
     {
-        Long cabinetId = mapper.selectCabinetIdByIdentity(adapter.getAdapterId(), key.devtype(), key.devid());
-        TDetectionCabinet cabinet = new TDetectionCabinet();
-        cabinet.setCabinetId(cabinetId);
-        cabinet.setAdapterId(adapter.getAdapterId());
-        cabinet.setCabinetCode(adapter.getAdapterCode() + "-" + key.devtype() + "-" + key.devid());
         DetectedDeviceMetadata metadata = channels.get(0).deviceMetadata();
-        cabinet.setCabinetName(metadata.name() == null
-                ? adapter.getAdapterName() + " " + key.devtype() + "-" + key.devid()
-                : metadata.name());
-        cabinet.setManufacturer(metadata.manufacturer());
-        cabinet.setModel(metadata.model());
-        cabinet.setMacAddress(metadata.macAddress());
-        cabinet.setCapacity(BigDecimal.ONE);
-        cabinet.setChannelCount(channels.size());
-        cabinet.setVendorDeviceType(key.devtype());
-        cabinet.setVendorDeviceId(key.devid());
-        cabinet.setIpAddress(channels.stream().map(DetectedChannel::serverIp).filter(value -> value != null && !value.isBlank())
-                .findFirst().orElse(null));
-        cabinet.setOnlineStatus(channels.stream().anyMatch(DetectedChannel::available) ? "ONLINE" : "OFFLINE");
-        cabinet.setLastSeenAtUtc(observedAt);
-        if (cabinetId == null)
-        {
-            try
-            {
-                mapper.insertDetectedCabinet(cabinet);
-                return cabinet.getCabinetId();
-            }
-            catch (DuplicateKeyException concurrentInsert)
-            {
-                cabinetId = mapper.selectCabinetIdByIdentity(adapter.getAdapterId(), key.devtype(), key.devid());
-                if (cabinetId == null) throw concurrentInsert;
-                cabinet.setCabinetId(cabinetId);
-            }
-        }
-        mapper.updateDetectedCabinet(cabinet);
-        return cabinetId;
+        String name = metadata.name() == null
+                ? source.adapterName() + " " + key.devtype() + "-" + key.devid()
+                : metadata.name();
+        String ip = channels.stream().map(DetectedChannel::serverIp)
+                .filter(value -> value != null && !value.isBlank()).findFirst().orElse(null);
+        List<ChannelCandidate> channelCandidates = channels.stream().map(item -> {
+            RealtimeSnapshot snapshot = snapshots.get(item.identity());
+            DeviceWorkStatus status = snapshot == null || snapshot.workStatus() == null
+                    ? DeviceWorkStatus.UNKNOWN : snapshot.workStatus();
+            return new ChannelCandidate(item.identity(), item.available(), status, observedAt);
+        }).toList();
+        return new CabinetCandidate(source.adapterCode(), source.adapterId(),
+                source.adapterCode() + "-" + key.devtype() + "-" + key.devid(),
+                key.devtype(), key.devid(), ip, metadata.macAddress(), name,
+                metadata.manufacturer(), metadata.model(), channelCandidates, observedAt, expiresAt);
     }
 
-    private TDetectionChannel toChannel(Long adapterId, Long cabinetId, DetectedChannel detected,
-            RealtimeSnapshot snapshot, Instant observedAt)
-    {
-        TDetectionChannel channel = new TDetectionChannel();
-        channel.setAdapterId(adapterId);
-        channel.setCabinetId(cabinetId);
-        channel.setDevtype(detected.identity().devtype());
-        channel.setDevid(detected.identity().devid());
-        channel.setSubdevid(detected.identity().subdevid());
-        channel.setChlid(detected.identity().chlid());
-        channel.setDisplayNo(detected.identity().subdevid() + "-" + detected.identity().chlid());
-        channel.setOnlineStatus(detected.available() ? "ONLINE" : "OFFLINE");
-        channel.setWorkStatus(snapshot == null || snapshot.workStatus() == null
-                ? "UNKNOWN" : snapshot.workStatus().name());
-        channel.setLastSeenAtUtc(observedAt);
-        channel.setStatus("0");
-        return channel;
-    }
+    /** 发现来源只携带候选标识和过期计算所需配置,不复用数据库实体。 */
+    public record DiscoverySource(String adapterCode, Long adapterId, String adapterName, int pollIntervalMs) { }
 
-    private record CabinetKey(int devtype, int devid)
-    {
-    }
+    private record DiscoveryObservation(List<DetectedChannel> channels,
+            Map<ChannelIdentity, RealtimeSnapshot> snapshots) { }
+
+    private record CabinetKey(int devtype, int devid) { }
 }

--
Gitblit v1.7.1