AuthSessionAppService.cs 9.34 KB
using FoodLabeling.Application.Contracts.Dtos.AuthSession;
using FoodLabeling.Application.Contracts.IServices;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Caching;
using FoodLabeling.Application.Services.DbModels;
using Yi.Framework.Rbac.Domain.Entities;
using Yi.Framework.Rbac.Domain.Shared.Caches;
using Yi.Framework.Rbac.Domain.Shared.Consts;
using Yi.Framework.SqlSugarCore.Abstractions;

namespace FoodLabeling.Application.Services;

/// <summary>
/// 当前登录会话:菜单权限与退出
/// </summary>
[Authorize]
public class AuthSessionAppService : ApplicationService, IAuthSessionAppService
{
    private readonly IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> _userCache;
    private readonly ISqlSugarDbContext _dbContext;
    private readonly ISqlSugarRepository<UserAggregateRoot, Guid> _userRepository;

    public AuthSessionAppService(
        ISqlSugarDbContext dbContext,
        ISqlSugarRepository<UserAggregateRoot, Guid> userRepository,
        IDistributedCache<UserInfoCacheItem, UserInfoCacheKey> userCache)
    {
        _dbContext = dbContext;
        _userRepository = userRepository;
        _userCache = userCache;
    }

    /// <inheritdoc />
    public virtual async Task<CurrentUserMenuPermissionsOutputDto> GetMyMenusAsync()
    {
        if (!CurrentUser.Id.HasValue)
        {
            throw new UserFriendlyException("用户未登录");
        }

        // 避免走 UserManager.GetInfoAsync -> UserRepository.GetUserAllInfoAsync 的导航加载
        // 这里直接按 UserRole/RoleMenu/Menu 表关联查询当前用户可见菜单与权限码
        var userId = CurrentUser.Id.Value;
        var user = await _userRepository.GetByIdAsync(userId);
        if (user is null || user.IsDeleted)
        {
            throw new UserFriendlyException("用户不存在");
        }

        var userRoleIds = await _dbContext.SqlSugarClient.Queryable<UserRoleEntity>()
            .Where(x => x.UserId == userId)
            .Select(x => x.RoleId)
            .ToListAsync();
        var distinctUserRoleIds = userRoleIds.Distinct().ToList();

        List<MenuDbEntity> menus;
        if (UserConst.Admin.Equals(user.UserName))
        {
            // MenuAggregateRoot(ParentId 为 Guid) 无法兼容 menu.ParentId=0/字符串:这里统一用 MenuDbEntity
            menus = await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>()
                .Where(x => x.IsDeleted == false)
                .ToListAsync();
        }
        else
        {
            var roleIdStrs = distinctUserRoleIds.Select(x => x.ToString()).Distinct().ToList();
            if (roleIdStrs.Count == 0)
            {
                menus = new List<MenuDbEntity>();
            }
            else
            {
                var menuIds = await _dbContext.SqlSugarClient.Queryable<RoleMenuDbEntity>()
                    .Where(x => roleIdStrs.Contains(x.RoleId))
                    .Select(x => x.MenuId)
                    .Distinct()
                    .ToListAsync();

                menus = menuIds.Count == 0
                    ? new List<MenuDbEntity>()
                    : await _dbContext.SqlSugarClient.Queryable<MenuDbEntity>()
                        .Where(x => x.IsDeleted == false && menuIds.Contains(x.Id))
                        .ToListAsync();
            }
        }

        var menuNodes = menus
            .Select(MapToNode)
            .OrderByDescending(x => x.OrderNum)
            .ThenBy(x => x.MenuName)
            .ToList();

        // 注意:经仓储查询 RoleAggregateRoot 会触发 YiRbacDbContext 的 IDataPermission 过滤,
        // 其表达式包含 roleInfo.Select(...).Contains(...),在当前 SqlSugar 版本下会报“不支持 Select”。
        // 角色展示名使用 RoleDbEntity 直查 Role 表;角色编码列表仍用 JWT(CurrentUser.Roles),与原先 RoleCodes 行为一致。
        var roleCodes = CurrentUser.Roles?.ToList() ?? new List<string>();
        var roleDisplay = await BuildRoleDisplayAsync(distinctUserRoleIds, roleCodes);

        var permissionCodes = menuNodes
            .Where(x => !string.IsNullOrWhiteSpace(x.PermissionCode))
            .Select(x => x.PermissionCode!.Trim())
            .Distinct()
            .OrderBy(x => x)
            .ToList();

        return new CurrentUserMenuPermissionsOutputDto
        {
            User = new CurrentUserBriefDto
            {
                Id = user.Id,
                UserName = user.UserName,
                Nick = user.Nick,
                Email = user.Email,
                Icon = user.Icon
            },
            RoleCodes = roleCodes.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()).Distinct().OrderBy(x => x).ToList(),
            PermissionCodes = permissionCodes,
            Menus = BuildMenuTree(menuNodes),
            LastUpdated = user.LastModificationTime,
            Role = roleDisplay,
            FullName = BuildFullName(user)
        };
    }

    /// <inheritdoc />
    [HttpPost]
    public virtual async Task<bool> LogoutAsync()
    {
        if (!CurrentUser.Id.HasValue)
        {
            return false;
        }

        await _userCache.RemoveAsync(new UserInfoCacheKey(CurrentUser.Id.Value));
        return true;
    }

    private static string BuildFullName(UserAggregateRoot user)
    {
        if (!string.IsNullOrWhiteSpace(user.Name))
        {
            return user.Name.Trim();
        }

        if (!string.IsNullOrWhiteSpace(user.Nick))
        {
            return user.Nick.Trim();
        }

        return user.UserName ?? string.Empty;
    }

    private async Task<string> BuildRoleDisplayAsync(
        IReadOnlyList<Guid> userRoleIds,
        IReadOnlyList<string> jwtRoleCodes)
    {
        var names = new List<string>();
        if (userRoleIds.Count > 0)
        {
            names = await _dbContext.SqlSugarClient.Queryable<RoleDbEntity>()
                .Where(r => !r.IsDeleted && userRoleIds.Contains(r.Id))
                .OrderBy(r => r.RoleName)
                .Select(r => r.RoleName)
                .ToListAsync();
        }

        if (names.Count == 0 && jwtRoleCodes.Count > 0)
        {
            var codes = jwtRoleCodes
                .Where(x => !string.IsNullOrWhiteSpace(x))
                .Select(x => x.Trim())
                .Distinct()
                .ToList();
            names = await _dbContext.SqlSugarClient.Queryable<RoleDbEntity>()
                .Where(r => !r.IsDeleted && codes.Contains(r.RoleCode))
                .OrderBy(r => r.RoleName)
                .Select(r => r.RoleName)
                .ToListAsync();
        }

        var distinctNames = names
            .Where(n => !string.IsNullOrWhiteSpace(n))
            .Select(n => n.Trim())
            .Distinct()
            .ToList();
        if (distinctNames.Count > 0)
        {
            return string.Join(", ", distinctNames);
        }

        var fallbackCodes = jwtRoleCodes
            .Where(x => !string.IsNullOrWhiteSpace(x))
            .Select(x => x.Trim())
            .Distinct()
            .ToList();
        return fallbackCodes.Count > 0 ? string.Join(", ", fallbackCodes) : string.Empty;
    }

    private static List<CurrentUserMenuNodeDto> BuildMenuTree(List<CurrentUserMenuNodeDto> flat)
    {
        var nodes = flat
            .GroupBy(x => x.Id)
            .Select(g => g.First())
            .ToList();
        var byId = nodes.ToDictionary(n => n.Id, n => n);

        foreach (var n in nodes)
        {
            n.Children = new List<CurrentUserMenuNodeDto>();
        }

        var roots = new List<CurrentUserMenuNodeDto>();
        foreach (var n in nodes)
        {
            var pid = string.IsNullOrWhiteSpace(n.ParentId) ? "0" : n.ParentId.Trim();
            if (pid == "0" || pid == "00000000-0000-0000-0000-000000000000")
            {
                roots.Add(n);
                continue;
            }

            if (byId.TryGetValue(pid, out var parent))
            {
                parent.Children.Add(n);
            }
            else
            {
                roots.Add(n);
            }
        }

        SortMenuTree(roots);
        return roots;
    }

    private static CurrentUserMenuNodeDto MapToNode(MenuDbEntity m)
    {
        return new CurrentUserMenuNodeDto
        {
            Id = m.Id,
            ParentId = string.IsNullOrWhiteSpace(m.ParentId) ? "0" : m.ParentId.Trim(),
            MenuName = m.MenuName ?? string.Empty,
            RouterName = m.RouterName,
            Router = m.Router,
            PermissionCode = m.PermissionCode,
            MenuType = m.MenuType,
            MenuSource = m.MenuSource,
            OrderNum = m.OrderNum,
            State = m.State,
            MenuIcon = m.MenuIcon,
            Component = m.Component,
            IsLink = m.IsLink,
            IsCache = m.IsCache,
            IsShow = m.IsShow,
            Query = m.Query,
            Remark = m.Remark
        };
    }

    private static void SortMenuTree(List<CurrentUserMenuNodeDto> level)
    {
        level.Sort((a, b) =>
        {
            var o = b.OrderNum.CompareTo(a.OrderNum);
            return o != 0 ? o : string.Compare(a.MenuName, b.MenuName, StringComparison.Ordinal);
        });

        foreach (var n in level)
        {
            if (n.Children.Count > 0)
            {
                SortMenuTree(n.Children);
            }
        }
    }
}