ltx 25830581d2 feat(api): 完善小程序赛事角色与秩序册检录接口
新增成绩查询、裁判认证及检录员账号管理接口,并统一小程序角色登录与个人中心数据。

检录任务绑定赛事秩序册场次,按项目、组别、场地和场次顺序执行;支持最多三名检录员、分配检录轮次及前序检录校验。

补充检录任务、检录记录、裁判申请实体及四份数据库迁移,包含表字段备注、秩序册绑定和检录序号。
2026-08-07 14:59:15 +08:00

277 lines
13 KiB
C#
Raw Permalink 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 YD_Event.Application.Auth;
using YD_Event.Application.Client.Dtos;
namespace YD_Event.Application.Client;
/// <summary>小程序检录员按秩序册列进行顺序检录。</summary>
[ApiDescriptionSettings("微信小程序接口")]
[Authorize]
public class CheckinController : IDynamicApiController
{
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _userRepository;
private readonly ISqlSugarRepository<CheckinTask> _taskRepository;
private readonly ISqlSugarRepository<CheckinRecord> _recordRepository;
private readonly CheckinScheduleService _scheduleService;
private readonly AuthManager _authManager;
public CheckinController(
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
ISqlSugarRepository<CheckinTask> taskRepository,
ISqlSugarRepository<CheckinRecord> recordRepository,
CheckinScheduleService scheduleService,
AuthManager authManager)
{
_userRepository = userRepository;
_taskRepository = taskRepository;
_recordRepository = recordRepository;
_scheduleService = scheduleService;
_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);
}
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();
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,
LockReason = previousPassed ? string.Empty : $"待通过{ToCheckinName(task.CheckinNo - 1)}"
};
}).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;
}
}
var activeTasks = await GetActiveEventTasksAsync(task.EventId);
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();
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;
}
}