更新项目配置,新增设备同步模块,优化WebSocket和Swagger配置,增强SCADA系统的免登录接口,支持数据字典项和登录日志的免登录查询与记录。调整Java编译设置,确保更好的开发体验。
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Prism.Events;
|
||||
using System.IO;
|
||||
using System.Net.WebSockets;
|
||||
using System.Text;
|
||||
using YY.Admin.Core.Events;
|
||||
using YY.Admin.Core.Services;
|
||||
using YY.Admin.Infrastructure.Storage;
|
||||
|
||||
namespace YY.Admin.Infrastructure.Hubs;
|
||||
|
||||
public class StompWebSocketService : ISignalRService
|
||||
{
|
||||
private readonly IConfiguration _configuration;
|
||||
private readonly IEventAggregator _eventAggregator;
|
||||
private readonly TokenStore _tokenStore;
|
||||
private ClientWebSocket? _socket;
|
||||
private string _deviceId = "default-device";
|
||||
private string _token = string.Empty;
|
||||
|
||||
public StompWebSocketService(
|
||||
IConfiguration configuration,
|
||||
IEventAggregator eventAggregator,
|
||||
TokenStore tokenStore)
|
||||
{
|
||||
_configuration = configuration;
|
||||
_eventAggregator = eventAggregator;
|
||||
_tokenStore = tokenStore;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ConnectAsync(string token, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_token = token ?? string.Empty;
|
||||
await ConnectUnifiedDeviceChannelAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task ConnectUnifiedDeviceChannelAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var anonymous = _configuration.GetValue("JeecgIntegration:AnonymousMode", true);
|
||||
if (anonymous)
|
||||
{
|
||||
_token = string.Empty;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(_token))
|
||||
{
|
||||
_token = await _tokenStore.GetTokenAsync(cancellationToken).ConfigureAwait(false) ?? string.Empty;
|
||||
}
|
||||
|
||||
_deviceId = ResolveDeviceId(_token);
|
||||
var wsUrl = ResolveWsUrl();
|
||||
var retryDelays = new[] { 0, 2, 5, 10, 30 };
|
||||
|
||||
foreach (var delay in retryDelays)
|
||||
{
|
||||
if (delay > 0)
|
||||
{
|
||||
await Task.Delay(TimeSpan.FromSeconds(delay), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_socket?.Dispose();
|
||||
_socket = new ClientWebSocket();
|
||||
_socket.Options.AddSubProtocol("v12.stomp");
|
||||
await _socket.ConnectAsync(new Uri(wsUrl), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
var connectFrame = anonymous || string.IsNullOrWhiteSpace(_token)
|
||||
? "CONNECT\naccept-version:1.2\nheart-beat:10000,10000\n\n\0"
|
||||
: BuildConnectFrame(_token);
|
||||
await SendFrameAsync(connectFrame, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 用户镜像变更:与后端 /topic/sync/jeecg-users 对齐(设备同步统一线路)
|
||||
await SendFrameAsync(BuildSubscribeFrame("sub-jeecg-users", "/topic/sync/jeecg-users"), cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// 非免密时同时订阅设备点对点指令队列
|
||||
if (!anonymous && !string.IsNullOrWhiteSpace(_token))
|
||||
{
|
||||
await SendFrameAsync(BuildSubscribeFrame("sub-device-command", $"/user/{_deviceId}/queue/command"), cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>().Publish(new NetworkStatusChangedPayload
|
||||
{
|
||||
IsOnline = true,
|
||||
ChangedAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
_ = Task.Run(() => ReceiveLoopAsync(cancellationToken), cancellationToken);
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>().Publish(new NetworkStatusChangedPayload
|
||||
{
|
||||
IsOnline = false,
|
||||
ChangedAt = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task SendDeviceStatusAsync(object status, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_socket == null || _socket.State != WebSocketState.Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(status);
|
||||
var frame = $"SEND\n" +
|
||||
$"destination:/app/device/status\n" +
|
||||
$"content-type:application/json\n" +
|
||||
$"content-length:{Encoding.UTF8.GetByteCount(json)}\n\n" +
|
||||
$"{json}\0";
|
||||
await SendFrameAsync(frame, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ReceiveLoopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
if (_socket == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var buffer = new byte[8192];
|
||||
while (_socket.State == WebSocketState.Open && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
WebSocketReceiveResult result;
|
||||
do
|
||||
{
|
||||
result = await _socket.ReceiveAsync(new ArraySegment<byte>(buffer), cancellationToken).ConfigureAwait(false);
|
||||
if (result.MessageType == WebSocketMessageType.Close)
|
||||
{
|
||||
_eventAggregator.GetEvent<NetworkStatusChangedEvent>().Publish(new NetworkStatusChangedPayload
|
||||
{
|
||||
IsOnline = false,
|
||||
ChangedAt = DateTime.UtcNow
|
||||
});
|
||||
await ConnectUnifiedDeviceChannelAsync(cancellationToken).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
ms.Write(buffer, 0, result.Count);
|
||||
} while (!result.EndOfMessage);
|
||||
|
||||
var text = Encoding.UTF8.GetString(ms.ToArray());
|
||||
if (!text.StartsWith("MESSAGE", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var idx = text.IndexOf("\n\n", StringComparison.Ordinal);
|
||||
if (idx < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var body = text[(idx + 2)..].TrimEnd('\0');
|
||||
_eventAggregator.GetEvent<RemoteCommandReceivedEvent>().Publish(new RemoteCommandPayload
|
||||
{
|
||||
DeviceId = _deviceId,
|
||||
CommandJson = body
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async Task SendFrameAsync(string frame, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_socket == null || _socket.State != WebSocketState.Open)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var data = Encoding.UTF8.GetBytes(frame);
|
||||
await _socket.SendAsync(new ArraySegment<byte>(data), WebSocketMessageType.Text, true, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private string ResolveWsUrl()
|
||||
{
|
||||
var baseUrl = _configuration.GetValue<string>("JeecgIntegration:BaseUrl")?.TrimEnd('/');
|
||||
if (string.IsNullOrWhiteSpace(baseUrl))
|
||||
{
|
||||
return "ws://127.0.0.1:8080/jeecg-boot/ws/device/websocket";
|
||||
}
|
||||
if (baseUrl.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return "wss://" + baseUrl["https://".Length..] + "/ws/device/websocket";
|
||||
}
|
||||
return "ws://" + baseUrl["http://".Length..] + "/ws/device/websocket";
|
||||
}
|
||||
|
||||
private static string ResolveDeviceId(string token)
|
||||
{
|
||||
try
|
||||
{
|
||||
var parts = token.Split('.');
|
||||
if (parts.Length < 2)
|
||||
{
|
||||
return "default-device";
|
||||
}
|
||||
var payload = parts[1].Replace('-', '+').Replace('_', '/');
|
||||
payload = payload.PadRight(payload.Length + (4 - payload.Length % 4) % 4, '=');
|
||||
var json = Encoding.UTF8.GetString(Convert.FromBase64String(payload));
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(json);
|
||||
if (doc.RootElement.TryGetProperty("deviceId", out var deviceId))
|
||||
{
|
||||
return deviceId.GetString() ?? "default-device";
|
||||
}
|
||||
if (doc.RootElement.TryGetProperty("username", out var username))
|
||||
{
|
||||
return username.GetString() ?? "default-device";
|
||||
}
|
||||
return "default-device";
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "default-device";
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildConnectFrame(string token)
|
||||
{
|
||||
return "CONNECT\n" +
|
||||
"accept-version:1.2\n" +
|
||||
"heart-beat:10000,10000\n" +
|
||||
$"Authorization:Bearer {token}\n\n\0";
|
||||
}
|
||||
|
||||
private static string BuildSubscribeFrame(string subscriptionId, string destination)
|
||||
{
|
||||
return "SUBSCRIBE\n" +
|
||||
$"id:{subscriptionId}\n" +
|
||||
$"destination:{destination}\n" +
|
||||
"ack:auto\n\n\0";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user