yeg
2026-09-29 b78295e8da96c3e5e76befeef9582ce5273dbb74
battery-business/src/main/java/com/battery/business/service/impl/DeviceDiscoveryPersistenceService.java
@@ -28,6 +28,11 @@
public class DeviceDiscoveryPersistenceService
{
    private static final Duration MINIMUM_CANDIDATE_LIFETIME = Duration.ofSeconds(30);
    /**
     * 直连厂商存储的来源,其通道登记清单是长期存在的事实,没有"过期"的对应语义:
     * 候选是否出现在界面上只由"已登记且未删除"决定,这个时间只是让候选一直有效。
     */
    static final Instant CANDIDATE_NEVER_EXPIRES = Instant.parse("9999-12-31T23:59:59Z");
    private final DeviceDiscoveryCandidateStore candidateStore;
    private final TDetectionCabinetMapper detectionCabinetMapper;
    private final TTestMapper testMapper;
@@ -75,7 +80,8 @@
                .map(item -> new CabinetCandidate(item.adapterCode(), item.adapterId(), item.deviceCode(),
                        item.devtype(), item.devid(), item.ip(), item.mac(), item.name(), item.manufacturer(), item.model(),
                        item.channels().stream().map(channel -> new ChannelCandidate(channel.identity(), false,
                                DeviceWorkStatus.UNOBSERVABLE, failedAt)).toList(), failedAt, item.expiresAt()))
                                DeviceWorkStatus.UNOBSERVABLE, failedAt, null, channel.lastDataAt())).toList(),
                        failedAt, item.expiresAt(), item.lastDataAt()))
                .toList());
        detectionCabinetMapper.markRegisteredFactsUnobservable(adapterCode);
        detectionCabinetMapper.markRegisteredCabinetsOffline(adapterCode);
@@ -108,9 +114,10 @@
                        LinkedHashMap::new, java.util.stream.Collectors.toList()));
        Duration lifetime = Duration.ofMillis(Math.max(MINIMUM_CANDIDATE_LIFETIME.toMillis(),
                (long) source.pollIntervalMs() * 3));
        Instant expiresAt = source.persistentCandidates() ? CANDIDATE_NEVER_EXPIRES : observedAt.plus(lifetime);
        List<CabinetCandidate> candidates = grouped.entrySet().stream()
                .map(entry -> toCandidate(source, entry.getKey(), entry.getValue(), snapshots,
                        observedAt, observedAt.plus(lifetime)))
                        observedAt, expiresAt))
                .toList();
        candidateStore.recordCabinets(candidates);
        if (detected.isEmpty()) markUnavailable(source.adapterCode());
@@ -133,7 +140,7 @@
    /**
     * 发现候选与正式设备是两份事实;正式登记后仍需把成功观测同步到 MySQL。
     * 离线轮询只更新当前状态,不覆盖最后成功观测时间;暂停期间不会进入此路径。
     * 暂停期间仍同步设备状态,但不请求该测试的测量;离线不覆盖最后成功观测时间。
     */
    private void synchronizeRegisteredFacts(DiscoverySource source, List<DetectedChannel> detected,
            Map<ChannelIdentity, RealtimeSnapshot> snapshots, Instant observedAt)
@@ -185,16 +192,27 @@
            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, snapshots.get(item.identity()));
            return new ChannelCandidate(item.identity(), item.available(), status, observedAt,
                    snapshots.get(item.identity()), item.lastDataAt());
        }).toList();
        Instant lastDataAt = channels.stream().map(DetectedChannel::lastDataAt)
                .filter(java.util.Objects::nonNull).max(Instant::compareTo).orElse(null);
        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);
                metadata.manufacturer(), metadata.model(), channelCandidates, observedAt, expiresAt, lastDataAt);
    }
    /** 发现来源只携带候选标识和过期计算所需配置,不复用数据库实体。 */
    public record DiscoverySource(String adapterCode, Long adapterId, String adapterName, int pollIntervalMs) { }
    public record DiscoverySource(String adapterCode, Long adapterId, String adapterName, int pollIntervalMs,
            boolean persistentCandidates)
    {
        /** 兼容按轮询过期计算候选的既有来源。 */
        public DiscoverySource(String adapterCode, Long adapterId, String adapterName, int pollIntervalMs)
        {
            this(adapterCode, adapterId, adapterName, pollIntervalMs, false);
        }
    }
    private record DiscoveryObservation(List<DetectedChannel> channels,
            Map<ChannelIdentity, RealtimeSnapshot> snapshots) { }