From f12afe8a3670f6e379ce064c483c57af4bbe2762 Mon Sep 17 00:00:00 2001
From: ltx <1006158920@qq.com>
Date: Wed, 28 Oct 2026 14:48:53 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=A3=81=E5=88=A4?=
=?UTF-8?q?=E4=BB=BB=E5=8A=A1=E5=88=86=E9=A1=B5=E4=B8=8E=E4=B8=8B=E4=B8=80?=
=?UTF-8?q?=E5=9C=BA=E6=9F=A5=E8=AF=A2=E6=8E=A5=E5=8F=A3=20=E6=89=B9?=
=?UTF-8?q?=E9=87=8F=E6=9F=A5=E8=AF=A2=E5=B9=B6=E7=A8=B3=E5=AE=9A=E6=8E=92?=
=?UTF-8?q?=E5=BA=8F=E8=A3=81=E5=88=A4=E4=BB=BB=E5=8A=A1=EF=BC=8C=E8=BF=94?=
=?UTF-8?q?=E5=9B=9E=E5=88=86=E9=A1=B5=E5=8F=8A=E5=90=84=E9=A1=B5=E5=9C=BA?=
=?UTF-8?q?=E6=AC=A1=E8=8C=83=E5=9B=B4=EF=BC=9B=20=E5=A2=9E=E5=8A=A0?=
=?UTF-8?q?=E6=8C=89=E5=87=BA=E5=9C=BA=20ID=20=E5=AE=9A=E4=BD=8D=E4=BB=BB?=
=?UTF-8?q?=E5=8A=A1=E5=92=8C=E6=9F=A5=E8=AF=A2=E5=BD=93=E5=89=8D=E4=BB=BB?=
=?UTF-8?q?=E5=8A=A1=E4=B9=8B=E5=90=8E=E4=B8=8B=E4=B8=80=E6=9D=A1=E6=9C=AA?=
=?UTF-8?q?=E8=AF=84=E5=88=86=E4=BB=BB=E5=8A=A1=E7=9A=84=E6=8E=A5=E5=8F=A3?=
=?UTF-8?q?=EF=BC=8C=E4=BC=98=E5=8C=96=E5=9C=BA=E6=AC=A1=E6=91=98=E8=A6=81?=
=?UTF-8?q?=E8=AE=A1=E6=95=B0=E3=80=82?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Client/CheckinScheduleService.cs | 10 +-
.../Judge/Dtos/JudgeScoringDtos.cs | 27 ++
.../Judge/JudgeScoringController.cs | 231 +++++++++++++++++-
3 files changed, 252 insertions(+), 16 deletions(-)
diff --git a/YD_Event.Application/Client/CheckinScheduleService.cs b/YD_Event.Application/Client/CheckinScheduleService.cs
index 82134e4..97a59ae 100644
--- a/YD_Event.Application/Client/CheckinScheduleService.cs
+++ b/YD_Event.Application/Client/CheckinScheduleService.cs
@@ -173,17 +173,13 @@ public class CheckinScheduleService : ITransient
var sessionOrderMap = sessions.ToDictionary(
x => x.Id,
x => GetImportedSessionOrder(x.SessionName, GetSessionOrder(x.SessionNo)));
- var orderedSessions = sessions
- .OrderBy(x => sessionOrderMap[x.Id])
- .ThenBy(x => venueMap.GetValueOrDefault(x.VenueId)?.Sort ?? int.MaxValue)
- .ThenBy(x => x.SessionNo)
- .ToList();
var sessionMap = sessions.ToDictionary(x => x.Id);
+ // 与成绩管理列表保持一致:导入记录的 ID 按秩序册预览行顺序生成。
+ // SessionName 中的“第 N 场”仅用于展示场次,不能用它重新排列选手。
var units = entries
.Where(x => sessionMap.ContainsKey(x.SessionId))
- .OrderBy(x => sessionOrderMap[x.SessionId])
- .ThenBy(x => x.PlayOrder)
+ .OrderBy(x => x.Id)
.Select((entry, index) =>
{
var session = sessionMap[entry.SessionId];
diff --git a/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs b/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs
index 82e2531..3469247 100644
--- a/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs
+++ b/YD_Event.Application/Judge/Dtos/JudgeScoringDtos.cs
@@ -1,5 +1,32 @@
namespace YD_Event.Application.Scoring.Dtos;
+/// 裁判任务分页结果;场次范围使用秩序册中的实际场次号。
+public class JudgeTaskPageDto
+{
+ public long EventId { get; set; }
+ public int Page { get; set; }
+ public int PageSize { get; set; }
+ public int TotalCount { get; set; }
+ public int TotalPages { get; set; }
+ public List PageRanges { get; set; } = new();
+ public List Items { get; set; } = new();
+}
+
+public class JudgeTaskPageRangeDto
+{
+ public int Page { get; set; }
+ public int FirstSessionOrder { get; set; }
+ public int LastSessionOrder { get; set; }
+}
+
+/// 仅含当前裁判可访问的出场信息,不含平台最终成绩。
+public class JudgeTaskDto
+{
+ public int Page { get; set; }
+ public JudgeSessionDto Session { get; set; } = new();
+ public JudgeEntryDto Entry { get; set; } = new();
+}
+
///
/// 裁判端查询的当前场次赛事状态与操作权限。
///
diff --git a/YD_Event.Application/Judge/JudgeScoringController.cs b/YD_Event.Application/Judge/JudgeScoringController.cs
index 1c9f3da..83a5d7e 100644
--- a/YD_Event.Application/Judge/JudgeScoringController.cs
+++ b/YD_Event.Application/Judge/JudgeScoringController.cs
@@ -187,12 +187,17 @@ public class JudgeScoringController : IDynamicApiController
.Where(x => sessionIds.Contains(x.SessionId) && x.RefereeId == referee.Id
&& x.IsCurrent && !x.DeleteMark
&& x.SheetStatus >= EventScoreCalculationService.SheetStatusSubmitted).ToListAsync();
+ var entriesBySession = entries.GroupBy(x => x.SessionId)
+ .ToDictionary(x => x.Key, x => x.ToList());
+ var scoredBySession = mySheets.GroupBy(x => x.SessionId)
+ .ToDictionary(x => x.Key, x => x.Count());
+ var assignmentsBySession = assignments.GroupBy(x => x.SessionId)
+ .ToDictionary(x => x.Key, x => x.First());
var rows = new List();
foreach (var session in sessions)
{
- var assignment = assignments.FirstOrDefault(x => x.SessionId == session.Id);
- if (assignment == null) continue;
+ if (!assignmentsBySession.TryGetValue(session.Id, out var assignment)) continue;
var roleType = JudgeRoleSchemaService.NormalizeRoleType(
string.IsNullOrWhiteSpace(assignment.JudgeRoleType) ? assignment.JudgeRole : assignment.JudgeRoleType);
var project = eventProjects.TryGetValue(session.EventProjectId, out var ep)
@@ -200,6 +205,7 @@ public class JudgeScoringController : IDynamicApiController
var rule = rules.GetValueOrDefault(session.ScoreRuleId);
var slot = JudgeRoleSchemaService.ParseSlots(rule?.JudgeRoleSchemaJson)
.FirstOrDefault(x => string.Equals(x.Code, roleType, StringComparison.OrdinalIgnoreCase));
+ entriesBySession.TryGetValue(session.Id, out var sessionEntries);
rows.Add(new JudgeSessionDto
{
SessionId = session.Id,
@@ -209,9 +215,8 @@ public class JudgeScoringController : IDynamicApiController
RuleName = rule?.RuleName ?? string.Empty,
SessionName = session.SessionName ?? session.SessionNo,
VenueName = venues.GetValueOrDefault(session.VenueId)?.VenueName ?? "-",
- GroupName = entries.Where(x => x.SessionId == session.Id).Select(x => x.GroupName)
- .Distinct().Count() == 1
- ? entries.First(x => x.SessionId == session.Id).GroupName
+ GroupName = sessionEntries?.Select(x => x.GroupName).Distinct().Count() == 1
+ ? sessionEntries[0].GroupName
: null,
StartTime = session.StartTime,
SessionStatus = session.SessionStatus,
@@ -219,8 +224,8 @@ public class JudgeScoringController : IDynamicApiController
JudgeRole = assignment.JudgeRole,
JudgeRoleType = roleType,
JudgeRoleName = slot?.Name ?? JudgeRoleSchemaService.RoleName(roleType),
- EntryCount = entries.Count(x => x.SessionId == session.Id),
- ScoredCount = mySheets.Count(x => x.SessionId == session.Id),
+ EntryCount = sessionEntries?.Count ?? 0,
+ ScoredCount = scoredBySession.GetValueOrDefault(session.Id),
ArrangementOrder = GetArrangementSequence(session.SessionNo) is var order
&& order < int.MaxValue
? order
@@ -230,6 +235,212 @@ public class JudgeScoringController : IDynamicApiController
return rows;
}
+ /// 分页读取当前裁判最近待执裁赛事的任务。
+ [HttpGet("/api/judge-scoring/tasks")]
+ [DisplayName("分页获取我的评分任务")]
+ public async Task GetTaskPage([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
+ {
+ ValidateTaskPageSize(pageSize);
+ if (page < 1) throw Oops.Bah("页码必须大于0");
+ var (eventId, tasks) = await LoadJudgeTasksAsync();
+ var totalPages = (tasks.Count + pageSize - 1) / pageSize;
+ var actualPage = totalPages == 0 ? 1 : Math.Min(page, totalPages);
+ for (var index = 0; index < tasks.Count; index++)
+ tasks[index].Page = index / pageSize + 1;
+
+ return new JudgeTaskPageDto
+ {
+ EventId = eventId,
+ Page = actualPage,
+ PageSize = pageSize,
+ TotalCount = tasks.Count,
+ TotalPages = totalPages,
+ PageRanges = tasks.Chunk(pageSize).Select((chunk, index) => new JudgeTaskPageRangeDto
+ {
+ Page = index + 1,
+ FirstSessionOrder = chunk[0].Entry.SessionOrder,
+ LastSessionOrder = chunk[^1].Entry.SessionOrder
+ }).ToList(),
+ Items = tasks.Skip((actualPage - 1) * pageSize).Take(pageSize).ToList()
+ };
+ }
+
+ /// 按出场ID定位本人任务,用于修正通知跳转。
+ [HttpGet("/api/judge-scoring/tasks/{sessionEntryId:long}")]
+ [DisplayName("按出场ID获取我的评分任务")]
+ public async Task GetTaskByEntry(long sessionEntryId, [FromQuery] int pageSize = 10)
+ {
+ ValidateTaskPageSize(pageSize);
+ var entry = await _entryRepository.GetFirstAsync(x => x.Id == sessionEntryId && !x.DeleteMark);
+ if (entry == null) throw Oops.Bah("评分任务不存在或不属于当前裁判");
+ var session = await _sessionRepository.GetFirstAsync(x => x.Id == entry.SessionId && !x.DeleteMark);
+ if (session == null) throw Oops.Bah("评分任务不存在或不属于当前裁判");
+ var (_, tasks) = await LoadJudgeTasksAsync(session.EventId);
+ var index = tasks.FindIndex(x => x.Entry.SessionEntryId == sessionEntryId);
+ if (index < 0) throw Oops.Bah("评分任务不存在或不属于当前裁判");
+ tasks[index].Page = index / pageSize + 1;
+ return tasks[index];
+ }
+
+ /// 只沿编排顺序向后查找下一条正常待评分任务。
+ [HttpGet("/api/judge-scoring/tasks/next")]
+ [DisplayName("获取下一条待评分任务")]
+ public async Task GetNextTask([FromQuery] long afterEntryId, [FromQuery] int pageSize = 10)
+ {
+ ValidateTaskPageSize(pageSize);
+ var (_, tasks) = await LoadJudgeTasksAsync();
+ var index = tasks.FindIndex(x => x.Entry.SessionEntryId == afterEntryId);
+ if (index < 0)
+ {
+ // The completed event may no longer be the selected event. Verify
+ // ownership before advancing to the first task of the new event.
+ var referee = await EnsureRefereeAsync();
+ var previous = await _entryRepository.GetFirstAsync(x => x.Id == afterEntryId && !x.DeleteMark);
+ if (previous == null) throw Oops.Bah("当前评分任务不存在或不属于当前裁判");
+ await GetMyAssignmentAsync(previous.SessionId, referee.Id);
+ }
+ for (var next = index + 1; next < tasks.Count; next++)
+ {
+ if (tasks[next].Entry.ScoreStatus is 2 or 3) continue;
+ tasks[next].Page = next / pageSize + 1;
+ return tasks[next];
+ }
+ return null;
+ }
+
+ private static void ValidateTaskPageSize(int pageSize)
+ {
+ if (pageSize is not (10 or 20 or 50)) throw Oops.Bah("每页数量只支持10、20或50");
+ }
+
+ /// 一次批量读取任务所需数据,避免对每个场次请求一次出场名单。
+ private async Task<(long EventId, List Tasks)> LoadJudgeTasksAsync(long? eventId = null)
+ {
+ var referee = await EnsureRefereeAsync();
+ var summaries = await GetSessions(eventId);
+ if (summaries.Count == 0) return (0, new List());
+
+ var targetEventId = summaries[0].EventId;
+ var sessionIds = summaries.Select(x => x.SessionId).ToList();
+ var summaryBySession = summaries.ToDictionary(x => x.SessionId);
+ var sessionOrder = summaries.Select((value, index) => (value.SessionId, index))
+ .ToDictionary(x => x.SessionId, x => x.index);
+ var sessions = (await _sessionRepository.AsQueryable()
+ .Where(x => sessionIds.Contains(x.Id)).ToListAsync())
+ .ToDictionary(x => x.Id);
+ var assignments = (await _assignmentRepository.AsQueryable()
+ .Where(x => sessionIds.Contains(x.SessionId) && x.RefereeId == referee.Id &&
+ x.AssignmentStatus == 1 && x.Status == AvailabilityStatus.Enable && !x.DeleteMark)
+ .ToListAsync()).GroupBy(x => x.SessionId)
+ .ToDictionary(x => x.Key, x => x.First());
+ var allEntries = await _entryRepository.AsQueryable()
+ .Where(x => sessionIds.Contains(x.SessionId) && !x.DeleteMark).ToListAsync();
+ var entryById = allEntries.ToDictionary(x => x.Id);
+ var entries = allEntries.Where(x => !x.IsVoided && x.Status == AvailabilityStatus.Enable).ToList();
+ if (entries.Count == 0) return (targetEventId, new List());
+ var sheets = await _sheetRepository.AsQueryable()
+ .Where(x => sessionIds.Contains(x.SessionId) && x.RefereeId == referee.Id && !x.DeleteMark)
+ .ToListAsync();
+ var submittedByEntry = sheets.Where(x => x.IsCurrent &&
+ x.SheetStatus >= EventScoreCalculationService.SheetStatusSubmitted)
+ .GroupBy(x => x.SessionEntryId).ToDictionary(x => x.Key, x => x.First());
+ var draftByEntry = sheets.Where(x => x.SheetStatus == SheetStatusDraft)
+ .GroupBy(x => x.SessionEntryId).ToDictionary(x => x.Key, x => x.First());
+ var assignmentIds = assignments.Values.Select(x => x.Id).ToList();
+ var corrections = await _correctionRepository.AsQueryable()
+ .Where(x => sessionIds.Contains(x.SessionId) && assignmentIds.Contains(x.JudgeAssignmentId) &&
+ (x.CorrectionStatus == CorrectionStatusApproved || x.CorrectionStatus == CorrectionStatusEditing))
+ .ToListAsync();
+ var correctionSheetIds = corrections.Select(x => x.OriginalScoreSheetId).ToHashSet();
+
+ var eventProjects = await _eventProjectRepository.AsQueryable()
+ .Where(x => x.EventId == targetEventId && !x.DeleteMark).ToListAsync();
+ var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
+ var projects = (await _projectRepository.AsQueryable()
+ .Where(x => projectIds.Contains(x.Id) && !x.DeleteMark).ToListAsync())
+ .ToDictionary(x => x.Id);
+ var eventProjectById = eventProjects.ToDictionary(x => x.Id);
+ var orderedProjectIds = projects.Values.OrderBy(x => x.SortIndex).ThenBy(x => x.Id)
+ .Select(x => x.Id).ToList();
+ var projectOrderById = orderedProjectIds.Select((id, index) => (id, index))
+ .ToDictionary(x => x.id, x => x.index + 1);
+ var venueIds = sessions.Values.Select(x => x.VenueId).Distinct().ToList();
+ var venues = (await _venueRepository.AsQueryable()
+ .Where(x => venueIds.Contains(x.Id) && !x.DeleteMark).ToListAsync())
+ .ToDictionary(x => x.Id);
+ var ruleIds = sessions.Values.Select(x => x.ScoreRuleId).Distinct().ToList();
+ var rules = (await _ruleRepository.AsQueryable()
+ .Where(x => ruleIds.Contains(x.Id) && !x.DeleteMark).ToListAsync())
+ .ToDictionary(x => x.Id);
+ var factorByRuleId = rules.ToDictionary(x => x.Key,
+ x => ScoreRuleConfigService.Parse(x.Value.RuleConfigJson).IsFactorTotal);
+
+ int DisplayPlayOrder(EventSessionEntry entry)
+ {
+ var result = entry.PlayOrder;
+ var sourceId = entry.RematchFromEntryId;
+ var visited = new HashSet { entry.Id };
+ while (sourceId.HasValue && visited.Add(sourceId.Value) &&
+ entryById.TryGetValue(sourceId.Value, out var source))
+ {
+ result = source.PlayOrder;
+ sourceId = source.RematchFromEntryId;
+ }
+ return result;
+ }
+
+ var tasks = new List(entries.Count);
+ foreach (var entry in entries)
+ {
+ if (!sessions.TryGetValue(entry.SessionId, out var session) ||
+ !assignments.TryGetValue(entry.SessionId, out var assignment)) continue;
+ var summary = summaryBySession[entry.SessionId];
+ var arrangement = GetArrangementSequence(session.SessionNo);
+ var eventProject = eventProjectById.GetValueOrDefault(session.EventProjectId);
+ var project = eventProject == null ? null : projects.GetValueOrDefault(eventProject.ProjectId);
+ var projectOrder = eventProject == null
+ ? orderedProjectIds.Count + 1
+ : projectOrderById.GetValueOrDefault(eventProject.ProjectId, orderedProjectIds.Count + 1);
+ var venue = venues.GetValueOrDefault(session.VenueId);
+ var venueOrder = venue?.Sort > 0 ? venue.Sort : 1;
+ var scope = project == null ? ScoreScope.Personal : ScoreSheetCalculationService.ResolveScope(project.ProjectType);
+ var submitted = submittedByEntry.GetValueOrDefault(entry.Id);
+ var draft = draftByEntry.GetValueOrDefault(entry.Id);
+ var inCorrection = submitted != null && correctionSheetIds.Contains(submitted.Id);
+ var status = inCorrection ? 3 : submitted != null ? 2 : draft != null ? 1 : 0;
+ var isFactorTotal = factorByRuleId.GetValueOrDefault(session.ScoreRuleId);
+ tasks.Add(new JudgeTaskDto
+ {
+ Session = summary,
+ Entry = new JudgeEntryDto
+ {
+ SessionEntryId = entry.Id,
+ EntryNo = entry.EntryNo,
+ PlayOrder = entry.PlayOrder,
+ SessionOrder = entry.SourceType == 1
+ ? GetImportedSessionOrder(session.SessionName,
+ BuildEntrySessionOrder(arrangement, DisplayPlayOrder(entry)))
+ : BuildEntrySessionOrder(arrangement, DisplayPlayOrder(entry)),
+ ScheduleOrder = entry.SourceType == 1 ? entry.Id :
+ BuildArrangementScheduleOrder(projectOrder, arrangement, venueOrder, entry.PlayOrder),
+ ParticipantName = DisplayParticipantName(entry, scope),
+ GroupName = entry.GroupName,
+ EntryStatus = entry.EntryStatus,
+ IsRematch = entry.IsRematch,
+ ScoreStatus = status,
+ ScoreStatusName = ScoreStatusName(status),
+ MyScore = submitted == null ? null : ScoreDisplayService.JudgeText(submitted, isFactorTotal),
+ IsDisqualified = submitted?.IsDisqualified ?? false,
+ ScoreSheetId = submitted?.Id ?? draft?.Id ?? 0
+ }
+ });
+ }
+ return (targetEventId, tasks.OrderBy(x => x.Entry.ScheduleOrder)
+ .ThenBy(x => sessionOrder[x.Session.SessionId])
+ .ThenBy(x => x.Entry.PlayOrder)
+ .ThenBy(x => x.Entry.SessionEntryId).ToList());
+ }
+
///
/// 查询当前场次的赛事阶段与操作权限,供裁判端进入场次或准备评分时刷新界面状态。
///
@@ -1302,10 +1513,12 @@ public class JudgeScoringController : IDynamicApiController
if (uploaded < 0m || uploaded > 1m)
throw Oops.Bah("扣分系数必须在0至1之间");
- // 明细行保留了递进扣分的未舍入小计,用它校验 0.125 等三位小数系数。
- var expected = calculation.Lines
+ // 明细行保留递进扣分的未舍入小计;累计扣分可超过 1,
+ // 但上传的扣分系数与最终 M=Max(0, 1-扣分) 使用同一 0..1 口径。
+ var rawDeduction = calculation.Lines
.Where(x => x.ScoreDimension == ScoreDimensionCode.Deduction)
.Sum(x => x.Subtotal);
+ var expected = Math.Clamp(rawDeduction, 0m, 1m);
if (Math.Abs(uploaded.Value - expected) > 0.0005m)
throw Oops.Bah($"扣分系数与扣分明细不一致,应为{expected:0.###}");
}