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 <cursoragent@cursor.com>
This commit is contained in:
parent
d8a98fbbc8
commit
7827b4803e
@ -23,14 +23,14 @@ public class SalesProductPricePublishSnapshotDto
|
||||
public string? PriceAreaName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 价格分区明细(国家/邮编等),发布时从 t_kpi_area_info 固化。
|
||||
/// 价格分区明细(国家/邮编等),发布时从 t_kpi_area_info 固化;邮编为左开右闭 (left,right]。
|
||||
/// </summary>
|
||||
public List<KpiAreaInfoOutput> PriceAreaInfos { get; set; } = [];
|
||||
public List<KpiAreaSnapshotRuleDto> PriceAreaInfos { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 价格项列表。
|
||||
/// 价格项列表(不含创建/更新时间)。
|
||||
/// </summary>
|
||||
public List<SalesProductPriceListOutput> Prices { get; set; } = [];
|
||||
public List<SalesProductPriceSnapshotItemDto> Prices { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -59,9 +59,9 @@ public class SalesProductRulesPublishSnapshotDto
|
||||
public string? BlacklistAreaName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 黑名单分区明细,发布时从 t_kpi_area_info 固化。
|
||||
/// 黑名单分区明细,发布时从 t_kpi_area_info 固化;邮编为左开右闭 (left,right]。
|
||||
/// </summary>
|
||||
public List<KpiAreaInfoOutput> BlacklistAreaInfos { get; set; } = [];
|
||||
public List<KpiAreaSnapshotRuleDto> BlacklistAreaInfos { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 偏远分区 ID(发布时固化)。
|
||||
@ -74,9 +74,9 @@ public class SalesProductRulesPublishSnapshotDto
|
||||
public string? RemoteAreaName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 偏远分区明细,发布时从 t_kpi_area_info 固化。
|
||||
/// 偏远分区明细,发布时从 t_kpi_area_info 固化;邮编为左开右闭 (left,right]。
|
||||
/// </summary>
|
||||
public List<KpiAreaInfoOutput> RemoteAreaInfos { get; set; } = [];
|
||||
public List<KpiAreaSnapshotRuleDto> RemoteAreaInfos { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 计重规则 ID(发布时固化)。
|
||||
@ -91,7 +91,7 @@ public class SalesProductRulesPublishSnapshotDto
|
||||
/// <summary>
|
||||
/// 计重规则明细,发布时从 t_weight_rule_info 固化。
|
||||
/// </summary>
|
||||
public List<WeightRuleInfoOutput> WeightRuleInfos { get; set; } = [];
|
||||
public List<WeightRuleSnapshotRuleDto> WeightRuleInfos { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// 计重进位 ID(发布时固化)。
|
||||
@ -106,7 +106,7 @@ public class SalesProductRulesPublishSnapshotDto
|
||||
/// <summary>
|
||||
/// 计重进位区间,发布时从 t_weight_rounding_info 固化。
|
||||
/// </summary>
|
||||
public List<WeightRoundingInfoOutput> WeightRoundingInfos { get; set; } = [];
|
||||
public List<WeightRoundingSnapshotRangeDto> WeightRoundingInfos { get; set; } = [];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
86
src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs
Normal file
86
src/Oms.Application/Dtos/Orders/PublishSnapshotRuleDtos.cs
Normal file
@ -0,0 +1,86 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Oms.Application.Dtos.Channels;
|
||||
|
||||
namespace Oms.Application.Dtos.Orders;
|
||||
|
||||
/// <summary>
|
||||
/// KPI 分区快照行(黑名单/偏远/价格分区),仅保留计费匹配字段。
|
||||
/// </summary>
|
||||
public class KpiAreaSnapshotRuleDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public string? CountryCode { get; set; }
|
||||
|
||||
public string? Province { get; set; }
|
||||
|
||||
public string? City { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 邮编区间,左开右闭,如 (50000,90000]。
|
||||
/// </summary>
|
||||
public string? PostcodeRange { get; set; }
|
||||
|
||||
/// <summary>旧快照兼容:反序列化后由 <see cref="PublishSnapshotNormalizer"/> 转为 PostcodeRange 并清空。</summary>
|
||||
public string? LeftPostcode { get; set; }
|
||||
|
||||
/// <summary>旧快照兼容:反序列化后由 <see cref="PublishSnapshotNormalizer"/> 转为 PostcodeRange 并清空。</summary>
|
||||
public string? RightPostcode { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计重规则快照行。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计重进位快照行。
|
||||
/// </summary>
|
||||
public class WeightRoundingSnapshotRangeDto
|
||||
{
|
||||
public int Id { get; set; }
|
||||
|
||||
public decimal Start { get; set; }
|
||||
|
||||
public decimal End { get; set; }
|
||||
|
||||
public string? Public { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 卖价项快照(不含创建/更新时间)。
|
||||
/// </summary>
|
||||
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<ChannelPriceFactorOutput> Factors { get; set; } = [];
|
||||
|
||||
public List<ChannelPriceDetailOutput> Details { get; set; } = [];
|
||||
}
|
||||
@ -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<int>().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<SalesProductPricePublishSnapshot>(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<SalesProductRulesPublishSnapshot>(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,
|
||||
|
||||
@ -250,7 +250,7 @@ public partial class OrderService
|
||||
}
|
||||
else
|
||||
{
|
||||
rulesSnapshot = OmsDocHelper.DeserializeJson<SalesProductRulesPublishSnapshotDto>(rulesRecord.Content)
|
||||
rulesSnapshot = OmsDocHelper.DeserializeRulesSnapshot(rulesRecord.Content)
|
||||
?? throw new BusinessException("未命中规则快照版本(内存/Redis/DB)");
|
||||
}
|
||||
|
||||
@ -265,7 +265,7 @@ public partial class OrderService
|
||||
}
|
||||
else
|
||||
{
|
||||
priceSnapshot = OmsDocHelper.DeserializeJson<SalesProductPricePublishSnapshotDto>(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;
|
||||
|
||||
@ -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<string> BuildPriceZoneKeys(
|
||||
KpiAreaSnapshotRuleDto matched,
|
||||
string? areaName,
|
||||
int? areaId)
|
||||
{
|
||||
var keys = new List<string>();
|
||||
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<string> BuildPriceZoneKeys(
|
||||
KpiAreaInfoOutput matched,
|
||||
string? areaName,
|
||||
|
||||
105
src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs
Normal file
105
src/Oms.Application/Shared/KpiAreaSnapshotMapper.cs
Normal file
@ -0,0 +1,105 @@
|
||||
using System.Text.RegularExpressions;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Domain.ChannelsAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// KPI 分区快照映射与左开右闭邮编匹配。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 将页码规范为至少 1。
|
||||
/// </summary>
|
||||
@ -93,6 +101,33 @@ public static class OmsDocHelper
|
||||
return JsonSerializer.Serialize(value, JsonOptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 序列化发布快照(忽略 null,避免写出旧兼容字段)。
|
||||
/// </summary>
|
||||
public static string? SerializeSnapshotJson<T>(T? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(value, SnapshotJsonOptions);
|
||||
}
|
||||
|
||||
public static SalesProductRulesPublishSnapshotDto? DeserializeRulesSnapshot(string? json)
|
||||
{
|
||||
var snapshot = DeserializeJson<SalesProductRulesPublishSnapshotDto>(json);
|
||||
PublishSnapshotNormalizer.NormalizeRulesSnapshot(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
public static SalesProductPricePublishSnapshotDto? DeserializePriceSnapshot(string? json)
|
||||
{
|
||||
var snapshot = DeserializeJson<SalesProductPricePublishSnapshotDto>(json);
|
||||
PublishSnapshotNormalizer.NormalizePriceSnapshot(snapshot);
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
private static object? ConvertJsonElement(JsonElement element) => element.ValueKind switch
|
||||
{
|
||||
JsonValueKind.Object => element.EnumerateObject()
|
||||
|
||||
@ -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<int?> 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)
|
||||
};
|
||||
}
|
||||
|
||||
50
src/Oms.Application/Shared/PublishSnapshotNormalizer.cs
Normal file
50
src/Oms.Application/Shared/PublishSnapshotNormalizer.cs
Normal file
@ -0,0 +1,50 @@
|
||||
using Oms.Application.Dtos.Orders;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 发布快照读取后规范化(旧字段迁移、去掉冗余)。
|
||||
/// </summary>
|
||||
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<KpiAreaSnapshotRuleDto>? 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
63
src/Oms.Application/Shared/PublishSnapshotSlimmer.cs
Normal file
63
src/Oms.Application/Shared/PublishSnapshotSlimmer.cs
Normal file
@ -0,0 +1,63 @@
|
||||
using Oms.Application.Dtos.Channels;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Domain.ChannelsAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 发布快照瘦身:去掉审计时间等计费无用字段。
|
||||
/// </summary>
|
||||
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
|
||||
};
|
||||
}
|
||||
@ -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
|
||||
/// 从快照内嵌黑名单/偏远明细匹配地址(不查库)。
|
||||
/// </summary>
|
||||
public static bool IsAreaHitFromSnapshot(
|
||||
IReadOnlyList<KpiAreaInfoOutput>? infos,
|
||||
IReadOnlyList<KpiAreaSnapshotRuleDto>? 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
|
||||
/// <summary>
|
||||
/// 将快照内嵌计重明细转为实体供计算器使用。
|
||||
/// </summary>
|
||||
public static List<WeightRuleInfo> ToWeightRuleEntities(IReadOnlyList<WeightRuleInfoOutput> infos) =>
|
||||
infos.Select(MapWeightRuleInfo).ToList();
|
||||
public static List<WeightRuleInfo> ToWeightRuleEntities(IReadOnlyList<WeightRuleSnapshotRuleDto> infos) =>
|
||||
infos.Select(PublishSnapshotSlimmer.ToWeightRuleEntity).ToList();
|
||||
|
||||
/// <summary>
|
||||
/// 将快照内嵌进位区间转为实体供计算器使用。
|
||||
/// </summary>
|
||||
public static List<WeightRoundingInfo> ToWeightRoundingEntities(IReadOnlyList<WeightRoundingInfoOutput> infos) =>
|
||||
infos.Select(MapWeightRoundingInfo).ToList();
|
||||
public static List<WeightRoundingInfo> ToWeightRoundingEntities(IReadOnlyList<WeightRoundingSnapshotRangeDto> infos) =>
|
||||
infos.Select(PublishSnapshotSlimmer.ToWeightRoundingEntity).ToList();
|
||||
|
||||
private static async Task ApplyKpiAreaToSnapshotAsync(
|
||||
SalesProductRulesPublishSnapshotDto snapshot,
|
||||
int? areaId,
|
||||
IKpiAreaRepository kpiAreaRepository,
|
||||
Action<SalesProductRulesPublishSnapshotDto, int?, string?, List<KpiAreaInfoOutput>> assign,
|
||||
Action<SalesProductRulesPublishSnapshotDto, int?, string?, List<KpiAreaSnapshotRuleDto>> 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
|
||||
};
|
||||
}
|
||||
|
||||
23
src/Oms.Tests/KpiAreaSnapshotMapperTests.cs
Normal file
23
src/Oms.Tests/KpiAreaSnapshotMapperTests.cs
Normal file
@ -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]");
|
||||
}
|
||||
}
|
||||
@ -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]"
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
@ -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<ServiceChannelPriceListOutput> BuildPayablePrices(
|
||||
params ServiceChannelPriceListOutput[] prices) => prices.ToList();
|
||||
|
||||
Loading…
Reference in New Issue
Block a user