using Microsoft.Extensions.Configuration; using Prism.Events; using System.Net.Http; using YY.Admin.Core.Events; using YY.Admin.Core.Services; using YY.Admin.Helper; namespace YY.Admin.Infrastructure.Network; public class NetworkMonitor : INetworkMonitor { private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly IEventAggregator _eventAggregator; private readonly SemaphoreSlim _startLock = new(1, 1); private readonly object _aggregateLock = new(); private Task? _loopTask; private CancellationTokenSource? _cts; private volatile bool _httpProbeOnline; private volatile bool _stompTransportOnline; private volatile bool _isOnline; public NetworkMonitor( IHttpClientFactory httpClientFactory, IConfiguration configuration, IEventAggregator eventAggregator) { _httpClientFactory = httpClientFactory; _configuration = configuration; _eventAggregator = eventAggregator; } public bool IsOnline => _isOnline; public event Action? StatusChanged; /// public void SetStompTransportOnline(bool online) { _stompTransportOnline = online; RecomputeAggregatedOnline(); } public async Task StartAsync(CancellationToken cancellationToken = default) { await _startLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { if (_cts != null) { return; } _cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _loopTask = Task.Run(() => MonitorLoopAsync(_cts.Token), _cts.Token); } finally { _startLock.Release(); } } private async Task MonitorLoopAsync(CancellationToken cancellationToken) { // 启动后立即探活一次,避免首屏 10 秒内 IsOnline 恒为 false await RunHttpProbeAndRecomputeAsync(cancellationToken).ConfigureAwait(false); using var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)) { await RunHttpProbeAndRecomputeAsync(cancellationToken).ConfigureAwait(false); } } private async Task RunHttpProbeAndRecomputeAsync(CancellationToken cancellationToken) { var httpOk = await ProbeAsync(cancellationToken).ConfigureAwait(false); _httpProbeOnline = httpOk; RecomputeAggregatedOnline(); } private void RecomputeAggregatedOnline() { var combined = ComputeCombinedOnline(); bool newValue; lock (_aggregateLock) { if (combined == _isOnline) { return; } _isOnline = combined; newValue = combined; } StatusChanged?.Invoke(newValue); _eventAggregator.GetEvent().Publish(new NetworkStatusChangedPayload { IsOnline = newValue, ChangedAt = DateTime.UtcNow }); } private bool ComputeCombinedOnline() { if (IsDisconnectedByUser()) { return false; } return _httpProbeOnline || _stompTransportOnline; } private async Task ProbeAsync(CancellationToken cancellationToken) { if (IsDisconnectedByUser()) { return false; } var baseUrl = _configuration.GetValue("JeecgIntegration:BaseUrl")?.TrimEnd('/'); if (string.IsNullOrWhiteSpace(baseUrl)) { return false; } // 探活策略:优先调用免登录接口,失败后再降级到健康检查接口 var probeUrls = new[] { $"{baseUrl}/sys/user/scada/queryUser?current=1&pageSize=1", $"{baseUrl}/sys/dict/scada/queryDictItem?pageNo=1&pageSize=1", $"{baseUrl}/actuator/health" }; using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); timeoutCts.CancelAfter(TimeSpan.FromSeconds(3)); var client = _httpClientFactory.CreateClient("JeecgApi"); foreach (var url in probeUrls) { try { using var req = new HttpRequestMessage(HttpMethod.Get, url); using var resp = await client.SendAsync(req, timeoutCts.Token).ConfigureAwait(false); if (resp.IsSuccessStatusCode) { return true; } } catch { // 当前探活地址失败后继续尝试下一个降级地址 } } return false; } private static bool IsDisconnectedByUser() { try { return ServerSettingsStore.Load().DisconnectConnection; } catch { return false; } } }