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