feat: 实现字典管理后端
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 字典缓存失效日志。
|
||||
/// </summary>
|
||||
public sealed class CacheInvalidationLog : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 发生时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字典编码。
|
||||
/// </summary>
|
||||
public string DictionaryCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 字典作用域。
|
||||
/// </summary>
|
||||
public DictionaryScope Scope { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 影响的缓存键数量。
|
||||
/// </summary>
|
||||
public int AffectedCacheKeyCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作人用户标识。
|
||||
/// </summary>
|
||||
public long OperatorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作类型。
|
||||
/// </summary>
|
||||
public CacheInvalidationOperation Operation { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Domain.Dictionary.ValueObjects;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
@@ -11,7 +12,7 @@ public sealed class DictionaryGroup : MultiTenantEntityBase
|
||||
/// <summary>
|
||||
/// 分组编码(唯一)。
|
||||
/// </summary>
|
||||
public string Code { get; set; } = string.Empty;
|
||||
public DictionaryCode Code { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 分组名称。
|
||||
@@ -23,6 +24,11 @@ public sealed class DictionaryGroup : MultiTenantEntityBase
|
||||
/// </summary>
|
||||
public DictionaryScope Scope { get; set; } = DictionaryScope.Business;
|
||||
|
||||
/// <summary>
|
||||
/// 是否允许租户覆盖。
|
||||
/// </summary>
|
||||
public bool AllowOverride { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 描述信息。
|
||||
/// </summary>
|
||||
@@ -33,6 +39,11 @@ public sealed class DictionaryGroup : MultiTenantEntityBase
|
||||
/// </summary>
|
||||
public bool IsEnabled { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// 并发控制字段。
|
||||
/// </summary>
|
||||
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>
|
||||
/// 字典项集合。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 字典导入审计日志。
|
||||
/// </summary>
|
||||
public sealed class DictionaryImportLog : MultiTenantEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 操作人用户标识。
|
||||
/// </summary>
|
||||
public long OperatorId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 字典分组编码。
|
||||
/// </summary>
|
||||
public string DictionaryGroupCode { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 导入文件名。
|
||||
/// </summary>
|
||||
public string FileName { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 文件大小(字节)。
|
||||
/// </summary>
|
||||
public long FileSize { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 文件格式(CSV/JSON)。
|
||||
/// </summary>
|
||||
public string Format { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 冲突处理模式。
|
||||
/// </summary>
|
||||
public ConflictResolutionMode ConflictMode { get; set; } = ConflictResolutionMode.Skip;
|
||||
|
||||
/// <summary>
|
||||
/// 成功导入数量。
|
||||
/// </summary>
|
||||
public int SuccessCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 跳过数量。
|
||||
/// </summary>
|
||||
public int SkipCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 错误明细(JSON)。
|
||||
/// </summary>
|
||||
public string? ErrorDetails { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime ProcessedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 处理耗时。
|
||||
/// </summary>
|
||||
public TimeSpan Duration { get; set; }
|
||||
}
|
||||
@@ -42,6 +42,11 @@ public sealed class DictionaryItem : MultiTenantEntityBase
|
||||
/// </summary>
|
||||
public string? Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 并发控制字段。
|
||||
/// </summary>
|
||||
public byte[] RowVersion { get; set; } = Array.Empty<byte>();
|
||||
|
||||
/// <summary>
|
||||
/// 导航属性:所属分组。
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 租户字典覆盖配置。
|
||||
/// </summary>
|
||||
public sealed class TenantDictionaryOverride : IMultiTenantEntity, IAuditableEntity
|
||||
{
|
||||
/// <summary>
|
||||
/// 所属租户 ID。
|
||||
/// </summary>
|
||||
public long TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 系统字典分组 ID。
|
||||
/// </summary>
|
||||
public long SystemDictionaryGroupId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否启用覆盖。
|
||||
/// </summary>
|
||||
public bool OverrideEnabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 隐藏的系统字典项 ID 列表。
|
||||
/// </summary>
|
||||
public long[] HiddenSystemItemIds { get; set; } = Array.Empty<long>();
|
||||
|
||||
/// <summary>
|
||||
/// 自定义排序映射(JSON)。
|
||||
/// </summary>
|
||||
public string CustomSortOrder { get; set; } = "{}";
|
||||
|
||||
/// <summary>
|
||||
/// 创建时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近更新时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除时间(UTC)。
|
||||
/// </summary>
|
||||
public DateTime? DeletedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建人用户标识。
|
||||
/// </summary>
|
||||
public long? CreatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最后更新人用户标识。
|
||||
/// </summary>
|
||||
public long? UpdatedBy { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 删除人用户标识。
|
||||
/// </summary>
|
||||
public long? DeletedBy { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存失效操作类型。
|
||||
/// </summary>
|
||||
public enum CacheInvalidationOperation
|
||||
{
|
||||
Create = 1,
|
||||
Update = 2,
|
||||
Delete = 3
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 字典导入冲突处理策略。
|
||||
/// </summary>
|
||||
public enum ConflictResolutionMode
|
||||
{
|
||||
/// <summary>
|
||||
/// 跳过重复项。
|
||||
/// </summary>
|
||||
Skip = 1,
|
||||
|
||||
/// <summary>
|
||||
/// 覆盖重复项。
|
||||
/// </summary>
|
||||
Overwrite = 2,
|
||||
|
||||
/// <summary>
|
||||
/// 追加新项。
|
||||
/// </summary>
|
||||
Append = 3
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 缓存失效日志仓储。
|
||||
/// </summary>
|
||||
public interface ICacheInvalidationLogRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 新增失效日志。
|
||||
/// </summary>
|
||||
Task AddAsync(CacheInvalidationLog log, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询失效日志。
|
||||
/// </summary>
|
||||
Task<(IReadOnlyList<CacheInvalidationLog> Items, int TotalCount)> GetPagedAsync(
|
||||
int page,
|
||||
int pageSize,
|
||||
DateTime? startDate,
|
||||
DateTime? endDate,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 保存变更。
|
||||
/// </summary>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
using TakeoutSaaS.Domain.Dictionary.Enums;
|
||||
using TakeoutSaaS.Domain.Dictionary.ValueObjects;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 字典分组仓储契约。
|
||||
/// </summary>
|
||||
public interface IDictionaryGroupRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 按 ID 获取字典分组。
|
||||
/// </summary>
|
||||
/// <param name="groupId">分组 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分组实体或 null。</returns>
|
||||
Task<DictionaryGroup?> GetByIdAsync(long groupId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按编码获取字典分组。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="code">分组编码。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分组实体或 null。</returns>
|
||||
Task<DictionaryGroup?> GetByCodeAsync(long tenantId, DictionaryCode code, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 分页获取字典分组。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="scope">作用域过滤。</param>
|
||||
/// <param name="keyword">关键字过滤。</param>
|
||||
/// <param name="isEnabled">启用状态过滤。</param>
|
||||
/// <param name="page">页码。</param>
|
||||
/// <param name="pageSize">页大小。</param>
|
||||
/// <param name="sortBy">排序字段。</param>
|
||||
/// <param name="sortDescending">是否降序。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分组集合。</returns>
|
||||
Task<IReadOnlyList<DictionaryGroup>> GetPagedAsync(
|
||||
long tenantId,
|
||||
DictionaryScope? scope,
|
||||
string? keyword,
|
||||
bool? isEnabled,
|
||||
int page,
|
||||
int pageSize,
|
||||
string? sortBy,
|
||||
bool sortDescending,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取满足条件的分组数量。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="scope">作用域过滤。</param>
|
||||
/// <param name="keyword">关键字过滤。</param>
|
||||
/// <param name="isEnabled">启用状态过滤。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分组数量。</returns>
|
||||
Task<int> CountAsync(
|
||||
long tenantId,
|
||||
DictionaryScope? scope,
|
||||
string? keyword,
|
||||
bool? isEnabled,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取字典分组。
|
||||
/// </summary>
|
||||
/// <param name="groupIds">分组 ID 列表。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分组集合。</returns>
|
||||
Task<IReadOnlyList<DictionaryGroup>> GetByIdsAsync(IEnumerable<long> groupIds, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增分组。
|
||||
/// </summary>
|
||||
/// <param name="group">分组实体。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task AddAsync(DictionaryGroup group, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 更新分组。
|
||||
/// </summary>
|
||||
/// <param name="group">分组实体。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task UpdateAsync(DictionaryGroup group, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 删除分组。
|
||||
/// </summary>
|
||||
/// <param name="group">分组实体。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task RemoveAsync(DictionaryGroup group, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化更改。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 字典导入日志仓储契约。
|
||||
/// </summary>
|
||||
public interface IDictionaryImportLogRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 新增导入日志。
|
||||
/// </summary>
|
||||
/// <param name="log">导入日志。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task AddAsync(DictionaryImportLog log, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化更改。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 字典项仓储契约。
|
||||
/// </summary>
|
||||
public interface IDictionaryItemRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 根据 ID 获取字典项。
|
||||
/// </summary>
|
||||
/// <param name="itemId">字典项 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>字典项或 null。</returns>
|
||||
Task<DictionaryItem?> GetByIdAsync(long itemId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取分组下字典项列表。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="groupId">分组 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>字典项集合。</returns>
|
||||
Task<IReadOnlyList<DictionaryItem>> GetByGroupIdAsync(long tenantId, long groupId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取合并后的系统/租户字典项。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="systemGroupId">系统分组 ID。</param>
|
||||
/// <param name="includeOverrides">是否包含租户覆盖。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>字典项集合。</returns>
|
||||
Task<IReadOnlyList<DictionaryItem>> GetMergedItemsAsync(long tenantId, long systemGroupId, bool includeOverrides, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增字典项。
|
||||
/// </summary>
|
||||
/// <param name="item">字典项实体。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task AddAsync(DictionaryItem item, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 更新字典项。
|
||||
/// </summary>
|
||||
/// <param name="item">字典项实体。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task UpdateAsync(DictionaryItem item, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 删除字典项。
|
||||
/// </summary>
|
||||
/// <param name="item">字典项实体。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task RemoveAsync(DictionaryItem item, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化更改。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
using TakeoutSaaS.Domain.Dictionary.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 租户字典覆盖仓储契约。
|
||||
/// </summary>
|
||||
public interface ITenantDictionaryOverrideRepository
|
||||
{
|
||||
/// <summary>
|
||||
/// 获取租户覆盖配置。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="systemGroupId">系统字典分组 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>覆盖配置或 null。</returns>
|
||||
Task<TenantDictionaryOverride?> GetAsync(long tenantId, long systemGroupId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取租户全部覆盖配置。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>覆盖配置列表。</returns>
|
||||
Task<IReadOnlyList<TenantDictionaryOverride>> ListAsync(long tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增覆盖配置。
|
||||
/// </summary>
|
||||
/// <param name="overrideConfig">覆盖配置。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task AddAsync(TenantDictionaryOverride overrideConfig, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 更新覆盖配置。
|
||||
/// </summary>
|
||||
/// <param name="overrideConfig">覆盖配置。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task UpdateAsync(TenantDictionaryOverride overrideConfig, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化更改。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步任务。</returns>
|
||||
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// 字典分组编码值对象。
|
||||
/// </summary>
|
||||
public readonly struct DictionaryCode : IEquatable<DictionaryCode>
|
||||
{
|
||||
private static readonly Regex CodePattern = new("^[a-zA-Z0-9_]{2,64}$", RegexOptions.Compiled);
|
||||
private readonly string? _value;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化字典编码并进行规范化。
|
||||
/// </summary>
|
||||
/// <param name="value">原始编码。</param>
|
||||
/// <exception cref="ArgumentException">编码非法时抛出。</exception>
|
||||
public DictionaryCode(string value)
|
||||
{
|
||||
_value = Normalize(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 规范化后的编码值。
|
||||
/// </summary>
|
||||
public string Value => _value ?? string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 判断编码是否符合规则。
|
||||
/// </summary>
|
||||
public static bool IsValid(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
return CodePattern.IsMatch(trimmed);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 规范化编码(去空格、转小写并校验)。
|
||||
/// </summary>
|
||||
/// <param name="value">原始编码。</param>
|
||||
/// <returns>规范化后的编码。</returns>
|
||||
/// <exception cref="ArgumentException">编码非法时抛出。</exception>
|
||||
public static string Normalize(string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
throw new ArgumentException("Dictionary code is required.", nameof(value));
|
||||
}
|
||||
|
||||
var trimmed = value.Trim();
|
||||
if (!CodePattern.IsMatch(trimmed))
|
||||
{
|
||||
throw new ArgumentException("Dictionary code must be 2-64 characters of letters, digits, or underscore.", nameof(value));
|
||||
}
|
||||
|
||||
return trimmed.ToLowerInvariant();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(DictionaryCode other)
|
||||
=> StringComparer.Ordinal.Equals(Value, other.Value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is DictionaryCode other && Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
=> StringComparer.Ordinal.GetHashCode(Value);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
=> Value;
|
||||
|
||||
public static bool operator ==(DictionaryCode left, DictionaryCode right)
|
||||
=> left.Equals(right);
|
||||
|
||||
public static bool operator !=(DictionaryCode left, DictionaryCode right)
|
||||
=> !left.Equals(right);
|
||||
|
||||
public static implicit operator string(DictionaryCode code)
|
||||
=> code.Value;
|
||||
|
||||
public static implicit operator DictionaryCode(string value)
|
||||
=> new(value);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Dictionary.ValueObjects;
|
||||
|
||||
/// <summary>
|
||||
/// 多语言字典值对象,封装语言键值映射。
|
||||
/// </summary>
|
||||
public sealed class I18nValue : IEquatable<I18nValue>
|
||||
{
|
||||
private static readonly string[] FallbackLocales = ["zh-CN", "en"];
|
||||
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
private readonly Dictionary<string, string> _values;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化多语言值。
|
||||
/// </summary>
|
||||
/// <param name="values">语言键值映射。</param>
|
||||
/// <exception cref="ArgumentException">传入值为空或无有效条目时抛出。</exception>
|
||||
public I18nValue(IDictionary<string, string> values)
|
||||
{
|
||||
if (values == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(values));
|
||||
}
|
||||
|
||||
_values = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var (key, value) in values)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
_values[key.Trim()] = value.Trim();
|
||||
}
|
||||
|
||||
if (_values.Count == 0)
|
||||
{
|
||||
throw new ArgumentException("I18nValue requires at least one non-empty entry.", nameof(values));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 语言键值只读视图。
|
||||
/// </summary>
|
||||
public IReadOnlyDictionary<string, string> Values => _values;
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定语言值,支持回退策略。
|
||||
/// </summary>
|
||||
/// <param name="locale">语言标识。</param>
|
||||
/// <returns>匹配语言值。</returns>
|
||||
public string Extract(string? locale)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(locale) && _values.TryGetValue(locale, out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
foreach (var fallback in FallbackLocales)
|
||||
{
|
||||
if (_values.TryGetValue(fallback, out var fallbackValue))
|
||||
{
|
||||
return fallbackValue;
|
||||
}
|
||||
}
|
||||
|
||||
return _values.Values.First();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 转换为普通字典。
|
||||
/// </summary>
|
||||
public Dictionary<string, string> ToDictionary()
|
||||
=> new(_values, StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
/// <summary>
|
||||
/// 转换为 JSON 字符串。
|
||||
/// </summary>
|
||||
public string ToJson()
|
||||
=> JsonSerializer.Serialize(_values, JsonOptions);
|
||||
|
||||
/// <summary>
|
||||
/// 从 JSON 字符串解析多语言值。
|
||||
/// </summary>
|
||||
public static I18nValue FromJson(string json)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(json))
|
||||
{
|
||||
throw new ArgumentException("JSON payload is required.", nameof(json));
|
||||
}
|
||||
|
||||
var values = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOptions) ?? new Dictionary<string, string>();
|
||||
return new I18nValue(values);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(I18nValue? other)
|
||||
{
|
||||
if (other == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ReferenceEquals(this, other))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_values.Count != other._values.Count)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var (key, value) in _values)
|
||||
{
|
||||
if (!other._values.TryGetValue(key, out var otherValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!string.Equals(value, otherValue, StringComparison.Ordinal))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj)
|
||||
=> obj is I18nValue other && Equals(other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hash = new HashCode();
|
||||
foreach (var pair in _values.OrderBy(item => item.Key, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
hash.Add(pair.Key, StringComparer.OrdinalIgnoreCase);
|
||||
hash.Add(pair.Value, StringComparer.Ordinal);
|
||||
}
|
||||
|
||||
return hash.ToHashCode();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user