using YD_Event.Application.Auth;
using YD_Event.Application.Event.Dtos;
namespace YD_Event.Application.Event;
/// 赛事裁判报名与分配。
public class JudgeAssignmentService : IDynamicApiController, ITransient
{
private readonly ISqlSugarRepository _eventRefereeRepository;
private readonly ISqlSugarRepository _refereeRepository;
private readonly ISqlSugarRepository _sessionRepository;
private readonly ISqlSugarRepository _venueRepository;
private readonly ISqlSugarRepository _eventProjectRepository;
private readonly ISqlSugarRepository _projectRepository;
private readonly ISqlSugarRepository _ruleRepository;
private readonly ISqlSugarRepository _assignmentRepository;
private readonly ISqlSugarRepository _eventRepository;
private readonly IIdGenerator _idGenerator;
private readonly AuthManager _authManager;
public JudgeAssignmentService(ISqlSugarRepository eventRefereeRepository,
ISqlSugarRepository refereeRepository,
ISqlSugarRepository sessionRepository, ISqlSugarRepository venueRepository,
ISqlSugarRepository eventProjectRepository, ISqlSugarRepository projectRepository,
ISqlSugarRepository ruleRepository, ISqlSugarRepository assignmentRepository,
ISqlSugarRepository eventRepository, IIdGenerator idGenerator, AuthManager authManager)
{
_eventRefereeRepository = eventRefereeRepository;
_refereeRepository = refereeRepository;
_sessionRepository = sessionRepository; _venueRepository = venueRepository; _eventProjectRepository = eventProjectRepository;
_projectRepository = projectRepository; _ruleRepository = ruleRepository; _assignmentRepository = assignmentRepository;
_eventRepository = eventRepository;
_idGenerator = idGenerator; _authManager = authManager;
}
[HttpGet("event-referees/{eventId}"), DisplayName("获取赛事裁判列表")]
public async Task> GetEventReferees(long eventId)
{
var relations = await _eventRefereeRepository.AsQueryable()
// 赛事裁判目前没有“禁用”业务,是否有效统一由软删除标记控制。
// 同时兼容早期迁移中被错误写成 Status=1 的存量记录。
.Where(x => x.EventId == eventId)
.OrderByDescending(x => x.CreatedTime).ToListAsync();
var referees = await _refereeRepository.AsQueryable()
.Where(x => relations.Select(r => r.RefereeId).Contains(x.Id)).ToListAsync();
return relations.Select(relation => ToEventRefereeOutput(relation,
referees.FirstOrDefault(x => x.Id == relation.RefereeId))).ToList();
}
[HttpGet("board/{eventId}"), DisplayName("获取裁判分配看板")]
public async Task GetBoard(long eventId)
{
var sessions = await _sessionRepository.AsQueryable().Where(x => x.EventId == eventId && x.SessionStatus != 90).ToListAsync();
if (sessions.Count == 0) return new JudgeAssignmentBoardOutput();
var venueMap = (await _venueRepository.AsQueryable().Where(x => x.EventId == eventId).ToListAsync()).ToDictionary(x => x.Id);
var eventProjects = await _eventProjectRepository.AsQueryable().Where(x => x.EventId == eventId).ToListAsync();
var projects = await _projectRepository.AsQueryable().Where(x => eventProjects.Select(e => e.ProjectId).Contains(x.Id)).ToListAsync();
var rules = await _ruleRepository.AsQueryable().Where(x => projects.Where(p => p.ScoreRuleId.HasValue).Select(p => p.ScoreRuleId!.Value).Contains(x.Id)).ToListAsync();
var assignments = await _assignmentRepository.AsQueryable().Where(x => x.EventId == eventId && x.AssignmentStatus < 9).ToListAsync();
var referees = await _refereeRepository.AsQueryable().Where(x => assignments.Select(a => a.RefereeId).Contains(x.Id)).ToListAsync();
var result = new JudgeAssignmentBoardOutput { ArrangementConfirmed = true, Submitted = assignments.Count > 0 && assignments.All(x => x.AssignmentStatus == 1) };
foreach (var ep in eventProjects)
{
var project = projects.FirstOrDefault(x => x.Id == ep.ProjectId); if (project == null) continue;
var rule = rules.FirstOrDefault(x => x.Id == project.ScoreRuleId);
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);
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 });
}
if (group.Sessions.Count > 0) result.Projects.Add(group);
}
return result;
}
[HttpGet("candidates/{eventId}/{sessionId}"), DisplayName("获取可分配裁判")]
public async Task> GetCandidates(long eventId, long sessionId)
{
if (!await _sessionRepository.IsAnyAsync(x => x.Id == sessionId && x.EventId == eventId)) throw Oops.Bah("场次不存在");
return await GetEventReferees(eventId);
}
[HttpPost("save"), DisplayName("保存裁判分配"), UnitOfWork]
public async Task Save([FromBody] SaveJudgeAssignmentsInput input)
{
var session = await _sessionRepository.GetFirstAsync(x => x.Id == input.SessionId && x.EventId == input.EventId);
if (session == null || session.SessionStatus == 90) throw Oops.Bah("场次不存在或已取消");
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("存在未选择裁判的分配记录");
var rule = await _ruleRepository.GetByIdAsync(session.ScoreRuleId);
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 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 = 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(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());
}
[HttpPost("submit"), DisplayName("提交裁判分配"), UnitOfWork]
public async Task Submit([FromBody] SubmitJudgeAssignmentsInput input)
{
var sessions = await _sessionRepository.AsQueryable().Where(x => x.EventId == input.EventId && x.SessionStatus != 90).ToListAsync();
if (sessions.Count == 0) throw Oops.Bah("请先确认赛事编排");
var assignments = await _assignmentRepository.AsQueryable().Where(x => x.EventId == input.EventId && x.AssignmentStatus == 0).ToListAsync();
if (sessions.Any(s => !assignments.Any(a => a.SessionId == s.Id))) throw Oops.Bah("仍有场次未分配裁判");
assignments.ForEach(x => { x.AssignmentStatus = 1; x.SubmittedTime = DateTime.Now; x.UpdatedUserId = _authManager.UserId; x.UpdatedTime = DateTime.Now; });
await _assignmentRepository.UpdateRangeAsync(assignments);
sessions.ForEach(x => { x.SessionStatus = 10; x.Version++; x.UpdatedUserId = _authManager.UserId; x.UpdatedTime = DateTime.Now; });
await _sessionRepository.UpdateRangeAsync(sessions);
}
[HttpPost("registration-code/{eventId}"), DisplayName("生成裁判报名码")]
public async Task GenerateRegistrationCode(long eventId)
{
var entity = await _eventRepository.GetByIdAsync(eventId);
if (entity == null) throw Oops.Bah("赛事不存在");
if (string.IsNullOrWhiteSpace(entity.RefereeRegistrationCode))
{
entity.RefereeRegistrationCode = $"RJ{eventId % 1000000:D6}{Random.Shared.Next(1000, 9999)}";
entity.UpdatedUserId = _authManager.UserId; entity.UpdatedTime = DateTime.Now;
await _eventRepository.UpdateAsync(entity);
}
return entity.RefereeRegistrationCode;
}
private static EventRefereeOutput ToEventRefereeOutput(EventReferee? relation,
YD_Event.Core.Entities.Referee? referee)
{
var roles = referee?.Roles ?? RefereeRole.None;
return new EventRefereeOutput
{
EventRefereeId = relation?.Id ?? 0,
ApplicationId = relation?.RefereeApplicationId ?? 0,
RefereeId = referee?.Id ?? relation?.RefereeId ?? 0,
Name = referee?.Name ?? "-",
RegionName = referee == null ? "-" : $"{referee.ProvinceName}/{referee.CityName}/{referee.DistrictName}",
LevelName = referee?.Level.Description() ?? "-",
CertificateUrl = referee?.CertificateUrl ?? string.Empty,
Mobile = referee?.Mobile ?? string.Empty,
Roles = (int)roles,
RoleNames = Enum.GetValues()
.Where(role => role != RefereeRole.None && roles.HasFlag(role))
.Select(role => role.Description()).ToList(),
CreatedTime = relation?.CreatedTime ?? referee?.CreatedTime ?? DateTime.MinValue,
ApplicationStatus = 2,
ApplicationStatusName = "已通过",
SourceType = relation?.SourceType ?? 1,
SourceTypeName = relation?.SourceType == 2 ? "小程序报名" : "后台添加"
};
}
///
/// 解析计分规则的裁判席位配置。
/// 配置中的每个 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,
"CJ" => (int)RefereeRole.Choreography,
"CQ" => (int)RefereeRole.CompletionQuality,
_ => (int)RefereeRole.Difficulty
};
}