2026-08-07 14:59:15 +08:00
|
|
|
|
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;
|
2026-10-08 16:13:04 +08:00
|
|
|
|
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
|
2026-10-08 10:12:54 +08:00
|
|
|
|
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;
|
2026-08-07 14:59:15 +08:00
|
|
|
|
private readonly EventArrangementService _arrangementService;
|
|
|
|
|
|
|
|
|
|
|
|
public CheckinScheduleService(
|
|
|
|
|
|
ISqlSugarRepository<EventInfo> eventRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventRegistrationPerson> personRepository,
|
2026-10-08 16:13:04 +08:00
|
|
|
|
ISqlSugarRepository<EventRegistration> registrationRepository,
|
2026-10-08 10:12:54 +08:00
|
|
|
|
ISqlSugarRepository<EventSession> sessionRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventSessionEntry> entryRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventProject> eventProjectRepository,
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventVenue> venueRepository,
|
2026-08-07 14:59:15 +08:00
|
|
|
|
EventArrangementService arrangementService)
|
|
|
|
|
|
{
|
|
|
|
|
|
_eventRepository = eventRepository;
|
|
|
|
|
|
_personRepository = personRepository;
|
2026-10-08 16:13:04 +08:00
|
|
|
|
_registrationRepository = registrationRepository;
|
2026-10-08 10:12:54 +08:00
|
|
|
|
_sessionRepository = sessionRepository;
|
|
|
|
|
|
_entryRepository = entryRepository;
|
|
|
|
|
|
_eventProjectRepository = eventProjectRepository;
|
|
|
|
|
|
_projectRepository = projectRepository;
|
|
|
|
|
|
_venueRepository = venueRepository;
|
2026-08-07 14:59:15 +08:00
|
|
|
|
_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("赛事不存在或已停用");
|
|
|
|
|
|
|
2026-10-08 10:12:54 +08:00
|
|
|
|
// 秩序册确认导入后已经固化了场次和出场顺序,检录直接沿用该顺序,
|
|
|
|
|
|
// 不再要求报名平台存在报名人员或重复执行赛事编排。
|
|
|
|
|
|
var importedOptions = await BuildImportedOptionsAsync(evt);
|
|
|
|
|
|
if (importedOptions != null) return importedOptions;
|
|
|
|
|
|
|
2026-08-07 14:59:15 +08:00
|
|
|
|
// 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);
|
2026-10-08 16:13:04 +08:00
|
|
|
|
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);
|
2026-08-07 14:59:15 +08:00
|
|
|
|
|
2026-09-21 14:55:44 +08:00
|
|
|
|
// 团队项目沿用报名项目作为检录单位;个人项目追加报名人员 ID,
|
|
|
|
|
|
// 避免单位账号在同一报名项目下填报多人时共用同一个检录键。
|
2026-08-07 14:59:15 +08:00
|
|
|
|
var units = rows.Select((row, index) => new CheckinOrderBookUnitDto
|
|
|
|
|
|
{
|
2026-09-21 14:55:44 +08:00
|
|
|
|
UnitKey = BuildUnitKey(row.EventProjectId, row.RegistrationItemId,
|
|
|
|
|
|
row.IsTeamProject ? null : row.RegistrationPersonIds.SingleOrDefault()),
|
|
|
|
|
|
IsTeamProject = row.IsTeamProject,
|
2026-08-07 14:59:15 +08:00
|
|
|
|
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,
|
2026-10-08 16:13:04 +08:00
|
|
|
|
RegistrationMode = row.RegistrationPersonIds
|
|
|
|
|
|
.Where(personMap.ContainsKey)
|
|
|
|
|
|
.Select(personId => registrationModes.GetValueOrDefault(personMap[personId].RegistrationId))
|
|
|
|
|
|
.FirstOrDefault(mode => mode > 0) is var mode && mode > 0 ? mode : null,
|
2026-08-07 14:59:15 +08:00
|
|
|
|
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>()
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-08 10:12:54 +08:00
|
|
|
|
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);
|
2026-10-10 09:04:52 +08:00
|
|
|
|
var sessionOrderMap = sessions.ToDictionary(
|
|
|
|
|
|
x => x.Id,
|
|
|
|
|
|
x => GetImportedSessionOrder(x.SessionName, GetSessionOrder(x.SessionNo)));
|
2026-10-08 10:12:54 +08:00
|
|
|
|
var sessionMap = sessions.ToDictionary(x => x.Id);
|
|
|
|
|
|
|
2026-10-22 08:43:25 +08:00
|
|
|
|
// 秩序册允许不同比赛日重复从“第1场”开始,不能再按场次数字排序。
|
|
|
|
|
|
// 导入时出场记录 ID 按 Excel 行顺序生成,因此按 ID 即为原始导入顺序。
|
2026-10-08 10:12:54 +08:00
|
|
|
|
var units = entries
|
|
|
|
|
|
.Where(x => sessionMap.ContainsKey(x.SessionId))
|
2026-10-22 08:43:25 +08:00
|
|
|
|
.OrderBy(x => x.Id)
|
2026-10-08 10:12:54 +08:00
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-10 09:04:52 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 14:59:15 +08:00
|
|
|
|
public static CheckinOrderBookUnitDto ResolveUnit(CheckinEventOptionsDto options, string unitKey)
|
|
|
|
|
|
{
|
|
|
|
|
|
var unit = options.Units.FirstOrDefault(x => x.UnitKey == unitKey);
|
|
|
|
|
|
return unit ?? throw Oops.Bah("所选检录对象不属于当前赛事秩序册");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-21 14:55:44 +08:00
|
|
|
|
/// <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;
|
|
|
|
|
|
}
|
2026-08-07 14:59:15 +08:00
|
|
|
|
|
|
|
|
|
|
private static string FormatBibNo(long personId)
|
|
|
|
|
|
{
|
|
|
|
|
|
var value = personId.ToString();
|
|
|
|
|
|
return value.Length > 6 ? value[^6..] : value.PadLeft(6, '0');
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|