using Admin.NET.Plugin.AiDOP.Entity; using Admin.NET.Plugin.AiDOP.Infrastructure; namespace Admin.NET.Plugin.AiDOP.Controllers; /// /// 智慧诊断选根与指标树:只沿 ParentId 下钻,无下级时自然截止,不跨树补位。 /// internal static class SmartDiagnosisTree { public const string SelectionSpecified = "specified"; public const string SelectionGlobalWorst = "global_worst"; internal readonly record struct ValueSnap(decimal? MetricValue, decimal? TargetValue); internal readonly record struct RootResolveResult( AdoSmartOpsKpiMaster? Root, string SelectionMode, string? Error); public static RootResolveResult ResolveRoot( List l1Metrics, IReadOnlyDictionary values, string? metricCode) { if (!string.IsNullOrWhiteSpace(metricCode)) { var specified = l1Metrics.FirstOrDefault(x => string.Equals(x.MetricCode, metricCode.Trim(), StringComparison.OrdinalIgnoreCase)); if (specified == null) return new RootResolveResult(null, SelectionSpecified, "metricCode 不是当前模块已启用的 L1 指标。"); return new RootResolveResult(specified, SelectionSpecified, null); } var worst = l1Metrics .OrderByDescending(x => DiagnosisSeverity(ComputeStatus(x, values))) .ThenByDescending(x => DiagnosisDeviation(x, values)) .ThenBy(x => x.SortNo) .FirstOrDefault(); return new RootResolveResult(worst, SelectionGlobalWorst, null); } public static List DirectChildren( IEnumerable kpis, long parentId, int level) => kpis .Where(x => x.MetricLevel == level && x.ParentId == parentId) .OrderBy(x => x.SortNo) .ToList(); public static string ComputeStatus(AdoSmartOpsKpiMaster kpi, IReadOnlyDictionary values) { values.TryGetValue(kpi.MetricCode, out var value); return AidopS4KpiMerge.AchievementLevel( value.MetricValue, value.TargetValue, kpi.Direction ?? "higher_is_better", kpi.YellowThreshold, kpi.RedThreshold); } public static decimal DiagnosisDeviation(AdoSmartOpsKpiMaster kpi, IReadOnlyDictionary values) { if (!values.TryGetValue(kpi.MetricCode, out var value) || value.MetricValue == null || value.TargetValue == null || value.TargetValue == 0) return 0m; var current = value.MetricValue.Value; var target = value.TargetValue.Value; return string.Equals(kpi.Direction, "lower_is_better", StringComparison.OrdinalIgnoreCase) ? Math.Max(0m, (current - target) / Math.Abs(target)) : Math.Max(0m, (target - current) / Math.Abs(target)); } public static int DiagnosisSeverity(string? status) => (status ?? "").ToLowerInvariant() switch { "red" => 3, "yellow" => 2, "green" => 1, _ => 0 }; public static string BuildDiagnosisRate(decimal? current, decimal? target, string? direction) { if (current == null || target == null || target == 0) return "-"; var ratio = string.Equals(direction, "lower_is_better", StringComparison.OrdinalIgnoreCase) ? target.Value / current.Value * 100m : current.Value / target.Value * 100m; return $"{decimal.Round(ratio, 0):0}%"; } }