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 = FoodLabelingDisplayConsts.AllRegion; public const string AllLocationsDisplay = FoodLabelingDisplayConsts.AllLocation; public const string EmptyDisplay = FoodLabelingDisplayConsts.NotAvailable; 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 范围。 /// Create/Update 共用;regionIdsgroupIdslocationIdsappliedLocationIds 可传哨兵 ALL, /// 即使 appliedRegionType / appliedLocationSPECIFIED 也会归档为对应维度 ALL 且不写关联快照。 /// public static async Task ResolveScopeForSaveAsync( ISqlSugarClient db, LabelTemplateCreateInputVo input) { var mergedRegionIds = NormalizeRegionIds(input); var mergedLocationIds = MergeExplicitLocationIds(input); var hasRegionArray = input.RegionIds is not null || input.GroupIds is not null; var hasLocationArray = input.LocationIds is not null || input.AppliedLocationIds is not null; // Create/Update 共用:优先识别 ALL 哨兵,避免后续 Count>0 或 Guid 存在性校验误伤编辑回显的 ["ALL"] var locationHasAll = AllScopeBindingHelper.HasAllScopeSentinelSelection(mergedLocationIds); var regionHasAll = AllScopeBindingHelper.HasAllScopeSentinelSelection(mergedRegionIds); var concreteLocations = LocationScopeBindingHelper.FilterConcreteScopeIds(mergedLocationIds); var (partnerType, partnerIdsForSave) = await AllScopeBindingHelper.NormalizePartnerScopeAsync( db, input.AppliedPartnerType, input.PartnerIds, input.CompanyIds, input.PartnerIds is not null || input.CompanyIds is not null); var partnerIds = partnerIdsForSave; var partnerContext = string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? partnerIds : null; string regionType; List regionIds; if (regionHasAll && concreteLocations.Count == 0) { regionType = ScopeAll; regionIds = new List(); } else { var regionResult = await AllScopeBindingHelper.NormalizeRegionScopeAsync( db, input.AppliedRegionType, LocationScopeBindingHelper.FilterConcreteScopeIds(mergedRegionIds), hasRegionArray, partnerContext); regionType = regionResult.Type; regionIds = regionResult.Ids; } string locationType; List locationIds; if (locationHasAll) { locationType = ScopeAll; locationIds = new List(); } else { var locationResult = await AllScopeBindingHelper.NormalizeLocationScopeAsync( db, input.AppliedLocationType, concreteLocations, hasLocationArray, partnerContext, string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? regionIds : null); locationType = locationResult.Type; locationIds = locationResult.Ids; } 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 && (string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) || string.Equals(locationType, ScopeSpecified, StringComparison.OrdinalIgnoreCase))) { 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 needsPartnerRows = string.Equals(scope.AppliedPartnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.PartnerIds.Count > 0; var needsRegionRows = string.Equals(scope.AppliedRegionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.RegionIds.Count > 0; var hasPartnerTable = await LabelTemplateScopeSchemaHelper.EnsurePartnerScopeTableAsync(db, needsPartnerRows); var hasRegionTable = await LabelTemplateScopeSchemaHelper.EnsureRegionScopeTableAsync(db, needsRegionRows); if (needsPartnerRows && !hasPartnerTable) { throw new UserFriendlyException( "Company 适用范围关联表尚未就绪,请联系管理员执行数据库迁移(fl_label_template_scope.sql)后重试"); } if (hasPartnerTable) { await db.Deleteable() .Where(x => x.TemplateId == templateId) .ExecuteCommandAsync(); } if (hasRegionTable) { await db.Deleteable() .Where(x => x.TemplateId == templateId) .ExecuteCommandAsync(); } await db.Deleteable() .Where(x => x.TemplateId == templateId) .ExecuteCommandAsync(); if (hasPartnerTable && needsPartnerRows) { 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 (hasRegionTable && needsRegionRows) { 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(); } await LabelTemplateScopeSchemaHelper.SetAppliedScopeTypesAsync( db, templateId, scope.AppliedPartnerType, scope.AppliedRegionType); } /// /// 删除模板适用范围子表;未执行 fl_label_template_scope.sql 时跳过 partner/region 表。 /// public static async Task DeleteTemplateScopeChildRowsAsync(ISqlSugarClient db, string templateId) { if (string.IsNullOrWhiteSpace(templateId)) { return; } 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(); } /// /// 批量加载模板适用范围展示与 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 storedScopeTypes = await LabelTemplateScopeSchemaHelper.GetAppliedScopeTypesMapAsync(db, templateIds); 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 (storedScopeTypes.TryGetValue(template.Id, out var storedTypes)) { partnerType = NormalizeScopeType(storedTypes.PartnerType, partnerType); regionType = NormalizeScopeType(storedTypes.RegionType, regionType); } if (hasExtendedScope && pIds.Count == 0 && lIds.Count > 0 && !string.Equals(partnerType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) { pIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, lIds); if (pIds.Count > 0) { partnerType = ScopeSpecified; } } if (hasExtendedScope && rIds.Count == 0 && lIds.Count > 0 && !string.Equals(regionType, ScopeSpecified, StringComparison.OrdinalIgnoreCase)) { 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); var collapsed = await ScopeAllEchoHelper.CollapseScopeIdsToAllSentinelAsync( db, pIds, rIds, lIds, ScopeAllEchoHelper.ForLabelTemplateDimensions(partnerType, regionType, locationType)); result[template.Id] = new LabelTemplateScopeDisplay { Company = companyDisplay, Region = regionDisplay, Location = locationDisplay, AppliedPartnerType = partnerType, AppliedRegionType = regionType, AppliedLocationType = locationType, PartnerIds = collapsed.PartnerIds, RegionIds = collapsed.RegionIds, LocationIds = collapsed.LocationIds }; } return result; } /// /// 列表筛选:按当前用户可见范围筛选模板。 /// 仅传 partnerId 时只按 Company 维度过滤(多公司绑定不会被门店 AND 误杀)。 /// public static async Task> ApplyTemplateScopeFilterAsync( ISqlSugarClient db, ISugarQueryable query, List? scopedLocationIds, string? partnerId = null, string? groupId = null, string? locationId = null) { var hasExtendedScope = await LabelTemplateScopeSchemaHelper.HasPartnerRegionScopeTablesAsync(db); var schema = await LabelTemplateScopeSchemaHelper.GetStatusAsync(db); // 仅 PartnerId:只返回 fl_label_template_partner 含该公司的模板。 // 不含 AppliedPartnerType=ALL(全公司模板在未传 PartnerId 时可见); // 也不把「无关联行」当成 ALL,避免其他公司数据漏出。 if (!LabelEntityListScopeHelper.ShouldApplyLocationAvailabilityFilter(partnerId, groupId, locationId) && !string.IsNullOrWhiteSpace(partnerId)) { var pid = partnerId.Trim(); if (!schema.HasPartnerScopeTable) { return query.Where(_ => false); } return query.Where(t => SqlFunc.Subqueryable() .Where(p => p.TemplateId == t.Id && p.PartnerId == pid) .Any()); } if (scopedLocationIds is null) { return query; } if (scopedLocationIds.Count == 0) { // 无可见门店:保留 Location=ALL(及无 location 关联)的模板 return 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); // Company:AppliedPartnerType=ALL 或关联命中(不用「无行=ALL」) if (schema.HasAppliedPartnerTypeColumn) { return query.Where( """ (AppliedPartnerType = @all OR EXISTS (SELECT 1 FROM fl_label_template_partner p WHERE p.TemplateId = fl_label_template.Id AND p.PartnerId IN (@partnerIds))) AND (NOT EXISTS (SELECT 1 FROM fl_label_template_region r WHERE r.TemplateId = fl_label_template.Id) OR EXISTS (SELECT 1 FROM fl_label_template_region r WHERE r.TemplateId = fl_label_template.Id AND r.GroupId IN (@groupIds))) AND (AppliedLocationType = @all OR EXISTS (SELECT 1 FROM fl_label_template_location l WHERE l.TemplateId = fl_label_template.Id AND l.LocationId IN (@locs))) """, new { all = ScopeAll, partnerIds = scopedPartnerIds, groupIds = scopedGroupIds.Count > 0 ? scopedGroupIds : new List { "__none__" }, locs = scopedLocationIds }); } return query.Where(t => 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) { var ids = LocationScopeBindingHelper.FilterConcreteScopeIds(regionIds); if (ids.Count == 0) { return; } var count = await db.Queryable() .Where(x => !x.IsDeleted && ids.Contains(x.Id)) .CountAsync(); if (count != ids.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; } }