2026-08-24 00:07:15 +08:00
|
|
|
|
using Microsoft.Extensions.Logging;
|
2026-08-07 14:59:15 +08:00
|
|
|
|
using YD_Event.Application.Auth;
|
|
|
|
|
|
using YD_Event.Application.Client.Dtos;
|
2026-08-24 00:07:15 +08:00
|
|
|
|
using YD_Event.Application.Sms;
|
2026-08-07 14:59:15 +08:00
|
|
|
|
|
|
|
|
|
|
namespace YD_Event.Application.Client;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>小程序检录员按秩序册列进行顺序检录。</summary>
|
|
|
|
|
|
[ApiDescriptionSettings("微信小程序接口")]
|
|
|
|
|
|
[Authorize]
|
2026-08-24 00:07:15 +08:00
|
|
|
|
public partial class CheckinController : IDynamicApiController
|
2026-08-07 14:59:15 +08:00
|
|
|
|
{
|
|
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _userRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<CheckinTask> _taskRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<CheckinRecord> _recordRepository;
|
2026-08-24 00:07:15 +08:00
|
|
|
|
private readonly ISqlSugarRepository<EventRegistrationPerson> _personRepository;
|
|
|
|
|
|
private readonly ISqlSugarRepository<EventRegistration> _registrationRepository;
|
2026-08-07 14:59:15 +08:00
|
|
|
|
private readonly CheckinScheduleService _scheduleService;
|
2026-08-24 00:07:15 +08:00
|
|
|
|
private readonly AliyunSmsService _smsService;
|
|
|
|
|
|
private readonly ILogger<CheckinController> _logger;
|
2026-08-07 14:59:15 +08:00
|
|
|
|
private readonly AuthManager _authManager;
|
|
|
|
|
|
|
|
|
|
|
|
public CheckinController(
|
|
|
|
|
|
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
|
|
|
|
|
|
ISqlSugarRepository<CheckinTask> taskRepository,
|
|
|
|
|
|
ISqlSugarRepository<CheckinRecord> recordRepository,
|
2026-08-24 00:07:15 +08:00
|
|
|
|
ISqlSugarRepository<EventRegistrationPerson> personRepository,
|
|
|
|
|
|
ISqlSugarRepository<EventRegistration> registrationRepository,
|
2026-08-07 14:59:15 +08:00
|
|
|
|
CheckinScheduleService scheduleService,
|
2026-08-24 00:07:15 +08:00
|
|
|
|
AliyunSmsService smsService,
|
|
|
|
|
|
ILogger<CheckinController> logger,
|
2026-08-07 14:59:15 +08:00
|
|
|
|
AuthManager authManager)
|
|
|
|
|
|
{
|
|
|
|
|
|
_userRepository = userRepository;
|
|
|
|
|
|
_taskRepository = taskRepository;
|
|
|
|
|
|
_recordRepository = recordRepository;
|
2026-08-24 00:07:15 +08:00
|
|
|
|
_personRepository = personRepository;
|
|
|
|
|
|
_registrationRepository = registrationRepository;
|
2026-08-07 14:59:15 +08:00
|
|
|
|
_scheduleService = scheduleService;
|
2026-08-24 00:07:15 +08:00
|
|
|
|
_smsService = smsService;
|
|
|
|
|
|
_logger = logger;
|
2026-08-07 14:59:15 +08:00
|
|
|
|
_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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-24 00:07:15 +08:00
|
|
|
|
[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 noticeName = $"{project.ProjectName}-{group.GroupName}";
|
|
|
|
|
|
var (total, success, failed) = await NotifyUnitsAsync(units, 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 = 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>对报名了所选项目组别的人员发送检录通知;单人失败不影响整体流程。</summary>
|
|
|
|
|
|
private async Task<(int Total, int Success, int Failed)> NotifyUnitsAsync(
|
|
|
|
|
|
List<CheckinOrderBookUnitDto> units, string eventName, string projectGroupName)
|
|
|
|
|
|
{
|
|
|
|
|
|
var personIds = units.SelectMany(x => x.RegistrationPersonIds).Distinct().ToList();
|
|
|
|
|
|
if (personIds.Count == 0) return (0, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
var persons = await _personRepository.AsQueryable()
|
|
|
|
|
|
.Where(x => personIds.Contains(x.Id) && !x.DeleteMark)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
|
|
|
|
|
|
// 报名人员未填手机号时回退到报名联系人手机号。
|
|
|
|
|
|
var registrationIds = persons.Select(x => x.RegistrationId).Distinct().ToList();
|
|
|
|
|
|
var registrations = registrationIds.Count == 0
|
|
|
|
|
|
? new List<EventRegistration>()
|
|
|
|
|
|
: await _registrationRepository.AsQueryable()
|
|
|
|
|
|
.Where(x => registrationIds.Contains(x.Id) && !x.DeleteMark).ToListAsync();
|
|
|
|
|
|
var contactMap = registrations.ToDictionary(x => x.Id, x => x.ContactPhone);
|
|
|
|
|
|
|
|
|
|
|
|
var success = 0;
|
|
|
|
|
|
var failed = 0;
|
|
|
|
|
|
foreach (var person in persons)
|
|
|
|
|
|
{
|
|
|
|
|
|
var phone = person.Phone;
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(phone))
|
|
|
|
|
|
contactMap.TryGetValue(person.RegistrationId, out phone);
|
|
|
|
|
|
if (string.IsNullOrWhiteSpace(phone))
|
|
|
|
|
|
{
|
|
|
|
|
|
failed++;
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try
|
|
|
|
|
|
{
|
|
|
|
|
|
var sent = await _smsService.SendCheckinNotice(phone, person.Name, projectGroupName);
|
|
|
|
|
|
if (sent) success++;
|
|
|
|
|
|
else failed++;
|
|
|
|
|
|
}
|
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
|
{
|
|
|
|
|
|
failed++;
|
|
|
|
|
|
_logger.LogWarning(ex, "检录通知发送失败:{EventName} {Project} {Phone}",
|
|
|
|
|
|
eventName, projectGroupName, phone);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return (persons.Count, success, failed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 14:59:15 +08:00
|
|
|
|
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();
|
|
|
|
|
|
|
2026-08-24 00:07:15 +08:00
|
|
|
|
var activeTasks = await GetActiveEventTasksAsync(task.EventId);
|
|
|
|
|
|
// 末检次(默认三检)通过即视为已上场。
|
|
|
|
|
|
var onStageKeys = await GetOnStageKeysAsync(activeTasks, allUnitKeys);
|
|
|
|
|
|
|
2026-08-07 14:59:15 +08:00
|
|
|
|
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,
|
2026-08-24 00:07:15 +08:00
|
|
|
|
LockReason = previousPassed ? string.Empty : $"待通过{ToCheckinName(task.CheckinNo - 1)}",
|
|
|
|
|
|
OnStage = onStageKeys.Contains(unit.UnitKey)
|
2026-08-07 14:59:15 +08:00
|
|
|
|
};
|
|
|
|
|
|
}).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()
|
|
|
|
|
|
.Where(x => x.CheckerUserId == userId && x.TaskStatus == 1 &&
|
|
|
|
|
|
x.Status == AvailabilityStatus.Enable && !x.DeleteMark)
|
|
|
|
|
|
.OrderByDescending(x => x.UpdatedTime).OrderByDescending(x => x.CreatedTime).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();
|
|
|
|
|
|
|
2026-08-24 00:07:15 +08:00
|
|
|
|
/// <summary>末检次通过的秩序册列即为已上场,供检录端与运动员端统一判定。</summary>
|
|
|
|
|
|
private async Task<HashSet<string>> GetOnStageKeysAsync(
|
|
|
|
|
|
List<CheckinTask> activeTasks, HashSet<string> validUnitKeys)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (activeTasks.Count == 0) return new HashSet<string>();
|
|
|
|
|
|
var lastTask = activeTasks.OrderByDescending(x => x.CheckinNo).First();
|
|
|
|
|
|
var records = await _recordRepository.AsQueryable()
|
|
|
|
|
|
.Where(x => x.TaskId == lastTask.Id && x.SessionKey != null && x.Checked && !x.DeleteMark)
|
|
|
|
|
|
.ToListAsync();
|
|
|
|
|
|
return records.Where(x => validUnitKeys.Contains(x.SessionKey!))
|
|
|
|
|
|
.Select(x => x.SessionKey!).ToHashSet();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-07 14:59:15 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|