using System.Text.RegularExpressions;
using Oms.Application.Services.Orders.Templates;
namespace Oms.Application.Services.Orders.Templates;
///
/// 客户 Excel 表头与标准字段智能匹配。
///
public static class OrderTemplateHeaderMatcher
{
private static readonly Regex NoisePattern = new(@"[\s\*\(\)()\[\]【】::,,.。/\\\-_]+", RegexOptions.Compiled);
private static readonly Dictionary Aliases = new(StringComparer.OrdinalIgnoreCase)
{
["客户单号"] = "clientNo",
["客户订单号"] = "clientNo",
["外部单号"] = "clientNo",
["运输方式"] = "productCode",
["产品代码"] = "productCode",
["产品编码"] = "productCode",
["货物属性"] = "cargoAttribute",
["出口报关方式"] = "exportCustomsMode",
["出口报关"] = "exportCustomsMode",
["出口资质备案Id"] = "exportQualificationId",
["出口资质备案"] = "exportQualificationId",
["资质备案Id"] = "exportQualificationId",
["仓库"] = "warehouseId",
["仓库ID"] = "warehouseId",
["长cm"] = "length",
["长"] = "length",
["宽cm"] = "width",
["宽"] = "width",
["高cm"] = "height",
["高"] = "height",
["实重kg"] = "actualWeight",
["实重"] = "actualWeight",
["申报币种"] = "declaredCurrency",
["申报金额"] = "declaredSum",
["申报总金额"] = "declaredSum",
["包裹数量"] = "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].unitPrice",
["单重"] = "items[0].unitWeight",
["单重kg"] = "items[0].unitWeight",
["申报数量"] = "items[0].quantity",
["总金额"] = "items[0].amount",
["总金额元"] = "items[0].amount",
["总重量"] = "items[0].weight",
["总重量kg"] = "items[0].weight",
};
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 sourceHeaders)
{
var fields = OrderTemplateFieldRegistry.GetFields(standardTemplateKey);
var usedColumns = new HashSet(StringComparer.OrdinalIgnoreCase);
var mappings = new List();
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 ReadHeaderRow(Stream stream)
{
stream.Position = 0;
var firstRow = MiniExcelLibs.MiniExcel.Query(stream, useHeaderRow: false)
.Take(1)
.Cast>()
.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);
}