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;
}
}