RbacRoleAppService.cs 12.4 KB
using FoodLabeling.Application.Contracts.Dtos.RbacRole;
using FoodLabeling.Application.Helpers;
using FoodLabeling.Application.Contracts.Dtos.Common;
using FoodLabeling.Application.Contracts.IServices;
using FoodLabeling.Application.Services.DbModels;
using Microsoft.AspNetCore.Mvc;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Domain.Entities;
using Volo.Abp.Uow;
using Yi.Framework.Rbac.Domain.Entities;
using Yi.Framework.SqlSugarCore.Abstractions;

namespace FoodLabeling.Application.Services;

/// <summary>
/// 角色管理(食品标签-美国版对外)
/// </summary>
public class RbacRoleAppService : ApplicationService, IRbacRoleAppService
{
    private readonly ISqlSugarDbContext _dbContext;
    private readonly ISqlSugarRepository<RoleAggregateRoot, Guid> _roleRepository;
    private readonly ISqlSugarRepository<MenuAggregateRoot, Guid> _menuRepository;
    private readonly ISqlSugarRepository<RoleMenuEntity> _roleMenuRepository;
    private readonly ISqlSugarRepository<RoleDeptEntity> _roleDeptRepository;
    private readonly ISqlSugarRepository<UserRoleEntity> _userRoleRepository;

    public RbacRoleAppService(
        ISqlSugarDbContext dbContext,
        ISqlSugarRepository<RoleAggregateRoot, Guid> roleRepository,
        ISqlSugarRepository<MenuAggregateRoot, Guid> menuRepository,
        ISqlSugarRepository<RoleMenuEntity> roleMenuRepository,
        ISqlSugarRepository<RoleDeptEntity> roleDeptRepository,
        ISqlSugarRepository<UserRoleEntity> userRoleRepository)
    {
        _dbContext = dbContext;
        _roleRepository = roleRepository;
        _menuRepository = menuRepository;
        _roleMenuRepository = roleMenuRepository;
        _roleDeptRepository = roleDeptRepository;
        _userRoleRepository = userRoleRepository;
    }

    /// <inheritdoc />
    public async Task<PagedResultWithPageDto<RbacRoleGetListOutputDto>> GetListAsync([FromQuery] RbacRoleGetListInputVo input)
    {
        RefAsync<int> total = 0;

        var query = _roleRepository._DbQueryable
            .Where(x => x.IsDeleted == false)
            .WhereIF(!string.IsNullOrWhiteSpace(input.RoleCode), x => x.RoleCode.Contains(input.RoleCode!.Trim()))
            .WhereIF(!string.IsNullOrWhiteSpace(input.RoleName), x => x.RoleName.Contains(input.RoleName!.Trim()))
            .WhereIF(input.State is not null, x => x.State == input.State);

        if (!string.IsNullOrWhiteSpace(input.Sorting))
        {
            query = query.OrderBy(input.Sorting);
        }
        else
        {
            query = query.OrderBy(x => x.OrderNum, OrderByType.Desc);
        }

        var entities = await query.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);

        var items = entities.Select(x => new RbacRoleGetListOutputDto
        {
            Id = x.Id,
            RoleName = x.RoleName ?? string.Empty,
            RoleCode = x.RoleCode ?? string.Empty,
            Remark = x.Remark,
            DataScope = (int)x.DataScope,
            State = x.State,
            OrderNum = x.OrderNum
        }).ToList();

        await FillAccessPermissionsAsync(items);

        var pageSize = input.MaxResultCount <= 0 ? items.Count : input.MaxResultCount;
        var pageIndex = pageSize <= 0 ? 1 : PagedQueryConvention.PageIndexFromSkipCount(input.SkipCount);
        var totalPages = pageSize <= 0 ? 0 : (int)Math.Ceiling(total / (double)pageSize);

