| | |
| | | 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) { } |
| | | } |