YD_Event.Admin/YD_Event.Application/Client/CheckinController.Progress.cs

374 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using YD_Event.Application.Client.Dtos;
namespace YD_Event.Application.Client;
/// <summary>面向运动员、教练的检录进度查询与检录投屏。</summary>
public partial class CheckinController
{
[HttpGet("progress")]
[DisplayName("查看赛事检录进度")]
public async Task<CheckinProgressOutput> 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<CheckinScreenCastOutput> 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<CheckinScreenCastUnitDto>()
: 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
};
}
/// <summary>汇总各检次的检录记录,生成项目组别三态与当前检录、当前比赛信息。</summary>
private async Task<CheckinProgressContext> BuildProgressContextAsync(
CheckinEventOptionsDto options, List<CheckinTask> 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<CheckinRecord>()
: 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<YD_Event.Core.Entities.AppUser>()
: 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<string, Dictionary<int, DateTime?>>();
var checkerNameByKey = new Dictionary<string, Dictionary<int, string>>();
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<int, DateTime?>();
passedByKey[record.SessionKey!] = rounds;
}
rounds[checkinNo] = record.CheckedTime;
if (!checkerNameByKey.TryGetValue(record.SessionKey!, out var checkerNames))
{
checkerNames = new Dictionary<int, string>();
checkerNameByKey[record.SessionKey!] = 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<CheckinTask> activeTasks,
HashSet<long> 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 => "已上场",
_ => "请留意检录通知"
}
};
}
/// <summary>我的报名人员:本人报名记录 + 我作为联系人提交的报名单下全部人员。</summary>
private async Task<HashSet<long>> 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<EventRegistrationPerson>()
: 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();
}
/// <summary>秩序册列状态:0-等待中,1-检录中,2-已检录,3-已上场。</summary>
private static int ResolveUnitStatus(
CheckinOrderBookUnitDto unit, CheckinProgressContext context, List<CheckinTask> 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;
}
/// <summary>检录大屏当前项目中的队伍状态仅跟随一检。</summary>
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 => "星期六",
_ => "星期日"
};
/// <summary>检录进度计算的中间上下文,避免重复查询检录记录。</summary>
private sealed class CheckinProgressContext
{
// 两个字典的键结构一致:外层为秩序册列 UnitKey,内层为检次号。
public Dictionary<string, Dictionary<int, DateTime?>> PassedByKey { get; init; } = new();
public Dictionary<string, Dictionary<int, string>> CheckerNameByKey { get; init; } = new();
public int FirstCheckinNo { get; init; } = 1;
public int OnStageCheckinNo { get; init; } = 1;
public List<int> ActiveCheckinNumbers { get; init; } = new();
public List<CheckinProgressStageDto> 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;
}
}