| | |
| | | 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 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.system.domain.TDetectionCabinet; |
| | | import com.ruoyi.system.domain.TDetectionChannel; |
| | | import com.ruoyi.system.domain.TDeviceAdapter; |
| | | import com.ruoyi.device.model.DetectedDeviceMetadata; |
| | | import com.ruoyi.device.model.DeviceWorkStatus; |
| | | import com.ruoyi.device.model.RealtimeSnapshot; |
| | | import com.ruoyi.device.registry.DeviceAdapterRegistry; |
| | | import com.ruoyi.system.domain.vo.AdapterDiscoveryResult; |
| | | import com.ruoyi.system.mapper.TDeviceAdapterMapper; |
| | | import com.ruoyi.system.mapper.TDetectionCabinetMapper; |
| | | import org.springframework.beans.factory.annotation.Autowired; |
| | | import org.springframework.stereotype.Service; |
| | | |
| | | /** 将适配器观测写入 Redis 候选,并同步已登记设备的实时事实。 */ |
| | | @Service |
| | | public class DeviceDiscoveryPersistenceService |
| | | { |
| | | private final TDeviceAdapterMapper mapper; |
| | | private static final Duration MINIMUM_CANDIDATE_LIFETIME = Duration.ofSeconds(30); |
| | | private final DeviceDiscoveryCandidateStore candidateStore; |
| | | private final TDetectionCabinetMapper detectionCabinetMapper; |
| | | |
| | | public DeviceDiscoveryPersistenceService(TDeviceAdapterMapper mapper) |
| | | @Autowired |
| | | public DeviceDiscoveryPersistenceService(DeviceDiscoveryCandidateStore candidateStore, |
| | | TDetectionCabinetMapper detectionCabinetMapper) |
| | | { |
| | | this.mapper = mapper; |
| | | this.candidateStore = candidateStore; |
| | | this.detectionCabinetMapper = detectionCabinetMapper; |
| | | } |
| | | |
| | | /** 在一个事务内 upsert 本轮观测事实;未返回通道保持原记录不变。 */ |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public AdapterDiscoveryResult persist(TDeviceAdapter adapter, List<DetectedChannel> detected, Instant observedAt) |
| | | /** 执行设备发现和可用通道查询,并将本轮观测写入 Redis 候选。 */ |
| | | public AdapterDiscoveryResult discover(DeviceAdapterRegistry registry, DiscoverySource source) |
| | | { |
| | | Map<CabinetKey, List<DetectedChannel>> cabinets = detected.stream().collect( |
| | | 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>> 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())); |
| | | 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); |
| | | synchronizeRegisteredFacts(source, detected, snapshots, observedAt); |
| | | return new AdapterDiscoveryResult(grouped.size(), detected.size(), observedAt); |
| | | } |
| | | |
| | | for (Map.Entry<CabinetKey, List<DetectedChannel>> entry : cabinets.entrySet()) |
| | | /** |
| | | * 发现候选与正式设备是两份事实;正式登记后仍需把成功观测同步到 MySQL, |
| | | * 否则开始接收无法取得可靠边界。离线轮询只更新当前状态,不覆盖最后成功观测时间。 |
| | | */ |
| | | private void synchronizeRegisteredFacts(DiscoverySource source, List<DetectedChannel> detected, |
| | | Map<ChannelIdentity, RealtimeSnapshot> snapshots, Instant observedAt) |
| | | { |
| | | Map<CabinetKey, Boolean> cabinetOnline = new LinkedHashMap<>(); |
| | | for (DetectedChannel channel : detected) |
| | | { |
| | | Long cabinetId = ensureCabinet(adapter, entry.getKey(), entry.getValue(), observedAt); |
| | | for (DetectedChannel item : entry.getValue()) |
| | | mapper.upsertDetectedChannel(toChannel(adapter.getAdapterId(), cabinetId, item, observedAt)); |
| | | ChannelIdentity identity = channel.identity(); |
| | | boolean successfulObservation = channel.available() && snapshots.containsKey(identity); |
| | | RealtimeSnapshot snapshot = snapshots.get(identity); |
| | | DeviceWorkStatus workStatus = snapshot == null || snapshot.workStatus() == null |
| | | ? DeviceWorkStatus.UNKNOWN : snapshot.workStatus(); |
| | | detectionCabinetMapper.updateRegisteredChannelObservation(source.adapterCode(), identity.devtype(), |
| | | identity.devid(), identity.subdevid(), identity.chlid(), channel.available() ? "ONLINE" : "OFFLINE", |
| | | workStatus.name(), observedAt, successfulObservation); |
| | | CabinetKey cabinet = new CabinetKey(identity.devtype(), identity.devid()); |
| | | cabinetOnline.merge(cabinet, channel.available(), Boolean::logicalOr); |
| | | } |
| | | return new AdapterDiscoveryResult(cabinets.size(), detected.size(), observedAt); |
| | | cabinetOnline.forEach((cabinet, online) -> detectionCabinetMapper.updateRegisteredCabinetObservation( |
| | | source.adapterCode(), cabinet.devtype(), cabinet.devid(), online ? "ONLINE" : "OFFLINE", observedAt, |
| | | online && detected.stream().anyMatch(channel -> channel.available() |
| | | && channel.identity().devtype() == cabinet.devtype() |
| | | && channel.identity().devid() == cabinet.devid() |
| | | && snapshots.containsKey(channel.identity())))); |
| | | } |
| | | |
| | | 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()); |
| | | cabinet.setCabinetName(adapter.getAdapterName() + " " + key.devtype() + "-" + key.devid()); |
| | | 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; |
| | | DetectedDeviceMetadata metadata = channels.get(0).deviceMetadata(); |
| | | 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, 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("UNKNOWN"); |
| | | 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) { } |
| | | } |