导入行成功后写入 batch job 的 orderId/serviceNo;预报失败时尽量关联草稿订单。Excel 读取与行映射支持表头归一化,兼容带括号说明的列名。 Co-authored-by: Cursor <cursoragent@cursor.com>
148 lines
5.2 KiB
C#
148 lines
5.2 KiB
C#
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);
|
||
}
|