表演赛和花样赛

This commit is contained in:
马梓彭 2026-08-23 16:21:49 +08:00
parent 300fc7f961
commit d94ad92a9c
14 changed files with 7604 additions and 458 deletions

View File

@ -71,6 +71,11 @@ public class EntrySymbolScore
/// </summary>
public int Sign { get; set; } = 1;
/// <summary>
/// 花样赛乘法公式里承担的因子:D、P、Q、M,为空表示不参与因子计算
/// </summary>
public string? Factor { get; set; }
/// <summary>
/// 聚合后的分值
/// </summary>
@ -137,6 +142,26 @@ public class EntryAggregateResult
/// </summary>
public string Formula { get; set; } = string.Empty;
/// <summary>
/// 花样赛的难度因子 D
/// </summary>
public decimal DifficultyFactor { get; set; }
/// <summary>
/// 花样赛的表现因子 P,按 (1 + P) 参与相乘
/// </summary>
public decimal PerformanceFactor { get; set; }
/// <summary>
/// 花样赛的规定元素因子 Q,交互绳花样恒为 1
/// </summary>
public decimal RequiredFactor { get; set; } = 1m;
/// <summary>
/// 花样赛的扣分因子 M = max(0, 1 - 主裁扣分合计)
/// </summary>
public decimal DeductionFactor { get; set; } = 1m;
/// <summary>
/// 取某个计分符号的分值
/// </summary>

View File

@ -234,6 +234,8 @@ public class EventScoreCalculationService : ITransient
var completed = true;
foreach (var role in ctx.Config.RoleAggregations)
{
// 花样赛主裁同时承担 Q(规定元素)与 M(扣分)两个因子,配置里会出现两条 T,进度只统计一次。
if (outcome.Roles.Any(x => string.Equals(x.RoleType, role.RoleType, StringComparison.OrdinalIgnoreCase))) continue;
var roleAssignments = assignments
.Where(x => string.Equals(JudgeRoleSchemaService.NormalizeRoleType(x.JudgeRoleType), role.RoleType, StringComparison.OrdinalIgnoreCase))
.ToList();

View File

@ -2,16 +2,22 @@ namespace YD_Event.Application.Scoring.Dtos;
/// <summary>
/// 计分规则 RuleConfigJson 的解析结果。
/// 规定赛用到的是 roleAggregations(创意分 T1、完成分 T2、扣分 T3)与 tieBreakers(并列时的比较顺序),
/// 花样赛只要在同一份配置里追加难度分聚合即可复用这套计算与排名代码。
/// 规定赛用到的是 roleAggregations(创意分 T1、完成分 T2、扣分 T3)与 tieBreakers(并列时的比较顺序);
/// 花样赛(车轮跳花样、交互绳花样)在同一份配置里追加难度分聚合,并把 totalMode 置为 factor,
/// 由各裁判类型的 factor 声明自己在 R = D × (1 + P) × Q × M 里承担哪个因子。
/// </summary>
public class ScoreRuleConfig
{
/// <summary>
/// 规则类型,如 PRESCRIBED(规定赛)
/// 规则类型,如 PRESCRIBED(规定赛)、FREESTYLE(花样赛)
/// </summary>
public string RuleType { get; set; } = "PRESCRIBED";
/// <summary>
/// 总分计算方式:linear-各计分符号按 ±1 加减(规定赛),factor-按 D/P/Q/M 因子相乘(花样赛)
/// </summary>
public string TotalMode { get; set; } = TotalModeLinear;
/// <summary>
/// 满分基准分(规定赛为 100,仅用于前端展示与校验提示)
/// </summary>
@ -32,6 +38,12 @@ public class ScoreRuleConfig
/// </summary>
public bool CorrectionNeedApproval { get; set; }
/// <summary>
/// 难度因子的除数。未配置时按参与难度因子的裁判类型数量取平均(个人花样 D=(DP+DM+DR)/3、车轮跳 D=(DA+DB)/2);
/// 交互绳花样按 D = DJ × 1.4 + DT 加权合并,需配置为 1。
/// </summary>
public decimal? DifficultyDivisor { get; set; }
/// <summary>
/// 各裁判类型的分值聚合方式
/// </summary>
@ -80,6 +92,47 @@ public class ScoreRuleConfig
/// 取最低值
/// </summary>
public const string AggregateMin = "min";
/// <summary>
/// 总分按各计分符号 ±1 加减(规定赛 R = T1 + T2 - T3)
/// </summary>
public const string TotalModeLinear = "linear";
/// <summary>
/// 总分按 D/P/Q/M 因子相乘(花样赛 R = D × (1 + P) × Q × M)
/// </summary>
public const string TotalModeFactor = "factor";
/// <summary>
/// 是否按因子相乘计算总分
/// </summary>
public bool IsFactorTotal => string.Equals(TotalMode, TotalModeFactor, StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// 花样赛总分公式 R = D × (1 + P) × Q × M 里的因子编码
/// </summary>
public static class ScoreFactorCode
{
/// <summary>
/// 难度因子 D,多个难度裁判类型先各自聚合再按均值合并
/// </summary>
public const string Difficulty = "D";
/// <summary>
/// 表现因子 P,作为 (1 + P) 参与相乘
/// </summary>
public const string Performance = "P";
/// <summary>
/// 规定元素因子 Q,交互绳花样不计
/// </summary>
public const string Required = "Q";
/// <summary>
/// 扣分因子 M,M = max(0, 1 - 扣分合计)
/// </summary>
public const string Deduction = "M";
}
/// <summary>
@ -121,4 +174,15 @@ public class RoleAggregationConfig
/// 参与总分时的符号:1-加分,-1-减分
/// </summary>
public int Sign { get; set; } = 1;
/// <summary>
/// 该裁判类型在花样赛乘法公式里承担的因子:D-难度,P-表现,Q-规定元素,M-扣分;为空表示不参与因子计算
/// </summary>
public string? Factor { get; set; }
/// <summary>
/// 因子系数。表现因子按 P = 聚合值 × 系数(个人/车轮 0.05、交互绳 0.033);
/// 难度因子用于交互绳的 D = DJ × 1.4 + DT 这类加权合并,未配置按 1 处理。
/// </summary>
public decimal FactorWeight { get; set; } = 1m;
}

View File

@ -81,6 +81,127 @@ public class ScoreItemDefinition
/// 是否为取消比赛资格原因项
/// </summary>
public bool IsDisqualifyReason => Item.CalcMode == ScoreCalcMode.RecordOnly;
/// <summary>
/// 是否由服务端联动生成。自动项不接受客户端录入(表现维度分、失误自动减分、规定元素缺失数)。
/// </summary>
public bool IsAuto { get; set; }
/// <summary>
/// 评分对象编码,两人车轮跳花样用 A、B 区分两名运动员
/// </summary>
public string? SubjectCode { get; set; }
/// <summary>
/// 评分对象名称。表演赛一名难度裁判要按个人绳、车轮跳、交互绳、旅行跳(或长绳)分块评分,
/// 客户端按此名称给每块加标题;车轮跳花样等一名裁判只看一个对象的项目不配置。
/// </summary>
public string? SubjectName { get; set; }
/// <summary>
/// 表现维度编码(音乐、完成质量等),同一维度的加项、减项、自动减项与维度分共用一个编码
/// </summary>
public string? DimensionCode { get; set; }
/// <summary>
/// 表现维度在单裁判加权分里的权重(音乐 0.20、创意 0.15、多样性 0.15、娱乐 0.25、完成质量 0.25)
/// </summary>
public decimal? DimensionWeight { get; set; }
/// <summary>
/// 表现维度的基准分(规程为 12 分,人工加减与失误自动减都在基准分上调整)
/// </summary>
public decimal? BaseValue { get; set; }
/// <summary>
/// 加减方向:1-该项每次计数使维度分 +1,-1-每次计数使维度分 -1,0-不参与维度分调整
/// </summary>
public int AdjustSign { get; set; }
/// <summary>
/// 是否为失误计数项,失误数按此项联动五个维度的自动减分
/// </summary>
public bool IsFailureCount { get; set; }
/// <summary>
/// 递增扣分序列。主裁失误按第 1 次 0.05、第 2 次 0.075、第 3 次起 0.1 累计扣分,最后一档重复到末次。
/// </summary>
public List<decimal>? StepDeductions { get; set; }
/// <summary>
/// 规定元素目标完成量(个人花样、车轮跳花样默认 4),只配在缺失数自动项上
/// </summary>
public decimal? RequiredTarget { get; set; }
/// <summary>
/// 规定元素是否按类别计完成量。
/// true 时只要该类别录入数量大于 0 就计一类(车轮跳花样的力量和体操、多摇、绳子运用、互动配合);
/// false 时按数量 × 单位分值折算(个人花样 3 级及以上按 1 计、2 级及以下按 0.5 计)。
/// </summary>
public bool RequiredAsCategory { get; set; }
/// <summary>
/// 难度分聚合方式:avg-按动作总数取平均(交互绳跳绳者),top-取最高 N 个动作的平均(交互绳摇绳者),
/// 为空表示按数量 × 等级分值直接累加(个人花样、车轮跳花样)
/// </summary>
public string? DifficultyAggregate { get; set; }
/// <summary>
/// 取最高 N 个动作时的动作数(交互绳摇绳者为 20)
/// </summary>
public int? DifficultyTopCount { get; set; }
}
/// <summary>
/// 难度分的聚合方式
/// </summary>
public static class DifficultyAggregateMode
{
/// <summary>
/// 按动作总数取平均:Σ(数量 × 等级分值) ÷ 动作总数
/// </summary>
public const string Average = "avg";
/// <summary>
/// 取难度等级最高的 N 个动作:Σ(入选动作分值) ÷ N
/// </summary>
public const string Top = "top";
}
/// <summary>
/// 计分规则明细的评分项类型
/// </summary>
public static class ScoreItemTypeCode
{
/// <summary>
/// 难度等级计数
/// </summary>
public const int DifficultyCount = 1;
/// <summary>
/// 规定元素计数
/// </summary>
public const int RequiredElement = 2;
/// <summary>
/// 表现维度
/// </summary>
public const int Performance = 3;
/// <summary>
/// 扣分项
/// </summary>
public const int Deduction = 4;
/// <summary>
/// 取消资格原因
/// </summary>
public const int Disqualify = 5;
/// <summary>
/// 规定赛评分项
/// </summary>
public const int Prescribed = 6;
}
/// <summary>
@ -210,6 +331,21 @@ public class ScoreSheetLine
/// 排序
/// </summary>
public int Sort { get; set; }
/// <summary>
/// 评分对象编码(两人车轮跳花样的 A、B)
/// </summary>
public string? SubjectCode { get; set; }
/// <summary>
/// 是否由服务端联动生成(表现维度的失误自动减分、规定元素缺失数)
/// </summary>
public bool IsAutoGenerated { get; set; }
/// <summary>
/// 来源评分项编码,自动项据此追溯联动来源
/// </summary>
public string? ParentItemCode { get; set; }
}
/// <summary>
@ -261,4 +397,45 @@ public class ScoreSheetCalculationResult
/// 计算明细
/// </summary>
public List<ScoreSheetLine> Lines { get; set; } = new();
/// <summary>
/// 规定元素完成量(3 级及以上按 1 计、2 级及以下按 0.5 计后的合计)
/// </summary>
public decimal RequiredCompletedCount { get; set; }
/// <summary>
/// 规定元素缺失量 = max(0, 目标数 - 完成量)
/// </summary>
public decimal RequiredMissingCount { get; set; }
/// <summary>
/// 花样赛的分步计算过程,落到 CalculationJson 便于复核与申诉
/// </summary>
public List<ScoreSheetStep> Steps { get; set; } = new();
}
/// <summary>
/// 评分单的一步计算过程
/// </summary>
public class ScoreSheetStep
{
/// <summary>
/// 步骤编码,如 DIFFICULTY_TOP20、PERFORMANCE_WEIGHTED、DEDUCTION_FAILURE
/// </summary>
public string Code { get; set; } = null!;
/// <summary>
/// 步骤说明
/// </summary>
public string Label { get; set; } = null!;
/// <summary>
/// 计算式
/// </summary>
public string Formula { get; set; } = string.Empty;
/// <summary>
/// 计算结果
/// </summary>
public decimal Value { get; set; }
}

