348 lines
15 KiB
C#
348 lines
15 KiB
C#
using EasyCaching.Core;
|
|
using YD_Event.Application.Auth;
|
|
using YD_Event.Application.Client.Dtos;
|
|
|
|
namespace YD_Event.Application.Client;
|
|
|
|
/// <summary>
|
|
/// 小程序裁判认证与执裁报名接口。
|
|
/// 裁判自己注册账号后在小程序提交认证资料,由后台审核;审核通过后才能针对赛事报名执裁。
|
|
/// </summary>
|
|
[ApiDescriptionSettings("微信小程序接口")]
|
|
[Authorize]
|
|
public class RefereeController : IDynamicApiController
|
|
{
|
|
private const string SmsCodeCachePrefix = "sms:code:";
|
|
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _userRepository;
|
|
private readonly ISqlSugarRepository<EventInfo> _eventRepository;
|
|
private readonly ISqlSugarRepository<RefereeApplication> _applicationRepository;
|
|
private readonly ISqlSugarRepository<YD_Event.Core.Entities.Referee> _refereeRepository;
|
|
private readonly ISqlSugarRepository<EventReferee> _eventRefereeRepository;
|
|
private readonly ISqlSugarRepository<EventRegistrationLimit> _limitRepository;
|
|
private readonly IEasyCachingProvider _cache;
|
|
private readonly AuthManager _authManager;
|
|
private readonly IIdGenerator _idGenerator;
|
|
|
|
public RefereeController(
|
|
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> userRepository,
|
|
ISqlSugarRepository<EventInfo> eventRepository,
|
|
ISqlSugarRepository<RefereeApplication> applicationRepository,
|
|
ISqlSugarRepository<YD_Event.Core.Entities.Referee> refereeRepository,
|
|
ISqlSugarRepository<EventReferee> eventRefereeRepository,
|
|
ISqlSugarRepository<EventRegistrationLimit> limitRepository,
|
|
IEasyCachingProvider cache,
|
|
AuthManager authManager,
|
|
IIdGenerator idGenerator)
|
|
{
|
|
_userRepository = userRepository;
|
|
_eventRepository = eventRepository;
|
|
_applicationRepository = applicationRepository;
|
|
_refereeRepository = refereeRepository;
|
|
_eventRefereeRepository = eventRefereeRepository;
|
|
_limitRepository = limitRepository;
|
|
_cache = cache;
|
|
_authManager = authManager;
|
|
_idGenerator = idGenerator;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取裁判认证页面的级别、角色下拉数据。
|
|
/// </summary>
|
|
[HttpGet("profile/options")]
|
|
[DisplayName("获取裁判认证选项")]
|
|
public RefereeProfileOptionsDto GetProfileOptions()
|
|
{
|
|
return new RefereeProfileOptionsDto
|
|
{
|
|
Levels = System.Enum.GetValues<RefereeLevel>()
|
|
.Select(x => new RefereeEnumOptionDto { Value = (int)x, Label = x.Description() }).ToList(),
|
|
Roles = System.Enum.GetValues<RefereeRole>()
|
|
.Where(x => x != RefereeRole.None)
|
|
.Select(x => new RefereeEnumOptionDto { Value = (int)x, Label = x.Description() }).ToList()
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// 获取我的裁判认证资料。尚未提交时返回 null。
|
|
/// </summary>
|
|
[HttpGet("profile")]
|
|
[DisplayName("获取我的裁判认证")]
|
|
public async Task<RefereeProfileDto?> GetProfile()
|
|
{
|
|
var userId = await EnsureRefereeAsync();
|
|
var referee = await _refereeRepository.GetFirstAsync(x => x.AppUserId == userId && !x.DeleteMark);
|
|
return referee == null ? null : ToProfileDto(referee);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 提交或更新裁判认证资料。已通过审核的资料再次提交后重新进入待审核。
|
|
/// </summary>
|
|
[HttpPost("profile/submit")]
|
|
[UnitOfWork]
|
|
[DisplayName("提交裁判认证")]
|
|
public async Task<RefereeProfileDto> SubmitProfile([FromBody] RefereeProfileSubmitInput input)
|
|
{
|
|
var userId = await EnsureRefereeAsync();
|
|
var user = await _userRepository.GetFirstAsync(x => x.Id == userId && !x.DeleteMark);
|
|
if (user == null) throw Oops.Bah("用户不存在");
|
|
|
|
var mobile = string.IsNullOrWhiteSpace(input.Mobile) ? user.Mobile : input.Mobile.Trim();
|
|
if (!Regex.IsMatch(mobile, @"^1[3-9]\d{9}$")) throw Oops.Bah("手机号格式不正确");
|
|
|
|
var referee = await _refereeRepository.GetFirstAsync(x => x.AppUserId == userId && !x.DeleteMark);
|
|
|
|
// 换绑手机号必须验证码校验,避免占用他人裁判档案。
|
|
var needVerify = !string.Equals(mobile, user.Mobile, StringComparison.Ordinal)
|
|
|| (referee != null && !string.Equals(mobile, referee.Mobile, StringComparison.Ordinal));
|
|
if (needVerify)
|
|
{
|
|
await VerifyCodeAsync(mobile, input.VerificationCode);
|
|
}
|
|
|
|
// 裁判库中手机号唯一,允许命中自己的档案。
|
|
var sameMobile = await _refereeRepository.AsQueryable()
|
|
.Where(x => x.Mobile == mobile && !x.DeleteMark).ToListAsync();
|
|
if (sameMobile.Any(x => referee == null || x.Id != referee.Id))
|
|
throw Oops.Bah("该手机号已存在于裁判库,请联系赛事组委会处理");
|
|
|
|
var isNew = referee == null;
|
|
referee ??= new YD_Event.Core.Entities.Referee
|
|
{
|
|
Id = _idGenerator.NextId(),
|
|
AppUserId = userId,
|
|
CreatedUserId = userId,
|
|
CreatedTime = DateTime.Now
|
|
};
|
|
|
|
referee.Name = input.Name.Trim();
|
|
referee.Mobile = mobile;
|
|
referee.ProvinceCode = input.ProvinceCode;
|
|
referee.ProvinceName = input.ProvinceName;
|
|
referee.CityCode = input.CityCode;
|
|
referee.CityName = input.CityName;
|
|
referee.DistrictCode = input.DistrictCode;
|
|
referee.DistrictName = input.DistrictName;
|
|
referee.Level = input.Level;
|
|
referee.CertificateUrl = input.CertificateUrl;
|
|
referee.Roles = input.Roles;
|
|
// 小程序提交的资料统一走后台审核。
|
|
referee.AuditStatus = AuditStatus.PendingAudit;
|
|
referee.AuditRemark = null;
|
|
referee.AuditUserId = null;
|
|
referee.AuditTime = null;
|
|
referee.Status = AvailabilityStatus.Enable;
|
|
|
|
if (isNew)
|
|
{
|
|
await _refereeRepository.InsertAsync(referee);
|
|
}
|
|
else
|
|
{
|
|
referee.UpdatedUserId = userId;
|
|
referee.UpdatedTime = DateTime.Now;
|
|
await _refereeRepository.UpdateAsync(referee);
|
|
}
|
|
|
|
if (needVerify) await _cache.RemoveAsync($"{SmsCodeCachePrefix}{mobile}");
|
|
return ToProfileDto(referee);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 查询指定赛事的裁判报名名额状态。
|
|
/// </summary>
|
|
[HttpGet("applications/quota")]
|
|
[DisplayName("获取赛事裁判名额")]
|
|
public async Task<RefereeQuotaDto> GetQuota([FromQuery] long eventId)
|
|
{
|
|
var userId = await EnsureRefereeAsync();
|
|
var evt = await _eventRepository.GetFirstAsync(x =>
|
|
x.Id == eventId && x.Status == AvailabilityStatus.Enable && !x.DeleteMark);
|
|
if (evt == null) throw Oops.Bah("赛事不存在");
|
|
|
|
var quota = await BuildQuotaAsync(evt, userId);
|
|
return quota;
|
|
}
|
|
|
|
[HttpGet("applications")]
|
|
[DisplayName("获取我的执裁报名")]
|
|
public async Task<RefereeApplicationListOutput> GetApplications()
|
|
{
|
|
var userId = await EnsureRefereeAsync();
|
|
var applications = await _applicationRepository.AsQueryable()
|
|
.Where(x => x.RefereeUserId == userId && x.Status == AvailabilityStatus.Enable)
|
|
.OrderByDescending(x => x.CreatedTime)
|
|
.ToListAsync();
|
|
|
|
var eventIds = applications.Select(x => x.EventId).Distinct().ToList();
|
|
var events = eventIds.Count == 0
|
|
? new List<EventInfo>()
|
|
: await _eventRepository.AsQueryable().Where(x => eventIds.Contains(x.Id)).ToListAsync();
|
|
var eventMap = events.ToDictionary(x => x.Id);
|
|
|
|
var rows = applications.Select(x =>
|
|
{
|
|
eventMap.TryGetValue(x.EventId, out var evt);
|
|
return new RefereeApplicationDto
|
|
{
|
|
Id = x.Id,
|
|
ApplicationId = x.Id,
|
|
EventId = x.EventId,
|
|
EventName = evt?.EventName ?? string.Empty,
|
|
CoverImage = evt?.CoverImage,
|
|
Address = evt?.DetailAddress,
|
|
EventStatus = evt == null ? 0 : (int)evt.EventStatus,
|
|
ApplicationStatus = x.ApplicationStatus,
|
|
ApplicationStatusText = ApplicationStatusText(x.ApplicationStatus),
|
|
AppliedAt = x.CreatedTime,
|
|
MatchStartTime = evt?.MatchStartTime ?? DateTime.MinValue,
|
|
MatchEndTime = evt?.MatchEndTime ?? DateTime.MinValue
|
|
};
|
|
}).ToList();
|
|
|
|
return new RefereeApplicationListOutput { Rows = rows, Total = rows.Count };
|
|
}
|
|
|
|
[HttpPost("applications/apply")]
|
|
[UnitOfWork]
|
|
[DisplayName("报名执裁")]
|
|
public async Task<object> Apply([FromBody] RefereeApplyInput input)
|
|
{
|
|
var userId = await EnsureRefereeAsync();
|
|
var evt = await _eventRepository.GetFirstAsync(x => x.Id == input.EventId && x.Status == AvailabilityStatus.Enable);
|
|
if (evt == null) throw Oops.Bah("赛事不存在");
|
|
if (evt.EventStatus != EventStatus.Enrolling) throw Oops.Bah("当前赛事不在执裁报名时间内");
|
|
|
|
var exists = await _applicationRepository.IsAnyAsync(x =>
|
|
x.EventId == input.EventId && x.RefereeUserId == userId && x.ApplicationStatus != 4);
|
|
if (exists) throw Oops.Bah("已报名执裁当前赛事");
|
|
|
|
var referee = await _refereeRepository.GetFirstAsync(x => x.AppUserId == userId && !x.DeleteMark);
|
|
if (referee == null) throw Oops.Bah("裁判档案不存在,请先完善裁判资料");
|
|
if (referee.AuditStatus != AuditStatus.Pass) throw Oops.Bah("裁判认证尚未通过审核,暂不能报名执裁");
|
|
|
|
var quota = await BuildQuotaAsync(evt, userId);
|
|
if (quota.IsFull) throw Oops.Bah("当前赛事裁判报名名额已满");
|
|
|
|
var application = new RefereeApplication
|
|
{
|
|
Id = _idGenerator.NextId(),
|
|
EventId = input.EventId,
|
|
RefereeUserId = userId,
|
|
// 小程序报名无需后台审批,提交成功即成为赛事裁判。
|
|
ApplicationStatus = 2,
|
|
Status = AvailabilityStatus.Enable,
|
|
CreatedUserId = userId,
|
|
CreatedTime = DateTime.Now
|
|
};
|
|
await _applicationRepository.InsertAsync(application);
|
|
|
|
if (!await _eventRefereeRepository.IsAnyAsync(x => x.EventId == input.EventId && x.RefereeId == referee.Id))
|
|
{
|
|
await _eventRefereeRepository.InsertAsync(new EventReferee
|
|
{
|
|
Id = _idGenerator.NextId(),
|
|
EventId = input.EventId,
|
|
RefereeId = referee.Id,
|
|
RefereeApplicationId = application.Id,
|
|
SourceType = 2,
|
|
Status = AvailabilityStatus.Enable,
|
|
CreatedUserId = userId,
|
|
CreatedTime = DateTime.Now
|
|
});
|
|
}
|
|
|
|
return new { success = true };
|
|
}
|
|
|
|
/// <summary>
|
|
/// 报名名额按赛事报名限制中的裁判限报数量控制,为 0 时不限名额。
|
|
/// </summary>
|
|
private async Task<RefereeQuotaDto> BuildQuotaAsync(EventInfo evt, long userId)
|
|
{
|
|
var limit = await _limitRepository.GetFirstAsync(x => x.EventId == evt.Id && !x.DeleteMark);
|
|
var max = limit?.RefereeLimit ?? 0;
|
|
var applied = await _applicationRepository.AsQueryable()
|
|
.Where(x => x.EventId == evt.Id && x.ApplicationStatus != 4 && x.Status == AvailabilityStatus.Enable)
|
|
.CountAsync();
|
|
var appliedByMe = await _applicationRepository.IsAnyAsync(x =>
|
|
x.EventId == evt.Id && x.RefereeUserId == userId && x.ApplicationStatus != 4);
|
|
|
|
var isFull = max > 0 && applied >= max && !appliedByMe;
|
|
var canApply = evt.EventStatus == EventStatus.Enrolling && !isFull && !appliedByMe;
|
|
|
|
string message;
|
|
if (appliedByMe) message = "已报名执裁当前赛事";
|
|
else if (isFull) message = "当前赛事裁判报名名额已满";
|
|
else if (evt.EventStatus != EventStatus.Enrolling) message = "当前赛事不在执裁报名时间内";
|
|
else message = max > 0 ? $"裁判名额未满,剩余{max - applied}个" : "裁判名额未满";
|
|
|
|
return new RefereeQuotaDto
|
|
{
|
|
EventId = evt.Id,
|
|
EventName = evt.EventName,
|
|
Limit = max,
|
|
Applied = applied,
|
|
Remaining = max > 0 ? Math.Max(0, max - applied) : 0,
|
|
IsFull = isFull,
|
|
Applied4Me = appliedByMe,
|
|
CanApply = canApply,
|
|
Message = message
|
|
};
|
|
}
|
|
|
|
private async Task VerifyCodeAsync(string mobile, string? code)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(code)) throw Oops.Bah("请输入验证码");
|
|
var cached = await _cache.GetAsync<string>($"{SmsCodeCachePrefix}{mobile}");
|
|
if (!cached.HasValue || cached.Value != code) throw Oops.Bah("验证码错误或已过期");
|
|
}
|
|
|
|
private async Task<long> EnsureRefereeAsync()
|
|
{
|
|
var userId = _authManager.UserId;
|
|
var user = await _userRepository.GetFirstAsync(x => x.Id == userId);
|
|
if (user == null) throw Oops.Bah("用户不存在");
|
|
if (user.MiniProgramRole != MiniProgramRole.Referee) throw Oops.Bah("当前账号不是裁判身份");
|
|
return userId;
|
|
}
|
|
|
|
private static RefereeProfileDto ToProfileDto(YD_Event.Core.Entities.Referee referee)
|
|
{
|
|
var roles = System.Enum.GetValues<RefereeRole>()
|
|
.Where(x => x != RefereeRole.None && referee.Roles.HasFlag(x)).ToList();
|
|
return new RefereeProfileDto
|
|
{
|
|
Id = referee.Id,
|
|
Name = referee.Name,
|
|
Mobile = referee.Mobile,
|
|
ProvinceCode = referee.ProvinceCode,
|
|
ProvinceName = referee.ProvinceName,
|
|
CityCode = referee.CityCode,
|
|
CityName = referee.CityName,
|
|
DistrictCode = referee.DistrictCode,
|
|
DistrictName = referee.DistrictName,
|
|
RegionName = $"{referee.ProvinceName}/{referee.CityName}/{referee.DistrictName}",
|
|
Level = referee.Level,
|
|
LevelName = referee.Level.Description(),
|
|
CertificateUrl = referee.CertificateUrl,
|
|
Roles = referee.Roles,
|
|
RoleValues = roles.Select(x => (int)x).ToList(),
|
|
RoleNames = roles.Select(x => x.Description()).ToList(),
|
|
AuditStatus = referee.AuditStatus,
|
|
AuditStatusText = referee.AuditStatus.Description(),
|
|
AuditRemark = referee.AuditRemark,
|
|
CanApply = referee.AuditStatus == AuditStatus.Pass,
|
|
CreatedTime = referee.CreatedTime
|
|
};
|
|
}
|
|
|
|
private static string ApplicationStatusText(int status) => status switch
|
|
{
|
|
2 => "报名成功",
|
|
3 => "报名未通过",
|
|
4 => "已取消",
|
|
_ => "待审批"
|
|
};
|
|
}
|