using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Http; using System.Security.Cryptography; 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; namespace TakeoutSaaS.Application.Dictionary.Services; /// /// 参数字典应用服务实现。 /// public sealed class DictionaryAppService( IDictionaryRepository repository, IDictionaryCache cache, ILogger logger) : IDictionaryAppService { /// /// 创建字典分组。 /// /// 创建请求。 /// 取消标记。 /// 分组 DTO。 public async Task CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default) { // 1. 规范化编码并确定租户 var normalizedCode = NormalizeCode(request.Code); var targetTenant = ResolveTargetTenant(request.Scope, request.TenantId); // 2. 校验编码唯一 var existing = await repository.FindGroupByCodeAsync(normalizedCode, cancellationToken); if (existing != null) { throw new BusinessException(ErrorCodes.Conflict, $"字典分组编码 {normalizedCode} 已存在"); } // 3. 构建分组实体 var group = new DictionaryGroup { Id = 0, TenantId = targetTenant, Code = normalizedCode, Name = request.Name.Trim(), Scope = request.Scope, AllowOverride = request.AllowOverride, Description = request.Description?.Trim(), IsEnabled = true, RowVersion = RandomNumberGenerator.GetBytes(16) }; // 4. 持久化并返回 await repository.AddGroupAsync(group, cancellationToken); await repository.SaveChangesAsync(cancellationToken); logger.LogInformation("创建字典分组:{Code}({Scope})", group.Code, group.Scope); return MapGroup(group, includeItems: false); } /// /// 更新字典分组。 /// /// 分组 ID。 /// 更新请求。 /// 取消标记。 /// 分组 DTO。 public async Task UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default) { // 1. 读取分组并校验权限 var group = await RequireGroupAsync(groupId, cancellationToken); if (request.RowVersion == null || request.RowVersion.Length == 0) { throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空"); } if (!request.RowVersion.SequenceEqual(group.RowVersion)) { throw new BusinessException(ErrorCodes.Conflict, "字典分组已被修改,请刷新后重试"); } // 2. 更新字段 group.Name = request.Name.Trim(); group.Description = request.Description?.Trim(); group.IsEnabled = request.IsEnabled; group.AllowOverride = request.AllowOverride; group.RowVersion = RandomNumberGenerator.GetBytes(16); // 3. 持久化并失效缓存 try { await repository.SaveChangesAsync(cancellationToken); } catch (Exception exception) when (IsConcurrencyException(exception)) { throw new BusinessException(ErrorCodes.Conflict, "字典分组已被修改,请刷新后重试"); } await InvalidateCacheAsync(group, cancellationToken); logger.LogInformation("更新字典分组:{GroupId}", group.Id); return MapGroup(group, includeItems: false); } /// /// 删除字典分组。 /// /// 分组 ID。 /// 取消标记。 public async Task DeleteGroupAsync(long groupId, CancellationToken cancellationToken = default) { // 1. 读取分组并校验权限 var group = await RequireGroupAsync(groupId, cancellationToken); // 2. 删除并失效缓存 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) { // 1. 确定查询范围并校验权限 var tenantId = request.TenantId ?? 0; var scope = ResolveScopeForQuery(request.Scope, tenantId); // 2. 查询分组及可选项 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; } /// /// 创建字典项。 /// /// 创建请求。 /// 取消标记。 /// 字典项 DTO。 public async Task CreateItemAsync(CreateDictionaryItemRequest request, CancellationToken cancellationToken = default) { // 1. 校验分组与权限 var group = await RequireGroupAsync(request.GroupId, cancellationToken); // 2. 构建字典项 var item = new DictionaryItem { Id = 0, TenantId = group.TenantId, GroupId = group.Id, Key = request.Key.Trim(), Value = DictionaryValueConverter.Serialize(request.Value), Description = request.Description?.Trim(), SortOrder = request.SortOrder, IsDefault = request.IsDefault, IsEnabled = request.IsEnabled, RowVersion = RandomNumberGenerator.GetBytes(16) }; // 3. 持久化并失效缓存 await repository.AddItemAsync(item, cancellationToken); await repository.SaveChangesAsync(cancellationToken); await InvalidateCacheAsync(group, cancellationToken); logger.LogInformation("新增字典项:{ItemId}", item.Id); return MapItem(item); } /// /// 更新字典项。 /// /// 字典项 ID。 /// 更新请求。 /// 取消标记。 /// 字典项 DTO。 public async Task UpdateItemAsync(long itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default) { // 1. 读取字典项与分组并校验权限 var item = await RequireItemAsync(itemId, cancellationToken); var group = await RequireGroupAsync(item.GroupId, cancellationToken); if (request.RowVersion == null || request.RowVersion.Length == 0) { throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空"); } if (!request.RowVersion.SequenceEqual(item.RowVersion)) { throw new BusinessException(ErrorCodes.Conflict, "字典项已被修改,请刷新后重试"); } // 2. 更新字段 item.Key = request.Key.Trim(); item.Value = DictionaryValueConverter.Serialize(request.Value); item.Description = request.Description?.Trim(); item.SortOrder = request.SortOrder; item.IsDefault = request.IsDefault; item.IsEnabled = request.IsEnabled; item.RowVersion = RandomNumberGenerator.GetBytes(16); // 3. 持久化并失效缓存 try { await repository.SaveChangesAsync(cancellationToken); } catch (Exception exception) when (IsConcurrencyException(exception)) { throw new BusinessException(ErrorCodes.Conflict, "字典项已被修改,请刷新后重试"); } await InvalidateCacheAsync(group, cancellationToken); logger.LogInformation("更新字典项:{ItemId}", item.Id); return MapItem(item); } /// /// 删除字典项。 /// /// 字典项 ID。 /// 取消标记。 public async Task DeleteItemAsync(long itemId, CancellationToken cancellationToken = default) { // 1. 读取字典项与分组并校验权限 var item = await RequireItemAsync(itemId, cancellationToken); var group = await RequireGroupAsync(item.GroupId, cancellationToken); // 2. 删除并失效缓存 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) { // 1. 规范化编码 var normalizedCodes = request.Codes .Where(code => !string.IsNullOrWhiteSpace(code)) .Select(NormalizeCode) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); if (normalizedCodes.Length == 0) { return new Dictionary>(StringComparer.OrdinalIgnoreCase); } // 2. 按租户合并系统与业务字典 var tenantId = request.TenantId ?? 0; var result = new Dictionary>(StringComparer.OrdinalIgnoreCase); foreach (var code in normalizedCodes) { var systemItems = await GetOrLoadCacheAsync(0, code, cancellationToken); if (tenantId == 0) { result[code] = systemItems; continue; } var tenantItems = await GetOrLoadCacheAsync(tenantId, code, cancellationToken); result[code] = MergeItems(systemItems, tenantItems); } return result; } private async Task RequireGroupAsync(long groupId, CancellationToken cancellationToken) { // 1. 读取分组,找不到抛异常 var group = await repository.FindGroupByIdAsync(groupId, cancellationToken); if (group == null) { throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在"); } return group; } private async Task RequireItemAsync(long itemId, CancellationToken cancellationToken) { // 1. 读取字典项,找不到抛异常 var item = await repository.FindItemByIdAsync(itemId, cancellationToken); if (item == null) { throw new BusinessException(ErrorCodes.NotFound, "字典项不存在"); } return item; } private static long ResolveTargetTenant(DictionaryScope scope, long? tenantId) { if (scope == DictionaryScope.System) { return 0; } if (!tenantId.HasValue || tenantId.Value <= 0) { throw new BusinessException(ErrorCodes.ValidationFailed, "业务参数需指定租户"); } return tenantId.Value; } private static string NormalizeCode(string code) => code.Trim().ToLowerInvariant(); private static DictionaryScope ResolveScopeForQuery(DictionaryScope? requestedScope, long tenantId) { if (requestedScope.HasValue) { return requestedScope.Value; } return tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business; } 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(long tenantId, string code, CancellationToken cancellationToken) { // 1. 先查缓存 var cached = await cache.GetAsync(tenantId, code, cancellationToken); if (cached != null) { return cached; } // 2. 从仓储加载并写入缓存 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, TenantId = group.TenantId, Name = group.Name, Scope = group.Scope, Description = group.Description, AllowOverride = group.AllowOverride, IsEnabled = group.IsEnabled, CreatedAt = group.CreatedAt, UpdatedAt = group.UpdatedAt, RowVersion = group.RowVersion, 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 = DictionaryValueConverter.Deserialize(item.Value), IsDefault = item.IsDefault, IsEnabled = item.IsEnabled, SortOrder = item.SortOrder, Description = item.Description, Source = item.TenantId == 0 ? "system" : "tenant", RowVersion = item.RowVersion }; private static bool IsConcurrencyException(Exception exception) => string.Equals(exception.GetType().Name, "DbUpdateConcurrencyException", StringComparison.Ordinal); }