using YD_Event.Application.Client.Dtos; using YD_Event.Application.Event; namespace YD_Event.Application.Client; /// 从秩序册实时生成检录项目、组别和列对象。 public class CheckinScheduleService : ITransient { private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarRepository _personRepository; private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _sessionRepository; private readonly ISqlSugarRepository _entryRepository; private readonly ISqlSugarRepository _eventProjectRepository; private readonly ISqlSugarRepository _projectRepository; private readonly ISqlSugarRepository _venueRepository; private readonly EventArrangementService _arrangementService; public CheckinScheduleService( ISqlSugarRepository eventRepository, ISqlSugarRepository personRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository sessionRepository, ISqlSugarRepository entryRepository, ISqlSugarRepository eventProjectRepository, ISqlSugarRepository projectRepository, ISqlSugarRepository venueRepository, EventArrangementService arrangementService) { _eventRepository = eventRepository; _personRepository = personRepository; _registrationRepository = registrationRepository; _sessionRepository = sessionRepository; _entryRepository = entryRepository; _eventProjectRepository = eventProjectRepository; _projectRepository = projectRepository; _venueRepository = venueRepository; _arrangementService = arrangementService; } public async Task 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() : 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() : (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(), Rounds = new List() }; } private async Task 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 sessionMap = sessions.ToDictionary(x => x.Id); // 秩序册允许不同比赛日重复从“第1场”开始,不能再按场次数字排序。 // 导入时出场记录 ID 按 Excel 行顺序生成,因此按 ID 即为原始导入顺序。 var units = entries .Where(x => sessionMap.ContainsKey(x.SessionId)) .OrderBy(x => x.Id) .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(), Athletes = new List { 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(), Rounds = new List() }; } 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("所选检录对象不属于当前赛事秩序册"); } /// /// 生成检录对象唯一键。团队项目不传人员 ID,个人项目必须传入人员 ID。 /// 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'); } }