using System.Text.Json; using FoodLabeling.Application.Contracts; using FoodLabeling.Application.Contracts.Dtos.AuthScope; using FoodLabeling.Application.Contracts.Dtos.UsAppAuth; using FoodLabeling.Application.Services.DbModels; using FoodLabeling.Domain.Entities; using Microsoft.Extensions.Caching.Distributed; using SqlSugar; using Volo.Abp; using Volo.Abp.Users; using Yi.Framework.SqlSugarCore.Abstractions; namespace FoodLabeling.Application.Helpers; /// /// App 管理员 Company → Region → Location 级联选店(与 5-26 auth-scope / us-app-auth 文档一致)。 /// public static class UsAppAuthScopeHelper { private const string CacheKeyPrefix = "FoodLabeling:UsAppAdminScope:"; public static void EnsureAdminAppToken(ICurrentUser currentUser) { if (currentUser.Id is null) { throw new UserFriendlyException("用户未登录"); } var kind = currentUser.FindClaim(UsAppJwtClaims.ClientKind)?.Value; if (!string.Equals(kind, UsAppJwtClaims.ClientKindUsApp, StringComparison.Ordinal)) { throw new UserFriendlyException("请使用 App 登录令牌调用该接口"); } if (!ReportsRoleHelper.IsAdminRole(currentUser)) { throw new UserFriendlyException("仅管理员可使用公司/区域/门店筛选接口"); } } public static async Task> ListCompaniesAsync(ISqlSugarClient db) { return await db.Queryable() .Where(x => !x.IsDeleted && x.State) .OrderBy(x => x.PartnerName) .Select(x => new AuthScopeCompanyOptionDto { Id = x.Id, PartnerName = x.PartnerName ?? string.Empty, State = x.State, }) .ToListAsync(); } public static async Task> ListRegionsAsync( ISqlSugarClient db, string partnerId) { var pid = partnerId.Trim(); if (string.IsNullOrEmpty(pid)) { throw new UserFriendlyException("请选择公司"); } return await db.Queryable() .Where(x => !x.IsDeleted && x.State && x.PartnerId == pid) .OrderBy(x => x.GroupName) .Select(x => new AuthScopeRegionOptionDto { Id = x.Id, GroupName = x.GroupName ?? string.Empty, PartnerId = x.PartnerId, State = x.State, }) .ToListAsync(); } public static async Task> ListLocationsAsync( ISqlSugarClient db, string partnerId, string groupId) { var ids = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync( db, partnerId, groupId, null); if (ids is null || ids.Count == 0) { return new List(); } var g = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == groupId.Trim()); var partner = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == partnerId.Trim()); var locations = (await db.Queryable() .Where(x => !x.IsDeleted && ids.Contains(x.Id.ToString())) .OrderBy(x => x.OrderNum) .ToListAsync()) .OrderBy(x => x.OrderNum) .ThenBy(x => x.LocationName, StringComparer.OrdinalIgnoreCase) .ToList(); return locations.Select(x => new AuthScopeLocationOptionDto { Id = x.Id.ToString(), LocationCode = x.LocationCode ?? string.Empty, LocationName = x.LocationName ?? string.Empty, FullAddress = BuildFullAddress(x), State = x.State, PartnerId = partner?.Id ?? partnerId.Trim(), GroupId = g?.Id ?? groupId.Trim(), GroupName = g?.GroupName ?? string.Empty, }).ToList(); } public static async Task SelectLocationAsync( ISqlSugarClient db, IDistributedCache cache, Guid userId, UsAppSelectAdminScopeLocationInputVo input) { var partnerId = input.PartnerId?.Trim() ?? string.Empty; var groupId = input.GroupId?.Trim() ?? string.Empty; var locationId = input.LocationId?.Trim() ?? string.Empty; if (string.IsNullOrEmpty(partnerId) || string.IsNullOrEmpty(groupId) || string.IsNullOrEmpty(locationId)) { throw new UserFriendlyException("请选择公司、区域和门店"); } var scopedIds = await LocationScopeBindingHelper.ResolveFilteredLocationIdsForListAsync( db, partnerId, groupId, null); if (scopedIds is null || !scopedIds.Contains(locationId)) { throw new UserFriendlyException("门店与所选公司/区域不匹配"); } if (!Guid.TryParse(locationId, out var locationGuid)) { throw new UserFriendlyException("无效的门店标识"); } var loc = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == locationGuid); if (loc is null) { throw new UserFriendlyException("门店不存在或已删除"); } var partner = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == partnerId); var group = await db.Queryable() .FirstAsync(x => !x.IsDeleted && x.Id == groupId); var bound = new UsAppBoundLocationDto { Id = loc.Id.ToString(), LocationCode = loc.LocationCode ?? string.Empty, LocationName = loc.LocationName ?? string.Empty, FullAddress = BuildFullAddress(loc), State = loc.State, }; var cacheItem = new UsAppAdminScopeCacheItem { PartnerId = partnerId, PartnerName = partner?.PartnerName?.Trim() ?? string.Empty, GroupId = groupId, GroupName = group?.GroupName?.Trim() ?? string.Empty, Location = bound, }; await SetAdminScopeCacheAsync(cache, userId, cacheItem); return new AuthScopeSelectLocationOutputDto { PartnerId = cacheItem.PartnerId, PartnerName = cacheItem.PartnerName, GroupId = cacheItem.GroupId, GroupName = cacheItem.GroupName, Location = bound, }; } public static async Task GetAdminScopeCacheAsync( IDistributedCache cache, Guid userId) { var raw = await cache.GetStringAsync(CacheKeyPrefix + userId); if (string.IsNullOrWhiteSpace(raw)) { return null; } try { return JsonSerializer.Deserialize(raw); } catch { return null; } } public static Task RemoveAdminScopeCacheAsync(IDistributedCache cache, Guid userId) => cache.RemoveAsync(CacheKeyPrefix + userId); private static async Task SetAdminScopeCacheAsync( IDistributedCache cache, Guid userId, UsAppAdminScopeCacheItem item) { var json = JsonSerializer.Serialize(item); await cache.SetStringAsync( CacheKeyPrefix + userId, json, new DistributedCacheEntryOptions { AbsoluteExpirationRelativeToNow = TimeSpan.FromHours(24), }); } private static string BuildFullAddress(LocationAggregateRoot loc) { var street = loc.Street?.Trim(); var city = loc.City?.Trim(); var state = loc.StateCode?.Trim(); var zip = loc.ZipCode?.Trim(); var line2Parts = new List(); if (!string.IsNullOrEmpty(city)) { line2Parts.Add(city); } if (!string.IsNullOrEmpty(state)) { line2Parts.Add(state); } var line2 = line2Parts.Count > 0 ? string.Join(", ", line2Parts) : string.Empty; if (!string.IsNullOrEmpty(zip)) { line2 = string.IsNullOrEmpty(line2) ? zip : $"{line2} {zip}"; } var segments = new List(); if (!string.IsNullOrEmpty(street)) { segments.Add(street); } if (!string.IsNullOrEmpty(line2)) { segments.Add(line2); } return segments.Count == 0 ? "无" : string.Join(", ", segments); } public sealed class UsAppAdminScopeCacheItem { public string PartnerId { get; set; } = string.Empty; public string PartnerName { get; set; } = string.Empty; public string GroupId { get; set; } = string.Empty; public string GroupName { get; set; } = string.Empty; public UsAppBoundLocationDto Location { get; set; } = new(); } }