namespace Admin.NET.Plugin.AiDOP.SmartOps; public static class SmartOpsImprovementEffectiveness { public const int WindowSize = 7; public const int MinSamples = 3; public const string AutoEffective = "AUTO_EFFECTIVE"; public const string AutoIneffective = "AUTO_INEFFECTIVE"; public const string AutoInconclusive = "AUTO_INCONCLUSIVE"; public const string InsufficientData = "INSUFFICIENT_DATA"; public static bool IsLowerBetter(string? direction) => string.Equals(direction?.Trim(), "lower_is_better", StringComparison.OrdinalIgnoreCase); public static IReadOnlyList TakeLastValid(IEnumerable values, int take = WindowSize) { return values.Where(x => x.HasValue).Select(x => x!.Value).TakeLast(take).ToList(); } public static bool MeetsTarget(decimal value, decimal? target, string? direction) { if (target is null) return false; return IsLowerBetter(direction) ? value <= target.Value : value >= target.Value; } public static int TrailingOnTargetDays(IReadOnlyList after, decimal? target, string? direction) { if (target is null || after.Count == 0) return 0; var count = 0; for (var i = after.Count - 1; i >= 0; i--) { if (!MeetsTarget(after[i], target, direction)) break; count++; } return count; } public static SmartOpsEffectivenessResult Evaluate( IReadOnlyList before, IReadOnlyList after, decimal? target, string? direction, int exceptionCountBefore = 0, int exceptionCountAfter = 0) { if (before.Count < MinSamples || after.Count < MinSamples) { return new SmartOpsEffectivenessResult( InsufficientData, Mean(before), Mean(after), null, before.Count, after.Count, TrailingOnTargetDays(after, target, direction), AchievementRate(after, target, direction), exceptionCountBefore, exceptionCountAfter, "有效样本不足 3 个,无法自动评价"); } var beforeMean = Mean(before)!.Value; var afterMean = Mean(after)!.Value; var lower = IsLowerBetter(direction); var meanImproved = lower ? afterMean < beforeMean : afterMean > beforeMean; var meanWorse = lower ? afterMean > beforeMean : afterMean < beforeMean; var delta = afterMean - beforeMean; var reached = target is not null && MeetsTarget(afterMean, target, direction); var closer = target is not null && Math.Abs(afterMean - target.Value) < Math.Abs(beforeMean - target.Value); string conclusion; string reason; if (meanImproved && (reached || closer)) { conclusion = AutoEffective; reason = reached ? "后窗口均值改善且已达目标" : "后窗口均值改善且更接近目标"; } else if (meanWorse) { conclusion = AutoIneffective; reason = "后窗口均值劣于改善前,未判定为有效"; } else { conclusion = AutoInconclusive; reason = target is null ? "缺少目标值,不能只按差值正负判定" : meanImproved ? "均值改善但未更接近或达到目标" : "前后窗口均值无显著方向变化"; } return new SmartOpsEffectivenessResult( conclusion, beforeMean, afterMean, delta, before.Count, after.Count, TrailingOnTargetDays(after, target, direction), AchievementRate(after, target, direction), exceptionCountBefore, exceptionCountAfter, reason); } private static decimal? Mean(IReadOnlyList values) => values.Count == 0 ? null : decimal.Round(values.Average(), 4); private static decimal AchievementRate(IReadOnlyList after, decimal? target, string? direction) { if (target is null || after.Count == 0) return 0; var hit = after.Count(v => MeetsTarget(v, target, direction)); return decimal.Round(hit * 100m / after.Count, 2); } } public sealed record SmartOpsEffectivenessResult( string Conclusion, decimal? BeforeMean, decimal? AfterMean, decimal? Delta, int BeforeSamples, int AfterSamples, int ConsecutiveOnTargetDays, decimal AchievementRate, int ExceptionCountBefore, int ExceptionCountAfter, string Reason);