feat(order): downstream createOrderOne API from order DB
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
e642f089ba
commit
eac7b84f6a
@ -0,0 +1,35 @@
|
|||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下游 createOrderOne 请求体预览(由订单表组装,不含 AppSecret)。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderDownstreamCreateOneBodyOutput
|
||||||
|
{
|
||||||
|
public int OrderId { get; set; }
|
||||||
|
|
||||||
|
public string? ServiceNo { get; set; }
|
||||||
|
|
||||||
|
public string SupplierCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public OrderParcelCreateInput Body { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下游 createOrderOne 转发结果。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderDownstreamForwardResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
|
||||||
|
public string SupplierCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string RequestJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? StatusCode { get; set; }
|
||||||
|
|
||||||
|
public string ResponseBody { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
24
src/Oms.Application/Integrations/Itfx/IItfxForwardClient.cs
Normal file
24
src/Oms.Application/Integrations/Itfx/IItfxForwardClient.cs
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
namespace Oms.Application.Integrations.Itfx;
|
||||||
|
|
||||||
|
public sealed class ItfxForwardResult
|
||||||
|
{
|
||||||
|
public bool Success { get; set; }
|
||||||
|
|
||||||
|
public string Url { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string RequestJson { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public int? StatusCode { get; set; }
|
||||||
|
|
||||||
|
public string ResponseBody { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IItfxForwardClient
|
||||||
|
{
|
||||||
|
Task<ItfxForwardResult> ForwardCreateOrderOneAsync(
|
||||||
|
string supplierCode,
|
||||||
|
string appKey,
|
||||||
|
string appSecret,
|
||||||
|
object body,
|
||||||
|
CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ namespace Oms.Application.Integrations.Itfx;
|
|||||||
public sealed class ItfxAppMetadata
|
public sealed class ItfxAppMetadata
|
||||||
{
|
{
|
||||||
public string AppKey { get; set; } = string.Empty;
|
public string AppKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string AppSecret { get; set; } = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ItfxServiceProviderMetadata
|
public sealed class ItfxServiceProviderMetadata
|
||||||
|
|||||||
72
src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
Normal file
72
src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Text.Json;
|
||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
|
||||||
|
namespace Oms.Application.Integrations.Itfx;
|
||||||
|
|
||||||
|
internal sealed class ItfxForwardClient(
|
||||||
|
IHttpClientFactory httpClientFactory,
|
||||||
|
IOptions<ItfxGatewayOptions> options,
|
||||||
|
IHttpContextAccessor httpContextAccessor) : IItfxForwardClient, IScopedDependency
|
||||||
|
{
|
||||||
|
public const string HttpClientName = nameof(ItfxForwardClient);
|
||||||
|
public const string CreateOrderOneStandardCode = "createOrderOne";
|
||||||
|
|
||||||
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
|
{
|
||||||
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
WriteIndented = true
|
||||||
|
};
|
||||||
|
|
||||||
|
public async Task<ItfxForwardResult> ForwardCreateOrderOneAsync(
|
||||||
|
string supplierCode,
|
||||||
|
string appKey,
|
||||||
|
string appSecret,
|
||||||
|
object body,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var settings = options.Value;
|
||||||
|
if (string.IsNullOrWhiteSpace(settings.BaseUrl))
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Itfx BaseUrl not configured");
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = $"{settings.BaseUrl.Trim().TrimEnd('/')}/InterfaceForward/ForwardWithSubscribe";
|
||||||
|
var url =
|
||||||
|
$"{root}?standardCode={CreateOrderOneStandardCode}&supplierCode={Uri.EscapeDataString(supplierCode)}";
|
||||||
|
var requestJson = JsonSerializer.Serialize(body, JsonOptions);
|
||||||
|
|
||||||
|
using var request = new HttpRequestMessage(HttpMethod.Post, url);
|
||||||
|
request.Headers.TryAddWithoutValidation("appkey", appKey);
|
||||||
|
request.Headers.TryAddWithoutValidation("appSecret", appSecret);
|
||||||
|
request.Headers.TryAddWithoutValidation("followName", "0");
|
||||||
|
ForwardAuthorization(request);
|
||||||
|
request.Content = JsonContent.Create(body, options: JsonOptions);
|
||||||
|
|
||||||
|
var client = httpClientFactory.CreateClient(HttpClientName);
|
||||||
|
client.Timeout = TimeSpan.FromSeconds(Math.Max(30, settings.TimeoutSeconds));
|
||||||
|
|
||||||
|
using var response = await client.SendAsync(request, cancellationToken);
|
||||||
|
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
|
return new ItfxForwardResult
|
||||||
|
{
|
||||||
|
Success = response.IsSuccessStatusCode,
|
||||||
|
Url = url,
|
||||||
|
RequestJson = requestJson,
|
||||||
|
StatusCode = (int)response.StatusCode,
|
||||||
|
ResponseBody = responseBody
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ForwardAuthorization(HttpRequestMessage request)
|
||||||
|
{
|
||||||
|
var authorization = httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(authorization))
|
||||||
|
{
|
||||||
|
request.Headers.Authorization = AuthenticationHeaderValue.Parse(authorization);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -24,9 +24,17 @@ internal sealed class ItfxMetadataClient(
|
|||||||
GetAsync<ItfxAppMetadata>($"/App/GetInfo?id={appId}", payload =>
|
GetAsync<ItfxAppMetadata>($"/App/GetInfo?id={appId}", payload =>
|
||||||
{
|
{
|
||||||
var appKey = ReadString(payload, "appKey", "AppKey");
|
var appKey = ReadString(payload, "appKey", "AppKey");
|
||||||
return string.IsNullOrWhiteSpace(appKey)
|
var appSecret = ReadString(payload, "appSecret", "AppSecret", "secret", "Secret");
|
||||||
? null
|
if (string.IsNullOrWhiteSpace(appKey))
|
||||||
: new ItfxAppMetadata { AppKey = appKey.Trim() };
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ItfxAppMetadata
|
||||||
|
{
|
||||||
|
AppKey = appKey.Trim(),
|
||||||
|
AppSecret = appSecret?.Trim() ?? string.Empty
|
||||||
|
};
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
public Task<ItfxServiceProviderMetadata?> GetServiceProviderAsync(
|
public Task<ItfxServiceProviderMetadata?> GetServiceProviderAsync(
|
||||||
|
|||||||
@ -9,7 +9,9 @@ public static class ItfxServiceExtensions
|
|||||||
{
|
{
|
||||||
services.Configure<ItfxGatewayOptions>(configuration.GetSection(ItfxGatewayOptions.SectionName));
|
services.Configure<ItfxGatewayOptions>(configuration.GetSection(ItfxGatewayOptions.SectionName));
|
||||||
services.AddHttpClient(ItfxMetadataClient.HttpClientName);
|
services.AddHttpClient(ItfxMetadataClient.HttpClientName);
|
||||||
|
services.AddHttpClient(ItfxForwardClient.HttpClientName);
|
||||||
services.AddScoped<IItfxMetadataClient, ItfxMetadataClient>();
|
services.AddScoped<IItfxMetadataClient, ItfxMetadataClient>();
|
||||||
|
services.AddScoped<IItfxForwardClient, ItfxForwardClient>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,85 @@
|
|||||||
|
using Fake.Application;
|
||||||
|
using Fake.Domain.Exceptions;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Application.Integrations.Itfx;
|
||||||
|
using Oms.Application.Shared;
|
||||||
|
|
||||||
|
namespace Oms.Application.Services.Orders;
|
||||||
|
|
||||||
|
public partial class OrderService
|
||||||
|
{
|
||||||
|
private OrderDownstreamCredentialResolver OrderDownstreamCredentialResolver =>
|
||||||
|
LazyServiceProvider.GetRequiredService<OrderDownstreamCredentialResolver>();
|
||||||
|
|
||||||
|
private IItfxForwardClient ItfxForwardClient =>
|
||||||
|
LazyServiceProvider.GetRequiredService<IItfxForwardClient>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按订单 Id 从数据库组装 createOrderOne 请求体(预览,不调用下游)。
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("/order/downstream-create-one-body")]
|
||||||
|
public async Task<OrderDownstreamCreateOneBodyOutput> GetDownstreamCreateOneBodyAsync([FromQuery] int id)
|
||||||
|
{
|
||||||
|
var (order, body) = await LoadDownstreamCreateOneContextAsync(id);
|
||||||
|
var creds = await OrderDownstreamCredentialResolver.ResolveAsync(order);
|
||||||
|
return new OrderDownstreamCreateOneBodyOutput
|
||||||
|
{
|
||||||
|
OrderId = order.Id,
|
||||||
|
ServiceNo = order.ServiceNo,
|
||||||
|
SupplierCode = creds.SupplierCode,
|
||||||
|
Body = body
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 按订单 Id 组装 createOrderOne 并转发至 itfx 下游。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("/order/downstream-forward-create-one")]
|
||||||
|
public async Task<OrderDownstreamForwardResult> DownstreamForwardCreateOneAsync([FromBody] OrderActionInput input)
|
||||||
|
{
|
||||||
|
var (order, body) = await LoadDownstreamCreateOneContextAsync(input.Id);
|
||||||
|
if (string.IsNullOrWhiteSpace(order.ServiceNo))
|
||||||
|
{
|
||||||
|
throw new BusinessException("订单运单号为空,无法预报下游");
|
||||||
|
}
|
||||||
|
|
||||||
|
var creds = await OrderDownstreamCredentialResolver.ResolveAsync(order);
|
||||||
|
var forward = await ItfxForwardClient.ForwardCreateOrderOneAsync(
|
||||||
|
creds.SupplierCode,
|
||||||
|
creds.AppKey,
|
||||||
|
creds.AppSecret,
|
||||||
|
body);
|
||||||
|
|
||||||
|
return new OrderDownstreamForwardResult
|
||||||
|
{
|
||||||
|
Success = forward.Success,
|
||||||
|
SupplierCode = creds.SupplierCode,
|
||||||
|
Url = forward.Url,
|
||||||
|
RequestJson = forward.RequestJson,
|
||||||
|
StatusCode = forward.StatusCode,
|
||||||
|
ResponseBody = forward.ResponseBody
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<(Domain.OrdersAggregate.Entities.Order order, OrderParcelCreateInput body)> LoadDownstreamCreateOneContextAsync(
|
||||||
|
int orderId)
|
||||||
|
{
|
||||||
|
var order = await GetOrderOrThrowAsync(orderId);
|
||||||
|
var orderIds = new[] { order.Id };
|
||||||
|
var senders = await OrderRepository.GetSendersByOrderIdsAsync(orderIds);
|
||||||
|
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync(orderIds);
|
||||||
|
var declarations = await OrderRepository.GetDeclarationsByOrderIdsAsync(orderIds);
|
||||||
|
var weights = await OrderRepository.GetWeightsByOrderIdsAsync(orderIds);
|
||||||
|
|
||||||
|
var body = await OrderDownstreamCreateOneBodyBuilder.BuildAsync(
|
||||||
|
order,
|
||||||
|
senders.FirstOrDefault(),
|
||||||
|
recipients.FirstOrDefault(),
|
||||||
|
declarations,
|
||||||
|
weights,
|
||||||
|
CountryRepository);
|
||||||
|
|
||||||
|
return (order, body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,183 @@
|
|||||||
|
using Oms.Application.Dtos.Orders;
|
||||||
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
using Oms.Domain.SystemAggregate.Repositories;
|
||||||
|
|
||||||
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从订单表及关联子表组装 createOrderOne 标准请求体。
|
||||||
|
/// </summary>
|
||||||
|
public static class OrderDownstreamCreateOneBodyBuilder
|
||||||
|
{
|
||||||
|
public static async Task<OrderParcelCreateInput> BuildAsync(
|
||||||
|
Order order,
|
||||||
|
OrderSender? sender,
|
||||||
|
OrderRecipient? recipient,
|
||||||
|
IReadOnlyList<OrderDeclaration> declarations,
|
||||||
|
IReadOnlyList<OrderWeight> weights,
|
||||||
|
ICountryRepository countryRepository,
|
||||||
|
CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var countryIds = new List<int>();
|
||||||
|
if (sender?.Country is > 0)
|
||||||
|
{
|
||||||
|
countryIds.Add(sender.Country.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recipient?.Country is > 0)
|
||||||
|
{
|
||||||
|
countryIds.Add(recipient.Country.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
var countries = countryIds.Count == 0
|
||||||
|
? []
|
||||||
|
: await countryRepository.GetListAsync(x => countryIds.Contains(x.Id));
|
||||||
|
var idToCode = countries.ToDictionary(x => x.Id, x => x.CountryCode);
|
||||||
|
|
||||||
|
var forecast = weights.FirstOrDefault(x =>
|
||||||
|
string.Equals(x.Type, OrderWeightTypeCodes.Forecast, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?? weights.FirstOrDefault();
|
||||||
|
|
||||||
|
return new OrderParcelCreateInput
|
||||||
|
{
|
||||||
|
RequestId = order.RequestId,
|
||||||
|
ClientNo = order.ClientNo,
|
||||||
|
TrackNo = order.TrackNo,
|
||||||
|
ServiceNo = order.ServiceNo,
|
||||||
|
ProductCode = order.ProductCode,
|
||||||
|
ChannelCode = order.ChannelCode,
|
||||||
|
CargoAttribute = order.CargoAttribute,
|
||||||
|
ExportCustomsMode = order.ExportCustomsMode,
|
||||||
|
WarehouseId = order.WarehouseId,
|
||||||
|
CustomerId = order.CustomerId,
|
||||||
|
Length = forecast?.Length,
|
||||||
|
Width = forecast?.Width,
|
||||||
|
Height = forecast?.Height,
|
||||||
|
ActualWeight = forecast?.ActualWeight,
|
||||||
|
VolumetricWeight = forecast?.VolumetricWeight,
|
||||||
|
ChargeableWeight = forecast?.ChargeableWeight,
|
||||||
|
Cubic = forecast?.Cubic,
|
||||||
|
IsCod = order.IsCod > 0,
|
||||||
|
CodCurrency = order.CodCurrency,
|
||||||
|
CodSum = order.CodSum,
|
||||||
|
DeclaredCurrency = order.DeclaredCurrency,
|
||||||
|
DeclaredSum = order.DeclaredSum,
|
||||||
|
Quantity = order.Quantity,
|
||||||
|
Receiver = MapReceiver(recipient, order.ConsigneeCountry, idToCode),
|
||||||
|
Sender = MapSender(sender, idToCode),
|
||||||
|
Items = declarations.Select(MapItem).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveCountryIso(int? countryId, string? orderConsigneeCountry, IReadOnlyDictionary<int, string> idToCode)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(orderConsigneeCountry))
|
||||||
|
{
|
||||||
|
var trimmed = orderConsigneeCountry.Trim();
|
||||||
|
if (trimmed.Length == 2)
|
||||||
|
{
|
||||||
|
return trimmed.ToUpperInvariant();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (countryId is > 0 && idToCode.TryGetValue(countryId.Value, out var code) && !string.IsNullOrWhiteSpace(code))
|
||||||
|
{
|
||||||
|
return code.Trim().ToUpperInvariant();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrderParcelReceiverInput? MapReceiver(
|
||||||
|
OrderRecipient? recipient,
|
||||||
|
string? orderConsigneeCountry,
|
||||||
|
IReadOnlyDictionary<int, string> idToCode)
|
||||||
|
{
|
||||||
|
if (recipient == null)
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(orderConsigneeCountry)
|
||||||
|
? null
|
||||||
|
: new OrderParcelReceiverInput { CountryCode = orderConsigneeCountry.Trim().ToUpperInvariant() };
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OrderParcelReceiverInput
|
||||||
|
{
|
||||||
|
RecipientsName = recipient.RecipientsName,
|
||||||
|
CompanyName = recipient.CompanyName,
|
||||||
|
Phone = recipient.Phone,
|
||||||
|
TelePhone = recipient.TelePhone,
|
||||||
|
Email = recipient.Email,
|
||||||
|
CountryCode = ResolveCountryIso(recipient.Country, orderConsigneeCountry, idToCode),
|
||||||
|
Province = recipient.Province,
|
||||||
|
City = recipient.City,
|
||||||
|
StreetNumber = recipient.StreetNumber,
|
||||||
|
StreetAddress1 = recipient.StreetAddress1,
|
||||||
|
StreetAddress2 = recipient.StreetAddress2,
|
||||||
|
StreetAddress3 = recipient.StreetAddress3,
|
||||||
|
ZipCode = recipient.ZipCode,
|
||||||
|
TaxType = recipient.TaxType,
|
||||||
|
TaxNo = recipient.TaxNo,
|
||||||
|
CertificateType = recipient.CertificateType,
|
||||||
|
CertificateNo = recipient.CertificateNo,
|
||||||
|
Eori = recipient.Eori,
|
||||||
|
Vat = recipient.Vat
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrderParcelSenderInput? MapSender(OrderSender? sender, IReadOnlyDictionary<int, string> idToCode)
|
||||||
|
{
|
||||||
|
if (sender == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OrderParcelSenderInput
|
||||||
|
{
|
||||||
|
SenderName = sender.SenderName,
|
||||||
|
Company = sender.Company,
|
||||||
|
Phone = sender.Phone,
|
||||||
|
TelePhone = sender.TelePhone,
|
||||||
|
Email = sender.Email,
|
||||||
|
CountryCode = ResolveCountryIso(sender.Country, null, idToCode),
|
||||||
|
Province = sender.Province,
|
||||||
|
City = sender.City,
|
||||||
|
ZipCode = sender.ZipCode,
|
||||||
|
StreetAddress1 = sender.StreetAddress1,
|
||||||
|
StreetAddress2 = sender.StreetAddress2,
|
||||||
|
StreetAddress3 = sender.StreetAddress3,
|
||||||
|
HouseNumber = sender.HouseNumber
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static OrderParcelItemInput MapItem(OrderDeclaration x) =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
CnName = x.CnName,
|
||||||
|
EnName = x.EnName,
|
||||||
|
Sku = x.Sku,
|
||||||
|
Unit = x.Unit,
|
||||||
|
UnitPrice = x.UnitPrice,
|
||||||
|
UnitWeight = x.UnitWeight,
|
||||||
|
Quantity = x.Quantity,
|
||||||
|
Amount = x.Amount,
|
||||||
|
Weight = x.Weight,
|
||||||
|
ImHsCode = x.ImHsCode,
|
||||||
|
ExHsCode = x.ExHsCode ?? x.HsCode,
|
||||||
|
SkuImage = x.SkuImage,
|
||||||
|
Category = x.Category,
|
||||||
|
Brand = x.Brand,
|
||||||
|
BrandType = x.BrandType,
|
||||||
|
IsMagnetic = x.IsMagnetic > 0,
|
||||||
|
Distribution = x.Distribution,
|
||||||
|
Material = x.Material,
|
||||||
|
EnMaterial = x.EnMaterial,
|
||||||
|
Purpose = x.Purpose,
|
||||||
|
Attributes = x.Attributes,
|
||||||
|
OriginCountry = x.OriginCountry,
|
||||||
|
SalesLink = x.SalesLink,
|
||||||
|
Model = x.Model,
|
||||||
|
Specifications = x.Specifications,
|
||||||
|
Comment = x.Comment
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
using Fake.DependencyInjection;
|
||||||
|
using Fake.Application;
|
||||||
|
using Oms.Application.Integrations.Itfx;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
|
public sealed class OrderDownstreamCredentials
|
||||||
|
{
|
||||||
|
public string SupplierCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string AppKey { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public string AppSecret { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 解析订单下游 itfx 凭证(优先订单绑定字段,回退渠道分组)。
|
||||||
|
/// </summary>
|
||||||
|
public class OrderDownstreamCredentialResolver(
|
||||||
|
IServiceChannelRepository serviceChannelRepository,
|
||||||
|
IServiceChannelGroupRepository serviceChannelGroupRepository,
|
||||||
|
IItfxMetadataClient itfxMetadataClient) : IScopedDependency
|
||||||
|
{
|
||||||
|
public async Task<OrderDownstreamCredentials> ResolveAsync(Order order, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(order.DownstreamSupplierCode)
|
||||||
|
&& !string.IsNullOrWhiteSpace(order.DownstreamAppKey)
|
||||||
|
&& order.DownstreamAppId is > 0)
|
||||||
|
{
|
||||||
|
var app = await itfxMetadataClient.GetAppAsync(order.DownstreamAppId.Value, cancellationToken);
|
||||||
|
var appSecret = app?.AppSecret?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(appSecret))
|
||||||
|
{
|
||||||
|
throw new BusinessException("应用 AppSecret 未配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OrderDownstreamCredentials
|
||||||
|
{
|
||||||
|
SupplierCode = order.DownstreamSupplierCode.Trim(),
|
||||||
|
AppKey = order.DownstreamAppKey.Trim(),
|
||||||
|
AppSecret = appSecret
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(order.ChannelCode))
|
||||||
|
{
|
||||||
|
throw new BusinessException("订单未绑定服务渠道,无法预报下游");
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel = await serviceChannelRepository.GetByChannelCodeAsync(order.ChannelCode.Trim(), cancellationToken);
|
||||||
|
if (channel == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException($"未找到渠道「{order.ChannelCode}」");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!channel.GroupId.HasValue || channel.GroupId.Value <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException($"渠道「{order.ChannelCode}」未关联服务商分组");
|
||||||
|
}
|
||||||
|
|
||||||
|
var group = await serviceChannelGroupRepository.FirstOrDefaultAsync(x => x.Id == channel.GroupId.Value, cancellationToken);
|
||||||
|
if (group == null)
|
||||||
|
{
|
||||||
|
throw new BusinessException("服务商分组不存在或已删除");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.AppId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException($"服务商「{group.Name}」未配置应用");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (group.ApiServiceProviderId <= 0)
|
||||||
|
{
|
||||||
|
throw new BusinessException($"服务商「{group.Name}」未配置 API 服务商");
|
||||||
|
}
|
||||||
|
|
||||||
|
var appInfo = await itfxMetadataClient.GetAppAsync(group.AppId, cancellationToken);
|
||||||
|
if (appInfo == null || string.IsNullOrWhiteSpace(appInfo.AppKey) || string.IsNullOrWhiteSpace(appInfo.AppSecret))
|
||||||
|
{
|
||||||
|
throw new BusinessException("应用 AppKey / AppSecret 未配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
var sp = await itfxMetadataClient.GetServiceProviderAsync(group.ApiServiceProviderId, cancellationToken);
|
||||||
|
if (sp == null || string.IsNullOrWhiteSpace(sp.Code))
|
||||||
|
{
|
||||||
|
throw new BusinessException("API 服务商编码(supplierCode)未配置");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new OrderDownstreamCredentials
|
||||||
|
{
|
||||||
|
SupplierCode = sp.Code.Trim(),
|
||||||
|
AppKey = appInfo.AppKey.Trim(),
|
||||||
|
AppSecret = appInfo.AppSecret.Trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user