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 ILogger _logger; // Redis key prefixes private const string LoginDebounceKeyPrefix = "login:debounce:"; private const string SmsCodeKeyPrefix = "sms:code:"; private const int DebounceSeconds = 3; public AuthService( HoneyBoxDbContext dbContext, IUserService userService, IJwtService jwtService, IWechatService wechatService, IIpLocationService ipLocationService, IRedisService redisService, 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)); _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 生成JWT Token _logger.LogInformation("[AuthService] 开始生成JWT Token: UserId={UserId}", user.Id); var token = _jwtService.GenerateToken(user); _logger.LogInformation("[AuthService] JWT Token生成成功,长度={Length}", token?.Length ?? 0); // 3.6 同时在数据库UserAccount表中存储account_token用于兼容旧系统 _logger.LogInformation("[AuthService] 更新UserAccount表..."); await CreateOrUpdateAccountTokenAsync(user.Id, token); _logger.LogInformation("[AuthService] UserAccount更新成功"); _logger.LogInformation("[AuthService] 微信登录成功: UserId={UserId}", user.Id); return new LoginResult { Success = true, Token = token, UserId = user.Id }; } 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 生成JWT Token var token = _jwtService.GenerateToken(user); // 3.6 同时在数据库UserAccount表中存储account_token用于兼容旧系统 await CreateOrUpdateAccountTokenAsync(user.Id, token); _logger.LogInformation("Mobile login successful: UserId={UserId}", user.Id); return new LoginResult { Success = true, Token = token, UserId = user.Id }; } 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 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); } #endregion }