331 lines
14 KiB
C#
331 lines
14 KiB
C#
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 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
|
||
};
|
||
}
|
||
|
||
/// <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();
|
||
|
||
var taskCheckinNoMap = activeTasks.ToDictionary(x => x.Id, x => x.CheckinNo);
|
||
var passedByKey = new Dictionary<string, Dictionary<int, DateTime?>>();
|
||
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;
|
||
}
|
||
|
||
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<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)
|
||
}).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.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 => "星期六",
|
||
_ => "星期日"
|
||
};
|
||
|
||
/// <summary>检录进度计算的中间上下文,避免重复查询检录记录。</summary>
|
||
private sealed class CheckinProgressContext
|
||
{
|
||
public Dictionary<string, Dictionary<int, DateTime?>> PassedByKey { get; init; } = new();
|
||
public int FirstCheckinNo { get; init; } = 1;
|
||
public int LastCheckinNo { 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;
|
||
}
|
||
}
|