S8HomeAlertQueryService.cs 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. using Admin.NET.Plugin.AiDOP.Infrastructure.S8;
  2. using Admin.NET.Plugin.AiDOP.SmartOps;
  3. namespace Admin.NET.Plugin.AiDOP.Service.S8;
  4. /// <summary>
  5. /// 九宫格 S8 的当前异常与模块灯态。异常权威是 ado_s8_exception,监控权威是 ado_s8_watch_rule。
  6. /// </summary>
  7. public sealed class S8HomeAlertQueryService : ITransient
  8. {
  9. public const int RecentLimit = 6;
  10. public const int MinFreshWindowSeconds = 600;
  11. /// <summary>列表、汇总、趋势共用的业务维度过滤。只匹配异常权威表上的可追溯字段。</summary>
  12. public const string TextFilterSql = """
  13. AND (@product IS NULL OR IFNULL(title,'') LIKE CONCAT('%', @product, '%')
  14. OR IFNULL(description,'') LIKE CONCAT('%', @product, '%')
  15. OR IFNULL(related_object_code,'') LIKE CONCAT('%', @product, '%')
  16. OR IFNULL(source_object_id,'') LIKE CONCAT('%', @product, '%')
  17. OR IFNULL(source_payload,'') LIKE CONCAT('%', @product, '%')
  18. OR IFNULL(exception_code,'') LIKE CONCAT('%', @product, '%'))
  19. AND (@orderNo IS NULL OR IFNULL(title,'') LIKE CONCAT('%', @orderNo, '%')
  20. OR IFNULL(description,'') LIKE CONCAT('%', @orderNo, '%')
  21. OR IFNULL(related_object_code,'') LIKE CONCAT('%', @orderNo, '%')
  22. OR IFNULL(source_object_id,'') LIKE CONCAT('%', @orderNo, '%')
  23. OR IFNULL(source_payload,'') LIKE CONCAT('%', @orderNo, '%')
  24. OR IFNULL(exception_code,'') LIKE CONCAT('%', @orderNo, '%'))
  25. AND (@productionLine IS NULL OR IFNULL(title,'') LIKE CONCAT('%', @productionLine, '%')
  26. OR IFNULL(description,'') LIKE CONCAT('%', @productionLine, '%')
  27. OR IFNULL(related_object_code,'') LIKE CONCAT('%', @productionLine, '%')
  28. OR IFNULL(source_object_id,'') LIKE CONCAT('%', @productionLine, '%')
  29. OR IFNULL(source_payload,'') LIKE CONCAT('%', @productionLine, '%')
  30. OR IFNULL(exception_code,'') LIKE CONCAT('%', @productionLine, '%'))
  31. """;
  32. private const string ActiveStatusSql = "status NOT IN ('CLOSED','REJECTED','RECOVERED')";
  33. private readonly ISqlSugarClient _db;
  34. public S8HomeAlertQueryService(ISqlSugarClient db) => _db = db;
  35. public async Task<List<S8HomeAlertItem>> GetRecentAsync(long tenantId, SmartOpsDashboardFilter filter)
  36. {
  37. var sql = $"""
  38. SELECT DATE_FORMAT(COALESCE(last_detected_at, created_at), '%H:%i') AS Time,
  39. IFNULL(NULLIF(stage_code,''), IFNULL(NULLIF(module_code,''), 'S8')) AS Module,
  40. IFNULL(NULLIF(title,''), IFNULL(NULLIF(description,''), '异常告警')) AS Message,
  41. severity AS Severity
  42. FROM ado_s8_exception
  43. WHERE tenant_id=@tenantId AND is_deleted=0 AND {ActiveStatusSql}
  44. AND (@ds IS NULL OR created_at >= @ds)
  45. AND (@de IS NULL OR created_at < DATE_ADD(@de, INTERVAL 1 DAY))
  46. {TextFilterSql}
  47. ORDER BY COALESCE(last_detected_at, created_at) DESC, id DESC
  48. LIMIT {RecentLimit}
  49. """;
  50. var args = FilterArgs(tenantId, filter);
  51. args["ds"] = filter.DateStart;
  52. args["de"] = filter.DateEnd;
  53. var rows = await _db.Ado.SqlQueryAsync<S8HomeAlertRaw>(sql, args);
  54. return rows.Select(MapAlert).ToList();
  55. }
  56. public async Task<S8HomeAlertSummary> GetCreatedSummaryAsync(
  57. long tenantId, SmartOpsDashboardFilter filter, DateTime windowStart, DateTime windowEnd, bool isToday)
  58. {
  59. var sql = $"""
  60. SELECT COUNT(*) AS Total,
  61. IFNULL(SUM(CASE WHEN UPPER(IFNULL(severity,'')) IN ('SERIOUS','HIGH','CRITICAL') THEN 1 ELSE 0 END), 0) AS Critical
  62. FROM ado_s8_exception
  63. WHERE tenant_id=@tenantId AND is_deleted=0
  64. AND created_at >= @ws AND created_at < @we
  65. {TextFilterSql}
  66. """;
  67. var args = FilterArgs(tenantId, filter);
  68. args["ws"] = windowStart;
  69. args["we"] = windowEnd;
  70. var row = (await _db.Ado.SqlQueryAsync<S8HomeSummaryRaw>(sql, args)).FirstOrDefault();
  71. return new S8HomeAlertSummary(windowStart, windowEnd, isToday, row?.Total ?? 0, row?.Critical ?? 0);
  72. }
  73. public async Task<List<S8HomeTrendPoint>> GetCreatedTrendAsync(
  74. long tenantId, SmartOpsDashboardFilter filter, DateTime start, DateTime end)
  75. {
  76. var sql = $"""
  77. SELECT DATE(created_at) AS BizDate, COUNT(*) AS Cnt
  78. FROM ado_s8_exception
  79. WHERE tenant_id=@tenantId AND is_deleted=0
  80. AND created_at >= @ds AND created_at < DATE_ADD(@de, INTERVAL 1 DAY)
  81. {TextFilterSql}
  82. GROUP BY DATE(created_at)
  83. ORDER BY BizDate
  84. """;
  85. var args = FilterArgs(tenantId, filter);
  86. args["ds"] = start;
  87. args["de"] = end;
  88. var rows = await _db.Ado.SqlQueryAsync<S8HomeTrendRaw>(sql, args);
  89. var byDate = rows.ToDictionary(x => x.BizDate.Date, x => x.Cnt);
  90. var points = new List<S8HomeTrendPoint>();
  91. for (var day = start.Date; day <= end.Date; day = day.AddDays(1))
  92. points.Add(new S8HomeTrendPoint(day, byDate.GetValueOrDefault(day)));
  93. return points;
  94. }
  95. public async Task<S8ModuleLampPayload> GetModuleLampsAsync(long tenantId, DateTime now)
  96. {
  97. const string exceptionSql = """
  98. SELECT stage_code AS StageCode, severity AS Severity
  99. FROM ado_s8_exception
  100. WHERE tenant_id=@tenantId AND is_deleted=0
  101. AND status NOT IN ('CLOSED','REJECTED','RECOVERED')
  102. AND stage_code IN ('S1','S2','S3','S4','S5','S6','S7')
  103. """;
  104. const string ruleSql = """
  105. SELECT stage_code AS StageCode, enabled AS Enabled, poll_interval_seconds AS PollIntervalSeconds,
  106. last_status AS LastStatus, last_run_at AS LastRunAt, paused_until AS PausedUntil
  107. FROM ado_s8_watch_rule
  108. WHERE tenant_id=@tenantId
  109. AND stage_code IN ('S1','S2','S3','S4','S5','S6','S7')
  110. """;
  111. var exceptions = await _db.Ado.SqlQueryAsync<S8StageSeverityRaw>(exceptionSql, new { tenantId });
  112. var ruleRows = await _db.Ado.SqlQueryAsync<S8WatchRuleRaw>(ruleSql, new { tenantId });
  113. var rules = ruleRows.Select(x => new S8WatchRuleSignal(
  114. x.StageCode ?? "",
  115. x.Enabled,
  116. x.PollIntervalSeconds,
  117. x.LastStatus,
  118. x.LastRunAt,
  119. x.PausedUntil)).ToList();
  120. var items = S8ModuleCode.All.Select(code =>
  121. {
  122. var stageRows = exceptions.Where(x => string.Equals(x.StageCode, code, StringComparison.Ordinal)).ToList();
  123. var serious = stageRows.Count(x => S8SeverityCode.IsSerious(x.Severity));
  124. var follow = stageRows.Count - serious;
  125. var stageRules = rules.Where(x => string.Equals(x.StageCode, code, StringComparison.Ordinal)).ToList();
  126. var decision = S8ModuleLampPolicy.Decide(serious, follow, stageRules, now);
  127. return new S8ModuleLampItem(
  128. code,
  129. S8ModuleCode.Label(code),
  130. decision.BusinessState,
  131. decision.MonitorState,
  132. serious,
  133. follow,
  134. stageRows.Count,
  135. stageRules.Count,
  136. stageRules.Count(x => x.Enabled),
  137. stageRules.Count(x => S8ModuleLampPolicy.IsRunnable(x, now)),
  138. stageRules.Where(x => x.LastRunAt.HasValue).Select(x => x.LastRunAt).Max(),
  139. decision.MonitorReasonCode);
  140. }).ToList();
  141. return new S8ModuleLampPayload(now, items);
  142. }
  143. private static S8HomeAlertItem MapAlert(S8HomeAlertRaw row)
  144. {
  145. var serious = S8SeverityCode.IsSerious(row.Severity);
  146. return new S8HomeAlertItem(
  147. row.Time ?? "",
  148. string.IsNullOrWhiteSpace(row.Module) ? "S8" : row.Module,
  149. string.IsNullOrWhiteSpace(row.Message) ? "异常告警" : row.Message,
  150. serious ? "critical" : "medium",
  151. serious ? "严重" : "关注");
  152. }
  153. private static Dictionary<string, object?> FilterArgs(long tenantId, SmartOpsDashboardFilter filter) => new()
  154. {
  155. ["tenantId"] = tenantId,
  156. ["product"] = BlankToNull(filter.Product),
  157. ["orderNo"] = BlankToNull(filter.OrderNo),
  158. ["productionLine"] = BlankToNull(filter.ProductionLine)
  159. };
  160. private static string? BlankToNull(string? value) => string.IsNullOrWhiteSpace(value) ? null : value;
  161. }
  162. public static class S8ModuleLampPolicy
  163. {
  164. public static int FreshWindowSeconds(int pollIntervalSeconds) =>
  165. Math.Max(pollIntervalSeconds * 2, S8HomeAlertQueryService.MinFreshWindowSeconds);
  166. public static bool IsRunnable(S8WatchRuleSignal rule, DateTime now) =>
  167. rule.Enabled && (!rule.PausedUntil.HasValue || rule.PausedUntil.Value <= now);
  168. public static S8ModuleLampDecision Decide(
  169. int seriousCount, int followCount, IReadOnlyList<S8WatchRuleSignal> rules, DateTime now)
  170. {
  171. var (monitorState, reason) = ResolveMonitor(rules, now);
  172. var business = seriousCount > 0 ? "CRITICAL"
  173. : followCount > 0 ? "WARNING"
  174. : monitorState == "HEALTHY" ? "NORMAL"
  175. : "UNKNOWN";
  176. return new S8ModuleLampDecision(business, monitorState, reason);
  177. }
  178. private static (string State, string Reason) ResolveMonitor(IReadOnlyList<S8WatchRuleSignal> rules, DateTime now)
  179. {
  180. if (rules.Count == 0)
  181. return ("UNMONITORED", "NO_RULE");
  182. var runnable = rules.Where(x => IsRunnable(x, now)).ToList();
  183. if (runnable.Count == 0)
  184. return ("PAUSED", rules.Any(x => x.Enabled) ? "RULE_PAUSED" : "RULE_DISABLED");
  185. var reasons = runnable.Select(x => RuleReason(x, now)).Where(x => x != "OK").ToList();
  186. if (reasons.Count == 0)
  187. return ("HEALTHY", "OK");
  188. if (reasons.Contains("RUN_FAILED"))
  189. return ("DEGRADED", "RUN_FAILED");
  190. if (reasons.Contains("NEVER_RUN"))
  191. return ("DEGRADED", "NEVER_RUN");
  192. return ("DEGRADED", "RUN_STALE");
  193. }
  194. private static string RuleReason(S8WatchRuleSignal rule, DateTime now)
  195. {
  196. if (!string.Equals(rule.LastStatus, "SUCCESS", StringComparison.OrdinalIgnoreCase))
  197. return rule.LastRunAt.HasValue ? "RUN_FAILED" : "NEVER_RUN";
  198. if (!rule.LastRunAt.HasValue)
  199. return "NEVER_RUN";
  200. var window = TimeSpan.FromSeconds(FreshWindowSeconds(rule.PollIntervalSeconds));
  201. return now - rule.LastRunAt.Value <= window ? "OK" : "RUN_STALE";
  202. }
  203. }
  204. public sealed record S8WatchRuleSignal(
  205. string StageCode,
  206. bool Enabled,
  207. int PollIntervalSeconds,
  208. string? LastStatus,
  209. DateTime? LastRunAt,
  210. DateTime? PausedUntil);
  211. public sealed record S8ModuleLampDecision(string BusinessState, string MonitorState, string MonitorReasonCode);
  212. public sealed record S8HomeAlertItem(string Time, string Module, string Message, string Level, string LevelLabel);
  213. public sealed record S8HomeAlertSummary(DateTime WindowStart, DateTime WindowEnd, bool IsToday, int Total, int Critical);
  214. public sealed record S8HomeTrendPoint(DateTime Date, int Count);
  215. public sealed record S8ModuleLampItem(
  216. string Code,
  217. string Name,
  218. string BusinessState,
  219. string MonitorState,
  220. int SeriousCount,
  221. int FollowCount,
  222. int OpenCount,
  223. int ConfiguredRuleCount,
  224. int EnabledRuleCount,
  225. int RunnableRuleCount,
  226. DateTime? LatestRunAt,
  227. string MonitorReasonCode);
  228. public sealed record S8ModuleLampPayload(DateTime AsOf, IReadOnlyList<S8ModuleLampItem> Items);
  229. internal sealed class S8HomeAlertRaw
  230. {
  231. public string? Time { get; set; }
  232. public string? Module { get; set; }
  233. public string? Message { get; set; }
  234. public string? Severity { get; set; }
  235. }
  236. internal sealed class S8HomeSummaryRaw
  237. {
  238. public int Total { get; set; }
  239. public int Critical { get; set; }
  240. }
  241. internal sealed class S8HomeTrendRaw
  242. {
  243. public DateTime BizDate { get; set; }
  244. public int Cnt { get; set; }
  245. }
  246. internal sealed class S8StageSeverityRaw
  247. {
  248. public string? StageCode { get; set; }
  249. public string? Severity { get; set; }
  250. }
  251. internal sealed class S8WatchRuleRaw
  252. {
  253. public string? StageCode { get; set; }
  254. public bool Enabled { get; set; }
  255. public int PollIntervalSeconds { get; set; }
  256. public string? LastStatus { get; set; }
  257. public DateTime? LastRunAt { get; set; }
  258. public DateTime? PausedUntil { get; set; }
  259. }