using FoodLabeling.Application.Services.DbModels; using SqlSugar; using Volo.Abp; namespace FoodLabeling.Application.Helpers; /// /// 标签适用 Region(fl_group.Id)范围:保存、展示、列表筛选与 App 门店校验。 /// public static class LabelRegionScopeHelper { public const string AppliedTypeAll = "ALL"; public const string AppliedTypeSpecified = "SPECIFIED"; public const string AllRegionsDisplay = "All Regions"; private const string EmptyDisplay = "无"; public sealed class LabelRegionScopeData { public string AppliedRegionType { get; init; } = AppliedTypeSpecified; public string Region { get; init; } = string.Empty; public List RegionIds { get; init; } = new(); } /// /// 读取标签绑定的 Region Id 列表。 /// public static async Task> LoadRegionIdsAsync(ISqlSugarClient db, string labelId) { var lid = labelId?.Trim(); if (string.IsNullOrWhiteSpace(lid)) { return new List(); } return await db.Queryable() .Where(x => x.LabelId == lid) .Select(x => x.GroupId) .ToListAsync(); } /// /// 批量读取标签 Region Id(按 LabelId 分组)。 /// public static async Task>> LoadRegionIdsMapAsync( ISqlSugarClient db, IReadOnlyList labelIds) { var ids = LocationScopeBindingHelper.NormalizeIds(labelIds); var result = ids.ToDictionary(x => x, _ => new List(), StringComparer.Ordinal); if (ids.Count == 0) { return result; } var rows = await db.Queryable() .Where(x => ids.Contains(x.LabelId)) .ToListAsync(); foreach (var row in rows) { if (string.IsNullOrWhiteSpace(row.LabelId) || string.IsNullOrWhiteSpace(row.GroupId)) { continue; } if (!result.TryGetValue(row.LabelId, out var list)) { list = new List(); result[row.LabelId] = list; } if (!list.Contains(row.GroupId, StringComparer.Ordinal)) { list.Add(row.GroupId.Trim()); } } foreach (var key in result.Keys.ToList()) { result[key] = result[key].OrderBy(x => x, StringComparer.Ordinal).ToList(); } return result; } /// /// 先删后插保存标签 Region 绑定;ALL 时仅删除关联行。 /// public static async Task SaveRegionIdsAsync( ISqlSugarClient db, string labelId, string appliedRegionType, IReadOnlyList regionIds, string? creatorId, DateTime now, Func idGenerator) { var lid = labelId?.Trim(); if (string.IsNullOrWhiteSpace(lid)) { return; } await db.Deleteable() .Where(x => x.LabelId == lid) .ExecuteCommandAsync(); if (!string.Equals(appliedRegionType, AppliedTypeSpecified, StringComparison.OrdinalIgnoreCase)) { return; } var groupIds = LocationScopeBindingHelper.NormalizeIds(regionIds); if (groupIds.Count == 0) { return; } var rows = groupIds.Select(gid => new FlLabelRegionDbEntity { Id = idGenerator(), LabelId = lid, GroupId = gid, CreationTime = now, CreatorId = creatorId }).ToList(); await db.Insertable(rows).ExecuteCommandAsync(); } /// /// 解析保存入参:ALL/SPECIFIED、Region Id 列表与唯一 location.Id。 /// public static async Task<(string AppliedRegionType, List RegionIds, string LocationId)> ResolveScopeForSaveAsync( ISqlSugarClient db, string? appliedRegionType, IReadOnlyList? regionIds, IReadOnlyList? groupIds, string? locationId, IReadOnlyList? locationIds, IReadOnlyList? partnerIds) { var normalizedRegionIds = MergeRegionIds(regionIds, groupIds); var explicitLocationId = locationId?.Trim(); var explicitLocationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); var type = (appliedRegionType ?? AppliedTypeSpecified).Trim().ToUpperInvariant(); if (normalizedRegionIds.Count > 0) { type = AppliedTypeSpecified; } if (normalizedRegionIds.Count == 0 && (!string.IsNullOrWhiteSpace(explicitLocationId) || explicitLocationIds.Count > 0)) { var locIdsForRegion = new List(); if (!string.IsNullOrWhiteSpace(explicitLocationId)) { locIdsForRegion.Add(explicitLocationId); } locIdsForRegion.AddRange(explicitLocationIds); normalizedRegionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( db, locIdsForRegion); if (normalizedRegionIds.Count > 0) { type = AppliedTypeSpecified; } } if (type != AppliedTypeAll && type != AppliedTypeSpecified) { throw new UserFriendlyException("适用Region范围不合法(ALL/SPECIFIED)"); } if (type == AppliedTypeAll) { var resolvedLoc = explicitLocationId; if (string.IsNullOrWhiteSpace(resolvedLoc) && explicitLocationIds.Count == 1) { resolvedLoc = explicitLocationIds[0]; } else if (string.IsNullOrWhiteSpace(resolvedLoc) && explicitLocationIds.Count > 1) { throw new UserFriendlyException("全选 Region 时请显式指定 locationId,或仅选择一个门店"); } if (!string.IsNullOrWhiteSpace(resolvedLoc)) { await LocationScopeBindingHelper.ValidateLocationIdsExistAsync( db, new List { resolvedLoc }); } return (AppliedTypeAll, new List(), resolvedLoc ?? string.Empty); } if (normalizedRegionIds.Count == 0 && string.IsNullOrWhiteSpace(explicitLocationId) && explicitLocationIds.Count == 0) { throw new UserFriendlyException("请选择适用 Region"); } await ValidateGroupIdsExistAsync(db, normalizedRegionIds); var mergedLocations = await LocationScopeBindingHelper.MergeToLocationIdsAsync( db, partnerIds, normalizedRegionIds, explicitLocationIds); if (!string.IsNullOrWhiteSpace(explicitLocationId)) { if (mergedLocations.Count > 0 && !mergedLocations.Contains(explicitLocationId, StringComparer.Ordinal)) { throw new UserFriendlyException("所选门店不在指定 Region 范围内"); } await LocationScopeBindingHelper.ValidateLocationIdsExistAsync( db, new List { explicitLocationId }); return (AppliedTypeSpecified, normalizedRegionIds, explicitLocationId); } if (mergedLocations.Count == 0) { throw new UserFriendlyException("指定适用 Region 时,至少需要匹配到一个有效门店"); } return (AppliedTypeSpecified, normalizedRegionIds, mergedLocations[0]); } /// /// 列表筛选:按 Region Id 过滤,命中 fl_label_regionAppliedRegionType=ALL。 /// public static ISugarQueryable ApplyGroupIdListFilter( ISugarQueryable query, string groupId) { var gid = groupId?.Trim(); if (string.IsNullOrWhiteSpace(gid)) { return query; } return query.Where(l => l.AppliedRegionType == AppliedTypeAll || SqlFunc.Subqueryable() .Where(lr => lr.LabelId == l.Id && lr.GroupId == gid) .Any()); } /// /// App 树/预览:判断标签是否适用于指定门店(ALL 或 Region 命中当前门店所属 Region)。 /// public static async Task LabelAppliesToLocationAsync( ISqlSugarClient db, FlLabelDbEntity label, string locationId) { if (label is null) { return false; } if (string.Equals(label.AppliedRegionType, AppliedTypeAll, StringComparison.OrdinalIgnoreCase)) { return true; } var locId = locationId?.Trim(); if (string.IsNullOrWhiteSpace(locId)) { return false; } var groupIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( db, new List { locId }); if (groupIds.Count == 0) { return false; } var regionLinks = await db.Queryable() .Where(x => x.LabelId == label.Id) .Select(x => x.GroupId) .ToListAsync(); return regionLinks.Any(gid => groupIds.Contains(gid, StringComparer.Ordinal)); } /// /// 构建列表/详情 Region 展示字段。 /// public static async Task> BuildRegionScopeMapAsync( ISqlSugarClient db, IReadOnlyList<(string Id, string AppliedRegionType)> labels) { var result = new Dictionary(StringComparer.Ordinal); if (labels.Count == 0) { return result; } foreach (var item in labels.Where(x => string.Equals(x.AppliedRegionType, AppliedTypeAll, StringComparison.OrdinalIgnoreCase))) { result[item.Id] = new LabelRegionScopeData { AppliedRegionType = AppliedTypeAll, Region = AllRegionsDisplay, RegionIds = new List() }; } var specifiedIds = labels .Where(x => string.Equals(x.AppliedRegionType, AppliedTypeSpecified, StringComparison.OrdinalIgnoreCase)) .Select(x => x.Id) .ToList(); if (specifiedIds.Count == 0) { return result; } var regionMap = await LoadRegionIdsMapAsync(db, specifiedIds); var allGroupIds = regionMap.Values.SelectMany(x => x).Distinct(StringComparer.Ordinal).ToList(); var groupNameMap = new Dictionary(StringComparer.Ordinal); if (allGroupIds.Count > 0) { var groups = await db.Queryable() .Where(x => !x.IsDeleted && allGroupIds.Contains(x.Id)) .ToListAsync(); foreach (var g in groups) { if (!string.IsNullOrWhiteSpace(g.Id)) { groupNameMap[g.Id] = g.GroupName?.Trim() ?? EmptyDisplay; } } } foreach (var labelId in specifiedIds) { var regionIds = regionMap.TryGetValue(labelId, out var ids) ? ids : new List(); if (regionIds.Count == 0) { result[labelId] = new LabelRegionScopeData { AppliedRegionType = AppliedTypeSpecified, Region = EmptyDisplay, RegionIds = new List() }; continue; } var names = regionIds .Select(id => groupNameMap.TryGetValue(id, out var name) ? name : id) .Where(x => !string.IsNullOrWhiteSpace(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList(); result[labelId] = new LabelRegionScopeData { AppliedRegionType = AppliedTypeSpecified, Region = names.Count > 0 ? string.Join(", ", names) : EmptyDisplay, RegionIds = regionIds }; } return result; } private static List MergeRegionIds( IReadOnlyList? regionIds, IReadOnlyList? groupIds) { var merged = new HashSet(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 ValidateGroupIdsExistAsync(ISqlSugarClient db, IReadOnlyList groupIds) { var ids = LocationScopeBindingHelper.NormalizeIds(groupIds); if (ids.Count == 0) { return; } var existCount = await db.Queryable() .Where(x => !x.IsDeleted && ids.Contains(x.Id)) .CountAsync(); if (existCount != ids.Count) { throw new UserFriendlyException("Region 不存在"); } } }