refactor: 移除身份跨租户忽略租户过滤
This commit is contained in:
@@ -57,7 +57,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
|
||||
// 4. 查询目标用户集合
|
||||
var includeDeleted = request.Operation == IdentityUserBatchOperation.Restore;
|
||||
var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, false, cancellationToken);
|
||||
var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, cancellationToken);
|
||||
var usersById = users.ToDictionary(user => user.Id, user => user, EqualityComparer<long>.Default);
|
||||
|
||||
// 5. 预计算租户管理员约束
|
||||
@@ -79,7 +79,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
|
||||
IncludeDeleted = false,
|
||||
Page = 1,
|
||||
PageSize = 1
|
||||
}, false, cancellationToken)).Total;
|
||||
}, cancellationToken)).Total;
|
||||
var remainingActiveAdmins = activeAdminCount;
|
||||
|
||||
// 6. 执行批量操作
|
||||
|
||||
@@ -137,7 +137,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
|
||||
Page = 1,
|
||||
PageSize = 1
|
||||
};
|
||||
var result = await identityUserRepository.SearchPagedAsync(filter, false, cancellationToken);
|
||||
var result = await identityUserRepository.SearchPagedAsync(filter, cancellationToken);
|
||||
if (result.Total <= 1)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员");
|
||||
|
||||
@@ -111,7 +111,7 @@ public sealed class DeleteIdentityUserCommandHandler(
|
||||
Page = 1,
|
||||
PageSize = 1
|
||||
};
|
||||
var result = await identityUserRepository.SearchPagedAsync(filter, false, cancellationToken);
|
||||
var result = await identityUserRepository.SearchPagedAsync(filter, cancellationToken);
|
||||
if (result.Total <= 1)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员");
|
||||
|
||||
@@ -30,7 +30,7 @@ public sealed class GetIdentityUserDetailQueryHandler(
|
||||
IdentityUser? user;
|
||||
if (request.IncludeDeleted)
|
||||
{
|
||||
user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, false, cancellationToken);
|
||||
user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -47,8 +47,8 @@ public sealed class ResetAdminPasswordByTokenCommandHandler(
|
||||
throw new BusinessException(ErrorCodes.BadRequest, "重置链接无效或已过期");
|
||||
}
|
||||
|
||||
// 3. 获取用户(可更新,忽略租户过滤器)并写入新密码哈希
|
||||
var user = await userRepository.GetForUpdateIgnoringTenantAsync(userId.Value, cancellationToken)
|
||||
// 3. 获取用户(可更新,强制租户隔离)并写入新密码哈希
|
||||
var user = await userRepository.GetForUpdateAsync(userId.Value, cancellationToken)
|
||||
?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
|
||||
|
||||
user.PasswordHash = passwordHasher.HashPassword(user, password);
|
||||
|
||||
@@ -36,7 +36,7 @@ public sealed class RestoreIdentityUserCommandHandler(
|
||||
}
|
||||
|
||||
// 3. 查询用户实体(包含已删除)
|
||||
var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, false, cancellationToken);
|
||||
var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, cancellationToken);
|
||||
if (user == null)
|
||||
{
|
||||
return false;
|
||||
|
||||
@@ -52,7 +52,7 @@ public sealed class SearchIdentityUsersQueryHandler(
|
||||
};
|
||||
|
||||
// 4. 执行分页查询
|
||||
var (items, total) = await identityUserRepository.SearchPagedAsync(filter, false, cancellationToken);
|
||||
var (items, total) = await identityUserRepository.SearchPagedAsync(filter, cancellationToken);
|
||||
if (items.Count == 0)
|
||||
{
|
||||
return new PagedResult<UserListItemDto>(Array.Empty<UserListItemDto>(), request.Page, request.PageSize, total);
|
||||
|
||||
@@ -8,15 +8,6 @@ namespace TakeoutSaaS.Domain.Identity.Repositories;
|
||||
/// </summary>
|
||||
public interface IIdentityUserRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据账号获取后台用户。
|
||||
/// </summary>
|
||||
/// <remarks>为保证多租户隔离,优先使用带租户参数的重载方法。</remarks>
|
||||
/// <param name="account">账号。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> FindByAccountAsync(string account, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据租户与账号获取后台用户。
|
||||
/// </summary>
|
||||
@@ -84,14 +75,6 @@ public interface IIdentityUserRepository
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(忽略租户过滤器,仅用于只读查询)。
|
||||
/// </summary>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> FindByIdIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(用于更新,返回可跟踪实体)。
|
||||
/// </summary>
|
||||
@@ -100,27 +83,16 @@ public interface IIdentityUserRepository
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> GetForUpdateAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(用于更新,忽略租户过滤器)。
|
||||
/// </summary>
|
||||
/// <remarks>用于跨租户场景(如平台生成的重置密码链接)。</remarks>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> GetForUpdateIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(用于更新,包含已删除数据)。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> GetForUpdateIncludingDeletedAsync(
|
||||
long tenantId,
|
||||
long userId,
|
||||
bool ignoreTenantFilter = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -136,12 +108,10 @@ public interface IIdentityUserRepository
|
||||
/// 分页查询后台用户列表。
|
||||
/// </summary>
|
||||
/// <param name="filter">查询过滤条件。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤器。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分页结果。</returns>
|
||||
Task<(IReadOnlyList<IdentityUser> Items, int Total)> SearchPagedAsync(
|
||||
IdentityUserSearchFilter filter,
|
||||
bool ignoreTenantFilter = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -159,14 +129,12 @@ public interface IIdentityUserRepository
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="userIds">用户 ID 集合。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤器。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户列表。</returns>
|
||||
Task<IReadOnlyList<IdentityUser>> GetForUpdateByIdsAsync(
|
||||
long tenantId,
|
||||
IEnumerable<long> userIds,
|
||||
bool includeDeleted,
|
||||
bool ignoreTenantFilter = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -9,15 +9,6 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||
/// </summary>
|
||||
public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIdentityUserRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据账号获取后台用户。
|
||||
/// </summary>
|
||||
/// <param name="account">账号。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
public Task<IdentityUser?> FindByAccountAsync(string account, CancellationToken cancellationToken = default)
|
||||
=> dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Account == account, cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 根据租户与账号获取后台用户。
|
||||
/// </summary>
|
||||
@@ -143,19 +134,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>
|
||||
@@ -165,42 +143,23 @@ 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);
|
||||
// 1. 构建查询(包含已删除数据,但强制租户隔离)
|
||||
return dbContext.IdentityUsers
|
||||
.IgnoreQueryFilters()
|
||||
.Where(x => x.TenantId == tenantId)
|
||||
.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -232,33 +191,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)
|
||||
{
|
||||
if (!filter.TenantId.HasValue || filter.TenantId.Value <= 0)
|
||||
{
|
||||
throw new InvalidOperationException("TenantId 不能为空且必须大于 0");
|
||||
}
|
||||
|
||||
var tenantId = filter.TenantId.Value;
|
||||
|
||||
// 1. 构建基础查询
|
||||
var query = dbContext.IdentityUsers.AsNoTracking();
|
||||
if (ignoreTenantFilter || filter.IncludeDeleted)
|
||||
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)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == filter.TenantId.Value);
|
||||
}
|
||||
// 2. 租户过滤(强制)
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
|
||||
if (!filter.IncludeDeleted)
|
||||
{
|
||||
@@ -288,22 +242,12 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
{
|
||||
var roleId = filter.RoleId.Value;
|
||||
var userRoles = dbContext.UserRoles.AsNoTracking();
|
||||
if (ignoreTenantFilter || filter.IncludeDeleted)
|
||||
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)
|
||||
{
|
||||
userRoles = userRoles.Where(x => x.TenantId == filter.TenantId.Value);
|
||||
}
|
||||
userRoles = userRoles.Where(x => x.TenantId == tenantId);
|
||||
|
||||
if (!filter.IncludeDeleted)
|
||||
{
|
||||
@@ -373,11 +317,12 @@ 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()
|
||||
public async Task<IReadOnlyList<IdentityUser>> GetByIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await dbContext.IdentityUsers.AsNoTracking()
|
||||
.Where(x => x.TenantId == tenantId && userIds.Contains(x.Id))
|
||||
.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<IdentityUser>)t.Result, cancellationToken);
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取后台用户(可用于更新,支持包含已删除数据)。
|
||||
@@ -385,33 +330,28 @@ 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. 构建基础查询
|
||||
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)
|
||||
if (includeDeleted)
|
||||
{
|
||||
query = query.IgnoreQueryFilters();
|
||||
}
|
||||
|
||||
if (!ignoreTenantFilter)
|
||||
{
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
}
|
||||
query = query.Where(x => x.TenantId == tenantId);
|
||||
|
||||
if (!includeDeleted)
|
||||
{
|
||||
@@ -419,8 +359,7 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde
|
||||
}
|
||||
|
||||
// 2. 返回列表
|
||||
return query.ToListAsync(cancellationToken)
|
||||
.ContinueWith(t => (IReadOnlyList<IdentityUser>)t.Result, cancellationToken);
|
||||
return await query.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user