All checks were successful
continuous-integration/drone/push Build is passing
- Add v1.2.0 complete database upgrade scripts for business and admin databases - Enhance WeChat login logic to handle OpenId reassociation when user logs in via phone number - Add UpdatedAt timestamp update in WeChat login record updates - Configure Docker container permissions for AgileConfig cache file writes - Add Caddy image tagging to CI-CD deployment documentation - Update frontend Config.js for deployment configuration management
778 lines
32 KiB
C#
778 lines
32 KiB
C#
using FreeSql;
|
||
|
||
using LiveForum.Code.ExceptionExtend;
|
||
using LiveForum.Code.JwtInfrastructure;
|
||
using LiveForum.Code.JwtInfrastructure.Interface;
|
||
using LiveForum.Code.Utility;
|
||
using LiveForum.IService.Auth;
|
||
using LiveForum.IService.Users;
|
||
using LiveForum.Model;
|
||
using LiveForum.Model.Dto.Others;
|
||
using LiveForum.Model.Enum;
|
||
using LiveForum.Model.Enum.Users;
|
||
|
||
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.Extensions.Options;
|
||
|
||
using System.Security.Claims;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
|
||
using SystemConvert = System.Convert;
|
||
|
||
namespace LiveForum.Service.Auth
|
||
{
|
||
/// <summary>
|
||
/// 登录服务
|
||
/// </summary>
|
||
public class LoginService : ILoginService
|
||
{
|
||
private readonly IJwtAuthManager _jwtAuthManager;
|
||
private readonly IBaseRepository<T_Users> _userRepository;
|
||
private readonly IBaseRepository<T_UserTokens> _userTokensRepository;
|
||
private readonly IBaseRepository<T_WechatMiniProgramLogins> _wechatMiniProgramLoginsRepository;
|
||
private readonly IBaseRepository<T_AccountPasswordLogins> _accountPasswordLoginsRepository;
|
||
private readonly JwtUserInfoModel _userInfoModel;
|
||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||
private readonly IOptionsSnapshot<AppSettings> _appSettingsSnapshot;
|
||
private readonly IUserInfoService _userInfoService;
|
||
private readonly IBaseRepository<T_IdentityGroups> _identityGroupsRepository;
|
||
private readonly IBaseRepository<T_UserIdentityGroups> _userIdentityGroupsRepository;
|
||
|
||
/// <summary>
|
||
/// 注意,要先创建私有对象,在构造函数中进行赋值
|
||
/// </summary>
|
||
/// <param name="jwtAuthManager">jwt管理对象</param>
|
||
/// <param name="userRepository">用户仓储类</param>
|
||
/// <param name="userTokensRepository">用户token仓储类</param>
|
||
/// <param name="wechatMiniProgramLoginsRepository">微信小程序登录仓储类</param>
|
||
/// <param name="accountPasswordLoginsRepository">账号密码登录仓储类</param>
|
||
/// <param name="userInfoModel">在验证jwt的时候进行</param>
|
||
/// <param name="httpContextAccessor">HTTP上下文访问器</param>
|
||
/// <param name="appSettingsSnapshot">应用配置快照(支持配置热更新)</param>
|
||
/// <param name="userInfoService">用户信息服务</param>
|
||
/// <param name="identityGroupsRepository">身份组仓储类</param>
|
||
/// <param name="userIdentityGroupsRepository">用户身份组关联仓储类</param>
|
||
public LoginService(IJwtAuthManager jwtAuthManager,
|
||
IBaseRepository<T_Users> userRepository,
|
||
IBaseRepository<T_UserTokens> userTokensRepository,
|
||
IBaseRepository<T_WechatMiniProgramLogins> wechatMiniProgramLoginsRepository,
|
||
IBaseRepository<T_AccountPasswordLogins> accountPasswordLoginsRepository,
|
||
JwtUserInfoModel userInfoModel,
|
||
IHttpContextAccessor httpContextAccessor,
|
||
IOptionsSnapshot<AppSettings> appSettingsSnapshot,
|
||
IUserInfoService userInfoService,
|
||
IBaseRepository<T_IdentityGroups> identityGroupsRepository,
|
||
IBaseRepository<T_UserIdentityGroups> userIdentityGroupsRepository)
|
||
{
|
||
_jwtAuthManager = jwtAuthManager;
|
||
_userRepository = userRepository;
|
||
_userTokensRepository = userTokensRepository;
|
||
_wechatMiniProgramLoginsRepository = wechatMiniProgramLoginsRepository;
|
||
_accountPasswordLoginsRepository = accountPasswordLoginsRepository;
|
||
_userInfoModel = userInfoModel;
|
||
_httpContextAccessor = httpContextAccessor;
|
||
_appSettingsSnapshot = appSettingsSnapshot;
|
||
_userInfoService = userInfoService;
|
||
_identityGroupsRepository = identityGroupsRepository;
|
||
_userIdentityGroupsRepository = userIdentityGroupsRepository;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 微信小程序登录
|
||
/// 实现步奏:
|
||
/// 1. 根据 openId 去查询T_WechatMiniProgramLogins用户是否存在
|
||
/// 2. 如果存在,获取对应的用户信息,生成JwtAccessToken返回(生成的jwt要看一下JwtUserInfoModel实体是否能满足)
|
||
/// 3. 如果不存在,创建用户,并保存T_WechatMiniProgramLogins记录,然后生成JwtAccessToken返回
|
||
/// 4. 将生成的token保存到T_UserTokens中
|
||
/// 5. 返回JwtAccessToken
|
||
/// </summary>
|
||
/// <param name="openId"></param>
|
||
/// <param name="sessionKey"></param>
|
||
/// <param name="unionId"></param>
|
||
/// <returns></returns>
|
||
/// <exception cref="NotImplementedException"></exception>
|
||
public async Task<JwtAccessToken> WechatMpLogin(string openId, string sessionKey = "", string unionId = "")
|
||
{
|
||
if (string.IsNullOrEmpty(openId))
|
||
{
|
||
throw new ArgumentNullException(nameof(openId), "openId 不能为空");
|
||
}
|
||
|
||
var now = DateTime.Now;
|
||
var clientIp = _httpContextAccessor.GetClientIpAddress();
|
||
T_WechatMiniProgramLogins wechatLogin;
|
||
T_Users user;
|
||
|
||
// 1. 根据 openId 查询微信小程序登录记录
|
||
wechatLogin = await _wechatMiniProgramLoginsRepository.Select
|
||
.Where(x => x.OpenId == openId)
|
||
.FirstAsync();
|
||
|
||
if (wechatLogin != null)
|
||
{
|
||
// 2. 如果存在,获取对应的用户信息
|
||
user = await _userRepository.Select
|
||
.Where(x => x.Id == wechatLogin.UserId)
|
||
.FirstAsync();
|
||
|
||
if (user == null)
|
||
{
|
||
// 用户已被删除(如管理后台删除),重新创建用户
|
||
var appSettings1 = _appSettingsSnapshot.Value;
|
||
var defaultNickName1 = string.IsNullOrEmpty(appSettings1.UserDefaultName)
|
||
? "用户"
|
||
: appSettings1.UserDefaultName;
|
||
var defaultAvatar1 = string.IsNullOrEmpty(appSettings1.UserDefaultIcon)
|
||
? ""
|
||
: appSettings1.UserDefaultIcon;
|
||
|
||
user = new T_Users
|
||
{
|
||
NickName = $"{defaultNickName1}_{openId.Substring(0, 8)}",
|
||
Avatar = defaultAvatar1,
|
||
Experience = 0,
|
||
LevelId = 1,
|
||
Status = UserStatusEnum.Normal,
|
||
CertifiedType = 0, // 0表示未认证
|
||
CertifiedStatus = CertifiedStatusEnum.未认证,
|
||
IsCertified = false,
|
||
IsVip = false,
|
||
UID = await _userInfoService.GenerateUniqueUIDAsync(),
|
||
CreatedAt = now,
|
||
UpdatedAt = now,
|
||
RegisterIp = clientIp,
|
||
LastLoginIp = clientIp,
|
||
LastLoginTime = now
|
||
};
|
||
|
||
await _userRepository.InsertAsync(user);
|
||
|
||
// 新用户自动关联默认身份组
|
||
await AssignDefaultIdentityGroupAsync(user.Id);
|
||
|
||
// 更新微信登录记录关联到新用户
|
||
wechatLogin.UserId = user.Id;
|
||
}
|
||
|
||
// 更新最后登录时间和IP
|
||
wechatLogin.LastLoginTime = now;
|
||
wechatLogin.LastLoginIp = clientIp;
|
||
await _wechatMiniProgramLoginsRepository.UpdateAsync(wechatLogin);
|
||
|
||
// 更新用户最后登录信息
|
||
user.LastLoginTime = now;
|
||
user.LastLoginIp = clientIp;
|
||
await _userRepository.UpdateAsync(user);
|
||
}
|
||
else
|
||
{
|
||
// 3. 如果不存在,创建新用户
|
||
// 使用配置中的默认头像和昵称(从 IOptionsSnapshot 获取最新配置)
|
||
var appSettings = _appSettingsSnapshot.Value;
|
||
var defaultNickName = string.IsNullOrEmpty(appSettings.UserDefaultName)
|
||
? "用户"
|
||
: appSettings.UserDefaultName;
|
||
var defaultAvatar = string.IsNullOrEmpty(appSettings.UserDefaultIcon)
|
||
? ""
|
||
: appSettings.UserDefaultIcon;
|
||
|
||
user = new T_Users
|
||
{
|
||
NickName = $"{defaultNickName}_{openId.Substring(0, 8)}",
|
||
Avatar = defaultAvatar,
|
||
Experience = 0,
|
||
LevelId = 1,
|
||
Status = UserStatusEnum.Normal,
|
||
CertifiedType = 0, // 0表示未认证
|
||
CertifiedStatus = CertifiedStatusEnum.未认证,
|
||
IsCertified = false,
|
||
IsVip = false,
|
||
UID = await _userInfoService.GenerateUniqueUIDAsync(),
|
||
CreatedAt = now,
|
||
UpdatedAt = now,
|
||
RegisterIp = clientIp,
|
||
LastLoginIp = clientIp,
|
||
LastLoginTime = now
|
||
};
|
||
|
||
await _userRepository.InsertAsync(user);
|
||
|
||
// 新用户自动关联默认身份组
|
||
await AssignDefaultIdentityGroupAsync(user.Id);
|
||
|
||
// 创建微信登录记录
|
||
wechatLogin = new T_WechatMiniProgramLogins
|
||
{
|
||
OpenId = openId,
|
||
SessionKey = sessionKey,
|
||
UnionId = unionId ?? "",
|
||
UserId = user.Id,
|
||
LastLoginIp = clientIp,
|
||
LastLoginTime = now,
|
||
CreatedAt = now,
|
||
UpdatedAt = now,
|
||
};
|
||
|
||
await _wechatMiniProgramLoginsRepository.InsertAsync(wechatLogin);
|
||
}
|
||
|
||
// 4. 生成JWT Token
|
||
var claims = new List<Claim>
|
||
{
|
||
new Claim("userId", user.Id.ToString()),
|
||
new Claim("loginType",((int)LoginTypeEnum.WechatLogin).ToString())
|
||
};
|
||
|
||
var jwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims, now);
|
||
// 3. 删除T_UserTokens中的token信息
|
||
await _userTokensRepository.Select
|
||
.Where(x => x.UserId == user.Id).ToDelete().ExecuteDeletedAsync();
|
||
// 5. 保存Token到数据库
|
||
var userToken = new T_UserTokens
|
||
{
|
||
UserId = user.Id,
|
||
AccessToken = jwtToken.Token,
|
||
LoginType = LoginTypeEnum.WechatLogin,
|
||
LoginId = wechatLogin.Id,
|
||
ClientIp = clientIp,
|
||
UserAgent = _httpContextAccessor.GetUserAgent(),
|
||
ExpiresAt = jwtToken.ExpireAt,
|
||
TokenType = "Bearer",
|
||
CreatedAt = now,
|
||
UpdatedAt = now
|
||
};
|
||
|
||
await _userTokensRepository.InsertAsync(userToken);
|
||
|
||
return jwtToken;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 刷新token
|
||
/// 1.根据注入的userInfoModel对象,获取当前用户信息
|
||
/// 2.生成新的JwtAccessToken
|
||
/// 3.更新T_UserTokens中的token信息
|
||
/// 4.旧的token作废,去除redis缓存(RemoveTokenByUserNameAsync)
|
||
/// 5.返回新的JwtAccessToken
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
/// <exception cref="NotImplementedException"></exception>
|
||
public async Task<JwtAccessToken> RefreshToken()
|
||
{
|
||
if (_userInfoModel == null || string.IsNullOrEmpty(_userInfoModel.UserName))
|
||
{
|
||
throw new Exception("用户未登录");
|
||
}
|
||
|
||
var now = DateTime.Now;
|
||
|
||
// 1. 根据注入的userInfoModel对象,获取当前用户信息
|
||
if (!long.TryParse(_userInfoModel.UserName, out long userId))
|
||
{
|
||
throw new Exception("无效的用户ID");
|
||
}
|
||
|
||
var user = await _userRepository.Select
|
||
.Where(x => x.Id == userId)
|
||
.FirstAsync();
|
||
|
||
if (user == null)
|
||
{
|
||
throw new Exception("用户不存在");
|
||
}
|
||
//if (_userInfoModel.ExpireAt < DateTime.Now.AddDays(3))
|
||
//{
|
||
|
||
//}
|
||
// 2. 生成新的JwtAccessToken
|
||
var claims = new List<Claim>
|
||
{
|
||
new Claim("userId", user.Id.ToString()),
|
||
new Claim("loginType",_userInfoModel.LoginType.ToString())
|
||
};
|
||
|
||
var newJwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims, now);
|
||
|
||
// 3. 删除T_UserTokens中的token信息
|
||
await _userTokensRepository.Select
|
||
.Where(x => x.UserId == userId).ToDelete().ExecuteDeletedAsync();
|
||
// 3. 更新T_UserTokens中的token信息(将旧的token标记为无效,插入新token)
|
||
//var oldTokens = await _userTokensRepository.Select
|
||
// .Where(x => x.UserId == userId)
|
||
// .ToListAsync();
|
||
|
||
//foreach (var oldToken in oldTokens)
|
||
//{
|
||
|
||
// oldToken.UpdatedAt = now;
|
||
// await _userTokensRepository.UpdateAsync(oldToken);
|
||
//}
|
||
|
||
// 插入新的token记录
|
||
var clientIp = _httpContextAccessor.GetClientIpAddress();
|
||
var newUserToken = new T_UserTokens
|
||
{
|
||
UserId = user.Id,
|
||
AccessToken = newJwtToken.Token,
|
||
LoginType = (LoginTypeEnum)_userInfoModel.LoginType, // 可以根据实际情况调整
|
||
ClientIp = clientIp,
|
||
UserAgent = _httpContextAccessor.GetUserAgent(),
|
||
ExpiresAt = newJwtToken.ExpireAt,
|
||
TokenType = "Bearer",
|
||
CreatedAt = now,
|
||
UpdatedAt = now,
|
||
TokenMD5 = newJwtToken.Token.ToMD5()
|
||
};
|
||
|
||
await _userTokensRepository.InsertAsync(newUserToken);
|
||
|
||
// 4. 旧的token作废,去除redis缓存 生成的时候好像会覆盖掉
|
||
//await _jwtAuthManager.RemoveTokenByUserNameAsync(_userInfoModel.UserName);
|
||
|
||
// 5. 返回新的JwtAccessToken
|
||
return newJwtToken;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 账号密码登录
|
||
/// </summary>
|
||
/// <param name="account">账号(用户名/手机号/邮箱)</param>
|
||
/// <param name="password">密码</param>
|
||
/// <param name="accountType">账号类型:1-用户名,2-手机号,3-邮箱</param>
|
||
/// <returns></returns>
|
||
public async Task<JwtAccessToken> AccountLogin(string account, string password, int accountType = 1)
|
||
{
|
||
// 1. 根据账号查找登录记录
|
||
var loginRecord = await _accountPasswordLoginsRepository.Select
|
||
.Where(x => x.LoginAccount == account && x.AccountType == accountType && !x.IsDeleted)
|
||
.FirstAsync();
|
||
|
||
if (loginRecord == null)
|
||
{
|
||
throw new LoginErrorException("账号不存在或密码错误");
|
||
}
|
||
|
||
// 2. 验证密码
|
||
if (!VerifyPassword(password, loginRecord.PasswordHash, loginRecord.PasswordSalt))
|
||
{
|
||
throw new LoginErrorException("账号不存在或密码错误");
|
||
}
|
||
|
||
// 3. 获取用户信息
|
||
var user = await _userRepository.Select.Where(x => x.Id == loginRecord.UserId).FirstAsync();
|
||
if (user == null || user.Status != UserStatusEnum.Normal)
|
||
{
|
||
throw new LoginErrorException("用户状态异常");
|
||
}
|
||
|
||
// 4. 更新登录信息
|
||
var clientIp = _httpContextAccessor.GetClientIpAddress(); ;
|
||
loginRecord.LastLoginTime = DateTime.Now;
|
||
loginRecord.LastLoginIp = clientIp;
|
||
await _accountPasswordLoginsRepository.UpdateAsync(loginRecord);
|
||
|
||
// 5. 更新用户最后登录信息
|
||
user.LastLoginTime = DateTime.Now;
|
||
user.LastLoginIp = clientIp;
|
||
await _userRepository.UpdateAsync(user);
|
||
|
||
// 6. 生成JWT Token
|
||
var claims = new List<Claim>
|
||
{
|
||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||
new Claim(ClaimTypes.Name, user.NickName),
|
||
new Claim("loginType", "2"), // 2-账号密码登录
|
||
new Claim("userId", loginRecord.Id.ToString())
|
||
};
|
||
|
||
var jwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims.ToList(), DateTime.Now);
|
||
// 3. 删除T_UserTokens中的token信息
|
||
await _userTokensRepository.Select
|
||
.Where(x => x.UserId == user.Id).ToDelete().ExecuteDeletedAsync();
|
||
// 7. 保存Token到数据库
|
||
var userToken = new T_UserTokens
|
||
{
|
||
UserId = user.Id,
|
||
AccessToken = jwtToken.Token,
|
||
TokenType = "Bearer",
|
||
ExpiresAt = jwtToken.ExpireAt,
|
||
LoginType = LoginTypeEnum.AccountPasswordLogin, // 2-账号密码登录
|
||
LoginId = loginRecord.Id,
|
||
ClientIp = clientIp,
|
||
UserAgent = _httpContextAccessor.GetUserAgent(),
|
||
CreatedAt = DateTime.Now,
|
||
UpdatedAt = DateTime.Now,
|
||
TokenMD5 = jwtToken.Token.ToMD5()
|
||
};
|
||
|
||
await _userTokensRepository.InsertAsync(userToken);
|
||
|
||
return jwtToken;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 账号密码注册
|
||
/// </summary>
|
||
/// <param name="loginAccount">登录账号(用户名/手机号/邮箱)</param>
|
||
/// <param name="accountType">账号类型:1-用户名,2-手机号,3-邮箱</param>
|
||
/// <param name="password">密码</param>
|
||
/// <param name="nickName">昵称</param>
|
||
/// <param name="verifyCode">验证码(手机号/邮箱注册时必填)</param>
|
||
/// <returns></returns>
|
||
public async Task<JwtAccessToken> AccountRegister(string loginAccount, int accountType, string password, string nickName, string verifyCode = "")
|
||
{
|
||
// 1. 验证账号是否已存在
|
||
var existingLogin = await _accountPasswordLoginsRepository.Select
|
||
.Where(x => x.LoginAccount == loginAccount && x.AccountType == accountType && !x.IsDeleted)
|
||
.FirstAsync();
|
||
|
||
if (existingLogin != null)
|
||
{
|
||
throw new LoginErrorException("该账号已存在");
|
||
}
|
||
|
||
// 2. 验证验证码(如果需要)
|
||
if (accountType == 2 || accountType == 3) // 手机号或邮箱注册
|
||
{
|
||
if (string.IsNullOrEmpty(verifyCode))
|
||
{
|
||
throw new LoginErrorException("验证码不能为空");
|
||
}
|
||
// TODO: 实现验证码验证逻辑
|
||
}
|
||
|
||
// 3. 密码加密
|
||
var (passwordHash, passwordSalt) = HashPassword(password);
|
||
|
||
// 4. 创建用户记录
|
||
var clientIp = _httpContextAccessor.GetClientIpAddress(); ;
|
||
var now = DateTime.Now;
|
||
|
||
// 使用配置中的默认头像(从 IOptionsSnapshot 获取最新配置)
|
||
var appSettings = _appSettingsSnapshot.Value;
|
||
var defaultAvatar = string.IsNullOrEmpty(appSettings.UserDefaultIcon)
|
||
? ""
|
||
: appSettings.UserDefaultIcon;
|
||
|
||
var user = new T_Users
|
||
{
|
||
NickName = nickName,
|
||
Avatar = defaultAvatar,
|
||
LevelId = 1, // 默认等级
|
||
Experience = 0,
|
||
Status = UserStatusEnum.Normal,
|
||
CertifiedType = 0, // 0表示未认证
|
||
CertifiedStatus = CertifiedStatusEnum.未认证,
|
||
IsCertified = false,
|
||
IsVip = false,
|
||
UID = await _userInfoService.GenerateUniqueUIDAsync(),
|
||
RegisterIp = clientIp,
|
||
LastLoginIp = clientIp,
|
||
LastLoginTime = now,
|
||
CreatedAt = now,
|
||
UpdatedAt = now
|
||
};
|
||
|
||
await _userRepository.InsertAsync(user);
|
||
|
||
// 新用户自动关联默认身份组
|
||
await AssignDefaultIdentityGroupAsync(user.Id);
|
||
|
||
// 5. 创建登录记录
|
||
var loginRecord = new T_AccountPasswordLogins
|
||
{
|
||
UserId = user.Id,
|
||
LoginAccount = loginAccount,
|
||
AccountType = (byte)accountType,
|
||
PasswordHash = passwordHash,
|
||
PasswordSalt = passwordSalt,
|
||
IsPrimary = true, // 第一个登录方式为主登录方式
|
||
CreatedAt = now,
|
||
UpdatedAt = now
|
||
};
|
||
|
||
await _accountPasswordLoginsRepository.InsertAsync(loginRecord);
|
||
|
||
// 6. 生成JWT Token
|
||
var claims = new List<Claim>
|
||
{
|
||
new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()),
|
||
new Claim(ClaimTypes.Name, user.NickName),
|
||
new Claim("loginType", "2"), // 2-账号密码登录
|
||
new Claim("userId", user.Id.ToString())
|
||
};
|
||
|
||
var jwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims.ToList(), DateTime.Now);
|
||
|
||
// 7. 保存Token到数据库
|
||
var userToken = new T_UserTokens
|
||
{
|
||
UserId = user.Id,
|
||
AccessToken = jwtToken.Token,
|
||
TokenType = "Bearer",
|
||
ExpiresAt = jwtToken.ExpireAt,
|
||
LoginType = LoginTypeEnum.AccountPasswordLogin, // 2-账号密码登录
|
||
LoginId = loginRecord.Id,
|
||
ClientIp = clientIp,
|
||
UserAgent = _httpContextAccessor.GetUserAgent(),
|
||
CreatedAt = now,
|
||
UpdatedAt = now,
|
||
TokenMD5 = jwtToken.Token.ToMD5()
|
||
};
|
||
|
||
await _userTokensRepository.InsertAsync(userToken);
|
||
|
||
return jwtToken;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 微信手机号登录
|
||
/// 用户匹配逻辑:手机号 → openId → 新用户
|
||
/// </summary>
|
||
public async Task<JwtAccessToken> WechatPhoneLogin(string openId, string phoneNumber, string sessionKey = "", string unionId = "")
|
||
{
|
||
if (string.IsNullOrEmpty(openId)) throw new ArgumentNullException(nameof(openId));
|
||
if (string.IsNullOrEmpty(phoneNumber)) throw new ArgumentNullException(nameof(phoneNumber));
|
||
|
||
var now = DateTime.Now;
|
||
var clientIp = _httpContextAccessor.GetClientIpAddress();
|
||
T_Users user = null;
|
||
T_WechatMiniProgramLogins wechatLogin = null;
|
||
|
||
// 1. 根据手机号查询用户
|
||
user = await _userRepository.Select
|
||
.Where(x => x.PhoneNumber == phoneNumber)
|
||
.FirstAsync();
|
||
|
||
if (user != null)
|
||
{
|
||
// 找到手机号用户,确保绑定/更新 OpenID
|
||
// 先按 UserId 查当前用户的微信记录
|
||
wechatLogin = await _wechatMiniProgramLoginsRepository.Select
|
||
.Where(x => x.UserId == user.Id)
|
||
.FirstAsync();
|
||
|
||
if (wechatLogin == null)
|
||
{
|
||
// 当前用户没有微信记录,检查该 OpenId 是否已被其他用户占用
|
||
var existingByOpenId = await _wechatMiniProgramLoginsRepository.Select
|
||
.Where(x => x.OpenId == openId)
|
||
.FirstAsync();
|
||
|
||
if (existingByOpenId != null)
|
||
{
|
||
// OpenId 已存在,将其重新关联到当前手机号用户
|
||
existingByOpenId.UserId = user.Id;
|
||
existingByOpenId.SessionKey = sessionKey;
|
||
existingByOpenId.LastLoginTime = now;
|
||
existingByOpenId.LastLoginIp = clientIp;
|
||
existingByOpenId.UpdatedAt = now;
|
||
await _wechatMiniProgramLoginsRepository.UpdateAsync(existingByOpenId);
|
||
wechatLogin = existingByOpenId;
|
||
}
|
||
else
|
||
{
|
||
wechatLogin = new T_WechatMiniProgramLogins
|
||
{
|
||
OpenId = openId,
|
||
SessionKey = sessionKey,
|
||
UnionId = unionId ?? "",
|
||
UserId = user.Id,
|
||
LastLoginIp = clientIp,
|
||
LastLoginTime = now,
|
||
CreatedAt = now,
|
||
UpdatedAt = now,
|
||
};
|
||
await _wechatMiniProgramLoginsRepository.InsertAsync(wechatLogin);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
wechatLogin.OpenId = openId;
|
||
wechatLogin.SessionKey = sessionKey;
|
||
wechatLogin.LastLoginTime = now;
|
||
wechatLogin.LastLoginIp = clientIp;
|
||
wechatLogin.UpdatedAt = now;
|
||
await _wechatMiniProgramLoginsRepository.UpdateAsync(wechatLogin);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 2. 根据 OpenID 查询
|
||
wechatLogin = await _wechatMiniProgramLoginsRepository.Select
|
||
.Where(x => x.OpenId == openId)
|
||
.FirstAsync();
|
||
|
||
if (wechatLogin != null)
|
||
{
|
||
user = await _userRepository.Select
|
||
.Where(x => x.Id == wechatLogin.UserId)
|
||
.FirstAsync();
|
||
|
||
if (user != null)
|
||
{
|
||
// 绑定手机号
|
||
user.PhoneNumber = phoneNumber;
|
||
await _userRepository.UpdateAsync(user);
|
||
|
||
wechatLogin.SessionKey = sessionKey;
|
||
wechatLogin.LastLoginTime = now;
|
||
wechatLogin.LastLoginIp = clientIp;
|
||
await _wechatMiniProgramLoginsRepository.UpdateAsync(wechatLogin);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 3. 新用户注册
|
||
if (user == null)
|
||
{
|
||
var appSettings = _appSettingsSnapshot.Value;
|
||
var defaultNickName = string.IsNullOrEmpty(appSettings.UserDefaultName) ? "用户" : appSettings.UserDefaultName;
|
||
var defaultAvatar = string.IsNullOrEmpty(appSettings.UserDefaultIcon) ? "" : appSettings.UserDefaultIcon;
|
||
|
||
user = new T_Users
|
||
{
|
||
NickName = $"{defaultNickName}_{openId.Substring(0, Math.Min(8, openId.Length))}",
|
||
Avatar = defaultAvatar,
|
||
PhoneNumber = phoneNumber,
|
||
Experience = 0,
|
||
LevelId = 1,
|
||
Status = UserStatusEnum.Normal,
|
||
CertifiedType = 0,
|
||
CertifiedStatus = CertifiedStatusEnum.未认证,
|
||
IsCertified = false,
|
||
IsVip = false,
|
||
UID = await _userInfoService.GenerateUniqueUIDAsync(),
|
||
CreatedAt = now,
|
||
UpdatedAt = now,
|
||
RegisterIp = clientIp,
|
||
LastLoginIp = clientIp,
|
||
LastLoginTime = now
|
||
};
|
||
await _userRepository.InsertAsync(user);
|
||
|
||
// 新用户自动关联默认身份组
|
||
await AssignDefaultIdentityGroupAsync(user.Id);
|
||
|
||
wechatLogin = new T_WechatMiniProgramLogins
|
||
{
|
||
OpenId = openId,
|
||
SessionKey = sessionKey,
|
||
UnionId = unionId ?? "",
|
||
UserId = user.Id,
|
||
LastLoginIp = clientIp,
|
||
LastLoginTime = now,
|
||
CreatedAt = now,
|
||
UpdatedAt = now,
|
||
};
|
||
await _wechatMiniProgramLoginsRepository.InsertAsync(wechatLogin);
|
||
}
|
||
|
||
// 更新用户最后登录信息
|
||
user.LastLoginTime = now;
|
||
user.LastLoginIp = clientIp;
|
||
await _userRepository.UpdateAsync(user);
|
||
|
||
// 生成JWT Token
|
||
var claims = new List<Claim>
|
||
{
|
||
new Claim("userId", user.Id.ToString()),
|
||
new Claim("loginType", ((int)LoginTypeEnum.WechatLogin).ToString())
|
||
};
|
||
|
||
var jwtToken = await _jwtAuthManager.GenerateTokensAsync(user.Id.ToString(), claims, now);
|
||
|
||
await _userTokensRepository.Select
|
||
.Where(x => x.UserId == user.Id).ToDelete().ExecuteDeletedAsync();
|
||
|
||
var userToken = new T_UserTokens
|
||
{
|
||
UserId = user.Id,
|
||
AccessToken = jwtToken.Token,
|
||
LoginType = LoginTypeEnum.WechatLogin,
|
||
LoginId = wechatLogin.Id,
|
||
ClientIp = clientIp,
|
||
UserAgent = _httpContextAccessor.GetUserAgent(),
|
||
ExpiresAt = jwtToken.ExpireAt,
|
||
TokenType = "Bearer",
|
||
CreatedAt = now,
|
||
UpdatedAt = now
|
||
};
|
||
await _userTokensRepository.InsertAsync(userToken);
|
||
|
||
return jwtToken;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 为新用户自动关联默认身份组
|
||
/// </summary>
|
||
/// <param name="userId">新用户ID</param>
|
||
private async Task AssignDefaultIdentityGroupAsync(long userId)
|
||
{
|
||
var defaultGroup = await _identityGroupsRepository.Select
|
||
.Where(x => x.IsDefault == true)
|
||
.FirstAsync();
|
||
|
||
if (defaultGroup != null)
|
||
{
|
||
await _userIdentityGroupsRepository.InsertAsync(new T_UserIdentityGroups
|
||
{
|
||
UserId = userId,
|
||
IdentityGroupId = defaultGroup.Id,
|
||
CreatedAt = DateTime.Now
|
||
});
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 密码加密
|
||
/// </summary>
|
||
/// <param name="password">原始密码</param>
|
||
/// <returns>加密后的密码哈希和盐值</returns>
|
||
private (string hash, string salt) HashPassword(string password)
|
||
{
|
||
// 生成随机盐值
|
||
var saltBytes = new byte[32];
|
||
using (var rng = RandomNumberGenerator.Create())
|
||
{
|
||
rng.GetBytes(saltBytes);
|
||
}
|
||
var salt = SystemConvert.ToBase64String(saltBytes);
|
||
|
||
// 使用PBKDF2进行密码哈希
|
||
using (var pbkdf2 = new Rfc2898DeriveBytes(password, saltBytes, 10000, HashAlgorithmName.SHA256))
|
||
{
|
||
var hashBytes = pbkdf2.GetBytes(32);
|
||
var hash = SystemConvert.ToBase64String(hashBytes);
|
||
return (hash, salt);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 验证密码
|
||
/// </summary>
|
||
/// <param name="password">原始密码</param>
|
||
/// <param name="hash">存储的密码哈希</param>
|
||
/// <param name="salt">存储的盐值</param>
|
||
/// <returns>密码是否正确</returns>
|
||
private bool VerifyPassword(string password, string hash, string salt)
|
||
{
|
||
try
|
||
{
|
||
var saltBytes = SystemConvert.FromBase64String(salt);
|
||
using (var pbkdf2 = new Rfc2898DeriveBytes(password, saltBytes, 10000, HashAlgorithmName.SHA256))
|
||
{
|
||
var hashBytes = pbkdf2.GetBytes(32);
|
||
var computedHash = SystemConvert.ToBase64String(hashBytes);
|
||
return computedHash == hash;
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
return false;
|
||
}
|
||
}
|
||
}
|
||
}
|