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
};
}
}