LabelRegionScopeHelper.cs 12.5 KB
using FoodLabeling.Application.Services.DbModels;
using SqlSugar;
using Volo.Abp;
using Volo.Abp.Guids;

namespace FoodLabeling.Application.Helpers;

/// <summary>
/// 标签适用 Region(<c>fl_group.Id</c>)范围:支持全选(ALL)、单选/多选(SPECIFIED + <c>fl_label_region</c>)。
/// </summary>
public static class LabelRegionScopeHelper
{
    public const string AppliedRegionAll = "ALL";
    public const string AppliedRegionSpecified = "SPECIFIED";
    public const string AllRegionsDisplay = "All Regions";

    public sealed class LabelRegionScopeSaveResult
    {
        public string AppliedRegionType { get; init; } = AppliedRegionSpecified;

        public List<string> RegionIds { get; init; } = new();

        public string? LocationId { get; init; }
    }

    public sealed class LabelRegionScopeDisplay
    {
        public string Region { get; init; } = string.Empty;

        public List<string> RegionIds { get; init; } = new();

        public List<string> GroupIds { get; init; } = new();
    }

    /// <summary>
    /// 解析新增/编辑入参中的 Region 范围与落库门店 Id。
    /// </summary>
    public static async Task<LabelRegionScopeSaveResult> ResolveScopeForSaveAsync(
        ISqlSugarClient db,
        string? appliedRegionType,
        IReadOnlyList<string>? regionIds,
        IReadOnlyList<string>? groupIds,
        string? locationId,
        IReadOnlyList<string>? locationIds)
    {
        var mergedRegionIds = NormalizeRegionIds(regionIds, groupIds);
        var type = (appliedRegionType ?? AppliedRegionAll).Trim().ToUpperInvariant();
        var hasScopeArrays = regionIds is not null || groupIds is not null || locationIds is not null;

        if (mergedRegionIds.Count > 0)
        {
            type = AppliedRegionSpecified;
        }
        else if (hasScopeArrays && string.Equals(type, AppliedRegionAll, StringComparison.OrdinalIgnoreCase))
        {
            type = AppliedRegionAll;
        }

        if (type != AppliedRegionAll && type != AppliedRegionSpecified)
        {
            throw new UserFriendlyException("适用 Region 范围不合法(ALL/SPECIFIED)");
        }

        if (string.Equals(type, AppliedRegionAll, StringComparison.OrdinalIgnoreCase))
        {
            var loc = locationId?.Trim();
            if (!string.IsNullOrWhiteSpace(loc))
            {
                await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, new List<string> { loc });
            }

            return new LabelRegionScopeSaveResult
            {
                AppliedRegionType = AppliedRegionAll,
                RegionIds = new List<string>(),
                LocationId = string.IsNullOrWhiteSpace(loc) ? null : loc
            };
        }

        if (mergedRegionIds.Count == 0 && string.IsNullOrWhiteSpace(locationId))
        {
            throw new UserFriendlyException("请选择适用 Region(regionIds),或指定门店 locationId");
        }

        await ValidateRegionIdsExistAsync(db, mergedRegionIds);

