From a4d7c89e44c3610e6276785557865d3644ea7595 Mon Sep 17 00:00:00 2001
From: zzh <1368735060@qq.com>
Date: Thu, 23 Jul 2026 10:49:19 +0800
Subject: [PATCH] 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.
---
.../ServiceChannelRulesPublishSnapshotDto.cs | 11 ++
.../SmallBagTrackUpsertRequest.cs | 2 +-
.../SmallBagTrackUpsertRequestBuilder.cs | 149 +++++++++++++++++-
.../SmallBagTrackUpsertRequestBuilderTests.cs | 35 ++++
4 files changed, 189 insertions(+), 8 deletions(-)
create mode 100644 src/Oms.Application/Dtos/Channels/ServiceChannelRulesPublishSnapshotDto.cs
diff --git a/src/Oms.Application/Dtos/Channels/ServiceChannelRulesPublishSnapshotDto.cs b/src/Oms.Application/Dtos/Channels/ServiceChannelRulesPublishSnapshotDto.cs
new file mode 100644
index 0000000..553f95f
--- /dev/null
+++ b/src/Oms.Application/Dtos/Channels/ServiceChannelRulesPublishSnapshotDto.cs
@@ -0,0 +1,11 @@
+namespace Oms.Application.Dtos.Channels;
+
+///
+/// 服务渠道规则发布快照(与 t_service_channel_publish_record Content 同构)。
+///
+public class ServiceChannelRulesPublishSnapshotDto
+{
+ public ServiceChannelSaveInput? Channel { get; set; }
+
+ public ServiceChannelDownstreamSnapshotDto? Downstream { get; set; }
+}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs
index 949bd1a..96bf2f3 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs
@@ -18,7 +18,7 @@ public class SmallBagTrackUpsertRequest
public string Product { get; set; } = string.Empty;
///
- /// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;取服务渠道 API 服务商 code)。
+ /// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;优先发布快照 SupplierCode)。
///
public string ApiCode { get; set; } = string.Empty;
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs
index 6ae8f4f..7b79ebe 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs
@@ -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
}
///
-/// 组装 UpsertSmallBag 请求:ApiCode 取服务渠道分组关联的 API 服务商 code,TrackNo 缺省用 ServiceNo。
+/// 组装 UpsertSmallBag 请求:ApiCode 优先订单/发布快照中的 supplierCode,再 fallback live itfx。
///
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 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 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 ResolveCustomerAsync(Order order, CancellationToken cancellationToken)
{
if (!order.CustomerId.HasValue)
@@ -85,12 +123,24 @@ internal sealed class SmallBagTrackUpsertRequestBuilder(
private async Task 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 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 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(rulesRecord.Content);
+ return NullIfEmpty(rulesSnapshot?.Downstream?.SupplierCode);
+ }
+
private async Task 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);
}
///
@@ -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();
}
diff --git a/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs b/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs
index 4880647..4be8521 100644
--- a/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs
+++ b/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs
@@ -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
+ {
+ 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
+ {
+ 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")]