using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Shared.Helpers; using SqlSugar; using Volo.Abp; namespace FoodLabeling.Application.Helpers; /// /// 培训文件适用范围:Company / Region / Location 三维度独立 ALL/SPECIFIED。 /// public static class TrainingFileScopeHelper { public sealed class TrainingFileScopeSaveResult { public string AppliedPartnerType { get; init; } = AllScopeBindingHelper.ScopeAll; public List PartnerIds { get; init; } = new(); public string AppliedRegionType { get; init; } = AllScopeBindingHelper.ScopeAll; public List RegionIds { get; init; } = new(); public string AvailabilityType { get; init; } = AllScopeBindingHelper.ScopeAll; public List LocationIds { get; init; } = new(); } public sealed class TrainingFileScopeDisplay { public string AppliedPartnerType { get; init; } = AllScopeBindingHelper.ScopeAll; public string Company { get; init; } = LabelEntityPartnerScopeHelper.AllCompaniesDisplay; public List PartnerIds { get; init; } = new(); public string AppliedRegionType { get; init; } = AllScopeBindingHelper.ScopeAll; public string Region { get; init; } = FoodLabelingDisplayConsts.AllRegion; public List RegionIds { get; init; } = new(); public string AvailabilityType { get; init; } = AllScopeBindingHelper.ScopeAll; public string Location { get; init; } = FoodLabelingDisplayConsts.AllLocation; public List LocationIds { get; init; } = new(); } public sealed class LocationScopeContext { public string LocationId { get; init; } = string.Empty; public string? PartnerId { get; init; } public string? GroupId { get; init; } } /// /// 解析保存入参中的三维度范围。 /// public static async Task ResolveScopeForSaveAsync( ISqlSugarClient db, string? appliedPartnerType, IReadOnlyList? partnerIds, IReadOnlyList? companyIds, string? appliedRegionType, IReadOnlyList? regionIds, IReadOnlyList? groupIds, string? availabilityType, IReadOnlyList? locationIds) { var partnerScope = await LabelEntityPartnerScopeHelper.ResolvePartnerScopeForSaveAsync( db, appliedPartnerType, partnerIds, companyIds); var partnerContext = string.Equals( partnerScope.AppliedPartnerType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? partnerScope.PartnerIds : null; var mergedRegionIds = NormalizeRegionIds(regionIds, groupIds); var hasRegionArray = regionIds is not null || groupIds is not null; var (regionType, regionIdsForSave) = await AllScopeBindingHelper.NormalizeRegionScopeAsync( db, appliedRegionType, mergedRegionIds, hasRegionArray, partnerContext); if (string.Equals(regionType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) && regionIdsForSave.Count > 0) { await ValidateGroupIdsExistAsync(db, regionIdsForSave); } var hasLocationArray = locationIds is not null; var (locationType, locationIdsForSave) = await AllScopeBindingHelper.NormalizeLocationScopeAsync( db, availabilityType, locationIds ?? new List(), hasLocationArray, partnerContext, regionIdsForSave); if (string.Equals(locationType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) && locationIdsForSave.Count > 0) { await LocationScopeBindingHelper.ValidateLocationIdsExistAsync(db, locationIdsForSave); } return new TrainingFileScopeSaveResult { AppliedPartnerType = partnerScope.AppliedPartnerType, PartnerIds = partnerScope.PartnerIds, AppliedRegionType = regionType, RegionIds = regionIdsForSave, AvailabilityType = locationType, LocationIds = locationIdsForSave }; } /// /// 保存培训文件三维度范围关联。 /// public static async Task SaveScopeAsync( ISqlSugarClient db, string trainingFileId, TrainingFileScopeSaveResult scope, string? currentUserId, DateTime now) { await db.Deleteable() .Where(x => x.TrainingFileId == trainingFileId) .ExecuteCommandAsync(); await db.Deleteable() .Where(x => x.TrainingFileId == trainingFileId) .ExecuteCommandAsync(); await db.Deleteable() .Where(x => x.TrainingFileId == trainingFileId) .ExecuteCommandAsync(); if (string.Equals(scope.AppliedPartnerType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.PartnerIds.Count > 0) { var rows = scope.PartnerIds.Select(pid => new FlTrainingFilePartnerDbEntity { Id = YitIdHelper.NextId().ToString(), TrainingFileId = trainingFileId, PartnerId = pid, CreationTime = now, CreatorId = currentUserId }).ToList(); await db.Insertable(rows).ExecuteCommandAsync(); } if (string.Equals(scope.AppliedRegionType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.RegionIds.Count > 0) { var rows = scope.RegionIds.Select(gid => new FlTrainingFileRegionDbEntity { Id = YitIdHelper.NextId().ToString(), TrainingFileId = trainingFileId, GroupId = gid, CreationTime = now, CreatorId = currentUserId }).ToList(); await db.Insertable(rows).ExecuteCommandAsync(); } if (string.Equals(scope.AvailabilityType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) && scope.LocationIds.Count > 0) { var rows = scope.LocationIds.Select(lid => new FlTrainingFileLocationDbEntity { Id = YitIdHelper.NextId().ToString(), TrainingFileId = trainingFileId, LocationId = lid, CreationTime = now, CreatorId = currentUserId }).ToList(); await db.Insertable(rows).ExecuteCommandAsync(); } } /// /// 构建多个培训文件的适用范围回显(列表/树批量用)。 /// public static async Task> BuildScopeDisplayMapAsync( ISqlSugarClient db, IReadOnlyList files) { if (files.Count == 0) { return new Dictionary(StringComparer.Ordinal); } var tasks = files.Select(async file => (file.Id, Display: await BuildScopeDisplayAsync(db, file))); var results = await Task.WhenAll(tasks); return results.ToDictionary(x => x.Id, x => x.Display, StringComparer.Ordinal); } /// /// 删除培训文件全部范围关联。 /// public static async Task DeleteScopeRowsAsync(ISqlSugarClient db, string trainingFileId) { await db.Deleteable() .Where(x => x.TrainingFileId == trainingFileId) .ExecuteCommandAsync(); await db.Deleteable() .Where(x => x.TrainingFileId == trainingFileId) .ExecuteCommandAsync(); await db.Deleteable() .Where(x => x.TrainingFileId == trainingFileId) .ExecuteCommandAsync(); } /// /// 构建详情/编辑回显用的范围数据。 /// public static async Task BuildScopeDisplayAsync( ISqlSugarClient db, FlTrainingFileDbEntity file) { var partnerIds = string.Equals( file.AppliedPartnerType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? await db.Queryable() .Where(x => x.TrainingFileId == file.Id) .Select(x => x.PartnerId) .ToListAsync() : new List(); var regionIds = string.Equals( file.AppliedRegionType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? await db.Queryable() .Where(x => x.TrainingFileId == file.Id) .Select(x => x.GroupId) .ToListAsync() : new List(); var locationIds = string.Equals( file.AvailabilityType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase) ? await db.Queryable() .Where(x => x.TrainingFileId == file.Id) .Select(x => x.LocationId) .ToListAsync() : new List(); partnerIds = LocationScopeBindingHelper.NormalizeIds(partnerIds); regionIds = LocationScopeBindingHelper.NormalizeIds(regionIds); locationIds = LocationScopeBindingHelper.NormalizeIds(locationIds); var collapsed = await ScopeAllEchoHelper.CollapseScopeIdsToAllSentinelAsync( db, partnerIds, regionIds, locationIds, ScopeAllEchoHelper.ForLabelTemplateDimensions( file.AppliedPartnerType, file.AppliedRegionType, file.AvailabilityType)); return new TrainingFileScopeDisplay { AppliedPartnerType = file.AppliedPartnerType, Company = await BuildPartnerDisplayAsync(db, file.AppliedPartnerType, collapsed.PartnerIds), PartnerIds = collapsed.PartnerIds, AppliedRegionType = file.AppliedRegionType, Region = await BuildRegionDisplayAsync(db, file.AppliedRegionType, collapsed.RegionIds), RegionIds = collapsed.RegionIds, AvailabilityType = file.AvailabilityType, Location = await BuildLocationDisplayAsync(db, file.AvailabilityType, collapsed.LocationIds), LocationIds = collapsed.LocationIds }; } /// /// 按门店上下文过滤可见文件(Company + Region + Location 三维度 AND)。 /// public static ISugarQueryable ApplyLocationVisibilityFilter( ISugarQueryable query, LocationScopeContext? context) { if (context is null || string.IsNullOrWhiteSpace(context.LocationId)) { return query; } var locationId = context.LocationId.Trim(); var partnerId = context.PartnerId?.Trim(); var groupId = context.GroupId?.Trim(); return query.Where(f => f.AppliedPartnerType == AllScopeBindingHelper.ScopeAll || (partnerId != null && SqlFunc.Subqueryable() .Where(p => p.TrainingFileId == f.Id && p.PartnerId == partnerId) .Any())) .Where(f => f.AppliedRegionType == AllScopeBindingHelper.ScopeAll || (groupId != null && SqlFunc.Subqueryable() .Where(r => r.TrainingFileId == f.Id && r.GroupId == groupId) .Any())) .Where(f => f.AvailabilityType == AllScopeBindingHelper.ScopeAll || SqlFunc.Subqueryable() .Where(l => l.TrainingFileId == f.Id && l.LocationId == locationId) .Any()); } /// /// 解析门店对应的 Company / Region 上下文。 /// public static async Task ResolveLocationScopeContextAsync( ISqlSugarClient db, string locationId) { if (string.IsNullOrWhiteSpace(locationId)) { return null; } var normalized = locationId.Trim(); var partnerIds = await LocationScopeBindingHelper.ResolvePartnerIdsFromLocationIdsAsync(db, new[] { normalized }); var groupIds = await LocationScopeBindingHelper.ResolveGroupIdsFromLocationIdsAsync(db, new[] { normalized }); return new LocationScopeContext { LocationId = normalized, PartnerId = partnerIds.FirstOrDefault(), GroupId = groupIds.FirstOrDefault() }; } 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 ValidateGroupIdsExistAsync(ISqlSugarClient db, List groupIds) { if (groupIds.Count == 0) { return; } var existing = await db.Queryable() .Where(x => !x.IsDeleted && groupIds.Contains(x.Id)) .Select(x => x.Id) .ToListAsync(); var existingSet = new HashSet(existing, StringComparer.OrdinalIgnoreCase); var missing = groupIds.Where(id => !existingSet.Contains(id)).ToList(); if (missing.Count > 0) { throw new UserFriendlyException("存在无效的 Region Id"); } } private static async Task BuildPartnerDisplayAsync( ISqlSugarClient db, string appliedPartnerType, IReadOnlyList partnerIds) { if (!string.Equals(appliedPartnerType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase)) { return LabelEntityPartnerScopeHelper.AllCompaniesDisplay; } if (LocationScopeBindingHelper.ContainsAllScopeSentinel(partnerIds)) { return LabelEntityPartnerScopeHelper.AllCompaniesDisplay; } if (partnerIds.Count == 0) { return FoodLabelingDisplayConsts.NotAvailable; } var rows = await db.Queryable() .Where(x => !x.IsDeleted && partnerIds.Contains(x.Id)) .Select(x => x.PartnerName) .ToListAsync(); var names = rows.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x!.Trim()).Distinct().OrderBy(x => x).ToList(); return names.Count > 0 ? string.Join(", ", names) : FoodLabelingDisplayConsts.NotAvailable; } private static async Task BuildRegionDisplayAsync( ISqlSugarClient db, string appliedRegionType, IReadOnlyList regionIds) { if (!string.Equals(appliedRegionType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase)) { return FoodLabelingDisplayConsts.AllRegion; } if (LocationScopeBindingHelper.ContainsAllScopeSentinel(regionIds) || regionIds.Count == 0) { return regionIds.Count == 0 ? FoodLabelingDisplayConsts.NotAvailable : FoodLabelingDisplayConsts.AllRegion; } var rows = await db.Queryable() .Where(x => !x.IsDeleted && regionIds.Contains(x.Id)) .Select(x => x.GroupName) .ToListAsync(); var names = rows.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x!.Trim()).Distinct().OrderBy(x => x).ToList(); return names.Count > 0 ? string.Join(", ", names) : FoodLabelingDisplayConsts.NotAvailable; } private static async Task BuildLocationDisplayAsync( ISqlSugarClient db, string availabilityType, IReadOnlyList locationIds) { if (!string.Equals(availabilityType, AllScopeBindingHelper.ScopeSpecified, StringComparison.OrdinalIgnoreCase)) { return FoodLabelingDisplayConsts.AllLocation; } if (LocationScopeBindingHelper.ContainsAllScopeSentinel(locationIds) || locationIds.Count == 0) { return locationIds.Count == 0 ? FoodLabelingDisplayConsts.NotAvailable : FoodLabelingDisplayConsts.AllLocation; } var guidList = locationIds.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); if (guidList.Count == 0) { return FoodLabelingDisplayConsts.NotAvailable; } var rows = await db.Queryable() .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) .Select(x => x.LocationName) .ToListAsync(); var names = rows.Where(x => !string.IsNullOrWhiteSpace(x)).Select(x => x!.Trim()).Distinct().OrderBy(x => x).ToList(); return names.Count > 0 ? string.Join(", ", names) : FoodLabelingDisplayConsts.NotAvailable; } }