522 lines
27 KiB
C#
522 lines
27 KiB
C#
using YD_Event.Application.Auth;
|
||
using YD_Event.Application.Event.Dtos;
|
||
using YD_Event.Application.Scoring;
|
||
using YD_Event.Core.Infrastructure.WeChat;
|
||
|
||
namespace YD_Event.Application.Event;
|
||
|
||
/// <summary>
|
||
/// 赛事裁判报名与分配
|
||
/// </summary>
|
||
[ApiDescriptionSettings("打分系统")]
|
||
[Route("api/judgeassignment")]
|
||
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;
|
||
private readonly WeChatMiniProgramCodeService _miniProgramCodeService;
|
||
private const string RefereeRegistrationPage = "pages/detail/detail";
|
||
|
||
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,
|
||
WeChatMiniProgramCodeService miniProgramCodeService)
|
||
{
|
||
_eventRefereeRepository = eventRefereeRepository;
|
||
_refereeRepository = refereeRepository;
|
||
_sessionRepository = sessionRepository; _venueRepository = venueRepository; _eventProjectRepository = eventProjectRepository;
|
||
_projectRepository = projectRepository; _ruleRepository = ruleRepository; _assignmentRepository = assignmentRepository;
|
||
_eventRepository = eventRepository;
|
||
_idGenerator = idGenerator; _authManager = authManager;
|
||
_miniProgramCodeService = miniProgramCodeService;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取赛事裁判列表
|
||
/// </summary>
|
||
[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();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取裁判分配看板
|
||
/// </summary>
|
||
[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) };
|
||
var projectMap = projects.ToDictionary(x => x.Id);
|
||
foreach (var ep in eventProjects
|
||
.OrderBy(x => projectMap.GetValueOrDefault(x.ProjectId)?.SortIndex ?? int.MaxValue)
|
||
.ThenBy(x => x.ProjectId)
|
||
.ThenBy(x => x.Id))
|
||
{
|
||
var project = projectMap.GetValueOrDefault(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, Order: index))
|
||
.ToDictionary(x => x.Code, x => x.Order, StringComparer.OrdinalIgnoreCase);
|
||
var group = new JudgeAssignmentProjectOutput
|
||
{
|
||
EventProjectId = ep.Id, ProjectName = project.Name,
|
||
RuleName = rule?.RuleName ?? "未配置计分规则", JudgeRoleSections = sections
|
||
};
|
||
// 裁判按“项目 + 场地”分配;同一场地因不同组别拆出的多个实际场次共用一套裁判。
|
||
var projectSessions = sessions.Where(x => x.EventProjectId == ep.Id)
|
||
.GroupBy(x => x.VenueId)
|
||
.OrderBy(x => venueMap.GetValueOrDefault(x.Key)?.Sort)
|
||
.ThenBy(x => x.Key);
|
||
foreach (var venueSessions in projectSessions)
|
||
{
|
||
var orderedSessions = venueSessions.OrderBy(x => x.SessionNo).ThenBy(x => x.Id).ToList();
|
||
var sessionIds = orderedSessions.Select(x => x.Id).ToHashSet();
|
||
var assignmentSessionId = assignments.Where(x => sessionIds.Contains(x.SessionId))
|
||
.Select(x => x.SessionId)
|
||
.FirstOrDefault();
|
||
var representativeSession = orderedSessions.FirstOrDefault(x => x.Id == assignmentSessionId)
|
||
?? orderedSessions[0];
|
||
var items = assignments.Where(x => x.SessionId == representativeSession.Id)
|
||
.Select(x => ToAssignmentItemOutput(x, referees.FirstOrDefault(r => r.Id == x.RefereeId), slotMap))
|
||
// 先按规则中的裁判类型排序(规则里没有的历史类型排到最后),同类型内再按席位编码排列。
|
||
.OrderBy(x => slotOrder.GetValueOrDefault(x.JudgeRoleType, int.MaxValue))
|
||
.ThenBy(x => x.JudgeRole, StringComparer.OrdinalIgnoreCase).ToList();
|
||
group.Sessions.Add(new JudgeAssignmentSessionOutput
|
||
{
|
||
SessionId = representativeSession.Id,
|
||
SessionName = $"{project.Name}-{venueMap.GetValueOrDefault(representativeSession.VenueId)?.VenueName ?? "-"}",
|
||
VenueName = venueMap.GetValueOrDefault(representativeSession.VenueId)?.VenueName ?? "-",
|
||
Assigned = items.Count > 0, Assignments = items
|
||
});
|
||
}
|
||
if (group.Sessions.Count > 0) result.Projects.Add(group);
|
||
}
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取可分配裁判(过滤同时段已分配的裁判)
|
||
/// </summary>
|
||
[HttpGet("candidates/{eventId}/{sessionId}"), DisplayName("获取可分配裁判")]
|
||
public async Task<List<EventRefereeOutput>> GetCandidates(long eventId, long sessionId)
|
||
{
|
||
var session = await _sessionRepository.GetFirstAsync(x => x.Id == sessionId && x.EventId == eventId);
|
||
if (session == null) throw Oops.Bah("场次不存在");
|
||
|
||
var allReferees = await GetEventReferees(eventId);
|
||
|
||
var eventSessions = await _sessionRepository.AsQueryable()
|
||
.Where(x => x.EventId == eventId && x.SessionStatus != 90)
|
||
.ToListAsync();
|
||
var assignmentUnitSessions = eventSessions
|
||
.Where(x => x.EventProjectId == session.EventProjectId && x.VenueId == session.VenueId)
|
||
.ToList();
|
||
var timedAssignmentUnitSessions = assignmentUnitSessions
|
||
.Where(x => x.StartTime.HasValue && x.EndTime.HasValue)
|
||
.ToList();
|
||
if (timedAssignmentUnitSessions.Count == 0)
|
||
{
|
||
return allReferees;
|
||
}
|
||
|
||
// 查找与分配单元内任一实际场次时间重叠的裁判;兄弟场次共用裁判,不互相判定为冲突。
|
||
var assignmentUnitSessionIds = assignmentUnitSessions.Select(x => x.Id).ToHashSet();
|
||
var conflictSessions = eventSessions
|
||
.Where(x => !assignmentUnitSessionIds.Contains(x.Id) && x.StartTime.HasValue && x.EndTime.HasValue)
|
||
.Where(x => timedAssignmentUnitSessions.Any(unit =>
|
||
x.StartTime!.Value <= unit.EndTime!.Value && x.EndTime!.Value >= unit.StartTime!.Value))
|
||
.ToList();
|
||
|
||
if (conflictSessions.Count == 0)
|
||
{
|
||
return allReferees;
|
||
}
|
||
|
||
var conflictSessionIds = conflictSessions.Select(x => x.Id).ToList();
|
||
var conflictAssignments = await _assignmentRepository.AsQueryable()
|
||
.Where(x => conflictSessionIds.Contains(x.SessionId) && x.AssignmentStatus < 9)
|
||
.ToListAsync();
|
||
|
||
var conflictRefereeIds = conflictAssignments.Select(x => x.RefereeId).Distinct().ToList();
|
||
|
||
// 过滤已分配裁判
|
||
return allReferees.Where(x => !conflictRefereeIds.Contains(x.RefereeId)).ToList();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存场地裁判分配。
|
||
/// 传入的场次仅用于定位“项目 + 场地”,分配结果会同步到该分配单元下的全部实际场次。
|
||
/// 同一裁判类型可分配多名裁判,人数上下限由计分规则的裁判席位配置决定。
|
||
/// </summary>
|
||
/// <param name="input">场次裁判分配信息。</param>
|
||
[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("场次不存在或已取消");
|
||
}
|
||
var eventInfo = await _eventRepository.GetFirstAsync(x => x.Id == input.EventId);
|
||
if (eventInfo == null) throw Oops.Bah("赛事不存在");
|
||
|
||
var eventSessions = await _sessionRepository.AsQueryable()
|
||
.Where(x => x.EventId == input.EventId && x.SessionStatus != 90)
|
||
.ToListAsync();
|
||
var lockedSession = eventSessions.FirstOrDefault(
|
||
x => !CompetitionStagePolicy.CanModifyAssignment(x.SessionStatus, eventInfo.EventStatus));
|
||
if (lockedSession != null)
|
||
{
|
||
throw Oops.Bah(CompetitionStagePolicy.AssignmentLockReason(
|
||
lockedSession.SessionStatus, eventInfo.EventStatus));
|
||
}
|
||
|
||
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 grouped = GroupByRoleType(input.Assignments, slots);
|
||
EnsureRefereeCount(slots, grouped);
|
||
var candidates = await GetEventReferees(input.EventId);
|
||
var seats = BuildSeats(slots, grouped, candidates);
|
||
|
||
// 评分、任务和成绩仍按实际场次关联,因此为每个兄弟场次各保存一份相同的裁判席位。
|
||
var assignmentUnitSessions = eventSessions
|
||
.Where(x => x.EventProjectId == session.EventProjectId && x.VenueId == session.VenueId)
|
||
.ToList();
|
||
var assignmentUnitSessionIds = assignmentUnitSessions.Select(x => x.Id).ToList();
|
||
|
||
// 赛前允许反复修改已提交的分配。保存新版本后先回到草稿态,重新提交后裁判端才可见。
|
||
await _assignmentRepository.SoftDeleteAsync(x => assignmentUnitSessionIds.Contains(x.SessionId) && x.AssignmentStatus < 9);
|
||
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);
|
||
var newAssignments = assignmentUnitSessions.SelectMany(sessionItem => seats.Select(x => new EventJudgeAssignment
|
||
{
|
||
Id = _idGenerator.NextId(), EventId = input.EventId, SessionId = sessionItem.Id,
|
||
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();
|
||
await _assignmentRepository.InsertRangeAsync(newAssignments);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 提交赛事全部场次的裁判分配。
|
||
/// 首次提交时,将本赛事所有裁判分配草稿更新为已提交,并将对应的准备中场次更新为就绪;
|
||
/// 赛前修改已提交的分配后,可调用本接口重新提交,本次仅更新发生修改的草稿场次,其他已提交场次保持不变。
|
||
/// 提交前会校验赛事已生成有效场次、每个有效场次均已分配裁判,且所有场次仍处于准备中或就绪状态。
|
||
/// 任一场次进入检录中、进行中或赛后状态后,均不允许再提交或重新提交裁判分配。
|
||
/// </summary>
|
||
/// <param name="input">赛事裁判分配提交信息。</param>
|
||
[HttpPost("submit"), DisplayName("提交裁判分配"), UnitOfWork]
|
||
public async Task Submit([FromBody] SubmitJudgeAssignmentsInput input)
|
||
{
|
||
var eventInfo = await _eventRepository.GetFirstAsync(x => x.Id == input.EventId);
|
||
if (eventInfo == null) throw Oops.Bah("赛事不存在");
|
||
var sessions = await _sessionRepository.AsQueryable()
|
||
.Where(x => x.EventId == input.EventId && x.SessionStatus != 90)
|
||
.ToListAsync();
|
||
if (sessions.Count == 0)
|
||
{
|
||
throw Oops.Bah("请先确认赛事编排");
|
||
}
|
||
|
||
var lockedSession = sessions.FirstOrDefault(
|
||
x => !CompetitionStagePolicy.CanModifyAssignment(x.SessionStatus, eventInfo.EventStatus));
|
||
if (lockedSession != null)
|
||
{
|
||
throw Oops.Bah(CompetitionStagePolicy.AssignmentLockReason(
|
||
lockedSession.SessionStatus, eventInfo.EventStatus));
|
||
}
|
||
|
||
// 重提时只有修改过的场次是草稿,其余场次仍是已提交;完整性校验必须同时考虑两者。
|
||
var assignments = await _assignmentRepository.AsQueryable()
|
||
.Where(x => x.EventId == input.EventId
|
||
&& (x.AssignmentStatus == 0 || x.AssignmentStatus == 1))
|
||
.ToListAsync();
|
||
if (sessions.Any(sessionItem => !assignments.Any(assignment => assignment.SessionId == sessionItem.Id)))
|
||
{
|
||
throw Oops.Bah("仍有场次未分配裁判");
|
||
}
|
||
|
||
var drafts = assignments.Where(x => x.AssignmentStatus == 0).ToList();
|
||
if (drafts.Count == 0)
|
||
{
|
||
return;
|
||
}
|
||
|
||
var now = DateTime.Now;
|
||
drafts.ForEach(assignment =>
|
||
{
|
||
assignment.AssignmentStatus = 1;
|
||
assignment.SubmittedTime = now;
|
||
assignment.UpdatedUserId = _authManager.UserId;
|
||
assignment.UpdatedTime = now;
|
||
});
|
||
await _assignmentRepository.UpdateRangeAsync(drafts);
|
||
|
||
var changedSessionIds = drafts.Select(x => x.SessionId).Distinct().ToHashSet();
|
||
var changedSessions = sessions.Where(x => changedSessionIds.Contains(x.Id)).ToList();
|
||
changedSessions.ForEach(sessionItem =>
|
||
{
|
||
if (sessionItem.SessionStatus == 0)
|
||
{
|
||
sessionItem.SessionStatus = 10;
|
||
}
|
||
|
||
sessionItem.Version++;
|
||
sessionItem.UpdatedUserId = _authManager.UserId;
|
||
sessionItem.UpdatedTime = now;
|
||
});
|
||
await _sessionRepository.UpdateRangeAsync(changedSessions);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成裁判报名码,确保唯一性
|
||
/// </summary>
|
||
[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))
|
||
{
|
||
// 生成唯一报名码,最多重试10次
|
||
for (int i = 0; i < 10; i++)
|
||
{
|
||
var code = $"RJ{eventId % 1000000:D6}{Random.Shared.Next(1000, 9999)}";
|
||
// 校验全局唯一性
|
||
if (!await _eventRepository.IsAnyAsync(x => x.RefereeRegistrationCode == code))
|
||
{
|
||
entity.RefereeRegistrationCode = code;
|
||
entity.UpdatedUserId = _authManager.UserId;
|
||
entity.UpdatedTime = DateTime.Now;
|
||
await _eventRepository.UpdateAsync(entity);
|
||
return code;
|
||
}
|
||
}
|
||
throw Oops.Bah("报名码生成失败,请重试");
|
||
}
|
||
return entity.RefereeRegistrationCode;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成裁判报名小程序码。
|
||
/// </summary>
|
||
[HttpPost("registration-qrcode/{eventId}"), DisplayName("生成裁判报名二维码")]
|
||
public async Task<JudgeRegistrationCodeOutput> GenerateRegistrationQrCode(long eventId)
|
||
{
|
||
var registrationCode = await GenerateRegistrationCode(eventId);
|
||
var image = await _miniProgramCodeService.GenerateAsync(
|
||
registrationCode, RefereeRegistrationPage);
|
||
return new JudgeRegistrationCodeOutput
|
||
{
|
||
RegistrationCode = registrationCode,
|
||
QrCodeDataUrl = $"data:image/jpeg;base64,{Convert.ToBase64String(image)}",
|
||
Page = RefereeRegistrationPage
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 组装赛事裁判输出,裁判档案缺失时用占位值兜底
|
||
/// </summary>
|
||
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>
|
||
/// 组装看板上的裁判席位输出,裁判类型名称取自计分规则
|
||
/// </summary>
|
||
private static JudgeAssignmentItemOutput ToAssignmentItemOutput(EventJudgeAssignment assignment,
|
||
YD_Event.Core.Entities.Referee? referee, Dictionary<string, JudgeRoleSlotOutput> slotMap)
|
||
{
|
||
// 裁判类型是后加的字段,存量分配记录只有席位编码(DP1),需要回退解析出类型(DP)。
|
||
var roleType = string.IsNullOrWhiteSpace(assignment.JudgeRoleType)
|
||
? NormalizeRoleType(assignment.JudgeRole)
|
||
: assignment.JudgeRoleType!;
|
||
return new JudgeAssignmentItemOutput
|
||
{
|
||
EventRefereeId = assignment.EventRefereeId,
|
||
JudgeRole = assignment.JudgeRole,
|
||
JudgeRoleType = roleType,
|
||
JudgeRoleName = slotMap.GetValueOrDefault(roleType)?.Name ?? roleType,
|
||
RefereeId = assignment.RefereeId,
|
||
RefereeName = referee?.Name ?? "-",
|
||
Mobile = referee?.Mobile ?? string.Empty
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按裁判类型给提交的分配记录归组,同一类型可以出现多条,表示该类型分配了多名裁判。
|
||
/// 前端既可能传裁判类型编码(DP),也可能传历史席位编码(DP1),统一归一到规则里的类型编码。
|
||
/// </summary>
|
||
private static Dictionary<string, List<SaveJudgeAssignmentItem>> GroupByRoleType(
|
||
List<SaveJudgeAssignmentItem> assignments, List<JudgeRoleSlotOutput> slots)
|
||
{
|
||
var slotMap = slots.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
|
||
var grouped = new Dictionary<string, List<SaveJudgeAssignmentItem>>(StringComparer.OrdinalIgnoreCase);
|
||
foreach (var item in 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))
|
||
{
|
||
list = new List<SaveJudgeAssignmentItem>();
|
||
grouped[roleType] = list;
|
||
}
|
||
list.Add(item);
|
||
}
|
||
return grouped;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 校验每个裁判类型分配的人数是否落在计分规则给出的上下限内
|
||
/// </summary>
|
||
private static void EnsureRefereeCount(List<JudgeRoleSlotOutput> slots,
|
||
Dictionary<string, List<SaveJudgeAssignmentItem>> grouped)
|
||
{
|
||
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} 名裁判");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 把归组后的分配记录展开成待落库的裁判席位,并校验裁判资格与重复分配。
|
||
/// 席位编码规则:类型限 1 人时直接用类型编码(T),可分配多人时按顺序追加序号(DP1、DP2)。
|
||
/// </summary>
|
||
private static List<JudgeSeat> BuildSeats(List<JudgeRoleSlotOutput> slots,
|
||
Dictionary<string, List<SaveJudgeAssignmentItem>> grouped, List<EventRefereeOutput> candidates)
|
||
{
|
||
var seats = new List<JudgeSeat>();
|
||
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(new JudgeSeat
|
||
{
|
||
Slot = slot,
|
||
SeatCode = slot.MaxCount == 1 ? slot.Code : $"{slot.Code}{seatNo}",
|
||
Candidate = candidate
|
||
});
|
||
}
|
||
}
|
||
var repeated = seats.GroupBy(x => x.Candidate.EventRefereeId).FirstOrDefault(x => x.Count() > 1);
|
||
if (repeated != null) throw Oops.Bah($"裁判{repeated.First().Candidate.Name}在同一场次只能担任一个裁判席位");
|
||
return seats;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 解析计分规则的裁判席位配置,与裁判端评分共用一份解析逻辑
|
||
/// </summary>
|
||
private static List<JudgeRoleSectionOutput> ParseRoleSections(string? schemaJson) => JudgeRoleSchemaService.ParseSections(schemaJson);
|
||
|
||
/// <summary>
|
||
/// 把席位编码归一为裁判类型编码,兼容 DP1、P2 这类带席位序号的历史编码
|
||
/// </summary>
|
||
private static string NormalizeRoleType(string? code) => JudgeRoleSchemaService.NormalizeRoleType(code);
|
||
|
||
/// <summary>
|
||
/// 裁判类型编码对应的执裁资格位,未单独列出的类型都按难度裁判处理
|
||
/// </summary>
|
||
private static int RoleFlag(string roleType) => JudgeRoleSchemaService.RoleFlagOf(roleType);
|
||
|
||
/// <summary>
|
||
/// 一条待落库的裁判席位
|
||
/// </summary>
|
||
private class JudgeSeat
|
||
{
|
||
/// <summary>
|
||
/// 席位所属的裁判类型
|
||
/// </summary>
|
||
public JudgeRoleSlotOutput Slot { get; set; } = null!;
|
||
|
||
/// <summary>
|
||
/// 席位编码,如 T、DP1、DP2
|
||
/// </summary>
|
||
public string SeatCode { get; set; } = null!;
|
||
|
||
/// <summary>
|
||
/// 该席位的执裁裁判
|
||
/// </summary>
|
||
public EventRefereeOutput Candidate { get; set; } = null!;
|
||
}
|
||
}
|