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; /// 小程序检录员按秩序册列进行顺序检录。 [ApiDescriptionSettings("微信小程序接口")] [Authorize] public partial class CheckinController : IDynamicApiController { private readonly ISqlSugarRepository _userRepository; private readonly ISqlSugarRepository _eventRepository; private readonly ISqlSugarRepository _taskRepository; private readonly ISqlSugarRepository _recordRepository; private readonly ISqlSugarRepository _personRepository; private readonly ISqlSugarRepository _registrationRepository; private readonly ISqlSugarRepository _projectRepository; private readonly ISqlSugarRepository _teamMemberRepository; private readonly CheckinScheduleService _scheduleService; private readonly AliyunSmsService _smsService; private readonly ILogger _logger; private readonly AuthManager _authManager; public CheckinController( ISqlSugarRepository userRepository, ISqlSugarRepository eventRepository, ISqlSugarRepository taskRepository, ISqlSugarRepository recordRepository, ISqlSugarRepository personRepository, ISqlSugarRepository registrationRepository, ISqlSugarRepository projectRepository, ISqlSugarRepository teamMemberRepository, CheckinScheduleService scheduleService, AliyunSmsService smsService, ILogger 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 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 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 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 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 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); } /// 按扫码内容定位秩序册列;支持号码布、报名人员ID和姓名。 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); } /// 检录顺序卡控:非一检必须先通过上一检次。 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); } /// /// 对报名了所选项目组别的人员发送检录通知;单人失败不影响整体流程。 /// 个人项目:通知运动员本人(从 EventRegistrationPerson 取)。 /// 团队项目:通知教练和领队(从 TeamMember 取,因为报名表中不存储教练/领队的 RegistrationItemId)。 /// 短信内容:您报名的{赛事名称}正在检录,当前检录至{项目-组别第1场次},您所在第{x场次},请提前前往检录区做好准备。 /// private async Task<(int Total, int Success, int Failed)> NotifyUnitsAsync( List 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); } /// 个人项目:取 RegistrationItemId 下的运动员及其场次号。 private async Task> CollectIndividualRecipientsAsync( List units) { // 构建 personId/userId → 场次号 的映射(优先使用秩序册编排的 SessionIndex,缺失时回退到序号) var userIdToOrder = new Dictionary(); var personIdToOrder = new Dictionary(); 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(); } /// /// 团队项目:运动员 → TeamMember → 同团队教练/领队 → AppUser。 /// 每位教练/领队的场次号取其所带队伍中最早的场次。 /// private async Task> CollectTeamCoachLeaderRecipientsAsync( List units) { // 构建 athleteUserId → 最早场次号(优先使用 SessionIndex) var athleteToOrder = new Dictionary(); 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(); 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(); 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(); } /// 降级路径:从 EventRegistrationPerson 查教练/领队(默认场次号 1)。 private async Task> FallbackCollectCoachLeaderAsync( List 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(); } /// 批量加载 AppUser 并返回字典。 private async Task> LoadUserMapAsync(List 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 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 previousPassedKeys; if (task.CheckinNo == 1) { previousPassedKeys = allUnitKeys; } else { var previousTask = await GetEventTaskAsync(task.EventId, task.CheckinNo - 1); var previousRecords = previousTask == null ? new List() : 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 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 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> 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(); /// 三检通过即为已上场;仅配置两检时由二检标记上场。 private async Task> GetOnStageKeysAsync( List activeTasks, HashSet validUnitKeys) { if (activeTasks.Count == 0) return new HashSet(); 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(); } /// 上场检次固定为三检;未配置三检但配置了二检时使用二检。 private static int ResolveOnStageCheckinNo(List 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 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 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; } }