UsAppAuthScopeHelper.cs 9.04 KB
using System.Text.Json;
using FoodLabeling.Application.Contracts;
using FoodLabeling.Application.Contracts.Dtos.AuthScope;
using FoodLabeling.Application.Contracts.Dtos.UsAppAuth;
using FoodLabeling.Application.Services.DbModels;
using FoodLabeling.Domain.Entities;
using Microsoft.Extensions.Caching.Distributed;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Users;
using Yi.Framework.SqlSugarCore.Abstractions;

namespace FoodLabeling.Application.Helpers;

/// <summary>
/// App 管理员 Company → Region → Location 级联选店(与 5-26 auth-scope / us-app-auth 文档一致)。
/// </summary>
public static class UsAppAuthScopeHelper
{
    private const string CacheKeyPrefix = "FoodLabeling:UsAppAdminScope:";

    public static void EnsureAdminAppToken(ICurrentUser currentUser)
    {
        if (currentUser.Id is null)
        {
            throw new UserFriendlyException("用户未登录");
        }

        var kind = currentUser.FindClaim(UsAppJwtClaims.ClientKind)?.Value;
        if (!string.Equals(kind, UsAppJwtClaims.ClientKindUsApp, StringComparison.Ordinal))
        {
            throw new UserFriendlyException("请使用 App 登录令牌调用该接口");
        }

        if (!ReportsRoleHelper.IsAdminRole(currentUser))
        {
            throw new UserFriendlyException("仅管理员可使用公司/区域/门店筛选接口");
        }
    }

    public static async Task<List<AuthScopeCompanyOptionDto>> ListCompaniesAsync(ISqlSugarClient db)
    {
        return await db.Queryable<FlPartnerDbEntity>()
            .Where(x => !x.IsDeleted && x.State)
            .OrderBy(x => x.PartnerName)
            .Select(x => new AuthScopeCompanyOptionDto
            {
                Id = x.Id,
                PartnerName = x.PartnerName ?? string.Empty,
                State = x.State,
            })
            .ToListAsync();
    }

    public static async Task<List<AuthScopeRegionOptionDto>> ListRegionsAsync(
        ISqlSugarClient db,
        string partnerId)
    {
        var pid = partnerId.Trim();
        if (string.IsNullOrEmpty(pid))
        {
            throw new UserFriendlyException("请选择公司");
        }

        return await db.Queryable<FlGroupDbEntity>()
            .Where(x => !x.IsDeleted && x.State && x.PartnerId == pid)
            .OrderBy(x => x.GroupName)
            .Select(x => new AuthScopeRegionOptionDto
            {
                Id = x.Id,
                GroupName = x.GroupName ?? string.Empty,
                PartnerId = x.PartnerId,
                State = x.State,
            })
            .ToListAsync();
    }

    public static async Task<List<AuthScopeLocationOptionDto>> ListLocationsAsync(
        ISqlSugarClient db,
        string partnerId,
        string groupId)
    {
        var ids = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync(
            db,
            partnerId,
            groupId,
            null);
        if (ids is null || ids.Count == 0)
        {
            return new List<AuthScopeLocationOptionDto>();
        }

        var g = await db.Queryable<FlGroupDbEntity>()
            .FirstAsync(x => !x.IsDeleted && x.Id == groupId.Trim());
        var partner = await db.Queryable<FlPartnerDbEntity>()
            .FirstAsync(x => !x.IsDeleted && x.Id == partnerId.Trim());

        var locations = (await db.Queryable<LocationAggregateRoot>()
            .Where(x => !x.IsDeleted && ids.Contains(x.Id.ToString()))
            .OrderBy(x => x.OrderNum)
            .ToListAsync())
            .OrderBy(x => x.OrderNum)
            .ThenBy(x => x.LocationName, StringComparer.OrdinalIgnoreCase)
            .ToList();

        return locations.Select(x => new AuthScopeLocationOptionDto
        {
            Id = x.Id.ToString(),
            LocationCode = x.LocationCode ?? string.Empty,
            LocationName = x.LocationName ?? string.Empty,
            FullAddress = BuildFullAddress(x),
            State = x.State,
            PartnerId = partner?.Id ?? partnerId.Trim(),
            GroupId = g?.Id ?? groupId.Trim(),
            GroupName = g?.GroupName ?? string.Empty,
        }).ToList();
    }

