feat: 实现字典管理后端
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.Dictionary.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存失效日志仓储实现。
|
||||
/// </summary>
|
||||
public sealed class CacheInvalidationLogRepository(DictionaryDbContext context) : ICacheInvalidationLogRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 新增失效日志。
|
||||
/// </summary>
|
||||
public Task AddAsync(CacheInvalidationLog log, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.CacheInvalidationLogs.Add(log);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询失效日志。
|
||||
/// </summary>
|
||||
public async Task<(IReadOnlyList<CacheInvalidationLog> Items, int TotalCount)> GetPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = context.CacheInvalidationLogs.AsNoTracking();
|
||||
|
||||
if (startDate.HasValue)
|
||||
{
|
||||
query = query.Where(log => log.Timestamp >= startDate.Value);
|
||||
}
|
||||
|
||||
if (endDate.HasValue)
|
||||
{
|
||||
query = query.Where(log => log.Timestamp <= endDate.Value);
|
||||
}
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
.OrderByDescending(log => log.Timestamp)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return (items, total);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存变更。
|
||||
/// </summary>
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
using TakeoutSaaS.Domain.Dictionary.ValueObjects;
|
||||
using TakeoutSaaS.Infrastructure.Dictionary.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 字典分组仓储实现。
|
||||
/// </summary>
|
||||
public sealed class DictionaryGroupRepository(DictionaryDbContext context) : IDictionaryGroupRepository
|
||||
{
|
||||
private static readonly Func<DictionaryDbContext, long, DictionaryCode, Task<DictionaryGroup?>> GetByCodeQuery =
|
||||
EF.CompileAsyncQuery((DictionaryDbContext db, long tenantId, DictionaryCode code) =>
|
||||
db.DictionaryGroups
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefault(group => group.TenantId == tenantId && group.DeletedAt == null && group.Code == code));
|
||||
|
||||
/// <summary>
|
||||
/// 按 ID 获取字典分组。
|
||||
/// </summary>
|
||||
public Task<DictionaryGroup?> GetByIdAsync(long groupId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.DictionaryGroups
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(group => group.Id == groupId && group.DeletedAt == null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按编码获取字典分组。
|
||||
/// </summary>
|
||||
public Task<DictionaryGroup?> GetByCodeAsync(long tenantId, DictionaryCode code, CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
return GetByCodeQuery(context, tenantId, code);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 分页获取字典分组。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<DictionaryGroup>> GetPagedAsync(
|
||||
long tenantId,
|
||||
DictionaryScope? scope,
|
||||
string? keyword,
|
||||
bool? isEnabled,
|
||||
int page,
|
||||
int pageSize,
|
||||
string? sortBy,
|
||||
bool sortDescending,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var query = BuildQuery(tenantId, scope, keyword, isEnabled);
|
||||
|
||||
var skip = Math.Max(page - 1, 0) * Math.Max(pageSize, 1);
|
||||
query = ApplyOrdering(query, sortBy, sortDescending);
|
||||
|
||||
return await query
|
||||
.Skip(skip)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取满足条件的分组数量。
|
||||
/// </summary>
|
||||
public Task<int> CountAsync(
|
||||
long tenantId,
|
||||
DictionaryScope? scope,
|
||||
string? keyword,
|
||||
bool? isEnabled,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return BuildQuery(tenantId, scope, keyword, isEnabled)
|
||||
.CountAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取字典分组。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<DictionaryGroup>> GetByIdsAsync(IEnumerable<long> groupIds, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var ids = groupIds?.Distinct().ToArray() ?? Array.Empty<long>();
|
||||
if (ids.Length == 0)
|
||||
{
|
||||
return Array.Empty<DictionaryGroup>();
|
||||
}
|
||||
|
||||
return await context.DictionaryGroups
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(group => ids.Contains(group.Id) && group.DeletedAt == null)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
private static IQueryable<DictionaryGroup> ApplyOrdering(IQueryable<DictionaryGroup> query, string? sortBy, bool sortDescending)
|
||||
{
|
||||
var normalized = sortBy?.Trim().ToLowerInvariant();
|
||||
return normalized switch
|
||||
{
|
||||
"name" => sortDescending ? query.OrderByDescending(group => group.Name) : query.OrderBy(group => group.Name),
|
||||
"createdat" => sortDescending ? query.OrderByDescending(group => group.CreatedAt) : query.OrderBy(group => group.CreatedAt),
|
||||
"updatedat" => sortDescending ? query.OrderByDescending(group => group.UpdatedAt) : query.OrderBy(group => group.UpdatedAt),
|
||||
_ => sortDescending ? query.OrderByDescending(group => group.Code) : query.OrderBy(group => group.Code)
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增分组。
|
||||
/// </summary>
|
||||
public Task AddAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.DictionaryGroups.Add(group);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新分组。
|
||||
/// </summary>
|
||||
public Task UpdateAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.DictionaryGroups.Update(group);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除分组。
|
||||
/// </summary>
|
||||
public Task RemoveAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.DictionaryGroups.Remove(group);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 持久化更改。
|
||||
/// </summary>
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> context.SaveChangesAsync(cancellationToken);
|
||||
|
||||
private IQueryable<DictionaryGroup> BuildQuery(long tenantId, DictionaryScope? scope, string? keyword, bool? isEnabled)
|
||||
{
|
||||
var query = context.DictionaryGroups
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(group => group.TenantId == tenantId && group.DeletedAt == null);
|
||||
|
||||
if (scope.HasValue)
|
||||
{
|
||||
query = query.Where(group => group.Scope == scope.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
var trimmed = keyword.Trim();
|
||||
query = query.Where(group =>
|
||||
EF.Property<string>(group, "Code").Contains(trimmed) ||
|
||||
group.Name.Contains(trimmed));
|
||||
}
|
||||
|
||||
if (isEnabled.HasValue)
|
||||
{
|
||||
query = query.Where(group => group.IsEnabled == isEnabled.Value);
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.Dictionary.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 字典导入日志仓储实现。
|
||||
/// </summary>
|
||||
public sealed class DictionaryImportLogRepository(DictionaryDbContext context) : IDictionaryImportLogRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 新增导入日志。
|
||||
/// </summary>
|
||||
public Task AddAsync(DictionaryImportLog log, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.DictionaryImportLogs.Add(log);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 持久化更改。
|
||||
/// </summary>
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.Dictionary.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 字典项仓储实现。
|
||||
/// </summary>
|
||||
public sealed class DictionaryItemRepository(DictionaryDbContext context) : IDictionaryItemRepository
|
||||
{
|
||||
private static readonly Func<DictionaryDbContext, long, long, IEnumerable<DictionaryItem>> GetByGroupQuery =
|
||||
EF.CompileQuery((DictionaryDbContext db, long tenantId, long groupId) =>
|
||||
(IEnumerable<DictionaryItem>)db.DictionaryItems
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(item => item.GroupId == groupId && item.TenantId == tenantId && item.DeletedAt == null)
|
||||
.OrderBy(item => item.SortOrder));
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取字典项。
|
||||
/// </summary>
|
||||
public Task<DictionaryItem?> GetByIdAsync(long itemId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.DictionaryItems
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(item => item.Id == itemId && item.DeletedAt == null, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取分组下字典项列表。
|
||||
/// </summary>
|
||||
public Task<IReadOnlyList<DictionaryItem>> GetByGroupIdAsync(
|
||||
long tenantId,
|
||||
long groupId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
_ = cancellationToken;
|
||||
return Task.FromResult<IReadOnlyList<DictionaryItem>>(
|
||||
GetByGroupQuery(context, tenantId, groupId).ToList());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取系统与租户合并的字典项列表。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<DictionaryItem>> GetMergedItemsAsync(
|
||||
long tenantId,
|
||||
long systemGroupId,
|
||||
bool includeOverrides,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var systemGroup = await context.DictionaryGroups
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(group => group.Id == systemGroupId && group.DeletedAt == null, cancellationToken);
|
||||
|
||||
if (systemGroup == null)
|
||||
{
|
||||
return Array.Empty<DictionaryItem>();
|
||||
}
|
||||
|
||||
var result = new List<DictionaryItem>();
|
||||
var systemItems = await context.DictionaryItems
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(item => item.GroupId == systemGroupId && item.TenantId == 0 && item.DeletedAt == null)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
result.AddRange(systemItems);
|
||||
|
||||
if (!includeOverrides || tenantId == 0)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var tenantGroup = await context.DictionaryGroups
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(group =>
|
||||
group.TenantId == tenantId &&
|
||||
group.DeletedAt == null &&
|
||||
group.Code == systemGroup.Code,
|
||||
cancellationToken);
|
||||
|
||||
if (tenantGroup == null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var tenantItems = await context.DictionaryItems
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(item => item.GroupId == tenantGroup.Id && item.TenantId == tenantId && item.DeletedAt == null)
|
||||
.OrderBy(item => item.SortOrder)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
result.AddRange(tenantItems);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增字典项。
|
||||
/// </summary>
|
||||
public Task AddAsync(DictionaryItem item, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.DictionaryItems.Add(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典项。
|
||||
/// </summary>
|
||||
public Task UpdateAsync(DictionaryItem item, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var entry = context.Entry(item);
|
||||
if (entry.State == EntityState.Detached)
|
||||
{
|
||||
context.DictionaryItems.Attach(item);
|
||||
entry = context.Entry(item);
|
||||
}
|
||||
|
||||
entry.State = EntityState.Modified;
|
||||
var originalVersion = item.RowVersion;
|
||||
var nextVersion = RandomNumberGenerator.GetBytes(16);
|
||||
entry.Property(x => x.RowVersion).OriginalValue = originalVersion;
|
||||
entry.Property(x => x.RowVersion).CurrentValue = nextVersion;
|
||||
item.RowVersion = nextVersion;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典项。
|
||||
/// </summary>
|
||||
public Task RemoveAsync(DictionaryItem item, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.DictionaryItems.Remove(item);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 持久化更改。
|
||||
/// </summary>
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Domain.Dictionary.ValueObjects;
|
||||
using TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.Dictionary.Persistence;
|
||||
|
||||
@@ -27,7 +28,7 @@ public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDicti
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>匹配分组或 null。</returns>
|
||||
public Task<DictionaryGroup?> FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default)
|
||||
=> context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == code, cancellationToken);
|
||||
=> context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == new DictionaryCode(code), cancellationToken);
|
||||
|
||||
/// <summary>
|
||||
/// 搜索分组列表。
|
||||
@@ -153,8 +154,8 @@ public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDicti
|
||||
// 1. 规范化编码
|
||||
var normalizedCodes = codes
|
||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||
.Select(code => code.Trim().ToLowerInvariant())
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Select(code => new DictionaryCode(code))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
if (normalizedCodes.Length == 0)
|
||||
@@ -167,7 +168,7 @@ public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDicti
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Include(item => item.Group)
|
||||
.Where(item => normalizedCodes.Contains(item.Group!.Code));
|
||||
.Where(item => normalizedCodes.Contains(item.Group!.Code) && item.DeletedAt == null);
|
||||
|
||||
// 3. 按租户或系统级过滤
|
||||
query = query.Where(item => item.TenantId == tenantId || (includeSystem && item.TenantId == 0));
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
using TakeoutSaaS.Infrastructure.Dictionary.Persistence;
|
||||
|
||||
namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 租户字典覆盖仓储实现。
|
||||
/// </summary>
|
||||
public sealed class TenantDictionaryOverrideRepository(DictionaryDbContext context) : ITenantDictionaryOverrideRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取租户覆盖配置。
|
||||
/// </summary>
|
||||
public Task<TenantDictionaryOverride?> GetAsync(long tenantId, long systemGroupId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return context.TenantDictionaryOverrides
|
||||
.IgnoreQueryFilters()
|
||||
.FirstOrDefaultAsync(config =>
|
||||
config.TenantId == tenantId &&
|
||||
config.SystemDictionaryGroupId == systemGroupId &&
|
||||
config.DeletedAt == null,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取租户全部覆盖配置。
|
||||
/// </summary>
|
||||
public async Task<IReadOnlyList<TenantDictionaryOverride>> ListAsync(long tenantId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
return await context.TenantDictionaryOverrides
|
||||
.AsNoTracking()
|
||||
.IgnoreQueryFilters()
|
||||
.Where(config => config.TenantId == tenantId && config.DeletedAt == null)
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 新增覆盖配置。
|
||||
/// </summary>
|
||||
public Task AddAsync(TenantDictionaryOverride overrideConfig, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.TenantDictionaryOverrides.Add(overrideConfig);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 更新覆盖配置。
|
||||
/// </summary>
|
||||
public Task UpdateAsync(TenantDictionaryOverride overrideConfig, CancellationToken cancellationToken = default)
|
||||
{
|
||||
context.TenantDictionaryOverrides.Update(overrideConfig);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 持久化更改。
|
||||
/// </summary>
|
||||
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||
=> context.SaveChangesAsync(cancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user