裁判报名分配限制人数

This commit is contained in:
马梓彭 2026-08-18 00:22:59 +08:00
parent 33a289568e
commit de1609e2f1
2 changed files with 127 additions and 24 deletions

View File

@ -36,10 +36,31 @@ public class JudgeAssignmentProjectOutput
public long EventProjectId { get; set; }
public string ProjectName { get; set; } = null!;
public string RuleName { get; set; } = null!;
public string JudgeRoleSchemaJson { get; set; } = "[]";
/// <summary>裁判大类分组(难度裁判、表现裁判、主裁),由计分规则的裁判席位配置解析而来。</summary>
public List<JudgeRoleSectionOutput> JudgeRoleSections { get; set; } = new();
public List<JudgeAssignmentSessionOutput> Sessions { get; set; } = new();
}
/// <summary>裁判大类分组。</summary>
public class JudgeRoleSectionOutput
{
public string Title { get; set; } = null!;
public List<JudgeRoleSlotOutput> Slots { get; set; } = new();
}
/// <summary>裁判类型(如 DP 力量体操、DM 多摇、P 表现、T 主裁),一个类型可分配多名裁判。</summary>
public class JudgeRoleSlotOutput
{
public string Code { get; set; } = null!;
public string Name { get; set; } = null!;
/// <summary>该类型至少需要分配的裁判人数。</summary>
public int MinCount { get; set; }
/// <summary>该类型最多可分配的裁判人数,null 表示不限人数(成绩按平均值计算)。</summary>
public int? MaxCount { get; set; }
/// <summary>执裁该类型所需具备的裁判角色位标记。</summary>
public int RoleFlag { get; set; }
}
public class JudgeAssignmentSessionOutput
{
public long SessionId { get; set; }
@ -52,7 +73,10 @@ public class JudgeAssignmentSessionOutput
public class JudgeAssignmentItemOutput
{
public long EventRefereeId { get; set; }
/// <summary>裁判席位编码(同一场次内唯一,如 DP1、DP2、T)。</summary>
public string JudgeRole { get; set; } = null!;
/// <summary>裁判类型编码(如 DP、DM、DR、P、T),对应计分规则中的裁判类型。</summary>
public string JudgeRoleType { get; set; } = null!;
public string JudgeRoleName { get; set; } = null!;
public long RefereeId { get; set; }
public string RefereeName { get; set; } = null!;
@ -68,6 +92,7 @@ public class SaveJudgeAssignmentsInput
public class SaveJudgeAssignmentItem
{
/// <summary>裁判类型编码;同一类型可重复出现,表示该类型分配了多名裁判。</summary>
public string JudgeRole { get; set; } = null!;
public long EventRefereeId { get; set; }
public long RefereeId { get; set; }

View File

@ -63,10 +63,22 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient
{
var project = projects.FirstOrDefault(x => x.Id == ep.ProjectId); if (project == null) continue;
var rule = rules.FirstOrDefault(x => x.Id == project.ScoreRuleId);
var group = new JudgeAssignmentProjectOutput { EventProjectId = ep.Id, ProjectName = project.Name, RuleName = rule?.RuleName ?? "未配置计分规则", JudgeRoleSchemaJson = rule?.JudgeRoleSchemaJson ?? "[]" };
var sections = ParseRoleSections(rule?.JudgeRoleSchemaJson);
var slots = sections.SelectMany(x => x.Slots).ToList();
var slotMap = slots.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
var slotOrder = slots.Select((slot, index) => (slot.Code, index)).ToDictionary(x => x.Code, x => x.index, StringComparer.OrdinalIgnoreCase);
var group = new JudgeAssignmentProjectOutput { EventProjectId = ep.Id, ProjectName = project.Name, RuleName = rule?.RuleName ?? "未配置计分规则", JudgeRoleSections = sections };
foreach (var session in sessions.Where(x => x.EventProjectId == ep.Id).OrderBy(x => venueMap.GetValueOrDefault(x.VenueId)?.Sort))
{
var items = assignments.Where(x => x.SessionId == session.Id).Select(x => { var referee = referees.FirstOrDefault(r => r.Id == x.RefereeId); return new JudgeAssignmentItemOutput { EventRefereeId = x.EventRefereeId, JudgeRole = x.JudgeRole, JudgeRoleName = x.JudgeRole, RefereeId = x.RefereeId, RefereeName = referee?.Name ?? "-", Mobile = referee?.Mobile ?? "" }; }).ToList();
var items = assignments.Where(x => x.SessionId == session.Id).Select(x =>
{
var referee = referees.FirstOrDefault(r => r.Id == x.RefereeId);
var roleType = string.IsNullOrWhiteSpace(x.JudgeRoleType) ? NormalizeRoleType(x.JudgeRole) : x.JudgeRoleType!;
return new JudgeAssignmentItemOutput { EventRefereeId = x.EventRefereeId, JudgeRole = x.JudgeRole, JudgeRoleType = roleType,
JudgeRoleName = slotMap.GetValueOrDefault(roleType)?.Name ?? roleType, RefereeId = x.RefereeId,
RefereeName = referee?.Name ?? "-", Mobile = referee?.Mobile ?? "" };
}).OrderBy(x => slotOrder.TryGetValue(x.JudgeRoleType, out var order) ? order : int.MaxValue)
.ThenBy(x => x.JudgeRole, StringComparer.OrdinalIgnoreCase).ToList();
group.Sessions.Add(new JudgeAssignmentSessionOutput { SessionId = session.Id, SessionName = session.SessionName ?? session.SessionNo,
VenueName = venueMap.GetValueOrDefault(session.VenueId)?.VenueName ?? "-", Assigned = items.Count > 0, Assignments = items });
}
@ -90,35 +102,57 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient
if (await _assignmentRepository.IsAnyAsync(x => x.SessionId == input.SessionId && x.AssignmentStatus == 1))
throw Oops.Bah("裁判分配已提交,不能再次修改");
if (input.Assignments.Any(x => string.IsNullOrWhiteSpace(x.JudgeRole) || (x.EventRefereeId <= 0 && x.RefereeId <= 0)))
throw Oops.Bah("裁判岗位未完整分配");
throw Oops.Bah("存在未选择裁判的分配记录");
var rule = await _ruleRepository.GetByIdAsync(session.ScoreRuleId);
var requiredRoles = JArray.Parse(rule?.JudgeRoleSchemaJson ?? "[]").SelectMany(x => x["slots"] ?? new JArray()).Select(x => x["code"]?.ToString()).Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
if (requiredRoles.Count == 0 || requiredRoles.Except(input.Assignments.Select(x => x.JudgeRole)).Any() || input.Assignments.Any(x => !requiredRoles.Contains(x.JudgeRole)))
throw Oops.Bah("请完成计分规则要求的全部裁判岗位分配");
if (input.Assignments.GroupBy(x => x.JudgeRole).Any(x => x.Count() > 1)) throw Oops.Bah("同一岗位只能分配一名裁判");
if (input.Assignments.GroupBy(x => x.EventRefereeId > 0 ? x.EventRefereeId : x.RefereeId).Any(x => x.Count() > 1))
throw Oops.Bah("同一场次一名裁判只能分配一个岗位");
var candidates = await GetEventReferees(input.EventId);
var slots = ParseRoleSections(rule?.JudgeRoleSchemaJson).SelectMany(x => x.Slots).ToList();
if (slots.Count == 0) throw Oops.Bah("当前项目的计分规则未配置裁判类型");
var slotMap = slots.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
// 同一裁判类型可分配多名裁判,先按类型归组再校验人数。
var grouped = new Dictionary<string, List<SaveJudgeAssignmentItem>>(StringComparer.OrdinalIgnoreCase);
foreach (var item in input.Assignments)
{
var candidate = item.EventRefereeId > 0
? candidates.FirstOrDefault(x => x.EventRefereeId == item.EventRefereeId)
: candidates.FirstOrDefault(x => x.RefereeId == item.RefereeId);
if (candidate == null) throw Oops.Bah("存在尚未关联到当前赛事的裁判");
item.EventRefereeId = candidate.EventRefereeId;
item.RefereeId = candidate.RefereeId;
var requiredFlag = RoleFlag(item.JudgeRole);
if (requiredFlag != 0 && (candidate.Roles & requiredFlag) == 0) throw Oops.Bah($"裁判{candidate.Name}不具备岗位{item.JudgeRole}资格");
var roleType = slotMap.TryGetValue(item.JudgeRole, out var matched) ? matched.Code : NormalizeRoleType(item.JudgeRole);
if (!slotMap.ContainsKey(roleType)) throw Oops.Bah($"裁判类型{item.JudgeRole}不属于当前项目的计分规则");
if (!grouped.TryGetValue(roleType, out var list)) grouped[roleType] = list = new List<SaveJudgeAssignmentItem>();
list.Add(item);
}
foreach (var slot in slots)
{
var count = grouped.TryGetValue(slot.Code, out var list) ? list.Count : 0;
if (count < slot.MinCount) throw Oops.Bah($"{slot.Name}至少需要分配 {slot.MinCount} 名裁判");
if (slot.MaxCount.HasValue && count > slot.MaxCount.Value) throw Oops.Bah($"{slot.Name}最多只能分配 {slot.MaxCount.Value} 名裁判");
}
var candidates = await GetEventReferees(input.EventId);
var seats = new List<(JudgeRoleSlotOutput Slot, string SeatCode, EventRefereeOutput Candidate)>();
foreach (var slot in slots)
{
if (!grouped.TryGetValue(slot.Code, out var list)) continue;
var seatNo = 0;
foreach (var item in list)
{
var candidate = item.EventRefereeId > 0
? candidates.FirstOrDefault(x => x.EventRefereeId == item.EventRefereeId)
: candidates.FirstOrDefault(x => x.RefereeId == item.RefereeId);
if (candidate == null) throw Oops.Bah("存在尚未关联到当前赛事的裁判");
if (slot.RoleFlag != 0 && (candidate.Roles & slot.RoleFlag) == 0) throw Oops.Bah($"裁判{candidate.Name}不具备{slot.Name}资格");
seatNo++;
seats.Add((slot, slot.MaxCount == 1 ? slot.Code : $"{slot.Code}{seatNo}", candidate));
}
}
var repeated = seats.GroupBy(x => x.Candidate.EventRefereeId).FirstOrDefault(x => x.Count() > 1);
if (repeated != null) throw Oops.Bah($"裁判{repeated.First().Candidate.Name}在同一场次只能担任一个裁判席位");
await _assignmentRepository.SoftDeleteAsync(x => x.SessionId == input.SessionId && x.AssignmentStatus == 0);
var now = DateTime.Now;
var refereeIds = input.Assignments.Select(x => x.RefereeId).Distinct().ToList();
var refereeIds = seats.Select(x => x.Candidate.RefereeId).Distinct().ToList();
var refereeUsers = (await _refereeRepository.AsQueryable()
.Where(x => refereeIds.Contains(x.Id)).ToListAsync())
.ToDictionary(x => x.Id, x => x.AppUserId);
await _assignmentRepository.InsertRangeAsync(input.Assignments.Select(x => new EventJudgeAssignment { Id = _idGenerator.NextId(), EventId = input.EventId,
SessionId = input.SessionId, EventRefereeId = x.EventRefereeId, RefereeId = x.RefereeId,
JudgeUserId = refereeUsers.GetValueOrDefault(x.RefereeId), JudgeRole = x.JudgeRole, AssignmentStatus = 0,
await _assignmentRepository.InsertRangeAsync(seats.Select(x => new EventJudgeAssignment { Id = _idGenerator.NextId(), EventId = input.EventId,
SessionId = input.SessionId, EventRefereeId = x.Candidate.EventRefereeId, RefereeId = x.Candidate.RefereeId,
JudgeUserId = refereeUsers.GetValueOrDefault(x.Candidate.RefereeId), JudgeRole = x.SeatCode, JudgeRoleType = x.Slot.Code, AssignmentStatus = 0,
Status = AvailabilityStatus.Enable, CreatedUserId = _authManager.UserId, CreatedTime = now }).ToList());
}
@ -176,7 +210,51 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient
};
}
private static int RoleFlag(string role) => role switch
/// <summary>
/// 解析计分规则的裁判席位配置。
/// 配置中的每个 slot 代表一个裁判类型(DP、DM、DR、P、T 等),可分配多名裁判;
/// 主裁固定 1 人,其余类型分配时不限人数(成绩按平均值计算),配置里显式给出 min/max 时以配置为准。
/// </summary>
private static List<JudgeRoleSectionOutput> ParseRoleSections(string? schemaJson)
{
var sections = new List<JudgeRoleSectionOutput>();
if (string.IsNullOrWhiteSpace(schemaJson)) return sections;
JArray array;
try { array = JArray.Parse(schemaJson); } catch { return sections; }
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var node in array)
{
var section = new JudgeRoleSectionOutput { Title = node["title"]?.ToString() ?? node["name"]?.ToString() ?? "裁判" };
foreach (var slotNode in node["slots"] as JArray ?? new JArray())
{
var code = slotNode["code"]?.ToString();
if (string.IsNullOrWhiteSpace(code)) continue;
var roleType = NormalizeRoleType(code);
// 同一类型在配置里重复出现(如历史 DP1/DP2 席位)时只保留一条,人数改由分配时决定。
if (!seen.Add(roleType)) continue;
section.Slots.Add(new JudgeRoleSlotOutput
{
Code = roleType,
Name = slotNode["name"]?.ToString() ?? roleType,
MinCount = Math.Max(slotNode["min"]?.Value<int?>() ?? 1, 0),
MaxCount = slotNode["max"]?.Value<int?>() ?? (roleType == "T" ? (int?)1 : null),
RoleFlag = RoleFlag(roleType)
});
}
if (section.Slots.Count > 0) sections.Add(section);
}
return sections;
}
/// <summary>把席位编码归一为裁判类型编码,兼容 DP1、P2 这类带席位序号的历史编码。</summary>
private static string NormalizeRoleType(string? code)
{
var trimmed = (code ?? string.Empty).Trim().ToUpperInvariant();
var letters = new string(trimmed.TakeWhile(ch => !char.IsDigit(ch)).ToArray());
return letters.Length > 0 ? letters : trimmed;
}
private static int RoleFlag(string roleType) => roleType switch
{
"T" => (int)RefereeRole.Chief,
"P" => (int)RefereeRole.Performance,