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; } }