From 321be399ee0ce4ef34cc78f1373581daac650e14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A9=AC=E6=A2=93=E5=BD=AD?= Date: Mon, 21 Sep 2026 14:55:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=AA=E4=BA=BA=E9=A1=B9=E7=9B=AE=E6=8C=89?= =?UTF-8?q?=E4=BA=BA=E6=A3=80=E5=BD=95=EF=BC=8C=E5=9B=A2=E4=BD=93=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=8C=89=E5=9B=A2=E9=98=9F=E6=A3=80=E5=BD=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Client/CheckinController.Progress.cs | 13 +- .../Client/CheckinController.cs | 117 ++++++++++++++---- .../Client/CheckinScheduleService.cs | 19 ++- .../Client/Dtos/CheckinAdminDtos.cs | 5 + .../Event/Dtos/ArrangementDtos.cs | 5 + .../Event/EventArrangementService.cs | 2 + 6 files changed, 131 insertions(+), 30 deletions(-) diff --git a/YD_Event.Application/Client/CheckinController.Progress.cs b/YD_Event.Application/Client/CheckinController.Progress.cs index 095b4b7..e9bd2ca 100644 --- a/YD_Event.Application/Client/CheckinController.Progress.cs +++ b/YD_Event.Application/Client/CheckinController.Progress.cs @@ -95,7 +95,6 @@ public partial class CheckinController private async Task BuildProgressContextAsync( CheckinEventOptionsDto options, List 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() @@ -118,20 +117,22 @@ public partial class CheckinController var taskCheckinNoMap = activeTasks.ToDictionary(x => x.Id, x => x.CheckinNo); var passedByKey = new Dictionary>(); var checkerNameByKey = new Dictionary>(); - foreach (var record in records.Where(x => validUnitKeys.Contains(x.SessionKey!))) + 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(record.SessionKey!, out var rounds)) + if (!passedByKey.TryGetValue(unitKey, out var rounds)) { rounds = new Dictionary(); - passedByKey[record.SessionKey!] = rounds; + passedByKey[unitKey] = rounds; } rounds[checkinNo] = record.CheckedTime; - if (!checkerNameByKey.TryGetValue(record.SessionKey!, out var checkerNames)) + if (!checkerNameByKey.TryGetValue(unitKey, out var checkerNames)) { checkerNames = new Dictionary(); - checkerNameByKey[record.SessionKey!] = checkerNames; + checkerNameByKey[unitKey] = checkerNames; } checkerNames[checkinNo] = checkerNameMap.GetValueOrDefault(record.CheckerUserId) ?? string.Empty; } diff --git a/YD_Event.Application/Client/CheckinController.cs b/YD_Event.Application/Client/CheckinController.cs index a5d60f1..1653b10 100644 --- a/YD_Event.Application/Client/CheckinController.cs +++ b/YD_Event.Application/Client/CheckinController.cs @@ -79,8 +79,8 @@ public partial class CheckinController : IDynamicApiController 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); + var previousPassed = previousTask != null && + await HasCheckedUnitRecordAsync(previousTask.Id, unit); if (!previousPassed) throw Oops.Bah($"该秩序册列尚未通过{ToCheckinName(task.CheckinNo - 1)},不能进行{ToCheckinName(task.CheckinNo)}"); } @@ -92,13 +92,11 @@ public partial class CheckinController : IDynamicApiController 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)) + if (laterTaskIds.Count > 0 && await HasCheckedUnitRecordAsync(laterTaskIds, unit)) throw Oops.Bah("该秩序册列已通过后续检录,不能取消当前检录"); } - var record = await _recordRepository.GetFirstAsync(x => - x.TaskId == task.Id && x.SessionKey == unit.UnitKey && !x.DeleteMark); + var record = await GetUnitRecordAsync(task.Id, unit); if (record == null) { record = new CheckinRecord @@ -119,6 +117,7 @@ public partial class CheckinController : IDynamicApiController } else { + record.SessionKey = unit.UnitKey; record.Round = task.CheckinNo; record.SessionOrder = unit.Order; record.GroupId = unit.GroupId; @@ -220,7 +219,7 @@ public partial class CheckinController : IDynamicApiController if (unit == null) return new ScanCheckinOutput { Success = false, Message = "扫码内容与当前赛事秩序册不匹配,请核对号码布" }; - await EnsurePreviousPassedAsync(task, unit.UnitKey); + await EnsurePreviousPassedAsync(task, unit); await UpsertRecordAsync(task, unit, checkerUserId, true); var context = await BuildContextAsync(task, input.EventProjectId ?? unit.EventProjectId); @@ -255,7 +254,7 @@ public partial class CheckinController : IDynamicApiController var options = await _scheduleService.GetOptionsAsync(task.EventId); var unit = CheckinScheduleService.ResolveUnit(options, input.UnitKey); - await EnsurePreviousPassedAsync(task, unit.UnitKey); + await EnsurePreviousPassedAsync(task, unit); await UpsertRecordAsync(task, unit, checkerUserId, true); // 上场后把当前列同步到任务,供运动员端展示"当前正在比赛"。 @@ -294,12 +293,12 @@ public partial class CheckinController : IDynamicApiController } /// 检录顺序卡控:非一检必须先通过上一检次。 - private async Task EnsurePreviousPassedAsync(CheckinTask task, string unitKey) + private async Task EnsurePreviousPassedAsync(CheckinTask task, CheckinOrderBookUnitDto unit) { 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); + var previousPassed = previousTask != null && + await HasCheckedUnitRecordAsync(previousTask.Id, unit); if (!previousPassed) throw Oops.Bah($"该秩序册列尚未通过{ToCheckinName(task.CheckinNo - 1)},不能进行{ToCheckinName(task.CheckinNo)}"); } @@ -307,8 +306,7 @@ public partial class CheckinController : IDynamicApiController 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); + var record = await GetUnitRecordAsync(task.Id, unit); if (record == null) { await _recordRepository.InsertAsync(new CheckinRecord @@ -328,6 +326,7 @@ public partial class CheckinController : IDynamicApiController return; } + record.SessionKey = unit.UnitKey; record.Round = task.CheckinNo; record.SessionOrder = unit.Order; record.GroupId = unit.GroupId; @@ -339,6 +338,74 @@ public partial class CheckinController : IDynamicApiController await _recordRepository.UpdateAsync(record); } + /// + /// 获取指定检录对象的记录;个人项目同时兼容未包含人员 ID 的历史检录键。 + /// + private async Task GetUnitRecordAsync(long taskId, CheckinOrderBookUnitDto unit) + { + if (unit.IsTeamProject) + { + return await _recordRepository.GetFirstAsync(x => + x.TaskId == taskId && x.SessionKey == unit.UnitKey && !x.DeleteMark); + } + + var legacyUnitKey = GetLegacyUnitKey(unit); + var registrationPersonId = unit.RegistrationPersonIds.FirstOrDefault(); + return await _recordRepository.GetFirstAsync(x => + x.TaskId == taskId && !x.DeleteMark && + (x.SessionKey == unit.UnitKey || + (x.SessionKey == legacyUnitKey && x.RegistrationPersonId == registrationPersonId))); + } + + /// + /// 判断单个检录任务中,指定检录对象是否已经通过检录。 + /// + private async Task HasCheckedUnitRecordAsync(long taskId, CheckinOrderBookUnitDto unit) => + await HasCheckedUnitRecordAsync(new List { taskId }, unit); + + /// + /// 判断多个后续检录任务中,指定检录对象是否已经通过检录。 + /// + private async Task HasCheckedUnitRecordAsync( + List taskIds, CheckinOrderBookUnitDto unit) + { + if (unit.IsTeamProject) + { + return await _recordRepository.IsAnyAsync(x => + taskIds.Contains(x.TaskId) && x.SessionKey == unit.UnitKey && + x.Checked && !x.DeleteMark); + } + + var legacyUnitKey = GetLegacyUnitKey(unit); + var registrationPersonId = unit.RegistrationPersonIds.FirstOrDefault(); + return await _recordRepository.IsAnyAsync(x => + taskIds.Contains(x.TaskId) && x.Checked && !x.DeleteMark && + (x.SessionKey == unit.UnitKey || + (x.SessionKey == legacyUnitKey && x.RegistrationPersonId == registrationPersonId))); + } + + /// + /// 获取旧版检录键。旧版仅包含赛事项目 ID 和报名项目 ID。 + /// + private static string GetLegacyUnitKey(CheckinOrderBookUnitDto unit) => + CheckinScheduleService.BuildUnitKey(unit.EventProjectId, unit.RegistrationItemId); + + /// + /// 将检录记录映射到当前检录键。旧版个人项目记录按保存的报名人员 ID 精确归属, + /// 防止同一单位账号报名的其他人员被一并判定为已检录。 + /// + private static string? ResolveRecordUnitKey( + IReadOnlyCollection units, CheckinRecord record) + { + var exactUnit = units.FirstOrDefault(x => x.UnitKey == record.SessionKey); + if (exactUnit != null) return exactUnit.UnitKey; + + return units.FirstOrDefault(x => + !x.IsTeamProject && + GetLegacyUnitKey(x) == record.SessionKey && + x.RegistrationPersonIds.Contains(record.RegistrationPersonId))?.UnitKey; + } + /// /// 对报名了所选项目的人员发送检录通知;单人失败不影响整体流程。 /// 个人项目:通知运动员本人(从 EventRegistrationPerson 取)。 @@ -602,12 +669,15 @@ public partial class CheckinController : IDynamicApiController 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 checkedKeys = records.Where(x => x.Checked) + .Select(x => ResolveRecordUnitKey(allUnits, x)) + .Where(x => x != null) + .Select(x => x!) + .ToHashSet(); var activeTasks = await GetActiveEventTasksAsync(task.EventId); // 三检通过视为已上场;仅配置两检时,二检通过即视为已上场。 - var onStageKeys = await GetOnStageKeysAsync(activeTasks, allUnitKeys); + var onStageKeys = await GetOnStageKeysAsync(activeTasks, allUnits); HashSet previousPassedKeys; if (task.CheckinNo == 1) @@ -622,8 +692,11 @@ public partial class CheckinController : IDynamicApiController : 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(); + previousPassedKeys = previousRecords + .Select(x => ResolveRecordUnitKey(allUnits, x)) + .Where(x => x != null) + .Select(x => x!) + .ToHashSet(); } var allStatusUnits = allUnits.Select(unit => @@ -737,7 +810,7 @@ public partial class CheckinController : IDynamicApiController /// 三检通过即为已上场;仅配置两检时由二检标记上场。 private async Task> GetOnStageKeysAsync( - List activeTasks, HashSet validUnitKeys) + List activeTasks, IReadOnlyCollection units) { if (activeTasks.Count == 0) return new HashSet(); var onStageCheckinNo = ResolveOnStageCheckinNo(activeTasks); @@ -745,8 +818,10 @@ public partial class CheckinController : IDynamicApiController 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(); + return records.Select(x => ResolveRecordUnitKey(units, x)) + .Where(x => x != null) + .Select(x => x!) + .ToHashSet(); } /// 上场检次固定为三检;未配置三检但配置了二检时使用二检。 diff --git a/YD_Event.Application/Client/CheckinScheduleService.cs b/YD_Event.Application/Client/CheckinScheduleService.cs index 4ef5271..8900557 100644 --- a/YD_Event.Application/Client/CheckinScheduleService.cs +++ b/YD_Event.Application/Client/CheckinScheduleService.cs @@ -38,9 +38,13 @@ public class CheckinScheduleService : ITransient .ToListAsync(); var personMap = persons.ToDictionary(x => x.Id); + // 团队项目沿用报名项目作为检录单位;个人项目追加报名人员 ID, + // 避免单位账号在同一报名项目下填报多人时共用同一个检录键。 var units = rows.Select((row, index) => new CheckinOrderBookUnitDto { - UnitKey = BuildUnitKey(row.EventProjectId, row.RegistrationItemId), + UnitKey = BuildUnitKey(row.EventProjectId, row.RegistrationItemId, + row.IsTeamProject ? null : row.RegistrationPersonIds.SingleOrDefault()), + IsTeamProject = row.IsTeamProject, Order = index + 1, SessionIndex = row.SessionIndex, VenueIndex = row.VenueIndex, @@ -108,8 +112,17 @@ public class CheckinScheduleService : ITransient return unit ?? throw Oops.Bah("所选检录对象不属于当前赛事秩序册"); } - public static string BuildUnitKey(long eventProjectId, long registrationItemId) => - $"EP{eventProjectId}-RI{registrationItemId}"; + /// + /// 生成检录对象唯一键。团队项目不传人员 ID,个人项目必须传入人员 ID。 + /// + public static string BuildUnitKey( + long eventProjectId, long registrationItemId, long? registrationPersonId = null) + { + var key = $"EP{eventProjectId}-RI{registrationItemId}"; + return registrationPersonId.HasValue && registrationPersonId.Value > 0 + ? $"{key}-RP{registrationPersonId.Value}" + : key; + } private static string FormatBibNo(long personId) { diff --git a/YD_Event.Application/Client/Dtos/CheckinAdminDtos.cs b/YD_Event.Application/Client/Dtos/CheckinAdminDtos.cs index 8f86ae0..818e735 100644 --- a/YD_Event.Application/Client/Dtos/CheckinAdminDtos.cs +++ b/YD_Event.Application/Client/Dtos/CheckinAdminDtos.cs @@ -33,6 +33,11 @@ public class CheckinProjectOptionDto public class CheckinOrderBookUnitDto { public string UnitKey { get; set; } = string.Empty; + + /// + /// 是否为团队项目,用于区分整队检录和个人逐人检录。 + /// + public bool IsTeamProject { get; set; } public int Order { get; set; } public int SessionIndex { get; set; } public int VenueIndex { get; set; } diff --git a/YD_Event.Application/Event/Dtos/ArrangementDtos.cs b/YD_Event.Application/Event/Dtos/ArrangementDtos.cs index a907baa..1cda8c8 100644 --- a/YD_Event.Application/Event/Dtos/ArrangementDtos.cs +++ b/YD_Event.Application/Event/Dtos/ArrangementDtos.cs @@ -126,6 +126,11 @@ public class ProjectArrangementSetting /// public class ArrangementPreviewOutput { + /// + /// 是否为团队项目。团队项目按报名项目整队检录,个人项目按报名人员逐人检录。 + /// + public bool IsTeamProject { get; set; } + /// /// 赛事项目ID,关联 EventProject.Id。 /// diff --git a/YD_Event.Application/Event/EventArrangementService.cs b/YD_Event.Application/Event/EventArrangementService.cs index fc158e3..b2f5e0c 100644 --- a/YD_Event.Application/Event/EventArrangementService.cs +++ b/YD_Event.Application/Event/EventArrangementService.cs @@ -476,6 +476,7 @@ namespace YD_Event.Application.Event; projectEntries.Add(new ArrangementPreviewOutput { + IsTeamProject = true, EventProjectId = eventProj.Id, ProjectId = proj.Id, EventProjectGroupId = teamGroup.Select(x => x.EventProjectGroupId).Distinct().Count() == 1 @@ -503,6 +504,7 @@ namespace YD_Event.Application.Event; projectEntries.Add(new ArrangementPreviewOutput { + IsTeamProject = false, EventProjectId = eventProj.Id, ProjectId = proj.Id, EventProjectGroupId = person.EventProjectGroupId,