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")]