From 5ce5a837dcbe64f1b7c7f04bc0f62fb349515975 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Thu, 17 Sep 2026 14:48:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E8=A3=81=E5=88=A4=E8=AF=84?= =?UTF-8?q?=E5=88=86=E8=B5=9B=E4=BA=8B=E7=8A=B6=E6=80=81=E5=88=A4=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Event/JudgeAssignmentService.cs | 14 ++-- .../Judge/CompetitionStagePolicy.cs | 64 +++++++++++++++++++ .../Judge/JudgeScoringController.cs | 52 +++++++++------ 3 files changed, 107 insertions(+), 23 deletions(-) diff --git a/YD_Event.Application/Event/JudgeAssignmentService.cs b/YD_Event.Application/Event/JudgeAssignmentService.cs index ffb0119..ef9460b 100644 --- a/YD_Event.Application/Event/JudgeAssignmentService.cs +++ b/YD_Event.Application/Event/JudgeAssignmentService.cs @@ -163,15 +163,18 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient { throw Oops.Bah("场次不存在或已取消"); } + var eventInfo = await _eventRepository.GetFirstAsync(x => x.Id == input.EventId); + if (eventInfo == null) throw Oops.Bah("赛事不存在"); var eventSessions = await _sessionRepository.AsQueryable() .Where(x => x.EventId == input.EventId && x.SessionStatus != 90) .ToListAsync(); var lockedSession = eventSessions.FirstOrDefault( - x => !CompetitionStagePolicy.CanModifyAssignment(x.SessionStatus)); + x => !CompetitionStagePolicy.CanModifyAssignment(x.SessionStatus, eventInfo.EventStatus)); if (lockedSession != null) { - throw Oops.Bah(CompetitionStagePolicy.AssignmentLockReason(lockedSession.SessionStatus)); + throw Oops.Bah(CompetitionStagePolicy.AssignmentLockReason( + lockedSession.SessionStatus, eventInfo.EventStatus)); } if (input.Assignments.Any(x => string.IsNullOrWhiteSpace(x.JudgeRole) || (x.EventRefereeId <= 0 && x.RefereeId <= 0))) @@ -219,6 +222,8 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient [HttpPost("submit"), DisplayName("提交裁判分配"), UnitOfWork] public async Task Submit([FromBody] SubmitJudgeAssignmentsInput input) { + var eventInfo = await _eventRepository.GetFirstAsync(x => x.Id == input.EventId); + if (eventInfo == null) throw Oops.Bah("赛事不存在"); var sessions = await _sessionRepository.AsQueryable() .Where(x => x.EventId == input.EventId && x.SessionStatus != 90) .ToListAsync(); @@ -228,10 +233,11 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient } var lockedSession = sessions.FirstOrDefault( - x => !CompetitionStagePolicy.CanModifyAssignment(x.SessionStatus)); + x => !CompetitionStagePolicy.CanModifyAssignment(x.SessionStatus, eventInfo.EventStatus)); if (lockedSession != null) { - throw Oops.Bah(CompetitionStagePolicy.AssignmentLockReason(lockedSession.SessionStatus)); + throw Oops.Bah(CompetitionStagePolicy.AssignmentLockReason( + lockedSession.SessionStatus, eventInfo.EventStatus)); } // 重提时只有修改过的场次是草稿,其余场次仍是已提交;完整性校验必须同时考虑两者。 diff --git a/YD_Event.Application/Judge/CompetitionStagePolicy.cs b/YD_Event.Application/Judge/CompetitionStagePolicy.cs index 2e686aa..5e50677 100644 --- a/YD_Event.Application/Judge/CompetitionStagePolicy.cs +++ b/YD_Event.Application/Judge/CompetitionStagePolicy.cs @@ -1,5 +1,7 @@ namespace YD_Event.Application.Scoring; +using YD_Event.Core.Enum; + /// /// 场次阶段与操作权限的统一判定。管理端分配和裁判端评分必须共用同一口径。 /// @@ -27,6 +29,14 @@ public static class CompetitionStagePolicy /// 允许修改返回 ,否则返回 。 public static bool CanModifyAssignment(int sessionStatus) => sessionStatus is 0 or 10; + /// + /// 结合赛事状态判断是否允许修改裁判分配。 + /// 赛事进入比赛中或已结束后,即使场次仍停留在草稿/就绪状态也必须锁定。 + /// + public static bool CanModifyAssignment(int sessionStatus, EventStatus eventStatus) => + CanModifyAssignment(sessionStatus) + && eventStatus is not EventStatus.Ongoing and not EventStatus.Ended; + /// /// 判断指定场次状态是否允许评分。 /// @@ -34,6 +44,16 @@ public static class CompetitionStagePolicy /// 允许评分返回 ,否则返回 。 public static bool CanScore(int sessionStatus) => sessionStatus is 20 or 30; + /// + /// 结合赛事状态判断指定场次是否允许评分。 + /// 已提交裁判分配的场次处于“就绪”状态;赛事进入比赛中后,该状态即可评分。 + /// + /// 场次状态。 + /// 赛事状态。 + /// 允许评分返回 ,否则返回 。 + public static bool CanScore(int sessionStatus, EventStatus eventStatus) => + eventStatus == EventStatus.Ongoing && sessionStatus is 10 or 20 or 30; + /// /// 根据场次状态获取赛事阶段编码。 /// @@ -46,6 +66,18 @@ public static class CompetitionStagePolicy _ => PHASE_POST_MATCH }; + /// + /// 结合赛事状态获取赛事阶段编码。 + /// + public static int PhaseOf(int sessionStatus, EventStatus eventStatus) + { + if (eventStatus == EventStatus.Ended) return PHASE_POST_MATCH; + if (eventStatus != EventStatus.Ongoing) return PHASE_PRE_MATCH; + return sessionStatus is 0 ? PHASE_PRE_MATCH : PhaseOf(sessionStatus) == PHASE_POST_MATCH + ? PHASE_POST_MATCH + : PHASE_IN_PROGRESS; + } + /// /// 根据场次状态获取赛事阶段名称。 /// @@ -58,6 +90,17 @@ public static class CompetitionStagePolicy _ => "赛后" }; + /// + /// 结合赛事状态获取赛事阶段名称。 + /// + public static string PhaseName(int sessionStatus, EventStatus eventStatus) => + PhaseOf(sessionStatus, eventStatus) switch + { + PHASE_PRE_MATCH => "赛前", + PHASE_IN_PROGRESS => "赛中", + _ => "赛后" + }; + /// /// 获取指定场次状态下禁止评分的提示信息。 /// @@ -73,6 +116,17 @@ public static class CompetitionStagePolicy _ => "当前赛事状态不可评分" }; + /// + /// 结合赛事状态获取禁止评分的提示信息。 + /// + public static string ScoreLockReason(int sessionStatus, EventStatus eventStatus) + { + if (eventStatus == EventStatus.Ended) return "赛事已结束,无法评分"; + if (eventStatus != EventStatus.Ongoing) return "赛事未开始"; + + return ScoreLockReason(sessionStatus); + } + /// /// 获取指定场次状态下禁止修改裁判分配的提示信息。 /// @@ -85,4 +139,14 @@ public static class CompetitionStagePolicy 90 => "赛事已取消,不能修改裁判分配", _ => "当前赛事状态不能修改裁判分配" }; + + /// + /// 结合赛事状态获取禁止修改裁判分配的提示信息。 + /// + public static string AssignmentLockReason(int sessionStatus, EventStatus eventStatus) + { + if (eventStatus == EventStatus.Ended) return "赛事已结束,不能修改裁判分配"; + if (eventStatus == EventStatus.Ongoing) return "赛事已开始,不能修改裁判分配"; + return AssignmentLockReason(sessionStatus); + } } diff --git a/YD_Event.Application/Judge/JudgeScoringController.cs b/YD_Event.Application/Judge/JudgeScoringController.cs index 74a96c9..aa6c78c 100644 --- a/YD_Event.Application/Judge/JudgeScoringController.cs +++ b/YD_Event.Application/Judge/JudgeScoringController.cs @@ -181,19 +181,20 @@ public class JudgeScoringController : IDynamicApiController await GetMyAssignmentAsync(sessionId, referee.Id); var session = await _sessionRepository.GetFirstAsync(x => x.Id == sessionId); if (session == null) throw Oops.Bah("比赛场次不存在"); + var eventInfo = await GetEventAsync(session.EventId); - var canScore = CompetitionStagePolicy.CanScore(session.SessionStatus); + var canScore = CompetitionStagePolicy.CanScore(session.SessionStatus, eventInfo.EventStatus); return new JudgeEventStatusDto { EventId = session.EventId, SessionId = session.Id, SessionStatus = session.SessionStatus, SessionStatusName = SessionStatusName(session.SessionStatus), - Phase = CompetitionStagePolicy.PhaseOf(session.SessionStatus), - PhaseName = CompetitionStagePolicy.PhaseName(session.SessionStatus), + Phase = CompetitionStagePolicy.PhaseOf(session.SessionStatus, eventInfo.EventStatus), + PhaseName = CompetitionStagePolicy.PhaseName(session.SessionStatus, eventInfo.EventStatus), CanScore = canScore, - CanModifyAssignment = CompetitionStagePolicy.CanModifyAssignment(session.SessionStatus), - Message = canScore ? null : CompetitionStagePolicy.ScoreLockReason(session.SessionStatus) + CanModifyAssignment = CompetitionStagePolicy.CanModifyAssignment(session.SessionStatus, eventInfo.EventStatus), + Message = canScore ? null : CompetitionStagePolicy.ScoreLockReason(session.SessionStatus, eventInfo.EventStatus) }; } @@ -255,7 +256,7 @@ public class JudgeScoringController : IDynamicApiController public async Task GetSheet(long sessionEntryId) { var referee = await EnsureRefereeAsync(); - var (entry, ctx, assignment, roleType) = await LoadSheetContextAsync(sessionEntryId, referee.Id); + var (entry, ctx, eventInfo, assignment, roleType) = await LoadSheetContextAsync(sessionEntryId, referee.Id); var definitions = ScoreSheetCalculationService.ItemsOf(ctx, roleType); if (definitions.Count == 0) throw Oops.Bah($"计分规则“{ctx.Rule.RuleName}”未配置{JudgeRoleSchemaService.RoleNameOf(ctx.RoleSlots, roleType)}的评分项"); @@ -279,12 +280,11 @@ public class JudgeScoringController : IDynamicApiController // 草稿允许留空,因此按非必填口径试算,只汇总已录入的部分。 var preview = ScoreSheetCalculationService.Calculate(definitions, inputs, ctx.Config, false); - var stageAllowsScoring = CompetitionStagePolicy.CanScore(ctx.Session.SessionStatus); + var stageAllowsScoring = CompetitionStagePolicy.CanScore(ctx.Session.SessionStatus, eventInfo.EventStatus); var scoreSheetAllowsEditing = submitted == null || correction != null; var canEdit = stageAllowsScoring && scoreSheetAllowsEditing; var members = await LoadMemberNamesAsync(entry); var venue = await _venueRepository.GetFirstAsync(x => x.Id == ctx.Session.VenueId); - var eventInfo = await _eventRepository.GetFirstAsync(x => x.Id == ctx.Session.EventId); var nextVersion = (submitted?.SheetVersion ?? 0) + 1; return new JudgeScoreSheetOutput @@ -297,7 +297,7 @@ public class JudgeScoringController : IDynamicApiController SheetStatus = source?.SheetStatus ?? SheetStatusDraft, CanEdit = canEdit, LockReason = canEdit ? null : !stageAllowsScoring - ? CompetitionStagePolicy.ScoreLockReason(ctx.Session.SessionStatus) + ? CompetitionStagePolicy.ScoreLockReason(ctx.Session.SessionStatus, eventInfo.EventStatus) : "已提交评分,如需修改请先申请成绩修正", IsCorrection = correction != null, // 幂等键随评分单版本递增,弱网重复提交同一版本只会落一张评分单。 @@ -372,8 +372,8 @@ public class JudgeScoringController : IDynamicApiController public async Task SaveDraft([FromBody] SaveJudgeScoreInput input) { var referee = await EnsureRefereeAsync(); - var (entry, ctx, assignment, roleType) = await LoadSheetContextAsync(input.SessionEntryId, referee.Id); - EnsureCanScore(ctx.Session.SessionStatus); + var (entry, ctx, eventInfo, assignment, roleType) = await LoadSheetContextAsync(input.SessionEntryId, referee.Id); + EnsureCanScore(ctx.Session.SessionStatus, eventInfo.EventStatus); var definitions = ScoreSheetCalculationService.ItemsOf(ctx, roleType); if (definitions.Count == 0) throw Oops.Bah($"计分规则“{ctx.Rule.RuleName}”未配置{JudgeRoleSchemaService.RoleNameOf(ctx.RoleSlots, roleType)}的评分项"); @@ -415,8 +415,8 @@ public class JudgeScoringController : IDynamicApiController public async Task Submit([FromBody] SubmitJudgeScoreInput input) { var referee = await EnsureRefereeAsync(); - var (entry, ctx, assignment, roleType) = await LoadSheetContextAsync(input.SessionEntryId, referee.Id); - EnsureCanScore(ctx.Session.SessionStatus); + var (entry, ctx, eventInfo, assignment, roleType) = await LoadSheetContextAsync(input.SessionEntryId, referee.Id); + EnsureCanScore(ctx.Session.SessionStatus, eventInfo.EventStatus); if (entry.IsVoided) throw Oops.Bah("该出场成绩已作废,无法提交评分"); if (entry.EntryStatus == EntryStatusWithdrawn) throw Oops.Bah("该运动员已退赛,无需评分"); var definitions = ScoreSheetCalculationService.ItemsOf(ctx, roleType); @@ -616,7 +616,8 @@ public class JudgeScoringController : IDynamicApiController if (sheet == null) throw Oops.Bah("评分单不存在"); var session = await _sessionRepository.GetFirstAsync(x => x.Id == sheet.SessionId); if (session == null) throw Oops.Bah("比赛场次不存在"); - EnsureCanScore(session.SessionStatus); + var eventInfo = await GetEventAsync(session.EventId); + EnsureCanScore(session.SessionStatus, eventInfo.EventStatus); if (!sheet.IsCurrent || sheet.SheetStatus < EventScoreCalculationService.SheetStatusSubmitted) throw Oops.Bah("该评分单尚未提交,可直接修改后提交"); if (sheet.SheetStatus == SheetStatusConfirmed) throw Oops.Bah("成绩已确认,请联系赛事管理员处理"); @@ -810,10 +811,21 @@ public class JudgeScoringController : IDynamicApiController /// 校验当前场次状态是否允许评分。 /// /// 场次状态。 - private static void EnsureCanScore(int sessionStatus) + /// 赛事状态。 + private static void EnsureCanScore(int sessionStatus, EventStatus eventStatus) { - if (!CompetitionStagePolicy.CanScore(sessionStatus)) - throw Oops.Bah(CompetitionStagePolicy.ScoreLockReason(sessionStatus)); + if (!CompetitionStagePolicy.CanScore(sessionStatus, eventStatus)) + throw Oops.Bah(CompetitionStagePolicy.ScoreLockReason(sessionStatus, eventStatus)); + } + + /// + /// 获取场次所属赛事,确保评分权限校验与后台展示使用同一赛事状态。 + /// + private async Task GetEventAsync(long eventId) + { + var eventInfo = await _eventRepository.GetFirstAsync(x => x.Id == eventId); + if (eventInfo == null) throw Oops.Bah("赛事不存在"); + return eventInfo; } /// @@ -830,16 +842,18 @@ public class JudgeScoringController : IDynamicApiController /// /// 加载评分表所需的出场、计分上下文与我的裁判席位 /// - private async Task<(EventSessionEntry Entry, ScoringContext Context, EventJudgeAssignment Assignment, string RoleType)> + private async Task<(EventSessionEntry Entry, ScoringContext Context, EventInfo Event, + EventJudgeAssignment Assignment, string RoleType)> LoadSheetContextAsync(long sessionEntryId, long refereeId) { var entry = await _entryRepository.GetFirstAsync(x => x.Id == sessionEntryId); if (entry == null) throw Oops.Bah("出场记录不存在"); var assignment = await GetMyAssignmentAsync(entry.SessionId, refereeId); var ctx = await _calculationService.LoadContextAsync(entry.SessionId); + var eventInfo = await GetEventAsync(ctx.Session.EventId); var roleType = JudgeRoleSchemaService.NormalizeRoleType( string.IsNullOrWhiteSpace(assignment.JudgeRoleType) ? assignment.JudgeRole : assignment.JudgeRoleType); - return (entry, ctx, assignment, roleType); + return (entry, ctx, eventInfo, assignment, roleType); } ///