YD_Event.Admin/YD_Event.Application/Scoring/ScoreRuleConfigService.cs

147 lines
6.9 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using YD_Event.Application.Scoring.Dtos;
namespace YD_Event.Application.Scoring;
/// <summary>
/// 计分规则配置解析服务
/// </summary>
public static class ScoreRuleConfigService
{
/// <summary>
/// 解析计分规则配置,配置缺失或格式不合法时回退到规定赛的默认配置,避免整场比赛因一处配置错误无法计分。
/// </summary>
public static ScoreRuleConfig Parse(string? configJson)
{
var config = new ScoreRuleConfig();
if (string.IsNullOrWhiteSpace(configJson)) return Fallback(config);
JObject root;
try { root = JObject.Parse(configJson); } catch (JsonException) { return Fallback(config); }
// 配置由人工维护,字段写成非数字等非法值时按默认值处理,不能让整场比赛无法计分。
try
{
config.RuleType = root["ruleType"]?.ToString() ?? config.RuleType;
config.TotalMode = (root["totalMode"]?.ToString() ?? config.TotalMode).Trim().ToLowerInvariant();
config.BaseScore = root["baseScore"]?.Value<decimal?>() ?? config.BaseScore;
config.ScoreDecimals = Math.Clamp(root["scoreDecimals"]?.Value<int?>() ?? config.ScoreDecimals, 0, 4);
config.DeductionUnit = root["deductionUnit"]?.Value<decimal?>() ?? config.DeductionUnit;
config.DifficultyDivisor = ReadDecimal(root["difficultyDivisor"]);
config.CorrectionNeedApproval = root["correctionNeedApproval"]?.Value<bool?>() ?? false;
}
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
{
return Fallback(new ScoreRuleConfig());
}
foreach (var node in root["roleAggregations"] as JArray ?? new JArray())
{
var roleType = node["roleType"]?.ToString();
if (string.IsNullOrWhiteSpace(roleType)) continue;
config.RoleAggregations.Add(new RoleAggregationConfig
{
RoleType = roleType!.Trim().ToUpperInvariant(),
Symbol = node["symbol"]?.ToString() ?? roleType!,
Label = node["label"]?.ToString() ?? roleType!,
SourceField = node["sourceField"]?.ToString() ?? nameof(EventJudgeScoreSheet.TotalScore),
TargetField = node["targetField"]?.ToString() ?? node["sourceField"]?.ToString() ?? nameof(EventResult.TotalScore),
Aggregate = (node["aggregate"]?.ToString() ?? ScoreRuleConfig.AggregateAvg).Trim().ToLowerInvariant(),
AggregateDecimals = ReadInt(node["aggregateDecimals"], 0, 4),
Sign = ReadSign(node["sign"]),
Factor = ReadFactor(node["factor"]),
FactorWeight = ReadDecimal(node["factorWeight"]) ?? 1m
});
}
var tieBreakers = (root["tieBreakers"] as JArray ?? new JArray())
.Select(x => x.ToString()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
if (tieBreakers.Count > 0) config.TieBreakers = tieBreakers;
ApplyBugRuleOverrides(config);
return Fallback(config);
}
/// <summary>个人花样与同步花样采用相同的表现分计算口径。</summary>
public static bool IsIndividualFamily(string ruleType) =>
ruleType.ToUpperInvariant() is "FREESTYLE_SINGLE" or "FREESTYLE_DOUBLE_SYNC" or "FREESTYLE_QUAD_SYNC";
/// <summary>补齐花样赛规则的默认计算参数。解析过程只生成内存配置,不改写数据库。</summary>
private static void ApplyBugRuleOverrides(ScoreRuleConfig config)
{
if (!config.IsFactorTotal) return;
foreach (var role in config.RoleAggregations.Where(x => x.Factor == ScoreFactorCode.Required))
{
if (IsIndividualFamily(config.RuleType))
{
// 单张评分单允许半个规定元素;同类型裁判取平均后四舍五入为整数参与 Q 计算。
role.AggregateDecimals = 0;
}
if (config.RuleType.StartsWith("FREESTYLE_WHEEL", StringComparison.OrdinalIgnoreCase))
role.FactorWeight = 0.025m;
}
}
/// <summary>
/// 读取参与总分的符号,非法值按加分处理
/// </summary>
private static int ReadSign(JToken? node)
{
try { return node?.Value<int?>() == -1 ? -1 : 1; }
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { return 1; }
}
/// <summary>
/// 读取乘法公式的因子编码,未配置或不是 D/P/Q/M 时返回 null(该裁判类型不参与因子计算)
/// </summary>
private static string? ReadFactor(JToken? node)
{
var factor = node?.ToString()?.Trim().ToUpperInvariant();
if (string.IsNullOrEmpty(factor)) return null;
return factor is ScoreFactorCode.Difficulty or ScoreFactorCode.Performance
or ScoreFactorCode.Required or ScoreFactorCode.Deduction ? factor : null;
}
/// <summary>
/// 读取配置里的数值,非数字等非法值按未配置处理
/// </summary>
private static decimal? ReadDecimal(JToken? node)
{
try { return node?.Value<decimal?>(); }
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { return null; }
}
/// <summary>
/// 读取受限整数配置,非法值按未配置处理。
/// </summary>
private static int? ReadInt(JToken? node, int min, int max)
{
try
{
var value = node?.Value<int?>();
return value.HasValue ? Math.Clamp(value.Value, min, max) : null;
}
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException)
{
return null;
}
}
/// <summary>
/// 未配置聚合方式时按规定赛的 T1/T2/T3 兜底
/// </summary>
private static ScoreRuleConfig Fallback(ScoreRuleConfig config)
{
if (config.RoleAggregations.Count > 0) return config;
config.RoleAggregations.AddRange(new[]
{
new RoleAggregationConfig { RoleType = "CJ", Symbol = "T1", Label = "创意分",
SourceField = nameof(EventJudgeScoreSheet.PerformanceScore), TargetField = nameof(EventResult.PerformanceScore), Aggregate = ScoreRuleConfig.AggregateAvg, Sign = 1 },
new RoleAggregationConfig { RoleType = "CQ", Symbol = "T2", Label = "完成分",
SourceField = nameof(EventJudgeScoreSheet.RequiredScore), TargetField = nameof(EventResult.RequiredScore), Aggregate = ScoreRuleConfig.AggregateAvg, Sign = 1 },
new RoleAggregationConfig { RoleType = "T", Symbol = "T3", Label = "扣分",
SourceField = nameof(EventJudgeScoreSheet.DeductionScore), TargetField = nameof(EventResult.DeductionScore), Aggregate = ScoreRuleConfig.AggregateSum, Sign = -1 }
});
return config;
}
}