feat: 新增租户管理端 TenantApi 并移除旧 API

This commit is contained in:
root
2026-01-29 11:39:57 +00:00
parent 17dc73c61d
commit 86ef0d6033
60 changed files with 450 additions and 1368 deletions

View File

@@ -1,14 +1,11 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Domain.Merchants.Services;
using TakeoutSaaS.Domain.Stores.Repositories;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -21,31 +18,19 @@ public sealed class ExportMerchantPdfQueryHandler(
IStoreRepository storeRepository,
ITenantRepository tenantRepository,
IMerchantExportService exportService,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
ITenantProvider tenantProvider)
: IRequestHandler<ExportMerchantPdfQuery, byte[]>
{
public async Task<byte[]> Handle(ExportMerchantPdfQuery request, CancellationToken cancellationToken)
{
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
if (merchant == null)
{
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止导出其他租户商户");
}
var stores = await storeRepository.GetByMerchantIdAsync(merchant.Id, merchant.TenantId, cancellationToken);
var auditLogs = await merchantRepository.GetAuditLogsAsync(merchant.Id, merchant.TenantId, cancellationToken);
var tenant = await tenantRepository.FindByIdAsync(merchant.TenantId, cancellationToken);

View File

@@ -1,12 +1,9 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -16,9 +13,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// </summary>
public sealed class GetMerchantAuditHistoryQueryHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantAuditHistoryQuery, IReadOnlyList<MerchantAuditLogDto>>
{
/// <inheritdoc />
@@ -27,23 +22,13 @@ public sealed class GetMerchantAuditHistoryQueryHandler(
CancellationToken cancellationToken)
{
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
if (merchant == null)
{
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户审核历史");
}
var logs = await merchantRepository.GetAuditLogsAsync(merchant.Id, merchant.TenantId, cancellationToken);
return logs.Select(MerchantMapping.ToDto).ToList();
}

View File

@@ -1,12 +1,9 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -16,9 +13,7 @@ namespace TakeoutSaaS.Application.App.Merchants.Handlers;
/// </summary>
public sealed class GetMerchantChangeHistoryQueryHandler(
IMerchantRepository merchantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantChangeHistoryQuery, IReadOnlyList<MerchantChangeLogDto>>
{
/// <inheritdoc />
@@ -27,23 +22,13 @@ public sealed class GetMerchantChangeHistoryQueryHandler(
CancellationToken cancellationToken)
{
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
if (merchant == null)
{
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户变更历史");
}
var logs = await merchantRepository.GetChangeLogsAsync(merchant.Id, merchant.TenantId, request.FieldName, cancellationToken);
return logs.Select(MerchantMapping.ToDto).ToList();
}

View File

@@ -1,14 +1,11 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Domain.Stores.Repositories;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -20,9 +17,7 @@ public sealed class GetMerchantDetailQueryHandler(
IMerchantRepository merchantRepository,
IStoreRepository storeRepository,
ITenantRepository tenantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantDetailQuery, MerchantDetailDto>
{
/// <summary>
@@ -33,25 +28,15 @@ public sealed class GetMerchantDetailQueryHandler(
/// <returns>商户详情 DTO。</returns>
public async Task<MerchantDetailDto> Handle(GetMerchantDetailQuery request, CancellationToken cancellationToken)
{
// 1. 获取权限与商户
// 1. 获取当前租户并查询商户
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
if (merchant == null)
{
throw new BusinessException(ErrorCodes.NotFound, "商户不存在");
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止访问其他租户的商户");
}
// 2. 查询门店与租户信息
var stores = await storeRepository.GetByMerchantIdAsync(merchant.Id, merchant.TenantId, cancellationToken);
var storeDtos = MerchantMapping.ToStoreDtos(stores);

View File

@@ -1,15 +1,12 @@
using MediatR;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.App.Merchants.Queries;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Repositories;
using TakeoutSaaS.Domain.Stores.Repositories;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Merchants.Handlers;
@@ -21,9 +18,7 @@ public sealed class GetMerchantListQueryHandler(
IMerchantRepository merchantRepository,
IStoreRepository storeRepository,
ITenantRepository tenantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
ITenantProvider tenantProvider)
: IRequestHandler<GetMerchantListQuery, PagedResult<MerchantListItemDto>>
{
/// <inheritdoc />
@@ -31,17 +26,14 @@ public sealed class GetMerchantListQueryHandler(
GetMerchantListQuery request,
CancellationToken cancellationToken)
{
// 1. 校验跨租户访问权限
// 1. 获取当前租户并校验跨租户访问
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询商户");
}
var effectiveTenantId = isSuperAdmin ? request.TenantId : currentTenantId;
var effectiveTenantId = currentTenantId;
// 2. 查询商户列表
var merchants = await merchantRepository.SearchAsync(

View File

@@ -2,8 +2,6 @@ using MediatR;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Merchants.Commands;
using TakeoutSaaS.Application.App.Merchants.Dto;
using TakeoutSaaS.Application.Identity;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Domain.Merchants.Entities;
using TakeoutSaaS.Domain.Merchants.Enums;
using TakeoutSaaS.Domain.Merchants.Repositories;
@@ -25,7 +23,6 @@ public sealed class UpdateMerchantCommandHandler(
ITenantRepository tenantRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService,
ILogger<UpdateMerchantCommandHandler> logger)
: IRequestHandler<UpdateMerchantCommand, UpdateMerchantResultDto?>
{
@@ -39,24 +36,15 @@ public sealed class UpdateMerchantCommandHandler(
// 1. 获取操作者权限
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 读取商户信息
var merchant = isSuperAdmin
? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken)
: await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken);
if (merchant == null)
{
return null;
}
if (!isSuperAdmin && merchant.TenantId != currentTenantId)
{
return null;
}
// 3. 规范化输入
var name = NormalizeRequired(request.Name, "商户名称");
var contactPhone = NormalizeRequired(request.ContactPhone, "联系电话");

View File

@@ -1,45 +1,12 @@
using Microsoft.AspNetCore.Http;
using System;
using System.Linq;
namespace TakeoutSaaS.Application.App.Stores;
internal static class StoreTenantAccess
{
private const string PermissionClaimType = "permission";
private const string ViewAllStoresPermission = "store:read:all";
private static readonly string[] PlatformRoleCodes =
{
"super-admin",
"SUPER_ADMIN",
"PlatformAdmin",
"platform-admin"
};
public static bool ShouldIgnoreTenantFilter(IHttpContextAccessor httpContextAccessor)
{
var httpContext = httpContextAccessor.HttpContext;
if (httpContext == null)
{
return false;
}
var user = httpContext.User;
if (user?.Identity?.IsAuthenticated != true)
{
return false;
}
if (PlatformRoleCodes.Any(user.IsInRole))
{
return true;
}
var permissions = user.FindAll(PermissionClaimType)
.Select(c => c.Value?.Trim())
.Where(value => !string.IsNullOrWhiteSpace(value))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
return permissions.Contains(ViewAllStoresPermission);
// 1. 租户管理端不允许跨租户访问门店数据
return false;
}
}

View File

@@ -1,13 +1,9 @@
using Microsoft.AspNetCore.Http;
using System.Security.Claims;
namespace TakeoutSaaS.Application.App.Subscriptions;
internal static class SubscriptionTenantAccess
{
private const string PermissionClaimType = "permission";
private const string PlatformAdminRole = "PlatformAdmin";
public static bool ShouldIgnoreTenantFilter(IHttpContextAccessor httpContextAccessor)
{
var httpContext = httpContextAccessor.HttpContext;
@@ -16,24 +12,7 @@ internal static class SubscriptionTenantAccess
// Background jobs / out-of-request execution should process across tenants.
return true;
}
var user = httpContext.User;
if (user?.Identity?.IsAuthenticated != true)
{
return false;
}
if (user.IsInRole(PlatformAdminRole))
{
return true;
}
var permissions = user.FindAll(PermissionClaimType)
.Select(c => c.Value?.Trim())
.Where(v => !string.IsNullOrWhiteSpace(v))
.ToHashSet(StringComparer.OrdinalIgnoreCase);
// Platform-level tenant permissions imply cross-tenant visibility.
return permissions.Contains("tenant:read");
// (空行后) 请求上下文下强制不允许跨租户
return false;
}
}

View File

@@ -1,20 +0,0 @@
using Microsoft.AspNetCore.Http;
namespace TakeoutSaaS.Application.Dictionary.Services;
internal static class DictionaryAccessHelper
{
internal static bool IsPlatformAdmin(IHttpContextAccessor httpContextAccessor)
{
var user = httpContextAccessor.HttpContext?.User;
if (user?.Identity?.IsAuthenticated != true)
{
return false;
}
return user.IsInRole("PlatformAdmin") ||
user.IsInRole("platform-admin") ||
user.IsInRole("super-admin") ||
user.IsInRole("SUPER_ADMIN");
}
}

View File

@@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using System.Security.Cryptography;
using TakeoutSaaS.Application.Dictionary.Abstractions;
using TakeoutSaaS.Application.Dictionary.Contracts;
@@ -20,7 +19,6 @@ public sealed class DictionaryAppService(
IDictionaryRepository repository,
IDictionaryCache cache,
ITenantProvider tenantProvider,
IHttpContextAccessor httpContextAccessor,
ILogger<DictionaryAppService> logger) : IDictionaryAppService
{
/// <summary>
@@ -356,17 +354,20 @@ public sealed class DictionaryAppService(
private void EnsureScopePermission(DictionaryScope scope)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
// 1. (空行后) 租户端不允许操作系统字典
if (scope == DictionaryScope.System && tenantId != 0)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许操作系统字典");
}
}
private void EnsurePlatformTenant(long tenantId)
{
if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
// 1. (空行后) 系统字典只能在平台租户TenantId=0上下文中操作
if (tenantId != 0)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许操作系统字典");
}
}

View File

@@ -1,6 +1,5 @@
using System.Security.Cryptography;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
using TakeoutSaaS.Application.Dictionary.Abstractions;
using TakeoutSaaS.Application.Dictionary.Contracts;
using TakeoutSaaS.Application.Dictionary.Models;
@@ -22,7 +21,6 @@ public sealed class DictionaryCommandService(
IDictionaryItemRepository itemRepository,
IDictionaryHybridCache cache,
ITenantProvider tenantProvider,
IHttpContextAccessor httpContextAccessor,
ILogger<DictionaryCommandService> logger)
{
/// <summary>
@@ -231,14 +229,16 @@ public sealed class DictionaryCommandService(
var tenantId = tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System)
{
if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
// 1. (空行后) 租户端禁止写入系统字典
if (tenantId != 0)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可创建系统字典");
throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许创建系统字典");
}
return 0;
}
// 2. (空行后) 业务字典必须在租户上下文中创建
if (tenantId == 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "业务字典必须在租户上下文中创建");
@@ -250,11 +250,14 @@ public sealed class DictionaryCommandService(
private void EnsureGroupAccess(DictionaryGroup group)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
// 1. (空行后) 租户端不允许操作系统字典
if (group.Scope == DictionaryScope.System && tenantId != 0)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许操作系统字典");
}
// 2. (空行后) 业务字典必须属于当前租户
if (group.Scope == DictionaryScope.Business && tenantId != group.TenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户字典");

View File

@@ -14,7 +14,6 @@ using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
using Microsoft.Extensions.Logging;
using Microsoft.AspNetCore.Http;
namespace TakeoutSaaS.Application.Dictionary.Services;
@@ -30,7 +29,6 @@ public sealed class DictionaryImportExportService(
IDictionaryHybridCache cache,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUser,
IHttpContextAccessor httpContextAccessor,
ILogger<DictionaryImportExportService> logger)
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
@@ -426,11 +424,14 @@ public sealed class DictionaryImportExportService(
private void EnsureGroupAccess(DictionaryGroup group)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
// 1. (空行后) 租户端不允许操作系统字典
if (group.Scope == DictionaryScope.System && tenantId != 0)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
throw new BusinessException(ErrorCodes.Forbidden, "租户端不允许操作系统字典");
}
// 2. (空行后) 业务字典必须属于当前租户
if (group.Scope == DictionaryScope.Business && tenantId != group.TenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户字典");

View File

@@ -31,24 +31,18 @@ public sealed class BatchIdentityUserOperationCommandHandler(
/// <inheritdoc />
public async Task<BatchIdentityUserOperationResult> Handle(BatchIdentityUserOperationCommand request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
// 1. 获取当前租户与操作者档案
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 校验跨租户访问权限
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
// 2. 校验跨租户访问
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户批量操作用户");
}
if (isSuperAdmin && !request.TenantId.HasValue)
{
throw new BusinessException(ErrorCodes.BadRequest, "批量操作必须指定租户");
}
// 3. 解析用户 ID 列表
var tenantId = request.TenantId ?? currentTenantId;
var tenantId = currentTenantId;
var userIds = ParseIds(request.UserIds, "用户");
if (userIds.Length == 0)
{
@@ -63,7 +57,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
// 4. 查询目标用户集合
var includeDeleted = request.Operation == IdentityUserBatchOperation.Restore;
var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, isSuperAdmin, cancellationToken);
var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, false, cancellationToken);
var usersById = users.ToDictionary(user => user.Id, user => user, EqualityComparer<long>.Default);
// 5. 预计算租户管理员约束
@@ -85,7 +79,7 @@ public sealed class BatchIdentityUserOperationCommandHandler(
IncludeDeleted = false,
Page = 1,
PageSize = 1
}, isSuperAdmin, cancellationToken)).Total;
}, false, cancellationToken)).Total;
var remainingActiveAdmins = activeAdminCount;
// 6. 执行批量操作

View File

@@ -28,27 +28,24 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
/// <inheritdoc />
public async Task<bool> Handle(ChangeIdentityUserStatusCommand request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
// 1. 获取当前租户与操作者档案
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 校验跨租户访问权限
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
// 2. 校验跨租户访问
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户修改用户状态");
}
// 3. 查询用户实体
var user = isSuperAdmin
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
if (user == null)
{
return false;
}
if (!isSuperAdmin && user.TenantId != currentTenantId)
if (user.TenantId != currentTenantId)
{
return false;
}
@@ -56,7 +53,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
// 4. 校验租户管理员保留规则
if (request.Status == IdentityUserStatus.Disabled && user.Status == IdentityUserStatus.Active)
{
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken);
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, cancellationToken);
}
// 5. 更新状态
@@ -114,7 +111,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
return true;
}
private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, bool ignoreTenantFilter, CancellationToken cancellationToken)
private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
// 1. 获取租户管理员角色
var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken);
@@ -140,7 +137,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler(
Page = 1,
PageSize = 1
};
var result = await identityUserRepository.SearchPagedAsync(filter, ignoreTenantFilter, cancellationToken);
var result = await identityUserRepository.SearchPagedAsync(filter, false, cancellationToken);
if (result.Total <= 1)
{
throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员");

View File

@@ -36,19 +36,18 @@ public sealed class CreateIdentityUserCommandHandler(
/// <inheritdoc />
public async Task<UserDetailDto> Handle(CreateIdentityUserCommand request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
// 1. 获取当前租户与操作者档案
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 校验跨租户访问权限
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
// 2. 校验跨租户访问
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户创建用户");
}
// 3. 规范化输入并准备校验
var tenantId = isSuperAdmin ? request.TenantId ?? currentTenantId : currentTenantId;
var tenantId = currentTenantId;
var account = request.Account.Trim();
var displayName = request.DisplayName.Trim();
var phone = string.IsNullOrWhiteSpace(request.Phone) ? null : request.Phone.Trim();

View File

@@ -28,27 +28,24 @@ public sealed class DeleteIdentityUserCommandHandler(
/// <inheritdoc />
public async Task<bool> Handle(DeleteIdentityUserCommand request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
// 1. 获取当前租户与操作者档案
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 校验跨租户访问权限
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
// 2. 校验跨租户访问
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户删除用户");
}
// 3. 查询用户实体
var user = isSuperAdmin
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
if (user == null)
{
return false;
}
if (!isSuperAdmin && user.TenantId != currentTenantId)
if (user.TenantId != currentTenantId)
{
return false;
}
@@ -56,7 +53,7 @@ public sealed class DeleteIdentityUserCommandHandler(
// 4. 校验租户管理员保留规则
if (user.Status == IdentityUserStatus.Active)
{
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken);
await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, cancellationToken);
}
// 5. 构建操作日志消息
@@ -88,7 +85,7 @@ public sealed class DeleteIdentityUserCommandHandler(
return true;
}
private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, bool ignoreTenantFilter, CancellationToken cancellationToken)
private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
// 1. 获取租户管理员角色
var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken);
@@ -114,7 +111,7 @@ public sealed class DeleteIdentityUserCommandHandler(
Page = 1,
PageSize = 1
};
var result = await identityUserRepository.SearchPagedAsync(filter, ignoreTenantFilter, cancellationToken);
var result = await identityUserRepository.SearchPagedAsync(filter, false, cancellationToken);
if (result.Total <= 1)
{
throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员");

View File

@@ -1,11 +1,9 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
using TakeoutSaaS.Domain.Identity.Entities;
using TakeoutSaaS.Domain.Identity.Enums;
using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
@@ -19,30 +17,24 @@ public sealed class GetIdentityUserDetailQueryHandler(
IRoleRepository roleRepository,
IRolePermissionRepository rolePermissionRepository,
IPermissionRepository permissionRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
ITenantProvider tenantProvider)
: IRequestHandler<GetIdentityUserDetailQuery, UserDetailDto?>
{
/// <inheritdoc />
public async Task<UserDetailDto?> Handle(GetIdentityUserDetailQuery request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
// 1. 获取当前租户
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 查询用户实体
IdentityUser? user;
if (request.IncludeDeleted)
{
user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken);
user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, false, cancellationToken);
}
else
{
user = isSuperAdmin
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
}
if (user == null)
@@ -50,7 +42,7 @@ public sealed class GetIdentityUserDetailQueryHandler(
return null;
}
if (!isSuperAdmin && user.TenantId != currentTenantId)
if (user.TenantId != currentTenantId)
{
return null;
}

View File

@@ -28,27 +28,24 @@ public sealed class ResetIdentityUserPasswordCommandHandler(
/// <inheritdoc />
public async Task<ResetIdentityUserPasswordResult> Handle(ResetIdentityUserPasswordCommand request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
// 1. 获取当前租户与操作者档案
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 校验跨租户访问权限
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
// 2. 校验跨租户访问
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
}
// 3. 查询用户实体
var user = isSuperAdmin
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
if (user == null)
{
throw new BusinessException(ErrorCodes.NotFound, "用户不存在");
}
if (!isSuperAdmin && user.TenantId != currentTenantId)
if (user.TenantId != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户重置密码");
}

View File

@@ -25,25 +25,24 @@ public sealed class RestoreIdentityUserCommandHandler(
/// <inheritdoc />
public async Task<bool> Handle(RestoreIdentityUserCommand request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
// 1. 获取当前租户与操作者档案
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 校验跨租户访问权限
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
// 2. 校验跨租户访问
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户恢复用户");
}
// 3. 查询用户实体(包含已删除)
var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken);
var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, false, cancellationToken);
if (user == null)
{
return false;
}
if (!isSuperAdmin && user.TenantId != currentTenantId)
if (user.TenantId != currentTenantId)
{
return false;
}

View File

@@ -1,5 +1,4 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Abstractions;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Application.Identity.Queries;
using TakeoutSaaS.Domain.Identity.Entities;
@@ -8,7 +7,6 @@ using TakeoutSaaS.Domain.Identity.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Identity.Handlers;
@@ -20,21 +18,17 @@ public sealed class SearchIdentityUsersQueryHandler(
IIdentityUserRepository identityUserRepository,
IUserRoleRepository userRoleRepository,
IRoleRepository roleRepository,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUserAccessor,
IAdminAuthService adminAuthService)
ITenantProvider tenantProvider)
: IRequestHandler<SearchIdentityUsersQuery, PagedResult<UserListItemDto>>
{
/// <inheritdoc />
public async Task<PagedResult<UserListItemDto>> Handle(SearchIdentityUsersQuery request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
// 1. 获取当前租户
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 校验跨租户访问权限
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
// 2. 校验跨租户访问
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户查询用户");
}
@@ -42,7 +36,7 @@ public sealed class SearchIdentityUsersQueryHandler(
// 3. 组装查询过滤条件
var filter = new IdentityUserSearchFilter
{
TenantId = isSuperAdmin ? request.TenantId : currentTenantId,
TenantId = currentTenantId,
Keyword = request.Keyword,
Status = request.Status,
RoleId = request.RoleId,
@@ -58,7 +52,7 @@ public sealed class SearchIdentityUsersQueryHandler(
};
// 4. 执行分页查询
var (items, total) = await identityUserRepository.SearchPagedAsync(filter, isSuperAdmin, cancellationToken);
var (items, total) = await identityUserRepository.SearchPagedAsync(filter, false, cancellationToken);
if (items.Count == 0)
{
return new PagedResult<UserListItemDto>(Array.Empty<UserListItemDto>(), request.Page, request.PageSize, total);

View File

@@ -31,27 +31,24 @@ public sealed class UpdateIdentityUserCommandHandler(
/// <inheritdoc />
public async Task<UserDetailDto?> Handle(UpdateIdentityUserCommand request, CancellationToken cancellationToken)
{
// 1. 获取操作者档案并判断权限
// 1. 获取当前租户与操作者档案
var currentTenantId = tenantProvider.GetCurrentTenantId();
var operatorProfile = await adminAuthService.GetProfileAsync(currentUserAccessor.UserId, cancellationToken);
var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile);
// 2. 校验跨租户访问权限
if (!isSuperAdmin && request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
// 2. 校验跨租户访问
if (request.TenantId.HasValue && request.TenantId.Value != currentTenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "禁止跨租户更新用户");
}
// 3. 获取用户实体
var user = isSuperAdmin
? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken)
: await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken);
if (user == null)
{
return null;
}
if (!isSuperAdmin && user.TenantId != currentTenantId)
if (user.TenantId != currentTenantId)
{
return null;
}

View File

@@ -1,19 +0,0 @@
using System.Collections.Frozen;
using System.Linq;
using TakeoutSaaS.Application.Identity.Contracts;
namespace TakeoutSaaS.Application.Identity;
internal static class IdentityUserAccess
{
private static readonly FrozenSet<string> SuperAdminRoleCodes = new[]
{
"super-admin",
"SUPER_ADMIN",
"PlatformAdmin",
"platform-admin"
}.ToFrozenSet(StringComparer.OrdinalIgnoreCase);
internal static bool IsSuperAdmin(CurrentUserProfile profile)
=> profile.Roles.Any(role => SuperAdminRoleCodes.Contains(role));
}

View File

@@ -29,14 +29,7 @@ public sealed class AdminAuthService(
ITenantContextAccessor tenantContextAccessor,
ITenantRepository tenantRepository) : IAdminAuthService
{
private readonly ITenantProvider _tenantProvider = tenantProvider;
private readonly ITenantContextAccessor _tenantContextAccessor = tenantContextAccessor;
private readonly ITenantRepository _tenantRepository = tenantRepository;
private readonly IUserRoleRepository _userRoleRepository = userRoleRepository;
private readonly IRoleRepository _roleRepository = roleRepository;
private readonly IPermissionRepository _permissionRepository = permissionRepository;
private readonly IRolePermissionRepository _rolePermissionRepository = rolePermissionRepository;
private readonly IMenuRepository _menuRepository = menuRepository;
private const string TenantAdminRoleCode = "tenant-admin";
/// <summary>
/// 管理后台登录:验证账号密码并生成令牌。
@@ -47,8 +40,15 @@ public sealed class AdminAuthService(
/// <exception cref="BusinessException">账号或密码错误时抛出</exception>
public async Task<TokenResponse> LoginAsync(AdminLoginRequest request, CancellationToken cancellationToken = default)
{
// 0. 强制要求租户上下文(严格多租户隔离)
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识,请使用 账号@手机号 登录或在 Header 指定租户");
}
// 1. 根据账号查找用户
var user = await userRepository.FindByAccountAsync(request.Account, cancellationToken)
var user = await userRepository.FindByAccountAsync(currentTenantId, request.Account, cancellationToken)
?? throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
// 2. 校验账号状态
@@ -114,27 +114,27 @@ public sealed class AdminAuthService(
throw new BusinessException(ErrorCodes.BadRequest, "账号格式错误,应为 账号@手机号");
}
var tenantId = await _tenantRepository.FindTenantIdByContactPhoneAsync(phonePart, cancellationToken);
var tenantId = await tenantRepository.FindTenantIdByContactPhoneAsync(phonePart, cancellationToken);
if (!tenantId.HasValue || tenantId.Value == 0)
{
throw new BusinessException(ErrorCodes.Unauthorized, "账号或密码错误");
}
var originalTenant = _tenantContextAccessor.Current;
_tenantContextAccessor.Current = new TenantContext(tenantId.Value, null, "login:simple:contact_phone");
var originalTenant = tenantContextAccessor.Current;
tenantContextAccessor.Current = new TenantContext(tenantId.Value, null, "login:simple:contact_phone");
try
{
return await LoginAsync(new AdminLoginRequest { Account = accountPart, Password = request.Password }, cancellationToken);
}
finally
{
_tenantContextAccessor.Current = originalTenant;
tenantContextAccessor.Current = originalTenant;
}
}
}
// 3. 未携带手机号时要求外部已解析租户Header/Host 等)
if (_tenantProvider.GetCurrentTenantId() == 0)
if (tenantProvider.GetCurrentTenantId() == 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识,请使用 账号@手机号 登录");
}
@@ -163,6 +163,9 @@ public sealed class AdminAuthService(
var user = await userRepository.FindByIdAsync(descriptor.UserId, cancellationToken)
?? throw new BusinessException(ErrorCodes.Unauthorized, "用户不存在");
// 2.1 校验租户上下文与用户租户一致
EnsureTenantMatched(user.TenantId);
// 3. 撤销旧刷新令牌(防止重复使用)
await refreshTokenStore.RevokeAsync(descriptor.Token, cancellationToken);
@@ -197,8 +200,8 @@ public sealed class AdminAuthService(
// 1. 读取档案以获取权限
var profile = await GetProfileAsync(userId, cancellationToken);
// 2. 读取菜单定义
var tenantId = _tenantProvider.GetCurrentTenantId();
var definitions = await _menuRepository.GetByTenantAsync(tenantId, cancellationToken);
var tenantId = tenantProvider.GetCurrentTenantId();
var definitions = await menuRepository.GetByTenantAsync(tenantId, cancellationToken);
// 3. 生成菜单树
var menu = BuildMenuTree(definitions, profile.Permissions);
@@ -210,7 +213,7 @@ public sealed class AdminAuthService(
/// </summary>
public async Task<UserPermissionDto?> GetUserPermissionsAsync(long userId, CancellationToken cancellationToken = default)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var tenantId = tenantProvider.GetCurrentTenantId();
var user = await userRepository.FindByIdAsync(userId, cancellationToken);
if (user == null || user.TenantId != tenantId)
{
@@ -244,7 +247,7 @@ public sealed class AdminAuthService(
bool sortDescending,
CancellationToken cancellationToken = default)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var tenantId = tenantProvider.GetCurrentTenantId();
var users = await userRepository.SearchAsync(tenantId, keyword, cancellationToken);
var sorted = sortBy?.ToLowerInvariant() switch
@@ -285,8 +288,10 @@ public sealed class AdminAuthService(
{
var tenantId = user.TenantId;
var roles = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken);
// 1. 强制仅允许租户管理员登录(平台不允许超级管理员)
EnsureTenantAdmin(tenantId, roles);
// 2. 加载权限并返回档案
var permissions = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken);
return new CurrentUserProfile
{
UserId = user.Id,
@@ -300,6 +305,38 @@ public sealed class AdminAuthService(
};
}
private void EnsureTenantMatched(long userTenantId)
{
// 1. 读取当前租户
var currentTenantId = tenantProvider.GetCurrentTenantId();
if (currentTenantId <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "缺少租户标识,请在 Header 指定租户");
}
// 2. 校验租户一致
if (currentTenantId != userTenantId)
{
throw new BusinessException(ErrorCodes.Unauthorized, "RefreshToken 无效或已过期");
}
}
private static void EnsureTenantAdmin(long tenantId, IReadOnlyCollection<string> roles)
{
// 1. 租户 ID 必须有效
if (tenantId <= 0)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅允许租户管理员登录");
}
// 2. 必须具备租户管理员角色
var isTenantAdmin = roles.Any(role => string.Equals(role, TenantAdminRoleCode, StringComparison.OrdinalIgnoreCase));
if (!isTenantAdmin)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅允许租户管理员登录");
}
}
private async Task ResetLockedUserAsync(long userId, CancellationToken cancellationToken)
{
// 1. 获取可更新实体
@@ -495,34 +532,34 @@ public sealed class AdminAuthService(
private async Task<string[]> ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
if (roleIds.Length == 0)
{
return Array.Empty<string>();
}
var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
var roles = await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
private async Task<string[]> ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken)
{
var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken);
var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray();
if (roleIds.Length == 0)
{
return Array.Empty<string>();
}
var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
if (permissionIds.Length == 0)
{
return Array.Empty<string>();
}
var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
var permissions = await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
}
@@ -532,22 +569,22 @@ public sealed class AdminAuthService(
CancellationToken cancellationToken)
{
var userIds = users.Select(x => x.Id).ToArray();
var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
var userRoleRelations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken);
var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray();
var roles = roleIds.Length == 0
? Array.Empty<Role>()
: await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer<long>.Default);
var rolePermissions = roleIds.Length == 0
? Array.Empty<RolePermission>()
: await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
: await rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken);
var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray();
var permissions = permissionIds.Length == 0
? Array.Empty<Permission>()
: await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
: await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
var rolePermissionsLookup = rolePermissions