个人项目按人检录,团体项目按团队检录
This commit is contained in:
parent
8a9905cdb2
commit
321be399ee
@ -95,7 +95,6 @@ public partial class CheckinController
|
||||
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>()
|
||||
@ -118,20 +117,22 @@ public partial class CheckinController
|
||||
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!)))
|
||||
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<int, DateTime?>();
|
||||
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<int, string>();
|
||||
checkerNameByKey[record.SessionKey!] = checkerNames;
|
||||
checkerNameByKey[unitKey] = checkerNames;
|
||||
}
|
||||
checkerNames[checkinNo] = checkerNameMap.GetValueOrDefault(record.CheckerUserId) ?? string.Empty;
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
/// <summary>检录顺序卡控:非一检必须先通过上一检次。</summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定检录对象的记录;个人项目同时兼容未包含人员 ID 的历史检录键。
|
||||
/// </summary>
|
||||
private async Task<CheckinRecord?> 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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断单个检录任务中,指定检录对象是否已经通过检录。
|
||||
/// </summary>
|
||||
private async Task<bool> HasCheckedUnitRecordAsync(long taskId, CheckinOrderBookUnitDto unit) =>
|
||||
await HasCheckedUnitRecordAsync(new List<long> { taskId }, unit);
|
||||
|
||||
/// <summary>
|
||||
/// 判断多个后续检录任务中,指定检录对象是否已经通过检录。
|
||||
/// </summary>
|
||||
private async Task<bool> HasCheckedUnitRecordAsync(
|
||||
List<long> 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)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取旧版检录键。旧版仅包含赛事项目 ID 和报名项目 ID。
|
||||
/// </summary>
|
||||
private static string GetLegacyUnitKey(CheckinOrderBookUnitDto unit) =>
|
||||
CheckinScheduleService.BuildUnitKey(unit.EventProjectId, unit.RegistrationItemId);
|
||||
|
||||
/// <summary>
|
||||
/// 将检录记录映射到当前检录键。旧版个人项目记录按保存的报名人员 ID 精确归属,
|
||||
/// 防止同一单位账号报名的其他人员被一并判定为已检录。
|
||||
/// </summary>
|
||||
private static string? ResolveRecordUnitKey(
|
||||
IReadOnlyCollection<CheckinOrderBookUnitDto> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对报名了所选项目的人员发送检录通知;单人失败不影响整体流程。
|
||||
/// 个人项目:通知运动员本人(从 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<string> 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
|
||||
|
||||
/// <summary>三检通过即为已上场;仅配置两检时由二检标记上场。</summary>
|
||||
private async Task<HashSet<string>> GetOnStageKeysAsync(
|
||||
List<CheckinTask> activeTasks, HashSet<string> validUnitKeys)
|
||||
List<CheckinTask> activeTasks, IReadOnlyCollection<CheckinOrderBookUnitDto> units)
|
||||
{
|
||||
if (activeTasks.Count == 0) return new HashSet<string>();
|
||||
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();
|
||||
}
|
||||
|
||||
/// <summary>上场检次固定为三检;未配置三检但配置了二检时使用二检。</summary>
|
||||
|
||||
@ -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}";
|
||||
/// <summary>
|
||||
/// 生成检录对象唯一键。团队项目不传人员 ID,个人项目必须传入人员 ID。
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
|
||||
@ -33,6 +33,11 @@ public class CheckinProjectOptionDto
|
||||
public class CheckinOrderBookUnitDto
|
||||
{
|
||||
public string UnitKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 是否为团队项目,用于区分整队检录和个人逐人检录。
|
||||
/// </summary>
|
||||
public bool IsTeamProject { get; set; }
|
||||
public int Order { get; set; }
|
||||
public int SessionIndex { get; set; }
|
||||
public int VenueIndex { get; set; }
|
||||
|
||||
@ -126,6 +126,11 @@ public class ProjectArrangementSetting
|
||||
/// </summary>
|
||||
public class ArrangementPreviewOutput
|
||||
{
|
||||
/// <summary>
|
||||
/// 是否为团队项目。团队项目按报名项目整队检录,个人项目按报名人员逐人检录。
|
||||
/// </summary>
|
||||
public bool IsTeamProject { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 赛事项目ID,关联 EventProject.Id。
|
||||
/// </summary>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user