refactor: AdminApi 剔除租户侧能力

This commit is contained in:
2026-01-29 23:24:44 +00:00
parent 71e5a9dc29
commit 4f8424adb6
139 changed files with 622 additions and 4691 deletions

View File

@@ -9,7 +9,6 @@ using TakeoutSaaS.Domain.Dictionary.Enums;
using TakeoutSaaS.Domain.Dictionary.Repositories;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Dictionary.Services;
@@ -19,8 +18,6 @@ namespace TakeoutSaaS.Application.Dictionary.Services;
public sealed class DictionaryAppService(
IDictionaryRepository repository,
IDictionaryCache cache,
ITenantProvider tenantProvider,
IHttpContextAccessor httpContextAccessor,
ILogger<DictionaryAppService> logger) : IDictionaryAppService
{
/// <summary>
@@ -33,7 +30,7 @@ public sealed class DictionaryAppService(
{
// 1. 规范化编码并确定租户
var normalizedCode = NormalizeCode(request.Code);
var targetTenant = ResolveTargetTenant(request.Scope);
var targetTenant = ResolveTargetTenant(request.Scope, request.TenantId);
// 2. 校验编码唯一
var existing = await repository.FindGroupByCodeAsync(normalizedCode, cancellationToken);
@@ -74,7 +71,6 @@ public sealed class DictionaryAppService(
{
// 1. 读取分组并校验权限
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureScopePermission(group.Scope);
if (request.RowVersion == null || request.RowVersion.Length == 0)
{
@@ -116,7 +112,6 @@ public sealed class DictionaryAppService(
{
// 1. 读取分组并校验权限
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureScopePermission(group.Scope);
// 2. 删除并失效缓存
await repository.RemoveGroupAsync(group, cancellationToken);
@@ -134,9 +129,8 @@ public sealed class DictionaryAppService(
public async Task<IReadOnlyList<DictionaryGroupDto>> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default)
{
// 1. 确定查询范围并校验权限
var tenantId = tenantProvider.GetCurrentTenantId();
var tenantId = request.TenantId ?? 0;
var scope = ResolveScopeForQuery(request.Scope, tenantId);
EnsureScopePermission(scope);
// 2. 查询分组及可选项
var groups = await repository.SearchGroupsAsync(scope, cancellationToken);
@@ -169,7 +163,6 @@ public sealed class DictionaryAppService(
{
// 1. 校验分组与权限
var group = await RequireGroupAsync(request.GroupId, cancellationToken);
EnsureScopePermission(group.Scope);
// 2. 构建字典项
var item = new DictionaryItem
@@ -206,7 +199,6 @@ public sealed class DictionaryAppService(
// 1. 读取字典项与分组并校验权限
var item = await RequireItemAsync(itemId, cancellationToken);
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
EnsureScopePermission(group.Scope);
if (request.RowVersion == null || request.RowVersion.Length == 0)
{
@@ -251,7 +243,6 @@ public sealed class DictionaryAppService(
// 1. 读取字典项与分组并校验权限
var item = await RequireItemAsync(itemId, cancellationToken);
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
EnsureScopePermission(group.Scope);
// 2. 删除并失效缓存
await repository.RemoveItemAsync(item, cancellationToken);
@@ -281,7 +272,7 @@ public sealed class DictionaryAppService(
}
// 2. 按租户合并系统与业务字典
var tenantId = tenantProvider.GetCurrentTenantId();
var tenantId = request.TenantId ?? 0;
var result = new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase);
foreach (var code in normalizedCodes)
@@ -324,21 +315,19 @@ public sealed class DictionaryAppService(
return item;
}
private long ResolveTargetTenant(DictionaryScope scope)
private static long ResolveTargetTenant(DictionaryScope scope, long? tenantId)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System)
{
EnsurePlatformTenant(tenantId);
return 0;
}
if (tenantId == 0)
if (!tenantId.HasValue || tenantId.Value <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "业务参数需指定租户");
throw new BusinessException(ErrorCodes.ValidationFailed, "业务参数需指定租户");
}
return tenantId;
return tenantId.Value;
}
private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant();
@@ -353,23 +342,6 @@ public sealed class DictionaryAppService(
return tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business;
}
private void EnsureScopePermission(DictionaryScope scope)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
}
}
private void EnsurePlatformTenant(long tenantId)
{
if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
}
}
private async Task InvalidateCacheAsync(DictionaryGroup group, CancellationToken cancellationToken)
{
await cache.RemoveAsync(group.TenantId, group.Code, cancellationToken);

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;
@@ -10,7 +9,6 @@ using TakeoutSaaS.Domain.Dictionary.Repositories;
using TakeoutSaaS.Domain.Dictionary.ValueObjects;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Dictionary.Services;
@@ -21,8 +19,6 @@ public sealed class DictionaryCommandService(
IDictionaryGroupRepository groupRepository,
IDictionaryItemRepository itemRepository,
IDictionaryHybridCache cache,
ITenantProvider tenantProvider,
IHttpContextAccessor httpContextAccessor,
ILogger<DictionaryCommandService> logger)
{
/// <summary>
@@ -30,7 +26,7 @@ public sealed class DictionaryCommandService(
/// </summary>
public async Task<DictionaryGroupDto> CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
{
var targetTenantId = ResolveTargetTenant(request.Scope);
var targetTenantId = ResolveTargetTenant(request.Scope, request.TenantId);
var code = new DictionaryCode(request.Code);
var existing = await groupRepository.GetByCodeAsync(targetTenantId, code, cancellationToken);
@@ -68,7 +64,6 @@ public sealed class DictionaryCommandService(
public async Task<DictionaryGroupDto> UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
{
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureGroupAccess(group);
EnsureRowVersion(request.RowVersion, group.RowVersion, "字典分组");
@@ -103,8 +98,6 @@ public sealed class DictionaryCommandService(
return false;
}
EnsureGroupAccess(group);
var items = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, cancellationToken);
foreach (var item in items)
{
@@ -125,7 +118,6 @@ public sealed class DictionaryCommandService(
public async Task<DictionaryItemDto> CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default)
{
var group = await RequireGroupAsync(request.GroupId, cancellationToken);
EnsureGroupAccess(group);
var items = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, cancellationToken);
var normalizedKey = request.Key.Trim();
@@ -168,7 +160,6 @@ public sealed class DictionaryCommandService(
{
var item = await RequireItemAsync(itemId, cancellationToken);
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
EnsureGroupAccess(group);
EnsureRowVersion(request.RowVersion, item.RowVersion, "字典项");
@@ -216,7 +207,6 @@ public sealed class DictionaryCommandService(
}
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
EnsureGroupAccess(group);
await itemRepository.RemoveAsync(item, cancellationToken);
await groupRepository.SaveChangesAsync(cancellationToken);
@@ -226,39 +216,19 @@ public sealed class DictionaryCommandService(
return true;
}
private long ResolveTargetTenant(DictionaryScope scope)
private static long ResolveTargetTenant(DictionaryScope scope, long? tenantId)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System)
{
if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可创建系统字典");
}
return 0;
}
if (tenantId == 0)
if (!tenantId.HasValue || tenantId.Value <= 0)
{
throw new BusinessException(ErrorCodes.BadRequest, "业务字典必须在租户上下文中创建");
throw new BusinessException(ErrorCodes.ValidationFailed, "业务字典必须指定 TenantId");
}
return tenantId;
}
private void EnsureGroupAccess(DictionaryGroup group)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
}
if (group.Scope == DictionaryScope.Business && tenantId != group.TenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户字典");
}
return tenantId.Value;
}
private static void EnsureRowVersion(byte[]? requestVersion, byte[] entityVersion, string resourceName)

View File

@@ -12,9 +12,7 @@ using TakeoutSaaS.Domain.Dictionary.ValueObjects;
using TakeoutSaaS.Shared.Abstractions.Constants;
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;
@@ -28,9 +26,7 @@ public sealed class DictionaryImportExportService(
IDictionaryItemRepository itemRepository,
IDictionaryImportLogRepository importLogRepository,
IDictionaryHybridCache cache,
ITenantProvider tenantProvider,
ICurrentUserAccessor currentUser,
IHttpContextAccessor httpContextAccessor,
ILogger<DictionaryImportExportService> logger)
{
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
@@ -41,7 +37,6 @@ public sealed class DictionaryImportExportService(
public async Task ExportToCsvAsync(long groupId, Stream output, CancellationToken cancellationToken = default)
{
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureGroupReadable(group);
var items = await ResolveExportItemsAsync(group, cancellationToken);
await WriteCsvAsync(group, items, output, cancellationToken);
@@ -53,7 +48,6 @@ public sealed class DictionaryImportExportService(
public async Task ExportToJsonAsync(long groupId, Stream output, CancellationToken cancellationToken = default)
{
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureGroupReadable(group);
var items = await ResolveExportItemsAsync(group, cancellationToken);
var payload = items.Select(item => new DictionaryExportRow
@@ -96,7 +90,6 @@ public sealed class DictionaryImportExportService(
{
var stopwatch = Stopwatch.StartNew();
var group = await RequireGroupAsync(request.GroupId, cancellationToken);
EnsureGroupWritable(group);
var errors = new List<DictionaryImportResultDto.ImportError>();
var validRows = new List<NormalizedRow>(rows.Count);
@@ -210,14 +203,6 @@ public sealed class DictionaryImportExportService(
private async Task<IReadOnlyList<DictionaryItemDto>> ResolveExportItemsAsync(DictionaryGroup group, CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (group.Scope == DictionaryScope.System && tenantId != 0)
{
var mergedItems = await itemRepository.GetMergedItemsAsync(tenantId, group.Id, includeOverrides: true, cancellationToken);
return mergedItems.Select(DictionaryMapper.ToItemDto).ToList();
}
var items = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, cancellationToken);
return items.Select(DictionaryMapper.ToItemDto).ToList();
}
@@ -423,34 +408,6 @@ public sealed class DictionaryImportExportService(
return group;
}
private void EnsureGroupAccess(DictionaryGroup group)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
}
if (group.Scope == DictionaryScope.Business && tenantId != group.TenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户字典");
}
}
private void EnsureGroupReadable(DictionaryGroup group)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (tenantId != 0 && group.Scope == DictionaryScope.Business && group.TenantId != tenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "无权访问其他租户字典");
}
}
private void EnsureGroupWritable(DictionaryGroup group)
{
EnsureGroupAccess(group);
}
private static DictionaryImportResultDto.ImportError CreateError(int rowNumber, string field, string message)
=> new()
{

View File

@@ -8,7 +8,6 @@ using TakeoutSaaS.Domain.Dictionary.ValueObjects;
using TakeoutSaaS.Shared.Abstractions.Constants;
using TakeoutSaaS.Shared.Abstractions.Exceptions;
using TakeoutSaaS.Shared.Abstractions.Results;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Dictionary.Services;
@@ -18,9 +17,7 @@ namespace TakeoutSaaS.Application.Dictionary.Services;
public sealed class DictionaryQueryService(
IDictionaryGroupRepository groupRepository,
IDictionaryItemRepository itemRepository,
DictionaryMergeService mergeService,
IDictionaryHybridCache cache,
ITenantProvider tenantProvider)
IDictionaryHybridCache cache)
{
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(30);
@@ -31,8 +28,21 @@ public sealed class DictionaryQueryService(
DictionaryGroupQuery query,
CancellationToken cancellationToken = default)
{
var tenantId = tenantProvider.GetCurrentTenantId();
// 1. 解析查询租户与作用域
var tenantId = query.TenantId ?? 0;
if (query.Scope == DictionaryScope.Business && tenantId <= 0)
{
throw new BusinessException(ErrorCodes.ValidationFailed, "Scope=Business 时必须指定 TenantId");
}
// 2. (空行后) 确定作用域与目标租户
var scope = query.Scope ?? (tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business);
if (scope == DictionaryScope.System)
{
tenantId = 0;
}
// 3. (空行后) 构建缓存键并加载分页数据
var sortDescending = string.Equals(query.SortOrder, "desc", StringComparison.OrdinalIgnoreCase);
var targetTenant = scope == DictionaryScope.System ? 0 : tenantId;
@@ -118,7 +128,6 @@ public sealed class DictionaryQueryService(
return null;
}
EnsureGroupReadable(group);
return DictionaryMapper.ToGroupDto(group);
}
@@ -139,7 +148,6 @@ public sealed class DictionaryQueryService(
throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在");
}
EnsureGroupReadable(group);
var items = await itemRepository.GetByGroupIdAsync(group.TenantId, groupId, token);
return items
.Where(item => item.IsEnabled)
@@ -162,7 +170,8 @@ public sealed class DictionaryQueryService(
throw new BusinessException(ErrorCodes.ValidationFailed, "字典编码格式不正确");
}
var tenantId = tenantProvider.GetCurrentTenantId();
// 1. 管理端默认读取系统字典TenantId=0
var tenantId = 0;
var normalized = new DictionaryCode(code);
var cacheKey = DictionaryCacheKeys.BuildDictionaryKey(tenantId, normalized);
@@ -177,17 +186,12 @@ public sealed class DictionaryQueryService(
return Array.Empty<DictionaryItemDto>();
}
if (tenantId == 0)
{
var systemItems = await itemRepository.GetByGroupIdAsync(0, systemGroup.Id, token);
return systemItems
.Where(item => item.IsEnabled)
.OrderBy(item => item.SortOrder)
.Select(DictionaryMapper.ToItemDto)
.ToList();
}
return await mergeService.MergeItemsAsync(tenantId, systemGroup.Id, token);
var systemItems = await itemRepository.GetByGroupIdAsync(0, systemGroup.Id, token);
return systemItems
.Where(item => item.IsEnabled)
.OrderBy(item => item.SortOrder)
.Select(DictionaryMapper.ToItemDto)
.ToList();
},
cancellationToken);
@@ -227,15 +231,6 @@ public sealed class DictionaryQueryService(
return result;
}
private void EnsureGroupReadable(DictionaryGroup group)
{
var tenantId = tenantProvider.GetCurrentTenantId();
if (tenantId != 0 && group.Scope == DictionaryScope.Business && group.TenantId != tenantId)
{
throw new BusinessException(ErrorCodes.Forbidden, "无权访问其他租户字典");
}
}
private sealed class DictionaryGroupPage
{
public IReadOnlyList<DictionaryGroupDto> Items { get; init; } = Array.Empty<DictionaryGroupDto>();