feat: support remote-zone factor matching and all-customer price tier

Split price/remote zone matching in billing and prefer the 全部客户 column for any price tier key.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-08-02 18:22:17 +08:00
parent c8107cb565
commit cc100d40cf
6 changed files with 256 additions and 21 deletions

View File

@ -118,6 +118,11 @@ public partial class SalesProductService
: OmsDocHelper.SerializeJson(additionalServices) : OmsDocHelper.SerializeJson(additionalServices)
}); });
var remoteAreaId = product.RemoteAreaId is > 0 ? product.RemoteAreaId : rulesSnapshot.RemoteAreaId;
var remoteZoneKeys = isRemote
? ParcelBillingCalculator.BuildRemoteZoneKeys(remoteAreaId, rulesSnapshot.RemoteAreaName)
: Array.Empty<string>();
var receivableLines = ParcelBillingCalculator.CalculateQuoteReceivableFeeLines( var receivableLines = ParcelBillingCalculator.CalculateQuoteReceivableFeeLines(
priceSnapshot, priceSnapshot,
priceVersion, priceVersion,
@ -130,7 +135,8 @@ public partial class SalesProductService
warehouseName, warehouseName,
conditionContext, conditionContext,
input.DeclaredSum, input.DeclaredSum,
input.DeclaredCurrency); input.DeclaredCurrency,
remoteZoneKeys);
if (receivableLines.Count == 0 && ParcelBillingCalculator.HasEnabledReceivablePriceItems(priceSnapshot)) if (receivableLines.Count == 0 && ParcelBillingCalculator.HasEnabledReceivablePriceItems(priceSnapshot))
{ {
@ -165,6 +171,7 @@ public partial class SalesProductService
input.WarehouseId, input.WarehouseId,
warehouseName, warehouseName,
isRemote, isRemote,
remoteZoneKeys,
input.DeclaredSum, input.DeclaredSum,
input.DeclaredCurrency, input.DeclaredCurrency,
priceVersion, priceVersion,
@ -307,6 +314,7 @@ public partial class SalesProductService
int? warehouseId, int? warehouseId,
string? warehouseName, string? warehouseName,
bool isRemote, bool isRemote,
IReadOnlyList<string> remoteZoneKeys,
decimal? declaredSum, decimal? declaredSum,
string? declaredCurrency, string? declaredCurrency,
string priceVersion, string priceVersion,
@ -333,6 +341,17 @@ public partial class SalesProductService
return []; return [];
} }
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == provider.ServiceChannelId.Value);
var payableRemoteZoneKeys = isRemote
? ParcelBillingCalculator.BuildRemoteZoneKeys(
channel?.RemoteAreaId is > 0 ? channel.RemoteAreaId : null)
: Array.Empty<string>();
// 渠道未配偏远分区时回落产品偏远键,便于模拟偏远费行
if (isRemote && payableRemoteZoneKeys.Count == 0)
{
payableRemoteZoneKeys = remoteZoneKeys;
}
var payableContext = OrderRuleEvaluationContext.FromOrderOnly(new Order var payableContext = OrderRuleEvaluationContext.FromOrderOnly(new Order
{ {
ProductCode = product.ProductCode, ProductCode = product.ProductCode,
@ -356,7 +375,8 @@ public partial class SalesProductService
warehouseName, warehouseName,
payableContext, payableContext,
declaredSum, declaredSum,
declaredCurrency); declaredCurrency,
payableRemoteZoneKeys);
if (payable.Count == 0) if (payable.Count == 0)
{ {

View File

@ -689,6 +689,13 @@ public partial class OrderService
string? warehouseName = null) string? warehouseName = null)
{ {
var conditionContext = BuildBillingConditionContext(order); var conditionContext = BuildBillingConditionContext(order);
var remoteAreaId = order.IsRemote > 0
? (snapshotBinding.Rules?.RemoteAreaId
?? snapshotBinding.Rules?.Product?.RemoteAreaId)
: null;
var remoteZoneKeys = ParcelBillingCalculator.BuildRemoteZoneKeys(
remoteAreaId is > 0 ? remoteAreaId : null,
snapshotBinding.Rules?.RemoteAreaName);
var fees = ParcelBillingCalculator.CalculateReceivableFees( var fees = ParcelBillingCalculator.CalculateReceivableFees(
snapshotBinding.Price, snapshotBinding.Price,
snapshotBinding.PriceVersion, snapshotBinding.PriceVersion,
@ -702,7 +709,8 @@ public partial class OrderService
warehouseName, warehouseName,
conditionContext, conditionContext,
order.DeclaredSum, order.DeclaredSum,
order.DeclaredCurrency); order.DeclaredCurrency,
remoteZoneKeys);
if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledReceivablePriceItems(snapshotBinding.Price)) if (fees.Count == 0 && ParcelBillingCalculator.HasEnabledReceivablePriceItems(snapshotBinding.Price))
{ {
@ -781,6 +789,9 @@ public partial class OrderService
channelIsRemote ? (byte)1 : (byte)0); channelIsRemote ? (byte)1 : (byte)0);
var channelPriceBinding = await ResolveChannelPriceBindingAsync(channel.Id, bindTime); var channelPriceBinding = await ResolveChannelPriceBindingAsync(channel.Id, bindTime);
var channelRemoteZoneKeys = channelIsRemote
? ParcelBillingCalculator.BuildRemoteZoneKeys(channel.RemoteAreaId)
: Array.Empty<string>();
var previewFees = ParcelBillingCalculator.CalculatePayableFees( var previewFees = ParcelBillingCalculator.CalculatePayableFees(
channelPriceBinding.Prices, channelPriceBinding.Prices,
channelPriceBinding.PriceVersion, channelPriceBinding.PriceVersion,
@ -793,7 +804,8 @@ public partial class OrderService
warehouseName, warehouseName,
payableContext, payableContext,
order.DeclaredSum, order.DeclaredSum,
order.DeclaredCurrency); order.DeclaredCurrency,
channelRemoteZoneKeys);
if (previewFees.Count == 0) if (previewFees.Count == 0)
{ {
continue; continue;

View File

@ -595,6 +595,12 @@ public partial class OrderService
context.PostalCode); context.PostalCode);
var conditionContext = BuildQuoteBillingConditionContext(context, isRemote); var conditionContext = BuildQuoteBillingConditionContext(context, isRemote);
var remoteZoneKeys = isRemote
? ParcelBillingCalculator.BuildRemoteZoneKeys(
ResolveEffectiveRemoteAreaId(product, snapshotBinding.Rules),
snapshotBinding.Rules?.RemoteAreaName)
: Array.Empty<string>();
foreach (var zoneAttempt in zoneAttempts) foreach (var zoneAttempt in zoneAttempts)
{ {
var feeLines = ParcelBillingCalculator.CalculateQuoteReceivableFeeLines( var feeLines = ParcelBillingCalculator.CalculateQuoteReceivableFeeLines(
@ -609,7 +615,8 @@ public partial class OrderService
context.WarehouseName, context.WarehouseName,
conditionContext, conditionContext,
context.DeclaredSum, context.DeclaredSum,
context.DeclaredCurrency); context.DeclaredCurrency,
remoteZoneKeys);
if (feeLines.Count == 0) if (feeLines.Count == 0)
{ {
continue; continue;

View File

@ -374,14 +374,34 @@ internal static class ParcelBillingCalculator
}; };
/// <summary> /// <summary>
/// 卖价/买价明细匹配共用上下文(分区、货物属性、计费重、仓库)。 /// 卖价/买价明细匹配共用上下文(价格分区、偏远分区、货物属性、计费重、仓库)。
/// </summary> /// </summary>
internal readonly record struct PriceDetailMatchContext( internal readonly record struct PriceDetailMatchContext(
IReadOnlyList<string> ZoneKeys, IReadOnlyList<string> ZoneKeys,
string CargoAttribute, string CargoAttribute,
decimal ChargeableWeight, decimal ChargeableWeight,
int? WarehouseId = null, int? WarehouseId = null,
string? WarehouseName = null); string? WarehouseName = null,
IReadOnlyList<string>? RemoteZoneKeys = null);
/// <summary>
/// 偏远命中时用生效 RemoteAreaId(及可选名称)作为偏远分区匹配键;非偏远返回空。
/// </summary>
public static IReadOnlyList<string> BuildRemoteZoneKeys(int? remoteAreaId, string? remoteAreaName = null)
{
if (remoteAreaId is not > 0)
{
return Array.Empty<string>();
}
var keys = new List<string> { remoteAreaId.Value.ToString(CultureInfo.InvariantCulture) };
if (!string.IsNullOrWhiteSpace(remoteAreaName))
{
keys.Add(remoteAreaName.Trim());
}
return keys;
}
public static List<OrderFee> CalculateQuoteReceivableFees( public static List<OrderFee> CalculateQuoteReceivableFees(
SalesProductPricePublishSnapshotDto priceSnapshot, SalesProductPricePublishSnapshotDto priceSnapshot,
@ -395,7 +415,8 @@ internal static class ParcelBillingCalculator
string? warehouseName = null, string? warehouseName = null,
OrderRuleEvaluationContext? conditionContext = null, OrderRuleEvaluationContext? conditionContext = null,
decimal? declaredSum = null, decimal? declaredSum = null,
string? declaredCurrency = null) => string? declaredCurrency = null,
IReadOnlyList<string>? remoteZoneKeys = null) =>
CalculateQuoteReceivableFeeLines( CalculateQuoteReceivableFeeLines(
priceSnapshot, priceSnapshot,
priceVersion, priceVersion,
@ -408,7 +429,8 @@ internal static class ParcelBillingCalculator
warehouseName, warehouseName,
conditionContext, conditionContext,
declaredSum, declaredSum,
declaredCurrency).Select(x => x.Fee).ToList(); declaredCurrency,
remoteZoneKeys).Select(x => x.Fee).ToList();
public static List<BillingFeeLine> CalculateQuoteReceivableFeeLines( public static List<BillingFeeLine> CalculateQuoteReceivableFeeLines(
SalesProductPricePublishSnapshotDto priceSnapshot, SalesProductPricePublishSnapshotDto priceSnapshot,
@ -422,7 +444,8 @@ internal static class ParcelBillingCalculator
string? warehouseName = null, string? warehouseName = null,
OrderRuleEvaluationContext? conditionContext = null, OrderRuleEvaluationContext? conditionContext = null,
decimal? declaredSum = null, decimal? declaredSum = null,
string? declaredCurrency = null) string? declaredCurrency = null,
IReadOnlyList<string>? remoteZoneKeys = null)
{ {
var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem( var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem(
p.Status, p.CostItem, p.PriceName, p.Currency, p.Details, p.Status, p.CostItem, p.PriceName, p.Currency, p.Details,
@ -440,7 +463,8 @@ internal static class ParcelBillingCalculator
warehouseName, warehouseName,
conditionContext, conditionContext,
declaredSum, declaredSum,
declaredCurrency); declaredCurrency,
remoteZoneKeys);
} }
public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) => public static bool HasEnabledQuotePriceItems(SalesProductPricePublishSnapshotDto priceSnapshot) =>
@ -656,7 +680,8 @@ internal static class ParcelBillingCalculator
string? warehouseName = null, string? warehouseName = null,
OrderRuleEvaluationContext? conditionContext = null, OrderRuleEvaluationContext? conditionContext = null,
decimal? declaredSum = null, decimal? declaredSum = null,
string? declaredCurrency = null) string? declaredCurrency = null,
IReadOnlyList<string>? remoteZoneKeys = null)
{ {
var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem( var items = priceSnapshot.Prices.Select(p => new ReceivableBillingPriceItem(
p.Status, p.CostItem, p.PriceName, p.Currency, p.Details, p.Status, p.CostItem, p.PriceName, p.Currency, p.Details,
@ -674,7 +699,8 @@ internal static class ParcelBillingCalculator
warehouseName, warehouseName,
conditionContext, conditionContext,
declaredSum, declaredSum,
declaredCurrency); declaredCurrency,
remoteZoneKeys);
} }
/// <summary> /// <summary>
@ -692,7 +718,8 @@ internal static class ParcelBillingCalculator
string? warehouseName = null, string? warehouseName = null,
OrderRuleEvaluationContext? conditionContext = null, OrderRuleEvaluationContext? conditionContext = null,
decimal? declaredSum = null, decimal? declaredSum = null,
string? declaredCurrency = null) string? declaredCurrency = null,
IReadOnlyList<string>? remoteZoneKeys = null)
{ {
var items = prices.Select(p => new PayableBillingPriceItem( var items = prices.Select(p => new PayableBillingPriceItem(
p.Status, p.CostItem, p.Currency, p.Details, p.Status, p.CostItem, p.Currency, p.Details,
@ -701,7 +728,13 @@ internal static class ParcelBillingCalculator
items, items,
priceVersion, priceVersion,
priceEffectiveTime, priceEffectiveTime,
new PriceDetailMatchContext(zoneKeys, cargoAttribute, chargeableWeight, warehouseId, warehouseName), new PriceDetailMatchContext(
zoneKeys,
cargoAttribute,
chargeableWeight,
warehouseId,
warehouseName,
remoteZoneKeys),
chargingStage, chargingStage,
conditionContext, conditionContext,
declaredSum, declaredSum,
@ -899,7 +932,8 @@ internal static class ParcelBillingCalculator
string? warehouseName = null, string? warehouseName = null,
OrderRuleEvaluationContext? conditionContext = null, OrderRuleEvaluationContext? conditionContext = null,
decimal? declaredSum = null, decimal? declaredSum = null,
string? declaredCurrency = null) => string? declaredCurrency = null,
IReadOnlyList<string>? remoteZoneKeys = null) =>
CalculateReceivableFeeLines( CalculateReceivableFeeLines(
priceItems, priceItems,
priceVersion, priceVersion,
@ -913,7 +947,8 @@ internal static class ParcelBillingCalculator
warehouseName, warehouseName,
conditionContext, conditionContext,
declaredSum, declaredSum,
declaredCurrency).Select(x => x.Fee).ToList(); declaredCurrency,
remoteZoneKeys).Select(x => x.Fee).ToList();
private static List<BillingFeeLine> CalculateReceivableFeeLines( private static List<BillingFeeLine> CalculateReceivableFeeLines(
IEnumerable<ReceivableBillingPriceItem> priceItems, IEnumerable<ReceivableBillingPriceItem> priceItems,
@ -928,7 +963,8 @@ internal static class ParcelBillingCalculator
string? warehouseName = null, string? warehouseName = null,
OrderRuleEvaluationContext? conditionContext = null, OrderRuleEvaluationContext? conditionContext = null,
decimal? declaredSum = null, decimal? declaredSum = null,
string? declaredCurrency = null) string? declaredCurrency = null,
IReadOnlyList<string>? remoteZoneKeys = null)
{ {
var pass1 = new List<(int SerialNumber, BillingFeeLine Line)>(); var pass1 = new List<(int SerialNumber, BillingFeeLine Line)>();
var percentFreightPending = new List<(int SerialNumber, ReceivableBillingPriceItem PriceItem, ChannelPriceDetailOutput Detail)>(); var percentFreightPending = new List<(int SerialNumber, ReceivableBillingPriceItem PriceItem, ChannelPriceDetailOutput Detail)>();
@ -951,7 +987,8 @@ internal static class ParcelBillingCalculator
cargoAttribute, cargoAttribute,
chargeableWeight, chargeableWeight,
warehouseId, warehouseId,
warehouseName); warehouseName,
remoteZoneKeys);
if (detail == null) if (detail == null)
{ {
continue; continue;
@ -1084,7 +1121,8 @@ internal static class ParcelBillingCalculator
match.CargoAttribute, match.CargoAttribute,
match.ChargeableWeight, match.ChargeableWeight,
match.WarehouseId, match.WarehouseId,
match.WarehouseName); match.WarehouseName,
match.RemoteZoneKeys);
if (detail == null) if (detail == null)
{ {
continue; continue;
@ -1269,6 +1307,7 @@ internal static class ParcelBillingCalculator
{ {
var factors = ParseFactorDictionary(candidate.FactorValuesJson); var factors = ParseFactorDictionary(candidate.FactorValuesJson);
if (!ZoneMatches(factors, match.ZoneKeys) if (!ZoneMatches(factors, match.ZoneKeys)
|| !RemoteZoneMatches(factors, match.RemoteZoneKeys)
|| !MatchCargoTypeFactor(factors, match.CargoAttribute) || !MatchCargoTypeFactor(factors, match.CargoAttribute)
|| !MatchWarehouseFactor(factors, match.WarehouseId, match.WarehouseName)) || !MatchWarehouseFactor(factors, match.WarehouseId, match.WarehouseName))
{ {
@ -1343,7 +1382,8 @@ internal static class ParcelBillingCalculator
string cargoAttribute, string cargoAttribute,
decimal chargeableWeight, decimal chargeableWeight,
int? warehouseId = null, int? warehouseId = null,
string? warehouseName = null) string? warehouseName = null,
IReadOnlyList<string>? remoteZoneKeys = null)
{ {
ChannelPriceDetailOutput? bestMatch = null; ChannelPriceDetailOutput? bestMatch = null;
var bestIntervalWidth = decimal.MaxValue; var bestIntervalWidth = decimal.MaxValue;
@ -1356,6 +1396,11 @@ internal static class ParcelBillingCalculator
continue; continue;
} }
if (!RemoteZoneMatches(factors, remoteZoneKeys))
{
continue;
}
if (!MatchCargoTypeFactor(factors, cargoAttribute)) if (!MatchCargoTypeFactor(factors, cargoAttribute))
{ {
continue; continue;
@ -1446,6 +1491,24 @@ internal static class ParcelBillingCalculator
return zoneKeys.Any(key => string.Equals(key.Trim(), expected, StringComparison.OrdinalIgnoreCase)); return zoneKeys.Any(key => string.Equals(key.Trim(), expected, StringComparison.OrdinalIgnoreCase));
} }
private static bool RemoteZoneMatches(Dictionary<string, string> factors, IReadOnlyList<string>? remoteZoneKeys)
{
if (!TryGetRemoteZoneFactorText(factors, out var remoteZoneValue))
{
return true;
}
if (remoteZoneKeys == null || remoteZoneKeys.Count == 0)
{
return false;
}
var expected = remoteZoneValue.Trim();
return remoteZoneKeys.Any(key =>
!string.IsNullOrWhiteSpace(key)
&& string.Equals(key.Trim(), expected, StringComparison.OrdinalIgnoreCase));
}
private static bool TryGetZoneFactorText(Dictionary<string, string> factors, out string zoneValue) private static bool TryGetZoneFactorText(Dictionary<string, string> factors, out string zoneValue)
{ {
if (TryGetFactorValue(factors, out zoneValue, "分区", "价格分区", "zone")) if (TryGetFactorValue(factors, out zoneValue, "分区", "价格分区", "zone"))
@ -1457,6 +1520,17 @@ internal static class ParcelBillingCalculator
return false; return false;
} }
private static bool TryGetRemoteZoneFactorText(Dictionary<string, string> factors, out string remoteZoneValue)
{
if (TryGetFactorValue(factors, out remoteZoneValue, "偏远分区", "remoteZone"))
{
return !string.IsNullOrWhiteSpace(remoteZoneValue);
}
remoteZoneValue = string.Empty;
return false;
}
private static bool TryGetWeightFactorText(Dictionary<string, string> factors, out string weightText) => private static bool TryGetWeightFactorText(Dictionary<string, string> factors, out string weightText) =>
TryGetFactorValue(factors, out weightText, "重量", "weight") && !string.IsNullOrWhiteSpace(weightText); TryGetFactorValue(factors, out weightText, "重量", "weight") && !string.IsNullOrWhiteSpace(weightText);
@ -1810,6 +1884,7 @@ internal static class ParcelBillingCalculator
continue; continue;
} }
// 续重邻档总价查找不按偏远分区过滤:邻档通常与运费同组,偏远费单独成项。
if (NormalizeCalculationFormula(candidate.CalculationFormula) != "total") if (NormalizeCalculationFormula(candidate.CalculationFormula) != "total")
{ {
continue; continue;
@ -1933,6 +2008,12 @@ internal static class ParcelBillingCalculator
return null; return null;
} }
// 「全部客户」优先:任意价区键共用这一列
if (TryGetMapPrice(map, out var allPrice, "全部客户", "all"))
{
return allPrice;
}
if (!string.IsNullOrWhiteSpace(priceTierKey) if (!string.IsNullOrWhiteSpace(priceTierKey)
&& map.TryGetValue(priceTierKey.Trim(), out var tierPrice)) && map.TryGetValue(priceTierKey.Trim(), out var tierPrice))
{ {
@ -1947,6 +2028,32 @@ internal static class ParcelBillingCalculator
return null; return null;
} }
private static bool TryGetMapPrice(
Dictionary<string, decimal> map,
out decimal price,
params string[] keys)
{
foreach (var key in keys)
{
if (map.TryGetValue(key, out price))
{
return true;
}
foreach (var pair in map)
{
if (string.Equals(pair.Key, key, StringComparison.OrdinalIgnoreCase))
{
price = pair.Value;
return true;
}
}
}
price = 0;
return false;
}
private static bool EvaluateCondition(string condition, decimal gw, decimal vw) private static bool EvaluateCondition(string condition, decimal gw, decimal vw)
{ {
if (string.IsNullOrWhiteSpace(condition)) if (string.IsNullOrWhiteSpace(condition))

View File

@ -557,5 +557,63 @@ public class ParcelBillingCalculatorTests
fees[0].Amount.Should().Be(12M); fees[0].Amount.Should().Be(12M);
} }
[Fact]
public void CalculateReceivableFees_AllCustomerColumn_ShouldApplyToAnyPriceTierKey()
{
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem(
1,
"freight",
ParcelBillingTestData.AllCustomerTierDetail(Zone, 33M)));
var fees = ParcelBillingCalculator.CalculateReceivableFees(
snapshot,
_meta.PriceVersion,
_meta.EffectiveTime,
1.2M,
new[] { Zone },
"general",
"VIP5",
OrderFeeChargingStageCodes.Forecast);
fees.Should().ContainSingle();
fees[0].Amount.Should().Be(33M);
}
[Fact]
public void CalculateReceivableFees_RemoteZoneFactor_ShouldRequireMatchingRemoteKeys()
{
var snapshot = ParcelBillingTestData.BuildReceivableSnapshot(
ParcelBillingTestData.ReceivablePriceItem(
1,
"remoteFee",
ParcelBillingTestData.RemoteZoneFixedDetail(15M, Zone, "88")));
var miss = ParcelBillingCalculator.CalculateReceivableFees(
snapshot,
_meta.PriceVersion,
_meta.EffectiveTime,
1.2M,
new[] { Zone },
"general",
null,
OrderFeeChargingStageCodes.Forecast,
remoteZoneKeys: Array.Empty<string>());
miss.Should().BeEmpty();
var hit = ParcelBillingCalculator.CalculateReceivableFees(
snapshot,
_meta.PriceVersion,
_meta.EffectiveTime,
1.2M,
new[] { Zone },
"general",
null,
OrderFeeChargingStageCodes.Forecast,
remoteZoneKeys: ParcelBillingCalculator.BuildRemoteZoneKeys(88));
hit.Should().ContainSingle();
hit[0].Amount.Should().Be(15M);
}
#endregion #endregion
} }

View File

@ -150,6 +150,37 @@ internal static class ParcelBillingTestData
CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = normalCustomerPrice } CustomerGroupValues = new Dictionary<string, decimal> { ["普通客户"] = normalCustomerPrice }
}; };
public static ChannelPriceDetailOutput AllCustomerTierDetail(
string zone,
decimal allCustomerPrice,
string? weightInterval = null) =>
new()
{
CalculationFormula = "total",
CalculatedValue = 0,
FactorValuesJson = new Dictionary<string, string>
{
["价格分区"] = zone,
["重量"] = weightInterval ?? "999"
},
CustomerGroupValues = new Dictionary<string, decimal> { ["全部客户"] = allCustomerPrice }
};
public static ChannelPriceDetailOutput RemoteZoneFixedDetail(
decimal amount,
string priceZone,
string remoteZoneId) =>
new()
{
CalculationFormula = "固定价",
CalculatedValue = amount,
FactorValuesJson = new Dictionary<string, string>
{
["价格分区"] = priceZone,
["偏远分区"] = remoteZoneId
}
};
public static WeightRuleInfo GwRule(string condition = "gw>=vw", string factor = "6000") => public static WeightRuleInfo GwRule(string condition = "gw>=vw", string factor = "6000") =>
new() new()
{ {