using EasyCaching.Core; using Microsoft.Extensions.Logging; using System.Text.RegularExpressions; using YD_Event.Application.Client.Dtos; namespace YD_Event.Application.Client; /// /// 微信小程序认证接口 /// [ApiDescriptionSettings("微信小程序接口")] public class OAuthController : IDynamicApiController { private readonly ISqlSugarRepository _appUserRepository; private readonly ISqlSugarRepository _refereeRepository; private readonly IEasyCachingProvider _cachingProvider; private readonly IIdGenerator _idGenerator; private readonly ILogger _logger; private const string SmsCodeCachePrefix = "sms:code:"; private const long RefereeAccessTokenExpiredMinutes = 24 * 60; private const int DefaultAccessTokenExpiresInSeconds = 2 * 60 * 60; private const int RefereeAccessTokenExpiresInSeconds = 24 * 60 * 60; public OAuthController( ISqlSugarRepository appUserRepository, ISqlSugarRepository refereeRepository, IEasyCachingProvider cachingProvider, IIdGenerator idGenerator, ILogger logger) { _appUserRepository = appUserRepository; _refereeRepository = refereeRepository; _cachingProvider = cachingProvider; _idGenerator = idGenerator; _logger = logger; } /// /// 登录接口(支持手机号密码登录和短信验证码登录;检录员仅支持密码) /// /// /// [HttpPost("login")] [AllowAnonymous] public async Task Login([FromBody] MiniProgramLoginInput input) { var mobile = input.Mobile?.Trim() ?? string.Empty; if (!Regex.IsMatch(mobile, @"^1[3-9]\d{9}$")) throw Oops.Bah("手机号格式不正确"); var isPasswordLogin = !string.IsNullOrEmpty(input.Password); var isSmsLogin = !string.IsNullOrEmpty(input.VerificationCode); if (!isPasswordLogin && !isSmsLogin) { throw Oops.Bah("请选择密码登录或短信验证码登录"); } if (isPasswordLogin && isSmsLogin) { throw Oops.Bah("请选择一种登录方式"); } YD_Event.Core.Entities.AppUser? appUser; if (isPasswordLogin) { appUser = await _appUserRepository.GetFirstAsync(x => !x.DeleteMark && x.Mobile == mobile); } else { appUser = await _appUserRepository.GetFirstAsync(x => !x.DeleteMark && x.Mobile == mobile); } // 统一的未注册账户判断逻辑 bool isRegistered = appUser != null && !string.IsNullOrEmpty(appUser.Password); if (!isRegistered) { if (appUser == null || string.IsNullOrEmpty(appUser.Password)) { throw Oops.Bah("未注册过账户"); } } if (isPasswordLogin) { var encryptedPassword = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUser.Id)}{input.Password}"); if (appUser.Password != encryptedPassword) { throw Oops.Bah("密码错误"); } } else { if (appUser.MiniProgramRole == MiniProgramRole.Checker) { throw Oops.Bah("检录员仅支持手机号密码登录"); } var cacheKey = $"{SmsCodeCachePrefix}{mobile}"; var cachedCodeResult = _cachingProvider.Get(cacheKey); string cachedCode = cachedCodeResult.Value; if (!cachedCodeResult.HasValue || cachedCode != input.VerificationCode) { throw Oops.Bah("验证码错误或已过期"); } _cachingProvider.Remove(cacheKey); } (string accessToken, string refreshToken) = GenerateTokens(appUser); var userInfo = new { appUser.Name, appUser.Id, appUser.Mobile, appUser.Avatar, appUser.NickName, Role = ToRoleCode(appUser.MiniProgramRole), RoleType = (int)NormalizeRole(appUser.MiniProgramRole) }; return new MiniProgramLoginOutput { AccessToken = accessToken, RefreshToken = refreshToken, ExpiresIn = appUser.MiniProgramRole == MiniProgramRole.Referee ? RefereeAccessTokenExpiresInSeconds : DefaultAccessTokenExpiresInSeconds, UserInfo = userInfo }; } /// /// 注册接口 /// /// /// [HttpPost("register")] [AllowAnonymous] [UnitOfWork] public async Task Register([FromBody] MiniProgramRegisterInput input) { var cacheKey = $"{SmsCodeCachePrefix}{input.Mobile}"; var cachedCodeResult = _cachingProvider.Get(cacheKey); string cachedCode = cachedCodeResult.Value; if (!cachedCodeResult.HasValue || cachedCode != input.VerificationCode) { throw Oops.Bah("验证码错误或已过期"); } _cachingProvider.Remove(cacheKey); // 检查手机号是否已存在 var existingUser = await _appUserRepository.GetFirstAsync(x => x.Mobile == input.Mobile && !x.DeleteMark); YD_Event.Core.Entities.AppUser appUser; bool isNewUser = false; if (existingUser != null) { // 手机号已存在,检查是否已设置密码 if (!string.IsNullOrEmpty(existingUser.Password)) { throw Oops.Bah("该手机号已注册"); } // 未设置密码,更新现有记录 appUser = existingUser; var encryptedPassword = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUser.Id)}{input.Password}"); appUser.Password = encryptedPassword; appUser.MiniProgramRole = ParseRole(input); appUser.PersonType = ParsePersonType(input, appUser.MiniProgramRole); appUser.UpdatedTime = DateTime.Now; } else { // 手机号不存在,创建新用户 isNewUser = true; long appUserId = _idGenerator.NewLong(); var encryptedPassword = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUserId)}{input.Password}"); var role = ParseRole(input); appUser = new YD_Event.Core.Entities.AppUser { Id = appUserId, Name = input.NickName ?? input.Mobile, NickName = input.NickName, Avatar = input.Avatar, Mobile = input.Mobile, Password = encryptedPassword, PersonType = ParsePersonType(input, role), MiniProgramRole = role, Status = AvailabilityStatus.Enable, AuditStatus = AuditStatus.PendingSubmission, CreatedTime = DateTime.Now, CreatedUserId = 0 }; } if (isNewUser) { await _appUserRepository.InsertAsync(appUser); } else { await _appUserRepository.UpdateAsync(appUser); } if (appUser.MiniProgramRole == MiniProgramRole.Referee) { await EnsureRefereeProfileAsync(appUser); } return new MiniProgramRegisterOutput { UserId = appUser.Id, Mobile = appUser.Mobile, NickName = appUser.NickName, Role = ToRoleCode(appUser.MiniProgramRole), RoleType = (int)NormalizeRole(appUser.MiniProgramRole), PersonType = (int)appUser.PersonType, RegisterTime = isNewUser ? appUser.CreatedTime : (appUser.UpdatedTime ?? appUser.CreatedTime) }; } /// /// 裁判账号注册完成后立即建立未提交档案,使其能够在后台裁判管理中显示。 /// 已由后台提前建档的裁判按手机号关联账号,并保留原审核状态及认证资料。 /// private async Task EnsureRefereeProfileAsync(YD_Event.Core.Entities.AppUser appUser) { var referee = await _refereeRepository.GetFirstAsync(x => !x.DeleteMark && (x.AppUserId == appUser.Id || x.Mobile == appUser.Mobile)); if (referee != null) { if (referee.AppUserId.HasValue && referee.AppUserId.Value != appUser.Id) throw Oops.Bah("该手机号已关联其他裁判账号,请联系赛事组委会处理"); if (!referee.AppUserId.HasValue) { referee.AppUserId = appUser.Id; referee.UpdatedUserId = appUser.Id; referee.UpdatedTime = DateTime.Now; await _refereeRepository.UpdateAsync(referee); } return; } await _refereeRepository.InsertAsync(new YD_Event.Core.Entities.Referee { Id = _idGenerator.NextId(), AppUserId = appUser.Id, Name = appUser.Name ?? appUser.NickName ?? appUser.Mobile, Mobile = appUser.Mobile, ProvinceCode = string.Empty, ProvinceName = string.Empty, CityCode = string.Empty, CityName = string.Empty, DistrictCode = string.Empty, DistrictName = string.Empty, Level = 0, CertificateUrl = string.Empty, Roles = RefereeRole.None, AuditStatus = AuditStatus.PendingSubmission, Status = AvailabilityStatus.Enable, CreatedUserId = appUser.Id, CreatedTime = DateTime.Now }); } /// /// 重置密码接口 /// /// /// [HttpPost("reset-password")] [AllowAnonymous] public async Task ResetPassword([FromBody] MiniProgramResetPasswordInput input) { var cacheKey = $"{SmsCodeCachePrefix}{input.Mobile}"; var cachedCodeResult = _cachingProvider.Get(cacheKey); string cachedCode = cachedCodeResult.Value; if (!cachedCodeResult.HasValue || cachedCode != input.VerificationCode) { throw Oops.Bah("验证码错误或已过期"); } _cachingProvider.Remove(cacheKey); var appUser = await _appUserRepository.GetFirstAsync(x => x.Mobile == input.Mobile); if (appUser == null) { throw Oops.Bah("用户不存在"); } var encryptedPassword = MD5Encryption.Encrypt($"{_idGenerator.Encode(appUser.Id)}{input.NewPassword}"); appUser.Password = encryptedPassword; appUser.UpdatedTime = DateTime.Now; appUser.UpdatedUserId = appUser.Id; await _appUserRepository.UpdateAsync(appUser); } /// /// 生成自定义 Token /// private (string AccessToken, string RefreshToken) GenerateTokens(YD_Event.Core.Entities.AppUser appUser) { long uniqueId = _idGenerator.NewLong(); long? accessTokenExpiredMinutes = appUser.MiniProgramRole == MiniProgramRole.Referee ? RefereeAccessTokenExpiredMinutes : null; string accessToken = JWTEncryption.Encrypt(new Dictionary() { [AuthClaimsConst.AuthIdKey] = appUser.Id, [AuthClaimsConst.AccountKey] = appUser.Mobile, [AuthClaimsConst.UuidKey] = uniqueId, [AuthClaimsConst.AuthPlatformTypeKey] = AuthPlatformType.WeChatApplet }, accessTokenExpiredMinutes); string refreshToken = JWTEncryption.GenerateRefreshToken(accessToken); return (accessToken, refreshToken); } private static MiniProgramRole ParseRole(MiniProgramRegisterInput input) { if (!string.IsNullOrWhiteSpace(input.Role)) { return input.Role.Trim().ToLowerInvariant() switch { "participant" or "athlete" or "user" => MiniProgramRole.Participant, "referee" or "judge" => MiniProgramRole.Referee, "checker" or "checkin" or "inspector" => throw Oops.Bah("检录员账号只能由后台添加并绑定赛事"), _ => throw Oops.Bah("不支持的注册角色") }; } if (input.RoleType == (int)MiniProgramRole.Checker) { throw Oops.Bah("检录员账号只能由后台添加并绑定赛事"); } if (input.RoleType is >= 1 and <= 2) { return (MiniProgramRole)input.RoleType.Value; } return MiniProgramRole.Participant; } /// 参赛者可选择运动员或教练身份;裁判、检录员统一按普通用户处理。 private static PersonType ParsePersonType(MiniProgramRegisterInput input, MiniProgramRole role) { if (role != MiniProgramRole.Participant) return PersonType.User; return input.PersonType switch { (int)PersonType.Coach => PersonType.Coach, (int)PersonType.Athlete => PersonType.Athlete, null => PersonType.Athlete, _ => throw Oops.Bah("参赛者身份只能选择运动员或教练") }; } private static MiniProgramRole NormalizeRole(MiniProgramRole role) { return role is MiniProgramRole.Participant or MiniProgramRole.Referee or MiniProgramRole.Checker ? role : MiniProgramRole.Participant; } private static string ToRoleCode(MiniProgramRole role) { return NormalizeRole(role) switch { MiniProgramRole.Referee => "referee", MiniProgramRole.Checker => "checker", _ => "participant" }; } }