using YD_Event.Application.Client.Dtos; namespace YD_Event.Application.Client; /// 面向运动员、教练的检录进度查询与检录投屏。 public partial class CheckinController { [HttpGet("progress")] [DisplayName("查看赛事检录进度")] public async Task GetProgress([FromQuery] long eventId) { var userId = _authManager.UserId; var options = await _scheduleService.GetOptionsAsync(eventId); var activeTasks = await GetActiveEventTasksAsync(eventId); var context = await BuildProgressContextAsync(options, activeTasks); // 报名人员与当前登录用户按 UserId 关联;教练可看到同一报名单下的运动员。 var myPersonIds = await ResolveMyPersonIdsAsync(eventId, userId); var mine = options.Units .Where(x => x.RegistrationPersonIds.Any(myPersonIds.Contains)) .OrderBy(x => x.Order) .Select(unit => BuildMineDto(unit, context, activeTasks, myPersonIds)) .ToList(); var now = DateTime.Now; return new CheckinProgressOutput { EventId = options.EventId, EventName = options.EventName, ServerTime = now, ServerTimeText = now.ToString("yyyy-MM-dd HH:mm:ss"), WeekDayText = ToWeekDayText(now), CurrentCheckin = context.CurrentCheckin, CurrentCompeting = context.CurrentCompeting, Stages = context.Stages, Mine = mine }; } [HttpGet("screen-cast")] [AllowAnonymous] [DisplayName("检录投屏")] public async Task GetScreenCast( [FromQuery] long eventId, [FromQuery] long? eventProjectId = null, [FromQuery] long? groupId = null) { var options = await _scheduleService.GetOptionsAsync(eventId); var activeTasks = await GetActiveEventTasksAsync(eventId); var context = await BuildProgressContextAsync(options, activeTasks); // 未指定项目组别时投屏当前检录中的项目组别。 var targetProjectId = eventProjectId ?? context.CurrentCheckin?.EventProjectId; var targetGroupId = groupId ?? context.CurrentCheckin?.GroupId; var units = options.Units .Where(x => (!targetProjectId.HasValue || x.EventProjectId == targetProjectId.Value) && (!targetGroupId.HasValue || x.GroupId == targetGroupId.Value)) .OrderBy(x => x.Order) .Select(unit => { var status = ResolveUnitStatus(unit, context, activeTasks); return new CheckinScreenCastUnitDto { UnitKey = unit.UnitKey, Order = unit.Order, SessionIndex = unit.SessionIndex, VenueName = unit.VenueName, ProjectName = unit.ProjectName, GroupName = unit.GroupName, TeamName = ResolveTeamName(unit), UnitName = unit.UnitName, AthleteNames = unit.Athletes.Select(x => x.Name).ToList(), CheckinStatus = status, CheckinStatusText = ToCheckinStatusText(status) }; }).ToList(); var now = DateTime.Now; return new CheckinScreenCastOutput { EventId = options.EventId, EventName = options.EventName, ServerTime = now, ServerTimeText = now.ToString("yyyy-MM-dd HH:mm:ss"), WeekDayText = ToWeekDayText(now), CurrentCheckin = context.CurrentCheckin, CurrentCompeting = context.CurrentCompeting, Stages = context.Stages, Units = units }; } /// 汇总各检次的检录记录,生成项目组别三态与当前检录、当前比赛信息。 private async Task BuildProgressContextAsync( CheckinEventOptionsDto options, List activeTasks) { var validUnitKeys = options.Units.Select(x => x.UnitKey).ToHashSet(); var taskIds = activeTasks.Select(x => x.Id).ToList(); var records = taskIds.Count == 0 ? new List() : await _recordRepository.AsQueryable() .Where(x => taskIds.Contains(x.TaskId) && x.SessionKey != null && x.Checked && !x.DeleteMark) .ToListAsync(); var taskCheckinNoMap = activeTasks.ToDictionary(x => x.Id, x => x.CheckinNo); var passedByKey = new Dictionary>(); foreach (var record in records.Where(x => validUnitKeys.Contains(x.SessionKey!))) { if (!taskCheckinNoMap.TryGetValue(record.TaskId, out var checkinNo)) continue; if (!passedByKey.TryGetValue(record.SessionKey!, out var rounds)) { rounds = new Dictionary(); passedByKey[record.SessionKey!] = rounds; } rounds[checkinNo] = record.CheckedTime; } var firstCheckinNo = activeTasks.Count == 0 ? 1 : activeTasks.Min(x => x.CheckinNo); var lastCheckinNo = activeTasks.Count == 0 ? 1 : activeTasks.Max(x => x.CheckinNo); var context = new CheckinProgressContext { PassedByKey = passedByKey, FirstCheckinNo = firstCheckinNo, LastCheckinNo = lastCheckinNo, ActiveCheckinNumbers = activeTasks.Select(x => x.CheckinNo).OrderBy(x => x).ToList() }; // 按秩序册顺序汇总项目组别阶段;一检开始即为检录中,三检全部通过即为已完成。 var stages = options.Units .GroupBy(x => new { x.EventProjectId, x.GroupId, x.ProjectName, x.GroupName }) .Select(group => { var groupUnits = group.OrderBy(x => x.Order).ToList(); var total = groupUnits.Count; var checkedCount = groupUnits.Count(x => context.IsPassed(x.UnitKey, firstCheckinNo)); var finishedCount = groupUnits.Count(x => context.IsPassed(x.UnitKey, lastCheckinNo)); return new CheckinProgressStageDto { EventProjectId = group.Key.EventProjectId, GroupId = group.Key.GroupId, ProjectName = group.Key.ProjectName, GroupName = group.Key.GroupName, DisplayName = $"{group.Key.ProjectName} - {group.Key.GroupName}", Order = groupUnits[0].Order, TotalUnits = total, CheckedUnits = checkedCount, ProgressPercent = total == 0 ? 0 : (int)Math.Round(checkedCount * 100d / total), StageStatus = ResolveStageStatus(total, checkedCount, finishedCount) }; }) .OrderBy(x => x.Order) .ToList(); // 未开始的阶段中最靠前的一个显示"即将开始",其余保持"等待中"。 var firstWaiting = stages.FirstOrDefault(x => x.StageStatus == 3); if (firstWaiting != null) firstWaiting.StageStatus = 2; foreach (var stage in stages) stage.StageStatusText = ToStageStatusText(stage.StageStatus); context.Stages = stages; context.CurrentCheckin = stages.FirstOrDefault(x => x.StageStatus == 1); // 当前正在比赛:末检次已通过且顺序最靠后的一列。 var competingUnit = options.Units .Where(x => context.IsPassed(x.UnitKey, lastCheckinNo)) .OrderByDescending(x => x.Order) .FirstOrDefault(); if (competingUnit != null) { context.CurrentCompeting = new CheckinProgressStageDto { EventProjectId = competingUnit.EventProjectId, GroupId = competingUnit.GroupId, ProjectName = competingUnit.ProjectName, GroupName = competingUnit.GroupName, DisplayName = $"{competingUnit.ProjectName} - {competingUnit.GroupName}", Order = competingUnit.Order, StageStatus = 1, StageStatusText = "比赛中", TeamName = ResolveTeamName(competingUnit), AthleteNames = competingUnit.Athletes.Select(x => x.Name).ToList() }; } if (context.CurrentCheckin != null) { var checkingUnit = options.Units .Where(x => x.EventProjectId == context.CurrentCheckin.EventProjectId && x.GroupId == context.CurrentCheckin.GroupId) .OrderBy(x => x.Order) .FirstOrDefault(x => !context.IsPassed(x.UnitKey, lastCheckinNo)); if (checkingUnit != null) { context.CurrentCheckin.TeamName = ResolveTeamName(checkingUnit); context.CurrentCheckin.AthleteNames = checkingUnit.Athletes.Select(x => x.Name).ToList(); } } return context; } private CheckinProgressMineDto BuildMineDto( CheckinOrderBookUnitDto unit, CheckinProgressContext context, List activeTasks, HashSet myPersonIds) { var status = ResolveUnitStatus(unit, context, activeTasks); var athleteNames = unit.Athletes.Where(x => myPersonIds.Contains(x.Id)).Select(x => x.Name).ToList(); if (athleteNames.Count == 0) athleteNames = unit.Athletes.Select(x => x.Name).ToList(); return new CheckinProgressMineDto { UnitKey = unit.UnitKey, EventProjectId = unit.EventProjectId, GroupId = unit.GroupId, ProjectName = unit.ProjectName, GroupName = unit.GroupName, DisplayName = $"{unit.ProjectName} - {unit.GroupName}", AthleteNames = athleteNames, TeamName = ResolveTeamName(unit), UnitName = string.IsNullOrWhiteSpace(unit.UnitName) ? "/" : unit.UnitName, SessionIndex = unit.SessionIndex, VenueName = unit.VenueName, CheckinStatus = status, CheckinStatusText = ToCheckinStatusText(status), Rounds = context.ActiveCheckinNumbers.Select(checkinNo => new CheckinProgressRoundDto { CheckinNo = checkinNo, CheckinName = ToCheckinName(checkinNo), Passed = context.IsPassed(unit.UnitKey, checkinNo), CheckedTime = context.GetCheckedTime(unit.UnitKey, checkinNo) }).ToList(), Notice = status switch { 1 => "请前往检录口", 2 => "已完成检录,请等待上场", 3 => "已上场", _ => "请留意检录通知" } }; } /// 我的报名人员:本人报名记录 + 我作为联系人提交的报名单下全部人员。 private async Task> ResolveMyPersonIdsAsync(long eventId, long userId) { var mine = await _personRepository.AsQueryable() .Where(x => x.UserId == userId && !x.DeleteMark) .ToListAsync(); var registrationIds = await _registrationRepository.AsQueryable() .Where(x => x.EventId == eventId && x.CreatedUserId == userId && !x.DeleteMark) .Select(x => x.Id).ToListAsync(); var teamPersons = registrationIds.Count == 0 ? new List() : await _personRepository.AsQueryable() .Where(x => registrationIds.Contains(x.RegistrationId) && !x.DeleteMark) .ToListAsync(); return mine.Select(x => x.Id).Concat(teamPersons.Select(x => x.Id)).ToHashSet(); } /// 秩序册列状态:0-等待中,1-检录中,2-已检录,3-已上场。 private static int ResolveUnitStatus( CheckinOrderBookUnitDto unit, CheckinProgressContext context, List activeTasks) { if (context.IsPassed(unit.UnitKey, context.LastCheckinNo)) return 3; if (context.IsPassed(unit.UnitKey, context.FirstCheckinNo)) return activeTasks.Count > 1 ? 2 : 3; var stage = context.CurrentCheckin; return stage != null && stage.EventProjectId == unit.EventProjectId && stage.GroupId == unit.GroupId ? 1 : 0; } private static int ResolveStageStatus(int total, int checkedCount, int finishedCount) { if (total > 0 && finishedCount == total) return 4; if (checkedCount > 0) return 1; return 3; } private static string ResolveTeamName(CheckinOrderBookUnitDto unit) { if (!string.IsNullOrWhiteSpace(unit.TeamName)) return unit.TeamName; return string.IsNullOrWhiteSpace(unit.UnitName) ? "/" : unit.UnitName; } private static string ToStageStatusText(int stageStatus) => stageStatus switch { 1 => "检录中", 2 => "即将开始", 4 => "已完成", _ => "等待中" }; private static string ToCheckinStatusText(int status) => status switch { 1 => "检录中", 2 => "已检录", 3 => "已上场", _ => "等待中" }; private static string ToWeekDayText(DateTime time) => time.DayOfWeek switch { DayOfWeek.Monday => "星期一", DayOfWeek.Tuesday => "星期二", DayOfWeek.Wednesday => "星期三", DayOfWeek.Thursday => "星期四", DayOfWeek.Friday => "星期五", DayOfWeek.Saturday => "星期六", _ => "星期日" }; /// 检录进度计算的中间上下文,避免重复查询检录记录。 private sealed class CheckinProgressContext { public Dictionary> PassedByKey { get; init; } = new(); public int FirstCheckinNo { get; init; } = 1; public int LastCheckinNo { get; init; } = 1; public List ActiveCheckinNumbers { get; init; } = new(); public List Stages { get; set; } = new(); public CheckinProgressStageDto? CurrentCheckin { get; set; } public CheckinProgressStageDto? CurrentCompeting { get; set; } public bool IsPassed(string unitKey, int checkinNo) => PassedByKey.TryGetValue(unitKey, out var rounds) && rounds.ContainsKey(checkinNo); public DateTime? GetCheckedTime(string unitKey, int checkinNo) => PassedByKey.TryGetValue(unitKey, out var rounds) && rounds.TryGetValue(checkinNo, out var time) ? time : null; } }