mi-assessment/server/MiAssessment/src/MiAssessment.Core/Services/AuthService.cs
18631081161 3ae1f99374
All checks were successful
continuous-integration/drone/push Build is passing
手机号
2026-03-25 23:44:30 +08:00

991 lines
38 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.Security.Cryptography;
using System.Text;
using System.Text.Json;
using MiAssessment.Core.Interfaces;
using MiAssessment.Model.Data;
using MiAssessment.Model.Entities;
using MiAssessment.Model.Models.Auth;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
namespace MiAssessment.Core.Services;
/// <summary>
/// <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5>
/// </summary>
public class AuthService : IAuthService
{
private readonly MiAssessmentDbContext _dbContext;
private readonly IUserService _userService;
private readonly IJwtService _jwtService;
private readonly IWechatService _wechatService;
private readonly IIpLocationService _ipLocationService;
private readonly IRedisService _redisService;
private readonly IConfigService _configService;
private readonly JwtSettings _jwtSettings;
private readonly ILogger<AuthService> _logger;
// Redis key prefixes
private const string LoginDebounceKeyPrefix = "login:debounce:";
private const string SmsCodeKeyPrefix = "sms:code:";
private const int DebounceSeconds = 3;
// Refresh Token <20><><EFBFBD><EFBFBD>
private const int RefreshTokenLength = 64;
public AuthService(
MiAssessmentDbContext dbContext,
IUserService userService,
IJwtService jwtService,
IWechatService wechatService,
IIpLocationService ipLocationService,
IRedisService redisService,
IConfigService configService,
JwtSettings jwtSettings,
ILogger<AuthService> logger)
{
_dbContext = dbContext ?? throw new ArgumentNullException(nameof(dbContext));
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
_jwtService = jwtService ?? throw new ArgumentNullException(nameof(jwtService));
_wechatService = wechatService ?? throw new ArgumentNullException(nameof(wechatService));
_ipLocationService = ipLocationService ?? throw new ArgumentNullException(nameof(ipLocationService));
_redisService = redisService ?? throw new ArgumentNullException(nameof(redisService));
_configService = configService ?? throw new ArgumentNullException(nameof(configService));
_jwtSettings = jwtSettings ?? throw new ArgumentNullException(nameof(jwtSettings));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// ΢<><CEA2>С<EFBFBD><D0A1><EFBFBD><EFBFBD><EFBFBD>¼
/// Requirements: 1.1-1.8
/// </summary>
public async Task<LoginResult> WechatMiniProgramLoginAsync(string code, int? pid, string? clickId, string? phoneCode = null)
{
_logger.LogInformation("[AuthService] ΢<>ŵ<EFBFBD>¼<EFBFBD><C2BC>ʼ<EFBFBD><CABC>code={Code}, pid={Pid}, hasPhoneCode={HasPhoneCode}", code, pid, !string.IsNullOrWhiteSpace(phoneCode));
if (string.IsNullOrWhiteSpace(code))
{
_logger.LogWarning("[AuthService] ΢<>ŵ<EFBFBD>¼ʧ<C2BC>ܣ<EFBFBD>codeΪ<65><CEAA>");
return new LoginResult
{
Success = false,
ErrorMessage = "<22><>Ȩcode<64><65><EFBFBD><EFBFBD>Ϊ<EFBFBD><CEAA>"
};
}
try
{
// 1.6 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> - 3<><33><EFBFBD>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD>¼
var debounceKey = $"{LoginDebounceKeyPrefix}wechat:{code}";
_logger.LogInformation("[AuthService] <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>: {Key}", debounceKey);
var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds));
if (!lockAcquired)
{
_logger.LogWarning("[AuthService] <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ܾ<EFBFBD><DCBE>ظ<EFBFBD><D8B8><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD>: {Code}", code);
return new LoginResult
{
Success = false,
ErrorMessage = "<22><><EFBFBD><EFBFBD>Ƶ<EFBFBD><C6B5><EFBFBD><EFBFBD>¼"
};
}
_logger.LogInformation("[AuthService] <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ȡ<EFBFBD>ɹ<EFBFBD>");
// 1.1 <20><><EFBFBD><EFBFBD>΢<EFBFBD><CEA2>API<50><49>ȡopenid<69><64>unionid
_logger.LogInformation("[AuthService] <20><>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD>΢<EFBFBD><CEA2>API<50><49>ȡopenid...");
var wechatResult = await _wechatService.GetOpenIdAsync(code);
_logger.LogInformation("[AuthService] ΢<><CEA2>API<50><49><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɣ<EFBFBD>Success={Success}, OpenId={OpenId}, UnionId={UnionId}, Error={Error}",
wechatResult.Success,
wechatResult.OpenId ?? "null",
wechatResult.UnionId ?? "null",
wechatResult.ErrorMessage ?? "null");
if (!wechatResult.Success)
{
_logger.LogWarning("[AuthService] ΢<><CEA2>API<50><49><EFBFBD><EFBFBD>ʧ<EFBFBD><CAA7>: {Error}", wechatResult.ErrorMessage);
return new LoginResult
{
Success = false,
ErrorMessage = wechatResult.ErrorMessage ?? "<22><>¼ʧ<C2BC>ܣ<EFBFBD><DCA3><EFBFBD><EFBFBD>Ժ<EFBFBD><D4BA><EFBFBD><EFBFBD><EFBFBD>"
};
}
var openId = wechatResult.OpenId!;
var unionId = wechatResult.UnionId;
// 1.2 <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD> - <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>unionid<69><64><EFBFBD>ң<EFBFBD><D2A3><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>openid<69><64><EFBFBD><EFBFBD>
User? user = null;
if (!string.IsNullOrWhiteSpace(unionId))
{
_logger.LogInformation("[AuthService] <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>unionid<69><64><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>: {UnionId}", unionId);
user = await _userService.GetUserByUnionIdAsync(unionId);
_logger.LogInformation("[AuthService] unionid<69><64><EFBFBD>ҽ<EFBFBD><D2BD>: {Found}", user != null ? $"<22>ҵ<EFBFBD><D2B5>û<EFBFBD>ID={user.Id}" : <>ҵ<EFBFBD>");
}
if (user == null)
{
_logger.LogInformation("[AuthService] <20><><EFBFBD><EFBFBD>ͨ<EFBFBD><CDA8>openid<69><64><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>: {OpenId}", openId);
user = await _userService.GetUserByOpenIdAsync(openId);
_logger.LogInformation("[AuthService] openid<69><64><EFBFBD>ҽ<EFBFBD><D2BD>: {Found}", user != null ? $"<22>ҵ<EFBFBD><D2B5>û<EFBFBD>ID={user.Id}" : <>ҵ<EFBFBD>");
}
if (user == null)
{
// 1.3 新用户,创建并绑定上级
_logger.LogInformation("[AuthService] 用户不存在,开始创建新用户, pid={Pid}", pid);
// 通过 phoneCode 获取手机号
string? mobile = null;
if (!string.IsNullOrWhiteSpace(phoneCode))
{
_logger.LogInformation("[AuthService] 开始通过phoneCode获取手机号");
var mobileResult = await _wechatService.GetMobileAsync(phoneCode);
if (mobileResult.Success && !string.IsNullOrWhiteSpace(mobileResult.Mobile))
{
mobile = mobileResult.Mobile;
_logger.LogInformation("[AuthService] 手机号获取成功");
}
else
{
_logger.LogWarning("[AuthService] 手机号获取失败: {Error}", mobileResult.ErrorMessage);
}
}
var createDto = new CreateUserDto
{
OpenId = openId,
UnionId = unionId,
Mobile = mobile,
Nickname = await GetDefaultNicknameAsync(),
Headimg = await GetDefaultAvatarAsync(openId),
Pid = pid ?? 0
};
_logger.LogInformation("[AuthService] CreateUserDto.Pid={Pid}", createDto.Pid);
user = await _userService.CreateUserAsync(createDto);
_logger.LogInformation("[AuthService] 新用户创建成功: UserId={UserId}, ParentUserId={ParentUserId}", user.Id, user.ParentUserId);
}
else
{
// 1.4 已有用户,不绑定上级
_logger.LogInformation("[AuthService] 已有用户登录: UserId={UserId}, ParentUserId={ParentUserId}, pid参数={Pid}(忽略)", user.Id, user.ParentUserId, pid);
if (string.IsNullOrWhiteSpace(user.UnionId) && !string.IsNullOrWhiteSpace(unionId))
{
_logger.LogInformation("[AuthService] 更新用户unionid: UserId={UserId}", user.Id);
await _userService.UpdateUserAsync(user.Id, new UpdateUserDto { UnionId = unionId });
_logger.LogInformation("[AuthService] unionid更新成功");
}
// 已有用户如果没有手机号,也尝试通过 phoneCode 补充
if (string.IsNullOrWhiteSpace(user.Phone) && !string.IsNullOrWhiteSpace(phoneCode))
{
_logger.LogInformation("[AuthService] 已有用户缺少手机号尝试通过phoneCode补充");
var mobileResult = await _wechatService.GetMobileAsync(phoneCode);
if (mobileResult.Success && !string.IsNullOrWhiteSpace(mobileResult.Mobile))
{
await _userService.UpdateUserAsync(user.Id, new UpdateUserDto { Mobile = mobileResult.Mobile });
user.Phone = mobileResult.Mobile;
_logger.LogInformation("[AuthService] 手机号补充成功");
}
}
}
// 1.5 <20><><EFBFBD><EFBFBD>˫ Token<65><6E>Access Token + Refresh Token<65><6E>
_logger.LogInformation("[AuthService] <20><>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD>˫ Token: UserId={UserId}", user.Id);
var loginResponse = await GenerateLoginResponseAsync(user, null);
_logger.LogInformation("[AuthService] ˫ Token <20><><EFBFBD>ɳɹ<C9B3><C9B9><EFBFBD>AccessToken<65><6E><EFBFBD><EFBFBD>={Length}", loginResponse.AccessToken?.Length ?? 0);
_logger.LogInformation("[AuthService] ΢<>ŵ<EFBFBD>¼<EFBFBD>ɹ<EFBFBD>: UserId={UserId}", user.Id);
return new LoginResult
{
Success = true,
Token = loginResponse.AccessToken, // <20><><EFBFBD>ݾɰ<DDBE>
UserId = user.Id,
LoginResponse = loginResponse
};
}
catch (Exception ex)
{
_logger.LogError(ex, "[AuthService] ΢<>ŵ<EFBFBD>¼<EFBFBD>쳣: code={Code}, Message={Message}, StackTrace={StackTrace}",
code, ex.Message, ex.StackTrace);
return new LoginResult
{
Success = false,
ErrorMessage = "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣ<EFBFBD><CFA3><EFBFBD><EFBFBD>Ժ<EFBFBD><D4BA><EFBFBD><EFBFBD><EFBFBD>"
};
}
}
/// <summary>
/// <20>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><D6A4><EFBFBD>¼
/// Requirements: 2.1-2.7
/// </summary>
public async Task<LoginResult> MobileLoginAsync(string mobile, string code, int? pid, string? clickId)
{
if (string.IsNullOrWhiteSpace(mobile))
{
return new LoginResult
{
Success = false,
ErrorMessage = "<22>ֻ<EFBFBD><D6BB>Ų<EFBFBD><C5B2><EFBFBD>Ϊ<EFBFBD><CEAA>"
};
}
if (string.IsNullOrWhiteSpace(code))
{
return new LoginResult
{
Success = false,
ErrorMessage = "<22><>֤<EFBFBD><EFBFBD><EBB2BB>Ϊ<EFBFBD><CEAA>"
};
}
try
{
// 2.6 <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> - 3<><33><EFBFBD>ڲ<EFBFBD><DAB2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ظ<EFBFBD><D8B8><EFBFBD>¼
var debounceKey = $"{LoginDebounceKeyPrefix}mobile:{mobile}";
var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds));
if (!lockAcquired)
{
_logger.LogWarning("Login debounce triggered for mobile: {Mobile}", MaskMobile(mobile));
return new LoginResult
{
Success = false,
ErrorMessage = "<22><><EFBFBD><EFBFBD>Ƶ<EFBFBD><C6B5><EFBFBD><EFBFBD>¼"
};
}
// 2.1 <20><>Redis<69><73>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>֤<EFBFBD><D6A4>֤<EFBFBD><D6A4>
var smsCodeKey = $"{SmsCodeKeyPrefix}{mobile}";
var storedCode = await _redisService.GetStringAsync(smsCodeKey);
if (string.IsNullOrWhiteSpace(storedCode) || storedCode != code)
{
_logger.LogWarning("SMS code verification failed for mobile: {Mobile}", MaskMobile(mobile));
return new LoginResult
{
Success = false,
ErrorMessage = "<22><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD><EFBFBD>"
};
}
// 2.2 <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>֤ͨ<D6A4><CDA8><EFBFBD><EFBFBD>ɾ<EFBFBD><C9BE>Redis<69>е<EFBFBD><D0B5><EFBFBD>֤<EFBFBD><D6A4>
await _redisService.DeleteAsync(smsCodeKey);
// <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
var user = await _userService.GetUserByMobileAsync(mobile);
if (user == null)
{
// 2.3 <20>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><DAA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
var createDto = new CreateUserDto
{
Mobile = mobile,
Nickname = await GetDefaultNicknameAsync(),
Headimg = await GetDefaultAvatarAsync(mobile),
Pid = pid ?? 0
};
user = await _userService.CreateUserAsync(createDto);
_logger.LogInformation("New user created via mobile login: UserId={UserId}, Mobile={Mobile}", user.Id, MaskMobile(mobile));
}
// 2.4 <20><><EFBFBD><EFBFBD>˫ Token<65><6E>Access Token + Refresh Token<65><6E>
var loginResponse = await GenerateLoginResponseAsync(user, null);
_logger.LogInformation("Mobile login successful: UserId={UserId}", user.Id);
return new LoginResult
{
Success = true,
Token = loginResponse.AccessToken, // <20><><EFBFBD>ݾɰ<DDBE>
UserId = user.Id,
LoginResponse = loginResponse
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Mobile login failed for mobile: {Mobile}", MaskMobile(mobile));
return new LoginResult
{
Success = false,
ErrorMessage = "<22><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣ<EFBFBD><CFA3><EFBFBD><EFBFBD>Ժ<EFBFBD><D4BA><EFBFBD><EFBFBD><EFBFBD>"
};
}
}
/// <summary>
/// <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD>
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> BindMobileAsync(long userId, string mobile, string code)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("<22>ֻ<EFBFBD><D6BB>Ų<EFBFBD><C5B2><EFBFBD>Ϊ<EFBFBD><CEAA>", nameof(mobile));
}
if (string.IsNullOrWhiteSpace(code))
{
throw new ArgumentException("<22><>֤<EFBFBD><EFBFBD><EBB2BB>Ϊ<EFBFBD><CEAA>", nameof(code));
}
// 5.1 <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD><D6A4>
var smsCodeKey = $"{SmsCodeKeyPrefix}{mobile}";
var storedCode = await _redisService.GetStringAsync(smsCodeKey);
if (string.IsNullOrWhiteSpace(storedCode) || storedCode != code)
{
_logger.LogWarning("SMS code verification failed for bind mobile: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
throw new InvalidOperationException("<22><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD><EFBFBD>");
}
// <20><>֤<EFBFBD><D6A4><EFBFBD><EFBFBD>֤ͨ<D6A4><CDA8><EFBFBD><EFBFBD>ɾ<EFBFBD><C9BE>
await _redisService.DeleteAsync(smsCodeKey);
// <20><>ȡ<EFBFBD><C8A1>ǰ<EFBFBD>û<EFBFBD>
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("<22>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// <20><><EFBFBD><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD>
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 <20>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB>󶨣<EFBFBD><F3B6A8A3><EFBFBD>Ҫ<EFBFBD>ϲ<EFBFBD><CFB2>˻<EFBFBD>
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 <20>ֻ<EFBFBD><D6BB><EFBFBD>δ<EFBFBD><CEB4><EFBFBD>󶨣<EFBFBD>ֱ<EFBFBD>Ӹ<EFBFBD><D3B8>µ<EFBFBD>ǰ<EFBFBD>û<EFBFBD><C3BB><EFBFBD><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD>
await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile });
_logger.LogInformation("Mobile bound successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
return new BindMobileResponse { Token = null };
}
/// <summary>
/// ΢<><CEA2><EFBFBD><EFBFBD>Ȩ<EFBFBD><C8A8><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD>
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> WechatBindMobileAsync(long userId, string wechatCode)
{
if (string.IsNullOrWhiteSpace(wechatCode))
{
throw new ArgumentException(<><CEA2><EFBFBD><EFBFBD>Ȩcode<64><65><EFBFBD><EFBFBD>Ϊ<EFBFBD><CEAA>", nameof(wechatCode));
}
// <20><><EFBFBD><EFBFBD>΢<EFBFBD><CEA2>API<50><49>ȡ<EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD>
var mobileResult = await _wechatService.GetMobileAsync(wechatCode);
if (!mobileResult.Success || string.IsNullOrWhiteSpace(mobileResult.Mobile))
{
_logger.LogWarning("WeChat get mobile failed: UserId={UserId}, Error={Error}", userId, mobileResult.ErrorMessage);
throw new InvalidOperationException(mobileResult.ErrorMessage ?? "<22><>ȡ<EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD>ʧ<EFBFBD><CAA7>");
}
var mobile = mobileResult.Mobile;
// <20><>ȡ<EFBFBD><C8A1>ǰ<EFBFBD>û<EFBFBD>
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("<22>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// <20><><EFBFBD><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD>
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 <20>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB>󶨣<EFBFBD><F3B6A8A3><EFBFBD>Ҫ<EFBFBD>ϲ<EFBFBD><CFB2>˻<EFBFBD>
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 <20>ֻ<EFBFBD><D6BB><EFBFBD>δ<EFBFBD><CEB4><EFBFBD>󶨣<EFBFBD>ֱ<EFBFBD>Ӹ<EFBFBD><D3B8>µ<EFBFBD>ǰ<EFBFBD>û<EFBFBD><C3BB><EFBFBD><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD>
await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile });
_logger.LogInformation("Mobile bound via WeChat successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
return new BindMobileResponse { Token = null };
}
/// <summary>
/// <20><>¼<EFBFBD><C2BC>¼<EFBFBD><C2BC>Ϣ
/// Requirements: 6.1, 6.3, 6.4
/// </summary>
public async Task<RecordLoginResponse> RecordLoginAsync(long userId, string? device, string? deviceInfo)
{
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
throw new InvalidOperationException("<22>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
try
{
// <20><>ȡ<EFBFBD>ͻ<EFBFBD><CDBB><EFBFBD>IP<49><50><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʹ<EFBFBD>ÿ<EFBFBD><C3BF>ַ<EFBFBD><D6B7><EFBFBD><EFBFBD><EFBFBD>Ϊռλ<D5BC><CEBB><EFBFBD><EFBFBD>ʵ<EFBFBD><CAB5>IPӦ<50><D3A6>Controller<65><72><EFBFBD>
var clientIp = deviceInfo ?? string.Empty;
var now = DateTime.Now;
// 6.1 <20><>¼<EFBFBD><C2BC>¼<EFBFBD><C2BC>־
var loginLog = new UserLoginLog
{
UserId = userId,
LoginType = "wechat",
LoginIp = clientIp,
UserAgent = device,
Platform = "miniprogram",
Status = 1,
CreateTime = now
};
await _dbContext.UserLoginLogs.AddAsync(loginLog);
// <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD>¼ʱ<C2BC><CAB1>
user.LastLoginTime = now;
user.LastLoginIp = clientIp;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Login recorded: UserId={UserId}, Device={Device}, IP={IP}", userId, device, clientIp);
// 6.4 <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD>uid<69><64><EFBFBD>dzƺ<C7B3>ͷ<EFBFBD><CDB7>
return new RecordLoginResponse
{
Uid = user.Uid,
Nickname = user.Nickname,
Headimg = user.Avatar
};
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to record login: UserId={UserId}", userId);
throw;
}
}
/// <summary>
/// H5<48><35><EFBFBD>ֻ<EFBFBD><D6BB>ţ<EFBFBD><C5A3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>֤<EFBFBD>
/// Requirements: 13.1
/// </summary>
public async Task<BindMobileResponse> BindMobileH5Async(long userId, string mobile)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("<22>ֻ<EFBFBD><D6BB>Ų<EFBFBD><C5B2><EFBFBD>Ϊ<EFBFBD><CEAA>", nameof(mobile));
}
// <20><>ȡ<EFBFBD><C8A1>ǰ<EFBFBD>û<EFBFBD>
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("<22>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// <20><><EFBFBD><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD>
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// <20>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB>󶨣<EFBFBD><F3B6A8A3><EFBFBD>Ҫ<EFBFBD>ϲ<EFBFBD><CFB2>˻<EFBFBD>
return await MergeAccountsAsync(currentUser, existingUser);
}
// <20>ֻ<EFBFBD><D6BB><EFBFBD>δ<EFBFBD><CEB4><EFBFBD>󶨣<EFBFBD>ֱ<EFBFBD>Ӹ<EFBFBD><D3B8>µ<EFBFBD>ǰ<EFBFBD>û<EFBFBD><C3BB><EFBFBD><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD>
await _userService.UpdateUserAsync(userId, new UpdateUserDto { Mobile = mobile });
_logger.LogInformation("H5 Mobile bound successfully: UserId={UserId}, Mobile={Mobile}", userId, MaskMobile(mobile));
return new BindMobileResponse { Token = null };
}
/// <summary>
/// <20>˺<EFBFBD>ע<EFBFBD><D7A2>
/// Requirements: 7.1-7.3
/// </summary>
public async Task LogOffAsync(long userId, int type)
{
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
throw new InvalidOperationException("<22>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
try
{
// 7.1 <20><>¼ע<C2BC><D7A2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>־
var action = type == 0 ? <><D7A2><EFBFBD>˺<EFBFBD>" : <><C8A1>ע<EFBFBD><D7A2>";
_logger.LogInformation("User log off request: UserId={UserId}, Type={Type}, Action={Action}", userId, type, action);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ӹ<EFBFBD><D3B8><EFBFBD><EFBFBD>ע<EFBFBD><D7A2><EFBFBD>߼<EFBFBD><DFBC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
// - <20><><EFBFBD>û<EFBFBD>״̬<D7B4><CCAC><EFBFBD><EFBFBD>Ϊ<EFBFBD><CEAA>ע<EFBFBD><D7A2>
// - <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD>صĻ<D8B5><C4BB><EFBFBD>
// - <20><><EFBFBD><EFBFBD>֪ͨ<CDA8><D6AA>
if (type == 0)
{
// ע<><D7A2><EFBFBD>˺<EFBFBD> - <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>״̬Ϊ<CCAC><CEAA><EFBFBD><EFBFBD>
user.Status = 0;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("User account deactivated: UserId={UserId}", userId);
}
else if (type == 1)
{
// ȡ<><C8A1>ע<EFBFBD><D7A2> - <20>ָ<EFBFBD><D6B8>û<EFBFBD>״̬
user.Status = 1;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("User account reactivated: UserId={UserId}", userId);
}
// 7.2 <20><><EFBFBD><EFBFBD>ע<EFBFBD><D7A2><EFBFBD>ɹ<EFBFBD><C9B9><EFBFBD><EFBFBD><EFBFBD>Ϣ<EFBFBD><CFA2>ͨ<EFBFBD><CDA8><EFBFBD><EFBFBD><EFBFBD>׳<EFBFBD><D7B3><EFBFBD><ECB3A3><EFBFBD><EFBFBD>ʾ<EFBFBD>ɹ<EFBFBD><C9B9><EFBFBD>
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process log off: UserId={UserId}, Type={Type}", userId, type);
throw;
}
}
#region Refresh Token Methods
/// <summary>
/// <20><><EFBFBD><EFBFBD> Refresh Token <20><><EFBFBD><EFBFBD><E6B4A2><EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD>
/// Requirements: 1.4, 1.5, 4.1
/// </summary>
/// <param name="userId"><3E>û<EFBFBD>ID</param>
/// <param name="ipAddress"><3E>ͻ<EFBFBD><CDBB><EFBFBD> IP <20><>ַ</param>
/// <returns><3E><><EFBFBD>ɵ<EFBFBD> Refresh Token <20><><EFBFBD><EFBFBD></returns>
private async Task<string> GenerateRefreshTokenAsync(long userId, string? ipAddress)
{
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token
var refreshToken = GenerateSecureRandomString(RefreshTokenLength);
// <20><><EFBFBD><EFBFBD> SHA256 <20><>ϣֵ<CFA3><D6B5><EFBFBD>ڴ洢
var tokenHash = ComputeSha256Hash(refreshToken);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʱ<EFBFBD>䣨7<E4A3A8>
var expiresAt = DateTime.Now.AddDays(_jwtSettings.RefreshTokenExpirationDays);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ݿ<EFBFBD><DDBF>¼
var userRefreshToken = new UserRefreshToken
{
UserId = userId,
TokenHash = tokenHash,
ExpiresAt = expiresAt,
CreatedAt = DateTime.Now,
CreatedByIp = ipAddress
};
await _dbContext.UserRefreshTokens.AddAsync(userRefreshToken);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Generated refresh token for user {UserId}, expires at {ExpiresAt}", userId, expiresAt);
return refreshToken;
}
/// <summary>
/// <20><><EFBFBD>ɵ<EFBFBD>¼<EFBFBD><C2BC>Ӧ<EFBFBD><D3A6><EFBFBD><EFBFBD><EFBFBD><EFBFBD>˫ Token<65><6E>
/// Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
/// </summary>
/// <param name="user"><3E>û<EFBFBD>ʵ<EFBFBD><CAB5></param>
/// <param name="ipAddress"><3E>ͻ<EFBFBD><CDBB><EFBFBD> IP <20><>ַ</param>
/// <returns><3E><>¼<EFBFBD><C2BC>Ӧ</returns>
private async Task<LoginResponse> GenerateLoginResponseAsync(User user, string? ipAddress)
{
// <20><><EFBFBD><EFBFBD> Access Token (JWT)
var accessToken = _jwtService.GenerateToken(user);
// <20><><EFBFBD><EFBFBD> Refresh Token <20><><EFBFBD>
var refreshToken = await GenerateRefreshTokenAsync(user.Id, ipAddress);
// <20><><EFBFBD><EFBFBD> Access Token <20><><EFBFBD><EFBFBD>ʱ<EFBFBD><EFBFBD>
var expiresIn = _jwtSettings.ExpirationMinutes * 60;
return new LoginResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresIn = expiresIn,
UserId = user.Id
};
}
/// <summary>
/// ˢ<><CBA2> Token
/// Requirements: 2.1-2.6
/// </summary>
public async Task<RefreshTokenResult> RefreshTokenAsync(string refreshToken, string? ipAddress)
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
_logger.LogWarning("Refresh token is empty");
return RefreshTokenResult.Fail(<><CBA2><EFBFBD><EFBFBD><EFBFBD>Ʋ<EFBFBD><C6B2><EFBFBD>Ϊ<EFBFBD><CEAA>");
}
try
{
// <20><><EFBFBD><EFBFBD> Token <20><>ϣֵ
var tokenHash = ComputeSha256Hash(refreshToken);
// <20><><EFBFBD><EFBFBD> Token <20><>¼
var storedToken = await _dbContext.UserRefreshTokens
.Include(t => t.User)
.FirstOrDefaultAsync(t => t.TokenHash == tokenHash);
if (storedToken == null)
{
_logger.LogWarning("Refresh token not found: {TokenHash}", tokenHash.Substring(0, 8) + "...");
return RefreshTokenResult.Fail("<22><>Ч<EFBFBD><D0A7>ˢ<EFBFBD><CBA2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
// <20><><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>ѹ<EFBFBD><D1B9><EFBFBD>
if (storedToken.IsExpired)
{
_logger.LogWarning("Refresh token expired for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail(<><CBA2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ѹ<EFBFBD><D1B9><EFBFBD>");
}
// <20><><EFBFBD><EFBFBD>Ƿ<EFBFBD><C7B7>ѳ<EFBFBD><D1B3><EFBFBD>
if (storedToken.IsRevoked)
{
_logger.LogWarning("Refresh token revoked for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail(<><CBA2><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧЧ");
}
// <20><><EFBFBD><EFBFBD>û<EFBFBD><C3BB>Ƿ<EFBFBD><C7B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч
var user = storedToken.User;
if (user == null)
{
_logger.LogWarning("User not found for refresh token");
return RefreshTokenResult.Fail("<22>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>");
}
if (user.Status == 0)
{
_logger.LogWarning("User {UserId} is disabled", user.Id);
return RefreshTokenResult.Fail("<22>˺<EFBFBD><CBBA>ѱ<EFBFBD><D1B1><EFBFBD><EFBFBD><EFBFBD>");
}
// Token <20>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD> Refresh Token
var newRefreshToken = GenerateSecureRandomString(RefreshTokenLength);
var newTokenHash = ComputeSha256Hash(newRefreshToken);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token <20><><EFBFBD><EFBFBD>¼<EFBFBD><C2BC><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϵ
storedToken.RevokedAt = DateTime.Now;
storedToken.RevokedByIp = ipAddress;
storedToken.ReplacedByToken = newTokenHash;
// <20><><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD> Token <20><>¼
var newUserRefreshToken = new UserRefreshToken
{
UserId = user.Id,
TokenHash = newTokenHash,
ExpiresAt = DateTime.Now.AddDays(_jwtSettings.RefreshTokenExpirationDays),
CreatedAt = DateTime.Now,
CreatedByIp = ipAddress
};
await _dbContext.UserRefreshTokens.AddAsync(newUserRefreshToken);
await _dbContext.SaveChangesAsync();
// <20><><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD> Access Token
var accessToken = _jwtService.GenerateToken(user);
var expiresIn = _jwtSettings.ExpirationMinutes * 60;
_logger.LogInformation("Token refreshed successfully for user {UserId}", user.Id);
return RefreshTokenResult.Ok(new LoginResponse
{
AccessToken = accessToken,
RefreshToken = newRefreshToken,
ExpiresIn = expiresIn,
UserId = user.Id
});
}
catch (Exception ex)
{
_logger.LogError(ex, "Error refreshing token");
return RefreshTokenResult.Fail(<><CBA2><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ʧ<EFBFBD>ܣ<EFBFBD><DCA3><EFBFBD><EFBFBD>Ժ<EFBFBD><D4BA><EFBFBD><EFBFBD><EFBFBD>");
}
}
/// <summary>
/// <20><><EFBFBD><EFBFBD> Token
/// Requirements: 4.4
/// </summary>
public async Task RevokeTokenAsync(string refreshToken, string? ipAddress)
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
_logger.LogWarning("Cannot revoke empty refresh token");
return;
}
try
{
// <20><><EFBFBD><EFBFBD> Token <20><>ϣֵ
var tokenHash = ComputeSha256Hash(refreshToken);
// <20><><EFBFBD><EFBFBD> Token <20><>¼
var storedToken = await _dbContext.UserRefreshTokens
.FirstOrDefaultAsync(t => t.TokenHash == tokenHash);
if (storedToken == null)
{
_logger.LogWarning("Refresh token not found for revocation");
return;
}
// <20><><EFBFBD><EFBFBD>Ѿ<EFBFBD><D1BE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֱ<EFBFBD>ӷ<EFBFBD><D3B7><EFBFBD>
if (storedToken.IsRevoked)
{
_logger.LogInformation("Refresh token already revoked");
return;
}
// <20><><EFBFBD><EFBFBD> Token
storedToken.RevokedAt = DateTime.Now;
storedToken.RevokedByIp = ipAddress;
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Refresh token revoked for user {UserId}", storedToken.UserId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error revoking refresh token");
throw;
}
}
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Token
/// Requirements: 4.4
/// </summary>
public async Task RevokeAllUserTokensAsync(long userId, string? ipAddress)
{
try
{
// <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>Ч<EFBFBD><D0A7> Token
var activeTokens = await _dbContext.UserRefreshTokens
.Where(t => t.UserId == userId && t.RevokedAt == null)
.ToListAsync();
if (!activeTokens.Any())
{
_logger.LogInformation("No active tokens found for user {UserId}", userId);
return;
}
var now = DateTime.Now;
foreach (var token in activeTokens)
{
token.RevokedAt = now;
token.RevokedByIp = ipAddress;
}
await _dbContext.SaveChangesAsync();
_logger.LogInformation("Revoked {Count} tokens for user {UserId}", activeTokens.Count, userId);
}
catch (Exception ex)
{
_logger.LogError(ex, "Error revoking all tokens for user {UserId}", userId);
throw;
}
}
#endregion
#region Private Helper Methods
/// <summary>
/// <20>ϲ<EFBFBD><CFB2>˻<EFBFBD> - <20><><EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD><C3BB><EFBFBD>openidǨ<64>Ƶ<EFBFBD><C6B5>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>û<EFBFBD>
/// </summary>
private async Task<BindMobileResponse> MergeAccountsAsync(User currentUser, User mobileUser)
{
using var transaction = await _dbContext.Database.BeginTransactionAsync();
try
{
_logger.LogInformation("Merging accounts: CurrentUserId={CurrentUserId}, MobileUserId={MobileUserId}",
currentUser.Id, mobileUser.Id);
// 5.2 <20><><EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD><C3BB><EFBFBD>openidǨ<64>Ƶ<EFBFBD><C6B5>ֻ<EFBFBD><D6BB><EFBFBD><EFBFBD>û<EFBFBD>
if (!string.IsNullOrWhiteSpace(currentUser.OpenId))
{
mobileUser.OpenId = currentUser.OpenId;
}
if (!string.IsNullOrWhiteSpace(currentUser.UnionId))
{
mobileUser.UnionId = currentUser.UnionId;
}
mobileUser.UpdateTime = DateTime.Now;
_dbContext.Users.Update(mobileUser);
// ɾ<><C9BE><EFBFBD><EFBFBD>ǰ<EFBFBD>û<EFBFBD>
_dbContext.Users.Remove(currentUser);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
// 5.3 <20><><EFBFBD><EFBFBD><EFBFBD>µ<EFBFBD>token
var newToken = _jwtService.GenerateToken(mobileUser);
_logger.LogInformation("Accounts merged successfully: NewUserId={NewUserId}", mobileUser.Id);
return new BindMobileResponse { Token = newToken };
}
catch (Exception ex)
{
await transaction.RollbackAsync();
_logger.LogError(ex, "Failed to merge accounts: CurrentUserId={CurrentUserId}, MobileUserId={MobileUserId}",
currentUser.Id, mobileUser.Id);
throw;
}
}
/// <summary>
/// 获取用户默认昵称(从配置读取前缀)
/// </summary>
private async Task<string> GetDefaultNicknameAsync()
{
try
{
var configJson = await _configService.GetConfigValueAsync("user_config");
if (!string.IsNullOrEmpty(configJson))
{
var config = JsonSerializer.Deserialize<JsonElement>(configJson);
if (config.TryGetProperty("default_nickname_prefix", out var prefixEl) &&
prefixEl.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(prefixEl.GetString()))
{
return $"{prefixEl.GetString()}{Random.Shared.Next(100000, 999999)}";
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "读取用户默认昵称配置失败,使用默认值");
}
return $"用户{Random.Shared.Next(100000, 999999)}";
}
/// <summary>
/// 获取用户默认头像(从配置读取,为空则自动生成)
/// </summary>
private async Task<string> GetDefaultAvatarAsync(string seed)
{
try
{
var configJson = await _configService.GetConfigValueAsync("user_config");
if (!string.IsNullOrEmpty(configJson))
{
var config = JsonSerializer.Deserialize<JsonElement>(configJson);
if (config.TryGetProperty("default_avatar", out var avatarEl) &&
avatarEl.ValueKind == JsonValueKind.String &&
!string.IsNullOrWhiteSpace(avatarEl.GetString()))
{
return avatarEl.GetString()!;
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "读取用户默认头像配置失败,使用系统生成");
}
return GenerateDefaultAvatar(seed);
}
/// <summary>
/// <20><><EFBFBD><EFBFBD>Ĭ<EFBFBD><C4AC>ͷ<EFBFBD><CDB7>URL
/// </summary>
private static string GenerateDefaultAvatar(string seed)
{
// ʹ<><CAB9><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>һ<EFBFBD><D2BB><EFBFBD>򵥵<EFBFBD>Ĭ<EFBFBD><C4AC>ͷ<EFBFBD><CDB7>URL
// ʵ<><CAB5><EFBFBD><EFBFBD>Ŀ<EFBFBD>п<EFBFBD><D0BF><EFBFBD>ʹ<EFBFBD><CAB9>Identicon<6F><6E><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ͷ<EFBFBD><CDB7><EFBFBD><EFBFBD><EFBFBD>ɷ<EFBFBD><C9B7><EFBFBD>
var hash = ComputeMd5(seed);
return $"https://api.dicebear.com/7.x/identicon/svg?seed={hash}";
}
/// <summary>
/// <20><><EFBFBD><EFBFBD>MD5<44><35>ϣ
/// </summary>
private static string ComputeMd5(string input)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = MD5.HashData(inputBytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><D6B7><EFBFBD>
/// </summary>
private static string GenerateRandomString(int length)
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
var result = new char[length];
for (int i = 0; i < length; i++)
{
result[i] = chars[Random.Shared.Next(chars.Length)];
}
return new string(result);
}
/// <summary>
/// <20><><EFBFBD><EFBFBD><EFBFBD>ֻ<EFBFBD><D6BB><EFBFBD>
/// </summary>
private static string MaskMobile(string mobile)
{
if (string.IsNullOrWhiteSpace(mobile) || mobile.Length < 7)
return "***";
return $"{mobile.Substring(0, 3)}****{mobile.Substring(mobile.Length - 4)}";
}
/// <summary>
/// <20><>ȡ<EFBFBD><C8A1><EFBFBD><EFBFBD>е<EFBFBD><D0B5><EFBFBD><EFBFBD><EFBFBD>
/// </summary>
private static int GetWeekOfYear(DateTime date)
{
var cal = System.Globalization.CultureInfo.CurrentCulture.Calendar;
return cal.GetWeekOfYear(date, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Monday);
}
/// <summary>
/// <20><><EFBFBD><EFBFBD> SHA256 <20><>ϣֵ
/// Requirements: 4.1
/// </summary>
private static string ComputeSha256Hash(string input)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = SHA256.HashData(inputBytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
/// <summary>
/// <20><><EFBFBD>ɰ<EFBFBD>ȫ<EFBFBD><C8AB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><D6B7><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD> Refresh Token<65><6E>
/// </summary>
private static string GenerateSecureRandomString(int length)
{
var randomBytes = new byte[length];
using var rng = RandomNumberGenerator.Create();
rng.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes)
.Replace("+", "-")
.Replace("/", "_")
.Replace("=", "")
.Substring(0, length);
}
#endregion
}