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
+ };
+ }
}