From 65c5b32bc8b33499824755fd2164ff39bc84e5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Thu, 27 Aug 2026 16:09:05 +0800 Subject: [PATCH] =?UTF-8?q?=E6=89=93=E5=88=86=E7=B3=BB=E7=BB=9F=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Workbench/Dtos/WorkbenchDtos.cs | 756 ++++++++++++ .../Workbench/WorkbenchController.cs | 1086 +++++++++++++++++ 2 files changed, 1842 insertions(+) create mode 100644 YD_Event.Application/Workbench/Dtos/WorkbenchDtos.cs create mode 100644 YD_Event.Application/Workbench/WorkbenchController.cs diff --git a/YD_Event.Application/Workbench/Dtos/WorkbenchDtos.cs b/YD_Event.Application/Workbench/Dtos/WorkbenchDtos.cs new file mode 100644 index 0000000..7922124 --- /dev/null +++ b/YD_Event.Application/Workbench/Dtos/WorkbenchDtos.cs @@ -0,0 +1,756 @@ +namespace YD_Event.Application.Workbench.Dtos; + +/// +/// 工作台场次列表查询输入 +/// +public class WorkbenchSessionQueryInput +{ + /// + /// 赛事ID + /// + public long EventId { get; set; } + + /// + /// 场地ID(可选) + /// + public long? VenueId { get; set; } + + /// + /// 项目ID(可选) + /// + public long? EventProjectId { get; set; } + + /// + /// 场次状态(可选):0-草稿,10-就绪,20-检录中,30-进行中,40-暂停,50-待确认,60-已结束,90-已取消 + /// + public int? SessionStatus { get; set; } + + /// + /// 关键词搜索(场次编号、场次名称) + /// + public string? Keyword { get; set; } +} + +/// +/// 工作台场次信息 +/// +public class WorkbenchSessionDto +{ + /// + /// 场次ID + /// + public long SessionId { get; set; } + + /// + /// 场次编号 + /// + public string SessionNo { get; set; } = string.Empty; + + /// + /// 场次名称 + /// + public string? SessionName { get; set; } + + /// + /// 赛事名称 + /// + public string EventName { get; set; } = string.Empty; + + /// + /// 项目名称 + /// + public string ProjectName { get; set; } = string.Empty; + + /// + /// 场地名称 + /// + public string VenueName { get; set; } = string.Empty; + + /// + /// 组别名称(多组别混合时为空) + /// + public string? GroupName { get; set; } + + /// + /// 场次开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 场次结束时间 + /// + public DateTime? EndTime { get; set; } + + /// + /// 场次状态:0-草稿,10-就绪,20-检录中,30-进行中,40-暂停,50-待确认,60-已结束,90-已取消 + /// + public int SessionStatus { get; set; } + + /// + /// 场次状态名称 + /// + public string SessionStatusName { get; set; } = string.Empty; + + /// + /// 当前出场记录ID + /// + public long? CurrentEntryId { get; set; } + + /// + /// 总出场数 + /// + public int TotalEntries { get; set; } + + /// + /// 已完成出场数 + /// + public int CompletedEntries { get; set; } + + /// + /// 已确认成绩数 + /// + public int ConfirmedResults { get; set; } +} + +/// +/// 工作台出场记录列表输入 +/// +public class WorkbenchEntryQueryInput +{ + /// + /// 场次ID + /// + public long SessionId { get; set; } + + /// + /// 出场状态(可选) + /// + public int? EntryStatus { get; set; } + + /// + /// 成绩状态(可选) + /// + public int? ResultStatus { get; set; } + + /// + /// 关键词搜索(参赛者姓名、队名、出场编号) + /// + public string? Keyword { get; set; } +} + +/// +/// 工作台出场记录详情 +/// +public class WorkbenchEntryDetailDto +{ + /// + /// 出场记录ID + /// + public long SessionEntryId { get; set; } + + /// + /// 出场编号 + /// + public string EntryNo { get; set; } = string.Empty; + + /// + /// 出场顺序 + /// + public int PlayOrder { get; set; } + + /// + /// 参赛者姓名 + /// + public string ParticipantName { get; set; } = string.Empty; + + /// + /// 队伍名称(个人项目为空) + /// + public string? TeamName { get; set; } + + /// + /// 组别名称 + /// + public string GroupName { get; set; } = string.Empty; + + /// + /// 出场状态:0-待出场,10-已检录,20-已叫号,30-进行中,40-已完成,80-退赛,90-取消比赛资格 + /// + public int EntryStatus { get; set; } + + /// + /// 出场状态名称 + /// + public string EntryStatusName { get; set; } = string.Empty; + + /// + /// 是否为重赛 + /// + public bool IsRematch { get; set; } + + /// + /// 成绩信息 + /// + public WorkbenchEntryResultDto? Result { get; set; } + + /// + /// 裁判评分详情列表 + /// + public List JudgeScores { get; set; } = new(); +} + +/// +/// 工作台出场成绩信息 +/// +public class WorkbenchEntryResultDto +{ + /// + /// 成绩ID + /// + public long ResultId { get; set; } + + /// + /// 难度分 + /// + public decimal DifficultyScore { get; set; } + + /// + /// 表现分 + /// + public decimal PerformanceScore { get; set; } + + /// + /// 规定分 + /// + public decimal RequiredScore { get; set; } + + /// + /// 扣分 + /// + public decimal DeductionScore { get; set; } + + /// + /// 总分 + /// + public decimal? TotalScore { get; set; } + + /// + /// 成绩显示文本 + /// + public string ScoreText { get; set; } = string.Empty; + + /// + /// 是否取消比赛资格 + /// + public bool IsDisqualified { get; set; } + + /// + /// 取消比赛资格原因 + /// + public string? DisqualifyReason { get; set; } + + /// + /// 成绩状态:0-计算中,10-待复核,20-已确认,30-已投屏,40-已更正,90-已作废 + /// + public int ResultStatus { get; set; } + + /// + /// 成绩状态名称 + /// + public string ResultStatusName { get; set; } = string.Empty; + + /// + /// 当前成绩版本号 + /// + public int ResultVersion { get; set; } + + /// + /// 排名 + /// + public int? Rank { get; set; } +} + +/// +/// 工作台裁判评分信息 +/// +public class WorkbenchJudgeScoreDto +{ + /// + /// 评分单ID + /// + public long ScoreSheetId { get; set; } + + /// + /// 裁判席位编码 + /// + public string JudgeRole { get; set; } = string.Empty; + + /// + /// 裁判类型编码 + /// + public string JudgeRoleType { get; set; } = string.Empty; + + /// + /// 裁判类型名称 + /// + public string JudgeRoleName { get; set; } = string.Empty; + + /// + /// 裁判姓名 + /// + public string JudgeName { get; set; } = string.Empty; + + /// + /// 评分状态:0-草稿,10-已提交,20-已确认,30-已被修正,90-已作废 + /// + public int SheetStatus { get; set; } + + /// + /// 评分状态名称 + /// + public string SheetStatusName { get; set; } = string.Empty; + + /// + /// 难度分 + /// + public decimal DifficultyScore { get; set; } + + /// + /// 表现分 + /// + public decimal PerformanceScore { get; set; } + + /// + /// 规定分 + /// + public decimal RequiredScore { get; set; } + + /// + /// 扣分 + /// + public decimal DeductionScore { get; set; } + + /// + /// 合计得分 + /// + public decimal TotalScore { get; set; } + + /// + /// 是否判定取消比赛资格 + /// + public bool IsDisqualified { get; set; } + + /// + /// 提交时间 + /// + public DateTime? SubmittedTime { get; set; } + + /// + /// 是否存在待处理的修正申请 + /// + public bool HasPendingCorrection { get; set; } +} + +/// +/// 成绩投屏输入 +/// +public class ResultScreenCastInput +{ + /// + /// 赛事ID + /// + public long EventId { get; set; } + + /// + /// 投屏类型:1-单个成绩,2-排名列表 + /// + public int CastType { get; set; } + + /// + /// 场地ID(可选,用于筛选投屏范围) + /// + public long? VenueId { get; set; } + + /// + /// 项目ID(投屏排名时必填) + /// + public long? EventProjectId { get; set; } + + /// + /// 组别ID(投屏排名时必填) + /// + public long? EventProjectGroupId { get; set; } + + /// + /// 场次ID(投屏单个成绩时必填) + /// + public long? SessionId { get; set; } + + /// + /// 出场记录ID(投屏单个成绩时必填) + /// + public long? SessionEntryId { get; set; } + + /// + /// 成绩ID(投屏单个成绩时必填) + /// + public long? EventResultId { get; set; } + + /// + /// 成绩版本号(可选,默认使用当前版本) + /// + public int? ResultVersion { get; set; } +} + +/// +/// 成绩投屏输出 +/// +public class ResultScreenCastOutput +{ + /// + /// 投屏记录ID + /// + public long ScreenCastId { get; set; } + + /// + /// 投屏类型:1-成绩,2-排名 + /// + public int CastType { get; set; } + + /// + /// 投屏时间 + /// + public DateTime CastTime { get; set; } + + /// + /// 投屏内容快照 + /// + public string PayloadJson { get; set; } = string.Empty; +} + +/// +/// 撤回投屏输入 +/// +public class WithdrawScreenCastInput +{ + /// + /// 投屏记录ID + /// + public long ScreenCastId { get; set; } +} + +/// +/// 获取当前投屏内容输入 +/// +public class GetCurrentScreenCastInput +{ + /// + /// 赛事ID + /// + public long EventId { get; set; } + + /// + /// 场地ID(可选) + /// + public long? VenueId { get; set; } +} + +/// +/// 当前投屏内容输出 +/// +public class CurrentScreenCastDto +{ + /// + /// 投屏记录ID + /// + public long ScreenCastId { get; set; } + + /// + /// 投屏类型:1-成绩,2-排名 + /// + public int CastType { get; set; } + + /// + /// 投屏状态:0-已投出,10-已撤回,20-已被新投屏替换 + /// + public int CastStatus { get; set; } + + /// + /// 投屏时间 + /// + public DateTime CastTime { get; set; } + + /// + /// 投屏内容(JSON格式) + /// + public string PayloadJson { get; set; } = string.Empty; +} + +/// +/// 成绩修正申请审批输入 +/// +public class ApproveScoreCorrectionInput +{ + /// + /// 修正申请ID + /// + public long CorrectionId { get; set; } + + /// + /// 审批结果:true-批准,false-拒绝 + /// + public bool IsApproved { get; set; } + + /// + /// 审批意见 + /// + public string? ApproveRemark { get; set; } +} + +/// +/// 成绩修正申请列表查询输入 +/// +public class ScoreCorrectionQueryInput +{ + /// + /// 赛事ID + /// + public long EventId { get; set; } + + /// + /// 修正状态(可选):0-已申请,10-已批准,20-已拒绝,30-裁判修改中,40-已完成,90-已取消 + /// + public int? CorrectionStatus { get; set; } + + /// + /// 关键词搜索 + /// + public string? Keyword { get; set; } +} + +/// +/// 成绩修正申请列表项 +/// +public class ScoreCorrectionListDto +{ + /// + /// 修正申请ID + /// + public long CorrectionId { get; set; } + + /// + /// 赛事名称 + /// + public string EventName { get; set; } = string.Empty; + + /// + /// 场次编号 + /// + public string SessionNo { get; set; } = string.Empty; + + /// + /// 出场编号 + /// + public string EntryNo { get; set; } = string.Empty; + + /// + /// 参赛者姓名 + /// + public string ParticipantName { get; set; } = string.Empty; + + /// + /// 裁判席位 + /// + public string JudgeRole { get; set; } = string.Empty; + + /// + /// 申请人姓名 + /// + public string ApplyUserName { get; set; } = string.Empty; + + /// + /// 申请修正原因 + /// + public string ApplyReason { get; set; } = string.Empty; + + /// + /// 申请时间 + /// + public DateTime CreatedTime { get; set; } + + /// + /// 修正状态:0-已申请,10-已批准,20-已拒绝,30-裁判修改中,40-已完成,90-已取消 + /// + public int CorrectionStatus { get; set; } + + /// + /// 修正状态名称 + /// + public string CorrectionStatusName { get; set; } = string.Empty; + + /// + /// 审批人姓名 + /// + public string? ApproveUserName { get; set; } + + /// + /// 审批时间 + /// + public DateTime? ApproveTime { get; set; } + + /// + /// 完成时间 + /// + public DateTime? CompletedTime { get; set; } +} + +/// +/// 成绩修正申请详情 +/// +public class ScoreCorrectionDetailDto +{ + /// + /// 修正申请ID + /// + public long CorrectionId { get; set; } + + /// + /// 赛事信息 + /// + public WorkbenchSessionDto Session { get; set; } = new(); + + /// + /// 出场信息 + /// + public WorkbenchEntryDetailDto Entry { get; set; } = new(); + + /// + /// 原评分单信息 + /// + public WorkbenchJudgeScoreDto OriginalScore { get; set; } = new(); + + /// + /// 新评分单信息(修正完成后有值) + /// + public WorkbenchJudgeScoreDto? NewScore { get; set; } + + /// + /// 申请人信息 + /// + public string ApplyUserName { get; set; } = string.Empty; + + /// + /// 申请修正原因 + /// + public string ApplyReason { get; set; } = string.Empty; + + /// + /// 申请时间 + /// + public DateTime CreatedTime { get; set; } + + /// + /// 修正状态:0-已申请,10-已批准,20-已拒绝,30-裁判修改中,40-已完成,90-已取消 + /// + public int CorrectionStatus { get; set; } + + /// + /// 审批人姓名 + /// + public string? ApproveUserName { get; set; } + + /// + /// 审批意见 + /// + public string? ApproveRemark { get; set; } + + /// + /// 审批时间 + /// + public DateTime? ApproveTime { get; set; } + + /// + /// 完成时间 + /// + public DateTime? CompletedTime { get; set; } +} + +/// +/// 重赛输入 +/// +public class CreateRematchInput +{ + /// + /// 原出场记录ID + /// + public long OriginalSessionEntryId { get; set; } + + /// + /// 重赛原因 + /// + public string RematchReason { get; set; } = string.Empty; +} + +/// +/// 重赛输出 +/// +public class CreateRematchOutput +{ + /// + /// 新出场记录ID + /// + public long NewSessionEntryId { get; set; } + + /// + /// 新出场编号 + /// + public string NewEntryNo { get; set; } = string.Empty; + + /// + /// 新出场顺序 + /// + public int NewPlayOrder { get; set; } +} + +/// +/// 成绩作废输入 +/// +public class VoidResultInput +{ + /// + /// 出场记录ID + /// + public long SessionEntryId { get; set; } + + /// + /// 作废原因 + /// + public string VoidReason { get; set; } = string.Empty; +} + +/// +/// 成绩确认输入 +/// +public class ConfirmResultInput +{ + /// + /// 成绩ID + /// + public long ResultId { get; set; } +} + +/// +/// 批量成绩确认输入 +/// +public class BatchConfirmResultInput +{ + /// + /// 场次ID + /// + public long SessionId { get; set; } + + /// + /// 成绩ID列表(可选,不传则确认该场次所有待确认成绩) + /// + public List? ResultIds { get; set; } +} diff --git a/YD_Event.Application/Workbench/WorkbenchController.cs b/YD_Event.Application/Workbench/WorkbenchController.cs new file mode 100644 index 0000000..58f9a41 --- /dev/null +++ b/YD_Event.Application/Workbench/WorkbenchController.cs @@ -0,0 +1,1086 @@ +using YD_Event.Application.Auth; +using YD_Event.Application.Workbench.Dtos; +using YD_Event.Core.Entities; + +namespace YD_Event.Application.Workbench; + +/// +/// 打分系统工作台接口(供工作台管理端调用) +/// +[ApiDescriptionSettings("打分系统工作台")] +[Authorize] +public class WorkbenchController : IDynamicApiController +{ + private readonly ISqlSugarRepository _eventRepository; + private readonly ISqlSugarRepository _venueRepository; + private readonly ISqlSugarRepository _eventProjectRepository; + private readonly ISqlSugarRepository _groupRepository; + private readonly ISqlSugarRepository _sessionRepository; + private readonly ISqlSugarRepository _entryRepository; + private readonly ISqlSugarRepository _assignmentRepository; + private readonly ISqlSugarRepository _sheetRepository; + private readonly ISqlSugarRepository _correctionRepository; + private readonly ISqlSugarRepository _resultRepository; + private readonly ISqlSugarRepository _resultVersionRepository; + private readonly ISqlSugarRepository _screenCastRepository; + private readonly ISqlSugarRepository _taskRepository; + private readonly ISqlSugarRepository _messageRepository; + private readonly ISqlSugarRepository _userRepository; + private readonly ISqlSugarRepository _refereeRepository; + private readonly ISqlSugarRepository _projectRepository; + private readonly AuthManager _authManager; + private readonly IIdGenerator _idGenerator; + + public WorkbenchController( + ISqlSugarRepository eventRepository, + ISqlSugarRepository venueRepository, + ISqlSugarRepository eventProjectRepository, + ISqlSugarRepository groupRepository, + ISqlSugarRepository sessionRepository, + ISqlSugarRepository entryRepository, + ISqlSugarRepository assignmentRepository, + ISqlSugarRepository sheetRepository, + ISqlSugarRepository correctionRepository, + ISqlSugarRepository resultRepository, + ISqlSugarRepository resultVersionRepository, + ISqlSugarRepository screenCastRepository, + ISqlSugarRepository taskRepository, + ISqlSugarRepository messageRepository, + ISqlSugarRepository userRepository, + ISqlSugarRepository refereeRepository, + ISqlSugarRepository projectRepository, + AuthManager authManager, + IIdGenerator idGenerator) + { + _eventRepository = eventRepository; + _venueRepository = venueRepository; + _eventProjectRepository = eventProjectRepository; + _groupRepository = groupRepository; + _sessionRepository = sessionRepository; + _entryRepository = entryRepository; + _assignmentRepository = assignmentRepository; + _sheetRepository = sheetRepository; + _correctionRepository = correctionRepository; + _resultRepository = resultRepository; + _resultVersionRepository = resultVersionRepository; + _screenCastRepository = screenCastRepository; + _taskRepository = taskRepository; + _messageRepository = messageRepository; + _userRepository = userRepository; + _refereeRepository = refereeRepository; + _projectRepository = projectRepository; + _authManager = authManager; + _idGenerator = idGenerator; + } + + #region 场次管理 + + /// + /// 获取工作台场次列表 + /// + [HttpGet("workbench/sessions")] + public async Task> GetSessions([FromQuery] WorkbenchSessionQueryInput input) + { + var query = _sessionRepository.AsQueryable() + .Where(s => !s.DeleteMark && s.EventId == input.EventId); + + if (input.VenueId.HasValue) + query = query.Where(s => s.VenueId == input.VenueId.Value); + + if (input.EventProjectId.HasValue) + query = query.Where(s => s.EventProjectId == input.EventProjectId.Value); + + if (input.SessionStatus.HasValue) + query = query.Where(s => s.SessionStatus == input.SessionStatus.Value); + + if (!string.IsNullOrWhiteSpace(input.Keyword)) + { + var keyword = input.Keyword.Trim(); + query = query.Where(s => s.SessionNo.Contains(keyword) || (s.SessionName != null && s.SessionName.Contains(keyword))); + } + + var sessions = await query + .OrderBy(s => s.StartTime) + .OrderBy(s => s.SessionNo) + .Select(s => new + { + s.Id, + s.SessionNo, + s.SessionName, + s.EventId, + s.VenueId, + s.EventProjectId, + s.EventProjectGroupId, + s.StartTime, + s.EndTime, + s.SessionStatus, + s.CurrentEntryId + }) + .ToListAsync(); + + if (!sessions.Any()) + return new List(); + + var eventIds = sessions.Select(s => s.EventId).Distinct().ToList(); + var venueIds = sessions.Select(s => s.VenueId).Distinct().ToList(); + var projectIds = sessions.Select(s => s.EventProjectId).Distinct().ToList(); + var groupIds = sessions.Where(s => s.EventProjectGroupId.HasValue).Select(s => s.EventProjectGroupId!.Value).Distinct().ToList(); + var sessionIds = sessions.Select(s => s.Id).ToList(); + + var events = await _eventRepository.AsQueryable() + .Where(e => eventIds.Contains(e.Id)) + .Select(e => new { e.Id, e.EventName }) + .ToListAsync(); + + var venues = await _venueRepository.AsQueryable() + .Where(v => venueIds.Contains(v.Id)) + .Select(v => new { v.Id, v.VenueName }) + .ToListAsync(); + + var eventProjects = await _eventProjectRepository.AsQueryable() + .Where(ep => projectIds.Contains(ep.Id)) + .Select(ep => new { ep.Id, ep.ProjectId }) + .ToListAsync(); + + var projectDict = (await _projectRepository.AsQueryable() + .Where(p => eventProjects.Select(ep => ep.ProjectId).Contains(p.Id)) + .Select(p => new { p.Id, p.Name }) + .ToListAsync()) + .ToDictionary(p => p.Id, p => p.Name); + + var groups = await _groupRepository.AsQueryable() + .Where(g => groupIds.Contains(g.Id)) + .Select(g => new { g.Id, g.GroupName }) + .ToListAsync(); + + var entryCounts = await _entryRepository.AsQueryable() + .Where(e => !e.DeleteMark && sessionIds.Contains(e.SessionId)) + .GroupBy(e => e.SessionId) + .Select(e => new + { + SessionId = e.SessionId, + TotalCount = SqlFunc.AggregateCount(e.Id), + CompletedCount = SqlFunc.AggregateSum(SqlFunc.IIF(e.EntryStatus == 40, 1, 0)) + }) + .ToListAsync(); + + var resultCounts = await _resultRepository.AsQueryable() + .InnerJoin((r, e) => r.SessionEntryId == e.Id) + .Where((r, e) => !r.DeleteMark && !e.DeleteMark && sessionIds.Contains(e.SessionId) && r.ResultStatus == 20) + .GroupBy((r, e) => e.SessionId) + .Select((r, e) => new + { + SessionId = e.SessionId, + ConfirmedCount = SqlFunc.AggregateCount(r.Id) + }) + .ToListAsync(); + + var eventDict = events.ToDictionary(e => e.Id, e => e.EventName); + var venueDict = venues.ToDictionary(v => v.Id, v => v.VenueName); + var eventProjectDict = eventProjects.ToDictionary(ep => ep.Id, ep => projectDict.GetValueOrDefault(ep.ProjectId, "")); + var groupDict = groups.ToDictionary(g => g.Id, g => g.GroupName); + var entryCountDict = entryCounts.ToDictionary(ec => ec.SessionId); + var resultCountDict = resultCounts.ToDictionary(rc => rc.SessionId); + + return sessions.Select(s => new WorkbenchSessionDto + { + SessionId = s.Id, + SessionNo = s.SessionNo, + SessionName = s.SessionName, + EventName = eventDict.GetValueOrDefault(s.EventId, ""), + ProjectName = eventProjectDict.GetValueOrDefault(s.EventProjectId, ""), + VenueName = venueDict.GetValueOrDefault(s.VenueId, ""), + GroupName = s.EventProjectGroupId.HasValue ? groupDict.GetValueOrDefault(s.EventProjectGroupId.Value) : null, + StartTime = s.StartTime, + EndTime = s.EndTime, + SessionStatus = s.SessionStatus, + SessionStatusName = GetSessionStatusName(s.SessionStatus), + CurrentEntryId = s.CurrentEntryId, + TotalEntries = entryCountDict.TryGetValue(s.Id, out var ec) ? ec.TotalCount : 0, + CompletedEntries = entryCountDict.TryGetValue(s.Id, out var ec2) ? ec2.CompletedCount : 0, + ConfirmedResults = resultCountDict.TryGetValue(s.Id, out var rc) ? rc.ConfirmedCount : 0 + }).ToList(); + } + + /// + /// 获取场次的出场记录列表 + /// + [HttpGet("workbench/sessions/{sessionId}/entries")] + public async Task> GetSessionEntries(long sessionId, [FromQuery] WorkbenchEntryQueryInput input) + { + var query = _entryRepository.AsQueryable() + .Where(e => !e.DeleteMark && e.SessionId == sessionId); + + if (input.EntryStatus.HasValue) + query = query.Where(e => e.EntryStatus == input.EntryStatus.Value); + + 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 + .OrderBy(e => e.PlayOrder) + .Select(e => new + { + e.Id, + e.EntryNo, + e.PlayOrder, + e.ParticipantName, + e.TeamName, + e.GroupName, + e.EntryStatus, + e.IsRematch + }) + .ToListAsync(); + + if (!entries.Any()) + return new List(); + + var entryIds = entries.Select(e => e.Id).ToList(); + + var results = await _resultRepository.AsQueryable() + .Where(r => !r.DeleteMark && entryIds.Contains(r.SessionEntryId!.Value)) + .Select(r => new + { + r.Id, + r.SessionEntryId, + r.DifficultyScore, + r.PerformanceScore, + r.RequiredScore, + r.DeductionScore, + r.TotalScore, + r.Score, + r.IsDisqualified, + r.DisqualifyReasonCode, + r.ResultStatus, + r.ResultVersion, + r.Rank + }) + .ToListAsync(); + + if (input.ResultStatus.HasValue) + { + var filteredEntryIds = results.Where(r => r.ResultStatus == input.ResultStatus.Value).Select(r => r.SessionEntryId).ToHashSet(); + entries = entries.Where(e => filteredEntryIds.Contains(e.Id)).ToList(); + entryIds = entries.Select(e => e.Id).ToList(); + } + + var sheets = await _sheetRepository.AsQueryable() + .Where(s => !s.DeleteMark && entryIds.Contains(s.SessionEntryId) && s.IsCurrent) + .OrderBy(s => s.JudgeRole) + .ToListAsync(); + + var refereeIds = sheets.Select(s => s.RefereeId).Distinct().ToList(); + var refereeMap = (await _refereeRepository.AsQueryable() + .Where(r => refereeIds.Contains(r.Id)) + .Select(r => new { r.Id, r.Name }) + .ToListAsync()) + .ToDictionary(r => r.Id, r => r.Name); + + var pendingCorrections = await _correctionRepository.AsQueryable() + .Where(c => !c.DeleteMark && entryIds.Contains(c.SessionEntryId) && c.CorrectionStatus < 20) + .Select(c => c.OriginalScoreSheetId) + .ToListAsync(); + + var pendingCorrectionSet = pendingCorrections.ToHashSet(); + var resultDict = results.ToDictionary(r => r.SessionEntryId!.Value); + + return entries.Select(e => + { + var result = resultDict.GetValueOrDefault(e.Id); + var entrySheets = sheets.Where(s => s.SessionEntryId == e.Id).ToList(); + + return new WorkbenchEntryDetailDto + { + SessionEntryId = e.Id, + EntryNo = e.EntryNo, + PlayOrder = e.PlayOrder, + ParticipantName = e.ParticipantName, + TeamName = e.TeamName, + GroupName = e.GroupName, + EntryStatus = e.EntryStatus, + EntryStatusName = GetEntryStatusName(e.EntryStatus), + IsRematch = e.IsRematch, + Result = result != null ? new WorkbenchEntryResultDto + { + ResultId = result.Id, + DifficultyScore = result.DifficultyScore, + PerformanceScore = result.PerformanceScore, + RequiredScore = result.RequiredScore, + DeductionScore = result.DeductionScore, + TotalScore = result.TotalScore, + ScoreText = result.Score, + IsDisqualified = result.IsDisqualified, + DisqualifyReason = result.DisqualifyReasonCode, + ResultStatus = result.ResultStatus, + ResultStatusName = GetResultStatusName(result.ResultStatus), + ResultVersion = result.ResultVersion, + Rank = result.Rank + } : null, + JudgeScores = entrySheets.Select(sheet => new WorkbenchJudgeScoreDto + { + ScoreSheetId = sheet.Id, + JudgeRole = sheet.JudgeRole, + JudgeRoleType = sheet.JudgeRoleType, + JudgeRoleName = GetJudgeRoleTypeName(sheet.JudgeRoleType), + JudgeName = refereeMap.GetValueOrDefault(sheet.RefereeId, ""), + SheetStatus = sheet.SheetStatus, + SheetStatusName = GetSheetStatusName(sheet.SheetStatus), + DifficultyScore = sheet.DifficultyScore, + PerformanceScore = sheet.PerformanceScore, + RequiredScore = sheet.RequiredScore, + DeductionScore = sheet.DeductionScore, + TotalScore = sheet.TotalScore, + IsDisqualified = sheet.IsDisqualified, + SubmittedTime = sheet.SubmittedTime, + HasPendingCorrection = pendingCorrectionSet.Contains(sheet.Id) + }).ToList() + }; + }).ToList(); + } + + #endregion + + #region 成绩投屏 + + /// + /// 投出成绩到大屏 + /// + [HttpPost("workbench/screencast")] + public async Task CastResult([FromBody] ResultScreenCastInput input) + { + var currentUserId = _authManager.UserId; + var now = DateTime.Now; + + if (input.CastType == 1) + { + if (!input.SessionEntryId.HasValue || !input.EventResultId.HasValue) + throw Oops.Bah("投出单个成绩时,必须提供出场记录ID和成绩ID"); + + var entry = await _entryRepository.GetFirstAsync(e => e.Id == input.SessionEntryId.Value && !e.DeleteMark) + ?? throw Oops.Bah("出场记录不存在"); + + var result = await _resultRepository.GetFirstAsync(r => r.Id == input.EventResultId.Value && !r.DeleteMark) + ?? throw Oops.Bah("成绩记录不存在"); + + var session = await _sessionRepository.GetByIdAsync(entry.SessionId) + ?? throw Oops.Bah("场次不存在"); + + var payload = new + { + EntryNo = entry.EntryNo, + ParticipantName = entry.ParticipantName, + TeamName = entry.TeamName, + GroupName = entry.GroupName, + DifficultyScore = result.DifficultyScore, + PerformanceScore = result.PerformanceScore, + RequiredScore = result.RequiredScore, + DeductionScore = result.DeductionScore, + TotalScore = result.TotalScore, + Score = result.Score, + IsDisqualified = result.IsDisqualified, + Rank = result.Rank + }; + + await _screenCastRepository.AsUpdateable() + .SetColumns(sc => sc.CastStatus == 20) + .Where(sc => sc.EventId == input.EventId && + (!input.VenueId.HasValue || sc.VenueId == input.VenueId.Value) && + sc.CastStatus == 0) + .ExecuteCommandAsync(); + + var screenCast = new EventResultScreenCast + { + Id = _idGenerator.NextId(), + EventId = input.EventId, + CastType = 1, + VenueId = input.VenueId, + EventProjectId = session.EventProjectId, + EventProjectGroupId = session.EventProjectGroupId, + SessionId = session.Id, + SessionEntryId = entry.Id, + EventResultId = result.Id, + ResultVersion = input.ResultVersion ?? result.ResultVersion, + PayloadJson = System.Text.Json.JsonSerializer.Serialize(payload), + CastStatus = 0, + CastTime = now, + CreatedUserId = currentUserId, + CreatedTime = now + }; + + await _screenCastRepository.InsertAsync(screenCast); + + return new ResultScreenCastOutput + { + ScreenCastId = screenCast.Id, + CastType = 1, + CastTime = now, + PayloadJson = screenCast.PayloadJson + }; + } + else if (input.CastType == 2) + { + if (!input.EventProjectId.HasValue || !input.EventProjectGroupId.HasValue) + throw Oops.Bah("投出排名时,必须提供项目ID和组别ID"); + + var results = await _resultRepository.AsQueryable() + .InnerJoin((r, e) => r.SessionEntryId == e.Id) + .InnerJoin((r, e, s) => e.SessionId == s.Id) + .Where((r, e, s) => !r.DeleteMark && !e.DeleteMark && !s.DeleteMark && + r.EventId == input.EventId && + s.EventProjectId == input.EventProjectId.Value && + (s.EventProjectGroupId == input.EventProjectGroupId.Value || + e.GroupName == SqlFunc.Subqueryable() + .Where(g => g.Id == input.EventProjectGroupId.Value) + .Select(g => g.GroupName)) && + r.ResultStatus >= 20) + .OrderBy((r, e, s) => r.Rank) + .Select((r, e, s) => new + { + r.Id, + e.EntryNo, + e.ParticipantName, + e.TeamName, + e.GroupName, + r.TotalScore, + r.Score, + r.Rank, + r.IsDisqualified + }) + .ToListAsync(); + + var payload = results.Select(r => new + { + r.EntryNo, + r.ParticipantName, + r.TeamName, + r.GroupName, + r.TotalScore, + r.Score, + r.Rank, + r.IsDisqualified + }).ToList(); + + await _screenCastRepository.AsUpdateable() + .SetColumns(sc => sc.CastStatus == 20) + .Where(sc => sc.EventId == input.EventId && + (!input.VenueId.HasValue || sc.VenueId == input.VenueId.Value) && + sc.CastStatus == 0) + .ExecuteCommandAsync(); + + var screenCast = new EventResultScreenCast + { + Id = _idGenerator.NextId(), + EventId = input.EventId, + CastType = 2, + VenueId = input.VenueId, + EventProjectId = input.EventProjectId.Value, + EventProjectGroupId = input.EventProjectGroupId.Value, + SessionId = null, + SessionEntryId = null, + EventResultId = null, + ResultVersion = null, + PayloadJson = System.Text.Json.JsonSerializer.Serialize(payload), + CastStatus = 0, + CastTime = now, + CreatedUserId = currentUserId, + CreatedTime = now + }; + + await _screenCastRepository.InsertAsync(screenCast); + + return new ResultScreenCastOutput + { + ScreenCastId = screenCast.Id, + CastType = 2, + CastTime = now, + PayloadJson = screenCast.PayloadJson + }; + } + else + { + throw Oops.Bah("无效的投屏类型"); + } + } + + /// + /// 撤回投屏内容 + /// + [HttpPost("workbench/screencast/withdraw")] + public async Task WithdrawScreenCast([FromBody] WithdrawScreenCastInput input) + { + var screenCast = await _screenCastRepository.GetByIdAsync(input.ScreenCastId) + ?? throw Oops.Bah("投屏记录不存在"); + + if (screenCast.CastStatus != 0) + throw Oops.Bah("该投屏已被撤回或替换,无法再次撤回"); + + screenCast.CastStatus = 10; + screenCast.UpdatedUserId = _authManager.UserId; + screenCast.UpdatedTime = DateTime.Now; + + await _screenCastRepository.UpdateAsync(screenCast); + } + + /// + /// 获取当前投屏内容 + /// + [HttpGet("workbench/screencast/current")] + public async Task GetCurrentScreenCast([FromQuery] GetCurrentScreenCastInput input) + { + var query = _screenCastRepository.AsQueryable() + .Where(sc => sc.EventId == input.EventId && sc.CastStatus == 0); + + if (input.VenueId.HasValue) + query = query.Where(sc => sc.VenueId == input.VenueId.Value); + + var screenCast = await query + .OrderByDescending(sc => sc.CastTime) + .FirstAsync(); + + if (screenCast == null) + return null; + + return new CurrentScreenCastDto + { + ScreenCastId = screenCast.Id, + CastType = screenCast.CastType, + CastStatus = screenCast.CastStatus, + CastTime = screenCast.CastTime, + PayloadJson = screenCast.PayloadJson + }; + } + + #endregion + + #region 成绩修正审批 + + /// + /// 获取成绩修正申请列表 + /// + [HttpGet("workbench/corrections")] + public async Task> GetCorrections([FromQuery] ScoreCorrectionQueryInput input) + { + var query = _correctionRepository.AsQueryable() + .Where(c => !c.DeleteMark && c.EventId == input.EventId); + + if (input.CorrectionStatus.HasValue) + query = query.Where(c => c.CorrectionStatus == input.CorrectionStatus.Value); + + if (!string.IsNullOrWhiteSpace(input.Keyword)) + { + var keyword = input.Keyword.Trim(); + query = query.Where(c => c.ApplyReason.Contains(keyword)); + } + + var corrections = await query + .OrderByDescending(c => c.CreatedTime) + .ToListAsync(); + + if (!corrections.Any()) + return new List(); + + var sessionIds = corrections.Select(c => c.SessionId).Distinct().ToList(); + var entryIds = corrections.Select(c => c.SessionEntryId).Distinct().ToList(); + var applyUserIds = corrections.Select(c => c.ApplyUserId).Distinct().ToList(); + var approveUserIds = corrections.Where(c => c.ApproveUserId.HasValue).Select(c => c.ApproveUserId!.Value).Distinct().ToList(); + + var sessions = (await _sessionRepository.AsQueryable() + .Where(s => sessionIds.Contains(s.Id)) + .Select(s => new { s.Id, s.SessionNo }) + .ToListAsync()) + .ToDictionary(s => s.Id, s => s.SessionNo); + + var entries = (await _entryRepository.AsQueryable() + .Where(e => entryIds.Contains(e.Id)) + .Select(e => new { e.Id, e.EntryNo, e.ParticipantName }) + .ToListAsync()) + .ToDictionary(e => e.Id); + + var eventInfo = await _eventRepository.GetFirstAsync(e => e.Id == input.EventId); + + var allUserIds = applyUserIds.Concat(approveUserIds).Distinct().ToList(); + var users = (await _userRepository.AsQueryable() + .Where(u => allUserIds.Contains(u.Id)) + .Select(u => new { u.Id, u.Name }) + .ToListAsync()) + .ToDictionary(u => u.Id, u => u.Name); + + return corrections.Select(c => + { + var entry = entries.GetValueOrDefault(c.SessionEntryId); + return new ScoreCorrectionListDto + { + CorrectionId = c.Id, + EventName = eventInfo?.EventName ?? "", + SessionNo = sessions.GetValueOrDefault(c.SessionId, ""), + EntryNo = entry?.EntryNo ?? "", + ParticipantName = entry?.ParticipantName ?? "", + JudgeRole = c.JudgeRole, + ApplyUserName = users.GetValueOrDefault(c.ApplyUserId, ""), + ApplyReason = c.ApplyReason, + CreatedTime = c.CreatedTime, + CorrectionStatus = c.CorrectionStatus, + CorrectionStatusName = GetCorrectionStatusName(c.CorrectionStatus), + ApproveUserName = c.ApproveUserId.HasValue ? users.GetValueOrDefault(c.ApproveUserId.Value, "") : null, + ApproveTime = c.ApproveTime, + CompletedTime = c.CompletedTime + }; + }).ToList(); + } + + /// + /// 获取成绩修正申请详情 + /// + [HttpGet("workbench/corrections/{correctionId}")] + public async Task GetCorrectionDetail(long correctionId) + { + var correction = await _correctionRepository.GetFirstAsync(c => c.Id == correctionId && !c.DeleteMark) + ?? throw Oops.Bah("修正申请不存在"); + + var session = await _sessionRepository.GetByIdAsync(correction.SessionId) + ?? throw Oops.Bah("场次不存在"); + + var entry = await _entryRepository.GetByIdAsync(correction.SessionEntryId) + ?? throw Oops.Bah("出场记录不存在"); + + var originalSheet = await _sheetRepository.GetByIdAsync(correction.OriginalScoreSheetId) + ?? throw Oops.Bah("原评分单不存在"); + + EventJudgeScoreSheet? newSheet = null; + if (correction.NewScoreSheetId.HasValue) + { + newSheet = await _sheetRepository.GetByIdAsync(correction.NewScoreSheetId.Value); + } + + var referee = await _refereeRepository.GetByIdAsync(originalSheet.RefereeId); + + var eventInfo = await _eventRepository.GetByIdAsync(correction.EventId); + var venue = await _venueRepository.GetByIdAsync(session.VenueId); + var eventProject = await _eventProjectRepository.GetByIdAsync(session.EventProjectId); + var project = eventProject != null ? await _projectRepository.GetByIdAsync(eventProject.ProjectId) : null; + + var applyUser = await _userRepository.GetByIdAsync(correction.ApplyUserId); + var approveUser = correction.ApproveUserId.HasValue + ? await _userRepository.GetByIdAsync(correction.ApproveUserId.Value) + : null; + + var result = await _resultRepository.GetFirstAsync(r => r.SessionEntryId == entry.Id && !r.DeleteMark); + + return new ScoreCorrectionDetailDto + { + CorrectionId = correction.Id, + Session = new WorkbenchSessionDto + { + SessionId = session.Id, + SessionNo = session.SessionNo, + SessionName = session.SessionName, + EventName = eventInfo?.EventName ?? "", + VenueName = venue?.VenueName ?? "", + ProjectName = project?.Name ?? "", + StartTime = session.StartTime, + SessionStatus = session.SessionStatus, + SessionStatusName = GetSessionStatusName(session.SessionStatus) + }, + Entry = new WorkbenchEntryDetailDto + { + SessionEntryId = entry.Id, + EntryNo = entry.EntryNo, + PlayOrder = entry.PlayOrder, + ParticipantName = entry.ParticipantName, + TeamName = entry.TeamName, + GroupName = entry.GroupName, + EntryStatus = entry.EntryStatus, + EntryStatusName = GetEntryStatusName(entry.EntryStatus), + IsRematch = entry.IsRematch, + Result = result != null ? new WorkbenchEntryResultDto + { + ResultId = result.Id, + DifficultyScore = result.DifficultyScore, + PerformanceScore = result.PerformanceScore, + RequiredScore = result.RequiredScore, + DeductionScore = result.DeductionScore, + TotalScore = result.TotalScore, + ScoreText = result.Score, + IsDisqualified = result.IsDisqualified, + ResultStatus = result.ResultStatus, + ResultStatusName = GetResultStatusName(result.ResultStatus), + ResultVersion = result.ResultVersion + } : null + }, + OriginalScore = new WorkbenchJudgeScoreDto + { + ScoreSheetId = originalSheet.Id, + JudgeRole = originalSheet.JudgeRole, + JudgeRoleType = originalSheet.JudgeRoleType, + JudgeRoleName = GetJudgeRoleTypeName(originalSheet.JudgeRoleType), + JudgeName = referee?.Name ?? "", + SheetStatus = originalSheet.SheetStatus, + SheetStatusName = GetSheetStatusName(originalSheet.SheetStatus), + DifficultyScore = originalSheet.DifficultyScore, + PerformanceScore = originalSheet.PerformanceScore, + RequiredScore = originalSheet.RequiredScore, + DeductionScore = originalSheet.DeductionScore, + TotalScore = originalSheet.TotalScore, + IsDisqualified = originalSheet.IsDisqualified, + SubmittedTime = originalSheet.SubmittedTime, + HasPendingCorrection = false + }, + NewScore = newSheet != null ? new WorkbenchJudgeScoreDto + { + ScoreSheetId = newSheet.Id, + JudgeRole = newSheet.JudgeRole, + JudgeRoleType = newSheet.JudgeRoleType, + JudgeRoleName = GetJudgeRoleTypeName(newSheet.JudgeRoleType), + JudgeName = referee?.Name ?? "", + SheetStatus = newSheet.SheetStatus, + SheetStatusName = GetSheetStatusName(newSheet.SheetStatus), + DifficultyScore = newSheet.DifficultyScore, + PerformanceScore = newSheet.PerformanceScore, + RequiredScore = newSheet.RequiredScore, + DeductionScore = newSheet.DeductionScore, + TotalScore = newSheet.TotalScore, + IsDisqualified = newSheet.IsDisqualified, + SubmittedTime = newSheet.SubmittedTime, + HasPendingCorrection = false + } : null, + ApplyUserName = applyUser?.Name ?? "", + ApplyReason = correction.ApplyReason, + CreatedTime = correction.CreatedTime, + CorrectionStatus = correction.CorrectionStatus, + ApproveUserName = approveUser?.Name, + ApproveRemark = correction.ApproveRemark, + ApproveTime = correction.ApproveTime, + CompletedTime = correction.CompletedTime + }; + } + + /// + /// 审批成绩修正申请 + /// + [HttpPost("workbench/corrections/approve")] + public async Task ApproveCorrection([FromBody] ApproveScoreCorrectionInput input) + { + var currentUserId = _authManager.UserId; + var now = DateTime.Now; + + var correction = await _correctionRepository.GetFirstAsync(c => c.Id == input.CorrectionId && !c.DeleteMark) + ?? throw Oops.Bah("修正申请不存在"); + + if (correction.CorrectionStatus != 0) + throw Oops.Bah("该申请已被处理,无法重复审批"); + + correction.ApproveUserId = currentUserId; + correction.ApproveRemark = input.ApproveRemark; + correction.ApproveTime = now; + correction.CorrectionStatus = input.IsApproved ? 10 : 20; + correction.UpdatedUserId = currentUserId; + correction.UpdatedTime = now; + + await _correctionRepository.UpdateAsync(correction); + + if (input.IsApproved) + { + var assignment = await _assignmentRepository.GetByIdAsync(correction.JudgeAssignmentId); + + var task = new EventJudgeTask + { + Id = _idGenerator.NextId(), + EventId = correction.EventId, + SessionId = correction.SessionId, + SessionEntryId = correction.SessionEntryId, + JudgeAssignmentId = correction.JudgeAssignmentId, + EventRefereeId = assignment?.EventRefereeId ?? 0, + RefereeId = assignment?.RefereeId ?? 0, + JudgeUserId = assignment?.JudgeUserId, + JudgeRole = assignment?.JudgeRole ?? "", + JudgeRoleType = assignment?.JudgeRoleType ?? "", + TaskSnapshotJson = "{}", + TaskType = 1, + TaskStatus = 0, + CreatedUserId = currentUserId, + CreatedTime = now + }; + + await _taskRepository.InsertAsync(task); + + correction.CorrectionTaskId = task.Id; + await _correctionRepository.UpdateAsync(correction); + + if (assignment?.JudgeUserId != null) + { + var message = new EventJudgeMessage + { + Id = _idGenerator.NextId(), + EventId = correction.EventId, + ReceiverUserId = assignment.JudgeUserId.Value, + RefereeId = assignment.RefereeId, + MessageType = 1, + Title = "成绩修正任务通知", + Content = $"您有一条成绩修正任务需要处理,申请原因:{correction.ApplyReason}", + RelationType = "ScoreCorrection", + RelationId = correction.Id, + IsRead = false, + CreatedUserId = currentUserId, + CreatedTime = now + }; + + await _messageRepository.InsertAsync(message); + } + } + } + + #endregion + + #region 成绩管理 + + /// + /// 确认成绩 + /// + [HttpPost("workbench/results/confirm")] + public async Task ConfirmResult([FromBody] ConfirmResultInput input) + { + var currentUserId = _authManager.UserId; + var now = DateTime.Now; + + var result = await _resultRepository.GetFirstAsync(r => r.Id == input.ResultId && !r.DeleteMark) + ?? throw Oops.Bah("成绩记录不存在"); + + if (result.ResultStatus == 90) + throw Oops.Bah("该成绩已作废"); + + if (result.ResultStatus != 10) + throw Oops.Bah("只能确认待复核状态的成绩"); + + result.ResultStatus = 20; + result.UpdatedUserId = currentUserId; + result.UpdatedTime = now; + + await _resultRepository.UpdateAsync(result); + } + + /// + /// 批量确认成绩 + /// + [HttpPost("workbench/results/batch-confirm")] + public async Task BatchConfirmResults([FromBody] BatchConfirmResultInput input) + { + var currentUserId = _authManager.UserId; + var now = DateTime.Now; + + var query = _resultRepository.AsQueryable() + .InnerJoin((r, e) => r.SessionEntryId == e.Id) + .Where((r, e) => !r.DeleteMark && !e.DeleteMark && e.SessionId == input.SessionId && r.ResultStatus == 10); + + if (input.ResultIds != null && input.ResultIds.Any()) + { + query = query.Where((r, e) => input.ResultIds.Contains(r.Id)); + } + + var results = await query.Select((r, e) => r).ToListAsync(); + + if (!results.Any()) + throw Oops.Bah("没有可确认的成绩"); + + foreach (var result in results) + { + result.ResultStatus = 20; + result.UpdatedUserId = currentUserId; + result.UpdatedTime = now; + } + + await _resultRepository.UpdateRangeAsync(results); + } + + /// + /// 创建重赛 + /// + [HttpPost("workbench/entries/rematch")] + public async Task CreateRematch([FromBody] CreateRematchInput input) + { + var currentUserId = _authManager.UserId; + var now = DateTime.Now; + + var originalEntry = await _entryRepository.GetFirstAsync(e => e.Id == input.OriginalSessionEntryId && !e.DeleteMark) + ?? throw Oops.Bah("原出场记录不存在"); + + var session = await _sessionRepository.GetByIdAsync(originalEntry.SessionId) + ?? throw Oops.Bah("场次不存在"); + + var maxPlayOrder = await _entryRepository.AsQueryable() + .Where(e => !e.DeleteMark && e.SessionId == originalEntry.SessionId) + .MaxAsync(e => e.PlayOrder); + + var newPlayOrder = maxPlayOrder + 1; + + originalEntry.IsVoided = true; + originalEntry.VoidReason = $"重赛原因:{input.RematchReason}"; + originalEntry.UpdatedUserId = currentUserId; + originalEntry.UpdatedTime = now; + await _entryRepository.UpdateAsync(originalEntry); + + var result = await _resultRepository.GetFirstAsync(r => r.SessionEntryId == originalEntry.Id && !r.DeleteMark); + if (result != null) + { + result.ResultStatus = 90; + result.UpdatedUserId = currentUserId; + result.UpdatedTime = now; + await _resultRepository.UpdateAsync(result); + } + + var newEntry = new EventSessionEntry + { + Id = _idGenerator.NextId(), + SessionId = originalEntry.SessionId, + ParticipationRecordId = originalEntry.ParticipationRecordId, + RegistrationItemId = originalEntry.RegistrationItemId, + SourceType = originalEntry.SourceType, + EntryNo = $"{originalEntry.EntryNo}-R", + PlayOrder = newPlayOrder, + EntryStatus = 0, + IsRematch = true, + RematchFromEntryId = originalEntry.Id, + IsVoided = false, + ParticipantName = originalEntry.ParticipantName, + TeamName = originalEntry.TeamName, + GroupName = originalEntry.GroupName, + Version = 1, + Status = Core.Enum.AvailabilityStatus.Enable, + DeleteMark = false, + CreatedUserId = currentUserId, + CreatedTime = now + }; + + await _entryRepository.InsertAsync(newEntry); + + return new CreateRematchOutput + { + NewSessionEntryId = newEntry.Id, + NewEntryNo = newEntry.EntryNo, + NewPlayOrder = newEntry.PlayOrder + }; + } + + /// + /// 作废成绩 + /// + [HttpPost("workbench/results/void")] + public async Task VoidResult([FromBody] VoidResultInput input) + { + var currentUserId = _authManager.UserId; + var now = DateTime.Now; + + var entry = await _entryRepository.GetFirstAsync(e => e.Id == input.SessionEntryId && !e.DeleteMark) + ?? throw Oops.Bah("出场记录不存在"); + + entry.IsVoided = true; + entry.VoidReason = input.VoidReason; + entry.UpdatedUserId = currentUserId; + entry.UpdatedTime = now; + + await _entryRepository.UpdateAsync(entry); + + var result = await _resultRepository.GetFirstAsync(r => r.SessionEntryId == entry.Id && !r.DeleteMark); + if (result != null) + { + result.ResultStatus = 90; + result.UpdatedUserId = currentUserId; + result.UpdatedTime = now; + await _resultRepository.UpdateAsync(result); + } + } + + #endregion + + #region 辅助方法 + + private static string GetSessionStatusName(int status) + { + return status switch + { + 0 => "草稿", + 10 => "就绪", + 20 => "检录中", + 30 => "进行中", + 40 => "暂停", + 50 => "待确认", + 60 => "已结束", + 90 => "已取消", + _ => "未知" + }; + } + + private static string GetEntryStatusName(int status) + { + return status switch + { + 0 => "待出场", + 10 => "已检录", + 20 => "已叫号", + 30 => "进行中", + 40 => "已完成", + 80 => "退赛", + 90 => "取消比赛资格", + _ => "未知" + }; + } + + private static string GetResultStatusName(int status) + { + return status switch + { + 0 => "计算中", + 10 => "待复核", + 20 => "已确认", + 30 => "已投屏", + 40 => "已更正", + 90 => "已作废", + _ => "未知" + }; + } + + private static string GetSheetStatusName(int status) + { + return status switch + { + 0 => "草稿", + 10 => "已提交", + 20 => "已确认", + 30 => "已被修正", + 90 => "已作废", + _ => "未知" + }; + } + + private static string GetCorrectionStatusName(int status) + { + return status switch + { + 0 => "已申请", + 10 => "已批准", + 20 => "已拒绝", + 30 => "裁判修改中", + 40 => "已完成", + 90 => "已取消", + _ => "未知" + }; + } + + private static string GetJudgeRoleTypeName(string roleType) + { + return roleType switch + { + "DP" => "难度裁判(DP)", + "DM" => "难度裁判(DM)", + "DR" => "难度裁判(DR)", + "P" => "表现裁判", + "T" => "主裁", + _ => roleType + }; + } + + #endregion +}