|
|
@@ -0,0 +1,212 @@
|
|
|
+using System.Data;
|
|
|
+using System.Globalization;
|
|
|
+using System.Text.Json;
|
|
|
+using Admin.NET.Plugin.AiDOP.Entity.S8;
|
|
|
+using SqlSugar;
|
|
|
+
|
|
|
+namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules;
|
|
|
+
|
|
|
+/// <summary>
|
|
|
+/// R2 TIMEOUT 类规则 evaluator MVP。
|
|
|
+/// params_json 约定(首版最小集合):
|
|
|
+/// { dueAtField, statusField, completedStates[], objectCodeField, objectIdField, graceMinutes, exceptionTypeCode }
|
|
|
+/// 判定:dueAt <= now - graceMinutes && status NOT IN completedStates → HIT。
|
|
|
+/// 不做严重度阶梯、不做 SLA 升级、不做事件触发。
|
|
|
+/// </summary>
|
|
|
+public class S8TimeoutRuleEvaluator : IS8RuleEvaluator, ITransient
|
|
|
+{
|
|
|
+ public const string RuleTypeCode = "TIMEOUT";
|
|
|
+ public string RuleType => RuleTypeCode;
|
|
|
+
|
|
|
+ private const string SqlDataSourceType = "SQL";
|
|
|
+
|
|
|
+ private readonly SqlSugarRepository<AdoS8DataSource> _dataSourceRep;
|
|
|
+
|
|
|
+ public S8TimeoutRuleEvaluator(SqlSugarRepository<AdoS8DataSource> dataSourceRep)
|
|
|
+ {
|
|
|
+ _dataSourceRep = dataSourceRep;
|
|
|
+ }
|
|
|
+
|
|
|
+ public async Task<List<S8RuleHit>> EvaluateAsync(
|
|
|
+ long tenantId,
|
|
|
+ long factoryId,
|
|
|
+ AdoS8WatchRule rule,
|
|
|
+ IReadOnlyList<AdoS8AlertRule> alertRules,
|
|
|
+ CancellationToken cancellationToken = default)
|
|
|
+ {
|
|
|
+ var hits = new List<S8RuleHit>();
|
|
|
+
|
|
|
+ if (string.IsNullOrWhiteSpace(rule.Expression) || string.IsNullOrWhiteSpace(rule.ParamsJson))
|
|
|
+ return hits;
|
|
|
+
|
|
|
+ TimeoutParams parameters;
|
|
|
+ try { parameters = TimeoutParams.Parse(rule.ParamsJson!); }
|
|
|
+ catch { return hits; }
|
|
|
+
|
|
|
+ if (string.IsNullOrWhiteSpace(parameters.DueAtField)
|
|
|
+ || string.IsNullOrWhiteSpace(parameters.StatusField)
|
|
|
+ || string.IsNullOrWhiteSpace(parameters.ExceptionTypeCode))
|
|
|
+ return hits;
|
|
|
+
|
|
|
+ var dataSource = await _dataSourceRep.AsQueryable()
|
|
|
+ .Where(x => x.Id == rule.DataSourceId
|
|
|
+ && x.TenantId == tenantId
|
|
|
+ && x.FactoryId == factoryId
|
|
|
+ && x.Enabled)
|
|
|
+ .FirstAsync();
|
|
|
+ if (dataSource == null
|
|
|
+ || string.IsNullOrWhiteSpace(dataSource.Endpoint)
|
|
|
+ || !string.Equals(dataSource.Type?.Trim(), SqlDataSourceType, StringComparison.OrdinalIgnoreCase))
|
|
|
+ return hits;
|
|
|
+
|
|
|
+ DataTable table;
|
|
|
+ try
|
|
|
+ {
|
|
|
+ using var db = CreateSqlScope(dataSource.Endpoint!);
|
|
|
+ table = await db.Ado.GetDataTableAsync(rule.Expression!);
|
|
|
+ }
|
|
|
+ catch
|
|
|
+ {
|
|
|
+ return hits;
|
|
|
+ }
|
|
|
+
|
|
|
+ var detectedAt = DateTime.Now;
|
|
|
+ var threshold = detectedAt.AddMinutes(-parameters.GraceMinutes);
|
|
|
+ var sourceObjectType = string.IsNullOrWhiteSpace(rule.SourceObjectType)
|
|
|
+ ? rule.WatchObjectType
|
|
|
+ : rule.SourceObjectType!;
|
|
|
+
|
|
|
+ foreach (DataRow row in table.Rows)
|
|
|
+ {
|
|
|
+ var status = ReadString(row, parameters.StatusField!) ?? string.Empty;
|
|
|
+ if (parameters.CompletedStates.Contains(status, StringComparer.OrdinalIgnoreCase))
|
|
|
+ continue;
|
|
|
+
|
|
|
+ var due = ReadDateTime(row, parameters.DueAtField!);
|
|
|
+ if (due == null || due > threshold) continue;
|
|
|
+
|
|
|
+ var objectCodeField = string.IsNullOrWhiteSpace(parameters.ObjectCodeField)
|
|
|
+ ? "related_object_code"
|
|
|
+ : parameters.ObjectCodeField!;
|
|
|
+ var relatedObjectCode = ReadString(row, objectCodeField) ?? string.Empty;
|
|
|
+ if (string.IsNullOrWhiteSpace(relatedObjectCode)) continue;
|
|
|
+
|
|
|
+ var sourceObjectId = string.IsNullOrWhiteSpace(parameters.ObjectIdField)
|
|
|
+ ? relatedObjectCode
|
|
|
+ : ReadString(row, parameters.ObjectIdField!) ?? relatedObjectCode;
|
|
|
+
|
|
|
+ var dedupKey = BuildDedupKey(tenantId, factoryId, rule.RuleCode, sourceObjectType, sourceObjectId);
|
|
|
+
|
|
|
+ hits.Add(new S8RuleHit
|
|
|
+ {
|
|
|
+ SourceRuleId = rule.Id,
|
|
|
+ SourceRuleCode = rule.RuleCode,
|
|
|
+ SourceObjectType = sourceObjectType,
|
|
|
+ SourceObjectId = sourceObjectId,
|
|
|
+ RelatedObjectCode = relatedObjectCode,
|
|
|
+ ExceptionTypeCode = parameters.ExceptionTypeCode!,
|
|
|
+ SceneCode = rule.SceneCode,
|
|
|
+ Severity = string.IsNullOrWhiteSpace(rule.Severity) ? "MEDIUM" : rule.Severity,
|
|
|
+ DedupKey = dedupKey,
|
|
|
+ SourcePayload = BuildPayload(row, due.Value, status, parameters),
|
|
|
+ DetectedAt = detectedAt,
|
|
|
+ Title = $"[超时] {sourceObjectType} {sourceObjectId} 已超期至 {due.Value:yyyy-MM-dd HH:mm:ss}(状态 {status})",
|
|
|
+ DataSourceId = dataSource.Id,
|
|
|
+ OccurrenceDeptId = ReadLong(row, "occurrence_dept_id"),
|
|
|
+ ResponsibleDeptId = ReadLong(row, "responsible_dept_id")
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ return hits;
|
|
|
+ }
|
|
|
+
|
|
|
+ private SqlSugarScope CreateSqlScope(string connectionString)
|
|
|
+ {
|
|
|
+ var dbType = _dataSourceRep.Context.CurrentConnectionConfig.DbType;
|
|
|
+ return new SqlSugarScope(new ConnectionConfig
|
|
|
+ {
|
|
|
+ ConfigId = $"s8-timeout-eval-{Guid.NewGuid():N}",
|
|
|
+ DbType = dbType,
|
|
|
+ ConnectionString = connectionString,
|
|
|
+ InitKeyType = InitKeyType.Attribute,
|
|
|
+ IsAutoCloseConnection = true
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string BuildDedupKey(long tenantId, long factoryId, string ruleCode, string sourceObjectType, string sourceObjectId) =>
|
|
|
+ $"T{tenantId}:F{factoryId}:R{ruleCode}:{sourceObjectType}:{sourceObjectId}";
|
|
|
+
|
|
|
+ private static string BuildPayload(DataRow row, DateTime dueAt, string status, TimeoutParams parameters)
|
|
|
+ {
|
|
|
+ var payload = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
|
|
|
+ foreach (DataColumn column in row.Table.Columns)
|
|
|
+ {
|
|
|
+ var value = row[column];
|
|
|
+ payload[column.ColumnName] = value == DBNull.Value ? null : value;
|
|
|
+ }
|
|
|
+ payload["__ruleType"] = RuleTypeCode;
|
|
|
+ payload["__dueAt"] = dueAt;
|
|
|
+ payload["__status"] = status;
|
|
|
+ payload["__graceMinutes"] = parameters.GraceMinutes;
|
|
|
+ payload["__sourceObjectType"] = parameters.ObjectIdField;
|
|
|
+ return JsonSerializer.Serialize(payload);
|
|
|
+ }
|
|
|
+
|
|
|
+ private static string? ReadString(DataRow row, string columnName)
|
|
|
+ {
|
|
|
+ if (string.IsNullOrWhiteSpace(columnName)) return null;
|
|
|
+ if (!row.Table.Columns.Contains(columnName)) return null;
|
|
|
+ var v = row[columnName];
|
|
|
+ return v == DBNull.Value ? null : Convert.ToString(v)?.Trim();
|
|
|
+ }
|
|
|
+
|
|
|
+ private static DateTime? ReadDateTime(DataRow row, string columnName)
|
|
|
+ {
|
|
|
+ if (!row.Table.Columns.Contains(columnName)) return null;
|
|
|
+ var v = row[columnName];
|
|
|
+ if (v == DBNull.Value) return null;
|
|
|
+ if (v is DateTime dt) return dt;
|
|
|
+ return DateTime.TryParse(Convert.ToString(v, CultureInfo.InvariantCulture), out var p) ? p : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static long? ReadLong(DataRow row, string columnName)
|
|
|
+ {
|
|
|
+ if (!row.Table.Columns.Contains(columnName)) return null;
|
|
|
+ var v = row[columnName];
|
|
|
+ if (v == DBNull.Value) return null;
|
|
|
+ return long.TryParse(Convert.ToString(v, CultureInfo.InvariantCulture), out var r) ? r : null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private sealed class TimeoutParams
|
|
|
+ {
|
|
|
+ public string? DueAtField { get; set; }
|
|
|
+ public string? StatusField { get; set; }
|
|
|
+ public List<string> CompletedStates { get; set; } = new();
|
|
|
+ public string? ObjectCodeField { get; set; }
|
|
|
+ public string? ObjectIdField { get; set; }
|
|
|
+ public int GraceMinutes { get; set; }
|
|
|
+ public string? ExceptionTypeCode { get; set; }
|
|
|
+
|
|
|
+ public static TimeoutParams Parse(string json)
|
|
|
+ {
|
|
|
+ using var doc = JsonDocument.Parse(json);
|
|
|
+ var root = doc.RootElement;
|
|
|
+ var p = new TimeoutParams();
|
|
|
+ if (root.TryGetProperty("dueAtField", out var v1)) p.DueAtField = v1.GetString();
|
|
|
+ if (root.TryGetProperty("statusField", out var v2)) p.StatusField = v2.GetString();
|
|
|
+ if (root.TryGetProperty("objectCodeField", out var v3)) p.ObjectCodeField = v3.GetString();
|
|
|
+ if (root.TryGetProperty("objectIdField", out var v4)) p.ObjectIdField = v4.GetString();
|
|
|
+ if (root.TryGetProperty("graceMinutes", out var v5) && v5.ValueKind == JsonValueKind.Number) p.GraceMinutes = v5.GetInt32();
|
|
|
+ if (root.TryGetProperty("exceptionTypeCode", out var v6)) p.ExceptionTypeCode = v6.GetString();
|
|
|
+ if (root.TryGetProperty("completedStates", out var v7) && v7.ValueKind == JsonValueKind.Array)
|
|
|
+ {
|
|
|
+ foreach (var s in v7.EnumerateArray())
|
|
|
+ {
|
|
|
+ var str = s.GetString();
|
|
|
+ if (!string.IsNullOrWhiteSpace(str)) p.CompletedStates.Add(str!);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return p;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|