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; /// /// 参数字典应用服务实现。 /// public sealed class DictionaryAppService : IDictionaryAppService { private readonly IDictionaryRepository _repository; private readonly IDictionaryCache _cache; private readonly ITenantProvider _tenantProvider; private readonly ILogger _logger; public DictionaryAppService( IDictionaryRepository repository, IDictionaryCache cache, ITenantProvider tenantProvider, ILogger logger) { _repository = repository; _cache = cache; _tenantProvider = tenantProvider; _logger = logger; } public async Task 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 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> 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(groups.Count); foreach (var group in groups) { IReadOnlyList items = Array.Empty(); 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 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 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>> 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>(StringComparer.OrdinalIgnoreCase); } var tenantId = _tenantProvider.GetCurrentTenantId(); var result = new Dictionary>(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 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 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> 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 MergeItems(IReadOnlyList systemItems, IReadOnlyList 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? 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() }; } 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 }; }