using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Entities; using SqlSugar; using Volo.Abp; using Volo.Abp.Guids; using Volo.Abp.Users; using Yi.Framework.Rbac.Domain.Entities; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Application.Helpers; /// /// Region(fl_group.Id)与 Location(location.Id)范围解析:Region 落库对应 location.GroupName。 /// public static class LocationScopeBindingHelper { /// /// 读取用户 userlocation 绑定的门店 Id(Guid 归一化,避免大小写导致漏绑)。 /// public static async Task> ResolveBoundLocationIdsForUserAsync( ISqlSugarClient db, Guid userId) { var userKey = TeamMemberListScopeHelper.UserKey(userId); var userIdStr = userId.ToString("D"); var rows = await db.Ado.SqlQueryAsync( """ SELECT LocationId FROM userlocation WHERE IsDeleted = 0 AND ( UserId = @UserIdD OR UserId = @UserIdLower OR LOWER(TRIM(UserId)) = @UserIdLower ) """, new { UserIdD = userIdStr, UserIdLower = userKey }); var locationIds = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var locationId in rows) { var locKey = TeamMemberListScopeHelper.NormalizeScopeKey(locationId); if (!string.IsNullOrEmpty(locKey)) { locationIds.Add(locKey); } } return locationIds.OrderBy(x => x, StringComparer.Ordinal).ToList(); } /// /// App 可选门店 Id:Company Admin 展开为绑定 Company 下全部门店;其它角色为 userlocation 绑定。 /// public static Task> ResolveAppAccessibleLocationIdsForUserAsync( ISqlSugarClient db, Guid userId) => ResolveAppAccessibleLocationIdsForUserAsync(db, userId, null); /// /// App 可选门店 Id;传入 时优先 JWT 声明识别 Company Admin,避免 ORM 查 Role 触发数据权限过滤器。 /// public static async Task> ResolveAppAccessibleLocationIdsForUserAsync( ISqlSugarClient db, Guid userId, ICurrentUser? currentUser) { var locationIds = await ResolveBoundLocationIdsForUserAsync(db, userId); if (locationIds.Count == 0) { return locationIds; } var isCompanyAdmin = currentUser?.Id == userId ? await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser) : await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, userId); if (!isCompanyAdmin) { return locationIds; } var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); if (partnerIds.Count == 0) { return locationIds; } var expanded = await ResolveLocationIdsFromPartnerIdsAsync(db, partnerIds); if (expanded.Count == 0) { return locationIds; } var merged = new HashSet(StringComparer.OrdinalIgnoreCase); foreach (var id in expanded) { var key = TeamMemberListScopeHelper.NormalizeScopeKey(id); if (!string.IsNullOrEmpty(key)) { merged.Add(key); } } return merged.OrderBy(x => x, StringComparer.Ordinal).ToList(); } public static async Task> ResolveDisplayRegionIdsForCurrentUserAsync( ISqlSugarClient db, ICurrentUser currentUser) { if (currentUser.Id is null) { return new List(); } return await ResolveDisplayRegionIdsForUserAsync(db, currentUser, null); } /// /// 与 Team Member 详情/编辑回显一致:Company Admin 展示绑定 Company 下全部 Region。 /// public static async Task> ResolveDisplayRegionIdsForUserAsync( ISqlSugarClient db, ICurrentUser currentUser, Guid? roleId) { if (currentUser.Id is null) { return new List(); } var locationIds = await ResolveBoundLocationIdsForUserAsync(db, currentUser.Id.Value); if (locationIds.Count == 0) { return new List(); } var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds); if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser) && partnerIds.Count > 0) { regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds); } return regionIds; } /// /// 按用户 Id 解析可见 Region(无 JWT 上下文时用,如 Team Member 详情)。 /// public static async Task> ResolveDisplayRegionIdsForUserAsync( ISqlSugarClient db, Guid userId, Guid? roleId) { var locationIds = await ResolveBoundLocationIdsForUserAsync(db, userId); if (locationIds.Count == 0) { return new List(); } var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, locationIds); var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, locationIds); if (await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, userId) && partnerIds.Count > 0) { regionIds = await ResolveGroupIdsFromPartnerIdsAsync(db, partnerIds); } return regionIds; } /// /// 根据成员绑定的门店 Id 解析 Company / Region 范围(Team Member 详情、编辑回显)。 /// public static async Task<(List PartnerIds, List RegionIds, List LocationIds)> ResolveMemberScopeFromLocationIdsAsync(ISqlSugarClient db, IReadOnlyList locationIds) { var normalized = NormalizeIds(locationIds); if (normalized.Count == 0) { return (new List(), new List(), new List()); } var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, normalized); var regionIds = await ResolveGroupIdsFromLocationIdsAsync(db, normalized); return (partnerIds, regionIds, normalized); } /// /// 列表筛选:门店 Id 优先,否则 Region(groupId),否则 Company(partnerId);均未传返回 null。 /// public static async Task?> ResolveFilteredLocationIdsForListAsync( ISqlSugarClient db, string? partnerId, string? groupId, string? locationId) { var locId = locationId?.Trim(); if (!string.IsNullOrWhiteSpace(locId)) { if (!Guid.TryParse(locId, out var locationGuid)) { return new List(); } var exists = await db.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id == locationGuid); return exists ? new List { locId } : new List(); } var gid = groupId?.Trim(); var pid = partnerId?.Trim(); if (string.IsNullOrWhiteSpace(pid) && string.IsNullOrWhiteSpace(gid)) { return null; } var q = db.Queryable().Where(x => !x.IsDeleted); if (!string.IsNullOrWhiteSpace(gid)) { var g = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == gid); if (g is null) { return new List(); } var gName = g.GroupName?.Trim() ?? string.Empty; var partner = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); if (partner is null) { return new List(); } var pName = partner.PartnerName?.Trim() ?? string.Empty; q = q.Where(x => x.GroupName == gName && x.Partner == pName); } else if (!string.IsNullOrWhiteSpace(pid)) { var partner = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == pid); if (partner is null) { return new List(); } var pName = partner.PartnerName?.Trim() ?? string.Empty; q = q.Where(x => x.Partner == pName); } return await q.Select(x => SqlFunc.ToString(x.Id)).ToListAsync(); } /// /// 列表筛选:按门店 Id 优先,否则按 Region(groupId)解析门店主键;均未传返回 null。 /// public static async Task?> ResolveScopedLocationIdsAsync( ISqlSugarClient db, string? groupId, string? locationId) { var locId = locationId?.Trim(); if (!string.IsNullOrWhiteSpace(locId)) { if (!Guid.TryParse(locId, out var locationGuid)) { return new List(); } var exists = await db.Queryable() .AnyAsync(x => !x.IsDeleted && x.Id == locationGuid); return exists ? new List { locId } : new List(); } var gid = groupId?.Trim(); if (string.IsNullOrWhiteSpace(gid)) { return null; } return await ResolveLocationIdsFromGroupIdsAsync(db, new List { gid }); } /// /// 将 Company(partnerId)、Region(groupIds)与显式 locationIds 合并为去重后的门店 Id 列表。 /// public static Task> MergeToLocationIdsAsync( ISqlSugarClient db, string? partnerId, IReadOnlyList? groupIds, IReadOnlyList? locationIds) => MergeToLocationIdsAsync(db, string.IsNullOrWhiteSpace(partnerId) ? null : new[] { partnerId.Trim() }, groupIds, locationIds); /// /// 将 Company(partnerIds)、Region(groupIds)与显式 locationIds 合并为去重后的门店 Id 列表。 /// public static async Task> MergeToLocationIdsAsync( ISqlSugarClient db, IReadOnlyList? partnerIds, IReadOnlyList? groupIds, IReadOnlyList? locationIds) { var merged = new HashSet(StringComparer.Ordinal); var fromPartner = await ResolveLocationIdsFromPartnerIdsAsync(db, partnerIds); foreach (var id in fromPartner) { merged.Add(id); } var fromGroups = await ResolveLocationIdsFromGroupIdsAsync(db, groupIds); foreach (var id in fromGroups) { merged.Add(id); } foreach (var id in NormalizeIds(locationIds)) { merged.Add(id); } return merged.ToList(); } /// /// 标签类型/分类/多选项门店范围落库:仅按 Region/Location 合并,不因 Company(partner)展开并集。 /// public static async Task> ResolveEntityLocationIdsForSaveAsync( ISqlSugarClient db, bool locationSpecified, IReadOnlyList? regionIds, IReadOnlyList? locationIds) { if (!locationSpecified) { return new List(); } var merged = await MergeToLocationIdsAsync( db, (IReadOnlyList?)null, regionIds, locationIds); if (merged.Count == 0) { throw new UserFriendlyException("指定适用区域或门店时,至少需要匹配到一个有效门店"); } await ValidateLocationIdsExistAsync(db, merged); return merged; } /// /// 根据已绑定门店反推适用的 Company Id(fl_partner.Id)。 /// public static async Task> ResolvePartnerIdsFromLocationIdsAsync( ISqlSugarClient db, IReadOnlyList locationIds) { var ids = NormalizeIds(locationIds); if (ids.Count == 0) { return new List(); } var guidList = ids.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); if (guidList.Count == 0) { return new List(); } var locs = await db.Queryable() .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) .ToListAsync(); if (locs.Count == 0) { return new List(); } var partnerNames = locs .Select(x => x.Partner?.Trim()) .Where(x => !string.IsNullOrEmpty(x)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToList(); if (partnerNames.Count == 0) { return new List(); } var partners = await db.Queryable() .Where(x => !x.IsDeleted) .ToListAsync(); var partnerIdSet = new HashSet(StringComparer.Ordinal); foreach (var name in partnerNames) { var match = partners.FirstOrDefault(p => string.Equals(p.PartnerName?.Trim(), name, StringComparison.OrdinalIgnoreCase) || string.Equals(p.Id, name, StringComparison.OrdinalIgnoreCase)); if (match is not null && !string.IsNullOrWhiteSpace(match.Id)) { partnerIdSet.Add(match.Id.Trim()); } } return partnerIdSet.OrderBy(x => x, StringComparer.Ordinal).ToList(); } /// /// 解析 Company(fl_partner.Id)下全部未删除门店 Id。 /// public static async Task> ResolveLocationIdsFromPartnerIdsAsync( ISqlSugarClient db, IReadOnlyList? partnerIds) { var ids = NormalizeIds(partnerIds); if (ids.Count == 0) { return new List(); } var result = new HashSet(StringComparer.Ordinal); foreach (var pid in ids) { var partner = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == pid); if (partner is null) { continue; } var pName = partner.PartnerName?.Trim() ?? string.Empty; if (string.IsNullOrEmpty(pName)) { continue; } var locIds = await db.Queryable() .Where(x => !x.IsDeleted && x.Partner == pName) .Select(x => SqlFunc.ToString(x.Id)) .ToListAsync(); foreach (var lid in locIds.Where(x => !string.IsNullOrWhiteSpace(x))) { result.Add(lid.Trim()); } } return result.ToList(); } /// /// 根据已绑定门店反推适用的 Region Id(fl_group.Id)。 /// public static async Task> ResolveGroupIdsFromLocationIdsAsync( ISqlSugarClient db, IReadOnlyList locationIds) { var ids = NormalizeIds(locationIds); if (ids.Count == 0) { return new List(); } var guidList = ids.Where(x => Guid.TryParse(x, out _)).Select(Guid.Parse).ToList(); if (guidList.Count == 0) { return new List(); } var locs = await db.Queryable() .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) .ToListAsync(); if (locs.Count == 0) { return new List(); } var partners = await db.Queryable() .Where(x => !x.IsDeleted) .ToListAsync(); var partnerNameToId = partners .Where(x => !string.IsNullOrWhiteSpace(x.PartnerName)) .GroupBy(x => x.PartnerName!.Trim(), StringComparer.OrdinalIgnoreCase) .ToDictionary(g => g.Key, g => g.First().Id, StringComparer.OrdinalIgnoreCase); var groups = await db.Queryable() .Where(x => !x.IsDeleted) .ToListAsync(); var groupIdSet = new HashSet(StringComparer.Ordinal); foreach (var loc in locs) { var gName = loc.GroupName?.Trim(); var pName = loc.Partner?.Trim(); if (string.IsNullOrEmpty(gName) || string.IsNullOrEmpty(pName)) { continue; } if (!partnerNameToId.TryGetValue(pName, out var partnerId)) { continue; } var match = groups.FirstOrDefault(g => g.PartnerId == partnerId && string.Equals(g.GroupName?.Trim(), gName, StringComparison.OrdinalIgnoreCase)); if (match is not null && !string.IsNullOrWhiteSpace(match.Id)) { groupIdSet.Add(match.Id.Trim()); } } return groupIdSet.OrderBy(x => x, StringComparer.Ordinal).ToList(); } /// /// 解析 Region(fl_group.Id)下全部未删除门店 Id。 /// public static async Task> ResolveLocationIdsFromGroupIdsAsync( ISqlSugarClient db, IReadOnlyList? groupIds) { var ids = NormalizeIds(groupIds); if (ids.Count == 0) { return new List(); } var result = new HashSet(StringComparer.Ordinal); foreach (var gid in ids) { var g = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == gid); if (g is null) { continue; } var partner = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == g.PartnerId); var pName = partner?.PartnerName?.Trim() ?? string.Empty; var gName = g.GroupName?.Trim() ?? string.Empty; if (string.IsNullOrEmpty(pName) || string.IsNullOrEmpty(gName)) { continue; } var locIds = await db.Queryable() .Where(x => !x.IsDeleted && x.Partner == pName && x.GroupName == gName) .Select(x => SqlFunc.ToString(x.Id)) .ToListAsync(); foreach (var lid in locIds.Where(x => !string.IsNullOrWhiteSpace(x))) { result.Add(lid.Trim()); } } return result.ToList(); } /// /// 解析 Company(fl_partner.Id)下全部 Region Id(fl_group.Id)。 /// public static async Task> ResolveGroupIdsFromPartnerIdsAsync( ISqlSugarClient db, IReadOnlyList? partnerIds) { var ids = NormalizeIds(partnerIds); if (ids.Count == 0) { return new List(); } var result = new HashSet(StringComparer.Ordinal); foreach (var pid in ids) { var groupIds = await db.Queryable() .Where(x => !x.IsDeleted && x.PartnerId == pid) .Select(x => x.Id) .ToListAsync(); foreach (var gid in groupIds.Where(x => !string.IsNullOrWhiteSpace(x))) { result.Add(gid.Trim()); } } return result.OrderBy(x => x, StringComparer.Ordinal).ToList(); } /// /// 校验合并后的门店 Id 均存在。 /// public static async Task ValidateLocationIdsExistAsync(ISqlSugarClient db, IReadOnlyList locationIds) { var ids = NormalizeIds(locationIds); if (ids.Count == 0) { return; } foreach (var id in ids) { if (!Guid.TryParse(id, out _)) { throw new UserFriendlyException("门店Id格式不正确"); } } var guidList = ids.Select(Guid.Parse).ToList(); var existCount = await db.Queryable() .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) .CountAsync(); if (existCount != ids.Count) { throw new UserFriendlyException("门店不存在"); } } public static List NormalizeIds(IReadOnlyList? raw) { return raw? .Where(x => !string.IsNullOrWhiteSpace(x)) .Select(x => x.Trim()) .Distinct(StringComparer.Ordinal) .ToList() ?? new List(); } /// /// Company Admin 在其绑定 Company 下新建 Region 后,将该 Region 下已有门店追加写入 userlocation(不删除已有绑定)。 /// public static async Task TryAutoBindCompanyAdminToNewRegionAsync( ISqlSugarClient db, ICurrentUser currentUser, IGuidGenerator guidGenerator, string partnerId, string groupName, string? creatorId) { if (currentUser.Id is null || ReportsRoleHelper.IsAdminRole(currentUser)) { return; } if (!await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser)) { return; } var pid = partnerId.Trim(); if (!await IsPartnerAllowedForUserAsync(db, currentUser.Id.Value, pid)) { return; } var locationIds = await ResolveLocationIdsForPartnerAndGroupNameAsync(db, pid, groupName); await AppendUserLocationBindingsAsync(db, guidGenerator, currentUser.Id.Value, locationIds, creatorId); } /// /// Company Admin 在其绑定 Company 下新建门店后,将该门店追加写入 userlocation(空 Region 先建后补门店场景)。 /// public static async Task TryAutoBindCompanyAdminToNewLocationAsync( ISqlSugarClient db, ICurrentUser currentUser, IGuidGenerator guidGenerator, string locationId, string? partnerName, string? creatorId) { if (currentUser.Id is null || ReportsRoleHelper.IsAdminRole(currentUser)) { return; } if (!await TeamMemberRoleHelper.IsCompanyAdminUserAsync(db, currentUser)) { return; } var partnerIds = await ResolvePartnerIdsFromLocationIdsAsync( db, new List { locationId }); if (partnerIds.Count == 0) { partnerIds = await ResolvePartnerIdsFromPartnerNamesAsync(db, partnerName); } if (partnerIds.Count == 0 || !await IsAnyPartnerAllowedForUserAsync(db, currentUser.Id.Value, partnerIds)) { return; } await AppendUserLocationBindingsAsync( db, guidGenerator, currentUser.Id.Value, new List { locationId }, creatorId); } /// /// 追加 userlocation 绑定(幂等;仅插入当前用户尚未绑定的门店)。 /// public static async Task AppendUserLocationBindingsAsync( ISqlSugarClient db, IGuidGenerator guidGenerator, Guid userId, IReadOnlyList locationIds, string? creatorId) { var wanted = NormalizeIds(locationIds); if (wanted.Count == 0) { return 0; } foreach (var id in wanted) { if (!Guid.TryParse(id, out _)) { return 0; } } var guidList = wanted.Select(Guid.Parse).ToList(); var existCount = await db.Queryable() .Where(x => !x.IsDeleted && guidList.Contains(x.Id)) .CountAsync(); if (existCount != wanted.Count) { return 0; } var existing = await ResolveBoundLocationIdsForUserAsync(db, userId); var existingSet = new HashSet(existing, StringComparer.OrdinalIgnoreCase); var toInsert = wanted.Where(x => !existingSet.Contains(x)).ToList(); if (toInsert.Count == 0) { return 0; } var now = DateTime.Now; var userIdString = TeamMemberListScopeHelper.UserKey(userId); var rows = toInsert.Select(locationId => new UserLocationDbEntity { Id = guidGenerator.Create().ToString(), IsDeleted = false, CreationTime = now, CreatorId = creatorId, UserId = userIdString, LocationId = locationId, ConcurrencyStamp = string.Empty }).ToList(); await db.Insertable(rows).ExecuteCommandAsync(); return toInsert.Count; } private static async Task IsPartnerAllowedForUserAsync( ISqlSugarClient db, Guid userId, string partnerId) { var boundLocationIds = await ResolveBoundLocationIdsForUserAsync(db, userId); if (boundLocationIds.Count == 0) { return false; } var allowedPartnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, boundLocationIds); return allowedPartnerIds.Contains(partnerId.Trim(), StringComparer.OrdinalIgnoreCase); } private static async Task IsAnyPartnerAllowedForUserAsync( ISqlSugarClient db, Guid userId, IReadOnlyList partnerIds) { var boundLocationIds = await ResolveBoundLocationIdsForUserAsync(db, userId); if (boundLocationIds.Count == 0) { return false; } var allowedPartnerIds = await ResolvePartnerIdsFromLocationIdsAsync(db, boundLocationIds); var allowedSet = new HashSet(allowedPartnerIds, StringComparer.OrdinalIgnoreCase); return partnerIds.Any(id => allowedSet.Contains(id)); } private static async Task> ResolvePartnerIdsFromPartnerNamesAsync( ISqlSugarClient db, string? partnerName) { var name = partnerName?.Trim(); if (string.IsNullOrWhiteSpace(name)) { return new List(); } var partners = await db.Queryable() .Where(x => !x.IsDeleted) .ToListAsync(); var match = partners.FirstOrDefault(p => string.Equals(p.PartnerName?.Trim(), name, StringComparison.OrdinalIgnoreCase) || string.Equals(p.Id, name, StringComparison.OrdinalIgnoreCase)); return match is null || string.IsNullOrWhiteSpace(match.Id) ? new List() : new List { match.Id.Trim() }; } private static async Task> ResolveLocationIdsForPartnerAndGroupNameAsync( ISqlSugarClient db, string partnerId, string groupName) { var partner = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == partnerId.Trim()); if (partner is null) { return new List(); } var pName = partner.PartnerName?.Trim() ?? string.Empty; var gName = groupName.Trim(); if (string.IsNullOrEmpty(pName) || string.IsNullOrEmpty(gName)) { return new List(); } return await db.Queryable() .Where(x => !x.IsDeleted && x.Partner == pName && x.GroupName == gName) .Select(x => SqlFunc.ToString(x.Id)) .ToListAsync(); } }