YD_Event.Admin/YD_Event.Application/Client/CheckinAdminController.cs

442 lines
19 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 System.Text.RegularExpressions;
using YD_Event.Application.Auth;
using YD_Event.Application.Client.Dtos;
using YD_Event.Application.Event;
namespace YD_Event.Application.Client;
/// <summary>后台检录员及赛事顺序检录管理。</summary>
[ApiDescriptionSettings("打分系统")]
[Route("api/checkinadmin")]
[Authorize]
public class CheckinAdminController : IDynamicApiController
{
private const int MaxCheckerCount = 3;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _userRepository;
private readonly ISqlSugarRepository<YD_Event.Core.Entities.SysUser> _sysUserRepository;
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
private readonly ISqlSugarRepository<CheckinTask> _taskRepository;
private readonly CheckinScheduleService _scheduleService;
private readonly AuthManager _authManager;
private readonly IIdGenerator _idGenerator;
public CheckinAdminController(
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
ISqlSugarRepository<YD_Event.Core.Entities.SysUser> sysUserRepository,
ISqlSugarRepository<EventInfo> eventRepository,
ISqlSugarRepository<CheckinTask> taskRepository,
CheckinScheduleService scheduleService,
AuthManager authManager,
IIdGenerator idGenerator)
{
_userRepository = userRepository;
_sysUserRepository = sysUserRepository;
_eventRepository = eventRepository;
_taskRepository = taskRepository;
_scheduleService = scheduleService;
_authManager = authManager;
_idGenerator = idGenerator;
}
[HttpGet("event-options")]
[DisplayName("获取赛事秩序册及检次分配选项")]
public async Task<CheckinEventOptionsDto> GetEventOptions([FromQuery] long eventId)
{
EnsureManager();
var options = await _scheduleService.GetOptionsAsync(eventId);
var tasks = await GetActiveEventTasksAsync(eventId);
options.MaxCheckers = MaxCheckerCount;
options.AssignedCheckinNumbers = tasks.Select(x => x.CheckinNo).OrderBy(x => x).ToList();
options.NextCheckinNo = Math.Min(tasks.Count + 1, MaxCheckerCount);
return options;
}
[HttpGet("checker/mobile-availability")]
[DisplayName("检查检录员手机号是否已存在其他身份")]
public async Task<CheckerAccountAvailabilityDto> CheckMobileAvailability(
[FromQuery] string mobile,
[FromQuery] long? eventId = null,
[FromQuery] long? excludeCheckerUserId = null)
{
EnsureManager();
mobile = mobile?.Trim() ?? string.Empty;
if (!IsValidMobile(mobile))
{
return new CheckerAccountAvailabilityDto { Available = false, Message = "手机号格式不正确" };
}
return await GetCheckerAvailabilityAsync(mobile, eventId, excludeCheckerUserId);
}
[HttpGet("checkers")]
[DisplayName("获取检录员赛事绑定列表")]
public async Task<List<CheckinCheckerDto>> GetCheckers([FromQuery] long? eventId)
{
EnsureManager();
var tasks = await _taskRepository.AsQueryable()
.Where(x => !x.DeleteMark && x.Status == AvailabilityStatus.Enable)
.WhereIF(eventId.HasValue, x => x.EventId == eventId.Value)
.OrderBy(x => x.CheckinNo)
.ToListAsync();
var checkerIds = tasks.Where(x => x.CheckerUserId.HasValue).Select(x => x.CheckerUserId!.Value).Distinct().ToList();
var eventIds = tasks.Select(x => x.EventId).Distinct().ToList();
var users = checkerIds.Count == 0
? new List<YD_Event.Core.Entities.AppUser>()
: await _userRepository.AsQueryable().Where(x => checkerIds.Contains(x.Id) && !x.DeleteMark).ToListAsync();
var events = eventIds.Count == 0
? new List<EventInfo>()
: await _eventRepository.AsQueryable().Where(x => eventIds.Contains(x.Id) && !x.DeleteMark).ToListAsync();
var userMap = users.ToDictionary(x => x.Id);
var eventMap = events.ToDictionary(x => x.Id);
return tasks.Where(x => x.CheckerUserId.HasValue).Select(task =>
{
userMap.TryGetValue(task.CheckerUserId!.Value, out var checker);
eventMap.TryGetValue(task.EventId, out var evt);
return ToDto(checker, task, evt?.EventName ?? string.Empty);
}).ToList();
}
[HttpPost("checker/save")]
[UnitOfWork]
[DisplayName("后台添加检录员并分配检次")]
public async Task<CheckinCheckerDto> SaveChecker([FromBody] SaveCheckerInput input)
{
EnsureManager();
var managerUserId = _authManager.UserId;
input.Name = input.Name?.Trim() ?? string.Empty;
input.Mobile = input.Mobile?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(input.Name)) throw Oops.Bah("姓名不能为空");
if (!IsValidMobile(input.Mobile)) throw Oops.Bah("手机号格式不正确");
if (input.CheckinNo is < 1 or > MaxCheckerCount) throw Oops.Bah("检次只能选择一检、二检或三检");
// 同时验证赛事存在且秩序册包含可检录列。
var options = await _scheduleService.GetOptionsAsync(input.EventId);
if (options.Units.Count == 0)
throw Oops.Bah("当前赛事秩序册没有可检录列");
CheckinTask? task = null;
YD_Event.Core.Entities.AppUser? checker = null;
if (input.CheckerUserId.HasValue)
{
checker = await _userRepository.GetFirstAsync(x => x.Id == input.CheckerUserId.Value && !x.DeleteMark);
if (checker == null || checker.MiniProgramRole != MiniProgramRole.Checker) throw Oops.Bah("检录员不存在");
task = await _taskRepository.GetFirstAsync(x =>
x.CheckerUserId == checker.Id && x.EventId == input.EventId && !x.DeleteMark);
if (task == null) throw Oops.Bah("检录员赛事绑定不存在");
if (task.CheckinNo != input.CheckinNo) throw Oops.Bah("已分配的检次不能直接修改;请从最高检次开始解除后重新分配");
}
else
{
var availability = await GetCheckerAvailabilityAsync(input.Mobile, input.EventId, null);
if (!availability.Available) throw Oops.Bah(availability.Message);
if (availability.ExistingChecker && availability.CheckerUserId.HasValue)
{
checker = await _userRepository.GetFirstAsync(x =>
x.Id == availability.CheckerUserId.Value && !x.DeleteMark);
if (checker == null) throw Oops.Bah("检录员账号不存在");
}
}
if (input.CheckerUserId.HasValue)
{
var editAvailability = await GetCheckerAvailabilityAsync(input.Mobile, input.EventId, checker?.Id);
if (!editAvailability.Available) throw Oops.Bah(editAvailability.Message);
}
var eventTasks = await GetActiveEventTasksAsync(input.EventId);
var otherTasks = task == null ? eventTasks : eventTasks.Where(x => x.Id != task.Id).ToList();
if (task == null && otherTasks.Count >= MaxCheckerCount) throw Oops.Bah("每个赛事最多只能添加3名检录员");
if (otherTasks.Any(x => x.CheckinNo == input.CheckinNo)) throw Oops.Bah($"{ToCheckinName(input.CheckinNo)}已分配给其他检录员");
var assignedNumbers = otherTasks.Select(x => x.CheckinNo).Append(input.CheckinNo).OrderBy(x => x).ToList();
var expectedNumbers = Enumerable.Range(1, assignedNumbers.Count).ToList();
if (!assignedNumbers.SequenceEqual(expectedNumbers)) throw Oops.Bah("检次必须从一检开始连续分配,不能跳过前一检");
if (checker == null)
{
if (string.IsNullOrWhiteSpace(input.Password) || input.Password.Length is < 6 or > 32)
throw Oops.Bah("新增检录员时密码必须为6-32位");
var checkerId = _idGenerator.NewLong();
var now = DateTime.Now;
checker = new YD_Event.Core.Entities.AppUser
{
Id = checkerId,
Name = input.Name,
Mobile = input.Mobile,
Password = EncryptPassword(checkerId, input.Password),
PersonType = PersonType.User,
MiniProgramRole = MiniProgramRole.Checker,
Status = AvailabilityStatus.Enable,
AuditStatus = AuditStatus.Pass,
AuditUserId = managerUserId,
AuditTime = now,
CreatedUserId = managerUserId,
CreatedTime = now
};
await _userRepository.InsertAsync(checker);
}
else
{
checker.Name = input.Name;
checker.Mobile = input.Mobile;
checker.Status = AvailabilityStatus.Enable;
if (!string.IsNullOrWhiteSpace(input.Password))
{
if (input.Password.Length is < 6 or > 32) throw Oops.Bah("密码必须为6-32位");
checker.Password = EncryptPassword(checker.Id, input.Password);
}
checker.UpdatedUserId = managerUserId;
checker.UpdatedTime = DateTime.Now;
await _userRepository.UpdateAsync(checker);
}
if (task == null)
{
// 兼容历史解绑数据:旧逻辑只禁用了任务,但唯一索引仍将其视为占用。
// 同一赛事、同一检次存在已停用任务时直接复用该任务槽位,避免再次插入触发唯一索引冲突。
task = await _taskRepository.GetFirstAsync(x =>
x.EventId == input.EventId && x.CheckinNo == input.CheckinNo && !x.DeleteMark);
if (task == null)
{
task = new CheckinTask
{
EventId = input.EventId,
CreatedUserId = managerUserId,
CreatedTime = DateTime.Now
};
}
task.CheckerUserId = checker.Id;
}
ApplyCheckinAssignment(task, input.CheckinNo, assignedNumbers.Count, input.Venue);
task.TaskStatus = 1;
task.Status = AvailabilityStatus.Enable;
task.DeleteMark = false;
task.UpdatedUserId = managerUserId;
task.UpdatedTime = DateTime.Now;
await _taskRepository.InsertOrUpdateAsync(task);
await SyncEventTaskTotalsAsync(input.EventId, assignedNumbers.Count, managerUserId);
return ToDto(checker, task, options.EventName);
}
[HttpPost("task/progress")]
[DisplayName("旧版切换场次接口")]
public Task<CheckinCheckerDto> UpdateProgress([FromBody] UpdateCheckinProgressInput input)
{
throw Oops.Bah("检录员已按一检、二检、三检固定分工,不再手动切换场次");
}
[HttpPost("checker/unbind")]
[UnitOfWork]
[DisplayName("解除检录员赛事绑定")]
public async Task Unbind([FromBody] UnbindCheckerInput input)
{
EnsureManager();
var task = await _taskRepository.GetFirstAsync(x => x.Id == input.TaskId && !x.DeleteMark);
if (task == null) throw Oops.Bah("检录任务不存在");
var eventInfo = await _eventRepository.GetFirstAsync(x => x.Id == task.EventId && !x.DeleteMark);
if (eventInfo == null) throw Oops.Bah("赛事不存在");
var eventStatus = GetEffectiveEventStatus(eventInfo, DateTime.Now);
if (eventStatus == EventStatus.Ongoing) throw Oops.Bah("赛事比赛中,不支持解除检录员绑定");
var laterExists = await _taskRepository.IsAnyAsync(x =>
x.EventId == task.EventId && x.CheckinNo > task.CheckinNo && x.TaskStatus == 1 &&
x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
if (laterExists) throw Oops.Bah("请先解除最高检次的检录员,检次不能出现空缺");
task.TaskStatus = 0;
task.Status = AvailabilityStatus.Disable;
task.DeleteMark = true;
task.UpdatedUserId = _authManager.UserId;
task.UpdatedTime = DateTime.Now;
await _taskRepository.UpdateAsync(task);
var remainingCount = (await GetActiveEventTasksAsync(task.EventId)).Count;
await SyncEventTaskTotalsAsync(task.EventId, remainingCount, _authManager.UserId);
}
private async Task<List<CheckinTask>> GetActiveEventTasksAsync(long eventId)
{
return 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 SyncEventTaskTotalsAsync(long eventId, int total, long operatorUserId)
{
var tasks = await GetActiveEventTasksAsync(eventId);
foreach (var task in tasks)
{
task.TotalRounds = total;
task.CurrentRound = task.CheckinNo;
task.RoundName = ToCheckinName(task.CheckinNo);
task.UpdatedUserId = operatorUserId;
task.UpdatedTime = DateTime.Now;
await _taskRepository.UpdateAsync(task);
}
}
private void EnsureManager()
{
if (_authManager.AuthPlatformType != AuthPlatformType.Manager)
throw Oops.Bah("仅后台账号可以管理检录员和赛事绑定");
}
private string EncryptPassword(long userId, string password) =>
MD5Encryption.Encrypt($"{_idGenerator.Encode(userId)}{password}");
private static bool IsValidMobile(string mobile) =>
Regex.IsMatch(mobile, @"^1[3-9]\d{9}$");
private async Task<CheckerAccountAvailabilityDto> GetCheckerAvailabilityAsync(
string mobile, long? eventId, long? excludeCheckerUserId)
{
// SqlSugar 无法稳定翻译“Nullable.HasValue + != Nullable.Value”的组合表达式,
// SQL 只按手机号查询,排除当前编辑对象的判断放到内存执行。
var usersWithSameMobile = await _userRepository.AsQueryable()
.Where(x => x.Mobile == mobile)
.ToListAsync();
var existingUser = usersWithSameMobile.FirstOrDefault(x =>
!excludeCheckerUserId.HasValue || x.Id != excludeCheckerUserId.Value);
if (existingUser != null)
{
if (existingUser.DeleteMark || existingUser.MiniProgramRole != MiniProgramRole.Checker)
{
return new CheckerAccountAvailabilityDto
{
Available = false,
Message = "该手机号已被添加过其他身份,不能重复添加"
};
}
var activeTasks = await _taskRepository.AsQueryable()
.Where(x => x.CheckerUserId == existingUser.Id && x.TaskStatus == 1 &&
x.Status == AvailabilityStatus.Enable && !x.DeleteMark)
.ToListAsync();
if (eventId.HasValue && activeTasks.Any(x => x.EventId == eventId.Value))
{
return new CheckerAccountAvailabilityDto
{
Available = false,
Message = "该检录员已添加到当前赛事"
};
}
var occupiedEventIds = activeTasks
.Where(x => !eventId.HasValue || x.EventId != eventId.Value)
.Select(x => x.EventId)
.Distinct()
.ToList();
if (occupiedEventIds.Count > 0)
{
var occupiedEvents = await _eventRepository.AsQueryable()
.Where(x => occupiedEventIds.Contains(x.Id) &&
x.Status == AvailabilityStatus.Enable && !x.DeleteMark)
.ToListAsync();
var now = DateTime.Now;
var occupiedEvent = occupiedEvents.FirstOrDefault(x =>
GetEffectiveEventStatus(x, now) != EventStatus.Ended);
if (occupiedEvent != null)
{
return new CheckerAccountAvailabilityDto
{
Available = false,
Message = $"该检录员已绑定未结束赛事“{occupiedEvent.EventName}”,暂不能添加到其他赛事"
};
}
}
return new CheckerAccountAvailabilityDto
{
Available = true,
Message = "该手机号为已释放的检录员账号,可直接绑定当前赛事",
ExistingChecker = true,
CheckerUserId = existingUser.Id,
CheckerName = existingUser.Name ?? string.Empty
};
}
var sysUserExists = await _sysUserRepository.IsAnyAsync(x =>
(x.Mobile == mobile || x.Account == mobile) && !x.DeleteMark);
return sysUserExists
? new CheckerAccountAvailabilityDto
{
Available = false,
Message = "该手机号已是后台系统账号,不能再添加为检录员"
}
: new CheckerAccountAvailabilityDto
{
Available = true,
Message = "手机号可以使用"
};
}
private static void ApplyCheckinAssignment(CheckinTask task, int checkinNo, int total, string? venue)
{
task.CheckinNo = checkinNo;
task.TotalRounds = total;
task.CurrentRound = checkinNo;
task.RoundName = ToCheckinName(checkinNo);
task.CurrentSessionKey = null;
task.CurrentSessionOrder = 0;
task.CurrentSessionName = null;
task.CurrentEventProjectId = null;
task.CurrentSessionVenue = null;
task.CurrentGroupId = null;
task.CurrentGroupName = "全部运动员";
task.GroupOrder = 0;
task.TotalGroups = 0;
task.NextGroupName = null;
task.Venue = venue?.Trim();
}
/// <summary>
/// 统一检录管理使用的赛事状态口径。后台状态与比赛时间不一致时按生命周期靠后的状态处理,
/// 避免状态同步延迟导致比赛中仍可解绑,或赛事结束后账号未及时释放。
/// </summary>
private static EventStatus GetEffectiveEventStatus(EventInfo eventInfo, DateTime now)
{
var calculatedStatus = EventStatusUpdateService.CalculateEventStatus(eventInfo, now);
if (calculatedStatus == EventStatus.Ended || eventInfo.EventStatus == EventStatus.Ended)
return EventStatus.Ended;
if (calculatedStatus == EventStatus.Ongoing || eventInfo.EventStatus == EventStatus.Ongoing)
return EventStatus.Ongoing;
return calculatedStatus;
}
private static string ToCheckinName(int checkinNo) => checkinNo switch
{
1 => "一检",
2 => "二检",
3 => "三检",
_ => $"第{checkinNo}检"
};
private static CheckinCheckerDto ToDto(YD_Event.Core.Entities.AppUser? checker, CheckinTask task, string eventName)
{
return new CheckinCheckerDto
{
CheckerUserId = checker?.Id ?? task.CheckerUserId ?? 0,
Name = checker?.Name ?? string.Empty,
Mobile = checker?.Mobile ?? string.Empty,
CheckinNo = task.CheckinNo,
TaskId = task.Id,
EventId = task.EventId,
EventName = eventName,
CurrentRound = task.CheckinNo,
RoundName = ToCheckinName(task.CheckinNo),
CurrentGroupName = "全部运动员",
Venue = task.Venue ?? string.Empty,
TaskStatus = task.TaskStatus,
CreatedTime = task.CreatedTime
};
}
}