裁判报名分配限制人数
This commit is contained in:
parent
33a289568e
commit
de1609e2f1
@ -36,10 +36,31 @@ public class JudgeAssignmentProjectOutput
|
|||||||
public long EventProjectId { get; set; }
|
public long EventProjectId { get; set; }
|
||||||
public string ProjectName { get; set; } = null!;
|
public string ProjectName { get; set; } = null!;
|
||||||
public string RuleName { 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();
|
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 class JudgeAssignmentSessionOutput
|
||||||
{
|
{
|
||||||
public long SessionId { get; set; }
|
public long SessionId { get; set; }
|
||||||
@ -52,7 +73,10 @@ public class JudgeAssignmentSessionOutput
|
|||||||
public class JudgeAssignmentItemOutput
|
public class JudgeAssignmentItemOutput
|
||||||
{
|
{
|
||||||
public long EventRefereeId { get; set; }
|
public long EventRefereeId { get; set; }
|
||||||
|
/// <summary>裁判席位编码(同一场次内唯一,如 DP1、DP2、T)。</summary>
|
||||||
public string JudgeRole { get; set; } = null!;
|
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 string JudgeRoleName { get; set; } = null!;
|
||||||
public long RefereeId { get; set; }
|
public long RefereeId { get; set; }
|
||||||
public string RefereeName { get; set; } = null!;
|
public string RefereeName { get; set; } = null!;
|
||||||
@ -68,6 +92,7 @@ public class SaveJudgeAssignmentsInput
|
|||||||
|
|
||||||
public class SaveJudgeAssignmentItem
|
public class SaveJudgeAssignmentItem
|
||||||
{
|
{
|
||||||
|
/// <summary>裁判类型编码;同一类型可重复出现,表示该类型分配了多名裁判。</summary>
|
||||||
public string JudgeRole { get; set; } = null!;
|
public string JudgeRole { get; set; } = null!;
|
||||||
public long EventRefereeId { get; set; }
|
public long EventRefereeId { get; set; }
|
||||||
public long RefereeId { get; set; }
|
public long RefereeId { get; set; }
|
||||||
|
|||||||
@ -63,10 +63,22 @@ public class JudgeAssignmentService : IDynamicApiController, ITransient
|
|||||||
{
|
{
|
||||||
var project = projects.FirstOrDefault(x => x.Id == ep.ProjectId); if (project == null) continue;
|
var project = projects.FirstOrDefault(x => x.Id == ep.ProjectId); if (project == null) continue;
|
||||||
var rule = rules.FirstOrDefault(x => x.Id == project.ScoreRuleId);
|
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))
|
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,
|
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 });
|
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))
|
if (await _assignmentRepository.IsAnyAsync(x => x.SessionId == input.SessionId && x.AssignmentStatus == 1))
|
||||||
throw Oops.Bah("裁判分配已提交,不能再次修改");
|
throw Oops.Bah("裁判分配已提交,不能再次修改");
|
||||||
if (input.Assignments.Any(x => string.IsNullOrWhiteSpace(x.JudgeRole) || (x.EventRefereeId <= 0 && x.RefereeId <= 0)))
|
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 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();
|
var slots = ParseRoleSections(rule?.JudgeRoleSchemaJson).SelectMany(x => x.Slots).ToList();
|
||||||
if (requiredRoles.Count == 0 || requiredRoles.Except(input.Assignments.Select(x => x.JudgeRole)).Any() || input.Assignments.Any(x => !requiredRoles.Contains(x.JudgeRole)))
|
if (slots.Count == 0) throw Oops.Bah("当前项目的计分规则未配置裁判类型");
|
||||||
throw Oops.Bah("请完成计分规则要求的全部裁判岗位分配");
|
var slotMap = slots.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
|
||||||
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 grouped = new Dictionary<string, List<SaveJudgeAssignmentItem>>(StringComparer.OrdinalIgnoreCase);
|
||||||
var candidates = await GetEventReferees(input.EventId);
|
|
||||||
foreach (var item in input.Assignments)
|
foreach (var item in input.Assignments)
|
||||||
{
|
{
|
||||||
var candidate = item.EventRefereeId > 0
|
var roleType = slotMap.TryGetValue(item.JudgeRole, out var matched) ? matched.Code : NormalizeRoleType(item.JudgeRole);
|
||||||
? candidates.FirstOrDefault(x => x.EventRefereeId == item.EventRefereeId)
|
if (!slotMap.ContainsKey(roleType)) throw Oops.Bah($"裁判类型{item.JudgeRole}不属于当前项目的计分规则");
|
||||||
: candidates.FirstOrDefault(x => x.RefereeId == item.RefereeId);
|
if (!grouped.TryGetValue(roleType, out var list)) grouped[roleType] = list = new List<SaveJudgeAssignmentItem>();
|
||||||
if (candidate == null) throw Oops.Bah("存在尚未关联到当前赛事的裁判");
|
list.Add(item);
|
||||||
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}资格");
|
|
||||||
}
|
}
|
||||||
|
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);
|
await _assignmentRepository.SoftDeleteAsync(x => x.SessionId == input.SessionId && x.AssignmentStatus == 0);
|
||||||
var now = DateTime.Now;
|
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()
|
var refereeUsers = (await _refereeRepository.AsQueryable()
|
||||||
.Where(x => refereeIds.Contains(x.Id)).ToListAsync())
|
.Where(x => refereeIds.Contains(x.Id)).ToListAsync())
|
||||||
.ToDictionary(x => x.Id, x => x.AppUserId);
|
.ToDictionary(x => x.Id, x => x.AppUserId);
|
||||||
await _assignmentRepository.InsertRangeAsync(input.Assignments.Select(x => new EventJudgeAssignment { Id = _idGenerator.NextId(), EventId = input.EventId,
|
await _assignmentRepository.InsertRangeAsync(seats.Select(x => new EventJudgeAssignment { Id = _idGenerator.NextId(), EventId = input.EventId,
|
||||||
SessionId = input.SessionId, EventRefereeId = x.EventRefereeId, RefereeId = x.RefereeId,
|
SessionId = input.SessionId, EventRefereeId = x.Candidate.EventRefereeId, RefereeId = x.Candidate.RefereeId,
|
||||||
JudgeUserId = refereeUsers.GetValueOrDefault(x.RefereeId), JudgeRole = x.JudgeRole, AssignmentStatus = 0,
|
JudgeUserId = refereeUsers.GetValueOrDefault(x.Candidate.RefereeId), JudgeRole = x.SeatCode, JudgeRoleType = x.Slot.Code, AssignmentStatus = 0,
|
||||||
Status = AvailabilityStatus.Enable, CreatedUserId = _authManager.UserId, CreatedTime = now }).ToList());
|
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,
|
"T" => (int)RefereeRole.Chief,
|
||||||
"P" => (int)RefereeRole.Performance,
|
"P" => (int)RefereeRole.Performance,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user