YD_Event.Admin/YD_Event.Application/Workbench/WorkbenchController.PagedEntries.cs
ltx e54112e08d fix: 修复赛事编排并完善成绩与检录大屏数据"
-m "修复赛事编排确认时 EventSessionEntry 写入无效 RegistrationItemId 导致的外键冲突。"
  -m "完善成绩展示数据,补充参赛单位名称,支持成绩管理矩阵及大屏展示。"
  -m "调整检录大屏进度规则:赛事检录顺序、当前检录项目及队伍状态统一跟随一检进度。"
  -m "当前比赛默认根据三检通过记录更新;赛事仅配置两检时,允许二检作为上场标记。"
  -m "一检全部完成后自动切换下一项目,全部完成后不再错误展示全部参赛队伍。
2026-10-30 10:42:40 +08:00

211 lines
11 KiB
C#

using YD_Event.Application.Scoring;
using YD_Event.Application.Workbench.Dtos;
using YD_Event.Core.Entities;
namespace YD_Event.Application.Workbench;
public partial class WorkbenchController
{
/// <summary>
/// 按项目分页获取成绩概要。先按秩序册顺序确定页边界,再仅为当前页查询成绩与裁判提交状态;
/// 评分项和计分板不随五秒轮询返回。
/// </summary>
[HttpGet("projects/{eventProjectId}/entries")]
public async Task<WorkbenchPagedEntriesOutput> GetProjectEntries(
long eventProjectId, [FromQuery] WorkbenchProjectEntriesInput input)
{
if (input.EventId <= 0 || eventProjectId <= 0) throw Oops.Bah("请选择赛事和项目");
var pageSize = input.PageSize is 20 or 50 or 100 ? input.PageSize : 20;
var sessions = await _sessionRepository.AsQueryable()
.Where(s => !s.DeleteMark && s.EventId == input.EventId && s.EventProjectId == eventProjectId)
.ToListAsync();
var output = new WorkbenchPagedEntriesOutput { PageSize = pageSize };
if (sessions.Count == 0) return output;
var sessionIds = sessions.Select(s => s.Id).ToList();
var query = _entryRepository.AsQueryable()
.Where(e => !e.DeleteMark && sessionIds.Contains(e.SessionId));
if (!string.IsNullOrWhiteSpace(input.Keyword))
{
var keyword = input.Keyword.Trim();
query = query.Where(e => e.EntryNo.Contains(keyword) || e.ParticipantName.Contains(keyword) ||
(e.TeamName != null && e.TeamName.Contains(keyword)));
}
var entries = await query.Select(e => new EventSessionEntry
{
Id = e.Id,
SessionId = e.SessionId,
EntryNo = e.EntryNo,
PlayOrder = e.PlayOrder,
SourceType = e.SourceType,
ParticipantName = e.ParticipantName,
TeamName = e.TeamName,
GroupName = e.GroupName,
EntryStatus = e.EntryStatus,
IsRematch = e.IsRematch
}).ToListAsync();
if (input.ResultStatus.HasValue && entries.Count > 0)
{
var allEntryIds = entries.Select(e => e.Id).ToList();
var statuses = await _resultRepository.AsQueryable()
.Where(r => !r.DeleteMark && r.EventId == input.EventId && r.SessionEntryId.HasValue && allEntryIds.Contains(r.SessionEntryId.Value))
.Select(r => new { r.SessionEntryId, r.ResultStatus, r.ResultVersion, r.Id })
.ToListAsync();
var currentStatuses = statuses.GroupBy(r => r.SessionEntryId!.Value)
.ToDictionary(g => g.Key, g => g.OrderByDescending(r => r.ResultVersion)
.ThenByDescending(r => r.Id).First().ResultStatus);
entries = entries.Where(e => currentStatuses.GetValueOrDefault(e.Id, -1) == input.ResultStatus.Value).ToList();
}
var sessionById = sessions.ToDictionary(s => s.Id);
var arrangementOrder = sessions.ToDictionary(s => s.Id, s => GetArrangementSequence(s.SessionNo));
var venueIds = sessions.Select(s => s.VenueId).Distinct().ToList();
var venueOrder = (await _venueRepository.AsQueryable()
.Where(v => venueIds.Contains(v.Id))
.Select(v => new { v.Id, v.Sort }).ToListAsync()).ToDictionary(v => v.Id, v => v.Sort);
int SessionOrder(EventSessionEntry entry)
{
var session = sessionById[entry.SessionId];
var order = arrangementOrder[session.Id];
return entry.SourceType == 1
? GetImportedSessionOrder(session.SessionName, order)
: BuildEntrySessionOrder(order, entry.PlayOrder);
}
long ScheduleOrder(EventSessionEntry entry)
{
if (entry.SourceType == 1) return entry.Id;
var session = sessionById[entry.SessionId];
return BuildEntryScheduleOrder(arrangementOrder[session.Id],
venueOrder.GetValueOrDefault(session.VenueId, 1), entry.PlayOrder);
}
var ordered = entries.OrderBy(ScheduleOrder).ThenBy(e => e.PlayOrder).ThenBy(e => e.Id).ToList();
output.Total = ordered.Count;
var pageCount = Math.Max(1, (ordered.Count + pageSize - 1) / pageSize);
output.Page = Math.Clamp(input.Page, 1, pageCount);
for (var index = 0; index < ordered.Count; index += pageSize)
{
var lastIndex = Math.Min(index + pageSize, ordered.Count) - 1;
output.PageRanges.Add(new WorkbenchPageRangeDto
{
Page = index / pageSize + 1,
FirstSessionOrder = SessionOrder(ordered[index]),
LastSessionOrder = SessionOrder(ordered[lastIndex])
});
}
var pageEntries = ordered.Skip((output.Page - 1) * pageSize).Take(pageSize).ToList();
if (pageEntries.Count == 0) return output;
var pageEntryIds = pageEntries.Select(e => e.Id).ToList();
var pageSessionIds = pageEntries.Select(e => e.SessionId).Distinct().ToList();
var results = await _resultRepository.AsQueryable()
.Where(r => !r.DeleteMark && r.EventId == input.EventId && r.SessionEntryId.HasValue && pageEntryIds.Contains(r.SessionEntryId.Value))
.ToListAsync();
var resultByEntry = results.GroupBy(r => r.SessionEntryId!.Value)
.ToDictionary(g => g.Key, g => g.OrderByDescending(r => r.ResultVersion).ThenByDescending(r => r.Id).First());
var sheets = await _sheetRepository.AsQueryable()
.Where(s => !s.DeleteMark && s.IsCurrent && pageEntryIds.Contains(s.SessionEntryId))
.ToListAsync();
var sheetsByEntry = sheets.GroupBy(s => s.SessionEntryId).ToDictionary(g => g.Key, g => g.ToList());
var assignments = await _assignmentRepository.AsQueryable()
.Where(a => !a.DeleteMark && pageSessionIds.Contains(a.SessionId) && a.AssignmentStatus < 9 &&
a.Status == Core.Enum.AvailabilityStatus.Enable)
.OrderBy(a => a.JudgeRole).ToListAsync();
var assignmentsBySession = assignments.GroupBy(a => a.SessionId).ToDictionary(g => g.Key, g => g.ToList());
var corrections = await _correctionRepository.AsQueryable()
.Where(c => !c.DeleteMark && pageEntryIds.Contains(c.SessionEntryId) &&
(c.CorrectionStatus == 0 || c.CorrectionStatus == 10 || c.CorrectionStatus == 30))
.Select(c => new { c.OriginalScoreSheetId, c.CorrectionStatus }).ToListAsync();
var pendingSheetIds = corrections.Select(c => c.OriginalScoreSheetId).ToHashSet();
var resubmissionSheetIds = corrections.Where(c => c.CorrectionStatus is 10 or 30)
.Select(c => c.OriginalScoreSheetId).ToHashSet();
var merchantIds = results.Where(r => r.MerchantCertificationId.HasValue)
.Select(r => r.MerchantCertificationId!.Value).Distinct().ToList();
var merchantNames = (await _merchantCertificationRepository.AsQueryable()
.Where(m => merchantIds.Contains(m.Id) && !m.DeleteMark)
.Select(m => new { m.Id, m.MerchantName }).ToListAsync())
.ToDictionary(m => m.Id, m => m.MerchantName);
WorkbenchJudgeScoreDto SummarySheet(EventJudgeScoreSheet sheet) => new()
{
ScoreSheetId = sheet.Id,
JudgeRole = sheet.JudgeRole,
JudgeRoleType = sheet.JudgeRoleType,
SheetStatus = sheet.SheetStatus,
SheetStatusName = GetSheetStatusName(sheet.SheetStatus),
HasPendingCorrection = pendingSheetIds.Contains(sheet.Id),
RequiresResubmission = resubmissionSheetIds.Contains(sheet.Id)
};
output.Items = pageEntries.Select(entry =>
{
resultByEntry.TryGetValue(entry.Id, out var result);
var entrySheets = sheetsByEntry.GetValueOrDefault(entry.Id) ?? new List<EventJudgeScoreSheet>();
var assigned = assignmentsBySession.GetValueOrDefault(entry.SessionId) ?? new List<EventJudgeAssignment>();
var judgeScores = assigned.Select(assignment =>
{
var sheet = entrySheets.FirstOrDefault(s => s.RefereeId == assignment.RefereeId &&
s.JudgeRole == assignment.JudgeRole);
return sheet != null ? SummarySheet(sheet) : new WorkbenchJudgeScoreDto
{
JudgeRole = assignment.JudgeRole,
JudgeRoleType = assignment.JudgeRoleType ?? string.Empty,
SheetStatus = 0,
SheetStatusName = "待提交"
};
}).ToList();
var assignmentKeys = assigned.Select(a => $"{a.RefereeId}:{a.JudgeRole}").ToHashSet();
judgeScores.AddRange(entrySheets.Where(s => !assignmentKeys.Contains($"{s.RefereeId}:{s.JudgeRole}"))
.Select(SummarySheet));
return new WorkbenchEntryDetailDto
{
SessionId = entry.SessionId,
SessionEntryId = entry.Id,
EntryNo = entry.EntryNo,
PlayOrder = entry.PlayOrder,
SessionOrder = SessionOrder(entry),
ScheduleOrder = ScheduleOrder(entry),
ParticipantName = entry.ParticipantName,
TeamName = entry.TeamName,
UnitName = result?.MerchantCertificationId is long merchantId
? merchantNames.GetValueOrDefault(merchantId) : null,
GroupName = entry.GroupName,
EntryStatus = entry.EntryStatus,
EntryStatusName = GetEntryStatusName(entry.EntryStatus),
IsRematch = entry.IsRematch,
JudgeScores = judgeScores,
Result = result == null ? null : new WorkbenchEntryResultDto
{
ResultId = result.Id,
DifficultyScore = result.DifficultyScore,
PerformanceScore = result.PerformanceScore,
RequiredScore = result.RequiredScore,
DeductionScore = result.DeductionScore,
DeductionFactor = ScoreDisplayService.DeductionFactor(result.CalculationJson),
TotalScore = result.TotalScore,
ScoreText = result.Score,
IsDisqualified = result.IsDisqualified,
DisqualifyReason = result.DisqualifyReasonCode,
ResultStatus = result.ResultStatus,
ResultStatusName = GetResultStatusName(result.ResultStatus),
ResultVersion = result.ResultVersion,
ReviewUserId = result.ReviewUserId,
ReviewTime = result.ReviewTime,
Rank = result.Rank
}
};
}).ToList();
return output;
}
/// <summary>点击详情时才加载该出场的裁判评分项和计分板地址。</summary>
[HttpGet("entries/{sessionEntryId}/detail")]
public async Task<WorkbenchEntryDetailDto> GetEntryDetail(long sessionEntryId)
{
var entry = await _entryRepository.GetFirstAsync(e => e.Id == sessionEntryId && !e.DeleteMark)
?? throw Oops.Bah("出场记录不存在");
var rows = await GetSessionEntries(entry.SessionId,
new WorkbenchEntryQueryInput { SessionEntryId = sessionEntryId });
return rows.FirstOrDefault() ?? throw Oops.Bah("出场记录不存在");
}
}