Files
TakeoutSaaS.AdminApi/src/Application/TakeoutSaaS.Application/Dictionary/Services/DictionaryAppService.cs
2025-11-23 12:47:29 +08:00

345 lines
12 KiB
C#

using System.Linq;
using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.Dictionary.Abstractions;
using TakeoutSaaS.Application.Dictionary.Contracts;
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;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.Dictionary.Services;
/// <summary>
/// 参数字典应用服务实现。
/// </summary>
public sealed class DictionaryAppService : IDictionaryAppService
{
private readonly IDictionaryRepository _repository;
private readonly IDictionaryCache _cache;
private readonly ITenantProvider _tenantProvider;
private readonly ILogger<DictionaryAppService> _logger;
public DictionaryAppService(
IDictionaryRepository repository,
IDictionaryCache cache,
ITenantProvider tenantProvider,
ILogger<DictionaryAppService> logger)
{
_repository = repository;
_cache = cache;
_tenantProvider = tenantProvider;
_logger = logger;
}
public async Task<DictionaryGroupDto> CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
{
var normalizedCode = NormalizeCode(request.Code);
var targetTenant = ResolveTargetTenant(request.Scope);
var existing = await _repository.FindGroupByCodeAsync(normalizedCode, cancellationToken);
if (existing != null)
{
throw new BusinessException(ErrorCodes.Conflict, $"字典分组编码 {normalizedCode} 已存在");
}
var group = new DictionaryGroup
{
Id = Guid.NewGuid(),
TenantId = targetTenant,
Code = normalizedCode,
Name = request.Name.Trim(),
Scope = request.Scope,
Description = request.Description?.Trim(),
IsEnabled = true
};
await _repository.AddGroupAsync(group, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
_logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope);
return MapGroup(group, includeItems: false);
}
public async Task<DictionaryGroupDto> UpdateGroupAsync(Guid groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default)
{
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureScopePermission(group.Scope);
group.Name = request.Name.Trim();
group.Description = request.Description?.Trim();
group.IsEnabled = request.IsEnabled;
await _repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("更新字典分组:{GroupId}", group.Id);
return MapGroup(group, includeItems: false);
}
public async Task DeleteGroupAsync(Guid groupId, CancellationToken cancellationToken = default)
{
var group = await RequireGroupAsync(groupId, cancellationToken);
EnsureScopePermission(group.Scope);
await _repository.RemoveGroupAsync(group, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("删除字典分组:{GroupId}", group.Id);
}
public async Task<IReadOnlyList<DictionaryGroupDto>> SearchGroupsAsync(DictionaryGroupQuery request, CancellationToken cancellationToken = default)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
var scope = ResolveScopeForQuery(request.Scope, tenantId);
EnsureScopePermission(scope);
var groups = await _repository.SearchGroupsAsync(scope, cancellationToken);
var includeItems = request.IncludeItems;
var result = new List<DictionaryGroupDto>(groups.Count);
foreach (var group in groups)
{
IReadOnlyList<DictionaryItemDto> items = Array.Empty<DictionaryItemDto>();
if (includeItems)
{
var itemEntities = await _repository.GetItemsByGroupIdAsync(group.Id, cancellationToken);
items = itemEntities.Select(MapItem).ToList();
}
result.Add(MapGroup(group, includeItems, items));
}
return result;
}
public async Task<DictionaryItemDto> CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default)
{
var group = await RequireGroupAsync(request.GroupId, cancellationToken);
EnsureScopePermission(group.Scope);
var item = new DictionaryItem
{
Id = Guid.NewGuid(),
TenantId = group.TenantId,
GroupId = group.Id,
Key = request.Key.Trim(),
Value = request.Value.Trim(),
Description = request.Description?.Trim(),
SortOrder = request.SortOrder,
IsDefault = request.IsDefault,
IsEnabled = request.IsEnabled
};
await _repository.AddItemAsync(item, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("新增字典项:{ItemId}", item.Id);
return MapItem(item);
}
public async Task<DictionaryItemDto> UpdateItemAsync(Guid itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default)
{
var item = await RequireItemAsync(itemId, cancellationToken);
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
EnsureScopePermission(group.Scope);
item.Value = request.Value.Trim();
item.Description = request.Description?.Trim();
item.SortOrder = request.SortOrder;
item.IsDefault = request.IsDefault;
item.IsEnabled = request.IsEnabled;
await _repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("更新字典项:{ItemId}", item.Id);
return MapItem(item);
}
public async Task DeleteItemAsync(Guid itemId, CancellationToken cancellationToken = default)
{
var item = await RequireItemAsync(itemId, cancellationToken);
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
EnsureScopePermission(group.Scope);
await _repository.RemoveItemAsync(item, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
await InvalidateCacheAsync(group, cancellationToken);
_logger.LogInformation("删除字典项:{ItemId}", item.Id);
}
public async Task<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>> GetCachedItemsAsync(DictionaryBatchQueryRequest request, CancellationToken cancellationToken = default)
{
var normalizedCodes = request.Codes
.Where(code => !string.IsNullOrWhiteSpace(code))
.Select(NormalizeCode)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray();
if (normalizedCodes.Length == 0)
{
return new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase);
}
var tenantId = _tenantProvider.GetCurrentTenantId();
var result = new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase);
foreach (var code in normalizedCodes)
{
var systemItems = await GetOrLoadCacheAsync(Guid.Empty, code, cancellationToken);
if (tenantId == Guid.Empty)
{
result[code] = systemItems;
continue;
}
var tenantItems = await GetOrLoadCacheAsync(tenantId, code, cancellationToken);
result[code] = MergeItems(systemItems, tenantItems);
}
return result;
}
private async Task<DictionaryGroup> RequireGroupAsync(Guid groupId, CancellationToken cancellationToken)
{
var group = await _repository.FindGroupByIdAsync(groupId, cancellationToken);
if (group == null)
{
throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在");
}
return group;
}
private async Task<DictionaryItem> RequireItemAsync(Guid itemId, CancellationToken cancellationToken)
{
var item = await _repository.FindItemByIdAsync(itemId, cancellationToken);
if (item == null)
{
throw new BusinessException(ErrorCodes.NotFound, "字典项不存在");
}
return item;
}
private Guid ResolveTargetTenant(DictionaryScope scope)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System)
{
EnsurePlatformTenant(tenantId);
return Guid.Empty;
}
if (tenantId == Guid.Empty)
{
throw new BusinessException(ErrorCodes.BadRequest, "业务参数需指定租户");
}
return tenantId;
}
private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant();
private static DictionaryScope ResolveScopeForQuery(DictionaryScope? requestedScope, Guid tenantId)
{
if (requestedScope.HasValue)
{
return requestedScope.Value;
}
return tenantId == Guid.Empty ? DictionaryScope.System : DictionaryScope.Business;
}
private void EnsureScopePermission(DictionaryScope scope)
{
var tenantId = _tenantProvider.GetCurrentTenantId();
if (scope == DictionaryScope.System && tenantId != Guid.Empty)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
}
}
private void EnsurePlatformTenant(Guid tenantId)
{
if (tenantId != Guid.Empty)
{
throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典");
}
}
private async Task InvalidateCacheAsync(DictionaryGroup group, CancellationToken cancellationToken)
{
await _cache.RemoveAsync(group.TenantId, group.Code, cancellationToken);
if (group.Scope == DictionaryScope.Business)
{
return;
}
// 系统参数更新需要逐租户重新合并,由调用方在下一次请求时重新加载
}
private async Task<IReadOnlyList<DictionaryItemDto>> GetOrLoadCacheAsync(Guid tenantId, string code, CancellationToken cancellationToken)
{
var cached = await _cache.GetAsync(tenantId, code, cancellationToken);
if (cached != null)
{
return cached;
}
var entities = await _repository.GetItemsByCodesAsync(new[] { code }, tenantId, includeSystem: false, cancellationToken);
var items = entities
.Where(item => item.IsEnabled && (item.Group?.IsEnabled ?? true))
.Select(MapItem)
.OrderBy(item => item.SortOrder)
.ToList();
await _cache.SetAsync(tenantId, code, items, cancellationToken);
return items;
}
private static IReadOnlyList<DictionaryItemDto> MergeItems(IReadOnlyList<DictionaryItemDto> systemItems, IReadOnlyList<DictionaryItemDto> tenantItems)
{
if (tenantItems.Count == 0)
{
return systemItems;
}
if (systemItems.Count == 0)
{
return tenantItems;
}
return systemItems.Concat(tenantItems)
.OrderBy(item => item.SortOrder)
.ToList();
}
private static DictionaryGroupDto MapGroup(DictionaryGroup group, bool includeItems, IReadOnlyList<DictionaryItemDto>? items = null)
{
return new DictionaryGroupDto
{
Id = group.Id,
Code = group.Code,
Name = group.Name,
Scope = group.Scope,
Description = group.Description,
IsEnabled = group.IsEnabled,
Items = includeItems ? items ?? group.Items.Select(MapItem).ToList() : Array.Empty<DictionaryItemDto>()
};
}
private static DictionaryItemDto MapItem(DictionaryItem item)
=> new()
{
Id = item.Id,
GroupId = item.GroupId,
Key = item.Key,
Value = item.Value,
IsDefault = item.IsDefault,
IsEnabled = item.IsEnabled,
SortOrder = item.SortOrder,
Description = item.Description
};
}