admin/src/Oms.Application/Shared/OrderDownstreamWeightImportHelper.cs
zzh 87abcecb65
All checks were successful
Build and Deploy / deploy (push) Successful in 1m45s
feat: 对账件重尺导入与应收只留一套
新增 downstream 重量导入并锁定渠道重算应付;重算应收清空全部阶段;费用软删与查询过滤,避免预报入库叠算。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-12 16:57:28 +08:00

252 lines
7.3 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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