yeg
2026-09-05 68156d14934ffb7d7b6e0dca63d3dcf3ed43c995
修复离线设备无法发现和登记的问题

移除发现列表及登记接口残留的在线限制,未登记离线设备保留候选并展示真实状态,已登记设备继续过滤。补充离线发现、登记和登记过滤回归,全量236项测试及打包通过。
6个文件已修改
62 ■■■■■ 已修改文件
battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/service/impl/TDetectionCabinetServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/service/impl/CollectorDiscoveryServiceImplTest.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
ruoyi-admin/src/test/java/com/battery/business/service/impl/TDetectionCabinetServiceImplTest.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
battery-business/src/main/java/com/battery/business/service/impl/TCollectorServiceImpl.java
@@ -98,8 +98,7 @@
        if (collector == null) throw new ServiceException("采集器信息不能为空");
        collector.setCollectorCode(trimToNull(collector.getCollectorCode()));
        CollectorCandidate candidate = candidateStore.findCollector(collector.getCollectorCode(), Instant.now())
                .filter(CollectorCandidate::online)
                .orElseThrow(() -> new ServiceException("请先选择在线的发现候选设备"));
                .orElseThrow(() -> new ServiceException("请先选择有效的发现候选设备"));
        collector.setIpAddress(candidate.ip());
        collector.setMacAddress(candidate.mac());
        collector.setChannelCount(candidate.channelCount());
@@ -120,7 +119,7 @@
        collector.setDelFlag("0");
        collector.setCreateTime(new Date());
        int rows = collectorMapper.insertCollector(collector);
        // 登记瞬间同步候选事实,避免正式记录暂时显示为离线且没有最近观测时间。
        // 登记只确认来源身份,在线与离线事实都按候选原样同步。
        collectorMapper.updateRegisteredObservation(collector.getCollectorCode(),
                candidate.online() ? "ONLINE" : "OFFLINE");
        return rows;
battery-business/src/main/java/com/battery/business/service/impl/TDetectionCabinetServiceImpl.java
@@ -101,7 +101,7 @@
    }
    /**
     * 读取 Redis 在线候选供原选择设备弹窗展示,不生成模拟设备。
     * 读取未过期且未登记的候选,离线设备仍保留身份并可选择。
     * 测试占用状态由实时通道工作状态聚合;测试归属留待后续绑定闭环确认。
     *
     * @return 可供选择的检测柜设备摘要
@@ -111,13 +111,12 @@
    {
        Set<String> registeredCodes = Set.copyOf(detectionCabinetMapper.selectRegisteredCabinetCodes());
        return candidateStore.snapshot(Instant.now()).cabinets().stream()
                .filter(CabinetCandidate::online)
                .filter(candidate -> !registeredCodes.contains(candidate.deviceCode()))
                .map(candidate -> {
                    boolean active = isActive(candidate);
                    boolean active = candidate.online() && isActive(candidate);
                    return new DiscoveredCabinetVo(
                            candidate.deviceCode(), candidate.ip(), candidate.mac(), candidate.name(),
                            candidate.manufacturer(), candidate.model(), candidate.channels().size(), "在线",
                            candidate.manufacturer(), candidate.model(), candidate.channels().size(), candidate.online() ? "在线" : "离线",
                            active ? "测试中" : "空闲",
                            active ? "待确认" : "-");
                })
@@ -136,8 +135,7 @@
    {
        normalizeBasicFields(detectionCabinet);
        CabinetCandidate candidate = candidateStore.findCabinet(detectionCabinet.getCabinetCode(), Instant.now())
                .filter(CabinetCandidate::online)
                .orElseThrow(() -> new ServiceException("请先选择在线的发现候选设备"));
                .orElseThrow(() -> new ServiceException("请先选择有效的发现候选设备"));
        detectionCabinet.setAdapterId(candidate.adapterId());
        detectionCabinet.setAdapterCode(candidate.adapterCode());
        detectionCabinet.setVendorDeviceType(candidate.devtype());
@@ -169,7 +167,7 @@
                value.setLastSeenAtUtc(null);
                detectionCabinetMapper.insertDetectionChannel(value);
            }
            // 登记瞬间的候选也是一次有效观测,立即写入正式事实,避免等待下一轮轮询才显示在线。
            // 登记只确认来源身份,在线与离线事实都按候选原样同步。
            detectionCabinetMapper.updateRegisteredCabinetObservation(candidate.adapterCode(), candidate.devtype(),
                    candidate.devid(), candidate.online() ? "ONLINE" : "OFFLINE", candidate.observedAt(),
                    candidate.online());
ruoyi-admin/src/main/java/com/battery/business/runtime/DevelopmentCollectorDiscoveryService.java
@@ -23,17 +23,16 @@
        this.collectorService = collectorService;
    }
    /** 返回在线且未登记的独立采集器快照,不混入 BTS 检测柜设备。 */
    /** 返回未过期且未登记的采集器,离线事实不影响发现展示或登记资格。 */
    @Override
    public List<DiscoveredCollectorVo> selectDiscoveredCollectorList()
    {
        Set<String> registeredCodes = collectorService.selectRegisteredCollectorCodes();
        return candidateStore.snapshot(Instant.now()).collectors().stream()
                .filter(collector -> collector.online()
                        && !registeredCodes.contains(collector.deviceCode()))
                .filter(collector -> !registeredCodes.contains(collector.deviceCode()))
                .map(collector -> new DiscoveredCollectorVo(collector.deviceCode(), collector.ip(), collector.mac(),
                        collector.name(), collector.manufacturer(), collector.model(), collector.channelCount(),
                        "在线", "空闲", "-"))
                        collector.online() ? "在线" : "离线", "空闲", "-"))
                .toList();
    }
}
ruoyi-admin/src/test/java/com/battery/business/service/impl/CollectorDiscoveryServiceImplTest.java
@@ -50,7 +50,7 @@
    }
    @Test
    public void shouldHideOfflineCollectors() throws Exception
    public void shouldReturnOfflineUnregisteredCollectors() throws Exception
    {
        try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
        {
@@ -65,7 +65,9 @@
            List<DiscoveredCollectorVo> result = service.selectDiscoveredCollectorList();
            assertEquals(0, result.size());
            assertEquals(1, result.size());
            assertEquals("dev-sim-collector-1", result.get(0).getDeviceCode());
            assertEquals("离线", result.get(0).getOnlineStatus());
        }
    }
