feat: 实现字典管理后端

This commit is contained in:
2025-12-30 19:38:13 +08:00
parent a427b0f22a
commit dc9f6136d6
83 changed files with 6901 additions and 50 deletions

View File

@@ -0,0 +1,91 @@
using CsvHelper;
using CsvHelper.Configuration;
using System.Globalization;
using System.Text;
using TakeoutSaaS.Application.Dictionary.Abstractions;
using TakeoutSaaS.Application.Dictionary.Models;
namespace TakeoutSaaS.Infrastructure.Dictionary.ImportExport;
/// <summary>
/// CSV 字典导入解析器。
/// </summary>
public sealed class CsvDictionaryParser : ICsvDictionaryParser
{
private static readonly CsvConfiguration CsvConfiguration = new(CultureInfo.InvariantCulture)
{
HasHeaderRecord = true,
MissingFieldFound = null,
BadDataFound = null,
DetectColumnCountChanges = false,
TrimOptions = TrimOptions.Trim,
PrepareHeaderForMatch = args => args.Header?.Trim().ToLowerInvariant() ?? string.Empty
};
/// <inheritdoc />
public async Task<IReadOnlyList<DictionaryImportRow>> ParseAsync(Stream stream, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stream);
if (stream.CanSeek)
{
stream.Position = 0;
}
var rows = new List<DictionaryImportRow>();
using var reader = new StreamReader(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false), detectEncodingFromByteOrderMarks: true, leaveOpen: true);
using var csv = new CsvReader(reader, CsvConfiguration);
if (!await csv.ReadAsync() || !csv.ReadHeader())
{
return rows;
}
while (await csv.ReadAsync())
{
cancellationToken.ThrowIfCancellationRequested();
var rowNumber = csv.Context?.Parser?.Row ?? 0;
rows.Add(new DictionaryImportRow
{
RowNumber = rowNumber,
Code = ReadString(csv, "code"),
Key = ReadString(csv, "key"),
Value = ReadString(csv, "value"),
SortOrder = ReadInt(csv, "sortorder"),
IsEnabled = ReadBool(csv, "isenabled"),
Description = ReadString(csv, "description"),
Source = ReadString(csv, "source")
});
}
return rows;
}
private static string? ReadString(CsvReader csv, string name)
{
return csv.TryGetField(name, out string? value)
? string.IsNullOrWhiteSpace(value) ? null : value
: null;
}
private static int? ReadInt(CsvReader csv, string name)
{
if (csv.TryGetField(name, out string? value) && int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var number))
{
return number;
}
return null;
}
private static bool? ReadBool(CsvReader csv, string name)
{
if (csv.TryGetField(name, out string? value) && bool.TryParse(value, out var flag))
{
return flag;
}
return null;
}
}

View File

@@ -0,0 +1,131 @@
using System.Text.Json;
using TakeoutSaaS.Application.Dictionary.Abstractions;
using TakeoutSaaS.Application.Dictionary.Models;
namespace TakeoutSaaS.Infrastructure.Dictionary.ImportExport;
/// <summary>
/// JSON 字典导入解析器。
/// </summary>
public sealed class JsonDictionaryParser : IJsonDictionaryParser
{
private static readonly JsonDocumentOptions DocumentOptions = new()
{
AllowTrailingCommas = true
};
/// <inheritdoc />
public async Task<IReadOnlyList<DictionaryImportRow>> ParseAsync(Stream stream, CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(stream);
if (stream.CanSeek)
{
stream.Position = 0;
}
using var document = await JsonDocument.ParseAsync(stream, DocumentOptions, cancellationToken);
if (document.RootElement.ValueKind != JsonValueKind.Array)
{
return Array.Empty<DictionaryImportRow>();
}
var rows = new List<DictionaryImportRow>();
var index = 0;
foreach (var element in document.RootElement.EnumerateArray())
{
cancellationToken.ThrowIfCancellationRequested();
index++;
rows.Add(new DictionaryImportRow
{
RowNumber = index,
Code = ReadString(element, "code"),
Key = ReadString(element, "key"),
Value = ReadValue(element, "value"),
SortOrder = ReadInt(element, "sortOrder"),
IsEnabled = ReadBool(element, "isEnabled"),
Description = ReadString(element, "description"),
Source = ReadString(element, "source")
});
}
return rows;
}
private static string? ReadString(JsonElement element, string propertyName)
{
if (!TryGetProperty(element, propertyName, out var value) || value.ValueKind == JsonValueKind.Null)
{
return null;
}
return value.ValueKind == JsonValueKind.String ? value.GetString() : value.GetRawText();
}
private static string? ReadValue(JsonElement element, string propertyName)
{
if (!TryGetProperty(element, propertyName, out var value) || value.ValueKind == JsonValueKind.Null)
{
return null;
}
return value.ValueKind == JsonValueKind.String ? value.GetString() : value.GetRawText();
}
private static int? ReadInt(JsonElement element, string propertyName)
{
if (!TryGetProperty(element, propertyName, out var value))
{
return null;
}
if (value.ValueKind == JsonValueKind.Number && value.TryGetInt32(out var number))
{
return number;
}
if (value.ValueKind == JsonValueKind.String && int.TryParse(value.GetString(), out var parsed))
{
return parsed;
}
return null;
}
private static bool? ReadBool(JsonElement element, string propertyName)
{
if (!TryGetProperty(element, propertyName, out var value))
{
return null;
}
if (value.ValueKind == JsonValueKind.True || value.ValueKind == JsonValueKind.False)
{
return value.GetBoolean();
}
if (value.ValueKind == JsonValueKind.String && bool.TryParse(value.GetString(), out var parsed))
{
return parsed;
}
return null;
}
private static bool TryGetProperty(JsonElement element, string propertyName, out JsonElement value)
{
foreach (var property in element.EnumerateObject())
{
if (string.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase))
{
value = property.Value;
return true;
}
}
value = default;
return false;
}
}