fix: resolve track ApiCode from product and channel publish snapshots

Read SupplierCode from rules publish snapshots before falling back to live itfx metadata so UpsertSmallBag gets apiCode at order create time.
This commit is contained in:
zzh 2026-07-23 10:49:19 +08:00
parent 15785cdf2b
commit a4d7c89e44
4 changed files with 189 additions and 8 deletions

View File

@ -0,0 +1,11 @@
namespace Oms.Application.Dtos.Channels;
/// <summary>
/// 服务渠道规则发布快照(与 t_service_channel_publish_record Content 同构)。
/// </summary>
public class ServiceChannelRulesPublishSnapshotDto
{
public ServiceChannelSaveInput? Channel { get; set; }
public ServiceChannelDownstreamSnapshotDto? Downstream { get; set; }
}

View File

@ -18,7 +18,7 @@ public class SmallBagTrackUpsertRequest
public string Product { get; set; } = string.Empty;
/// <summary>
/// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;取服务渠道 API 服务商 code)。
/// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;优先发布快照 SupplierCode)。
/// </summary>
public string ApiCode { get; set; } = string.Empty;

View File

@ -1,6 +1,8 @@
using Fake.DependencyInjection;
using Oms.Application.Dtos.Channels;
using Oms.Application.Dtos.Orders;
using Oms.Application.Integrations.Itfx;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Entities;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.CustomersAggregate.Entities;
@ -20,7 +22,7 @@ public interface ISmallBagTrackUpsertRequestBuilder
}
/// <summary>
/// 组装 UpsertSmallBag 请求:ApiCode 取服务渠道分组关联的 API 服务商 code,TrackNo 缺省用 ServiceNo。
/// 组装 UpsertSmallBag 请求:ApiCode 优先订单/发布快照中的 supplierCode,再 fallback live itfx。
/// </summary>
internal sealed class SmallBagTrackUpsertRequestBuilder(
IOrderRepository orderRepository,
@ -28,8 +30,11 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
IServiceChannelRepository serviceChannelRepository,
IServiceChannelGroupRepository serviceChannelGroupRepository,
IItfxMetadataClient itfxMetadataClient,
ISalesProductRepository salesProductRepository) : ISmallBagTrackUpsertRequestBuilder, IScopedDependency
ISalesProductRepository salesProductRepository,
AllocationProviderSnapshotEnricher allocationProviderSnapshotEnricher) : ISmallBagTrackUpsertRequestBuilder, IScopedDependency
{
private const string RulesOperatorType = "rules";
public async Task<SmallBagTrackUpsertRequest> BuildAsync(
Order order,
OrderParcelReceiverInput? receiverInput = null,
@ -73,6 +78,39 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
return order.ServiceNo?.Trim() ?? string.Empty;
}
internal static string? PickSupplierCodeFromProviders(
IEnumerable<SalesProductAllocationProviderSnapshotDto> providers,
string? channelCode)
{
var list = providers.ToList();
if (list.Count == 0)
{
return null;
}
if (!string.IsNullOrWhiteSpace(channelCode))
{
var matched = list.FirstOrDefault(provider =>
string.Equals(provider.ChannelCode?.Trim(), channelCode.Trim(), StringComparison.OrdinalIgnoreCase));
var matchedCode = NullIfEmpty(matched?.SupplierCode);
if (!string.IsNullOrWhiteSpace(matchedCode))
{
return matchedCode;
}
}
foreach (var provider in list.OrderBy(x => x.SerialNumber))
{
var code = NullIfEmpty(provider.SupplierCode);
if (!string.IsNullOrWhiteSpace(code))
{
return code;
}
}
return null;
}
private async Task<Customer?> ResolveCustomerAsync(Order order, CancellationToken cancellationToken)
{
if (!order.CustomerId.HasValue)
@ -85,12 +123,24 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
private async Task<string> ResolveTrackApiCodeAsync(Order order, CancellationToken cancellationToken)
{
var fromOrder = order.DownstreamSupplierCode?.Trim();
var fromOrder = NullIfEmpty(order.DownstreamSupplierCode);
if (!string.IsNullOrWhiteSpace(fromOrder))
{
return fromOrder;
}
var fromProductSnapshot = await ResolveApiCodeFromProductRulesSnapshotAsync(order, cancellationToken);
if (!string.IsNullOrWhiteSpace(fromProductSnapshot))
{
return fromProductSnapshot;
}
var fromChannelSnapshot = await ResolveApiCodeFromChannelRulesSnapshotAsync(order, cancellationToken);
if (!string.IsNullOrWhiteSpace(fromChannelSnapshot))
{
return fromChannelSnapshot;
}
var fromChannel = await ResolveApiCodeFromChannelCodeAsync(order.ChannelCode, cancellationToken);
if (!string.IsNullOrWhiteSpace(fromChannel))
{
@ -101,6 +151,89 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
return provisional ?? string.Empty;
}
private async Task<string?> ResolveApiCodeFromProductRulesSnapshotAsync(
Order order,
CancellationToken cancellationToken)
{
var productCode = order.ProductCode?.Trim();
if (string.IsNullOrWhiteSpace(productCode))
{
return null;
}
var product = await salesProductRepository.GetByProductCodeAsync(productCode, cancellationToken);
if (product == null)
{
return null;
}
var bindTime = order.CreateTime == default ? DateTime.Now : order.CreateTime;
var rulesVersion = order.SalesProductRulesVersion?.Trim();
SalesProductPublishRecord? rulesRecord = null;
if (!string.IsNullOrWhiteSpace(rulesVersion)
&& !rulesVersion.StartsWith("LIVE-", StringComparison.OrdinalIgnoreCase))
{
rulesRecord = await salesProductRepository.GetPublishRecordByVersionAsync(
product.Id,
RulesOperatorType,
rulesVersion,
cancellationToken);
}
else
{
rulesRecord = await salesProductRepository.GetEffectivePublishRecordAsync(
product.Id,
RulesOperatorType,
bindTime,
cancellationToken);
}
var rulesSnapshot = OmsDocHelper.DeserializeRulesSnapshot(rulesRecord?.Content);
if (rulesSnapshot?.Providers == null || rulesSnapshot.Providers.Count == 0)
{
return null;
}
var providers = rulesSnapshot.Providers;
if (providers.Any(AllocationProviderSnapshotEnricher.NeedsDownstreamEnrichment))
{
await allocationProviderSnapshotEnricher.EnrichAsync(providers, cancellationToken);
}
return PickSupplierCodeFromProviders(providers, order.ChannelCode);
}
private async Task<string?> ResolveApiCodeFromChannelRulesSnapshotAsync(
Order order,
CancellationToken cancellationToken)
{
var channelCode = order.ChannelCode?.Trim();
if (string.IsNullOrWhiteSpace(channelCode))
{
return null;
}
var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode, cancellationToken);
if (channel == null)
{
return null;
}
var bindTime = order.CreateTime == default ? DateTime.Now : order.CreateTime;
var rulesRecord = await serviceChannelRepository.GetEffectivePublishRecordAsync(
channel.Id,
RulesOperatorType,
bindTime,
cancellationToken);
if (rulesRecord?.Content == null)
{
return null;
}
var rulesSnapshot = OmsDocHelper.DeserializeJson<ServiceChannelRulesPublishSnapshotDto>(rulesRecord.Content);
return NullIfEmpty(rulesSnapshot?.Downstream?.SupplierCode);
}
private async Task<string?> ResolveApiCodeFromChannelCodeAsync(
string? channelCode,
CancellationToken cancellationToken)
@ -139,8 +272,7 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
var serviceProvider = await itfxMetadataClient.GetServiceProviderAsync(
group.ApiServiceProviderId,
cancellationToken);
var code = serviceProvider?.Code?.Trim();
return string.IsNullOrWhiteSpace(code) ? null : code;
return NullIfEmpty(serviceProvider?.Code);
}
/// <summary>
@ -156,13 +288,13 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
return null;
}
var product = await salesProductRepository.GetByProductCodeAsync(productCode);
var product = await salesProductRepository.GetByProductCodeAsync(productCode, cancellationToken);
if (product == null)
{
return null;
}
var providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id);
var providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id, cancellationToken);
var channelIds = providers
.Where(x => x.ServiceChannelId.HasValue)
.Select(x => x.ServiceChannelId!.Value)
@ -185,4 +317,7 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
return null;
}
private static string? NullIfEmpty(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}

