From 8efb6a3707980ea46c367db91021b520cd044cd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Sun, 20 Sep 2026 09:40:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8Dbug=EF=BC=8C=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E4=B8=9A=E5=8A=A1=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Client/CheckinAdminController.cs | 53 ++++-- .../Client/RefereeController.cs | 127 ++++++++++++--- .../Judge/JudgeScoringController.cs | 153 +++++++++++++++--- 3 files changed, 278 insertions(+), 55 deletions(-) diff --git a/YD_Event.Application/Client/CheckinAdminController.cs b/YD_Event.Application/Client/CheckinAdminController.cs index a9953f2..0c3777e 100644 --- a/YD_Event.Application/Client/CheckinAdminController.cs +++ b/YD_Event.Application/Client/CheckinAdminController.cs @@ -1,6 +1,7 @@ using System.Text.RegularExpressions; using YD_Event.Application.Auth; using YD_Event.Application.Client.Dtos; +using YD_Event.Application.Event; namespace YD_Event.Application.Client; @@ -194,13 +195,21 @@ public class CheckinAdminController : IDynamicApiController if (task == null) { - task = new CheckinTask + // 兼容历史解绑数据:旧逻辑只禁用了任务,但唯一索引仍将其视为占用。 + // 同一赛事、同一检次存在已停用任务时直接复用该任务槽位,避免再次插入触发唯一索引冲突。 + task = await _taskRepository.GetFirstAsync(x => + x.EventId == input.EventId && x.CheckinNo == input.CheckinNo && !x.DeleteMark); + if (task == null) { - EventId = input.EventId, - CheckerUserId = checker.Id, - CreatedUserId = managerUserId, - CreatedTime = DateTime.Now - }; + task = new CheckinTask + { + EventId = input.EventId, + CreatedUserId = managerUserId, + CreatedTime = DateTime.Now + }; + } + + task.CheckerUserId = checker.Id; } ApplyCheckinAssignment(task, input.CheckinNo, assignedNumbers.Count, input.Venue); @@ -230,6 +239,11 @@ public class CheckinAdminController : IDynamicApiController EnsureManager(); var task = await _taskRepository.GetFirstAsync(x => x.Id == input.TaskId && !x.DeleteMark); if (task == null) throw Oops.Bah("检录任务不存在"); + var eventInfo = await _eventRepository.GetFirstAsync(x => x.Id == task.EventId && !x.DeleteMark); + if (eventInfo == null) throw Oops.Bah("赛事不存在"); + var eventStatus = GetEffectiveEventStatus(eventInfo, DateTime.Now); + if (eventStatus == EventStatus.Ongoing) throw Oops.Bah("赛事比赛中,不支持解除检录员绑定"); + var laterExists = await _taskRepository.IsAnyAsync(x => x.EventId == task.EventId && x.CheckinNo > task.CheckinNo && x.TaskStatus == 1 && x.Status == AvailabilityStatus.Enable && !x.DeleteMark); @@ -237,6 +251,7 @@ public class CheckinAdminController : IDynamicApiController task.TaskStatus = 0; task.Status = AvailabilityStatus.Disable; + task.DeleteMark = true; task.UpdatedUserId = _authManager.UserId; task.UpdatedTime = DateTime.Now; await _taskRepository.UpdateAsync(task); @@ -320,10 +335,13 @@ public class CheckinAdminController : IDynamicApiController .ToList(); if (occupiedEventIds.Count > 0) { - var occupiedEvent = await _eventRepository.AsQueryable() - .Where(x => occupiedEventIds.Contains(x.Id) && !x.DeleteMark && - x.EventStatus != EventStatus.Ended) - .FirstAsync(); + var occupiedEvents = await _eventRepository.AsQueryable() + .Where(x => occupiedEventIds.Contains(x.Id) && + x.Status == AvailabilityStatus.Enable && !x.DeleteMark) + .ToListAsync(); + var now = DateTime.Now; + var occupiedEvent = occupiedEvents.FirstOrDefault(x => + GetEffectiveEventStatus(x, now) != EventStatus.Ended); if (occupiedEvent != null) { return new CheckerAccountAvailabilityDto @@ -378,6 +396,21 @@ public class CheckinAdminController : IDynamicApiController task.Venue = venue?.Trim(); } + /// + /// 统一检录管理使用的赛事状态口径。后台状态与比赛时间不一致时按生命周期靠后的状态处理, + /// 避免状态同步延迟导致比赛中仍可解绑,或赛事结束后账号未及时释放。 + /// + private static EventStatus GetEffectiveEventStatus(EventInfo eventInfo, DateTime now) + { + var calculatedStatus = EventStatusUpdateService.CalculateEventStatus(eventInfo, now); + if (calculatedStatus == EventStatus.Ended || eventInfo.EventStatus == EventStatus.Ended) + return EventStatus.Ended; + if (calculatedStatus == EventStatus.Ongoing || eventInfo.EventStatus == EventStatus.Ongoing) + return EventStatus.Ongoing; + + return calculatedStatus; + } + private static string ToCheckinName(int checkinNo) => checkinNo switch { 1 => "一检", diff --git a/YD_Event.Application/Client/RefereeController.cs b/YD_Event.Application/Client/RefereeController.cs index 1d89785..1ae4aa6 100644 --- a/YD_Event.Application/Client/RefereeController.cs +++ b/YD_Event.Application/Client/RefereeController.cs @@ -1,6 +1,7 @@ using EasyCaching.Core; using YD_Event.Application.Auth; using YD_Event.Application.Client.Dtos; +using YD_Event.Application.Event; namespace YD_Event.Application.Client; @@ -190,7 +191,7 @@ public class RefereeController : IDynamicApiController { var userId = await EnsureRefereeAsync(); var applications = await _applicationRepository.AsQueryable() - .Where(x => x.RefereeUserId == userId && x.Status == AvailabilityStatus.Enable) + .Where(x => x.RefereeUserId == userId && x.Status == AvailabilityStatus.Enable && !x.DeleteMark) .OrderByDescending(x => x.CreatedTime) .ToListAsync(); @@ -229,20 +230,29 @@ public class RefereeController : IDynamicApiController public async Task Apply([FromBody] RefereeApplyInput input) { var userId = await EnsureRefereeAsync(); - var evt = await _eventRepository.GetFirstAsync(x => x.Id == input.EventId && x.Status == AvailabilityStatus.Enable); + var evt = await _eventRepository.GetFirstAsync(x => + x.Id == input.EventId && x.Status == AvailabilityStatus.Enable && !x.DeleteMark); if (evt == null) throw Oops.Bah("赛事不存在"); - if (evt.EventStatus != EventStatus.Enrolling) throw Oops.Bah("当前赛事不在执裁报名时间内"); + if (GetEffectiveEventStatus(evt) != EventStatus.Enrolling) + throw Oops.Bah("当前赛事不在执裁报名时间内"); - var exists = await _applicationRepository.IsAnyAsync(x => - x.EventId == input.EventId && x.RefereeUserId == userId && x.ApplicationStatus != 4); - if (exists) throw Oops.Bah("已报名执裁当前赛事"); - - var referee = await _refereeRepository.GetFirstAsync(x => x.AppUserId == userId && !x.DeleteMark); + // 同一裁判的报名请求必须串行化,否则并发报名两场重叠赛事时, + // 两个事务可能都在对方写入前通过撞期校验。锁顺序固定为:裁判 -> 赛事名额。 + var referee = await _refereeRepository.AsQueryable() + .With("UPDLOCK, HOLDLOCK") + .Where(x => x.AppUserId == userId && !x.DeleteMark) + .FirstAsync(); if (referee == null) throw Oops.Bah("裁判档案不存在,请先完善裁判资料"); if (referee.AuditStatus != AuditStatus.Pass) throw Oops.Bah("裁判认证尚未通过审核,暂不能报名执裁"); - var quota = await BuildQuotaAsync(evt, userId); - if (quota.IsFull) throw Oops.Bah("当前赛事裁判报名名额已满"); + var exists = await _applicationRepository.IsAnyAsync(x => + x.EventId == input.EventId && x.RefereeUserId == userId && + (x.ApplicationStatus == 1 || x.ApplicationStatus == 2) && + x.Status == AvailabilityStatus.Enable && !x.DeleteMark); + if (exists) throw Oops.Bah("已报名执裁当前赛事"); + + var quota = await BuildQuotaAsync(evt, userId, true, referee.Id); + if (!quota.CanApply) throw Oops.Bah(quota.Message); var application = new RefereeApplication { @@ -257,7 +267,8 @@ public class RefereeController : IDynamicApiController }; await _applicationRepository.InsertAsync(application); - if (!await _eventRefereeRepository.IsAnyAsync(x => x.EventId == input.EventId && x.RefereeId == referee.Id)) + if (!await _eventRefereeRepository.IsAnyAsync(x => + x.EventId == input.EventId && x.RefereeId == referee.Id && !x.DeleteMark)) { await _eventRefereeRepository.InsertAsync(new EventReferee { @@ -278,23 +289,40 @@ public class RefereeController : IDynamicApiController /// /// 报名名额按赛事报名限制中的裁判限报数量控制,为 0 时不限名额。 /// - private async Task BuildQuotaAsync(EventInfo evt, long userId) + private async Task BuildQuotaAsync( + EventInfo evt, long userId, bool lockForUpdate = false, long? refereeId = null) { var limit = await _limitRepository.GetFirstAsync(x => x.EventId == evt.Id && !x.DeleteMark); var max = limit?.RefereeLimit ?? 0; - var applied = await _applicationRepository.AsQueryable() - .Where(x => x.EventId == evt.Id && x.ApplicationStatus != 4 && x.Status == AvailabilityStatus.Enable) - .CountAsync(); - var appliedByMe = await _applicationRepository.IsAnyAsync(x => - x.EventId == evt.Id && x.RefereeUserId == userId && x.ApplicationStatus != 4); + var applicationsQuery = _applicationRepository.AsQueryable(); + if (lockForUpdate) + { + // Apply 由 UnitOfWork 包裹。更新锁和范围锁会把同一赛事的“统计名额并新增报名”串行化, + // 防止最后一个名额被并发请求同时占用。 + applicationsQuery = applicationsQuery.With("UPDLOCK, HOLDLOCK"); + } + + var activeApplications = await applicationsQuery + .Where(x => x.EventId == evt.Id && + (x.ApplicationStatus == 1 || x.ApplicationStatus == 2) && + x.Status == AvailabilityStatus.Enable && !x.DeleteMark) + .Select(x => new { x.Id, x.RefereeUserId }) + .ToListAsync(); + var applied = activeApplications.Count; + var appliedByMe = activeApplications.Any(x => x.RefereeUserId == userId); + var conflictingEvent = appliedByMe + ? null + : await GetConflictingCommittedEventAsync(userId, refereeId, evt); var isFull = max > 0 && applied >= max && !appliedByMe; - var canApply = evt.EventStatus == EventStatus.Enrolling && !isFull && !appliedByMe; + var eventStatus = GetEffectiveEventStatus(evt); + var canApply = eventStatus == EventStatus.Enrolling && !isFull && !appliedByMe && conflictingEvent == null; string message; if (appliedByMe) message = "已报名执裁当前赛事"; else if (isFull) message = "当前赛事裁判报名名额已满"; - else if (evt.EventStatus != EventStatus.Enrolling) message = "当前赛事不在执裁报名时间内"; + else if (eventStatus != EventStatus.Enrolling) message = "当前赛事不在执裁报名时间内"; + else if (conflictingEvent != null) message = BuildTimeConflictMessage(conflictingEvent, evt); else message = max > 0 ? $"裁判名额未满,剩余{max - applied}个" : "裁判名额未满"; return new RefereeQuotaDto @@ -311,6 +339,64 @@ public class RefereeController : IDynamicApiController }; } + /// + /// 查找当前裁判已报名、后台添加或秩序册导入,且执裁时间与目标赛事重叠的赛事。 + /// 时间区间按左闭右开处理,相邻赛事首尾时间相等时不视为冲突。 + /// + private async Task GetConflictingCommittedEventAsync( + long userId, long? refereeId, EventInfo targetEvent) + { + var appliedEventIds = await _applicationRepository.AsQueryable() + .Where(x => x.RefereeUserId == userId && x.EventId != targetEvent.Id && + (x.ApplicationStatus == 1 || x.ApplicationStatus == 2) && + x.Status == AvailabilityStatus.Enable && !x.DeleteMark) + .Select(x => x.EventId) + .Distinct() + .ToListAsync(); + + if (!refereeId.HasValue) + { + refereeId = (await _refereeRepository.GetFirstAsync(x => + x.AppUserId == userId && !x.DeleteMark))?.Id; + } + + if (refereeId.HasValue) + { + var relatedEventIds = await _eventRefereeRepository.AsQueryable() + .Where(x => x.RefereeId == refereeId.Value && x.EventId != targetEvent.Id && !x.DeleteMark) + .Select(x => x.EventId) + .Distinct() + .ToListAsync(); + appliedEventIds = appliedEventIds.Concat(relatedEventIds).Distinct().ToList(); + } + + if (appliedEventIds.Count == 0) return null; + + return await _eventRepository.AsQueryable() + .Where(x => appliedEventIds.Contains(x.Id) && x.Status == AvailabilityStatus.Enable && !x.DeleteMark && + x.MatchStartTime < targetEvent.MatchEndTime && + x.MatchEndTime > targetEvent.MatchStartTime) + .OrderBy(x => x.MatchStartTime) + .FirstAsync(); + } + + private static string BuildTimeConflictMessage(EventInfo appliedEvent, EventInfo targetEvent) => + $"您已参与执裁:\n{appliedEvent.EventName}\n{FormatMatchPeriod(appliedEvent)}\n\n" + + $"当前赛事执裁时间:\n{FormatMatchPeriod(targetEvent)}\n\n" + + "执裁时间存在重叠,暂无法报名。"; + + private static string FormatMatchPeriod(EventInfo evt) + { + var start = evt.MatchStartTime; + var end = evt.MatchEndTime; + return start.Date == end.Date + ? $"{start:M月d日 HH:mm}-{end:HH:mm}" + : $"{start:M月d日 HH:mm}-{end:M月d日 HH:mm}"; + } + + private static EventStatus GetEffectiveEventStatus(EventInfo evt) => + EventStatusUpdateService.CalculateEventStatus(evt, DateTime.Now); + private async Task VerifyCodeAsync(string mobile, string? code) { if (string.IsNullOrWhiteSpace(code)) throw Oops.Bah("请输入验证码"); @@ -321,7 +407,8 @@ public class RefereeController : IDynamicApiController private async Task EnsureRefereeAsync() { var userId = _authManager.UserId; - var user = await _userRepository.GetFirstAsync(x => x.Id == userId); + var user = await _userRepository.GetFirstAsync(x => + x.Id == userId && x.Status == AvailabilityStatus.Enable && !x.DeleteMark); if (user == null) throw Oops.Bah("用户不存在"); if (user.MiniProgramRole != MiniProgramRole.Referee) throw Oops.Bah("当前账号不是裁判身份"); return userId; diff --git a/YD_Event.Application/Judge/JudgeScoringController.cs b/YD_Event.Application/Judge/JudgeScoringController.cs index 856981c..b5bf8d1 100644 --- a/YD_Event.Application/Judge/JudgeScoringController.cs +++ b/YD_Event.Application/Judge/JudgeScoringController.cs @@ -1,5 +1,6 @@ using YD_Event.Application.Auth; using YD_Event.Application.Common; +using YD_Event.Application.Event; using YD_Event.Application.Scoring.Dtos; using System.Text.RegularExpressions; @@ -101,27 +102,64 @@ public class JudgeScoringController : IDynamicApiController { var referee = await EnsureRefereeAsync(); var assignments = await _assignmentRepository.AsQueryable() - .Where(x => x.RefereeId == referee.Id && x.AssignmentStatus == 1) + .Where(x => x.RefereeId == referee.Id && x.AssignmentStatus == 1 && + x.Status == AvailabilityStatus.Enable && !x.DeleteMark) .WhereIF(eventId.HasValue, x => x.EventId == eventId!.Value) .ToListAsync(); if (assignments.Count == 0) return new List(); var sessionIds = assignments.Select(x => x.SessionId).Distinct().ToList(); var sessions = await _sessionRepository.AsQueryable() - .Where(x => sessionIds.Contains(x.Id) && x.SessionStatus != 90).ToListAsync(); + .Where(x => sessionIds.Contains(x.Id) && x.SessionStatus != 90 && + x.Status == AvailabilityStatus.Enable && !x.DeleteMark) + .ToListAsync(); if (sessions.Count == 0) return new List(); var eventIds = sessions.Select(x => x.EventId).Distinct().ToList(); - var events = (await _eventRepository.AsQueryable().Where(x => eventIds.Contains(x.Id)).ToListAsync()) + var events = (await _eventRepository.AsQueryable() + .Where(x => eventIds.Contains(x.Id) && x.Status == AvailabilityStatus.Enable && !x.DeleteMark) + .ToListAsync()) .ToDictionary(x => x.Id); + var now = DateTime.Now; + EventInfo targetEvent; + if (eventId.HasValue) + { + targetEvent = events.GetValueOrDefault(eventId.Value); + } + else + { + targetEvent = events.Values + .Where(x => GetEffectiveEventStatus(x, now) == EventStatus.Ongoing) + .OrderBy(x => x.MatchStartTime) + .ThenBy(x => x.Id) + .FirstOrDefault() + ?? events.Values + .Where(x => GetEffectiveEventStatus(x, now) != EventStatus.Ended && + x.MatchStartTime > now) + .OrderBy(x => x.MatchStartTime) + .ThenBy(x => x.Id) + .FirstOrDefault(); + } + + if (targetEvent == null) return new List(); + + sessions = sessions.Where(x => x.EventId == targetEvent.Id).ToList(); + assignments = assignments.Where(x => x.EventId == targetEvent.Id).ToList(); + sessionIds = sessions.Select(x => x.Id).ToList(); + var venues = (await _venueRepository.AsQueryable() - .Where(x => sessions.Select(s => s.VenueId).Contains(x.Id)).ToListAsync()).ToDictionary(x => x.Id); + .Where(x => sessions.Select(s => s.VenueId).Contains(x.Id) && + x.Status == AvailabilityStatus.Enable && !x.DeleteMark) + .ToListAsync()).ToDictionary(x => x.Id); var eventProjects = (await _eventProjectRepository.AsQueryable() - .Where(x => sessions.Select(s => s.EventProjectId).Contains(x.Id)).ToListAsync()).ToDictionary(x => x.Id); + .Where(x => sessions.Select(s => s.EventProjectId).Contains(x.Id) && !x.DeleteMark) + .ToListAsync()).ToDictionary(x => x.Id); var projects = (await _projectRepository.AsQueryable() - .Where(x => eventProjects.Values.Select(e => e.ProjectId).Contains(x.Id)).ToListAsync()).ToDictionary(x => x.Id); + .Where(x => eventProjects.Values.Select(e => e.ProjectId).Contains(x.Id) && !x.DeleteMark) + .ToListAsync()).ToDictionary(x => x.Id); var rules = (await _ruleRepository.AsQueryable() - .Where(x => sessions.Select(s => s.ScoreRuleId).Contains(x.Id)).ToListAsync()).ToDictionary(x => x.Id); + .Where(x => sessions.Select(s => s.ScoreRuleId).Contains(x.Id) && !x.DeleteMark) + .ToListAsync()).ToDictionary(x => x.Id); sessions = sessions .OrderBy(x => eventProjects.TryGetValue(x.EventProjectId, out var eventProject) @@ -131,15 +169,20 @@ public class JudgeScoringController : IDynamicApiController .ThenBy(x => eventProjects.TryGetValue(x.EventProjectId, out var eventProject) ? eventProject.ProjectId : long.MaxValue) - .ThenBy(x => x.StartTime ?? DateTime.MaxValue) + .ThenBy(x => GetArrangementSequence(x.SessionNo)) + .ThenBy(x => venues.TryGetValue(x.VenueId, out var venue) ? venue.Sort : int.MaxValue) .ThenBy(x => x.SessionNo) + .ThenBy(x => x.Id) .ToList(); var entries = await _entryRepository.AsQueryable() - .Where(x => sessionIds.Contains(x.SessionId) && !x.IsVoided).ToListAsync(); + .Where(x => sessionIds.Contains(x.SessionId) && !x.IsVoided && + x.Status == AvailabilityStatus.Enable && !x.DeleteMark) + .ToListAsync(); var mySheets = await _sheetRepository.AsQueryable() .Where(x => sessionIds.Contains(x.SessionId) && x.RefereeId == referee.Id - && x.IsCurrent && x.SheetStatus >= EventScoreCalculationService.SheetStatusSubmitted).ToListAsync(); + && x.IsCurrent && !x.DeleteMark + && x.SheetStatus >= EventScoreCalculationService.SheetStatusSubmitted).ToListAsync(); var rows = new List(); foreach (var session in sessions) @@ -286,7 +329,9 @@ public class JudgeScoringController : IDynamicApiController : await _detailRepository.GetListAsync(x => x.ScoreSheetId == source.Id); var inputs = details.Select(x => new ScoreItemInputValue { - ItemCode = x.ItemCode, InputValue = x.InputValue, CountValue = x.CountValue + ItemCode = x.ItemCode, + InputValue = x.InputValue, + CountValue = x.CountValue }).ToList(); // 草稿允许留空,因此按非必填口径试算,只汇总已录入的部分。 var preview = ScoreSheetCalculationService.Calculate(definitions, inputs, ctx.Config, false); @@ -538,7 +583,9 @@ public class JudgeScoringController : IDynamicApiController IsDisqualified = outcome.IsDisqualified, Symbols = (outcome.Aggregate?.Symbols ?? new List()).Select(symbol => new JudgeSymbolScoreDto { - Symbol = symbol.Symbol, Label = symbol.Label, Sign = symbol.Sign, + Symbol = symbol.Symbol, + Label = symbol.Label, + Sign = symbol.Sign, Value = ctx.Config.Format(symbol.Value), Judges = symbol.Judges.Select(judge => new JudgeSymbolJudgeDto { @@ -683,12 +730,18 @@ public class JudgeScoringController : IDynamicApiController : (await _correctionRepository.GetListAsync(x => correctionIds.Contains(x.Id))).ToDictionary(x => x.Id); return messages.Select(x => new JudgeMessageDto { - Id = x.Id, EventId = x.EventId, MessageType = x.MessageType, Title = x.Title, Content = x.Content, - RelationType = x.RelationType, RelationId = x.RelationId, + Id = x.Id, + EventId = x.EventId, + MessageType = x.MessageType, + Title = x.Title, + Content = x.Content, + RelationType = x.RelationType, + RelationId = x.RelationId, SessionId = x.RelationId.HasValue ? corrections.GetValueOrDefault(x.RelationId.Value)?.SessionId : null, SessionEntryId = x.RelationId.HasValue ? corrections.GetValueOrDefault(x.RelationId.Value)?.SessionEntryId : null, ScoreSheetId = x.RelationId.HasValue ? corrections.GetValueOrDefault(x.RelationId.Value)?.OriginalScoreSheetId : null, - IsRead = x.IsRead, CreatedTime = x.CreatedTime + IsRead = x.IsRead, + CreatedTime = x.CreatedTime }).ToList(); } @@ -770,12 +823,22 @@ public class JudgeScoringController : IDynamicApiController if (current.Count > 0) await _attachmentRepository.UpdateRangeAsync(current); var attachment = new EventJudgeScoreAttachment { - Id = _idGenerator.NextId(), EventId = sheet.EventId, SessionId = sheet.SessionId, - SessionEntryId = sheet.SessionEntryId, ScoreSheetId = sheet.Id, - JudgeAssignmentId = sheet.JudgeAssignmentId, RefereeId = referee.Id, - AttachmentType = AnnotationAttachmentType, FileName = Path.GetFileName(file.FileName), - FileUrl = url, FileSize = file.Length, ContentType = "image/png", IdempotencyKey = key, - IsCurrent = true, CreatedUserId = _authManager.UserId, CreatedTime = now + Id = _idGenerator.NextId(), + EventId = sheet.EventId, + SessionId = sheet.SessionId, + SessionEntryId = sheet.SessionEntryId, + ScoreSheetId = sheet.Id, + JudgeAssignmentId = sheet.JudgeAssignmentId, + RefereeId = referee.Id, + AttachmentType = AnnotationAttachmentType, + FileName = Path.GetFileName(file.FileName), + FileUrl = url, + FileSize = file.Length, + ContentType = "image/png", + IdempotencyKey = key, + IsCurrent = true, + CreatedUserId = _authManager.UserId, + CreatedTime = now }; await _attachmentRepository.InsertAsync(attachment); return ToAttachmentDto(attachment); @@ -1077,7 +1140,9 @@ public class JudgeScoringController : IDynamicApiController private static List ToInputValues(List items) => items.Select(x => new ScoreItemInputValue { - ItemCode = x.ItemCode, InputValue = x.InputValue, CountValue = x.CountValue + ItemCode = x.ItemCode, + InputValue = x.InputValue, + CountValue = x.CountValue }).ToList(); /// @@ -1098,7 +1163,10 @@ public class JudgeScoringController : IDynamicApiController RequiredMissingCount = calculation.RequiredMissingCount, Steps = calculation.Steps.Select(x => new JudgeScoreStepDto { - Code = x.Code, Label = x.Label, Formula = x.Formula, Value = x.Value + Code = x.Code, + Label = x.Label, + Formula = x.Formula, + Value = x.Value }).ToList() }; @@ -1107,8 +1175,11 @@ public class JudgeScoringController : IDynamicApiController /// private static JudgeRoleProgressDto ToProgressDto(EntryRoleProgress progress) => new() { - RoleType = progress.RoleType, RoleName = progress.RoleName, Symbol = progress.Symbol, - ExpectedCount = progress.ExpectedCount, SubmittedCount = progress.SubmittedCount, + RoleType = progress.RoleType, + RoleName = progress.RoleName, + Symbol = progress.Symbol, + ExpectedCount = progress.ExpectedCount, + SubmittedCount = progress.SubmittedCount, PendingJudges = progress.PendingJudges }; @@ -1186,6 +1257,38 @@ public class JudgeScoringController : IDynamicApiController /// private const int EntryStatusWithdrawn = 80; + /// + /// 获取场次在赛事编排中的顺序。自动编排和秩序册导入生成的场次号末段均保存场次顺序; + /// 速度项目只有项目和场地段,同一项目内按场地顺序排列。 + /// + private static int GetArrangementSequence(string? sessionNo) + { + if (string.IsNullOrWhiteSpace(sessionNo)) return int.MaxValue; + + var match = Regex.Match(sessionNo, @"^EP\d+-(?:V)?\d+(?:-(?\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 + : 0; + } + + /// + /// 获取用于裁判场次选择的赛事实时状态,避免后台状态同步延迟导致比赛中场次不可见。 + /// + private static EventStatus GetEffectiveEventStatus(EventInfo eventInfo, DateTime now) + { + var calculatedStatus = EventStatusUpdateService.CalculateEventStatus(eventInfo, now); + if (calculatedStatus == EventStatus.Ended || eventInfo.EventStatus == EventStatus.Ended) + return EventStatus.Ended; + if (calculatedStatus == EventStatus.Ongoing || eventInfo.EventStatus == EventStatus.Ongoing) + return EventStatus.Ongoing; + + return calculatedStatus; + } + /// /// 场次状态名称 ///