SmartDiagnosisEvidenceService.cs 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. using System.Text.RegularExpressions;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. using SqlSugar;
  4. namespace Admin.NET.Plugin.AiDOP.SmartOps;
  5. public sealed class SmartDiagnosisEvidenceService : ITransient
  6. {
  7. private readonly ISqlSugarClient _db;
  8. public SmartDiagnosisEvidenceService(ISqlSugarClient db) => _db = db;
  9. public async Task<SmartDiagnosisEvidenceResult> QueryAsync(
  10. long tenantId,
  11. long factoryId,
  12. string moduleCode,
  13. SmartOpsDashboardFilter? filter = null,
  14. int limit = SmartDiagnosisEvidenceRegistry.DefaultLimit)
  15. {
  16. SmartOpsTrustedScopeRules.EnsureBusinessTenant(tenantId);
  17. var cap = Math.Clamp(limit, 1, SmartDiagnosisEvidenceRegistry.MaxLimit);
  18. var sources = SmartDiagnosisEvidenceRegistry.Resolve(moduleCode);
  19. return await QuerySourcesAsync(tenantId, factoryId, sources, filter, cap, null, "module");
  20. }
  21. public async Task<SmartDiagnosisEvidenceResult> QueryMetricAsync(
  22. long tenantId,
  23. long factoryId,
  24. string moduleCode,
  25. string metricCode,
  26. SmartOpsDashboardFilter? filter = null,
  27. int limit = SmartDiagnosisEvidenceRegistry.DefaultLimit)
  28. {
  29. SmartOpsTrustedScopeRules.EnsureBusinessTenant(tenantId);
  30. var cap = Math.Clamp(limit, 1, SmartDiagnosisEvidenceRegistry.MaxLimit);
  31. var normalizedMetric = (metricCode ?? "").Trim().ToUpperInvariant();
  32. var sources = SmartDiagnosisEvidenceRegistry.ResolveMetric(moduleCode, normalizedMetric);
  33. return await QuerySourcesAsync(
  34. tenantId,
  35. factoryId,
  36. sources,
  37. filter,
  38. cap,
  39. normalizedMetric,
  40. sources.Count == 0 ? "none" : "exact_metric");
  41. }
  42. private async Task<SmartDiagnosisEvidenceResult> QuerySourcesAsync(
  43. long tenantId,
  44. long factoryId,
  45. IReadOnlyList<SmartDiagnosisEvidenceSource> sources,
  46. SmartOpsDashboardFilter? filter,
  47. int cap,
  48. string? metricCode,
  49. string matchScope)
  50. {
  51. var result = new SmartDiagnosisEvidenceResult
  52. {
  53. AsOf = DateTime.Today.ToString("yyyy-MM-dd"),
  54. Domain = string.Join(",", sources.Select(x => x.Domain.ToString()).Distinct()),
  55. MetricCode = metricCode,
  56. MatchScope = matchScope,
  57. };
  58. if (sources.Count == 0)
  59. {
  60. result.Scope = metricCode == null ? "unsupported" : "unsupported_metric";
  61. return result;
  62. }
  63. var items = new List<SmartDiagnosisEvidenceItem>();
  64. var total = 0;
  65. foreach (var source in sources)
  66. {
  67. var (sourceTotal, sourceItems) = await QuerySourceAsync(
  68. source,
  69. tenantId,
  70. factoryId,
  71. filter,
  72. cap,
  73. metricCode);
  74. total += sourceTotal;
  75. result.Sources.Add(source.Key);
  76. items.AddRange(sourceItems);
  77. }
  78. result.Total = total;
  79. result.Items = items
  80. .OrderByDescending(x => Severity(x.Status))
  81. .ThenByDescending(x => x.Contribution ?? 0)
  82. .Take(cap)
  83. .ToList();
  84. result.Returned = result.Items.Count;
  85. result.Truncated = total > result.Returned;
  86. result.Scope = result.Returned == 0 ? "no_data" : "filtered_atomic";
  87. var latest = result.Items
  88. .Select(x => x.OccurredAt)
  89. .Where(x => !string.IsNullOrWhiteSpace(x))
  90. .OrderByDescending(x => x)
  91. .FirstOrDefault();
  92. if (!string.IsNullOrWhiteSpace(latest))
  93. result.AsOf = latest;
  94. return result;
  95. }
  96. private async Task<(int Total, List<SmartDiagnosisEvidenceItem> Items)> QuerySourceAsync(
  97. SmartDiagnosisEvidenceSource source,
  98. long tenantId,
  99. long factoryId,
  100. SmartOpsDashboardFilter? filter,
  101. int limit,
  102. string? metricCode)
  103. {
  104. var parameters = new List<SugarParameter>
  105. {
  106. new("@tenantId", tenantId),
  107. new("@factoryId", factoryId),
  108. new("@limit", limit),
  109. };
  110. if (!string.IsNullOrWhiteSpace(metricCode))
  111. parameters.Add(new SugarParameter("@metricCode", metricCode));
  112. var extraWhere = BuildFilterSql(source, filter, parameters);
  113. var countSql = InjectWhere(source.CountSql, extraWhere);
  114. var itemSql = InjectWhere(source.ItemSql, extraWhere);
  115. var total = await _db.Ado.GetIntAsync(countSql, parameters.ToArray());
  116. if (total <= 0)
  117. return (0, new List<SmartDiagnosisEvidenceItem>());
  118. var rows = await _db.Ado.SqlQueryAsync<SmartDiagnosisEvidenceSqlRow>(itemSql, parameters.ToArray());
  119. var items = rows.Select(row => new SmartDiagnosisEvidenceItem
  120. {
  121. ObjectType = source.ObjectType,
  122. ObjectCode = row.ObjectCode ?? "",
  123. Title = string.IsNullOrWhiteSpace(row.Title) ? row.ObjectCode ?? source.Key : row.Title,
  124. Contribution = row.Contribution,
  125. ContributionUnit = source.ContributionUnit,
  126. Status = string.IsNullOrWhiteSpace(row.Status) ? "green" : row.Status,
  127. OccurredAt = row.OccurredAt?.ToString("yyyy-MM-dd"),
  128. DrillPath = BuildDrillPath(source, row.ObjectCode),
  129. }).ToList();
  130. return (total, items);
  131. }
  132. private static string? BuildDrillPath(SmartDiagnosisEvidenceSource source, string? objectCode)
  133. {
  134. if (string.IsNullOrWhiteSpace(source.DrillPath))
  135. return null;
  136. if (string.IsNullOrWhiteSpace(source.DrillQueryKey) || string.IsNullOrWhiteSpace(objectCode))
  137. return source.DrillPath;
  138. var sep = source.DrillPath.Contains('?') ? "&" : "?";
  139. return source.DrillPath + sep + source.DrillQueryKey + "=" + Uri.EscapeDataString(objectCode);
  140. }
  141. private static string BuildFilterSql(
  142. SmartDiagnosisEvidenceSource source,
  143. SmartOpsDashboardFilter? filter,
  144. List<SugarParameter> parameters)
  145. {
  146. if (filter == null) return "";
  147. var values = filter.ToContextDictionary();
  148. var clauses = new List<string>();
  149. foreach (var map in source.Filters)
  150. {
  151. if (!values.TryGetValue(map.QueryKey, out var raw) || string.IsNullOrWhiteSpace(raw))
  152. continue;
  153. var param = "@f_" + map.QueryKey;
  154. clauses.Add($"{map.SqlColumn} = {param}");
  155. parameters.Add(new SugarParameter(param, raw.Trim()));
  156. }
  157. if (!string.IsNullOrWhiteSpace(source.DateColumn)
  158. && DateColumnPattern.IsMatch(source.DateColumn))
  159. {
  160. if (filter.DateStart.HasValue)
  161. {
  162. clauses.Add($"{source.DateColumn} >= @f_dateStart");
  163. parameters.Add(new SugarParameter("@f_dateStart", filter.DateStart.Value.Date));
  164. }
  165. if (filter.DateEnd.HasValue)
  166. {
  167. clauses.Add($"{source.DateColumn} <= @f_dateEnd");
  168. parameters.Add(new SugarParameter("@f_dateEnd", filter.DateEnd.Value.Date));
  169. }
  170. }
  171. return clauses.Count == 0 ? "" : " AND " + string.Join(" AND ", clauses);
  172. }
  173. private static readonly Regex DateColumnPattern = new(
  174. "^[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)?$",
  175. RegexOptions.Compiled | RegexOptions.CultureInvariant);
  176. private static string InjectWhere(string sql, string extraWhere)
  177. {
  178. if (string.IsNullOrWhiteSpace(extraWhere)) return sql;
  179. var orderAt = sql.LastIndexOf("ORDER BY", StringComparison.OrdinalIgnoreCase);
  180. if (orderAt < 0)
  181. return sql + extraWhere;
  182. return sql[..orderAt] + extraWhere + " " + sql[orderAt..];
  183. }
  184. private static int Severity(string? status) => (status ?? "").ToLowerInvariant() switch
  185. {
  186. "red" => 3,
  187. "yellow" => 2,
  188. _ => 1
  189. };
  190. }