View File

@ -217,6 +217,16 @@ public class JudgeSheetHeaderDto
/// </summary>
public string RuleType { get; set; } = string.Empty;
/// <summary>
/// 总分计算方式:linear-各计分符号加减(规定赛),factor-按 D/P/Q/M 因子相乘(花样赛)
/// </summary>
public string TotalMode { get; set; } = string.Empty;
/// <summary>
/// 总分公式,如 T1 + T2 - T3、D × (1 + P) × Q × M
/// </summary>
public string Formula { get; set; } = string.Empty;
/// <summary>
/// 评分项适用范围:personal-个人项目,team-集体项目
/// </summary>
@ -313,6 +323,63 @@ public class JudgeScoreItemDto
/// </summary>
public bool IsDisqualifyReason { get; set; }
/// <summary>
/// 是否由服务端联动生成。自动项(表现维度分、规定元素缺失数)只读展示,不接受录入。
/// </summary>
public bool IsAutoGenerated { get; set; }
/// <summary>
/// 评分对象编码,两人车轮跳花样用 A、B 区分两名运动员,客户端据此分组展示
/// </summary>
public string? SubjectCode { get; set; }
/// <summary>
/// 评分对象名称。表演赛一名难度裁判要按个人绳、车轮跳、交互绳、旅行跳(或长绳)分块评分,
/// 客户端按此名称给每块加标题。
/// </summary>
public string? SubjectName { get; set; }
/// <summary>
/// 表现维度编码,同一维度的加项、减项、自动减项与维度分共用一个编码,客户端据此把维度聚成一组
/// </summary>
public string? DimensionCode { get; set; }
/// <summary>
/// 表现维度在单裁判加权分里的权重
/// </summary>
public decimal? DimensionWeight { get; set; }
/// <summary>
/// 表现维度的基准分(规程为 12 分)
/// </summary>
public decimal? BaseValue { get; set; }
/// <summary>
/// 加减方向:1-每次计数使维度分 +1,-1-每次计数使维度分 -1,0-不参与维度分调整
/// </summary>
public int AdjustSign { get; set; }
/// <summary>
/// 是否为失误计数项,客户端增减该项时需同步刷新五个维度的自动减分
/// </summary>
public bool IsFailureCount { get; set; }
/// <summary>
/// 递增扣分序列,主裁失误按第 1 次 0.05、第 2 次 0.075、第 3 次起 0.1 累计
/// </summary>
public List<decimal>? StepDeductions { get; set; }
/// <summary>
/// 规定元素目标完成量(个人花样、车轮跳花样为 4)
/// </summary>
public decimal? RequiredTarget { get; set; }
/// <summary>
/// 规定元素是否按类别计完成量。true 时该类别录入数量大于 0 即计一类(车轮跳花样),
/// false 时按数量 × 单位分值折算(个人花样 3 级及以上按 1 计、2 级及以下按 0.5 计)。
/// </summary>
public bool RequiredAsCategory { get; set; }
/// <summary>
/// 排序
/// </summary>
@ -444,6 +511,47 @@ public class JudgeScorePreviewDto
/// 取消比赛资格原因
/// </summary>
public string? DisqualifyReasonName { get; set; }
/// <summary>
/// 规定元素完成量(3 级及以上按 1 计、2 级及以下按 0.5 计后的合计)
/// </summary>
public decimal RequiredCompletedCount { get; set; }
/// <summary>
/// 规定元素缺失量 = max(0, 目标数 - 完成量)
/// </summary>
public decimal RequiredMissingCount { get; set; }
/// <summary>
/// 花样赛的分步计算过程,客户端可原样展示预估分的推导明细
/// </summary>
public List<JudgeScoreStepDto> Steps { get; set; } = new();
}
/// <summary>
/// 评分单的一步计算过程
/// </summary>
public class JudgeScoreStepDto
{
/// <summary>
/// 步骤编码
/// </summary>
public string Code { get; set; } = string.Empty;
/// <summary>
/// 步骤说明
/// </summary>
public string Label { get; set; } = string.Empty;
/// <summary>
/// 计算式
/// </summary>
public string Formula { get; set; } = string.Empty;
/// <summary>
/// 计算结果
/// </summary>
public decimal Value { get; set; }
}
/// <summary>
@ -636,12 +744,43 @@ public class JudgeEntryResultDto
/// </summary>
public List<JudgeSymbolScoreDto> Symbols { get; set; } = new();
/// <summary>
/// 花样赛的乘法因子。规定赛为 null,花样赛按 R = D × (1 + P) × Q × M 输出各因子值。
/// </summary>
public JudgeScoreFactorDto? Factors { get; set; }
/// <summary>
/// 各裁判类型的提交进度
/// </summary>
public List<JudgeRoleProgressDto> Progress { get; set; } = new();
}
/// <summary>
/// 花样赛总分公式 R = D × (1 + P) × Q × M 的各因子值
/// </summary>
public class JudgeScoreFactorDto
{
/// <summary>
/// 难度因子 D
/// </summary>
public string Difficulty { get; set; } = string.Empty;
/// <summary>
/// 表现因子 P,按 (1 + P) 参与相乘
/// </summary>
public string Performance { get; set; } = string.Empty;
/// <summary>
/// 规定元素因子 Q = max(0, 1 - 系数 × 缺失数),交互绳花样恒为 1
/// </summary>
public string Required { get; set; } = string.Empty;
/// <summary>
/// 扣分因子 M = max(0, 1 - 主裁扣分合计)
/// </summary>
public string Deduction { get; set; } = string.Empty;
}
/// <summary>
/// 单个计分符号的得分
/// </summary>

View File

@ -5,8 +5,13 @@ namespace YD_Event.Application.Scoring;
/// <summary>
/// 打分系统裁判端评分接口(独立于微信小程序,供裁判端页面调用)。
/// 规定赛计分:创意编排裁判创意分取平均记作 T1,完成质量裁判完成分取平均记作 T2,
/// 规定赛计分(totalMode=linear):创意编排裁判创意分取平均记作 T1,完成质量裁判完成分取平均记作 T2,
/// 主裁每项违例从 100 分中扣 5 分记作 T3,总分 = T1 + T2 - T3。
/// 花样赛计分(totalMode=factor):两人车轮跳花样 R = D × (1 + P) × Q × M,
/// 其中 D = (dA + dB) / 2、P = 表现裁判均值 × 0.05、Q = max(0, 1 - 0.05 × 规定元素缺失类别数)、M = max(0, 1 - 主裁扣分合计);
/// 交互绳花样 R = D × (1 + P) × M,其中 D = dJ × 1.4 + dT、P = 表现裁判均值 × 0.033,不计 Q。
/// 表演赛 R = D × (1 + P) × Q × M,其中 D = (A + B) / 2(A/B 组难度裁判各自取平均)、P = 表现裁判均值 × 0.05、
/// Q = max(0, 1 - 0.05 × 5 类规定元素缺失数)、M = max(0, 1 - (失误 + 违例))。
/// 裁判提交的分值一律在服务端按计分规则重算,客户端上传的合计值不参与落库。
/// </summary>
[ApiDescriptionSettings("打分系统裁判端")]
@ -268,6 +273,8 @@ public class JudgeScoringController : IDynamicApiController
JudgeRoleType = roleType,
JudgeRoleName = JudgeRoleSchemaService.RoleNameOf(ctx.RoleSlots, roleType),
RuleType = ctx.Config.RuleType,
TotalMode = ctx.Config.TotalMode,
Formula = EntryResultCalculationService.BuildFormula(ctx.Config),
Scope = ctx.Scope,
BaseScore = ctx.Config.BaseScore,
ScoreDecimals = ctx.Config.ScoreDecimals
@ -292,6 +299,17 @@ public class JudgeScoringController : IDynamicApiController
Remark = definition.Remark,
IsRequired = definition.Item.IsRequired,
IsDisqualifyReason = definition.IsDisqualifyReason,
IsAutoGenerated = definition.IsAuto,
SubjectCode = definition.SubjectCode,
SubjectName = definition.SubjectName,
DimensionCode = definition.DimensionCode,
DimensionWeight = definition.DimensionWeight,
BaseValue = definition.BaseValue,
AdjustSign = definition.AdjustSign,
IsFailureCount = definition.IsFailureCount,
StepDeductions = definition.StepDeductions,
RequiredTarget = definition.RequiredTarget,
RequiredAsCategory = definition.RequiredAsCategory,
Sort = definition.Item.Sort,
InputValue = line?.InputValue,
CountValue = line?.CountValue,
@ -448,6 +466,15 @@ public class JudgeScoringController : IDynamicApiController
Value = ctx.Config.Format(judge.Value)
}).ToList()
}).ToList(),
Factors = !ctx.Config.IsFactorTotal || outcome.Aggregate == null
? null
: new JudgeScoreFactorDto
{
Difficulty = ctx.Config.Format(outcome.Aggregate.DifficultyFactor),
Performance = ctx.Config.Format(outcome.Aggregate.PerformanceFactor),
Required = ctx.Config.Format(outcome.Aggregate.RequiredFactor),
Deduction = ctx.Config.Format(outcome.Aggregate.DeductionFactor)
},
Progress = outcome.Roles.Select(ToProgressDto).ToList()
};
}).ToList();
@ -739,9 +766,9 @@ public class JudgeScoringController : IDynamicApiController
sheet.RequiredScore = calculation.RequiredScore;
sheet.DeductionScore = calculation.DeductionScore;
sheet.TotalScore = calculation.TotalScore;
sheet.RequiredCompletedCount = calculation.Lines
.Where(x => x.ScoreDimension == ScoreDimensionCode.Required).Sum(x => x.CountValue ?? 0m);
sheet.RequiredMissingCount = 0m;
// 规定元素完成量、缺失量由服务端按折算系数与目标数算出,供成绩聚合换算 Q 因子。
sheet.RequiredCompletedCount = calculation.RequiredCompletedCount;
sheet.RequiredMissingCount = calculation.RequiredMissingCount;
sheet.IsDisqualified = calculation.IsDisqualified;
sheet.DisqualifyReasonCode = calculation.DisqualifyReasonCode;
sheet.DisqualifyRemark = calculation.IsDisqualified ? disqualifyRemark : null;
@ -771,6 +798,9 @@ public class JudgeScoringController : IDynamicApiController
ItemName = line.ItemName,
ItemType = line.ItemType,
ScoreDimension = line.ScoreDimension,
SubjectCode = line.SubjectCode,
IsAutoGenerated = line.IsAutoGenerated,
ParentItemCode = line.ParentItemCode,
CountValue = line.CountValue,
InputValue = line.InputValue,
UnitValue = line.UnitValue,
@ -825,7 +855,13 @@ public class JudgeScoringController : IDynamicApiController
TotalScore = calculation.TotalScore,
TotalScoreText = config.Format(calculation.TotalScore),
IsDisqualified = calculation.IsDisqualified,
DisqualifyReasonName = calculation.DisqualifyReasonName
DisqualifyReasonName = calculation.DisqualifyReasonName,
RequiredCompletedCount = calculation.RequiredCompletedCount,
RequiredMissingCount = calculation.RequiredMissingCount,
Steps = calculation.Steps.Select(x => new JudgeScoreStepDto
{
Code = x.Code, Label = x.Label, Formula = x.Formula, Value = x.Value
}).ToList()
};
/// <summary>

View File