@@ -79,6 +81,7 @@
            DevelopmentCollectorDiscoveryService service =
                    new DevelopmentCollectorDiscoveryService(store, collectorService);
            TCollectorMapper collectorMapper = mock(TCollectorMapper.class);
            simulator.updateCollector(1, "127.0.1.2", 4, false);
            new DevelopmentCollectorStatusSynchronizer(simulator, store, collectorMapper).synchronizeOnlineStatus();
            when(collectorService.selectRegisteredCollectorCodes())
                    .thenReturn(Set.of("dev-sim-collector-1"))
ruoyi-admin/src/test/java/com/battery/business/service/impl/TCollectorServiceImplTest.java
@@ -166,6 +166,18 @@
    }
    @Test
    void offlineCollectorCanBeRegisteredWithoutChangingOnlineFact()
    {
        java.time.Instant now = java.time.Instant.now();
        candidateStore.replaceCollectors("dev-sim", java.util.List.of(new CollectorCandidate(
                "dev-sim", "COL-001", "192.168.1.10", "AA:BB:CC:DD:EE:FF", "一号采集器",
                "开发模拟器", "Virtual Collector", 8, false, now, now.plusSeconds(60))));
        when(collectorMapper.insertCollector(any(TCollector.class))).thenReturn(1);
        assertEquals(1, collectorService.insertCollector(collector));
        verify(collectorMapper).updateRegisteredObservation("COL-001", "OFFLINE");
    }
    @Test
    void insertCollectorShouldRejectDuplicateCode()
    {
        collector.setCollectorId(null);
ruoyi-admin/src/test/java/com/battery/business/service/impl/TDetectionCabinetServiceImplTest.java
@@ -228,6 +228,23 @@
        assertEquals(8, devices.get(0).getChannelCount());
    }
    @Test
    void offlineCabinetRemainsDiscoverableAndCanBeRegistered()
    {
        var store = new InMemoryDeviceDiscoveryCandidateStore();
        store.recordCabinets(List.of(candidate("CABINET-001", false, DeviceWorkStatus.UNOBSERVABLE)));
        var service = new TDetectionCabinetServiceImpl(detectionCabinetMapper, store);
        when(detectionCabinetMapper.selectRegisteredCabinetCodes()).thenReturn(List.of());
        var devices = service.selectDiscoveredCabinetList();
        assertEquals(1, devices.size());
        assertEquals("离线", devices.get(0).getOnlineStatus());
        when(detectionCabinetMapper.insertDetectionCabinet(any(TDetectionCabinet.class))).thenReturn(1);
        assertEquals(1, service.insertDetectionCabinet(detectionCabinet));
        assertEquals("OFFLINE", detectionCabinet.getOnlineStatus());
        when(detectionCabinetMapper.selectRegisteredCabinetCodes()).thenReturn(List.of("CABINET-001"));
        assertEquals(0, service.selectDiscoveredCabinetList().size());
    }
    /**
     * 验证重复设备编码会阻止新增且不会执行写库操作。
     */
@@ -255,7 +272,7 @@
        ServiceException exception = assertThrows(ServiceException.class,
                () -> detectionCabinetService.insertDetectionCabinet(detectionCabinet));
        assertEquals("请先选择在线的发现候选设备", exception.getMessage());
        assertEquals("请先选择有效的发现候选设备", exception.getMessage());
        verify(detectionCabinetMapper, never()).insertDetectionCabinet(any());
    }