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
+}