refactor: AdminApi 剔除租户侧能力
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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>();
|
||||
|
||||
Reference in New Issue
Block a user