fix: 计重进位快照兼容与 KPI 分区明细 Excel 导入
- 发布规则快照反序列化合并 StartValue/EndValue 至 Start/End - 小包创建进位时快照区间无效则降级读 t_weight_rounding_info - 新增 KPI 分区明细导入模板下载与 Excel 批量导入接口 - 补充快照进位与导入相关单元测试 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
7827b4803e
commit
91ba6d5d59
19
src/Oms.Application/Dtos/Channels/KpiAreaInfoImportInput.cs
Normal file
19
src/Oms.Application/Dtos/Channels/KpiAreaInfoImportInput.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// KPI 分区明细 Excel 导入入参。
|
||||||
|
/// </summary>
|
||||||
|
public class KpiAreaInfoImportInput
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 分区主键。
|
||||||
|
/// </summary>
|
||||||
|
public int AreaId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Excel 文件。
|
||||||
|
/// </summary>
|
||||||
|
public IFormFile File { get; set; } = null!;
|
||||||
|
}
|
||||||
23
src/Oms.Application/Dtos/Channels/KpiAreaInfoImportOutput.cs
Normal file
23
src/Oms.Application/Dtos/Channels/KpiAreaInfoImportOutput.cs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// KPI 分区明细 Excel 导入结果。
|
||||||
|
/// </summary>
|
||||||
|
public class KpiAreaInfoImportOutput
|
||||||
|
{
|
||||||
|
public int SuccessCount { get; set; }
|
||||||
|
|
||||||
|
public int FailCount { get; set; }
|
||||||
|
|
||||||
|
public List<KpiAreaInfoImportFailRow> Failures { get; set; } = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导入失败行。
|
||||||
|
/// </summary>
|
||||||
|
public class KpiAreaInfoImportFailRow
|
||||||
|
{
|
||||||
|
public int RowNumber { get; set; }
|
||||||
|
|
||||||
|
public string Reason { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@ -55,6 +55,14 @@ public class WeightRoundingSnapshotRangeDto
|
|||||||
public decimal End { get; set; }
|
public decimal End { get; set; }
|
||||||
|
|
||||||
public string? Public { get; set; }
|
public string? Public { get; set; }
|
||||||
|
|
||||||
|
/// <summary>旧快照/明细 API 兼容(t_weight_rounding_info.StartValue);读取后由 <see cref="PublishSnapshotNormalizer"/> 合并至 <see cref="Start"/>。</summary>
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
public decimal StartValue { get; set; }
|
||||||
|
|
||||||
|
/// <summary>旧快照/明细 API 兼容(t_weight_rounding_info.EndValue);读取后由 <see cref="PublishSnapshotNormalizer"/> 合并至 <see cref="End"/>。</summary>
|
||||||
|
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
|
||||||
|
public decimal EndValue { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -1,11 +1,13 @@
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using Fake.Application;
|
using Fake.Application;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Oms.Application.Dtos.Channels;
|
using Oms.Application.Dtos.Channels;
|
||||||
using Oms.Application.Dtos.Shared;
|
using Oms.Application.Dtos.Shared;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
using Oms.Domain.SystemAggregate.Repositories;
|
||||||
|
|
||||||
namespace Oms.Application.Services.Channels;
|
namespace Oms.Application.Services.Channels;
|
||||||
|
|
||||||
@ -17,6 +19,8 @@ public class KpiAreaService : ApplicationService
|
|||||||
{
|
{
|
||||||
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
||||||
|
|
||||||
|
private ICountryRepository CountryRepository => LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询KPI分区列表。
|
/// 查询KPI分区列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -210,6 +214,69 @@ public class KpiAreaService : ApplicationService
|
|||||||
await KpiAreaRepository.DeleteInfoAsync(input.Id);
|
await KpiAreaRepository.DeleteInfoAsync(input.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下载 KPI 分区明细导入模板。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/channel/kpi-area/info/import-template")]
|
||||||
|
public async Task<FileContentResult> DownloadInfoImportTemplateAsync([FromQuery] string? areaType)
|
||||||
|
{
|
||||||
|
var bytes = await KpiAreaInfoImportHelper.BuildTemplateBytesAsync();
|
||||||
|
return new FileContentResult(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
|
||||||
|
{
|
||||||
|
FileDownloadName = KpiAreaInfoImportHelper.ResolveTemplateFileName(areaType)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 导入 KPI 分区明细(Excel)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/channel/kpi-area/info/import")]
|
||||||
|
public async Task<KpiAreaInfoImportOutput> ImportInfoAsync([FromForm] KpiAreaInfoImportInput input)
|
||||||
|
{
|
||||||
|
if (input.File == null || input.File.Length == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("请上传 Excel 文件");
|
||||||
|
}
|
||||||
|
|
||||||
|
await GetAreaOrThrowAsync(input.AreaId);
|
||||||
|
|
||||||
|
await using var stream = input.File.OpenReadStream();
|
||||||
|
var parsedRows = await KpiAreaInfoImportHelper.ParseImportRowsAsync(stream);
|
||||||
|
if (parsedRows.Count == 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException("Excel 中无有效数据行");
|
||||||
|
}
|
||||||
|
|
||||||
|
var countries = await CountryRepository.GetListAsync();
|
||||||
|
var countriesByCode = countries
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x.CountryCode))
|
||||||
|
.GroupBy(x => x.CountryCode.Trim(), StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToDictionary(g => g.Key, g => g.First(), StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
var output = new KpiAreaInfoImportOutput();
|
||||||
|
foreach (var (rowNumber, entity) in parsedRows)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
KpiAreaInfoImportHelper.PrepareEntityForImport(entity, countriesByCode);
|
||||||
|
entity.AreaId = input.AreaId;
|
||||||
|
await KpiAreaRepository.InsertInfoAsync(entity);
|
||||||
|
output.SuccessCount++;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
output.FailCount++;
|
||||||
|
output.Failures.Add(new KpiAreaInfoImportFailRow
|
||||||
|
{
|
||||||
|
RowNumber = rowNumber,
|
||||||
|
Reason = ex.Message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
private static Expression<Func<KpiArea, bool>> BuildAreaPredicate(KpiAreaListInput input)
|
private static Expression<Func<KpiArea, bool>> BuildAreaPredicate(KpiAreaListInput input)
|
||||||
{
|
{
|
||||||
var appKey = input.AppKey?.Trim();
|
var appKey = input.AppKey?.Trim();
|
||||||
|
|||||||
@ -597,10 +597,13 @@ public partial class OrderService
|
|||||||
throw new BusinessException("未配置计重进位");
|
throw new BusinessException("未配置计重进位");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var snapshotRanges = rulesSnapshot?.WeightRoundingInfos;
|
||||||
|
var fromSnapshot = snapshotRanges is { Count: > 0 }
|
||||||
|
&& snapshotRanges.Any(x => !string.IsNullOrWhiteSpace(x.Public));
|
||||||
IReadOnlyList<WeightRoundingInfo> ranges;
|
IReadOnlyList<WeightRoundingInfo> ranges;
|
||||||
if (rulesSnapshot?.WeightRoundingInfos is { Count: > 0 })
|
if (fromSnapshot)
|
||||||
{
|
{
|
||||||
ranges = RulesSnapshotHelper.ToWeightRoundingEntities(rulesSnapshot.WeightRoundingInfos);
|
ranges = RulesSnapshotHelper.ToWeightRoundingEntities(snapshotRanges!);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@ -613,8 +616,21 @@ public partial class OrderService
|
|||||||
ranges = await WeightRoundingRepository.GetAllInfosByRoundingIdAsync(weightRoundingId.Value);
|
ranges = await WeightRoundingRepository.GetAllInfosByRoundingIdAsync(weightRoundingId.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
forecast.ChargeableWeight = ParcelBillingCalculator.ApplyRounding(rawWeight, ranges);
|
forecast.ChargeableWeight = ParcelBillingCalculator.ApplyRounding(rawWeight, ranges);
|
||||||
}
|
}
|
||||||
|
catch (InvalidOperationException) when (fromSnapshot)
|
||||||
|
{
|
||||||
|
var dbRanges = await WeightRoundingRepository.GetAllInfosByRoundingIdAsync(weightRoundingId.Value);
|
||||||
|
if (dbRanges.Count == 0)
|
||||||
|
{
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
forecast.ChargeableWeight = ParcelBillingCalculator.ApplyRounding(rawWeight, dbRanges);
|
||||||
|
}
|
||||||
|
}
|
||||||
catch (BusinessException)
|
catch (BusinessException)
|
||||||
{
|
{
|
||||||
throw;
|
throw;
|
||||||
|
|||||||
230
src/Oms.Application/Shared/KpiAreaInfoImportHelper.cs
Normal file
230
src/Oms.Application/Shared/KpiAreaInfoImportHelper.cs
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using MiniExcelLibs;
|
||||||
|
using Oms.Application.Dtos.Channels;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
using Oms.Domain.SystemAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// KPI 分区明细 Excel 模板与导入解析。
|
||||||
|
/// </summary>
|
||||||
|
public static class KpiAreaInfoImportHelper
|
||||||
|
{
|
||||||
|
private static readonly Regex ChinesePattern = new(@"[\u4E00-\u9FA5]", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private static readonly Dictionary<string, string> HeaderAliases = new(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["国家代码"] = nameof(KpiAreaInfo.CountryCode),
|
||||||
|
["CountryCode"] = nameof(KpiAreaInfo.CountryCode),
|
||||||
|
["countryCode"] = nameof(KpiAreaInfo.CountryCode),
|
||||||
|
["国家名称"] = nameof(KpiAreaInfo.CountryName),
|
||||||
|
["CountryName"] = nameof(KpiAreaInfo.CountryName),
|
||||||
|
["countryName"] = nameof(KpiAreaInfo.CountryName),
|
||||||
|
["州/省"] = nameof(KpiAreaInfo.Province),
|
||||||
|
["州省"] = nameof(KpiAreaInfo.Province),
|
||||||
|
["Province"] = nameof(KpiAreaInfo.Province),
|
||||||
|
["province"] = nameof(KpiAreaInfo.Province),
|
||||||
|
["城市"] = nameof(KpiAreaInfo.City),
|
||||||
|
["City"] = nameof(KpiAreaInfo.City),
|
||||||
|
["city"] = nameof(KpiAreaInfo.City),
|
||||||
|
["起始邮编"] = nameof(KpiAreaInfo.LeftPostcode),
|
||||||
|
["LeftPostcode"] = nameof(KpiAreaInfo.LeftPostcode),
|
||||||
|
["leftPostcode"] = nameof(KpiAreaInfo.LeftPostcode),
|
||||||
|
["leftPostCode"] = nameof(KpiAreaInfo.LeftPostcode),
|
||||||
|
["截止邮编"] = nameof(KpiAreaInfo.RightPostcode),
|
||||||
|
["RightPostcode"] = nameof(KpiAreaInfo.RightPostcode),
|
||||||
|
["rightPostcode"] = nameof(KpiAreaInfo.RightPostcode),
|
||||||
|
["rightPostCode"] = nameof(KpiAreaInfo.RightPostcode),
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task<byte[]> BuildTemplateBytesAsync()
|
||||||
|
{
|
||||||
|
var rows = new List<Dictionary<string, object?>>
|
||||||
|
{
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
["国家代码"] = "US",
|
||||||
|
["国家名称"] = "美国",
|
||||||
|
["州/省"] = "CA",
|
||||||
|
["城市"] = "",
|
||||||
|
["起始邮编"] = "90000",
|
||||||
|
["截止邮编"] = "90199"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var stream = new MemoryStream();
|
||||||
|
await MiniExcel.SaveAsAsync(stream, rows);
|
||||||
|
return stream.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string ResolveTemplateFileName(string? areaType) =>
|
||||||
|
areaType?.Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"blacklist" => "黑名单分区明细导入模板.xlsx",
|
||||||
|
"remote" => "偏远分区明细导入模板.xlsx",
|
||||||
|
"price" => "价格分区明细导入模板.xlsx",
|
||||||
|
_ => "分区明细导入模板.xlsx"
|
||||||
|
};
|
||||||
|
|
||||||
|
public static async Task<List<(int RowNumber, KpiAreaInfo Entity)>> ParseImportRowsAsync(Stream fileStream)
|
||||||
|
{
|
||||||
|
var rows = await MiniExcel.QueryAsync(fileStream, useHeaderRow: true);
|
||||||
|
var result = new List<(int RowNumber, KpiAreaInfo Entity)>();
|
||||||
|
var rowNumber = 1;
|
||||||
|
|
||||||
|
foreach (var row in rows)
|
||||||
|
{
|
||||||
|
rowNumber++;
|
||||||
|
if (row is not IDictionary<string, object> rowDict)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsEmptyRow(rowDict))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result.Add((rowNumber, ParseRow(rowDict)));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void PrepareEntityForImport(
|
||||||
|
KpiAreaInfo entity,
|
||||||
|
IReadOnlyDictionary<string, Country> countriesByCode)
|
||||||
|
{
|
||||||
|
ValidateEntity(entity);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(entity.CountryName)
|
||||||
|
&& !string.IsNullOrWhiteSpace(entity.CountryCode))
|
||||||
|
{
|
||||||
|
var code = entity.CountryCode.Trim();
|
||||||
|
if (countriesByCode.TryGetValue(code, out var country))
|
||||||
|
{
|
||||||
|
entity.CountryName = country.ChineseName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(entity.CountryName))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("国家名称不能为空,请填写国家名称或有效的国家代码");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static KpiAreaInfo ParseRow(IDictionary<string, object> rowDict)
|
||||||
|
{
|
||||||
|
var mapped = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (header, value) in rowDict)
|
||||||
|
{
|
||||||
|
if (!HeaderAliases.TryGetValue(header.Trim(), out var field))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
mapped[field] = NormalizeCell(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new KpiAreaInfo
|
||||||
|
{
|
||||||
|
CountryCode = mapped.GetValueOrDefault(nameof(KpiAreaInfo.CountryCode)),
|
||||||
|
CountryName = mapped.GetValueOrDefault(nameof(KpiAreaInfo.CountryName)),
|
||||||
|
Province = mapped.GetValueOrDefault(nameof(KpiAreaInfo.Province)),
|
||||||
|
City = mapped.GetValueOrDefault(nameof(KpiAreaInfo.City)),
|
||||||
|
LeftPostcode = mapped.GetValueOrDefault(nameof(KpiAreaInfo.LeftPostcode)),
|
||||||
|
RightPostcode = mapped.GetValueOrDefault(nameof(KpiAreaInfo.RightPostcode))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidateEntity(KpiAreaInfo entity)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(entity.CountryCode))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("国家代码不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.CountryCode = entity.CountryCode.Trim();
|
||||||
|
if (entity.CountryCode.Length > 16)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("国家代码不可超过16个字符");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(entity.CountryName))
|
||||||
|
{
|
||||||
|
entity.CountryName = entity.CountryName.Trim();
|
||||||
|
if (entity.CountryName.Length > 64)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("国家名称不可超过64个字符");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entity.Province = TrimOrNull(entity.Province);
|
||||||
|
entity.City = TrimOrNull(entity.City);
|
||||||
|
entity.LeftPostcode = TrimOrNull(entity.LeftPostcode);
|
||||||
|
entity.RightPostcode = TrimOrNull(entity.RightPostcode);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(entity.Province) && entity.Province.Length > 128)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("州/省不可超过128个字符");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(entity.City) && entity.City.Length > 128)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("城市不可超过128个字符");
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidatePostcode(entity.LeftPostcode, "起始邮编");
|
||||||
|
ValidatePostcode(entity.RightPostcode, "截止邮编");
|
||||||
|
|
||||||
|
var hasLeft = !string.IsNullOrWhiteSpace(entity.LeftPostcode);
|
||||||
|
var hasRight = !string.IsNullOrWhiteSpace(entity.RightPostcode);
|
||||||
|
if (hasLeft != hasRight)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("起始邮编与截止邮编需同时填写或同时留空");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ValidatePostcode(string? value, string label)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ChinesePattern.IsMatch(value))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{label}不能包含中文");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.Length > 30)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{label}不可超过30个字符");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TrimOrNull(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeCell(object? value)
|
||||||
|
{
|
||||||
|
if (value is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var text = Convert.ToString(value, System.Globalization.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)));
|
||||||
|
}
|
||||||
@ -16,6 +16,7 @@ public static class PublishSnapshotNormalizer
|
|||||||
|
|
||||||
NormalizeKpiAreas(snapshot.BlacklistAreaInfos);
|
NormalizeKpiAreas(snapshot.BlacklistAreaInfos);
|
||||||
NormalizeKpiAreas(snapshot.RemoteAreaInfos);
|
NormalizeKpiAreas(snapshot.RemoteAreaInfos);
|
||||||
|
NormalizeWeightRoundingRanges(snapshot.WeightRoundingInfos);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void NormalizePriceSnapshot(SalesProductPricePublishSnapshotDto? snapshot)
|
public static void NormalizePriceSnapshot(SalesProductPricePublishSnapshotDto? snapshot)
|
||||||
@ -47,4 +48,34 @@ public static class PublishSnapshotNormalizer
|
|||||||
area.RightPostcode = null;
|
area.RightPostcode = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void NormalizeWeightRoundingRanges(List<WeightRoundingSnapshotRangeDto>? ranges)
|
||||||
|
{
|
||||||
|
if (ranges is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var range in ranges)
|
||||||
|
{
|
||||||
|
if (range.StartValue != 0)
|
||||||
|
{
|
||||||
|
range.Start = range.StartValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (range.EndValue != 0)
|
||||||
|
{
|
||||||
|
range.End = range.EndValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
range.StartValue = 0;
|
||||||
|
range.EndValue = 0;
|
||||||
|
|
||||||
|
if (range.Start == 0 && range.End == 0 && !string.IsNullOrWhiteSpace(range.Public))
|
||||||
|
{
|
||||||
|
// 无效区间占位,避免误用导致「计重进位区间未匹配」;后续会降级读 DB。
|
||||||
|
range.Public = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/Oms.Tests/KpiAreaInfoImportHelperTests.cs
Normal file
43
src/Oms.Tests/KpiAreaInfoImportHelperTests.cs
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
|
using Oms.Domain.SystemAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Tests;
|
||||||
|
|
||||||
|
public class KpiAreaInfoImportHelperTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void PrepareEntityForImport_ValidRow_ShouldFillCountryName()
|
||||||
|
{
|
||||||
|
var entity = new KpiAreaInfo
|
||||||
|
{
|
||||||
|
CountryCode = "US",
|
||||||
|
LeftPostcode = "90000",
|
||||||
|
RightPostcode = "90199"
|
||||||
|
};
|
||||||
|
var countries = new Dictionary<string, Country>(StringComparer.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
["US"] = new Country { CountryCode = "US", ChineseName = "美国" }
|
||||||
|
};
|
||||||
|
|
||||||
|
KpiAreaInfoImportHelper.PrepareEntityForImport(entity, countries);
|
||||||
|
|
||||||
|
entity.CountryName.Should().Be("美国");
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void PrepareEntityForImport_MissingPostcodePair_ShouldThrow()
|
||||||
|
{
|
||||||
|
var entity = new KpiAreaInfo
|
||||||
|
{
|
||||||
|
CountryCode = "US",
|
||||||
|
CountryName = "美国",
|
||||||
|
LeftPostcode = "90000"
|
||||||
|
};
|
||||||
|
|
||||||
|
var act = () => KpiAreaInfoImportHelper.PrepareEntityForImport(entity, new Dictionary<string, Country>());
|
||||||
|
|
||||||
|
act.Should().Throw<InvalidOperationException>().WithMessage("*截止邮编*");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -121,4 +121,34 @@ public class RulesSnapshotVerificationTests
|
|||||||
rawWeight.Should().Be(0.5M);
|
rawWeight.Should().Be(0.5M);
|
||||||
chargeable.Should().Be(0.5M);
|
chargeable.Should().Be(0.5M);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void DeserializeRulesSnapshot_StartValueEndValue_ShouldApplyRoundingFor12kg()
|
||||||
|
{
|
||||||
|
const string json = """
|
||||||
|
{
|
||||||
|
"WeightRoundingId": 1,
|
||||||
|
"WeightRoundingInfos": [
|
||||||
|
{ "Id": 1, "StartValue": 0, "EndValue": 99, "Public": "0.5" }
|
||||||
|
],
|
||||||
|
"WeightRuleInfos": [
|
||||||
|
{ "Id": 1, "VolumetricFactor": "8000", "Condition": "gw>vw", "BillableWeight": "gw" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
""";
|
||||||
|
|
||||||
|
var snapshot = OmsDocHelper.DeserializeRulesSnapshot(json);
|
||||||
|
snapshot.Should().NotBeNull();
|
||||||
|
snapshot!.WeightRoundingInfos[0].Start.Should().Be(0);
|
||||||
|
snapshot.WeightRoundingInfos[0].End.Should().Be(99);
|
||||||
|
|
||||||
|
var forecast = new OrderWeight { Length = 10, Width = 10, Height = 10, ActualWeight = 1.2M };
|
||||||
|
var ruleInfos = RulesSnapshotHelper.ToWeightRuleEntities(snapshot.WeightRuleInfos);
|
||||||
|
var (_, rawWeight) = ParcelBillingCalculator.CalculateRawBillableWeight(forecast, ruleInfos);
|
||||||
|
var ranges = RulesSnapshotHelper.ToWeightRoundingEntities(snapshot.WeightRoundingInfos);
|
||||||
|
var chargeable = ParcelBillingCalculator.ApplyRounding(rawWeight, ranges);
|
||||||
|
|
||||||
|
rawWeight.Should().Be(1.2M);
|
||||||
|
chargeable.Should().Be(1.5M);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user