@ -0,0 +1,530 @@
namespace YD_Event.Application.JudgeScore;
/// <summary>
/// 花样赛内置计分规则定义(车轮跳花样、交互绳花样)。
/// <para>
/// 只在初始化/同步规则库时使用;运行期的评分项一律从 ScoreRule / ScoreRuleItem 读取,
/// 后台调整评分项、难度分值、席位配置后无需改代码。
/// </para>
/// <para>
/// 计分口径(裁判端原型 + 花样跳绳裁判端逻辑与测试验收说明书):<br/>
/// 车轮跳花样:DA×2、DB×2、P×2、T×1;dA=(da1+da2)/2、dB=(db1+db2)/2、D=(dA+dB)/2;<br/>
/// P=average(p1,p2)×0.05;Q=max(0,1-0.05×规定元素缺失类别数);M=max(0,1-(失误+违例+间歇));<br/>
/// 总分 R=max(0, D×(1+P)×Q×M)。<br/>
/// 交互绳花样:DJ×2、DT×2、P×2、T×1;DJ 按全部动作取均值、DT 取最高 20 个动作(分母固定 20);<br/>
/// dJ=(DJ1+DJ2)/2、dT=(DT1+DT2)/2、D=dJ×1.4+dT;P=average(p1,p2)×0.033;<br/>
/// M=max(0,1-(失误+违例+空间违例));总分 R=max(0, D×(1+P)×M),不计 Q。
/// </para>
/// </summary>
public static class FreestyleRuleDefinition
{
/// <summary>
/// 车轮跳花样规则编码
/// </summary>
public const string WheelRuleCode = "ROPE_FREESTYLE_WHEEL";
/// <summary>
/// 车轮跳花样规则名称
/// </summary>
public const string WheelRuleName = "车轮跳花样计分规则";
/// <summary>
/// 交互绳花样规则编码
/// </summary>
public const string WireRuleCode = "ROPE_FREESTYLE_WIRE";
/// <summary>
/// 交互绳花样规则名称
/// </summary>
public const string WireRuleName = "交互绳花样计分规则";
/// <summary>
/// 运动员A难度裁判角色类别编码(车轮跳花样)
/// </summary>
public const string RoleDifficultyA = "DA";
/// <summary>
/// 运动员B难度裁判角色类别编码(车轮跳花样)
/// </summary>
public const string RoleDifficultyB = "DB";
/// <summary>
/// 跳绳运动员难度裁判角色类别编码(交互绳花样,按全部动作取均值)
/// </summary>
public const string RoleDifficultyJumper = "DJ";
/// <summary>
/// 摇绳运动员难度裁判角色类别编码(交互绳花样,取最高 20 个动作)
/// </summary>
public const string RoleDifficultyTurner = "DT";
/// <summary>
/// 表现裁判角色类别编码
/// </summary>
public const string RolePerformance = "P";
/// <summary>
/// 主裁判角色类别编码
/// </summary>
public const string RoleChief = "T";
/// <summary>
/// 评分对象:运动员A(车轮跳花样)
/// </summary>
public const string SubjectA = "A";
/// <summary>
/// 评分对象:运动员B(车轮跳花样)
/// </summary>
public const string SubjectB = "B";
/// <summary>
/// 表现维度的基准分。人工加减与失误自动减都在基准分上调整。
/// </summary>
public const decimal PerformanceBaseValue = 12m;
/// <summary>
/// 表现维度分的上限(下限为 0)
/// </summary>
public const decimal PerformanceMaxValue = 24m;
/// <summary>
/// 车轮跳花样、精英个人花样的表现因子系数:P = average(p1,p2) × 0.05
/// </summary>
public const decimal PerformanceFactorWeight = 0.05m;
/// <summary>
/// 交互绳花样的表现因子系数:P = average(p1,p2) × 0.033
/// </summary>
public const decimal WirePerformanceFactorWeight = 0.033m;
/// <summary>
/// 每缺 1 类规定元素的 Q 扣减系数:Q = max(0, 1 - 0.05 × 缺失类别数)
/// </summary>
public const decimal RequiredMissingUnit = 0.05m;
/// <summary>
/// 车轮跳花样的规定元素目标类别数(力量和体操、多摇、绳子运用、互动配合)
/// </summary>
public const decimal WheelRequiredTarget = 4m;
/// <summary>
/// 摇绳运动员难度取用的动作数(少于 20 个动作时分母仍按 20)
/// </summary>
public const int TurnerTopCount = 20;
/// <summary>
/// 违例、间歇、空间违例的每次扣减值
/// </summary>
public const decimal ViolationUnitDeduction = 0.05m;
/// <summary>
/// 失误的递增扣减序列:第 1 次 0.05、第 2 次 0.075、第 3 次起每次 0.1
/// </summary>
public static readonly decimal[] FailureStepDeductions = { 0.05m, 0.075m, 0.1m };
/// <summary>
/// 难度等级分值表。必须查表取值,不得用浮点公式推导。
/// </summary>
public static IReadOnlyList<DifficultyLevelDefinition> DifficultyLevels { get; } = new List<DifficultyLevelDefinition>
{
new("DIFF_LEVEL_0", "0 级", 0m),
new("DIFF_LEVEL_0_5", "0.5 级", 0.12m),
new("DIFF_LEVEL_1", "1 级", 0.15m),
new("DIFF_LEVEL_2", "2 级", 0.23m),
new("DIFF_LEVEL_3", "3 级", 0.34m),
new("DIFF_LEVEL_4", "4 级", 0.51m),
new("DIFF_LEVEL_5", "5 级", 0.76m),
new("DIFF_LEVEL_6", "6 级", 1.14m),
new("DIFF_LEVEL_7", "7 级", 1.71m),
new("DIFF_LEVEL_8", "8 级", 2.56m)
};
/// <summary>
/// 表现五维。权重合计必须为 1,页面按此顺序展示。
/// </summary>
public static IReadOnlyList<PerformanceDimensionDefinition> PerformanceDimensions { get; } = new List<PerformanceDimensionDefinition>
{
new("MUSIC", "音乐", 0.20m),
new("QUALITY", "完成质量", 0.25m),
new("ENTERTAINMENT", "娱乐", 0.25m),
new("CREATIVITY", "创意", 0.15m),
new("VARIETY", "多样性", 0.15m)
};
/// <summary>
/// 取消比赛资格原因,与裁判端原型逐字一致
/// </summary>
public static IReadOnlyList<DisqualifyReasonDefinition> DisqualifyReasons { get; } = new List<DisqualifyReasonDefinition>
{
new("DQ_DISCIPLINE", "违反赛场纪律"),
new("DQ_MEMBER_COUNT", "参赛人员不符合规定(不足或超过)"),
new("DQ_NO_ROPE_SHOES", "未穿跳绳鞋"),
new("DQ_EXTERNAL_AID", "使用外部助力器材"),
new("DQ_UNCIVILIZED", "比赛中嚼口香糖/不文明用语等"),
new("DQ_ANTI_OLYMPIC", "有悖奥林匹克精神的元素(描绘暴力、战争、宗教、色情有关的元素)"),
new("DQ_LATE_60S", "运动员(队)被叫超过60秒未上场"),
new("DQ_TATTOO", "显露纹身")
};
/// <summary>
/// 车轮跳花样的规定元素类别,只要该类别有动作即计一类完成
/// </summary>
public static IReadOnlyList<RequiredElementDefinition> WheelRequiredElements { get; } = new List<RequiredElementDefinition>
{
new("REQ_STRENGTH_GYMNASTICS", "力量和体操"),
new("REQ_MULTIPLE_ROPE", "多摇"),
new("REQ_ROPE_MANIPULATION", "绳子运用"),
new("REQ_INTERACTION", "互动配合")
};
/// <summary>
/// 车轮跳花样裁判席位配置:运动员A/B难度裁判各 2 人、表现裁判 2 人、主裁 1 人
/// </summary>
public const string WheelJudgeRoleSchemaJson = """
[{"title":"难度裁判","slots":[{"code":"DA","name":"难度裁判-DA(运动员A难度裁判)","min":2},{"code":"DB","name":"难度裁判-DB(运动员B难度裁判)","min":2}]},{"title":"表现裁判","slots":[{"code":"P","name":"表现裁判-P","min":2}]},{"title":"主裁判","slots":[{"code":"T","name":"主裁判-T","min":1,"max":1}]}]
""";
/// <summary>
/// 交互绳花样裁判席位配置:跳绳者/摇绳者难度裁判各 2 人、表现裁判 2 人、主裁 1 人
/// </summary>
public const string WireJudgeRoleSchemaJson = """
[{"title":"难度裁判","slots":[{"code":"DJ","name":"难度裁判-DJ(跳绳运动员难度裁判)","min":2},{"code":"DT","name":"难度裁判-DT(摇绳运动员难度裁判)","min":2}]},{"title":"表现裁判","slots":[{"code":"P","name":"表现裁判-P","min":2}]},{"title":"主裁判","slots":[{"code":"T","name":"主裁判-T","min":1,"max":1}]}]
""";
/// <summary>
/// 车轮跳花样计分参数配置。
/// totalMode=factor 表示按 R = D × (1 + P) × Q × M 相乘;difficultyDivisor=2 对应 D=(dA+dB)/2;
/// 主裁同时承担 Q(规定元素缺失数)与 M(扣分合计)两个因子,因此配置了两条 T。
/// </summary>
public const string WheelRuleConfigJson = """
{"ruleType":"FREESTYLE_WHEEL","totalMode":"factor","baseScore":0,"scoreDecimals":2,"difficultyDivisor":2,"correctionNeedApproval":false,"roleAggregations":[{"roleType":"DA","symbol":"dA","label":"运动员A难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1},{"roleType":"DB","symbol":"dB","label":"运动员B难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1},{"roleType":"P","symbol":"P","label":"表现","sourceField":"PerformanceScore","targetField":"PerformanceScore","aggregate":"avg","sign":1,"factor":"P","factorWeight":0.05},{"roleType":"T","symbol":"Q","label":"规定元素缺失","sourceField":"RequiredMissingCount","targetField":"RequiredScore","aggregate":"max","sign":1,"factor":"Q","factorWeight":0.05},{"roleType":"T","symbol":"M","label":"扣分","sourceField":"DeductionScore","targetField":"DeductionScore","aggregate":"sum","sign":-1,"factor":"M","factorWeight":1}],"tieBreakers":["TotalScore","-DeductionScore","RequiredScore","PerformanceScore","DifficultyScore"]}
""";
/// <summary>
/// 交互绳花样计分参数配置。
/// D = dJ × 1.4 + dT 是加权合并而非取平均,因此 difficultyDivisor 配置为 1;
/// 交互绳总分无 Q 因子,主裁只配置 M。
/// </summary>
public const string WireRuleConfigJson = """
{"ruleType":"FREESTYLE_WIRE","totalMode":"factor","baseScore":0,"scoreDecimals":2,"difficultyDivisor":1,"correctionNeedApproval":false,"roleAggregations":[{"roleType":"DJ","symbol":"dJ","label":"跳绳运动员难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1.4},{"roleType":"DT","symbol":"dT","label":"摇绳运动员难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1},{"roleType":"P","symbol":"P","label":"表现","sourceField":"PerformanceScore","targetField":"PerformanceScore","aggregate":"avg","sign":1,"factor":"P","factorWeight":0.033},{"roleType":"T","symbol":"M","label":"扣分","sourceField":"DeductionScore","targetField":"DeductionScore","aggregate":"sum","sign":-1,"factor":"M","factorWeight":1}],"tieBreakers":["TotalScore","-DeductionScore","PerformanceScore","DifficultyScore"]}
""";
/// <summary>
/// 车轮跳花样全部评分项
/// </summary>
public static IReadOnlyList<PrescribedRuleItemDefinition> WheelItems { get; } = BuildWheelItems();
/// <summary>
/// 交互绳花样全部评分项
/// </summary>
public static IReadOnlyList<PrescribedRuleItemDefinition> WireItems { get; } = BuildWireItems();
/// <summary>
/// 构造车轮跳花样评分项:DA/DB 难度等级、表现五维与失误、主裁扣分与规定元素、取消资格原因
/// </summary>
private static List<PrescribedRuleItemDefinition> BuildWheelItems()
{
var items = new List<PrescribedRuleItemDefinition>();
var sort = 0;
// 运动员A、B 的难度裁判各自只看一名运动员,难度明细带 subjectCode 便于后端聚合与定位。
items.AddRange(BuildDifficultyItems(RoleDifficultyA, SubjectA, null, null, null, null, ref sort));
items.AddRange(BuildDifficultyItems(RoleDifficultyB, SubjectB, null, null, null, null, ref sort));
items.AddRange(BuildPerformanceItems(ref sort));
items.AddRange(BuildChiefDeductionItems("T_INTERVAL", "间歇", ref sort));
// 规定元素:每类只要有动作即计一类完成,缺失类别数由服务端算出后参与 Q。
foreach (var element in WheelRequiredElements)
{
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RoleChief, ItemCode = element.ItemCode, ItemName = element.ItemName,
ItemType = ScoreItemType.RequiredElement, ScoreDimension = ScoreDimensionType.Required,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = 1m, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.RequiredElement()
});
}
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RoleChief, ItemCode = "REQUIRED_MISSING", ItemName = "规定元素缺失(个)",
ItemType = ScoreItemType.RequiredElement, ScoreDimension = ScoreDimensionType.None,
CalcMode = ScoreCalcMode.DirectValue,
MinValue = 0m, MaxValue = WheelRequiredTarget, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.RequiredMissing(WheelRequiredTarget)
});
items.AddRange(BuildDisqualifyItems(ref sort));
return items;
}
/// <summary>
/// 构造交互绳花样评分项:DJ 按全部动作取均值、DT 取最高 20 个动作,表现与主裁扣分同车轮跳但不计规定元素
/// </summary>
private static List<PrescribedRuleItemDefinition> BuildWireItems()
{
var items = new List<PrescribedRuleItemDefinition>();
var sort = 0;
items.AddRange(BuildDifficultyItems(RoleDifficultyJumper, null, null, Scoring.Dtos.DifficultyAggregateMode.Average, null, null, ref sort));
items.AddRange(BuildDifficultyItems(RoleDifficultyTurner, null, null, Scoring.Dtos.DifficultyAggregateMode.Top, TurnerTopCount, null, ref sort));
items.AddRange(BuildPerformanceItems(ref sort));
items.AddRange(BuildChiefDeductionItems("T_SPACE_VIOLATION", "空间违例", ref sort));
items.AddRange(BuildDisqualifyItems(ref sort));
return items;
}
/// <summary>
/// 构造一个难度裁判类型的等级计数项。
/// aggregate 为空表示按数量 × 等级分值直接累加;avg 按动作总数取均值;top 取最高 topCount 个动作。
/// itemCodePrefix 用于表演赛把同一裁判的多个类别拆成独立评分项(如 SR_DIFF_LEVEL_3),避免编码互相覆盖。
/// </summary>
internal static List<PrescribedRuleItemDefinition> BuildDifficultyItems(string roleType, string? subjectCode,
string? subjectName, string? aggregate, int? topCount, string? itemCodePrefix, ref int sort)
{
var items = new List<PrescribedRuleItemDefinition>();
foreach (var level in DifficultyLevels)
{
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = roleType,
ItemCode = string.IsNullOrEmpty(itemCodePrefix) ? level.ItemCode : $"{itemCodePrefix}_{level.ItemCode}",
ItemName = level.LevelName,
ItemType = ScoreItemType.DifficultyCount, ScoreDimension = ScoreDimensionType.Difficulty,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = level.LevelValue, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.Difficulty(subjectCode, subjectName, aggregate, topCount)
});
}
return items;
}
/// <summary>
/// 构造表现裁判评分项:五维各一组人工加/减项 + 一个只读维度分自动项,再加一个失误次数项。
/// 失误联动五维自动减分由服务端在提交时重算,客户端只能改失误次数。
/// </summary>
internal static List<PrescribedRuleItemDefinition> BuildPerformanceItems(ref int sort)
{
var items = new List<PrescribedRuleItemDefinition>();
foreach (var dimension in PerformanceDimensions)
{
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RolePerformance, ItemCode = $"{dimension.Code}_PLUS", ItemName = $"{dimension.Name}(加数量)",
ItemType = ScoreItemType.Performance, ScoreDimension = ScoreDimensionType.Performance,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = 1m, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.PerformanceAdjust(dimension.Code, 1)
});
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RolePerformance, ItemCode = $"{dimension.Code}_MINUS", ItemName = $"{dimension.Name}(减数量)",
ItemType = ScoreItemType.Performance, ScoreDimension = ScoreDimensionType.Performance,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = 1m, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.PerformanceAdjust(dimension.Code, -1)
});
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RolePerformance, ItemCode = $"{dimension.Code}_SCORE", ItemName = dimension.Name,
ItemType = ScoreItemType.Performance, ScoreDimension = ScoreDimensionType.Performance,
CalcMode = ScoreCalcMode.DirectValue,
UnitValue = 1m, MinValue = 0m, MaxValue = PerformanceMaxValue, StepValue = 1m,
DefaultValue = PerformanceBaseValue, IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.PerformanceDimension(dimension.Code, dimension.Weight)
});
}
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RolePerformance, ItemCode = "FAILURE_COUNT", ItemName = "失误次数",
ItemType = ScoreItemType.Performance, ScoreDimension = ScoreDimensionType.None,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = 1m, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.FailureCount()
});
return items;
}
/// <summary>
/// 构造主裁扣分项。失误按递增序列扣减,违例与 extraCode 指定的第三项(车轮跳为间歇、交互绳为空间违例)按每次 0.05 扣减。
/// extraCode 为空时只生成失误与违例两项(表演赛)。
/// </summary>
internal static List<PrescribedRuleItemDefinition> BuildChiefDeductionItems(string? extraCode, string? extraName, ref int sort)
{
var items = new List<PrescribedRuleItemDefinition>
{
new()
{
JudgeRoleType = RoleChief, ItemCode = "T_FAILURE", ItemName = "失误",
ItemType = ScoreItemType.Deduction, ScoreDimension = ScoreDimensionType.Deduction,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = FailureStepDeductions[0], MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.StepDeduction(FailureStepDeductions)
},
new()
{
JudgeRoleType = RoleChief, ItemCode = "T_VIOLATION", ItemName = "违例",
ItemType = ScoreItemType.Deduction, ScoreDimension = ScoreDimensionType.Deduction,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = ViolationUnitDeduction, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.Count("次")
}
};
if (string.IsNullOrEmpty(extraCode)) return items;
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RoleChief, ItemCode = extraCode, ItemName = extraName ?? extraCode,
ItemType = ScoreItemType.Deduction, ScoreDimension = ScoreDimensionType.Deduction,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = ViolationUnitDeduction, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.Count("次")
});
return items;
}
/// <summary>
/// 构造主裁的取消比赛资格原因项,勾选任一项即整条成绩按 0 分处理
/// </summary>
internal static List<PrescribedRuleItemDefinition> BuildDisqualifyItems(ref int sort)
{
var items = new List<PrescribedRuleItemDefinition>();
foreach (var reason in DisqualifyReasons)
{
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RoleChief, ItemCode = reason.ItemCode, ItemName = reason.ReasonName,
ItemType = ScoreItemType.Disqualify, ScoreDimension = ScoreDimensionType.None,
CalcMode = ScoreCalcMode.RecordOnly,
IsRequired = false, Sort = ++sort
});
}
return items;
}
}
/// <summary>
/// 一个难度等级的定义
/// </summary>
/// <param name="ItemCode">评分项编码</param>
/// <param name="LevelName">等级名称</param>
/// <param name="LevelValue">等级分值</param>
public record DifficultyLevelDefinition(string ItemCode, string LevelName, decimal LevelValue);
/// <summary>
/// 一个表现维度的定义
/// </summary>
/// <param name="Code">维度编码</param>
/// <param name="Name">维度名称</param>
/// <param name="Weight">在单裁判加权分里的权重</param>
public record PerformanceDimensionDefinition(string Code, string Name, decimal Weight);
/// <summary>
/// 一条取消比赛资格原因的定义
/// </summary>
/// <param name="ItemCode">评分项编码</param>
/// <param name="ReasonName">原因名称</param>
public record DisqualifyReasonDefinition(string ItemCode, string ReasonName);
/// <summary>
/// 一类规定元素的定义
/// </summary>
/// <param name="ItemCode">评分项编码</param>
/// <param name="ItemName">类别名称</param>
public record RequiredElementDefinition(string ItemCode, string ItemName);
/// <summary>
/// 花样赛评分项扩展配置(ScoreRuleItem.ExtensionJson)的构造。
/// 字段名与 ScoreSheetCalculationService.CreateDefinition 的解析逻辑一一对应。
/// </summary>
public static class FreestyleItemExtension
{
/// <summary>
/// 难度等级计数项:subjectCode/subjectName 区分同一裁判要评的多个对象(车轮跳的两名运动员、表演赛的多个器械类别),
/// difficultyAggregate 指定聚合方式
/// </summary>
public static string Difficulty(string? subjectCode, string? subjectName, string? aggregate, int? topCount)
{
var node = new JObject { ["inputMode"] = "count", ["unitLabel"] = "个" };
if (!string.IsNullOrEmpty(subjectCode)) node["subjectCode"] = subjectCode;
if (!string.IsNullOrEmpty(subjectName)) node["subjectName"] = subjectName;
if (!string.IsNullOrEmpty(aggregate)) node["difficultyAggregate"] = aggregate;
if (topCount is > 0) node["difficultyTopCount"] = topCount.Value;
return node.ToString(Formatting.None);
}
/// <summary>
/// 表现维度的人工加/减项:adjustSign 为 1 表示每次计数使维度分 +1,-1 表示 -1
/// </summary>
public static string PerformanceAdjust(string dimensionCode, int adjustSign) =>
new JObject
{
["inputMode"] = "count", ["unitLabel"] = "次",
["dimensionCode"] = dimensionCode, ["adjustSign"] = adjustSign
}.ToString(Formatting.None);
/// <summary>
/// 表现维度分(只读自动项):基准 12 分,按权重参与单裁判加权分
/// </summary>
public static string PerformanceDimension(string dimensionCode, decimal weight) =>
new JObject
{
["inputMode"] = "score", ["isAuto"] = true,
["dimensionCode"] = dimensionCode, ["dimensionWeight"] = weight,
["baseValue"] = FreestyleRuleDefinition.PerformanceBaseValue
}.ToString(Formatting.None);
/// <summary>
/// 失误次数项:每次失误使五个维度的自动减分各 +1
/// </summary>
public static string FailureCount() =>
new JObject
{
["inputMode"] = "count", ["unitLabel"] = "次", ["isFailureCount"] = true,
["remark"] = "表现裁判记一次失误,其对应的表现五个方面(音乐,完成质量,创意,娱乐和多样性)减数量+1"
}.ToString(Formatting.None);
/// <summary>
/// 递增扣分项:第 1 次 0.05、第 2 次 0.075、第 3 次起每次 0.1
/// </summary>
public static string StepDeduction(IEnumerable<decimal> steps) =>
new JObject
{
["inputMode"] = "count", ["unitLabel"] = "次",
["stepDeductions"] = new JArray(steps.Cast<object>().ToArray()),
["remark"] = "第1次扣0.05,第2次扣0.075,第3次起每次扣0.1"
}.ToString(Formatting.None);
/// <summary>
/// 普通计数项
/// </summary>
public static string Count(string unitLabel) =>
new JObject { ["inputMode"] = "count", ["unitLabel"] = unitLabel }.ToString(Formatting.None);
/// <summary>
/// 规定元素类别项:录入数量大于 0 即计一类完成
/// </summary>
public static string RequiredElement() =>
new JObject
{
["inputMode"] = "count", ["unitLabel"] = "个", ["requiredAsCategory"] = true
}.ToString(Formatting.None);
/// <summary>
/// 规定元素缺失数(只读自动项):缺失数 = max(0, 目标类别数 - 完成类别数)
/// </summary>
public static string RequiredMissing(decimal target) =>
new JObject
{
["inputMode"] = "score", ["isAuto"] = true, ["unitLabel"] = "个", ["requiredTarget"] = target
}.ToString(Formatting.None);
}

