feat: 增加裁判任务分页与下一场查询接口
批量查询并稳定排序裁判任务,返回分页及各页场次范围; 增加按出场 ID 定位任务和查询当前任务之后下一条未评分任务的接口,优化场次摘要计数。
This commit is contained in:
parent
2e00719d56
commit
f12afe8a36
@ -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];
|
||||
|
||||
@ -1,5 +1,32 @@
|
||||
namespace YD_Event.Application.Scoring.Dtos;
|
||||
|
||||
/// <summary>裁判任务分页结果;场次范围使用秩序册中的实际场次号。</summary>
|
||||
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<JudgeTaskPageRangeDto> PageRanges { get; set; } = new();
|
||||
public List<JudgeTaskDto> Items { get; set; } = new();
|
||||
}
|
||||
|
||||
public class JudgeTaskPageRangeDto
|
||||
{
|
||||
public int Page { get; set; }
|
||||
public int FirstSessionOrder { get; set; }
|
||||
public int LastSessionOrder { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>仅含当前裁判可访问的出场信息,不含平台最终成绩。</summary>
|
||||
public class JudgeTaskDto
|
||||
{
|
||||
public int Page { get; set; }
|
||||
public JudgeSessionDto Session { get; set; } = new();
|
||||
public JudgeEntryDto Entry { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 裁判端查询的当前场次赛事状态与操作权限。
|
||||
/// </summary>
|
||||
|
||||
@ -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<JudgeSessionDto>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>分页读取当前裁判最近待执裁赛事的任务。</summary>
|
||||
[HttpGet("/api/judge-scoring/tasks")]
|
||||
[DisplayName("分页获取我的评分任务")]
|
||||
public async Task<JudgeTaskPageDto> 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()
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>按出场ID定位本人任务,用于修正通知跳转。</summary>
|
||||
[HttpGet("/api/judge-scoring/tasks/{sessionEntryId:long}")]
|
||||
[DisplayName("按出场ID获取我的评分任务")]
|
||||
public async Task<JudgeTaskDto> 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];
|
||||
}
|
||||
|
||||
/// <summary>只沿编排顺序向后查找下一条正常待评分任务。</summary>
|
||||
[HttpGet("/api/judge-scoring/tasks/next")]
|
||||
[DisplayName("获取下一条待评分任务")]
|
||||
public async Task<JudgeTaskDto?> 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");
|
||||
}
|
||||
|
||||
/// <summary>一次批量读取任务所需数据,避免对每个场次请求一次出场名单。</summary>
|
||||
private async Task<(long EventId, List<JudgeTaskDto> Tasks)> LoadJudgeTasksAsync(long? eventId = null)
|
||||
{
|
||||
var referee = await EnsureRefereeAsync();
|
||||
var summaries = await GetSessions(eventId);
|
||||
if (summaries.Count == 0) return (0, new List<JudgeTaskDto>());
|
||||
|
||||
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<JudgeTaskDto>());
|
||||
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<long> { 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<JudgeTaskDto>(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());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询当前场次的赛事阶段与操作权限,供裁判端进入场次或准备评分时刷新界面状态。
|
||||
/// </summary>
|
||||
@ -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.###}");
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user