diff --git a/src/Oms.Application/Dtos/Channels/ServiceChannelDownstreamSnapshotDto.cs b/src/Oms.Application/Dtos/Channels/ServiceChannelDownstreamSnapshotDto.cs index abb2a5a..de5c2a0 100644 --- a/src/Oms.Application/Dtos/Channels/ServiceChannelDownstreamSnapshotDto.cs +++ b/src/Oms.Application/Dtos/Channels/ServiceChannelDownstreamSnapshotDto.cs @@ -1,7 +1,7 @@ namespace Oms.Application.Dtos.Channels; /// -/// 服务渠道下游对接元数据(发布快照固化,不含 AppSecret)。 +/// 服务渠道下游对接元数据(发布快照固化)。 /// public class ServiceChannelDownstreamSnapshotDto { @@ -25,6 +25,11 @@ public class ServiceChannelDownstreamSnapshotDto /// public string? AppKey { get; set; } + /// + /// itfx 应用 AppSecret。 + /// + public string? AppSecret { get; set; } + /// /// itfx 应用名称。 /// diff --git a/src/Oms.Application/Integrations/Itfx/ItfxAuthorizationContext.cs b/src/Oms.Application/Integrations/Itfx/ItfxAuthorizationContext.cs new file mode 100644 index 0000000..6a4fbab --- /dev/null +++ b/src/Oms.Application/Integrations/Itfx/ItfxAuthorizationContext.cs @@ -0,0 +1,18 @@ +using System.Threading; + +namespace Oms.Application.Integrations.Itfx; + +/// +/// 让后台任务也能把当前用户 Bearer 透传给 itfx 元数据/转发请求。 +/// +internal static class ItfxAuthorizationContext +{ + private static readonly AsyncLocal AuthorizationValue = new(); + + public static string? Authorization + { + get => AuthorizationValue.Value; + set => AuthorizationValue.Value = value; + } +} + diff --git a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs index 196f874..2114ffa 100644 --- a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs +++ b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs @@ -63,7 +63,9 @@ internal sealed class ItfxForwardClient( private void ForwardAuthorization(HttpRequestMessage request) { - var authorization = httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString(); + var authorization = + ItfxAuthorizationContext.Authorization + ?? httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString(); if (!string.IsNullOrWhiteSpace(authorization)) { request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization); diff --git a/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs b/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs index 5e545c5..9d4969b 100644 --- a/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs +++ b/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs @@ -102,7 +102,9 @@ internal sealed class ItfxMetadataClient( private void ForwardAuthorization(HttpClient client) { - var authorization = httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString(); + var authorization = + ItfxAuthorizationContext.Authorization + ?? httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString(); if (!string.IsNullOrWhiteSpace(authorization)) { client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(authorization); diff --git a/src/Oms.Application/Messaging/OrderBatchJobIntegrationEvent.cs b/src/Oms.Application/Messaging/OrderBatchJobIntegrationEvent.cs index 9798f66..da44168 100644 --- a/src/Oms.Application/Messaging/OrderBatchJobIntegrationEvent.cs +++ b/src/Oms.Application/Messaging/OrderBatchJobIntegrationEvent.cs @@ -20,6 +20,11 @@ public class OrderBatchJobIntegrationEvent : IntegrationEvent public string? PayloadJson { get; set; } + /// + /// 调用第三方轨迹网关所需的鉴权(Bearer 等)。 + /// + public string? Authorization { get; set; } + public int OperatorUserId { get; set; } public string MessageId { get; set; } = string.Empty; @@ -33,6 +38,7 @@ public class OrderBatchJobIntegrationEvent : IntegrationEvent OrderId = message.OrderId, CustomerId = message.CustomerId, PayloadJson = message.PayloadJson, + Authorization = message.Authorization, OperatorUserId = message.OperatorUserId, MessageId = message.MessageId, }; @@ -46,6 +52,7 @@ public class OrderBatchJobIntegrationEvent : IntegrationEvent OrderId = OrderId, CustomerId = CustomerId, PayloadJson = PayloadJson, + Authorization = Authorization, OperatorUserId = OperatorUserId, MessageId = MessageId, }; diff --git a/src/Oms.Application/Messaging/OrderBatchJobMessage.cs b/src/Oms.Application/Messaging/OrderBatchJobMessage.cs index 6b2522b..7f25598 100644 --- a/src/Oms.Application/Messaging/OrderBatchJobMessage.cs +++ b/src/Oms.Application/Messaging/OrderBatchJobMessage.cs @@ -17,6 +17,11 @@ public class OrderBatchJobMessage public string? PayloadJson { get; set; } + /// + /// 调用第三方轨迹网关所需的鉴权(Bearer 等)。 + /// + public string? Authorization { get; set; } + public int OperatorUserId { get; set; } public string MessageId { get; set; } = string.Empty; diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs index acb9cfb..ac97499 100644 --- a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs +++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs @@ -83,6 +83,12 @@ public sealed class OrderBatchActionExecutor( request = await smallBagTrackUpsertRequestBuilder.BuildAsync(order, cancellationToken: cancellationToken); } + if (string.IsNullOrWhiteSpace(request.Authorization) + && !string.IsNullOrWhiteSpace(message.Authorization)) + { + request.Authorization = message.Authorization.Trim(); + } + var trackResult = await smallBagTrackOrderClient.UpsertDebugAsync(request, cancellationToken); var serviceNo = request.OrderNo?.Trim(); @@ -124,61 +130,74 @@ public sealed class OrderBatchActionExecutor( OrderBatchJobMessage message, CancellationToken cancellationToken) { - OrderParcelCreateInput body; - Order order; - - if (!string.IsNullOrWhiteSpace(message.PayloadJson)) + var authorization = message.Authorization; + if (!string.IsNullOrWhiteSpace(authorization)) { - body = JsonConvert.DeserializeObject(message.PayloadJson!) - ?? throw new BusinessException("下游预报消息体无效"); - - // 凭证解析仍需订单上的渠道/下游绑定字段;有 payload 时只查主表一次 - var existing = await orderRepository.FirstOrDefaultAsync(x => x.Id == message.OrderId, cancellationToken); - if (existing == null) - { - return OrderBatchActionExecuteResult.BusinessFail("订单不存在"); - } - - order = existing; + ItfxAuthorizationContext.Authorization = authorization.Trim(); } - else + + try { - var bundles = await orderRepository.GetDownstreamCreateOneBundlesAsync([message.OrderId], cancellationToken); - if (!bundles.TryGetValue(message.OrderId, out var bundle)) + OrderParcelCreateInput body; + Order order; + + if (!string.IsNullOrWhiteSpace(message.PayloadJson)) { - return OrderBatchActionExecuteResult.BusinessFail("订单不存在"); + body = JsonConvert.DeserializeObject(message.PayloadJson!) + ?? throw new BusinessException("下游预报消息体无效"); + + // 凭证解析仍需订单上的渠道/下游绑定字段;有 payload 时只查主表一次 + var existing = await orderRepository.FirstOrDefaultAsync(x => x.Id == message.OrderId, cancellationToken); + if (existing == null) + { + return OrderBatchActionExecuteResult.BusinessFail("订单不存在"); + } + + order = existing; + } + else + { + var bundles = await orderRepository.GetDownstreamCreateOneBundlesAsync([message.OrderId], cancellationToken); + if (!bundles.TryGetValue(message.OrderId, out var bundle)) + { + return OrderBatchActionExecuteResult.BusinessFail("订单不存在"); + } + + order = bundle.Order; + body = await OrderDownstreamCreateOneBodyBuilder.BuildAsync( + bundle.Order, + bundle.Sender, + bundle.Recipient, + bundle.Declarations, + bundle.Weights, + countryRepository, + cancellationToken); } - order = bundle.Order; - body = await OrderDownstreamCreateOneBodyBuilder.BuildAsync( - bundle.Order, - bundle.Sender, - bundle.Recipient, - bundle.Declarations, - bundle.Weights, - countryRepository, + if (string.IsNullOrWhiteSpace(order.ServiceNo)) + { + return OrderBatchActionExecuteResult.BusinessFail("订单运单号为空,无法预报下游"); + } + + var creds = await downstreamCredentialResolver.ResolveAsync(order, cancellationToken); + var forward = await itfxForwardClient.ForwardCreateOrderOneAsync( + creds.SupplierCode, + creds.AppKey, + creds.AppSecret, + body, cancellationToken); - } - if (string.IsNullOrWhiteSpace(order.ServiceNo)) + if (!forward.Success) + { + return OrderBatchActionExecuteResult.BusinessFail( + $"下游预报失败: HTTP {forward.StatusCode} {forward.ResponseBody}"); + } + + return OrderBatchActionExecuteResult.Ok(); + } + finally { - return OrderBatchActionExecuteResult.BusinessFail("订单运单号为空,无法预报下游"); + ItfxAuthorizationContext.Authorization = null; } - - var creds = await downstreamCredentialResolver.ResolveAsync(order, cancellationToken); - var forward = await itfxForwardClient.ForwardCreateOrderOneAsync( - creds.SupplierCode, - creds.AppKey, - creds.AppSecret, - body, - cancellationToken); - - if (!forward.Success) - { - return OrderBatchActionExecuteResult.BusinessFail( - $"下游预报失败: HTTP {forward.StatusCode} {forward.ResponseBody}"); - } - - return OrderBatchActionExecuteResult.Ok(); } } diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs index 903e1d7..a313f04 100644 --- a/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs +++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs @@ -34,6 +34,7 @@ public sealed class OrderBatchJobCoordinator( public async Task CreateAndEnqueueAsync( OrderBatchJobCreateInput input, int operatorUserId, + string? authorization = null, CancellationToken cancellationToken = default) { if (!publisher.IsEnabled) @@ -142,6 +143,7 @@ public sealed class OrderBatchJobCoordinator( OrderId = item.OrderId, CustomerId = order?.CustomerId, PayloadJson = item.PayloadJson, + Authorization = authorization, OperatorUserId = operatorUserId, MessageId = item.MessageId ?? $"{job.Id}:{item.OrderId}:{action}", }; diff --git a/src/Oms.Application/Services/Orders/OrderService.BatchJob.cs b/src/Oms.Application/Services/Orders/OrderService.BatchJob.cs index f46c14c..a82e40e 100644 --- a/src/Oms.Application/Services/Orders/OrderService.BatchJob.cs +++ b/src/Oms.Application/Services/Orders/OrderService.BatchJob.cs @@ -1,5 +1,6 @@ using Fake.Application; using Fake.Security.Claims; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Shared; @@ -75,7 +76,18 @@ public partial class OrderService public Task BatchTrackUpsertAsync([FromBody] OrderBatchJobCreateInput input) { input.Action = OrderBatchActionCodes.TrackUpsertSmallBag; - return BatchJobCoordinator.CreateAndEnqueueAsync(input, CurrentUser.GetId()); + var authorization = LazyServiceProvider + .GetRequiredService() + .HttpContext? + .Request + .Headers + .Authorization + .ToString(); + + return BatchJobCoordinator.CreateAndEnqueueAsync( + input, + CurrentUser.GetId(), + string.IsNullOrWhiteSpace(authorization) ? null : authorization); } /// @@ -85,7 +97,18 @@ public partial class OrderService public Task BatchDownstreamForwardAsync([FromBody] OrderBatchJobCreateInput input) { input.Action = OrderBatchActionCodes.DownstreamForwardCreateOne; - return BatchJobCoordinator.CreateAndEnqueueAsync(input, CurrentUser.GetId()); + var authorization = LazyServiceProvider + .GetRequiredService() + .HttpContext? + .Request + .Headers + .Authorization + .ToString(); + + return BatchJobCoordinator.CreateAndEnqueueAsync( + input, + CurrentUser.GetId(), + string.IsNullOrWhiteSpace(authorization) ? null : authorization); } private Task EnqueueOrderBatchAsync(string action, OrderBatchJobCreateInput input) diff --git a/src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs b/src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs index 2c26636..afb1ef4 100644 --- a/src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs +++ b/src/Oms.Application/Shared/AllocationProviderSnapshotEnricher.cs @@ -161,6 +161,11 @@ public class AllocationProviderSnapshotEnricher( snapshot.AppKey = app.AppKey; } + if (!string.IsNullOrWhiteSpace(app?.AppSecret)) + { + snapshot.AppSecret = app.AppSecret; + } + snapshot.AppName = NullIfEmpty(app?.Name); } diff --git a/src/Oms.Application/Shared/OrderDownstreamCredentialResolver.cs b/src/Oms.Application/Shared/OrderDownstreamCredentialResolver.cs index f665651..9b21417 100644 --- a/src/Oms.Application/Shared/OrderDownstreamCredentialResolver.cs +++ b/src/Oms.Application/Shared/OrderDownstreamCredentialResolver.cs @@ -1,5 +1,6 @@ using Fake.DependencyInjection; using Fake.Application; +using Oms.Application.Dtos.Channels; using Oms.Application.Integrations.Itfx; using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.OrdersAggregate.Entities; @@ -25,6 +26,12 @@ public class OrderDownstreamCredentialResolver( { public async Task ResolveAsync(Order order, CancellationToken cancellationToken = default) { + var fromSnapshot = await TryResolveFromChannelRulesSnapshotAsync(order, cancellationToken); + if (fromSnapshot != null) + { + return fromSnapshot; + } + if (!string.IsNullOrWhiteSpace(order.DownstreamSupplierCode) && !string.IsNullOrWhiteSpace(order.DownstreamAppKey) && order.DownstreamAppId is > 0) @@ -95,4 +102,78 @@ public class OrderDownstreamCredentialResolver( AppSecret = appInfo.AppSecret.Trim() }; } + + private async Task TryResolveFromChannelRulesSnapshotAsync( + Order order, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(order.ChannelCode)) + { + return null; + } + + var channel = await serviceChannelRepository.GetByChannelCodeAsync(order.ChannelCode.Trim(), cancellationToken); + if (channel == null) + { + return null; + } + + // 规则快照里固化了 itfx 应用信息(含 AppSecret)。 + const string rulesOperatorType = "rules"; + var bindTime = order.CreateTime == default ? DateTime.Now : order.CreateTime; + var record = await serviceChannelRepository.GetEffectivePublishRecordAsync( + channel.Id, + rulesOperatorType, + bindTime, + cancellationToken); + + if (record == null || string.IsNullOrWhiteSpace(record.Content)) + { + return null; + } + + var snapshot = OmsDocHelper.DeserializeJson(record.Content); + var downstream = snapshot?.Downstream; + if (downstream == null) + { + return null; + } + + // 订单绑定的 AppId 与快照不一致时,直接回退旧逻辑。 + if (order.DownstreamAppId is > 0 + && downstream.AppId is > 0 + && order.DownstreamAppId.Value != downstream.AppId.Value) + { + return null; + } + + var appSecret = downstream.AppSecret?.Trim(); + if (string.IsNullOrWhiteSpace(appSecret)) + { + return null; + } + + var appKey = !string.IsNullOrWhiteSpace(order.DownstreamAppKey) + ? order.DownstreamAppKey.Trim() + : downstream.AppKey?.Trim(); + if (string.IsNullOrWhiteSpace(appKey)) + { + return null; + } + + var supplierCode = !string.IsNullOrWhiteSpace(order.DownstreamSupplierCode) + ? order.DownstreamSupplierCode.Trim() + : downstream.SupplierCode?.Trim(); + if (string.IsNullOrWhiteSpace(supplierCode)) + { + return null; + } + + return new OrderDownstreamCredentials + { + SupplierCode = supplierCode, + AppKey = appKey, + AppSecret = appSecret + }; + } }