From 7827b4803eb2efcda19d542d9220d45a55db8f9f Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Fri, 26 Jun 2026 14:10:22 +0800 Subject: [PATCH] feat: slim rules/price publish snapshots with PostcodeRange (left,right] Use slim DTOs without audit timestamps; migrate legacy LeftPostcode/RightPostcode on read. Co-authored-by: Cursor --- .../Dtos/Orders/ProductBillingSnapshotDtos.cs | 20 ++-- .../Dtos/Orders/PublishSnapshotRuleDtos.cs | 86 ++++++++++++++ .../Channels/SalesProductService.Helpers.cs | 49 ++++++-- .../Orders/OrderService.ParcelBilling.cs | 8 +- .../Orders/ParcelBillingCalculator.cs | 58 +++++++++- .../Shared/KpiAreaSnapshotMapper.cs | 105 ++++++++++++++++++ src/Oms.Application/Shared/OmsDocHelper.cs | 35 ++++++ .../Shared/PriceAreaSnapshotHelper.cs | 17 +-- .../Shared/PublishSnapshotNormalizer.cs | 50 +++++++++ .../Shared/PublishSnapshotSlimmer.cs | 63 +++++++++++ .../Shared/RulesSnapshotHelper.cs | 86 ++------------ src/Oms.Tests/KpiAreaSnapshotMapperTests.cs | 23 ++++ .../PriceCheckSnapshotVerificationTests.cs | 31 +++--- .../RulesSnapshotHelperPublishTests.cs | 8 +- .../RulesSnapshotVerificationTests.cs | 23 ++-- .../Support/ParcelBillingTestData.cs | 7 +- 16 files changed, 519 insertions(+), 150 deletions(-) create mode 100644 src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs create mode 100644 src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs create mode 100644 src/Oms.Application/Shared/PublishSnapshotNormalizer.cs create mode 100644 src/Oms.Application/Shared/PublishSnapshotSlimmer.cs create mode 100644 src/Oms.Tests/KpiAreaSnapshotMapperTests.cs diff --git a/src/Oms.Application/Dtos/Orders/ProductBillingSnapshotDtos.cs b/src/Oms.Application/Dtos/Orders/ProductBillingSnapshotDtos.cs index d4d6fdc..809b136 100644 --- a/src/Oms.Application/Dtos/Orders/ProductBillingSnapshotDtos.cs +++ b/src/Oms.Application/Dtos/Orders/ProductBillingSnapshotDtos.cs @@ -23,14 +23,14 @@ public class SalesProductPricePublishSnapshotDto public string? PriceAreaName { get; set; } /// - /// 价格分区明细(国家/邮编等),发布时从 t_kpi_area_info 固化。 + /// 价格分区明细(国家/邮编等),发布时从 t_kpi_area_info 固化;邮编为左开右闭 (left,right]。 /// - public List PriceAreaInfos { get; set; } = []; + public List PriceAreaInfos { get; set; } = []; /// - /// 价格项列表。 + /// 价格项列表(不含创建/更新时间)。 /// - public List Prices { get; set; } = []; + public List Prices { get; set; } = []; } /// @@ -59,9 +59,9 @@ public class SalesProductRulesPublishSnapshotDto public string? BlacklistAreaName { get; set; } /// - /// 黑名单分区明细,发布时从 t_kpi_area_info 固化。 + /// 黑名单分区明细,发布时从 t_kpi_area_info 固化;邮编为左开右闭 (left,right]。 /// - public List BlacklistAreaInfos { get; set; } = []; + public List BlacklistAreaInfos { get; set; } = []; /// /// 偏远分区 ID(发布时固化)。 @@ -74,9 +74,9 @@ public class SalesProductRulesPublishSnapshotDto public string? RemoteAreaName { get; set; } /// - /// 偏远分区明细,发布时从 t_kpi_area_info 固化。 + /// 偏远分区明细,发布时从 t_kpi_area_info 固化;邮编为左开右闭 (left,right]。 /// - public List RemoteAreaInfos { get; set; } = []; + public List RemoteAreaInfos { get; set; } = []; /// /// 计重规则 ID(发布时固化)。 @@ -91,7 +91,7 @@ public class SalesProductRulesPublishSnapshotDto /// /// 计重规则明细,发布时从 t_weight_rule_info 固化。 /// - public List WeightRuleInfos { get; set; } = []; + public List WeightRuleInfos { get; set; } = []; /// /// 计重进位 ID(发布时固化)。 @@ -106,7 +106,7 @@ public class SalesProductRulesPublishSnapshotDto /// /// 计重进位区间,发布时从 t_weight_rounding_info 固化。 /// - public List WeightRoundingInfos { get; set; } = []; + public List WeightRoundingInfos { get; set; } = []; } /// diff --git a/src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs b/src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs new file mode 100644 index 0000000..9321cfe --- /dev/null +++ b/src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs @@ -0,0 +1,86 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Oms.Application.Dtos.Channels; + +namespace Oms.Application.Dtos.Orders; + +/// +/// KPI 分区快照行(黑名单/偏远/价格分区),仅保留计费匹配字段。 +/// +public class KpiAreaSnapshotRuleDto +{ + public int Id { get; set; } + + public string? CountryCode { get; set; } + + public string? Province { get; set; } + + public string? City { get; set; } + + /// + /// 邮编区间,左开右闭,如 (50000,90000]。 + /// + public string? PostcodeRange { get; set; } + + /// 旧快照兼容:反序列化后由 转为 PostcodeRange 并清空。 + public string? LeftPostcode { get; set; } + + /// 旧快照兼容:反序列化后由 转为 PostcodeRange 并清空。 + public string? RightPostcode { get; set; } +} + +/// +/// 计重规则快照行。 +/// +public class WeightRuleSnapshotRuleDto +{ + public int Id { get; set; } + + public string Condition { get; set; } = string.Empty; + + public string BillableWeight { get; set; } = string.Empty; + + public string VolumetricFactor { get; set; } = string.Empty; +} + +/// +/// 计重进位快照行。 +/// +public class WeightRoundingSnapshotRangeDto +{ + public int Id { get; set; } + + public decimal Start { get; set; } + + public decimal End { get; set; } + + public string? Public { get; set; } +} + +/// +/// 卖价项快照(不含创建/更新时间)。 +/// +public class SalesProductPriceSnapshotItemDto +{ + public int Id { get; set; } + + public int SerialNumber { get; set; } + + public byte Status { get; set; } + + public string PriceName { get; set; } = string.Empty; + + public string CostItem { get; set; } = string.Empty; + + public string CostUnit { get; set; } = string.Empty; + + public string Currency { get; set; } = string.Empty; + + public string? ChargingTime { get; set; } + + public string? ChargingCondition { get; set; } + + public List Factors { get; set; } = []; + + public List Details { get; set; } = []; +} diff --git a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs index 29d8f24..9306171 100644 --- a/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs +++ b/src/Oms.Application/Services/Channels/SalesProductService.Helpers.cs @@ -184,7 +184,7 @@ public partial class SalesProductService var priceSnapshot = new SalesProductPricePublishSnapshot { SalesProductId = salesProductId, - Prices = await GetPriceOutputsAsync(salesProductId) + Prices = (await GetPriceOutputsAsync(salesProductId)).Select(PublishSnapshotSlimmer.SlimPrice).ToList() }; await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(priceSnapshot, product, KpiAreaRepository); return priceSnapshot; @@ -230,7 +230,7 @@ public partial class SalesProductService PublishUser = string.IsNullOrWhiteSpace(CurrentUser.UserName) ? CurrentUser.GetId().ToString() : CurrentUser.UserName!, PublishTime = DateTime.Now, Description = description, - Content = OmsDocHelper.SerializeJson(snapshot) + Content = OmsDocHelper.SerializeSnapshotJson(snapshot) }); } @@ -249,7 +249,7 @@ public partial class SalesProductService if (operatorType == PriceOperatorType) { - var snapshot = OmsDocHelper.DeserializeJson(record.Content); + var snapshot = OmsDocHelper.DeserializePriceSnapshot(record.Content); if (snapshot == null) { throw new Fake.Application.BusinessException("发布快照无效"); @@ -259,12 +259,12 @@ public partial class SalesProductService await SavePriceAsync(new SalesProductPriceSaveAllInput { SalesProductId = input.SalesProductId, - Prices = snapshot.Prices.Select(MapPriceSaveInput).ToList() + Prices = snapshot.Prices.Select(price => MapPriceSaveInput(price, input.SalesProductId)).ToList() }); return; } - var rulesSnapshot = OmsDocHelper.DeserializeJson(record.Content); + var rulesSnapshot = OmsDocHelper.DeserializeRulesSnapshot(record.Content); if (rulesSnapshot == null) { throw new Fake.Application.BusinessException("发布快照无效"); @@ -408,12 +408,47 @@ public partial class SalesProductService }).ToList(); } - private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceListOutput output) + private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceListOutput output) => + MapPriceSaveInput(output, output.SalesProductId); + + private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceSnapshotItemDto output, int salesProductId) { return new SalesProductPriceSaveInput { Id = output.Id, - SalesProductId = output.SalesProductId, + SalesProductId = salesProductId, + SerialNumber = output.SerialNumber, + Status = (EnableStatus)output.Status, + PriceName = output.PriceName, + CostItem = output.CostItem, + CostUnit = output.CostUnit, + Currency = output.Currency, + ChargingTime = output.ChargingTime, + ChargingCondition = output.ChargingCondition, + Factors = output.Factors.Select(item => new ChannelPriceFactorInput + { + Id = item.Id, + SerialNumber = item.SerialNumber, + Factor = item.Factor, + FactorValues = item.FactorValues + }).ToList(), + Details = output.Details.Select(item => new ChannelPriceDetailInput + { + Id = item.Id, + FactorValuesJson = item.FactorValuesJson, + CalculationFormula = item.CalculationFormula, + CalculatedValue = item.CalculatedValue, + CustomerGroupValues = item.CustomerGroupValues + }).ToList() + }; + } + + private static SalesProductPriceSaveInput MapPriceSaveInput(SalesProductPriceListOutput output, int salesProductId) + { + return new SalesProductPriceSaveInput + { + Id = output.Id, + SalesProductId = salesProductId, SerialNumber = output.SerialNumber, Status = (EnableStatus)output.Status, PriceName = output.PriceName, diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index 72998d8..7b21faa 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -250,7 +250,7 @@ public partial class OrderService } else { - rulesSnapshot = OmsDocHelper.DeserializeJson(rulesRecord.Content) + rulesSnapshot = OmsDocHelper.DeserializeRulesSnapshot(rulesRecord.Content) ?? throw new BusinessException("未命中规则快照版本(内存/Redis/DB)"); } @@ -265,7 +265,7 @@ public partial class OrderService } else { - priceSnapshot = OmsDocHelper.DeserializeJson(priceRecord.Content) + priceSnapshot = OmsDocHelper.DeserializePriceSnapshot(priceRecord.Content) ?? throw new BusinessException("未命中价格快照版本(内存/Redis/DB)"); if (priceSnapshot.Prices.Count == 0) { @@ -330,7 +330,7 @@ public partial class OrderService var snapshot = new SalesProductPricePublishSnapshotDto { SalesProductId = product.Id, - Prices = prices.Select(price => new SalesProductPriceListOutput + Prices = prices.Select(price => PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = price.Id, SalesProductId = price.SalesProductId, @@ -348,7 +348,7 @@ public partial class OrderService Details = detailMap.TryGetValue(price.Id, out var details) ? details.Select(MapPriceDetailOutput).ToList() : [] - }).ToList() + })).ToList() }; await PriceAreaSnapshotHelper.ApplyPriceAreaToSnapshotAsync(snapshot, product, KpiAreaRepository); return snapshot; diff --git a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs index 4f24e7c..29ca860 100644 --- a/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs +++ b/src/Oms.Application/Services/Orders/ParcelBillingCalculator.cs @@ -146,7 +146,7 @@ internal static class ParcelBillingCalculator } var matched = priceSnapshot.PriceAreaInfos.FirstOrDefault(rule => - IsAreaMatched(rule, countryCode, province, city, zipCode)); + KpiAreaSnapshotMapper.IsRuleMatched(rule, countryCode, province, city, zipCode)); if (matched == null) { return null; @@ -221,6 +221,62 @@ internal static class ParcelBillingCalculator return keys.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); } + public static IReadOnlyList BuildPriceZoneKeys( + KpiAreaSnapshotRuleDto matched, + string? areaName, + int? areaId) + { + var keys = new List(); + var name = ResolveZoneName(matched, areaName); + if (!string.IsNullOrWhiteSpace(name)) + { + keys.Add(name); + } + + keys.Add(matched.Id.ToString(CultureInfo.InvariantCulture)); + if (!string.IsNullOrWhiteSpace(matched.Province)) + { + keys.Add(matched.Province); + } + + if (!string.IsNullOrWhiteSpace(matched.CountryCode)) + { + keys.Add(matched.CountryCode); + } + + if (areaId is > 0) + { + keys.Add(areaId.Value.ToString(CultureInfo.InvariantCulture)); + } + + if (!string.IsNullOrWhiteSpace(areaName)) + { + keys.Add(areaName); + } + + return keys.Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + } + + public static string? ResolveZoneName(KpiAreaSnapshotRuleDto matched, string? areaName) + { + if (!string.IsNullOrWhiteSpace(matched.Province)) + { + return matched.Province; + } + + if (!string.IsNullOrWhiteSpace(matched.City)) + { + return matched.City; + } + + if (!string.IsNullOrWhiteSpace(matched.CountryCode)) + { + return matched.CountryCode; + } + + return areaName; + } + public static IReadOnlyList BuildPriceZoneKeys( KpiAreaInfoOutput matched, string? areaName, diff --git a/src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs b/src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs new file mode 100644 index 0000000..166cf03 --- /dev/null +++ b/src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs @@ -0,0 +1,105 @@ +using System.Text.RegularExpressions; +using Oms.Application.Dtos.Orders; +using Oms.Domain.ChannelsAggregate.Entities; + +namespace Oms.Application.Shared; + +/// +/// KPI 分区快照映射与左开右闭邮编匹配。 +/// +public static class KpiAreaSnapshotMapper +{ + private static readonly Regex PostcodeRangePattern = new( + @"^\(\s*([^,)\]]*)\s*,\s*([^,)\]]*)\s*\]$", + RegexOptions.Compiled | RegexOptions.CultureInvariant); + + public static KpiAreaSnapshotRuleDto MapToSnapshotRule(KpiAreaInfo entity) => + new() + { + Id = entity.Id, + CountryCode = entity.CountryCode, + Province = entity.Province, + City = entity.City, + PostcodeRange = BuildPostcodeRange(entity.LeftPostcode, entity.RightPostcode) + }; + + public static string? BuildPostcodeRange(string? left, string? right) + { + if (string.IsNullOrWhiteSpace(left) && string.IsNullOrWhiteSpace(right)) + { + return null; + } + + return $"({left?.Trim()},{right?.Trim()}]"; + } + + public static string? ResolvePostcodeRange(KpiAreaSnapshotRuleDto rule) + { + if (!string.IsNullOrWhiteSpace(rule.PostcodeRange)) + { + return rule.PostcodeRange.Trim(); + } + + return BuildPostcodeRange(rule.LeftPostcode, rule.RightPostcode); + } + + public static bool IsRuleMatched( + KpiAreaSnapshotRuleDto rule, + string countryCode, + string? province, + string? city, + string? zipCode) + { + if (!string.IsNullOrWhiteSpace(rule.CountryCode) + && !string.Equals(rule.CountryCode, countryCode, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(rule.Province) + && !string.Equals(rule.Province, province, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + if (!string.IsNullOrWhiteSpace(rule.City) + && !string.Equals(rule.City, city, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var postcodeRange = ResolvePostcodeRange(rule); + if (string.IsNullOrWhiteSpace(postcodeRange)) + { + return true; + } + + if (string.IsNullOrWhiteSpace(zipCode)) + { + return false; + } + + return IsZipInLeftOpenRightClosedRange(zipCode.Trim(), postcodeRange); + } + + public static bool IsZipInLeftOpenRightClosedRange(string zipCode, string postcodeRange) + { + var match = PostcodeRangePattern.Match(postcodeRange.Trim()); + if (!match.Success) + { + return false; + } + + var left = match.Groups[1].Value.Trim(); + var right = match.Groups[2].Value.Trim(); + if (long.TryParse(zipCode, out var zip) + && long.TryParse(left, out var leftValue) + && long.TryParse(right, out var rightValue)) + { + return zip > leftValue && zip <= rightValue; + } + + return string.Compare(zipCode, left, StringComparison.OrdinalIgnoreCase) > 0 + && string.Compare(zipCode, right, StringComparison.OrdinalIgnoreCase) <= 0; + } +} diff --git a/src/Oms.Application/Shared/OmsDocHelper.cs b/src/Oms.Application/Shared/OmsDocHelper.cs index 3299407..102b40d 100644 --- a/src/Oms.Application/Shared/OmsDocHelper.cs +++ b/src/Oms.Application/Shared/OmsDocHelper.cs @@ -1,5 +1,7 @@ using System.Text.Json; using System.Text.Json.Nodes; +using System.Text.Json.Serialization; +using Oms.Application.Dtos.Orders; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.SystemAggregate.Consts; @@ -15,6 +17,12 @@ public static class OmsDocHelper PropertyNameCaseInsensitive = true }; + private static readonly JsonSerializerOptions SnapshotJsonOptions = new() + { + PropertyNameCaseInsensitive = true, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull + }; + /// /// 将页码规范为至少 1。 /// @@ -93,6 +101,33 @@ public static class OmsDocHelper return JsonSerializer.Serialize(value, JsonOptions); } + /// + /// 序列化发布快照(忽略 null,避免写出旧兼容字段)。 + /// + public static string? SerializeSnapshotJson(T? value) + { + if (value is null) + { + return null; + } + + return JsonSerializer.Serialize(value, SnapshotJsonOptions); + } + + public static SalesProductRulesPublishSnapshotDto? DeserializeRulesSnapshot(string? json) + { + var snapshot = DeserializeJson(json); + PublishSnapshotNormalizer.NormalizeRulesSnapshot(snapshot); + return snapshot; + } + + public static SalesProductPricePublishSnapshotDto? DeserializePriceSnapshot(string? json) + { + var snapshot = DeserializeJson(json); + PublishSnapshotNormalizer.NormalizePriceSnapshot(snapshot); + return snapshot; + } + private static object? ConvertJsonElement(JsonElement element) => element.ValueKind switch { JsonValueKind.Object => element.EnumerateObject() diff --git a/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs b/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs index ab0bd80..6a4b7e2 100644 --- a/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs +++ b/src/Oms.Application/Shared/PriceAreaSnapshotHelper.cs @@ -37,7 +37,7 @@ public static class PriceAreaSnapshotHelper var area = await kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == priceAreaId.Value, cancellationToken); snapshot.PriceAreaName = area?.Name; var infos = await kpiAreaRepository.GetAllInfosByAreaIdAsync(priceAreaId.Value, cancellationToken); - snapshot.PriceAreaInfos = infos.Select(MapInfoOutput).ToList(); + snapshot.PriceAreaInfos = infos.Select(KpiAreaSnapshotMapper.MapToSnapshotRule).ToList(); } internal static async Task ResolvePriceAreaIdAsync( @@ -74,19 +74,4 @@ public static class PriceAreaSnapshotHelper return null; } - - private static KpiAreaInfoOutput MapInfoOutput(KpiAreaInfo entity) => - new() - { - Id = entity.Id, - AreaId = entity.AreaId, - CountryCode = entity.CountryCode, - CountryName = entity.CountryName, - Province = entity.Province, - City = entity.City, - LeftPostcode = entity.LeftPostcode, - RightPostcode = entity.RightPostcode, - CreateTime = entity.CreateTime, - UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) - }; } diff --git a/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs b/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs new file mode 100644 index 0000000..ac3ff63 --- /dev/null +++ b/src/Oms.Application/Shared/PublishSnapshotNormalizer.cs @@ -0,0 +1,50 @@ +using Oms.Application.Dtos.Orders; + +namespace Oms.Application.Shared; + +/// +/// 发布快照读取后规范化(旧字段迁移、去掉冗余)。 +/// +public static class PublishSnapshotNormalizer +{ + public static void NormalizeRulesSnapshot(SalesProductRulesPublishSnapshotDto? snapshot) + { + if (snapshot is null) + { + return; + } + + NormalizeKpiAreas(snapshot.BlacklistAreaInfos); + NormalizeKpiAreas(snapshot.RemoteAreaInfos); + } + + public static void NormalizePriceSnapshot(SalesProductPricePublishSnapshotDto? snapshot) + { + if (snapshot is null) + { + return; + } + + NormalizeKpiAreas(snapshot.PriceAreaInfos); + } + + public static void NormalizeKpiAreas(List? areas) + { + if (areas is null) + { + return; + } + + foreach (var area in areas) + { + if (string.IsNullOrWhiteSpace(area.PostcodeRange) + && (!string.IsNullOrWhiteSpace(area.LeftPostcode) || !string.IsNullOrWhiteSpace(area.RightPostcode))) + { + area.PostcodeRange = KpiAreaSnapshotMapper.BuildPostcodeRange(area.LeftPostcode, area.RightPostcode); + } + + area.LeftPostcode = null; + area.RightPostcode = null; + } + } +} diff --git a/src/Oms.Application/Shared/PublishSnapshotSlimmer.cs b/src/Oms.Application/Shared/PublishSnapshotSlimmer.cs new file mode 100644 index 0000000..99e01fe --- /dev/null +++ b/src/Oms.Application/Shared/PublishSnapshotSlimmer.cs @@ -0,0 +1,63 @@ +using Oms.Application.Dtos.Channels; +using Oms.Application.Dtos.Orders; +using Oms.Domain.ChannelsAggregate.Entities; + +namespace Oms.Application.Shared; + +/// +/// 发布快照瘦身:去掉审计时间等计费无用字段。 +/// +public static class PublishSnapshotSlimmer +{ + public static SalesProductPriceSnapshotItemDto SlimPrice(SalesProductPriceListOutput price) => + new() + { + Id = price.Id, + SerialNumber = price.SerialNumber, + Status = price.Status, + PriceName = price.PriceName, + CostItem = price.CostItem, + CostUnit = price.CostUnit, + Currency = price.Currency, + ChargingTime = price.ChargingTime, + ChargingCondition = price.ChargingCondition, + Factors = price.Factors, + Details = price.Details + }; + + public static WeightRuleSnapshotRuleDto SlimWeightRule(WeightRuleInfo entity) => + new() + { + Id = entity.Id, + Condition = entity.Condition, + BillableWeight = entity.BillableWeight, + VolumetricFactor = entity.VolumetricFactor + }; + + public static WeightRoundingSnapshotRangeDto SlimWeightRounding(WeightRoundingInfo entity) => + new() + { + Id = entity.Id, + Start = entity.StartValue, + End = entity.EndValue, + Public = entity.Public + }; + + public static WeightRuleInfo ToWeightRuleEntity(WeightRuleSnapshotRuleDto rule) => + new() + { + Id = rule.Id, + Condition = rule.Condition, + BillableWeight = rule.BillableWeight, + VolumetricFactor = rule.VolumetricFactor + }; + + public static WeightRoundingInfo ToWeightRoundingEntity(WeightRoundingSnapshotRangeDto range) => + new() + { + Id = range.Id, + StartValue = range.Start, + EndValue = range.End, + Public = range.Public + }; +} diff --git a/src/Oms.Application/Shared/RulesSnapshotHelper.cs b/src/Oms.Application/Shared/RulesSnapshotHelper.cs index 6ea474a..fc5744c 100644 --- a/src/Oms.Application/Shared/RulesSnapshotHelper.cs +++ b/src/Oms.Application/Shared/RulesSnapshotHelper.cs @@ -1,4 +1,3 @@ -using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders; using Oms.Domain.ChannelsAggregate.Entities; @@ -54,7 +53,7 @@ public static class RulesSnapshotHelper /// 从快照内嵌黑名单/偏远明细匹配地址(不查库)。 /// public static bool IsAreaHitFromSnapshot( - IReadOnlyList? infos, + IReadOnlyList? infos, string countryCode, string? province, string? city, @@ -65,7 +64,7 @@ public static class RulesSnapshotHelper return false; } - return infos.Any(rule => ParcelBillingCalculator.IsAreaMatched( + return infos.Any(rule => KpiAreaSnapshotMapper.IsRuleMatched( rule, countryCode, province, @@ -76,20 +75,20 @@ public static class RulesSnapshotHelper /// /// 将快照内嵌计重明细转为实体供计算器使用。 /// - public static List ToWeightRuleEntities(IReadOnlyList infos) => - infos.Select(MapWeightRuleInfo).ToList(); + public static List ToWeightRuleEntities(IReadOnlyList infos) => + infos.Select(PublishSnapshotSlimmer.ToWeightRuleEntity).ToList(); /// /// 将快照内嵌进位区间转为实体供计算器使用。 /// - public static List ToWeightRoundingEntities(IReadOnlyList infos) => - infos.Select(MapWeightRoundingInfo).ToList(); + public static List ToWeightRoundingEntities(IReadOnlyList infos) => + infos.Select(PublishSnapshotSlimmer.ToWeightRoundingEntity).ToList(); private static async Task ApplyKpiAreaToSnapshotAsync( SalesProductRulesPublishSnapshotDto snapshot, int? areaId, IKpiAreaRepository kpiAreaRepository, - Action> assign, + Action> assign, CancellationToken cancellationToken) { if (!areaId.HasValue || areaId.Value <= 0) @@ -104,7 +103,7 @@ public static class RulesSnapshotHelper snapshot, areaId, area?.Name, - infos.Select(MapKpiAreaInfo).ToList()); + infos.Select(KpiAreaSnapshotMapper.MapToSnapshotRule).ToList()); } private static async Task ApplyWeightRuleToSnapshotAsync( @@ -125,7 +124,7 @@ public static class RulesSnapshotHelper var rule = await weightRuleRepository.FirstOrDefaultAsync(x => x.Id == product.WeightRuleId.Value, cancellationToken); snapshot.WeightRuleName = rule?.Name; var infos = await weightRuleRepository.GetAllInfosByRuleIdAsync(product.WeightRuleId.Value, cancellationToken); - snapshot.WeightRuleInfos = infos.Select(MapWeightRuleInfoOutput).ToList(); + snapshot.WeightRuleInfos = infos.Select(PublishSnapshotSlimmer.SlimWeightRule).ToList(); } private static async Task ApplyWeightRoundingToSnapshotAsync( @@ -150,71 +149,6 @@ public static class RulesSnapshotHelper var infos = await weightRoundingRepository.GetAllInfosByRoundingIdAsync( product.WeightRoundingId.Value, cancellationToken); - snapshot.WeightRoundingInfos = infos.Select(MapWeightRoundingInfoOutput).ToList(); + snapshot.WeightRoundingInfos = infos.Select(PublishSnapshotSlimmer.SlimWeightRounding).ToList(); } - - private static KpiAreaInfoOutput MapKpiAreaInfo(KpiAreaInfo entity) => - new() - { - Id = entity.Id, - AreaId = entity.AreaId, - CountryCode = entity.CountryCode, - CountryName = entity.CountryName, - Province = entity.Province, - City = entity.City, - LeftPostcode = entity.LeftPostcode, - RightPostcode = entity.RightPostcode, - CreateTime = entity.CreateTime, - UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) - }; - - private static WeightRuleInfoOutput MapWeightRuleInfoOutput(WeightRuleInfo entity) => - new() - { - Id = entity.Id, - RuleId = entity.RuleId, - Client = entity.Client, - Condition = entity.Condition, - BillableWeight = entity.BillableWeight, - VolumetricFactor = entity.VolumetricFactor, - CreateTime = entity.CreateTime, - UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) - }; - - private static WeightRoundingInfoOutput MapWeightRoundingInfoOutput(WeightRoundingInfo entity) => - new() - { - Id = entity.Id, - RoundingId = entity.RoundingId, - StartValue = entity.StartValue, - EndValue = entity.EndValue, - Public = entity.Public, - CustomerGroup1 = entity.CustomerGroup1, - CustomerGroup2 = entity.CustomerGroup2, - CreateTime = entity.CreateTime, - UpdateTime = OmsDocHelper.NullIfDefault(entity.UpdateTime) - }; - - private static WeightRuleInfo MapWeightRuleInfo(WeightRuleInfoOutput output) => - new() - { - Id = output.Id, - RuleId = output.RuleId, - Client = output.Client, - Condition = output.Condition, - BillableWeight = output.BillableWeight, - VolumetricFactor = output.VolumetricFactor - }; - - private static WeightRoundingInfo MapWeightRoundingInfo(WeightRoundingInfoOutput output) => - new() - { - Id = output.Id, - RoundingId = output.RoundingId, - StartValue = output.StartValue, - EndValue = output.EndValue, - Public = output.Public, - CustomerGroup1 = output.CustomerGroup1, - CustomerGroup2 = output.CustomerGroup2 - }; } diff --git a/src/Oms.Tests/KpiAreaSnapshotMapperTests.cs b/src/Oms.Tests/KpiAreaSnapshotMapperTests.cs new file mode 100644 index 0000000..a612862 --- /dev/null +++ b/src/Oms.Tests/KpiAreaSnapshotMapperTests.cs @@ -0,0 +1,23 @@ +using FluentAssertions; +using Oms.Application.Shared; + +namespace Oms.Tests; + +public class KpiAreaSnapshotMapperTests +{ + [Theory] + [InlineData("06000", "(00000,50000]", true)] + [InlineData("00000", "(00000,50000]", false)] + [InlineData("50000", "(00000,50000]", true)] + [InlineData("50001", "(00000,50000]", false)] + public void IsZipInLeftOpenRightClosedRange_ShouldMatchExpected(string zip, string range, bool expected) + { + KpiAreaSnapshotMapper.IsZipInLeftOpenRightClosedRange(zip, range).Should().Be(expected); + } + + [Fact] + public void BuildPostcodeRange_ShouldFormatLeftOpenRightClosed() + { + KpiAreaSnapshotMapper.BuildPostcodeRange("00000", "50000").Should().Be("(00000,50000]"); + } +} diff --git a/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs b/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs index 244976c..cdf68e6 100644 --- a/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs +++ b/src/Oms.Tests/PriceCheckSnapshotVerificationTests.cs @@ -2,6 +2,7 @@ using FluentAssertions; using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders; +using Oms.Application.Shared; using Oms.Domain.CustomersAggregate.Enums; using Oms.Domain.OrdersAggregate.Consts; @@ -17,7 +18,7 @@ public class PriceCheckSnapshotVerificationTests SalesProductId = 1, Prices = [ - new SalesProductPriceListOutput + PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 1, Status = 1, @@ -30,15 +31,15 @@ public class PriceCheckSnapshotVerificationTests Detail(26, 1, "total", "3", "1,1.5", 13), Detail(27, 1, "total", "3", "1.5,2", 14), ] - }, - new SalesProductPriceListOutput + }), + PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 2, Status = 1, CostItem = "handling", ChargingTime = "outbound", Details = [Detail(28, 2, "total", "3", "0,10", 20)] - } + }) ] }; @@ -90,7 +91,7 @@ public class PriceCheckSnapshotVerificationTests SalesProductId = 1, Prices = [ - new SalesProductPriceListOutput + PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 1, Status = 1, @@ -100,14 +101,14 @@ public class PriceCheckSnapshotVerificationTests Detail(34, 1, "total", "3", "(0-0.5]", 11), Detail(35, 1, "total", "3", "(0.5-1]", 12), ] - }, - new SalesProductPriceListOutput + }), + PublishSnapshotSlimmer.SlimPrice(new SalesProductPriceListOutput { Id = 2, Status = 1, CostItem = "handling", Details = [Detail(38, 2, "total", "3", "(0-10]", 20)] - } + }) ] }; @@ -151,23 +152,17 @@ public class PriceCheckSnapshotVerificationTests PriceAreaName = "美国分区", PriceAreaInfos = [ - new KpiAreaInfoOutput + new KpiAreaSnapshotRuleDto { Id = 4, - AreaId = 3, CountryCode = "US", - CountryName = "美国", - LeftPostcode = "50000", - RightPostcode = "90000" + PostcodeRange = "(50000,90000]" }, - new KpiAreaInfoOutput + new KpiAreaSnapshotRuleDto { Id = 3, - AreaId = 3, CountryCode = "US", - CountryName = "美国", - LeftPostcode = "00000", - RightPostcode = "50000" + PostcodeRange = "(00000,50000]" } ] }; diff --git a/src/Oms.Tests/RulesSnapshotHelperPublishTests.cs b/src/Oms.Tests/RulesSnapshotHelperPublishTests.cs index 3ccdda0..c27ad84 100644 --- a/src/Oms.Tests/RulesSnapshotHelperPublishTests.cs +++ b/src/Oms.Tests/RulesSnapshotHelperPublishTests.cs @@ -79,10 +79,14 @@ public class RulesSnapshotHelperPublishTests snapshot.WeightRoundingId.Should().Be(2); snapshot.WeightRoundingInfos.Should().HaveCount(1); - var json = OmsDocHelper.SerializeJson(snapshot); - json.Should().Contain("BlacklistAreaInfos"); + var json = OmsDocHelper.SerializeSnapshotJson(snapshot); + json.Should().Contain("PostcodeRange"); + json.Should().Contain("(90000,90010]"); json.Should().Contain("WeightRuleInfos"); json.Should().Contain("WeightRoundingInfos"); json.Should().Contain("RemoteAreaInfos"); + json.Should().NotContain("CreateTime"); + json.Should().NotContain("UpdateTime"); + json.Should().NotContain("LeftPostcode"); } } diff --git a/src/Oms.Tests/RulesSnapshotVerificationTests.cs b/src/Oms.Tests/RulesSnapshotVerificationTests.cs index 5bc9206..8ec6e40 100644 --- a/src/Oms.Tests/RulesSnapshotVerificationTests.cs +++ b/src/Oms.Tests/RulesSnapshotVerificationTests.cs @@ -1,5 +1,4 @@ using FluentAssertions; -using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; using Oms.Application.Services.Orders; using Oms.Application.Shared; @@ -18,34 +17,29 @@ public class RulesSnapshotVerificationTests BlacklistAreaId = 10, BlacklistAreaInfos = [ - new KpiAreaInfoOutput + new KpiAreaSnapshotRuleDto { Id = 1, - AreaId = 10, CountryCode = "US", - LeftPostcode = "90000", - RightPostcode = "90010" + PostcodeRange = "(90000,90010]" } ], RemoteAreaId = 20, RemoteAreaInfos = [ - new KpiAreaInfoOutput + new KpiAreaSnapshotRuleDto { Id = 2, - AreaId = 20, CountryCode = "US", - LeftPostcode = "06000", - RightPostcode = "06999" + PostcodeRange = "(05999,06999]" } ], WeightRuleId = 1, WeightRuleInfos = [ - new WeightRuleInfoOutput + new WeightRuleSnapshotRuleDto { Id = 1, - RuleId = 1, VolumetricFactor = "6000", Condition = "gw>vw", BillableWeight = "gw" @@ -54,12 +48,11 @@ public class RulesSnapshotVerificationTests WeightRoundingId = 1, WeightRoundingInfos = [ - new WeightRoundingInfoOutput + new WeightRoundingSnapshotRangeDto { Id = 1, - RoundingId = 1, - StartValue = 0, - EndValue = 30, + Start = 0, + End = 30, Public = "0.5" } ] diff --git a/src/Oms.Tests/Support/ParcelBillingTestData.cs b/src/Oms.Tests/Support/ParcelBillingTestData.cs index 57dffe2..dbb6595 100644 --- a/src/Oms.Tests/Support/ParcelBillingTestData.cs +++ b/src/Oms.Tests/Support/ParcelBillingTestData.cs @@ -1,6 +1,7 @@ using System.Globalization; using Oms.Application.Dtos.Channels; using Oms.Application.Dtos.Orders; +using Oms.Application.Shared; using Oms.Domain.ChannelsAggregate.Entities; namespace Oms.Tests.Support; @@ -13,7 +14,11 @@ internal static class ParcelBillingTestData public static SalesProductPricePublishSnapshotDto BuildReceivableSnapshot( params SalesProductPriceListOutput[] prices) => - new() { SalesProductId = 1, Prices = prices.ToList() }; + new() + { + SalesProductId = 1, + Prices = prices.Select(PublishSnapshotSlimmer.SlimPrice).ToList() + }; public static List BuildPayablePrices( params ServiceChannelPriceListOutput[] prices) => prices.ToList();