feat: 实现字典管理后端
This commit is contained in:
@@ -0,0 +1,246 @@
|
||||
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.Results;
|
||||
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
||||
|
||||
namespace TakeoutSaaS.Application.Dictionary.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 字典查询服务。
|
||||
/// </summary>
|
||||
public sealed class DictionaryQueryService(
|
||||
IDictionaryGroupRepository groupRepository,
|
||||
IDictionaryItemRepository itemRepository,
|
||||
DictionaryMergeService mergeService,
|
||||
IDictionaryHybridCache cache,
|
||||
ITenantProvider tenantProvider)
|
||||
{
|
||||
private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(30);
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典分组分页数据。
|
||||
/// </summary>
|
||||
public async Task<PagedResult<DictionaryGroupDto>> GetGroupsAsync(
|
||||
DictionaryGroupQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
var scope = query.Scope ?? (tenantId == 0 ? DictionaryScope.System : DictionaryScope.Business);
|
||||
var sortDescending = string.Equals(query.SortOrder, "desc", StringComparison.OrdinalIgnoreCase);
|
||||
var targetTenant = scope == DictionaryScope.System ? 0 : tenantId;
|
||||
|
||||
var cacheKey = DictionaryCacheKeys.BuildGroupKey(
|
||||
targetTenant,
|
||||
scope,
|
||||
query.Page,
|
||||
query.PageSize,
|
||||
query.Keyword,
|
||||
query.IsEnabled,
|
||||
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
|
||||
};
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
var page = cached ?? new DictionaryGroupPage
|
||||
{
|
||||
Items = Array.Empty<DictionaryGroupDto>(),
|
||||
Page = query.Page,
|
||||
PageSize = query.PageSize,
|
||||
TotalCount = 0
|
||||
};
|
||||
|
||||
return new PagedResult<DictionaryGroupDto>(page.Items, page.Page, page.PageSize, page.TotalCount);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取字典分组详情。
|
||||
/// </summary>
|
||||
public async Task<DictionaryGroupDto?> GetGroupByIdAsync(long groupId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var group = await groupRepository.GetByIdAsync(groupId, cancellationToken);
|
||||
if (group == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
EnsureGroupReadable(group);
|
||||
return DictionaryMapper.ToGroupDto(group);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取分组下字典项列表。
|
||||
/// </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, "字典分组不存在");
|
||||
}
|
||||
|
||||
EnsureGroupReadable(group);
|
||||
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>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取合并后的字典项列表。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<DictionaryItemDto>> GetMergedDictionaryAsync(string code, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!DictionaryCode.IsValid(code))
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.ValidationFailed, "字典编码格式不正确");
|
||||
}
|
||||
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
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>();
|
||||
}
|
||||
|
||||
if (tenantId == 0)
|
||||
{
|
||||
var systemItems = await itemRepository.GetByGroupIdAsync(0, systemGroup.Id, token);
|
||||
return systemItems
|
||||
.Where(item => item.IsEnabled)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.Select(DictionaryMapper.ToItemDto)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
return await mergeService.MergeItemsAsync(tenantId, systemGroup.Id, token);
|
||||
},
|
||||
cancellationToken);
|
||||
|
||||
return cached ?? Array.Empty<DictionaryItemDto>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取字典项。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyDictionary<string, IReadOnlyList<DictionaryItemDto>>> BatchGetDictionariesAsync(
|
||||
IEnumerable<string> codes,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedCodes = codes
|
||||
.Where(DictionaryCode.IsValid)
|
||||
.Select(code => new DictionaryCode(code).Value)
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var result = new Dictionary<string, IReadOnlyList<DictionaryItemDto>>(StringComparer.OrdinalIgnoreCase);
|
||||
if (normalizedCodes.Length == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var tasks = normalizedCodes.Select(async code =>
|
||||
{
|
||||
var items = await GetMergedDictionaryAsync(code, cancellationToken);
|
||||
return (code, items);
|
||||
});
|
||||
|
||||
foreach (var pair in await Task.WhenAll(tasks))
|
||||
{
|
||||
result[pair.code] = pair.items;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private void EnsureGroupReadable(DictionaryGroup group)
|
||||
{
|
||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||
if (tenantId != 0 && group.Scope == DictionaryScope.Business && group.TenantId != tenantId)
|
||||
{
|
||||
throw new BusinessException(ErrorCodes.Forbidden, "无权访问其他租户字典");
|
||||
}
|
||||
}
|
||||
|
||||
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