From 1545bc385f5efcc057420405fd2be686507e0714 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Wed, 22 Jul 2026 20:11:25 +0800 Subject: [PATCH] feat: ????????????? A0101/A0301/A0701 Fix UpsertSmallBag ApiCode/TrackNo assembly and MQ-disabled direct fallback; append lifecycle tracks on parcel create, inbound, and outbound. Redact channel fields for customer order list. Co-authored-by: Cursor --- .../ISmallBagTrackOrderClient.cs | 9 ++ .../ISmallBagTrackOrderNotifier.cs | 5 + .../SmallBagTrackLifecycleEvents.cs | 23 +++ .../SmallBagTrack/SmallBagTrackOrderClient.cs | 145 +++++++++++++++-- .../SmallBagTrackOrderNotifier.cs | 34 +++- .../SmallBagTrackServiceExtensions.cs | 1 + .../SmallBagTrackUpsertRequest.cs | 7 +- .../SmallBagTrackUpsertRequestBuilder.cs | 147 ++++++++++++++++++ .../SmallBagTrackUpsertTrackApiRequest.cs | 43 +++++ .../Orders/Batch/OrderBatchActionExecutor.cs | 29 +--- .../Orders/Batch/OrderBatchJobCoordinator.cs | 7 +- .../Services/Orders/OrderService.Helpers.cs | 22 ++- .../Orders/OrderService.ParcelBilling.cs | 73 +++++---- src/Oms.E2E.Tests/Fixtures/OrderE2eFixture.cs | 4 + .../SmallBagTrackUpsertRequestBuilderTests.cs | 67 ++++++++ 15 files changed, 536 insertions(+), 80 deletions(-) create mode 100644 src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs create mode 100644 src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs create mode 100644 src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs create mode 100644 src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs diff --git a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs index bc339a2..9714178 100644 --- a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs +++ b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs @@ -4,6 +4,15 @@ public interface ISmallBagTrackOrderClient { Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default); + /// + /// 追加一条生命周期轨迹节点(POST ProviderService/Track/Upsert)。 + /// + Task AppendLifecycleTrackAsync( + string orderNo, + string trackCode, + string? authorization = null, + CancellationToken cancellationToken = default); + /// /// 同步调用并返回完整请求/响应(仅调试)。 /// diff --git a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderNotifier.cs b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderNotifier.cs index 000217b..9858605 100644 --- a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderNotifier.cs +++ b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderNotifier.cs @@ -6,4 +6,9 @@ namespace Oms.Application.Integrations.SmallBagTrack; public interface ISmallBagTrackOrderNotifier { void Enqueue(SmallBagTrackUpsertRequest request, int orderId, int? customerId = null); + + /// + /// 异步追加生命周期轨迹节点。 + /// + void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null); } diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs new file mode 100644 index 0000000..fceed43 --- /dev/null +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs @@ -0,0 +1,23 @@ +namespace Oms.Application.Integrations.SmallBagTrack; + +/// +/// 小包订单生命周期默认轨迹节点(POST ProviderService/Track/Upsert)。 +/// +internal static class SmallBagTrackLifecycleEvents +{ + public const string CreateTrackCode = "A0101"; + + public const string InboundTrackCode = "A0301"; + + public const string OutboundTrackCode = "A0701"; + + public const string TrackPlace = "SZ"; + + public const float TrackTimeZone = 8f; + + /// 轨迹订单类型:小包。 + public const int OrderType = 1; + + /// 数据来源:内部系统。 + public const int Source = 2; +} diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs index 5149e26..3059463 100644 --- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs @@ -44,8 +44,24 @@ internal sealed class SmallBagTrackOrderClient( } logger.LogInformation("UpsertSmallBag succeeded for order {OrderNo}", request.OrderNo); + + if (request.AppendInitialTrackOnSuccess) + { + await AppendLifecycleTrackAsync( + request.OrderNo, + SmallBagTrackLifecycleEvents.CreateTrackCode, + request.Authorization, + cancellationToken); + } } + public Task AppendLifecycleTrackAsync( + string orderNo, + string trackCode, + string? authorization = null, + CancellationToken cancellationToken = default) => + AppendLifecycleTrackCoreAsync(orderNo, trackCode, authorization, cancellationToken); + public async Task UpsertDebugAsync( SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default) @@ -67,14 +83,120 @@ internal sealed class SmallBagTrackOrderClient( } var appKey = settings.AppKey.Trim(); - var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); - var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, settings.AppSecret.Trim()); var body = SmallBagTrackUpsertApiRequest.FromBusinessRequest(request); var url = SmallBagTrackUpsertApiRequest.BuildEndpointUrl(settings.BaseUrl); var authorization = ResolveAuthorization(request); - var curlCommand = SmallBagTrackCurlBuilder.Build(url, appKey, timestamp, sign, authorization, body, JsonOptions); var requestJson = JsonSerializer.Serialize(body, JsonOptions); + var response = await SendSignedPostAsync( + settings, + appKey, + url, + body, + authorization, + cancellationToken); + + var curlCommand = SmallBagTrackCurlBuilder.Build( + url, + appKey, + response.Timestamp, + response.Sign, + authorization, + body, + JsonOptions); + + return new SmallBagTrackUpsertResult + { + Skipped = false, + Success = response.Response.IsSuccessStatusCode, + Url = url, + RequestJson = requestJson, + CurlCommand = curlCommand, + StatusCode = (int)response.Response.StatusCode, + ResponseBody = response.ResponseBody + }; + } + + private async Task AppendLifecycleTrackCoreAsync( + string orderNo, + string trackCode, + string? authorization, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(orderNo)) + { + logger.LogWarning("Skip lifecycle track append: orderNo empty"); + return; + } + + if (string.IsNullOrWhiteSpace(trackCode)) + { + logger.LogWarning("Skip lifecycle track append for order {OrderNo}: trackCode empty", orderNo); + return; + } + + var settings = await configProvider.GetAsync(cancellationToken); + if (!settings.Enabled + || string.IsNullOrWhiteSpace(settings.BaseUrl) + || string.IsNullOrWhiteSpace(settings.AppKey) + || string.IsNullOrWhiteSpace(settings.AppSecret)) + { + return; + } + + var appKey = settings.AppKey.Trim(); + var trackTime = DateTime.Now; + var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent( + appKey, + orderNo.Trim(), + trackCode.Trim(), + trackTime); + var url = SmallBagTrackUpsertTrackApiRequest.BuildEndpointUrl(settings.BaseUrl); + + try + { + var response = await SendSignedPostAsync( + settings, + appKey, + url, + body, + authorization, + cancellationToken); + + if (response.Response.IsSuccessStatusCode) + { + logger.LogInformation( + "Lifecycle track {TrackCode} appended for order {OrderNo} at {TrackPlace}", + trackCode, + orderNo, + SmallBagTrackLifecycleEvents.TrackPlace); + return; + } + + logger.LogWarning( + "Lifecycle track {TrackCode} append failed for order {OrderNo}: {StatusCode} {Body}", + trackCode, + orderNo, + (int)response.Response.StatusCode, + response.ResponseBody); + } + catch (Exception ex) + { + logger.LogWarning(ex, "Lifecycle track {TrackCode} append failed for order {OrderNo}", trackCode, orderNo); + } + } + + private async Task SendSignedPostAsync( + SmallBagTrackOptions settings, + string appKey, + string url, + TBody body, + string? authorization, + CancellationToken cancellationToken) + { + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, settings.AppSecret.Trim()); + using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url); httpRequest.Headers.TryAddWithoutValidation("appKey", appKey); httpRequest.Headers.TryAddWithoutValidation("timestamp", timestamp.ToString()); @@ -87,16 +209,7 @@ internal sealed class SmallBagTrackOrderClient( using var response = await client.SendAsync(httpRequest, cancellationToken); var responseBody = await response.Content.ReadAsStringAsync(cancellationToken); - return new SmallBagTrackUpsertResult - { - Skipped = false, - Success = response.IsSuccessStatusCode, - Url = url, - RequestJson = requestJson, - CurlCommand = curlCommand, - StatusCode = (int)response.StatusCode, - ResponseBody = responseBody - }; + return new SignedTrackHttpResponse(response, responseBody, timestamp, sign); } private string? ResolveAuthorization(SmallBagTrackUpsertRequest request) @@ -125,4 +238,10 @@ internal sealed class SmallBagTrackOrderClient( Skipped = true, SkipReason = reason }; + + private sealed record SignedTrackHttpResponse( + HttpResponseMessage Response, + string ResponseBody, + long Timestamp, + string Sign); } diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderNotifier.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderNotifier.cs index 31024d3..2f728b6 100644 --- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderNotifier.cs +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderNotifier.cs @@ -16,7 +16,11 @@ internal sealed class SmallBagTrackOrderNotifier( { using var scope = scopeFactory.CreateScope(); var coordinator = scope.ServiceProvider.GetRequiredService(); - await coordinator.PublishTrackUpsertAsync(request, orderId, customerId); + var published = await coordinator.PublishTrackUpsertAsync(request, orderId, customerId); + if (!published) + { + await FallbackDirectAsync(request, orderId); + } } catch (Exception ex) { @@ -24,12 +28,12 @@ internal sealed class SmallBagTrackOrderNotifier( ex, "RabbitMQ track upsert publish failed, fallback to direct call. orderId={OrderId}", orderId); - await FallbackDirectAsync(request); + await FallbackDirectAsync(request, orderId); } }); } - private async Task FallbackDirectAsync(SmallBagTrackUpsertRequest request) + private async Task FallbackDirectAsync(SmallBagTrackUpsertRequest request, int orderId) { try { @@ -39,7 +43,29 @@ internal sealed class SmallBagTrackOrderNotifier( } catch (Exception ex) { - logger.LogWarning(ex, "Fallback UpsertSmallBag failed for order {OrderNo}", request.OrderNo); + logger.LogWarning(ex, "Direct UpsertSmallBag failed for orderId={OrderId} orderNo={OrderNo}", orderId, request.OrderNo); } } + + public void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null) + { + _ = Task.Run(async () => + { + try + { + using var scope = scopeFactory.CreateScope(); + var client = scope.ServiceProvider.GetRequiredService(); + await client.AppendLifecycleTrackAsync(orderNo, trackCode, authorization); + } + catch (Exception ex) + { + logger.LogWarning( + ex, + "Async lifecycle track append failed. orderId={OrderId} orderNo={OrderNo} trackCode={TrackCode}", + orderId, + orderNo, + trackCode); + } + }); + } } diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs index 74cc34b..d8d0bb3 100644 --- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs @@ -10,6 +10,7 @@ public static class SmallBagTrackServiceExtensions services.Configure(configuration.GetSection(SmallBagTrackOptions.SectionName)); services.AddHttpClient(SmallBagTrackOrderClient.HttpClientName); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddSingleton(); return services; diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs index 726d2ce..2cbf4b5 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,取订单 DownstreamSupplierCode)。 + /// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;优先 ChannelForecastCode)。 /// public string ApiCode { get; set; } = string.Empty; @@ -28,6 +28,11 @@ public class SmallBagTrackUpsertRequest /// 转发到轨迹网关的 Authorization(异步入队前从当前 HTTP 请求捕获)。 /// public string? Authorization { get; set; } + + /// + /// UpsertSmallBag 成功后是否追加创单默认轨迹(A0101/SZ)。 + /// + public bool AppendInitialTrackOnSuccess { get; set; } } public class SmallBagTrackConsigneeRequest diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs new file mode 100644 index 0000000..7c20b4e --- /dev/null +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs @@ -0,0 +1,147 @@ +using Fake.DependencyInjection; +using Oms.Application.Dtos.Orders; +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 BuildAsync( + Order order, + OrderParcelReceiverInput? receiverInput = null, + Customer? customer = null, + CancellationToken cancellationToken = default); +} + +/// +/// 组装 UpsertSmallBag 请求:ApiCode 优先 ChannelForecastCode,TrackNo 缺省用 ServiceNo。 +/// +internal sealed class SmallBagTrackUpsertRequestBuilder( + IOrderRepository orderRepository, + ICustomerRepository customerRepository, + IServiceChannelRepository serviceChannelRepository, + ISalesProductRepository salesProductRepository) : ISmallBagTrackUpsertRequestBuilder, IScopedDependency +{ + public async Task 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; + } + + private async Task 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 ResolveTrackApiCodeAsync(Order order, CancellationToken cancellationToken) + { + var fromOrder = order.ChannelForecastCode?.Trim(); + if (!string.IsNullOrWhiteSpace(fromOrder)) + { + return fromOrder; + } + + var channelCode = order.ChannelCode?.Trim(); + if (!string.IsNullOrWhiteSpace(channelCode)) + { + var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode, cancellationToken); + var forecast = channel?.ForecastCode?.Trim(); + if (!string.IsNullOrWhiteSpace(forecast)) + { + return forecast; + } + } + + var provisional = await ResolveProvisionalApiCodeFromProductAsync(order, cancellationToken); + return provisional ?? string.Empty; + } + + /// + /// 创单尚未排货时,用销售产品排货规则里首个渠道的 ForecastCode 作为轨迹 ApiCode 预占位。 + /// + private async Task ResolveProvisionalApiCodeFromProductAsync( + Order order, + CancellationToken cancellationToken) + { + var productCode = order.ProductCode?.Trim(); + if (string.IsNullOrWhiteSpace(productCode)) + { + return null; + } + + var product = await salesProductRepository.GetByProductCodeAsync(productCode); + if (product == null) + { + return null; + } + + var providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id); + 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 forecast = channel.ForecastCode?.Trim(); + if (!string.IsNullOrWhiteSpace(forecast)) + { + return forecast; + } + } + + return null; + } +} diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs new file mode 100644 index 0000000..24f2c05 --- /dev/null +++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs @@ -0,0 +1,43 @@ +namespace Oms.Application.Integrations.SmallBagTrack; + +internal sealed class SmallBagTrackUpsertTrackApiRequest +{ + public string AppKey { get; set; } = string.Empty; + + public int OrderType { get; set; } + + public string OrderNo { get; set; } = string.Empty; + + public bool IsAdd { get; set; } = true; + + public string TrackCode { get; set; } = string.Empty; + + public string TrackPlace { get; set; } = string.Empty; + + public DateTime TrackTime { get; set; } + + public float TrackTimeZone { get; set; } + + public int Source { get; set; } + + public static SmallBagTrackUpsertTrackApiRequest CreateLifecycleEvent( + string appKey, + string orderNo, + string trackCode, + DateTime trackTime) => + new() + { + AppKey = appKey, + OrderType = SmallBagTrackLifecycleEvents.OrderType, + OrderNo = orderNo, + IsAdd = true, + TrackCode = trackCode, + TrackPlace = SmallBagTrackLifecycleEvents.TrackPlace, + TrackTime = trackTime, + TrackTimeZone = SmallBagTrackLifecycleEvents.TrackTimeZone, + Source = SmallBagTrackLifecycleEvents.Source + }; + + public static string BuildEndpointUrl(string baseUrl) => + $"{baseUrl.Trim().TrimEnd('/')}/ProviderService/Track/Upsert"; +} diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs index e989bc9..66dd698 100644 --- a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs +++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs @@ -17,6 +17,7 @@ public sealed class OrderBatchActionExecutor( IOrderRepository orderRepository, ICountryRepository countryRepository, ISmallBagTrackOrderClient smallBagTrackOrderClient, + ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder, OrderDownstreamCredentialResolver downstreamCredentialResolver, IItfxForwardClient itfxForwardClient, OrderBatchCreateActionHandler createActionHandler, @@ -79,7 +80,7 @@ public sealed class OrderBatchActionExecutor( return OrderBatchActionExecuteResult.BusinessFail("订单运单号为空,无法同步轨迹"); } - request = await BuildTrackRequestAsync(order, cancellationToken); + request = await smallBagTrackUpsertRequestBuilder.BuildAsync(order, cancellationToken: cancellationToken); } await smallBagTrackOrderClient.UpsertAsync(request, cancellationToken); @@ -147,30 +148,4 @@ public sealed class OrderBatchActionExecutor( return OrderBatchActionExecuteResult.Ok(); } - - private async Task BuildTrackRequestAsync( - Domain.OrdersAggregate.Entities.Order order, - CancellationToken cancellationToken) - { - var recipients = await orderRepository.GetRecipientsByOrderIdsAsync([order.Id], cancellationToken); - var recipient = recipients.FirstOrDefault(); - - return new SmallBagTrackUpsertRequest - { - OrderNo = order.ServiceNo ?? string.Empty, - TrackNo = order.TrackNo?.Trim() ?? string.Empty, - OtherNo = order.ClientNo?.Trim() ?? string.Empty, - Channel = order.ChannelCode?.Trim() ?? string.Empty, - Product = order.ProductCode?.Trim() ?? string.Empty, - ApiCode = order.DownstreamSupplierCode?.Trim() ?? string.Empty, - Consignee = new SmallBagTrackConsigneeRequest - { - CountryCode = order.ConsigneeCountry?.Trim() ?? string.Empty, - Province = recipient?.Province?.Trim() ?? string.Empty, - Postcode = recipient?.ZipCode?.Trim() ?? string.Empty, - City = recipient?.City?.Trim() ?? string.Empty, - Email = recipient?.Email?.Trim() ?? string.Empty, - } - }; - } } diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs index 4e29a8b..903e1d7 100644 --- a/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs +++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs @@ -366,12 +366,12 @@ public sealed class OrderBatchJobCoordinator( output.TaskName = string.IsNullOrWhiteSpace(meta.TaskName) ? null : meta.TaskName.Trim(); } - public async Task PublishTrackUpsertAsync(SmallBagTrackUpsertRequest request, int orderId, int? customerId) + public async Task PublishTrackUpsertAsync(SmallBagTrackUpsertRequest request, int orderId, int? customerId) { if (!publisher.IsEnabled) { - logger.LogDebug("RabbitMQ disabled, skip track upsert publish for order {OrderId}", orderId); - return; + logger.LogDebug("RabbitMQ disabled, track upsert will use direct call. orderId={OrderId}", orderId); + return false; } var message = new OrderBatchJobMessage @@ -387,6 +387,7 @@ public sealed class OrderBatchJobCoordinator( }; await publisher.PublishAsync(message); + return true; } public async Task GetJobAsync(int jobId, CancellationToken cancellationToken = default) diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs index 40fabab..8105ddd 100644 --- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs +++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs @@ -89,7 +89,7 @@ public partial class OrderService : await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id)); var customerMap = customers.ToDictionary(x => x.Id); - return orders.Select(order => + var outputs = orders.Select(order => { senderMap.TryGetValue(order.Id, out var sender); recipientMap.TryGetValue(order.Id, out var recipient); @@ -198,6 +198,26 @@ public partial class OrderService ReceivableAmount = feeOutputs.Count == 0 ? null : receivableAmount }; }).ToList(); + + if (OmsSessionHelper.IsCustomerLogin(CurrentUser)) + { + RedactChannelFieldsForCustomer(outputs); + } + + return outputs; + } + + private static void RedactChannelFieldsForCustomer(IEnumerable outputs) + { + foreach (var output in outputs) + { + output.ChannelCode = null; + output.ChannelName = null; + output.DownstreamSupplierCode = null; + output.DownstreamAppKey = null; + output.DownstreamAppId = null; + output.ChannelForecastCode = null; + } } private async Task> BuildOrderExportRowsAsync(IReadOnlyCollection orders) diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs index e06e475..a8393a0 100644 --- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs +++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs @@ -155,7 +155,7 @@ public partial class OrderService order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); - await EnqueueSmallBagTrackUpsertAsync(order, customer, input.Receiver); + await EnqueueSmallBagTrackUpsertAsync(order, customer, input.Receiver, appendInitialTrack: true); await EnqueueGenerateLabelPdfAsync(order); try @@ -1625,6 +1625,16 @@ public partial class OrderService order.ReceiveTime = DateTime.Now; order.UpdateTime = DateTime.Now; await OrderRepository.UpdateAsync(order); + + EnqueueLifecycleTrackAppend(order, SmallBagTrackLifecycleEvents.InboundTrackCode); + + if (allocated) + { + var customer = order.CustomerId.HasValue + ? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value) + : null; + await EnqueueSmallBagTrackUpsertAsync(order, customer, receiver: null); + } } /// @@ -1706,6 +1716,8 @@ public partial class OrderService order.ShipTime = now; order.UpdateTime = now; await OrderRepository.UpdateAsync(order); + + EnqueueLifecycleTrackAppend(order, SmallBagTrackLifecycleEvents.OutboundTrackCode); } private async Task BuildParcelCreateOutputAsync(Order order) @@ -1764,9 +1776,11 @@ public partial class OrderService private async Task EnqueueSmallBagTrackUpsertAsync( Order order, Customer? customer, - OrderParcelReceiverInput? receiver) + OrderParcelReceiverInput? receiver, + bool appendInitialTrack = false) { var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver); + request.AppendInitialTrackOnSuccess = appendInitialTrack; var authorization = LazyServiceProvider .GetRequiredService() .HttpContext? @@ -1782,6 +1796,28 @@ public partial class OrderService SmallBagTrackOrderNotifier.Enqueue(request, order.Id, customer?.Id); } + private void EnqueueLifecycleTrackAppend(Order order, string trackCode) + { + if (string.IsNullOrWhiteSpace(order.ServiceNo)) + { + return; + } + + var authorization = LazyServiceProvider + .GetRequiredService() + .HttpContext? + .Request + .Headers + .Authorization + .ToString(); + + SmallBagTrackOrderNotifier.EnqueueLifecycleTrackAppend( + order.ServiceNo, + trackCode, + order.Id, + string.IsNullOrWhiteSpace(authorization) ? null : authorization); + } + /// /// 按订单同步轨迹(UpsertSmallBag),返回完整请求与响应便于联调。 /// @@ -1802,37 +1838,12 @@ public partial class OrderService Order order, OrderParcelReceiverInput? receiverInput = null) { - Customer? customer = null; - if (order.CustomerId.HasValue) - { - customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value); - } - - var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]); - var recipient = recipients.FirstOrDefault(); - - var apiCode = order.DownstreamSupplierCode?.Trim() ?? string.Empty; - - return new SmallBagTrackUpsertRequest - { - Client = customer?.CustomerCode?.Trim() ?? string.Empty, - OrderNo = order.ServiceNo, - TrackNo = order.TrackNo?.Trim() ?? string.Empty, - 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 - } - }; + return await SmallBagTrackUpsertRequestBuilder.BuildAsync(order, receiverInput); } + private ISmallBagTrackUpsertRequestBuilder SmallBagTrackUpsertRequestBuilder => + LazyServiceProvider.GetRequiredService(); + private ISmallBagTrackOrderClient SmallBagTrackOrderClient => LazyServiceProvider.GetRequiredService(); diff --git a/src/Oms.E2E.Tests/Fixtures/OrderE2eFixture.cs b/src/Oms.E2E.Tests/Fixtures/OrderE2eFixture.cs index 292851f..f9938e1 100644 --- a/src/Oms.E2E.Tests/Fixtures/OrderE2eFixture.cs +++ b/src/Oms.E2E.Tests/Fixtures/OrderE2eFixture.cs @@ -87,6 +87,10 @@ public sealed class OrderE2eFixture : IAsyncLifetime public void Enqueue(SmallBagTrackUpsertRequest request, int orderId, int? customerId = null) { } + + public void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null) + { + } } } diff --git a/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs b/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs new file mode 100644 index 0000000..4880647 --- /dev/null +++ b/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs @@ -0,0 +1,67 @@ +using Oms.Application.Integrations.SmallBagTrack; +using Oms.Domain.OrdersAggregate.Entities; +using Xunit; + +namespace Oms.Tests; + +public class SmallBagTrackUpsertRequestBuilderTests +{ + [Fact] + public void ResolveTrackNo_UsesTrackNoWhenPresent() + { + var order = new Order + { + ServiceNo = "IDE2607221859123456", + TrackNo = "LP123456789CN" + }; + + Assert.Equal("LP123456789CN", SmallBagTrackUpsertRequestBuilder.ResolveTrackNo(order)); + } + + [Fact] + public void ResolveTrackNo_FallsBackToServiceNoWhenTrackNoEmpty() + { + var order = new Order + { + ServiceNo = "IDE2607221859123456", + TrackNo = null + }; + + Assert.Equal("IDE2607221859123456", SmallBagTrackUpsertRequestBuilder.ResolveTrackNo(order)); + } + + [Fact] + public void CreateLifecycleEvent_UsesConfiguredDefaults() + { + var trackTime = new DateTime(2026, 7, 22, 20, 7, 0); + var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent( + "APP001", + "IDE2607221859123456", + SmallBagTrackLifecycleEvents.CreateTrackCode, + trackTime); + + Assert.Equal("APP001", body.AppKey); + Assert.Equal("IDE2607221859123456", body.OrderNo); + Assert.True(body.IsAdd); + Assert.Equal("A0101", body.TrackCode); + Assert.Equal("SZ", body.TrackPlace); + Assert.Equal(trackTime, body.TrackTime); + Assert.Equal(8f, body.TrackTimeZone); + Assert.Equal(1, body.OrderType); + Assert.Equal(2, body.Source); + } + + [Theory] + [InlineData("A0301")] + [InlineData("A0701")] + public void CreateLifecycleEvent_SupportsInboundAndOutboundCodes(string trackCode) + { + var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent( + "APP001", + "IDE2607221859123456", + trackCode, + DateTime.Now); + + Assert.Equal(trackCode, body.TrackCode); + } +}