using FoodLabeling.Application.Contracts.Dtos.LabelTemplate; using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Entities; using SqlSugar; using Volo.Abp; using Volo.Abp.Guids; namespace FoodLabeling.Application.Helpers; /// /// 标签模板适用 Company / Region / Location:各维度独立 ALL/SPECIFIED + 多选落库。 /// public static class LabelTemplateScopeHelper { public const string ScopeAll = "ALL"; public const string ScopeSpecified = "SPECIFIED"; public const string AllCompaniesDisplay = "All Companies"; public const string AllRegionsDisplay = "All Regions"; public const string AllLocationsDisplay = "All Locations"; public const string EmptyDisplay = "无"; public sealed class LabelTemplateScopeSaveResult { public string AppliedPartnerType { get; init; } = ScopeAll; public string AppliedRegionType { get; init; } = ScopeAll; public string AppliedLocationType { get; init; } = ScopeAll; public List PartnerIds { get; init; } = new(); public List RegionIds { get; init; } = new(); public List LocationIds { get; init; } = new(); } public sealed class LabelTemplateScopeDisplay { public string Company { get; init; } = AllCompaniesDisplay; public string Region { get; init; } = AllRegionsDisplay; public string Location { get; init; } = AllLocationsDisplay; public string AppliedPartnerType { get; init; } = ScopeAll; public string AppliedRegionType { get; init; } = ScopeAll; public string AppliedLocationType { get; init; } = ScopeAll; public List PartnerIds { get; init; } = new(); public List RegionIds { get; init; } = new(); public List LocationIds { get; init; } = new(); } /// /// 解析新增/编辑入参中的 Company / Region / Location 范围。 /// public static async Task ResolveScopeForSaveAsync( ISqlSugarClient db, LabelTemplateCreateInputVo input) { var partnerIds = NormalizePartnerIds(input); var regionIds = NormalizeRegionIds(input); var locationIds = MergeExplicitLocationIds(input); var partnerType = ResolveDimensionType( input.AppliedPartnerType, partnerIds, input.PartnerIds is not null || input.CompanyIds is not null); var regionType = ResolveDimensionType( input.AppliedRegionType, regionIds, input.RegionIds is not null || input.GroupIds is not null); var locationType = ResolveDimensionType( input.AppliedLocationType, locationIds, input.LocationIds is not null || input.AppliedLocationIds is not null); ValidateDimensionType("Company", partnerType); ValidateDimensionType("Region", regionType); ValidateDimensionType("Location", locationType); if (string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && partnerIds.Count == 0) { throw new UserFriendlyException("指定适用 Company 时,partnerIds/companyIds 不能为空"); } if (string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && regionIds.Count == 0) { throw new UserFriendlyException("指定适用 Region 时,regionIds/groupIds 不能为空"); } if (string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && locationIds.Count == 0) { throw new UserFriendlyException("指定适用门店时,locationIds/appliedLocationIds 不能为空"); } if (partnerIds.Count > 0) { await ValidatePartnerIdsExistAsync(db, partnerIds); } if (regionIds.Count > 0) { await ValidateRegionIdsExistAsync(db, regionIds); } if (locationIds.Count > 0) { await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, locationIds); } var anySpecified = string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) || string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) || string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase); if (anySpecified) { var merged = await LocationScopeBindingHelper.MergeToLocationIdsAsync( db, string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? partnerIds : null, string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? regionIds : null, string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? locationIds : null); if (merged.Count == 0) { throw new UserFriendlyException("指定适用范围后至少需要匹配到一个有效门店"); } } return new LabelTemplateScopeSaveResult { AppliedPartnerType = partnerType, AppliedRegionType = regionType, AppliedLocationType = locationType, PartnerIds = partnerIds, RegionIds = regionIds, LocationIds = locationIds }; } public static async Task SaveTemplateScopeAsync( ISqlSugarClient db, IGuidGenerator guidGenerator, string templateId, LabelTemplateScopeSaveResult scope, string? currentUserId, DateTime now) { var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db); if (hasExtendedScope) { await db.Deleteable() .Where(x => x.TemplateId == templateId) .ExecuteCommandAsync(); await db.Deleteable() .Where(x => x.TemplateId == templateId) .ExecuteCommandAsync(); } await db.Deleteable() .Where(x => x.TemplateId == templateId) .ExecuteCommandAsync(); if (hasExtendedScope && string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.PartnerIds.Count > 0) { var rows = scope.PartnerIds.Select(pid => new FlLabelTemplatePartnerDbEntity { Id = guidGenerator.Create().ToString(), TemplateId = templateId, PartnerId = pid, CreationTime = now, CreatorId = currentUserId }).ToList(); await db.Insertable(rows).ExecuteCommandAsync(); } if (hasExtendedScope && string.Equals(scope.AppliedRegionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.RegionIds.Count > 0) { var rows = scope.RegionIds.Select(gid => new FlLabelTemplateRegionDbEntity { Id = guidGenerator.Create().ToString(), TemplateId = templateId, GroupId = gid, CreationTime = now, CreatorId = currentUserId }).ToList(); await db.Insertable(rows).ExecuteCommandAsync(); } if (string.Equals(scope.AppliedLocationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.LocationIds.Count > 0) { var rows = scope.LocationIds.Select(locId => new FlLabelTemplateLocationDbEntity { Id = guidGenerator.Create().ToString(), TemplateId = templateId, LocationId = locId }).ToList(); await db.Insertable(rows).ExecuteCommandAsync(); } } /// /// 批量加载模板适用范围展示与 Id 数组(详情/列表)。 /// public static async Task> BuildScopeDisplayMapAsync( ISqlSugarClient db, IReadOnlyList templates) { var result = new Dictionary(StringComparer.Ordinal); if (templates.Count == 0) { return result; } var templateIds = templates.Select(x => x.Id).Distinct(StringComparer.Ordinal).ToList(); var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db); var partnerLinks = hasExtendedScope ? await db.Queryable() .Where(x => templateIds.Contains(x.TemplateId)) .ToListAsync() : new List(); var regionLinks = hasExtendedScope ? await db.Queryable() .Where(x => templateIds.Contains(x.TemplateId)) .ToListAsync() : new List(); var locationLinks = await db.Queryable() .Where(x => templateIds.Contains(x.TemplateId)) .ToListAsync(); var partnerIdSet = partnerLinks.Select(x => x.PartnerId).Distinct(StringComparer.Ordinal).ToList(); var regionIdSet = regionLinks.Select(x => x.GroupId).Distinct(StringComparer.Ordinal).ToList(); var locationIdSet = locationLinks.Select(x => x.LocationId).Distinct(StringComparer.Ordinal).ToList(); var partnerNameById = new Dictionary(StringComparer.Ordinal); if (partnerIdSet.Count > 0) { var partners = await db.Queryable() .Where(x => !x.IsDeleted && partnerIdSet.Contains(x.Id)) .ToListAsync(); foreach (var p in partners) { var name = p.PartnerName?.Trim(); partnerNameById[p.Id] = string.IsNullOrEmpty(name) ? p.Id : name; } } var regionNameById = new Dictionary(StringComparer.Ordinal); if (regionIdSet.Count > 0) { var groups = await db.Queryable() .Where(x => !x.IsDeleted && regionIdSet.Contains(x.Id)) .ToListAsync(); foreach (var g in groups) { var name = g.GroupName?.Trim(); regionNameById[g.Id] = string.IsNullOrEmpty(name) ? g.Id : name; } } var locById = new Dictionary(StringComparer.Ordinal); if (locationIdSet.Count > 0) { var guidList = locationIdSet.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); if (guidList.Count > 0) { var locs = await db.Queryable() .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) .ToListAsync(); foreach (var loc in locs) { locById[loc.Id.ToString()] = loc; } } } foreach (var template in templates) { var locationType = NormalizeScopeType(template.AppliedLocationType, ScopeAll); var pIds = LocationScopeBindingHelper.NormalizeIds( partnerLinks.Where(x => x.TemplateId == template.Id).Select(x => x.PartnerId).ToList()); var rIds = LocationScopeBindingHelper.NormalizeIds( regionLinks.Where(x => x.TemplateId == template.Id).Select(x => x.GroupId).ToList()); var lIds = LocationScopeBindingHelper.NormalizeIds( locationLinks.Where(x => x.TemplateId == template.Id).Select(x => x.LocationId).ToList()); var partnerType = pIds.Count > 0 ? ScopeSpecified : ScopeAll; var regionType = rIds.Count > 0 ? ScopeSpecified : ScopeAll; if (hasExtendedScope && pIds.Count == 0 && lIds.Count > 0) { pIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, lIds); if (pIds.Count > 0) { partnerType = ScopeSpecified; } } if (hasExtendedScope && rIds.Count == 0 && lIds.Count > 0) { rIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, lIds); if (rIds.Count > 0) { regionType = ScopeSpecified; } } if (lIds.Count == 0 && !string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) { locationType = ScopeAll; } else if (lIds.Count > 0) { locationType = ScopeSpecified; } var companyDisplay = string.Equals(partnerType, ScopeAll, StringComparison.OrdinalIgnoreCase) ? AllCompaniesDisplay : FormatNames(pIds.Select(id => partnerNameById.TryGetValue(id, out var n) ? n : id)); var regionDisplay = string.Equals(regionType, ScopeAll, StringComparison.OrdinalIgnoreCase) ? AllRegionsDisplay : FormatNames(rIds.Select(id => regionNameById.TryGetValue(id, out var n) ? n : id)); var locationDisplay = string.Equals(locationType, ScopeAll, StringComparison.OrdinalIgnoreCase) ? AllLocationsDisplay : FormatLocationNames(lIds, locById); result[template.Id] = new LabelTemplateScopeDisplay { Company = companyDisplay, Region = regionDisplay, Location = locationDisplay, AppliedPartnerType = partnerType, AppliedRegionType = regionType, AppliedLocationType = locationType, PartnerIds = pIds, RegionIds = rIds, LocationIds = lIds }; } return result; } /// /// 列表权限:按当前用户可见门店筛选模板(各维度 AND)。 /// public static async Task> ApplyTemplateScopeFilterAsync( ISqlSugarClient db, ISugarQueryable query, List? scopedLocationIds) { if (scopedLocationIds is null) { return query; } var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db); if (scopedLocationIds.Count == 0) { return hasExtendedScope ? query.Where(t => t.AppliedLocationType == ScopeAll && !SqlFunc.Subqueryable() .Where(l => l.TemplateId == t.Id) .Any() && !SqlFunc.Subqueryable() .Where(p => p.TemplateId == t.Id) .Any() && !SqlFunc.Subqueryable() .Where(r => r.TemplateId == t.Id) .Any()) : query.Where(t => t.AppliedLocationType == ScopeAll && !SqlFunc.Subqueryable() .Where(l => l.TemplateId == t.Id) .Any()); } if (!hasExtendedScope) { return query.Where(t => t.AppliedLocationType == ScopeAll || SqlFunc.Subqueryable() .Where(l => l.TemplateId == t.Id && scopedLocationIds.Contains(l.LocationId)) .Any()); } var scopedPartnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync( db, scopedLocationIds); var scopedGroupIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync( db, scopedLocationIds); return query.Where(t => ( t.AppliedLocationType == ScopeAll && !SqlFunc.Subqueryable() .Where(l => l.TemplateId == t.Id) .Any() && !SqlFunc.Subqueryable() .Where(p => p.TemplateId == t.Id) .Any() && !SqlFunc.Subqueryable() .Where(r => r.TemplateId == t.Id) .Any() ) || ( (!SqlFunc.Subqueryable() .Where(p => p.TemplateId == t.Id) .Any() || SqlFunc.Subqueryable() .Where(p => p.TemplateId == t.Id && scopedPartnerIds.Contains(p.PartnerId)) .Any()) && (!SqlFunc.Subqueryable() .Where(r => r.TemplateId == t.Id) .Any() || SqlFunc.Subqueryable() .Where(r => r.TemplateId == t.Id && scopedGroupIds.Contains(r.GroupId)) .Any()) && (t.AppliedLocationType == ScopeAll || SqlFunc.Subqueryable() .Where(l => l.TemplateId == t.Id && scopedLocationIds.Contains(l.LocationId)) .Any()) )); } private static string ResolveDimensionType(string? type, List ids, bool hasArrayInPayload) { if (ids.Count > 0) { return ScopeSpecified; } var normalized = (type ?? ScopeAll).Trim().ToUpperInvariant(); if (hasArrayInPayload && string.Equals(normalized, ScopeAll, StringComparison.OrdinalIgnoreCase)) { return ScopeAll; } return string.Equals(normalized, ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? ScopeSpecified : ScopeAll; } private static void ValidateDimensionType(string label, string type) { if (type != ScopeAll && type != ScopeSpecified) { throw new UserFriendlyException($"适用{label}范围不合法(ALL/SPECIFIED)"); } } private static string NormalizeScopeType(string? type, string fallback) { var t = (type ?? fallback).Trim().ToUpperInvariant(); return t == ScopeSpecified ? ScopeSpecified : ScopeAll; } public static List NormalizePartnerIds(LabelTemplateCreateInputVo input) { var merged = new HashSet(StringComparer.Ordinal); foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.PartnerIds)) { merged.Add(id); } foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.CompanyIds)) { merged.Add(id); } return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); } public static List NormalizeRegionIds(LabelTemplateCreateInputVo input) { var merged = new HashSet(StringComparer.Ordinal); foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.RegionIds)) { merged.Add(id); } foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.GroupIds)) { merged.Add(id); } return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); } public static List MergeExplicitLocationIds(LabelTemplateCreateInputVo input) { var merged = new HashSet(StringComparer.Ordinal); foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.LocationIds)) { merged.Add(id); } foreach (var id in LocationScopeBindingHelper.NormalizeIds(input.AppliedLocationIds)) { merged.Add(id); } return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); } private static async Task ValidatePartnerIdsExistAsync(ISqlSugarClient db, List partnerIds) { if (partnerIds.Count == 0) { return; } var count = await db.Queryable() .Where(x => !x.IsDeleted && partnerIds.Contains(x.Id)) .CountAsync(); if (count != partnerIds.Count) { throw new UserFriendlyException("存在无效的 Company(partnerIds/companyIds),请刷新后重试"); } } private static async Task ValidateRegionIdsExistAsync(ISqlSugarClient db, List regionIds) { if (regionIds.Count == 0) { return; } var count = await db.Queryable() .Where(x => !x.IsDeleted && regionIds.Contains(x.Id)) .CountAsync(); if (count != regionIds.Count) { throw new UserFriendlyException("存在无效的 Region(regionIds/groupIds),请刷新后重试"); } } private static string FormatNames(IEnumerable names) { var list = names .Select(x => x?.Trim()) .Where(x => !string.IsNullOrEmpty(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .OrderBy(x => x, StringComparer.OrdinalIgnoreCase) .ToList(); return list.Count > 0 ? string.Join(", ", list) : EmptyDisplay; } private static string FormatLocationNames( List locationIds, Dictionary locById) { var names = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var lid in locationIds) { if (!locById.TryGetValue(lid, out var loc)) { continue; } var locName = loc.LocationName?.Trim(); if (string.IsNullOrEmpty(locName)) { locName = loc.LocationCode?.Trim(); } if (!string.IsNullOrEmpty(locName)) { names.Add(locName); } } return names.Count > 0 ? string.Join(", ", names.OrderBy(x => x, StringComparer.OrdinalIgnoreCase)) : EmptyDisplay; } }