AllScopeBindingHelper.cs 12.8 KB
using FoodLabeling.Application.Services.DbModels;
using SqlSugar;

namespace FoodLabeling.Application.Helpers;

/// <summary>
/// 「适用全部 Region/Location/Company」:保存为 ALL(不写关联快照),查询时动态包含后续新增的 Region/Location。
/// </summary>
public static class AllScopeBindingHelper
{
    public const string ScopeAll = "ALL";
    public const string ScopeSpecified = "SPECIFIED";

    public const string AllRegionsDisplay = FoodLabelingDisplayConsts.AllRegion;
    public const string AllLocationsDisplay = FoodLabelingDisplayConsts.AllLocation;
    public const string AllCompaniesDisplay = "All Companies";

    /// <summary>选中 Id 是否覆盖当前上下文中全部可选项(用于将「全选」规范为 ALL)。</summary>
    public static bool IsFullIdSelection(IReadOnlyList<string> selected, IReadOnlyList<string> universe)
    {
        if (universe.Count == 0)
        {
            return selected.Count == 0;
        }

        var universeSet = new HashSet<string>(
            universe.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()),
            StringComparer.OrdinalIgnoreCase);
        var selectedSet = new HashSet<string>(
            selected.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()),
            StringComparer.OrdinalIgnoreCase);

        if (selectedSet.Count != universeSet.Count)
        {
            return false;
        }

        return selectedSet.SetEquals(universeSet);
    }

    /// <summary>
    /// 解析维度类型:显式 ALL、空数组 ALL、或 Id 列表全选时视为 ALL。
    /// 入参显式 <see cref="ScopeSpecified"/> 时保留 SPECIFIED 并落库全部 Id,不因「当前上下文全选」折叠为 ALL。
    /// </summary>
    public static string ResolveDimensionType(
        string? declaredType,
        IReadOnlyList<string> ids,
        bool hasArrayInPayload,
        bool isFullSelection)
    {
        if (IsDeclaredSpecified(declaredType))
        {
            return ScopeSpecified;
        }

        if (isFullSelection || (ids.Count == 0 && IsDeclaredAll(declaredType)))
        {
            return ScopeAll;
        }

        if (ids.Count > 0)
        {
            return ScopeSpecified;
        }

        if (hasArrayInPayload && IsDeclaredAll(declaredType))
        {
            return ScopeAll;
        }

        var normalized = (declaredType ?? ScopeAll).Trim().ToUpperInvariant();
        return normalized == ScopeSpecified ? ScopeSpecified : ScopeAll;
    }

    public static bool IsDeclaredAll(string? type) =>
        string.Equals((type ?? ScopeAll).Trim(), ScopeAll, StringComparison.OrdinalIgnoreCase);

    public static bool IsDeclaredSpecified(string? type) =>
        string.Equals((type ?? string.Empty).Trim(), ScopeSpecified, StringComparison.OrdinalIgnoreCase);

    /// <summary>全部 Company(<c>fl_partner.Id</c>)。</summary>
    public static async Task<List<string>> ResolveAllPartnerIdsAsync(ISqlSugarClient db)
    {
        var rows = await db.Queryable<FlPartnerDbEntity>()
            .Where(x => !x.IsDeleted)
            .Select(x => x.Id)
            .ToListAsync();
        return LocationScopeBindingHelper.NormalizeIds(rows);
    }

    /// <summary>全部 Region;可按 Company 限定。</summary>
    public static async Task<List<string>> ResolveAllRegionIdsAsync(
        ISqlSugarClient db,
        IReadOnlyList<string>? partnerIds)
    {
        var query = db.Queryable<FlGroupDbEntity>().Where(x => !x.IsDeleted);
        var pids = LocationScopeBindingHelper.NormalizeIds(partnerIds);
        if (pids.Count > 0)
        {
            query = query.Where(x => pids.Contains(x.PartnerId));
        }

        var rows = await query.Select(x => x.Id).ToListAsync();
        return LocationScopeBindingHelper.NormalizeIds(rows);
    }

    /// <summary>全部门店;可按 Company / Region 限定。</summary>
    public static async Task<List<string>> ResolveAllLocationIdsAsync(
        ISqlSugarClient db,
        IReadOnlyList<string>? partnerIds,
        IReadOnlyList<string>? regionIds)
    {
        var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
            db,
            partnerIds,
            regionIds,
            null);
        return LocationScopeBindingHelper.NormalizeIds(merged);
    }

    /// <summary>Company 维度是否应存为 ALL(含「勾选了全部 Company」)。</summary>
    public static async Task<(string Type, List<string> Ids)> NormalizePartnerScopeAsync(
        ISqlSugarClient db,
        string? declaredType,
        IReadOnlyList<string>? partnerIds,
        IReadOnlyList<string>? companyIds,
        bool hasArrayInPayload)
    {
        var ids = LabelEntityPartnerScopeHelper.NormalizePartnerIds(partnerIds, companyIds);
        var allPartners = await ResolveAllPartnerIdsAsync(db);
        // 勾选当前全部 Company(含前端仍传 SPECIFIED + 全量 Id)→ 归档 ALL,后续新增 Company 动态适用
        var isFull = ids.Count > 0 && IsFullIdSelection(ids, allPartners);
        var type = ResolveDimensionType(declaredType, ids, hasArrayInPayload, isFull);
        return (type, type == ScopeSpecified ? ids : new List<string>());
    }

    /// <summary>Region 维度是否应存为 ALL。</summary>
    public static async Task<(string Type, List<string> Ids)> NormalizeRegionScopeAsync(
        ISqlSugarClient db,
        string? declaredType,
        IReadOnlyList<string> regionIds,
        bool hasArrayInPayload,
        IReadOnlyList<string>? partnerIdsForContext)
    {
        var ids = LocationScopeBindingHelper.NormalizeIds(regionIds);
        var allRegions = await ResolveAllRegionIdsAsync(db, partnerIdsForContext);
        var isFull = ids.Count > 0 && IsFullIdSelection(ids, allRegions);
        var type = ResolveDimensionType(declaredType, ids, hasArrayInPayload, isFull);
        return (type, type == ScopeSpecified ? ids : new List<string>());
    }

    /// <summary>Location 维度是否应存为 ALL。</summary>
    public static async Task<(string Type, List<string> Ids)> NormalizeLocationScopeAsync(
        ISqlSugarClient db,
        string? declaredType,
        IReadOnlyList<string> locationIds,
        bool hasArrayInPayload,
        IReadOnlyList<string>? partnerIdsForContext,
        IReadOnlyList<string>? regionIdsForContext)
    {
        var ids = LocationScopeBindingHelper.NormalizeIds(locationIds);
        var allLocations = await ResolveAllLocationIdsAsync(db, partnerIdsForContext, regionIdsForContext);
        var isFull = ids.Count > 0 && IsFullIdSelection(ids, allLocations);
        var type = ResolveDimensionType(declaredType, ids, hasArrayInPayload, isFull);
        return (type, type == ScopeSpecified ? ids : new List<string>());
    }

    /// <summary>
    /// 由 PartnerId / Region / Location 反推 Company 上下文,用于判断「Select All」是否覆盖该公司全部门店。
    /// </summary>
    public static async Task<List<string>?> ResolvePartnerContextFromScopeAsync(
        ISqlSugarClient db,
        string? partnerId,
        IReadOnlyList<string>? regionOrGroupIds,
        IReadOnlyList<string>? locationIds)
    {
        if (!string.IsNullOrWhiteSpace(partnerId))
        {
            return new List<string> { partnerId.Trim() };
        }

        var regionIds = LocationScopeBindingHelper.NormalizeIds(regionOrGroupIds);
        if (regionIds.Count > 0)
        {
            var rows = await db.Queryable<FlGroupDbEntity>()
                .Where(g => !g.IsDeleted && regionIds.Contains(g.Id))
                .Select(g => g.PartnerId)
                .ToListAsync();
            var normalized = LocationScopeBindingHelper.NormalizeIds(rows);
            return normalized.Count > 0 ? normalized : null;
        }

        var locIds = LocationScopeBindingHelper.NormalizeIds(locationIds);
        if (locIds.Count == 0)
        {
            return null;
        }

        var fromLoc = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, locIds);
        var fromLocNormalized = LocationScopeBindingHelper.NormalizeIds(fromLoc);
        return fromLocNormalized.Count > 0 ? fromLocNormalized : null;
    }

    /// <summary>
    /// 标签类型/分类/多选项/产品分类:Region+Location 合并后是否应视为 ALL。
    /// </summary>
    public static async Task<bool> ShouldTreatMergedLocationScopeAsAllAsync(
        ISqlSugarClient db,
        string? declaredAvailabilityType,
        IReadOnlyList<string>? regionIds,
        IReadOnlyList<string>? locationIds,
        bool hasScopeArrays,
        IReadOnlyList<string>? partnerIdsForContext)
    {
        if (IsDeclaredAll(declaredAvailabilityType)
            && LocationScopeBindingHelper.NormalizeIds(regionIds).Count == 0
            && LocationScopeBindingHelper.NormalizeIds(locationIds).Count == 0)
        {
            return true;
        }

        if (hasScopeArrays
            && LocationScopeBindingHelper.NormalizeIds(regionIds).Count == 0
            && LocationScopeBindingHelper.NormalizeIds(locationIds).Count == 0
            && IsDeclaredAll(declaredAvailabilityType))
        {
            return true;
        }

        // 前端 Select All 常传 SPECIFIED + 当前全量 Id:覆盖 Company 上下文全部门店时归档 ALL
        var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
            db,
            (IReadOnlyList<string>?)null,
            regionIds,
            locationIds);
        if (merged.Count == 0)
        {
            return false;
        }

        var partnerContext = LocationScopeBindingHelper.NormalizeIds(partnerIdsForContext);
        if (partnerContext.Count == 0)
        {
            partnerContext = await ResolvePartnerContextFromScopeAsync(db, null, regionIds, locationIds)
                             ?? new List<string>();
        }

        var allLocations = await ResolveAllLocationIdsAsync(
            db,
            partnerContext.Count > 0 ? partnerContext : null,
            null);
        return IsFullIdSelection(merged, allLocations);
    }

    /// <summary>产品门店范围是否应存为 ALL(不写 <c>fl_location_product</c> 快照)。</summary>
    public static async Task<bool> ShouldTreatProductLocationScopeAsAllAsync(
        ISqlSugarClient db,
        string? declaredAvailabilityType,
        string? partnerId,
        IReadOnlyList<string>? groupIds,
        IReadOnlyList<string>? locationIds,
        bool hasScopePayload)
    {
        if (IsDeclaredAll(declaredAvailabilityType)
            && string.IsNullOrWhiteSpace(partnerId)
            && LocationScopeBindingHelper.NormalizeIds(groupIds).Count == 0
            && LocationScopeBindingHelper.NormalizeIds(locationIds).Count == 0)
        {
            return true;
        }

        if (!hasScopePayload)
        {
            return false;
        }

        var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
            db,
            partnerId,
            groupIds,
            locationIds);
        if (merged.Count == 0)
        {
            return false;
        }

        // 未传 partnerId 时从 Region/门店反推 Company,避免用「全系统门店」误判导致无法归档 ALL
        var partnerContext = await ResolvePartnerContextFromScopeAsync(
            db, partnerId, groupIds, locationIds);
        var allLocations = await ResolveAllLocationIdsAsync(db, partnerContext, null);
        return IsFullIdSelection(merged, allLocations);
    }

    /// <summary>
    /// 详情回显:AvailabilityType/Applied*=ALL 时不落快照,展开为当前可见 Company/Region/Location 全集(对齐 Label)。
    /// <paramref name="scopedLocationIds"/> 为 null 表示管理员全量;非 null 为可见门店范围。
    /// </summary>
    public static async Task<(List<string> PartnerIds, List<string> RegionIds, List<string> LocationIds)>
        ResolveDisplayIdsForAllScopeAsync(
            ISqlSugarClient db,
            IReadOnlyList<string>? scopedLocationIds,
            IReadOnlyList<string>? preferredPartnerIds = null)
    {
        var preferred = LocationScopeBindingHelper.NormalizeIds(preferredPartnerIds);

        if (scopedLocationIds is null)
        {
            var partners = preferred.Count > 0 ? preferred : await ResolveAllPartnerIdsAsync(db);
            var partnerContext = partners.Count > 0 ? partners : null;
            var regions = await ResolveAllRegionIdsAsync(db, partnerContext);
            var locations = await ResolveAllLocationIdsAsync(db, partnerContext, null);
            return (partners, regions, locations);
        }

        var locs = LocationScopeBindingHelper.NormalizeIds(scopedLocationIds);
        var regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, locs);
        var partnerIds = preferred.Count > 0
            ? preferred
            : await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, locs);
        return (partnerIds, regionIds, locs);
    }
}