using System.Security.Cryptography; using System.Text; using HoneyBox.Core.Interfaces; using HoneyBox.Model.Data; using HoneyBox.Model.Entities; using HoneyBox.Model.Models.Auth; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; namespace HoneyBox.Core.Services; /// /// 认证服务实现 /// public class AuthService : IAuthService { private readonly HoneyBoxDbContext _dbContext; private readonly IUserService _userService; private readonly IJwtService _jwtService; private readonly IWechatService _wechatService; private readonly IIpLocationService _ipLocationService; private readonly IRedisService _redisService; 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( HoneyBoxDbContext dbContext, IUserService userService, IJwtService jwtService, IWechatService wechatService, IIpLocationService ipLocationService, IRedisService redisService, 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)); _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) { _logger.LogInformation("[AuthService] 微信登录开始,code={Code}, pid={Pid}, clickId={ClickId}", code, pid, clickId); 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] 用户不存在,开始创建新用户..."); var createDto = new CreateUserDto { OpenId = openId, UnionId = unionId, Nickname = $"用户{Random.Shared.Next(100000, 999999)}", Headimg = GenerateDefaultAvatar(openId), Pid = pid ?? 0, ClickId = ParseClickId(clickId) }; user = await _userService.CreateUserAsync(createDto); _logger.LogInformation("[AuthService] 新用户创建成功: UserId={UserId}, OpenId={OpenId}", user.Id, openId); } else { // 1.4 用户存在,更新unionid(如果之前为空) 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更新成功"); } } // 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); // 3.6 同时在数据库UserAccount表中存储account_token用于兼容旧系统 _logger.LogInformation("[AuthService] 更新UserAccount表..."); await CreateOrUpdateAccountTokenAsync(user.Id, loginResponse.AccessToken); _logger.LogInformation("[AuthService] UserAccount更新成功"); _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 = $"用户{Random.Shared.Next(100000, 999999)}", Headimg = GenerateDefaultAvatar(mobile), Pid = pid ?? 0, ClickId = ParseClickId(clickId) }; 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); // 3.6 同时在数据库UserAccount表中存储account_token用于兼容旧系统 await CreateOrUpdateAccountTokenAsync(user.Id, loginResponse.AccessToken); _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(int 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(int 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(int 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; // 6.2 解析IP地址获取地理位置 IpLocationResult? locationResult = null; if (!string.IsNullOrWhiteSpace(clientIp)) { locationResult = await _ipLocationService.GetLocationAsync(clientIp); } var now = DateTime.UtcNow; var today = DateOnly.FromDateTime(now); // 6.1 记录登录日志 var loginLog = new UserLoginLog { UserId = userId, LoginDate = today, LoginTime = now, LastLoginTime = now, Device = device, Ip = clientIp, Location = locationResult?.Success == true ? $"{locationResult.Province}{locationResult.City}" : null, Year = now.Year, Month = now.Month, Week = GetWeekOfYear(now) }; await _dbContext.UserLoginLogs.AddAsync(loginLog); // 6.3 更新UserAccount表中的最后登录时间和IP信息 var userAccount = await _dbContext.UserAccounts.FirstOrDefaultAsync(ua => ua.UserId == userId); if (userAccount != null) { userAccount.LastLoginTime = now; userAccount.LastLoginIp = clientIp; if (locationResult?.Success == true) { userAccount.IpProvince = locationResult.Province; userAccount.IpCity = locationResult.City; userAccount.IpAdcode = locationResult.Adcode; } _dbContext.UserAccounts.Update(userAccount); } // 更新用户最后登录时间 user.LastLoginTime = now; _dbContext.Users.Update(user); await _dbContext.SaveChangesAsync(); _logger.LogInformation("Login recorded: UserId={UserId}, Device={Device}, IP={IP}", userId, device, clientIp); // 6.4 返回用户的uid、昵称和头像 return new RecordLoginResponse { Uid = user.Uid, Nickname = user.Nickname, Headimg = user.HeadImg }; } catch (Exception ex) { _logger.LogError(ex, "Failed to record login: UserId={UserId}", userId); throw; } } /// /// H5绑定手机号(无需验证码) /// Requirements: 13.1 /// public async Task BindMobileH5Async(int 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(int 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(int 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; // 更新 UserAccount 表中的 token(兼容旧系统) await CreateOrUpdateAccountTokenAsync(user.Id, accessToken); _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(int 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.UpdatedAt = DateTime.UtcNow; _dbContext.Users.Update(mobileUser); // 删除当前用户的账户记录 var currentUserAccount = await _dbContext.UserAccounts.FirstOrDefaultAsync(ua => ua.UserId == currentUser.Id); if (currentUserAccount != null) { _dbContext.UserAccounts.Remove(currentUserAccount); } // 删除当前用户 _dbContext.Users.Remove(currentUser); await _dbContext.SaveChangesAsync(); await transaction.CommitAsync(); // 5.3 生成新的token var newToken = _jwtService.GenerateToken(mobileUser); await CreateOrUpdateAccountTokenAsync(mobileUser.Id, newToken); _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; } } /// /// 创建或更新账户Token(用于兼容旧系统) /// private async Task CreateOrUpdateAccountTokenAsync(int userId, string jwtToken) { var tokenNum = GenerateRandomString(10); var time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var accountToken = ComputeMd5($"{userId}{tokenNum}{time}"); var userAccount = await _dbContext.UserAccounts.FirstOrDefaultAsync(ua => ua.UserId == userId); if (userAccount == null) { userAccount = new UserAccount { UserId = userId, AccountToken = accountToken, TokenNum = tokenNum, TokenTime = DateTime.UtcNow, LastLoginTime = DateTime.UtcNow, LastLoginIp = string.Empty }; await _dbContext.UserAccounts.AddAsync(userAccount); } else { userAccount.AccountToken = accountToken; userAccount.TokenNum = tokenNum; userAccount.TokenTime = DateTime.UtcNow; userAccount.LastLoginTime = DateTime.UtcNow; _dbContext.UserAccounts.Update(userAccount); } await _dbContext.SaveChangesAsync(); } /// /// 生成默认头像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); } /// /// 解析ClickId /// private static int? ParseClickId(string? clickId) { if (string.IsNullOrWhiteSpace(clickId)) return null; return int.TryParse(clickId, out var result) ? result : null; } /// /// 脱敏手机号 /// 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 }