diff --git a/YD_Event.Application/Event/Dtos/JudgeAssignmentDtos.cs b/YD_Event.Application/Event/Dtos/JudgeAssignmentDtos.cs
index e901664..018aa95 100644
--- a/YD_Event.Application/Event/Dtos/JudgeAssignmentDtos.cs
+++ b/YD_Event.Application/Event/Dtos/JudgeAssignmentDtos.cs
@@ -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; } = "[]";
+ /// 裁判大类分组(难度裁判、表现裁判、主裁),由计分规则的裁判席位配置解析而来。
+ public List JudgeRoleSections { get; set; } = new();
public List Sessions { get; set; } = new();
}
+/// 裁判大类分组。
+public class JudgeRoleSectionOutput
+{
+ public string Title { get; set; } = null!;
+ public List Slots { get; set; } = new();
+}
+
+/// 裁判类型(如 DP 力量体操、DM 多摇、P 表现、T 主裁),一个类型可分配多名裁判。
+public class JudgeRoleSlotOutput
+{
+ public string Code { get; set; } = null!;
+ public string Name { get; set; } = null!;
+ /// 该类型至少需要分配的裁判人数。
+ public int MinCount { get; set; }
+ /// 该类型最多可分配的裁判人数,null 表示不限人数(成绩按平均值计算)。
+ public int? MaxCount { get; set; }
+ /// 执裁该类型所需具备的裁判角色位标记。
+ 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; }
+ /// 裁判席位编码(同一场次内唯一,如 DP1、DP2、T)。
public string JudgeRole { get; set; } = null!;
+ /// 裁判类型编码(如 DP、DM、DR、P、T),对应计分规则中的裁判类型。
+ 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
{
+ /// 裁判类型编码;同一类型可重复出现,表示该类型分配了多名裁判。
public string JudgeRole { get; set; } = null!;
public long EventRefereeId { get; set; }
public long RefereeId { get; set; }
diff --git a/YD_Event.Application/Event/JudgeAssignmentService.cs b/YD_Event.Application/Event/JudgeAssignmentService.cs
index e3f33d7..1d253e6 100644
--- a/YD_Event.Application/Event/JudgeAssignmentService.cs
+++ b/YD_Event.Application/Event/JudgeAssignmentService.cs
@@ -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>(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();
+ 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
+ ///
+ /// 解析计分规则的裁判席位配置。
+ /// 配置中的每个 slot 代表一个裁判类型(DP、DM、DR、P、T 等),可分配多名裁判;
+ /// 主裁固定 1 人,其余类型分配时不限人数(成绩按平均值计算),配置里显式给出 min/max 时以配置为准。
+ ///
+ private static List ParseRoleSections(string? schemaJson)
+ {
+ var sections = new List();
+ if (string.IsNullOrWhiteSpace(schemaJson)) return sections;
+ JArray array;
+ try { array = JArray.Parse(schemaJson); } catch { return sections; }
+ var seen = new HashSet(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() ?? 1, 0),
+ MaxCount = slotNode["max"]?.Value() ?? (roleType == "T" ? (int?)1 : null),
+ RoleFlag = RoleFlag(roleType)
+ });
+ }
+ if (section.Slots.Count > 0) sections.Add(section);
+ }
+ return sections;
+ }
+
+ /// 把席位编码归一为裁判类型编码,兼容 DP1、P2 这类带席位序号的历史编码。
+ 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,