All checks were successful
Build and Deploy / deploy (push) Successful in 1m45s
新增 downstream 重量导入并锁定渠道重算应付;重算应收清空全部阶段;费用软删与查询过滤,避免预报入库叠算。 Co-authored-by: Cursor <cursoragent@cursor.com>
252 lines
7.3 KiB
C#
252 lines
7.3 KiB
C#
using System.Globalization;
|
||
using MiniExcelLibs;
|
||
using Oms.Application.Dtos.Orders;
|
||
|
||
namespace Oms.Application.Shared;
|
||
|
||
/// <summary>
|
||
/// 对账件重尺(downstream)Excel 模板与解析。
|
||
/// 仅写入下游对账重量,触发应付重算;不改应收。
|
||
/// </summary>
|
||
public static class OrderDownstreamWeightImportHelper
|
||
{
|
||
public const int MaxRows = 2000;
|
||
|
||
private static readonly Dictionary<string, string> 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<byte[]> BuildTemplateBytesAsync()
|
||
{
|
||
var rows = new List<Dictionary<string, object?>>
|
||
{
|
||
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<List<(int RowNumber, OrderDownstreamWeightImportRow Row)>> 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<string, object> 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<string, object> 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<string, object> rowDict)
|
||
{
|
||
var mapped = new Dictionary<string, string?>(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<string, object> rowDict) =>
|
||
rowDict.Values.All(value => string.IsNullOrWhiteSpace(NormalizeCell(value)));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对账件重尺导入行。
|
||
/// </summary>
|
||
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; }
|
||
|
||
/// <summary>
|
||
/// Excel 单元格解析错误(校验阶段直接抛出)。
|
||
/// </summary>
|
||
public string? ParseError { get; set; }
|
||
}
|