first commit
This commit is contained in:
@@ -0,0 +1,300 @@
|
||||
using F9WebAiTest.Application.Common;
|
||||
using F9WebAiTest.Application.Dtos;
|
||||
using F9WebAiTest.Application.Dtos.Auth;
|
||||
using F9WebAiTest.Application.Interfaces.Security;
|
||||
using F9WebAiTest.Domain.Common;
|
||||
using F9WebAiTest.Domain.Entities.SYS.Departments;
|
||||
using F9WebAiTest.Domain.Entities.SYS.RBAC;
|
||||
using F9WebAiTest.Domain.Enums.SYS.RBAC;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using RefreshToken = F9WebAiTest.Domain.Entities.RefreshToken;
|
||||
|
||||
namespace F9WebAiTest.Application.Services.Auth;
|
||||
|
||||
public class AuthService : IAuthService
|
||||
{
|
||||
private readonly IRepository<User> _userRepo;
|
||||
private readonly IRepository<UserRole> _userRoleRepo;
|
||||
private readonly IRepository<RolePermission> _rolePermissionRepo;
|
||||
private readonly IRepository<Permission> _permissionRepo;
|
||||
private readonly IRepository<Menu> _menuRepo;
|
||||
private readonly IRepository<Role> _roleRepo;
|
||||
private readonly IRepository<Department> _deptRepo;
|
||||
private readonly IRepository<RefreshToken> _refreshTokenRepo;
|
||||
private readonly IJwtService _jwt;
|
||||
private readonly IConfiguration _config;
|
||||
private readonly IPasswordHasher _passwordHasher;
|
||||
|
||||
public AuthService(
|
||||
IRepository<User> userRepo,
|
||||
IRepository<UserRole> userRoleRepo,
|
||||
IRepository<RolePermission> rolePermissionRepo,
|
||||
IRepository<Permission> permissionRepo,
|
||||
IRepository<Menu> menuRepo,
|
||||
IRepository<Role> roleRepo,
|
||||
IRepository<Department> deptRepo,
|
||||
IRepository<RefreshToken> refreshTokenRepo,
|
||||
IJwtService jwt,
|
||||
IConfiguration config,
|
||||
IPasswordHasher passwordHasher)
|
||||
{
|
||||
_userRepo = userRepo;
|
||||
_userRoleRepo = userRoleRepo;
|
||||
_rolePermissionRepo = rolePermissionRepo;
|
||||
_permissionRepo = permissionRepo;
|
||||
_menuRepo = menuRepo;
|
||||
_roleRepo = roleRepo;
|
||||
_deptRepo = deptRepo;
|
||||
_refreshTokenRepo = refreshTokenRepo;
|
||||
_jwt = jwt;
|
||||
_config = config;
|
||||
_passwordHasher = passwordHasher;
|
||||
}
|
||||
|
||||
public async Task<LoginResultDto> LoginAsync(LoginDto dto)
|
||||
{
|
||||
var user = await _userRepo.FirstOrDefaultAsync(u => u.LoginName == dto.Username);
|
||||
if (user == null || !VerifyPassword(dto.Password, user.PasswordHash))
|
||||
throw new ServiceException("用户名或密码错误", 401);
|
||||
|
||||
if (!user.IsActive)
|
||||
throw new ServiceException("账号已被禁用,请联系管理员", 403);
|
||||
|
||||
return await IssueTokensAsync(user);
|
||||
}
|
||||
|
||||
public async Task<LoginResultDto> RefreshTokenAsync(string refreshToken)
|
||||
{
|
||||
var stored = await _refreshTokenRepo.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
||||
if (stored == null || stored.IsRevoked || stored.ExpiresAt < DateTime.UtcNow)
|
||||
throw new ServiceException("刷新令牌无效或已过期", 401);
|
||||
|
||||
var user = await _userRepo.GetByIdAsync(stored.EmployeeId);
|
||||
if (user == null || !user.IsActive)
|
||||
throw new ServiceException("用户不存在或已禁用", 401);
|
||||
|
||||
// 撤销旧 token,签发新 token(旋转)
|
||||
stored.IsRevoked = true;
|
||||
await _refreshTokenRepo.UpdateAsync(stored);
|
||||
|
||||
return await IssueTokensAsync(user);
|
||||
}
|
||||
|
||||
public async Task RevokeRefreshTokenAsync(string refreshToken)
|
||||
{
|
||||
var stored = await _refreshTokenRepo.FirstOrDefaultAsync(rt => rt.Token == refreshToken);
|
||||
if (stored != null)
|
||||
{
|
||||
stored.IsRevoked = true;
|
||||
await _refreshTokenRepo.UpdateAsync(stored);
|
||||
await _refreshTokenRepo.SaveChangesAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<EmployeeInfoDto> GetCurrentUserAsync(long userId)
|
||||
{
|
||||
var user = await _userRepo.GetByIdAsync(userId);
|
||||
if (user == null) throw new ServiceException("用户不存在", 404);
|
||||
return await BuildUserInfoAsync(user);
|
||||
}
|
||||
|
||||
public async Task<List<MenuDto>> GetUserMenusAsync(long userId)
|
||||
{
|
||||
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId))
|
||||
.Select(ur => ur.RoleId).ToList();
|
||||
|
||||
// 超级管理员返回全部菜单
|
||||
var isAdmin = await IsAdminAsync(userId);
|
||||
|
||||
List<Menu> menus;
|
||||
if (isAdmin)
|
||||
{
|
||||
menus = await _menuRepo.FindAsync(m => m.IsActive);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 通过 Permission 表关联菜单:RolePermission → Permission(ResourceType=菜单) → ResourceId(=Menu.Id)
|
||||
var permissionIds = (await _rolePermissionRepo.FindAsync(rp => roleIds.Contains(rp.RoleId)))
|
||||
.Select(rp => rp.PermissionId).Distinct().ToList();
|
||||
var permittedMenuIds = (await _permissionRepo.FindAsync(p =>
|
||||
permissionIds.Contains(p.Id) && p.ResourceType == Resource_Type_Enum.菜单))
|
||||
.Select(p => p.ResourceId).Distinct().ToHashSet();
|
||||
|
||||
// 全部活跃菜单
|
||||
var allMenus = await _menuRepo.FindAsync(m => m.IsActive);
|
||||
var menuDict = allMenus.ToDictionary(m => m.Id);
|
||||
|
||||
// 有权限的菜单 + 它们的祖先目录(父/祖目录自动包含,保证菜单树完整,非管理员也能看到一级菜单)
|
||||
var resultIds = new HashSet<long>();
|
||||
foreach (var id in permittedMenuIds)
|
||||
{
|
||||
var currentId = id;
|
||||
while (currentId != 0 && menuDict.ContainsKey(currentId) && resultIds.Add(currentId))
|
||||
{
|
||||
currentId = menuDict[currentId].ParentId;
|
||||
}
|
||||
}
|
||||
|
||||
menus = allMenus.Where(m => resultIds.Contains(m.Id)).ToList();
|
||||
}
|
||||
|
||||
// 只返回目录和菜单(分组/页面),按排序
|
||||
menus = menus.Where(m => m.MenuType != MenuType_Enum.默认)
|
||||
.OrderBy(m => m.SortOrder).ToList();
|
||||
|
||||
return BuildMenuTree(menus);
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetUserPermissionsAsync(long userId)
|
||||
{
|
||||
var isAdmin = await IsAdminAsync(userId);
|
||||
if (isAdmin)
|
||||
{
|
||||
// 超级管理员:返回所有权限单元 Code(含菜单权限 + 资源权限)
|
||||
return (await _permissionRepo.FindAsync(p => !string.IsNullOrEmpty(p.Code)))
|
||||
.Select(p => p.Code).Distinct().ToList();
|
||||
}
|
||||
|
||||
// 普通用户:角色 → RolePermission → Permission → Code(统一权限模型)
|
||||
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId))
|
||||
.Select(ur => ur.RoleId).ToList();
|
||||
var permissionIds = (await _rolePermissionRepo.FindAsync(rp => roleIds.Contains(rp.RoleId)))
|
||||
.Select(rp => rp.PermissionId).Distinct().ToList();
|
||||
return (await _permissionRepo.FindAsync(p => permissionIds.Contains(p.Id) && !string.IsNullOrEmpty(p.Code)))
|
||||
.Select(p => p.Code).Distinct().ToList();
|
||||
}
|
||||
|
||||
public async Task ChangePasswordAsync(long userId, ChangePasswordDto dto)
|
||||
{
|
||||
var user = await _userRepo.GetByIdAsync(userId);
|
||||
if (user == null) throw new ServiceException("用户不存在", 404);
|
||||
if (!VerifyPassword(dto.OldPassword, user.PasswordHash))
|
||||
throw new ServiceException("原密码错误");
|
||||
if ((dto.NewPassword ?? "").Length < 6)
|
||||
throw new ServiceException("新密码至少6位");
|
||||
|
||||
// 新密码与创建用户一致:BCrypt(MD5(明文))
|
||||
user.SetPassword(_passwordHasher.HashPassword(_passwordHasher.GetMd5Hash(dto.NewPassword)));
|
||||
await _userRepo.UpdateAsync(user);
|
||||
await _userRepo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task UpdateProfileAsync(long userId, UpdateProfileDto dto)
|
||||
{
|
||||
var user = await _userRepo.GetByIdAsync(userId);
|
||||
if (user == null) throw new ServiceException("用户不存在", 404);
|
||||
|
||||
user.UpdateProfile(dto.Nickname, dto.Avatar, dto.Email, dto.Phone, null);
|
||||
await _userRepo.UpdateAsync(user);
|
||||
await _userRepo.SaveChangesAsync();
|
||||
}
|
||||
|
||||
// ---- 私有方法 ----
|
||||
|
||||
/// <summary>校验密码,兼容旧格式 BCrypt(明文) 与新格式 BCrypt(MD5(明文))</summary>
|
||||
private bool VerifyPassword(string password, string passwordHash)
|
||||
{
|
||||
if (string.IsNullOrEmpty(passwordHash)) return false;
|
||||
if (_passwordHasher.VerifyPassword(password, passwordHash)) return true;
|
||||
return _passwordHasher.VerifyPassword(_passwordHasher.GetMd5Hash(password), passwordHash);
|
||||
}
|
||||
|
||||
private async Task<LoginResultDto> IssueTokensAsync(User user)
|
||||
{
|
||||
var roles = await GetRoleCodesAsync(user.Id);
|
||||
var accessToken = _jwt.GenerateAccessToken(user, roles);
|
||||
var refreshToken = _jwt.GenerateRefreshToken();
|
||||
var expireMinutes = int.Parse(_config["Jwt:AccessTokenExpireMinutes"] ?? "120");
|
||||
|
||||
// 存储 refresh token
|
||||
var expireDays = int.Parse(_config["Jwt:RefreshTokenExpireDays"] ?? "7");
|
||||
await _refreshTokenRepo.AddAsync(new RefreshToken
|
||||
{
|
||||
EmployeeId = user.Id,
|
||||
Token = refreshToken,
|
||||
ExpiresAt = DateTime.UtcNow.AddDays(expireDays)
|
||||
});
|
||||
await _refreshTokenRepo.SaveChangesAsync();
|
||||
|
||||
var userInfo = await BuildUserInfoAsync(user);
|
||||
return new LoginResultDto
|
||||
{
|
||||
AccessToken = accessToken,
|
||||
RefreshToken = refreshToken,
|
||||
AccessTokenExpiresAt = DateTime.UtcNow.AddMinutes(expireMinutes),
|
||||
User = userInfo
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<List<string>> GetRoleCodesAsync(long userId)
|
||||
{
|
||||
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId))
|
||||
.Select(ur => ur.RoleId).ToList();
|
||||
var roles = await _roleRepo.FindAsync(r => roleIds.Contains(r.Id));
|
||||
return roles.Select(r => r.Code).ToList();
|
||||
}
|
||||
|
||||
private async Task<bool> IsAdminAsync(long userId)
|
||||
{
|
||||
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == userId))
|
||||
.Select(ur => ur.RoleId).ToList();
|
||||
var roles = await _roleRepo.FindAsync(r => roleIds.Contains(r.Id));
|
||||
return roles.Any(r => r.Code == "admin");
|
||||
}
|
||||
|
||||
private async Task<EmployeeInfoDto> BuildUserInfoAsync(User user)
|
||||
{
|
||||
var dept = await _deptRepo.GetByIdAsync(user.DepartmentId);
|
||||
var roleIds = (await _userRoleRepo.FindAsync(ur => ur.UserId == user.Id))
|
||||
.Select(ur => ur.RoleId).ToList();
|
||||
var roles = await _roleRepo.FindAsync(r => roleIds.Contains(r.Id));
|
||||
|
||||
return new EmployeeInfoDto
|
||||
{
|
||||
Id = user.Id,
|
||||
Username = user.LoginName,
|
||||
RealName = user.UserName,
|
||||
Nickname = user.Nickname,
|
||||
Avatar = user.Avatar,
|
||||
Phone = user.PhoneNumber,
|
||||
Email = user.Email,
|
||||
DepartmentId = user.DepartmentId,
|
||||
DepartmentName = dept?.Name ?? "",
|
||||
Status = user.IsActive ? 1 : 0,
|
||||
Roles = roles.Select(r => r.Code).ToList(),
|
||||
Permissions = await GetUserPermissionsAsync(user.Id)
|
||||
};
|
||||
}
|
||||
|
||||
private static List<MenuDto> BuildMenuTree(List<Menu> menus)
|
||||
{
|
||||
var dict = menus.ToDictionary(m => m.Id, m => new MenuDto
|
||||
{
|
||||
Id = m.Id,
|
||||
ParentId = m.ParentId == 0 ? null : m.ParentId,
|
||||
Name = m.Name,
|
||||
Type = m.MenuType == MenuType_Enum.分组 ? "directory" : "menu",
|
||||
Path = m.Path,
|
||||
Component = m.Component,
|
||||
Icon = m.IconSvg,
|
||||
Permission = m.Permission,
|
||||
Sort = m.SortOrder,
|
||||
Status = m.IsActive ? 1 : 0
|
||||
});
|
||||
|
||||
var roots = new List<MenuDto>();
|
||||
foreach (var menu in menus)
|
||||
{
|
||||
var dto = dict[menu.Id];
|
||||
if (menu.ParentId == 0 || !dict.ContainsKey(menu.ParentId))
|
||||
{
|
||||
roots.Add(dto);
|
||||
}
|
||||
else
|
||||
{
|
||||
dict[menu.ParentId].Children.Add(dto);
|
||||
}
|
||||
}
|
||||
return roots.OrderBy(m => m.Sort).ToList();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user