admin/src/Oms.Application/Services/Orders/Templates/OrderTemplateHeaderMatcher.cs
zzh 7d5bc2408f fix(order): 批量导入回写订单 ID 并增强 Excel 表头映射匹配
导入行成功后写入 batch job 的 orderId/serviceNo;预报失败时尽量关联草稿订单。Excel 读取与行映射支持表头归一化,兼容带括号说明的列名。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-08 18:34:59 +08:00

148 lines
5.2 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text.RegularExpressions;
using Oms.Application.Services.Orders.Templates;
namespace Oms.Application.Services.Orders.Templates;
/// <summary>
/// 客户 Excel 表头与标准字段智能匹配。
/// </summary>
public static class OrderTemplateHeaderMatcher
{
private static readonly Regex NoisePattern = new(@"[\s\*\(\)()\[\]【】::,,.。/\\\-_]+", RegexOptions.Compiled);
private static readonly Dictionary<string, string> Aliases = new(StringComparer.OrdinalIgnoreCase)
{
["客户单号"] = "clientNo",
["客户订单号"] = "clientNo",
["外部单号"] = "clientNo",
["跟踪号"] = "trackNo",
["运单号"] = "serviceNo",
["运输方式"] = "productCode",
["产品代码"] = "productCode",
["产品编码"] = "productCode",
["货物属性"] = "cargoAttribute",
["出口报关方式"] = "exportCustomsMode",
["仓库"] = "warehouseId",
["仓库ID"] = "warehouseId",
["长cm"] = "length",
["长"] = "length",
["宽cm"] = "width",
["宽"] = "width",
["高cm"] = "height",
["高"] = "height",
["实重kg"] = "actualWeight",
["实重"] = "actualWeight",
["申报币种"] = "declaredCurrency",
["包裹数量"] = "quantity",
["收件人名称"] = "receiver.recipientsName",
["收件姓名"] = "receiver.recipientsName",
["收件人手机"] = "receiver.phone",
["收件国家代码"] = "receiver.countryCode",
["收件国家"] = "receiver.countryCode",
["收件地址1"] = "receiver.streetAddress1",
["收件邮编"] = "receiver.zipCode",
["发件人姓名"] = "sender.senderName",
["申报品名英"] = "items[0].enName",
["品名英"] = "items[0].enName",
["申报品名中"] = "items[0].cnName",
["单价"] = "items[0].unitPrice",
["单重"] = "items[0].unitWeight",
["申报数量"] = "items[0].quantity",
};
public static string? MatchStandardFieldKey(string? sourceHeader, string? templateKey)
{
var normalized = Normalize(sourceHeader);
if (string.IsNullOrWhiteSpace(normalized))
{
return null;
}
if (Aliases.TryGetValue(normalized, out var aliasKey))
{
return aliasKey;
}
var fields = OrderTemplateFieldRegistry.GetFields(templateKey);
var byLabel = fields.FirstOrDefault(f => Normalize(f.Label) == normalized);
if (byLabel != null)
{
return byLabel.FieldKey;
}
return fields.FirstOrDefault(f =>
normalized.Contains(Normalize(f.Label), StringComparison.OrdinalIgnoreCase) ||
Normalize(f.Label).Contains(normalized, StringComparison.OrdinalIgnoreCase))?.FieldKey;
}
public static OrderTemplateColumnMappingDocument BuildSuggestedMapping(
string standardTemplateKey,
IReadOnlyList<string> sourceHeaders)
{
var fields = OrderTemplateFieldRegistry.GetFields(standardTemplateKey);
var usedColumns = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var mappings = new List<OrderTemplateColumnMappingItem>();
foreach (var field in fields)
{
var matched = sourceHeaders.FirstOrDefault(h =>
{
var key = MatchStandardFieldKey(h, standardTemplateKey);
return string.Equals(key, field.FieldKey, StringComparison.OrdinalIgnoreCase);
});
if (!string.IsNullOrWhiteSpace(matched) && usedColumns.Add(matched))
{
mappings.Add(new OrderTemplateColumnMappingItem
{
StandardFieldKey = field.FieldKey,
SourceColumn = matched,
Required = field.Required
});
}
}
var mappedColumns = mappings.Select(x => x.SourceColumn).ToHashSet(StringComparer.OrdinalIgnoreCase);
return new OrderTemplateColumnMappingDocument
{
StandardTemplateKey = standardTemplateKey,
HeaderRowIndex = 1,
DataStartRowIndex = 2,
Mappings = mappings,
UnmappedSourceColumns = sourceHeaders.Where(h => !mappedColumns.Contains(h)).ToList()
};
}
public static List<string> ReadHeaderRow(Stream stream)
{
stream.Position = 0;
var firstRow = MiniExcelLibs.MiniExcel.Query(stream, useHeaderRow: false)
.Take(1)
.Cast<IDictionary<string, object>>()
.FirstOrDefault();
if (firstRow == null)
{
return [];
}
return firstRow.Values
.Select(v => v?.ToString()?.Trim())
.Where(v => !string.IsNullOrWhiteSpace(v))
.Select(v => v!)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
public static string NormalizeHeader(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return string.Empty;
}
return NoisePattern.Replace(text.Trim(), string.Empty);
}
private static string Normalize(string? text) => NormalizeHeader(text);
}