YD_Event.Admin/YD_Event.Application/Scoring/ScoreRuleConfigService.cs
马梓彭 8e66cd293b 修复花样跳绳 计分规则与显示
- 修正个人及同步花样表现分失误重复计算
- 合并表现裁判与主裁失误计算得分系数 M
- 修正车轮跳规定元素数量及 Q 系数
- 修正同步花样规定元素等级和小数缺失量
- 统一 D/P/Q/M 舍入精度及总分计算
- 统一平板、后端、工作台和投屏显示口径
- 增加计分回归用例和上线规则迁移脚本
2026-09-29 17:19:03 +08:00

143 lines
6.7 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)) role.AggregateDecimals = null;
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;
}
}