View File

@ -0,0 +1,171 @@
namespace YD_Event.Application.JudgeScore;
/// <summary>
/// 表演赛(个人花样基础套路赛)内置计分规则定义。
/// <para>
/// 只在初始化/同步规则库时使用;运行期的评分项一律从 ScoreRule / ScoreRuleItem 读取,
/// 后台调整评分项、难度分值、席位配置后无需改代码。
/// </para>
/// <para>
/// 计分口径(裁判端原型 + 花样跳绳裁判端逻辑与测试验收说明书 6.7):<br/>
/// 席位:难度A组×3、难度B组×3、表现P×3、主裁T×1。<br/>
/// A 组裁判一人评 个人绳(DP+DM)、车轮跳(DA)、交互绳(DT_INTERACTION)、旅行跳(DT_TRAVEL) 四个类别,A=average(A1,A2,A3);<br/>
/// B 组裁判一人评 个人绳(DR)、车轮跳(DB)、交互绳(DJ)、长绳(DL) 四个类别,B=average(B1,B2,B3);<br/>
/// 总难度 D=(A+B)/2;表现 P=average(p1,p2,p3)×0.05;<br/>
/// 规定元素 Q=max(0,1-0.05×缺失类别数),5 类为 个人绳、车轮跳、交互绳、长绳(6米以上)、旅行跳;<br/>
/// 扣分 M=max(0,1-(失误+违例)),表演赛不记间歇与空间违例;<br/>
/// 总分 R=max(0, D×(1+P)×Q×M)。
/// </para>
/// </summary>
public static class ShowcaseRuleDefinition
{
/// <summary>
/// 表演赛规则编码
/// </summary>
public const string RuleCode = "ROPE_SHOWCASE";
/// <summary>
/// 表演赛规则名称
/// </summary>
public const string RuleName = "表演赛计分规则";
/// <summary>
/// A 组难度裁判角色类别编码。一人评个人绳、车轮跳、交互绳、旅行跳四个类别。
/// </summary>
public const string RoleDifficultyGroupA = "DGA";
/// <summary>
/// B 组难度裁判角色类别编码。一人评个人绳、车轮跳、交互绳、长绳四个类别。
/// </summary>
public const string RoleDifficultyGroupB = "DGB";
/// <summary>
/// 表现裁判角色类别编码
/// </summary>
public const string RolePerformance = FreestyleRuleDefinition.RolePerformance;
/// <summary>
/// 主裁判角色类别编码
/// </summary>
public const string RoleChief = FreestyleRuleDefinition.RoleChief;
/// <summary>
/// 规定元素目标类别数:个人绳、车轮跳、交互绳、长绳(6米以上)、旅行跳
/// </summary>
public const decimal RequiredTarget = 5m;
/// <summary>
/// A 组难度裁判要评的类别。
/// 逻辑说明书要求把脑图里两个同名 DT 拆成 DT_INTERACTION(交互绳)与 DT_TRAVEL(旅行跳),避免字段互相覆盖。
/// </summary>
public static IReadOnlyList<ShowcaseDifficultyCategory> GroupACategories { get; } = new List<ShowcaseDifficultyCategory>
{
new("DP_DM", "个人绳", "DP+DM(个人绳)"),
new("DA", "车轮跳", "DA(车轮跳)"),
new("DT_INTERACTION", "交互绳", "DT(交互绳)"),
new("DT_TRAVEL", "旅行跳", "DT(旅行跳)")
};
/// <summary>
/// B 组难度裁判要评的类别
/// </summary>
public static IReadOnlyList<ShowcaseDifficultyCategory> GroupBCategories { get; } = new List<ShowcaseDifficultyCategory>
{
new("DR", "个人绳", "DR(个人绳)"),
new("DB", "车轮跳", "DB(车轮跳)"),
new("DJ", "交互绳", "DJ(交互绳)"),
new("DL", "长绳", "DL(长绳)")
};
/// <summary>
/// 规定元素类别。成套动作中必须出现这 5 类,每缺一类参与 Q 扣减。
/// </summary>
public static IReadOnlyList<RequiredElementDefinition> RequiredElements { get; } = new List<RequiredElementDefinition>
{
new("REQ_SINGLE_ROPE", "个人绳"),
new("REQ_WHEEL", "车轮跳"),
new("REQ_WIRE", "交互绳"),
new("REQ_LONG_ROPE", "长绳"),
new("REQ_TRAVEL", "旅行跳")
};
/// <summary>
/// 表演赛裁判席位配置:A/B 组难度裁判各 3 人、表现裁判 3 人、主裁 1 人
/// </summary>
public const string JudgeRoleSchemaJson = """
[{"title":"难度裁判","slots":[{"code":"DGA","name":"难度裁判-A组裁判","min":3},{"code":"DGB","name":"难度裁判-B组裁判","min":3}]},{"title":"表现裁判","slots":[{"code":"P","name":"表现裁判-P","min":3}]},{"title":"主裁判","slots":[{"code":"T","name":"主裁判-T","min":1,"max":1}]}]
""";
/// <summary>
/// 表演赛计分参数配置。
/// totalMode=factor 表示按 R = D × (1 + P) × Q × M 相乘;A 组、B 组各自取平均后 difficultyDivisor=2 对应 D=(A+B)/2;
/// 主裁同时承担 Q(规定元素缺失数)与 M(扣分合计)两个因子,因此配置了两条 T。
/// </summary>
public const string RuleConfigJson = """
{"ruleType":"SHOWCASE","totalMode":"factor","baseScore":0,"scoreDecimals":2,"difficultyDivisor":2,"correctionNeedApproval":false,"roleAggregations":[{"roleType":"DGA","symbol":"A","label":"A组难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1},{"roleType":"DGB","symbol":"B","label":"B组难度","sourceField":"DifficultyScore","targetField":"DifficultyScore","aggregate":"avg","sign":1,"factor":"D","factorWeight":1},{"roleType":"P","symbol":"P","label":"表现","sourceField":"PerformanceScore","targetField":"PerformanceScore","aggregate":"avg","sign":1,"factor":"P","factorWeight":0.05},{"roleType":"T","symbol":"Q","label":"规定元素缺失","sourceField":"RequiredMissingCount","targetField":"RequiredScore","aggregate":"max","sign":1,"factor":"Q","factorWeight":0.05},{"roleType":"T","symbol":"M","label":"扣分","sourceField":"DeductionScore","targetField":"DeductionScore","aggregate":"sum","sign":-1,"factor":"M","factorWeight":1}],"tieBreakers":["TotalScore","-DeductionScore","RequiredScore","PerformanceScore","DifficultyScore"]}
""";
/// <summary>
/// 表演赛全部评分项
/// </summary>
public static IReadOnlyList<PrescribedRuleItemDefinition> Items { get; } = BuildItems();
/// <summary>
/// 构造表演赛评分项:A/B 组各 4 个类别的难度等级、表现五维与失误、主裁扣分与 5 类规定元素、取消资格原因
/// </summary>
private static List<PrescribedRuleItemDefinition> BuildItems()
{
var items = new List<PrescribedRuleItemDefinition>();
var sort = 0;
// 一名难度裁判要按类别分块评分,因此每个类别的难度等级项都用类别编码作前缀独立成项。
foreach (var category in GroupACategories)
{
items.AddRange(FreestyleRuleDefinition.BuildDifficultyItems(RoleDifficultyGroupA,
category.Code, category.DisplayName, null, null, category.Code, ref sort));
}
foreach (var category in GroupBCategories)
{
items.AddRange(FreestyleRuleDefinition.BuildDifficultyItems(RoleDifficultyGroupB,
category.Code, category.DisplayName, null, null, category.Code, ref sort));
}
items.AddRange(FreestyleRuleDefinition.BuildPerformanceItems(ref sort));
// 表演赛主裁只记失误与违例,不记间歇、空间违例。
items.AddRange(FreestyleRuleDefinition.BuildChiefDeductionItems(null, null, ref sort));
// 规定元素:5 类中每类只要有动作即计一类完成,缺失类别数由服务端算出后参与 Q。
foreach (var element in RequiredElements)
{
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RoleChief, ItemCode = element.ItemCode, ItemName = element.ItemName,
ItemType = ScoreItemType.RequiredElement, ScoreDimension = ScoreDimensionType.Required,
CalcMode = ScoreCalcMode.CountByUnit,
UnitValue = 1m, MinValue = 0m, MaxValue = 50m, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.RequiredElement()
});
}
items.Add(new PrescribedRuleItemDefinition
{
JudgeRoleType = RoleChief, ItemCode = "REQUIRED_MISSING", ItemName = "规定元素缺失(个)",
ItemType = ScoreItemType.RequiredElement, ScoreDimension = ScoreDimensionType.None,
CalcMode = ScoreCalcMode.DirectValue,
MinValue = 0m, MaxValue = RequiredTarget, StepValue = 1m, DefaultValue = 0m,
IsRequired = false, Sort = ++sort,
ExtensionJson = FreestyleItemExtension.RequiredMissing(RequiredTarget)
});
items.AddRange(FreestyleRuleDefinition.BuildDisqualifyItems(ref sort));
return items;
}
}
/// <summary>
/// 表演赛一个难度类别的定义
/// </summary>
/// <param name="Code">类别编码,同时作为评分项编码前缀与评分对象编码</param>
/// <param name="Name">类别名称</param>
/// <param name="DisplayName">页面分块标题,与裁判端原型逐字一致</param>
public record ShowcaseDifficultyCategory(string Code, string Name, string DisplayName);

