diff --git a/YD_Event.Application/Event/Scoring/EntryResultCalculator.cs b/YD_Event.Application/Event/Scoring/EntryResultCalculator.cs index 848675a..a4a2248 100644 --- a/YD_Event.Application/Event/Scoring/EntryResultCalculator.cs +++ b/YD_Event.Application/Event/Scoring/EntryResultCalculator.cs @@ -97,6 +97,8 @@ public class EntrySymbolScore /// public class EntryAggregateResult { + /// 舍入前的因子,仅用于复核;计算使用舍入后的因子。 + public EntryRawFactors? RawFactors { get; set; } /// /// 难度分(规定赛不使用,花样赛复用同一套聚合时写入) /// @@ -159,7 +161,7 @@ public class EntryAggregateResult /// /// 花样赛的扣分因子 M = max(0, 1 - 最终扣分合计)。 - /// 含表现裁判失误项的规则会先把表现裁判与主裁的失误次数取平均,再与主裁其他扣分合并。 + /// 含表现裁判失误项的规则会先取表现裁判失误平均数,再加主裁失误数并取整,最后与主裁其他扣分合并。 /// public decimal DeductionFactor { get; set; } = 1m; @@ -175,8 +177,11 @@ public class EntryAggregateResult Symbols.FirstOrDefault(x => string.Equals(x.Symbol, symbol, StringComparison.OrdinalIgnoreCase))?.Value ?? 0m; } +/// 保存总分计算前的原始因子,便于复核舍入过程。 +public record EntryRawFactors(decimal D, decimal P, decimal Q, decimal M); + /// -/// 花样赛失误次数跨裁判平均及扣分换算明细 +/// 花样赛表现裁判失误平均、主裁失误合并及扣分换算明细 /// public class EntryFailureAggregation { @@ -191,15 +196,25 @@ public class EntryFailureAggregation public decimal ChiefFailureCount { get; set; } /// - /// 参与平均的裁判人数(表现裁判人数 + 1 名主裁) + /// 参与平均的表现裁判人数(主裁不参与该平均) /// public int JudgeCount { get; set; } /// - /// 未取整的平均失误次数 + /// 兼容历史快照的字段;新快照表示“表现裁判平均失误数 + 主裁失误数”的未取整结果 /// public decimal AverageFailureCount { get; set; } + /// + /// 表现裁判内部平均失误次数 + /// + public decimal PerformanceAverageFailureCount { get; set; } + + /// + /// 表现裁判平均失误次数与主裁失误次数之和,尚未取整 + /// + public decimal CombinedFailureCount { get; set; } + /// /// 四舍五入后用于阶梯扣分的失误次数 /// @@ -211,7 +226,7 @@ public class EntryFailureAggregation public decimal OriginalChiefFailureDeduction { get; set; } /// - /// 平均失误次数换算出的新失误扣分 + /// 合并失误次数换算出的新失误扣分 /// public decimal FailureDeduction { get; set; } diff --git a/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs b/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs index 6f1c3ab..e801551 100644 --- a/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs +++ b/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs @@ -467,6 +467,8 @@ public class JudgeScoreItemDto /// public class JudgeScoreSheetOutput { + /// 历史互动配合数据缺少等级时,提示重新核对录入,不能自动推断。 + public string? RuleMigrationNotice { get; set; } /// /// 出场记录ID /// diff --git a/YD_Event.Application/Judge/JudgeScoringController.cs b/YD_Event.Application/Judge/JudgeScoringController.cs index 74af7c5..4c12df0 100644 --- a/YD_Event.Application/Judge/JudgeScoringController.cs +++ b/YD_Event.Application/Judge/JudgeScoringController.cs @@ -1,4 +1,4 @@ -using YD_Event.Application.Auth; +using YD_Event.Application.Auth; using YD_Event.Application.Common; using YD_Event.Application.Event; using YD_Event.Application.Scoring.Dtos; @@ -12,7 +12,7 @@ namespace YD_Event.Application.Scoring; /// 主裁每项违例从 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 - 最终扣分合计);失误次数由表现裁判与主裁按人数平均,其他扣分仍取主裁记录; +/// 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 - (失误 + 违例))。 @@ -333,7 +333,7 @@ public class JudgeScoringController : IDynamicApiController IsRematch = entry.IsRematch, ScoreStatus = status, ScoreStatusName = ScoreStatusName(status), - MyScore = submitted == null ? null : config.Format(submitted.TotalScore), + MyScore = submitted == null ? null : ScoreDisplayService.JudgeText(submitted, config.IsFactorTotal), IsDisqualified = submitted?.IsDisqualified ?? false, ScoreSheetId = submitted?.Id ?? draft?.Id ?? 0 }; @@ -365,7 +365,12 @@ public class JudgeScoringController : IDynamicApiController var details = source == null ? new List() : await _detailRepository.GetListAsync(x => x.ScoreSheetId == source.Id); - var inputs = details.Select(x => new ScoreItemInputValue + var legacyInteraction = roleType == "T" + && (ctx.Config.RuleType.ToUpperInvariant() is "FREESTYLE_DOUBLE_SYNC" or "FREESTYLE_QUAD_SYNC") + ? details.FirstOrDefault(x => x.ItemCode == "REQ_INTERACTION") : null; + // 仅排除已退役的互动配合录入项,历史明细仍保留在原评分单中供复核。 + var inputs = details.Where(x => legacyInteraction == null || x.ItemCode != "REQ_INTERACTION") + .Select(x => new ScoreItemInputValue { ItemCode = x.ItemCode, InputValue = x.InputValue, @@ -387,6 +392,8 @@ public class JudgeScoringController : IDynamicApiController return new JudgeScoreSheetOutput { + RuleMigrationNotice = legacyInteraction == null ? null + : $"旧互动配合记录为 {legacyInteraction.CountValue ?? 0m} 个,未区分等级。原记录已保留,请重新核对两档数量(含0)后保存或提交。", SessionEntryId = entry.Id, SessionId = ctx.Session.Id, JudgeTaskId = draft?.JudgeTaskId ?? submitted?.JudgeTaskId ?? 0, @@ -651,7 +658,7 @@ public class JudgeScoringController : IDynamicApiController 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) + Deduction = outcome.Aggregate.DeductionFactor.ToString("F3", System.Globalization.CultureInfo.InvariantCulture) }, Progress = outcome.Roles.Select(ToProgressDto).ToList() }; @@ -695,7 +702,7 @@ public class JudgeScoringController : IDynamicApiController SessionEntryId = sheet.SessionEntryId, EntryNo = entry?.EntryNo ?? string.Empty, ParticipantName = entry == null ? string.Empty : DisplayParticipantName(entry, ctx.Scope), - MyScore = ctx.Config.Format(sheet.TotalScore), + MyScore = ScoreDisplayService.JudgeText(sheet, ctx.Config.IsFactorTotal), SubmittedTime = sheet.SubmittedTime, CorrectionStatus = status, CorrectionStatusName = CorrectionStatusName(status), @@ -1032,7 +1039,7 @@ public class JudgeScoringController : IDynamicApiController /// /// 个人项始终显示出场人快照;只有集体项才优先显示队名。 - /// 兼容修复前已经编排、TeamName 被误写为单位名的历史出场记录。 + /// 兼容历史出场记录中的单位名称字段。 /// private static string DisplayParticipantName(EventSessionEntry entry, string scope) => scope == ScoreScope.Team && !string.IsNullOrWhiteSpace(entry.TeamName) @@ -1221,7 +1228,7 @@ public class JudgeScoringController : IDynamicApiController { ScoreSheetId = sheet.Id, IsDuplicate = isDuplicate, - MyScore = ctx.Config.Format(sheet.TotalScore), + MyScore = ScoreDisplayService.JudgeText(sheet, ctx.Config.IsFactorTotal), ResultCompleted = outcome?.IsCompleted ?? false, TotalScore = !string.Equals(JudgeRoleSchemaService.NormalizeRoleType(sheet.JudgeRoleType), JudgeRoleSchemaService.RoleChief, StringComparison.OrdinalIgnoreCase) diff --git a/YD_Event.Application/Scoring/EntryResultCalculationService.cs b/YD_Event.Application/Scoring/EntryResultCalculationService.cs index adab47e..1d6415c 100644 --- a/YD_Event.Application/Scoring/EntryResultCalculationService.cs +++ b/YD_Event.Application/Scoring/EntryResultCalculationService.cs @@ -7,7 +7,7 @@ namespace YD_Event.Application.Scoring; /// 规定赛:T1=3 名创意编排裁判创意分的平均值,T2=3 名完成质量裁判完成分的平均值, /// T3=主裁违例扣分(每项扣 5 分)合计,总分=T1+T2-T3,均四舍五入保留两位小数。 /// 花样赛:各难度裁判类型先聚合再按难度除数合并为 D,表现裁判平均分乘系数得 P(个人/车轮 0.05、交互绳 0.033), -/// 规定元素缺失数换算为 Q;扣分中的失误由表现裁判与主裁按人数平均后换算,再加主裁其他扣分, +/// 规定元素缺失数换算为 Q;扣分中的失误先取表现裁判平均数,再加主裁失误数,取整后换算,并加上主裁其他扣分, /// 得到 M = max(0, 1 - 扣分),总分 = D × (1 + P) × Q × M(交互绳不计 Q)。 /// 排名:总分高者在前,总分相同按规则配置的比较字段依次比较,全部相同则并列,其后名次按 N+M 规则顺延。 /// @@ -61,9 +61,7 @@ public static class EntryResultCalculationService total += role.Sign * symbol.Value; } - // 所有同时配置了表现裁判失误与主裁失误的花样规则统一采用跨裁判平均: - // (各表现裁判失误数 + 主裁失误数) / (表现裁判人数 + 1),四舍五入后再套用失误阶梯。 - // 主裁的违例、间歇、空间违例等其他扣分保持原值,且先剔除主裁原失误扣分以避免重复计算。 + // 表现裁判失误先求平均,再与主裁失误合计后计算阶梯扣分;其他扣分保持原值。 ApplyCombinedFailureDeduction(config, result, sheets, scoreDetails, ruleItems, scope); result.TotalScore = config.IsFactorTotal ? ApplyFactorTotal(config, result) : config.Round(total); @@ -117,7 +115,7 @@ public static class EntryResultCalculationService StringComparison.OrdinalIgnoreCase)) .OrderBy(x => x.JudgeRole).ThenBy(x => x.Id) .ToList(); - // 现有规则要求至少 1 名表现裁判且主裁固定 1 人;数据异常时保留旧算法,避免静默改变口径。 + // 规则要求至少一名表现裁判和一名主裁;数据不完整时不执行跨裁判失误合并。 if (performanceSheets.Count == 0 || chiefSheets.Count != 1) return; var detailsBySheet = scoreDetails.GroupBy(x => x.ScoreSheetId) @@ -132,9 +130,10 @@ public static class EntryResultCalculationService .ToList(); var chiefSheet = chiefSheets[0]; var chiefCount = FailureCount(chiefSheet.Id, ChiefFailureItemCode); - var judgeCount = performanceCounts.Count + 1; - var averageCount = (performanceCounts.Sum() + chiefCount) / judgeCount; - var roundedCount = (int)Math.Round(averageCount, 0, MidpointRounding.AwayFromZero); + var performanceJudgeCount = performanceCounts.Count; + var performanceAverageCount = performanceCounts.Average(); + var combinedCount = performanceAverageCount + chiefCount; + var roundedCount = (int)Math.Round(combinedCount, 0, MidpointRounding.AwayFromZero); var chiefDefinition = ScoreSheetCalculationService.CreateDefinition(chiefFailureItem, scope); var steps = chiefDefinition.StepDeductions; @@ -149,7 +148,7 @@ public static class EntryResultCalculationService .Sum(x => x.Subtotal) : 0m; var otherDeduction = Math.Max(0m, deductionSymbol.Value - originalChiefFailureDeduction); - // m 可能包含 0.075,因子计算前不得提前按最终展示精度取整。 + // 保留扣分过程的精度,避免阶梯扣分在因子计算前丢失小数。 var totalDeduction = otherDeduction + failureDeduction; deductionSymbol.Value = totalDeduction; result.DeductionScore = totalDeduction; @@ -157,8 +156,11 @@ public static class EntryResultCalculationService { PerformanceFailureCounts = performanceCounts, ChiefFailureCount = chiefCount, - JudgeCount = judgeCount, - AverageFailureCount = averageCount, + JudgeCount = performanceJudgeCount, + PerformanceAverageFailureCount = performanceAverageCount, + CombinedFailureCount = combinedCount, + // 保留历史字段以兼容已有计算快照;新快照记录合并后的未取整失误数。 + AverageFailureCount = combinedCount, RoundedFailureCount = roundedCount, OriginalChiefFailureDeduction = originalChiefFailureDeduction, FailureDeduction = failureDeduction, @@ -212,12 +214,17 @@ public static class EntryResultCalculationService deduction = Math.Max(0m, 1m - penalty); } + // 总分计算前,D/P/Q 保留两位、M 保留三位;规定缺失量保留小数,失误次数在扣分流程中取整。 + result.RawFactors = new EntryRawFactors(difficulty, performance, required, deduction); + difficulty = Math.Round(difficulty, 2, MidpointRounding.AwayFromZero); + performance = Math.Round(performance, 2, MidpointRounding.AwayFromZero); + required = Math.Round(required, 2, MidpointRounding.AwayFromZero); + deduction = Math.Round(deduction, 3, MidpointRounding.AwayFromZero); result.DifficultyFactor = difficulty; result.PerformanceFactor = performance; result.RequiredFactor = required; result.DeductionFactor = deduction; - // 排名的并列比较取的是成绩表上的四个分值字段,这里统一回写为花样赛口径: - // 难度分=D,表现分=P,规定分=Q,扣分=最终扣分合计(越小越靠前)。 + // 花样赛排名字段分别保存 D、P、Q 和扣分合计。 result.DifficultyScore = difficulty; result.PerformanceScore = performance; result.RequiredScore = result.RequiredFactor; @@ -308,6 +315,8 @@ public static class EntryResultCalculationService totalMode = config.TotalMode, formula = result.Formula, scoreDecimals = config.ScoreDecimals, + calculationVersion = "54-69-70-71-v2", + rawFactors = result.RawFactors, totalScore = result.TotalScore, isDisqualified = result.IsDisqualified, disqualifyReasonCode = result.DisqualifyReasonCode, @@ -326,6 +335,8 @@ public static class EntryResultCalculationService chiefFailureCount = result.FailureAggregation.ChiefFailureCount, judgeCount = result.FailureAggregation.JudgeCount, averageFailureCount = result.FailureAggregation.AverageFailureCount, + performanceAverageFailureCount = result.FailureAggregation.PerformanceAverageFailureCount, + combinedFailureCount = result.FailureAggregation.CombinedFailureCount, roundedFailureCount = result.FailureAggregation.RoundedFailureCount, originalChiefFailureDeduction = result.FailureAggregation.OriginalChiefFailureDeduction, failureDeduction = result.FailureAggregation.FailureDeduction, diff --git a/YD_Event.Application/Scoring/ScoreDisplayService.cs b/YD_Event.Application/Scoring/ScoreDisplayService.cs new file mode 100644 index 0000000..09eb41e --- /dev/null +++ b/YD_Event.Application/Scoring/ScoreDisplayService.cs @@ -0,0 +1,35 @@ +namespace YD_Event.Application.Scoring; + +/// 从已保存的计算快照读取展示值,不能在查询时静默重算历史成绩。 +public static class ScoreDisplayService +{ + /// 按角色格式化本人得分,主裁系数保留三位。 + public static string JudgeText(EventJudgeScoreSheet sheet, bool isFactorTotal) => + sheet.IsDisqualified ? "DQ" : JudgeValue(sheet, isFactorTotal).ToString( + isFactorTotal && JudgeRoleSchemaService.NormalizeRoleType(sheet.JudgeRoleType) == "T" ? "F3" : "F2", + System.Globalization.CultureInfo.InvariantCulture); + + /// 本裁判分值;主裁为本人的系数预览,综合M以成绩快照为准。 + public static decimal JudgeValue(EventJudgeScoreSheet sheet, bool isFactorTotal) + { + if (!isFactorTotal) return sheet.TotalScore; + var role = JudgeRoleSchemaService.NormalizeRoleType(sheet.JudgeRoleType); + if (role == "T") return Math.Max(0m, 1m - sheet.DeductionScore); + if (role == "P") return sheet.PerformanceScore; + return role.StartsWith("D", StringComparison.OrdinalIgnoreCase) ? sheet.DifficultyScore : sheet.TotalScore; + } + + /// 花样得分系数 M;非花样或缺少计算快照时不推断数值。 + public static decimal? DeductionFactor(string? calculationJson) + { + if (string.IsNullOrWhiteSpace(calculationJson)) return null; + try + { + return JObject.Parse(calculationJson)["factors"]?["m"]?.Value(); + } + catch (Exception ex) when (ex is JsonException or FormatException or InvalidCastException or OverflowException) + { + return null; + } + } +} diff --git a/YD_Event.Application/Scoring/ScoreRuleConfigService.cs b/YD_Event.Application/Scoring/ScoreRuleConfigService.cs index b43ec51..84cc3b9 100644 --- a/YD_Event.Application/Scoring/ScoreRuleConfigService.cs +++ b/YD_Event.Application/Scoring/ScoreRuleConfigService.cs @@ -57,9 +57,26 @@ public static class ScoreRuleConfigService .Select(x => x.ToString()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList(); if (tieBreakers.Count > 0) config.TieBreakers = tieBreakers; + ApplyBugRuleOverrides(config); return Fallback(config); } + /// 个人花样与同步花样采用相同的表现分计算口径。 + public static bool IsIndividualFamily(string ruleType) => + ruleType.ToUpperInvariant() is "FREESTYLE_SINGLE" or "FREESTYLE_DOUBLE_SYNC" or "FREESTYLE_QUAD_SYNC"; + + /// 补齐花样赛规则的默认计算参数。解析过程只生成内存配置,不改写数据库。 + 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; + } + } + /// /// 读取参与总分的符号,非法值按加分处理 /// diff --git a/YD_Event.Application/Scoring/ScoreSheetCalculationService.cs b/YD_Event.Application/Scoring/ScoreSheetCalculationService.cs index 5ed31e5..d35fdb5 100644 --- a/YD_Event.Application/Scoring/ScoreSheetCalculationService.cs +++ b/YD_Event.Application/Scoring/ScoreSheetCalculationService.cs @@ -1,4 +1,4 @@ -using YD_Event.Application.Scoring.Dtos; +using YD_Event.Application.Scoring.Dtos; namespace YD_Event.Application.Scoring; @@ -32,6 +32,7 @@ public static class ScoreSheetCalculationService public static List ResolveItems(IEnumerable ruleItems, string judgeRoleType, string scope) { var matchedItems = ruleItems + .Where(x => !x.DeleteMark) .Where(x => string.Equals(x.JudgeRoleType, judgeRoleType, StringComparison.OrdinalIgnoreCase)) .Where(x => MatchScope(x, scope)) .OrderBy(x => x.ItemType).ThenBy(x => x.Sort).ThenBy(x => x.Id) @@ -79,7 +80,7 @@ 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"]); @@ -102,7 +103,7 @@ public static class ScoreSheetCalculationService } /// - /// 兼容尚未执行规则数据更新的环境:车轮跳 4 类规定元素每类均需完成 4 个。 + /// 车轮跳包含四类规定元素,每类最多录入四个,合计规定数量为十六个。 /// private static void ApplyWheelRequiredElementCompatibility(ScoreRuleItem item, ScoreItemDefinition definition, bool isWheelRequiredRule) @@ -178,6 +179,12 @@ public static class ScoreSheetCalculationService public static ScoreSheetCalculationResult Calculate(List definitions, List inputs, ScoreRuleConfig config, bool requireAll) { + // 同步主裁需要分别录入两个等级区间,未分级数量不能自动归入任一等级。 + if ((config.RuleType.ToUpperInvariant() is "FREESTYLE_DOUBLE_SYNC" or "FREESTYLE_QUAD_SYNC") + && definitions.Any(x => x.Item.JudgeRoleType == "T") + && (!definitions.Any(x => x.ItemCode == "REQ_INTERACTION_N3_PLUS") + || !definitions.Any(x => x.ItemCode == "REQ_INTERACTION_N2_MINUS"))) + throw Oops.Bah("同步花样互动配合规则尚未更新,请先刷新评分任务并使用分级录入"); var inputMap = inputs .Where(x => !string.IsNullOrWhiteSpace(x.ItemCode)) .GroupBy(x => x.ItemCode.Trim(), StringComparer.OrdinalIgnoreCase) @@ -211,6 +218,7 @@ public static class ScoreSheetCalculationService } // 表现维度的加减项、递增扣分项、规定元素计数项各有专门算法,不按小计直接累加。 + if (definition.IsFailureCount) continue; if (definition.DimensionCode != null && definition.AdjustSign != 0) continue; if (definition.StepDeductions is { Count: > 0 }) continue; if (definition.Item.ItemType == ScoreItemTypeCode.RequiredElement) continue; @@ -229,8 +237,7 @@ public static class ScoreSheetCalculationService ApplyStepDeductions(definitions, result, config); ApplyRequiredElements(definitions, result, config); - // 花样赛的 D/P/Q/M 需要继续参与跨裁判乘法,不能在单张评分单阶段提前按最终展示精度取整。 - // 尤其失误第 2 档为 0.075,提前保留两位会改变 M。规定赛仍沿用原有两位小数口径。 + // 花样赛因子在跨裁判汇总时统一舍入;规定赛沿用原有精度。 if (!config.IsFactorTotal) { result.DifficultyScore = config.Round(result.DifficultyScore); @@ -239,7 +246,7 @@ public static class ScoreSheetCalculationService result.DeductionScore = config.Round(result.DeductionScore); } // 难度裁判的规定元素完成量仅作为独立记录,不能计入本裁判难度分。 - // 同时兼容尚未配置 factor 模式的个人花样规则;规定赛完成质量裁判仍保留 RequiredScore 加总。 + // 非因子模式保留规定赛的完成分汇总方式。 var isDifficultySheet = definitions.Any(x => !x.IsAuto && x.Item.ItemType != ScoreItemTypeCode.RequiredElement && (x.Item.ItemType == ScoreItemTypeCode.DifficultyCount || @@ -303,8 +310,8 @@ public static class ScoreSheetCalculationService } /// - /// 表现维度计算。个人花样的失误单独参与总成绩扣分,不进入表现分; - /// 其他花样项目沿用失误联动表现维度的既有口径。 + /// 表现维度计算。个人及同步花样的失误在每个维度扣一次,同时独立提交给综合扣分 M; + /// 其他花样项目沿用额外失误联动表现维度的既有口径。 /// 每个维度 = 基准分 + 人工加 - (人工减 + 失误自动减) - 失误次数,限制在上下限内, /// 再按各维度权重加权求和得到本裁判的表现分。失误自动减分作为自动项落明细,裁判不可直接修改。 /// @@ -316,13 +323,13 @@ public static class ScoreSheetCalculationService .ToList(); if (dimensions.Count == 0) return; - // 个人花样的失误由成绩汇总中的 M 系数单独处理,不能再降低本裁判的表现分。 + // 个人及同步花样的每个表现维度按一次失误扣减,失误数量另用于综合扣分。 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 isIndividualFreestyle = string.Equals(config.RuleType, "FREESTYLE_SINGLE", StringComparison.OrdinalIgnoreCase); - var performanceFailureCount = isIndividualFreestyle ? 0m : failureCount; + var isIndividualFamily = ScoreRuleConfigService.IsIndividualFamily(config.RuleType); + var performanceFailureCount = failureCount; var hasPerformanceMarks = definitions.Any(x => !x.IsAuto && x.AdjustSign != 0 && result.Lines.Any(line => string.Equals(line.ItemCode, x.ItemCode, StringComparison.OrdinalIgnoreCase) && line.CountValue is > 0m)); @@ -341,10 +348,10 @@ public static class ScoreSheetCalculationService .FirstOrDefault(x => string.Equals(x.ItemCode, adjust.ItemCode, StringComparison.OrdinalIgnoreCase))?.CountValue ?? 0m; raw += adjust.AdjustSign * count * (adjust.Item.UnitValue ?? 1m); } - // 其他花样项目沿用失误联动;个人花样这里不扣,汇总时由 M 系数处理。 - raw -= performanceFailureCount * 2m * (dimension.Item.UnitValue ?? 1m); + // 其他花样项目保留失误联动减号;个人及同步花样按一次失误扣减。 + raw -= performanceFailureCount * (isIndividualFamily ? 1m : 2m) * (dimension.Item.UnitValue ?? 1m); // 维度分限制在规则给出的上下限内(规程为 0~24)并取整。 - var value = isIndividualFreestyle && !hasPerformanceMarks + var value = !hasPerformanceMarks && performanceFailureCount == 0m ? 0m : Math.Clamp(Math.Round(raw, 0, MidpointRounding.AwayFromZero), dimension.MinValue, dimension.MaxValue); weighted += value * dimension.DimensionWeight!.Value; @@ -361,10 +368,10 @@ public static class ScoreSheetCalculationService result.Steps.Add(new ScoreSheetStep { Code = $"DIM_{dimension.DimensionCode}", Label = dimension.Item.ItemName, - Formula = isIndividualFreestyle && !hasPerformanceMarks - ? "尚未录入表现加减,表现分为 0;失误单独计入扣分" - : isIndividualFreestyle - ? $"{Trim(dimension.BaseValue ?? 0m)} + 人工加 - 人工减(失误单独计入扣分)" + Formula = !hasPerformanceMarks && performanceFailureCount == 0m + ? "尚未录入表现加减或失误,表现分为 0" + : isIndividualFamily + ? $"{Trim(dimension.BaseValue ?? 0m)} + 人工加 - 人工减 - 失误{Trim(performanceFailureCount)}次" : $"{Trim(dimension.BaseValue ?? 0m)} + 人工加 - (人工减 + 失误联动减{Trim(performanceFailureCount)}) - 失误{Trim(performanceFailureCount)}次", Value = value }); @@ -516,6 +523,8 @@ public static class ScoreSheetCalculationService } case ScoreCalcMode.CountTimesUnit: { + if (requireAll && item.IsRequired && input?.CountValue == null) + throw Oops.Bah($"请先录入“{item.ItemName}”的数量(无完成动作请填0)"); var count = input?.CountValue ?? (requireAll ? item.DefaultValue ?? 0m : (decimal?)null); if (!count.HasValue) return null; if (count.Value < 0m) throw Oops.Bah($"“{item.ItemName}”的次数不能为负数"); diff --git a/YD_Event.Application/Workbench/Dtos/WorkbenchDtos.cs b/YD_Event.Application/Workbench/Dtos/WorkbenchDtos.cs index 540be7d..0918c73 100644 --- a/YD_Event.Application/Workbench/Dtos/WorkbenchDtos.cs +++ b/YD_Event.Application/Workbench/Dtos/WorkbenchDtos.cs @@ -291,6 +291,11 @@ public class WorkbenchEntryDetailDto /// public class WorkbenchEntryResultDto { + /// 已保存的花样得分系数 M;规定赛为空。 + public decimal? DeductionFactor { get; set; } + + /// 用于复核的计算快照(包括失误平均和因子舍入过程)。 + public string? CalculationJson { get; set; } /// /// 成绩ID /// diff --git a/YD_Event.Application/Workbench/WorkbenchController.cs b/YD_Event.Application/Workbench/WorkbenchController.cs index e40c5c6..f1af823 100644 --- a/YD_Event.Application/Workbench/WorkbenchController.cs +++ b/YD_Event.Application/Workbench/WorkbenchController.cs @@ -1,4 +1,4 @@ -using YD_Event.Application.Auth; +using YD_Event.Application.Auth; using YD_Event.Application.Scoring; using YD_Event.Application.Scoring.Dtos; using YD_Event.Application.Workbench.Dtos; @@ -338,6 +338,7 @@ public class WorkbenchController : IDynamicApiController r.PerformanceScore, r.RequiredScore, r.DeductionScore, + r.CalculationJson, r.TotalScore, r.Score, r.IsDisqualified, @@ -499,6 +500,8 @@ public class WorkbenchController : IDynamicApiController PerformanceScore = result.PerformanceScore, RequiredScore = result.RequiredScore, DeductionScore = result.DeductionScore, + DeductionFactor = ScoreDisplayService.DeductionFactor(result.CalculationJson), + CalculationJson = result.CalculationJson, TotalScore = result.TotalScore, ScoreText = result.Score, IsDisqualified = result.IsDisqualified, @@ -677,6 +680,8 @@ public class WorkbenchController : IDynamicApiController PerformanceScore = result.PerformanceScore, RequiredScore = result.RequiredScore, DeductionScore = result.DeductionScore, + DeductionFactor = ScoreDisplayService.DeductionFactor(result.CalculationJson), + CalculationJson = result.CalculationJson, TotalScore = result.TotalScore, Score = result.Score, IsDisqualified = result.IsDisqualified, @@ -784,6 +789,7 @@ public class WorkbenchController : IDynamicApiController r.PerformanceScore, r.RequiredScore, r.DeductionScore, + r.CalculationJson, r.TotalScore, r.Score, r.IsDisqualified, @@ -1179,6 +1185,8 @@ public class WorkbenchController : IDynamicApiController PerformanceScore = result.PerformanceScore, RequiredScore = result.RequiredScore, DeductionScore = result.DeductionScore, + DeductionFactor = ScoreDisplayService.DeductionFactor(result.CalculationJson), + CalculationJson = result.CalculationJson, TotalScore = result.TotalScore, ScoreText = result.Score, IsDisqualified = result.IsDisqualified, @@ -1744,7 +1752,9 @@ public class WorkbenchController : IDynamicApiController r.ProjectId, r.DifficultyScore, r.PerformanceScore, + r.RequiredScore, r.DeductionScore, + r.CalculationJson, r.TotalScore, r.Score, r.IsDisqualified, @@ -1785,19 +1795,21 @@ public class WorkbenchController : IDynamicApiController string.IsNullOrWhiteSpace(x.Team) ? x.Participant : x.Team, projectName, x.GroupName, - Num(x.DifficultyScore), - Num(x.PerformanceScore), - Num(x.DeductionScore), - x.TotalScore.HasValue ? Num(x.TotalScore.Value) : x.Score, + x.DifficultyScore.ToString("F2", System.Globalization.CultureInfo.InvariantCulture), + x.PerformanceScore.ToString("F2", System.Globalization.CultureInfo.InvariantCulture), + x.RequiredScore.ToString("F2", System.Globalization.CultureInfo.InvariantCulture), + x.DeductionScore.ToString("F3", System.Globalization.CultureInfo.InvariantCulture), + ScoreDisplayService.DeductionFactor(x.CalculationJson)?.ToString("F3", System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty, + x.TotalScore.HasValue ? x.TotalScore.Value.ToString("F2", System.Globalization.CultureInfo.InvariantCulture) : x.Score, x.IsDisqualified ? "取消资格" : GetResultStatusName(x.ResultStatus), x.ResultVersion, FmtTime(x.ReviewTime) }; }).ToList(); - var headers = new[] { "名次", "出场编号", "姓名/队名", "项目", "组别", "难度分", "表现分", "扣分", "总分", "状态", "成绩版本", "确认时间" }; + var headers = new[] { "名次", "出场编号", "姓名/队名", "项目", "组别", "难度分", "表现分", "规定分", "扣分合计", "得分系数M", "总分", "状态", "成绩版本", "确认时间" }; var bytes = await new WorkbenchListExcelExporter("成绩册", $"成绩册(导出时间:{DateTime.Now:yyyy-MM-dd HH:mm})", headers, rows, - new double[] { 8, 16, 16, 16, 12, 10, 10, 10, 10, 10, 10, 20 }).ExportAsync(); + new double[] { 8, 16, 16, 16, 12, 10, 10, 10, 10, 12, 10, 10, 10, 20 }).ExportAsync(); return ExcelFile(bytes, $"成绩册_{input.EventId}_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); } @@ -1865,6 +1877,9 @@ public class WorkbenchController : IDynamicApiController .GroupBy(d => d.ScoreSheetId) .ToDictionary(g => g.Key, g => g.ToList()); + var scoreRuleIds = sheets.Select(x => x.ScoreRuleId).Distinct().ToList(); + var factorRules = (await _scoreRuleRepository.GetListAsync(x => scoreRuleIds.Contains(x.Id))) + .ToDictionary(x => x.Id, x => ScoreRuleConfigService.Parse(x.RuleConfigJson).IsFactorTotal); var rows = new List(); foreach (var sheet in sheets) { @@ -1888,7 +1903,10 @@ public class WorkbenchController : IDynamicApiController Num(sheet.DifficultyScore), Num(sheet.PerformanceScore), Num(sheet.DeductionScore), - Num(sheet.TotalScore), + Num(sheet.RequiredCompletedCount), + Num(sheet.RequiredMissingCount), + ScoreDisplayService.JudgeValue(sheet, factorRules.GetValueOrDefault(sheet.ScoreRuleId)) + .ToString(factorRules.GetValueOrDefault(sheet.ScoreRuleId) && sheet.JudgeRoleType == "T" ? "F3" : "F2", System.Globalization.CultureInfo.InvariantCulture), sheet.IsDisqualified ? "DQ" : string.Empty, sheet.SheetVersion, sheet.RuleVersion, @@ -1897,9 +1915,9 @@ public class WorkbenchController : IDynamicApiController }); } - var headers = new[] { "场次编号", "出场编号", "姓名/队名", "裁判", "裁判角色", "评分项明细", "难度分", "表现分", "扣分", "合计", "DQ", "评分单版本", "规则版本", "原评分单", "提交时间" }; + var headers = new[] { "场次编号", "出场编号", "姓名/队名", "裁判", "裁判角色", "评分项明细", "难度分", "表现分", "扣分合计", "规定完成量", "规定缺失量", "分数/本裁判系数", "DQ", "评分单版本", "规则版本", "原评分单", "提交时间" }; var bytes = await new WorkbenchListExcelExporter("评分单", $"评分单明细(导出时间:{DateTime.Now:yyyy-MM-dd HH:mm})", headers, rows, - new double[] { 20, 16, 16, 12, 14, 42, 10, 10, 10, 10, 8, 10, 10, 18, 20 }).ExportAsync(); + new double[] { 20, 16, 16, 12, 14, 42, 10, 10, 10, 12, 12, 18, 8, 10, 10, 18, 20 }).ExportAsync(); return ExcelFile(bytes, $"评分单_{input.EventId}_{DateTime.Now:yyyyMMddHHmmss}.xlsx"); } diff --git a/YD_Event.Core/InitData/scorerule.txt b/YD_Event.Core/InitData/scorerule.txt index 488365d..2d97ba7 100644 --- a/YD_Event.Core/InitData/scorerule.txt +++ b/YD_Event.Core/InitData/scorerule.txt @@ -1,4 +1,4 @@ -[ +[ { "Id": 3000, "RuleCode": "ROPE_PRESCRIBED", @@ -17,7 +17,7 @@ "RuleCode": "ROPE_FREESTYLE_WHEEL", "RuleName": "车轮跳花样计分规则", "JudgeRoleSchemaJson": "[{\"title\":\"主裁判\",\"slots\":[{\"code\":\"T\",\"name\":\"主裁判-T\",\"min\":1,\"max\":1}]},{\"title\":\"难度裁判\",\"slots\":[{\"code\":\"DA\",\"name\":\"难度裁判-DA(运动员A难度裁判)\",\"min\":1,\"max\":2},{\"code\":\"DB\",\"name\":\"难度裁判-DB(运动员B难度裁判)\",\"min\":1,\"max\":2}]},{\"title\":\"表现裁判\",\"slots\":[{\"code\":\"P\",\"name\":\"表现裁判-P\",\"min\":1,\"max\":2}]}]", - "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\"]}", + "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.025},{\"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,