feat: 提交后端其余改动
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
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,4 +1,5 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using System.Security.Cryptography;
|
||||
using TakeoutSaaS.Application.Dictionary.Abstractions;
|
||||
using TakeoutSaaS.Application.Dictionary.Contracts;
|
||||
@@ -19,6 +20,7 @@ public sealed class DictionaryAppService(
|
||||
IDictionaryRepository repository,
|
||||
IDictionaryCache cache,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<DictionaryAppService> logger) : IDictionaryAppService
|
||||
{
|
||||
/// <summary>
|
||||
@@ -354,7 +356,7 @@ public sealed class DictionaryAppService(
|
||||
private void EnsureScopePermission(DictionaryScope scope)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (scope == DictionaryScope.System && tenantId != 0)
|
||||
if (scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
|
||||
}
|
||||
@@ -362,7 +364,7 @@ public sealed class DictionaryAppService(
|
||||
|
||||
private void EnsurePlatformTenant(long tenantId)
|
||||
{
|
||||
if (tenantId != 0)
|
||||
if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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;
|
||||
@@ -21,6 +22,7 @@ public sealed class DictionaryCommandService(
|
||||
IDictionaryItemRepository itemRepository,
|
||||
IDictionaryHybridCache cache,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<DictionaryCommandService> logger)
|
||||
{
|
||||
/// <summary>
|
||||
@@ -229,7 +231,7 @@ public sealed class DictionaryCommandService(
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (scope == DictionaryScope.System)
|
||||
{
|
||||
if (tenantId != 0)
|
||||
if (tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可创建系统字典");
|
||||
}
|
||||
@@ -248,7 +250,7 @@ public sealed class DictionaryCommandService(
|
||||
private void EnsureGroupAccess(DictionaryGroup group)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (group.Scope == DictionaryScope.System && tenantId != 0)
|
||||
if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ 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;
|
||||
|
||||
@@ -29,6 +30,7 @@ public sealed class DictionaryImportExportService(
|
||||
IDictionaryHybridCache cache,
|
||||
ITenantProvider tenantProvider,
|
||||
ICurrentUserAccessor currentUser,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<DictionaryImportExportService> logger)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
@@ -424,7 +426,7 @@ public sealed class DictionaryImportExportService(
|
||||
private void EnsureGroupAccess(DictionaryGroup group)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (group.Scope == DictionaryScope.System && tenantId != 0)
|
||||
if (group.Scope == DictionaryScope.System && tenantId != 0 && !DictionaryAccessHelper.IsPlatformAdmin(httpContextAccessor))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||
|
||||
namespace TakeoutSaaS.Application.Dictionary.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 字典标签覆盖服务。
|
||||
/// </summary>
|
||||
public sealed class DictionaryLabelOverrideService(
|
||||
IDictionaryLabelOverrideRepository overrideRepository,
|
||||
IDictionaryItemRepository itemRepository)
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取租户的所有标签覆盖。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="overrideType">可选的覆盖类型过滤。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
public async Task<IReadOnlyList<LabelOverrideDto>> GetOverridesAsync(
|
||||
long tenantId,
|
||||
OverrideType? overrideType = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var overrides = await overrideRepository.ListByTenantAsync(tenantId, overrideType, cancellationToken);
|
||||
return overrides.Select(MapToDto).ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定字典项的覆盖配置。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="dictionaryItemId">字典项 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
public async Task<LabelOverrideDto?> GetOverrideByItemIdAsync(
|
||||
long tenantId,
|
||||
long dictionaryItemId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entity = await overrideRepository.GetByItemIdAsync(tenantId, dictionaryItemId, cancellationToken);
|
||||
return entity == null ? null : MapToDto(entity);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建或更新租户对系统字典的标签覆盖(租户定制)。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="request">覆盖请求。</param>
|
||||
/// <param name="operatorId">操作人 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
public async Task<LabelOverrideDto> UpsertTenantOverrideAsync(
|
||||
long tenantId,
|
||||
UpsertLabelOverrideRequest request,
|
||||
long operatorId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 验证字典项存在且为系统字典
|
||||
var item = await itemRepository.GetByIdAsync(request.DictionaryItemId, cancellationToken);
|
||||
if (item == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "字典项不存在");
|
||||
}
|
||||
|
||||
if (item.TenantId != 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "租户只能覆盖系统字典项");
|
||||
}
|
||||
|
||||
// 2. 查找现有覆盖或创建新记录
|
||||
var existing = await overrideRepository.GetByItemIdAsync(tenantId, request.DictionaryItemId, cancellationToken);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
if (existing.OverrideType == OverrideType.PlatformEnforcement)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "平台强制覆盖不可由租户修改");
|
||||
}
|
||||
|
||||
existing.OverrideValue = DictionaryValueConverter.Serialize(request.OverrideValue);
|
||||
existing.Reason = request.Reason;
|
||||
existing.UpdatedAt = now;
|
||||
existing.UpdatedBy = operatorId;
|
||||
await overrideRepository.UpdateAsync(existing, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing = new DictionaryLabelOverride
|
||||
{
|
||||
TenantId = tenantId,
|
||||
DictionaryItemId = request.DictionaryItemId,
|
||||
OriginalValue = item.Value,
|
||||
OverrideValue = DictionaryValueConverter.Serialize(request.OverrideValue),
|
||||
OverrideType = OverrideType.TenantCustomization,
|
||||
Reason = request.Reason,
|
||||
CreatedAt = now,
|
||||
CreatedBy = operatorId
|
||||
};
|
||||
await overrideRepository.AddAsync(existing, cancellationToken);
|
||||
}
|
||||
|
||||
await overrideRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// 重新加载以获取完整信息
|
||||
existing.DictionaryItem = item;
|
||||
return MapToDto(existing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建或更新平台对租户字典的强制覆盖(平台强制)。
|
||||
/// </summary>
|
||||
/// <param name="targetTenantId">目标租户 ID。</param>
|
||||
/// <param name="request">覆盖请求。</param>
|
||||
/// <param name="operatorId">操作人 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
public async Task<LabelOverrideDto> UpsertPlatformOverrideAsync(
|
||||
long targetTenantId,
|
||||
UpsertLabelOverrideRequest request,
|
||||
long operatorId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 验证字典项存在
|
||||
var item = await itemRepository.GetByIdAsync(request.DictionaryItemId, cancellationToken);
|
||||
if (item == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "字典项不存在");
|
||||
}
|
||||
|
||||
// 2. 查找现有覆盖或创建新记录
|
||||
var existing = await overrideRepository.GetByItemIdAsync(targetTenantId, request.DictionaryItemId, cancellationToken);
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
if (existing != null)
|
||||
{
|
||||
existing.OverrideValue = DictionaryValueConverter.Serialize(request.OverrideValue);
|
||||
existing.OverrideType = OverrideType.PlatformEnforcement;
|
||||
existing.Reason = request.Reason;
|
||||
existing.UpdatedAt = now;
|
||||
existing.UpdatedBy = operatorId;
|
||||
await overrideRepository.UpdateAsync(existing, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
existing = new DictionaryLabelOverride
|
||||
{
|
||||
TenantId = targetTenantId,
|
||||
DictionaryItemId = request.DictionaryItemId,
|
||||
OriginalValue = item.Value,
|
||||
OverrideValue = DictionaryValueConverter.Serialize(request.OverrideValue),
|
||||
OverrideType = OverrideType.PlatformEnforcement,
|
||||
Reason = request.Reason,
|
||||
CreatedAt = now,
|
||||
CreatedBy = operatorId
|
||||
};
|
||||
await overrideRepository.AddAsync(existing, cancellationToken);
|
||||
}
|
||||
|
||||
await overrideRepository.SaveChangesAsync(cancellationToken);
|
||||
|
||||
existing.DictionaryItem = item;
|
||||
return MapToDto(existing);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除覆盖配置。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="dictionaryItemId">字典项 ID。</param>
|
||||
/// <param name="operatorId">操作人 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
public async Task<bool> DeleteOverrideAsync(
|
||||
long tenantId,
|
||||
long dictionaryItemId,
|
||||
long operatorId,
|
||||
bool allowPlatformEnforcement = true,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var existing = await overrideRepository.GetByItemIdAsync(tenantId, dictionaryItemId, cancellationToken);
|
||||
if (existing == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!allowPlatformEnforcement && existing.OverrideType == OverrideType.PlatformEnforcement)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "平台强制覆盖不可由租户删除");
|
||||
}
|
||||
|
||||
existing.DeletedBy = operatorId;
|
||||
await overrideRepository.DeleteAsync(existing, cancellationToken);
|
||||
await overrideRepository.SaveChangesAsync(cancellationToken);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取字典项的覆盖值映射。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="dictionaryItemIds">字典项 ID 列表。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>字典项 ID 到覆盖值的映射。</returns>
|
||||
public async Task<Dictionary<long, Dictionary<string, string>>> GetOverrideValuesMapAsync(
|
||||
long tenantId,
|
||||
IEnumerable<long> dictionaryItemIds,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var overrides = await overrideRepository.GetByItemIdsAsync(tenantId, dictionaryItemIds, cancellationToken);
|
||||
return overrides.ToDictionary(
|
||||
x => x.DictionaryItemId,
|
||||
x => DictionaryValueConverter.Deserialize(x.OverrideValue));
|
||||
}
|
||||
|
||||
private static LabelOverrideDto MapToDto(DictionaryLabelOverride entity)
|
||||
{
|
||||
return new LabelOverrideDto
|
||||
{
|
||||
Id = entity.Id,
|
||||
TenantId = entity.TenantId,
|
||||
DictionaryItemId = entity.DictionaryItemId,
|
||||
DictionaryItemKey = entity.DictionaryItem?.Key ?? string.Empty,
|
||||
OriginalValue = DictionaryValueConverter.Deserialize(entity.OriginalValue),
|
||||
OverrideValue = DictionaryValueConverter.Deserialize(entity.OverrideValue),
|
||||
OverrideType = entity.OverrideType,
|
||||
Reason = entity.Reason,
|
||||
CreatedAt = entity.CreatedAt,
|
||||
UpdatedAt = entity.UpdatedAt,
|
||||
CreatedBy = entity.CreatedBy,
|
||||
UpdatedBy = entity.UpdatedBy
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user