From de98fe81cca90dd855e0519f065e77fce56333ef Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期三, 26 八月 2026 16:28:30 +0800
Subject: [PATCH] fix(device): address adapter discovery review
---
ruoyi-device/src/main/java/com/ruoyi/device/registry/DeviceAdapterRegistry.java | 91 ++++++++++--
ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java | 7 +
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsTransportException.java | 2
ruoyi-system/src/main/resources/mapper/system/TDeviceAdapterMapper.xml | 3
ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDeviceAdapterMapper.java | 6
ruoyi-system/src/main/java/com/ruoyi/system/service/IDeviceAdapterService.java | 17 ++
ruoyi-device/src/main/java/com/ruoyi/device/api/DeviceAdapterException.java | 17 ++
ruoyi-device/src/test/java/com/ruoyi/device/registry/BtsDeviceAdapterFactoryContractTest.java | 2
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceDiscoveryPersistenceService.java | 14 +
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImpl.java | 36 +++-
ruoyi-admin/src/main/resources/db/migration/V5__adapter_discovery_runtime.sql | 20 ++
ruoyi-admin/src/test/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImplTest.java | 46 +++++-
ruoyi-device/src/test/java/com/ruoyi/device/registry/DeviceAdapterRegistryTest.java | 106 ++++++++++++++
ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsProtocolException.java | 5
14 files changed, 314 insertions(+), 58 deletions(-)
diff --git a/ruoyi-admin/src/main/resources/db/migration/V5__adapter_discovery_runtime.sql b/ruoyi-admin/src/main/resources/db/migration/V5__adapter_discovery_runtime.sql
index 904a8d3..8168259 100644
--- a/ruoyi-admin/src/main/resources/db/migration/V5__adapter_discovery_runtime.sql
+++ b/ruoyi-admin/src/main/resources/db/migration/V5__adapter_discovery_runtime.sql
@@ -11,3 +11,23 @@
ADD CONSTRAINT chk_adapter_connection_status
CHECK (connection_status IN ('DISCONNECTED', 'CONNECTED', 'ERROR')),
ADD KEY idx_adapter_poll_due (status, connection_status, last_attempt_at_utc, del_flag);
+
+-- 适配器页面复用“检测柜”菜单;按钮权限必须进入 sys_menu,普通角色才可被授予 REST 权限。
+SET @adapter_permission_parent := (
+ SELECT menu_id FROM sys_menu
+ WHERE perms = 'system:detectionCabinet:list'
+ ORDER BY menu_id DESC LIMIT 1
+);
+SET @adapter_permission_base := (SELECT COALESCE(MAX(menu_id), 0) FROM sys_menu);
+
+INSERT INTO sys_menu
+ (menu_id, menu_name, parent_id, order_num, path, component, query, route_name,
+ is_frame, is_cache, menu_type, visible, status, perms, icon,
+ create_by, create_time, update_by, update_time, remark)
+VALUES
+ (@adapter_permission_base + 1, '适配器列表', @adapter_permission_parent, 11, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:deviceAdapter:list', '#', 'admin', NOW(), '', NULL, ''),
+ (@adapter_permission_base + 2, '适配器查看', @adapter_permission_parent, 12, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:deviceAdapter:query', '#', 'admin', NOW(), '', NULL, ''),
+ (@adapter_permission_base + 3, '适配器新增', @adapter_permission_parent, 13, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:deviceAdapter:add', '#', 'admin', NOW(), '', NULL, ''),
+ (@adapter_permission_base + 4, '适配器编辑', @adapter_permission_parent, 14, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:deviceAdapter:edit', '#', 'admin', NOW(), '', NULL, ''),
+ (@adapter_permission_base + 5, '适配器启停', @adapter_permission_parent, 15, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:deviceAdapter:changeStatus', '#', 'admin', NOW(), '', NULL, ''),
+ (@adapter_permission_base + 6, '适配器发现', @adapter_permission_parent, 16, '', NULL, NULL, '', 1, 0, 'F', '0', '0', 'system:deviceAdapter:discover', '#', 'admin', NOW(), '', NULL, '');
diff --git a/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java b/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java
index 57294b0..4550a66 100644
--- a/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java
+++ b/ruoyi-admin/src/test/java/com/ruoyi/config/FlywayMigrationContractTest.java
@@ -35,6 +35,13 @@
assertTrue(v5.contains("connection_status"));
assertTrue(v5.contains("last_observed_at_utc"));
assertTrue(v5.contains("chk_adapter_connection_status"));
+ assertTrue(v5.contains("system:deviceAdapter:list"));
+ assertTrue(v5.contains("system:deviceAdapter:query"));
+ assertTrue(v5.contains("system:deviceAdapter:add"));
+ assertTrue(v5.contains("system:deviceAdapter:edit"));
+ assertTrue(v5.contains("system:deviceAdapter:changeStatus"));
+ assertTrue(v5.contains("system:deviceAdapter:discover"));
+ assertTrue(v5.contains("system:detectionCabinet:list"));
assertFalse(v5.matches("(?s).*\\bDELETE\\s+FROM\\b.*"));
}
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 4e6c437..2be39cd 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
@@ -4,11 +4,15 @@
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import java.math.BigDecimal;
+import java.time.Instant;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -16,9 +20,11 @@
import org.mockito.junit.jupiter.MockitoExtension;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.device.api.DeviceAdapter;
-import com.ruoyi.device.bts.BtsTransportException;
+import com.ruoyi.device.api.DeviceAdapterException;
import com.ruoyi.device.model.ChannelIdentity;
import com.ruoyi.device.model.DetectedChannel;
+import com.ruoyi.device.model.DeviceWorkStatus;
+import com.ruoyi.device.model.RealtimeSnapshot;
import com.ruoyi.device.registry.DeviceAdapterRegistry;
import com.ruoyi.system.domain.TDetectionCabinet;
import com.ruoyi.system.domain.TDetectionChannel;
@@ -75,30 +81,49 @@
void repeatedDiscoveryUpsertsStableChannelIdentityWithoutDeletingMissingChannels()
{
when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter);
- when(registry.require("lab-bts")).thenReturn(deviceAdapter);
when(deviceAdapter.discoverChannels()).thenReturn(List.of(
new DetectedChannel(new ChannelIdentity("lab-bts", 22, 1, 2, 3), "192.168.1.35", true),
new DetectedChannel(new ChannelIdentity("lab-bts", 22, 1, 2, 4), "192.168.1.35", false)));
+ stubRegistryExecution();
when(adapterMapper.selectCabinetIdByIdentity(7L, 22, 1)).thenReturn(10L);
assertEquals(2, service.discover(7L).channelCount());
assertEquals(2, service.discover(7L).channelCount());
verify(adapterMapper, times(4)).upsertDetectedChannel(any(TDetectionChannel.class));
- verify(adapterMapper, never()).deleteMissingChannels(any(), any());
verify(adapterMapper, times(2)).updateDiscoverySuccess(eq(7L), any());
verify(adapterMapper, times(2)).updateDetectedCabinet(any(TDetectionCabinet.class));
+ }
+
+ @Test
+ void discoveryPersistsRealtimeWorkStatusForAvailableChannels()
+ {
+ ChannelIdentity identity = new ChannelIdentity("lab-bts", 22, 1, 2, 3);
+ when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter);
+ when(deviceAdapter.discoverChannels()).thenReturn(List.of(
+ new DetectedChannel(identity, "192.168.1.35", true)));
+ when(deviceAdapter.inquire(Set.of(identity))).thenReturn(Map.of(identity,
+ new RealtimeSnapshot(identity, DeviceWorkStatus.WORKING, 1, 2, "cc", null,
+ BigDecimal.ONE, BigDecimal.TEN, null, null, 3L, 2L, null,
+ Instant.parse("2026-08-26T01:02:03Z"), Map.of())));
+ when(adapterMapper.selectCabinetIdByIdentity(7L, 22, 1)).thenReturn(10L);
+ stubRegistryExecution();
+
+ service.discover(7L);
+
+ var channel = org.mockito.ArgumentCaptor.forClass(TDetectionChannel.class);
+ verify(adapterMapper).upsertDetectedChannel(channel.capture());
+ assertEquals("WORKING", channel.getValue().getWorkStatus());
}
@Test
void connectionFailureHasStableCodeAndLaterDiscoveryClearsError()
{
when(adapterMapper.selectDeviceAdapterById(7L)).thenReturn(adapter);
- when(registry.require("lab-bts")).thenReturn(deviceAdapter);
when(deviceAdapter.discoverChannels())
- .thenThrow(new BtsTransportException(BtsTransportException.Reason.CONNECTION_FAILED,
- "connection refused", null))
+ .thenThrow(new DeviceAdapterException("CONNECTION_FAILED", "connection refused", null))
.thenReturn(List.of());
+ stubRegistryExecution();
ServiceException failure = assertThrows(ServiceException.class, () -> service.discover(7L));
assertEquals(DeviceAdapterErrorCodes.DISCOVERY_FAILED, failure.getCode());
@@ -107,4 +132,11 @@
assertEquals(0, service.discover(7L).channelCount());
verify(adapterMapper).updateDiscoverySuccess(eq(7L), any());
}
+
+ @SuppressWarnings("unchecked")
+ private void stubRegistryExecution()
+ {
+ when(registry.execute(eq("lab-bts"), any())).thenAnswer(invocation ->
+ ((Function<DeviceAdapter, Object>) invocation.getArgument(1)).apply(deviceAdapter));
+ }
}
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/api/DeviceAdapterException.java b/ruoyi-device/src/main/java/com/ruoyi/device/api/DeviceAdapterException.java
index 30060a4..9c5b090 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/api/DeviceAdapterException.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/api/DeviceAdapterException.java
@@ -3,14 +3,29 @@
public class DeviceAdapterException extends RuntimeException
{
private static final long serialVersionUID = 1L;
+ private static final String DEFAULT_FAILURE_CODE = "ADAPTER_FAILURE";
+
+ private final String failureCode;
public DeviceAdapterException(String message)
{
- super(message);
+ this(DEFAULT_FAILURE_CODE, message, null);
}
public DeviceAdapterException(String message, Throwable cause)
{
+ this(DEFAULT_FAILURE_CODE, message, cause);
+ }
+
+ public DeviceAdapterException(String failureCode, String message, Throwable cause)
+ {
super(message, cause);
+ this.failureCode = failureCode;
+ }
+
+ /** 返回供业务层持久化和恢复判断使用的稳定失败分类。 */
+ public String failureCode()
+ {
+ return failureCode;
}
}
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsProtocolException.java b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsProtocolException.java
index e619424..171ebbd 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsProtocolException.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsProtocolException.java
@@ -30,13 +30,12 @@
public BtsProtocolException(Reason reason, String message)
{
- super(message);
- this.reason = reason;
+ this(reason, message, null);
}
public BtsProtocolException(Reason reason, String message, Throwable cause)
{
- super(message, cause);
+ super(reason.name(), message, cause);
this.reason = reason;
}
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsTransportException.java b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsTransportException.java
index f6fe0f8..0f63a59 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsTransportException.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/bts/BtsTransportException.java
@@ -23,7 +23,7 @@
public BtsTransportException(Reason reason, String message, Throwable cause)
{
- super(message, cause);
+ super(reason.name(), message, cause);
this.reason = reason;
}
diff --git a/ruoyi-device/src/main/java/com/ruoyi/device/registry/DeviceAdapterRegistry.java b/ruoyi-device/src/main/java/com/ruoyi/device/registry/DeviceAdapterRegistry.java
index b4d8895..bda7ccf 100644
--- a/ruoyi-device/src/main/java/com/ruoyi/device/registry/DeviceAdapterRegistry.java
+++ b/ruoyi-device/src/main/java/com/ruoyi/device/registry/DeviceAdapterRegistry.java
@@ -5,6 +5,8 @@
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
+import java.util.function.Function;
import com.ruoyi.device.api.DeviceAdapter;
/** 管理启用设备适配器的运行时实例及其会话生命周期。 */
@@ -23,7 +25,7 @@
}
/** 按持久化配置注册、替换或移除运行时实例。 */
- public synchronized void synchronize(AdapterInstanceConfig config)
+ public void synchronize(AdapterInstanceConfig config)
{
Objects.requireNonNull(config, "config");
if (!config.enabled())
@@ -32,23 +34,42 @@
return;
}
- RegisteredAdapter current = adapters.get(config.adapterCode());
- if (current != null && current.config().equals(config)) return;
+ RegisteredAdapter previous;
+ synchronized (this)
+ {
+ RegisteredAdapter current = adapters.get(config.adapterCode());
+ if (current != null && current.config.equals(config)) return;
- DeviceAdapterFactory factory = factories.get(normalizeType(config.adapterType()));
- if (factory == null) throw new AdapterRegistryException("Unsupported device adapter type: " + config.adapterType());
+ DeviceAdapterFactory factory = factories.get(normalizeType(config.adapterType()));
+ if (factory == null)
+ throw new AdapterRegistryException("Unsupported device adapter type: " + config.adapterType());
- DeviceAdapter replacement = factory.create(config);
- RegisteredAdapter previous = adapters.put(config.adapterCode(), new RegisteredAdapter(config, replacement));
- if (previous != null) previous.adapter().close();
+ DeviceAdapter replacement = factory.create(config);
+ previous = adapters.put(config.adapterCode(), new RegisteredAdapter(config, replacement));
+ }
+ closeWhenIdle(previous);
}
- /** 返回已启用适配器;未注册时给出可识别失败。 */
- public synchronized DeviceAdapter require(String adapterCode)
+ /** 在实例级串行边界内执行一次适配器操作;未注册时给出可识别失败。 */
+ public <T> T execute(String adapterCode, Function<DeviceAdapter, T> operation)
{
- RegisteredAdapter registered = adapters.get(adapterCode);
- if (registered == null) throw new AdapterRegistryException("Device adapter is not enabled: " + adapterCode);
- return registered.adapter();
+ Objects.requireNonNull(operation, "operation");
+ RegisteredAdapter registered;
+ synchronized (this)
+ {
+ registered = adapters.get(adapterCode);
+ if (registered == null)
+ throw new AdapterRegistryException("Device adapter is not enabled: " + adapterCode);
+ registered.operationLock.lock();
+ }
+ try
+ {
+ return operation.apply(registered.adapter);
+ }
+ finally
+ {
+ registered.operationLock.unlock();
+ }
}
public synchronized boolean contains(String adapterCode)
@@ -57,16 +78,39 @@
}
@Override
- public synchronized void close()
+ public void close()
{
- adapters.values().forEach(registered -> registered.adapter().close());
- adapters.clear();
+ List<RegisteredAdapter> registered;
+ synchronized (this)
+ {
+ registered = List.copyOf(adapters.values());
+ adapters.clear();
+ }
+ registered.forEach(this::closeWhenIdle);
}
private void remove(String adapterCode)
{
- RegisteredAdapter previous = adapters.remove(adapterCode);
- if (previous != null) previous.adapter().close();
+ RegisteredAdapter previous;
+ synchronized (this)
+ {
+ previous = adapters.remove(adapterCode);
+ }
+ closeWhenIdle(previous);
+ }
+
+ private void closeWhenIdle(RegisteredAdapter registered)
+ {
+ if (registered == null) return;
+ registered.operationLock.lock();
+ try
+ {
+ registered.adapter.close();
+ }
+ finally
+ {
+ registered.operationLock.unlock();
+ }
}
private static String normalizeType(String value)
@@ -74,7 +118,16 @@
return Objects.requireNonNull(value, "adapterType").trim().toUpperCase(Locale.ROOT);
}
- private record RegisteredAdapter(AdapterInstanceConfig config, DeviceAdapter adapter)
+ private static final class RegisteredAdapter
{
+ private final AdapterInstanceConfig config;
+ private final DeviceAdapter adapter;
+ private final ReentrantLock operationLock = new ReentrantLock(true);
+
+ private RegisteredAdapter(AdapterInstanceConfig config, DeviceAdapter adapter)
+ {
+ this.config = config;
+ this.adapter = adapter;
+ }
}
}
diff --git a/ruoyi-device/src/test/java/com/ruoyi/device/registry/BtsDeviceAdapterFactoryContractTest.java b/ruoyi-device/src/test/java/com/ruoyi/device/registry/BtsDeviceAdapterFactoryContractTest.java
index 43817b2..322d22f 100644
--- a/ruoyi-device/src/test/java/com/ruoyi/device/registry/BtsDeviceAdapterFactoryContractTest.java
+++ b/ruoyi-device/src/test/java/com/ruoyi/device/registry/BtsDeviceAdapterFactoryContractTest.java
@@ -22,7 +22,7 @@
registry.synchronize(new AdapterInstanceConfig("lab-bts", "BTS", "127.0.0.1", simulator.port(),
5000, "Asia/Hong_Kong", true, AdapterCapabilities.observeOnly()));
- var adapter = registry.require("lab-bts");
+ var adapter = registry.execute("lab-bts", value -> value);
var discovered = adapter.discoverChannels();
assertEquals("22-1-2-3", discovered.get(0).identity().vendorKey());
diff --git a/ruoyi-device/src/test/java/com/ruoyi/device/registry/DeviceAdapterRegistryTest.java b/ruoyi-device/src/test/java/com/ruoyi/device/registry/DeviceAdapterRegistryTest.java
index b062175..83c002c 100644
--- a/ruoyi-device/src/test/java/com/ruoyi/device/registry/DeviceAdapterRegistryTest.java
+++ b/ruoyi-device/src/test/java/com/ruoyi/device/registry/DeviceAdapterRegistryTest.java
@@ -7,6 +7,11 @@
import static org.junit.jupiter.api.Assertions.assertNotSame;
import java.util.ArrayList;
import java.util.List;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;
import com.ruoyi.device.api.DeviceAdapter;
@@ -25,10 +30,10 @@
AdapterInstanceConfig config = config("127.0.0.1", true);
registry.synchronize(config);
- DeviceAdapter first = registry.require("lab-bts");
+ DeviceAdapter first = registry.execute("lab-bts", adapter -> adapter);
registry.synchronize(config);
- assertSame(first, registry.require("lab-bts"));
+ assertSame(first, registry.execute("lab-bts", adapter -> adapter));
assertFalse(factory.adapters.get(0).closed.get());
}
}
@@ -45,13 +50,106 @@
registry.synchronize(config("127.0.0.2", true));
assertTrue(first.closed.get());
- assertNotSame(first, registry.require("lab-bts"));
+ assertNotSame(first, registry.execute("lab-bts", adapter -> adapter));
registry.synchronize(config("127.0.0.2", false));
assertTrue(factory.adapters.get(1).closed.get());
assertFalse(registry.contains("lab-bts"));
- assertThrows(AdapterRegistryException.class, () -> registry.require("lab-bts"));
+ assertThrows(AdapterRegistryException.class,
+ () -> registry.execute("lab-bts", adapter -> adapter));
+ }
+ }
+
+ @Test
+ void serializesOperationsForTheSameAdapterInstance() throws Exception
+ {
+ RecordingFactory factory = new RecordingFactory();
+ try (DeviceAdapterRegistry registry = new DeviceAdapterRegistry(List.of(factory)))
+ {
+ registry.synchronize(config("127.0.0.1", true));
+ CountDownLatch firstEntered = new CountDownLatch(1);
+ CountDownLatch releaseFirst = new CountDownLatch(1);
+ CountDownLatch secondEntered = new CountDownLatch(1);
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ try
+ {
+ Future<?> first = executor.submit(() -> registry.execute("lab-bts", adapter -> {
+ firstEntered.countDown();
+ await(releaseFirst);
+ return null;
+ }));
+ assertTrue(firstEntered.await(1, TimeUnit.SECONDS));
+
+ Future<?> second = executor.submit(() -> registry.execute("lab-bts", adapter -> {
+ secondEntered.countDown();
+ return null;
+ }));
+
+ assertFalse(secondEntered.await(100, TimeUnit.MILLISECONDS));
+ releaseFirst.countDown();
+ first.get(1, TimeUnit.SECONDS);
+ second.get(1, TimeUnit.SECONDS);
+ assertTrue(secondEntered.await(1, TimeUnit.SECONDS));
+ }
+ finally
+ {
+ executor.shutdownNow();
+ }
+ }
+ }
+
+ @Test
+ void configurationReplacementWaitsForRunningOperationBeforeClosingAdapter() throws Exception
+ {
+ RecordingFactory factory = new RecordingFactory();
+ try (DeviceAdapterRegistry registry = new DeviceAdapterRegistry(List.of(factory)))
+ {
+ registry.synchronize(config("127.0.0.1", true));
+ StubAdapter firstAdapter = factory.adapters.get(0);
+ CountDownLatch operationEntered = new CountDownLatch(1);
+ CountDownLatch releaseOperation = new CountDownLatch(1);
+ CountDownLatch replacementStarted = new CountDownLatch(1);
+ ExecutorService executor = Executors.newFixedThreadPool(2);
+ try
+ {
+ Future<?> operation = executor.submit(() -> registry.execute("lab-bts", adapter -> {
+ operationEntered.countDown();
+ await(releaseOperation);
+ return null;
+ }));
+ assertTrue(operationEntered.await(1, TimeUnit.SECONDS));
+
+ Future<?> replacement = executor.submit(() -> {
+ replacementStarted.countDown();
+ registry.synchronize(config("127.0.0.2", true));
+ });
+ assertTrue(replacementStarted.await(1, TimeUnit.SECONDS));
+ assertFalse(replacement.isDone());
+ assertFalse(firstAdapter.closed.get());
+
+ releaseOperation.countDown();
+ operation.get(1, TimeUnit.SECONDS);
+ replacement.get(1, TimeUnit.SECONDS);
+ assertTrue(firstAdapter.closed.get());
+ }
+ finally
+ {
+ executor.shutdownNow();
+ }
+ }
+ }
+
+ private static void await(CountDownLatch latch)
+ {
+ try
+ {
+ if (!latch.await(1, TimeUnit.SECONDS)) throw new AssertionError("等待适配器测试信号超时");
+ }
+ catch (InterruptedException exception)
+ {
+ Thread.currentThread().interrupt();
+ throw new AssertionError("等待适配器测试信号被中断", exception);
}
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDeviceAdapterMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDeviceAdapterMapper.java
index e9f3825..cdc4da5 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDeviceAdapterMapper.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/TDeviceAdapterMapper.java
@@ -26,10 +26,4 @@
int updateDetectedCabinet(TDetectionCabinet cabinet);
int upsertDetectedChannel(TDetectionChannel channel);
List<TDetectionChannel> selectDetectionChannelsByAdapterId(Long adapterId);
-
- /** 发现只追加或更新,不提供“本轮缺失即删除”的持久化操作。 */
- default int deleteMissingChannels(Long adapterId, List<String> identities)
- {
- throw new UnsupportedOperationException("Discovery must not delete missing channels");
- }
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IDeviceAdapterService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IDeviceAdapterService.java
index 2abceec..abca2da 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IDeviceAdapterService.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IDeviceAdapterService.java
@@ -7,13 +7,30 @@
public interface IDeviceAdapterService
{
+ /** 按管理端筛选条件查询设备适配器实例。 */
List<TDeviceAdapter> selectList(TDeviceAdapter query);
+
+ /** 查询设备适配器详情;实例不存在时返回稳定业务错误。 */
TDeviceAdapter selectById(Long adapterId);
+
+ /** 新增默认禁用的只读设备适配器配置。 */
int insert(TDeviceAdapter adapter);
+
+ /** 更新设备适配器配置,并同步已启用的运行时实例。 */
int update(TDeviceAdapter adapter);
+
+ /** 启用或停用设备适配器,并同步运行时生命周期。 */
int changeStatus(Long adapterId, boolean enabled, String updateBy);
+
+ /** 执行一次发现和实时观测,并持久化本轮设备事实。 */
AdapterDiscoveryResult discover(Long adapterId);
+
+ /** 查询指定适配器已发现且未被删除的通道。 */
List<TDetectionChannel> selectChannels(Long adapterId);
+
+ /** 启动时恢复所有已启用适配器的运行时实例。 */
void synchronizeEnabledAdapters();
+
+ /** 对达到轮询周期的已启用适配器执行发现。 */
void pollDueAdapters();
}
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImpl.java
index 5b06844..6d26688 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/DeviceAdapterServiceImpl.java
@@ -3,16 +3,21 @@
import java.time.Duration;
import java.time.Instant;
import java.time.ZoneId;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Service;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.device.api.DeviceAdapterException;
-import com.ruoyi.device.bts.BtsProtocolException;
-import com.ruoyi.device.bts.BtsTransportException;
import com.ruoyi.device.model.AdapterCapabilities;
+import com.ruoyi.device.model.ChannelIdentity;
+import com.ruoyi.device.model.DetectedChannel;
+import com.ruoyi.device.model.RealtimeSnapshot;
import com.ruoyi.device.registry.AdapterInstanceConfig;
import com.ruoyi.device.registry.AdapterRegistryException;
import com.ruoyi.device.registry.DeviceAdapterRegistry;
@@ -110,21 +115,25 @@
try
{
registry.synchronize(toRuntimeConfig(adapter));
- List<com.ruoyi.device.model.DetectedChannel> channels = registry.require(adapter.getAdapterCode()).discoverChannels();
+ DiscoveryObservation observation = registry.execute(adapter.getAdapterCode(), runtime -> {
+ List<DetectedChannel> channels = runtime.discoverChannels();
+ Set<ChannelIdentity> available = channels.stream()
+ .filter(DetectedChannel::available)
+ .map(DetectedChannel::identity)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ return new DiscoveryObservation(channels, runtime.inquire(available));
+ });
Instant observedAt = Instant.now();
- AdapterDiscoveryResult result = persistence.persist(adapter, channels, observedAt);
+ AdapterDiscoveryResult result = persistence.persist(adapter, observation.channels(),
+ observation.snapshots(), observedAt);
mapper.updateDiscoverySuccess(adapterId, observedAt);
return result;
}
- catch (BtsTransportException exception)
+ catch (DeviceAdapterException exception)
{
- return failDiscovery(adapterId, exception.reason().name(), exception);
+ return failDiscovery(adapterId, exception.failureCode(), exception);
}
- catch (BtsProtocolException exception)
- {
- return failDiscovery(adapterId, exception.reason().name(), exception);
- }
- catch (DeviceAdapterException | AdapterRegistryException exception)
+ catch (AdapterRegistryException exception)
{
return failDiscovery(adapterId, "ADAPTER_FAILURE", exception);
}
@@ -196,4 +205,9 @@
}
private ServiceException error(String message, int code) { return new ServiceException(message, code); }
+
+ private record DiscoveryObservation(List<DetectedChannel> channels,
+ Map<ChannelIdentity, RealtimeSnapshot> snapshots)
+ {
+ }
}
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 2989aaa..55f051b 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
@@ -9,6 +9,8 @@
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.device.model.DetectedChannel;
+import com.ruoyi.device.model.ChannelIdentity;
+import com.ruoyi.device.model.RealtimeSnapshot;
import com.ruoyi.system.domain.TDetectionCabinet;
import com.ruoyi.system.domain.TDetectionChannel;
import com.ruoyi.system.domain.TDeviceAdapter;
@@ -27,7 +29,8 @@
/** 在一个事务内 upsert 本轮观测事实;未返回通道保持原记录不变。 */
@Transactional(rollbackFor = Exception.class)
- public AdapterDiscoveryResult persist(TDeviceAdapter adapter, List<DetectedChannel> detected, Instant observedAt)
+ public AdapterDiscoveryResult persist(TDeviceAdapter adapter, List<DetectedChannel> detected,
+ Map<ChannelIdentity, RealtimeSnapshot> snapshots, Instant observedAt)
{
Map<CabinetKey, List<DetectedChannel>> cabinets = detected.stream().collect(
java.util.stream.Collectors.groupingBy(
@@ -38,7 +41,8 @@
{
Long cabinetId = ensureCabinet(adapter, entry.getKey(), entry.getValue(), observedAt);
for (DetectedChannel item : entry.getValue())
- mapper.upsertDetectedChannel(toChannel(adapter.getAdapterId(), cabinetId, item, observedAt));
+ mapper.upsertDetectedChannel(toChannel(adapter.getAdapterId(), cabinetId, item,
+ snapshots.get(item.identity()), observedAt));
}
return new AdapterDiscoveryResult(cabinets.size(), detected.size(), observedAt);
}
@@ -78,7 +82,8 @@
return cabinetId;
}
- private TDetectionChannel toChannel(Long adapterId, Long cabinetId, DetectedChannel detected, Instant observedAt)
+ private TDetectionChannel toChannel(Long adapterId, Long cabinetId, DetectedChannel detected,
+ RealtimeSnapshot snapshot, Instant observedAt)
{
TDetectionChannel channel = new TDetectionChannel();
channel.setAdapterId(adapterId);
@@ -89,7 +94,8 @@
channel.setChlid(detected.identity().chlid());
channel.setDisplayNo(detected.identity().subdevid() + "-" + detected.identity().chlid());
channel.setOnlineStatus(detected.available() ? "ONLINE" : "OFFLINE");
- channel.setWorkStatus("UNKNOWN");
+ channel.setWorkStatus(snapshot == null || snapshot.workStatus() == null
+ ? "UNKNOWN" : snapshot.workStatus().name());
channel.setLastSeenAtUtc(observedAt);
channel.setStatus("0");
return channel;
diff --git a/ruoyi-system/src/main/resources/mapper/system/TDeviceAdapterMapper.xml b/ruoyi-system/src/main/resources/mapper/system/TDeviceAdapterMapper.xml
index de165c7..eea0964 100644
--- a/ruoyi-system/src/main/resources/mapper/system/TDeviceAdapterMapper.xml
+++ b/ruoyi-system/src/main/resources/mapper/system/TDeviceAdapterMapper.xml
@@ -131,7 +131,8 @@
values(#{adapterId}, #{cabinetId}, #{devtype}, #{devid}, #{subdevid}, #{chlid}, #{displayNo},
#{onlineStatus}, #{workStatus}, #{lastSeenAtUtc}, '0', sysdate(), 0)
on duplicate key update cabinet_id=values(cabinet_id), display_no=values(display_no),
- online_status=values(online_status), last_seen_at_utc=values(last_seen_at_utc), update_time=sysdate()
+ online_status=values(online_status), work_status=values(work_status),
+ last_seen_at_utc=values(last_seen_at_utc), update_time=sysdate()
</insert>
<select id="selectDetectionChannelsByAdapterId" resultMap="ChannelResult">
--
Gitblit v1.7.1