View File

@ -1,3 +1,4 @@
using Oms.Application.Dtos.Channels;
using Oms.Application.Integrations.SmallBagTrack;
using Oms.Domain.OrdersAggregate.Entities;
using Xunit;
@ -51,6 +52,40 @@ public class SmallBagTrackUpsertRequestBuilderTests
Assert.Equal(2, body.Source);
}
[Fact]
public void PickSupplierCodeFromProviders_MatchesChannelCode()
{
var providers = new List<SalesProductAllocationProviderSnapshotDto>
{
new()
{
SerialNumber = 1,
ChannelCode = "test01",
SupplierCode = "F1"
},
new()
{
SerialNumber = 2,
ChannelCode = "other",
SupplierCode = "X9"
}
};
Assert.Equal("F1", SmallBagTrackUpsertRequestBuilder.PickSupplierCodeFromProviders(providers, "test01"));
}
[Fact]
public void PickSupplierCodeFromProviders_FallsBackToFirstSupplierCode()
{
var providers = new List<SalesProductAllocationProviderSnapshotDto>
{
new() { SerialNumber = 2, ChannelCode = "other", SupplierCode = "X9" },
new() { SerialNumber = 1, ChannelCode = "test01", SupplierCode = "F1" }
};
Assert.Equal("F1", SmallBagTrackUpsertRequestBuilder.PickSupplierCodeFromProviders(providers, "unknown"));
}
[Theory]
[InlineData("A0301")]
[InlineData("A0701")]