S8TimeoutRuleEvaluator.cs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. using System.Data;
  2. using System.Globalization;
  3. using System.Text.Json;
  4. using Admin.NET.Plugin.AiDOP.Entity.S8;
  5. using SqlSugar;
  6. namespace Admin.NET.Plugin.AiDOP.Service.S8.Rules;
  7. /// <summary>
  8. /// R2 TIMEOUT 类规则 evaluator MVP。
  9. /// params_json 约定(首版最小集合):
  10. /// { dueAtField, statusField, completedStates[], objectCodeField, objectIdField, graceMinutes, exceptionTypeCode }
  11. /// 判定:dueAt &lt;= now - graceMinutes && status NOT IN completedStates → HIT。
  12. /// 不做严重度阶梯、不做 SLA 升级、不做事件触发。
  13. /// </summary>
  14. public class S8TimeoutRuleEvaluator : IS8RuleEvaluator, ITransient
  15. {
  16. public const string RuleTypeCode = "TIMEOUT";
  17. public string RuleType => RuleTypeCode;
  18. private const string SqlDataSourceType = "SQL";
  19. private readonly SqlSugarRepository<AdoS8DataSource> _dataSourceRep;
  20. public S8TimeoutRuleEvaluator(SqlSugarRepository<AdoS8DataSource> dataSourceRep)
  21. {
  22. _dataSourceRep = dataSourceRep;
  23. }
  24. public async Task<List<S8RuleHit>> EvaluateAsync(
  25. long tenantId,
  26. long factoryId,
  27. AdoS8WatchRule rule,
  28. IReadOnlyList<AdoS8AlertRule> alertRules,
  29. CancellationToken cancellationToken = default)
  30. {
  31. var hits = new List<S8RuleHit>();
  32. if (string.IsNullOrWhiteSpace(rule.Expression) || string.IsNullOrWhiteSpace(rule.ParamsJson))
  33. return hits;
  34. TimeoutParams parameters;
  35. try { parameters = TimeoutParams.Parse(rule.ParamsJson!); }
  36. catch { return hits; }
  37. if (string.IsNullOrWhiteSpace(parameters.DueAtField)
  38. || string.IsNullOrWhiteSpace(parameters.StatusField)
  39. || string.IsNullOrWhiteSpace(parameters.ExceptionTypeCode))
  40. return hits;
  41. var dataSource = await _dataSourceRep.AsQueryable()
  42. .Where(x => x.Id == rule.DataSourceId
  43. && x.TenantId == tenantId
  44. && x.FactoryId == factoryId
  45. && x.Enabled)
  46. .FirstAsync();
  47. if (dataSource == null
  48. || string.IsNullOrWhiteSpace(dataSource.Endpoint)
  49. || !string.Equals(dataSource.Type?.Trim(), SqlDataSourceType, StringComparison.OrdinalIgnoreCase))
  50. return hits;
  51. DataTable table;
  52. try
  53. {
  54. using var db = CreateSqlScope(dataSource.Endpoint!);
  55. table = await db.Ado.GetDataTableAsync(rule.Expression!);
  56. }
  57. catch
  58. {
  59. return hits;
  60. }
  61. var detectedAt = DateTime.Now;
  62. var threshold = detectedAt.AddMinutes(-parameters.GraceMinutes);
  63. var sourceObjectType = string.IsNullOrWhiteSpace(rule.SourceObjectType)
  64. ? rule.WatchObjectType
  65. : rule.SourceObjectType!;
  66. foreach (DataRow row in table.Rows)
  67. {
  68. var status = ReadString(row, parameters.StatusField!) ?? string.Empty;
  69. if (parameters.CompletedStates.Contains(status, StringComparer.OrdinalIgnoreCase))
  70. continue;
  71. var due = ReadDateTime(row, parameters.DueAtField!);
  72. if (due == null || due > threshold) continue;
  73. var objectCodeField = string.IsNullOrWhiteSpace(parameters.ObjectCodeField)
  74. ? "related_object_code"
  75. : parameters.ObjectCodeField!;
  76. var relatedObjectCode = ReadString(row, objectCodeField) ?? string.Empty;
  77. if (string.IsNullOrWhiteSpace(relatedObjectCode)) continue;
  78. var sourceObjectId = string.IsNullOrWhiteSpace(parameters.ObjectIdField)
  79. ? relatedObjectCode
  80. : ReadString(row, parameters.ObjectIdField!) ?? relatedObjectCode;
  81. var dedupKey = BuildDedupKey(tenantId, factoryId, rule.RuleCode, sourceObjectType, sourceObjectId);
  82. hits.Add(new S8RuleHit
  83. {
  84. SourceRuleId = rule.Id,
  85. SourceRuleCode = rule.RuleCode,
  86. SourceObjectType = sourceObjectType,
  87. SourceObjectId = sourceObjectId,
  88. RelatedObjectCode = relatedObjectCode,
  89. ExceptionTypeCode = parameters.ExceptionTypeCode!,
  90. SceneCode = rule.SceneCode,
  91. Severity = string.IsNullOrWhiteSpace(rule.Severity) ? "MEDIUM" : rule.Severity,
  92. DedupKey = dedupKey,
  93. SourcePayload = BuildPayload(row, due.Value, status, parameters),
  94. DetectedAt = detectedAt,
  95. Title = $"[超时] {sourceObjectType} {sourceObjectId} 已超期至 {due.Value:yyyy-MM-dd HH:mm:ss}(状态 {status})",
  96. DataSourceId = dataSource.Id,
  97. OccurrenceDeptId = ReadLong(row, "occurrence_dept_id"),
  98. ResponsibleDeptId = ReadLong(row, "responsible_dept_id")
  99. });
  100. }
  101. return hits;
  102. }
  103. private SqlSugarScope CreateSqlScope(string connectionString)
  104. {
  105. var dbType = _dataSourceRep.Context.CurrentConnectionConfig.DbType;
  106. return new SqlSugarScope(new ConnectionConfig
  107. {
  108. ConfigId = $"s8-timeout-eval-{Guid.NewGuid():N}",
  109. DbType = dbType,
  110. ConnectionString = connectionString,
  111. InitKeyType = InitKeyType.Attribute,
  112. IsAutoCloseConnection = true
  113. });
  114. }
  115. private static string BuildDedupKey(long tenantId, long factoryId, string ruleCode, string sourceObjectType, string sourceObjectId) =>
  116. $"T{tenantId}:F{factoryId}:R{ruleCode}:{sourceObjectType}:{sourceObjectId}";
  117. private static string BuildPayload(DataRow row, DateTime dueAt, string status, TimeoutParams parameters)
  118. {
  119. var payload = new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase);
  120. foreach (DataColumn column in row.Table.Columns)
  121. {
  122. var value = row[column];
  123. payload[column.ColumnName] = value == DBNull.Value ? null : value;
  124. }
  125. payload["__ruleType"] = RuleTypeCode;
  126. payload["__dueAt"] = dueAt;
  127. payload["__status"] = status;
  128. payload["__graceMinutes"] = parameters.GraceMinutes;
  129. payload["__sourceObjectType"] = parameters.ObjectIdField;
  130. return JsonSerializer.Serialize(payload);
  131. }
  132. private static string? ReadString(DataRow row, string columnName)
  133. {
  134. if (string.IsNullOrWhiteSpace(columnName)) return null;
  135. if (!row.Table.Columns.Contains(columnName)) return null;
  136. var v = row[columnName];
  137. return v == DBNull.Value ? null : Convert.ToString(v)?.Trim();
  138. }
  139. private static DateTime? ReadDateTime(DataRow row, string columnName)
  140. {
  141. if (!row.Table.Columns.Contains(columnName)) return null;
  142. var v = row[columnName];
  143. if (v == DBNull.Value) return null;
  144. if (v is DateTime dt) return dt;
  145. return DateTime.TryParse(Convert.ToString(v, CultureInfo.InvariantCulture), out var p) ? p : null;
  146. }
  147. private static long? ReadLong(DataRow row, string columnName)
  148. {
  149. if (!row.Table.Columns.Contains(columnName)) return null;
  150. var v = row[columnName];
  151. if (v == DBNull.Value) return null;
  152. return long.TryParse(Convert.ToString(v, CultureInfo.InvariantCulture), out var r) ? r : null;
  153. }
  154. private sealed class TimeoutParams
  155. {
  156. public string? DueAtField { get; set; }
  157. public string? StatusField { get; set; }
  158. public List<string> CompletedStates { get; set; } = new();
  159. public string? ObjectCodeField { get; set; }
  160. public string? ObjectIdField { get; set; }
  161. public int GraceMinutes { get; set; }
  162. public string? ExceptionTypeCode { get; set; }
  163. public static TimeoutParams Parse(string json)
  164. {
  165. using var doc = JsonDocument.Parse(json);
  166. var root = doc.RootElement;
  167. var p = new TimeoutParams();
  168. if (root.TryGetProperty("dueAtField", out var v1)) p.DueAtField = v1.GetString();
  169. if (root.TryGetProperty("statusField", out var v2)) p.StatusField = v2.GetString();
  170. if (root.TryGetProperty("objectCodeField", out var v3)) p.ObjectCodeField = v3.GetString();
  171. if (root.TryGetProperty("objectIdField", out var v4)) p.ObjectIdField = v4.GetString();
  172. if (root.TryGetProperty("graceMinutes", out var v5) && v5.ValueKind == JsonValueKind.Number) p.GraceMinutes = v5.GetInt32();
  173. if (root.TryGetProperty("exceptionTypeCode", out var v6)) p.ExceptionTypeCode = v6.GetString();
  174. if (root.TryGetProperty("completedStates", out var v7) && v7.ValueKind == JsonValueKind.Array)
  175. {
  176. foreach (var s in v7.EnumerateArray())
  177. {
  178. var str = s.GetString();
  179. if (!string.IsNullOrWhiteSpace(str)) p.CompletedStates.Add(str!);
  180. }
  181. }
  182. return p;
  183. }
  184. }
  185. }