admin/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs
zzh a4d7c89e44 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.
2026-07-23 10:49:19 +08:00

324 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
namespace Oms.Application.Integrations.SmallBagTrack;
public interface ISmallBagTrackUpsertRequestBuilder
{
Task<SmallBagTrackUpsertRequest> BuildAsync(
Order order,
OrderParcelReceiverInput? receiverInput = null,
Customer? customer = null,
CancellationToken cancellationToken = default);
}
/// <summary>
/// 组装 UpsertSmallBag 请求:ApiCode 优先订单/发布快照中的 supplierCode,再 fallback live itfx。
/// </summary>
internal sealed class SmallBagTrackUpsertRequestBuilder(
IOrderRepository orderRepository,
ICustomerRepository customerRepository,
IServiceChannelRepository serviceChannelRepository,
IServiceChannelGroupRepository serviceChannelGroupRepository,
IItfxMetadataClient itfxMetadataClient,
ISalesProductRepository salesProductRepository,
AllocationProviderSnapshotEnricher allocationProviderSnapshotEnricher) : ISmallBagTrackUpsertRequestBuilder, IScopedDependency
{
private const string RulesOperatorType = "rules";
public async Task<SmallBagTrackUpsertRequest> BuildAsync(
Order order,
OrderParcelReceiverInput? receiverInput = null,
Customer? customer = null,
CancellationToken cancellationToken = default)
{
customer ??= await ResolveCustomerAsync(order, cancellationToken);
var recipients = await orderRepository.GetRecipientsByOrderIdsAsync([order.Id], cancellationToken);
var recipient = recipients.FirstOrDefault();
var apiCode = await ResolveTrackApiCodeAsync(order, cancellationToken);
return new SmallBagTrackUpsertRequest
{
Client = customer?.CustomerCode?.Trim() ?? string.Empty,
OrderNo = order.ServiceNo?.Trim() ?? string.Empty,
TrackNo = ResolveTrackNo(order),
OtherNo = order.ClientNo?.Trim() ?? string.Empty,
Channel = order.ChannelCode?.Trim() ?? string.Empty,
Product = order.ProductCode?.Trim() ?? string.Empty,
ApiCode = apiCode,
Consignee = new SmallBagTrackConsigneeRequest
{
CountryCode = receiverInput?.CountryCode?.Trim() ?? order.ConsigneeCountry?.Trim() ?? string.Empty,
Province = receiverInput?.Province?.Trim() ?? recipient?.Province?.Trim() ?? string.Empty,
Postcode = receiverInput?.ZipCode?.Trim() ?? recipient?.ZipCode?.Trim() ?? string.Empty,
City = receiverInput?.City?.Trim() ?? recipient?.City?.Trim() ?? string.Empty,
Email = receiverInput?.Email?.Trim() ?? recipient?.Email?.Trim() ?? string.Empty
}
};
}
internal static string ResolveTrackNo(Order order)
{
var trackNo = order.TrackNo?.Trim();
if (!string.IsNullOrWhiteSpace(trackNo))
{
return trackNo;
}
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)
{
return null;
}
return await customerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value, cancellationToken);
}
private async Task<string> ResolveTrackApiCodeAsync(Order order, CancellationToken cancellationToken)
{
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))
{
return fromChannel;
}
var provisional = await ResolveProvisionalApiCodeFromProductAsync(order, cancellationToken);
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)
{
if (string.IsNullOrWhiteSpace(channelCode))
{
return null;
}
var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode.Trim(), cancellationToken);
if (channel == null)
{
return null;
}
return await ResolveApiCodeFromServiceChannelAsync(channel, cancellationToken);
}
private async Task<string?> ResolveApiCodeFromServiceChannelAsync(
ServiceChannel channel,
CancellationToken cancellationToken)
{
if (!channel.GroupId.HasValue || channel.GroupId.Value <= 0)
{
return null;
}
var group = await serviceChannelGroupRepository.FirstOrDefaultAsync(
x => x.Id == channel.GroupId.Value,
cancellationToken);
if (group == null || group.ApiServiceProviderId <= 0)
{
return null;
}
var serviceProvider = await itfxMetadataClient.GetServiceProviderAsync(
group.ApiServiceProviderId,
cancellationToken);
return NullIfEmpty(serviceProvider?.Code);
}
/// <summary>
/// 创单尚未排货时,用销售产品排货规则里首个服务渠道的 API 服务商 code 作为轨迹 ApiCode 预占位。
/// </summary>
private async Task<string?> ResolveProvisionalApiCodeFromProductAsync(
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 providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id, cancellationToken);
var channelIds = providers
.Where(x => x.ServiceChannelId.HasValue)
.Select(x => x.ServiceChannelId!.Value)
.Distinct()
.ToArray();
if (channelIds.Length == 0)
{
return null;
}
var channels = await serviceChannelRepository.GetListAsync(x => channelIds.Contains(x.Id));
foreach (var channel in channels.OrderBy(x => x.Id))
{
var apiCode = await ResolveApiCodeFromServiceChannelAsync(channel, cancellationToken);
if (!string.IsNullOrWhiteSpace(apiCode))
{
return apiCode;
}
}
return null;
}
private static string? NullIfEmpty(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}