HaniBlindBox/server/HoneyBox/src/HoneyBox.Core/Services/AuthService.cs
2026-01-20 20:35:32 +08:00

707 lines
26 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Security.Cryptography;
using System.Text;
using 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;
/// <summary>
/// 认证服务实现
/// </summary>
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<AuthService> _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<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));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <summary>
/// 微信小程序登录
/// Requirements: 1.1-1.8
/// </summary>
public async Task<LoginResult> 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 = "网络故障,请稍后再试"
};
}
}
/// <summary>
/// 手机号验证码登录
/// 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 = "手机号不能为空"
};
}
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 = "网络故障,请稍后再试"
};
}
}
/// <summary>
/// 验证码绑定手机号
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> 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 };
}
/// <summary>
/// 微信授权绑定手机号
/// Requirements: 5.1-5.5
/// </summary>
public async Task<BindMobileResponse> 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 };
}
/// <summary>
/// 记录登录信息
/// Requirements: 6.1, 6.3, 6.4
/// </summary>
public async Task<RecordLoginResponse> 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;
}
}
/// <summary>
/// H5绑定手机号无需验证码
/// Requirements: 13.1
/// </summary>
public async Task<BindMobileResponse> 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 };
}
/// <summary>
/// 账号注销
/// Requirements: 7.1-7.3
/// </summary>
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
/// <summary>
/// 合并账户 - 将当前用户的openid迁移到手机号用户
/// </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 将当前用户的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;
}
}
/// <summary>
/// 创建或更新账户Token用于兼容旧系统
/// </summary>
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();
}
/// <summary>
/// 生成默认头像URL
/// </summary>
private static string GenerateDefaultAvatar(string seed)
{
// 使用种子生成一个简单的默认头像URL
// 实际项目中可以使用Identicon库或其他头像生成服务
var hash = ComputeMd5(seed);
return $"https://api.dicebear.com/7.x/identicon/svg?seed={hash}";
}
/// <summary>
/// 计算MD5哈希
/// </summary>
private static string ComputeMd5(string input)
{
var inputBytes = Encoding.UTF8.GetBytes(input);
var hashBytes = MD5.HashData(inputBytes);
return Convert.ToHexString(hashBytes).ToLowerInvariant();
}
/// <summary>
/// 生成随机字符串
/// </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>
/// 解析ClickId
/// </summary>
private static int? ParseClickId(string? clickId)
{
if (string.IsNullOrWhiteSpace(clickId))
return null;
return int.TryParse(clickId, out var result) ? result : null;
}
/// <summary>
/// 脱敏手机号
/// </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>
/// 获取年份中的周数
/// </summary>
private static int GetWeekOfYear(DateTime date)
{
var cal = System.Globalization.CultureInfo.CurrentCulture.Calendar;
return cal.GetWeekOfYear(date, System.Globalization.CalendarWeekRule.FirstDay, DayOfWeek.Monday);
}
#endregion
}