Fix downstream forecast: pass auth + cache itfx app secret
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
28d14034bd
commit
fffecb5964
@ -1,7 +1,7 @@
|
||||
namespace Oms.Application.Dtos.Channels;
|
||||
|
||||
/// <summary>
|
||||
/// 服务渠道下游对接元数据(发布快照固化,不含 AppSecret)。
|
||||
/// 服务渠道下游对接元数据(发布快照固化)。
|
||||
/// </summary>
|
||||
public class ServiceChannelDownstreamSnapshotDto
|
||||
{
|
||||
@ -25,6 +25,11 @@ public class ServiceChannelDownstreamSnapshotDto
|
||||
/// </summary>
|
||||
public string? AppKey { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// itfx 应用 AppSecret。
|
||||
/// </summary>
|
||||
public string? AppSecret { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// itfx 应用名称。
|
||||
/// </summary>
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -20,6 +20,11 @@ public class OrderBatchJobIntegrationEvent : IntegrationEvent
|
||||
|
||||
public string? PayloadJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 调用第三方轨迹网关所需的鉴权(Bearer 等)。
|
||||
/// </summary>
|
||||
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,
|
||||
};
|
||||
|
||||
@ -17,6 +17,11 @@ public class OrderBatchJobMessage
|
||||
|
||||
public string? PayloadJson { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 调用第三方轨迹网关所需的鉴权(Bearer 等)。
|
||||
/// </summary>
|
||||
public string? Authorization { get; set; }
|
||||
|
||||
public int OperatorUserId { get; set; }
|
||||
|
||||
public string MessageId { get; set; } = string.Empty;
|
||||
|
||||
@ -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<OrderParcelCreateInput>(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<OrderParcelCreateInput>(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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,6 +34,7 @@ public sealed class OrderBatchJobCoordinator(
|
||||
public async Task<OrderBatchJobOutput> 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}",
|
||||
};
|
||||
|
||||
@ -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<OrderBatchJobOutput> BatchTrackUpsertAsync([FromBody] OrderBatchJobCreateInput input)
|
||||
{
|
||||
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>
|
||||
@ -85,7 +97,18 @@ public partial class OrderService
|
||||
public Task<OrderBatchJobOutput> BatchDownstreamForwardAsync([FromBody] OrderBatchJobCreateInput input)
|
||||
{
|
||||
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)
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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<OrderDownstreamCredentials> 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<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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user