20261008-ide1.0发布 #1
@ -0,0 +1,22 @@
|
|||||||
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 服务渠道发布同步态(轻量,不含快照正文)。
|
||||||
|
/// </summary>
|
||||||
|
public class ServiceChannelPublishSyncStatusOutput
|
||||||
|
{
|
||||||
|
/// <summary>相对最新生效版本是否有待发布变更。</summary>
|
||||||
|
public bool Pending { get; set; }
|
||||||
|
|
||||||
|
/// <summary>最新生效版本号;无则 null。</summary>
|
||||||
|
public string? OnlineVersion { get; set; }
|
||||||
|
|
||||||
|
/// <summary>最新生效发布时间。</summary>
|
||||||
|
public DateTime? OnlinePublishTime { get; set; }
|
||||||
|
|
||||||
|
/// <summary>待发布快照内容哈希(规范化后)。</summary>
|
||||||
|
public string? PendingContentHash { get; set; }
|
||||||
|
|
||||||
|
/// <summary>线上快照内容哈希。</summary>
|
||||||
|
public string? OnlineContentHash { get; set; }
|
||||||
|
}
|
||||||
@ -1,3 +1,6 @@
|
|||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
namespace Oms.Application.Dtos.Channels;
|
namespace Oms.Application.Dtos.Channels;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -8,4 +11,17 @@ public class ServiceChannelRulesPublishSnapshotDto
|
|||||||
public ServiceChannelSaveInput? Channel { get; set; }
|
public ServiceChannelSaveInput? Channel { get; set; }
|
||||||
|
|
||||||
public ServiceChannelDownstreamSnapshotDto? Downstream { get; set; }
|
public ServiceChannelDownstreamSnapshotDto? Downstream { get; set; }
|
||||||
|
|
||||||
|
/// <summary>偏远分区 ID。</summary>
|
||||||
|
public int? RemoteAreaId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>偏远分区名称。</summary>
|
||||||
|
public string? RemoteAreaName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>偏远分区包引用(规则发布时钉包)。</summary>
|
||||||
|
public KpiAreaPackRefDto? RemoteAreaRef { get; set; }
|
||||||
|
|
||||||
|
/// <summary>运行时 hydrate / LIVE 用,不落发布 JSON。</summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<KpiAreaSnapshotRuleDto> RemoteAreaInfos { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -129,12 +129,25 @@ public class SalesProductRulesPublishSnapshotDto
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 服务渠道买价发布快照。
|
/// 服务渠道买价发布快照(渠道自有价区,与产品卖价分区独立)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class ServiceChannelPricePublishSnapshotDto
|
public class ServiceChannelPricePublishSnapshotDto
|
||||||
{
|
{
|
||||||
public int ServiceChannelId { get; set; }
|
public int ServiceChannelId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>价格分区 ID(单表时冗余展示;以 PriceAreaRefs 为准)。</summary>
|
||||||
|
public int? PriceAreaId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>价格分区名称。</summary>
|
||||||
|
public string? PriceAreaName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>渠道价区包引用(买价发布时钉包)。</summary>
|
||||||
|
public List<KpiAreaPackRefDto> PriceAreaRefs { get; set; } = [];
|
||||||
|
|
||||||
|
/// <summary>运行时 hydrate / LIVE 用,不落发布 JSON。</summary>
|
||||||
|
[JsonIgnore]
|
||||||
|
public List<KpiAreaSnapshotRuleDto> PriceAreaInfos { get; set; } = [];
|
||||||
|
|
||||||
public List<ServiceChannelPriceSnapshotItemDto> Prices { get; set; } = [];
|
public List<ServiceChannelPriceSnapshotItemDto> Prices { get; set; } = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -185,13 +185,16 @@ public partial class SalesProductService
|
|||||||
payableFees = await TrySimulatePayableFeesAsync(
|
payableFees = await TrySimulatePayableFeesAsync(
|
||||||
product,
|
product,
|
||||||
rulesSnapshot,
|
rulesSnapshot,
|
||||||
zoneKeys,
|
|
||||||
chargeableWeight,
|
chargeableWeight,
|
||||||
cargoAttribute,
|
cargoAttribute,
|
||||||
input.WarehouseId,
|
input.WarehouseId,
|
||||||
warehouseName,
|
warehouseName,
|
||||||
isRemote,
|
isRemote,
|
||||||
remoteZoneKeys,
|
remoteZoneKeys,
|
||||||
|
countryCode,
|
||||||
|
input.Province,
|
||||||
|
input.City,
|
||||||
|
input.ZipCode,
|
||||||
input.DeclaredSum,
|
input.DeclaredSum,
|
||||||
input.DeclaredCurrency,
|
input.DeclaredCurrency,
|
||||||
priceVersion,
|
priceVersion,
|
||||||
@ -316,13 +319,16 @@ public partial class SalesProductService
|
|||||||
private async Task<List<SalesProductBillingSimulateFeeOutput>> TrySimulatePayableFeesAsync(
|
private async Task<List<SalesProductBillingSimulateFeeOutput>> TrySimulatePayableFeesAsync(
|
||||||
SalesProduct product,
|
SalesProduct product,
|
||||||
SalesProductRulesPublishSnapshotDto rulesSnapshot,
|
SalesProductRulesPublishSnapshotDto rulesSnapshot,
|
||||||
IReadOnlyList<string> zoneKeys,
|
|
||||||
decimal chargeableWeight,
|
decimal chargeableWeight,
|
||||||
string cargoAttribute,
|
string cargoAttribute,
|
||||||
int? warehouseId,
|
int? warehouseId,
|
||||||
string? warehouseName,
|
string? warehouseName,
|
||||||
bool isRemote,
|
bool productIsRemote,
|
||||||
IReadOnlyList<string> remoteZoneKeys,
|
IReadOnlyList<string> productRemoteZoneKeys,
|
||||||
|
string countryCode,
|
||||||
|
string? province,
|
||||||
|
string? city,
|
||||||
|
string? zipCode,
|
||||||
decimal? declaredSum,
|
decimal? declaredSum,
|
||||||
string? declaredCurrency,
|
string? declaredCurrency,
|
||||||
string priceVersion,
|
string priceVersion,
|
||||||
@ -344,7 +350,7 @@ public partial class SalesProductService
|
|||||||
WarehouseId = warehouseId,
|
WarehouseId = warehouseId,
|
||||||
DeclaredSum = declaredSum,
|
DeclaredSum = declaredSum,
|
||||||
DeclaredCurrency = declaredCurrency,
|
DeclaredCurrency = declaredCurrency,
|
||||||
IsRemote = isRemote ? (byte)1 : (byte)0
|
IsRemote = productIsRemote ? (byte)1 : (byte)0
|
||||||
})).Passed;
|
})).Passed;
|
||||||
var provider = filtered.OrderBy(x => x.SerialNumber).FirstOrDefault(x => x.ServiceChannelId is > 0);
|
var provider = filtered.OrderBy(x => x.SerialNumber).FirstOrDefault(x => x.ServiceChannelId is > 0);
|
||||||
if (provider?.ServiceChannelId is not > 0)
|
if (provider?.ServiceChannelId is not > 0)
|
||||||
@ -353,33 +359,81 @@ public partial class SalesProductService
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var channelPrices = await BuildChannelPriceOutputsAsync(provider.ServiceChannelId.Value);
|
var channelId = provider.ServiceChannelId.Value;
|
||||||
if (channelPrices.Count == 0)
|
var channelPriceSnapshot = new ServiceChannelPricePublishSnapshotDto
|
||||||
{
|
{
|
||||||
warnings.Add($"服务渠道 {provider.ServiceChannelId} 无买价,跳过应付估算");
|
ServiceChannelId = channelId,
|
||||||
|
Prices = (await BuildChannelPriceOutputsAsync(channelId))
|
||||||
|
.Select(PublishSnapshotSlimmer.SlimChannelPrice)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
if (channelPriceSnapshot.Prices.Count == 0)
|
||||||
|
{
|
||||||
|
warnings.Add($"服务渠道 {channelId} 无买价,跳过应付估算");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == provider.ServiceChannelId.Value);
|
await PriceAreaSnapshotHelper.ApplyLiveChannelPriceAreaInfosAsync(
|
||||||
var payableRemoteZoneKeys = isRemote
|
channelPriceSnapshot,
|
||||||
? ParcelBillingCalculator.BuildRemoteZoneKeys(
|
KpiAreaRepository,
|
||||||
channel?.RemoteAreaId is > 0 ? channel.RemoteAreaId : null)
|
KpiAreaPackService);
|
||||||
: Array.Empty<string>();
|
var channelPrices = channelPriceSnapshot.Prices
|
||||||
// 渠道未配偏远分区时回落产品偏远键,便于模拟偏远费行
|
.Select(PublishSnapshotSlimmer.ToChannelPriceListOutput)
|
||||||
if (isRemote && payableRemoteZoneKeys.Count == 0)
|
.ToList();
|
||||||
|
|
||||||
|
IReadOnlyList<string> payableZoneKeys = Array.Empty<string>();
|
||||||
|
if (channelPriceSnapshot.PriceAreaInfos is { Count: > 0 })
|
||||||
{
|
{
|
||||||
payableRemoteZoneKeys = remoteZoneKeys;
|
var resolved = ParcelBillingCalculator.TryResolveZoneKeysFromChannelPriceSnapshot(
|
||||||
|
channelPriceSnapshot,
|
||||||
|
countryCode,
|
||||||
|
province,
|
||||||
|
city,
|
||||||
|
zipCode);
|
||||||
|
if (resolved == null || resolved.Count == 0)
|
||||||
|
{
|
||||||
|
warnings.Add($"服务渠道 {channelId} 买价分区未匹配,跳过应付估算");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
payableZoneKeys = resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId);
|
||||||
|
var channelRemoteAreaId = channel?.RemoteAreaId;
|
||||||
|
var channelIsRemote = productIsRemote;
|
||||||
|
if (channelRemoteAreaId is > 0)
|
||||||
|
{
|
||||||
|
var channelRules = new ServiceChannelRulesPublishSnapshotDto();
|
||||||
|
await KpiAreaPackService.LoadLiveRemoteIntoChannelRulesSnapshotAsync(
|
||||||
|
channelRules,
|
||||||
|
channelRemoteAreaId);
|
||||||
|
channelIsRemote = GeoPartitionMatcher.IsHit(
|
||||||
|
channelRules.RemoteAreaInfos,
|
||||||
|
countryCode,
|
||||||
|
province,
|
||||||
|
city,
|
||||||
|
zipCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
var payableRemoteZoneKeys = channelIsRemote
|
||||||
|
? ParcelBillingCalculator.BuildRemoteZoneKeys(
|
||||||
|
channelRemoteAreaId is > 0 ? channelRemoteAreaId : null)
|
||||||
|
: Array.Empty<string>();
|
||||||
|
if (channelIsRemote && payableRemoteZoneKeys.Count == 0)
|
||||||
|
{
|
||||||
|
payableRemoteZoneKeys = productRemoteZoneKeys;
|
||||||
}
|
}
|
||||||
|
|
||||||
var payableContext = OrderRuleEvaluationContext.FromOrderOnly(new Order
|
var payableContext = OrderRuleEvaluationContext.FromOrderOnly(new Order
|
||||||
{
|
{
|
||||||
ProductCode = product.ProductCode,
|
ProductCode = product.ProductCode,
|
||||||
ConsigneeCountry = null,
|
ConsigneeCountry = countryCode,
|
||||||
CargoAttribute = cargoAttribute,
|
CargoAttribute = cargoAttribute,
|
||||||
WarehouseId = warehouseId,
|
WarehouseId = warehouseId,
|
||||||
DeclaredSum = declaredSum,
|
DeclaredSum = declaredSum,
|
||||||
DeclaredCurrency = declaredCurrency,
|
DeclaredCurrency = declaredCurrency,
|
||||||
IsRemote = isRemote ? (byte)1 : (byte)0
|
IsRemote = channelIsRemote ? (byte)1 : (byte)0
|
||||||
});
|
});
|
||||||
|
|
||||||
var payable = ParcelBillingCalculator.CalculatePayableFees(
|
var payable = ParcelBillingCalculator.CalculatePayableFees(
|
||||||
@ -387,7 +441,7 @@ public partial class SalesProductService
|
|||||||
priceVersion,
|
priceVersion,
|
||||||
priceEffectiveTime,
|
priceEffectiveTime,
|
||||||
chargeableWeight,
|
chargeableWeight,
|
||||||
zoneKeys,
|
payableZoneKeys,
|
||||||
cargoAttribute,
|
cargoAttribute,
|
||||||
OrderFeeChargingStageCodes.Forecast,
|
OrderFeeChargingStageCodes.Forecast,
|
||||||
warehouseId,
|
warehouseId,
|
||||||
@ -399,11 +453,11 @@ public partial class SalesProductService
|
|||||||
|
|
||||||
if (payable.Count == 0)
|
if (payable.Count == 0)
|
||||||
{
|
{
|
||||||
warnings.Add($"服务渠道 {provider.ServiceChannelId} 买价未匹配,跳过应付估算");
|
warnings.Add($"服务渠道 {channelId} 买价未匹配,跳过应付估算");
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings.Add($"应付按排货服务渠道 {provider.ServiceChannelId} 估算(首个可用渠道)");
|
warnings.Add($"应付按排货服务渠道 {channelId} 估算(首个可用渠道,渠道价区)");
|
||||||
return payable.Select(x => new SalesProductBillingSimulateFeeOutput
|
return payable.Select(x => new SalesProductBillingSimulateFeeOutput
|
||||||
{
|
{
|
||||||
CostItem = x.CostItem,
|
CostItem = x.CostItem,
|
||||||
|
|||||||
@ -243,23 +243,29 @@ public partial class ServiceChannelService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 组装买价发布快照(不落库),与产品卖价 §5.3.2 对称。
|
/// 组装买价发布快照(不落库):钉渠道自有价区包。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<ServiceChannelPricePublishSnapshot> BuildPricePublishSnapshotAsync(int serviceChannelId)
|
private async Task<ServiceChannelPricePublishSnapshotDto> BuildPricePublishSnapshotAsync(int serviceChannelId)
|
||||||
{
|
{
|
||||||
return new ServiceChannelPricePublishSnapshot
|
var snapshot = new ServiceChannelPricePublishSnapshotDto
|
||||||
{
|
{
|
||||||
ServiceChannelId = serviceChannelId,
|
ServiceChannelId = serviceChannelId,
|
||||||
Prices = (await GetPriceOutputsAsync(serviceChannelId))
|
Prices = (await GetPriceOutputsAsync(serviceChannelId))
|
||||||
.Select(PublishSnapshotSlimmer.SlimChannelPrice)
|
.Select(PublishSnapshotSlimmer.SlimChannelPrice)
|
||||||
.ToList()
|
.ToList()
|
||||||
};
|
};
|
||||||
|
await PriceAreaSnapshotHelper.ApplyChannelPriceAreaToSnapshotAsync(
|
||||||
|
snapshot,
|
||||||
|
KpiAreaRepository,
|
||||||
|
KpiAreaPackService,
|
||||||
|
ResolvePublishUser());
|
||||||
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 组装渠道规则发布快照(不落库)。
|
/// 组装渠道规则发布快照(不落库):钉偏远分区包。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<ServiceChannelRulesPublishSnapshot> BuildRulesPublishSnapshotAsync(int serviceChannelId)
|
private async Task<ServiceChannelRulesPublishSnapshotDto> BuildRulesPublishSnapshotAsync(int serviceChannelId)
|
||||||
{
|
{
|
||||||
var entity = await GetEntityOrThrowAsync(serviceChannelId);
|
var entity = await GetEntityOrThrowAsync(serviceChannelId);
|
||||||
ServiceChannelDownstreamSnapshotDto? downstream = null;
|
ServiceChannelDownstreamSnapshotDto? downstream = null;
|
||||||
@ -272,13 +278,39 @@ public partial class ServiceChannelService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new ServiceChannelRulesPublishSnapshot
|
var snapshot = new ServiceChannelRulesPublishSnapshotDto
|
||||||
{
|
{
|
||||||
Channel = MapChannelToSaveInput(entity),
|
Channel = MapChannelToSaveInput(entity),
|
||||||
Downstream = downstream
|
Downstream = downstream,
|
||||||
|
RemoteAreaInfos = []
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (entity.RemoteAreaId is > 0)
|
||||||
|
{
|
||||||
|
var packRef = await KpiAreaPackService.EnsurePackAsync(
|
||||||
|
entity.RemoteAreaId.Value,
|
||||||
|
KpiAreaPackService.AreaTypeRemote,
|
||||||
|
ResolvePublishUser(),
|
||||||
|
description: "service-channel-rules-publish");
|
||||||
|
snapshot.RemoteAreaId = entity.RemoteAreaId;
|
||||||
|
snapshot.RemoteAreaName = packRef.AreaName;
|
||||||
|
snapshot.RemoteAreaRef = packRef;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snapshot.RemoteAreaId = null;
|
||||||
|
snapshot.RemoteAreaName = null;
|
||||||
|
snapshot.RemoteAreaRef = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string ResolvePublishUser() =>
|
||||||
|
string.IsNullOrWhiteSpace(CurrentUser.UserName)
|
||||||
|
? CurrentUser.GetId<int>().ToString()
|
||||||
|
: CurrentUser.UserName!;
|
||||||
|
|
||||||
private static ServiceChannelSaveInput MapChannelToSaveInput(ServiceChannel entity) =>
|
private static ServiceChannelSaveInput MapChannelToSaveInput(ServiceChannel entity) =>
|
||||||
new()
|
new()
|
||||||
{
|
{
|
||||||
@ -334,7 +366,7 @@ public partial class ServiceChannelService
|
|||||||
|
|
||||||
if (operatorType == PriceOperatorType)
|
if (operatorType == PriceOperatorType)
|
||||||
{
|
{
|
||||||
var snapshot = OmsDocHelper.DeserializeJson<ServiceChannelPricePublishSnapshot>(record.Content);
|
var snapshot = OmsDocHelper.DeserializeChannelPriceSnapshot(record.Content);
|
||||||
if (snapshot == null)
|
if (snapshot == null)
|
||||||
{
|
{
|
||||||
throw new Fake.Application.BusinessException("发布快照无效");
|
throw new Fake.Application.BusinessException("发布快照无效");
|
||||||
@ -349,8 +381,8 @@ public partial class ServiceChannelService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var rulesSnapshot = OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshot>(record.Content);
|
var rulesSnapshot = OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(record.Content);
|
||||||
if (rulesSnapshot == null)
|
if (rulesSnapshot?.Channel == null)
|
||||||
{
|
{
|
||||||
throw new Fake.Application.BusinessException("发布快照无效");
|
throw new Fake.Application.BusinessException("发布快照无效");
|
||||||
}
|
}
|
||||||
@ -658,17 +690,4 @@ public partial class ServiceChannelService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class ServiceChannelPricePublishSnapshot
|
|
||||||
{
|
|
||||||
public int ServiceChannelId { get; set; }
|
|
||||||
|
|
||||||
public List<ServiceChannelPriceSnapshotItemDto> Prices { get; set; } = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
private sealed class ServiceChannelRulesPublishSnapshot
|
|
||||||
{
|
|
||||||
public ServiceChannelSaveInput Channel { get; set; } = new();
|
|
||||||
|
|
||||||
public ServiceChannelDownstreamSnapshotDto? Downstream { get; set; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -20,6 +20,8 @@ public partial class ServiceChannelService : ApplicationService
|
|||||||
|
|
||||||
private IServiceChannelRepository ServiceChannelRepository => LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
|
private IServiceChannelRepository ServiceChannelRepository => LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
|
||||||
private IServiceChannelGroupRepository ServiceChannelGroupRepository => LazyServiceProvider.GetRequiredService<IServiceChannelGroupRepository>();
|
private IServiceChannelGroupRepository ServiceChannelGroupRepository => LazyServiceProvider.GetRequiredService<IServiceChannelGroupRepository>();
|
||||||
|
private IKpiAreaRepository KpiAreaRepository => LazyServiceProvider.GetRequiredService<IKpiAreaRepository>();
|
||||||
|
private KpiAreaPackService KpiAreaPackService => LazyServiceProvider.GetRequiredService<KpiAreaPackService>();
|
||||||
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
private AllocationProviderSnapshotEnricher AllocationProviderSnapshotEnricher =>
|
||||||
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
LazyServiceProvider.GetRequiredService<AllocationProviderSnapshotEnricher>();
|
||||||
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
|
||||||
@ -400,6 +402,63 @@ public partial class ServiceChannelService : ApplicationService
|
|||||||
return OmsDocHelper.ToSnapshotApiContent(snapshot)!;
|
return OmsDocHelper.ToSnapshotApiContent(snapshot)!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发布同步态(轻量):对比待发布与最新生效快照,不回传正文。与产品卖价对称。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/channel/service-channel/publish/sync-status")]
|
||||||
|
public async Task<ServiceChannelPublishSyncStatusOutput> GetPublishSyncStatusAsync(
|
||||||
|
[FromQuery] int serviceChannelId,
|
||||||
|
[FromQuery] string operatorType)
|
||||||
|
{
|
||||||
|
var normalized = NormalizeOperatorType(operatorType);
|
||||||
|
await GetEntityOrThrowAsync(serviceChannelId);
|
||||||
|
|
||||||
|
object pendingSnapshot = normalized == PriceOperatorType
|
||||||
|
? await BuildPricePublishSnapshotAsync(serviceChannelId)
|
||||||
|
: await BuildRulesPublishSnapshotAsync(serviceChannelId);
|
||||||
|
var pendingJson = OmsDocHelper.SerializeSnapshotJson(pendingSnapshot) ?? string.Empty;
|
||||||
|
var pendingHash = ComputeContentHash(pendingJson);
|
||||||
|
|
||||||
|
var onlineRecord = await ServiceChannelRepository.GetEffectivePublishRecordAsync(
|
||||||
|
serviceChannelId,
|
||||||
|
normalized,
|
||||||
|
DateTime.Now);
|
||||||
|
if (onlineRecord == null)
|
||||||
|
{
|
||||||
|
return new ServiceChannelPublishSyncStatusOutput
|
||||||
|
{
|
||||||
|
Pending = true,
|
||||||
|
OnlineVersion = null,
|
||||||
|
OnlinePublishTime = null,
|
||||||
|
PendingContentHash = pendingHash,
|
||||||
|
OnlineContentHash = null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var onlineJson = onlineRecord.Content ?? string.Empty;
|
||||||
|
object? onlineNormalized = normalized == PriceOperatorType
|
||||||
|
? OmsDocHelper.DeserializeChannelPriceSnapshot(onlineJson)
|
||||||
|
: OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(onlineJson);
|
||||||
|
var onlineCanon = OmsDocHelper.SerializeSnapshotJson(onlineNormalized) ?? string.Empty;
|
||||||
|
var onlineHash = ComputeContentHash(onlineCanon);
|
||||||
|
|
||||||
|
return new ServiceChannelPublishSyncStatusOutput
|
||||||
|
{
|
||||||
|
Pending = !string.Equals(pendingHash, onlineHash, StringComparison.Ordinal),
|
||||||
|
OnlineVersion = onlineRecord.Version,
|
||||||
|
OnlinePublishTime = onlineRecord.PublishTime,
|
||||||
|
PendingContentHash = pendingHash,
|
||||||
|
OnlineContentHash = onlineHash
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ComputeContentHash(string content)
|
||||||
|
{
|
||||||
|
var hash = global::System.Security.Cryptography.SHA256.HashData(
|
||||||
|
global::System.Text.Encoding.UTF8.GetBytes(content));
|
||||||
|
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询服务渠道发布规则列表。
|
/// 查询服务渠道发布规则列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1057,8 +1057,9 @@ public partial class OrderService
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var channelIsRemote = await IsChannelRemoteHitAsync(
|
var channelRulesBinding = await ResolveChannelRulesBindingAsync(channel, bindTime);
|
||||||
channel,
|
var channelIsRemote = IsChannelRemoteHit(
|
||||||
|
channelRulesBinding,
|
||||||
order.ConsigneeCountry ?? string.Empty,
|
order.ConsigneeCountry ?? string.Empty,
|
||||||
recipient?.Province,
|
recipient?.Province,
|
||||||
recipient?.City,
|
recipient?.City,
|
||||||
@ -1068,15 +1069,38 @@ 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 channelZoneKeys = ResolveChannelZoneKeysOrSkip(
|
||||||
|
channelPriceBinding,
|
||||||
|
order.ConsigneeCountry ?? string.Empty,
|
||||||
|
recipient?.Province,
|
||||||
|
recipient?.City,
|
||||||
|
recipient?.ZipCode,
|
||||||
|
out var channelZoneMissReason);
|
||||||
|
if (channelZoneKeys == null)
|
||||||
|
{
|
||||||
|
skipped.Add(new OrderAllocationSkippedOutput
|
||||||
|
{
|
||||||
|
ServiceChannelId = channel.Id,
|
||||||
|
ChannelCode = channel.ChannelCode,
|
||||||
|
ChannelName = channel.ChannelName,
|
||||||
|
Stage = AllocationResolver.SkipStageBuyPrice,
|
||||||
|
MatchCondition = AllocationResolver.TruncateMatchCondition(provider.MatchCondition),
|
||||||
|
Reason = channelZoneMissReason
|
||||||
|
?? $"渠道买价分区未匹配(版本 {channelPriceBinding.PriceVersion})"
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var channelRemoteZoneKeys = channelIsRemote
|
var channelRemoteZoneKeys = channelIsRemote
|
||||||
? ParcelBillingCalculator.BuildRemoteZoneKeys(channel.RemoteAreaId)
|
? ParcelBillingCalculator.BuildRemoteZoneKeys(
|
||||||
|
channelRulesBinding.RemoteAreaId ?? channel.RemoteAreaId)
|
||||||
: Array.Empty<string>();
|
: Array.Empty<string>();
|
||||||
var previewFees = ParcelBillingCalculator.CalculatePayableFees(
|
var previewFees = ParcelBillingCalculator.CalculatePayableFees(
|
||||||
channelPriceBinding.Prices,
|
channelPriceBinding.Prices,
|
||||||
channelPriceBinding.PriceVersion,
|
channelPriceBinding.PriceVersion,
|
||||||
channelPriceBinding.PriceEffectiveTime,
|
channelPriceBinding.PriceEffectiveTime,
|
||||||
chargeableWeight,
|
chargeableWeight,
|
||||||
zoneKeys,
|
channelZoneKeys,
|
||||||
cargoAttribute,
|
cargoAttribute,
|
||||||
chargingStage,
|
chargingStage,
|
||||||
order.WarehouseId,
|
order.WarehouseId,
|
||||||
@ -1090,7 +1114,7 @@ public partial class OrderService
|
|||||||
var miss = ParcelBillingCalculator.ExplainPayablePriceMiss(
|
var miss = ParcelBillingCalculator.ExplainPayablePriceMiss(
|
||||||
channelPriceBinding.Prices,
|
channelPriceBinding.Prices,
|
||||||
chargeableWeight,
|
chargeableWeight,
|
||||||
zoneKeys,
|
channelZoneKeys,
|
||||||
cargoAttribute,
|
cargoAttribute,
|
||||||
order.WarehouseId,
|
order.WarehouseId,
|
||||||
warehouseName,
|
warehouseName,
|
||||||
@ -1369,6 +1393,15 @@ public partial class OrderService
|
|||||||
public string PriceVersion { get; init; } = string.Empty;
|
public string PriceVersion { get; init; } = string.Empty;
|
||||||
|
|
||||||
public DateTime PriceEffectiveTime { get; init; }
|
public DateTime PriceEffectiveTime { get; init; }
|
||||||
|
|
||||||
|
public ServiceChannelPricePublishSnapshotDto Snapshot { get; init; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ChannelRulesBinding
|
||||||
|
{
|
||||||
|
public int? RemoteAreaId { get; init; }
|
||||||
|
|
||||||
|
public ServiceChannelRulesPublishSnapshotDto Snapshot { get; init; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<ChannelPriceBinding> ResolveChannelPriceBindingAsync(int serviceChannelId, DateTime bindTime)
|
private async Task<ChannelPriceBinding> ResolveChannelPriceBindingAsync(int serviceChannelId, DateTime bindTime)
|
||||||
@ -1382,23 +1415,157 @@ public partial class OrderService
|
|||||||
var snapshot = OmsDocHelper.DeserializeChannelPriceSnapshot(priceRecord.Content);
|
var snapshot = OmsDocHelper.DeserializeChannelPriceSnapshot(priceRecord.Content);
|
||||||
if (snapshot?.Prices.Count > 0)
|
if (snapshot?.Prices.Count > 0)
|
||||||
{
|
{
|
||||||
|
await KpiAreaPackService.HydrateChannelPriceSnapshotAsync(snapshot);
|
||||||
return new ChannelPriceBinding
|
return new ChannelPriceBinding
|
||||||
{
|
{
|
||||||
Prices = snapshot.Prices.Select(PublishSnapshotSlimmer.ToChannelPriceListOutput).ToList(),
|
Prices = snapshot.Prices.Select(PublishSnapshotSlimmer.ToChannelPriceListOutput).ToList(),
|
||||||
PriceVersion = priceRecord.Version,
|
PriceVersion = priceRecord.Version,
|
||||||
PriceEffectiveTime = priceRecord.PublishTime
|
PriceEffectiveTime = priceRecord.PublishTime,
|
||||||
|
Snapshot = snapshot
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var liveSnapshot = await BuildLiveChannelPriceSnapshotAsync(serviceChannelId);
|
||||||
return new ChannelPriceBinding
|
return new ChannelPriceBinding
|
||||||
{
|
{
|
||||||
Prices = await BuildChannelPriceOutputsAsync(serviceChannelId),
|
Prices = liveSnapshot.Prices.Select(PublishSnapshotSlimmer.ToChannelPriceListOutput).ToList(),
|
||||||
PriceVersion = $"LIVE-{serviceChannelId}",
|
PriceVersion = $"LIVE-{serviceChannelId}",
|
||||||
PriceEffectiveTime = bindTime
|
PriceEffectiveTime = bindTime,
|
||||||
|
Snapshot = liveSnapshot
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<ChannelRulesBinding> ResolveChannelRulesBindingAsync(
|
||||||
|
ServiceChannel channel,
|
||||||
|
DateTime bindTime)
|
||||||
|
{
|
||||||
|
var rulesRecord = await ServiceChannelRepository.GetEffectivePublishRecordAsync(
|
||||||
|
channel.Id,
|
||||||
|
RulesOperatorType,
|
||||||
|
bindTime);
|
||||||
|
if (rulesRecord?.Content != null)
|
||||||
|
{
|
||||||
|
var snapshot = OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(rulesRecord.Content);
|
||||||
|
if (snapshot != null)
|
||||||
|
{
|
||||||
|
await KpiAreaPackService.HydrateChannelRulesSnapshotAsync(snapshot);
|
||||||
|
return new ChannelRulesBinding
|
||||||
|
{
|
||||||
|
RemoteAreaId = snapshot.RemoteAreaRef?.AreaId
|
||||||
|
?? snapshot.RemoteAreaId
|
||||||
|
?? snapshot.Channel?.RemoteAreaId
|
||||||
|
?? channel.RemoteAreaId,
|
||||||
|
Snapshot = snapshot
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var live = new ServiceChannelRulesPublishSnapshotDto
|
||||||
|
{
|
||||||
|
Channel = new ServiceChannelSaveInput
|
||||||
|
{
|
||||||
|
Id = channel.Id,
|
||||||
|
RemoteAreaId = channel.RemoteAreaId
|
||||||
|
},
|
||||||
|
RemoteAreaInfos = []
|
||||||
|
};
|
||||||
|
await KpiAreaPackService.LoadLiveRemoteIntoChannelRulesSnapshotAsync(
|
||||||
|
live,
|
||||||
|
channel.RemoteAreaId);
|
||||||
|
return new ChannelRulesBinding
|
||||||
|
{
|
||||||
|
RemoteAreaId = channel.RemoteAreaId,
|
||||||
|
Snapshot = live
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<ServiceChannelPricePublishSnapshotDto> BuildLiveChannelPriceSnapshotAsync(
|
||||||
|
int serviceChannelId)
|
||||||
|
{
|
||||||
|
var snapshot = new ServiceChannelPricePublishSnapshotDto
|
||||||
|
{
|
||||||
|
ServiceChannelId = serviceChannelId,
|
||||||
|
Prices = (await BuildChannelPriceOutputsAsync(serviceChannelId))
|
||||||
|
.Select(PublishSnapshotSlimmer.SlimChannelPrice)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
await PriceAreaSnapshotHelper.ApplyLiveChannelPriceAreaInfosAsync(
|
||||||
|
snapshot,
|
||||||
|
KpiAreaRepository,
|
||||||
|
KpiAreaPackService);
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按渠道买价价区包解析 zoneKeys;有价区配置但未命中时返回 null(跳过该渠道)。
|
||||||
|
/// 无价区配置时返回空列表(不回落产品卖价分区)。
|
||||||
|
/// </summary>
|
||||||
|
private static IReadOnlyList<string>? ResolveChannelZoneKeysOrSkip(
|
||||||
|
ChannelPriceBinding binding,
|
||||||
|
string countryCode,
|
||||||
|
string? province,
|
||||||
|
string? city,
|
||||||
|
string? zipCode,
|
||||||
|
out string? missReason)
|
||||||
|
{
|
||||||
|
missReason = null;
|
||||||
|
var snapshot = binding.Snapshot;
|
||||||
|
if (snapshot.PriceAreaRefs is { Count: > 0 }
|
||||||
|
&& snapshot.PriceAreaInfos is not { Count: > 0 })
|
||||||
|
{
|
||||||
|
missReason =
|
||||||
|
$"渠道价格分区包未加载: refs={snapshot.PriceAreaRefs.Count} version={binding.PriceVersion}";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.PriceAreaInfos is not { Count: > 0 })
|
||||||
|
{
|
||||||
|
return Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var keys = ParcelBillingCalculator.TryResolveZoneKeysFromChannelPriceSnapshot(
|
||||||
|
snapshot,
|
||||||
|
countryCode,
|
||||||
|
province,
|
||||||
|
city,
|
||||||
|
zipCode);
|
||||||
|
if (keys == null || keys.Count == 0)
|
||||||
|
{
|
||||||
|
missReason = $"渠道买价分区未匹配(版本 {binding.PriceVersion})";
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsChannelRemoteHit(
|
||||||
|
ChannelRulesBinding binding,
|
||||||
|
string countryCode,
|
||||||
|
string? province,
|
||||||
|
string? city,
|
||||||
|
string? zipCode)
|
||||||
|
{
|
||||||
|
var snapshot = binding.Snapshot;
|
||||||
|
if (snapshot.RemoteAreaRef != null && snapshot.RemoteAreaInfos is not { Count: > 0 })
|
||||||
|
{
|
||||||
|
throw new BusinessException(
|
||||||
|
$"渠道偏远分区包未加载: areaId={snapshot.RemoteAreaRef.AreaId} version={snapshot.RemoteAreaRef.PackVersion}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.RemoteAreaInfos is { Count: > 0 })
|
||||||
|
{
|
||||||
|
return GeoPartitionMatcher.IsHit(
|
||||||
|
snapshot.RemoteAreaInfos,
|
||||||
|
countryCode,
|
||||||
|
province,
|
||||||
|
city,
|
||||||
|
zipCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task<SalesProductRulesPublishSnapshotDto> BuildLiveRulesSnapshotAsync(SalesProduct product)
|
private async Task<SalesProductRulesPublishSnapshotDto> BuildLiveRulesSnapshotAsync(SalesProduct product)
|
||||||
{
|
{
|
||||||
var snapshot = new SalesProductRulesPublishSnapshotDto
|
var snapshot = new SalesProductRulesPublishSnapshotDto
|
||||||
@ -2970,19 +3137,4 @@ public partial class OrderService
|
|||||||
return OrderRuleEvaluationContext.FromOrderOnly(evalOrder);
|
return OrderRuleEvaluationContext.FromOrderOnly(evalOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> IsChannelRemoteHitAsync(
|
|
||||||
ServiceChannel channel,
|
|
||||||
string countryCode,
|
|
||||||
string? province,
|
|
||||||
string? city,
|
|
||||||
string? zipCode)
|
|
||||||
{
|
|
||||||
if (channel.RemoteAreaId is not > 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var rules = await KpiAreaRepository.GetAllInfosByAreaIdAsync(channel.RemoteAreaId.Value);
|
|
||||||
return GeoPartitionMatcher.IsHit(rules, countryCode, province, city, zipCode);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -107,15 +107,53 @@ internal static class ParcelBillingCalculator
|
|||||||
string countryCode,
|
string countryCode,
|
||||||
string? province,
|
string? province,
|
||||||
string? city,
|
string? city,
|
||||||
|
string? zipCode) =>
|
||||||
|
TryResolveZoneKeysFromAreaInfos(
|
||||||
|
priceSnapshot?.PriceAreaInfos,
|
||||||
|
priceSnapshot?.PriceAreaId,
|
||||||
|
priceSnapshot?.PriceAreaName,
|
||||||
|
countryCode,
|
||||||
|
province,
|
||||||
|
city,
|
||||||
|
zipCode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从渠道买价快照分区规则解析分区键(不查库;与产品卖价独立)。
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<string>? TryResolveZoneKeysFromChannelPriceSnapshot(
|
||||||
|
ServiceChannelPricePublishSnapshotDto? priceSnapshot,
|
||||||
|
string countryCode,
|
||||||
|
string? province,
|
||||||
|
string? city,
|
||||||
|
string? zipCode) =>
|
||||||
|
TryResolveZoneKeysFromAreaInfos(
|
||||||
|
priceSnapshot?.PriceAreaInfos,
|
||||||
|
priceSnapshot?.PriceAreaId,
|
||||||
|
priceSnapshot?.PriceAreaName,
|
||||||
|
countryCode,
|
||||||
|
province,
|
||||||
|
city,
|
||||||
|
zipCode);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按分区规则列表匹配地址并生成 zone 键。
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<string>? TryResolveZoneKeysFromAreaInfos(
|
||||||
|
IReadOnlyList<KpiAreaSnapshotRuleDto>? priceAreaInfos,
|
||||||
|
int? priceAreaId,
|
||||||
|
string? priceAreaName,
|
||||||
|
string countryCode,
|
||||||
|
string? province,
|
||||||
|
string? city,
|
||||||
string? zipCode)
|
string? zipCode)
|
||||||
{
|
{
|
||||||
if (priceSnapshot?.PriceAreaInfos == null || priceSnapshot.PriceAreaInfos.Count == 0)
|
if (priceAreaInfos == null || priceAreaInfos.Count == 0)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var matched = GeoPartitionMatcher.MatchSnapshot(
|
var matched = GeoPartitionMatcher.MatchSnapshot(
|
||||||
priceSnapshot.PriceAreaInfos,
|
priceAreaInfos,
|
||||||
countryCode,
|
countryCode,
|
||||||
province,
|
province,
|
||||||
city,
|
city,
|
||||||
@ -125,11 +163,10 @@ internal static class ParcelBillingCalculator
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 多价格分区表时优先用规则自身 AreaId,避免落到快照单一 PriceAreaId
|
var areaId = matched.AreaId > 0 ? matched.AreaId : priceAreaId;
|
||||||
var areaId = matched.AreaId > 0 ? matched.AreaId : priceSnapshot.PriceAreaId;
|
|
||||||
var areaName = !string.IsNullOrWhiteSpace(matched.AreaName)
|
var areaName = !string.IsNullOrWhiteSpace(matched.AreaName)
|
||||||
? matched.AreaName
|
? matched.AreaName
|
||||||
: priceSnapshot.PriceAreaName;
|
: priceAreaName;
|
||||||
var keys = BuildPriceZoneKeys(matched, areaName, areaId);
|
var keys = BuildPriceZoneKeys(matched, areaName, areaId);
|
||||||
return keys.Count > 0 ? keys : null;
|
return keys.Count > 0 ? keys : null;
|
||||||
}
|
}
|
||||||
@ -476,10 +513,22 @@ internal static class ParcelBillingCalculator
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 收集卖价快照中所有启用价格项的明细分区键(去重)。
|
/// 收集卖价快照中所有启用价格项的明细分区键(去重)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static IReadOnlyList<string> CollectDistinctDetailZoneKeys(SalesProductPricePublishSnapshotDto priceSnapshot)
|
public static IReadOnlyList<string> CollectDistinctDetailZoneKeys(SalesProductPricePublishSnapshotDto priceSnapshot) =>
|
||||||
|
CollectDistinctDetailZoneKeys(
|
||||||
|
priceSnapshot.Prices.Select(p => (p.Status, p.Details)));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 收集渠道买价快照中所有启用价格项的明细分区键(去重)。
|
||||||
|
/// </summary>
|
||||||
|
public static IReadOnlyList<string> CollectDistinctDetailZoneKeys(ServiceChannelPricePublishSnapshotDto priceSnapshot) =>
|
||||||
|
CollectDistinctDetailZoneKeys(
|
||||||
|
priceSnapshot.Prices.Select(p => (p.Status, p.Details)));
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> CollectDistinctDetailZoneKeys(
|
||||||
|
IEnumerable<(byte Status, List<ChannelPriceDetailOutput>? Details)> prices)
|
||||||
{
|
{
|
||||||
var zones = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
var zones = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||||
foreach (var price in priceSnapshot.Prices.Where(x => EnableStatusRules.IsEnabled(x.Status)))
|
foreach (var price in prices.Where(x => EnableStatusRules.IsEnabled(x.Status)))
|
||||||
{
|
{
|
||||||
foreach (var detail in price.Details ?? [])
|
foreach (var detail in price.Details ?? [])
|
||||||
{
|
{
|
||||||
|
|||||||
@ -3,6 +3,7 @@ using System.Text;
|
|||||||
using Fake.Application;
|
using Fake.Application;
|
||||||
using Fake.DependencyInjection;
|
using Fake.DependencyInjection;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
|
using Oms.Application.Dtos.Channels;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Domain.ChannelsAggregate.Entities;
|
using Oms.Domain.ChannelsAggregate.Entities;
|
||||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
@ -254,6 +255,131 @@ public class KpiAreaPackService : IScopedDependency
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道买价:Refs hydrate 到 Infos。
|
||||||
|
/// </summary>
|
||||||
|
public async Task HydrateChannelPriceSnapshotAsync(
|
||||||
|
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (snapshot.PriceAreaRefs is not { Count: > 0 })
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var merged = new List<KpiAreaSnapshotRuleDto>();
|
||||||
|
var seen = new HashSet<int>();
|
||||||
|
foreach (var packRef in snapshot.PriceAreaRefs)
|
||||||
|
{
|
||||||
|
var rules = await GetRulesOrThrowAsync(packRef, cancellationToken);
|
||||||
|
foreach (var rule in rules)
|
||||||
|
{
|
||||||
|
if (seen.Add(rule.Id))
|
||||||
|
{
|
||||||
|
merged.Add(rule);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.PriceAreaInfos = merged;
|
||||||
|
if (snapshot.PriceAreaRefs.Count == 1)
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = snapshot.PriceAreaRefs[0].AreaId;
|
||||||
|
snapshot.PriceAreaName = snapshot.PriceAreaRefs[0].AreaName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = null;
|
||||||
|
snapshot.PriceAreaName = string.Join(
|
||||||
|
"、",
|
||||||
|
snapshot.PriceAreaRefs
|
||||||
|
.Select(x => x.AreaName)
|
||||||
|
.Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道规则:偏远 Ref hydrate。
|
||||||
|
/// </summary>
|
||||||
|
public async Task HydrateChannelRulesSnapshotAsync(
|
||||||
|
ServiceChannelRulesPublishSnapshotDto snapshot,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (snapshot.RemoteAreaRef != null)
|
||||||
|
{
|
||||||
|
snapshot.RemoteAreaInfos = await GetRulesOrThrowAsync(snapshot.RemoteAreaRef, cancellationToken);
|
||||||
|
snapshot.RemoteAreaId = snapshot.RemoteAreaRef.AreaId;
|
||||||
|
snapshot.RemoteAreaName = snapshot.RemoteAreaRef.AreaName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadLiveInfosIntoChannelPriceSnapshotAsync(
|
||||||
|
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||||
|
IReadOnlyList<int> areaIds,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var merged = new List<KpiAreaSnapshotRuleDto>();
|
||||||
|
var seen = new HashSet<int>();
|
||||||
|
var names = new List<string>();
|
||||||
|
foreach (var areaId in areaIds.Where(id => id > 0).Distinct())
|
||||||
|
{
|
||||||
|
var area = await _kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == areaId, cancellationToken);
|
||||||
|
if (!string.IsNullOrWhiteSpace(area?.Name))
|
||||||
|
{
|
||||||
|
names.Add(area.Name.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
var infos = await _kpiAreaRepository.GetAllInfosByAreaIdAsync(areaId, cancellationToken);
|
||||||
|
foreach (var info in infos)
|
||||||
|
{
|
||||||
|
if (!seen.Add(info.Id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
merged.Add(KpiAreaSnapshotMapper.MapToSnapshotRule(info, area?.Name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PublishSnapshotNormalizer.NormalizeKpiAreas(merged);
|
||||||
|
snapshot.PriceAreaInfos = merged;
|
||||||
|
snapshot.PriceAreaRefs = [];
|
||||||
|
if (areaIds.Count == 1)
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = areaIds[0];
|
||||||
|
snapshot.PriceAreaName = names.Count > 0 ? names[0] : null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = null;
|
||||||
|
snapshot.PriceAreaName = names.Count > 0 ? string.Join("、", names) : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadLiveRemoteIntoChannelRulesSnapshotAsync(
|
||||||
|
ServiceChannelRulesPublishSnapshotDto snapshot,
|
||||||
|
int? remoteAreaId,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
snapshot.RemoteAreaRef = null;
|
||||||
|
if (remoteAreaId is > 0)
|
||||||
|
{
|
||||||
|
var area = await _kpiAreaRepository.FirstOrDefaultAsync(x => x.Id == remoteAreaId.Value, cancellationToken);
|
||||||
|
var infos = await _kpiAreaRepository.GetAllInfosByAreaIdAsync(remoteAreaId.Value, cancellationToken);
|
||||||
|
var rules = infos.Select(i => KpiAreaSnapshotMapper.MapToSnapshotRule(i, area?.Name)).ToList();
|
||||||
|
PublishSnapshotNormalizer.NormalizeKpiAreas(rules);
|
||||||
|
snapshot.RemoteAreaId = remoteAreaId;
|
||||||
|
snapshot.RemoteAreaName = area?.Name;
|
||||||
|
snapshot.RemoteAreaInfos = rules;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snapshot.RemoteAreaId = null;
|
||||||
|
snapshot.RemoteAreaName = null;
|
||||||
|
snapshot.RemoteAreaInfos = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task LoadLiveInfosIntoRulesSnapshotAsync(
|
public async Task LoadLiveInfosIntoRulesSnapshotAsync(
|
||||||
SalesProductRulesPublishSnapshotDto snapshot,
|
SalesProductRulesPublishSnapshotDto snapshot,
|
||||||
int? blacklistAreaId,
|
int? blacklistAreaId,
|
||||||
|
|||||||
@ -127,6 +127,96 @@ public static class PriceAreaSnapshotHelper
|
|||||||
return ids.Count > 0 ? ids[0] : null;
|
return ids.Count > 0 ? ids[0] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道买价发布:从明细推断价区表并钉包(与产品卖价分区独立)。
|
||||||
|
/// </summary>
|
||||||
|
public static async Task ApplyChannelPriceAreaToSnapshotAsync(
|
||||||
|
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||||
|
IKpiAreaRepository kpiAreaRepository,
|
||||||
|
KpiAreaPackService packService,
|
||||||
|
string? publishUser = null,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var areaIds = await CollectResolvedChannelPriceAreaIdsAsync(
|
||||||
|
snapshot,
|
||||||
|
kpiAreaRepository,
|
||||||
|
cancellationToken);
|
||||||
|
snapshot.PriceAreaInfos = [];
|
||||||
|
if (areaIds.Count == 0)
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = null;
|
||||||
|
snapshot.PriceAreaName = null;
|
||||||
|
snapshot.PriceAreaRefs = [];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var refs = new List<KpiAreaPackRefDto>();
|
||||||
|
foreach (var areaId in areaIds)
|
||||||
|
{
|
||||||
|
refs.Add(await packService.EnsurePackAsync(
|
||||||
|
areaId,
|
||||||
|
KpiAreaPackService.AreaTypePrice,
|
||||||
|
publishUser,
|
||||||
|
description: "service-channel-price-publish",
|
||||||
|
cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
snapshot.PriceAreaRefs = refs;
|
||||||
|
if (refs.Count == 1)
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = refs[0].AreaId;
|
||||||
|
snapshot.PriceAreaName = refs[0].AreaName;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
snapshot.PriceAreaId = null;
|
||||||
|
snapshot.PriceAreaName = string.Join(
|
||||||
|
"、",
|
||||||
|
refs.Select(x => x.AreaName).Where(x => !string.IsNullOrWhiteSpace(x)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 渠道买价 LIVE:不钉包,从当前库装 Infos。
|
||||||
|
/// </summary>
|
||||||
|
public static async Task ApplyLiveChannelPriceAreaInfosAsync(
|
||||||
|
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||||
|
IKpiAreaRepository kpiAreaRepository,
|
||||||
|
KpiAreaPackService packService,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var areaIds = await CollectResolvedChannelPriceAreaIdsAsync(
|
||||||
|
snapshot,
|
||||||
|
kpiAreaRepository,
|
||||||
|
cancellationToken);
|
||||||
|
await packService.LoadLiveInfosIntoChannelPriceSnapshotAsync(snapshot, areaIds, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static async Task<IReadOnlyList<int>> CollectResolvedChannelPriceAreaIdsAsync(
|
||||||
|
ServiceChannelPricePublishSnapshotDto snapshot,
|
||||||
|
IKpiAreaRepository kpiAreaRepository,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var result = new List<int>();
|
||||||
|
var seen = new HashSet<int>();
|
||||||
|
|
||||||
|
void Add(int? areaId)
|
||||||
|
{
|
||||||
|
if (areaId is > 0 && seen.Add(areaId.Value))
|
||||||
|
{
|
||||||
|
result.Add(areaId.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var zoneKeys = ParcelBillingCalculator.CollectDistinctDetailZoneKeys(snapshot);
|
||||||
|
foreach (var zoneKey in zoneKeys)
|
||||||
|
{
|
||||||
|
Add(await TryResolveZoneKeyToAreaIdAsync(zoneKey, kpiAreaRepository, cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<int?> TryResolveZoneKeyToAreaIdAsync(
|
private static async Task<int?> TryResolveZoneKeyToAreaIdAsync(
|
||||||
string zoneKey,
|
string zoneKey,
|
||||||
IKpiAreaRepository kpiAreaRepository,
|
IKpiAreaRepository kpiAreaRepository,
|
||||||
|
|||||||
@ -38,6 +38,7 @@ public static class PublishSnapshotNormalizer
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NormalizeKpiAreas(snapshot.PriceAreaInfos);
|
||||||
snapshot.Prices = SortChannelPriceItems(snapshot.Prices);
|
snapshot.Prices = SortChannelPriceItems(snapshot.Prices);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user