All checks were successful
continuous-integration/drone/push Build is passing
991 lines
38 KiB
C#
991 lines
38 KiB
C#
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
|
||
}
|