fix(device): address adapter discovery review
| | |
| | | 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, ''); |
| | |
| | | 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.*")); |
| | | } |
| | | |
| | |
| | | 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; |
| | |
| | | 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; |
| | |
| | | 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()); |
| | |
| | | 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)); |
| | | } |
| | | } |
| | |
| | | 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; |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | | } |
| | | |
| | |
| | | |
| | | public BtsTransportException(Reason reason, String message, Throwable cause) |
| | | { |
| | | super(message, cause); |
| | | super(reason.name(), message, cause); |
| | | this.reason = reason; |
| | | } |
| | | |
| | |
| | | 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; |
| | | |
| | | /** 管理启用设备适配器的运行时实例及其会话生命周期。 */ |
| | |
| | | } |
| | | |
| | | /** 按持久化配置注册、替换或移除运行时实例。 */ |
| | | public synchronized void synchronize(AdapterInstanceConfig config) |
| | | public void synchronize(AdapterInstanceConfig config) |
| | | { |
| | | Objects.requireNonNull(config, "config"); |
| | | if (!config.enabled()) |
| | |
| | | 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) |
| | |
| | | } |
| | | |
| | | @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) |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | |
| | | 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()); |
| | |
| | | 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; |
| | |
| | | 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()); |
| | | } |
| | | } |
| | |
| | | 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); |
| | | } |
| | | } |
| | | |
| | |
| | | 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"); |
| | | } |
| | | } |
| | |
| | | |
| | | 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(); |
| | | } |
| | |
| | | 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; |
| | |
| | | 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); |
| | | } |
| | |
| | | } |
| | | |
| | | private ServiceException error(String message, int code) { return new ServiceException(message, code); } |
| | | |
| | | private record DiscoveryObservation(List<DetectedChannel> channels, |
| | | Map<ChannelIdentity, RealtimeSnapshot> snapshots) |
| | | { |
| | | } |
| | | } |
| | |
| | | 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; |
| | |
| | | |
| | | /** 在一个事务内 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( |
| | |
| | | { |
| | | 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); |
| | | } |
| | |
| | | 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); |
| | |
| | | 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; |
| | |
| | | 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"> |