View File

@ -6,7 +6,9 @@ namespace YD_Event.Application.Scoring;
/// 成绩聚合与排名计算服务。
/// 规定赛:T1=3 名创意编排裁判创意分的平均值,T2=3 名完成质量裁判完成分的平均值,
/// T3=主裁违例扣分(每项扣 5 分)合计,总分=T1+T2-T3,均四舍五入保留两位小数。
/// 排名:总分高者在前,总分相同依次比较 T2、T1,全部相同则并列,其后名次按 N+M 规则顺延。
/// 花样赛:各难度裁判类型先聚合再按难度除数合并为 D,表现裁判平均分乘系数得 P(个人/车轮 0.05、交互绳 0.033),
/// 规定元素缺失数换算为 Q,主裁扣分换算为 M = max(0, 1 - 扣分),总分 = D × (1 + P) × Q × M(交互绳不计 Q)。
/// 排名:总分高者在前,总分相同按规则配置的比较字段依次比较,全部相同则并列,其后名次按 N+M 规则顺延。
/// </summary>
public static class EntryResultCalculationService
{
@ -24,7 +26,7 @@ public static class EntryResultCalculationService
var symbol = new EntrySymbolScore
{
Symbol = role.Symbol, Label = role.Label, RoleType = role.RoleType,
Aggregate = role.Aggregate, Sign = role.Sign
Aggregate = role.Aggregate, Sign = role.Sign, Factor = role.Factor
};
var roleSheets = sheets
.Where(x => string.Equals(JudgeRoleSchemaService.NormalizeRoleType(x.JudgeRoleType), role.RoleType, StringComparison.OrdinalIgnoreCase))
@ -47,7 +49,7 @@ public static class EntryResultCalculationService
total += role.Sign * symbol.Value;
}
result.TotalScore = config.Round(total);
result.TotalScore = config.IsFactorTotal ? ApplyFactorTotal(config, result) : config.Round(total);
// 任一裁判判定取消资格即取消该出场成绩,取消资格后不计总分、不参与排名。
var disqualified = sheets.FirstOrDefault(x => x.IsDisqualified);
@ -60,6 +62,82 @@ public static class EntryResultCalculationService
return result;
}
/// <summary>
/// 花样赛按乘法公式算总分:
/// D 由各难度裁判类型的聚合值加权后除以难度除数(未配置除数时按难度裁判类型数量取平均);
/// P = 表现裁判聚合值 × 表现系数;Q = max(0, 1 - 系数 × 规定元素缺失数);M = max(0, 1 - 扣分合计);
/// 未配置某个因子(如交互绳不计 Q)时该因子按 1 处理,不影响相乘结果。
/// <para>
/// 算完后把 D、P、Q 与扣分合计回写到成绩的四个分值字段,覆盖按裁判类型逐个写入的中间值
/// (车轮跳的 DA、DB 都写难度分会互相覆盖),排名的并列比较字段据此取值。
/// </para>
/// </summary>
private static decimal ApplyFactorTotal(ScoreRuleConfig config, EntryAggregateResult result)
{
var difficultySymbols = FactorSymbols(config, result, ScoreFactorCode.Difficulty);
var difficulty = 0m;
if (difficultySymbols.Count > 0)
{
foreach (var (symbol, weight) in difficultySymbols) difficulty += symbol.Value * weight;
// 除数为 0 视为漏配,按裁判类型数量取平均,避免除零。
var divisor = config.DifficultyDivisor is > 0m ? config.DifficultyDivisor!.Value : difficultySymbols.Count;
difficulty = config.Round(difficulty / divisor);
}
var performance = 0m;
foreach (var (symbol, weight) in FactorSymbols(config, result, ScoreFactorCode.Performance))
performance += symbol.Value * weight;
performance = config.Round(performance);
// 规定元素与扣分都是“扣减型”因子,聚合值越大因子越小,下限取 0。
var required = 1m;
var requiredSymbols = FactorSymbols(config, result, ScoreFactorCode.Required);
if (requiredSymbols.Count > 0)
{
var missing = 0m;
foreach (var (symbol, weight) in requiredSymbols) missing += symbol.Value * weight;
required = Math.Max(0m, 1m - missing);
}
var deduction = 1m;
var penalty = 0m;
var deductionSymbols = FactorSymbols(config, result, ScoreFactorCode.Deduction);
if (deductionSymbols.Count > 0)
{
foreach (var (symbol, weight) in deductionSymbols) penalty += symbol.Value * weight;
deduction = Math.Max(0m, 1m - penalty);
}
result.DifficultyFactor = difficulty;
result.PerformanceFactor = performance;
result.RequiredFactor = config.Round(required);
result.DeductionFactor = config.Round(deduction);
// 排名的并列比较取的是成绩表上的四个分值字段,这里统一回写为花样赛口径:
// 难度分=D,表现分=P,规定分=Q,扣分=主裁扣分合计(越小越靠前)。
result.DifficultyScore = difficulty;
result.PerformanceScore = performance;
result.RequiredScore = result.RequiredFactor;
result.DeductionScore = config.Round(penalty);
return config.Round(Math.Max(0m, difficulty * (1m + performance) * required * deduction));
}
/// <summary>
/// 取承担某个因子的计分符号及其因子系数
/// </summary>
private static List<(EntrySymbolScore Symbol, decimal Weight)> FactorSymbols(ScoreRuleConfig config,
EntryAggregateResult result, string factor)
{
var list = new List<(EntrySymbolScore, decimal)>();
foreach (var role in config.RoleAggregations)
{
if (!string.Equals(role.Factor, factor, StringComparison.OrdinalIgnoreCase)) continue;
var symbol = result.Symbols.FirstOrDefault(x => string.Equals(x.Symbol, role.Symbol, StringComparison.OrdinalIgnoreCase));
if (symbol == null) continue;
list.Add((symbol, role.FactorWeight));
}
return list;
}
/// <summary>
/// 按规则的并列比较顺序计算名次,名次并列时其后名次按 N+M 规则顺延(两个并列第一,下一名为第三名)
/// </summary>
@ -88,10 +166,11 @@ public static class EntryResultCalculationService
}
/// <summary>
/// 生成计分公式文本,如 T1 + T2 - T3
/// 生成计分公式文本。规定赛为 T1 + T2 - T3,花样赛为 D × (1 + P) × Q × M(未配置的因子不出现在公式里)。
/// </summary>
public static string BuildFormula(ScoreRuleConfig config)
{
if (config.IsFactorTotal) return BuildFactorFormula(config);
var text = new System.Text.StringBuilder();
foreach (var role in config.RoleAggregations)
{
@ -101,6 +180,19 @@ public static class EntryResultCalculationService
return text.ToString();
}
/// <summary>
/// 生成花样赛的乘法公式文本,只列出规则里实际配置了的因子
/// </summary>
private static string BuildFactorFormula(ScoreRuleConfig config)
{
var parts = new List<string>();
if (config.RoleAggregations.Any(x => x.Factor == ScoreFactorCode.Difficulty)) parts.Add("D");
if (config.RoleAggregations.Any(x => x.Factor == ScoreFactorCode.Performance)) parts.Add("(1 + P)");
if (config.RoleAggregations.Any(x => x.Factor == ScoreFactorCode.Required)) parts.Add("Q");
if (config.RoleAggregations.Any(x => x.Factor == ScoreFactorCode.Deduction)) parts.Add("M");
return string.Join(" × ", parts);
}
/// <summary>
/// 计算明细 JSON,落库到成绩的 CalculationJson,成绩复核与申诉时可完整还原计算过程
/// </summary>
@ -109,14 +201,22 @@ public static class EntryResultCalculationService
return JsonConvert.SerializeObject(new
{
ruleType = config.RuleType,
totalMode = config.TotalMode,
formula = result.Formula,
scoreDecimals = config.ScoreDecimals,
totalScore = result.TotalScore,
isDisqualified = result.IsDisqualified,
disqualifyReasonCode = result.DisqualifyReasonCode,
factors = config.IsFactorTotal
? new
{
d = result.DifficultyFactor, p = result.PerformanceFactor,
q = result.RequiredFactor, m = result.DeductionFactor
}
: null,
symbols = result.Symbols.Select(x => new
{
x.Symbol, x.Label, x.RoleType, x.Aggregate, x.Sign, x.Value, x.JudgeCount,
x.Symbol, x.Label, x.RoleType, x.Aggregate, x.Sign, x.Factor, x.Value, x.JudgeCount,
judges = x.Judges.Select(j => new { j.ScoreSheetId, j.RefereeId, j.JudgeName, j.JudgeRole, j.Value })
})
});

View File

@ -22,9 +22,11 @@ public static class ScoreRuleConfigService
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)
@ -44,7 +46,9 @@ public static class ScoreRuleConfigService
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(),
Sign = ReadSign(node["sign"])
Sign = ReadSign(node["sign"]),
Factor = ReadFactor(node["factor"]),
FactorWeight = ReadDecimal(node["factorWeight"]) ?? 1m
});
}
@ -64,6 +68,26 @@ public static class ScoreRuleConfigService
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>
/// 未配置聚合方式时按规定赛的 T1/T2/T3 兜底
/// </summary>