        return new PagedResultWithPageDto<RbacRoleGetListOutputDto>
        {
            PageIndex = pageIndex,
            PageSize = pageSize,
            TotalCount = total,
            TotalPages = totalPages,
            Items = items
        };
    }

    /// <inheritdoc />
    public async Task<RbacRoleGetOutputDto> GetAsync(Guid id)
    {
        var entity = await _roleRepository.GetSingleAsync(x => x.Id == id && x.IsDeleted == false);
        if (entity is null)
        {
            throw new UserFriendlyException("角色不存在");
        }

        var menuIds = await _dbContext.SqlSugarClient.Queryable<RoleMenuDbEntity>()
            .Where(x => x.RoleId == id.ToString())
            .Select(x => x.MenuId)
            .ToListAsync();

        var dto = new RbacRoleGetOutputDto
        {
            Id = entity.Id,
            RoleName = entity.RoleName ?? string.Empty,
            RoleCode = entity.RoleCode ?? string.Empty,
            Remark = entity.Remark,
            DataScope = (int)entity.DataScope,
            State = entity.State,
            OrderNum = entity.OrderNum,
            MenuIds = menuIds
        };
        await FillAccessPermissionsAsync(new List<RbacRoleGetListOutputDto> { dto });
        return dto;
    }

    /// <inheritdoc />
    [UnitOfWork]
    public async Task<RbacRoleGetOutputDto> CreateAsync([FromBody] RbacRoleCreateInputVo input)
    {
        var roleName = input.RoleName?.Trim();
        var roleCode = input.RoleCode?.Trim();
        if (string.IsNullOrWhiteSpace(roleName))
        {
            throw new UserFriendlyException("角色名称不能为空");
        }

        if (string.IsNullOrWhiteSpace(roleCode))
        {
            throw new UserFriendlyException("角色编码不能为空");
        }

        var isExist = await _roleRepository.IsAnyAsync(x => x.RoleCode == roleCode || x.RoleName == roleName);
        if (isExist)
        {
            throw new UserFriendlyException("角色名称或编码已存在");
        }

        var entity = new RoleAggregateRoot
        {
            RoleName = roleName,
            RoleCode = roleCode,
            Remark = input.Remark?.Trim(),
            DataScope = (Yi.Framework.Rbac.Domain.Shared.Enums.DataScopeEnum)input.DataScope,
            State = input.State,
            OrderNum = input.OrderNum ?? 0
        };
        EntityHelper.TrySetId(entity, () => GuidGenerator.Create());

        await _roleRepository.InsertAsync(entity);

        await ApplyRoleMenuBindingsAsync(entity.Id, input);

        return await GetAsync(entity.Id);
    }

    /// <inheritdoc />
    [UnitOfWork]
    public async Task<RbacRoleGetOutputDto> UpdateAsync(Guid id, [FromBody] RbacRoleUpdateInputVo input)
    {
        var entity = await _roleRepository.GetSingleAsync(x => x.Id == id && x.IsDeleted == false);
        if (entity is null)
        {
            throw new UserFriendlyException("角色不存在");
        }

        var roleName = input.RoleName?.Trim();
        var roleCode = input.RoleCode?.Trim();
        if (string.IsNullOrWhiteSpace(roleName))
        {
            throw new UserFriendlyException("角色名称不能为空");
        }

        if (string.IsNullOrWhiteSpace(roleCode))
        {
            throw new UserFriendlyException("角色编码不能为空");
        }

        var isExist = await _roleRepository._DbQueryable
            .Where(x => x.Id != entity.Id && x.IsDeleted == false)
            .AnyAsync(x => x.RoleCode == roleCode || x.RoleName == roleName);
        if (isExist)
        {
            throw new UserFriendlyException("角色名称或编码已存在");
        }

        entity.RoleName = roleName;
        entity.RoleCode = roleCode;
        entity.Remark = input.Remark?.Trim();
        entity.DataScope = (Yi.Framework.Rbac.Domain.Shared.Enums.DataScopeEnum)input.DataScope;
        entity.State = input.State;
        if (input.OrderNum is not null)
        {
            entity.OrderNum = input.OrderNum.Value;
        }

        await _roleRepository.UpdateAsync(entity);

        await ApplyRoleMenuBindingsAsync(entity.Id, input);

        return await GetAsync(entity.Id);
    }

    /// <summary>
    /// 新增/编辑时按 menuIds 或 accessPermissions 绑定角色菜单(二者都未传则不改绑定)
    /// </summary>
    private async Task ApplyRoleMenuBindingsAsync(Guid roleId, RbacRoleCreateInputVo input)
    {
        if (input.MenuIds is not null)
        {
            await SetRoleMenusAsync(roleId, input.MenuIds);
            return;
        }

        if (input.AccessPermissions is not null)
        {
            var menuIds = await ResolveMenuIdsFromAccessPermissionsAsync(input.AccessPermissions);
            await SetRoleMenusAsync(roleId, menuIds);
        }
    }

    private async Task SetRoleMenusAsync(Guid roleId, List<Guid> menuIds)
    {
        var distinct = menuIds?.Distinct().ToList() ?? new List<Guid>();
        await _roleMenuRepository.DeleteAsync(x => x.RoleId == roleId);

        if (distinct.Count == 0)
        {
            return;
        }

        var existMenuIds = await _menuRepository._DbQueryable
            .Where(x => x.IsDeleted == false)
            .Where(x => distinct.Contains(x.Id))
            .Select(x => x.Id)
            .ToListAsync();

        if (existMenuIds.Count == 0)
        {
            return;
        }

        var entities = existMenuIds.Select(menuId => new RoleMenuEntity
        {
            RoleId = roleId,
            MenuId = menuId
        }).ToList();

        await _roleMenuRepository.InsertRangeAsync(entities);
    }

    private async Task<List<Guid>> ResolveMenuIdsFromAccessPermissionsAsync(string accessPermissions)
    {
        var codes = accessPermissions
            .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
            .Where(c => !string.IsNullOrWhiteSpace(c))
            .Distinct(StringComparer.Ordinal)
            .ToList();

        if (codes.Count == 0)
        {
            return new List<Guid>();
        }

        var menus = await _menuRepository._DbQueryable
            .Where(m => m.IsDeleted == false && m.PermissionCode != null)
            .Where(m => codes.Contains(m.PermissionCode!))
            .Select(m => m.Id)
            .ToListAsync();

        return menus;
    }

    private async Task FillAccessPermissionsAsync(List<RbacRoleGetListOutputDto> items)
    {
        if (items.Count == 0)
        {
            return;
        }

        var map = await GetAccessPermissionsByRoleIdsAsync(items.Select(x => x.Id).ToList());
        foreach (var item in items)
        {
            item.AccessPermissions = map.GetValueOrDefault(item.Id, string.Empty);
        }
    }

    /// <summary>
    /// 按角色汇总已绑定菜单上的 PermissionCode(去重、英文逗号+空格拼接)
    /// </summary>
    private async Task<Dictionary<Guid, string>> GetAccessPermissionsByRoleIdsAsync(List<Guid> roleIds)
    {
        var result = roleIds.Distinct().ToDictionary(id => id, _ => string.Empty);
        if (result.Count == 0)
        {
            return result;
        }

        var distinctRoleIds = result.Keys.ToList();
        var links = await _roleMenuRepository._DbQueryable
            .Where(rm => distinctRoleIds.Contains(rm.RoleId))
            .Select(rm => new { rm.RoleId, rm.MenuId })
            .ToListAsync();
        if (links.Count == 0)
        {
            return result;
        }

        var menuIds = links.Select(x => x.MenuId).Distinct().ToList();
        var menus = await _menuRepository._DbQueryable
            .Where(m => menuIds.Contains(m.Id) && m.IsDeleted == false)
            .Select(m => new { m.Id, m.PermissionCode })
            .ToListAsync();
        var permByMenuId = menus.ToDictionary(x => x.Id, x => x.PermissionCode);

        var byRole = distinctRoleIds.ToDictionary(id => id, _ => new HashSet<string>(StringComparer.Ordinal));
        foreach (var link in links)
        {
            if (!permByMenuId.TryGetValue(link.MenuId, out var code) || string.IsNullOrWhiteSpace(code))
            {
                continue;
            }

            if (byRole.TryGetValue(link.RoleId, out var set))
            {
                set.Add(code.Trim());
            }
        }

        foreach (var kv in byRole)
        {
            if (kv.Value.Count == 0)
            {
                continue;
            }

            result[kv.Key] = string.Join(", ", kv.Value.OrderBy(x => x, StringComparer.Ordinal));
        }

        return result;
    }

    /// <inheritdoc />
    [UnitOfWork]
    public async Task DeleteAsync([FromBody] List<Guid> ids)
    {
        var idList = ids?.Distinct().ToList() ?? new List<Guid>();
        if (idList.Count == 0)
        {
            return;
        }

        await _roleMenuRepository.DeleteAsync(x => idList.Contains(x.RoleId));
        await _roleDeptRepository.DeleteAsync(x => idList.Contains(x.RoleId));
        await _userRoleRepository.DeleteAsync(x => idList.Contains(x.RoleId));

        // 角色表为软删(ISoftDelete)
        await _roleRepository.DeleteAsync(x => idList.Contains(x.Id));
    }
}