290 lines
8.7 KiB
C#
290 lines
8.7 KiB
C#
using Microsoft.Extensions.Configuration;
|
|
using System.Net.Http;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using YY.Admin.Core.Util;
|
|
|
|
namespace YY.Admin.Services.Service.Jeecg;
|
|
|
|
/// <summary>
|
|
/// 登录日志上报:
|
|
/// 1) WebSocket 优先;
|
|
/// 2) 失败自动 HTTP 兜底;
|
|
/// 3) 仍失败则写本地队列,后台自动补传。
|
|
/// </summary>
|
|
public class JeecgLoginLogReportService : IJeecgLoginLogReportService, IClientLogReportSink, ISingletonDependency
|
|
{
|
|
private readonly IConfiguration _configuration;
|
|
private readonly IJeecgBackendGateway _jeecgBackendGateway;
|
|
private readonly HttpClient _httpClient;
|
|
private readonly SemaphoreSlim _queueLock = new(1, 1);
|
|
private readonly CancellationTokenSource _syncCts = new();
|
|
private int _started;
|
|
|
|
private sealed class LoginLogQueueItem
|
|
{
|
|
public string Id { get; set; } = Guid.NewGuid().ToString("N");
|
|
public string Category { get; set; } = "LOGIN";
|
|
public string Account { get; set; } = string.Empty;
|
|
public bool? Success { get; set; }
|
|
public string Message { get; set; } = string.Empty;
|
|
public string? Exception { get; set; }
|
|
public int LogType { get; set; } = 1;
|
|
public int OperateType { get; set; } = 5;
|
|
public string Method { get; set; } = "LOGIN";
|
|
public string RequestUrl { get; set; } = "/desktop/log";
|
|
public long Timestamp { get; set; } = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
|
public string ClientType { get; set; } = "gkj";
|
|
}
|
|
|
|
public JeecgLoginLogReportService(
|
|
IConfiguration configuration,
|
|
IJeecgBackendGateway jeecgBackendGateway,
|
|
HttpClient httpClient)
|
|
{
|
|
_configuration = configuration;
|
|
_jeecgBackendGateway = jeecgBackendGateway;
|
|
_httpClient = httpClient;
|
|
}
|
|
|
|
public void StartBackgroundSync()
|
|
{
|
|
if (Interlocked.Exchange(ref _started, 1) == 1)
|
|
{
|
|
return;
|
|
}
|
|
|
|
_ = Task.Run(() => BackgroundSyncLoopAsync(_syncCts.Token), _syncCts.Token);
|
|
}
|
|
|
|
public async Task ReportLoginAsync(string account, bool success, string message, CancellationToken cancellationToken = default)
|
|
{
|
|
await ReportLogAsync("LOGIN", message, account, success, null, cancellationToken);
|
|
}
|
|
|
|
public async Task ReportLogAsync(
|
|
string category,
|
|
string message,
|
|
string? account = null,
|
|
bool? success = null,
|
|
string? exception = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
var normalizedCategory = string.IsNullOrWhiteSpace(category) ? "OPERATION" : category.Trim().ToUpperInvariant();
|
|
var item = BuildQueueItem(normalizedCategory, account, success, message, exception);
|
|
if (await TrySendToBackendAsync(item, cancellationToken))
|
|
{
|
|
return;
|
|
}
|
|
|
|
await EnqueueAsync(item, cancellationToken);
|
|
}
|
|
|
|
private async Task BackgroundSyncLoopAsync(CancellationToken cancellationToken)
|
|
{
|
|
while (!cancellationToken.IsCancellationRequested)
|
|
{
|
|
try
|
|
{
|
|
await FlushQueueAsync(cancellationToken);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"日志离线补传失败: {ex.Message}");
|
|
}
|
|
|
|
try
|
|
{
|
|
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task<bool> TrySendToBackendAsync(LoginLogQueueItem item, CancellationToken cancellationToken)
|
|
{
|
|
var payload = new
|
|
{
|
|
cmd = "SCADA_LOG",
|
|
category = item.Category,
|
|
account = item.Account,
|
|
success = item.Success,
|
|
message = item.Message,
|
|
exception = item.Exception,
|
|
logType = item.LogType,
|
|
operateType = item.OperateType,
|
|
method = item.Method,
|
|
requestUrl = item.RequestUrl,
|
|
clientType = item.ClientType,
|
|
timestamp = item.Timestamp
|
|
};
|
|
var json = JsonSerializer.Serialize(payload);
|
|
|
|
if (await _jeecgBackendGateway.SendWebSocketOneShotAsync(json, cancellationToken))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var baseUrl = _configuration.GetValue<string>("JeecgIntegration:BaseUrl")?.TrimEnd('/');
|
|
if (string.IsNullOrWhiteSpace(baseUrl))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var url = $"{baseUrl}/sys/log/scada/addLoginLog";
|
|
using var req = new HttpRequestMessage(HttpMethod.Post, url)
|
|
{
|
|
Content = new StringContent(json, Encoding.UTF8, "application/json")
|
|
};
|
|
|
|
try
|
|
{
|
|
using var resp = await _httpClient.SendAsync(req, cancellationToken);
|
|
return resp.IsSuccessStatusCode;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async Task EnqueueAsync(LoginLogQueueItem item, CancellationToken cancellationToken)
|
|
{
|
|
await _queueLock.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
var lines = new List<string>();
|
|
var path = GetQueuePath();
|
|
if (File.Exists(path))
|
|
{
|
|
lines = File.ReadAllLines(path).Where(l => !string.IsNullOrWhiteSpace(l)).ToList();
|
|
}
|
|
lines.Add(JsonSerializer.Serialize(item));
|
|
EnsureQueueDir(path);
|
|
File.WriteAllLines(path, lines);
|
|
}
|
|
finally
|
|
{
|
|
_queueLock.Release();
|
|
}
|
|
}
|
|
|
|
private async Task FlushQueueAsync(CancellationToken cancellationToken)
|
|
{
|
|
await _queueLock.WaitAsync(cancellationToken);
|
|
try
|
|
{
|
|
var path = GetQueuePath();
|
|
if (!File.Exists(path))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var lines = File.ReadAllLines(path).Where(l => !string.IsNullOrWhiteSpace(l)).ToList();
|
|
if (lines.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var remain = new List<string>();
|
|
foreach (var line in lines)
|
|
{
|
|
LoginLogQueueItem? item = null;
|
|
try
|
|
{
|
|
item = JsonSerializer.Deserialize<LoginLogQueueItem>(line);
|
|
}
|
|
catch
|
|
{
|
|
// 解析失败的数据直接丢弃,避免阻塞后续
|
|
continue;
|
|
}
|
|
|
|
if (item == null || !await TrySendToBackendAsync(item, cancellationToken))
|
|
{
|
|
remain.Add(line);
|
|
}
|
|
}
|
|
|
|
if (remain.Count == 0)
|
|
{
|
|
File.Delete(path);
|
|
}
|
|
else
|
|
{
|
|
File.WriteAllLines(path, remain);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_queueLock.Release();
|
|
}
|
|
}
|
|
|
|
private static string GetQueuePath()
|
|
{
|
|
var dir = AppWritablePaths.EnsureDirectoryExists(AppWritablePaths.AccountSettingsRootDirectory);
|
|
return Path.Combine(dir, "offline-scada-log-queue.jsonl");
|
|
}
|
|
|
|
private static void EnsureQueueDir(string path)
|
|
{
|
|
var dir = Path.GetDirectoryName(path);
|
|
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir))
|
|
{
|
|
Directory.CreateDirectory(dir);
|
|
}
|
|
}
|
|
|
|
private static LoginLogQueueItem BuildQueueItem(
|
|
string category,
|
|
string? account,
|
|
bool? success,
|
|
string? message,
|
|
string? exception)
|
|
{
|
|
var item = new LoginLogQueueItem
|
|
{
|
|
Category = category,
|
|
Account = account ?? string.Empty,
|
|
Success = success,
|
|
Message = message ?? string.Empty,
|
|
Exception = exception
|
|
};
|
|
|
|
switch (category)
|
|
{
|
|
case "LOGIN":
|
|
item.LogType = 1;
|
|
item.OperateType = 1;
|
|
item.Method = "LOGIN";
|
|
item.RequestUrl = "/desktop/login";
|
|
break;
|
|
case "EXCEPTION":
|
|
item.LogType = 2;
|
|
item.OperateType = 5;
|
|
item.Method = "ERROR";
|
|
item.RequestUrl = "/desktop/exception";
|
|
break;
|
|
case "WARNING":
|
|
item.LogType = 2;
|
|
item.OperateType = 4;
|
|
item.Method = "WARNING";
|
|
item.RequestUrl = "/desktop/warning";
|
|
break;
|
|
default:
|
|
item.LogType = 1;
|
|
item.OperateType = 5;
|
|
item.Method = "OPERATION";
|
|
item.RequestUrl = "/desktop/operation";
|
|
break;
|
|
}
|
|
|
|
return item;
|
|
}
|
|
}
|