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}%";
}
}