AccountManagementStructureHelper.cs 6.32 KB
using FoodLabeling.Application.Services.DbModels;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Users;
using Yi.Framework.Rbac.Domain.Shared.Consts;
using Yi.Framework.SqlSugarCore.Abstractions;

namespace FoodLabeling.Application.Helpers;

/// <summary>
/// Account Management 结构变更权限:平台管理员、Company Admin 角色,或具备 manage_people 访问权限的用户可维护 Region/Location。
/// </summary>
public static class AccountManagementStructureHelper
{
    /// <summary>
    /// 是否可新增/编辑/删除 Region(fl_group)与门店(location)。
    /// </summary>
    public static async Task EnsureCanMutateRegionAndLocationAsync(
        ICurrentUser currentUser,
        ISqlSugarDbContext dbContext)
    {
        if (await CanMutateRegionAndLocationAsync(currentUser, dbContext))
        {
            return;
        }

        throw new UserFriendlyException("You do not have permission to manage regions or locations.");
    }

    public static async Task<bool> CanMutateRegionAndLocationAsync(
        ICurrentUser currentUser,
        ISqlSugarDbContext dbContext)
    {
        if (ReportsRoleHelper.IsAdminRole(currentUser))
        {
            return true;
        }

        if (IsCompanyAdminFromClaims(currentUser))
        {
            return true;
        }

        if (currentUser.Id is null)
        {
            return false;
        }

        var db = dbContext.SqlSugarClient;
        if (await IsCompanyAdminRoleFromDbAsync(currentUser, db))
        {
            return true;
        }

        var accessCodes = await UserRoleAccessPermissionHelper.ResolveMergedAccessPermissionCodesAsync(
            db, currentUser.Id.Value);
        return UserRoleAccessPermissionHelper.CanManageRegionAndLocationByAccessPermissions(accessCodes);
    }

    /// <summary>
    /// 非平台管理员仅能在其数据范围内的公司下维护 Region。
    /// </summary>
    public static async Task EnsurePartnerIdAllowedForRegionMutationAsync(
        ICurrentUser currentUser,
        ISqlSugarDbContext dbContext,
        string partnerId)
    {
        if (ReportsRoleHelper.IsAdminRole(currentUser))
        {
            return;
        }

        var pid = partnerId?.Trim() ?? string.Empty;
        if (string.IsNullOrWhiteSpace(pid))
        {
            throw new UserFriendlyException("Parent company is required.");
        }

        var scope = await PartnerScopeHelper.ResolvePartnerScopeAsync(currentUser, dbContext);
        if (scope.IsUnrestricted || scope.AllowedPartnerIds.Any(x => string.Equals(x, pid, StringComparison.Ordinal)))
        {
            return;
        }

        throw new UserFriendlyException("You can only manage regions for your assigned company.");
    }

    /// <summary>
    /// 非平台管理员仅能在其数据范围内的公司下维护门店(location.Partner 为公司名称)。
    /// </summary>
    public static async Task EnsureLocationPartnerAllowedForMutationAsync(
        ICurrentUser currentUser,
        ISqlSugarDbContext dbContext,
        string? partnerName)
    {
        if (ReportsRoleHelper.IsAdminRole(currentUser))
        {
            return;
        }

        var name = partnerName?.Trim() ?? string.Empty;
        if (string.IsNullOrWhiteSpace(name))
        {
            return;
        }

        var scope = await PartnerScopeHelper.ResolvePartnerScopeAsync(currentUser, dbContext);
        if (scope.IsUnrestricted)
        {
            return;
        }

        if (scope.AllowedPartnerIds.Count == 0)
        {
            throw new UserFriendlyException("You can only manage locations for your assigned company.");
        }

        var allowedNames = await dbContext.SqlSugarClient.Queryable<FlPartnerDbEntity>()
            .Where(x => !x.IsDeleted && scope.AllowedPartnerIds.Contains(x.Id))
            .Select(x => x.PartnerName)
            .ToListAsync();

        if (allowedNames.Any(n =>
                !string.IsNullOrWhiteSpace(n) &&
                string.Equals(n.Trim(), name, StringComparison.OrdinalIgnoreCase)))
        {
            return;
        }

        throw new UserFriendlyException("You can only manage locations for your assigned company.");
    }

    private static async Task<bool> IsCompanyAdminRoleFromDbAsync(ICurrentUser currentUser, ISqlSugarClient db)
    {
        if (currentUser.Id is null)
        {
            return false;
        }

        var roleRows = await db.Ado.SqlQueryAsync<CompanyRoleRow>(
            @"SELECT r.RoleCode, r.RoleName
              FROM UserRole ur
              INNER JOIN Role r ON ur.RoleId = r.Id
              WHERE ur.UserId = @UserId AND r.IsDeleted = 0 AND r.State = 1",
            new { UserId = currentUser.Id.Value });

        return roleRows.Any(r =>
            IsCompanyAdminRoleToken(r.RoleCode) || IsCompanyAdminRoleToken(r.RoleName));
    }

    private static bool IsCompanyAdminFromClaims(ICurrentUser currentUser)
    {
        if (currentUser.Roles is not null)
        {
            foreach (var r in currentUser.Roles)
            {
                if (IsCompanyAdminRoleToken(r))
                {
                    return true;
                }
            }
        }

        var rolesClaim = currentUser.FindClaims(TokenTypeConst.Roles).Select(x => x.Value).FirstOrDefault();
        if (!string.IsNullOrWhiteSpace(rolesClaim))
        {
            foreach (var part in rolesClaim.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
            {
                if (IsCompanyAdminRoleToken(part))
                {
                    return true;
                }
            }
        }

        return false;
    }

    private static bool IsCompanyAdminRoleToken(string? value)
    {
        if (string.IsNullOrWhiteSpace(value))
        {
            return false;
        }

        var key = NormalizeRoleKey(value);
        if (key == "companyadmin" || key.Contains("companyadmin", StringComparison.Ordinal))
        {
            return true;
        }

        return value.Trim().Contains("partner", StringComparison.OrdinalIgnoreCase);
    }

    private static string NormalizeRoleKey(string value) =>
        new string(value.Trim().ToLowerInvariant().Where(c => char.IsLetterOrDigit(c)).ToArray());

    private sealed class CompanyRoleRow
    {
        public string? RoleCode { get; set; }

        public string? RoleName { get; set; }
    }
}