1423 lines
58 KiB
C#
1423 lines
58 KiB
C#
using YD_Event.Application.Auth;
|
||
using YD_Event.Application.Workbench.Dtos;
|
||
using YD_Event.Core.Entities;
|
||
using Newtonsoft.Json.Linq;
|
||
|
||
namespace YD_Event.Application.Workbench;
|
||
|
||
/// <summary>
|
||
/// 打分系统工作台接口(供工作台管理端调用)
|
||
/// </summary>
|
||
[ApiDescriptionSettings("打分系统")]
|
||
[Authorize]
|
||
public class WorkbenchController : IDynamicApiController
|
||
{
|
||
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
|
||
private readonly ISqlSugarRepository<EventVenue> _venueRepository;
|
||
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
|
||
private readonly ISqlSugarRepository<EventProjectGroup> _groupRepository;
|
||
private readonly ISqlSugarRepository<EventSession> _sessionRepository;
|
||
private readonly ISqlSugarRepository<EventSessionEntry> _entryRepository;
|
||
private readonly ISqlSugarRepository<EventJudgeAssignment> _assignmentRepository;
|
||
private readonly ISqlSugarRepository<EventJudgeScoreSheet> _sheetRepository;
|
||
private readonly ISqlSugarRepository<EventScoreCorrection> _correctionRepository;
|
||
private readonly ISqlSugarRepository<EventResult> _resultRepository;
|
||
private readonly ISqlSugarRepository<EventResultVersion> _resultVersionRepository;
|
||
private readonly ISqlSugarRepository<EventResultScreenCast> _screenCastRepository;
|
||
private readonly ISqlSugarRepository<EventJudgeTask> _taskRepository;
|
||
private readonly ISqlSugarRepository<EventJudgeMessage> _messageRepository;
|
||
private readonly ISqlSugarRepository<Core.Entities.AppUser> _userRepository;
|
||
private readonly ISqlSugarRepository<Core.Entities.Referee> _refereeRepository;
|
||
private readonly ISqlSugarRepository<Core.Entities.Project> _projectRepository;
|
||
private readonly ISqlSugarRepository<EventJudgeScoreDetail> _scoreDetailRepository;
|
||
private readonly ISqlSugarRepository<SysOperationLog> _operationLogRepository;
|
||
private readonly ISqlSugarRepository<SysUser> _sysUserRepository;
|
||
private readonly AuthManager _authManager;
|
||
private readonly IIdGenerator _idGenerator;
|
||
|
||
public WorkbenchController(
|
||
ISqlSugarRepository<EventInfo> eventRepository,
|
||
ISqlSugarRepository<EventVenue> venueRepository,
|
||
ISqlSugarRepository<EventProject> eventProjectRepository,
|
||
ISqlSugarRepository<EventProjectGroup> groupRepository,
|
||
ISqlSugarRepository<EventSession> sessionRepository,
|
||
ISqlSugarRepository<EventSessionEntry> entryRepository,
|
||
ISqlSugarRepository<EventJudgeAssignment> assignmentRepository,
|
||
ISqlSugarRepository<EventJudgeScoreSheet> sheetRepository,
|
||
ISqlSugarRepository<EventScoreCorrection> correctionRepository,
|
||
ISqlSugarRepository<EventResult> resultRepository,
|
||
ISqlSugarRepository<EventResultVersion> resultVersionRepository,
|
||
ISqlSugarRepository<EventResultScreenCast> screenCastRepository,
|
||
ISqlSugarRepository<EventJudgeTask> taskRepository,
|
||
ISqlSugarRepository<EventJudgeMessage> messageRepository,
|
||
ISqlSugarRepository<Core.Entities.AppUser> userRepository,
|
||
ISqlSugarRepository<Core.Entities.Referee> refereeRepository,
|
||
ISqlSugarRepository<Core.Entities.Project> projectRepository,
|
||
ISqlSugarRepository<EventJudgeScoreDetail> scoreDetailRepository,
|
||
ISqlSugarRepository<SysOperationLog> operationLogRepository,
|
||
ISqlSugarRepository<SysUser> sysUserRepository,
|
||
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;
|
||
_scoreDetailRepository = scoreDetailRepository;
|
||
_operationLogRepository = operationLogRepository;
|
||
_sysUserRepository = sysUserRepository;
|
||
_authManager = authManager;
|
||
_idGenerator = idGenerator;
|
||
}
|
||
|
||
#region 场次管理
|
||
|
||
/// <summary>
|
||
/// 获取工作台场次列表
|
||
/// </summary>
|
||
[HttpGet("workbench/sessions")]
|
||
public async Task<List<WorkbenchSessionDto>> 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<WorkbenchSessionDto>();
|
||
|
||
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<EventSessionEntry>((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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取场次的出场记录列表
|
||
/// </summary>
|
||
[HttpGet("workbench/sessions/{sessionId}/entries")]
|
||
public async Task<List<WorkbenchEntryDetailDto>> 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<WorkbenchEntryDetailDto>();
|
||
|
||
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.ReviewUserId,
|
||
r.ReviewTime,
|
||
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,
|
||
ReviewUserId = result.ReviewUserId,
|
||
ReviewTime = result.ReviewTime,
|
||
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 成绩投屏
|
||
|
||
/// <summary>
|
||
/// 投出成绩到大屏
|
||
/// </summary>
|
||
[HttpPost("workbench/screencast")]
|
||
public async Task<ResultScreenCastOutput> 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 => new EventResultScreenCast { CastStatus = 20 },
|
||
appendColumnsByDataFilter: true)
|
||
.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<EventSessionEntry>((r, e) => r.SessionEntryId == e.Id)
|
||
.InnerJoin<EventSession>((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<EventProjectGroup>()
|
||
.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 => new EventResultScreenCast { CastStatus = 20 },
|
||
appendColumnsByDataFilter: true)
|
||
.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("无效的投屏类型");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 撤回投屏内容
|
||
/// </summary>
|
||
[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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前投屏内容
|
||
/// </summary>
|
||
[HttpGet("workbench/screencast/current")]
|
||
public async Task<CurrentScreenCastDto?> 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 成绩修正审批
|
||
|
||
/// <summary>
|
||
/// 获取成绩修正申请列表
|
||
/// </summary>
|
||
[HttpGet("workbench/corrections")]
|
||
public async Task<List<ScoreCorrectionListDto>> 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<ScoreCorrectionListDto>();
|
||
|
||
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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取成绩修正申请详情
|
||
/// </summary>
|
||
[HttpGet("workbench/corrections/{correctionId}")]
|
||
public async Task<ScoreCorrectionDetailDto> 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,
|
||
ReviewUserId = result.ReviewUserId,
|
||
ReviewTime = result.ReviewTime
|
||
} : 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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 审批成绩修正申请
|
||
/// </summary>
|
||
[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 成绩管理
|
||
|
||
/// <summary>
|
||
/// 确认成绩
|
||
/// </summary>
|
||
[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.ReviewUserId = currentUserId;
|
||
result.ReviewTime = now;
|
||
result.UpdatedUserId = currentUserId;
|
||
result.UpdatedTime = now;
|
||
|
||
await _resultRepository.UpdateAsync(result);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量确认成绩
|
||
/// </summary>
|
||
[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<EventSessionEntry>((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.ReviewUserId = currentUserId;
|
||
result.ReviewTime = now;
|
||
result.UpdatedUserId = currentUserId;
|
||
result.UpdatedTime = now;
|
||
}
|
||
|
||
await _resultRepository.UpdateRangeAsync(results);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 创建重赛
|
||
/// </summary>
|
||
[HttpPost("workbench/entries/rematch")]
|
||
public async Task<CreateRematchOutput> 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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 作废成绩
|
||
/// </summary>
|
||
[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 数据导出
|
||
|
||
/// <summary>
|
||
/// 导出成绩册(与成绩管理页面筛选条件一致,默认仅导出已确认、未DQ成绩)
|
||
/// </summary>
|
||
[DisplayName("导出成绩册")]
|
||
[HttpGet("workbench/results/export")]
|
||
public async Task<IActionResult> ExportResults([FromQuery] ResultExportQueryInput input)
|
||
{
|
||
var query = _resultRepository.AsQueryable()
|
||
.InnerJoin<EventSessionEntry>((r, e) => r.SessionEntryId == e.Id)
|
||
.LeftJoin<EventSession>((r, e, s) => e.SessionId == s.Id)
|
||
.Where((r, e, s) => !r.DeleteMark && !e.DeleteMark && r.EventId == input.EventId);
|
||
|
||
if (input.ResultStatus.HasValue)
|
||
{
|
||
var resultStatus = input.ResultStatus.Value;
|
||
query = query.Where((r, e, s) => r.ResultStatus == resultStatus);
|
||
}
|
||
if (!input.IncludeDq)
|
||
query = query.Where((r, e, s) => !r.IsDisqualified);
|
||
if (!string.IsNullOrWhiteSpace(input.GroupName))
|
||
{
|
||
var groupName = input.GroupName.Trim();
|
||
query = query.Where((r, e, s) => e.GroupName.Contains(groupName) || r.GroupName.Contains(groupName));
|
||
}
|
||
if (input.EventProjectId.HasValue)
|
||
{
|
||
var eventProjectId = input.EventProjectId.Value;
|
||
query = query.Where((r, e, s) => s.EventProjectId == eventProjectId);
|
||
}
|
||
|
||
var flat = await query
|
||
.OrderBy((r, e, s) => r.Rank)
|
||
.Select((r, e, s) => new
|
||
{
|
||
r.Id,
|
||
r.Rank,
|
||
EntryNo = e.EntryNo,
|
||
Participant = e.ParticipantName,
|
||
Team = e.TeamName,
|
||
GroupName = e.GroupName,
|
||
r.ProjectId,
|
||
r.DifficultyScore,
|
||
r.PerformanceScore,
|
||
r.DeductionScore,
|
||
r.TotalScore,
|
||
r.Score,
|
||
r.IsDisqualified,
|
||
r.ResultStatus,
|
||
r.ResultVersion,
|
||
r.ReviewTime,
|
||
EventProjectId = (long?)s.EventProjectId
|
||
})
|
||
.ToListAsync();
|
||
|
||
if (!flat.Any())
|
||
throw Oops.Bah("暂无数据可导出");
|
||
|
||
var eventProjectIds = flat.Where(x => x.EventProjectId != null).Select(x => x.EventProjectId!.Value).Distinct().ToList();
|
||
var eventProjects = await _eventProjectRepository.AsQueryable()
|
||
.Where(x => eventProjectIds.Contains(x.Id))
|
||
.Select(x => new { x.Id, x.ProjectId })
|
||
.ToListAsync();
|
||
var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
|
||
var projectNames = (await _projectRepository.AsQueryable()
|
||
.Where(x => projectIds.Contains(x.Id))
|
||
.Select(x => new { x.Id, x.Name })
|
||
.ToListAsync())
|
||
.ToDictionary(x => x.Id, x => x.Name);
|
||
var eventProjectProject = eventProjects.ToDictionary(x => x.Id, x => x.ProjectId);
|
||
|
||
var rows = flat.Select(x =>
|
||
{
|
||
long? projectId = x.EventProjectId != null && eventProjectProject.TryGetValue(x.EventProjectId.Value, out var pid)
|
||
? pid
|
||
: x.ProjectId > 0 ? (long?)x.ProjectId : null;
|
||
var projectName = projectId != null && projectNames.TryGetValue(projectId.Value, out var pn) ? pn : string.Empty;
|
||
|
||
return new object?[]
|
||
{
|
||
x.IsDisqualified ? "DQ" : (x.Rank > 0 ? x.Rank.ToString() : string.Empty),
|
||
x.EntryNo,
|
||
string.IsNullOrWhiteSpace(x.Team) ? x.Participant : x.Team,
|
||
projectName,
|
||
x.GroupName,
|
||
Num(x.DifficultyScore),
|
||
Num(x.PerformanceScore),
|
||
Num(x.DeductionScore),
|
||
x.TotalScore.HasValue ? Num(x.TotalScore.Value) : x.Score,
|
||
x.IsDisqualified ? "取消资格" : GetResultStatusName(x.ResultStatus),
|
||
x.ResultVersion,
|
||
FmtTime(x.ReviewTime)
|
||
};
|
||
}).ToList();
|
||
|
||
var headers = new[] { "名次", "出场编号", "姓名/队名", "项目", "组别", "难度分", "表现分", "扣分", "总分", "状态", "成绩版本", "确认时间" };
|
||
var bytes = await new WorkbenchListExcelExporter("成绩册", $"成绩册(导出时间:{DateTime.Now:yyyy-MM-dd HH:mm})", headers, rows,
|
||
new double[] { 8, 16, 16, 16, 12, 10, 10, 10, 10, 10, 10, 20 }).ExportAsync();
|
||
return ExcelFile(bytes, $"成绩册_{input.EventId}_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导出评分单明细(按场次/项目,逐出场逐裁判列出评分项与提交信息)
|
||
/// </summary>
|
||
[DisplayName("导出评分单明细")]
|
||
[HttpGet("workbench/score-sheets/export")]
|
||
public async Task<IActionResult> ExportScoreSheets([FromQuery] ScoreSheetExportQueryInput input)
|
||
{
|
||
if (!input.SessionId.HasValue && !input.EventProjectId.HasValue)
|
||
throw Oops.Bah("场次ID与赛事项目ID至少传一个");
|
||
|
||
var query = _sheetRepository.AsQueryable()
|
||
.Where(s => !s.DeleteMark && s.EventId == input.EventId && s.IsCurrent);
|
||
|
||
if (input.SessionId.HasValue)
|
||
{
|
||
var sessionId = input.SessionId.Value;
|
||
query = query.Where(s => s.SessionId == sessionId);
|
||
}
|
||
if (input.EventProjectId.HasValue)
|
||
{
|
||
var eventProjectId = input.EventProjectId.Value;
|
||
var projectSessionIds = await _sessionRepository.AsQueryable()
|
||
.Where(s => !s.DeleteMark && s.EventProjectId == eventProjectId)
|
||
.Select(s => s.Id)
|
||
.ToListAsync();
|
||
query = query.Where(s => projectSessionIds.Contains(s.SessionId));
|
||
}
|
||
|
||
var sheets = await query
|
||
.OrderBy(s => s.SessionId)
|
||
.OrderBy(s => s.SessionEntryId)
|
||
.OrderBy(s => s.JudgeRole)
|
||
.ToListAsync();
|
||
|
||
if (!sheets.Any())
|
||
throw Oops.Bah("暂无数据可导出");
|
||
|
||
var entryIds = sheets.Select(s => s.SessionEntryId).Distinct().ToList();
|
||
var sessionIds = sheets.Select(s => s.SessionId).Distinct().ToList();
|
||
var refereeIds = sheets.Select(s => s.RefereeId).Distinct().ToList();
|
||
var sheetIds = sheets.Select(s => s.Id).ToList();
|
||
|
||
var entries = (await _entryRepository.AsQueryable()
|
||
.Where(e => entryIds.Contains(e.Id))
|
||
.Select(e => new { e.Id, e.EntryNo, e.ParticipantName, e.TeamName })
|
||
.ToListAsync())
|
||
.ToDictionary(e => e.Id);
|
||
var sessions = (await _sessionRepository.AsQueryable()
|
||
.Where(s => sessionIds.Contains(s.Id))
|
||
.Select(s => new { s.Id, s.SessionNo })
|
||
.ToListAsync())
|
||
.ToDictionary(s => s.Id);
|
||
var referees = (await _refereeRepository.AsQueryable()
|
||
.Where(r => refereeIds.Contains(r.Id))
|
||
.Select(r => new { r.Id, r.Name })
|
||
.ToListAsync())
|
||
.ToDictionary(r => r.Id);
|
||
var details = (await _scoreDetailRepository.AsQueryable()
|
||
.Where(d => !d.DeleteMark && sheetIds.Contains(d.ScoreSheetId))
|
||
.OrderBy(d => d.Sort)
|
||
.ToListAsync())
|
||
.GroupBy(d => d.ScoreSheetId)
|
||
.ToDictionary(g => g.Key, g => g.ToList());
|
||
|
||
var rows = new List<object?[]>();
|
||
foreach (var sheet in sheets)
|
||
{
|
||
entries.TryGetValue(sheet.SessionEntryId, out var entry);
|
||
sessions.TryGetValue(sheet.SessionId, out var session);
|
||
referees.TryGetValue(sheet.RefereeId, out var referee);
|
||
details.TryGetValue(sheet.Id, out var sheetDetails);
|
||
|
||
var detailText = sheetDetails == null
|
||
? string.Empty
|
||
: string.Join("\n", sheetDetails.Select(d => $"{d.ItemName}:{FmtDec(d.InputValue ?? d.CountValue ?? d.Subtotal)}"));
|
||
|
||
rows.Add(new object?[]
|
||
{
|
||
session?.SessionNo ?? string.Empty,
|
||
entry?.EntryNo ?? string.Empty,
|
||
string.IsNullOrWhiteSpace(entry?.TeamName) ? entry?.ParticipantName : entry.TeamName,
|
||
referee?.Name ?? string.Empty,
|
||
GetJudgeRoleTypeName(sheet.JudgeRoleType),
|
||
detailText,
|
||
Num(sheet.DifficultyScore),
|
||
Num(sheet.PerformanceScore),
|
||
Num(sheet.DeductionScore),
|
||
Num(sheet.TotalScore),
|
||
sheet.IsDisqualified ? "DQ" : string.Empty,
|
||
sheet.SheetVersion,
|
||
sheet.RuleVersion,
|
||
sheet.OriginalSheetId.HasValue ? sheet.OriginalSheetId.Value.ToString() : string.Empty,
|
||
FmtTime(sheet.SubmittedTime)
|
||
});
|
||
}
|
||
|
||
var headers = new[] { "场次编号", "出场编号", "姓名/队名", "裁判", "裁判角色", "评分项明细", "难度分", "表现分", "扣分", "合计", "DQ", "评分单版本", "规则版本", "原评分单", "提交时间" };
|
||
var bytes = await new WorkbenchListExcelExporter("评分单", $"评分单明细(导出时间:{DateTime.Now:yyyy-MM-dd HH:mm})", headers, rows,
|
||
new double[] { 20, 16, 16, 12, 14, 42, 10, 10, 10, 10, 8, 10, 10, 18, 20 }).ExportAsync();
|
||
return ExcelFile(bytes, $"评分单_{input.EventId}_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
|
||
}
|
||
|
||
/// <summary>
|
||
/// 导出操作审计日志(按赛事ID匹配请求参数,支持时间范围、操作人筛选,最多导出10000条)
|
||
/// </summary>
|
||
[DisplayName("导出操作审计")]
|
||
[HttpGet("workbench/audit/export")]
|
||
public async Task<IActionResult> ExportAudit([FromQuery] AuditExportQueryInput input)
|
||
{
|
||
var eventText = input.EventId.ToString();
|
||
// 日志表没有结构化 EventId,只先用文本缩小候选集,随后解析 JSON 做精确参数匹配。
|
||
var query = _operationLogRepository.AsQueryable()
|
||
.Where(l => l.Parameter != null && l.Parameter.Contains(eventText));
|
||
|
||
if (input.StartTime.HasValue)
|
||
{
|
||
var startTime = input.StartTime.Value;
|
||
query = query.Where(l => l.CreatedTime >= startTime);
|
||
}
|
||
if (input.EndTime.HasValue)
|
||
{
|
||
var endTime = input.EndTime.Value.Date.AddDays(1);
|
||
query = query.Where(l => l.CreatedTime < endTime);
|
||
}
|
||
if (input.OperatorId.HasValue)
|
||
{
|
||
var operatorId = input.OperatorId.Value;
|
||
query = query.Where(l => l.UserId == operatorId);
|
||
}
|
||
if (input.SessionId.HasValue)
|
||
{
|
||
var sessionText = input.SessionId.Value.ToString();
|
||
query = query.Where(l => l.Parameter != null && l.Parameter.Contains(sessionText));
|
||
}
|
||
|
||
var candidates = await query.OrderByDescending(l => l.CreatedTime).Take(50000).ToListAsync();
|
||
var logs = candidates
|
||
.Where(l => HasNumericParameter(l.Parameter, "EventId", input.EventId))
|
||
.Where(l => !input.SessionId.HasValue || HasNumericParameter(l.Parameter, "SessionId", input.SessionId.Value))
|
||
.Take(10000)
|
||
.ToList();
|
||
if (!logs.Any())
|
||
throw Oops.Bah("暂无数据可导出");
|
||
|
||
var userIds = logs.Where(l => l.UserId.HasValue).Select(l => l.UserId!.Value).Distinct().ToList();
|
||
var userNames = (await _sysUserRepository.AsQueryable()
|
||
.Where(u => userIds.Contains(u.Id))
|
||
.Select(u => new { u.Id, u.Name })
|
||
.ToListAsync())
|
||
.ToDictionary(u => u.Id, u => u.Name);
|
||
|
||
var rows = logs.Select(l => new object?[]
|
||
{
|
||
FmtTime(l.CreatedTime),
|
||
l.UserId.HasValue && userNames.TryGetValue(l.UserId.Value, out var name) ? name : l.UserId?.ToString() ?? string.Empty,
|
||
$"{l.ControllerName}/{l.ActionName}",
|
||
l.Description ?? string.Empty,
|
||
TrimLog(l.Parameter),
|
||
l.HttpStatusCode,
|
||
l.RemoteIp
|
||
}).ToList();
|
||
|
||
var headers = new[] { "操作时间", "操作人", "操作对象", "动作", "请求参数", "结果码", "IP" };
|
||
var bytes = await new WorkbenchListExcelExporter("操作审计", $"操作审计(导出时间:{DateTime.Now:yyyy-MM-dd HH:mm})", headers, rows,
|
||
new double[] { 20, 14, 28, 22, 60, 10, 16 }).ExportAsync();
|
||
return ExcelFile(bytes, $"操作审计_{input.EventId}_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
|
||
}
|
||
|
||
private static decimal Num(decimal value) => Math.Round(value, 3);
|
||
|
||
private static string FmtDec(decimal? value) => value.HasValue ? value.Value.ToString("0.##") : string.Empty;
|
||
|
||
private static string FmtTime(DateTime? time) => time.HasValue ? time.Value.ToString("yyyy-MM-dd HH:mm:ss") : string.Empty;
|
||
|
||
private static string TrimLog(string? text)
|
||
{
|
||
if (string.IsNullOrEmpty(text))
|
||
return string.Empty;
|
||
return text.Length > 2000 ? text[..2000] : text;
|
||
}
|
||
|
||
private static bool HasNumericParameter(string? json, string parameterName, long expectedValue)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(json))
|
||
return false;
|
||
|
||
try
|
||
{
|
||
var token = JToken.Parse(json);
|
||
if (token is not JContainer container)
|
||
return false;
|
||
|
||
return container.Descendants()
|
||
.OfType<JProperty>()
|
||
.Any(property => property.Name.Equals(parameterName, StringComparison.OrdinalIgnoreCase) &&
|
||
long.TryParse(property.Value.ToString(), out var value) &&
|
||
value == expectedValue);
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
|
||
private static IActionResult ExcelFile(byte[] bytes, string fileName) =>
|
||
new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||
{
|
||
FileDownloadName = fileName
|
||
};
|
||
|
||
#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
|
||
}
|