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 units = !targetProjectId.HasValue ? new List() : options.Units .Where(x => x.EventProjectId == targetProjectId.Value) .OrderBy(x => x.Order) .Select(unit => { // 投屏的“当前检录项目”卡片只表达一检状态,避免二检/三检进度覆盖为“已上场”。 var status = ResolveFirstCheckinUnitStatus(unit, context); 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 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(); // 检录员姓名按 CheckerUserId 批量查一次,避免逐条检录记录反查用户; // AppUser.Name 为实名认证姓名,未认证的检录员退回昵称展示。 var checkerIds = records.Select(x => x.CheckerUserId).Distinct().ToList(); var checkers = checkerIds.Count == 0 ? new List() : await _userRepository.AsQueryable() .Where(x => checkerIds.Contains(x.Id) && !x.DeleteMark) .ToListAsync(); var checkerNameMap = checkers.ToDictionary( x => x.Id, x => !string.IsNullOrWhiteSpace(x.Name) ? x.Name! : x.NickName ?? string.Empty); var taskCheckinNoMap = activeTasks.ToDictionary(x => x.Id, x => x.CheckinNo); var passedByKey = new Dictionary>(); var checkerNameByKey = new Dictionary>(); foreach (var record in records) { var unitKey = ResolveRecordUnitKey(options.Units, record); if (unitKey == null) continue; if (!taskCheckinNoMap.TryGetValue(record.TaskId, out var checkinNo)) continue; if (!passedByKey.TryGetValue(unitKey, out var rounds)) { rounds = new Dictionary(); passedByKey[unitKey] = rounds; } rounds[checkinNo] = record.CheckedTime; if (!checkerNameByKey.TryGetValue(unitKey, out var checkerNames)) { checkerNames = new Dictionary(); checkerNameByKey[unitKey] = checkerNames; } checkerNames[checkinNo] = checkerNameMap.GetValueOrDefault(record.CheckerUserId) ?? string.Empty; } var firstCheckinNo = activeTasks.Count == 0 ? 1 : activeTasks.Min(x => x.CheckinNo); var onStageCheckinNo = ResolveOnStageCheckinNo(activeTasks); var context = new CheckinProgressContext { PassedByKey = passedByKey, CheckerNameByKey = checkerNameByKey, FirstCheckinNo = firstCheckinNo, OnStageCheckinNo = onStageCheckinNo, ActiveCheckinNumbers = activeTasks.Select(x => x.CheckinNo).OrderBy(x => x).ToList() }; // 赛事检录顺序完全跟随一检进度:一检开始即为检录中,一检全部通过即切换下一项目。 var stages = options.Units .GroupBy(x => new { x.EventProjectId, x.ProjectName }) .Select(group => { var groupUnits = group.OrderBy(x => x.Order).ToList(); var total = groupUnits.Count; var checkedCount = groupUnits.Count(x => context.IsPassed(x.UnitKey, firstCheckinNo)); return new CheckinProgressStageDto { EventProjectId = group.Key.EventProjectId, GroupId = null, ProjectName = group.Key.ProjectName, GroupName = string.Empty, DisplayName = group.Key.ProjectName, Order = groupUnits[0].Order, TotalUnits = total, CheckedUnits = checkedCount, ProgressPercent = total == 0 ? 0 : (int)Math.Round(checkedCount * 100d / total), StageStatus = ResolveStageStatus(total, checkedCount) }; }) .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 is 1 or 2); // 当前正在比赛:三检通过视为上场;赛事仅配置两检时,二检通过即视为上场。 var competingUnit = options.Units .Where(x => context.IsPassed(x.UnitKey, onStageCheckinNo)) .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), UnitName = string.IsNullOrWhiteSpace(competingUnit.UnitName) ? "/" : competingUnit.UnitName, // 秩序册列没有独立的报名类型字段,以 TeamName 是否为空区分个人与团队报名。 RegistrationTypeText = string.IsNullOrWhiteSpace(competingUnit.TeamName) ? "个人报名" : "团队报名", AthleteNames = competingUnit.Athletes.Select(x => x.Name).ToList() }; } if (context.CurrentCheckin != null) { var checkingUnit = options.Units .Where(x => x.EventProjectId == context.CurrentCheckin.EventProjectId) .OrderBy(x => x.Order) .FirstOrDefault(x => !context.IsPassed(x.UnitKey, firstCheckinNo)); 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), CheckerName = context.GetCheckerName(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.OnStageCheckinNo)) 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 ? 1 : 0; } /// 检录大屏当前项目中的队伍状态仅跟随一检。 private static int ResolveFirstCheckinUnitStatus( CheckinOrderBookUnitDto unit, CheckinProgressContext context) { if (context.IsPassed(unit.UnitKey, context.FirstCheckinNo)) return 2; var stage = context.CurrentCheckin; return stage != null && stage.EventProjectId == unit.EventProjectId ? 1 : 0; } private static int ResolveStageStatus(int total, int checkedCount) { if (total > 0 && checkedCount == 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 { // 两个字典的键结构一致:外层为秩序册列 UnitKey,内层为检次号。 public Dictionary> PassedByKey { get; init; } = new(); public Dictionary> CheckerNameByKey { get; init; } = new(); public int FirstCheckinNo { get; init; } = 1; public int OnStageCheckinNo { 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; public string GetCheckerName(string unitKey, int checkinNo) => CheckerNameByKey.TryGetValue(unitKey, out var rounds) && rounds.TryGetValue(checkinNo, out var name) ? name : string.Empty; } }