feat: 商户冻结/解冻功能及字典缓存重构

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
MSuMshk
2026-02-04 10:46:32 +08:00
parent 754dd788ea
commit f69904e195
54 changed files with 753 additions and 1385 deletions

View File

@@ -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)

View File

@@ -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";
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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; }
}
}