SmartDiagnosisTree.cs 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. using Admin.NET.Plugin.AiDOP.Entity;
  2. using Admin.NET.Plugin.AiDOP.Infrastructure;
  3. namespace Admin.NET.Plugin.AiDOP.Controllers;
  4. /// <summary>
  5. /// 智慧诊断选根与指标树:只沿 ParentId 下钻,无下级时自然截止,不跨树补位。
  6. /// </summary>
  7. internal static class SmartDiagnosisTree
  8. {
  9. public const string SelectionSpecified = "specified";
  10. public const string SelectionGlobalWorst = "global_worst";
  11. internal readonly record struct ValueSnap(decimal? MetricValue, decimal? TargetValue);
  12. internal readonly record struct RootResolveResult(
  13. AdoSmartOpsKpiMaster? Root,
  14. string SelectionMode,
  15. string? Error);
  16. public static RootResolveResult ResolveRoot(
  17. List<AdoSmartOpsKpiMaster> l1Metrics,
  18. IReadOnlyDictionary<string, ValueSnap> values,
  19. string? metricCode)
  20. {
  21. if (!string.IsNullOrWhiteSpace(metricCode))
  22. {
  23. var specified = l1Metrics.FirstOrDefault(x =>
  24. string.Equals(x.MetricCode, metricCode.Trim(), StringComparison.OrdinalIgnoreCase));
  25. if (specified == null)
  26. return new RootResolveResult(null, SelectionSpecified, "metricCode 不是当前模块已启用的 L1 指标。");
  27. return new RootResolveResult(specified, SelectionSpecified, null);
  28. }
  29. var metricsWithValues = l1Metrics
  30. .Where(x => values.TryGetValue(x.MetricCode, out var value) && value.MetricValue.HasValue)
  31. .ToList();
  32. var candidates = metricsWithValues.Count > 0 ? metricsWithValues : l1Metrics;
  33. var worst = candidates
  34. .OrderByDescending(x => DiagnosisSeverity(ComputeStatus(x, values)))
  35. .ThenByDescending(x => DiagnosisDeviation(x, values))
  36. .ThenBy(x => x.SortNo)
  37. .FirstOrDefault();
  38. return new RootResolveResult(worst, SelectionGlobalWorst, null);
  39. }
  40. public static List<AdoSmartOpsKpiMaster> DirectChildren(
  41. IEnumerable<AdoSmartOpsKpiMaster> kpis,
  42. long parentId,
  43. int level) =>
  44. kpis
  45. .Where(x => x.MetricLevel == level && x.ParentId == parentId)
  46. .OrderBy(x => x.SortNo)
  47. .ToList();
  48. public static string ComputeStatus(AdoSmartOpsKpiMaster kpi, IReadOnlyDictionary<string, ValueSnap> values)
  49. {
  50. values.TryGetValue(kpi.MetricCode, out var value);
  51. return AidopS4KpiMerge.AchievementLevel(
  52. value.MetricValue,
  53. value.TargetValue,
  54. kpi.Direction ?? "higher_is_better",
  55. kpi.YellowThreshold,
  56. kpi.RedThreshold);
  57. }
  58. public static decimal DiagnosisDeviation(AdoSmartOpsKpiMaster kpi, IReadOnlyDictionary<string, ValueSnap> values)
  59. {
  60. if (!values.TryGetValue(kpi.MetricCode, out var value) || value.MetricValue == null || value.TargetValue == null || value.TargetValue == 0)
  61. return 0m;
  62. var current = value.MetricValue.Value;
  63. var target = value.TargetValue.Value;
  64. return string.Equals(kpi.Direction, "lower_is_better", StringComparison.OrdinalIgnoreCase)
  65. ? Math.Max(0m, (current - target) / Math.Abs(target))
  66. : Math.Max(0m, (target - current) / Math.Abs(target));
  67. }
  68. public static int DiagnosisSeverity(string? status) => (status ?? "").ToLowerInvariant() switch
  69. {
  70. "red" => 3,
  71. "yellow" => 2,
  72. "green" => 1,
  73. _ => 0
  74. };
  75. public static string BuildDiagnosisRate(decimal? current, decimal? target, string? direction)
  76. {
  77. if (current == null || target == null || target == 0) return "-";
  78. var lowerIsBetter = string.Equals(direction, "lower_is_better", StringComparison.OrdinalIgnoreCase);
  79. if (lowerIsBetter && current == 0) return "-";
  80. var ratio = lowerIsBetter
  81. ? target.Value / current.Value * 100m
  82. : current.Value / target.Value * 100m;
  83. return $"{decimal.Round(ratio, 0):0}%";
  84. }
  85. }