YD_Event.Admin/YD_Event.Application/Client/CheckinScheduleService.cs
2026-10-10 09:04:52 +08:00

296 lines
14 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.Client.Dtos;
using YD_Event.Application.Event;
namespace YD_Event.Application.Client;
/// <summary>从秩序册实时生成检录项目、组别和列对象。</summary>
public class CheckinScheduleService : ITransient
{
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
private readonly ISqlSugarRepository<EventRegistrationPerson> _personRepository;
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<EventSession> _sessionRepository;
private readonly ISqlSugarRepository<EventSessionEntry> _entryRepository;
private readonly ISqlSugarRepository<EventProject> _eventProjectRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
private readonly ISqlSugarRepository<EventVenue> _venueRepository;
private readonly EventArrangementService _arrangementService;
public CheckinScheduleService(
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<EventRegistrationPerson> personRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<EventSession> sessionRepository,
ISqlSugarRepository<EventSessionEntry> entryRepository,
ISqlSugarRepository<EventProject> eventProjectRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<EventVenue> venueRepository,
EventArrangementService arrangementService)
{
_eventRepository = eventRepository;
_personRepository = personRepository;
_registrationRepository = registrationRepository;
_sessionRepository = sessionRepository;
_entryRepository = entryRepository;
_eventProjectRepository = eventProjectRepository;
_projectRepository = projectRepository;
_venueRepository = venueRepository;
_arrangementService = arrangementService;
}
public async Task<CheckinEventOptionsDto> GetOptionsAsync(long eventId)
{
var evt = await _eventRepository.GetFirstAsync(x =>
x.Id == eventId && !x.DeleteMark && x.Status == AvailabilityStatus.Enable);
if (evt == null) throw Oops.Bah("赛事不存在或已停用");
// 秩序册确认导入后已经固化了场次和出场顺序,检录直接沿用该顺序,
// 不再要求报名平台存在报名人员或重复执行赛事编排。
var importedOptions = await BuildImportedOptionsAsync(evt);
if (importedOptions != null) return importedOptions;
// Preview 的每一行就是秩序册中的一列:个人项目一人,团队项目可以多人。
var rows = await _arrangementService.Preview(eventId);
if (rows.Count == 0) throw Oops.Bah("当前赛事尚未生成有效秩序册,不能创建检录任务");
var personIds = rows.SelectMany(x => x.RegistrationPersonIds).Distinct().ToList();
var persons = personIds.Count == 0
? new List<EventRegistrationPerson>()
: await _personRepository.AsQueryable()
.Where(x => personIds.Contains(x.Id) && !x.DeleteMark && x.PersonType == PersonType.Athlete)
.ToListAsync();
var personMap = persons.ToDictionary(x => x.Id);
var registrationIds = persons.Select(x => x.RegistrationId).Distinct().ToList();
var registrationModes = registrationIds.Count == 0
? new Dictionary<long, int>()
: (await _registrationRepository.AsQueryable()
.Where(x => registrationIds.Contains(x.Id) && !x.DeleteMark)
.ToListAsync())
.ToDictionary(x => x.Id, x => x.RegistrationMode);
// 团队项目沿用报名项目作为检录单位;个人项目追加报名人员 ID,
// 避免单位账号在同一报名项目下填报多人时共用同一个检录键。
var units = rows.Select((row, index) => new CheckinOrderBookUnitDto
{
UnitKey = BuildUnitKey(row.EventProjectId, row.RegistrationItemId,
row.IsTeamProject ? null : row.RegistrationPersonIds.SingleOrDefault()),
IsTeamProject = row.IsTeamProject,
Order = index + 1,
SessionIndex = row.SessionIndex,
VenueIndex = row.VenueIndex,
VenueName = $"{row.VenueIndex}号场地",
EventProjectId = row.EventProjectId,
ProjectId = row.ProjectId,
ProjectName = row.ProjectName,
GroupId = row.EventProjectGroupId,
GroupName = row.GroupName,
RegistrationItemId = row.RegistrationItemId,
RegistrationMode = row.RegistrationPersonIds
.Where(personMap.ContainsKey)
.Select(personId => registrationModes.GetValueOrDefault(personMap[personId].RegistrationId))
.FirstOrDefault(mode => mode > 0) is var mode && mode > 0 ? mode : null,
TeamName = row.TeamName ?? string.Empty,
UnitName = row.UnitName ?? string.Empty,
RegistrationPersonIds = row.RegistrationPersonIds.Distinct().ToList(),
Athletes = row.RegistrationPersonIds.Distinct()
.Where(personMap.ContainsKey)
.Select(personId =>
{
var person = personMap[personId];
return new CheckinAthleteDto
{
Id = person.Id,
UserId = person.UserId,
BibNo = FormatBibNo(person.Id),
Name = person.Name,
TeamName = row.TeamName ?? string.Empty
};
}).ToList()
}).ToList();
var projects = units.GroupBy(x => new { x.EventProjectId, x.ProjectId, x.ProjectName })
.Select((project, projectIndex) => new CheckinProjectOptionDto
{
EventProjectId = project.Key.EventProjectId,
ProjectId = project.Key.ProjectId,
ProjectName = project.Key.ProjectName,
Order = projectIndex + 1,
Groups = project.Where(x => x.GroupId.HasValue)
.GroupBy(x => new { Id = x.GroupId!.Value, x.GroupName })
.Select((group, groupIndex) => new CheckinGroupOptionDto
{
Id = group.Key.Id,
EventProjectId = project.Key.EventProjectId,
ProjectId = project.Key.ProjectId,
ProjectName = project.Key.ProjectName,
GroupName = group.Key.GroupName,
Order = groupIndex + 1
}).ToList()
}).ToList();
return new CheckinEventOptionsDto
{
EventId = evt.Id,
EventName = evt.EventName,
Projects = projects,
Units = units,
// 兼容旧调用方:Sessions 不再是检录对象。
Sessions = new List<CheckinSessionOptionDto>(),
Rounds = new List<CheckinRoundOptionDto>()
};
}
private async Task<CheckinEventOptionsDto?> BuildImportedOptionsAsync(EventInfo evt)
{
var sessions = await _sessionRepository.AsQueryable()
.Where(x => x.EventId == evt.Id && !x.DeleteMark && x.Status == AvailabilityStatus.Enable)
.ToListAsync();
if (sessions.Count == 0) return null;
var sessionIds = sessions.Select(x => x.Id).ToList();
var entries = await _entryRepository.AsQueryable()
.Where(x => sessionIds.Contains(x.SessionId) && x.SourceType == 1 &&
!x.DeleteMark && x.Status == AvailabilityStatus.Enable)
.ToListAsync();
if (entries.Count == 0) return null;
var eventProjectIds = sessions.Select(x => x.EventProjectId).Distinct().ToList();
var eventProjects = await _eventProjectRepository.AsQueryable()
.Where(x => eventProjectIds.Contains(x.Id) && !x.DeleteMark)
.ToListAsync();
var projectIds = eventProjects.Select(x => x.ProjectId).Distinct().ToList();
var projects = await _projectRepository.AsQueryable()
.Where(x => projectIds.Contains(x.Id) && !x.DeleteMark)
.ToListAsync();
var venueIds = sessions.Select(x => x.VenueId).Distinct().ToList();
var venues = await _venueRepository.AsQueryable()
.Where(x => venueIds.Contains(x.Id) && !x.DeleteMark)
.ToListAsync();
var eventProjectMap = eventProjects.ToDictionary(x => x.Id);
var projectMap = projects.ToDictionary(x => x.Id);
var venueMap = venues.ToDictionary(x => x.Id);
var sessionOrderMap = sessions.ToDictionary(
x => x.Id,
x => GetImportedSessionOrder(x.SessionName, GetSessionOrder(x.SessionNo)));
var orderedSessions = sessions
.OrderBy(x => sessionOrderMap[x.Id])
.ThenBy(x => venueMap.GetValueOrDefault(x.VenueId)?.Sort ?? int.MaxValue)
.ThenBy(x => x.SessionNo)
.ToList();
var sessionMap = sessions.ToDictionary(x => x.Id);
var units = entries
.Where(x => sessionMap.ContainsKey(x.SessionId))
.OrderBy(x => sessionOrderMap[x.SessionId])
.ThenBy(x => x.PlayOrder)
.Select((entry, index) =>
{
var session = sessionMap[entry.SessionId];
eventProjectMap.TryGetValue(session.EventProjectId, out var eventProject);
var project = eventProject == null ? null : projectMap.GetValueOrDefault(eventProject.ProjectId);
venueMap.TryGetValue(session.VenueId, out var venue);
return new CheckinOrderBookUnitDto
{
UnitKey = $"SE{entry.Id}",
IsTeamProject = !string.IsNullOrWhiteSpace(entry.TeamName),
Order = index + 1,
SessionIndex = sessionOrderMap[session.Id],
VenueIndex = venue?.Sort ?? 0,
VenueName = venue?.VenueName ?? string.Empty,
EventProjectId = session.EventProjectId,
ProjectId = eventProject?.ProjectId ?? 0,
ProjectName = project?.Name ?? string.Empty,
GroupId = session.EventProjectGroupId,
GroupName = entry.GroupName,
RegistrationItemId = 0,
TeamName = entry.TeamName ?? string.Empty,
UnitName = string.Empty,
RegistrationPersonIds = new List<long>(),
Athletes = new List<CheckinAthleteDto>
{
new()
{
Id = entry.Id,
BibNo = entry.EntryNo,
Name = entry.ParticipantName,
TeamName = entry.TeamName ?? string.Empty,
OrderBookSessionName = session.SessionName ?? string.Empty
}
}
};
}).ToList();
var projectOptions = units
.GroupBy(x => new { x.EventProjectId, x.ProjectId, x.ProjectName })
.Select((project, projectIndex) => new CheckinProjectOptionDto
{
EventProjectId = project.Key.EventProjectId,
ProjectId = project.Key.ProjectId,
ProjectName = project.Key.ProjectName,
Order = projectIndex + 1,
Groups = project.Where(x => x.GroupId.HasValue)
.GroupBy(x => new { Id = x.GroupId!.Value, x.GroupName })
.Select((group, groupIndex) => new CheckinGroupOptionDto
{
Id = group.Key.Id,
EventProjectId = project.Key.EventProjectId,
ProjectId = project.Key.ProjectId,
ProjectName = project.Key.ProjectName,
GroupName = group.Key.GroupName,
Order = groupIndex + 1
}).ToList()
}).ToList();
return new CheckinEventOptionsDto
{
EventId = evt.Id,
EventName = evt.EventName,
Projects = projectOptions,
Units = units,
Sessions = new List<CheckinSessionOptionDto>(),
Rounds = new List<CheckinRoundOptionDto>()
};
}
private static int GetSessionOrder(string? sessionNo)
{
if (string.IsNullOrWhiteSpace(sessionNo)) return int.MaxValue;
var segment = sessionNo.Split('-').LastOrDefault();
return int.TryParse(segment, out var value) ? value : int.MaxValue;
}
private static int GetImportedSessionOrder(string? sessionName, int fallback)
{
if (string.IsNullOrWhiteSpace(sessionName)) return fallback;
var normalized = sessionName.Trim();
if (normalized.StartsWith('第') && normalized.EndsWith('场'))
normalized = normalized[1..^1].Trim();
return int.TryParse(normalized, out var value) && value > 0 ? value : fallback;
}
public static CheckinOrderBookUnitDto ResolveUnit(CheckinEventOptionsDto options, string unitKey)
{
var unit = options.Units.FirstOrDefault(x => x.UnitKey == unitKey);
return unit ?? throw Oops.Bah("所选检录对象不属于当前赛事秩序册");
}
/// <summary>
/// 生成检录对象唯一键。团队项目不传人员 ID,个人项目必须传入人员 ID。
/// </summary>
public static string BuildUnitKey(
long eventProjectId, long registrationItemId, long? registrationPersonId = null)
{
var key = $"EP{eventProjectId}-RI{registrationItemId}";
return registrationPersonId.HasValue && registrationPersonId.Value > 0
? $"{key}-RP{registrationPersonId.Value}"
: key;
}
private static string FormatBibNo(long personId)
{
var value = personId.ToString();
return value.Length > 6 ? value[^6..] : value.PadLeft(6, '0');
}
}