feat: 商户冻结/解冻功能及字典缓存重构
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
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;
|
||||
@@ -17,7 +16,6 @@ namespace TakeoutSaaS.Application.Dictionary.Services;
|
||||
/// </summary>
|
||||
public sealed class DictionaryAppService(
|
||||
IDictionaryRepository repository,
|
||||
IDictionaryCache cache,
|
||||
ILogger<DictionaryAppService> logger) : IDictionaryAppService
|
||||
{
|
||||
/// <summary>
|
||||
@@ -49,8 +47,7 @@ public sealed class DictionaryAppService(
|
||||
Scope = request.Scope,
|
||||
AllowOverride = request.AllowOverride,
|
||||
Description = request.Description?.Trim(),
|
||||
IsEnabled = true,
|
||||
RowVersion = RandomNumberGenerator.GetBytes(16)
|
||||
IsEnabled = true
|
||||
};
|
||||
|
||||
// 4. 持久化并返回
|
||||
@@ -72,12 +69,12 @@ public sealed class DictionaryAppService(
|
||||
// 1. 读取分组并校验权限
|
||||
var group = await RequireGroupAsync(groupId, cancellationToken);
|
||||
|
||||
if (request.RowVersion == null || request.RowVersion.Length == 0)
|
||||
if (request.RowVersion == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
|
||||
}
|
||||
|
||||
if (!request.RowVersion.SequenceEqual(group.RowVersion))
|
||||
if (request.RowVersion != group.RowVersion)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "字典分组已被修改,请刷新后重试");
|
||||
}
|
||||
@@ -87,9 +84,8 @@ public sealed class DictionaryAppService(
|
||||
group.Description = request.Description?.Trim();
|
||||
group.IsEnabled = request.IsEnabled;
|
||||
group.AllowOverride = request.AllowOverride;
|
||||
group.RowVersion = RandomNumberGenerator.GetBytes(16);
|
||||
|
||||
// 3. 持久化并失效缓存
|
||||
// 3. 持久化
|
||||
try
|
||||
{
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
@@ -98,7 +94,6 @@ public sealed class DictionaryAppService(
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "字典分组已被修改,请刷新后重试");
|
||||
}
|
||||
await InvalidateCacheAsync(group, cancellationToken);
|
||||
logger.LogInformation("更新字典分组:{GroupId}", group.Id);
|
||||
return MapGroup(group, includeItems: false);
|
||||
}
|
||||
@@ -113,10 +108,9 @@ public sealed class DictionaryAppService(
|
||||
// 1. 读取分组并校验权限
|
||||
var group = await RequireGroupAsync(groupId, cancellationToken);
|
||||
|
||||
// 2. 删除并失效缓存
|
||||
// 2. 删除
|
||||
await repository.RemoveGroupAsync(group, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateCacheAsync(group, cancellationToken);
|
||||
logger.LogInformation("删除字典分组:{GroupId}", group.Id);
|
||||
}
|
||||
|
||||
@@ -175,14 +169,12 @@ public sealed class DictionaryAppService(
|
||||
Description = request.Description?.Trim(),
|
||||
SortOrder = request.SortOrder,
|
||||
IsDefault = request.IsDefault,
|
||||
IsEnabled = request.IsEnabled,
|
||||
RowVersion = RandomNumberGenerator.GetBytes(16)
|
||||
IsEnabled = request.IsEnabled
|
||||
};
|
||||
|
||||
// 3. 持久化并失效缓存
|
||||
// 3. 持久化
|
||||
await repository.AddItemAsync(item, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateCacheAsync(group, cancellationToken);
|
||||
logger.LogInformation("新增字典项:{ItemId}", item.Id);
|
||||
return MapItem(item);
|
||||
}
|
||||
@@ -200,12 +192,12 @@ public sealed class DictionaryAppService(
|
||||
var item = await RequireItemAsync(itemId, cancellationToken);
|
||||
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
|
||||
|
||||
if (request.RowVersion == null || request.RowVersion.Length == 0)
|
||||
if (request.RowVersion == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
|
||||
}
|
||||
|
||||
if (!request.RowVersion.SequenceEqual(item.RowVersion))
|
||||
if (request.RowVersion != item.RowVersion)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "字典项已被修改,请刷新后重试");
|
||||
}
|
||||
@@ -217,9 +209,8 @@ public sealed class DictionaryAppService(
|
||||
item.SortOrder = request.SortOrder;
|
||||
item.IsDefault = request.IsDefault;
|
||||
item.IsEnabled = request.IsEnabled;
|
||||
item.RowVersion = RandomNumberGenerator.GetBytes(16);
|
||||
|
||||
// 3. 持久化并失效缓存
|
||||
// 3. 持久化
|
||||
try
|
||||
{
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
@@ -228,7 +219,6 @@ public sealed class DictionaryAppService(
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, "字典项已被修改,请刷新后重试");
|
||||
}
|
||||
await InvalidateCacheAsync(group, cancellationToken);
|
||||
logger.LogInformation("更新字典项:{ItemId}", item.Id);
|
||||
return MapItem(item);
|
||||
}
|
||||
@@ -244,15 +234,14 @@ public sealed class DictionaryAppService(
|
||||
var item = await RequireItemAsync(itemId, cancellationToken);
|
||||
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
|
||||
|
||||
// 2. 删除并失效缓存
|
||||
// 2. 删除
|
||||
await repository.RemoveItemAsync(item, cancellationToken);
|
||||
await repository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateCacheAsync(group, cancellationToken);
|
||||
logger.LogInformation("删除字典项:{ItemId}", item.Id);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取缓存中的字典项。
|
||||
/// 批量获取字典项。
|
||||
/// </summary>
|
||||
/// <param name="request">批量查询请求。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
@@ -277,14 +266,14 @@ public sealed class DictionaryAppService(
|
||||
|
||||
foreach (var code in normalizedCodes)
|
||||
{
|
||||
var systemItems = await GetOrLoadCacheAsync(0, code, cancellationToken);
|
||||
var systemItems = await LoadItemsAsync(0, code, cancellationToken);
|
||||
if (tenantId == 0)
|
||||
{
|
||||
result[code] = systemItems;
|
||||
continue;
|
||||
}
|
||||
|
||||
var tenantItems = await GetOrLoadCacheAsync(tenantId, code, cancellationToken);
|
||||
var tenantItems = await LoadItemsAsync(tenantId, code, cancellationToken);
|
||||
result[code] = MergeItems(systemItems, tenantItems);
|
||||
}
|
||||
|
||||
@@ -342,36 +331,15 @@ public sealed class DictionaryAppService(
|
||||
return tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business;
|
||||
}
|
||||
|
||||
private async Task InvalidateCacheAsync(DictionaryGroup group, CancellationToken cancellationToken)
|
||||
private async Task<IReadOnlyList<DictionaryItemDto>> LoadItemsAsync(long tenantId, string code, CancellationToken cancellationToken)
|
||||
{
|
||||
await cache.RemoveAsync(group.TenantId, group.Code, cancellationToken);
|
||||
if (group.Scope == DictionaryScope.Business)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 系统参数更新需要逐租户重新合并,由调用方在下一次请求时重新加载
|
||||
}
|
||||
|
||||
private async Task<IReadOnlyList<DictionaryItemDto>> 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
|
||||
return 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)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Domain.Dictionary.ValueObjects;
|
||||
|
||||
namespace TakeoutSaaS.Application.Dictionary.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 字典缓存键生成器。
|
||||
/// </summary>
|
||||
internal static class DictionaryCacheKeys
|
||||
{
|
||||
internal const string DictionaryPrefix = "dict:";
|
||||
internal const string GroupPrefix = "dict:groups:";
|
||||
internal const string ItemPrefix = "dict:items:";
|
||||
|
||||
internal static string BuildDictionaryKey(long tenantId, DictionaryCode code)
|
||||
=> $"{DictionaryPrefix}{tenantId}:{code.Value}";
|
||||
|
||||
internal static string BuildGroupKey(
|
||||
long tenantId,
|
||||
DictionaryScope scope,
|
||||
int page,
|
||||
int pageSize,
|
||||
string? keyword,
|
||||
bool? isEnabled,
|
||||
string? sortBy,
|
||||
bool sortDescending)
|
||||
{
|
||||
return $"{GroupPrefix}{tenantId}:{scope}:{page}:{pageSize}:{Normalize(keyword)}:{Normalize(isEnabled)}:{Normalize(sortBy)}:{(sortDescending ? "desc" : "asc")}";
|
||||
}
|
||||
|
||||
internal static string BuildGroupPrefix(long tenantId)
|
||||
=> $"{GroupPrefix}{tenantId}:";
|
||||
|
||||
internal static string BuildItemKey(long groupId)
|
||||
=> $"{ItemPrefix}{groupId}";
|
||||
|
||||
private static string Normalize(string? value)
|
||||
=> string.IsNullOrWhiteSpace(value)
|
||||
? "all"
|
||||
: value.Trim().ToLowerInvariant().Replace(":", "_", StringComparison.Ordinal);
|
||||
|
||||
private static string Normalize(bool? value)
|
||||
=> value.HasValue ? (value.Value ? "1" : "0") : "all";
|
||||
}
|
||||
@@ -1,6 +1,4 @@
|
||||
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;
|
||||
@@ -18,7 +16,6 @@ namespace TakeoutSaaS.Application.Dictionary.Services;
|
||||
public sealed class DictionaryCommandService(
|
||||
IDictionaryGroupRepository groupRepository,
|
||||
IDictionaryItemRepository itemRepository,
|
||||
IDictionaryHybridCache cache,
|
||||
ILogger<DictionaryCommandService> logger)
|
||||
{
|
||||
/// <summary>
|
||||
@@ -43,16 +40,11 @@ public sealed class DictionaryCommandService(
|
||||
Scope = request.Scope,
|
||||
AllowOverride = request.AllowOverride,
|
||||
Description = request.Description?.Trim(),
|
||||
IsEnabled = true,
|
||||
RowVersion = RandomNumberGenerator.GetBytes(16)
|
||||
IsEnabled = true
|
||||
};
|
||||
|
||||
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);
|
||||
@@ -71,7 +63,6 @@ public sealed class DictionaryCommandService(
|
||||
group.Description = request.Description?.Trim();
|
||||
group.IsEnabled = request.IsEnabled;
|
||||
group.AllowOverride = request.AllowOverride;
|
||||
group.RowVersion = RandomNumberGenerator.GetBytes(16);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -82,7 +73,6 @@ public sealed class DictionaryCommandService(
|
||||
throw new BusinessException(ErrorCodes.Conflict, "字典分组已被修改,请刷新后重试");
|
||||
}
|
||||
|
||||
await InvalidateGroupCacheAsync(group, CacheInvalidationOperation.Update, cancellationToken);
|
||||
logger.LogInformation("更新字典分组 {GroupId}", group.Id);
|
||||
return DictionaryMapper.ToGroupDto(group);
|
||||
}
|
||||
@@ -106,7 +96,6 @@ public sealed class DictionaryCommandService(
|
||||
|
||||
await groupRepository.RemoveAsync(group, cancellationToken);
|
||||
await groupRepository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateGroupCacheAsync(group, CacheInvalidationOperation.Delete, cancellationToken);
|
||||
|
||||
logger.LogInformation("删除字典分组 {GroupId}", group.Id);
|
||||
return true;
|
||||
@@ -141,13 +130,11 @@ public sealed class DictionaryCommandService(
|
||||
Description = request.Description?.Trim(),
|
||||
SortOrder = sortOrder,
|
||||
IsDefault = request.IsDefault,
|
||||
IsEnabled = request.IsEnabled,
|
||||
RowVersion = RandomNumberGenerator.GetBytes(16)
|
||||
IsEnabled = request.IsEnabled
|
||||
};
|
||||
|
||||
await itemRepository.AddAsync(item, cancellationToken);
|
||||
await groupRepository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateItemCacheAsync(group, CacheInvalidationOperation.Create, cancellationToken);
|
||||
|
||||
logger.LogInformation("新增字典项 {ItemId}", item.Id);
|
||||
return DictionaryMapper.ToItemDto(item);
|
||||
@@ -179,7 +166,6 @@ public sealed class DictionaryCommandService(
|
||||
item.SortOrder = request.SortOrder;
|
||||
item.IsDefault = request.IsDefault;
|
||||
item.IsEnabled = request.IsEnabled;
|
||||
item.RowVersion = RandomNumberGenerator.GetBytes(16);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -190,7 +176,6 @@ public sealed class DictionaryCommandService(
|
||||
throw new BusinessException(ErrorCodes.Conflict, "字典项已被修改,请刷新后重试");
|
||||
}
|
||||
|
||||
await InvalidateItemCacheAsync(group, CacheInvalidationOperation.Update, cancellationToken);
|
||||
logger.LogInformation("更新字典项 {ItemId}", item.Id);
|
||||
return DictionaryMapper.ToItemDto(item);
|
||||
}
|
||||
@@ -206,11 +191,8 @@ public sealed class DictionaryCommandService(
|
||||
return false;
|
||||
}
|
||||
|
||||
var group = await RequireGroupAsync(item.GroupId, cancellationToken);
|
||||
|
||||
await itemRepository.RemoveAsync(item, cancellationToken);
|
||||
await groupRepository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateItemCacheAsync(group, CacheInvalidationOperation.Delete, cancellationToken);
|
||||
|
||||
logger.LogInformation("删除字典项 {ItemId}", item.Id);
|
||||
return true;
|
||||
@@ -231,14 +213,14 @@ public sealed class DictionaryCommandService(
|
||||
return tenantId.Value;
|
||||
}
|
||||
|
||||
private static void EnsureRowVersion(byte[]? requestVersion, byte[] entityVersion, string resourceName)
|
||||
private static void EnsureRowVersion(uint requestVersion, uint entityVersion, string resourceName)
|
||||
{
|
||||
if (requestVersion == null || requestVersion.Length == 0)
|
||||
if (requestVersion == 0)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "RowVersion 不能为空");
|
||||
}
|
||||
|
||||
if (!requestVersion.SequenceEqual(entityVersion))
|
||||
if (requestVersion != entityVersion)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Conflict, $"{resourceName}已被修改,请刷新后重试");
|
||||
}
|
||||
@@ -266,45 +248,6 @@ public sealed class DictionaryCommandService(
|
||||
return item;
|
||||
}
|
||||
|
||||
private Task InvalidateGroupCacheAsync(
|
||||
DictionaryGroup group,
|
||||
CacheInvalidationOperation operation,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
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<Task>
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using TakeoutSaaS.Application.Dictionary.Abstractions;
|
||||
@@ -25,7 +24,6 @@ public sealed class DictionaryImportExportService(
|
||||
IDictionaryGroupRepository groupRepository,
|
||||
IDictionaryItemRepository itemRepository,
|
||||
IDictionaryImportLogRepository importLogRepository,
|
||||
IDictionaryHybridCache cache,
|
||||
ICurrentUserAccessor currentUser,
|
||||
ILogger<DictionaryImportExportService> logger)
|
||||
{
|
||||
@@ -163,8 +161,7 @@ public sealed class DictionaryImportExportService(
|
||||
SortOrder = sortOrder,
|
||||
IsEnabled = row.IsEnabled ?? true,
|
||||
IsDefault = false,
|
||||
Description = row.Description,
|
||||
RowVersion = RandomNumberGenerator.GetBytes(16)
|
||||
Description = row.Description
|
||||
};
|
||||
|
||||
await itemRepository.AddAsync(item, cancellationToken);
|
||||
@@ -173,7 +170,6 @@ public sealed class DictionaryImportExportService(
|
||||
}
|
||||
|
||||
await itemRepository.SaveChangesAsync(cancellationToken);
|
||||
await InvalidateGroupCacheAsync(group, cancellationToken);
|
||||
|
||||
var result = BuildResult(successCount, skipCount, errors, stopwatch.Elapsed);
|
||||
await RecordImportLogAsync(request, group, format, result, stopwatch.Elapsed, cancellationToken);
|
||||
@@ -380,23 +376,6 @@ public sealed class DictionaryImportExportService(
|
||||
}
|
||||
}
|
||||
|
||||
private async Task InvalidateGroupCacheAsync(DictionaryGroup group, CancellationToken cancellationToken)
|
||||
{
|
||||
var tasks = new List<Task>
|
||||
{
|
||||
cache.InvalidateAsync(DictionaryCacheKeys.BuildGroupPrefix(group.TenantId), CacheInvalidationOperation.Update, cancellationToken),
|
||||
cache.InvalidateAsync(DictionaryCacheKeys.BuildItemKey(group.Id), CacheInvalidationOperation.Update, cancellationToken),
|
||||
cache.InvalidateAsync(DictionaryCacheKeys.BuildDictionaryKey(group.TenantId, group.Code), CacheInvalidationOperation.Update, cancellationToken)
|
||||
};
|
||||
|
||||
if (group.Scope == DictionaryScope.System)
|
||||
{
|
||||
tasks.Add(cache.InvalidateAsync(DictionaryCacheKeys.DictionaryPrefix, CacheInvalidationOperation.Update, cancellationToken));
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks);
|
||||
}
|
||||
|
||||
private async Task<DictionaryGroup> RequireGroupAsync(long groupId, CancellationToken cancellationToken)
|
||||
{
|
||||
var group = await groupRepository.GetByIdAsync(groupId, cancellationToken);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Text.Json;
|
||||
using TakeoutSaaS.Application.Dictionary.Abstractions;
|
||||
using TakeoutSaaS.Application.Dictionary.Models;
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
@@ -16,8 +15,7 @@ namespace TakeoutSaaS.Application.Dictionary.Services;
|
||||
public sealed class DictionaryOverrideService(
|
||||
IDictionaryGroupRepository groupRepository,
|
||||
IDictionaryItemRepository itemRepository,
|
||||
ITenantDictionaryOverrideRepository overrideRepository,
|
||||
IDictionaryHybridCache cache)
|
||||
ITenantDictionaryOverrideRepository overrideRepository)
|
||||
{
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
|
||||
|
||||
@@ -106,10 +104,6 @@ public sealed class DictionaryOverrideService(
|
||||
}
|
||||
|
||||
await overrideRepository.SaveChangesAsync(cancellationToken);
|
||||
await cache.InvalidateAsync(
|
||||
DictionaryCacheKeys.BuildDictionaryKey(tenantId, systemGroup.Code),
|
||||
CacheInvalidationOperation.Update,
|
||||
cancellationToken);
|
||||
|
||||
return MapOverrideDto(config, systemGroup.Code);
|
||||
}
|
||||
@@ -134,10 +128,6 @@ public sealed class DictionaryOverrideService(
|
||||
config.OverrideEnabled = false;
|
||||
await overrideRepository.UpdateAsync(config, cancellationToken);
|
||||
await overrideRepository.SaveChangesAsync(cancellationToken);
|
||||
await cache.InvalidateAsync(
|
||||
DictionaryCacheKeys.BuildDictionaryKey(tenantId, systemGroup.Code),
|
||||
CacheInvalidationOperation.Update,
|
||||
cancellationToken);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -191,10 +181,6 @@ public sealed class DictionaryOverrideService(
|
||||
}
|
||||
|
||||
await overrideRepository.SaveChangesAsync(cancellationToken);
|
||||
await cache.InvalidateAsync(
|
||||
DictionaryCacheKeys.BuildDictionaryKey(tenantId, systemGroup.Code),
|
||||
CacheInvalidationOperation.Update,
|
||||
cancellationToken);
|
||||
|
||||
return MapOverrideDto(config, systemGroup.Code);
|
||||
}
|
||||
@@ -245,10 +231,6 @@ public sealed class DictionaryOverrideService(
|
||||
}
|
||||
|
||||
await overrideRepository.SaveChangesAsync(cancellationToken);
|
||||
await cache.InvalidateAsync(
|
||||
DictionaryCacheKeys.BuildDictionaryKey(tenantId, systemGroup.Code),
|
||||
CacheInvalidationOperation.Update,
|
||||
cancellationToken);
|
||||
|
||||
return MapOverrideDto(config, systemGroup.Code);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
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;
|
||||
@@ -16,11 +14,8 @@ namespace TakeoutSaaS.Application.Dictionary.Services;
|
||||
/// </summary>
|
||||
public sealed class DictionaryQueryService(
|
||||
IDictionaryGroupRepository groupRepository,
|
||||
IDictionaryItemRepository itemRepository,
|
||||
IDictionaryHybridCache cache)
|
||||
IDictionaryItemRepository itemRepository)
|
||||
{
|
||||
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(30);
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典分组分页数据。
|
||||
/// </summary>
|
||||
@@ -34,87 +29,55 @@ public sealed class DictionaryQueryService(
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "Scope=Business 时必须指定 TenantId");
|
||||
}
|
||||
|
||||
// 2. (空行后) 确定作用域与目标租户
|
||||
|
||||
// 2. 确定作用域与目标租户
|
||||
var scope = query.Scope ?? (tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business);
|
||||
if (scope == DictionaryScope.System)
|
||||
{
|
||||
tenantId = 0;
|
||||
}
|
||||
|
||||
// 3. (空行后) 构建缓存键并加载分页数据
|
||||
|
||||
// 3. 查询分页数据
|
||||
var sortDescending = string.Equals(query.SortOrder, "desc", StringComparison.OrdinalIgnoreCase);
|
||||
var targetTenant = scope == DictionaryScope.System ? 0 : tenantId;
|
||||
|
||||
var cacheKey = DictionaryCacheKeys.BuildGroupKey(
|
||||
var groups = await groupRepository.GetPagedAsync(
|
||||
targetTenant,
|
||||
scope,
|
||||
query.Page,
|
||||
query.PageSize,
|
||||
query.Keyword,
|
||||
query.IsEnabled,
|
||||
query.Page,
|
||||
query.PageSize,
|
||||
query.SortBy,
|
||||
sortDescending);
|
||||
|
||||
var cached = await cache.GetOrCreateAsync<DictionaryGroupPage>(
|
||||
cacheKey,
|
||||
CacheTtl,
|
||||
async token =>
|
||||
{
|
||||
var groups = await groupRepository.GetPagedAsync(
|
||||
targetTenant,
|
||||
scope,
|
||||
query.Keyword,
|
||||
query.IsEnabled,
|
||||
query.Page,
|
||||
query.PageSize,
|
||||
query.SortBy,
|
||||
sortDescending,
|
||||
token);
|
||||
|
||||
var total = await groupRepository.CountAsync(
|
||||
targetTenant,
|
||||
scope,
|
||||
query.Keyword,
|
||||
query.IsEnabled,
|
||||
token);
|
||||
|
||||
var items = new List<DictionaryGroupDto>(groups.Count);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
IReadOnlyList<DictionaryItemDto>? groupItems = null;
|
||||
if (query.IncludeItems)
|
||||
{
|
||||
var groupItemEntities = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, token);
|
||||
groupItems = groupItemEntities
|
||||
.Where(item => item.IsEnabled)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.Select(DictionaryMapper.ToItemDto)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
items.Add(DictionaryMapper.ToGroupDto(group, groupItems));
|
||||
}
|
||||
|
||||
return new DictionaryGroupPage
|
||||
{
|
||||
Items = items,
|
||||
Page = query.Page,
|
||||
PageSize = query.PageSize,
|
||||
TotalCount = total
|
||||
};
|
||||
},
|
||||
sortDescending,
|
||||
cancellationToken);
|
||||
|
||||
var page = cached ?? new DictionaryGroupPage
|
||||
{
|
||||
Items = Array.Empty<DictionaryGroupDto>(),
|
||||
Page = query.Page,
|
||||
PageSize = query.PageSize,
|
||||
TotalCount = 0
|
||||
};
|
||||
var total = await groupRepository.CountAsync(
|
||||
targetTenant,
|
||||
scope,
|
||||
query.Keyword,
|
||||
query.IsEnabled,
|
||||
cancellationToken);
|
||||
|
||||
return new PagedResult<DictionaryGroupDto>(page.Items, page.Page, page.PageSize, page.TotalCount);
|
||||
// 4. 转换为 DTO
|
||||
var items = new List<DictionaryGroupDto>(groups.Count);
|
||||
foreach (var group in groups)
|
||||
{
|
||||
IReadOnlyList<DictionaryItemDto>? groupItems = null;
|
||||
if (query.IncludeItems)
|
||||
{
|
||||
var groupItemEntities = await itemRepository.GetByGroupIdAsync(group.TenantId, group.Id, cancellationToken);
|
||||
groupItems = groupItemEntities
|
||||
.Where(item => item.IsEnabled)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.Select(DictionaryMapper.ToItemDto)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
items.Add(DictionaryMapper.ToGroupDto(group, groupItems));
|
||||
}
|
||||
|
||||
return new PagedResult<DictionaryGroupDto>(items, query.Page, query.PageSize, total);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -136,28 +99,18 @@ public sealed class DictionaryQueryService(
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<DictionaryItemDto>> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var cacheKey = DictionaryCacheKeys.BuildItemKey(groupId);
|
||||
var cached = await cache.GetOrCreateAsync<IReadOnlyList<DictionaryItemDto>>(
|
||||
cacheKey,
|
||||
CacheTtl,
|
||||
async token =>
|
||||
{
|
||||
var group = await groupRepository.GetByIdAsync(groupId, token);
|
||||
if (group == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在");
|
||||
}
|
||||
var group = await groupRepository.GetByIdAsync(groupId, cancellationToken);
|
||||
if (group == null)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.NotFound, "字典分组不存在");
|
||||
}
|
||||
|
||||
var items = await itemRepository.GetByGroupIdAsync(group.TenantId, groupId, token);
|
||||
return items
|
||||
.Where(item => item.IsEnabled)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.Select(DictionaryMapper.ToItemDto)
|
||||
.ToList();
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
return cached ?? Array.Empty<DictionaryItemDto>();
|
||||
var items = await itemRepository.GetByGroupIdAsync(group.TenantId, groupId, cancellationToken);
|
||||
return items
|
||||
.Where(item => item.IsEnabled)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.Select(DictionaryMapper.ToItemDto)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -171,31 +124,20 @@ public sealed class DictionaryQueryService(
|
||||
}
|
||||
|
||||
// 1. 管理端默认读取系统字典(TenantId=0)
|
||||
var tenantId = 0;
|
||||
var normalized = new DictionaryCode(code);
|
||||
var cacheKey = DictionaryCacheKeys.BuildDictionaryKey(tenantId, normalized);
|
||||
|
||||
var cached = await cache.GetOrCreateAsync<IReadOnlyList<DictionaryItemDto>>(
|
||||
cacheKey,
|
||||
CacheTtl,
|
||||
async token =>
|
||||
{
|
||||
var systemGroup = await groupRepository.GetByCodeAsync(0, normalized, token);
|
||||
if (systemGroup == null || !systemGroup.IsEnabled)
|
||||
{
|
||||
return Array.Empty<DictionaryItemDto>();
|
||||
}
|
||||
var systemGroup = await groupRepository.GetByCodeAsync(0, normalized, cancellationToken);
|
||||
if (systemGroup == null || !systemGroup.IsEnabled)
|
||||
{
|
||||
return Array.Empty<DictionaryItemDto>();
|
||||
}
|
||||
|
||||
var systemItems = await itemRepository.GetByGroupIdAsync(0, systemGroup.Id, token);
|
||||
return systemItems
|
||||
.Where(item => item.IsEnabled)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.Select(DictionaryMapper.ToItemDto)
|
||||
.ToList();
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
return cached ?? Array.Empty<DictionaryItemDto>();
|
||||
var systemItems = await itemRepository.GetByGroupIdAsync(0, systemGroup.Id, cancellationToken);
|
||||
return systemItems
|
||||
.Where(item => item.IsEnabled)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.Select(DictionaryMapper.ToItemDto)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -230,12 +172,4 @@ public sealed class DictionaryQueryService(
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private sealed class DictionaryGroupPage
|
||||
{
|
||||
public IReadOnlyList<DictionaryGroupDto> Items { get; init; } = Array.Empty<DictionaryGroupDto>();
|
||||
public int Page { get; init; }
|
||||
public int PageSize { get; init; }
|
||||
public int TotalCount { get; init; }
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user