319 lines
11 KiB
C#

using EasyCaching.Core;
using Microsoft.Extensions.Logging;
using System.Text.RegularExpressions;
using YD_Event.Application.Client.Dtos;
namespace YD_Event.Application.Client;
/// <summary>
/// 微信小程序认证接口
/// </summary>
[ApiDescriptionSettings("微信小程序接口")]
public class OAuthController : IDynamicApiController
{
private readonly ISqlSugarRepository<YD_Event.Core.Entities.AppUser> _appUserRepository;
private readonly IEasyCachingProvider _cachingProvider;
private readonly IIdGenerator _idGenerator;
private readonly ILogger<OAuthController> _logger;
private const string SmsCodeCachePrefix = "sms:code:";
public OAuthController(
ISqlSugarRepository<YD_Event.Core.Entities.AppUser> appUserRepository,
IEasyCachingProvider cachingProvider,
IIdGenerator idGenerator,
ILogger<OAuthController> logger)
{
_appUserRepository = appUserRepository;
_cachingProvider = cachingProvider;
_idGenerator = idGenerator;
_logger = logger;
}
/// <summary>
/// 登录接口(支持手机号密码登录和短信验证码登录;检录员仅支持密码)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("login")]
[AllowAnonymous]
public async Task<MiniProgramLoginOutput> 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<string>(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 = 7200,
UserInfo = userInfo
};
}
/// <summary>
/// 注册接口
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("register")]
[AllowAnonymous]
[UnitOfWork]
public async Task<MiniProgramRegisterOutput> Register([FromBody] MiniProgramRegisterInput input)
{
var cacheKey = $"{SmsCodeCachePrefix}{input.Mobile}";
var cachedCodeResult = _cachingProvider.Get<string>(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);
}
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)
};
}
/// <summary>
/// 重置密码接口
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("reset-password")]
[AllowAnonymous]
public async Task ResetPassword([FromBody] MiniProgramResetPasswordInput input)
{
var cacheKey = $"{SmsCodeCachePrefix}{input.Mobile}";
var cachedCodeResult = _cachingProvider.Get<string>(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);
}
/// <summary>
/// 生成自定义 Token
/// </summary>
private (string AccessToken, string RefreshToken) GenerateTokens(YD_Event.Core.Entities.AppUser appUser)
{
long uniqueId = _idGenerator.NewLong();
string accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>()
{
[AuthClaimsConst.AuthIdKey] = appUser.Id,
[AuthClaimsConst.AccountKey] = appUser.Mobile,
[AuthClaimsConst.UuidKey] = uniqueId,
[AuthClaimsConst.AuthPlatformTypeKey] = AuthPlatformType.WeChatApplet
});
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;
}
/// <summary>参赛者可选择运动员或教练身份;裁判、检录员统一按普通用户处理。</summary>
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"
};
}
}