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