2026-09-29 17:19:03 +08:00
|
|
|
|
using YD_Event.Application.Auth;
|
2026-09-08 16:27:27 +08:00
|
|
|
|
using YD_Event.Application.Scoring;
|
2026-09-16 10:39:11 +08:00
|
|
|
|
using YD_Event.Application.Scoring.Dtos;
|
2026-08-27 16:09:05 +08:00
|
|
|
|
using YD_Event.Application.Workbench.Dtos;
|
|
|
|
|
|
using YD_Event.Core.Entities;
|
2026-08-30 21:46:05 +08:00
|
|
|
|
using Newtonsoft.Json.Linq;
|
2026-09-23 10:34:34 +08:00
|
|
|
|
using System.Text.RegularExpressions;
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
namespace YD_Event.Application.Workbench;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 打分系统工作台接口(供工作台管理端调用)
|
|
|
|
|
|
/// </summary>
|
2026-08-30 21:46:05 +08:00
|
|
|
|
[ApiDescriptionSettings("打分系统")]
|
2026-09-08 16:27:27 +08:00
|
|
|
|
[Route("api/workbench")]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
[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;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
private readonly ISqlSugarRepository<Core.Entities.ScoreRule> _scoreRuleRepository;
|
2026-08-30 21:46:05 +08:00
|
|
|
|
private readonly ISqlSugarRepository<EventJudgeScoreDetail> _scoreDetailRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<SysOperationLog> _operationLogRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<SysUser> _sysUserRepository;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
private readonly ISqlSugarRepository<MerchantCertification> _merchantCertificationRepository;
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
ISqlSugarRepository<Core.Entities.ScoreRule> scoreRuleRepository,
|
2026-08-30 21:46:05 +08:00
|
|
|
|
ISqlSugarRepository<EventJudgeScoreDetail> scoreDetailRepository,
|
|
|
|
|
|
ISqlSugarRepository<SysOperationLog> operationLogRepository,
|
|
|
|
|
|
ISqlSugarRepository<SysUser> sysUserRepository,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
ISqlSugarRepository<MerchantCertification> merchantCertificationRepository,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
_scoreRuleRepository = scoreRuleRepository;
|
2026-08-30 21:46:05 +08:00
|
|
|
|
_scoreDetailRepository = scoreDetailRepository;
|
|
|
|
|
|
_operationLogRepository = operationLogRepository;
|
|
|
|
|
|
_sysUserRepository = sysUserRepository;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
_merchantCertificationRepository = merchantCertificationRepository;
|
2026-08-27 16:09:05 +08:00
|
|
|
|
_authManager = authManager;
|
|
|
|
|
|
_idGenerator = idGenerator;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#region 场次管理
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取可进入打分工作台的赛事列表
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpGet("events")]
|
2026-09-04 17:15:03 +08:00
|
|
|
|
public async Task<PageResult<WorkbenchEventDto>> GetEvents([FromQuery] WorkbenchEventQueryInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mode = input.SourceMode?.Trim().ToLowerInvariant();
|
|
|
|
|
|
var query = _eventRepository.AsQueryable()
|
|
|
|
|
|
.Where(e => !e.DeleteMark)
|
|
|
|
|
|
.WhereIF(!string.IsNullOrWhiteSpace(input.EventName), e => e.EventName.Contains(input.EventName!.Trim()));
|
|
|
|
|
|
|
|
|
|
|
|
// 报名平台入口只展示进行中/已结束的赛事;秩序册入口不限制赛事状态
|
|
|
|
|
|
if (mode == "registration")
|
|
|
|
|
|
query = query.Where(e => e.EventStatus == Core.Enum.EventStatus.Ongoing || e.EventStatus == Core.Enum.EventStatus.Ended);
|
|
|
|
|
|
else if (mode != "orderbook")
|
|
|
|
|
|
throw Oops.Bah("数据来源只能为registration或orderbook");
|
|
|
|
|
|
|
|
|
|
|
|
return await query.OrderByDescending(e => e.MatchStartTime)
|
|
|
|
|
|
.Select(e => new WorkbenchEventDto
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = e.Id,
|
|
|
|
|
|
EventName = e.EventName,
|
|
|
|
|
|
EventStatus = (int)e.EventStatus,
|
|
|
|
|
|
MatchStartTime = e.MatchStartTime,
|
|
|
|
|
|
MatchEndTime = e.MatchEndTime
|
|
|
|
|
|
}).ToPagedListAsync(input);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取工作台场次列表
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpGet("sessions")]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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
|
|
|
|
|
|
.Select(s => new
|
|
|
|
|
|
{
|
|
|
|
|
|
s.Id,
|
|
|
|
|
|
s.SessionNo,
|
|
|
|
|
|
s.SessionName,
|
|
|
|
|
|
s.EventId,
|
|
|
|
|
|
s.VenueId,
|
|
|
|
|
|
s.EventProjectId,
|
|
|
|
|
|
s.EventProjectGroupId,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
s.ScoreRuleId,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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))
|
2026-09-17 16:27:30 +08:00
|
|
|
|
.Select(p => new { p.Id, p.Name, p.SortIndex })
|
2026-08-27 16:09:05 +08:00
|
|
|
|
.ToListAsync())
|
2026-09-17 16:27:30 +08:00
|
|
|
|
.ToDictionary(p => p.Id);
|
|
|
|
|
|
|
|
|
|
|
|
var eventProjectDict = eventProjects.ToDictionary(ep => ep.Id);
|
|
|
|
|
|
sessions = sessions
|
2026-09-23 10:34:34 +08:00
|
|
|
|
// 场次号末段保存了秩序册预览中的全局编排序号。先按该序号排序,
|
|
|
|
|
|
// 前端再按项目合并时,项目顺序便与它在秩序册中首次出现的顺序一致。
|
|
|
|
|
|
.OrderBy(s => GetArrangementSequence(s.SessionNo))
|
|
|
|
|
|
.ThenBy(s => eventProjectDict.TryGetValue(s.EventProjectId, out var eventProject)
|
2026-09-17 16:27:30 +08:00
|
|
|
|
&& projectDict.TryGetValue(eventProject.ProjectId, out var project)
|
|
|
|
|
|
? project.SortIndex
|
|
|
|
|
|
: int.MaxValue)
|
|
|
|
|
|
.ThenBy(s => eventProjectDict.TryGetValue(s.EventProjectId, out var eventProject)
|
|
|
|
|
|
? eventProject.ProjectId
|
|
|
|
|
|
: long.MaxValue)
|
|
|
|
|
|
.ThenBy(s => s.StartTime ?? DateTime.MaxValue)
|
|
|
|
|
|
.ThenBy(s => s.SessionNo)
|
|
|
|
|
|
.ToList();
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
var groups = await _groupRepository.AsQueryable()
|
|
|
|
|
|
.Where(g => groupIds.Contains(g.Id))
|
|
|
|
|
|
.Select(g => new { g.Id, g.GroupName })
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 批量查出场次绑定的计分规则编码,供前端按规则渲染评分维度
|
|
|
|
|
|
var ruleIds = sessions.Select(s => s.ScoreRuleId).Distinct().ToList();
|
|
|
|
|
|
var ruleDict = (await _scoreRuleRepository.AsQueryable()
|
|
|
|
|
|
.Where(r => !r.DeleteMark && ruleIds.Contains(r.Id))
|
|
|
|
|
|
.Select(r => new { r.Id, r.RuleCode })
|
|
|
|
|
|
.ToListAsync()).ToDictionary(r => r.Id, r => r.RuleCode);
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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)
|
2026-09-04 17:15:03 +08:00
|
|
|
|
.Where((r, e) => !r.DeleteMark && !e.DeleteMark && sessionIds.Contains(e.SessionId) &&
|
|
|
|
|
|
// 已确认(20)、已投屏(30)、已更正(40)均计入已完成成绩数
|
|
|
|
|
|
(r.ResultStatus == 20 || r.ResultStatus == 30 || r.ResultStatus == 40))
|
2026-08-27 16:09:05 +08:00
|
|
|
|
.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);
|
2026-09-17 16:27:30 +08:00
|
|
|
|
var eventProjectNameDict = eventProjects.ToDictionary(ep => ep.Id,
|
|
|
|
|
|
ep => projectDict.GetValueOrDefault(ep.ProjectId)?.Name ?? "");
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
EventId = s.EventId,
|
|
|
|
|
|
VenueId = s.VenueId,
|
|
|
|
|
|
EventProjectId = s.EventProjectId,
|
|
|
|
|
|
EventProjectGroupId = s.EventProjectGroupId,
|
|
|
|
|
|
ScoreRuleCode = ruleDict.GetValueOrDefault(s.ScoreRuleId, string.Empty),
|
2026-08-27 16:09:05 +08:00
|
|
|
|
SessionNo = s.SessionNo,
|
|
|
|
|
|
SessionName = s.SessionName,
|
|
|
|
|
|
EventName = eventDict.GetValueOrDefault(s.EventId, ""),
|
2026-09-17 16:27:30 +08:00
|
|
|
|
ProjectName = eventProjectNameDict.GetValueOrDefault(s.EventProjectId, ""),
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpGet("sessions/{sessionId}/entries")]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
public async Task<List<WorkbenchEntryDetailDto>> GetSessionEntries(long sessionId, [FromQuery] WorkbenchEntryQueryInput input)
|
|
|
|
|
|
{
|
2026-09-08 16:27:27 +08:00
|
|
|
|
var session = await _sessionRepository.GetByIdAsync(sessionId)
|
|
|
|
|
|
?? throw Oops.Bah("场次不存在");
|
|
|
|
|
|
var scoreRule = await _scoreRuleRepository.GetByIdAsync(session.ScoreRuleId);
|
|
|
|
|
|
var roleSlots = JudgeRoleSchemaService.ParseSlots(scoreRule?.JudgeRoleSchemaJson);
|
2026-09-23 15:18:13 +08:00
|
|
|
|
var venue = await _venueRepository.GetByIdAsync(session.VenueId);
|
|
|
|
|
|
var venueOrder = venue?.Sort > 0 ? venue.Sort : 1;
|
|
|
|
|
|
var sessionArrangementOrder = GetArrangementSequence(session.SessionNo);
|
2026-09-08 16:27:27 +08:00
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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,
|
2026-09-23 15:18:13 +08:00
|
|
|
|
e.SourceType,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
r.CalculationJson,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
r.TotalScore,
|
|
|
|
|
|
r.Score,
|
|
|
|
|
|
r.IsDisqualified,
|
|
|
|
|
|
r.DisqualifyReasonCode,
|
|
|
|
|
|
r.ResultStatus,
|
|
|
|
|
|
r.ResultVersion,
|
2026-08-30 21:46:05 +08:00
|
|
|
|
r.ReviewUserId,
|
|
|
|
|
|
r.ReviewTime,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
r.Rank,
|
|
|
|
|
|
r.MerchantCertificationId
|
2026-08-27 16:09:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 批量查询成绩关联的参赛单位名称(单位信息存于商户认证表)
|
|
|
|
|
|
var merchantIds = results.Where(r => r.MerchantCertificationId.HasValue)
|
|
|
|
|
|
.Select(r => r.MerchantCertificationId!.Value).Distinct().ToList();
|
|
|
|
|
|
var merchantNames = (await _merchantCertificationRepository.AsQueryable()
|
|
|
|
|
|
.Where(m => merchantIds.Contains(m.Id) && !m.DeleteMark)
|
|
|
|
|
|
.Select(m => new { m.Id, m.MerchantName }).ToListAsync())
|
|
|
|
|
|
.ToDictionary(m => m.Id, m => m.MerchantName);
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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();
|
|
|
|
|
|
|
2026-09-23 10:34:34 +08:00
|
|
|
|
// 表头必须以场次裁判分配为准,不能等评分单生成后才出现裁判列。
|
|
|
|
|
|
var assignments = await _assignmentRepository.AsQueryable()
|
|
|
|
|
|
.Where(a => !a.DeleteMark && a.SessionId == sessionId && a.AssignmentStatus < 9 &&
|
|
|
|
|
|
a.Status == Core.Enum.AvailabilityStatus.Enable)
|
|
|
|
|
|
.OrderBy(a => a.JudgeRole)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 批量查询当前评分单的动态评分项明细,按评分单分组挂到各裁判评分下
|
|
|
|
|
|
var sheetIds = sheets.Select(s => s.Id).ToList();
|
|
|
|
|
|
var scoreItems = await _scoreDetailRepository.AsQueryable()
|
|
|
|
|
|
.Where(d => !d.DeleteMark && sheetIds.Contains(d.ScoreSheetId))
|
|
|
|
|
|
.OrderBy(d => d.Sort)
|
|
|
|
|
|
.Select(d => new
|
|
|
|
|
|
{
|
|
|
|
|
|
d.ScoreSheetId,
|
|
|
|
|
|
d.ItemCode,
|
|
|
|
|
|
d.ItemName,
|
|
|
|
|
|
d.SubjectCode,
|
|
|
|
|
|
d.ItemType,
|
|
|
|
|
|
d.ScoreDimension,
|
|
|
|
|
|
d.CountValue,
|
|
|
|
|
|
d.InputValue,
|
|
|
|
|
|
d.Subtotal,
|
|
|
|
|
|
d.Sort
|
|
|
|
|
|
}).ToListAsync();
|
|
|
|
|
|
|
2026-09-23 10:34:34 +08:00
|
|
|
|
var refereeIds = assignments.Select(a => a.RefereeId)
|
|
|
|
|
|
.Concat(sheets.Select(s => s.RefereeId)).Distinct().ToList();
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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();
|
2026-09-16 15:20:14 +08:00
|
|
|
|
// 集体项目为每名队员留存同一出场成绩,记分台按出场展示一行。
|
|
|
|
|
|
// 稳定选取一个代表记录,避免同队多名队员导致重复字典键。
|
|
|
|
|
|
var resultDict = results.GroupBy(r => r.SessionEntryId!.Value)
|
|
|
|
|
|
.ToDictionary(g => g.Key, g => g.OrderByDescending(r => r.ResultVersion)
|
|
|
|
|
|
.ThenBy(r => r.Id).First());
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
2026-09-23 10:34:34 +08:00
|
|
|
|
WorkbenchJudgeScoreDto MapSheet(EventJudgeScoreSheet sheet) => new()
|
|
|
|
|
|
{
|
|
|
|
|
|
ScoreSheetId = sheet.Id,
|
|
|
|
|
|
JudgeRole = sheet.JudgeRole,
|
|
|
|
|
|
JudgeRoleType = sheet.JudgeRoleType,
|
|
|
|
|
|
JudgeRoleName = JudgeRoleSchemaService.RoleNameOf(roleSlots, 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),
|
|
|
|
|
|
ScoreItems = scoreItems.Where(d => d.ScoreSheetId == sheet.Id).Select(d => new WorkbenchScoreItemDto
|
|
|
|
|
|
{
|
|
|
|
|
|
ItemCode = d.ItemCode,
|
|
|
|
|
|
ItemName = d.ItemName,
|
|
|
|
|
|
SubjectCode = d.SubjectCode,
|
|
|
|
|
|
ItemType = d.ItemType,
|
|
|
|
|
|
ScoreDimension = d.ScoreDimension,
|
|
|
|
|
|
CountValue = d.CountValue,
|
|
|
|
|
|
InputValue = d.InputValue,
|
|
|
|
|
|
Subtotal = d.Subtotal,
|
|
|
|
|
|
Sort = d.Sort
|
|
|
|
|
|
}).ToList()
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
return entries.Select(e =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var result = resultDict.GetValueOrDefault(e.Id);
|
|
|
|
|
|
var entrySheets = sheets.Where(s => s.SessionEntryId == e.Id).ToList();
|
|
|
|
|
|
|
2026-09-23 10:34:34 +08:00
|
|
|
|
var assignedScores = assignments.Select(assignment =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var sheet = entrySheets.FirstOrDefault(s => s.RefereeId == assignment.RefereeId &&
|
|
|
|
|
|
s.JudgeRole == assignment.JudgeRole);
|
|
|
|
|
|
return sheet != null ? MapSheet(sheet) : new WorkbenchJudgeScoreDto
|
|
|
|
|
|
{
|
|
|
|
|
|
ScoreSheetId = 0,
|
|
|
|
|
|
JudgeRole = assignment.JudgeRole,
|
|
|
|
|
|
JudgeRoleType = assignment.JudgeRoleType ?? string.Empty,
|
|
|
|
|
|
JudgeRoleName = JudgeRoleSchemaService.RoleNameOf(roleSlots, assignment.JudgeRoleType ?? string.Empty),
|
|
|
|
|
|
JudgeName = refereeMap.GetValueOrDefault(assignment.RefereeId, ""),
|
|
|
|
|
|
SheetStatus = 0,
|
|
|
|
|
|
SheetStatusName = "待提交"
|
|
|
|
|
|
};
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
var assignmentKeys = assignments.Select(a => $"{a.RefereeId}:{a.JudgeRole}").ToHashSet();
|
|
|
|
|
|
assignedScores.AddRange(entrySheets
|
|
|
|
|
|
.Where(sheet => !assignmentKeys.Contains($"{sheet.RefereeId}:{sheet.JudgeRole}"))
|
|
|
|
|
|
.Select(MapSheet));
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
return new WorkbenchEntryDetailDto
|
|
|
|
|
|
{
|
|
|
|
|
|
SessionEntryId = e.Id,
|
|
|
|
|
|
EntryNo = e.EntryNo,
|
|
|
|
|
|
PlayOrder = e.PlayOrder,
|
2026-09-23 15:18:13 +08:00
|
|
|
|
SessionOrder = BuildEntrySessionOrder(sessionArrangementOrder, e.PlayOrder),
|
|
|
|
|
|
// 导入数据按预览行生成雪花 ID;自动编排数据按场次起始序号、出场序号和场地序号还原。
|
|
|
|
|
|
ScheduleOrder = e.SourceType == 1
|
|
|
|
|
|
? e.Id
|
|
|
|
|
|
: BuildEntryScheduleOrder(sessionArrangementOrder, venueOrder, e.PlayOrder),
|
2026-08-27 16:09:05 +08:00
|
|
|
|
ParticipantName = e.ParticipantName,
|
|
|
|
|
|
TeamName = e.TeamName,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
UnitName = result?.MerchantCertificationId is long merchantId
|
|
|
|
|
|
? merchantNames.GetValueOrDefault(merchantId)
|
|
|
|
|
|
: null,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
DeductionFactor = ScoreDisplayService.DeductionFactor(result.CalculationJson),
|
|
|
|
|
|
CalculationJson = result.CalculationJson,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
TotalScore = result.TotalScore,
|
|
|
|
|
|
ScoreText = result.Score,
|
|
|
|
|
|
IsDisqualified = result.IsDisqualified,
|
|
|
|
|
|
DisqualifyReason = result.DisqualifyReasonCode,
|
|
|
|
|
|
ResultStatus = result.ResultStatus,
|
|
|
|
|
|
ResultStatusName = GetResultStatusName(result.ResultStatus),
|
|
|
|
|
|
ResultVersion = result.ResultVersion,
|
2026-08-30 21:46:05 +08:00
|
|
|
|
ReviewUserId = result.ReviewUserId,
|
|
|
|
|
|
ReviewTime = result.ReviewTime,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
Rank = result.Rank
|
|
|
|
|
|
} : null,
|
2026-09-23 10:34:34 +08:00
|
|
|
|
JudgeScores = assignedScores
|
2026-08-27 16:09:05 +08:00
|
|
|
|
};
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-08 16:27:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取某个出场可发起成绩修正的裁判:
|
|
|
|
|
|
/// 裁判类型取该场次项目的计分规则配置,裁判人数取【裁判报名与分配】为本场次分配的裁判。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpGet("entries/{sessionEntryId}/correction-judges")]
|
|
|
|
|
|
public async Task<List<CorrectionJudgeOptionDto>> GetCorrectionJudges(long sessionEntryId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var entry = await _entryRepository.GetFirstAsync(e => e.Id == sessionEntryId && !e.DeleteMark)
|
|
|
|
|
|
?? throw Oops.Bah("出场记录不存在");
|
|
|
|
|
|
|
|
|
|
|
|
var session = await _sessionRepository.GetByIdAsync(entry.SessionId)
|
|
|
|
|
|
?? throw Oops.Bah("场次不存在");
|
|
|
|
|
|
|
|
|
|
|
|
// 裁判类型来自项目绑定的计分规则,保证与裁判分配看板同源
|
|
|
|
|
|
var scoreRule = await _scoreRuleRepository.GetByIdAsync(session.ScoreRuleId);
|
|
|
|
|
|
var roleSlots = JudgeRoleSchemaService.ParseSlots(scoreRule?.JudgeRoleSchemaJson);
|
|
|
|
|
|
|
|
|
|
|
|
// 裁判数量来自该场次的裁判分配(排除已停用的分配)
|
|
|
|
|
|
var assignments = await _assignmentRepository.AsQueryable()
|
|
|
|
|
|
.Where(a => !a.DeleteMark && a.SessionId == session.Id && a.AssignmentStatus < 9 &&
|
|
|
|
|
|
a.Status == Core.Enum.AvailabilityStatus.Enable)
|
|
|
|
|
|
.OrderBy(a => a.JudgeRole)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
if (assignments.Count == 0) return new List<CorrectionJudgeOptionDto>();
|
|
|
|
|
|
|
|
|
|
|
|
var refereeIds = assignments.Select(a => a.RefereeId).Distinct().ToList();
|
|
|
|
|
|
var refereeNames = (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 sheets = await _sheetRepository.AsQueryable()
|
|
|
|
|
|
.Where(s => !s.DeleteMark && s.SessionEntryId == entry.Id && s.IsCurrent)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
var sheetIds = sheets.Select(s => s.Id).ToList();
|
|
|
|
|
|
var pendingSheetIds = (await _correctionRepository.AsQueryable()
|
|
|
|
|
|
.Where(c => !c.DeleteMark && sheetIds.Contains(c.OriginalScoreSheetId) && c.CorrectionStatus < 20)
|
|
|
|
|
|
.Select(c => c.OriginalScoreSheetId).ToListAsync()).ToHashSet();
|
|
|
|
|
|
|
|
|
|
|
|
// 规则里配置的类型排在前,规则中未配置的历史类型排到最后
|
|
|
|
|
|
var slotOrder = roleSlots.Select((slot, index) => (slot.Code, Order: index))
|
|
|
|
|
|
.ToDictionary(x => x.Code, x => x.Order, StringComparer.OrdinalIgnoreCase);
|
|
|
|
|
|
|
|
|
|
|
|
return assignments
|
|
|
|
|
|
.GroupBy(a => JudgeRoleSchemaService.NormalizeRoleType(a.JudgeRoleType ?? a.JudgeRole),
|
|
|
|
|
|
StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
|
.OrderBy(g => slotOrder.GetValueOrDefault(g.Key, int.MaxValue))
|
|
|
|
|
|
.ThenBy(g => g.Key, StringComparer.OrdinalIgnoreCase)
|
|
|
|
|
|
.Select(group =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var roleName = JudgeRoleSchemaService.RoleNameOf(roleSlots, group.Key);
|
|
|
|
|
|
var ordered = group.OrderBy(a => a.JudgeRole, StringComparer.OrdinalIgnoreCase).ToList();
|
|
|
|
|
|
return new CorrectionJudgeOptionDto
|
|
|
|
|
|
{
|
|
|
|
|
|
JudgeRoleType = group.Key,
|
|
|
|
|
|
JudgeRoleName = roleName,
|
|
|
|
|
|
Judges = ordered.Select((assignment, index) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var judgeName = refereeNames.GetValueOrDefault(assignment.RefereeId, "");
|
|
|
|
|
|
var sheet = sheets.FirstOrDefault(s => s.JudgeAssignmentId == assignment.Id);
|
|
|
|
|
|
// 同一类型多名裁判时按席位顺序编号,单人时不加序号
|
|
|
|
|
|
var seatLabel = ordered.Count > 1 ? $"{roleName}{index + 1}" : roleName;
|
|
|
|
|
|
var hasPending = sheet != null && pendingSheetIds.Contains(sheet.Id);
|
|
|
|
|
|
var submitted = sheet != null && sheet.SheetStatus >= 10 && sheet.SheetStatus < 90;
|
|
|
|
|
|
return new CorrectionJudgeItemDto
|
|
|
|
|
|
{
|
|
|
|
|
|
JudgeAssignmentId = assignment.Id,
|
|
|
|
|
|
JudgeRole = assignment.JudgeRole,
|
|
|
|
|
|
RefereeId = assignment.RefereeId,
|
|
|
|
|
|
JudgeName = judgeName,
|
|
|
|
|
|
DisplayName = string.IsNullOrWhiteSpace(judgeName) ? seatLabel : $"{seatLabel}({judgeName})",
|
|
|
|
|
|
ScoreSheetId = sheet?.Id,
|
|
|
|
|
|
SheetStatus = sheet?.SheetStatus,
|
|
|
|
|
|
SheetStatusName = sheet != null ? GetSheetStatusName(sheet.SheetStatus) : null,
|
|
|
|
|
|
TotalScore = sheet?.TotalScore,
|
|
|
|
|
|
CanCorrect = submitted && !hasPending,
|
|
|
|
|
|
DisabledReason = sheet == null ? "该裁判尚未提交评分"
|
|
|
|
|
|
: hasPending ? "该评分单已有修正流程在处理中"
|
|
|
|
|
|
: !submitted ? $"评分单当前为“{GetSheetStatusName(sheet.SheetStatus)}”,不能修正"
|
|
|
|
|
|
: null
|
|
|
|
|
|
};
|
|
|
|
|
|
}).ToList()
|
|
|
|
|
|
};
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 成绩投屏
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 投出成绩到大屏
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpPost("screencast")]
|
2026-09-04 17:15:03 +08:00
|
|
|
|
[UnitOfWork]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
public async Task<ResultScreenCastOutput> CastResult([FromBody] ResultScreenCastInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentUserId = _authManager.UserId;
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 两种投屏类型都先校验赛事存在且未删除
|
|
|
|
|
|
if (input.EventId <= 0 || !await _eventRepository.IsAnyAsync(e => e.Id == input.EventId && !e.DeleteMark))
|
|
|
|
|
|
throw Oops.Bah("赛事不存在或已删除");
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
if (input.CastType == 1)
|
|
|
|
|
|
{
|
2026-09-04 17:15:03 +08:00
|
|
|
|
if (!input.SessionId.HasValue || !input.SessionEntryId.HasValue || !input.EventResultId.HasValue)
|
|
|
|
|
|
throw Oops.Bah("投出单个成绩时,必须提供场次ID、出场记录ID和成绩ID");
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
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("成绩记录不存在");
|
2026-09-04 17:15:03 +08:00
|
|
|
|
var previousResultStatus = result.ResultStatus;
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
var session = await _sessionRepository.GetByIdAsync(entry.SessionId)
|
|
|
|
|
|
?? throw Oops.Bah("场次不存在");
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 投屏前校验归属关系与成绩状态,防止跨赛事/跨场次或过期版本被投出
|
|
|
|
|
|
if (session.DeleteMark || session.Id != input.SessionId.Value || session.EventId != input.EventId ||
|
|
|
|
|
|
(input.VenueId.HasValue && session.VenueId != input.VenueId.Value))
|
|
|
|
|
|
throw Oops.Bah("赛事、场地、场次和出场记录不匹配");
|
|
|
|
|
|
if (result.EventId != input.EventId || result.SessionId != session.Id || result.SessionEntryId != entry.Id)
|
|
|
|
|
|
throw Oops.Bah("成绩记录与当前赛事出场记录不匹配");
|
|
|
|
|
|
if (result.ResultStatus is not (20 or 30 or 40))
|
|
|
|
|
|
throw Oops.Bah("只有已确认的有效成绩可以投屏");
|
|
|
|
|
|
if (result.IsDisqualified)
|
|
|
|
|
|
throw Oops.Bah("DQ成绩不能投屏");
|
|
|
|
|
|
if (input.ResultVersion.HasValue && input.ResultVersion.Value != result.ResultVersion)
|
|
|
|
|
|
throw Oops.Bah("成绩已产生新版本,请刷新后重新投屏");
|
|
|
|
|
|
|
|
|
|
|
|
// 该出场应到裁判的评分单必须全部提交,否则成绩未定稿,不允许投屏
|
|
|
|
|
|
var expectedJudgeCount = await _assignmentRepository.AsQueryable()
|
|
|
|
|
|
.Where(a => !a.DeleteMark && a.SessionId == session.Id && a.Status == Core.Enum.AvailabilityStatus.Enable)
|
|
|
|
|
|
.CountAsync();
|
|
|
|
|
|
var submittedJudgeCount = await _sheetRepository.AsQueryable()
|
|
|
|
|
|
.Where(s => !s.DeleteMark && s.IsCurrent && s.SessionEntryId == entry.Id &&
|
|
|
|
|
|
s.SheetStatus >= 10 && s.SheetStatus < 90)
|
|
|
|
|
|
.Select(s => s.JudgeAssignmentId).Distinct().CountAsync();
|
|
|
|
|
|
if (expectedJudgeCount <= 0 || submittedJudgeCount < expectedJudgeCount)
|
|
|
|
|
|
throw Oops.Bah("相关裁判评分单尚未全部提交,不能投屏");
|
|
|
|
|
|
|
2026-09-18 17:39:01 +08:00
|
|
|
|
var unitName = string.Empty;
|
|
|
|
|
|
if (result.MerchantCertificationId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var merchant = await _merchantCertificationRepository.GetFirstAsync(m =>
|
|
|
|
|
|
m.Id == result.MerchantCertificationId.Value && !m.DeleteMark);
|
|
|
|
|
|
unitName = merchant?.MerchantName ?? string.Empty;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
var payload = new
|
|
|
|
|
|
{
|
|
|
|
|
|
EntryNo = entry.EntryNo,
|
|
|
|
|
|
ParticipantName = entry.ParticipantName,
|
|
|
|
|
|
TeamName = entry.TeamName,
|
2026-09-18 17:39:01 +08:00
|
|
|
|
UnitName = unitName,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
GroupName = entry.GroupName,
|
|
|
|
|
|
DifficultyScore = result.DifficultyScore,
|
|
|
|
|
|
PerformanceScore = result.PerformanceScore,
|
|
|
|
|
|
RequiredScore = result.RequiredScore,
|
|
|
|
|
|
DeductionScore = result.DeductionScore,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
DeductionFactor = ScoreDisplayService.DeductionFactor(result.CalculationJson),
|
|
|
|
|
|
CalculationJson = result.CalculationJson,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
TotalScore = result.TotalScore,
|
|
|
|
|
|
Score = result.Score,
|
|
|
|
|
|
IsDisqualified = result.IsDisqualified,
|
|
|
|
|
|
Rank = result.Rank
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 同赛事同场地此前投出中的记录统一置为“已被新投屏替换(20)”,保证大屏只展示最新一条
|
2026-09-09 15:50:03 +08:00
|
|
|
|
var activeScreenCastUpdate = _screenCastRepository.AsUpdateable()
|
2026-08-30 21:46:05 +08:00
|
|
|
|
.SetColumns(sc => new EventResultScreenCast { CastStatus = 20 },
|
|
|
|
|
|
appendColumnsByDataFilter: true)
|
2026-09-09 15:50:03 +08:00
|
|
|
|
.Where(sc => sc.EventId == input.EventId && sc.CastStatus == 0);
|
|
|
|
|
|
if (input.VenueId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var venueId = input.VenueId.Value;
|
|
|
|
|
|
activeScreenCastUpdate = activeScreenCastUpdate.Where(sc => sc.VenueId == venueId);
|
|
|
|
|
|
}
|
|
|
|
|
|
await activeScreenCastUpdate.ExecuteCommandAsync();
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 投屏成功后成绩置为“已投屏(30)”,同步当前版本状态并记录审计
|
|
|
|
|
|
result.ResultStatus = 30;
|
|
|
|
|
|
result.UpdatedUserId = currentUserId;
|
|
|
|
|
|
result.UpdatedTime = now;
|
|
|
|
|
|
await _resultRepository.UpdateAsync(result);
|
|
|
|
|
|
await SyncResultVersionStatusAsync(result, 30, currentUserId, now);
|
|
|
|
|
|
await WriteAuditAsync("投出单人成绩", input.EventId, result.Id, previousResultStatus, 30, sessionId: session.Id);
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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");
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 投屏排名前校验项目归属当前赛事、组别归属该项目
|
|
|
|
|
|
var eventProject = await _eventProjectRepository.GetFirstAsync(ep =>
|
|
|
|
|
|
ep.Id == input.EventProjectId.Value && ep.EventId == input.EventId && !ep.DeleteMark)
|
|
|
|
|
|
?? throw Oops.Bah("所选项目不属于当前赛事");
|
|
|
|
|
|
_ = eventProject;
|
2026-09-09 15:37:49 +08:00
|
|
|
|
var eventProjectGroup = await _groupRepository.GetFirstAsync(g =>
|
|
|
|
|
|
g.Id == input.EventProjectGroupId.Value &&
|
|
|
|
|
|
g.EventProjectId == input.EventProjectId.Value && !g.DeleteMark)
|
|
|
|
|
|
?? throw Oops.Bah("所选组别不属于当前赛事项目");
|
|
|
|
|
|
var groupName = eventProjectGroup.GroupName;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
|
2026-09-10 16:28:51 +08:00
|
|
|
|
var rankingQuery = _resultRepository.AsQueryable()
|
2026-08-27 16:09:05 +08:00
|
|
|
|
.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 &&
|
2026-09-09 15:37:49 +08:00
|
|
|
|
(s.EventProjectGroupId == input.EventProjectGroupId.Value ||
|
|
|
|
|
|
e.GroupName == groupName) &&
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 只投屏有效成绩(已确认20/已投屏30/已更正40),排除DQ成绩
|
|
|
|
|
|
(r.ResultStatus == 20 || r.ResultStatus == 30 || r.ResultStatus == 40) &&
|
2026-09-10 16:28:51 +08:00
|
|
|
|
!r.IsDisqualified);
|
|
|
|
|
|
|
|
|
|
|
|
// 排名按大屏对应场地隔离,避免同项目同组别在多个场地比赛时串榜。
|
|
|
|
|
|
// 使用显式分支和值类型局部变量,避免 nullable 条件被 SqlSugar 翻译成非法 SQL。
|
|
|
|
|
|
if (input.VenueId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var venueId = input.VenueId.Value;
|
|
|
|
|
|
rankingQuery = rankingQuery.Where((r, e, s) => s.VenueId == venueId);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var results = await rankingQuery
|
2026-08-27 16:09:05 +08:00
|
|
|
|
.OrderBy((r, e, s) => r.Rank)
|
|
|
|
|
|
.Select((r, e, s) => new
|
|
|
|
|
|
{
|
|
|
|
|
|
r.Id,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
r.ResultVersion,
|
2026-09-16 10:39:11 +08:00
|
|
|
|
SessionEntryId = e.Id,
|
|
|
|
|
|
s.ScoreRuleId,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
e.EntryNo,
|
|
|
|
|
|
e.ParticipantName,
|
|
|
|
|
|
e.TeamName,
|
|
|
|
|
|
e.GroupName,
|
2026-09-16 10:39:11 +08:00
|
|
|
|
r.DifficultyScore,
|
|
|
|
|
|
r.PerformanceScore,
|
|
|
|
|
|
r.RequiredScore,
|
|
|
|
|
|
r.DeductionScore,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
r.CalculationJson,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
r.TotalScore,
|
|
|
|
|
|
r.Score,
|
2026-09-18 17:39:01 +08:00
|
|
|
|
r.IsDisqualified,
|
|
|
|
|
|
r.MerchantCertificationId
|
2026-08-27 16:09:05 +08:00
|
|
|
|
})
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
if (!results.Any())
|
|
|
|
|
|
throw Oops.Bah("当前项目组别没有可投屏的已确认成绩");
|
|
|
|
|
|
|
2026-09-18 17:39:01 +08:00
|
|
|
|
var rankingMerchantIds = results
|
|
|
|
|
|
.Where(r => r.MerchantCertificationId.HasValue)
|
|
|
|
|
|
.Select(r => r.MerchantCertificationId!.Value)
|
|
|
|
|
|
.Distinct()
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
var rankingMerchantNames = (await _merchantCertificationRepository.AsQueryable()
|
|
|
|
|
|
.Where(m => rankingMerchantIds.Contains(m.Id) && !m.DeleteMark)
|
|
|
|
|
|
.Select(m => new { m.Id, m.MerchantName })
|
|
|
|
|
|
.ToListAsync())
|
|
|
|
|
|
.ToDictionary(m => m.Id, m => m.MerchantName);
|
|
|
|
|
|
|
2026-09-16 10:39:11 +08:00
|
|
|
|
// 落库名次可能是成绩刚生成时连同“待复核”成绩一起计算的。
|
|
|
|
|
|
// 投屏时必须以上面已经过状态筛选的有效成绩重新排名,避免待复核成绩隐形占位。
|
|
|
|
|
|
var scoreRuleId = results.Select(r => r.ScoreRuleId).First();
|
|
|
|
|
|
var scoreRule = await _scoreRuleRepository.GetByIdAsync(scoreRuleId);
|
|
|
|
|
|
var rankCandidates = results
|
|
|
|
|
|
.GroupBy(r => r.SessionEntryId)
|
|
|
|
|
|
.Select(group =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var row = group.First();
|
|
|
|
|
|
return new RankCandidate
|
|
|
|
|
|
{
|
|
|
|
|
|
SessionEntryId = group.Key,
|
|
|
|
|
|
TotalScore = row.TotalScore ?? 0m,
|
|
|
|
|
|
DifficultyScore = row.DifficultyScore,
|
|
|
|
|
|
PerformanceScore = row.PerformanceScore,
|
|
|
|
|
|
RequiredScore = row.RequiredScore,
|
|
|
|
|
|
DeductionScore = row.DeductionScore,
|
|
|
|
|
|
IsDisqualified = row.IsDisqualified,
|
|
|
|
|
|
IsRanked = row.TotalScore.HasValue
|
|
|
|
|
|
};
|
|
|
|
|
|
})
|
|
|
|
|
|
.ToList();
|
|
|
|
|
|
EntryResultCalculationService.AssignRanks(rankCandidates,
|
|
|
|
|
|
ScoreRuleConfigService.Parse(scoreRule?.RuleConfigJson));
|
|
|
|
|
|
var screenRankByEntry = rankCandidates.ToDictionary(x => x.SessionEntryId, x => x.Rank);
|
|
|
|
|
|
|
|
|
|
|
|
var payload = results
|
|
|
|
|
|
.OrderBy(r => screenRankByEntry[r.SessionEntryId] == 0
|
|
|
|
|
|
? int.MaxValue
|
|
|
|
|
|
: screenRankByEntry[r.SessionEntryId])
|
|
|
|
|
|
.ThenBy(r => r.EntryNo)
|
|
|
|
|
|
.Select(r => new
|
2026-08-27 16:09:05 +08:00
|
|
|
|
{
|
|
|
|
|
|
r.EntryNo,
|
|
|
|
|
|
r.ParticipantName,
|
|
|
|
|
|
r.TeamName,
|
2026-09-18 17:39:01 +08:00
|
|
|
|
UnitName = r.MerchantCertificationId.HasValue
|
|
|
|
|
|
? rankingMerchantNames.GetValueOrDefault(r.MerchantCertificationId.Value)
|
|
|
|
|
|
: null,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
r.GroupName,
|
|
|
|
|
|
r.TotalScore,
|
|
|
|
|
|
r.Score,
|
2026-09-16 10:39:11 +08:00
|
|
|
|
Rank = screenRankByEntry[r.SessionEntryId],
|
2026-08-27 16:09:05 +08:00
|
|
|
|
r.IsDisqualified
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 同赛事同场地此前投出中的记录统一置为“已被新投屏替换(20)”,保证大屏只展示最新一条
|
2026-09-09 15:50:03 +08:00
|
|
|
|
var activeScreenCastUpdate = _screenCastRepository.AsUpdateable()
|
2026-08-30 21:46:05 +08:00
|
|
|
|
.SetColumns(sc => new EventResultScreenCast { CastStatus = 20 },
|
|
|
|
|
|
appendColumnsByDataFilter: true)
|
2026-09-09 15:50:03 +08:00
|
|
|
|
.Where(sc => sc.EventId == input.EventId && sc.CastStatus == 0);
|
|
|
|
|
|
if (input.VenueId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var venueId = input.VenueId.Value;
|
|
|
|
|
|
activeScreenCastUpdate = activeScreenCastUpdate.Where(sc => sc.VenueId == venueId);
|
|
|
|
|
|
}
|
|
|
|
|
|
await activeScreenCastUpdate.ExecuteCommandAsync();
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 组别排名投屏后,组内全部成绩置为“已投屏(30)”
|
|
|
|
|
|
var rankedResultIds = results.Select(r => r.Id).ToList();
|
|
|
|
|
|
await _resultRepository.AsUpdateable()
|
|
|
|
|
|
.SetColumns(r => new EventResult
|
|
|
|
|
|
{
|
|
|
|
|
|
ResultStatus = 30,
|
|
|
|
|
|
UpdatedUserId = currentUserId,
|
|
|
|
|
|
UpdatedTime = now
|
|
|
|
|
|
}, appendColumnsByDataFilter: true)
|
|
|
|
|
|
.Where(r => rankedResultIds.Contains(r.Id))
|
|
|
|
|
|
.ExecuteCommandAsync();
|
|
|
|
|
|
var rankedVersions = await _resultVersionRepository.AsQueryable()
|
|
|
|
|
|
.Where(v => !v.DeleteMark && rankedResultIds.Contains(v.EventResultId))
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
// 只同步各成绩的当前版本,历史版本保留原状态留痕
|
2026-09-09 15:37:49 +08:00
|
|
|
|
var currentRankedVersionIds = rankedVersions
|
2026-09-04 17:15:03 +08:00
|
|
|
|
.Where(v => results.Any(r => r.Id == v.EventResultId && r.ResultVersion == v.ResultVersion))
|
2026-09-09 15:37:49 +08:00
|
|
|
|
.Select(v => v.Id)
|
2026-09-04 17:15:03 +08:00
|
|
|
|
.ToList();
|
2026-09-09 15:37:49 +08:00
|
|
|
|
if (currentRankedVersionIds.Count > 0)
|
|
|
|
|
|
await _resultVersionRepository.AsUpdateable()
|
|
|
|
|
|
.SetColumns(v => new EventResultVersion
|
|
|
|
|
|
{
|
|
|
|
|
|
ResultStatus = 30,
|
|
|
|
|
|
UpdatedUserId = currentUserId,
|
|
|
|
|
|
UpdatedTime = now
|
|
|
|
|
|
}, appendColumnsByDataFilter: true)
|
|
|
|
|
|
.Where(v => currentRankedVersionIds.Contains(v.Id))
|
|
|
|
|
|
.ExecuteCommandAsync();
|
2026-09-04 17:15:03 +08:00
|
|
|
|
await WriteAuditAsync("投出组别排名", input.EventId, input.EventProjectGroupId.Value, null, 30);
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
return new ResultScreenCastOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
ScreenCastId = screenCast.Id,
|
|
|
|
|
|
CastType = 2,
|
|
|
|
|
|
CastTime = now,
|
|
|
|
|
|
PayloadJson = screenCast.PayloadJson
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
throw Oops.Bah("无效的投屏类型");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 撤回投屏内容
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpPost("screencast/withdraw")]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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);
|
2026-09-04 17:15:03 +08:00
|
|
|
|
await WriteAuditAsync("撤回投屏", screenCast.EventId, screenCast.Id, 0, 10, sessionId: screenCast.SessionId);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取当前投屏内容
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpGet("screencast/current")]
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 大屏展示端无登录态,允许匿名拉取当前投屏内容
|
|
|
|
|
|
[AllowAnonymous]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-09-06 22:33:52 +08:00
|
|
|
|
var screenCast = (await query
|
2026-08-27 16:09:05 +08:00
|
|
|
|
.OrderByDescending(sc => sc.CastTime)
|
2026-09-06 22:33:52 +08:00
|
|
|
|
.Take(1)
|
|
|
|
|
|
.ToListAsync())
|
|
|
|
|
|
.FirstOrDefault();
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
if (screenCast == null)
|
|
|
|
|
|
return null;
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 补全赛事、项目、组别、场地、场次与计分规则信息,供大屏标题区展示
|
|
|
|
|
|
var eventInfo = await _eventRepository.GetByIdAsync(screenCast.EventId);
|
|
|
|
|
|
var venue = screenCast.VenueId.HasValue
|
|
|
|
|
|
? await _venueRepository.GetByIdAsync(screenCast.VenueId.Value)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
var eventProject = screenCast.EventProjectId.HasValue
|
|
|
|
|
|
? await _eventProjectRepository.GetByIdAsync(screenCast.EventProjectId.Value)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
var project = eventProject != null
|
|
|
|
|
|
? await _projectRepository.GetByIdAsync(eventProject.ProjectId)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
var group = screenCast.EventProjectGroupId.HasValue
|
|
|
|
|
|
? await _groupRepository.GetByIdAsync(screenCast.EventProjectGroupId.Value)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
var session = screenCast.SessionId.HasValue
|
|
|
|
|
|
? await _sessionRepository.GetByIdAsync(screenCast.SessionId.Value)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
var rule = session != null
|
|
|
|
|
|
? await _scoreRuleRepository.GetByIdAsync(session.ScoreRuleId)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
return new CurrentScreenCastDto
|
|
|
|
|
|
{
|
|
|
|
|
|
ScreenCastId = screenCast.Id,
|
|
|
|
|
|
CastType = screenCast.CastType,
|
|
|
|
|
|
CastStatus = screenCast.CastStatus,
|
|
|
|
|
|
CastTime = screenCast.CastTime,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
EventName = eventInfo?.EventName ?? string.Empty,
|
|
|
|
|
|
ProjectName = project?.Name ?? string.Empty,
|
|
|
|
|
|
ScoreRuleCode = rule?.RuleCode ?? string.Empty,
|
|
|
|
|
|
GroupName = group?.GroupName ?? string.Empty,
|
|
|
|
|
|
VenueName = venue?.VenueName ?? string.Empty,
|
|
|
|
|
|
SessionNo = session?.SessionNo ?? string.Empty,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
PayloadJson = screenCast.PayloadJson
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 成绩修正审批
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取成绩修正申请列表
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpGet("corrections")]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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();
|
2026-09-08 14:07:59 +08:00
|
|
|
|
var applyUserIds = corrections.Where(c => c.ApplyUserId.HasValue)
|
|
|
|
|
|
.Select(c => c.ApplyUserId!.Value).Distinct().ToList();
|
|
|
|
|
|
var applyRefereeIds = corrections.Where(c => c.ApplyRefereeId.HasValue)
|
|
|
|
|
|
.Select(c => c.ApplyRefereeId!.Value).Distinct().ToList();
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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();
|
2026-09-08 14:07:59 +08:00
|
|
|
|
var users = (await _sysUserRepository.AsQueryable()
|
2026-08-27 16:09:05 +08:00
|
|
|
|
.Where(u => allUserIds.Contains(u.Id))
|
|
|
|
|
|
.Select(u => new { u.Id, u.Name })
|
|
|
|
|
|
.ToListAsync())
|
|
|
|
|
|
.ToDictionary(u => u.Id, u => u.Name);
|
2026-09-08 14:07:59 +08:00
|
|
|
|
var applyingReferees = (await _refereeRepository.AsQueryable()
|
|
|
|
|
|
.Where(r => applyRefereeIds.Contains(r.Id))
|
|
|
|
|
|
.Select(r => new { r.Id, r.Name })
|
|
|
|
|
|
.ToListAsync())
|
|
|
|
|
|
.ToDictionary(r => r.Id, r => r.Name);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-09-08 14:07:59 +08:00
|
|
|
|
ApplyUserName = c.ApplyRefereeId.HasValue
|
|
|
|
|
|
? applyingReferees.GetValueOrDefault(c.ApplyRefereeId.Value, "")
|
|
|
|
|
|
: c.ApplyUserId.HasValue ? users.GetValueOrDefault(c.ApplyUserId.Value, "") : "",
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpGet("corrections/{correctionId}")]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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);
|
2026-09-06 22:33:52 +08:00
|
|
|
|
var scoreRule = await _scoreRuleRepository.GetByIdAsync(session.ScoreRuleId);
|
|
|
|
|
|
var roleSlots = JudgeRoleSchemaService.ParseSlots(scoreRule?.JudgeRoleSchemaJson);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
|
2026-09-08 14:07:59 +08:00
|
|
|
|
var applyUser = correction.ApplyUserId.HasValue
|
|
|
|
|
|
? await _sysUserRepository.GetByIdAsync(correction.ApplyUserId.Value)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
var applyReferee = correction.ApplyRefereeId.HasValue
|
|
|
|
|
|
? await _refereeRepository.GetByIdAsync(correction.ApplyRefereeId.Value)
|
|
|
|
|
|
: null;
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
EventId = session.EventId,
|
|
|
|
|
|
VenueId = session.VenueId,
|
|
|
|
|
|
EventProjectId = session.EventProjectId,
|
|
|
|
|
|
EventProjectGroupId = session.EventProjectGroupId,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
DeductionFactor = ScoreDisplayService.DeductionFactor(result.CalculationJson),
|
|
|
|
|
|
CalculationJson = result.CalculationJson,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
TotalScore = result.TotalScore,
|
|
|
|
|
|
ScoreText = result.Score,
|
|
|
|
|
|
IsDisqualified = result.IsDisqualified,
|
|
|
|
|
|
ResultStatus = result.ResultStatus,
|
|
|
|
|
|
ResultStatusName = GetResultStatusName(result.ResultStatus),
|
2026-08-30 21:46:05 +08:00
|
|
|
|
ResultVersion = result.ResultVersion,
|
|
|
|
|
|
ReviewUserId = result.ReviewUserId,
|
|
|
|
|
|
ReviewTime = result.ReviewTime
|
2026-08-27 16:09:05 +08:00
|
|
|
|
} : 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,
|
2026-09-06 22:33:52 +08:00
|
|
|
|
JudgeRoleName = JudgeRoleSchemaService.RoleNameOf(roleSlots, newSheet.JudgeRoleType),
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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,
|
2026-09-08 14:07:59 +08:00
|
|
|
|
ApplyUserName = applyReferee?.Name ?? applyUser?.Name ?? "",
|
2026-08-27 16:09:05 +08:00
|
|
|
|
ApplyReason = correction.ApplyReason,
|
|
|
|
|
|
CreatedTime = correction.CreatedTime,
|
|
|
|
|
|
CorrectionStatus = correction.CorrectionStatus,
|
|
|
|
|
|
ApproveUserName = approveUser?.Name,
|
|
|
|
|
|
ApproveRemark = correction.ApproveRemark,
|
|
|
|
|
|
ApproveTime = correction.ApproveTime,
|
|
|
|
|
|
CompletedTime = correction.CompletedTime
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 审批成绩修正申请
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpPost("corrections/approve")]
|
2026-09-04 17:15:03 +08:00
|
|
|
|
[UnitOfWork]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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)
|
|
|
|
|
|
{
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 批准修正需向原裁判下发修正任务,原裁判分配已删除时无法继续
|
2026-08-27 16:09:05 +08:00
|
|
|
|
var assignment = await _assignmentRepository.GetByIdAsync(correction.JudgeAssignmentId);
|
2026-09-04 17:15:03 +08:00
|
|
|
|
if (assignment == null || assignment.DeleteMark)
|
|
|
|
|
|
throw Oops.Bah("原裁判分配不存在,无法下发修正任务");
|
2026-09-22 09:11:48 +08:00
|
|
|
|
|
|
|
|
|
|
var originalSheet = await _sheetRepository.GetByIdAsync(correction.OriginalScoreSheetId)
|
|
|
|
|
|
?? throw Oops.Bah("原评分单不存在,无法下发修正任务");
|
|
|
|
|
|
var originalTask = originalSheet.JudgeTaskId > 0
|
|
|
|
|
|
? await _taskRepository.GetByIdAsync(originalSheet.JudgeTaskId)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
var session = await _sessionRepository.GetByIdAsync(correction.SessionId)
|
|
|
|
|
|
?? throw Oops.Bah("比赛场次不存在,无法下发修正任务");
|
|
|
|
|
|
var receiverUserId = await ResolveJudgeUserIdAsync(assignment, originalSheet);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
var task = new EventJudgeTask
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NextId(),
|
|
|
|
|
|
EventId = correction.EventId,
|
|
|
|
|
|
SessionId = correction.SessionId,
|
|
|
|
|
|
SessionEntryId = correction.SessionEntryId,
|
|
|
|
|
|
JudgeAssignmentId = correction.JudgeAssignmentId,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
EventRefereeId = assignment.EventRefereeId,
|
|
|
|
|
|
RefereeId = assignment.RefereeId,
|
2026-09-22 09:11:48 +08:00
|
|
|
|
JudgeUserId = receiverUserId,
|
2026-09-04 17:15:03 +08:00
|
|
|
|
JudgeRole = assignment.JudgeRole,
|
|
|
|
|
|
JudgeRoleType = assignment.JudgeRoleType,
|
2026-09-22 09:11:48 +08:00
|
|
|
|
ScoreRuleId = originalTask?.ScoreRuleId ?? session.ScoreRuleId,
|
|
|
|
|
|
RuleVersion = originalTask?.RuleVersion ?? originalSheet.RuleVersion,
|
|
|
|
|
|
TaskSnapshotJson = originalTask?.TaskSnapshotJson ?? "{}",
|
2026-08-27 16:09:05 +08:00
|
|
|
|
TaskType = 1,
|
2026-09-22 09:11:48 +08:00
|
|
|
|
TaskStatus = 10,
|
|
|
|
|
|
TaskVersion = (originalTask?.TaskVersion ?? 0) + 1,
|
|
|
|
|
|
OriginalTaskId = originalTask?.Id,
|
|
|
|
|
|
DispatchedTime = now,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
CreatedUserId = currentUserId,
|
|
|
|
|
|
CreatedTime = now
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await _taskRepository.InsertAsync(task);
|
|
|
|
|
|
|
|
|
|
|
|
correction.CorrectionTaskId = task.Id;
|
|
|
|
|
|
await _correctionRepository.UpdateAsync(correction);
|
|
|
|
|
|
|
2026-09-22 09:11:48 +08:00
|
|
|
|
if (receiverUserId.HasValue)
|
2026-08-27 16:09:05 +08:00
|
|
|
|
{
|
|
|
|
|
|
var message = new EventJudgeMessage
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NextId(),
|
|
|
|
|
|
EventId = correction.EventId,
|
2026-09-22 09:11:48 +08:00
|
|
|
|
ReceiverUserId = receiverUserId.Value,
|
2026-08-27 16:09:05 +08:00
|
|
|
|
RefereeId = assignment.RefereeId,
|
|
|
|
|
|
MessageType = 1,
|
|
|
|
|
|
Title = "成绩修正任务通知",
|
|
|
|
|
|
Content = $"您有一条成绩修正任务需要处理,申请原因:{correction.ApplyReason}",
|
2026-09-22 09:11:48 +08:00
|
|
|
|
RelationType = nameof(EventScoreCorrection),
|
2026-08-27 16:09:05 +08:00
|
|
|
|
RelationId = correction.Id,
|
|
|
|
|
|
IsRead = false,
|
|
|
|
|
|
CreatedUserId = currentUserId,
|
|
|
|
|
|
CreatedTime = now
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
await _messageRepository.InsertAsync(message);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-09-04 17:15:03 +08:00
|
|
|
|
await WriteAuditAsync(input.IsApproved ? "批准成绩修正" : "驳回成绩修正",
|
|
|
|
|
|
correction.EventId, correction.Id, 0, correction.CorrectionStatus,
|
|
|
|
|
|
input.IsApproved ? correction.ApplyReason : input.ApproveRemark, correction.SessionId);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-08 16:27:27 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 后台发起成绩修正,可一次勾选同一出场下的多名裁判。
|
|
|
|
|
|
/// 发起即代表已批准,被勾选的裁判可直接重新提交评分单。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[HttpPost("corrections/initiate")]
|
|
|
|
|
|
[UnitOfWork]
|
|
|
|
|
|
public async Task<List<long>> InitiateCorrections([FromBody] WorkbenchCorrectionInitiateInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentUserId = _authManager.UserId;
|
|
|
|
|
|
var now = DateTime.Now;
|
|
|
|
|
|
var reason = NormalizeReason(input.Reason, "修正原因");
|
|
|
|
|
|
|
|
|
|
|
|
var entry = await _entryRepository.GetFirstAsync(e => e.Id == input.SessionEntryId && !e.DeleteMark)
|
|
|
|
|
|
?? throw Oops.Bah("出场记录不存在");
|
2026-09-22 09:11:48 +08:00
|
|
|
|
var session = await _sessionRepository.GetByIdAsync(entry.SessionId)
|
|
|
|
|
|
?? throw Oops.Bah("比赛场次不存在");
|
2026-09-08 16:27:27 +08:00
|
|
|
|
|
|
|
|
|
|
var sheetIds = input.ScoreSheetIds.Where(id => id > 0).Distinct().ToList();
|
|
|
|
|
|
if (sheetIds.Count == 0) throw Oops.Bah("请至少选择一名裁判");
|
|
|
|
|
|
|
|
|
|
|
|
// 只允许修正本出场当前生效且已提交的评分单
|
|
|
|
|
|
var sheets = await _sheetRepository.AsQueryable()
|
|
|
|
|
|
.Where(s => !s.DeleteMark && sheetIds.Contains(s.Id) && s.SessionEntryId == entry.Id && s.IsCurrent)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
var missing = sheetIds.Except(sheets.Select(s => s.Id)).ToList();
|
|
|
|
|
|
if (missing.Count > 0) throw Oops.Bah("所选裁判的评分单不存在或已不是当前生效版本");
|
|
|
|
|
|
|
|
|
|
|
|
var notSubmitted = sheets.FirstOrDefault(s => s.SheetStatus < 10 || s.SheetStatus >= 90);
|
|
|
|
|
|
if (notSubmitted != null)
|
|
|
|
|
|
throw Oops.Bah($"{notSubmitted.JudgeRole}号位评分单当前为“{GetSheetStatusName(notSubmitted.SheetStatus)}”,不能修正");
|
|
|
|
|
|
|
|
|
|
|
|
var pendingSheetIds = (await _correctionRepository.AsQueryable()
|
2026-09-22 09:11:48 +08:00
|
|
|
|
.Where(c => !c.DeleteMark && sheetIds.Contains(c.OriginalScoreSheetId)
|
|
|
|
|
|
&& (c.CorrectionStatus == 0 || c.CorrectionStatus == 10 || c.CorrectionStatus == 30))
|
2026-09-08 16:27:27 +08:00
|
|
|
|
.Select(c => c.OriginalScoreSheetId).ToListAsync()).ToHashSet();
|
|
|
|
|
|
var pending = sheets.FirstOrDefault(s => pendingSheetIds.Contains(s.Id));
|
|
|
|
|
|
if (pending != null)
|
|
|
|
|
|
throw Oops.Bah($"{pending.JudgeRole}号位评分单已有修正流程在处理中");
|
|
|
|
|
|
|
|
|
|
|
|
var correctionIds = new List<long>();
|
|
|
|
|
|
foreach (var sheet in sheets.OrderBy(s => s.JudgeRole, StringComparer.OrdinalIgnoreCase))
|
|
|
|
|
|
{
|
|
|
|
|
|
var assignment = await _assignmentRepository.GetByIdAsync(sheet.JudgeAssignmentId);
|
|
|
|
|
|
if (assignment == null || assignment.DeleteMark)
|
|
|
|
|
|
throw Oops.Bah($"{sheet.JudgeRole}号位的裁判分配不存在,无法下发修正任务");
|
|
|
|
|
|
|
|
|
|
|
|
var correction = new EventScoreCorrection
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NextId(),
|
|
|
|
|
|
EventId = sheet.EventId,
|
|
|
|
|
|
SessionId = sheet.SessionId,
|
|
|
|
|
|
SessionEntryId = sheet.SessionEntryId,
|
|
|
|
|
|
OriginalScoreSheetId = sheet.Id,
|
|
|
|
|
|
JudgeAssignmentId = sheet.JudgeAssignmentId,
|
|
|
|
|
|
JudgeRole = sheet.JudgeRole,
|
|
|
|
|
|
ApplyUserId = currentUserId,
|
|
|
|
|
|
ApplyReason = reason,
|
|
|
|
|
|
// 后台发起本身即代表已批准,裁判端可直接重新评分
|
|
|
|
|
|
CorrectionStatus = 10,
|
|
|
|
|
|
ApproveUserId = currentUserId,
|
|
|
|
|
|
ApproveRemark = "赛事管理员发起",
|
|
|
|
|
|
ApproveTime = now,
|
|
|
|
|
|
CreatedUserId = currentUserId,
|
|
|
|
|
|
CreatedTime = now
|
|
|
|
|
|
};
|
|
|
|
|
|
await _correctionRepository.InsertAsync(correction);
|
|
|
|
|
|
|
2026-09-22 09:11:48 +08:00
|
|
|
|
var originalTask = sheet.JudgeTaskId > 0
|
|
|
|
|
|
? await _taskRepository.GetByIdAsync(sheet.JudgeTaskId)
|
|
|
|
|
|
: null;
|
|
|
|
|
|
var receiverUserId = await ResolveJudgeUserIdAsync(assignment, sheet);
|
2026-09-08 16:27:27 +08:00
|
|
|
|
var task = new EventJudgeTask
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NextId(),
|
|
|
|
|
|
EventId = correction.EventId,
|
|
|
|
|
|
SessionId = correction.SessionId,
|
|
|
|
|
|
SessionEntryId = correction.SessionEntryId,
|
|
|
|
|
|
JudgeAssignmentId = correction.JudgeAssignmentId,
|
|
|
|
|
|
EventRefereeId = assignment.EventRefereeId,
|
|
|
|
|
|
RefereeId = assignment.RefereeId,
|
2026-09-22 09:11:48 +08:00
|
|
|
|
JudgeUserId = receiverUserId,
|
2026-09-08 16:27:27 +08:00
|
|
|
|
JudgeRole = assignment.JudgeRole,
|
|
|
|
|
|
JudgeRoleType = assignment.JudgeRoleType,
|
2026-09-22 09:11:48 +08:00
|
|
|
|
ScoreRuleId = originalTask?.ScoreRuleId ?? session.ScoreRuleId,
|
|
|
|
|
|
RuleVersion = originalTask?.RuleVersion ?? sheet.RuleVersion,
|
|
|
|
|
|
TaskSnapshotJson = originalTask?.TaskSnapshotJson ?? "{}",
|
2026-09-08 16:27:27 +08:00
|
|
|
|
TaskType = 1,
|
2026-09-22 09:11:48 +08:00
|
|
|
|
TaskStatus = 10,
|
|
|
|
|
|
TaskVersion = (originalTask?.TaskVersion ?? 0) + 1,
|
|
|
|
|
|
OriginalTaskId = originalTask?.Id,
|
|
|
|
|
|
DispatchedTime = now,
|
2026-09-08 16:27:27 +08:00
|
|
|
|
CreatedUserId = currentUserId,
|
|
|
|
|
|
CreatedTime = now
|
|
|
|
|
|
};
|
|
|
|
|
|
await _taskRepository.InsertAsync(task);
|
|
|
|
|
|
|
|
|
|
|
|
correction.CorrectionTaskId = task.Id;
|
|
|
|
|
|
await _correctionRepository.UpdateAsync(correction);
|
|
|
|
|
|
|
|
|
|
|
|
// 裁判未绑定小程序账号时消息无处投递,只保留修正记录,不阻断主流程
|
2026-09-22 09:11:48 +08:00
|
|
|
|
if (receiverUserId.HasValue)
|
2026-09-08 16:27:27 +08:00
|
|
|
|
{
|
|
|
|
|
|
await _messageRepository.InsertAsync(new EventJudgeMessage
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NextId(),
|
|
|
|
|
|
EventId = correction.EventId,
|
2026-09-22 09:11:48 +08:00
|
|
|
|
ReceiverUserId = receiverUserId.Value,
|
2026-09-08 16:27:27 +08:00
|
|
|
|
RefereeId = assignment.RefereeId,
|
|
|
|
|
|
MessageType = 1,
|
|
|
|
|
|
Title = "成绩修正通知",
|
|
|
|
|
|
Content = $"请对{assignment.JudgeRole}号位的评分重新提交,原因:{reason}",
|
2026-09-22 09:11:48 +08:00
|
|
|
|
RelationType = nameof(EventScoreCorrection),
|
2026-09-08 16:27:27 +08:00
|
|
|
|
RelationId = correction.Id,
|
|
|
|
|
|
IsRead = false,
|
|
|
|
|
|
CreatedUserId = currentUserId,
|
|
|
|
|
|
CreatedTime = now
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
correctionIds.Add(correction.Id);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await WriteAuditAsync("发起成绩修正", sheets[0].EventId, entry.Id, null, 10,
|
|
|
|
|
|
$"裁判数量:{sheets.Count},原因:{reason}", entry.SessionId);
|
|
|
|
|
|
return correctionIds;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
#endregion
|
|
|
|
|
|
|
|
|
|
|
|
#region 成绩管理
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 确认成绩
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpPost("results/confirm")]
|
2026-09-04 17:15:03 +08:00
|
|
|
|
[UnitOfWork]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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("只能确认待复核状态的成绩");
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 当前版本由成绩修正产生时确认为“已更正(40)”,常规成绩确认为“已确认(20)”
|
|
|
|
|
|
var currentVersion = await _resultVersionRepository.GetFirstAsync(v =>
|
|
|
|
|
|
!v.DeleteMark && v.EventResultId == result.Id && v.ResultVersion == result.ResultVersion);
|
|
|
|
|
|
var confirmedStatus = currentVersion?.ScoreCorrectionId.HasValue == true ? 40 : 20;
|
|
|
|
|
|
result.ResultStatus = confirmedStatus;
|
2026-08-30 21:46:05 +08:00
|
|
|
|
result.ReviewUserId = currentUserId;
|
|
|
|
|
|
result.ReviewTime = now;
|
2026-08-27 16:09:05 +08:00
|
|
|
|
result.UpdatedUserId = currentUserId;
|
|
|
|
|
|
result.UpdatedTime = now;
|
|
|
|
|
|
|
|
|
|
|
|
await _resultRepository.UpdateAsync(result);
|
2026-09-04 17:15:03 +08:00
|
|
|
|
await SyncResultVersionStatusAsync(result, confirmedStatus, currentUserId, now, currentVersion);
|
|
|
|
|
|
await WriteAuditAsync("确认成绩", result.EventId, result.Id, 10, confirmedStatus, sessionId: result.SessionId);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 批量确认成绩
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpPost("results/batch-confirm")]
|
2026-09-04 17:15:03 +08:00
|
|
|
|
[UnitOfWork]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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);
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 必须显式提交选中的成绩,过滤非法Id并去重,防止误确认整场次
|
|
|
|
|
|
var requestedIds = input.ResultIds.Where(id => id > 0).Distinct().ToList();
|
|
|
|
|
|
if (!requestedIds.Any())
|
|
|
|
|
|
throw Oops.Bah("请选择至少一条待确认成绩");
|
|
|
|
|
|
query = query.Where((r, e) => requestedIds.Contains(r.Id));
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
var results = await query.Select((r, e) => r).ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
if (!results.Any())
|
|
|
|
|
|
throw Oops.Bah("没有可确认的成绩");
|
2026-09-04 17:15:03 +08:00
|
|
|
|
if (results.Count != requestedIds.Count)
|
|
|
|
|
|
throw Oops.Bah("选中成绩中包含非当前场次或非待复核数据,请刷新后重试");
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 批量取出当前版本,逐条同步确认状态(含修正产生的已更正40状态)
|
|
|
|
|
|
var resultIds = results.Select(r => r.Id).ToList();
|
|
|
|
|
|
var resultVersions = await _resultVersionRepository.AsQueryable()
|
|
|
|
|
|
.Where(v => !v.DeleteMark && resultIds.Contains(v.EventResultId))
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
var currentResultVersions = new List<EventResultVersion>();
|
2026-08-27 16:09:05 +08:00
|
|
|
|
foreach (var result in results)
|
|
|
|
|
|
{
|
2026-09-04 17:15:03 +08:00
|
|
|
|
var version = resultVersions.FirstOrDefault(v =>
|
|
|
|
|
|
v.EventResultId == result.Id && v.ResultVersion == result.ResultVersion);
|
|
|
|
|
|
result.ResultStatus = version?.ScoreCorrectionId.HasValue == true ? 40 : 20;
|
2026-08-30 21:46:05 +08:00
|
|
|
|
result.ReviewUserId = currentUserId;
|
|
|
|
|
|
result.ReviewTime = now;
|
2026-08-27 16:09:05 +08:00
|
|
|
|
result.UpdatedUserId = currentUserId;
|
|
|
|
|
|
result.UpdatedTime = now;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
if (version != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
version.ResultStatus = result.ResultStatus;
|
|
|
|
|
|
version.UpdatedUserId = currentUserId;
|
|
|
|
|
|
version.UpdatedTime = now;
|
|
|
|
|
|
currentResultVersions.Add(version);
|
|
|
|
|
|
}
|
2026-08-27 16:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
await _resultRepository.UpdateRangeAsync(results);
|
2026-09-04 17:15:03 +08:00
|
|
|
|
if (currentResultVersions.Count > 0)
|
|
|
|
|
|
await _resultVersionRepository.UpdateRangeAsync(currentResultVersions);
|
|
|
|
|
|
await WriteAuditAsync("批量确认成绩", results[0].EventId, input.SessionId, 10,
|
|
|
|
|
|
results.Any(r => r.ResultStatus == 40) ? "20/40" : "20",
|
|
|
|
|
|
$"成绩数量:{results.Count}", input.SessionId);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 创建重赛
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpPost("entries/rematch")]
|
2026-09-04 17:15:03 +08:00
|
|
|
|
[UnitOfWork]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
public async Task<CreateRematchOutput> CreateRematch([FromBody] CreateRematchInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentUserId = _authManager.UserId;
|
|
|
|
|
|
var now = DateTime.Now;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
var reason = NormalizeReason(input.RematchReason, "重赛原因");
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
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("场次不存在");
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 已作废的出场不能重复发起重赛,避免生成多条重赛记录
|
|
|
|
|
|
if (originalEntry.IsVoided)
|
|
|
|
|
|
throw Oops.Bah("该出场记录已作废,不能重复创建重赛");
|
|
|
|
|
|
|
|
|
|
|
|
// 出场序号只在同组别内连续,重赛出场追加到本组别末尾
|
|
|
|
|
|
var groupLastPlayOrder = await _entryRepository.AsQueryable()
|
|
|
|
|
|
.Where(e => !e.DeleteMark && e.SessionId == originalEntry.SessionId && e.GroupName == originalEntry.GroupName)
|
2026-08-27 16:09:05 +08:00
|
|
|
|
.MaxAsync(e => e.PlayOrder);
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
var newPlayOrder = groupLastPlayOrder + 1;
|
|
|
|
|
|
// 占用该序号及之后的同组别出场倒序后移一位,避免出场序号冲突
|
|
|
|
|
|
var laterEntries = await _entryRepository.AsQueryable()
|
2026-09-06 22:33:52 +08:00
|
|
|
|
.Where(e => !e.DeleteMark && e.SessionId == originalEntry.SessionId &&
|
|
|
|
|
|
e.GroupName == originalEntry.GroupName && e.PlayOrder >= newPlayOrder)
|
2026-09-04 17:15:03 +08:00
|
|
|
|
.OrderByDescending(e => e.PlayOrder)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
foreach (var laterEntry in laterEntries)
|
|
|
|
|
|
{
|
|
|
|
|
|
laterEntry.PlayOrder++;
|
|
|
|
|
|
laterEntry.UpdatedUserId = currentUserId;
|
|
|
|
|
|
laterEntry.UpdatedTime = now;
|
|
|
|
|
|
await _entryRepository.UpdateAsync(laterEntry);
|
|
|
|
|
|
}
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
originalEntry.IsVoided = true;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
originalEntry.VoidReason = $"重赛原因:{reason}";
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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);
|
2026-09-04 17:15:03 +08:00
|
|
|
|
await SyncResultVersionStatusAsync(result, 90, currentUserId, now);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-09-04 17:15:03 +08:00
|
|
|
|
await WriteAuditAsync("创建重赛", session.EventId, originalEntry.Id, "有效", "已作废并创建重赛", reason, session.Id);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
return new CreateRematchOutput
|
|
|
|
|
|
{
|
|
|
|
|
|
NewSessionEntryId = newEntry.Id,
|
|
|
|
|
|
NewEntryNo = newEntry.EntryNo,
|
|
|
|
|
|
NewPlayOrder = newEntry.PlayOrder
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 作废成绩
|
|
|
|
|
|
/// </summary>
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpPost("results/void")]
|
2026-09-04 17:15:03 +08:00
|
|
|
|
[UnitOfWork]
|
2026-08-27 16:09:05 +08:00
|
|
|
|
public async Task VoidResult([FromBody] VoidResultInput input)
|
|
|
|
|
|
{
|
|
|
|
|
|
var currentUserId = _authManager.UserId;
|
|
|
|
|
|
var now = DateTime.Now;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
var reason = NormalizeReason(input.VoidReason, "作废原因");
|
2026-08-27 16:09:05 +08:00
|
|
|
|
|
|
|
|
|
|
var entry = await _entryRepository.GetFirstAsync(e => e.Id == input.SessionEntryId && !e.DeleteMark)
|
|
|
|
|
|
?? throw Oops.Bah("出场记录不存在");
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 已作废的出场记录直接拦截,避免重复写入作废状态与原因
|
|
|
|
|
|
if (entry.IsVoided)
|
|
|
|
|
|
throw Oops.Bah("该出场记录已作废");
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
entry.IsVoided = true;
|
2026-09-04 17:15:03 +08:00
|
|
|
|
entry.VoidReason = reason;
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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);
|
2026-09-04 17:15:03 +08:00
|
|
|
|
await SyncResultVersionStatusAsync(result, 90, currentUserId, now);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
}
|
2026-09-04 17:15:03 +08:00
|
|
|
|
var voidSession = await _sessionRepository.GetByIdAsync(entry.SessionId);
|
|
|
|
|
|
await WriteAuditAsync("作废成绩", voidSession?.EventId ?? 0, entry.Id, "有效", "已作废", reason, entry.SessionId);
|
2026-08-27 16:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
|
2026-08-30 21:46:05 +08:00
|
|
|
|
#region 数据导出
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 导出成绩册(与成绩管理页面筛选条件一致,默认仅导出已确认、未DQ成绩)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[DisplayName("导出成绩册")]
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpGet("results/export")]
|
2026-08-30 21:46:05 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2026-09-04 17:15:03 +08:00
|
|
|
|
// 支持按单个场次缩小导出范围
|
|
|
|
|
|
if (input.SessionId.HasValue)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sessionId = input.SessionId.Value;
|
|
|
|
|
|
query = query.Where((r, e, s) => e.SessionId == sessionId);
|
|
|
|
|
|
}
|
|
|
|
|
|
// 关键词匹配出场编号、参赛者姓名或队名
|
|
|
|
|
|
if (!string.IsNullOrWhiteSpace(input.Keyword))
|
|
|
|
|
|
{
|
|
|
|
|
|
var keyword = input.Keyword.Trim();
|
|
|
|
|
|
query = query.Where((r, e, s) => e.EntryNo.Contains(keyword) || e.ParticipantName.Contains(keyword) ||
|
|
|
|
|
|
(e.TeamName != null && e.TeamName.Contains(keyword)));
|
|
|
|
|
|
}
|
2026-08-30 21:46:05 +08:00
|
|
|
|
|
|
|
|
|
|
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,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
r.RequiredScore,
|
2026-08-30 21:46:05 +08:00
|
|
|
|
r.DeductionScore,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
r.CalculationJson,
|
2026-08-30 21:46:05 +08:00
|
|
|
|
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,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
x.DifficultyScore.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
|
|
|
|
|
|
x.PerformanceScore.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
|
|
|
|
|
|
x.RequiredScore.ToString("F2", System.Globalization.CultureInfo.InvariantCulture),
|
|
|
|
|
|
x.DeductionScore.ToString("F3", System.Globalization.CultureInfo.InvariantCulture),
|
|
|
|
|
|
ScoreDisplayService.DeductionFactor(x.CalculationJson)?.ToString("F3", System.Globalization.CultureInfo.InvariantCulture) ?? string.Empty,
|
|
|
|
|
|
x.TotalScore.HasValue ? x.TotalScore.Value.ToString("F2", System.Globalization.CultureInfo.InvariantCulture) : x.Score,
|
2026-08-30 21:46:05 +08:00
|
|
|
|
x.IsDisqualified ? "取消资格" : GetResultStatusName(x.ResultStatus),
|
|
|
|
|
|
x.ResultVersion,
|
|
|
|
|
|
FmtTime(x.ReviewTime)
|
|
|
|
|
|
};
|
|
|
|
|
|
}).ToList();
|
|
|
|
|
|
|
2026-09-29 17:19:03 +08:00
|
|
|
|
var headers = new[] { "名次", "出场编号", "姓名/队名", "项目", "组别", "难度分", "表现分", "规定分", "扣分合计", "得分系数M", "总分", "状态", "成绩版本", "确认时间" };
|
2026-08-30 21:46:05 +08:00
|
|
|
|
var bytes = await new WorkbenchListExcelExporter("成绩册", $"成绩册(导出时间:{DateTime.Now:yyyy-MM-dd HH:mm})", headers, rows,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
new double[] { 8, 16, 16, 16, 12, 10, 10, 10, 10, 12, 10, 10, 10, 20 }).ExportAsync();
|
2026-08-30 21:46:05 +08:00
|
|
|
|
return ExcelFile(bytes, $"成绩册_{input.EventId}_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 导出评分单明细(按场次/项目,逐出场逐裁判列出评分项与提交信息)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[DisplayName("导出评分单明细")]
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpGet("score-sheets/export")]
|
2026-08-30 21:46:05 +08:00
|
|
|
|
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());
|
|
|
|
|
|
|
2026-09-29 17:19:03 +08:00
|
|
|
|
var scoreRuleIds = sheets.Select(x => x.ScoreRuleId).Distinct().ToList();
|
|
|
|
|
|
var factorRules = (await _scoreRuleRepository.GetListAsync(x => scoreRuleIds.Contains(x.Id)))
|
|
|
|
|
|
.ToDictionary(x => x.Id, x => ScoreRuleConfigService.Parse(x.RuleConfigJson).IsFactorTotal);
|
2026-08-30 21:46:05 +08:00
|
|
|
|
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),
|
2026-09-29 17:19:03 +08:00
|
|
|
|
Num(sheet.RequiredCompletedCount),
|
|
|
|
|
|
Num(sheet.RequiredMissingCount),
|
|
|
|
|
|
ScoreDisplayService.JudgeValue(sheet, factorRules.GetValueOrDefault(sheet.ScoreRuleId))
|
|
|
|
|
|
.ToString(factorRules.GetValueOrDefault(sheet.ScoreRuleId) && sheet.JudgeRoleType == "T" ? "F3" : "F2", System.Globalization.CultureInfo.InvariantCulture),
|
2026-08-30 21:46:05 +08:00
|
|
|
|
sheet.IsDisqualified ? "DQ" : string.Empty,
|
|
|
|
|
|
sheet.SheetVersion,
|
|
|
|
|
|
sheet.RuleVersion,
|
|
|
|
|
|
sheet.OriginalSheetId.HasValue ? sheet.OriginalSheetId.Value.ToString() : string.Empty,
|
|
|
|
|
|
FmtTime(sheet.SubmittedTime)
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-29 17:19:03 +08:00
|
|
|
|
var headers = new[] { "场次编号", "出场编号", "姓名/队名", "裁判", "裁判角色", "评分项明细", "难度分", "表现分", "扣分合计", "规定完成量", "规定缺失量", "分数/本裁判系数", "DQ", "评分单版本", "规则版本", "原评分单", "提交时间" };
|
2026-08-30 21:46:05 +08:00
|
|
|
|
var bytes = await new WorkbenchListExcelExporter("评分单", $"评分单明细(导出时间:{DateTime.Now:yyyy-MM-dd HH:mm})", headers, rows,
|
2026-09-29 17:19:03 +08:00
|
|
|
|
new double[] { 20, 16, 16, 12, 14, 42, 10, 10, 10, 12, 12, 18, 8, 10, 10, 18, 20 }).ExportAsync();
|
2026-08-30 21:46:05 +08:00
|
|
|
|
return ExcelFile(bytes, $"评分单_{input.EventId}_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 导出操作审计日志(按赛事ID匹配请求参数,支持时间范围、操作人筛选,最多导出10000条)
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
[DisplayName("导出操作审计")]
|
2026-09-06 22:33:52 +08:00
|
|
|
|
[HttpGet("audit/export")]
|
2026-08-30 21:46:05 +08:00
|
|
|
|
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
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
#region 辅助方法
|
|
|
|
|
|
|
2026-09-23 10:34:34 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取场次在秩序册/自动编排中的全局顺序。
|
|
|
|
|
|
/// 秩序册导入及花样赛自动编排生成的场次号末段均保存该顺序;
|
|
|
|
|
|
/// 无全局序号的速度项目回退到项目配置及场地顺序。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static int GetArrangementSequence(string? sessionNo)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(sessionNo)) return int.MaxValue;
|
|
|
|
|
|
|
|
|
|
|
|
var match = Regex.Match(sessionNo, @"^EP\d+-(?:V)?\d+(?:-(?<sequence>\d+))?$",
|
|
|
|
|
|
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
|
|
|
|
|
if (!match.Success) return int.MaxValue;
|
|
|
|
|
|
|
|
|
|
|
|
return match.Groups["sequence"].Success &&
|
|
|
|
|
|
int.TryParse(match.Groups["sequence"].Value, out var sequence)
|
|
|
|
|
|
? sequence
|
|
|
|
|
|
: int.MaxValue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-23 15:18:13 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 将自动编排场次中的出场记录还原为秩序册预览的全局顺序。
|
|
|
|
|
|
/// 同一组别、同一场地的多条记录共用一个场次,因此需要在场次起始序号上叠加出场序号。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static long BuildEntryScheduleOrder(int sessionArrangementOrder, int venueOrder, int playOrder)
|
|
|
|
|
|
{
|
|
|
|
|
|
var taskSessionOrder = BuildEntrySessionOrder(sessionArrangementOrder, playOrder);
|
|
|
|
|
|
|
|
|
|
|
|
return (long)taskSessionOrder * 1_000_000L + Math.Max(venueOrder, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 获取单条出场记录在秩序册中的实际场次号。
|
|
|
|
|
|
/// 花样项目的场次号保存的是本场起始序号;速度项目没有起始序号时按出场顺序计算。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static int BuildEntrySessionOrder(int sessionArrangementOrder, int playOrder)
|
|
|
|
|
|
{
|
|
|
|
|
|
var safePlayOrder = Math.Max(playOrder, 1);
|
|
|
|
|
|
return sessionArrangementOrder > 0 && sessionArrangementOrder < int.MaxValue
|
|
|
|
|
|
? sessionArrangementOrder + safePlayOrder - 1
|
|
|
|
|
|
: safePlayOrder;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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 => "已作废",
|
|
|
|
|
|
_ => "未知"
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 规范化重赛/作废原因:去除首尾空格并校验长度为2-200个字符
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static string NormalizeReason(string? value, string fieldName)
|
|
|
|
|
|
{
|
|
|
|
|
|
var reason = value?.Trim() ?? string.Empty;
|
|
|
|
|
|
if (reason.Length < 2 || reason.Length > 200)
|
|
|
|
|
|
throw Oops.Bah($"{fieldName}长度必须为2-200个字符");
|
|
|
|
|
|
return reason;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-22 09:11:48 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// Resolve the App user that currently owns a judge assignment. Older assignments may not
|
|
|
|
|
|
/// have JudgeUserId populated, while the referee profile or submitted sheet still has it.
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task<long?> ResolveJudgeUserIdAsync(EventJudgeAssignment assignment,
|
|
|
|
|
|
EventJudgeScoreSheet? sheet = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var receiverUserId = sheet?.JudgeUserId ?? assignment.JudgeUserId;
|
|
|
|
|
|
if (receiverUserId.HasValue) return receiverUserId;
|
|
|
|
|
|
|
|
|
|
|
|
var referee = await _refereeRepository.GetByIdAsync(assignment.RefereeId);
|
|
|
|
|
|
return referee?.AppUserId;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-04 17:15:03 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 同步成绩当前版本的状态,保证 EventResult 与 EventResultVersion 状态一致
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task SyncResultVersionStatusAsync(EventResult result, int status, long userId,
|
|
|
|
|
|
DateTime now, EventResultVersion? version = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
version ??= await _resultVersionRepository.GetFirstAsync(v =>
|
|
|
|
|
|
!v.DeleteMark && v.EventResultId == result.Id && v.ResultVersion == result.ResultVersion);
|
|
|
|
|
|
if (version == null) return;
|
|
|
|
|
|
version.ResultStatus = status;
|
|
|
|
|
|
version.UpdatedUserId = userId;
|
|
|
|
|
|
version.UpdatedTime = now;
|
|
|
|
|
|
await _resultVersionRepository.UpdateAsync(version);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 写入工作台业务操作审计日志,记录投屏、确认、修正、重赛、作废等关键动作的状态流转
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private async Task WriteAuditAsync(string action, long eventId, long objectId,
|
|
|
|
|
|
object? beforeStatus, object? afterStatus, string? reason = null, long? sessionId = null)
|
|
|
|
|
|
{
|
|
|
|
|
|
var parameter = System.Text.Json.JsonSerializer.Serialize(new
|
|
|
|
|
|
{
|
|
|
|
|
|
EventId = eventId,
|
|
|
|
|
|
SessionId = sessionId,
|
|
|
|
|
|
ObjectId = objectId,
|
|
|
|
|
|
BeforeStatus = beforeStatus,
|
|
|
|
|
|
AfterStatus = afterStatus,
|
|
|
|
|
|
Reason = reason
|
|
|
|
|
|
});
|
|
|
|
|
|
await _operationLogRepository.InsertAsync(new SysOperationLog
|
|
|
|
|
|
{
|
|
|
|
|
|
Id = _idGenerator.NextId(),
|
|
|
|
|
|
UserId = _authManager.UserId,
|
|
|
|
|
|
LogLevel = Microsoft.Extensions.Logging.LogLevel.Information,
|
|
|
|
|
|
Description = action,
|
|
|
|
|
|
ActionName = action,
|
|
|
|
|
|
ControllerName = nameof(WorkbenchController),
|
|
|
|
|
|
HttpMethod = "BUSINESS",
|
|
|
|
|
|
HttpStatusCode = 200,
|
|
|
|
|
|
Message = action,
|
|
|
|
|
|
RequestUrl = "/workbench/audit",
|
|
|
|
|
|
Parameter = parameter,
|
|
|
|
|
|
Response = null,
|
|
|
|
|
|
TraceId = string.Empty,
|
|
|
|
|
|
ThreadId = Environment.CurrentManagedThreadId,
|
|
|
|
|
|
UserAgent = string.Empty,
|
|
|
|
|
|
RemoteIp = string.Empty,
|
|
|
|
|
|
Location = string.Empty,
|
|
|
|
|
|
OsDescription = null,
|
|
|
|
|
|
Elapsed = 0,
|
|
|
|
|
|
CreatedTime = DateTime.Now
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-27 16:09:05 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|