2026-08-11 23:41:18 +08:00
|
|
|
using YD_Event.Application.Auth;
|
|
|
|
|
using YD_Event.Application.Event.Dtos;
|
|
|
|
|
|
|
|
|
|
namespace YD_Event.Application.Event;
|
|
|
|
|
|
|
|
|
|
/// <summary>赛事裁判报名与分配。</summary>
|
|
|
|
|
public class JudgeAssignmentService : IDynamicApiController, ITransient
|
|
|
|
|
{
|
2026-08-17 00:14:48 +08:00
|
|
|
private readonly ISqlSugarRepository<EventReferee> _eventRefereeRepository;
|
2026-08-11 23:41:18 +08:00
|
|
|
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;
|
|
|
|
|
|
2026-08-17 00:14:48 +08:00
|
|
|
public JudgeAssignmentService(ISqlSugarRepository<EventReferee> eventRefereeRepository,
|
2026-08-11 23:41:18 +08:00
|
|
|
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)
|
|
|
|
|
{
|
2026-08-17 00:14:48 +08:00
|
|
|
_eventRefereeRepository = eventRefereeRepository;
|
|
|
|
|
_refereeRepository = refereeRepository;
|
2026-08-11 23:41:18 +08:00
|
|
|
_sessionRepository = sessionRepository; _venueRepository = venueRepository; _eventProjectRepository = eventProjectRepository;
|
|
|
|
|
_projectRepository = projectRepository; _ruleRepository = ruleRepository; _assignmentRepository = assignmentRepository;
|
|
|
|
|
_eventRepository = eventRepository;
|
|
|
|
|
_idGenerator = idGenerator; _authManager = authManager;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 00:14:48 +08:00
|
|
|
[HttpGet("event-referees/{eventId}"), DisplayName("获取赛事裁判列表")]
|
|
|
|
|
public async Task<List<EventRefereeOutput>> GetEventReferees(long eventId)
|
2026-08-11 23:41:18 +08:00
|
|
|
{
|
2026-08-17 00:14:48 +08:00
|
|
|
var relations = await _eventRefereeRepository.AsQueryable()
|
|
|
|
|
// 赛事裁判目前没有“禁用”业务,是否有效统一由软删除标记控制。
|
|
|
|
|
// 同时兼容早期迁移中被错误写成 Status=1 的存量记录。
|
|
|
|
|
.Where(x => x.EventId == eventId)
|
2026-08-11 23:41:18 +08:00
|
|
|
.OrderByDescending(x => x.CreatedTime).ToListAsync();
|
2026-08-17 00:14:48 +08:00
|
|
|
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();
|
2026-08-11 23:41:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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 group = new JudgeAssignmentProjectOutput { EventProjectId = ep.Id, ProjectName = project.Name, RuleName = rule?.RuleName ?? "未配置计分规则", JudgeRoleSchemaJson = rule?.JudgeRoleSchemaJson ?? "[]" };
|
|
|
|
|
foreach (var session in sessions.Where(x => x.EventProjectId == ep.Id).OrderBy(x => venueMap.GetValueOrDefault(x.VenueId)?.Sort))
|
|
|
|
|
{
|
2026-08-17 00:14:48 +08:00
|
|
|
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();
|
2026-08-11 23:41:18 +08:00
|
|
|
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("获取可分配裁判")]
|
2026-08-17 00:14:48 +08:00
|
|
|
public async Task<List<EventRefereeOutput>> GetCandidates(long eventId, long sessionId)
|
2026-08-11 23:41:18 +08:00
|
|
|
{
|
|
|
|
|
if (!await _sessionRepository.IsAnyAsync(x => x.Id == sessionId && x.EventId == eventId)) throw Oops.Bah("场次不存在");
|
2026-08-17 00:14:48 +08:00
|
|
|
return await GetEventReferees(eventId);
|
2026-08-11 23:41:18 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
[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("裁判分配已提交,不能再次修改");
|
2026-08-17 00:14:48 +08:00
|
|
|
if (input.Assignments.Any(x => string.IsNullOrWhiteSpace(x.JudgeRole) || (x.EventRefereeId <= 0 && x.RefereeId <= 0)))
|
|
|
|
|
throw Oops.Bah("裁判岗位未完整分配");
|
2026-08-11 23:41:18 +08:00
|
|
|
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("同一岗位只能分配一名裁判");
|
2026-08-17 00:14:48 +08:00
|
|
|
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);
|
2026-08-11 23:41:18 +08:00
|
|
|
foreach (var item in input.Assignments)
|
|
|
|
|
{
|
2026-08-17 00:14:48 +08:00
|
|
|
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;
|
2026-08-11 23:41:18 +08:00
|
|
|
var requiredFlag = RoleFlag(item.JudgeRole);
|
|
|
|
|
if (requiredFlag != 0 && (candidate.Roles & requiredFlag) == 0) throw Oops.Bah($"裁判{candidate.Name}不具备岗位{item.JudgeRole}资格");
|
|
|
|
|
}
|
|
|
|
|
await _assignmentRepository.SoftDeleteAsync(x => x.SessionId == input.SessionId && x.AssignmentStatus == 0);
|
|
|
|
|
var now = DateTime.Now;
|
2026-08-17 00:14:48 +08:00
|
|
|
var refereeIds = input.Assignments.Select(x => x.RefereeId).Distinct().ToList();
|
|
|
|
|
var refereeUsers = (await _refereeRepository.AsQueryable()
|
|
|
|
|
.Where(x => refereeIds.Contains(x.Id)).ToListAsync())
|
|
|
|
|
.ToDictionary(x => x.Id, x => x.AppUserId);
|
2026-08-11 23:41:18 +08:00
|
|
|
await _assignmentRepository.InsertRangeAsync(input.Assignments.Select(x => new EventJudgeAssignment { Id = _idGenerator.NextId(), EventId = input.EventId,
|
2026-08-17 00:14:48 +08:00
|
|
|
SessionId = input.SessionId, EventRefereeId = x.EventRefereeId, RefereeId = x.RefereeId,
|
|
|
|
|
JudgeUserId = refereeUsers.GetValueOrDefault(x.RefereeId), JudgeRole = x.JudgeRole, AssignmentStatus = 0,
|
2026-08-11 23:41:18 +08:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-17 00:14:48 +08:00
|
|
|
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 ? "小程序报名" : "后台添加"
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 23:41:18 +08:00
|
|
|
private static int RoleFlag(string role) => role switch
|
|
|
|
|
{
|
|
|
|
|
"T" => (int)RefereeRole.Chief,
|
|
|
|
|
"P" => (int)RefereeRole.Performance,
|
|
|
|
|
"CJ" => (int)RefereeRole.Choreography,
|
|
|
|
|
"CQ" => (int)RefereeRole.CompletionQuality,
|
|
|
|
|
_ => (int)RefereeRole.Difficulty
|
|
|
|
|
};
|
|
|
|
|
}
|