Fix downstream forecast: pass auth + cache itfx app secret

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-27 18:08:09 +08:00
parent 28d14034bd
commit fffecb5964
11 changed files with 220 additions and 51 deletions

View File

@ -1,7 +1,7 @@
namespace Oms.Application.Dtos.Channels; namespace Oms.Application.Dtos.Channels;
/// <summary> /// <summary>
/// 服务渠道下游对接元数据(发布快照固化,不含 AppSecret)。 /// 服务渠道下游对接元数据(发布快照固化)。
/// </summary> /// </summary>
public class ServiceChannelDownstreamSnapshotDto public class ServiceChannelDownstreamSnapshotDto
{ {
@ -25,6 +25,11 @@ public class ServiceChannelDownstreamSnapshotDto
/// </summary> /// </summary>
public string? AppKey { get; set; } public string? AppKey { get; set; }
/// <summary>
/// itfx 应用 AppSecret。
/// </summary>
public string? AppSecret { get; set; }
/// <summary> /// <summary>
/// itfx 应用名称。 /// itfx 应用名称。
/// </summary> /// </summary>

View File

@ -0,0 +1,18 @@
using System.Threading;
namespace Oms.Application.Integrations.Itfx;
/// <summary>
/// 让后台任务也能把当前用户 Bearer 透传给 itfx 元数据/转发请求。
/// </summary>
internal static class ItfxAuthorizationContext
{
private static readonly AsyncLocal<string?> AuthorizationValue = new();
public static string? Authorization
{
get => AuthorizationValue.Value;
set => AuthorizationValue.Value = value;
}
}

View File

@ -63,7 +63,9 @@ internal sealed class ItfxForwardClient(
private void ForwardAuthorization(HttpRequestMessage request) 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)) if (!string.IsNullOrWhiteSpace(authorization))
{ {
request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization); request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);

View File

@ -102,7 +102,9 @@ internal sealed class ItfxMetadataClient(
private void ForwardAuthorization(HttpClient client) 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)) if (!string.IsNullOrWhiteSpace(authorization))
{ {
client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(authorization); client.DefaultRequestHeaders.Authorization = AuthenticationHeaderValue.Parse(authorization);

View File

@ -20,6 +20,11 @@ public class OrderBatchJobIntegrationEvent : IntegrationEvent
public string? PayloadJson { get; set; } public string? PayloadJson { get; set; }
/// <summary>
/// 调用第三方轨迹网关所需的鉴权(Bearer 等)。
/// </summary>
public string? Authorization { get; set; }
public int OperatorUserId { get; set; } public int OperatorUserId { get; set; }
public string MessageId { get; set; } = string.Empty; public string MessageId { get; set; } = string.Empty;
@ -33,6 +38,7 @@ public class OrderBatchJobIntegrationEvent : IntegrationEvent
OrderId = message.OrderId, OrderId = message.OrderId,
CustomerId = message.CustomerId, CustomerId = message.CustomerId,
PayloadJson = message.PayloadJson, PayloadJson = message.PayloadJson,
Authorization = message.Authorization,
OperatorUserId = message.OperatorUserId, OperatorUserId = message.OperatorUserId,
MessageId = message.MessageId, MessageId = message.MessageId,
}; };
@ -46,6 +52,7 @@ public class OrderBatchJobIntegrationEvent : IntegrationEvent
OrderId = OrderId, OrderId = OrderId,
CustomerId = CustomerId, CustomerId = CustomerId,
PayloadJson = PayloadJson, PayloadJson = PayloadJson,
Authorization = Authorization,
OperatorUserId = OperatorUserId, OperatorUserId = OperatorUserId,
MessageId = MessageId, MessageId = MessageId,
}; };

View File

@ -17,6 +17,11 @@ public class OrderBatchJobMessage
public string? PayloadJson { get; set; } public string? PayloadJson { get; set; }
/// <summary>
/// 调用第三方轨迹网关所需的鉴权(Bearer 等)。
/// </summary>
public string? Authorization { get; set; }
public int OperatorUserId { get; set; } public int OperatorUserId { get; set; }
public string MessageId { get; set; } = string.Empty; public string MessageId { get; set; } = string.Empty;

View File

