From de1609e2f12b5b57b26013cfed17ac379db0537c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Tue, 18 Aug 2026 00:22:59 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A3=81=E5=88=A4=E6=8A=A5=E5=90=8D=E5=88=86?= =?UTF-8?q?=E9=85=8D=E9=99=90=E5=88=B6=E4=BA=BA=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Event/Dtos/JudgeAssignmentDtos.cs | 27 +++- .../Event/JudgeAssignmentService.cs | 124 ++++++++++++++---- 2 files changed, 127 insertions(+), 24 deletions(-) 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,