refactor: 移除身份跨租户忽略租户过滤

This commit is contained in:
root
2026-01-29 13:48:31 +00:00
parent 8276174526
commit b3d611304b
9 changed files with 36 additions and 129 deletions

View File

@@ -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. 执行批量操作

View File

@@ -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, "至少保留一个管理员");

View File

@@ -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, "至少保留一个管理员");

View File

@@ -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
{

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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>

View File

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