using System.Globalization;
using MiniExcelLibs;
using Oms.Application.Dtos.Orders;
namespace Oms.Application.Shared;
///
/// 对账件重尺(downstream)Excel 模板与解析。
/// 仅写入下游对账重量,触发应付重算;不改应收。
///
public static class OrderDownstreamWeightImportHelper
{
public const int MaxRows = 2000;
private static readonly Dictionary HeaderAliases = new(StringComparer.OrdinalIgnoreCase)
{
["运单号"] = "WaybillNo",
["单号"] = "WaybillNo",
["服务商单号"] = "WaybillNo",
["WaybillNo"] = "WaybillNo",
["waybillNo"] = "WaybillNo",
["ServiceNo"] = "WaybillNo",
["serviceNo"] = "WaybillNo",
["长"] = "Length",
["长度"] = "Length",
["Length"] = "Length",
["length"] = "Length",
["宽"] = "Width",
["宽度"] = "Width",
["Width"] = "Width",
["width"] = "Width",
["高"] = "Height",
["高度"] = "Height",
["Height"] = "Height",
["height"] = "Height",
["实重"] = "ActualWeight",
["重量"] = "ActualWeight",
["ActualWeight"] = "ActualWeight",
["actualWeight"] = "ActualWeight",
["Weight"] = "ActualWeight",
["weight"] = "ActualWeight",
};
public static async Task BuildTemplateBytesAsync()
{
var rows = new List>
{
new()
{
["运单号"] = "OMS202601010001",
["长"] = 30,
["宽"] = 20,
["高"] = 10,
["实重"] = 1.25
}
};
await using var stream = new MemoryStream();
await MiniExcel.SaveAsAsync(stream, rows);
return stream.ToArray();
}
public static string TemplateFileName => "对账件重尺导入模板.xlsx";
public static async Task> ParseImportRowsAsync(
Stream fileStream)
{
var rows = await MiniExcel.QueryAsync(fileStream, useHeaderRow: true);
var result = new List<(int RowNumber, OrderDownstreamWeightImportRow Row)>();
var rowNumber = 1;
foreach (var row in rows)
{
rowNumber++;
if (row is not IDictionary rowDict)
{
continue;
}
if (IsEmptyRow(rowDict))
{
continue;
}
try
{
result.Add((rowNumber, ParseRow(rowDict)));
}
catch (InvalidOperationException ex)
{
result.Add((rowNumber, new OrderDownstreamWeightImportRow
{
WaybillNo = TryPeekWaybillNo(rowDict),
ParseError = ex.Message
}));
}
}
return result;
}
public static void ValidateRow(OrderDownstreamWeightImportRow row)
{
if (!string.IsNullOrWhiteSpace(row.ParseError))
{
throw new InvalidOperationException(row.ParseError);
}
if (string.IsNullOrWhiteSpace(row.WaybillNo))
{
throw new InvalidOperationException("运单号不能为空");
}
row.WaybillNo = row.WaybillNo.Trim();
if (row.WaybillNo.Length > 64)
{
throw new InvalidOperationException("运单号不可超过64个字符");
}
if (!row.ActualWeight.HasValue || row.ActualWeight.Value <= 0)
{
throw new InvalidOperationException("实重必须大于0");
}
ValidateOptionalDimension(row.Length, "长");
ValidateOptionalDimension(row.Width, "宽");
ValidateOptionalDimension(row.Height, "高");
var hasAnyDim = row.Length.HasValue || row.Width.HasValue || row.Height.HasValue;
var hasAllDim = row.Length.HasValue && row.Width.HasValue && row.Height.HasValue;
if (hasAnyDim && !hasAllDim)
{
throw new InvalidOperationException("长/宽/高需同时填写或全部留空(留空则沿用订单已有尺寸)");
}
}
private static string? TryPeekWaybillNo(IDictionary rowDict)
{
foreach (var (header, value) in rowDict)
{
if (string.IsNullOrWhiteSpace(header))
{
continue;
}
if (HeaderAliases.TryGetValue(header.Trim(), out var field)
&& string.Equals(field, "WaybillNo", StringComparison.OrdinalIgnoreCase))
{
return NormalizeCell(value);
}
}
return null;
}
private static void ValidateOptionalDimension(decimal? value, string label)
{
if (!value.HasValue)
{
return;
}
if (value.Value <= 0)
{
throw new InvalidOperationException($"{label}必须大于0");
}
}
private static OrderDownstreamWeightImportRow ParseRow(IDictionary rowDict)
{
var mapped = new Dictionary(StringComparer.OrdinalIgnoreCase);
foreach (var (header, value) in rowDict)
{
if (string.IsNullOrWhiteSpace(header))
{
continue;
}
if (!HeaderAliases.TryGetValue(header.Trim(), out var field))
{
continue;
}
mapped[field] = NormalizeCell(value);
}
return new OrderDownstreamWeightImportRow
{
WaybillNo = mapped.GetValueOrDefault("WaybillNo"),
Length = ParseDecimal(mapped.GetValueOrDefault("Length")),
Width = ParseDecimal(mapped.GetValueOrDefault("Width")),
Height = ParseDecimal(mapped.GetValueOrDefault("Height")),
ActualWeight = ParseDecimal(mapped.GetValueOrDefault("ActualWeight"))
};
}
private static decimal? ParseDecimal(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return null;
}
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.InvariantCulture, out var value))
{
return value;
}
if (decimal.TryParse(text, NumberStyles.Any, CultureInfo.GetCultureInfo("zh-CN"), out value))
{
return value;
}
throw new InvalidOperationException($"数值格式无效:{text}");
}
private static string? NormalizeCell(object? value)
{
if (value is null)
{
return null;
}
var text = Convert.ToString(value, CultureInfo.InvariantCulture)?.Trim();
return string.IsNullOrWhiteSpace(text) ? null : text;
}
private static bool IsEmptyRow(IDictionary rowDict) =>
rowDict.Values.All(value => string.IsNullOrWhiteSpace(NormalizeCell(value)));
}
///
/// 对账件重尺导入行。
///
public sealed class OrderDownstreamWeightImportRow
{
public string? WaybillNo { get; set; }
public decimal? Length { get; set; }
public decimal? Width { get; set; }
public decimal? Height { get; set; }
public decimal? ActualWeight { get; set; }
///
/// Excel 单元格解析错误(校验阶段直接抛出)。
///
public string? ParseError { get; set; }
}