YD_Event.Admin/YD_Event.Application/Event/JudgeAssignmentService.cs

266 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using YD_Event.Application.Auth;
using YD_Event.Application.Event.Dtos;
namespace YD_Event.Application.Event;
/// <summary>赛事裁判报名与分配。</summary>
public class JudgeAssignmentService : IDynamicApiController, ITransient
{
private readonly ISqlSugarRepository<EventReferee> _eventRefereeRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Referee> _refereeRepository;
private readonly ISqlSugarRepository<EventSession> _sessionRepository;
private readonly ISqlSugarRepository<EventVenue> _venueRepository;
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.ScoreRule> _ruleRepository;
private readonly ISqlSugarRepository<EventJudgeAssignment> _assignmentRepository;
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
private readonly IIdGenerator _idGenerator;
private readonly AuthManager _authManager;
public JudgeAssignmentService(ISqlSugarRepository<EventReferee> eventRefereeRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Referee> refereeRepository,
ISqlSugarRepository<EventSession> sessionRepository, ISqlSugarRepository<EventVenue> venueRepository,
ISqlSugarRepository<EventProject> eventProjectRepository, ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<YD_Event.Core.Entities.ScoreRule> ruleRepository, ISqlSugarRepository<EventJudgeAssignment> assignmentRepository,
ISqlSugarRepository<EventInfo> 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<List<EventRefereeOutput>> 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<JudgeAssignmentBoardOutput> 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<List<EventRefereeOutput>> 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<string, List<SaveJudgeAssignmentItem>>(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<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 = 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<string> 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<RefereeRole>()
.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 ? "小程序报名" : "后台添加"
};
}
/// <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,
"CJ" => (int)RefereeRole.Choreography,
"CQ" => (int)RefereeRole.CompletionQuality,
_ => (int)RefereeRole.Difficulty
};
}