View File

@ -62,9 +62,55 @@ public static class ScoreSheetCalculationService
// 个人项目与集体项目的分值上限不同,按当前项目范围取值。
var scopedMax = ReadDecimal(extension["maxByScope"]?[scope]);
if (scopedMax.HasValue) definition.MaxValue = scopedMax.Value;
// 花样赛扩展:评分对象(车轮跳的 A/B)、表现维度权重、失误递增扣分、规定元素折算、难度聚合方式。
definition.IsAuto = ReadBool(extension["isAuto"]) ?? false;
definition.SubjectCode = Normalize(extension["subjectCode"]);
definition.SubjectName = Normalize(extension["subjectName"]);
definition.DimensionCode = Normalize(extension["dimensionCode"]);
definition.DimensionWeight = ReadDecimal(extension["dimensionWeight"]);
definition.BaseValue = ReadDecimal(extension["baseValue"]);
definition.AdjustSign = ReadInt(extension["adjustSign"]) ?? 0;
definition.IsFailureCount = ReadBool(extension["isFailureCount"]) ?? false;
definition.RequiredTarget = ReadDecimal(extension["requiredTarget"]);
definition.RequiredAsCategory = ReadBool(extension["requiredAsCategory"]) ?? false;
definition.DifficultyAggregate = Normalize(extension["difficultyAggregate"])?.ToLowerInvariant();
definition.DifficultyTopCount = ReadInt(extension["difficultyTopCount"]);
if (extension["stepDeductions"] is JArray stepDeductions && stepDeductions.Count > 0)
{
definition.StepDeductions = stepDeductions
.Select(x => ReadDecimal(x)).Where(x => x.HasValue).Select(x => x!.Value).ToList();
}
return definition;
}
/// <summary>
/// 读取扩展配置里的文本,空白按未配置处理
/// </summary>
private static string? Normalize(JToken? node)
{
var text = node?.ToString()?.Trim();
return string.IsNullOrEmpty(text) ? null : text;
}
/// <summary>
/// 读取扩展配置里的整数,非法值按未配置处理
/// </summary>
private static int? ReadInt(JToken? node)
{
try { return node?.Value<int?>(); }
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { return null; }
}
/// <summary>
/// 读取扩展配置里的布尔值,非法值按未配置处理
/// </summary>
private static bool? ReadBool(JToken? node)
{
try { return node?.Value<bool?>(); }
catch (Exception ex) when (ex is FormatException or InvalidCastException or OverflowException) { return null; }
}
/// <summary>
/// 读取扩展配置里的数值,非数字等非法值按未配置处理
/// </summary>
@ -89,6 +135,7 @@ public static class ScoreSheetCalculationService
/// <summary>
/// 按计分规则计算一张评分单。
/// requireAll 为 true(正式提交)时校验必填项与取值范围;为 false(保存草稿)时允许留空,只算已录入的部分。
/// 花样赛的表现维度分、失误自动减分、规定元素缺失数一律由服务端联动生成,客户端传来的自动项不参与计算。
/// </summary>
public static ScoreSheetCalculationResult Calculate(List<ScoreItemDefinition> definitions,
List<ScoreItemInputValue> inputs, ScoreRuleConfig config, bool requireAll)
@ -101,7 +148,9 @@ public static class ScoreSheetCalculationService
if (unknown != null) throw Oops.Bah($"评分项{unknown}不属于当前裁判的评分表");
var result = new ScoreSheetCalculationResult();
foreach (var definition in definitions)
// 自动项由服务端重建,先按录入项算出各维度小计。
var manual = definitions.Where(x => !x.IsAuto).ToList();
foreach (var definition in manual)
{
inputMap.TryGetValue(definition.ItemCode, out var input);
var line = BuildLine(definition, input, requireAll);
@ -120,6 +169,12 @@ public static class ScoreSheetCalculationService
continue;
}
// 表现维度的加减项、递增扣分项、规定元素计数项各有专门算法,不按小计直接累加。
if (definition.DimensionCode != null && definition.AdjustSign != 0) continue;
if (definition.StepDeductions is { Count: > 0 }) continue;
if (definition.Item.ScoreDimension == ScoreDimensionCode.Required
&& definition.Item.ItemType == ScoreItemTypeCode.RequiredElement) continue;
switch (definition.Item.ScoreDimension)
{
case ScoreDimensionCode.Difficulty: result.DifficultyScore += line.Subtotal; break;
@ -129,14 +184,207 @@ public static class ScoreSheetCalculationService
}
}
ApplyDifficultyAggregate(definitions, result, config);
ApplyPerformanceDimensions(definitions, result, config);
ApplyStepDeductions(definitions, result, config);
ApplyRequiredElements(definitions, result, config);
result.DifficultyScore = config.Round(result.DifficultyScore);
result.PerformanceScore = config.Round(result.PerformanceScore);
result.RequiredScore = config.Round(result.RequiredScore);
result.DeductionScore = config.Round(result.DeductionScore);
result.TotalScore = config.Round(result.DifficultyScore + result.PerformanceScore + result.RequiredScore - result.DeductionScore);
// 花样赛的单张评分单只提供本裁判的分项值,最终 R 由跨裁判聚合按 D×(1+P)×Q×M 计算。
result.TotalScore = config.IsFactorTotal
? config.Round(result.DifficultyScore + result.PerformanceScore + result.RequiredScore)
: config.Round(result.DifficultyScore + result.PerformanceScore + result.RequiredScore - result.DeductionScore);
return result;
}
/// <summary>
/// 难度分聚合。交互绳跳绳者按全部动作取平均、摇绳者取最高 20 个动作的平均,
/// 其余项目(个人花样、车轮跳花样)保持数量 × 等级分值累加。
/// </summary>
private static void ApplyDifficultyAggregate(List<ScoreItemDefinition> definitions,
ScoreSheetCalculationResult result, ScoreRuleConfig config)
{
var mode = definitions.Select(x => x.DifficultyAggregate).FirstOrDefault(x => !string.IsNullOrEmpty(x));
if (mode == null) return;
var difficultyLines = result.Lines
.Where(x => x.ScoreDimension == ScoreDimensionCode.Difficulty && x.CountValue is > 0m).ToList();
var actionCount = difficultyLines.Sum(x => x.CountValue!.Value);
if (actionCount <= 0m)
{
result.DifficultyScore = 0m;
return;
}
if (mode == DifficultyAggregateMode.Top)
{
// 取难度等级最高的 N 个动作:按单位分值降序逐级取用,分母固定为规则配置的动作数。
var topCount = definitions.Select(x => x.DifficultyTopCount).FirstOrDefault(x => x is > 0) ?? 20;
var remaining = (decimal)topCount;
var selected = 0m;
foreach (var line in difficultyLines.OrderByDescending(x => x.UnitValue ?? 0m))
{
if (remaining <= 0m) break;
var taken = Math.Min(remaining, line.CountValue!.Value);
selected += taken * (line.UnitValue ?? 0m);
remaining -= taken;
}
result.DifficultyScore = selected / topCount;
result.Steps.Add(new ScoreSheetStep
{
Code = "DIFFICULTY_TOP", Label = $"取最高{topCount}个动作的难度均值",
Formula = $"Σ(入选动作分值) ÷ {topCount}", Value = config.Round(result.DifficultyScore)
});
return;
}
result.DifficultyScore = result.DifficultyScore / actionCount;
result.Steps.Add(new ScoreSheetStep
{
Code = "DIFFICULTY_AVG", Label = "按动作总数取难度均值",
Formula = $"Σ(数量 × 等级分值) ÷ {Trim(actionCount)}", Value = config.Round(result.DifficultyScore)
});
}
/// <summary>
/// 表现维度计算。每个维度 = 基准分 + 人工加 - 人工减 - 失误自动减,限制在上下限内,
/// 再按各维度权重加权求和得到本裁判的表现分。失误自动减分作为自动项落明细,裁判不可直接修改。
/// </summary>
private static void ApplyPerformanceDimensions(List<ScoreItemDefinition> definitions,
ScoreSheetCalculationResult result, ScoreRuleConfig config)
{
var dimensions = definitions
.Where(x => x.DimensionCode != null && x.DimensionWeight is > 0m && x.IsAuto)
.ToList();
if (dimensions.Count == 0) return;
// 失误数联动五个维度各自动减 1 次。
var failure = definitions.FirstOrDefault(x => x.IsFailureCount);
var failureCount = failure == null
? 0m
: result.Lines.FirstOrDefault(x => string.Equals(x.ItemCode, failure.ItemCode, StringComparison.OrdinalIgnoreCase))?.CountValue ?? 0m;
var weighted = 0m;
foreach (var dimension in dimensions)
{
var adjustItems = definitions
.Where(x => !x.IsAuto && x.AdjustSign != 0
&& string.Equals(x.DimensionCode, dimension.DimensionCode, StringComparison.OrdinalIgnoreCase))
.ToList();
var raw = dimension.BaseValue ?? 0m;
foreach (var adjust in adjustItems)
{
var count = result.Lines
.FirstOrDefault(x => string.Equals(x.ItemCode, adjust.ItemCode, StringComparison.OrdinalIgnoreCase))?.CountValue ?? 0m;
raw += adjust.AdjustSign * count * (adjust.Item.UnitValue ?? 1m);
}
raw -= failureCount * (dimension.Item.UnitValue ?? 1m);
// 维度分限制在规则给出的上下限内(规程为 0~24)并取整。
var value = Math.Clamp(Math.Round(raw, 0, MidpointRounding.AwayFromZero), dimension.MinValue, dimension.MaxValue);
weighted += value * dimension.DimensionWeight!.Value;
result.Lines.Add(new ScoreSheetLine
{
ScoreRuleItemId = dimension.Item.Id, ItemCode = dimension.ItemCode, ItemName = dimension.Item.ItemName,
ItemType = dimension.Item.ItemType, ScoreDimension = dimension.Item.ScoreDimension,
InputValue = value, UnitValue = dimension.Item.UnitValue, Subtotal = value,
Sort = dimension.Item.Sort, IsAutoGenerated = true,
SubjectCode = dimension.SubjectCode,
ParentItemCode = failure?.ItemCode
});
result.Steps.Add(new ScoreSheetStep
{
Code = $"DIM_{dimension.DimensionCode}", Label = dimension.Item.ItemName,
Formula = $"{Trim(dimension.BaseValue ?? 0m)} + 人工加 - 人工减 - 失误{Trim(failureCount)}次 × {Trim(dimension.DimensionWeight.Value)}",
Value = value
});
}
result.PerformanceScore = weighted;
result.Steps.Add(new ScoreSheetStep
{
Code = "PERFORMANCE_WEIGHTED", Label = "表现分(各维度加权求和)",
Formula = "Σ(维度分 × 权重)", Value = config.Round(weighted)
});
}
/// <summary>
/// 递增扣分。主裁失误第 1 次扣 0.05、第 2 次 0.075、第 3 次起每次 0.1,序列用尽后按最后一档重复。
/// </summary>
private static void ApplyStepDeductions(List<ScoreItemDefinition> definitions,
ScoreSheetCalculationResult result, ScoreRuleConfig config)
{
foreach (var definition in definitions.Where(x => x.StepDeductions is { Count: > 0 }))
{
var line = result.Lines
.FirstOrDefault(x => string.Equals(x.ItemCode, definition.ItemCode, StringComparison.OrdinalIgnoreCase));
var count = (int)(line?.CountValue ?? 0m);
if (count <= 0) continue;
var steps = definition.StepDeductions!;
var deduction = 0m;
for (var i = 0; i < count; i++) deduction += steps[Math.Min(i, steps.Count - 1)];
if (line != null) line.Subtotal = deduction;
result.DeductionScore += deduction;
result.Steps.Add(new ScoreSheetStep
{
Code = $"STEP_{definition.ItemCode}", Label = definition.Item.ItemName,
Formula = string.Join(" + ", Enumerable.Range(0, count).Select(i => Trim(steps[Math.Min(i, steps.Count - 1)]))),
Value = config.Round(deduction)
});
}
}
/// <summary>
/// 规定元素完成量与缺失量。
/// 按数量折算时(个人花样)3 级及以上按 1 计、2 级及以下按 0.5 计;
/// 按类别计数时(车轮跳花样)每个类别只要录入数量大于 0 就计一类。
/// 缺失量 = max(0, 目标数 - 完成量),作为自动项落明细,供跨裁判聚合按 Q = 1 - 系数 × 缺失量 计算。
/// </summary>
private static void ApplyRequiredElements(List<ScoreItemDefinition> definitions,
ScoreSheetCalculationResult result, ScoreRuleConfig config)
{
var elements = definitions
.Where(x => !x.IsAuto && x.Item.ItemType == ScoreItemTypeCode.RequiredElement).ToList();
if (elements.Count == 0) return;
var completed = 0m;
foreach (var element in elements)
{
var line = result.Lines
.FirstOrDefault(x => string.Equals(x.ItemCode, element.ItemCode, StringComparison.OrdinalIgnoreCase));
if (line == null) continue;
var count = line.CountValue ?? 0m;
// 折算系数写在评分项的单位分值上:个人花样 3 级及以上为 1、2 级及以下为 0.5;按类别计数时录入即算一类。
line.Subtotal = element.RequiredAsCategory
? (count > 0m ? 1m : 0m)
: count * (element.Item.UnitValue ?? 1m);
completed += line.Subtotal;
}
result.RequiredCompletedCount = completed;
result.RequiredScore = completed;
var missingItem = definitions.FirstOrDefault(x => x.IsAuto && x.RequiredTarget.HasValue);
if (missingItem == null) return;
var target = missingItem.RequiredTarget!.Value;
var missing = Math.Max(0m, target - completed);
result.RequiredMissingCount = missing;
result.Lines.Add(new ScoreSheetLine
{
ScoreRuleItemId = missingItem.Item.Id, ItemCode = missingItem.ItemCode, ItemName = missingItem.Item.ItemName,
ItemType = missingItem.Item.ItemType, ScoreDimension = missingItem.Item.ScoreDimension,
InputValue = missing, CountValue = missing, Subtotal = 0m,
Sort = missingItem.Item.Sort, IsAutoGenerated = true
});
result.Steps.Add(new ScoreSheetStep
{
Code = "REQUIRED_MISSING", Label = "规定元素缺失数",
Formula = $"max(0, {Trim(target)} - {Trim(completed)})", Value = config.Round(missing)
});
}
/// <summary>
/// 计算明细 JSON,落库到评分单的 CalculationJson 便于复核与追溯
/// </summary>
@ -154,11 +402,15 @@ public static class ScoreSheetCalculationService
totalScore = result.TotalScore,
isDisqualified = result.IsDisqualified,
disqualifyReasonCode = result.DisqualifyReasonCode,
requiredCompletedCount = result.RequiredCompletedCount,
requiredMissingCount = result.RequiredMissingCount,
lines = result.Lines.Select(x => new
{
x.ItemCode, x.ItemName, x.ItemType, x.ScoreDimension,
x.CountValue, x.InputValue, x.UnitValue, x.Subtotal
})
x.CountValue, x.InputValue, x.UnitValue, x.Subtotal,
x.SubjectCode, x.IsAutoGenerated, x.ParentItemCode
}),
steps = result.Steps.Select(x => new { x.Code, x.Label, x.Formula, x.Value })
});
}
@ -172,7 +424,8 @@ public static class ScoreSheetCalculationService
{
ScoreRuleItemId = item.Id, ItemCode = item.ItemCode, ItemName = item.ItemName,
ItemType = item.ItemType, ScoreDimension = item.ScoreDimension,
UnitValue = item.UnitValue, Sort = item.Sort
UnitValue = item.UnitValue, Sort = item.Sort,
SubjectCode = definition.SubjectCode
};
switch (item.CalcMode)

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,54 @@
[
{
"Id": 3000,
"RuleCode": "ROPE_PRESCRIBED",
"RuleName": "规定赛计分规则",
"JudgeRoleSchemaJson": "[{\"title\":\"创意编排裁判\",\"slots\":[{\"code\":\"CJ\",\"name\":\"创意编排裁判\",\"min\":1}]},{\"title\":\"完成质量裁判\",\"slots\":[{\"code\":\"CQ\",\"name\":\"完成质量裁判\",\"min\":1}]},{\"title\":\"主裁判\",\"slots\":[{\"code\":\"T\",\"name\":\"主裁判\",\"min\":1,\"max\":1}]}]",
"RuleConfigJson": "{\"ruleType\":\"PRESCRIBED\",\"baseScore\":100,\"scoreDecimals\":2,\"deductionUnit\":5,\"correctionNeedApproval\":false,\"roleAggregations\":[{\"roleType\":\"CJ\",\"symbol\":\"T1\",\"label\":\"创意分\",\"sourceField\":\"PerformanceScore\",\"targetField\":\"PerformanceScore\",\"aggregate\":\"avg\",\"sign\":1},{\"roleType\":\"CQ\",\"symbol\":\"T2\",\"label\":\"完成分\",\"sourceField\":\"RequiredScore\",\"targetField\":\"RequiredScore\",\"aggregate\":\"avg\",\"sign\":1},{\"roleType\":\"T\",\"symbol\":\"T3\",\"label\":\"扣分\",\"sourceField\":\"DeductionScore\",\"targetField\":\"DeductionScore\",\"aggregate\":\"sum\",\"sign\":-1}],\"tieBreakers\":[\"TotalScore\",\"RequiredScore\",\"PerformanceScore\"]}",
"RuleVersion": 1,
"Status": 0,
"Sort": 1,
"DeleteMark": false,
"CreatedUserId": 1000,
"CreatedTime": "2026-08-19T00:00:00"
}
]
{
"Id": 3000,
"RuleCode": "ROPE_PRESCRIBED",
"RuleName": "规定赛计分规则",
"JudgeRoleSchemaJson": "[{\"title\":\"创意编排裁判\",\"slots\":[{\"code\":\"CJ\",\"name\":\"创意编排裁判\",\"min\":1}]},{\"title\":\"完成质量裁判\",\"slots\":[{\"code\":\"CQ\",\"name\":\"完成质量裁判\",\"min\":1}]},{\"title\":\"主裁判\",\"slots\":[{\"code\":\"T\",\"name\":\"主裁判\",\"min\":1,\"max\":1}]}]",
"RuleConfigJson": "{\"ruleType\":\"PRESCRIBED\",\"baseScore\":100,\"scoreDecimals\":2,\"deductionUnit\":5,\"correctionNeedApproval\":false,\"roleAggregations\":[{\"roleType\":\"CJ\",\"symbol\":\"T1\",\"label\":\"创意分\",\"sourceField\":\"PerformanceScore\",\"targetField\":\"PerformanceScore\",\"aggregate\":\"avg\",\"sign\":1},{\"roleType\":\"CQ\",\"symbol\":\"T2\",\"label\":\"完成分\",\"sourceField\":\"RequiredScore\",\"targetField\":\"RequiredScore\",\"aggregate\":\"avg\",\"sign\":1},{\"roleType\":\"T\",\"symbol\":\"T3\",\"label\":\"扣分\",\"sourceField\":\"DeductionScore\",\"targetField\":\"DeductionScore\",\"aggregate\":\"sum\",\"sign\":-1}],\"tieBreakers\":[\"TotalScore\",\"RequiredScore\",\"PerformanceScore\"]}",
"RuleVersion": 1,
"Status": 0,
"Sort": 1,
"DeleteMark": false,
"CreatedUserId": 1000,
"CreatedTime": "2026-08-19T00:00:00"
},
{
"Id": 3100,
"RuleCode": "ROPE_FREESTYLE_WHEEL",
"RuleName": "车轮跳花样计分规则",
"JudgeRoleSchemaJson": "[{\"title\":\"难度裁判\",\"slots\":[{\"code\":\"DA\",\"name\":\"难度裁判-DA(运动员A难度裁判)\",\"min\":2},{\"code\":\"DB\",\"name\":\"难度裁判-DB(运动员B难度裁判)\",\"min\":2}]},{\"title\":\"表现裁判\",\"slots\":[{\"code\":\"P\",\"name\":\"表现裁判-P\",\"min\":2}]},{\"title\":\"主裁判\",\"slots\":[{\"code\":\"T\",\"name\":\"主裁判-T\",\"min\":1,\"max\":1}]}]",
"RuleConfigJson": "{\"ruleType\":\"FREESTYLE_WHEEL\",\"totalMode\":\"factor\",\"baseScore\":0,\"scoreDecimals\":2,\"difficultyDivisor\":2,\"correctionNeedApproval\":false,\"roleAggregations\":[{\"roleType\":\"DA\",\"symbol\":\"dA\",\"label\":\"运动员A难度\",\"sourceField\":\"DifficultyScore\",\"targetField\":\"DifficultyScore\",\"aggregate\":\"avg\",\"sign\":1,\"factor\":\"D\",\"factorWeight\":1},{\"roleType\":\"DB\",\"symbol\":\"dB\",\"label\":\"运动员B难度\",\"sourceField\":\"DifficultyScore\",\"targetField\":\"DifficultyScore\",\"aggregate\":\"avg\",\"sign\":1,\"factor\":\"D\",\"factorWeight\":1},{\"roleType\":\"P\",\"symbol\":\"P\",\"label\":\"表现\",\"sourceField\":\"PerformanceScore\",\"targetField\":\"PerformanceScore\",\"aggregate\":\"avg\",\"sign\":1,\"factor\":\"P\",\"factorWeight\":0.05},{\"roleType\":\"T\",\"symbol\":\"Q\",\"label\":\"规定元素缺失\",\"sourceField\":\"RequiredMissingCount\",\"targetField\":\"RequiredScore\",\"aggregate\":\"max\",\"sign\":1,\"factor\":\"Q\",\"factorWeight\":0.05},{\"roleType\":\"T\",\"symbol\":\"M\",\"label\":\"扣分\",\"sourceField\":\"DeductionScore\",\"targetField\":\"DeductionScore\",\"aggregate\":\"sum\",\"sign\":-1,\"factor\":\"M\",\"factorWeight\":1}],\"tieBreakers\":[\"TotalScore\",\"-DeductionScore\",\"RequiredScore\",\"PerformanceScore\",\"DifficultyScore\"]}",
"RuleVersion": 1,
"Status": 0,
"Sort": 2,
"DeleteMark": false,
"CreatedUserId": 1000,
"CreatedTime": "2026-08-19T00:00:00"
},
{
"Id": 3200,
"RuleCode": "ROPE_FREESTYLE_WIRE",
"RuleName": "交互绳花样计分规则",
"JudgeRoleSchemaJson": "[{\"title\":\"难度裁判\",\"slots\":[{\"code\":\"DJ\",\"name\":\"难度裁判-DJ(跳绳运动员难度裁判)\",\"min\":2},{\"code\":\"DT\",\"name\":\"难度裁判-DT(摇绳运动员难度裁判)\",\"min\":2}]},{\"title\":\"表现裁判\",\"slots\":[{\"code\":\"P\",\"name\":\"表现裁判-P\",\"min\":2}]},{\"title\":\"主裁判\",\"slots\":[{\"code\":\"T\",\"name\":\"主裁判-T\",\"min\":1,\"max\":1}]}]",
"RuleConfigJson": "{\"ruleType\":\"FREESTYLE_WIRE\",\"totalMode\":\"factor\",\"baseScore\":0,\"scoreDecimals\":2,\"difficultyDivisor\":1,\"correctionNeedApproval\":false,\"roleAggregations\":[{\"roleType\":\"DJ\",\"symbol\":\"dJ\",\"label\":\"跳绳运动员难度\",\"sourceField\":\"DifficultyScore\",\"targetField\":\"DifficultyScore\",\"aggregate\":\"avg\",\"sign\":1,\"factor\":\"D\",\"factorWeight\":1.4},{\"roleType\":\"DT\",\"symbol\":\"dT\",\"label\":\"摇绳运动员难度\",\"sourceField\":\"DifficultyScore\",\"targetField\":\"DifficultyScore\",\"aggregate\":\"avg\",\"sign\":1,\"factor\":\"D\",\"factorWeight\":1},{\"roleType\":\"P\",\"symbol\":\"P\",\"label\":\"表现\",\"sourceField\":\"PerformanceScore\",\"targetField\":\"PerformanceScore\",\"aggregate\":\"avg\",\"sign\":1,\"factor\":\"P\",\"factorWeight\":0.033},{\"roleType\":\"T\",\"symbol\":\"M\",\"label\":\"扣分\",\"sourceField\":\"DeductionScore\",\"targetField\":\"DeductionScore\",\"aggregate\":\"sum\",\"sign\":-1,\"factor\":\"M\",\"factorWeight\":1}],\"tieBreakers\":[\"TotalScore\",\"-DeductionScore\",\"PerformanceScore\",\"DifficultyScore\"]}",
"RuleVersion": 1,
"Status": 0,
"Sort": 3,
"DeleteMark": false,
"CreatedUserId": 1000,
"CreatedTime": "2026-08-19T00:00:00"
},
{
"Id": 3300,
"RuleCode": "ROPE_SHOWCASE",
"RuleName": "表演赛计分规则",
"JudgeRoleSchemaJson": "[{\"title\":\"难度裁判\",\"slots\":[{\"code\":\"DGA\",\"name\":\"难度裁判-A组裁判\",\"min\":3},{\"code\":\"DGB\",\"name\":\"难度裁判-B组裁判\",\"min\":3}]},{\"title\":\"表现裁判\",\"slots\":[{\"code\":\"P\",\"name\":\"表现裁判-P\",\"min\":3}]},{\"title\":\"主裁判\",\"slots\":[{\"code\":\"T\",\"name\":\"主裁判-T\",\"min\":1,\"max\":1}]}]",
"RuleConfigJson": "{\"ruleType\":\"SHOWCASE\",\"totalMode\":\"factor\",\"baseScore\":0,\"scoreDecimals\":2,\"difficultyDivisor\":2,\"correctionNeedApproval\":false,\"roleAggregations\":[{\"roleType\":\"DGA\",\"symbol\":\"A\",\"label\":\"A组难度\",\"sourceField\":\"DifficultyScore\",\"targetField\":\"DifficultyScore\",\"aggregate\":\"avg\",\"sign\":1,\"factor\":\"D\",\"factorWeight\":1},{\"roleType\":\"DGB\",\"symbol\":\"B\",\"label\":\"B组难度\",\"sourceField\":\"DifficultyScore\",\"targetField\":\"DifficultyScore\",\"aggregate\":\"avg\",\"sign\":1,\"factor\":\"D\",\"factorWeight\":1},{\"roleType\":\"P\",\"symbol\":\"P\",\"label\":\"表现\",\"sourceField\":\"PerformanceScore\",\"targetField\":\"PerformanceScore\",\"aggregate\":\"avg\",\"sign\":1,\"factor\":\"P\",\"factorWeight\":0.05},{\"roleType\":\"T\",\"symbol\":\"Q\",\"label\":\"规定元素缺失\",\"sourceField\":\"RequiredMissingCount\",\"targetField\":\"RequiredScore\",\"aggregate\":\"max\",\"sign\":1,\"factor\":\"Q\",\"factorWeight\":0.05},{\"roleType\":\"T\",\"symbol\":\"M\",\"label\":\"扣分\",\"sourceField\":\"DeductionScore\",\"targetField\":\"DeductionScore\",\"aggregate\":\"sum\",\"sign\":-1,\"factor\":\"M\",\"factorWeight\":1}],\"tieBreakers\":[\"TotalScore\",\"-DeductionScore\",\"RequiredScore\",\"PerformanceScore\",\"DifficultyScore\"]}",
"RuleVersion": 1,
"Status": 0,
"Sort": 4,
"DeleteMark": false,
"CreatedUserId": 1000,
"CreatedTime": "2026-08-19T00:00:00"
}
]

File diff suppressed because it is too large Load Diff