马梓彭 99a1857088 完善二期接口
补齐接口文档和上线SQL
优化底层保存逻辑
2026-08-30 21:46:05 +08:00

1423 lines
58 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}