admin/src/Oms.Application/Shared/CountryImportHelper.cs
zzh 8f64bbf02d
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
feat: 国家地区支持增删改与导入,列表返回创建和更新时间
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-16 15:54:22 +08:00

180 lines
5.9 KiB
C#

using System.Text.RegularExpressions;
using MiniExcelLibs;
namespace Oms.Application.Shared;
/// <summary>
/// 国家/地区 Excel 模板与导入解析。
/// </summary>
public static class CountryImportHelper
{
public const string TemplateSheetName = "导入模板";
public const string TemplateFileName = "国家地区导入模板.xlsx";
private static readonly Regex CountryCodePattern = new("^[A-Za-z]{2}$", RegexOptions.Compiled);
private static readonly Dictionary<string, string> HeaderAliases = new(StringComparer.OrdinalIgnoreCase)
{
["国家二字码"] = nameof(CountryImportRow.CountryCode),
["二字码"] = nameof(CountryImportRow.CountryCode),
["国家代码"] = nameof(CountryImportRow.CountryCode),
["CountryCode"] = nameof(CountryImportRow.CountryCode),
["countryCode"] = nameof(CountryImportRow.CountryCode),
["中文名"] = nameof(CountryImportRow.ChineseName),
["国家中文名"] = nameof(CountryImportRow.ChineseName),
["ChineseName"] = nameof(CountryImportRow.ChineseName),
["chineseName"] = nameof(CountryImportRow.ChineseName),
["英文名"] = nameof(CountryImportRow.EnglishName),
["国家英文名"] = nameof(CountryImportRow.EnglishName),
["EnglishName"] = nameof(CountryImportRow.EnglishName),
["englishName"] = nameof(CountryImportRow.EnglishName),
};
public static async Task<byte[]> BuildTemplateBytesAsync()
{
var templateRows = new List<Dictionary<string, object?>>
{
new()
{
["国家二字码"] = "CN",
["中文名"] = "中国",
["英文名"] = "China"
},
new()
{
["国家二字码"] = "US",
["中文名"] = "美国",
["英文名"] = "United States"
}
};
var sheets = new Dictionary<string, object>
{
[TemplateSheetName] = templateRows
};
await using var stream = new MemoryStream();
await MiniExcel.SaveAsAsync(stream, sheets);
return stream.ToArray();
}
public static async Task<List<(int RowNumber, CountryImportRow Row)>> ParseImportRowsAsync(Stream fileStream)
{
if (fileStream.CanSeek)
{
fileStream.Position = 0;
}
var sheetNames = MiniExcel.GetSheetNames(fileStream);
var sheetName = sheetNames.FirstOrDefault(x =>
string.Equals(x, TemplateSheetName, StringComparison.OrdinalIgnoreCase))
?? sheetNames.FirstOrDefault();
if (fileStream.CanSeek)
{
fileStream.Position = 0;
}
var rows = await MiniExcel.QueryAsync(fileStream, useHeaderRow: true, sheetName: sheetName);
var result = new List<(int RowNumber, CountryImportRow Row)>();
var rowNumber = 1;
foreach (var row in rows)
{
rowNumber++;
if (row is not IDictionary<string, object> rowDict || IsEmptyRow(rowDict))
{
continue;
}
result.Add((rowNumber, ParseRow(rowDict)));
}
return result;
}
public static void Normalize(CountryImportRow row)
{
row.CountryCode = row.CountryCode?.Trim().ToUpperInvariant() ?? string.Empty;
row.ChineseName = row.ChineseName?.Trim() ?? string.Empty;
row.EnglishName = row.EnglishName?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(row.CountryCode))
{
throw new InvalidOperationException("国家二字码不能为空");
}
if (!CountryCodePattern.IsMatch(row.CountryCode))
{
throw new InvalidOperationException("国家二字码须为 2 位字母,如 CN、US");
}
if (string.IsNullOrWhiteSpace(row.ChineseName))
{
throw new InvalidOperationException("中文名不能为空");
}
if (row.ChineseName.Length > 128)
{
throw new InvalidOperationException("中文名不可超过 128 个字符");
}
if (string.IsNullOrWhiteSpace(row.EnglishName))
{
throw new InvalidOperationException("英文名不能为空");
}
if (row.EnglishName.Length > 128)
{
throw new InvalidOperationException("英文名不可超过 128 个字符");
}
}
private static CountryImportRow ParseRow(IDictionary<string, object> rowDict)
{
var mapped = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
foreach (var (header, value) in rowDict)
{
if (string.IsNullOrWhiteSpace(header) || !HeaderAliases.TryGetValue(header.Trim(), out var field))
{
continue;
}
mapped[field] = NormalizeCell(value);
}
return new CountryImportRow
{
CountryCode = mapped.GetValueOrDefault(nameof(CountryImportRow.CountryCode)) ?? string.Empty,
ChineseName = mapped.GetValueOrDefault(nameof(CountryImportRow.ChineseName)) ?? string.Empty,
EnglishName = mapped.GetValueOrDefault(nameof(CountryImportRow.EnglishName)) ?? string.Empty
};
}
private static string? NormalizeCell(object? value)
{
if (value is null)
{
return null;
}
var text = Convert.ToString(value, System.Globalization.CultureInfo.InvariantCulture)?.Trim();
return string.IsNullOrWhiteSpace(text) ? null : text;
}
private static bool IsEmptyRow(IDictionary<string, object> rowDict) =>
rowDict.Values.All(value => string.IsNullOrWhiteSpace(NormalizeCell(value)));
}
/// <summary>
/// 国家导入行。
/// </summary>
public class CountryImportRow
{
public string CountryCode { get; set; } = string.Empty;
public string ChineseName { get; set; } = string.Empty;
public string EnglishName { get; set; } = string.Empty;
}