feat: 新增租户管理端 TenantApi 并移除旧 API
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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, "联系电话");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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, "租户端不允许操作系统字典");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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, "无权操作其他租户字典");
|
||||
|
||||
@@ -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, "无权操作其他租户字典");
|
||||
|
||||
@@ -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. 执行批量操作
|
||||
|
||||
@@ -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, "至少保留一个管理员");
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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, "至少保留一个管理员");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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, "禁止跨租户重置密码");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user