From d2b53841c31b82df4f71b800ad18a323694c10d7 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期六, 29 八月 2026 23:13:40 +0800
Subject: [PATCH] 修复设备发现状态同步与开始接收边界
---
ruoyi-admin/src/test/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizerTest.java | 7 ++
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceDiscoveryPersistenceService.java | 40 ++++++++++++
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TCollectorMapper.java | 4 +
ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizer.java | 14 ++++
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImplTest.java | 31 ++++++++++
ruoyi-system/src/main/resources/mapper/system/TCollectorMapper.xml | 6 ++
ruoyi-system/src/main/resources/mapper/system/TDetectionCabinetMapper.xml | 22 +++++++
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDetectionCabinetMapper.java | 13 ++++
8 files changed, 133 insertions(+), 4 deletions(-)
diff --git a/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizer.java b/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizer.java
index 395f1fc..22af3a1 100644
--- a/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizer.java
+++ b/ruoyi-admin/src/main/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizer.java
@@ -8,6 +8,7 @@
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore;
import com.ruoyi.device.discovery.DeviceDiscoveryCandidateStore.CollectorCandidate;
+import com.ruoyi.system.mapper.TCollectorMapper;
/** 将模拟采集器的发现和实时状态写入 Redis,不回写正式 MySQL 记录。 */
@Component
@@ -15,12 +16,20 @@
{
private final BtsDevelopmentSimulator simulator;
private final DeviceDiscoveryCandidateStore candidateStore;
+ private final TCollectorMapper collectorMapper;
public DevelopmentCollectorStatusSynchronizer(BtsDevelopmentSimulator simulator,
DeviceDiscoveryCandidateStore candidateStore)
{
+ this(simulator, candidateStore, null);
+ }
+
+ public DevelopmentCollectorStatusSynchronizer(BtsDevelopmentSimulator simulator,
+ DeviceDiscoveryCandidateStore candidateStore, TCollectorMapper collectorMapper)
+ {
this.simulator = simulator;
this.candidateStore = candidateStore;
+ this.collectorMapper = collectorMapper;
}
/** 完整替换 dev-sim 采集器快照,删除配置后候选立即消失。 */
@@ -35,5 +44,10 @@
observedAt.plus(Duration.ofSeconds(30))))
.toList();
candidateStore.replaceCollectors(BtsDevelopmentSimulator.ADAPTER_CODE, candidates);
+ if (collectorMapper != null)
+ {
+ candidates.forEach(candidate -> collectorMapper.updateRegisteredObservation(candidate.deviceCode(),
+ candidate.online() ? "ONLINE" : "OFFLINE"));
+ }
}
}
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizerTest.java b/ruoyi-admin/src/test/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizerTest.java
index cd05a02..16da2ef 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizerTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/config/DevelopmentCollectorStatusSynchronizerTest.java
@@ -4,6 +4,9 @@
import org.junit.jupiter.api.Test;
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
import com.ruoyi.device.discovery.InMemoryDeviceDiscoveryCandidateStore;
+import com.ruoyi.system.mapper.TCollectorMapper;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
class DevelopmentCollectorStatusSynchronizerTest
{
@@ -13,13 +16,15 @@
try (BtsDevelopmentSimulator simulator = new BtsDevelopmentSimulator(0))
{
InMemoryDeviceDiscoveryCandidateStore store = new InMemoryDeviceDiscoveryCandidateStore();
+ TCollectorMapper collectorMapper = mock(TCollectorMapper.class);
DevelopmentCollectorStatusSynchronizer synchronizer =
- new DevelopmentCollectorStatusSynchronizer(simulator, store);
+ new DevelopmentCollectorStatusSynchronizer(simulator, store, collectorMapper);
simulator.updateCollector(1, "127.0.1.2", 4, false);
synchronizer.synchronizeOnlineStatus();
assertEquals(false, store.snapshot(java.time.Instant.now()).collectors().get(0).online());
+ verify(collectorMapper).updateRegisteredObservation("dev-sim-collector-1", "OFFLINE");
simulator.deleteCollector(1);
synchronizer.synchronizeOnlineStatus();
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImplTest.java b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImplTest.java
index 8001b0d..4f1d265 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImplTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImplTest.java
@@ -39,6 +39,7 @@
import com.ruoyi.system.domain.TDetectionChannel;
import com.ruoyi.system.domain.TDeviceAdapter;
import com.ruoyi.system.mapper.TDeviceAdapterMapper;
+import com.ruoyi.system.mapper.TDetectionCabinetMapper;
import com.ruoyi.system.service.DeviceAdapterErrorCodes;
@ExtendWith(MockitoExtension.class)
@@ -53,6 +54,9 @@
@Mock
private DeviceAdapter deviceAdapter;
+ @Mock
+ private TDetectionCabinetMapper detectionCabinetMapper;
+
private DeviceAdapterServiceImpl service;
private TDeviceAdapter adapter;
private InMemoryDeviceDiscoveryCandidateStore candidateStore;
@@ -62,7 +66,7 @@
{
candidateStore = new InMemoryDeviceDiscoveryCandidateStore();
service = new DeviceAdapterServiceImpl(adapterMapper, registry,
- new DeviceDiscoveryPersistenceService(candidateStore), candidateStore);
+ new DeviceDiscoveryPersistenceService(candidateStore, detectionCabinetMapper), candidateStore);
adapter = new TDeviceAdapter();
adapter.setAdapterId(7L);
adapter.setAdapterCode("lab-bts");
@@ -162,6 +166,10 @@
var channel = candidateStore.snapshot(Instant.now()).cabinets().get(0).channels().get(0);
assertEquals(DeviceWorkStatus.WORKING, channel.workStatus());
+ verify(detectionCabinetMapper).updateRegisteredChannelObservation(eq("lab-bts"), eq(22), eq(1), eq(2),
+ eq(3), eq("ONLINE"), eq("WORKING"), any(), eq(true));
+ verify(detectionCabinetMapper).updateRegisteredCabinetObservation(eq("lab-bts"), eq(22), eq(1),
+ eq("ONLINE"), any(), eq(true));
}
@Test
@@ -179,6 +187,27 @@
verify(deviceAdapter, never()).inquire(any());
assertEquals(false, cabinet.online());
assertTrue(cabinet.channels().stream().noneMatch(channel -> channel.online()));
+ verify(detectionCabinetMapper).updateRegisteredChannelObservation(eq("lab-bts"), eq(22), eq(1), eq(1),
+ eq(1), eq("OFFLINE"), eq("UNKNOWN"), any(), eq(false));
+ verify(detectionCabinetMapper).updateRegisteredCabinetObservation(eq("lab-bts"), eq(22), eq(1),
+ eq("OFFLINE"), any(), eq(false));
+ }
+
+ @Test
+ void availableChannelWithoutSuccessfulSnapshotCannotAdvanceLastSeenBoundary()
+ {
+ ChannelIdentity identity = new ChannelIdentity("lab-bts", 22, 1, 1, 1);
+ when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter);
+ when(deviceAdapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "127.0.0.1", true)));
+ when(deviceAdapter.inquire(Set.of(identity))).thenReturn(Map.of());
+ stubRegistryExecution();
+
+ service.discover(7L);
+
+ verify(detectionCabinetMapper).updateRegisteredChannelObservation(eq("lab-bts"), eq(22), eq(1), eq(1),
+ eq(1), eq("ONLINE"), eq("UNKNOWN"), any(), eq(false));
+ verify(detectionCabinetMapper).updateRegisteredCabinetObservation(eq("lab-bts"), eq(22), eq(1),
+ eq("ONLINE"), any(), eq(false));
}
@Test
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TCollectorMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TCollectorMapper.java
index 2ea8c9b..a92dabc 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TCollectorMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TCollectorMapper.java
@@ -29,6 +29,10 @@
/** 新增采集器。 */
int insertCollector(TCollector collector);
+ /** 同步正式采集器的实时在线事实,不触碰软删除记录。 */
+ int updateRegisteredObservation(@Param("collectorCode") String collectorCode,
+ @Param("onlineStatus") String onlineStatus);
+
/** 编辑采集器基础资料。 */
int updateCollector(TCollector collector);
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDetectionCabinetMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDetectionCabinetMapper.java
index a53f24e..7105265 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDetectionCabinetMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDetectionCabinetMapper.java
@@ -61,6 +61,19 @@
/** 写入正式登记检测柜的稳定通道身份。 */
int insertDetectionChannel(com.ruoyi.system.domain.TDetectionChannel channel);
+ /** 同步正式检测柜的最近发现事实,不触碰软删除记录。 */
+ int updateRegisteredCabinetObservation(@Param("adapterCode") String adapterCode,
+ @Param("devtype") int devtype, @Param("devid") int devid,
+ @Param("onlineStatus") String onlineStatus, @Param("lastSeenAtUtc") java.time.Instant lastSeenAtUtc,
+ @Param("updateLastSeen") boolean updateLastSeen);
+
+ /** 按检测柜通道复合身份同步最近发现事实,不触碰软删除记录。 */
+ int updateRegisteredChannelObservation(@Param("adapterCode") String adapterCode,
+ @Param("devtype") int devtype, @Param("devid") int devid,
+ @Param("subdevid") int subdevid, @Param("chlid") int chlid,
+ @Param("onlineStatus") String onlineStatus, @Param("workStatus") String workStatus,
+ @Param("lastSeenAtUtc") java.time.Instant lastSeenAtUtc, @Param("updateLastSeen") boolean updateLastSeen);
+
/** 软删除正式检测柜对应的通道拓扑。 */
int deleteDetectionChannelsByCabinetId(@Param("cabinetId") Long cabinetId);
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 08582ef..87a4392 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
@@ -17,18 +17,24 @@
import com.ruoyi.device.model.RealtimeSnapshot;
import com.ruoyi.device.registry.DeviceAdapterRegistry;
import com.ruoyi.system.domain.vo.AdapterDiscoveryResult;
+import com.ruoyi.system.mapper.TDetectionCabinetMapper;
+import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
-/** 将适配器观测写入 Redis 候选存储,不创建正式 MySQL 设备或通道。 */
+/** 将适配器观测写入 Redis 候选,并同步已登记设备的实时事实。 */
@Service
public class DeviceDiscoveryPersistenceService
{
private static final Duration MINIMUM_CANDIDATE_LIFETIME = Duration.ofSeconds(30);
private final DeviceDiscoveryCandidateStore candidateStore;
+ private final TDetectionCabinetMapper detectionCabinetMapper;
- public DeviceDiscoveryPersistenceService(DeviceDiscoveryCandidateStore candidateStore)
+ @Autowired
+ public DeviceDiscoveryPersistenceService(DeviceDiscoveryCandidateStore candidateStore,
+ TDetectionCabinetMapper detectionCabinetMapper)
{
this.candidateStore = candidateStore;
+ this.detectionCabinetMapper = detectionCabinetMapper;
}
/** 执行设备发现和可用通道查询,并将本轮观测写入 Redis 候选。 */
@@ -63,9 +69,39 @@
observedAt, observedAt.plus(lifetime)))
.toList();
candidateStore.recordCabinets(candidates);
+ synchronizeRegisteredFacts(source, detected, snapshots, observedAt);
return new AdapterDiscoveryResult(grouped.size(), detected.size(), observedAt);
}
+ /**
+ * 发现候选与正式设备是两份事实;正式登记后仍需把成功观测同步到 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)
+ {
+ 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);
+ }
+ 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 CabinetCandidate toCandidate(DiscoverySource source, CabinetKey key, List<DetectedChannel> channels,
Map<ChannelIdentity, RealtimeSnapshot> snapshots, Instant observedAt, Instant expiresAt)
{
diff --git a/ruoyi-system/src/main/resources/mapper/system/TCollectorMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TCollectorMapper.xml
index 30b1af1..70e19b1 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TCollectorMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TCollectorMapper.xml
@@ -100,6 +100,12 @@
#{status}, #{delFlag}, #{createBy}, #{createTime}, #{remark})
</insert>
+ <update id="updateRegisteredObservation">
+ update t_collector
+ set online_status = #{onlineStatus}, update_time = sysdate()
+ where collector_code = #{collectorCode} and del_flag = 0
+ </update>
+
<update id="updateCollector" parameterType="TCollector">
update t_collector
<set>
diff --git a/ruoyi-system/src/main/resources/mapper/system/TDetectionCabinetMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TDetectionCabinetMapper.xml
index d7508de..8ec81b0 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TDetectionCabinetMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TDetectionCabinetMapper.xml
@@ -109,6 +109,28 @@
#{onlineStatus}, #{workStatus}, #{lastSeenAtUtc}, '0', sysdate(), 0)
</insert>
+ <update id="updateRegisteredCabinetObservation">
+ update t_detection_cabinet
+ set online_status = #{onlineStatus}, update_time = sysdate()
+ <if test="updateLastSeen">, last_seen_at_utc = #{lastSeenAtUtc}</if>
+ where adapter_code = #{adapterCode}
+ and vendor_device_type = #{devtype} and vendor_device_id = #{devid}
+ and del_flag = 0 and registration_status = 'REGISTERED'
+ </update>
+
+ <update id="updateRegisteredChannelObservation">
+ update t_detection_channel
+ set online_status = #{onlineStatus}, work_status = #{workStatus}, update_time = sysdate()
+ <if test="updateLastSeen">, last_seen_at_utc = #{lastSeenAtUtc}</if>
+ where adapter_code = #{adapterCode}
+ and devtype = #{devtype} and devid = #{devid}
+ and subdevid = #{subdevid} and chlid = #{chlid}
+ and del_flag = 0
+ and exists (select 1 from t_detection_cabinet cabinet
+ where cabinet.cabinet_id = t_detection_channel.cabinet_id
+ and cabinet.del_flag = 0 and cabinet.registration_status = 'REGISTERED')
+ </update>
+
<update id="updateDetectionCabinet" parameterType="TDetectionCabinet">
update t_detection_cabinet
<set>
--
Gitblit v1.7.1