772 lines
35 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 Microsoft.Extensions.Logging;
using YD_Event.Application.Auth;
using YD_Event.Application.Client.Dtos;
using YD_Event.Application.Sms;
namespace YD_Event.Application.Client;
/// <summary>小程序检录员按秩序册列进行顺序检录。</summary>
[ApiDescriptionSettings("微信小程序接口")]
[Authorize]
public partial class CheckinController : IDynamicApiController
{
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _userRepository;
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
private readonly ISqlSugarRepository<CheckinTask> _taskRepository;
private readonly ISqlSugarRepository<CheckinRecord> _recordRepository;
private readonly ISqlSugarRepository<EventRegistrationPerson> _personRepository;
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Project> _projectRepository;
private readonly ISqlSugarRepository<TeamMember> _teamMemberRepository;
private readonly CheckinScheduleService _scheduleService;
private readonly AliyunSmsService _smsService;
private readonly ILogger<CheckinController> _logger;
private readonly AuthManager _authManager;
public CheckinController(
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<CheckinTask> taskRepository,
ISqlSugarRepository<CheckinRecord> recordRepository,
ISqlSugarRepository<EventRegistrationPerson> personRepository,
ISqlSugarRepository<EventRegistration> registrationRepository,
ISqlSugarRepository<YD_Event.Core.Entities.Project> projectRepository,
ISqlSugarRepository<TeamMember> teamMemberRepository,
CheckinScheduleService scheduleService,
AliyunSmsService smsService,
ILogger<CheckinController> logger,
AuthManager authManager)
{
_userRepository = userRepository;
_eventRepository = eventRepository;
_taskRepository = taskRepository;
_recordRepository = recordRepository;
_personRepository = personRepository;
_registrationRepository = registrationRepository;
_projectRepository = projectRepository;
_teamMemberRepository = teamMemberRepository;
_scheduleService = scheduleService;
_smsService = smsService;
_logger = logger;
_authManager = authManager;
}
[HttpGet("current")]
[DisplayName("获取当前赛事检录任务")]
public async Task<CheckinContextDto?> GetCurrent(
[FromQuery] long? eventProjectId = null,
[FromQuery] long? groupId = null)
{
var userId = await EnsureCheckerAsync();
var task = await GetCurrentTaskAsync(userId);
return task == null ? null : await BuildContextAsync(task, eventProjectId, groupId);
}
[HttpPost("status")]
[UnitOfWork]
[DisplayName("更新秩序册列当前检次状态")]
public async Task<CheckinContextDto> UpdateStatus([FromBody] UpdateCheckinStatusInput input)
{
var checkerUserId = await EnsureCheckerAsync();
var task = await GetCurrentTaskAsync(checkerUserId);
if (task == null) throw Oops.Bah("当前账号尚未由后台绑定赛事检录任务");
var options = await _scheduleService.GetOptionsAsync(task.EventId);
var unit = CheckinScheduleService.ResolveUnit(options, input.UnitKey);
if (task.CheckinNo > 1)
{
var previousTask = await GetEventTaskAsync(task.EventId, task.CheckinNo - 1);
var previousPassed = previousTask != null && await _recordRepository.IsAnyAsync(x =>
x.TaskId == previousTask.Id && x.SessionKey == unit.UnitKey && x.Checked && !x.DeleteMark);
if (!previousPassed)
throw Oops.Bah($"该秩序册列尚未通过{ToCheckinName(task.CheckinNo - 1)},不能进行{ToCheckinName(task.CheckinNo)}");
}
if (!input.Checked)
{
var laterTasks = await _taskRepository.AsQueryable()
.Where(x => x.EventId == task.EventId && x.CheckinNo > task.CheckinNo &&
x.TaskStatus == 1 && x.Status == AvailabilityStatus.Enable && !x.DeleteMark)
.ToListAsync();
var laterTaskIds = laterTasks.Select(x => x.Id).ToList();
if (laterTaskIds.Count > 0 && await _recordRepository.IsAnyAsync(x =>
laterTaskIds.Contains(x.TaskId) && x.SessionKey == unit.UnitKey && x.Checked && !x.DeleteMark))
throw Oops.Bah("该秩序册列已通过后续检录,不能取消当前检录");
}
var record = await _recordRepository.GetFirstAsync(x =>
x.TaskId == task.Id && x.SessionKey == unit.UnitKey && !x.DeleteMark);
if (record == null)
{
record = new CheckinRecord
{
TaskId = task.Id,
Round = task.CheckinNo,
SessionKey = unit.UnitKey,
SessionOrder = unit.Order,
GroupId = unit.GroupId,
RegistrationPersonId = unit.RegistrationPersonIds.FirstOrDefault(),
CheckerUserId = checkerUserId,
Checked = input.Checked,
CheckedTime = input.Checked ? DateTime.Now : null,
CreatedUserId = checkerUserId,
CreatedTime = DateTime.Now
};
await _recordRepository.InsertAsync(record);
}
else
{
record.Round = task.CheckinNo;
record.SessionOrder = unit.Order;
record.GroupId = unit.GroupId;
record.CheckerUserId = checkerUserId;
record.Checked = input.Checked;
record.CheckedTime = input.Checked ? DateTime.Now : null;
record.UpdatedUserId = checkerUserId;
record.UpdatedTime = DateTime.Now;
await _recordRepository.UpdateAsync(record);
}
// 保持页面当前项目/组别筛选;UnitKey 已独立校验检录项归属。
return await BuildContextAsync(task, input.EventProjectId, input.GroupId);
}
[HttpPost("start")]
[UnitOfWork]
[DisplayName("开始检录并通知报名人员")]
public async Task<StartCheckinOutput> Start([FromBody] StartCheckinInput input)
{
var checkerUserId = await EnsureCheckerAsync();
var task = await GetCurrentTaskAsync(checkerUserId);
if (task == null) throw Oops.Bah("当前账号尚未由后台绑定赛事检录任务");
var options = await _scheduleService.GetOptionsAsync(task.EventId);
var project = options.Projects.FirstOrDefault(x => x.EventProjectId == input.EventProjectId)
?? throw Oops.Bah("所选参赛项目不属于当前赛事秩序册");
var group = project.Groups.FirstOrDefault(x => x.Id == input.GroupId)
?? throw Oops.Bah("所选参赛组别不属于当前参赛项目");
var units = options.Units
.Where(x => x.EventProjectId == input.EventProjectId && x.GroupId == input.GroupId)
.OrderBy(x => x.Order).ToList();
if (units.Count == 0) throw Oops.Bah("所选项目组别没有可检录的参赛队伍");
// 记录当前检录进度,供运动员端与投屏读取"当前检录项目"。
task.CurrentEventProjectId = input.EventProjectId;
task.CurrentGroupId = input.GroupId;
task.CurrentGroupName = group.GroupName;
task.GroupOrder = group.Order;
task.TotalGroups = project.Groups.Count;
task.NextGroupName = project.Groups.FirstOrDefault(x => x.Order > group.Order)?.GroupName;
task.CurrentSessionKey = units[0].UnitKey;
task.CurrentSessionOrder = units[0].Order;
task.CurrentSessionName = $"{project.ProjectName}-{group.GroupName}";
task.CurrentSessionVenue = units[0].VenueName;
task.UpdatedUserId = checkerUserId;
task.UpdatedTime = DateTime.Now;
await _taskRepository.UpdateAsync(task);
var total = 0;
var success = 0;
var failed = 0;
if (task.CheckinNo == 1)
{
var projectEntity = await _projectRepository.GetFirstAsync(x =>
x.Id == project.ProjectId && !x.DeleteMark && x.Status == AvailabilityStatus.Enable);
if (projectEntity == null) throw Oops.Bah("参赛项目不存在或已停用");
var noticeName = $"{project.ProjectName}-{group.GroupName}";
(total, success, failed) = await NotifyUnitsAsync(
units, projectEntity.ProjectType, options.EventName, noticeName);
}
var context = await BuildContextAsync(task, input.EventProjectId, input.GroupId);
return new StartCheckinOutput
{
TaskId = task.Id,
EventProjectId = project.EventProjectId,
ProjectName = project.ProjectName,
GroupId = group.Id,
GroupName = group.GroupName,
UnitCount = units.Count,
NoticeTotal = total,
SmsSuccessCount = success,
SmsFailedCount = failed,
Message = task.CheckinNo != 1
? $"已开始{ToCheckinName(task.CheckinNo)}"
: success > 0
? $"已开始{ToCheckinName(task.CheckinNo)},已通知{success}人,{failed}人通知失败"
: $"已开始{ToCheckinName(task.CheckinNo)},共{total}人待通知",
Context = context
};
}
[HttpPost("scan")]
[UnitOfWork]
[DisplayName("扫码检录")]
public async Task<ScanCheckinOutput> Scan([FromBody] ScanCheckinInput input)
{
var checkerUserId = await EnsureCheckerAsync();
var task = await GetCurrentTaskAsync(checkerUserId);
if (task == null) throw Oops.Bah("当前账号尚未由后台绑定赛事检录任务");
var code = input.Code?.Trim() ?? string.Empty;
if (code.Length == 0) throw Oops.Bah("扫码内容不能为空");
var options = await _scheduleService.GetOptionsAsync(task.EventId);
var unit = ResolveScannedUnit(options, code, out var athlete);
if (unit == null)
return new ScanCheckinOutput { Success = false, Message = "扫码内容与当前赛事秩序册不匹配,请核对号码布" };
await EnsurePreviousPassedAsync(task, unit.UnitKey);
await UpsertRecordAsync(task, unit, checkerUserId, true);
var context = await BuildContextAsync(task,
input.EventProjectId ?? unit.EventProjectId,
input.GroupId ?? unit.GroupId ?? 0);
return new ScanCheckinOutput
{
Success = true,
Message = $"{ToCheckinName(task.CheckinNo)}检录成功",
UnitKey = unit.UnitKey,
ProjectName = unit.ProjectName,
GroupName = unit.GroupName,
UnitName = string.IsNullOrWhiteSpace(unit.TeamName) ? unit.UnitName : unit.TeamName,
AthleteName = athlete?.Name ?? string.Join(", ", unit.Athletes.Select(x => x.Name)),
BibNo = athlete?.BibNo ?? string.Empty,
Context = context
};
}
[HttpPost("on-stage")]
[UnitOfWork]
[DisplayName("标记参赛队伍上场")]
public async Task<CheckinContextDto> OnStage([FromBody] CheckinOnStageInput input)
{
var checkerUserId = await EnsureCheckerAsync();
var task = await GetCurrentTaskAsync(checkerUserId);
if (task == null) throw Oops.Bah("当前账号尚未由后台绑定赛事检录任务");
var activeTasks = await GetActiveEventTasksAsync(task.EventId);
var lastCheckinNo = activeTasks.Count == 0 ? task.CheckinNo : activeTasks.Max(x => x.CheckinNo);
if (task.CheckinNo != lastCheckinNo)
throw Oops.Bah($"上场由{ToCheckinName(lastCheckinNo)}检录员操作");
var options = await _scheduleService.GetOptionsAsync(task.EventId);
var unit = CheckinScheduleService.ResolveUnit(options, input.UnitKey);
await EnsurePreviousPassedAsync(task, unit.UnitKey);
await UpsertRecordAsync(task, unit, checkerUserId, true);
// 上场后把当前列同步到任务,供运动员端展示"当前正在比赛"。
task.CurrentEventProjectId = unit.EventProjectId;
task.CurrentGroupId = unit.GroupId;
task.CurrentGroupName = unit.GroupName;
task.CurrentSessionKey = unit.UnitKey;
task.CurrentSessionOrder = unit.Order;
task.CurrentSessionName = $"{unit.ProjectName}-{unit.GroupName}";
task.CurrentSessionVenue = unit.VenueName;
task.UpdatedUserId = checkerUserId;
task.UpdatedTime = DateTime.Now;
await _taskRepository.UpdateAsync(task);
return await BuildContextAsync(task, input.EventProjectId, input.GroupId);
}
/// <summary>按扫码内容定位秩序册列;支持号码布、报名人员ID和姓名。</summary>
private static CheckinOrderBookUnitDto? ResolveScannedUnit(
CheckinEventOptionsDto options, string code, out CheckinAthleteDto? athlete)
{
athlete = null;
foreach (var unit in options.Units.OrderBy(x => x.Order))
{
var matched = unit.Athletes.FirstOrDefault(x =>
x.BibNo == code || x.Id.ToString() == code || x.Name == code);
if (matched != null)
{
athlete = matched;
return unit;
}
}
// 兼容团队二维码:扫码内容直接是秩序册列标识。
return options.Units.FirstOrDefault(x => x.UnitKey == code);
}
/// <summary>检录顺序卡控:非一检必须先通过上一检次。</summary>
private async Task EnsurePreviousPassedAsync(CheckinTask task, string unitKey)
{
if (task.CheckinNo <= 1) return;
var previousTask = await GetEventTaskAsync(task.EventId, task.CheckinNo - 1);
var previousPassed = previousTask != null && await _recordRepository.IsAnyAsync(x =>
x.TaskId == previousTask.Id && x.SessionKey == unitKey && x.Checked && !x.DeleteMark);
if (!previousPassed)
throw Oops.Bah($"该秩序册列尚未通过{ToCheckinName(task.CheckinNo - 1)},不能进行{ToCheckinName(task.CheckinNo)}");
}
private async Task UpsertRecordAsync(
CheckinTask task, CheckinOrderBookUnitDto unit, long checkerUserId, bool isChecked)
{
var record = await _recordRepository.GetFirstAsync(x =>
x.TaskId == task.Id && x.SessionKey == unit.UnitKey && !x.DeleteMark);
if (record == null)
{
await _recordRepository.InsertAsync(new CheckinRecord
{
TaskId = task.Id,
Round = task.CheckinNo,
SessionKey = unit.UnitKey,
SessionOrder = unit.Order,
GroupId = unit.GroupId,
RegistrationPersonId = unit.RegistrationPersonIds.FirstOrDefault(),
CheckerUserId = checkerUserId,
Checked = isChecked,
CheckedTime = isChecked ? DateTime.Now : null,
CreatedUserId = checkerUserId,
CreatedTime = DateTime.Now
});
return;
}
record.Round = task.CheckinNo;
record.SessionOrder = unit.Order;
record.GroupId = unit.GroupId;
record.CheckerUserId = checkerUserId;
record.Checked = isChecked;
record.CheckedTime = isChecked ? DateTime.Now : null;
record.UpdatedUserId = checkerUserId;
record.UpdatedTime = DateTime.Now;
await _recordRepository.UpdateAsync(record);
}
/// <summary>
/// 对报名了所选项目组别的人员发送检录通知;单人失败不影响整体流程。
/// 个人项目:通知运动员本人(从 EventRegistrationPerson 取)。
/// 团队项目:通知教练和领队(从 TeamMember 取,因为报名表中不存储教练/领队的 RegistrationItemId)。
/// 短信内容:您报名的{赛事名称}正在检录,当前检录至{项目-组别第1场次},您所在第{x场次},请提前前往检录区做好准备。
/// </summary>
private async Task<(int Total, int Success, int Failed)> NotifyUnitsAsync(
List<CheckinOrderBookUnitDto> units,
ProjectTypeEnum projectType,
string eventName,
string projectGroupName)
{
var isIndividualProject = projectType is ProjectTypeEnum.PersonalSpeed or ProjectTypeEnum.PersonalFreestyle;
var isTeamProject = projectType is ProjectTypeEnum.TeamSpeed or ProjectTypeEnum.TeamFreestyle;
if (!isIndividualProject && !isTeamProject)
throw Oops.Bah("当前项目类型不支持检录短信通知");
// 收集通知接收人:{ Phone, SessionOrder }
List<(string? Phone, int SessionOrder)> recipients;
if (isIndividualProject)
{
recipients = await CollectIndividualRecipientsAsync(units);
}
else
{
recipients = await CollectTeamCoachLeaderRecipientsAsync(units);
}
// 按手机号去重(同一教练带多个队取最早场次)
var missingPhoneCount = recipients.Count(x => string.IsNullOrWhiteSpace(x.Phone));
var validRecipients = recipients
.Where(x => !string.IsNullOrWhiteSpace(x.Phone))
.GroupBy(x => x.Phone!.Trim())
.Select(g => g.OrderBy(x => x.SessionOrder).First())
.ToList();
// 当前检录进度:获取该项目组别在秩序册中的起始场次
var firstSessionIndex = units.Select(x => x.SessionIndex).FirstOrDefault(x => x > 0);
if (firstSessionIndex <= 0) firstSessionIndex = 1;
var currentSession = $"{projectGroupName}第{firstSessionIndex}场次";
var success = 0;
var failed = missingPhoneCount;
foreach (var recipient in validRecipients)
{
try
{
var sent = await _smsService.SendCheckinNotice(
recipient.Phone!.Trim(), eventName, currentSession, recipient.SessionOrder);
if (sent) success++;
else failed++;
}
catch (Exception ex)
{
failed++;
_logger.LogWarning(ex, "检录通知发送失败:{EventName} {Project} {Phone}",
eventName, projectGroupName, recipient.Phone);
}
}
return (validRecipients.Count + missingPhoneCount, success, failed);
}
/// <summary>个人项目:取 RegistrationItemId 下的运动员及其场次号。</summary>
private async Task<List<(string? Phone, int SessionOrder)>> CollectIndividualRecipientsAsync(
List<CheckinOrderBookUnitDto> units)
{
// 构建 personId/userId → 场次号 的映射(优先使用秩序册编排的 SessionIndex,缺失时回退到序号)
var userIdToOrder = new Dictionary<long, int>();
var personIdToOrder = new Dictionary<long, int>();
for (var i = 0; i < units.Count; i++)
{
var sessionOrder = units[i].SessionIndex > 0 ? units[i].SessionIndex : (i + 1);
foreach (var athlete in units[i].Athletes)
{
if (athlete.UserId > 0 && !userIdToOrder.ContainsKey(athlete.UserId))
userIdToOrder[athlete.UserId] = sessionOrder;
if (!personIdToOrder.ContainsKey(athlete.Id))
personIdToOrder[athlete.Id] = sessionOrder;
}
}
var registrationItemIds = units.Select(x => x.RegistrationItemId).Distinct().ToList();
if (registrationItemIds.Count == 0) return new();
var persons = await _personRepository.AsQueryable()
.Where(x => registrationItemIds.Contains(x.RegistrationItemId)
&& x.PersonType == PersonType.Athlete && !x.DeleteMark)
.ToListAsync();
persons = persons
.GroupBy(x => x.UserId > 0 ? $"user:{x.UserId}" : $"person:{x.Id}")
.Select(x => x.First())
.ToList();
var userIds = persons.Where(x => x.UserId > 0).Select(x => x.UserId).Distinct().ToList();
var userMap = await LoadUserMapAsync(userIds);
return persons.Select(p =>
{
userMap.TryGetValue(p.UserId, out var user);
var phone = !string.IsNullOrWhiteSpace(p.Phone) ? p.Phone : user?.Mobile;
// 取该人的场次号
var order = 0;
if (p.UserId > 0) userIdToOrder.TryGetValue(p.UserId, out order);
if (order == 0) personIdToOrder.TryGetValue(p.Id, out order);
if (order == 0) order = 1; // 兜底
return (phone, order);
}).ToList();
}
/// <summary>
/// 团队项目:运动员 → TeamMember → 同团队教练/领队 → AppUser。
/// 每位教练/领队的场次号取其所带队伍中最早的场次。
/// </summary>
private async Task<List<(string? Phone, int SessionOrder)>> CollectTeamCoachLeaderRecipientsAsync(
List<CheckinOrderBookUnitDto> units)
{
// 构建 athleteUserId → 最早场次号(优先使用 SessionIndex)
var athleteToOrder = new Dictionary<long, int>();
for (var i = 0; i < units.Count; i++)
{
var sessionOrder = units[i].SessionIndex > 0 ? units[i].SessionIndex : (i + 1);
foreach (var a in units[i].Athletes.Where(a => a.UserId > 0))
{
if (!athleteToOrder.ContainsKey(a.UserId))
athleteToOrder[a.UserId] = sessionOrder;
}
}
var athleteUserIds = athleteToOrder.Keys.ToList();
if (athleteUserIds.Count == 0) return new();
// 步骤1:运动员 → TeamMember(运动员身份)
var athleteMembers = await _teamMemberRepository.AsQueryable()
.Where(x => athleteUserIds.Contains(x.UserId) && !x.DeleteMark)
.ToListAsync();
var teamIds = athleteMembers.Select(x => x.TeamId).Distinct().ToList();
if (teamIds.Count == 0) return new();
// 构建 teamId → 最早场次号(取该队运动员中最早的场次)
var teamToOrder = new Dictionary<long, int>();
foreach (var am in athleteMembers)
{
if (athleteToOrder.TryGetValue(am.UserId, out var order))
{
if (!teamToOrder.ContainsKey(am.TeamId) || teamToOrder[am.TeamId] > order)
teamToOrder[am.TeamId] = order;
}
}
// 步骤2:同 TeamId 的教练/领队
var coachLeaders = await _teamMemberRepository.AsQueryable()
.Where(x => teamIds.Contains(x.TeamId)
&& (x.PersonType == PersonType.Coach || x.PersonType == PersonType.Leader)
&& !x.DeleteMark)
.ToListAsync();
if (coachLeaders.Count == 0)
{
// 降级:从 EventRegistrationPerson 查同 RegistrationId 下的教练/领队
return await FallbackCollectCoachLeaderAsync(units);
}
// 教练/领队 UserId → 最早场次号
var coachOrderMap = new Dictionary<long, int>();
foreach (var cl in coachLeaders)
{
if (teamToOrder.TryGetValue(cl.TeamId, out var order))
{
if (!coachOrderMap.ContainsKey(cl.UserId) || coachOrderMap[cl.UserId] > order)
coachOrderMap[cl.UserId] = order;
}
}
var userMap = await LoadUserMapAsync(coachOrderMap.Keys.ToList());
return coachOrderMap.Select(kv =>
{
userMap.TryGetValue(kv.Key, out var user);
return (user?.Mobile, kv.Value);
}).ToList();
}
/// <summary>降级路径:从 EventRegistrationPerson 查教练/领队(默认场次号 1)。</summary>
private async Task<List<(string? Phone, int SessionOrder)>> FallbackCollectCoachLeaderAsync(
List<CheckinOrderBookUnitDto> units)
{
var registrationItemIds = units.Select(x => x.RegistrationItemId).Distinct().ToList();
if (registrationItemIds.Count == 0) return new();
var registrationIds = await _personRepository.AsQueryable()
.Where(x => registrationItemIds.Contains(x.RegistrationItemId) && !x.DeleteMark)
.Select(x => x.RegistrationId)
.Distinct()
.ToListAsync();
if (registrationIds.Count == 0) return new();
var fallbackPersons = await _personRepository.AsQueryable()
.Where(x => registrationIds.Contains(x.RegistrationId)
&& (x.PersonType == PersonType.Coach || x.PersonType == PersonType.Leader)
&& !x.DeleteMark)
.ToListAsync();
var coachUserIds = fallbackPersons.Where(p => p.UserId > 0).Select(p => p.UserId).Distinct().ToList();
if (coachUserIds.Count == 0) return new();
var userMap = await LoadUserMapAsync(coachUserIds);
var defaultOrder = units.Select(x => x.SessionIndex).FirstOrDefault(x => x > 0);
if (defaultOrder <= 0) defaultOrder = 1;
return coachUserIds.Select(uid =>
{
userMap.TryGetValue(uid, out var user);
return (user?.Mobile, defaultOrder);
}).ToList();
}
/// <summary>批量加载 AppUser 并返回字典。</summary>
private async Task<Dictionary<long, YD_Event.Core.Entities.AppUser>> LoadUserMapAsync(List<long> userIds)
{
if (userIds.Count == 0) return new();
var users = await _userRepository.AsQueryable()
.Where(x => userIds.Contains(x.Id) && !x.DeleteMark)
.ToListAsync();
return users.ToDictionary(x => x.Id);
}
private async Task<CheckinContextDto> BuildContextAsync(CheckinTask task, long? eventProjectId, long? groupId)
{
var options = await _scheduleService.GetOptionsAsync(task.EventId);
var selectedProject = options.Projects.FirstOrDefault(x => x.EventProjectId == eventProjectId)
?? options.Projects.FirstOrDefault();
if (selectedProject == null) throw Oops.Bah("当前赛事秩序册没有可检录项目");
var selectedGroup = selectedProject.Groups.FirstOrDefault(x => x.Id == groupId)
?? selectedProject.Groups.FirstOrDefault();
if (selectedGroup == null) throw Oops.Bah("所选参赛项目没有可检录组别");
var allUnits = options.Units.OrderBy(x => x.Order).ToList();
var units = allUnits
.Where(x => x.EventProjectId == selectedProject.EventProjectId && x.GroupId == selectedGroup.Id)
.OrderBy(x => x.Order).ToList();
var allUnitKeys = allUnits.Select(x => x.UnitKey).ToHashSet();
var records = await _recordRepository.AsQueryable()
.Where(x => x.TaskId == task.Id && x.SessionKey != null && !x.DeleteMark)
.ToListAsync();
var checkedKeys = records.Where(x => x.Checked && allUnitKeys.Contains(x.SessionKey!))
.Select(x => x.SessionKey!).ToHashSet();
var activeTasks = await GetActiveEventTasksAsync(task.EventId);
// 三检通过视为已上场;仅配置两检时,二检通过即视为已上场。
var onStageKeys = await GetOnStageKeysAsync(activeTasks, allUnitKeys);
HashSet<string> previousPassedKeys;
if (task.CheckinNo == 1)
{
previousPassedKeys = allUnitKeys;
}
else
{
var previousTask = await GetEventTaskAsync(task.EventId, task.CheckinNo - 1);
var previousRecords = previousTask == null
? new List<CheckinRecord>()
: await _recordRepository.AsQueryable()
.Where(x => x.TaskId == previousTask.Id && x.SessionKey != null && x.Checked && !x.DeleteMark)
.ToListAsync();
previousPassedKeys = previousRecords.Where(x => allUnitKeys.Contains(x.SessionKey!))
.Select(x => x.SessionKey!).ToHashSet();
}
var allStatusUnits = allUnits.Select(unit =>
{
var previousPassed = previousPassedKeys.Contains(unit.UnitKey);
return new CheckinOrderBookUnitStatusDto
{
UnitKey = unit.UnitKey,
Order = unit.Order,
SessionIndex = unit.SessionIndex,
VenueIndex = unit.VenueIndex,
VenueName = unit.VenueName,
EventProjectId = unit.EventProjectId,
ProjectId = unit.ProjectId,
ProjectName = unit.ProjectName,
GroupId = unit.GroupId,
GroupName = unit.GroupName,
RegistrationItemId = unit.RegistrationItemId,
TeamName = unit.TeamName,
UnitName = unit.UnitName,
RegistrationPersonIds = unit.RegistrationPersonIds,
Athletes = unit.Athletes,
Checked = checkedKeys.Contains(unit.UnitKey),
PreviousPassed = previousPassed,
CanCheck = previousPassed,
LockReason = previousPassed ? string.Empty : $"待通过{ToCheckinName(task.CheckinNo - 1)}",
OnStage = onStageKeys.Contains(unit.UnitKey)
};
}).ToList();
var statusUnits = allStatusUnits
.Where(x => x.EventProjectId == selectedProject.EventProjectId && x.GroupId == selectedGroup.Id)
.ToList();
// 为项目、组别选择区计算当前检次概况,全部完成时由小程序显示绿色状态。
foreach (var project in options.Projects)
{
var projectUnits = allStatusUnits.Where(x => x.EventProjectId == project.EventProjectId).ToList();
ApplyProgress(projectUnits, out var projectTotal, out var projectChecked,
out var projectLocked, out var projectPercent, out var projectCompleted);
project.TotalUnits = projectTotal;
project.CheckedUnits = projectChecked;
project.LockedUnits = projectLocked;
project.ProgressPercent = projectPercent;
project.IsCompleted = projectCompleted;
foreach (var group in project.Groups)
{
var groupUnits = projectUnits.Where(x => x.GroupId == group.Id).ToList();
ApplyProgress(groupUnits, out var groupTotal, out var groupChecked,
out var groupLocked, out var groupPercent, out var groupCompleted);
group.TotalUnits = groupTotal;
group.CheckedUnits = groupChecked;
group.LockedUnits = groupLocked;
group.ProgressPercent = groupPercent;
group.IsCompleted = groupCompleted;
}
}
return new CheckinContextDto
{
TaskId = task.Id,
EventId = task.EventId,
EventName = options.EventName,
TotalCheckins = activeTasks.Count,
CurrentCheckinNo = task.CheckinNo,
CurrentCheckinName = ToCheckinName(task.CheckinNo),
TotalRounds = activeTasks.Count,
CurrentRound = task.CheckinNo,
RoundName = ToCheckinName(task.CheckinNo),
SelectedEventProjectId = selectedProject.EventProjectId,
SelectedGroupId = selectedGroup.Id,
CurrentGroupId = selectedGroup.Id,
CurrentGroupName = selectedGroup.GroupName,
Projects = options.Projects,
Units = statusUnits,
PendingUnits = statusUnits.Where(x => x.CanCheck && !x.Checked).ToList(),
// 已检录跟随当前选中的项目和组别,便于现场聚焦操作。
CheckedUnits = statusUnits.Where(x => x.Checked).ToList(),
LockedUnits = statusUnits.Where(x => !x.CanCheck).ToList(),
Rounds = activeTasks.Select(x => new CheckinRoundOptionDto
{ Round = x.CheckinNo, RoundName = ToCheckinName(x.CheckinNo) }).ToList()
};
}
private async Task<CheckinTask?> GetCurrentTaskAsync(long userId) =>
await _taskRepository.AsQueryable()
.InnerJoin(_eventRepository.AsQueryable(), (task, evt) => task.EventId == evt.Id)
.Where((task, evt) => task.CheckerUserId == userId && task.TaskStatus == 1 &&
task.Status == AvailabilityStatus.Enable && !task.DeleteMark &&
evt.EventStatus != EventStatus.Ended && !evt.DeleteMark)
.OrderByDescending((task, evt) => task.UpdatedTime)
.OrderByDescending((task, evt) => task.CreatedTime)
.Select((task, evt) => task)
.FirstAsync();
private async Task<CheckinTask?> GetEventTaskAsync(long eventId, int checkinNo) =>
await _taskRepository.GetFirstAsync(x => x.EventId == eventId && x.CheckinNo == checkinNo &&
x.TaskStatus == 1 && x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
private async Task<List<CheckinTask>> GetActiveEventTasksAsync(long eventId) =>
await _taskRepository.AsQueryable()
.Where(x => x.EventId == eventId && x.TaskStatus == 1 &&
x.Status == AvailabilityStatus.Enable && !x.DeleteMark)
.OrderBy(x => x.CheckinNo).ToListAsync();
/// <summary>三检通过即为已上场;仅配置两检时由二检标记上场。</summary>
private async Task<HashSet<string>> GetOnStageKeysAsync(
List<CheckinTask> activeTasks, HashSet<string> validUnitKeys)
{
if (activeTasks.Count == 0) return new HashSet<string>();
var onStageCheckinNo = ResolveOnStageCheckinNo(activeTasks);
var onStageTask = activeTasks.First(x => x.CheckinNo == onStageCheckinNo);
var records = await _recordRepository.AsQueryable()
.Where(x => x.TaskId == onStageTask.Id && x.SessionKey != null && x.Checked && !x.DeleteMark)
.ToListAsync();
return records.Where(x => validUnitKeys.Contains(x.SessionKey!))
.Select(x => x.SessionKey!).ToHashSet();
}
/// <summary>上场检次固定为三检;未配置三检但配置了二检时使用二检。</summary>
private static int ResolveOnStageCheckinNo(List<CheckinTask> activeTasks)
{
if (activeTasks.Any(x => x.CheckinNo == 3)) return 3;
if (activeTasks.Any(x => x.CheckinNo == 2)) return 2;
return activeTasks.Count == 0 ? 1 : activeTasks.Min(x => x.CheckinNo);
}
private async Task<long> EnsureCheckerAsync()
{
var userId = _authManager.UserId;
var user = await _userRepository.GetFirstAsync(x => x.Id == userId && !x.DeleteMark);
if (user == null) throw Oops.Bah("用户不存在");
if (user.MiniProgramRole != MiniProgramRole.Checker) throw Oops.Bah("当前账号不是检录员身份");
return userId;
}
private static string ToCheckinName(int checkinNo) => checkinNo switch
{
1 => "一检", 2 => "二检", 3 => "三检", _ => $"第{checkinNo}检"
};
private static void ApplyProgress(
List<CheckinOrderBookUnitStatusDto> units,
out int total,
out int checkedCount,
out int lockedCount,
out int percent,
out bool completed)
{
total = units.Count;
checkedCount = units.Count(x => x.Checked);
lockedCount = units.Count(x => !x.CanCheck);
percent = total == 0 ? 0 : (int)Math.Round(checkedCount * 100d / total);
completed = total > 0 && checkedCount == total;
}
}