2026-08-22 20:34:05 +08:00
|
|
|
|
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;
|
2026-08-23 16:21:49 +08:00
|
|
|
|
config.TotalMode = (root["totalMode"]?.ToString() ?? config.TotalMode).Trim().ToLowerInvariant();
|
2026-08-22 20:34:05 +08:00
|
|
|
|
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;
|
2026-08-23 16:21:49 +08:00
|
|
|
|
config.DifficultyDivisor = ReadDecimal(root["difficultyDivisor"]);
|
2026-08-22 20:34:05 +08:00
|
|
|
|
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(),
|
2026-09-22 08:55:05 +08:00
|
|
|
|
AggregateDecimals = ReadInt(node["aggregateDecimals"], 0, 4),
|
2026-08-23 16:21:49 +08:00
|
|
|
|
Sign = ReadSign(node["sign"]),
|
|
|
|
|
|
Factor = ReadFactor(node["factor"]),
|
|
|
|
|
|
FactorWeight = ReadDecimal(node["factorWeight"]) ?? 1m
|
2026-08-22 20:34:05 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-09-29 17:19:03 +08:00
|
|
|
|
ApplyBugRuleOverrides(config);
|
2026-08-22 20:34:05 +08:00
|
|
|
|
return Fallback(config);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-29 17:19:03 +08:00
|
|
|
|
/// <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))
|
|
|
|
|
|
{
|
2026-10-09 09:55:11 +08:00
|
|
|
|
if (IsIndividualFamily(config.RuleType))
|
|
|
|
|
|
{
|
2026-10-10 11:07:22 +08:00
|
|
|
|
// 单张评分单允许半个规定元素;同类型裁判取平均后四舍五入为整数参与 Q 计算。
|
|
|
|
|
|
role.AggregateDecimals = 0;
|
2026-10-09 09:55:11 +08:00
|
|
|
|
}
|
2026-09-29 17:19:03 +08:00
|
|
|
|
if (config.RuleType.StartsWith("FREESTYLE_WHEEL", StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
role.FactorWeight = 0.025m;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-22 20:34:05 +08:00
|
|
|
|
/// <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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-23 16:21:49 +08:00
|
|
|
|
/// <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; }
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 08:55:05 +08:00
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-22 20:34:05 +08:00
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|