namespace Admin.NET.Plugin.AiDOP.Infrastructure; /// /// 期量差与绿黄红(见 doc/plan/关键定义说明/期量差说明.md)。 /// 达成比例 = 实际 ÷ 目标 × 100%。 /// higher_is_better 默认黄 95 / 严重红 80;lower_is_better 默认黄 110 / 严重红 120。 /// YellowThreshold 分绿黄红;RedThreshold 仅决定红色是否严重告警(闪烁)。 /// public static class AidopS4KpiMerge { public const string Green = "green"; public const string Yellow = "yellow"; public const string Red = "red"; public const decimal DefaultHigherYellow = 95m; public const decimal DefaultHigherRed = 80m; public const decimal DefaultLowerYellow = 110m; public const decimal DefaultLowerRed = 120m; public sealed record KpiAchievementEvaluation( string Status, decimal? RatioPercent, bool IsBeyondRedThreshold, decimal EffectiveYellowThreshold, decimal EffectiveRedThreshold); public static bool IsLowerBetter(string? direction) => string.Equals(direction, "lower_is_better", StringComparison.OrdinalIgnoreCase); /// /// 保存校验。两个阈值都为空时通过;只配一个时另一个按方向默认值参与组合校验。 /// 不交换配置值。 /// public static bool TryValidateThresholds( string? direction, decimal? yellowThreshold, decimal? redThreshold, out string? error) { error = null; if (yellowThreshold == null && redThreshold == null) return true; var lower = IsLowerBetter(direction); var yellow = yellowThreshold ?? (lower ? DefaultLowerYellow : DefaultHigherYellow); var red = redThreshold ?? (lower ? DefaultLowerRed : DefaultHigherRed); if (lower) { if (yellow >= 100m && yellow <= red) return true; error = "lower_is_better 阈值不合法,应满足 100 <= YellowThreshold <= RedThreshold。"; return false; } if (red > 0m && red <= yellow && yellow <= 100m) return true; error = "higher_is_better 阈值不合法,应满足 0 < RedThreshold <= YellowThreshold <= 100。"; return false; } public static KpiAchievementEvaluation Evaluate( decimal? current, decimal? target, string? direction, decimal? yellowThreshold = null, decimal? redThreshold = null) { var (yellow, red) = ResolveEffectiveThresholds(direction, yellowThreshold, redThreshold); if (current == null || target == null || target.Value == 0) return new KpiAchievementEvaluation(Yellow, null, false, yellow, red); var ratio = current.Value / target.Value * 100m; if (IsLowerBetter(direction)) { var status = ratio <= 100m ? Green : ratio <= yellow ? Yellow : Red; var blink = status == Red && ratio > red; return new KpiAchievementEvaluation(status, ratio, blink, yellow, red); } { var status = ratio >= 100m ? Green : ratio >= yellow ? Yellow : Red; var blink = status == Red && ratio < red; return new KpiAchievementEvaluation(status, ratio, blink, yellow, red); } } public static string AchievementLevel( decimal? current, decimal? target, string direction, decimal? yellowThreshold = null, decimal? redThreshold = null) => Evaluate(current, target, direction, yellowThreshold, redThreshold).Status; public static bool IsBeyondRedThreshold( decimal? current, decimal? target, string? direction, decimal? yellowThreshold = null, decimal? redThreshold = null) => Evaluate(current, target, direction, yellowThreshold, redThreshold).IsBeyondRedThreshold; /// 期量差数值(当前−目标);展示用箭头与符号在调用方处理。 public static decimal? GapValue(decimal? current, decimal? target) { if (current == null || target == null) return null; return current.Value - target.Value; } public static string GapArrow(decimal? gap) { if (gap == null) return "flat"; return gap.Value >= 0 ? "up" : "down"; } private static (decimal Yellow, decimal Red) ResolveEffectiveThresholds( string? direction, decimal? yellowThreshold, decimal? redThreshold) { var lower = IsLowerBetter(direction); var defaults = lower ? (DefaultLowerYellow, DefaultLowerRed) : (DefaultHigherYellow, DefaultHigherRed); if (!TryValidateThresholds(direction, yellowThreshold, redThreshold, out _)) return defaults; return ( yellowThreshold ?? defaults.Item1, redThreshold ?? defaults.Item2); } }