using FoodLabeling.Application.Services.DbModels; using SqlSugar; namespace FoodLabeling.Application.Helpers; /// /// 「适用全部 Region/Location/Company」:保存为 ALL(不写关联快照),查询时动态包含后续新增的 Region/Location。 /// 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"; /// 选中 Id 是否覆盖当前上下文中全部可选项(用于将「全选」规范为 ALL)。 public static bool IsFullIdSelection(IReadOnlyList selected, IReadOnlyList universe) { if (universe.Count == 0) { return selected.Count == 0; } var universeSet = new HashSet( universe.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()), StringComparer.OrdinalIgnoreCase); var selectedSet = new HashSet( selected.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x.Trim()), StringComparer.OrdinalIgnoreCase); if (selectedSet.Count != universeSet.Count) { return false; } return selectedSet.SetEquals(universeSet); } /// /// 解析维度类型:显式 ALL、空数组 ALL、或 Id 列表全选时视为 ALL。 /// 入参显式 时保留 SPECIFIED 并落库全部 Id,不因「当前上下文全选」折叠为 ALL。 /// public static string ResolveDimensionType( string? declaredType, IReadOnlyList 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); /// 全部 Company(fl_partner.Id)。 public static async Task> ResolveAllPartnerIdsAsync(ISqlSugarClient db) { var rows = await db.Queryable() .Where(x => !x.IsDeleted) .Select(x => x.Id) .ToListAsync(); return LocationScopeBindingHelper.NormalizeIds(rows); } /// 全部 Region;可按 Company 限定。 public static async Task> ResolveAllRegionIdsAsync( ISqlSugarClient db, IReadOnlyList? partnerIds) { var query = db.Queryable().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); } /// /// 全部门店;可按 Company / Region 限定。 /// 同时传 Company 与 Region 时取交集(该 Region 下且属于这些 Company 的门店), /// 禁止并集——否则回显「区内全选」无法折叠为 locationIds:["ALL"]。 /// public static async Task> ResolveAllLocationIdsAsync( ISqlSugarClient db, IReadOnlyList? partnerIds, IReadOnlyList? regionIds) { var partners = LocationScopeBindingHelper.NormalizeIds(partnerIds); var regions = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); if (regions.Count > 0) { var fromRegions = await LocationScopeBindingHelper.ResolveLocationIdsFromGroupIdsAsync(db, regions); if (partners.Count == 0) { return LocationScopeBindingHelper.NormalizeIds(fromRegions); } var partnerLocSet = new HashSet( await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partners), StringComparer.OrdinalIgnoreCase); return LocationScopeBindingHelper.NormalizeIds( fromRegions.Where(id => partnerLocSet.Contains(id)).ToList()); } if (partners.Count > 0) { return await LocationScopeBindingHelper.ResolveLocationIdsFromPartnerIdsAsync(db, partners); } var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( db, (IReadOnlyList?)null, null, null); return LocationScopeBindingHelper.NormalizeIds(merged); } /// Id 数组是否含 ALL 哨兵(大小写不敏感;与具体 Guid 同传时以 ALL 为准)。 public static bool HasAllScopeSentinelSelection(IReadOnlyList? ids) => LocationScopeBindingHelper.ContainsAllScopeSentinel(ids); /// Company 维度是否应存为 ALL(含「勾选了全部 Company」)。 public static async Task<(string Type, List Ids)> NormalizePartnerScopeAsync( ISqlSugarClient db, string? declaredType, IReadOnlyList? partnerIds, IReadOnlyList? companyIds, bool hasArrayInPayload) { var ids = LabelEntityPartnerScopeHelper.NormalizePartnerIds(partnerIds, companyIds); if (HasAllScopeSentinelSelection(ids)) { return (ScopeAll, new List()); } 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()); } /// Region 维度是否应存为 ALL。 public static async Task<(string Type, List Ids)> NormalizeRegionScopeAsync( ISqlSugarClient db, string? declaredType, IReadOnlyList regionIds, bool hasArrayInPayload, IReadOnlyList? partnerIdsForContext) { if (HasAllScopeSentinelSelection(regionIds)) { return (ScopeAll, new List()); } var ids = LocationScopeBindingHelper.FilterConcreteScopeIds(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()); } /// Location 维度是否应存为 ALL。 public static async Task<(string Type, List Ids)> NormalizeLocationScopeAsync( ISqlSugarClient db, string? declaredType, IReadOnlyList locationIds, bool hasArrayInPayload, IReadOnlyList? partnerIdsForContext, IReadOnlyList? regionIdsForContext) { if (HasAllScopeSentinelSelection(locationIds)) { return (ScopeAll, new List()); } var ids = LocationScopeBindingHelper.FilterConcreteScopeIds(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()); } /// /// 由 PartnerId / Region / Location 反推 Company 上下文,用于判断「Select All」是否覆盖该公司全部门店。 /// public static async Task?> ResolvePartnerContextFromScopeAsync( ISqlSugarClient db, string? partnerId, IReadOnlyList? regionOrGroupIds, IReadOnlyList? locationIds) { if (!string.IsNullOrWhiteSpace(partnerId)) { return new List { partnerId.Trim() }; } var regionIds = LocationScopeBindingHelper.FilterConcreteScopeIds(regionOrGroupIds); if (regionIds.Count > 0) { var rows = await db.Queryable() .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.FilterConcreteScopeIds(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; } /// /// 标签类型/分类/多选项/产品分类:Region+Location 合并后是否应视为 ALL。 /// public static async Task ShouldTreatMergedLocationScopeAsAllAsync( ISqlSugarClient db, string? declaredAvailabilityType, IReadOnlyList? regionIds, IReadOnlyList? locationIds, bool hasScopeArrays, IReadOnlyList? partnerIdsForContext) { var concreteLocations = LocationScopeBindingHelper.FilterConcreteScopeIds(locationIds); var concreteRegions = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); // 具体 Region(非全局 ALL)下即使覆盖该区全部门店,也只存 SPECIFIED 快照,不升全局 AvailabilityType=ALL if (concreteRegions.Count > 0) { return false; } // locationIds 含 ALL 且无具体 Region:由调用方按 Company 展开或归档全局 ALL if (HasAllScopeSentinelSelection(locationIds)) { return true; } if (HasAllScopeSentinelSelection(regionIds) && concreteLocations.Count == 0) { return true; } if (IsDeclaredAll(declaredAvailabilityType) && concreteRegions.Count == 0 && concreteLocations.Count == 0) { return true; } if (hasScopeArrays && concreteRegions.Count == 0 && concreteLocations.Count == 0 && IsDeclaredAll(declaredAvailabilityType)) { return true; } // 前端 Select All 常传 SPECIFIED + 当前全量 Id:覆盖上下文全部门店时归档 ALL。 // 有具体 locationIds 时:只按这些门店落 SPECIFIED 快照,禁止再升成 AvailabilityType=ALL // (否则「Region=ALL + 选 1 店」在公司仅 1 店或误判全选时会清空快照并回显成全局 ALL)。 if (concreteLocations.Count > 0) { return false; } List merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( db, (IReadOnlyList?)null, concreteRegions, concreteLocations); if (merged.Count == 0) { return false; } var partnerContext = LocationScopeBindingHelper.NormalizeIds(partnerIdsForContext); if (partnerContext.Count == 0) { partnerContext = await ResolvePartnerContextFromScopeAsync(db, null, concreteRegions, concreteLocations) ?? new List(); } var allLocations = await ResolveAllLocationIdsAsync( db, partnerContext.Count > 0 ? partnerContext : null, concreteRegions.Count > 0 ? concreteRegions : null); return IsFullIdSelection(merged, allLocations); } /// 产品门店范围是否应存为 ALL(不写 fl_location_product 快照)。 public static async Task ShouldTreatProductLocationScopeAsAllAsync( ISqlSugarClient db, string? declaredAvailabilityType, string? partnerId, IReadOnlyList? groupIds, IReadOnlyList? locationIds, bool hasScopePayload) { // partnerId 字符串为 ALL 哨兵:全选 Company,归档 ALL(禁止把 ALL 当 Guid 查库) if (LocationScopeBindingHelper.IsAllScopeSentinel(partnerId)) { return true; } var concreteLocations = LocationScopeBindingHelper.FilterConcreteScopeIds(locationIds); var concreteGroups = LocationScopeBindingHelper.FilterConcreteScopeIds(groupIds); // 具体 Region 下不升全局 AvailabilityType=ALL(与 ShouldTreatMergedLocationScopeAsAllAsync 一致) if (concreteGroups.Count > 0) { return false; } if (HasAllScopeSentinelSelection(locationIds)) { return true; } if (HasAllScopeSentinelSelection(groupIds) && concreteLocations.Count == 0) { return true; } if (IsDeclaredAll(declaredAvailabilityType) && string.IsNullOrWhiteSpace(partnerId) && concreteGroups.Count == 0 && concreteLocations.Count == 0) { return true; } if (!hasScopePayload) { return false; } // 有具体门店时不升全局 ALL if (concreteLocations.Count > 0) { return false; } var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( db, partnerId, concreteGroups, concreteLocations); if (merged.Count == 0) { return false; } var partnerContext = await ResolvePartnerContextFromScopeAsync( db, partnerId, concreteGroups, concreteLocations); var allLocations = await ResolveAllLocationIdsAsync( db, partnerContext, concreteGroups.Count > 0 ? concreteGroups : null); return IsFullIdSelection(merged, allLocations); } /// /// 详情回显:AvailabilityType/Applied*=ALL 时不落快照,展开为当前可见 Company/Region/Location 全集(对齐 Label)。 /// 为 null 表示管理员全量;非 null 为可见门店范围。 /// public static async Task<(List PartnerIds, List RegionIds, List LocationIds)> ResolveDisplayIdsForAllScopeAsync( ISqlSugarClient db, IReadOnlyList? scopedLocationIds, IReadOnlyList? 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); } /// /// 标签/产品分类等:解析 Region + Location 落库(对齐 Label:Region=ALL 时可保留具体门店快照)。 /// public sealed class LabelEntityRegionLocationSaveResult { public string AvailabilityType { get; init; } = ScopeSpecified; public string AppliedRegionType { get; init; } = ScopeAll; public List LocationIds { get; init; } = new(); } public static async Task ResolveLabelEntityRegionLocationForSaveAsync( ISqlSugarClient db, string? declaredAvailabilityType, IReadOnlyList? partnerIdsForContext, IReadOnlyList? mergedRegionIdsRaw, IReadOnlyList? locationIdsRaw, bool hasScopeArrays) { var normalizedLocationIds = LocationScopeBindingHelper.NormalizeIds(locationIdsRaw); var mergedRegionIds = LocationScopeBindingHelper.NormalizeIds(mergedRegionIdsRaw); var regionIds = LocationScopeBindingHelper.FilterConcreteScopeIds(mergedRegionIds); var explicitLocationIds = LocationScopeBindingHelper.FilterConcreteScopeIds(normalizedLocationIds); var locationHasAll = HasAllScopeSentinelSelection(normalizedLocationIds); var regionHasAllSentinel = HasAllScopeSentinelSelection(mergedRegionIds); var hasConcretePartner = LocationScopeBindingHelper.NormalizeIds(partnerIdsForContext).Count > 0; // 前端 Select All Region 常展开为全量 Guid,需识别为 Region=ALL var regionHasAll = regionHasAllSentinel; if (!regionHasAll && regionIds.Count > 0) { var allRegions = await ResolveAllRegionIdsAsync( db, hasConcretePartner ? partnerIdsForContext : null); regionHasAll = allRegions.Count > 0 && IsFullIdSelection(regionIds, allRegions); } // location ALL 哨兵,或具体 Region 下「空选/全选门店」:有 Company/Region 上下文则展开 SPECIFIED var locationCoversConcreteRegions = false; if (!locationHasAll && regionIds.Count > 0 && !regionHasAll) { if (explicitLocationIds.Count == 0) { locationCoversConcreteRegions = true; } else { var regionUniverse = await ResolveAllLocationIdsAsync( db, hasConcretePartner ? partnerIdsForContext : null, regionIds); locationCoversConcreteRegions = regionUniverse.Count > 0 && IsFullIdSelection(explicitLocationIds, regionUniverse); } } if (locationHasAll || locationCoversConcreteRegions) { var expandRegions = regionHasAll ? null : (regionIds.Count > 0 ? regionIds : null); var expanded = await LocationScopeBindingHelper.ExpandScopedAllLocationsForSaveAsync( db, hasConcretePartner ? partnerIdsForContext : null, expandRegions); if (expanded is not null) { return new LabelEntityRegionLocationSaveResult { AvailabilityType = ScopeSpecified, AppliedRegionType = regionHasAll || regionIds.Count == 0 ? ScopeAll : ScopeSpecified, LocationIds = expanded }; } return new LabelEntityRegionLocationSaveResult { AvailabilityType = ScopeAll, AppliedRegionType = ScopeAll, LocationIds = new List() }; } // Region=ALL + 具体门店:Region 存 ALL,门店 SPECIFIED 快照(可回显 regionIds=["ALL"] + 单个 locationId) if (regionHasAll && explicitLocationIds.Count > 0) { await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, explicitLocationIds); return new LabelEntityRegionLocationSaveResult { AvailabilityType = ScopeSpecified, AppliedRegionType = ScopeAll, LocationIds = explicitLocationIds }; } // Region=ALL 且无具体门店 → 双 ALL if (regionHasAll) { return new LabelEntityRegionLocationSaveResult { AvailabilityType = ScopeAll, AppliedRegionType = ScopeAll, LocationIds = new List() }; } // 有具体门店(可同传具体 Region):以门店为准 if (explicitLocationIds.Count > 0) { await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, explicitLocationIds); return new LabelEntityRegionLocationSaveResult { AvailabilityType = ScopeSpecified, AppliedRegionType = ScopeSpecified, LocationIds = explicitLocationIds }; } if (await ShouldTreatMergedLocationScopeAsAllAsync( db, declaredAvailabilityType, regionIds, explicitLocationIds, hasScopeArrays, partnerIdsForContext)) { return new LabelEntityRegionLocationSaveResult { AvailabilityType = ScopeAll, AppliedRegionType = ScopeAll, LocationIds = new List() }; } var availabilityType = (declaredAvailabilityType ?? ScopeAll).Trim().ToUpperInvariant(); if (regionIds.Count > 0) { availabilityType = ScopeSpecified; } else if (hasScopeArrays && IsDeclaredAll(availabilityType)) { availabilityType = ScopeAll; } if (availabilityType != ScopeAll && availabilityType != ScopeSpecified) { throw new UserFriendlyException("门店可用范围不合法(ALL/SPECIFIED)"); } var locationSpecified = string.Equals(availabilityType, ScopeSpecified, StringComparison.OrdinalIgnoreCase); var savedLocationIds = await LocationScopeBindingHelper.ResolveEntityLocationIdsForSaveAsync( db, locationSpecified, regionIds, explicitLocationIds); return new LabelEntityRegionLocationSaveResult { AvailabilityType = locationSpecified ? ScopeSpecified : ScopeAll, AppliedRegionType = locationSpecified ? ScopeSpecified : ScopeAll, LocationIds = savedLocationIds }; } }