Files
f9-web-ai-test/backend/F9WebAiTest.Application/Services/Auth/AuthService.cs
T
2026-10-08 10:43:59 +08:00

301 lines
12 KiB
C#

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();
}
}