完善裁判评分赛事状态判断

This commit is contained in:
马梓彭 2026-09-17 14:48:11 +08:00
parent 55e6fa71f3
commit 5ce5a837dc
3 changed files with 107 additions and 23 deletions

View File

@ -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));
}
// 重提时只有修改过的场次是草稿,其余场次仍是已提交;完整性校验必须同时考虑两者。

View File

@ -1,5 +1,7 @@
namespace YD_Event.Application.Scoring;
using YD_Event.Core.Enum;
/// <summary>
/// 场次阶段与操作权限的统一判定。管理端分配和裁判端评分必须共用同一口径。
/// </summary>
@ -27,6 +29,14 @@ public static class CompetitionStagePolicy
/// <returns>允许修改返回 <see langword="true"/>,否则返回 <see langword="false"/>。</returns>
public static bool CanModifyAssignment(int sessionStatus) => sessionStatus is 0 or 10;
/// <summary>
/// 结合赛事状态判断是否允许修改裁判分配。
/// 赛事进入比赛中或已结束后,即使场次仍停留在草稿/就绪状态也必须锁定。
/// </summary>
public static bool CanModifyAssignment(int sessionStatus, EventStatus eventStatus) =>
CanModifyAssignment(sessionStatus)
&& eventStatus is not EventStatus.Ongoing and not EventStatus.Ended;
/// <summary>
/// 判断指定场次状态是否允许评分。
/// </summary>
@ -34,6 +44,16 @@ public static class CompetitionStagePolicy
/// <returns>允许评分返回 <see langword="true"/>,否则返回 <see langword="false"/>。</returns>
public static bool CanScore(int sessionStatus) => sessionStatus is 20 or 30;
/// <summary>
/// 结合赛事状态判断指定场次是否允许评分。
/// 已提交裁判分配的场次处于“就绪”状态;赛事进入比赛中后,该状态即可评分。
/// </summary>
/// <param name="sessionStatus">场次状态。</param>
/// <param name="eventStatus">赛事状态。</param>
/// <returns>允许评分返回 <see langword="true"/>,否则返回 <see langword="false"/>。</returns>
public static bool CanScore(int sessionStatus, EventStatus eventStatus) =>
eventStatus == EventStatus.Ongoing && sessionStatus is 10 or 20 or 30;
/// <summary>
/// 根据场次状态获取赛事阶段编码。
/// </summary>
@ -46,6 +66,18 @@ public static class CompetitionStagePolicy
_ => PHASE_POST_MATCH
};
/// <summary>
/// 结合赛事状态获取赛事阶段编码。
/// </summary>
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;
}
/// <summary>
/// 根据场次状态获取赛事阶段名称。
/// </summary>
@ -58,6 +90,17 @@ public static class CompetitionStagePolicy
_ => "赛后"
};
/// <summary>
/// 结合赛事状态获取赛事阶段名称。
/// </summary>
public static string PhaseName(int sessionStatus, EventStatus eventStatus) =>
PhaseOf(sessionStatus, eventStatus) switch
{
PHASE_PRE_MATCH => "赛前",
PHASE_IN_PROGRESS => "赛中",
_ => "赛后"
};
/// <summary>
/// 获取指定场次状态下禁止评分的提示信息。
/// </summary>
@ -73,6 +116,17 @@ public static class CompetitionStagePolicy
_ => "当前赛事状态不可评分"
};
/// <summary>
/// 结合赛事状态获取禁止评分的提示信息。
/// </summary>
public static string ScoreLockReason(int sessionStatus, EventStatus eventStatus)
{
if (eventStatus == EventStatus.Ended) return "赛事已结束,无法评分";
if (eventStatus != EventStatus.Ongoing) return "赛事未开始";
return ScoreLockReason(sessionStatus);
}
/// <summary>
/// 获取指定场次状态下禁止修改裁判分配的提示信息。
/// </summary>
@ -85,4 +139,14 @@ public static class CompetitionStagePolicy
90 => "赛事已取消,不能修改裁判分配",
_ => "当前赛事状态不能修改裁判分配"
};
/// <summary>
/// 结合赛事状态获取禁止修改裁判分配的提示信息。
/// </summary>
public static string AssignmentLockReason(int sessionStatus, EventStatus eventStatus)
{
if (eventStatus == EventStatus.Ended) return "赛事已结束,不能修改裁判分配";
if (eventStatus == EventStatus.Ongoing) return "赛事已开始,不能修改裁判分配";
return AssignmentLockReason(sessionStatus);
}
}

View File

@ -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<JudgeScoreSheetOutput> 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<JudgeScorePreviewDto> 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<SubmitJudgeScoreOutput> 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
/// 校验当前场次状态是否允许评分。
/// </summary>
/// <param name="sessionStatus">场次状态。</param>
private static void EnsureCanScore(int sessionStatus)
/// <param name="eventStatus">赛事状态。</param>
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));
}
/// <summary>
/// 获取场次所属赛事,确保评分权限校验与后台展示使用同一赛事状态。
/// </summary>
private async Task<EventInfo> GetEventAsync(long eventId)
{
var eventInfo = await _eventRepository.GetFirstAsync(x => x.Id == eventId);
if (eventInfo == null) throw Oops.Bah("赛事不存在");
return eventInfo;
}
/// <summary>
@ -830,16 +842,18 @@ public class JudgeScoringController : IDynamicApiController
/// <summary>
/// 加载评分表所需的出场、计分上下文与我的裁判席位
/// </summary>
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);
}
/// <summary>