diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..cd8074f
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,71 @@
+# .NET 开发规范
+
+你是一名资深 .NET 后端开发工程师,精通 C#、ASP.NET Core 和 Entity Framework Core。
+
+## 代码风格与结构
+- 编写简洁、符合惯例的 C# 代码,并提供准确示例。
+- 遵循 .NET 和 ASP.NET Core 的规范与最佳实践。
+- 根据需求使用面向对象和函数式编程模式。
+- 使用 LINQ 和 Lambda 表达式处理集合操作。
+- 使用描述性变量和方法名(如 `IsUserSignedIn`、`CalculateTotal`)。
+- 文件结构遵循 .NET 约定(Controllers、Models、Services 等)。
+
+## 命名规范
+- 类名、方法名及公共成员使用 PascalCase。
+- 局部变量和私有字段使用 camelCase。
+- 常量使用 UPPERCASE。
+- 接口名前缀使用 "I"(如 `IUserService`)。
+
+## C# 与 .NET 使用
+- 根据需要使用 C# 10+ 特性(如 record 类型、模式匹配、空合并赋值)。
+- 利用 ASP.NET Core 内置功能和中间件。
+- 使用 Entity Framework Core 高效进行数据库操作。
+
+## 语法与格式
+- 遵循 C# 编码规范:[C# Coding Conventions](https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/coding-style/coding-conventions)。
+- 使用 C# 表达性语法(如空条件运算符、字符串插值)。
+- 当类型明显时使用 `var` 进行隐式类型声明。
+
+## 错误处理与验证
+- 异常用于处理异常情况,而非控制流程。
+- 使用 .NET 内置日志或第三方日志库实现错误记录。
+- 使用 Data Annotations 或 Fluent Validation 进行模型验证。
+- 实现全局异常处理中间件。
+- 返回适当的 HTTP 状态码和一致的错误响应。
+
+## API 设计
+- 遵循 RESTful API 设计原则。
+- 控制器中使用特性路由(Attribute Routing)。
+- 对 API 实现版本控制。
+- 使用 Action Filters 处理横切关注点(Cross-Cutting Concerns)。
+
+## 性能优化
+- 对 I/O 密集型操作使用异步编程(async/await)。
+- 使用 IMemoryCache 或分布式缓存实现缓存策略。
+- 使用高效的 LINQ 查询,避免 N+1 查询问题。
+- 对大数据集实现分页(Pagination)。
+
+## 核心约定
+- 使用依赖注入(Dependency Injection)实现低耦合和可测试性。
+- 根据复杂度选择实现 Repository 模式或直接使用 Entity Framework Core。
+- 需要时使用 AutoMapper 进行对象映射。
+- 使用 IHostedService 或 BackgroundService 实现后台任务。
+
+## 安全
+- 使用认证和授权中间件。
+- 对无状态 API 使用 JWT 认证。
+- 强制 HTTPS 并启用 SSL。
+- 实施适当的 CORS 策略。
+
+## API 文档
+- 使用 Swagger/OpenAPI 生成 API 文档(通过 Swashbuckle.AspNetCore)。
+- 为控制器和模型提供 XML 注释以增强 Swagger 文档。
+
+## 重要执行限制(强制)
+- AI **禁止直接执行任何 SQL 语句、git 命令**。
+ - ✅ 允许:编写SQL代码、生成SQL文件、执行查询SQL、写出git命令文本,交由人工审核后手动执行。
+ - ❌ 禁止:模拟执行、直接运行除查询SQL外的其他SQL文件 / git 命令、输出类似“已执行sql、已提交git”这类执行结果。
+- 输出SQL时,优先生成可保存为 `.sql` 文件的完整脚本,并附带风险说明、执行前置检查项。
+- 输出Git命令时,只输出命令文本,附带操作说明与风险提示,不模拟提交、推送等执行动作。
+
+遵循 Microsoft 官方文档及 ASP.NET Core 指南,确保路由、控制器、模型及其他 API 组件符合最佳实践。
\ No newline at end of file
diff --git a/YD_Event.Application/Event/Scoring/EntryResultCalculator.cs b/YD_Event.Application/Event/Scoring/EntryResultCalculator.cs
index e7ccf4e..848675a 100644
--- a/YD_Event.Application/Event/Scoring/EntryResultCalculator.cs
+++ b/YD_Event.Application/Event/Scoring/EntryResultCalculator.cs
@@ -158,10 +158,16 @@ public class EntryAggregateResult
public decimal RequiredFactor { get; set; } = 1m;
///
- /// 花样赛的扣分因子 M = max(0, 1 - 主裁扣分合计)
+ /// 花样赛的扣分因子 M = max(0, 1 - 最终扣分合计)。
+ /// 含表现裁判失误项的规则会先把表现裁判与主裁的失误次数取平均,再与主裁其他扣分合并。
///
public decimal DeductionFactor { get; set; } = 1m;
+ ///
+ /// 花样赛表现裁判与主裁失误合并计算明细;当前规则不包含两类失误项时为空
+ ///
+ public EntryFailureAggregation? FailureAggregation { get; set; }
+
///
/// 取某个计分符号的分值
///
@@ -169,6 +175,57 @@ public class EntryAggregateResult
Symbols.FirstOrDefault(x => string.Equals(x.Symbol, symbol, StringComparison.OrdinalIgnoreCase))?.Value ?? 0m;
}
+///
+/// 花样赛失误次数跨裁判平均及扣分换算明细
+///
+public class EntryFailureAggregation
+{
+ ///
+ /// 各表现裁判记录的失误次数
+ ///
+ public List PerformanceFailureCounts { get; set; } = new();
+
+ ///
+ /// 主裁记录的失误次数
+ ///
+ public decimal ChiefFailureCount { get; set; }
+
+ ///
+ /// 参与平均的裁判人数(表现裁判人数 + 1 名主裁)
+ ///
+ public int JudgeCount { get; set; }
+
+ ///
+ /// 未取整的平均失误次数
+ ///
+ public decimal AverageFailureCount { get; set; }
+
+ ///
+ /// 四舍五入后用于阶梯扣分的失误次数
+ ///
+ public int RoundedFailureCount { get; set; }
+
+ ///
+ /// 主裁评分单中原失误扣分
+ ///
+ public decimal OriginalChiefFailureDeduction { get; set; }
+
+ ///
+ /// 平均失误次数换算出的新失误扣分
+ ///
+ public decimal FailureDeduction { get; set; }
+
+ ///
+ /// 主裁除失误之外的其他扣分
+ ///
+ public decimal OtherDeduction { get; set; }
+
+ ///
+ /// 最终扣分
+ ///
+ public decimal TotalDeduction { get; set; }
+}
+
///
/// 参与排名的一条成绩
///
diff --git a/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs b/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs
index e70c32f..2b67d6c 100644
--- a/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs
+++ b/YD_Event.Application/Event/Scoring/EventScoreCalculationService.cs
@@ -16,6 +16,7 @@ public class EventScoreCalculationService : ITransient
private readonly ISqlSugarRepository _ruleItemRepository;
private readonly ISqlSugarRepository _assignmentRepository;
private readonly ISqlSugarRepository _sheetRepository;
+ private readonly ISqlSugarRepository _detailRepository;
private readonly ISqlSugarRepository _refereeRepository;
private readonly ISqlSugarRepository _resultRepository;
private readonly ISqlSugarRepository _resultVersionRepository;
@@ -67,6 +68,7 @@ public class EventScoreCalculationService : ITransient
ISqlSugarRepository ruleItemRepository,
ISqlSugarRepository assignmentRepository,
ISqlSugarRepository sheetRepository,
+ ISqlSugarRepository detailRepository,
ISqlSugarRepository refereeRepository,
ISqlSugarRepository resultRepository,
ISqlSugarRepository resultVersionRepository,
@@ -83,6 +85,7 @@ public class EventScoreCalculationService : ITransient
_ruleItemRepository = ruleItemRepository;
_assignmentRepository = assignmentRepository;
_sheetRepository = sheetRepository;
+ _detailRepository = detailRepository;
_refereeRepository = refereeRepository;
_resultRepository = resultRepository;
_resultVersionRepository = resultVersionRepository;
@@ -181,15 +184,26 @@ public class EventScoreCalculationService : ITransient
: await _sheetRepository.GetListAsync(x => entryIds.Contains(x.SessionEntryId)
&& assignmentIds.Contains(x.JudgeAssignmentId)
&& x.IsCurrent && x.SheetStatus >= SheetStatusSubmitted);
+ var sheetIds = sheets.Select(x => x.Id).ToList();
+ var scoreDetails = sheetIds.Count == 0
+ ? new List()
+ : await _detailRepository.GetListAsync(x => sheetIds.Contains(x.ScoreSheetId) && !x.DeleteMark
+ && (x.ItemCode == "FAILURE_COUNT" || x.ItemCode == "T_FAILURE"));
var judgeNames = await LoadJudgeNamesAsync(assignments);
var sheetsByEntry = sheets.GroupBy(x => x.SessionEntryId).ToDictionary(x => x.Key, x => x.ToList());
+ var detailsBySheet = scoreDetails.GroupBy(x => x.ScoreSheetId).ToDictionary(x => x.Key, x => x.ToList());
var outcomes = new List();
var written = new List();
foreach (var entry in entries)
{
sheetsByEntry.TryGetValue(entry.Id, out var entrySheets);
- var outcome = Evaluate(entry, ctx, assignments, entrySheets ?? new List(), judgeNames);
+ entrySheets ??= new List();
+ var entryDetails = entrySheets
+ .Where(x => detailsBySheet.ContainsKey(x.Id))
+ .SelectMany(x => detailsBySheet[x.Id])
+ .ToList();
+ var outcome = Evaluate(entry, ctx, assignments, entrySheets, entryDetails, judgeNames);
outcomes.Add(outcome);
if (persist && outcome.IsCompleted && !outcome.IsSkipped) written.Add(entry);
}
@@ -217,6 +231,7 @@ public class EventScoreCalculationService : ITransient
///
private static EntryCalculationOutcome Evaluate(EventSessionEntry entry, ScoringContext ctx,
List assignments, List entrySheets,
+ List entryDetails,
IReadOnlyDictionary judgeNames)
{
var outcome = new EntryCalculationOutcome
@@ -262,7 +277,8 @@ public class EventScoreCalculationService : ITransient
if (entrySheets.Count > 0)
{
- outcome.Aggregate = EntryResultCalculationService.Aggregate(ctx.Config, entrySheets, judgeNames);
+ outcome.Aggregate = EntryResultCalculationService.Aggregate(ctx.Config, entrySheets, judgeNames,
+ entryDetails, ctx.RuleItems, ctx.Scope);
// 出场被判取消比赛资格时,成绩按取消资格处理。
if (entry.EntryStatus == 90)
{
diff --git a/YD_Event.Application/Judge/JudgeScoringController.cs b/YD_Event.Application/Judge/JudgeScoringController.cs
index b5bf8d1..0bd3be9 100644
--- a/YD_Event.Application/Judge/JudgeScoringController.cs
+++ b/YD_Event.Application/Judge/JudgeScoringController.cs
@@ -11,7 +11,8 @@ namespace YD_Event.Application.Scoring;
/// 规定赛计分(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 - 主裁扣分合计);
+/// 其中 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 - (失误 + 违例))。
diff --git a/YD_Event.Application/Scoring/EntryResultCalculationService.cs b/YD_Event.Application/Scoring/EntryResultCalculationService.cs
index 3d31f14..ee0674a 100644
--- a/YD_Event.Application/Scoring/EntryResultCalculationService.cs
+++ b/YD_Event.Application/Scoring/EntryResultCalculationService.cs
@@ -7,16 +7,25 @@ namespace YD_Event.Application.Scoring;
/// 规定赛:T1=3 名创意编排裁判创意分的平均值,T2=3 名完成质量裁判完成分的平均值,
/// T3=主裁违例扣分(每项扣 5 分)合计,总分=T1+T2-T3,均四舍五入保留两位小数。
/// 花样赛:各难度裁判类型先聚合再按难度除数合并为 D,表现裁判平均分乘系数得 P(个人/车轮 0.05、交互绳 0.033),
-/// 规定元素缺失数换算为 Q,主裁扣分换算为 M = max(0, 1 - 扣分),总分 = D × (1 + P) × Q × M(交互绳不计 Q)。
+/// 规定元素缺失数换算为 Q;扣分中的失误由表现裁判与主裁按人数平均后换算,再加主裁其他扣分,
+/// 得到 M = max(0, 1 - 扣分),总分 = D × (1 + P) × Q × M(交互绳不计 Q)。
/// 排名:总分高者在前,总分相同按规则配置的比较字段依次比较,全部相同则并列,其后名次按 N+M 规则顺延。
///
public static class EntryResultCalculationService
{
+ private const string RolePerformance = "P";
+ private const string RoleChief = "T";
+ private const string PerformanceFailureItemCode = "FAILURE_COUNT";
+ private const string ChiefFailureItemCode = "T_FAILURE";
+
///
/// 把某个出场的各裁判评分单聚合成一条成绩。judgeNames 用于在计算明细里带上裁判姓名,可为空。
///
public static EntryAggregateResult Aggregate(ScoreRuleConfig config, List sheets,
- IReadOnlyDictionary? judgeNames = null)
+ IReadOnlyDictionary? judgeNames = null,
+ IReadOnlyCollection? scoreDetails = null,
+ IReadOnlyCollection? ruleItems = null,
+ string scope = ScoreScope.Personal)
{
var result = new EntryAggregateResult { Formula = BuildFormula(config) };
var total = 0m;
@@ -49,6 +58,11 @@ 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);
// 任一裁判判定取消资格即取消该出场成绩,取消资格后不计总分、不参与排名。
@@ -62,6 +76,93 @@ public static class EntryResultCalculationService
return result;
}
+ ///
+ /// 合并表现裁判与主裁记录的失误次数,并替换扣分因子中的失误部分。
+ /// 以评分项是否同时存在 P/FAILURE_COUNT 和 T/T_FAILURE 作为启用条件,规定赛等不含这些评分项的规则不受影响。
+ ///
+ private static void ApplyCombinedFailureDeduction(ScoreRuleConfig config, EntryAggregateResult result,
+ IReadOnlyCollection sheets,
+ IReadOnlyCollection? scoreDetails,
+ IReadOnlyCollection? ruleItems,
+ string scope)
+ {
+ if (!config.IsFactorTotal || scoreDetails == null || ruleItems == null) return;
+
+ var performanceFailureItem = ruleItems.FirstOrDefault(x =>
+ string.Equals(x.JudgeRoleType, RolePerformance, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(x.ItemCode, PerformanceFailureItemCode, StringComparison.OrdinalIgnoreCase));
+ var chiefFailureItem = ruleItems.FirstOrDefault(x =>
+ string.Equals(x.JudgeRoleType, RoleChief, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(x.ItemCode, ChiefFailureItemCode, StringComparison.OrdinalIgnoreCase));
+ if (performanceFailureItem == null || chiefFailureItem == null) return;
+
+ var deductionRole = config.RoleAggregations.FirstOrDefault(x =>
+ string.Equals(x.Factor, ScoreFactorCode.Deduction, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(x.RoleType, RoleChief, StringComparison.OrdinalIgnoreCase));
+ if (deductionRole == null) return;
+ var deductionSymbol = result.Symbols.FirstOrDefault(x =>
+ string.Equals(x.Symbol, deductionRole.Symbol, StringComparison.OrdinalIgnoreCase));
+ if (deductionSymbol == null) return;
+
+ var performanceSheets = sheets
+ .Where(x => string.Equals(JudgeRoleSchemaService.NormalizeRoleType(x.JudgeRoleType), RolePerformance,
+ StringComparison.OrdinalIgnoreCase))
+ .OrderBy(x => x.JudgeRole).ThenBy(x => x.Id)
+ .ToList();
+ var chiefSheets = sheets
+ .Where(x => string.Equals(JudgeRoleSchemaService.NormalizeRoleType(x.JudgeRoleType), RoleChief,
+ 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)
+ .ToDictionary(x => x.Key, x => x.ToList());
+ decimal FailureCount(long sheetId, string itemCode) => detailsBySheet.TryGetValue(sheetId, out var details)
+ ? details.Where(x => string.Equals(x.ItemCode, itemCode, StringComparison.OrdinalIgnoreCase))
+ .Sum(x => x.CountValue ?? x.InputValue ?? 0m)
+ : 0m;
+
+ var performanceCounts = performanceSheets
+ .Select(x => FailureCount(x.Id, PerformanceFailureItemCode))
+ .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 chiefDefinition = ScoreSheetCalculationService.CreateDefinition(chiefFailureItem, scope);
+ var steps = chiefDefinition.StepDeductions;
+ if (steps == null || steps.Count == 0) return;
+ var failureDeduction = 0m;
+ for (var i = 0; i < roundedCount; i++)
+ failureDeduction += steps[Math.Min(i, steps.Count - 1)];
+
+ var originalChiefFailureDeduction = detailsBySheet.TryGetValue(chiefSheet.Id, out var chiefDetails)
+ ? chiefDetails
+ .Where(x => string.Equals(x.ItemCode, ChiefFailureItemCode, StringComparison.OrdinalIgnoreCase))
+ .Sum(x => x.Subtotal)
+ : 0m;
+ var otherDeduction = Math.Max(0m, deductionSymbol.Value - originalChiefFailureDeduction);
+ var totalDeduction = config.Round(otherDeduction + failureDeduction);
+ deductionSymbol.Value = totalDeduction;
+ result.DeductionScore = totalDeduction;
+ result.FailureAggregation = new EntryFailureAggregation
+ {
+ PerformanceFailureCounts = performanceCounts,
+ ChiefFailureCount = chiefCount,
+ JudgeCount = judgeCount,
+ AverageFailureCount = averageCount,
+ RoundedFailureCount = roundedCount,
+ OriginalChiefFailureDeduction = config.Round(originalChiefFailureDeduction),
+ FailureDeduction = config.Round(failureDeduction),
+ OtherDeduction = config.Round(otherDeduction),
+ TotalDeduction = totalDeduction
+ };
+ }
+
///
/// 花样赛按乘法公式算总分:
/// D 由各难度裁判类型的聚合值加权后除以难度除数(未配置除数时按难度裁判类型数量取平均);
@@ -113,7 +214,7 @@ public static class EntryResultCalculationService
result.RequiredFactor = config.Round(required);
result.DeductionFactor = config.Round(deduction);
// 排名的并列比较取的是成绩表上的四个分值字段,这里统一回写为花样赛口径:
- // 难度分=D,表现分=P,规定分=Q,扣分=主裁扣分合计(越小越靠前)。
+ // 难度分=D,表现分=P,规定分=Q,扣分=最终扣分合计(越小越靠前)。
result.DifficultyScore = difficulty;
result.PerformanceScore = performance;
result.RequiredScore = result.RequiredFactor;
@@ -214,6 +315,20 @@ public static class EntryResultCalculationService
q = result.RequiredFactor, m = result.DeductionFactor
}
: null,
+ failureAggregation = result.FailureAggregation == null
+ ? null
+ : new
+ {
+ performanceFailureCounts = result.FailureAggregation.PerformanceFailureCounts,
+ chiefFailureCount = result.FailureAggregation.ChiefFailureCount,
+ judgeCount = result.FailureAggregation.JudgeCount,
+ averageFailureCount = result.FailureAggregation.AverageFailureCount,
+ roundedFailureCount = result.FailureAggregation.RoundedFailureCount,
+ originalChiefFailureDeduction = result.FailureAggregation.OriginalChiefFailureDeduction,
+ failureDeduction = result.FailureAggregation.FailureDeduction,
+ otherDeduction = result.FailureAggregation.OtherDeduction,
+ totalDeduction = result.FailureAggregation.TotalDeduction
+ },
symbols = result.Symbols.Select(x => new
{
x.Symbol, x.Label, x.RoleType, x.Aggregate, x.Sign, x.Factor, x.Value, x.JudgeCount,