@ -83,6 +83,12 @@ public sealed class OrderBatchActionExecutor(
request = await smallBagTrackUpsertRequestBuilder.BuildAsync(order, cancellationToken: cancellationToken); 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 trackResult = await smallBagTrackOrderClient.UpsertDebugAsync(request, cancellationToken);
var serviceNo = request.OrderNo?.Trim(); var serviceNo = request.OrderNo?.Trim();
@ -123,6 +129,14 @@ public sealed class OrderBatchActionExecutor(
private async Task<OrderBatchActionExecuteResult> ExecuteDownstreamForwardAsync( private async Task<OrderBatchActionExecuteResult> ExecuteDownstreamForwardAsync(
OrderBatchJobMessage message, OrderBatchJobMessage message,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{
var authorization = message.Authorization;
if (!string.IsNullOrWhiteSpace(authorization))
{
ItfxAuthorizationContext.Authorization = authorization.Trim();
}
try
{ {
OrderParcelCreateInput body; OrderParcelCreateInput body;
Order order; Order order;
@ -181,4 +195,9 @@ public sealed class OrderBatchActionExecutor(
return OrderBatchActionExecuteResult.Ok(); return OrderBatchActionExecuteResult.Ok();
} }
finally
{
ItfxAuthorizationContext.Authorization = null;
}
}
} }

View File

@ -34,6 +34,7 @@ public sealed class OrderBatchJobCoordinator(
public async Task<OrderBatchJobOutput> CreateAndEnqueueAsync( public async Task<OrderBatchJobOutput> CreateAndEnqueueAsync(
OrderBatchJobCreateInput input, OrderBatchJobCreateInput input,
int operatorUserId, int operatorUserId,
string? authorization = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
if (!publisher.IsEnabled) if (!publisher.IsEnabled)
@ -142,6 +143,7 @@ public sealed class OrderBatchJobCoordinator(
OrderId = item.OrderId, OrderId = item.OrderId,
CustomerId = order?.CustomerId, CustomerId = order?.CustomerId,
PayloadJson = item.PayloadJson, PayloadJson = item.PayloadJson,
Authorization = authorization,
OperatorUserId = operatorUserId, OperatorUserId = operatorUserId,
MessageId = item.MessageId ?? $"{job.Id}:{item.OrderId}:{action}", MessageId = item.MessageId ?? $"{job.Id}:{item.OrderId}:{action}",
}; };

View File

@ -1,5 +1,6 @@
using Fake.Application; using Fake.Application;
using Fake.Security.Claims; using Fake.Security.Claims;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
@ -75,7 +76,18 @@ public partial class OrderService
public Task<OrderBatchJobOutput> BatchTrackUpsertAsync([FromBody] OrderBatchJobCreateInput input) public Task<OrderBatchJobOutput> BatchTrackUpsertAsync([FromBody] OrderBatchJobCreateInput input)
{ {
input.Action = OrderBatchActionCodes.TrackUpsertSmallBag; input.Action = OrderBatchActionCodes.TrackUpsertSmallBag;
return BatchJobCoordinator.CreateAndEnqueueAsync(input, CurrentUser.GetId<int>()); var authorization = LazyServiceProvider
.GetRequiredService<IHttpContextAccessor>()
.HttpContext?
.Request
.Headers
.Authorization
.ToString();
return BatchJobCoordinator.CreateAndEnqueueAsync(
input,
CurrentUser.GetId<int>(),
string.IsNullOrWhiteSpace(authorization) ? null : authorization);
} }
/// <summary> /// <summary>
@ -85,7 +97,18 @@ public partial class OrderService
public Task<OrderBatchJobOutput> BatchDownstreamForwardAsync([FromBody] OrderBatchJobCreateInput input) public Task<OrderBatchJobOutput> BatchDownstreamForwardAsync([FromBody] OrderBatchJobCreateInput input)
{ {
input.Action = OrderBatchActionCodes.DownstreamForwardCreateOne; input.Action = OrderBatchActionCodes.DownstreamForwardCreateOne;
return BatchJobCoordinator.CreateAndEnqueueAsync(input, CurrentUser.GetId<int>()); var authorization = LazyServiceProvider
.GetRequiredService<IHttpContextAccessor>()
.HttpContext?
.Request
.Headers
.Authorization
.ToString();
return BatchJobCoordinator.CreateAndEnqueueAsync(
input,
CurrentUser.GetId<int>(),
string.IsNullOrWhiteSpace(authorization) ? null : authorization);
} }
private Task<OrderBatchJobOutput> EnqueueOrderBatchAsync(string action, OrderBatchJobCreateInput input) private Task<OrderBatchJobOutput> EnqueueOrderBatchAsync(string action, OrderBatchJobCreateInput input)

View File

@ -161,6 +161,11 @@ public class AllocationProviderSnapshotEnricher(
snapshot.AppKey = app.AppKey; snapshot.AppKey = app.AppKey;
} }
if (!string.IsNullOrWhiteSpace(app?.AppSecret))
{
snapshot.AppSecret = app.AppSecret;
}
snapshot.AppName = NullIfEmpty(app?.Name); snapshot.AppName = NullIfEmpty(app?.Name);
} }

View File

@ -1,5 +1,6 @@
using Fake.DependencyInjection; using Fake.DependencyInjection;
using Fake.Application; using Fake.Application;
using Oms.Application.Dtos.Channels;
using Oms.Application.Integrations.Itfx; using Oms.Application.Integrations.Itfx;
using Oms.Domain.ChannelsAggregate.Repositories; using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Entities;
@ -25,6 +26,12 @@ public class OrderDownstreamCredentialResolver(
{ {
public async Task<OrderDownstreamCredentials> ResolveAsync(Order order, CancellationToken cancellationToken = default) public async Task<OrderDownstreamCredentials> ResolveAsync(Order order, CancellationToken cancellationToken = default)
{ {
var fromSnapshot = await TryResolveFromChannelRulesSnapshotAsync(order, cancellationToken);
if (fromSnapshot != null)
{
return fromSnapshot;
}
if (!string.IsNullOrWhiteSpace(order.DownstreamSupplierCode) if (!string.IsNullOrWhiteSpace(order.DownstreamSupplierCode)
&& !string.IsNullOrWhiteSpace(order.DownstreamAppKey) && !string.IsNullOrWhiteSpace(order.DownstreamAppKey)
&& order.DownstreamAppId is > 0) && order.DownstreamAppId is > 0)
@ -95,4 +102,78 @@ public class OrderDownstreamCredentialResolver(
AppSecret = appInfo.AppSecret.Trim() AppSecret = appInfo.AppSecret.Trim()
}; };
} }
private async Task<OrderDownstreamCredentials?> 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<ServiceChannelRulesPublishSnapshotDto>(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
};
}
} }