using System.Globalization;
using System.Text.Json;
using System.Text.RegularExpressions;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.Shared;
namespace Oms.Application.Services.Orders;
///
/// 小包预报:计重、分区匹配、卖价算价。
///
internal static class ParcelBillingCalculator
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true
};
public static decimal CalculateCubic(decimal length, decimal width, decimal height) =>
Math.Round(length * width * height / 1_000_000M, 6, MidpointRounding.AwayFromZero);
public static (decimal VolumetricWeight, decimal RawBillableWeight) CalculateRawBillableWeight(
OrderWeight forecast,
IReadOnlyList rules)
{
var length = forecast.Length ?? 0;
var width = forecast.Width ?? 0;
var height = forecast.Height ?? 0;
var gw = forecast.ActualWeight ?? 0;
if (gw <= 0 || length <= 0 || width <= 0 || height <= 0)
{
throw new InvalidOperationException("件重尺数据不完整");
}
if (rules.Count == 0)
{
throw new InvalidOperationException("计重规则明细为空");
}
foreach (var rule in rules.OrderBy(x => x.Id))
{
if (!decimal.TryParse(rule.VolumetricFactor, NumberStyles.Any, CultureInfo.InvariantCulture, out var factor) || factor <= 0)
{
continue;
}
var vw = Math.Round(length * width * height / factor, 3, MidpointRounding.AwayFromZero);
if (!EvaluateCondition(rule.Condition, gw, vw))
{
continue;
}
var raw = string.Equals(rule.BillableWeight, "gw", StringComparison.OrdinalIgnoreCase) ? gw : vw;
return (vw, raw);
}
throw new InvalidOperationException("计重规则条件均未命中");
}
public static decimal ApplyRounding(decimal rawWeight, IReadOnlyList ranges)
{
var hit = ranges
.OrderBy(x => x.Id)
.FirstOrDefault(x => rawWeight >= x.StartValue && rawWeight <= x.EndValue);
if (hit == null)
{
throw new InvalidOperationException("计重进位区间未匹配");
}
if (!decimal.TryParse(hit.Public, NumberStyles.Any, CultureInfo.InvariantCulture, out var unit) || unit <= 0)
{
throw new InvalidOperationException("计重进位值无效");
}
var units = Math.Ceiling(rawWeight / unit);
return Math.Round(units * unit, 3, MidpointRounding.AwayFromZero);
}
public static bool IsAreaMatched(
KpiAreaInfo 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;
}
if (!string.IsNullOrWhiteSpace(rule.LeftPostcode) || !string.IsNullOrWhiteSpace(rule.RightPostcode))
{
if (string.IsNullOrWhiteSpace(zipCode))
{
return false;
}
if (!IsPostcodeInRange(zipCode, rule.LeftPostcode, rule.RightPostcode))
{
return false;
}
}
return true;
}
public static bool IsAreaMatched(
KpiAreaInfoOutput rule,
string countryCode,
string? province,
string? city,
string? zipCode) =>
IsAreaMatched(MapInfoOutputToEntity(rule), countryCode, province, city, zipCode);
///
/// 从卖价快照内固化的分区规则解析分区键(不查库)。
///
public static IReadOnlyList? TryResolveZoneKeysFromPriceSnapshot(
SalesProductPricePublishSnapshotDto? priceSnapshot,
string countryCode,
string? province,
string? city,
string? zipCode)
{
if (priceSnapshot?.PriceAreaInfos == null || priceSnapshot.PriceAreaInfos.Count == 0)
{
return null;
}
var matched = priceSnapshot.PriceAreaInfos.FirstOrDefault(rule =>
KpiAreaSnapshotMapper.IsRuleMatched(rule, countryCode, province, city, zipCode));
if (matched == null)
{
return null;
}
var keys = BuildPriceZoneKeys(matched, priceSnapshot.PriceAreaName, priceSnapshot.PriceAreaId);
return keys.Count > 0 ? keys : null;
}
///
/// 查价仅填国家时:若快照已配置该目的国分区(即使规则带邮编区间),
/// 则返回卖价明细中的分区键供估算报价;未配置该国家时返回空(不跨国家兜底)。
///
public static IReadOnlyList CollectCountryEstimateZoneKeys(
SalesProductPricePublishSnapshotDto? priceSnapshot,
string countryCode)
{
if (priceSnapshot?.PriceAreaInfos == null
|| priceSnapshot.PriceAreaInfos.Count == 0
|| string.IsNullOrWhiteSpace(countryCode))
{
return [];
}
var countryConfigured = priceSnapshot.PriceAreaInfos.Any(rule =>
string.IsNullOrWhiteSpace(rule.CountryCode)
|| string.Equals(rule.CountryCode, countryCode, StringComparison.OrdinalIgnoreCase));
if (!countryConfigured)
{
return [];
}
return CollectDistinctDetailZoneKeys(priceSnapshot);
}
public static string? ResolveZoneName(KpiAreaInfo? matched, KpiArea? area)
{
if (matched == null)
{
return null;
}
if (!string.IsNullOrWhiteSpace(matched.Province))
{
return matched.Province;
}
if (!string.IsNullOrWhiteSpace(matched.City))
{
return matched.City;
}
if (!string.IsNullOrWhiteSpace(matched.CountryName))
{
return matched.CountryName;
}
return area?.Name;
}
public static IReadOnlyList BuildPriceZoneKeys(KpiAreaInfo? matched, KpiArea? area)
{
var keys = new List();
if (matched != null)
{
var name = ResolveZoneName(matched, area);
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.CountryName))
{
keys.Add(matched.CountryName);
}
}
if (area != null)
{
keys.Add(area.Id.ToString(CultureInfo.InvariantCulture));
if (!string.IsNullOrWhiteSpace(area.Name))
{
keys.Add(area.Name);
}
if (!string.IsNullOrWhiteSpace(area.SystemName))
{
keys.Add(area.SystemName);
}
}
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,
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.CountryName))
{
keys.Add(matched.CountryName);
}
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(KpiAreaInfoOutput matched, string? areaName)
{
if (!string.IsNullOrWhiteSpace(matched.Province))
{
return matched.Province;
}
if (!string.IsNullOrWhiteSpace(matched.City))
{
return matched.City;
}
if (!string.IsNullOrWhiteSpace(matched.CountryName))
{
return matched.CountryName;
}
return areaName;
}
private static KpiAreaInfo MapInfoOutputToEntity(KpiAreaInfoOutput rule) =>
new()
{
Id = rule.Id,
AreaId = rule.AreaId,
CountryCode = rule.CountryCode,
CountryName = rule.CountryName,
Province = rule.Province,
City = rule.City,
LeftPostcode = rule.LeftPostcode,
RightPostcode = rule.RightPostcode
};
///
/// 卖价/买价明细匹配共用上下文(分区、货物属性、计费重、仓库)。
///
internal readonly record struct PriceDetailMatchContext(
IReadOnlyList ZoneKeys,
string CargoAttribute,
decimal ChargeableWeight,
int? WarehouseId = null,
string? WarehouseName = null);
public static List CalculateQuoteReceivableFees(
SalesProductPricePublishSnapshotDto priceSnapshot,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
string? priceTierKey,
int? warehouseId = null,
string? warehouseName = null) =>
CalculateQuoteReceivableFeeLines(
priceSnapshot,
priceVersion,
priceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
priceTierKey,
warehouseId,
warehouseName).Select(x => x.Fee).ToList();
public static List CalculateQuoteReceivableFeeLines(
SalesProductPricePublishSnapshotDto priceSnapshot,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
string? priceTierKey,
int? warehouseId = null,
string? warehouseName = null)
{
var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem(
p.Status, p.CostItem, p.PriceName, p.Currency, p.Details));
return CalculateReceivableFeeLines(
items,
priceVersion,
priceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
priceTierKey,
OrderFeeChargingStageCodes.Forecast,
warehouseId,
warehouseName);
}
public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
HasEnabledReceivablePriceItems(priceSnapshot);
public static bool HasEnabledReceivablePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
priceSnapshot.Prices.Any(x => EnableStatusRules.IsEnabled(x.Status));
///
/// 收集卖价快照中所有启用价格项的明细分区键(去重)。
///
public static IReadOnlyList CollectDistinctDetailZoneKeys(SalesProductPricePublishSnapshotDto priceSnapshot)
{
var zones = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var price in priceSnapshot.Prices.Where(x => EnableStatusRules.IsEnabled(x.Status)))
{
foreach (var detail in price.Details ?? [])
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
if (TryGetZoneFactorText(factors, out var zone))
{
zones.Add(zone.Trim());
}
}
}
return zones.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList();
}
///
/// 查价未命中时,按价格项输出具体原因(分区/货物类型/重量区间等)。
///
public static string ExplainQuotePriceMiss(
SalesProductPricePublishSnapshotDto priceSnapshot,
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
string? priceTierKey,
int? warehouseId = null,
string? warehouseName = null)
{
var cargoLabel = GetCargoAttributeLabel(cargoAttribute);
var warehouseLabel = FormatWarehouseLabel(warehouseId, warehouseName);
var enabledPrices = priceSnapshot.Prices.Where(x => EnableStatusRules.IsEnabled(x.Status)).ToList();
if (enabledPrices.Count == 0)
{
return "卖价快照中无启用的价格项";
}
var reasons = new List();
foreach (var price in enabledPrices)
{
var label = !string.IsNullOrWhiteSpace(price.PriceName) ? price.PriceName! : price.CostItem;
var details = price.Details ?? [];
if (details.Count == 0)
{
reasons.Add($"【{label}】价格项Id={price.Id}:无明细行");
continue;
}
var zoneMatchedDetails = details.Where(detail =>
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
return ZoneMatches(factors, zoneKeys);
}).ToList();
if (zoneMatchedDetails.Count == 0)
{
var detailZones = details
.Select(detail =>
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
return TryGetZoneFactorText(factors, out var zone) ? zone.Trim() : null;
})
.Where(zone => !string.IsNullOrWhiteSpace(zone))
.Distinct(StringComparer.OrdinalIgnoreCase);
reasons.Add(
$"【{label}】价格项Id={price.Id}:分区不匹配,明细分区=[{string.Join(", ", detailZones)}]," +
$"解析分区键=[{string.Join(", ", zoneKeys)}]");
continue;
}
var cargoMatchedDetails = zoneMatchedDetails.Where(detail =>
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
return MatchCargoTypeFactor(factors, cargoAttribute);
}).ToList();
if (cargoMatchedDetails.Count == 0)
{
var requiredCargo = zoneMatchedDetails
.Select(detail =>
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
return TryGetFactorValue(factors, out var cargo, "货物类型", "cargo") ? cargo.Trim() : null;
})
.Where(cargo => !string.IsNullOrWhiteSpace(cargo))
.Distinct(StringComparer.OrdinalIgnoreCase);
var requiredText = requiredCargo.Any() ? string.Join(", ", requiredCargo) : "(明细未限定货物类型)";
reasons.Add($"【{label}】价格项Id={price.Id}:货物类型不匹配,当前={cargoLabel},明细要求=[{requiredText}]");
continue;
}
var warehouseMatchedDetails = cargoMatchedDetails.Where(detail =>
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
return MatchWarehouseFactor(factors, warehouseId, warehouseName);
}).ToList();
if (warehouseMatchedDetails.Count == 0)
{
var requiredWarehouse = cargoMatchedDetails
.Select(detail =>
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
return TryGetFactorValue(factors, out var wh, "仓库", "warehouse") ? wh.Trim() : null;
})
.Where(wh => !string.IsNullOrWhiteSpace(wh))
.Distinct(StringComparer.OrdinalIgnoreCase);
var requiredText = requiredWarehouse.Any() ? string.Join(", ", requiredWarehouse) : "(明细未限定仓库)";
reasons.Add(
$"【{label}】价格项Id={price.Id}:仓库不匹配,当前={warehouseLabel},明细要求=[{requiredText}]");
continue;
}
var weightMatched = false;
var intervals = new List();
foreach (var detail in warehouseMatchedDetails)
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
if (!TryGetWeightFactorText(factors, out var weightText))
{
weightMatched = true;
break;
}
intervals.Add(weightText);
if (TryParseWeightInterval(weightText, out var rangeStart, out var rangeEnd)
&& WeightInLeftOpenRightClosedInterval(chargeableWeight, rangeStart, rangeEnd))
{
weightMatched = true;
break;
}
}
if (!weightMatched)
{
var intervalText = intervals.Count > 0
? string.Join(", ", intervals.Distinct(StringComparer.OrdinalIgnoreCase))
: "无有效重量区间";
reasons.Add(
$"【{label}】价格项Id={price.Id}:计费重 {chargeableWeight}kg 未落入重量区间 [{intervalText}](左开右闭 (start,end])");
continue;
}
var priceTierKeyLabel = string.IsNullOrWhiteSpace(priceTierKey) ? "未登录/默认" : priceTierKey;
reasons.Add(
$"【{label}】价格项Id={price.Id}:明细已匹配但金额为0或客户组「{priceTierKeyLabel}」无对应单价");
}
return string.Join(";", reasons);
}
public static List CalculateForecastReceivableFees(
SalesProductPricePublishSnapshotDto priceSnapshot,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
string zoneName,
string cargoAttribute,
string? priceTierKey) =>
CalculateReceivableFees(
priceSnapshot,
priceVersion,
priceEffectiveTime,
chargeableWeight,
new[] { zoneName },
cargoAttribute,
priceTierKey,
OrderFeeChargingStageCodes.Forecast);
public static List CalculateForecastReceivableFees(
SalesProductPricePublishSnapshotDto priceSnapshot,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
string? priceTierKey) =>
CalculateReceivableFees(
priceSnapshot,
priceVersion,
priceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
priceTierKey,
OrderFeeChargingStageCodes.Forecast);
///
/// 应收费用:卖价全部启用项参与;ChargingStage 由件重尺阶段决定。
///
public static List CalculateReceivableFees(
SalesProductPricePublishSnapshotDto priceSnapshot,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
string? priceTierKey,
string chargingStage,
int? warehouseId = null,
string? warehouseName = null)
{
var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem(
p.Status, p.CostItem, p.PriceName, p.Currency, p.Details));
return CalculateReceivableFeesFromItems(
items,
priceVersion,
priceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
priceTierKey,
chargingStage,
warehouseId,
warehouseName);
}
///
/// 应付费用:买价全部启用项参与;渠道买价不区分客户组。
///
public static List CalculatePayableFees(
IReadOnlyList prices,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
string chargingStage,
int? warehouseId = null,
string? warehouseName = null)
{
var items = prices.Select(p => new PayableBillingPriceItem(
p.Status, p.CostItem, p.Currency, p.Details));
return CalculatePayableFeesFromItems(
items,
priceVersion,
priceEffectiveTime,
new PriceDetailMatchContext(zoneKeys, cargoAttribute, chargeableWeight, warehouseId, warehouseName),
chargingStage);
}
///
/// 预报阶段应付费用(买价快照)。
///
public static List CalculateForecastPayableFees(
IReadOnlyList prices,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
string zoneName,
string cargoAttribute,
int? warehouseId = null,
string? warehouseName = null) =>
CalculatePayableFees(
prices,
priceVersion,
priceEffectiveTime,
chargeableWeight,
new[] { zoneName },
cargoAttribute,
OrderFeeChargingStageCodes.Forecast,
warehouseId,
warehouseName);
public static bool HasEnabledForecastPriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
HasEnabledReceivablePriceItems(priceSnapshot);
public static bool HasEnabledPayablePriceItems(IReadOnlyList prices) =>
prices.Any(x => EnableStatusRules.IsEnabled(x.Status));
public static bool HasEnabledForecastPayablePriceItems(IReadOnlyList prices) =>
HasEnabledPayablePriceItems(prices);
public static string GetCargoAttributeLabel(string? cargoAttribute) => cargoAttribute switch
{
CargoAttributeCodes.Electronics => "3c&带电",
CargoAttributeCodes.Sensitive => "敏感货",
_ => "普货"
};
public static string MapCargoTypeToAttribute(string? cargoType)
{
if (string.IsNullOrWhiteSpace(cargoType))
{
return CargoAttributeCodes.General;
}
var value = cargoType.Trim();
if (value.Contains("敏感", StringComparison.OrdinalIgnoreCase))
{
return CargoAttributeCodes.Sensitive;
}
if (value.Contains("带电", StringComparison.OrdinalIgnoreCase)
|| value.Contains("3c", StringComparison.OrdinalIgnoreCase)
|| value.Equals("electronics", StringComparison.OrdinalIgnoreCase))
{
return CargoAttributeCodes.Electronics;
}
if (value.Equals(CargoAttributeCodes.General, StringComparison.OrdinalIgnoreCase)
|| value.Equals(CargoAttributeCodes.Electronics, StringComparison.OrdinalIgnoreCase)
|| value.Equals(CargoAttributeCodes.Sensitive, StringComparison.OrdinalIgnoreCase))
{
return value.ToLowerInvariant();
}
return CargoAttributeCodes.General;
}
public static (decimal Freight, decimal OtherFees, decimal TotalCost, string CostFormula) SummarizeFees(
IReadOnlyList fees)
{
var summary = SummarizeQuoteFees(fees.Select(f => new BillingFeeLine
{
Fee = f,
PriceName = ResolveFeeDisplayName(f.CostItem, null),
FormulaPart = FormatDecimal(f.Amount)
}).ToList());
return (summary.Freight, summary.OtherFees, summary.TotalCost, summary.CostFormula);
}
public static QuoteFeeSummary SummarizeQuoteFees(IReadOnlyList feeLines)
{
if (feeLines.Count == 0)
{
return new QuoteFeeSummary();
}
var freightLines = feeLines.Where(x => IsFreightCostItem(x.Fee.CostItem)).ToList();
var otherLines = feeLines.Where(x => !IsFreightCostItem(x.Fee.CostItem)).ToList();
var freight = freightLines.Sum(x => x.Fee.Amount);
var otherFees = otherLines.Sum(x => x.Fee.Amount);
var total = feeLines.Sum(x => x.Fee.Amount);
var formulaParts = feeLines
.Select(x => x.FormulaPart)
.Where(part => !string.IsNullOrWhiteSpace(part))
.ToList();
var costFormula = formulaParts.Count > 0 ? string.Join("+", formulaParts) : FormatDecimal(total);
return new QuoteFeeSummary
{
Freight = freight,
OtherFees = otherFees,
TotalCost = total,
CostFormula = costFormula,
OtherFeeList = otherLines.Select(x => new OrderPriceCheckFeeOutput
{
CostItem = x.Fee.CostItem,
Name = ResolveFeeDisplayName(x.Fee.CostItem, x.PriceName),
Amount = x.Fee.Amount,
FormulaPart = x.FormulaPart
}).ToList()
};
}
private static bool IsFreightCostItem(string costItem) =>
costItem.Equals("freight", StringComparison.OrdinalIgnoreCase)
|| costItem.Contains("运费", StringComparison.OrdinalIgnoreCase);
internal static string ResolveFeeDisplayName(string costItem, string? priceName)
{
if (!string.IsNullOrWhiteSpace(priceName))
{
return priceName.Trim();
}
return costItem.ToLowerInvariant() switch
{
"freight" => "运费",
"handling" => "操作费",
_ => costItem
};
}
internal static string FormatDecimal(decimal value) =>
value.ToString("0.####", CultureInfo.InvariantCulture);
public static OrderWeight BuildForecastWeight(ParcelQuoteContext context) =>
new()
{
Type = OrderWeightTypeCodes.Forecast,
Length = context.Length,
Width = context.Width,
Height = context.Height,
ActualWeight = context.ActualWeight,
Cubic = CalculateCubic(context.Length, context.Width, context.Height)
};
private static List CalculateReceivableFeesFromItems(
IEnumerable priceItems,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
string? priceTierKey,
string chargingStage,
int? warehouseId = null,
string? warehouseName = null)
{
if (chargeableWeight <= 0)
{
return [];
}
return CalculateReceivableFeeLines(
priceItems,
priceVersion,
priceEffectiveTime,
chargeableWeight,
zoneKeys,
cargoAttribute,
priceTierKey,
chargingStage,
warehouseId,
warehouseName).Select(x => x.Fee).ToList();
}
private static List CalculateReceivableFeeLines(
IEnumerable priceItems,
string priceVersion,
DateTime priceEffectiveTime,
decimal chargeableWeight,
IReadOnlyList zoneKeys,
string cargoAttribute,
string? priceTierKey,
string chargingStage,
int? warehouseId = null,
string? warehouseName = null)
{
var fees = new List();
foreach (var priceItem in priceItems.Where(x => EnableStatusRules.IsEnabled(x.Status)))
{
var detail = MatchPriceDetail(
priceItem.Details,
zoneKeys,
cargoAttribute,
chargeableWeight,
warehouseId,
warehouseName);
if (detail == null)
{
continue;
}
var amount = CalculateDetailAmount(
priceItem.Details,
detail,
zoneKeys,
cargoAttribute,
chargeableWeight,
priceTierKey,
warehouseId,
warehouseName);
var formulaPart = BuildDetailFormulaExpression(
priceItem.Details,
detail,
zoneKeys,
cargoAttribute,
chargeableWeight,
priceTierKey,
warehouseId,
warehouseName);
fees.Add(new BillingFeeLine
{
Fee = new OrderFee
{
Type = OrderFeeTypeCodes.Receivable,
CostItem = priceItem.CostItem,
Amount = amount,
Currency = priceItem.Currency,
PriceNo = priceVersion,
PriceEffectiveTime = priceEffectiveTime,
ChargingStage = chargingStage
},
PriceName = priceItem.PriceName,
FormulaPart = formulaPart
});
}
return fees;
}
private static List CalculatePayableFeesFromItems(
IEnumerable priceItems,
string priceVersion,
DateTime priceEffectiveTime,
PriceDetailMatchContext match,
string chargingStage)
{
if (match.ChargeableWeight <= 0)
{
return [];
}
var fees = new List();
foreach (var priceItem in priceItems.Where(x => EnableStatusRules.IsEnabled(x.Status)))
{
var detail = MatchPriceDetail(
priceItem.Details,
match.ZoneKeys,
match.CargoAttribute,
match.ChargeableWeight,
match.WarehouseId,
match.WarehouseName);
if (detail == null)
{
continue;
}
var amount = CalculatePayableDetailAmount(priceItem.Details, detail, match);
if (amount <= 0)
{
continue;
}
fees.Add(new OrderFee
{
Type = OrderFeeTypeCodes.Payable,
CostItem = priceItem.CostItem,
Amount = amount,
Currency = priceItem.Currency,
PriceNo = priceVersion,
PriceEffectiveTime = priceEffectiveTime,
ChargingStage = chargingStage
});
}
return fees;
}
///
/// 渠道买价明细金额:与下游服务商结算,仅使用 CalculatedValue / 续重配置,不区分客户组。
///
private static decimal CalculatePayableDetailAmount(
IReadOnlyList allDetails,
ChannelPriceDetailOutput detail,
PriceDetailMatchContext match)
{
return NormalizeCalculationFormula(detail.CalculationFormula) switch
{
"unit" => CalculateBuyUnitPrice(detail, match.ChargeableWeight),
"total" => ResolveBuyPrice(detail),
"continue" => CalculateBuyContinueAmount(allDetails, detail, match),
_ => ResolveBuyPrice(detail)
};
}
private static decimal ResolveBuyPrice(ChannelPriceDetailOutput detail) =>
detail.CalculatedValue ?? 0;
private static decimal CalculateBuyUnitPrice(ChannelPriceDetailOutput detail, decimal chargeableWeight)
{
var unitPrice = ResolveBuyPrice(detail);
return Math.Round(chargeableWeight * unitPrice, 4, MidpointRounding.AwayFromZero);
}
private static decimal CalculateBuyContinueAmount(
IReadOnlyList allDetails,
ChannelPriceDetailOutput detail,
PriceDetailMatchContext match)
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
if (TryGetWeightFactorText(factors, out var weightText)
&& TryParseWeightInterval(weightText, out var rangeStart, out _)
&& TryFindAdjacentBuyTotalBasePrice(allDetails, match, rangeStart, out var basePrice))
{
if (match.ChargeableWeight <= rangeStart)
{
return Math.Round(basePrice, 4, MidpointRounding.AwayFromZero);
}
var continueUnitPrice = ResolveBuyPrice(detail);
return Math.Round(
basePrice + (match.ChargeableWeight - rangeStart) * continueUnitPrice,
4,
MidpointRounding.AwayFromZero);
}
return CalculateBuyContinuePrice(detail, match.ChargeableWeight);
}
private static bool TryFindAdjacentBuyTotalBasePrice(
IReadOnlyList allDetails,
PriceDetailMatchContext match,
decimal continueRangeStart,
out decimal basePrice)
{
basePrice = 0;
foreach (var candidate in allDetails)
{
var factors = ParseFactorDictionary(candidate.FactorValuesJson);
if (!ZoneMatches(factors, match.ZoneKeys)
|| !MatchCargoTypeFactor(factors, match.CargoAttribute)
|| !MatchWarehouseFactor(factors, match.WarehouseId, match.WarehouseName))
{
continue;
}
if (NormalizeCalculationFormula(candidate.CalculationFormula) != "total")
{
continue;
}
if (!TryGetWeightFactorText(factors, out var weightText)
|| !TryParseWeightInterval(weightText, out _, out var rangeEnd))
{
continue;
}
if (rangeEnd != continueRangeStart)
{
continue;
}
basePrice = ResolveBuyPrice(candidate);
return true;
}
return false;
}
private static decimal CalculateBuyContinuePrice(
ChannelPriceDetailOutput detail,
decimal chargeableWeight)
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
var continuePricing = TryReadContinuePricing(factors, detail);
var firstWeight = continuePricing?.FirstWeight ?? 1M;
var firstPrice = continuePricing?.FirstPrice ?? ResolveBuyPrice(detail);
var continueUnit = continuePricing?.ContinueUnit ?? 0.5M;
var continuePrice = continuePricing?.ContinuePrice ?? firstPrice;
if (chargeableWeight <= firstWeight)
{
return Math.Round(firstPrice, 4, MidpointRounding.AwayFromZero);
}
var overflow = chargeableWeight - firstWeight;
var continueUnits = Math.Ceiling(overflow / continueUnit);
return Math.Round(firstPrice + continueUnits * continuePrice, 4, MidpointRounding.AwayFromZero);
}
private readonly record struct ReceivableBillingPriceItem(
byte Status,
string CostItem,
string PriceName,
string Currency,
List Details);
private readonly record struct PayableBillingPriceItem(
byte Status,
string CostItem,
string Currency,
List Details);
private static ChannelPriceDetailOutput? MatchPriceDetail(
IReadOnlyList details,
IReadOnlyList zoneKeys,
string cargoAttribute,
decimal chargeableWeight,
int? warehouseId = null,
string? warehouseName = null)
{
ChannelPriceDetailOutput? bestMatch = null;
var bestIntervalWidth = decimal.MaxValue;
foreach (var detail in details)
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
if (!ZoneMatches(factors, zoneKeys))
{
continue;
}
if (!MatchCargoTypeFactor(factors, cargoAttribute))
{
continue;
}
if (!MatchWarehouseFactor(factors, warehouseId, warehouseName))
{
continue;
}
if (!TryGetWeightFactorText(factors, out var weightText))
{
if (bestMatch == null)
{
bestMatch = detail;
}
continue;
}
if (!TryParseWeightInterval(weightText, out var rangeStart, out var rangeEnd))
{
continue;
}
if (!WeightInLeftOpenRightClosedInterval(chargeableWeight, rangeStart, rangeEnd))
{
continue;
}
var width = rangeEnd - rangeStart;
if (width < bestIntervalWidth)
{
bestMatch = detail;
bestIntervalWidth = width;
}
}
return bestMatch;
}
///
/// 解析卖价明细重量区间,遵循左开右闭 (start, end]。
/// 单值如 0.5 兼容为 (0, 0.5];显式区间支持 0,0.5 / 0-0.5。
///
internal static bool TryParseWeightInterval(string? weightText, out decimal start, out decimal end)
{
start = 0;
end = 0;
if (string.IsNullOrWhiteSpace(weightText))
{
return false;
}
var text = weightText.Trim()
.Trim('[', ']', '(', ')');
char[] separators = [',', '-', ',', '~', '~'];
var parts = text.Split(separators, StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
if (parts.Length == 2
&& decimal.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out start)
&& decimal.TryParse(parts[1], NumberStyles.Any, CultureInfo.InvariantCulture, out end))
{
return end > start;
}
if (parts.Length == 1
&& decimal.TryParse(parts[0], NumberStyles.Any, CultureInfo.InvariantCulture, out end)
&& end > 0)
{
start = 0;
return true;
}
return false;
}
internal static bool WeightInLeftOpenRightClosedInterval(decimal chargeableWeight, decimal start, decimal end) =>
chargeableWeight > start && chargeableWeight <= end;
private static bool ZoneMatches(Dictionary factors, IReadOnlyList zoneKeys)
{
if (!TryGetZoneFactorText(factors, out var zoneValue))
{
return true;
}
var expected = zoneValue.Trim();
return zoneKeys.Any(key => string.Equals(key.Trim(), expected, StringComparison.OrdinalIgnoreCase));
}
private static bool TryGetZoneFactorText(Dictionary factors, out string zoneValue)
{
if (TryGetFactorValue(factors, out zoneValue, "分区", "价格分区", "zone"))
{
return !string.IsNullOrWhiteSpace(zoneValue);
}
zoneValue = string.Empty;
return false;
}
private static bool TryGetWeightFactorText(Dictionary factors, out string weightText) =>
TryGetFactorValue(factors, out weightText, "重量", "weight") && !string.IsNullOrWhiteSpace(weightText);
///
/// 货物类型因子匹配:明细可能存中文标签(普货)或英文 code(general),查价侧统一为 cargoAttribute。
///
private static bool MatchCargoTypeFactor(Dictionary factors, string cargoAttribute)
{
if (!TryGetFactorValue(factors, out var actual, "货物类型", "cargo") || string.IsNullOrWhiteSpace(actual))
{
return true;
}
var trimmed = actual.Trim();
var expectedLabel = GetCargoAttributeLabel(cargoAttribute);
if (string.Equals(trimmed, expectedLabel, StringComparison.OrdinalIgnoreCase))
{
return true;
}
if (string.Equals(trimmed, cargoAttribute, StringComparison.OrdinalIgnoreCase))
{
return true;
}
var actualAttribute = MapCargoTypeToAttribute(trimmed);
return string.Equals(actualAttribute, cargoAttribute, StringComparison.OrdinalIgnoreCase);
}
private static bool MatchWarehouseFactor(
Dictionary factors,
int? warehouseId,
string? warehouseName)
{
if (!TryGetFactorValue(factors, out var actual, "仓库", "warehouse") || string.IsNullOrWhiteSpace(actual))
{
return true;
}
var expected = actual.Trim();
if (warehouseId.HasValue && string.Equals(expected, warehouseId.Value.ToString(), StringComparison.Ordinal))
{
return true;
}
if (!string.IsNullOrWhiteSpace(warehouseName)
&& string.Equals(expected, warehouseName.Trim(), StringComparison.OrdinalIgnoreCase))
{
return true;
}
return false;
}
private static string FormatWarehouseLabel(int? warehouseId, string? warehouseName)
{
if (!string.IsNullOrWhiteSpace(warehouseName))
{
return warehouseName.Trim();
}
return warehouseId.HasValue ? warehouseId.Value.ToString() : "未指定";
}
private static bool TryGetFactorValue(
Dictionary factors,
out string value,
params string[] keys)
{
foreach (var key in keys)
{
if (factors.TryGetValue(key, out var hit) && !string.IsNullOrWhiteSpace(hit))
{
value = hit;
return true;
}
}
value = string.Empty;
return false;
}
private static Dictionary ParseFactorDictionary(object? factorValuesJson)
{
if (factorValuesJson == null)
{
return new Dictionary(StringComparer.OrdinalIgnoreCase);
}
if (factorValuesJson is JsonElement element)
{
return element.EnumerateObject()
.ToDictionary(x => x.Name, x => x.Value.ToString(), StringComparer.OrdinalIgnoreCase);
}
var json = JsonSerializer.Serialize(factorValuesJson, JsonOptions);
return JsonSerializer.Deserialize>(json, JsonOptions)
?? new Dictionary(StringComparer.OrdinalIgnoreCase);
}
private static bool MatchFactorValue(Dictionary factors, string key, string expected)
{
if (!factors.TryGetValue(key, out var actual) || string.IsNullOrWhiteSpace(actual))
{
return true;
}
return string.Equals(actual.Trim(), expected, StringComparison.OrdinalIgnoreCase);
}
private static decimal CalculateDetailAmount(
IReadOnlyList allDetails,
ChannelPriceDetailOutput detail,
IReadOnlyList zoneKeys,
string cargoAttribute,
decimal chargeableWeight,
string? priceTierKey,
int? warehouseId = null,
string? warehouseName = null)
{
return NormalizeCalculationFormula(detail.CalculationFormula) switch
{
"unit" => CalculateUnitPrice(detail, chargeableWeight, priceTierKey),
"total" => ResolvePriceTierKeyPrice(detail, priceTierKey) ?? detail.CalculatedValue ?? 0,
"continue" => CalculateContinueAmount(
allDetails,
detail,
zoneKeys,
cargoAttribute,
chargeableWeight,
priceTierKey,
warehouseId,
warehouseName),
_ => detail.CalculatedValue ?? 0
};
}
internal static string BuildDetailFormulaExpression(
IReadOnlyList allDetails,
ChannelPriceDetailOutput detail,
IReadOnlyList zoneKeys,
string cargoAttribute,
decimal chargeableWeight,
string? priceTierKey,
int? warehouseId = null,
string? warehouseName = null)
{
return NormalizeCalculationFormula(detail.CalculationFormula) switch
{
"unit" => BuildUnitFormulaExpression(detail, chargeableWeight, priceTierKey),
"total" => FormatDecimal(
ResolvePriceTierKeyPrice(detail, priceTierKey) ?? detail.CalculatedValue ?? 0),
"continue" => BuildContinueFormulaExpression(
allDetails,
detail,
zoneKeys,
cargoAttribute,
chargeableWeight,
priceTierKey,
warehouseId,
warehouseName),
_ => FormatDecimal(detail.CalculatedValue ?? 0)
};
}
private static string BuildUnitFormulaExpression(
ChannelPriceDetailOutput detail,
decimal chargeableWeight,
string? priceTierKey)
{
var unitPrice = ResolvePriceTierKeyPrice(detail, priceTierKey) ?? detail.CalculatedValue ?? 0;
return $"{FormatDecimal(unitPrice)}*{FormatDecimal(chargeableWeight)}";
}
private static string BuildContinueFormulaExpression(
IReadOnlyList allDetails,
ChannelPriceDetailOutput detail,
IReadOnlyList zoneKeys,
string cargoAttribute,
decimal chargeableWeight,
string? priceTierKey,
int? warehouseId = null,
string? warehouseName = null)
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
if (TryGetWeightFactorText(factors, out var weightText)
&& TryParseWeightInterval(weightText, out var rangeStart, out _)
&& TryFindAdjacentTotalBasePrice(
allDetails,
zoneKeys,
cargoAttribute,
rangeStart,
priceTierKey,
warehouseId,
warehouseName,
out var basePrice))
{
if (chargeableWeight <= rangeStart)
{
return FormatDecimal(basePrice);
}
var continueUnitPrice = ResolvePriceTierKeyPrice(detail, priceTierKey) ?? detail.CalculatedValue ?? 0;
return
$"{FormatDecimal(basePrice)}+({FormatDecimal(chargeableWeight)}-{FormatDecimal(rangeStart)})*{FormatDecimal(continueUnitPrice)}";
}
var continuePricing = TryReadContinuePricing(factors, detail);
var firstWeight = continuePricing?.FirstWeight ?? 1M;
var firstPrice = continuePricing?.FirstPrice ?? detail.CalculatedValue ?? 0;
var continueUnit = continuePricing?.ContinueUnit ?? 0.5M;
var continuePrice = continuePricing?.ContinuePrice ?? firstPrice;
if (!string.IsNullOrWhiteSpace(priceTierKey))
{
var groupPrice = ResolvePriceTierKeyPrice(detail, priceTierKey);
if (groupPrice.HasValue)
{
firstPrice = groupPrice.Value;
}
}
if (chargeableWeight <= firstWeight)
{
return FormatDecimal(firstPrice);
}
var overflow = chargeableWeight - firstWeight;
var continueUnits = Math.Ceiling(overflow / continueUnit);
return
$"{FormatDecimal(firstPrice)}+{FormatDecimal(continueUnits)}*{FormatDecimal(continuePrice)}";
}
///
/// 单价:计费重 × 单价。
///
private static decimal CalculateUnitPrice(
ChannelPriceDetailOutput detail,
decimal chargeableWeight,
string? priceTierKey)
{
var unitPrice = ResolvePriceTierKeyPrice(detail, priceTierKey) ?? detail.CalculatedValue ?? 0;
return Math.Round(chargeableWeight * unitPrice, 4, MidpointRounding.AwayFromZero);
}
///
/// 续单价:优先「下档总价 + (计费重-区间起点)×续单价」;否则回退单行 continuePricing(首重/续重进位)。
///
private static decimal CalculateContinueAmount(
IReadOnlyList allDetails,
ChannelPriceDetailOutput detail,
IReadOnlyList zoneKeys,
string cargoAttribute,
decimal chargeableWeight,
string? priceTierKey,
int? warehouseId = null,
string? warehouseName = null)
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
if (TryGetWeightFactorText(factors, out var weightText)
&& TryParseWeightInterval(weightText, out var rangeStart, out _)
&& TryFindAdjacentTotalBasePrice(
allDetails,
zoneKeys,
cargoAttribute,
rangeStart,
priceTierKey,
warehouseId,
warehouseName,
out var basePrice))
{
var continueUnitPrice = ResolvePriceTierKeyPrice(detail, priceTierKey) ?? detail.CalculatedValue ?? 0;
if (chargeableWeight <= rangeStart)
{
return Math.Round(basePrice, 4, MidpointRounding.AwayFromZero);
}
return Math.Round(
basePrice + (chargeableWeight - rangeStart) * continueUnitPrice,
4,
MidpointRounding.AwayFromZero);
}
return CalculateContinuePrice(detail, chargeableWeight, priceTierKey);
}
///
/// 查找与续单价区间左端点衔接的下一档总价(如 (0,0.5] 总价 10 对应 (0.5,1] 续单价)。
///
private static bool TryFindAdjacentTotalBasePrice(
IReadOnlyList allDetails,
IReadOnlyList zoneKeys,
string cargoAttribute,
decimal continueRangeStart,
string? priceTierKey,
int? warehouseId,
string? warehouseName,
out decimal basePrice)
{
basePrice = 0;
foreach (var candidate in allDetails)
{
var factors = ParseFactorDictionary(candidate.FactorValuesJson);
if (!ZoneMatches(factors, zoneKeys)
|| !MatchCargoTypeFactor(factors, cargoAttribute)
|| !MatchWarehouseFactor(factors, warehouseId, warehouseName))
{
continue;
}
if (NormalizeCalculationFormula(candidate.CalculationFormula) != "total")
{
continue;
}
if (!TryGetWeightFactorText(factors, out var weightText)
|| !TryParseWeightInterval(weightText, out _, out var rangeEnd))
{
continue;
}
if (rangeEnd != continueRangeStart)
{
continue;
}
basePrice = ResolvePriceTierKeyPrice(candidate, priceTierKey) ?? candidate.CalculatedValue ?? 0;
return true;
}
return false;
}
///
/// 计算公式字典编码归一化(calculation_formula)。
///
internal static string NormalizeCalculationFormula(string? formula)
{
var value = formula?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(value))
{
return string.Empty;
}
return value.ToLowerInvariant() switch
{
"unit" or "单价" => "unit",
"total" or "总价" or "固定价" => "total",
"continueunit" or "continue_unit" or "continueprice" or "continue_price" or "续单价" or "续重价" => "continue",
_ => value.ToLowerInvariant()
};
}
private static decimal CalculateContinuePrice(
ChannelPriceDetailOutput detail,
decimal chargeableWeight,
string? priceTierKey)
{
var factors = ParseFactorDictionary(detail.FactorValuesJson);
var continuePricing = TryReadContinuePricing(factors, detail);
var firstWeight = continuePricing?.FirstWeight ?? 1M;
var firstPrice = continuePricing?.FirstPrice ?? detail.CalculatedValue ?? 0;
var continueUnit = continuePricing?.ContinueUnit ?? 0.5M;
var continuePrice = continuePricing?.ContinuePrice ?? firstPrice;
if (!string.IsNullOrWhiteSpace(priceTierKey))
{
var groupPrice = ResolvePriceTierKeyPrice(detail, priceTierKey);
if (groupPrice.HasValue)
{
firstPrice = groupPrice.Value;
}
}
if (chargeableWeight <= firstWeight)
{
return Math.Round(firstPrice, 4, MidpointRounding.AwayFromZero);
}
var overflow = chargeableWeight - firstWeight;
var continueUnits = Math.Ceiling(overflow / continueUnit);
return Math.Round(firstPrice + continueUnits * continuePrice, 4, MidpointRounding.AwayFromZero);
}
private static ContinuePricingConfig? TryReadContinuePricing(
Dictionary factors,
ChannelPriceDetailOutput detail)
{
if (factors.TryGetValue("continuePricing", out var json) && !string.IsNullOrWhiteSpace(json))
{
return JsonSerializer.Deserialize(json, JsonOptions);
}
if (detail.FactorValuesJson is JsonElement element
&& element.TryGetProperty("continuePricing", out var continueNode))
{
return continueNode.Deserialize(JsonOptions);
}
if (factors.TryGetValue("首重", out var firstWeightText)
&& decimal.TryParse(firstWeightText, NumberStyles.Any, CultureInfo.InvariantCulture, out var firstWeight))
{
decimal.TryParse(factors.GetValueOrDefault("首重价"), NumberStyles.Any, CultureInfo.InvariantCulture, out var firstPrice);
decimal.TryParse(factors.GetValueOrDefault("续重单位"), NumberStyles.Any, CultureInfo.InvariantCulture, out var continueUnit);
decimal.TryParse(factors.GetValueOrDefault("续重价"), NumberStyles.Any, CultureInfo.InvariantCulture, out var continuePrice);
return new ContinuePricingConfig
{
FirstWeight = firstWeight,
FirstPrice = firstPrice > 0 ? firstPrice : detail.CalculatedValue ?? 0,
ContinueUnit = continueUnit > 0 ? continueUnit : 0.5M,
ContinuePrice = continuePrice > 0 ? continuePrice : detail.CalculatedValue ?? 0
};
}
return null;
}
private static decimal? ResolvePriceTierKeyPrice(ChannelPriceDetailOutput detail, string? priceTierKey)
{
if (detail.CustomerGroupValues == null)
{
return null;
}
var json = JsonSerializer.Serialize(detail.CustomerGroupValues, JsonOptions);
var map = JsonSerializer.Deserialize>(json, JsonOptions);
if (map == null)
{
return null;
}
if (!string.IsNullOrWhiteSpace(priceTierKey)
&& map.TryGetValue(priceTierKey.Trim(), out var tierPrice))
{
return tierPrice;
}
if (map.TryGetValue("普通客户", out var normalPrice))
{
return normalPrice;
}
return null;
}
private static bool EvaluateCondition(string condition, decimal gw, decimal vw)
{
if (string.IsNullOrWhiteSpace(condition))
{
return true;
}
var expr = Regex.Replace(condition.Trim(), @"\bgw\b", gw.ToString(CultureInfo.InvariantCulture), RegexOptions.IgnoreCase);
expr = Regex.Replace(expr, @"\bvw\b", vw.ToString(CultureInfo.InvariantCulture), RegexOptions.IgnoreCase);
expr = expr.Replace("==", "=");
try
{
var result = new global::System.Data.DataTable().Compute(expr, string.Empty);
return result is bool boolResult && boolResult
|| result is int intResult && intResult != 0
|| result is decimal decimalResult && decimalResult != 0
|| result is double doubleResult && Math.Abs(doubleResult) > double.Epsilon;
}
catch
{
return false;
}
}
private static bool IsPostcodeInRange(string zipCode, string? left, string? right)
{
if (string.IsNullOrWhiteSpace(left) && string.IsNullOrWhiteSpace(right))
{
return true;
}
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;
}
private sealed class ContinuePricingConfig
{
public decimal FirstWeight { get; set; }
public decimal FirstPrice { get; set; }
public decimal ContinueUnit { get; set; }
public decimal ContinuePrice { get; set; }
}
}