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;
}
}