232 lines
7.7 KiB
C#
232 lines
7.7 KiB
C#
using System.IdentityModel.Tokens.Jwt;
|
||
using System.Security.Claims;
|
||
using System.Text;
|
||
using Microsoft.Extensions.Logging;
|
||
using Microsoft.Extensions.Options;
|
||
using Microsoft.IdentityModel.Tokens;
|
||
using XiangYi.Application.DTOs.Responses;
|
||
using XiangYi.Application.Interfaces;
|
||
using XiangYi.Application.Options;
|
||
using XiangYi.Core.Constants;
|
||
using XiangYi.Core.Entities.Biz;
|
||
using XiangYi.Core.Exceptions;
|
||
using XiangYi.Core.Interfaces;
|
||
using XiangYi.Infrastructure.Cache;
|
||
using XiangYi.Infrastructure.WeChat;
|
||
|
||
namespace XiangYi.Application.Services;
|
||
|
||
/// <summary>
|
||
/// 认证服务实现
|
||
/// </summary>
|
||
public class AuthService : IAuthService
|
||
{
|
||
private readonly IRepository<User> _userRepository;
|
||
private readonly IWeChatService _weChatService;
|
||
private readonly ICacheService _cacheService;
|
||
private readonly ISystemConfigService _systemConfigService;
|
||
private readonly JwtOptions _jwtOptions;
|
||
private readonly ILogger<AuthService> _logger;
|
||
private static readonly Random _random = new();
|
||
|
||
public AuthService(
|
||
IRepository<User> userRepository,
|
||
IWeChatService weChatService,
|
||
ICacheService cacheService,
|
||
ISystemConfigService systemConfigService,
|
||
IOptions<JwtOptions> jwtOptions,
|
||
ILogger<AuthService> logger)
|
||
{
|
||
_userRepository = userRepository;
|
||
_weChatService = weChatService;
|
||
_cacheService = cacheService;
|
||
_systemConfigService = systemConfigService;
|
||
_jwtOptions = jwtOptions.Value;
|
||
_logger = logger;
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<LoginResponse> LoginAsync(string code)
|
||
{
|
||
// 1. 调用微信接口获取openid
|
||
var weChatResult = await _weChatService.Code2SessionAsync(code);
|
||
if (!weChatResult.Success || string.IsNullOrEmpty(weChatResult.OpenId))
|
||
{
|
||
_logger.LogWarning("微信登录失败: {ErrorCode} - {ErrorMessage}",
|
||
weChatResult.ErrorCode, weChatResult.ErrorMessage);
|
||
throw new BusinessException(ErrorCodes.WeChatLoginFailed,
|
||
weChatResult.ErrorMessage ?? "微信登录失败");
|
||
}
|
||
|
||
// 2. 查找或创建用户
|
||
var user = await _userRepository.GetListAsync(u => u.OpenId == weChatResult.OpenId);
|
||
var existingUser = user.FirstOrDefault();
|
||
bool isNewUser = false;
|
||
|
||
if (existingUser == null)
|
||
{
|
||
// 新用户,创建用户记录
|
||
isNewUser = true;
|
||
var xiangQinNo = await GenerateXiangQinNoAsync();
|
||
|
||
// 获取默认头像
|
||
var defaultAvatar = await _systemConfigService.GetDefaultAvatarAsync();
|
||
|
||
existingUser = new User
|
||
{
|
||
OpenId = weChatResult.OpenId,
|
||
UnionId = weChatResult.UnionId,
|
||
XiangQinNo = xiangQinNo,
|
||
Avatar = defaultAvatar,
|
||
Status = 1,
|
||
ContactCount = 2,
|
||
CreateTime = DateTime.Now,
|
||
UpdateTime = DateTime.Now
|
||
};
|
||
|
||
existingUser = await _userRepository.AddAsync(existingUser);
|
||
_logger.LogInformation("新用户注册成功: UserId={UserId}, XiangQinNo={XiangQinNo}",
|
||
existingUser.Id, xiangQinNo);
|
||
}
|
||
else
|
||
{
|
||
// 更新最后登录时间
|
||
existingUser.LastLoginTime = DateTime.Now;
|
||
existingUser.UpdateTime = DateTime.Now;
|
||
await _userRepository.UpdateAsync(existingUser);
|
||
}
|
||
|
||
// 3. 生成JWT Token
|
||
var token = GenerateToken(existingUser.Id, existingUser.OpenId);
|
||
|
||
// 4. 缓存Token
|
||
await _cacheService.SetUserTokenAsync(existingUser.Id, token);
|
||
|
||
_logger.LogInformation("用户登录成功: UserId={UserId}", existingUser.Id);
|
||
|
||
return new LoginResponse
|
||
{
|
||
Token = token,
|
||
UserId = existingUser.Id,
|
||
Nickname = existingUser.Nickname,
|
||
Avatar = existingUser.Avatar,
|
||
XiangQinNo = existingUser.XiangQinNo,
|
||
IsProfileCompleted = existingUser.IsProfileCompleted,
|
||
IsMember = existingUser.IsMember,
|
||
MemberLevel = existingUser.MemberLevel,
|
||
IsRealName = existingUser.IsRealName,
|
||
IsNewUser = isNewUser
|
||
};
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<BindPhoneResponse> BindPhoneAsync(long userId, string code)
|
||
{
|
||
// 1. 获取用户
|
||
var user = await _userRepository.GetByIdAsync(userId);
|
||
if (user == null)
|
||
{
|
||
throw new BusinessException(ErrorCodes.UserNotFound, "用户不存在");
|
||
}
|
||
|
||
// 2. 调用微信接口获取手机号
|
||
var phone = await _weChatService.GetPhoneNumberAsync(code);
|
||
if (string.IsNullOrEmpty(phone))
|
||
{
|
||
throw new BusinessException(ErrorCodes.WeChatServiceError, "获取手机号失败");
|
||
}
|
||
|
||
// 3. 更新用户手机号
|
||
user.Phone = phone;
|
||
user.UpdateTime = DateTime.Now;
|
||
await _userRepository.UpdateAsync(user);
|
||
|
||
_logger.LogInformation("用户绑定手机号成功: UserId={UserId}", userId);
|
||
|
||
// 4. 返回脱敏后的手机号
|
||
return new BindPhoneResponse
|
||
{
|
||
Phone = MaskPhone(phone)
|
||
};
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public string GenerateToken(long userId, string openId)
|
||
{
|
||
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtOptions.Secret));
|
||
var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
|
||
|
||
var claims = new[]
|
||
{
|
||
new Claim(ClaimTypes.NameIdentifier, userId.ToString()),
|
||
new Claim("openid", openId),
|
||
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
|
||
new Claim(JwtRegisteredClaimNames.Iat, DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(), ClaimValueTypes.Integer64)
|
||
};
|
||
|
||
var token = new JwtSecurityToken(
|
||
issuer: _jwtOptions.Issuer,
|
||
audience: _jwtOptions.Audience,
|
||
claims: claims,
|
||
expires: DateTime.UtcNow.AddMinutes(_jwtOptions.ExpireMinutes),
|
||
signingCredentials: credentials
|
||
);
|
||
|
||
return new JwtSecurityTokenHandler().WriteToken(token);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<string> GenerateXiangQinNoAsync()
|
||
{
|
||
const int maxAttempts = 100;
|
||
|
||
for (int i = 0; i < maxAttempts; i++)
|
||
{
|
||
var xiangQinNo = GenerateXiangQinNo();
|
||
|
||
// 检查是否已存在
|
||
var exists = await _userRepository.ExistsAsync(u => u.XiangQinNo == xiangQinNo);
|
||
if (!exists)
|
||
{
|
||
return xiangQinNo;
|
||
}
|
||
}
|
||
|
||
// 如果多次尝试都失败,抛出异常
|
||
throw new BusinessException(ErrorCodes.SystemError, "生成相亲编号失败,请重试");
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<bool> ValidateTokenAsync(long userId, string token)
|
||
{
|
||
return await _cacheService.ValidateUserTokenAsync(userId, token);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 生成6位相亲编号(首位不为0)
|
||
/// </summary>
|
||
/// <returns>相亲编号</returns>
|
||
public static string GenerateXiangQinNo()
|
||
{
|
||
// 首位:1-9
|
||
var firstDigit = _random.Next(1, 10);
|
||
// 后5位:0-9
|
||
var remainingDigits = _random.Next(0, 100000);
|
||
|
||
return $"{firstDigit}{remainingDigits:D5}";
|
||
}
|
||
|
||
/// <summary>
|
||
/// 手机号脱敏
|
||
/// </summary>
|
||
private static string MaskPhone(string phone)
|
||
{
|
||
if (string.IsNullOrEmpty(phone) || phone.Length < 7)
|
||
{
|
||
return phone;
|
||
}
|
||
|
||
return $"{phone[..3]}****{phone[^4..]}";
|
||
}
|
||
}
|