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);
}
}