        var explicitLocationId = locationId?.Trim();
        var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds);
        var mergedLocations = await LocationScopeBindingHelper.MergeToLocationIdsAsync(
            db, (IReadOnlyList<string>?)null, mergedRegionIds, explicitLocationIds);

        string? resolvedLocationId;
        if (!string.IsNullOrWhiteSpace(explicitLocationId))
        {
            if (mergedLocations.Count > 0 && !mergedLocations.Contains(explicitLocationId, StringComparer.Ordinal))
            {
                throw new UserFriendlyException("所选门店不在指定 Region 范围内");
            }

            await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, new List<string> { explicitLocationId });
            resolvedLocationId = explicitLocationId;
        }
        else if (mergedLocations.Count == 0)
        {
            throw new UserFriendlyException("所选 Region 下无有效门店,请检查 Region 或补充 locationId");
        }
        else if (mergedLocations.Count == 1)
        {
            resolvedLocationId = mergedLocations[0];
        }
        else
        {
            resolvedLocationId = mergedLocations[0];
        }

        return new LabelRegionScopeSaveResult
        {
            AppliedRegionType = AppliedRegionSpecified,
            RegionIds = mergedRegionIds,
            LocationId = resolvedLocationId
        };
    }

    public static async Task SaveLabelRegionsAsync(
        ISqlSugarClient db,
        IGuidGenerator guidGenerator,
        string labelId,
        string appliedRegionType,
        IReadOnlyList<string> regionIds,
        string? currentUserId,
        DateTime now)
    {
        var schema = await LabelRegionSchemaHelper.GetStatusAsync(db);
        if (!schema.HasLabelRegionTable)
        {
            return;
        }

        await db.Deleteable<FlLabelRegionDbEntity>()
            .Where(x => x.LabelId == labelId)
            .ExecuteCommandAsync();

        if (!string.Equals(appliedRegionType, AppliedRegionSpecified, StringComparison.OrdinalIgnoreCase)
            || regionIds.Count == 0)
        {
            return;
        }

        var rows = regionIds.Select(gid => new FlLabelRegionDbEntity
        {
            Id = guidGenerator.Create().ToString(),
            LabelId = labelId,
            GroupId = gid,
            CreationTime = now,
            CreatorId = currentUserId
        }).ToList();

        await db.Insertable(rows).ExecuteCommandAsync();
    }

    public static async Task<List<string>> GetRegionIdsForLabelAsync(ISqlSugarClient db, string labelId)
    {
        var schema = await LabelRegionSchemaHelper.GetStatusAsync(db);
        if (!schema.HasLabelRegionTable)
        {
            return new List<string>();
        }

        return LocationScopeBindingHelper.NormalizeIds(
            await db.Queryable<FlLabelRegionDbEntity>()
                .Where(x => x.LabelId == labelId)
                .Select(x => x.GroupId)
                .ToListAsync());
    }

    public static async Task<LabelRegionScopeDisplay> BuildScopeDisplayAsync(
        ISqlSugarClient db,
        string labelId,
        string? appliedRegionType,
        string? locationId)
    {
        if (string.Equals(appliedRegionType, AppliedRegionAll, StringComparison.OrdinalIgnoreCase))
        {
            return new LabelRegionScopeDisplay
            {
                Region = AllRegionsDisplay,
                RegionIds = new List<string>(),
                GroupIds = new List<string>()
            };
        }

        var regionIds = await GetRegionIdsForLabelAsync(db, labelId);
        if (regionIds.Count == 0 && !string.IsNullOrWhiteSpace(locationId))
        {
            regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(
                db, new List<string> { locationId.Trim() });
        }

        if (regionIds.Count == 0)
        {
            return new LabelRegionScopeDisplay
            {
                Region = "无",
                RegionIds = new List<string>(),
                GroupIds = new List<string>()
            };
        }

        var names = await db.Queryable<FlGroupDbEntity>()
            .Where(g => !g.IsDeleted && regionIds.Contains(g.Id))
            .OrderBy(g => g.GroupName)
            .Select(g => g.GroupName)
            .ToListAsync();
        var regionText = names.Count > 0
            ? string.Join(", ", names.Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!.Trim()).Distinct())
            : "无";

        return new LabelRegionScopeDisplay
        {
            Region = regionText,
            RegionIds = regionIds,
            GroupIds = regionIds
        };
    }

    /// <summary>
    /// 列表/筛选:标签是否落在指定 Region(<c>fl_group.Id</c>)或关联门店范围内。
    /// </summary>
    public static ISugarQueryable<FlLabelDbEntity> ApplyLabelRegionListFilter(
        ISugarQueryable<FlLabelDbEntity> query,
        string groupId,
        List<string>? scopedLocationIds,
        LabelRegionSchemaHelper.LabelRegionSchemaStatus schema)
    {
        var gid = groupId.Trim();
        if (scopedLocationIds is not { Count: > 0 })
        {
            return query.Where(_ => false);
        }

        if (!schema.HasLabelRegionTable && !schema.HasAppliedRegionTypeColumn)
        {
            return query.Where(l => scopedLocationIds.Contains(l.LocationId));
        }

        if (!schema.HasLabelRegionTable)
        {
            return query.Where(
                "(AppliedRegionType = @all OR LocationId IN (@locs))",
                new { all = AppliedRegionAll, locs = scopedLocationIds });
        }

        if (!schema.HasAppliedRegionTypeColumn)
        {
            return query.Where(l =>
                SqlFunc.Subqueryable<FlLabelRegionDbEntity>()
                    .Where(lr => lr.LabelId == l.Id && lr.GroupId == gid)
                    .Any() ||
                scopedLocationIds.Contains(l.LocationId));
        }

        return query.Where(
            """
            (AppliedRegionType = @all
             OR EXISTS (SELECT 1 FROM fl_label_region lr WHERE lr.LabelId = fl_label.Id AND lr.GroupId = @gid)
             OR LocationId IN (@locs))
            """,
            new { all = AppliedRegionAll, gid, locs = scopedLocationIds });
    }

    /// <summary>
    /// 解析门店所属的 Region Id(用于 App 树、打印校验)。
    /// </summary>
    public static async Task<List<string>> ResolveGroupIdsForLocationAsync(ISqlSugarClient db, string locationId)
    {
        return await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(
            db, new List<string> { locationId.Trim() });
    }

    /// <summary>
    /// 标签是否适用于指定门店(LocationId 命中、Region 关联或 ALL)。
    /// </summary>
    /// <summary>
    /// 校验标签是否适用于当前门店(否则抛出友好异常)。
    /// </summary>
    public static async Task EnsureLabelAppliesToLocationAsync(
        ISqlSugarClient db,
        string labelId,
        string locationId)
    {
        var label = await db.Queryable<FlLabelDbEntity>()
            .FirstAsync(x => !x.IsDeleted && x.Id == labelId);
        var groupIds = await ResolveGroupIdsForLocationAsync(db, locationId);
        if (!await LabelAppliesToLocationAsync(db, label, locationId, groupIds))
        {
            throw new UserFriendlyException("该标签不属于当前门店或未配置适用 Region");
        }
    }

    public static async Task<bool> LabelAppliesToLocationAsync(
        ISqlSugarClient db,
        FlLabelDbEntity label,
        string locationId,
        List<string> groupIdsForLocation)
    {
        var appliedType = await LabelRegionSchemaHelper.GetAppliedRegionTypeForLabelAsync(
            db, label.Id, label.AppliedRegionType);

        if (string.Equals(appliedType, AppliedRegionAll, StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        if (!string.IsNullOrWhiteSpace(label.LocationId) &&
            string.Equals(label.LocationId.Trim(), locationId, StringComparison.OrdinalIgnoreCase))
        {
            return true;
        }

        if (groupIdsForLocation.Count == 0)
        {
            return false;
        }

        var schema = await LabelRegionSchemaHelper.GetStatusAsync(db);
        if (!schema.HasLabelRegionTable)
        {
            return false;
        }

        return await db.Queryable<FlLabelRegionDbEntity>()
            .AnyAsync(lr => lr.LabelId == label.Id && groupIdsForLocation.Contains(lr.GroupId));
    }

    private static List<string> NormalizeRegionIds(
        IReadOnlyList<string>? regionIds,
        IReadOnlyList<string>? groupIds)
    {
        var merged = new HashSet<string>(StringComparer.Ordinal);
        foreach (var id in LocationScopeBindingHelper.NormalizeIds(regionIds))
        {
            merged.Add(id);
        }

        foreach (var id in LocationScopeBindingHelper.NormalizeIds(groupIds))
        {
            merged.Add(id);
        }

        return merged.OrderBy(x => x, StringComparer.Ordinal).ToList();
    }

    private static async Task ValidateRegionIdsExistAsync(ISqlSugarClient db, List<string> regionIds)
    {
        if (regionIds.Count == 0)
        {
            return;
        }

        var count = await db.Queryable<FlGroupDbEntity>()
            .Where(g => !g.IsDeleted && regionIds.Contains(g.Id))
            .CountAsync();
        if (count != regionIds.Count)
        {
            throw new UserFriendlyException("存在无效的 Region Id,请刷新后重试");
        }
    }
}