Files
qhmes/yy-admin-master/YY.Admin.Services/Service/Jeecg/JeecgLoginLogReportService.cs

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;
}
}