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