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;
///
/// ��֤����ʵ��
///
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 _logger;
// Redis key prefixes
private const string LoginDebounceKeyPrefix = "login:debounce:";
private const string SmsCodeKeyPrefix = "sms:code:";
private const int DebounceSeconds = 3;
// Refresh Token ����
private const int RefreshTokenLength = 64;
public AuthService(
MiAssessmentDbContext dbContext,
IUserService userService,
IJwtService jwtService,
IWechatService wechatService,
IIpLocationService ipLocationService,
IRedisService redisService,
IConfigService configService,
JwtSettings jwtSettings,
ILogger 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));
}
///
/// ��С�����¼
/// Requirements: 1.1-1.8
///
public async Task WechatMiniProgramLoginAsync(string code, int? pid, string? clickId, string? phoneCode = null)
{
_logger.LogInformation("[AuthService] �ŵ�¼��ʼ��code={Code}, pid={Pid}, hasPhoneCode={HasPhoneCode}", code, pid, !string.IsNullOrWhiteSpace(phoneCode));
if (string.IsNullOrWhiteSpace(code))
{
_logger.LogWarning("[AuthService] �ŵ�¼ʧ�ܣ�codeΪ��");
return new LoginResult
{
Success = false,
ErrorMessage = "��Ȩcode����Ϊ��"
};
}
try
{
// 1.6 �������� - 3���ڲ������ظ���¼
var debounceKey = $"{LoginDebounceKeyPrefix}wechat:{code}";
_logger.LogInformation("[AuthService] ��������: {Key}", debounceKey);
var lockAcquired = await _redisService.TryAcquireLockAsync(debounceKey, "1", TimeSpan.FromSeconds(DebounceSeconds));
if (!lockAcquired)
{
_logger.LogWarning("[AuthService] �����������ܾ��ظ���¼����: {Code}", code);
return new LoginResult
{
Success = false,
ErrorMessage = "����Ƶ����¼"
};
}
_logger.LogInformation("[AuthService] ��������ȡ�ɹ�");
// 1.1 ������API��ȡopenid��unionid
_logger.LogInformation("[AuthService] ��ʼ������API��ȡopenid...");
var wechatResult = await _wechatService.GetOpenIdAsync(code);
_logger.LogInformation("[AuthService] ��API������ɣ�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] ��API����ʧ��: {Error}", wechatResult.ErrorMessage);
return new LoginResult
{
Success = false,
ErrorMessage = wechatResult.ErrorMessage ?? "��¼ʧ�ܣ����Ժ�����"
};
}
var openId = wechatResult.OpenId!;
var unionId = wechatResult.UnionId;
// 1.2 �����û� - ����ͨ��unionid���ң����ͨ��openid����
User? user = null;
if (!string.IsNullOrWhiteSpace(unionId))
{
_logger.LogInformation("[AuthService] ����ͨ��unionid�����û�: {UnionId}", unionId);
user = await _userService.GetUserByUnionIdAsync(unionId);
_logger.LogInformation("[AuthService] unionid���ҽ��: {Found}", user != null ? $"�ҵ��û�ID={user.Id}" : "δ�ҵ�");
}
if (user == null)
{
_logger.LogInformation("[AuthService] ����ͨ��openid�����û�: {OpenId}", openId);
user = await _userService.GetUserByOpenIdAsync(openId);
_logger.LogInformation("[AuthService] openid���ҽ��: {Found}", user != null ? $"�ҵ��û�ID={user.Id}" : "δ�ҵ�");
}
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 ����˫ Token��Access Token + Refresh Token��
_logger.LogInformation("[AuthService] ��ʼ����˫ Token: UserId={UserId}", user.Id);
var loginResponse = await GenerateLoginResponseAsync(user, null);
_logger.LogInformation("[AuthService] ˫ Token ���ɳɹ���AccessToken����={Length}", loginResponse.AccessToken?.Length ?? 0);
_logger.LogInformation("[AuthService] �ŵ�¼�ɹ�: UserId={UserId}", user.Id);
return new LoginResult
{
Success = true,
Token = loginResponse.AccessToken, // ���ݾɰ�
UserId = user.Id,
LoginResponse = loginResponse
};
}
catch (Exception ex)
{
_logger.LogError(ex, "[AuthService] �ŵ�¼�쳣: code={Code}, Message={Message}, StackTrace={StackTrace}",
code, ex.Message, ex.StackTrace);
return new LoginResult
{
Success = false,
ErrorMessage = "������ϣ����Ժ�����"
};
}
}
///
/// �ֻ�����֤���¼
/// Requirements: 2.1-2.7
///
public async Task MobileLoginAsync(string mobile, string code, int? pid, string? clickId)
{
if (string.IsNullOrWhiteSpace(mobile))
{
return new LoginResult
{
Success = false,
ErrorMessage = "�ֻ��Ų���Ϊ��"
};
}
if (string.IsNullOrWhiteSpace(code))
{
return new LoginResult
{
Success = false,
ErrorMessage = "��֤�벻��Ϊ��"
};
}
try
{
// 2.6 �������� - 3���ڲ������ظ���¼
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 = "����Ƶ����¼"
};
}
// 2.1 ��Redis��ȡ����֤��֤��
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 = "��֤�����"
};
}
// 2.2 ��֤����֤ͨ����ɾ��Redis�е���֤��
await _redisService.DeleteAsync(smsCodeKey);
// �����û�
var user = await _userService.GetUserByMobileAsync(mobile);
if (user == null)
{
// 2.3 �û������ڣ��������û�
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 ����˫ Token��Access Token + Refresh Token��
var loginResponse = await GenerateLoginResponseAsync(user, null);
_logger.LogInformation("Mobile login successful: UserId={UserId}", user.Id);
return new LoginResult
{
Success = true,
Token = loginResponse.AccessToken, // ���ݾɰ�
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 = "������ϣ����Ժ�����"
};
}
}
///
/// ��֤����ֻ���
/// Requirements: 5.1-5.5
///
public async Task BindMobileAsync(long userId, string mobile, string code)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("�ֻ��Ų���Ϊ��", nameof(mobile));
}
if (string.IsNullOrWhiteSpace(code))
{
throw new ArgumentException("��֤�벻��Ϊ��", nameof(code));
}
// 5.1 ��֤������֤��
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("��֤�����");
}
// ��֤����֤ͨ����ɾ��
await _redisService.DeleteAsync(smsCodeKey);
// ��ȡ��ǰ�û�
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("�û�������");
}
// ����ֻ����Ƿ��ѱ������û���
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 �ֻ����ѱ������û�����Ҫ�ϲ��˻�
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 �ֻ���δ����ֱ�Ӹ��µ�ǰ�û����ֻ���
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 };
}
///
/// ����Ȩ���ֻ���
/// Requirements: 5.1-5.5
///
public async Task WechatBindMobileAsync(long userId, string wechatCode)
{
if (string.IsNullOrWhiteSpace(wechatCode))
{
throw new ArgumentException("����Ȩcode����Ϊ��", nameof(wechatCode));
}
// ������API��ȡ�ֻ���
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 ?? "��ȡ�ֻ���ʧ��");
}
var mobile = mobileResult.Mobile;
// ��ȡ��ǰ�û�
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("�û�������");
}
// ����ֻ����Ƿ��ѱ������û���
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// 5.2 �ֻ����ѱ������û�����Ҫ�ϲ��˻�
return await MergeAccountsAsync(currentUser, existingUser);
}
// 5.4 �ֻ���δ����ֱ�Ӹ��µ�ǰ�û����ֻ���
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 };
}
///
/// ��¼��¼��Ϣ
/// Requirements: 6.1, 6.3, 6.4
///
public async Task RecordLoginAsync(long userId, string? device, string? deviceInfo)
{
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
throw new InvalidOperationException("�û�������");
}
try
{
// ��ȡ�ͻ���IP������ʹ�ÿ��ַ�����Ϊռλ����ʵ��IPӦ��Controller���룩
var clientIp = deviceInfo ?? string.Empty;
var now = DateTime.Now;
// 6.1 ��¼��¼��־
var loginLog = new UserLoginLog
{
UserId = userId,
LoginType = "wechat",
LoginIp = clientIp,
UserAgent = device,
Platform = "miniprogram",
Status = 1,
CreateTime = now
};
await _dbContext.UserLoginLogs.AddAsync(loginLog);
// �����û�����¼ʱ��
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 �����û���uid���dzƺ�ͷ��
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;
}
}
///
/// H5���ֻ��ţ�������֤�룩
/// Requirements: 13.1
///
public async Task BindMobileH5Async(long userId, string mobile)
{
if (string.IsNullOrWhiteSpace(mobile))
{
throw new ArgumentException("�ֻ��Ų���Ϊ��", nameof(mobile));
}
// ��ȡ��ǰ�û�
var currentUser = await _userService.GetUserByIdAsync(userId);
if (currentUser == null)
{
throw new InvalidOperationException("�û�������");
}
// ����ֻ����Ƿ��ѱ������û���
var existingUser = await _userService.GetUserByMobileAsync(mobile);
if (existingUser != null && existingUser.Id != userId)
{
// �ֻ����ѱ������û�����Ҫ�ϲ��˻�
return await MergeAccountsAsync(currentUser, existingUser);
}
// �ֻ���δ����ֱ�Ӹ��µ�ǰ�û����ֻ���
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 };
}
///
/// �˺�ע��
/// Requirements: 7.1-7.3
///
public async Task LogOffAsync(long userId, int type)
{
var user = await _userService.GetUserByIdAsync(userId);
if (user == null)
{
throw new InvalidOperationException("�û�������");
}
try
{
// 7.1 ��¼ע��������־
var action = type == 0 ? "ע���˺�" : "ȡ��ע��";
_logger.LogInformation("User log off request: UserId={UserId}, Type={Type}, Action={Action}", userId, type, action);
// ����������Ӹ����ע���������磺
// - ���û�״̬����Ϊ��ע��
// - �����û���صĻ���
// - ����֪ͨ��
if (type == 0)
{
// ע���˺� - ���������û�״̬Ϊ����
user.Status = 0;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("User account deactivated: UserId={UserId}", userId);
}
else if (type == 1)
{
// ȡ��ע�� - �ָ��û�״̬
user.Status = 1;
_dbContext.Users.Update(user);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("User account reactivated: UserId={UserId}", userId);
}
// 7.2 ����ע���ɹ�����Ϣ��ͨ�����׳��쳣����ʾ�ɹ���
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to process log off: UserId={UserId}, Type={Type}", userId, type);
throw;
}
}
#region Refresh Token Methods
///
/// ���� Refresh Token ���洢�����ݿ�
/// Requirements: 1.4, 1.5, 4.1
///
/// �û�ID
/// �ͻ��� IP ��ַ
/// ���ɵ� Refresh Token ����
private async Task GenerateRefreshTokenAsync(long userId, string? ipAddress)
{
// ������� Refresh Token
var refreshToken = GenerateSecureRandomString(RefreshTokenLength);
// ���� SHA256 ��ϣֵ���ڴ洢
var tokenHash = ComputeSha256Hash(refreshToken);
// �������ʱ�䣨7�죩
var expiresAt = DateTime.Now.AddDays(_jwtSettings.RefreshTokenExpirationDays);
// �������ݿ��¼
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;
}
///
/// ���ɵ�¼��Ӧ������˫ Token��
/// Requirements: 1.1, 1.2, 1.3, 1.4, 1.5
///
/// �û�ʵ��
/// �ͻ��� IP ��ַ
/// ��¼��Ӧ
private async Task GenerateLoginResponseAsync(User user, string? ipAddress)
{
// ���� Access Token (JWT)
var accessToken = _jwtService.GenerateToken(user);
// ���� Refresh Token ���洢
var refreshToken = await GenerateRefreshTokenAsync(user.Id, ipAddress);
// ���� Access Token ����ʱ�䣨�룩
var expiresIn = _jwtSettings.ExpirationMinutes * 60;
return new LoginResponse
{
AccessToken = accessToken,
RefreshToken = refreshToken,
ExpiresIn = expiresIn,
UserId = user.Id
};
}
///
/// ˢ�� Token
/// Requirements: 2.1-2.6
///
public async Task RefreshTokenAsync(string refreshToken, string? ipAddress)
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
_logger.LogWarning("Refresh token is empty");
return RefreshTokenResult.Fail("ˢ�����Ʋ���Ϊ��");
}
try
{
// ���� Token ��ϣֵ
var tokenHash = ComputeSha256Hash(refreshToken);
// ���� Token ��¼
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("��Ч��ˢ������");
}
// ����Ƿ��ѹ���
if (storedToken.IsExpired)
{
_logger.LogWarning("Refresh token expired for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail("ˢ�������ѹ���");
}
// ����Ƿ��ѳ���
if (storedToken.IsRevoked)
{
_logger.LogWarning("Refresh token revoked for user {UserId}", storedToken.UserId);
return RefreshTokenResult.Fail("ˢ��������ʧЧ");
}
// ����û��Ƿ��������Ч
var user = storedToken.User;
if (user == null)
{
_logger.LogWarning("User not found for refresh token");
return RefreshTokenResult.Fail("�û�������");
}
if (user.Status == 0)
{
_logger.LogWarning("User {UserId} is disabled", user.Id);
return RefreshTokenResult.Fail("�˺��ѱ�����");
}
// Token �ֻ��������µ� Refresh Token
var newRefreshToken = GenerateSecureRandomString(RefreshTokenLength);
var newTokenHash = ComputeSha256Hash(newRefreshToken);
// ������ Token ����¼������ϵ
storedToken.RevokedAt = DateTime.Now;
storedToken.RevokedByIp = ipAddress;
storedToken.ReplacedByToken = newTokenHash;
// �����µ� Token ��¼
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();
// �����µ� 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("ˢ������ʧ�ܣ����Ժ�����");
}
}
///
/// ���� Token
/// Requirements: 4.4
///
public async Task RevokeTokenAsync(string refreshToken, string? ipAddress)
{
if (string.IsNullOrWhiteSpace(refreshToken))
{
_logger.LogWarning("Cannot revoke empty refresh token");
return;
}
try
{
// ���� Token ��ϣֵ
var tokenHash = ComputeSha256Hash(refreshToken);
// ���� Token ��¼
var storedToken = await _dbContext.UserRefreshTokens
.FirstOrDefaultAsync(t => t.TokenHash == tokenHash);
if (storedToken == null)
{
_logger.LogWarning("Refresh token not found for revocation");
return;
}
// ����Ѿ�������ֱ�ӷ���
if (storedToken.IsRevoked)
{
_logger.LogInformation("Refresh token already revoked");
return;
}
// ���� 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;
}
}
///
/// �����û������� Token
/// Requirements: 4.4
///
public async Task RevokeAllUserTokensAsync(long userId, string? ipAddress)
{
try
{
// �����û�������Ч�� 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
///
/// �ϲ��˻� - ����ǰ�û���openidǨ�Ƶ��ֻ����û�
///
private async Task 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 ����ǰ�û���openidǨ�Ƶ��ֻ����û�
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);
// ɾ����ǰ�û�
_dbContext.Users.Remove(currentUser);
await _dbContext.SaveChangesAsync();
await transaction.CommitAsync();
// 5.3 �����µ�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;
}
}
///
/// 获取用户默认昵称(从配置读取前缀)
///
private async Task GetDefaultNicknameAsync()
{
try
{
var configJson = await _configService.GetConfigValueAsync("user_config");
if (!string.IsNullOrEmpty(configJson))
{
var config = JsonSerializer.Deserialize(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)}";
}
///
/// 获取用户默认头像(从配置读取,为空则自动生成)
///
private async Task GetDefaultAvatarAsync(string seed)
{
try
{
var configJson = await _configService.GetConfigValueAsync("user_config");
if (!string.IsNullOrEmpty(configJson))
{
var config = JsonSerializer.Deserialize(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);
}
///
/// ����Ĭ��ͷ��URL
///
private static string GenerateDefaultAvatar(string seed)
{
// ʹ����������һ����Ĭ��ͷ��URL
// ʵ����Ŀ�п���ʹ��Identicon�������ͷ�����ɷ���
var hash = ComputeMd5(seed);
return $"https://api.dicebear.com/7.x/identicon/svg?seed={hash}";
}
///
/// ����MD5��ϣ
///
private static string ComputeMd5(string input)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = MD5.HashData(inputBytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
///
/// ��������ַ���
///
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);
}
///
/// �����ֻ���
///
private static string MaskMobile(string mobile)
{
if (string.IsNullOrWhiteSpace(mobile) || mobile.Length < 7)
return "***";
return $"{mobile.Substring(0, 3)}****{mobile.Substring(mobile.Length - 4)}";
}
///
/// ��ȡ����е�����
///
private static int GetWeekOfYear(DateTime date)
{
var cal = System.Globalization.CultureInfo.CurrentCulture.Calendar;
return cal.GetWeekOfYear(date, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Monday);
}
///
/// ���� SHA256 ��ϣֵ
/// Requirements: 4.1
///
private static string ComputeSha256Hash(string input)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = SHA256.HashData(inputBytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
///
/// ���ɰ�ȫ������ַ��������� Refresh Token��
///
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
}