using System.Net.Http; using System.Net.Http.Json; using System.Text; using System.Text.Json; using System.Text.RegularExpressions; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Configuration; using Prism.Events; using YY.Admin.Core.Events; using YY.Admin.Core.Services; namespace YY.Admin.Services.Service.EquipmentDb; /// /// 订阅 MES 下发的 EQ_* 命令,执行设备库 SQL 并回传结果。 /// public sealed class EquipmentDbCommandHandler { private static readonly JsonSerializerOptions JsonOptions = new() { PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase }; private readonly IEquipmentDbConnectionFactory _connectionFactory; private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; private readonly ISignalRService _signalRService; private readonly HashSet _processedRequestIds = new(); private readonly object _dedupeLock = new(); public EquipmentDbCommandHandler( IEventAggregator eventAggregator, IEquipmentDbConnectionFactory connectionFactory, IHttpClientFactory httpClientFactory, IConfiguration configuration, ISignalRService signalRService) { _connectionFactory = connectionFactory; _httpClientFactory = httpClientFactory; _configuration = configuration; _signalRService = signalRService; eventAggregator.GetEvent() .Subscribe(OnRemoteCommand, ThreadOption.BackgroundThread); } private void OnRemoteCommand(RemoteCommandPayload payload) { if (payload == null || string.IsNullOrWhiteSpace(payload.CommandJson)) { return; } try { using var doc = JsonDocument.Parse(payload.CommandJson); if (!doc.RootElement.TryGetProperty("cmd", out var cmdEl)) { return; } var cmd = cmdEl.GetString() ?? string.Empty; if (!cmd.StartsWith("EQ_", StringComparison.OrdinalIgnoreCase)) { return; } var request = JsonSerializer.Deserialize(payload.CommandJson, JsonOptions); if (request == null) { return; } _ = Task.Run(() => HandleAsync(request, CancellationToken.None)); } catch { // ignore non-EQ payloads } } public async Task HandleAsync(EquipmentDbCommandDto request, CancellationToken cancellationToken) { if (request == null || string.IsNullOrWhiteSpace(request.RequestId)) { return; } lock (_dedupeLock) { if (!_processedRequestIds.Add(request.RequestId)) { return; } if (_processedRequestIds.Count > 2000) { _processedRequestIds.Clear(); _processedRequestIds.Add(request.RequestId); } } var result = new EquipmentDbResultDto { RequestId = request.RequestId, DeviceId = ResolveDeviceId(request.DeviceId), Cmd = request.Cmd, RespondedAt = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; try { switch ((request.Cmd ?? string.Empty).ToUpperInvariant()) { case "EQ_TEST": await HandleTestAsync(result, cancellationToken).ConfigureAwait(false); break; case "EQ_QUERY": case "EQ_SYNC_PULL": await HandleQueryAsync(request, result, cancellationToken).ConfigureAwait(false); break; case "EQ_WRITE": await HandleWriteAsync(request, result, cancellationToken).ConfigureAwait(false); break; case "EQ_SYNC_PUSH": await HandleSyncPushAsync(request, result, cancellationToken).ConfigureAwait(false); break; default: result.Success = false; result.Error = "未知命令: " + request.Cmd; break; } } catch (Exception ex) { result.Success = false; result.Error = ex.Message; result.DbConnected = false; result.DbMessage = ex.Message; } await SendResultAsync(result, cancellationToken).ConfigureAwait(false); } private async Task HandleTestAsync(EquipmentDbResultDto result, CancellationToken cancellationToken) { var (ok, message) = await _connectionFactory.TestAsync(cancellationToken).ConfigureAwait(false); result.Success = ok; result.DbConnected = ok; result.DbMessage = message; result.Error = ok ? null : message; await ReportStatusAsync(ok, message, cancellationToken).ConfigureAwait(false); } private async Task HandleQueryAsync(EquipmentDbCommandDto request, EquipmentDbResultDto result, CancellationToken cancellationToken) { EquipmentDbWhitelist.ValidateSql(request.Sql, request.Table); await using var conn = _connectionFactory.CreateConnection(); await conn.OpenAsync(cancellationToken).ConfigureAwait(false); await using var cmd = conn.CreateCommand(); cmd.CommandText = request.Sql!; cmd.CommandTimeout = 60; BindParams(cmd, request.Params); var rows = new List>(); await using var reader = await cmd.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { var row = new Dictionary(StringComparer.OrdinalIgnoreCase); for (var i = 0; i < reader.FieldCount; i++) { object? val = reader.IsDBNull(i) ? null : reader.GetValue(i); if (val is DateTime dt) { val = dt.ToString("yyyy-MM-dd HH:mm:ss"); } row[reader.GetName(i)] = val; } rows.Add(row); } result.Success = true; result.Rows = rows; result.Affected = rows.Count; result.DbConnected = true; result.DbMessage = "ok"; } private async Task HandleWriteAsync(EquipmentDbCommandDto request, EquipmentDbResultDto result, CancellationToken cancellationToken) { EquipmentDbWhitelist.ValidateSql(request.Sql, request.Table); await using var conn = _connectionFactory.CreateConnection(); await conn.OpenAsync(cancellationToken).ConfigureAwait(false); await using var cmd = conn.CreateCommand(); cmd.CommandText = request.Sql!; cmd.CommandTimeout = 60; BindParams(cmd, request.Params); var affected = await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); result.Success = true; result.Affected = affected; result.DbConnected = true; result.DbMessage = "ok"; } private async Task HandleSyncPushAsync(EquipmentDbCommandDto request, EquipmentDbResultDto result, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(request.Table) || !EquipmentDbWhitelist.IsAllowedTable(request.Table)) { throw new InvalidOperationException("EQ_SYNC_PUSH 表不在白名单"); } if (request.Payload == null || request.Payload.Count == 0) { throw new InvalidOperationException("EQ_SYNC_PUSH payload 为空"); } request.Payload.TryGetValue("mode", out var modeObj); var mode = Convert.ToString(modeObj)?.ToLowerInvariant() ?? "insert"; var rows = ExtractRows(request.Payload); if (rows.Count == 0) { result.Success = true; result.Affected = 0; result.DbConnected = true; return; } List? keys = null; if (request.Payload.TryGetValue("keys", out var keysObj) && keysObj != null) { keys = JsonSerializer.Deserialize>(JsonSerializer.Serialize(keysObj), JsonOptions); } await using var conn = _connectionFactory.CreateConnection(); await conn.OpenAsync(cancellationToken).ConfigureAwait(false); var affected = 0; foreach (var row in rows) { if (mode == "upsert" && keys != null && keys.Count > 0) { affected += await UpsertRowAsync(conn, request.Table!, row, keys, cancellationToken).ConfigureAwait(false); } else if (mode == "update" && keys != null && keys.Count > 0) { affected += await UpdateRowAsync(conn, request.Table!, row, keys, cancellationToken).ConfigureAwait(false); } else { affected += await InsertRowAsync(conn, request.Table!, row, cancellationToken).ConfigureAwait(false); } } result.Success = true; result.Affected = affected; result.DbConnected = true; result.DbMessage = "ok"; } private static List> ExtractRows(Dictionary payload) { if (!payload.TryGetValue("rows", out var rowsObj) || rowsObj == null) { throw new InvalidOperationException("EQ_SYNC_PUSH 缺少 rows"); } if (rowsObj is JsonElement je) { return JsonSerializer.Deserialize>>(je.GetRawText(), JsonOptions) ?? new List>(); } return JsonSerializer.Deserialize>>(JsonSerializer.Serialize(rowsObj), JsonOptions) ?? new List>(); } private static async Task InsertRowAsync( SqlConnection conn, string table, Dictionary row, CancellationToken cancellationToken) { var cols = row.Keys.ToList(); foreach (var c in cols) { if (!Regex.IsMatch(c, @"^[A-Za-z0-9_]+$")) { throw new InvalidOperationException("非法列名: " + c); } } var colList = string.Join(",", cols.Select(c => "[" + c + "]")); var paramList = string.Join(",", cols.Select((_, i) => "@p" + i)); await using var cmd = conn.CreateCommand(); cmd.CommandText = $"INSERT INTO [{table}] ({colList}) VALUES ({paramList})"; for (var i = 0; i < cols.Count; i++) { cmd.Parameters.AddWithValue("@p" + i, ToDbValue(row[cols[i]])); } return await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } private static async Task UpdateRowAsync( SqlConnection conn, string table, Dictionary row, List keys, CancellationToken cancellationToken) { var setCols = row.Keys.Where(k => !keys.Contains(k, StringComparer.OrdinalIgnoreCase)).ToList(); if (setCols.Count == 0) { return 0; } var sets = string.Join(",", setCols.Select((c, i) => $"[{c}]=@s{i}")); var wheres = string.Join(" AND ", keys.Select((k, i) => $"[{k}]=@k{i}")); await using var cmd = conn.CreateCommand(); cmd.CommandText = $"UPDATE [{table}] SET {sets} WHERE {wheres}"; for (var i = 0; i < setCols.Count; i++) { cmd.Parameters.AddWithValue("@s" + i, ToDbValue(GetIgnoreCase(row, setCols[i]))); } for (var i = 0; i < keys.Count; i++) { cmd.Parameters.AddWithValue("@k" + i, ToDbValue(GetIgnoreCase(row, keys[i]))); } return await cmd.ExecuteNonQueryAsync(cancellationToken).ConfigureAwait(false); } private static async Task UpsertRowAsync( SqlConnection conn, string table, Dictionary row, List keys, CancellationToken cancellationToken) { var updated = await UpdateRowAsync(conn, table, row, keys, cancellationToken).ConfigureAwait(false); if (updated > 0) { return updated; } return await InsertRowAsync(conn, table, row, cancellationToken).ConfigureAwait(false); } private static object? GetIgnoreCase(Dictionary row, string key) { if (row.TryGetValue(key, out var v)) { return v; } foreach (var kv in row) { if (string.Equals(kv.Key, key, StringComparison.OrdinalIgnoreCase)) { return kv.Value; } } return null; } private static object ToDbValue(object? value) { if (value == null) { return DBNull.Value; } if (value is JsonElement je) { return je.ValueKind switch { JsonValueKind.Null => DBNull.Value, JsonValueKind.String => je.GetString() ?? (object)DBNull.Value, JsonValueKind.Number => je.TryGetInt64(out var l) ? l : je.GetDouble(), JsonValueKind.True => true, JsonValueKind.False => false, _ => je.ToString() }; } return value; } private static void BindParams(SqlCommand cmd, List? parameters) { if (parameters == null) { return; } for (var i = 0; i < parameters.Count; i++) { cmd.Parameters.AddWithValue("@p" + i, ToDbValue(parameters[i])); } if (cmd.CommandText.Contains('?')) { var sb = new StringBuilder(); var idx = 0; foreach (var ch in cmd.CommandText) { if (ch == '?') { sb.Append("@p").Append(idx++); } else { sb.Append(ch); } } cmd.CommandText = sb.ToString(); } } private async Task SendResultAsync(EquipmentDbResultDto result, CancellationToken cancellationToken) { var json = JsonSerializer.Serialize(result, JsonOptions); var useRest = json.Length > 200_000 || (result.Rows?.Count ?? 0) > 500; if (useRest) { try { var client = _httpClientFactory.CreateClient("JeecgApi"); result.LastChunk = true; result.ChunkIndex = 0; result.ChunkTotal = 1; var resp = await client.PostAsJsonAsync("/xslmes/deviceAgent/anon/resultChunk", result, cancellationToken) .ConfigureAwait(false); resp.EnsureSuccessStatusCode(); return; } catch { // fall through } } if (_signalRService is IEquipmentDbStompSender sender) { await sender.SendAgentResultAsync(result, cancellationToken).ConfigureAwait(false); } } public async Task ReportStatusAsync(bool dbConnected, string? dbMessage, CancellationToken cancellationToken = default) { var deviceId = ResolveDeviceId(null); var status = new Dictionary { ["deviceId"] = deviceId, ["hostName"] = Environment.MachineName, ["dbConnected"] = dbConnected, ["dbMessage"] = dbMessage, ["ts"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }; if (_signalRService is IEquipmentDbStompSender sender) { await sender.SendAgentStatusAsync(status, cancellationToken).ConfigureAwait(false); } try { var client = _httpClientFactory.CreateClient("JeecgApi"); await client.PostAsJsonAsync("/xslmes/deviceAgent/anon/heartbeat", status, cancellationToken).ConfigureAwait(false); } catch { // ignore } } private string ResolveDeviceId(string? fromRequest) { if (!string.IsNullOrWhiteSpace(fromRequest)) { return fromRequest!; } var settings = _connectionFactory.CurrentSettings; if (!string.IsNullOrWhiteSpace(settings.DeviceId)) { return settings.DeviceId; } return _configuration.GetValue("EquipmentDb:DeviceId") ?? Environment.MachineName; } } public sealed class EquipmentDbCommandDto { public string? RequestId { get; set; } public string? Cmd { get; set; } public string? DeviceId { get; set; } public string? Table { get; set; } public string? Sql { get; set; } public string? SqlKey { get; set; } public List? Params { get; set; } public Dictionary? Payload { get; set; } public int? TimeoutMs { get; set; } } public sealed class EquipmentDbResultDto { public string? RequestId { get; set; } public string? DeviceId { get; set; } public string? Cmd { get; set; } public bool Success { get; set; } public string? Error { get; set; } public int? Affected { get; set; } public List>? Rows { get; set; } public bool? DbConnected { get; set; } public string? DbMessage { get; set; } public long? RespondedAt { get; set; } public int? ChunkIndex { get; set; } public int? ChunkTotal { get; set; } public bool? LastChunk { get; set; } }