feat: 分区明细导入模板去掉国家名称,并增加枚举值 Sheet
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s

导入模板只保留国家代码;Sheet2 列出匹配维度/方式的编码与名称,导入时按代码补国家名称。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-06 10:58:23 +08:00
parent 5bf81b5138
commit 04dc9ca1ff
2 changed files with 110 additions and 6 deletions

View File

@ -11,6 +11,9 @@ namespace Oms.Application.Shared;
/// </summary>
public static class KpiAreaInfoImportHelper
{
public const string TemplateSheetName = "导入模板";
public const string EnumSheetName = "枚举值";
private static readonly Regex ChinesePattern = new(@"[\u4E00-\u9FA5]", RegexOptions.Compiled);
private static readonly Dictionary<string, string> HeaderAliases = new(StringComparer.OrdinalIgnoreCase)
@ -48,12 +51,11 @@ public static class KpiAreaInfoImportHelper
public static async Task<byte[]> BuildTemplateBytesAsync()
{
var rows = new List<Dictionary<string, object?>>
var templateRows = new List<Dictionary<string, object?>>
{
new()
{
["国家代码"] = "US",
["国家名称"] = "美国",
["匹配维度"] = "zip",
["匹配方式"] = "range",
["州/省"] = "",
@ -64,7 +66,6 @@ public static class KpiAreaInfoImportHelper
new()
{
["国家代码"] = "US",
["国家名称"] = "美国",
["匹配维度"] = "zip",
["匹配方式"] = "prefix",
["州/省"] = "",
@ -75,7 +76,6 @@ public static class KpiAreaInfoImportHelper
new()
{
["国家代码"] = "US",
["国家名称"] = "美国",
["匹配维度"] = "state",
["匹配方式"] = "exact",
["州/省"] = "CA",
@ -85,11 +85,44 @@ public static class KpiAreaInfoImportHelper
}
};
var enumRows = new List<Dictionary<string, object?>>
{
new() { ["枚举类型"] = "匹配维度", ["编码"] = "country", ["名称"] = "仅国家" },
new() { ["枚举类型"] = "匹配维度", ["编码"] = "zip", ["名称"] = "邮编" },
new() { ["枚举类型"] = "匹配维度", ["编码"] = "state", ["名称"] = "州/省" },
new() { ["枚举类型"] = "匹配维度", ["编码"] = "city", ["名称"] = "城市" },
new() { ["枚举类型"] = "匹配方式", ["编码"] = "prefix", ["名称"] = "前缀" },
new() { ["枚举类型"] = "匹配方式", ["编码"] = "exact", ["名称"] = "精确" },
new() { ["枚举类型"] = "匹配方式", ["编码"] = "range", ["名称"] = "区间" },
new() { ["枚举类型"] = "匹配方式", ["编码"] = "all", ["名称"] = "不限" }
};
var sheets = new Dictionary<string, object>
{
[TemplateSheetName] = templateRows,
[EnumSheetName] = enumRows
};
await using var stream = new MemoryStream();
await MiniExcel.SaveAsAsync(stream, rows);
await MiniExcel.SaveAsAsync(stream, sheets);
return stream.ToArray();
}
internal static string? ResolveImportSheetName(IEnumerable<string> sheetNames)
{
var names = sheetNames.Where(x => !string.IsNullOrWhiteSpace(x)).ToList();
if (names.Count == 0)
{
return null;
}
return names.FirstOrDefault(x =>
string.Equals(x, TemplateSheetName, StringComparison.OrdinalIgnoreCase))
?? names.FirstOrDefault(x =>
!string.Equals(x, EnumSheetName, StringComparison.OrdinalIgnoreCase))
?? names[0];
}
public static string ResolveTemplateFileName(string? areaType) =>
areaType?.Trim().ToLowerInvariant() switch
{
@ -101,7 +134,20 @@ public static class KpiAreaInfoImportHelper
public static async Task<List<(int RowNumber, KpiAreaInfo Entity)>> ParseImportRowsAsync(Stream fileStream)
{
var rows = await MiniExcel.QueryAsync(fileStream, useHeaderRow: true);
if (fileStream.CanSeek)
{
fileStream.Position = 0;
}
var sheetNames = MiniExcel.GetSheetNames(fileStream);
var sheetName = ResolveImportSheetName(sheetNames);
if (fileStream.CanSeek)
{
fileStream.Position = 0;
}
var rows = await MiniExcel.QueryAsync(fileStream, useHeaderRow: true, sheetName: sheetName);
var result = new List<(int RowNumber, KpiAreaInfo Entity)>();
var rowNumber = 1;

View File

@ -1,4 +1,5 @@
using FluentAssertions;
using MiniExcelLibs;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.SystemAggregate.Entities;
@ -7,6 +8,63 @@ namespace Oms.Tests;
public class KpiAreaInfoImportHelperTests
{
[Fact]
public async Task BuildTemplateBytesAsync_ShouldUseNamedSheetsWithoutCountryName()
{
var bytes = await KpiAreaInfoImportHelper.BuildTemplateBytesAsync();
await using var stream = new MemoryStream(bytes);
var sheetNames = MiniExcel.GetSheetNames(stream);
sheetNames.Should().Equal(
KpiAreaInfoImportHelper.TemplateSheetName,
KpiAreaInfoImportHelper.EnumSheetName);
stream.Position = 0;
var templateRows = (await MiniExcel.QueryAsync(
stream,
useHeaderRow: true,
sheetName: KpiAreaInfoImportHelper.TemplateSheetName))
.Cast<IDictionary<string, object>>()
.ToList();
templateRows.Should().NotBeEmpty();
templateRows[0].Keys.Should().NotContain(key => key.Contains("国家名称"));
templateRows[0].Should().ContainKey("国家代码");
templateRows[0].Should().ContainKey("匹配维度");
templateRows[0].Should().ContainKey("匹配方式");
stream.Position = 0;
var enumRows = (await MiniExcel.QueryAsync(
stream,
useHeaderRow: true,
sheetName: KpiAreaInfoImportHelper.EnumSheetName))
.Cast<IDictionary<string, object>>()
.ToList();
enumRows.Should().HaveCount(8);
enumRows.Should().Contain(row =>
Convert.ToString(row["枚举类型"]) == "匹配维度"
&& Convert.ToString(row["编码"]) == "zip"
&& Convert.ToString(row["名称"]) == "邮编");
enumRows.Should().Contain(row =>
Convert.ToString(row["枚举类型"]) == "匹配方式"
&& Convert.ToString(row["编码"]) == "range"
&& Convert.ToString(row["名称"]) == "区间");
}
[Fact]
public async Task ParseImportRowsAsync_ShouldReadTemplateSheetOnly()
{
var bytes = await KpiAreaInfoImportHelper.BuildTemplateBytesAsync();
await using var stream = new MemoryStream(bytes);
var rows = await KpiAreaInfoImportHelper.ParseImportRowsAsync(stream);
rows.Should().HaveCount(3);
rows[0].Entity.CountryCode.Should().Be("US");
rows[0].Entity.CountryName.Should().BeNull();
rows[0].Entity.MatchDim.Should().Be("zip");
rows[0].Entity.MatchMode.Should().Be("range");
}
[Fact]
public void PrepareEntityForImport_ValidRow_ShouldFillCountryName()
{