using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Entities; using SqlSugar; using Volo.Abp; using Volo.Abp.Guids; namespace FoodLabeling.Application.Helpers; /// /// ?? Region?fl_group.Id????? fl_label_location ??????????????? /// public static class LabelRegionScopeHelper { public const string AppliedRegionAll = "ALL"; public const string AppliedRegionSpecified = "SPECIFIED"; public const string AllRegionsDisplay = "All Regions"; public const string AllLocationsDisplay = "All Locations"; public sealed class LabelRegionScopeSaveResult { public string AppliedRegionType { get; init; } = AppliedRegionSpecified; public List RegionIds { get; init; } = new(); /// ???? Id ???location.Id???? fl_label_location? public List LocationIds { get; init; } = new(); /// ?? fl_label.LocationId?? ??? public string? PrimaryLocationId => LocationIds.Count > 0 ? LocationIds[0] : null; } public sealed class LabelRegionScopeDisplay { public string Region { get; init; } = string.Empty; public List RegionIds { get; init; } = new(); public List GroupIds { get; init; } = new(); } public sealed class LabelLocationScopeDisplay { public string Location { get; init; } = string.Empty; public List LocationIds { get; init; } = new(); } /// /// ??/????? Region ??? Id ??? /// public static async Task ResolveScopeForSaveAsync( ISqlSugarClient db, string? appliedRegionType, IReadOnlyList? regionIds, IReadOnlyList? groupIds, string? locationId, IReadOnlyList? 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?"); } var explicitLocationIds = MergeExplicitLocationIds(locationId, locationIds); if (string.Equals(type, AppliedRegionAll, StringComparison.OrdinalIgnoreCase)) { if (explicitLocationIds.Count > 0) { await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, explicitLocationIds); } return new LabelRegionScopeSaveResult { AppliedRegionType = AppliedRegionAll, RegionIds = new List(), LocationIds = explicitLocationIds }; } if (mergedRegionIds.Count == 0 && explicitLocationIds.Count == 0) { throw new UserFriendlyException("??? Region?regionIds??/????locationIds?"); } if (mergedRegionIds.Count > 0) { await ValidateRegionIdsExistAsync(db, mergedRegionIds); } var mergedLocations = await LocationScopeBindingHelper.MergeToLocationIdsAsync( db, (IReadOnlyList?)null, mergedRegionIds, explicitLocationIds); if (mergedLocations.Count == 0) { throw new UserFriendlyException("? Region ???????????? Region ? locationIds"); } if (explicitLocationIds.Count > 0) { var invalid = explicitLocationIds .Where(id => !mergedLocations.Contains(id, StringComparer.Ordinal)) .ToList(); if (invalid.Count > 0) { throw new UserFriendlyException("???????? Region ???"); } } await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, mergedLocations); return new LabelRegionScopeSaveResult { AppliedRegionType = AppliedRegionSpecified, RegionIds = mergedRegionIds, LocationIds = mergedLocations }; } public static async Task SaveLabelRegionsAsync( ISqlSugarClient db, IGuidGenerator guidGenerator, string labelId, string appliedRegionType, IReadOnlyList regionIds, string? currentUserId, DateTime now) { var schema = await LabelRegionSchemaHelper.GetStatusAsync(db); if (!schema.HasLabelRegionTable) { return; } await db.Deleteable() .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(); } /// ????????? fl_label_location? public static async Task SaveLabelLocationsAsync( ISqlSugarClient db, IGuidGenerator guidGenerator, string labelId, IReadOnlyList locationIds, string? currentUserId, DateTime now) { var schema = await LabelRegionSchemaHelper.GetStatusAsync(db); if (!schema.HasLabelLocationTable) { return; } await db.Deleteable() .Where(x => x.LabelId == labelId) .ExecuteCommandAsync(); if (locationIds.Count == 0) { return; } var rows = locationIds.Select(locId => new FlLabelLocationDbEntity { Id = guidGenerator.Create().ToString(), LabelId = labelId, LocationId = locId, CreationTime = now, CreatorId = currentUserId }).ToList(); await db.Insertable(rows).ExecuteCommandAsync(); } public static async Task> GetRegionIdsForLabelAsync(ISqlSugarClient db, string labelId) { var schema = await LabelRegionSchemaHelper.GetStatusAsync(db); if (!schema.HasLabelRegionTable) { return new List(); } return LocationScopeBindingHelper.NormalizeIds( await db.Queryable() .Where(x => x.LabelId == labelId) .Select(x => x.GroupId) .ToListAsync()); } public static async Task> GetLocationIdsForLabelAsync( ISqlSugarClient db, string labelId, string? fallbackLocationId) { var schema = await LabelRegionSchemaHelper.GetStatusAsync(db); if (schema.HasLabelLocationTable) { var ids = LocationScopeBindingHelper.NormalizeIds( await db.Queryable() .Where(x => x.LabelId == labelId) .Select(x => x.LocationId) .ToListAsync()); if (ids.Count > 0) { return ids; } } var fallback = fallbackLocationId?.Trim(); return string.IsNullOrWhiteSpace(fallback) ? new List() : new List { fallback }; } public static async Task>> BuildLocationIdsMapAsync( ISqlSugarClient db, IReadOnlyList labelIds, IReadOnlyDictionary fallbackLocationIdByLabelId) { var result = new Dictionary>(StringComparer.Ordinal); if (labelIds.Count == 0) { return result; } foreach (var labelId in labelIds) { fallbackLocationIdByLabelId.TryGetValue(labelId, out var fallback); result[labelId] = new List(); if (!string.IsNullOrWhiteSpace(fallback)) { result[labelId].Add(fallback.Trim()); } } var schema = await LabelRegionSchemaHelper.GetStatusAsync(db); if (!schema.HasLabelLocationTable) { return result; } var links = await db.Queryable() .Where(x => labelIds.Contains(x.LabelId)) .ToListAsync(); foreach (var g in links.GroupBy(x => x.LabelId, StringComparer.Ordinal)) { var ids = LocationScopeBindingHelper.NormalizeIds(g.Select(x => x.LocationId).ToList()); if (ids.Count > 0) { result[g.Key] = ids; } } return result; } public static async Task BuildScopeDisplayAsync( ISqlSugarClient db, string labelId, string? appliedRegionType, IReadOnlyList locationIds) { if (string.Equals(appliedRegionType, AppliedRegionAll, StringComparison.OrdinalIgnoreCase)) { return new LabelRegionScopeDisplay { Region = AllRegionsDisplay, RegionIds = new List(), GroupIds = new List() }; } var regionIds = await GetRegionIdsForLabelAsync(db, labelId); if (regionIds.Count == 0 && locationIds.Count > 0) { regionIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, locationIds.ToList()); } if (regionIds.Count == 0) { return new LabelRegionScopeDisplay { Region = "?", RegionIds = new List(), GroupIds = new List() }; } var names = await db.Queryable() .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 }; } public static async Task BuildLocationDisplayAsync( ISqlSugarClient db, string? appliedRegionType, IReadOnlyList locationIds) { if (string.Equals(appliedRegionType, AppliedRegionAll, StringComparison.OrdinalIgnoreCase) && locationIds.Count == 0) { return new LabelLocationScopeDisplay { Location = AllLocationsDisplay, LocationIds = new List() }; } if (locationIds.Count == 0) { return new LabelLocationScopeDisplay { Location = "?", LocationIds = new List() }; } var guidList = locationIds.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); var names = new List(); if (guidList.Count > 0) { names = await db.Queryable() .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) .OrderBy(x => x.LocationName) .Select(x => x.LocationName ?? x.LocationCode) .ToListAsync(); } var locationText = names.Count > 0 ? string.Join(", ", names.Where(n => !string.IsNullOrWhiteSpace(n)).Select(n => n!.Trim()).Distinct()) : "?"; return new LabelLocationScopeDisplay { Location = locationText, LocationIds = locationIds.ToList() }; } /// /// ??? Region?fl_group.Id?? scoped ????? /// public static ISugarQueryable ApplyLabelRegionListFilter( ISqlSugarClient db, ISugarQueryable query, string groupId, List? scopedLocationIds, LabelRegionSchemaHelper.LabelRegionSchemaStatus schema) { var gid = groupId.Trim(); if (scopedLocationIds is not { Count: > 0 }) { return query.Where(_ => false); } if (!schema.HasLabelRegionTable && !schema.HasAppliedRegionTypeColumn) { return ApplyLabelLocationMatchFilter(query, scopedLocationIds, schema); } if (!schema.HasLabelRegionTable) { return query.Where( "(AppliedRegionType = @all OR LocationId IN (@locs))", new { all = AppliedRegionAll, locs = scopedLocationIds }); } if (!schema.HasAppliedRegionTypeColumn) { if (schema.HasLabelLocationTable) { return query.Where( """ (EXISTS (SELECT 1 FROM fl_label_region lr WHERE lr.LabelId = fl_label.Id AND lr.GroupId = @gid) OR LocationId IN (@locs) OR EXISTS (SELECT 1 FROM fl_label_location ll WHERE ll.LabelId = fl_label.Id AND ll.LocationId IN (@locs))) """, new { gid, locs = scopedLocationIds }); } return query.Where( """ (EXISTS (SELECT 1 FROM fl_label_region lr WHERE lr.LabelId = fl_label.Id AND lr.GroupId = @gid) OR LocationId IN (@locs)) """, new { gid, locs = scopedLocationIds }); } if (schema.HasLabelLocationTable) { 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) OR EXISTS (SELECT 1 FROM fl_label_location ll WHERE ll.LabelId = fl_label.Id AND ll.LocationId IN (@locs))) """, new { all = AppliedRegionAll, gid, locs = scopedLocationIds }); } 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 }); } /// ? Region Id ???? scoped ???? public static ISugarQueryable ApplyGroupIdListFilter( ISqlSugarClient db, ISugarQueryable query, string groupId, LabelRegionSchemaHelper.LabelRegionSchemaStatus schema) { var gid = groupId.Trim(); if (!schema.HasLabelRegionTable && !schema.HasAppliedRegionTypeColumn) { return query.Where(_ => false); } if (!schema.HasLabelRegionTable) { return query.Where("AppliedRegionType = @all", new { all = AppliedRegionAll }); } if (!schema.HasAppliedRegionTypeColumn) { return query.Where(l => SqlFunc.Subqueryable() .Where(lr => lr.LabelId == l.Id && lr.GroupId == gid) .Any()); } return query.Where( """ (AppliedRegionType = @all OR EXISTS (SELECT 1 FROM fl_label_region lr WHERE lr.LabelId = fl_label.Id AND lr.GroupId = @gid)) """, new { all = AppliedRegionAll, gid }); } /// ? scoped ?????fl_label_location ???? LocationId?? public static ISugarQueryable ApplyLabelLocationListFilter( ISqlSugarClient db, ISugarQueryable query, List scopedLocationIds, LabelRegionSchemaHelper.LabelRegionSchemaStatus schema) { if (scopedLocationIds.Count == 0) { return query.Where(_ => false); } return ApplyLabelLocationMatchFilter(query, scopedLocationIds, schema); } /// /// ?????? Region Id?? App ??/????? /// public static async Task> ResolveGroupIdsForLocationAsync(ISqlSugarClient db, string locationId) { return await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( db, new List { locationId.Trim() }); } /// /// ?????????????????????? /// public static async Task EnsureLabelAppliesToLocationAsync( ISqlSugarClient db, string labelId, string locationId) { var label = await LabelQueryHelper.QueryProjected(db) .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 LabelAppliesToLocationAsync( ISqlSugarClient db, FlLabelDbEntity label, string locationId, List groupIdsForLocation) { var appliedType = await LabelRegionSchemaHelper.GetAppliedRegionTypeForLabelAsync( db, label.Id, label.AppliedRegionType); if (string.Equals(appliedType, AppliedRegionAll, StringComparison.OrdinalIgnoreCase)) { var locationIds = await GetLocationIdsForLabelAsync(db, label.Id, label.LocationId); if (locationIds.Count == 0) { return true; } return locationIds.Contains(locationId.Trim(), StringComparer.OrdinalIgnoreCase); } var schema = await LabelRegionSchemaHelper.GetStatusAsync(db); if (schema.HasLabelLocationTable) { var hitLocation = await db.Queryable() .AnyAsync(x => x.LabelId == label.Id && x.LocationId == locationId); if (hitLocation) { return true; } } if (!string.IsNullOrWhiteSpace(label.LocationId) && string.Equals(label.LocationId.Trim(), locationId, StringComparison.OrdinalIgnoreCase)) { return true; } if (groupIdsForLocation.Count == 0) { return false; } if (!schema.HasLabelRegionTable) { return false; } return await db.Queryable() .AnyAsync(lr => lr.LabelId == label.Id && groupIdsForLocation.Contains(lr.GroupId)); } /// /// App labeling-tree 单门店可见性过滤 SQL(多表 join 时 fl_label 别名为 l)。 /// public static (string Sql, object Parameters) BuildAppLabelingTreeStoreFilterSql( LabelRegionSchemaHelper.LabelRegionSchemaStatus schema, string locationId, IReadOnlyList storeGroupIds) { var loc = locationId.Trim(); var groupIds = LocationScopeBindingHelper.NormalizeIds(storeGroupIds); var hasLocTable = schema.HasLabelLocationTable; var hasRegTable = schema.HasLabelRegionTable; var hasAppliedCol = schema.HasAppliedRegionTypeColumn; if (!hasAppliedCol) { return (BuildAppLabelingTreeSpecifiedOnlySql(hasLocTable, hasRegTable, groupIds), new { loc, groupIds }); } var allGlobalSql = hasLocTable ? """ (NOT EXISTS (SELECT 1 FROM fl_label_location ll WHERE ll.LabelId = l.Id) AND (l.LocationId IS NULL OR l.LocationId = '')) """ : "(l.LocationId IS NULL OR l.LocationId = '')"; var locMatchParts = new List { allGlobalSql, "l.LocationId = @loc" }; if (hasLocTable) { locMatchParts.Insert(1, """ EXISTS (SELECT 1 FROM fl_label_location ll WHERE ll.LabelId = l.Id AND ll.LocationId = @loc) """); } var allBranchSql = string.Join(" OR ", locMatchParts); var specifiedParts = new List { "l.LocationId = @loc" }; if (hasLocTable) { specifiedParts.Insert(0, """ EXISTS (SELECT 1 FROM fl_label_location ll WHERE ll.LabelId = l.Id AND ll.LocationId = @loc) """); } if (hasRegTable && groupIds.Count > 0) { specifiedParts.Add( """ EXISTS (SELECT 1 FROM fl_label_region lr WHERE lr.LabelId = l.Id AND lr.GroupId IN (@groupIds)) """); } var specifiedBranchSql = string.Join(" OR ", specifiedParts); var sql = $""" ((l.AppliedRegionType = @all AND ({allBranchSql})) OR (l.AppliedRegionType <> @all AND ({specifiedBranchSql}))) """; return (sql, new { all = AppliedRegionAll, loc, groupIds }); } private static string BuildAppLabelingTreeSpecifiedOnlySql( bool hasLocTable, bool hasRegTable, IReadOnlyList groupIds) { var parts = new List { "l.LocationId = @loc" }; if (hasLocTable) { parts.Insert(0, """ EXISTS (SELECT 1 FROM fl_label_location ll WHERE ll.LabelId = l.Id AND ll.LocationId = @loc) """); } if (hasRegTable && groupIds.Count > 0) { parts.Add( """ EXISTS (SELECT 1 FROM fl_label_region lr WHERE lr.LabelId = l.Id AND lr.GroupId IN (@groupIds)) """); } return $"({string.Join(" OR ", parts)})"; } private static ISugarQueryable ApplyLabelLocationMatchFilter( ISugarQueryable query, List scopedLocationIds, LabelRegionSchemaHelper.LabelRegionSchemaStatus schema) { if (schema.HasLabelLocationTable) { return query.Where( """ (LocationId IN (@locs) OR EXISTS (SELECT 1 FROM fl_label_location ll WHERE ll.LabelId = fl_label.Id AND ll.LocationId IN (@locs))) """, new { locs = scopedLocationIds }); } return query.Where(l => scopedLocationIds.Contains(l.LocationId)); } private static List MergeExplicitLocationIds(string? locationId, IReadOnlyList? locationIds) { var merged = LocationScopeBindingHelper.NormalizeIds(locationIds); var single = locationId?.Trim(); if (!string.IsNullOrWhiteSpace(single) && !merged.Contains(single, StringComparer.Ordinal)) { merged.Insert(0, single); } return merged; } private static List NormalizeRegionIds( 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 ValidateRegionIdsExistAsync(ISqlSugarClient db, List regionIds) { if (regionIds.Count == 0) { return; } var count = await db.Queryable() .Where(g => !g.IsDeleted && regionIds.Contains(g.Id)) .CountAsync(); if (count != regionIds.Count) { throw new UserFriendlyException("????? Region Id???????"); } } }