    public static async Task<AuthScopeSelectLocationOutputDto> SelectLocationAsync(
        ISqlSugarClient db,
        IDistributedCache cache,
        Guid userId,
        UsAppSelectAdminScopeLocationInputVo input)
    {
        var partnerId = input.PartnerId?.Trim() ?? string.Empty;
        var groupId = input.GroupId?.Trim() ?? string.Empty;
        var locationId = input.LocationId?.Trim() ?? string.Empty;
        if (string.IsNullOrEmpty(partnerId) || string.IsNullOrEmpty(groupId) || string.IsNullOrEmpty(locationId))
        {
            throw new UserFriendlyException("请选择公司、区域和门店");
        }

        var scopedIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync(
            db,
            partnerId,
            groupId,
            null);
        if (scopedIds is null || !scopedIds.Contains(locationId))
        {
            throw new UserFriendlyException("门店与所选公司/区域不匹配");
        }

        if (!Guid.TryParse(locationId, out var locationGuid))
        {
            throw new UserFriendlyException("无效的门店标识");
        }

        var loc = await db.Queryable<LocationAggregateRoot>()
            .FirstAsync(x => !x.IsDeleted && x.Id == locationGuid);
        if (loc is null)
        {
            throw new UserFriendlyException("门店不存在或已删除");
        }

        var partner = await db.Queryable<FlPartnerDbEntity>()
            .FirstAsync(x => !x.IsDeleted && x.Id == partnerId);
        var group = await db.Queryable<FlGroupDbEntity>()
            .FirstAsync(x => !x.IsDeleted && x.Id == groupId);

        var bound = new UsAppBoundLocationDto
        {
            Id = loc.Id.ToString(),
            LocationCode = loc.LocationCode ?? string.Empty,
            LocationName = loc.LocationName ?? string.Empty,
            FullAddress = BuildFullAddress(loc),
            State = loc.State,
        };

        var cacheItem = new UsAppAdminScopeCacheItem
        {
            PartnerId = partnerId,
            PartnerName = partner?.PartnerName?.Trim() ?? string.Empty,
            GroupId = groupId,
            GroupName = group?.GroupName?.Trim() ?? string.Empty,
            Location = bound,
        };

        await SetAdminScopeCacheAsync(cache, userId, cacheItem);

        return new AuthScopeSelectLocationOutputDto
        {
            PartnerId = cacheItem.PartnerId,
            PartnerName = cacheItem.PartnerName,
            GroupId = cacheItem.GroupId,
            GroupName = cacheItem.GroupName,
            Location = bound,
        };
    }

    public static async Task<UsAppAdminScopeCacheItem?> GetAdminScopeCacheAsync(
        IDistributedCache cache,
        Guid userId)
    {
        var raw = await cache.GetStringAsync(CacheKeyPrefix + userId);
        if (string.IsNullOrWhiteSpace(raw))
        {
            return null;
        }

        try
        {
            return JsonSerializer.Deserialize<UsAppAdminScopeCacheItem>(raw);
        }
        catch
        {
            return null;
        }
    }

    public static Task RemoveAdminScopeCacheAsync(IDistributedCache cache, Guid userId) =>
        cache.RemoveAsync(CacheKeyPrefix + userId);

    private static async Task SetAdminScopeCacheAsync(
        IDistributedCache cache,
        Guid userId,
        UsAppAdminScopeCacheItem item)
    {
        var json = JsonSerializer.Serialize(item);
        await cache.SetStringAsync(
            CacheKeyPrefix + userId,
            json,
            new DistributedCacheEntryOptions
            {
                AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24),
            });
    }

    private static string BuildFullAddress(LocationAggregateRoot loc)
    {
        var street = loc.Street?.Trim();
        var city = loc.City?.Trim();
        var state = loc.StateCode?.Trim();
        var zip = loc.ZipCode?.Trim();

        var line2Parts = new List<string>();
        if (!string.IsNullOrEmpty(city))
        {
            line2Parts.Add(city);
        }

        if (!string.IsNullOrEmpty(state))
        {
            line2Parts.Add(state);
        }

        var line2 = line2Parts.Count > 0 ? string.Join(", ", line2Parts) : string.Empty;
        if (!string.IsNullOrEmpty(zip))
        {
            line2 = string.IsNullOrEmpty(line2) ? zip : $"{line2} {zip}";
        }

        var segments = new List<string>();
        if (!string.IsNullOrEmpty(street))
        {
            segments.Add(street);
        }

        if (!string.IsNullOrEmpty(line2))
        {
            segments.Add(line2);
        }

        return segments.Count == 0 ? "无" : string.Join(", ", segments);
    }

    public sealed class UsAppAdminScopeCacheItem
    {
        public string PartnerId { get; set; } = string.Empty;

        public string PartnerName { get; set; } = string.Empty;

        public string GroupId { get; set; } = string.Empty;

        public string GroupName { get; set; } = string.Empty;

        public UsAppBoundLocationDto Location { get; set; } = new();
    }
}