using System.Security.Cryptography; 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.Domain.Dictionary.ValueObjects; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Dictionary.Services; /// /// 字典写操作服务。 /// public sealed class DictionaryCommandService( IDictionaryGroupRepository groupRepository, IDictionaryItemRepository itemRepository, IDictionaryHybridCache cache, ITenantProvider tenantProvider, ILogger logger) { /// /// 创建字典分组。 /// public async Task CreateGroupAsync(CreateDictionaryGroupRequest request, CancellationToken cancellationToken = default) { var targetTenantId = ResolveTargetTenant(request.Scope); var code = new DictionaryCode(request.Code); var existing = await groupRepository.GetByCodeAsync(targetTenantId, code, cancellationToken); if (existing != null) { throw new BusinessException(ErrorCodes.Conflict, $"字典分组编码 {code.Value} 已存在"); } var group = new DictionaryGroup { TenantId = targetTenantId, Code = code, Name = request.Name.Trim(), Scope = request.Scope, AllowOverride = request.AllowOverride, Description = request.Description?.Trim(), IsEnabled = true, RowVersion = RandomNumberGenerator.GetBytes(16) }; await groupRepository.AddAsync(group, cancellationToken); await groupRepository.SaveChangesAsync(cancellationToken); await cache.InvalidateAsync( DictionaryCacheKeys.BuildGroupPrefix(targetTenantId), CacheInvalidationOperation.Create, cancellationToken); logger.LogInformation("创建字典分组 {GroupCode}", group.Code); return DictionaryMapper.ToGroupDto(group); } /// /// 更新字典分组。 /// public async Task UpdateGroupAsync(long groupId, UpdateDictionaryGroupRequest request, CancellationToken cancellationToken = default) { var group = await RequireGroupAsync(groupId, cancellationToken); EnsureGroupAccess(group); EnsureRowVersion(request.RowVersion, group.RowVersion, "字典分组"); group.Name = request.Name.Trim(); group.Description = request.Description?.Trim(); group.IsEnabled = request.IsEnabled; group.AllowOverride = request.AllowOverride; group.RowVersion = RandomNumberGenerator.GetBytes(16); try { await groupRepository.SaveChangesAsync(cancellationToken); } catch (Exception exception) when (IsConcurrencyException(exception)) { throw new BusinessException(ErrorCodes.Conflict, "字典分组已被修改,请刷新后重试"); } await InvalidateGroupCacheAsync(group, CacheInvalidationOperation.Update, cancellationToken); logger.LogInformation("更新字典分组 {GroupId}", group.Id); return DictionaryMapper.ToGroupDto(group); } /// /// 删除字典分组。 /// public async Task DeleteGroupAsync(long groupId, CancellationToken cancellationToken = default) { var group = await groupRepository.GetByIdAsync(groupId, cancellationToken); if (group == null) { return false; } EnsureGroupAccess(group); var items = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, cancellationToken); foreach (var item in items) { await itemRepository.RemoveAsync(item, cancellationToken); } await groupRepository.RemoveAsync(group, cancellationToken); await groupRepository.SaveChangesAsync(cancellationToken); await InvalidateGroupCacheAsync(group, CacheInvalidationOperation.Delete, cancellationToken); logger.LogInformation("删除字典分组 {GroupId}", group.Id); return true; } /// /// 创建字典项。 /// public async Task 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(); if (items.Any(item => string.Equals(item.Key, normalizedKey, StringComparison.OrdinalIgnoreCase))) { throw new BusinessException(ErrorCodes.Conflict, $"字典项键 {normalizedKey} 已存在"); } var sortOrder = request.SortOrder; if (sortOrder <= 0) { sortOrder = items.Count == 0 ? 10 : items.Max(item => item.SortOrder) + 10; } var item = new DictionaryItem { TenantId = group.TenantId, GroupId = group.Id, Key = normalizedKey, Value = DictionaryValueConverter.Serialize(request.Value), Description = request.Description?.Trim(), SortOrder = sortOrder, IsDefault = request.IsDefault, IsEnabled = request.IsEnabled, RowVersion = RandomNumberGenerator.GetBytes(16) }; await itemRepository.AddAsync(item, cancellationToken); await groupRepository.SaveChangesAsync(cancellationToken); await InvalidateItemCacheAsync(group, CacheInvalidationOperation.Create, cancellationToken); logger.LogInformation("新增字典项 {ItemId}", item.Id); return DictionaryMapper.ToItemDto(item); } /// /// 更新字典项。 /// public async Task UpdateItemAsync(long itemId, UpdateDictionaryItemRequest request, CancellationToken cancellationToken = default) { var item = await RequireItemAsync(itemId, cancellationToken); var group = await RequireGroupAsync(item.GroupId, cancellationToken); EnsureGroupAccess(group); EnsureRowVersion(request.RowVersion, item.RowVersion, "字典项"); var normalizedKey = request.Key.Trim(); if (!string.Equals(item.Key, normalizedKey, StringComparison.OrdinalIgnoreCase)) { var items = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, cancellationToken); if (items.Any(existing => existing.Id != item.Id && string.Equals(existing.Key, normalizedKey, StringComparison.OrdinalIgnoreCase))) { throw new BusinessException(ErrorCodes.Conflict, $"字典项键 {normalizedKey} 已存在"); } } item.Key = normalizedKey; 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); try { await groupRepository.SaveChangesAsync(cancellationToken); } catch (Exception exception) when (IsConcurrencyException(exception)) { throw new BusinessException(ErrorCodes.Conflict, "字典项已被修改,请刷新后重试"); } await InvalidateItemCacheAsync(group, CacheInvalidationOperation.Update, cancellationToken); logger.LogInformation("更新字典项 {ItemId}", item.Id); return DictionaryMapper.ToItemDto(item); } /// /// 删除字典项。 /// public async Task DeleteItemAsync(long itemId, CancellationToken cancellationToken = default) { var item = await itemRepository.GetByIdAsync(itemId, cancellationToken); if (item == null) { return false; } var group = await RequireGroupAsync(item.GroupId, cancellationToken); EnsureGroupAccess(group); await itemRepository.RemoveAsync(item, cancellationToken); await groupRepository.SaveChangesAsync(cancellationToken); await InvalidateItemCacheAsync(group, CacheInvalidationOperation.Delete, cancellationToken); logger.LogInformation("删除字典项 {ItemId}", item.Id); return true; } private long ResolveTargetTenant(DictionaryScope scope) { var tenantId = tenantProvider.GetCurrentTenantId(); if (scope == DictionaryScope.System) { if (tenantId != 0) { throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可创建系统字典"); } return 0; } if (tenantId == 0) { throw new BusinessException(ErrorCodes.BadRequest, "业务字典必须在租户上下文中创建"); } return tenantId; } private void EnsureGroupAccess(DictionaryGroup group) { var tenantId = tenantProvider.GetCurrentTenantId(); if (group.Scope == DictionaryScope.System && tenantId != 0) { throw new BusinessException(ErrorCodes.Forbidden, "仅平台管理员可操作系统字典"); } if (group.Scope == DictionaryScope.Business && tenantId != group.TenantId) { throw new BusinessException(ErrorCodes.Forbidden, "无权操作其他租户字典"); } } private static void EnsureRowVersion(byte[]? requestVersion, byte[] entityVersion, string resourceName) { if (requestVersion == null || requestVersion.Length == 0) { throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空"); } if (!requestVersion.SequenceEqual(entityVersion)) { throw new BusinessException(ErrorCodes.Conflict, $"{resourceName}已被修改,请刷新后重试"); } } private async Task RequireGroupAsync(long groupId, CancellationToken cancellationToken) { var group = await groupRepository.GetByIdAsync(groupId, cancellationToken); if (group == null) { throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在"); } return group; } private async Task RequireItemAsync(long itemId, CancellationToken cancellationToken) { var item = await itemRepository.GetByIdAsync(itemId, cancellationToken); if (item == null) { throw new BusinessException(ErrorCodes.NotFound, "字典项不存在"); } return item; } private Task InvalidateGroupCacheAsync( DictionaryGroup group, CacheInvalidationOperation operation, CancellationToken cancellationToken) { var tasks = new List { cache.InvalidateAsync(DictionaryCacheKeys.BuildGroupPrefix(group.TenantId), operation, cancellationToken), cache.InvalidateAsync(DictionaryCacheKeys.BuildItemKey(group.Id), operation, cancellationToken), cache.InvalidateAsync(DictionaryCacheKeys.BuildDictionaryKey(group.TenantId, group.Code), operation, cancellationToken) }; if (group.Scope == DictionaryScope.System) { tasks.Add(cache.InvalidateAsync(DictionaryCacheKeys.DictionaryPrefix, operation, cancellationToken)); } return Task.WhenAll(tasks); } private Task InvalidateItemCacheAsync( DictionaryGroup group, CacheInvalidationOperation operation, CancellationToken cancellationToken) { var tasks = new List { cache.InvalidateAsync(DictionaryCacheKeys.BuildItemKey(group.Id), operation, cancellationToken), cache.InvalidateAsync(DictionaryCacheKeys.BuildDictionaryKey(group.TenantId, group.Code), operation, cancellationToken) }; if (group.Scope == DictionaryScope.System) { tasks.Add(cache.InvalidateAsync(DictionaryCacheKeys.DictionaryPrefix, operation, cancellationToken)); } return Task.WhenAll(tasks); } private static bool IsConcurrencyException(Exception exception) => string.Equals(exception.GetType().Name, "DbUpdateConcurrencyException", StringComparison.Ordinal); }