refactor: 管理端去租户过滤并Portal化RBAC菜单
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
@@ -13,9 +12,8 @@ public sealed class TakeoutAdminDbContext(
|
||||
DbContextOptions<TakeoutAdminDbContext> options,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor? currentUserAccessor = null,
|
||||
IIdGenerator? idGenerator = null,
|
||||
IHttpContextAccessor? httpContextAccessor = null)
|
||||
: TakeoutAppDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor)
|
||||
IIdGenerator? idGenerator = null)
|
||||
: TakeoutAppDbContext(options, tenantProvider, currentUserAccessor, idGenerator)
|
||||
{
|
||||
/// <summary>
|
||||
/// 配置实体映射关系(不启用租户过滤)。
|
||||
@@ -27,4 +25,3 @@ public sealed class TakeoutAdminDbContext(
|
||||
OnModelCreatingCore(modelBuilder);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using TakeoutSaaS.Infrastructure.App.Persistence.Configurations;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.App.Persistence;
|
||||
|
||||
@@ -37,9 +36,8 @@ public class TakeoutAppDbContext(
|
||||
DbContextOptions options,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor? currentUserAccessor = null,
|
||||
IIdGenerator? idGenerator = null,
|
||||
IHttpContextAccessor? httpContextAccessor = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor)
|
||||
IIdGenerator? idGenerator = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator)
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户聚合根。
|
||||
|
||||
@@ -17,19 +17,23 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories;
|
||||
public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRepository
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task<Store?> FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<Store?> FindByIdAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false)
|
||||
{
|
||||
var query = context.Stores.AsNoTracking();
|
||||
if (tenantId <= 0)
|
||||
|
||||
// 1. 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. (空行后) 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 3. (空行后) 返回门店实体
|
||||
return query
|
||||
.Where(x => x.Id == storeId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -47,58 +51,68 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<Store>> SearchAsync(
|
||||
long tenantId,
|
||||
long? tenantId,
|
||||
long? merchantId,
|
||||
StoreStatus? status,
|
||||
StoreAuditStatus? auditStatus,
|
||||
StoreBusinessStatus? businessStatus,
|
||||
StoreOwnershipType? ownershipType,
|
||||
string? keyword,
|
||||
bool ignoreTenantFilter = false,
|
||||
bool includeDeleted = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = context.Stores.AsNoTracking();
|
||||
if (ignoreTenantFilter)
|
||||
|
||||
// 1. 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. (空行后) 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 3. (空行后) 可选过滤:商户
|
||||
if (merchantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.MerchantId == merchantId.Value);
|
||||
}
|
||||
|
||||
// 4. (空行后) 可选过滤:状态
|
||||
if (status.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Status == status.Value);
|
||||
}
|
||||
|
||||
// 5. (空行后) 可选过滤:审核状态
|
||||
if (auditStatus.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.AuditStatus == auditStatus.Value);
|
||||
}
|
||||
|
||||
// 6. (空行后) 可选过滤:经营状态
|
||||
if (businessStatus.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.BusinessStatus == businessStatus.Value);
|
||||
}
|
||||
|
||||
// 7. (空行后) 可选过滤:主体类型
|
||||
if (ownershipType.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.OwnershipType == ownershipType.Value);
|
||||
}
|
||||
|
||||
// 8. (空行后) 可选过滤:关键词
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
var trimmed = keyword.Trim();
|
||||
query = query.Where(x => x.Name.Contains(trimmed) || x.Code.Contains(trimmed));
|
||||
}
|
||||
|
||||
// 9. (空行后) 查询并返回结果
|
||||
var stores = await query
|
||||
.OrderBy(x => x.Name)
|
||||
.ToListAsync(cancellationToken);
|
||||
@@ -152,15 +166,14 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
}
|
||||
|
||||
var query = context.Stores.AsNoTracking();
|
||||
if (!tenantId.HasValue || tenantId.Value <= 0)
|
||||
{
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
else
|
||||
|
||||
// 1. 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 2. (空行后) 分组统计门店数量
|
||||
return await query
|
||||
.Where(x => merchantIds.Contains(x.MerchantId))
|
||||
.GroupBy(x => x.MerchantId)
|
||||
@@ -169,19 +182,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreBusinessHour>> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<StoreBusinessHour>> GetBusinessHoursAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false)
|
||||
{
|
||||
var query = context.StoreBusinessHours.AsNoTracking();
|
||||
if (tenantId <= 0)
|
||||
|
||||
// 1. 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. (空行后) 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 3. (空行后) 查询并返回营业时段
|
||||
var hours = await query
|
||||
.Where(x => x.StoreId == storeId)
|
||||
.OrderBy(x => x.DayOfWeek)
|
||||
@@ -192,19 +209,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StoreFee?> GetStoreFeeAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<StoreFee?> GetStoreFeeAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false)
|
||||
{
|
||||
var query = context.StoreFees.AsNoTracking();
|
||||
if (tenantId <= 0)
|
||||
|
||||
// 1. 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. (空行后) 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 3. (空行后) 返回费用配置
|
||||
return query
|
||||
.Where(x => x.StoreId == storeId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -224,19 +245,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreQualification>> GetQualificationsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<StoreQualification>> GetQualificationsAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false)
|
||||
{
|
||||
var query = context.StoreQualifications.AsNoTracking();
|
||||
if (tenantId <= 0)
|
||||
|
||||
// 1. 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. (空行后) 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 3. (空行后) 查询并返回资质列表
|
||||
var qualifications = await query
|
||||
.Where(x => x.StoreId == storeId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -305,19 +330,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreDeliveryZone>> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<StoreDeliveryZone>> GetDeliveryZonesAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false)
|
||||
{
|
||||
var query = context.StoreDeliveryZones.AsNoTracking();
|
||||
if (tenantId <= 0)
|
||||
|
||||
// 1. 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. (空行后) 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 3. (空行后) 查询并返回配送区域
|
||||
var zones = await query
|
||||
.Where(x => x.StoreId == storeId)
|
||||
.OrderBy(x => x.SortOrder)
|
||||
@@ -327,38 +356,46 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StoreDeliveryZone?> FindDeliveryZoneByIdAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<StoreDeliveryZone?> FindDeliveryZoneByIdAsync(long deliveryZoneId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false)
|
||||
{
|
||||
var query = context.StoreDeliveryZones.AsQueryable();
|
||||
if (tenantId <= 0)
|
||||
|
||||
// 1. 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. (空行后) 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 3. (空行后) 返回配送区域实体
|
||||
return query
|
||||
.Where(x => x.Id == deliveryZoneId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreHoliday>> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task<IReadOnlyList<StoreHoliday>> GetHolidaysAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false)
|
||||
{
|
||||
var query = context.StoreHolidays.AsNoTracking();
|
||||
if (tenantId <= 0)
|
||||
|
||||
// 1. 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. (空行后) 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 3. (空行后) 查询并返回节假日
|
||||
var holidays = await query
|
||||
.Where(x => x.StoreId == storeId)
|
||||
.OrderBy(x => x.Date)
|
||||
@@ -368,19 +405,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<StoreHoliday?> FindHolidayByIdAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<StoreHoliday?> FindHolidayByIdAsync(long holidayId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false)
|
||||
{
|
||||
var query = context.StoreHolidays.AsQueryable();
|
||||
if (tenantId <= 0)
|
||||
|
||||
// 1. 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. (空行后) 可选租户过滤
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 3. (空行后) 返回节假日实体
|
||||
return query
|
||||
.Where(x => x.Id == holidayId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -624,19 +665,16 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteDeliveryZoneAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task DeleteDeliveryZoneAsync(long deliveryZoneId, long? tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 查询目标配送区域
|
||||
var query = context.StoreDeliveryZones.AsQueryable();
|
||||
if (tenantId <= 0)
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行软删除
|
||||
var existing = await query
|
||||
.Where(x => x.Id == deliveryZoneId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
@@ -648,19 +686,16 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteHolidayAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task DeleteHolidayAsync(long holidayId, long? tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 查询目标节假日
|
||||
var query = context.StoreHolidays.AsQueryable();
|
||||
if (tenantId <= 0)
|
||||
if (tenantId.HasValue)
|
||||
{
|
||||
query = query.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
else
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
query = query.Where(x => x.TenantId == tenantId.Value);
|
||||
}
|
||||
|
||||
// 2. (空行后) 执行软删除
|
||||
var existing = await query
|
||||
.Where(x => x.Id == holidayId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
@@ -18,9 +18,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
public async Task<TenantSubscription?> FindByIdAsync(
|
||||
long subscriptionId,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
var query = ignoreTenantFilter
|
||||
var query = includeDeleted
|
||||
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
||||
: dbContext.TenantSubscriptions;
|
||||
|
||||
@@ -32,10 +32,10 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
public async Task<IReadOnlyList<TenantSubscription>> FindByIdsAsync(
|
||||
IEnumerable<long> subscriptionIds,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
var ids = subscriptionIds.ToList();
|
||||
var query = ignoreTenantFilter
|
||||
var query = includeDeleted
|
||||
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
||||
: dbContext.TenantSubscriptions;
|
||||
|
||||
@@ -48,10 +48,10 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
public async Task<(IReadOnlyList<SubscriptionWithRelations> Items, int Total)> SearchPagedAsync(
|
||||
SubscriptionSearchFilter filter,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
// 1. 构建基础查询
|
||||
var subscriptionQuery = ignoreTenantFilter
|
||||
var subscriptionQuery = includeDeleted
|
||||
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
||||
: dbContext.TenantSubscriptions;
|
||||
|
||||
@@ -134,9 +134,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
public async Task<SubscriptionDetailInfo?> GetDetailAsync(
|
||||
long subscriptionId,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
var subscriptionQuery = ignoreTenantFilter
|
||||
var subscriptionQuery = includeDeleted
|
||||
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
||||
: dbContext.TenantSubscriptions;
|
||||
|
||||
@@ -173,11 +173,11 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
public async Task<IReadOnlyList<SubscriptionWithTenant>> FindByIdsWithTenantAsync(
|
||||
IEnumerable<long> subscriptionIds,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
var ids = subscriptionIds.ToList();
|
||||
|
||||
var query = ignoreTenantFilter
|
||||
var query = includeDeleted
|
||||
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
||||
: dbContext.TenantSubscriptions;
|
||||
|
||||
@@ -201,10 +201,10 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
DateTime now,
|
||||
DateTime renewalThreshold,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
// 1. 查询开启自动续费且即将到期的活跃订阅
|
||||
var subscriptionQuery = ignoreTenantFilter
|
||||
var subscriptionQuery = includeDeleted
|
||||
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
||||
: dbContext.TenantSubscriptions;
|
||||
|
||||
@@ -232,10 +232,10 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
DateTime startOfDay,
|
||||
DateTime endOfDay,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
// 1. 查询到期落在指定区间的订阅(且未开启自动续费)
|
||||
var subscriptionQuery = ignoreTenantFilter
|
||||
var subscriptionQuery = includeDeleted
|
||||
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
||||
: dbContext.TenantSubscriptions;
|
||||
|
||||
@@ -268,9 +268,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
public async Task<IReadOnlyList<TenantSubscription>> FindExpiredActiveSubscriptionsAsync(
|
||||
DateTime now,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
var query = ignoreTenantFilter
|
||||
var query = includeDeleted
|
||||
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
||||
: dbContext.TenantSubscriptions;
|
||||
|
||||
@@ -285,9 +285,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
DateTime now,
|
||||
int gracePeriodDays,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
var query = ignoreTenantFilter
|
||||
var query = includeDeleted
|
||||
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
|
||||
: dbContext.TenantSubscriptions;
|
||||
|
||||
@@ -364,9 +364,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta
|
||||
public async Task<IReadOnlyList<TenantQuotaUsage>> GetQuotaUsagesAsync(
|
||||
long tenantId,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false)
|
||||
bool includeDeleted = false)
|
||||
{
|
||||
var query = ignoreTenantFilter
|
||||
var query = includeDeleted
|
||||
? dbContext.TenantQuotaUsages.IgnoreQueryFilters()
|
||||
: dbContext.TenantQuotaUsages;
|
||||
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using System.Reflection;
|
||||
using System.Linq;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Common.Persistence;
|
||||
|
||||
@@ -16,23 +14,12 @@ public abstract class TenantAwareDbContext(
|
||||
DbContextOptions options,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor? currentUserAccessor = null,
|
||||
IIdGenerator? idGenerator = null,
|
||||
IHttpContextAccessor? httpContextAccessor = null) : AppDbContext(options, currentUserAccessor, idGenerator)
|
||||
IIdGenerator? idGenerator = null) : AppDbContext(options, currentUserAccessor, idGenerator)
|
||||
{
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider ?? throw new ArgumentNullException(nameof(tenantProvider));
|
||||
private readonly IHttpContextAccessor? _httpContextAccessor = httpContextAccessor;
|
||||
private static readonly string[] PlatformRoleCodes =
|
||||
{
|
||||
"super-admin",
|
||||
"SUPER_ADMIN",
|
||||
"PlatformAdmin",
|
||||
"platform-admin"
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 当前请求租户 ID。
|
||||
/// </summary>
|
||||
protected long CurrentTenantId => _tenantProvider.GetCurrentTenantId();
|
||||
protected long CurrentTenantId => tenantProvider.GetCurrentTenantId();
|
||||
|
||||
/// <summary>
|
||||
/// 保存前填充租户元数据并执行基础处理。
|
||||
@@ -86,22 +73,8 @@ public abstract class TenantAwareDbContext(
|
||||
{
|
||||
if (entry.State == EntityState.Added && entry.Entity.TenantId == 0 && tenantId != 0)
|
||||
{
|
||||
if (!IsPlatformAdmin())
|
||||
{
|
||||
entry.Entity.TenantId = tenantId;
|
||||
}
|
||||
entry.Entity.TenantId = tenantId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPlatformAdmin()
|
||||
{
|
||||
var user = _httpContextAccessor?.HttpContext?.User;
|
||||
if (user?.Identity?.IsAuthenticated != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return PlatformRoleCodes.Any(user.IsInRole);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ using TakeoutSaaS.Infrastructure.Common.Persistence;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence;
|
||||
|
||||
@@ -18,9 +17,8 @@ public sealed class DictionaryDbContext(
|
||||
DbContextOptions<DictionaryDbContext> options,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor? currentUserAccessor = null,
|
||||
IIdGenerator? idGenerator = null,
|
||||
IHttpContextAccessor? httpContextAccessor = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor)
|
||||
IIdGenerator? idGenerator = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator)
|
||||
{
|
||||
/// <summary>
|
||||
/// 字典分组集合。
|
||||
|
||||
@@ -125,19 +125,6 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
public Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(忽略租户过滤器,仅用于只读查询)。
|
||||
/// </summary>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
public Task<IdentityUser?> FindByIdIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.IdentityUsers
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.DeletedAt == null)
|
||||
.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(用于更新,返回可跟踪实体)。
|
||||
/// </summary>
|
||||
@@ -147,43 +134,16 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
public Task<IdentityUser?> GetForUpdateAsync(long userId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.IdentityUsers.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(用于更新,忽略租户过滤器)。
|
||||
/// </summary>
|
||||
/// <remarks>用于跨租户场景(如平台生成的重置密码链接)。</remarks>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
public Task<IdentityUser?> GetForUpdateIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.IdentityUsers
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.DeletedAt == null)
|
||||
.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(用于更新,包含已删除数据)。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
public Task<IdentityUser?> GetForUpdateIncludingDeletedAsync(
|
||||
long tenantId,
|
||||
long userId,
|
||||
bool ignoreTenantFilter = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 构建查询(包含已删除数据)
|
||||
var query = dbContext.IdentityUsers.IgnoreQueryFilters();
|
||||
if (!ignoreTenantFilter)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
}
|
||||
|
||||
// 2. 返回实体
|
||||
return query.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
}
|
||||
public Task<IdentityUser?> GetForUpdateIncludingDeletedAsync(long userId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.IdentityUsers
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 按租户与关键字搜索后台用户(只读)。
|
||||
@@ -214,40 +174,28 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
/// 分页查询后台用户列表。
|
||||
/// </summary>
|
||||
/// <param name="filter">查询过滤条件。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤器。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分页结果。</returns>
|
||||
public async Task<(IReadOnlyList<IdentityUser> Items, int Total)> SearchPagedAsync(
|
||||
IdentityUserSearchFilter filter,
|
||||
bool ignoreTenantFilter = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 构建基础查询
|
||||
var query = dbContext.IdentityUsers.AsNoTracking();
|
||||
if (ignoreTenantFilter || filter.IncludeDeleted)
|
||||
|
||||
// 2. (空行后) 包含软删除数据时忽略全局过滤
|
||||
if (filter.IncludeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
// 2. 租户过滤
|
||||
if (!ignoreTenantFilter)
|
||||
{
|
||||
if (filter.TenantId.HasValue && filter.TenantId.Value != 0)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == filter.TenantId.Value);
|
||||
}
|
||||
}
|
||||
else if (filter.TenantId.HasValue && filter.TenantId.Value != 0)
|
||||
// 3. (空行后) 可选租户过滤
|
||||
if (filter.TenantId.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == filter.TenantId.Value);
|
||||
}
|
||||
|
||||
if (!filter.IncludeDeleted)
|
||||
{
|
||||
query = query.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
|
||||
// 3. 关键字筛选
|
||||
// 4. (空行后) 关键字筛选
|
||||
if (!string.IsNullOrWhiteSpace(filter.Keyword))
|
||||
{
|
||||
var normalized = filter.Keyword.Trim();
|
||||
@@ -259,43 +207,35 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
|| (x.Email != null && EF.Functions.ILike(x.Email, likeValue)));
|
||||
}
|
||||
|
||||
// 4. 状态过滤
|
||||
// 5. (空行后) 状态过滤
|
||||
if (filter.Status.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.Status == filter.Status.Value);
|
||||
}
|
||||
|
||||
// 5. 角色过滤
|
||||
// 6. (空行后) 角色过滤
|
||||
if (filter.RoleId.HasValue)
|
||||
{
|
||||
var roleId = filter.RoleId.Value;
|
||||
var userRoles = dbContext.UserRoles.AsNoTracking();
|
||||
if (ignoreTenantFilter || filter.IncludeDeleted)
|
||||
|
||||
// 6.1 包含软删除数据时忽略全局过滤
|
||||
if (filter.IncludeDeleted)
|
||||
{
|
||||
userRoles = userRoles.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
if (!ignoreTenantFilter)
|
||||
{
|
||||
if (filter.TenantId.HasValue && filter.TenantId.Value != 0)
|
||||
{
|
||||
userRoles = userRoles.Where(x => x.TenantId == filter.TenantId.Value);
|
||||
}
|
||||
}
|
||||
else if (filter.TenantId.HasValue && filter.TenantId.Value != 0)
|
||||
// 6.2 (空行后) 可选租户过滤
|
||||
if (filter.TenantId.HasValue)
|
||||
{
|
||||
userRoles = userRoles.Where(x => x.TenantId == filter.TenantId.Value);
|
||||
}
|
||||
|
||||
if (!filter.IncludeDeleted)
|
||||
{
|
||||
userRoles = userRoles.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
|
||||
// 6.3 (空行后) 用户角色关联过滤
|
||||
query = query.Where(user => userRoles.Any(role => role.UserId == user.Id && role.RoleId == roleId));
|
||||
}
|
||||
|
||||
// 6. 时间范围过滤
|
||||
// 7. (空行后) 时间范围过滤
|
||||
if (filter.CreatedAtFrom.HasValue)
|
||||
{
|
||||
query = query.Where(x => x.CreatedAt >= filter.CreatedAtFrom.Value);
|
||||
@@ -316,7 +256,7 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
query = query.Where(x => x.LastLoginAt <= filter.LastLoginTo.Value);
|
||||
}
|
||||
|
||||
// 7. 排序
|
||||
// 8. (空行后) 排序
|
||||
var sorted = filter.SortBy?.ToLowerInvariant() switch
|
||||
{
|
||||
"account" => filter.SortDescending
|
||||
@@ -336,7 +276,7 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
: query.OrderBy(x => x.CreatedAt)
|
||||
};
|
||||
|
||||
// 8. 分页
|
||||
// 9. (空行后) 分页
|
||||
var page = filter.Page <= 0 ? 1 : filter.Page;
|
||||
var pageSize = filter.PageSize <= 0 ? 20 : filter.PageSize;
|
||||
var total = await sorted.CountAsync(cancellationToken);
|
||||
@@ -355,11 +295,21 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
/// <param name="userIds">用户 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户列表。</returns>
|
||||
public Task<IReadOnlyList<IdentityUser>> GetByIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default)
|
||||
=> dbContext.IdentityUsers.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && userIds.Contains(x.Id))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<IdentityUser>)t.Result, cancellationToken);
|
||||
public async Task<IReadOnlyList<IdentityUser>> GetByIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 去重并快速返回空集合
|
||||
var ids = userIds.Distinct().ToArray();
|
||||
if (ids.Length == 0)
|
||||
{
|
||||
return Array.Empty<IdentityUser>();
|
||||
}
|
||||
|
||||
// 2. (空行后) 查询并返回列表
|
||||
return await dbContext.IdentityUsers
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && ids.Contains(x.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取后台用户(可用于更新,支持包含已删除数据)。
|
||||
@@ -367,42 +317,33 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="userIds">用户 ID 集合。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤器。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户列表。</returns>
|
||||
public Task<IReadOnlyList<IdentityUser>> GetForUpdateByIdsAsync(
|
||||
public async Task<IReadOnlyList<IdentityUser>> GetForUpdateByIdsAsync(
|
||||
long tenantId,
|
||||
IEnumerable<long> userIds,
|
||||
bool includeDeleted,
|
||||
bool ignoreTenantFilter = false,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 构建基础查询
|
||||
// 1. 去重并快速返回空集合
|
||||
var ids = userIds.Distinct().ToArray();
|
||||
if (ids.Length == 0)
|
||||
{
|
||||
return Task.FromResult<IReadOnlyList<IdentityUser>>(Array.Empty<IdentityUser>());
|
||||
return Array.Empty<IdentityUser>();
|
||||
}
|
||||
|
||||
var query = dbContext.IdentityUsers.Where(x => ids.Contains(x.Id));
|
||||
if (ignoreTenantFilter || includeDeleted)
|
||||
// 2. (空行后) 构建查询
|
||||
var query = dbContext.IdentityUsers
|
||||
.Where(x => x.TenantId == tenantId && ids.Contains(x.Id));
|
||||
|
||||
// 3. (空行后) 包含软删除数据时忽略全局过滤
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
if (!ignoreTenantFilter)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
}
|
||||
|
||||
if (!includeDeleted)
|
||||
{
|
||||
query = query.Where(x => x.DeletedAt == null);
|
||||
}
|
||||
|
||||
// 2. 返回列表
|
||||
return query.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<IdentityUser>)t.Result, cancellationToken);
|
||||
// 4. (空行后) 返回列表
|
||||
return await query.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
@@ -13,10 +14,9 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi
|
||||
/// 根据权限 ID 获取权限。
|
||||
/// </summary>
|
||||
/// <param name="permissionId">权限 ID。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限实体或 null。</returns>
|
||||
public Task<Permission?> FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<Permission?> FindByIdAsync(long permissionId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
@@ -26,10 +26,9 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi
|
||||
/// 根据权限编码获取权限。
|
||||
/// </summary>
|
||||
/// <param name="code">权限编码。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限实体或 null。</returns>
|
||||
public Task<Permission?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<Permission?> FindByCodeAsync(string code, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
@@ -38,11 +37,10 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi
|
||||
/// <summary>
|
||||
/// 根据权限编码集合批量获取权限。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="codes">权限编码集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限列表。</returns>
|
||||
public Task<IReadOnlyList<Permission>> GetByCodesAsync(long tenantId, IEnumerable<string> codes, CancellationToken cancellationToken = default)
|
||||
public Task<IReadOnlyList<Permission>> GetByCodesAsync(IEnumerable<string> codes, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 规范化编码集合
|
||||
var normalizedCodes = codes
|
||||
@@ -63,11 +61,10 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi
|
||||
/// <summary>
|
||||
/// 根据权限 ID 集合批量获取权限。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="permissionIds">权限 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限列表。</returns>
|
||||
public Task<IReadOnlyList<Permission>> GetByIdsAsync(long tenantId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default)
|
||||
public Task<IReadOnlyList<Permission>> GetByIdsAsync(IEnumerable<long> permissionIds, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
@@ -76,19 +73,19 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi
|
||||
.ContinueWith(t => (IReadOnlyList<Permission>)t.Result, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 按关键字搜索权限。
|
||||
/// 按 Portal 与关键字搜索权限。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="keyword">搜索关键字。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限列表。</returns>
|
||||
public Task<IReadOnlyList<Permission>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default)
|
||||
public Task<IReadOnlyList<Permission>> SearchAsync(PortalType portal, string? keyword, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 构建基础查询
|
||||
var query = dbContext.Permissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.DeletedAt == null);
|
||||
.Where(x => x.DeletedAt == null && x.Portal == portal);
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
// 2. 追加关键字过滤
|
||||
@@ -133,10 +130,9 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi
|
||||
/// 删除指定权限。
|
||||
/// </summary>
|
||||
/// <param name="permissionId">权限 ID。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
public async Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task DeleteAsync(long permissionId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 查询目标权限
|
||||
var entity = await dbContext.Permissions.FirstOrDefaultAsync(x => x.Id == permissionId, cancellationToken);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
@@ -12,15 +13,20 @@ public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IR
|
||||
/// <summary>
|
||||
/// 根据角色 ID 集合获取角色权限映射。
|
||||
/// </summary>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="roleIds">角色 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色权限映射列表。</returns>
|
||||
public Task<IReadOnlyList<RolePermission>> GetByRoleIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
|
||||
public Task<IReadOnlyList<RolePermission>> GetByRoleIdsAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
IEnumerable<long> roleIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> dbContext.RolePermissions
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeletedAt == null && roleIds.Contains(x.RoleId))
|
||||
.Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null && roleIds.Contains(x.RoleId))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<RolePermission>)t.Result, cancellationToken);
|
||||
|
||||
@@ -46,32 +52,51 @@ public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IR
|
||||
/// <summary>
|
||||
/// 替换指定角色的权限集合。
|
||||
/// </summary>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="permissionIds">权限 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
public async Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default)
|
||||
public async Task ReplaceRolePermissionsAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
long roleId,
|
||||
IEnumerable<long> permissionIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 0. 防御性参数检查
|
||||
if (portal == PortalType.Tenant && (!tenantId.HasValue || tenantId.Value == 0))
|
||||
{
|
||||
throw new ArgumentException("Portal=Tenant 时必须提供有效的 TenantId", nameof(tenantId));
|
||||
}
|
||||
|
||||
// 1. 使用执行策略保证可靠性
|
||||
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
await strategy.ExecuteAsync(async () =>
|
||||
{
|
||||
await using var trx = await dbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
// 1. 删除旧记录(原生 SQL,避免跟踪干扰)
|
||||
await dbContext.Database.ExecuteSqlRawAsync(
|
||||
"DELETE FROM \"role_permissions\" WHERE \"TenantId\" = {0} AND \"RoleId\" = {1};",
|
||||
parameters: new object[] { tenantId, roleId },
|
||||
cancellationToken: cancellationToken);
|
||||
// 1. 删除旧记录
|
||||
await dbContext.RolePermissions
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.Portal == portal && x.TenantId == tenantId && x.RoleId == roleId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
// 2. 插入新记录(防重复)
|
||||
foreach (var permissionId in permissionIds.Distinct())
|
||||
// 2. 插入新记录(权限去重)
|
||||
var distinctPermissionIds = permissionIds.Distinct().ToArray();
|
||||
if (distinctPermissionIds.Length > 0)
|
||||
{
|
||||
await dbContext.Database.ExecuteSqlRawAsync(
|
||||
"INSERT INTO \"role_permissions\" (\"TenantId\",\"RoleId\",\"PermissionId\",\"CreatedAt\",\"CreatedBy\",\"UpdatedAt\",\"UpdatedBy\",\"DeletedAt\",\"DeletedBy\") VALUES ({0},{1},{2},NOW(),NULL,NULL,NULL,NULL,NULL) ON CONFLICT DO NOTHING;",
|
||||
parameters: new object[] { tenantId, roleId, permissionId },
|
||||
cancellationToken: cancellationToken);
|
||||
var toAdd = distinctPermissionIds.Select(permissionId => new RolePermission
|
||||
{
|
||||
Portal = portal,
|
||||
TenantId = tenantId,
|
||||
RoleId = roleId,
|
||||
PermissionId = permissionId
|
||||
});
|
||||
|
||||
await dbContext.RolePermissions.AddRangeAsync(toAdd, cancellationToken);
|
||||
await dbContext.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
|
||||
await trx.CommitAsync(cancellationToken);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
@@ -12,58 +13,70 @@ public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleReposit
|
||||
/// <summary>
|
||||
/// 根据角色 ID 获取角色。
|
||||
/// </summary>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色实体或 null。</returns>
|
||||
public Task<Role?> FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<Role?> FindByIdAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Roles
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken);
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.Id == roleId && x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null,
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 根据角色编码获取角色。
|
||||
/// </summary>
|
||||
/// <param name="code">角色编码。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="code">角色编码。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色实体或 null。</returns>
|
||||
public Task<Role?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<Role?> FindByCodeAsync(PortalType portal, long? tenantId, string code, CancellationToken cancellationToken = default)
|
||||
=> dbContext.Roles
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken);
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.Code == code && x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null,
|
||||
cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 根据角色 ID 集合获取角色列表。
|
||||
/// </summary>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="roleIds">角色 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色列表。</returns>
|
||||
public Task<IReadOnlyList<Role>> GetByIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
|
||||
public Task<IReadOnlyList<Role>> GetByIdsAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
IEnumerable<long> roleIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> dbContext.Roles
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && roleIds.Contains(x.Id) && x.DeletedAt == null)
|
||||
.Where(x => x.Portal == portal && x.TenantId == tenantId && roleIds.Contains(x.Id) && x.DeletedAt == null)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<Role>)t.Result, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 按关键字搜索角色。
|
||||
/// </summary>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="keyword">搜索关键字。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色列表。</returns>
|
||||
public Task<IReadOnlyList<Role>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default)
|
||||
public Task<IReadOnlyList<Role>> SearchAsync(PortalType portal, long? tenantId, string? keyword, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 构建基础查询
|
||||
var query = dbContext.Roles
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeletedAt == null);
|
||||
.Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null);
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
// 2. 追加关键字过滤
|
||||
@@ -107,14 +120,15 @@ public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleReposit
|
||||
/// <summary>
|
||||
/// 软删除角色。
|
||||
/// </summary>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
public async Task DeleteAsync(long roleId, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task DeleteAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 查询目标角色
|
||||
var entity = await dbContext.Roles.FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId, cancellationToken);
|
||||
var entity = await dbContext.Roles.FirstOrDefaultAsync(x => x.Id == roleId && x.Portal == portal && x.TenantId == tenantId, cancellationToken);
|
||||
if (entity != null)
|
||||
{
|
||||
// 2. 标记删除时间
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
@@ -12,42 +13,58 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol
|
||||
/// <summary>
|
||||
/// 根据用户 ID 集合获取用户角色映射。
|
||||
/// </summary>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="userIds">用户 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>用户角色映射列表。</returns>
|
||||
public Task<IReadOnlyList<UserRole>> GetByUserIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default)
|
||||
public Task<IReadOnlyList<UserRole>> GetByUserIdsAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
IEnumerable<long> userIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> dbContext.UserRoles
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeletedAt == null && userIds.Contains(x.UserId))
|
||||
.Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null && userIds.Contains(x.UserId))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<UserRole>)t.Result, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定用户的角色集合。
|
||||
/// </summary>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>用户角色列表。</returns>
|
||||
public Task<IReadOnlyList<UserRole>> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default)
|
||||
public Task<IReadOnlyList<UserRole>> GetByUserIdAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
long userId,
|
||||
CancellationToken cancellationToken = default)
|
||||
=> dbContext.UserRoles
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeletedAt == null && x.UserId == userId)
|
||||
.Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null && x.UserId == userId)
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<UserRole>)t.Result, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 替换指定用户的角色集合。
|
||||
/// </summary>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="roleIds">角色 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
public async Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
|
||||
public async Task ReplaceUserRolesAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
long userId,
|
||||
IEnumerable<long> roleIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 使用执行策略保障一致性
|
||||
var strategy = dbContext.Database.CreateExecutionStrategy();
|
||||
@@ -58,7 +75,7 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol
|
||||
// 2. 读取当前角色映射
|
||||
var existing = await dbContext.UserRoles
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == tenantId && x.UserId == userId)
|
||||
.Where(x => x.Portal == portal && x.TenantId == tenantId && x.UserId == userId)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
// 3. 去重并构建目标集合
|
||||
@@ -90,6 +107,7 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol
|
||||
.Where(roleId => !existingRoleMap.ContainsKey(roleId))
|
||||
.Select(roleId => new UserRole
|
||||
{
|
||||
Portal = portal,
|
||||
TenantId = tenantId,
|
||||
UserId = userId,
|
||||
RoleId = roleId
|
||||
@@ -105,15 +123,16 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol
|
||||
/// <summary>
|
||||
/// 统计指定角色下的用户数量。
|
||||
/// </summary>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>用户数量。</returns>
|
||||
public Task<int> CountUsersByRoleAsync(long tenantId, long roleId, CancellationToken cancellationToken = default)
|
||||
public Task<int> CountUsersByRoleAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default)
|
||||
=> dbContext.UserRoles
|
||||
.IgnoreQueryFilters()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && x.DeletedAt == null && x.RoleId == roleId)
|
||||
.Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null && x.RoleId == roleId)
|
||||
.CountAsync(cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -2,11 +2,11 @@ using MassTransit;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Infrastructure.Common.Persistence;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
|
||||
@@ -17,9 +17,8 @@ public sealed class IdentityDbContext(
|
||||
DbContextOptions<IdentityDbContext> options,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor? currentUserAccessor = null,
|
||||
IIdGenerator? idGenerator = null,
|
||||
IHttpContextAccessor? httpContextAccessor = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor)
|
||||
IIdGenerator? idGenerator = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator)
|
||||
{
|
||||
/// <summary>
|
||||
/// 管理后台用户集合。
|
||||
@@ -93,7 +92,12 @@ public sealed class IdentityDbContext(
|
||||
/// <param name="builder">实体构建器。</param>
|
||||
private static void ConfigureIdentityUser(EntityTypeBuilder<IdentityUser> builder)
|
||||
{
|
||||
builder.ToTable("identity_users");
|
||||
builder.ToTable("identity_users", t =>
|
||||
{
|
||||
t.HasCheckConstraint(
|
||||
"CK_identity_users_Portal_Tenant",
|
||||
"(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)");
|
||||
});
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.Account).HasMaxLength(64).IsRequired();
|
||||
builder.Property(x => x.DisplayName).HasMaxLength(64).IsRequired();
|
||||
@@ -110,18 +114,33 @@ public sealed class IdentityDbContext(
|
||||
.IsRowVersion()
|
||||
.IsConcurrencyToken()
|
||||
.HasColumnType("bytea");
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.Portal).HasConversion<int>().IsRequired();
|
||||
builder.Property(x => x.TenantId);
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
|
||||
builder.HasIndex(x => x.TenantId);
|
||||
builder.HasIndex(x => new { x.TenantId, x.Account }).IsUnique();
|
||||
builder.HasIndex(x => x.TenantId).HasFilter("\"Portal\" = 1");
|
||||
|
||||
builder.HasIndex(x => x.Account)
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 0");
|
||||
builder.HasIndex(x => new { x.TenantId, x.Account })
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 1");
|
||||
|
||||
builder.HasIndex(x => x.Phone)
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 0 AND \"Phone\" IS NOT NULL");
|
||||
builder.HasIndex(x => new { x.TenantId, x.Phone })
|
||||
.IsUnique()
|
||||
.HasFilter("\"Phone\" IS NOT NULL");
|
||||
.HasFilter("\"Portal\" = 1 AND \"Phone\" IS NOT NULL");
|
||||
|
||||
builder.HasIndex(x => x.Email)
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 0 AND \"Email\" IS NOT NULL");
|
||||
builder.HasIndex(x => new { x.TenantId, x.Email })
|
||||
.IsUnique()
|
||||
.HasFilter("\"Email\" IS NOT NULL");
|
||||
.HasFilter("\"Portal\" = 1 AND \"Email\" IS NOT NULL");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -146,23 +165,36 @@ public sealed class IdentityDbContext(
|
||||
|
||||
private static void ConfigureRole(EntityTypeBuilder<Role> builder)
|
||||
{
|
||||
builder.ToTable("roles");
|
||||
builder.ToTable("roles", t =>
|
||||
{
|
||||
t.HasCheckConstraint(
|
||||
"CK_roles_Portal_Tenant",
|
||||
"(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)");
|
||||
});
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.Portal).HasConversion<int>().IsRequired();
|
||||
builder.Property(x => x.TenantId);
|
||||
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
|
||||
builder.Property(x => x.Code).HasMaxLength(64).IsRequired();
|
||||
builder.Property(x => x.Description).HasMaxLength(256);
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
builder.HasIndex(x => x.TenantId);
|
||||
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
|
||||
|
||||
builder.HasIndex(x => x.Code)
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 0");
|
||||
builder.HasIndex(x => new { x.TenantId, x.Code })
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 1");
|
||||
builder.HasIndex(x => x.TenantId)
|
||||
.HasFilter("\"Portal\" = 1");
|
||||
}
|
||||
|
||||
private static void ConfigurePermission(EntityTypeBuilder<Permission> builder)
|
||||
{
|
||||
builder.ToTable("permissions");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.Portal).HasConversion<int>().IsRequired();
|
||||
builder.Property(x => x.ParentId).IsRequired();
|
||||
builder.Property(x => x.SortOrder).IsRequired();
|
||||
builder.Property(x => x.Type).HasMaxLength(16).IsRequired();
|
||||
@@ -171,9 +203,8 @@ public sealed class IdentityDbContext(
|
||||
builder.Property(x => x.Description).HasMaxLength(256);
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
builder.HasIndex(x => x.TenantId);
|
||||
builder.HasIndex(x => new { x.TenantId, x.ParentId, x.SortOrder });
|
||||
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
|
||||
builder.HasIndex(x => new { x.Portal, x.ParentId, x.SortOrder });
|
||||
builder.HasIndex(x => x.Code).IsUnique();
|
||||
}
|
||||
|
||||
private static void ConfigureRoleTemplate(EntityTypeBuilder<RoleTemplate> builder)
|
||||
@@ -200,35 +231,59 @@ public sealed class IdentityDbContext(
|
||||
|
||||
private static void ConfigureUserRole(EntityTypeBuilder<UserRole> builder)
|
||||
{
|
||||
builder.ToTable("user_roles");
|
||||
builder.ToTable("user_roles", t =>
|
||||
{
|
||||
t.HasCheckConstraint(
|
||||
"CK_user_roles_Portal_Tenant",
|
||||
"(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)");
|
||||
});
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.Portal).HasConversion<int>().IsRequired();
|
||||
builder.Property(x => x.TenantId);
|
||||
builder.Property(x => x.UserId).IsRequired();
|
||||
builder.Property(x => x.RoleId).IsRequired();
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
builder.HasIndex(x => x.TenantId);
|
||||
builder.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique();
|
||||
builder.HasIndex(x => new { x.UserId, x.RoleId })
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 0");
|
||||
builder.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId })
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 1");
|
||||
builder.HasIndex(x => x.TenantId)
|
||||
.HasFilter("\"Portal\" = 1");
|
||||
}
|
||||
|
||||
private static void ConfigureRolePermission(EntityTypeBuilder<RolePermission> builder)
|
||||
{
|
||||
builder.ToTable("role_permissions");
|
||||
builder.ToTable("role_permissions", t =>
|
||||
{
|
||||
t.HasCheckConstraint(
|
||||
"CK_role_permissions_Portal_Tenant",
|
||||
"(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)");
|
||||
});
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.Portal).HasConversion<int>().IsRequired();
|
||||
builder.Property(x => x.TenantId);
|
||||
builder.Property(x => x.RoleId).IsRequired();
|
||||
builder.Property(x => x.PermissionId).IsRequired();
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
builder.HasIndex(x => x.TenantId);
|
||||
builder.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique();
|
||||
builder.HasIndex(x => new { x.RoleId, x.PermissionId })
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 0");
|
||||
builder.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId })
|
||||
.IsUnique()
|
||||
.HasFilter("\"Portal\" = 1");
|
||||
builder.HasIndex(x => x.TenantId)
|
||||
.HasFilter("\"Portal\" = 1");
|
||||
}
|
||||
|
||||
private static void ConfigureMenuDefinition(EntityTypeBuilder<MenuDefinition> builder)
|
||||
{
|
||||
builder.ToTable("menu_definitions");
|
||||
builder.HasKey(x => x.Id);
|
||||
builder.Property(x => x.TenantId).IsRequired();
|
||||
builder.Property(x => x.Portal).HasConversion<int>().IsRequired();
|
||||
builder.Property(x => x.ParentId).IsRequired();
|
||||
builder.Property(x => x.Name).HasMaxLength(64).IsRequired();
|
||||
builder.Property(x => x.Path).HasMaxLength(256).IsRequired();
|
||||
@@ -243,6 +298,6 @@ public sealed class IdentityDbContext(
|
||||
builder.Property(x => x.AuthListJson).HasColumnType("text");
|
||||
ConfigureAuditableEntity(builder);
|
||||
ConfigureSoftDeleteEntity(builder);
|
||||
builder.HasIndex(x => new { x.TenantId, x.ParentId, x.SortOrder });
|
||||
builder.HasIndex(x => new { x.Portal, x.ParentId, x.SortOrder });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
|
||||
@@ -10,15 +11,13 @@ namespace TakeoutSaaS.Infrastructure.Identity.Repositories;
|
||||
/// </summary>
|
||||
public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuRepository
|
||||
{
|
||||
private const long PlatformRootTenantId = 1000000000001;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<IReadOnlyList<MenuDefinition>> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<IReadOnlyList<MenuDefinition>> GetByPortalAsync(PortalType portal, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return dbContext.MenuDefinitions
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == PlatformRootTenantId && x.DeletedAt == null)
|
||||
.Where(x => x.Portal == portal && x.DeletedAt == null)
|
||||
.OrderBy(x => x.ParentId)
|
||||
.ThenBy(x => x.SortOrder)
|
||||
.ToListAsync(cancellationToken)
|
||||
@@ -26,13 +25,13 @@ public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuReposit
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<MenuDefinition?> FindByIdAsync(long id, long tenantId, CancellationToken cancellationToken = default)
|
||||
public Task<MenuDefinition?> FindByIdAsync(long id, PortalType portal, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return dbContext.MenuDefinitions
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(
|
||||
x => x.Id == id && x.TenantId == PlatformRootTenantId && x.DeletedAt == null,
|
||||
x => x.Id == id && x.Portal == portal && x.DeletedAt == null,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
@@ -50,11 +49,11 @@ public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuReposit
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task DeleteAsync(long id, long tenantId, CancellationToken cancellationToken = default)
|
||||
public async Task DeleteAsync(long id, PortalType portal, CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 查询目标
|
||||
var entity = await dbContext.MenuDefinitions
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, cancellationToken);
|
||||
.FirstOrDefaultAsync(x => x.Id == id && x.Portal == portal, cancellationToken);
|
||||
|
||||
// 2. 存在则删除
|
||||
if (entity is not null)
|
||||
|
||||
@@ -71,25 +71,33 @@ public sealed class JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptio
|
||||
/// <returns>Claims 集合</returns>
|
||||
private static List<Claim> BuildClaims(CurrentUserProfile profile)
|
||||
{
|
||||
// 1. 构建基础身份 Claim
|
||||
var userId = profile.UserId.ToString();
|
||||
var claims = new List<Claim>
|
||||
{
|
||||
new(JwtRegisteredClaimNames.Sub, userId),
|
||||
new(ClaimTypes.NameIdentifier, userId),
|
||||
new(JwtRegisteredClaimNames.UniqueName, profile.Account),
|
||||
new("tenant_id", profile.TenantId.ToString()),
|
||||
new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
|
||||
};
|
||||
|
||||
// 2. (空行后) 仅租户端账号写入 tenant_id Claim
|
||||
if (profile.TenantId.HasValue)
|
||||
{
|
||||
claims.Add(new Claim("tenant_id", profile.TenantId.Value.ToString()));
|
||||
}
|
||||
|
||||
// 3. (空行后) 可选写入商户上下文 Claim
|
||||
if (profile.MerchantId.HasValue)
|
||||
{
|
||||
claims.Add(new Claim("merchant_id", profile.MerchantId.Value.ToString()));
|
||||
}
|
||||
|
||||
// 4. (空行后) 写入角色与权限 Claim
|
||||
claims.AddRange(profile.Roles.Select(role => new Claim(ClaimTypes.Role, role)));
|
||||
|
||||
claims.AddRange(profile.Permissions.Select(permission => new Claim("permission", permission)));
|
||||
|
||||
// 5. (空行后) 返回 Claim 集合
|
||||
return claims;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ using TakeoutSaaS.Infrastructure.Common.Persistence;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
using TakeoutSaaS.Shared.Abstractions.Security;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Logs.Persistence;
|
||||
|
||||
@@ -18,9 +17,8 @@ public sealed class TakeoutLogsDbContext(
|
||||
DbContextOptions<TakeoutLogsDbContext> options,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor? currentUserAccessor = null,
|
||||
IIdGenerator? idGenerator = null,
|
||||
IHttpContextAccessor? httpContextAccessor = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor)
|
||||
IIdGenerator? idGenerator = null)
|
||||
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator)
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户审计日志集合。
|
||||
|
||||
Reference in New Issue
Block a user