mi-assessment/server/MiAssessment/src/MiAssessment.Core/Services/AuthService.cs
zpc d14e96ac97 feat(config): 添加用户默认配置功能(UID、昵称前缀、默认头像)
- UserConfigSetting 模型增加 default_nickname_prefix 和 default_avatar 字段
- Admin ConfigController 新增 user/get 和 user/update 接口
- 后台管理前端新增用户配置 tab 页面
- AuthService 创建用户时从配置读取默认昵称前缀和头像,支持 fallback
2026-02-20 21:25:22 +08:00

957 lines
36 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)
{
_logger.LogInformation("[AuthService] ΢<>ŵ<EFBFBD>¼<EFBFBD><C2BC>ʼ<EFBFBD><CABC>code={Code}, pid={Pid}", code, pid);
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 <20>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><DAA3><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>
_logger.LogInformation("[AuthService] <20>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ڣ<EFBFBD><DAA3><EFBFBD>ʼ<EFBFBD><CABC><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>...");
var createDto = new CreateUserDto
{
OpenId = openId,
UnionId = unionId,
Nickname = await GetDefaultNicknameAsync(),
Headimg = await GetDefaultAvatarAsync(openId),
Pid = pid ?? 0
};
user = await _userService.CreateUserAsync(createDto);
_logger.LogInformation("[AuthService] <20><><EFBFBD>û<EFBFBD><C3BB><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɹ<EFBFBD>: UserId={UserId}, OpenId={OpenId}", user.Id, openId);
}
else
{
// 1.4 <20>û<EFBFBD><C3BB><EFBFBD><EFBFBD>ڣ<EFBFBD><DAA3><EFBFBD><EFBFBD><EFBFBD>unionid<69><64><EFBFBD><EFBFBD><EFBFBD>֮ǰΪ<C7B0>գ<EFBFBD>
if (string.IsNullOrWhiteSpace(user.UnionId) && !string.IsNullOrWhiteSpace(unionId))
{
_logger.LogInformation("[AuthService] <20><><EFBFBD><EFBFBD><EFBFBD>û<EFBFBD>unionid: UserId={UserId}", user.Id);
await _userService.UpdateUserAsync(user.Id, new UpdateUserDto { UnionId = unionId });
_logger.LogInformation("[AuthService] unionid<69><64><EFBFBD>³ɹ<C2B3>");
}
}
// 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
}