fix: 对齐 createOrderOne 下游预报请求体,productCode 取预报渠道 code
All checks were successful
Build and Deploy / deploy (push) Successful in 1m46s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m46s
移除 channelCode 等多余字段,改用专用 ItfxCreateOrderOneBody。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
14f3df3fa5
commit
6b920cc49d
57
src/Oms.Application/Dtos/Orders/ItfxCreateOrderOneBody.cs
Normal file
57
src/Oms.Application/Dtos/Orders/ItfxCreateOrderOneBody.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
namespace Oms.Application.Dtos.Orders;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// itfx createOrderOne(ForwardWithSubscribe)标准请求体。
|
||||||
|
/// 仅包含下游接口字段,不含 OMS 内部扩展参数。
|
||||||
|
/// </summary>
|
||||||
|
public class ItfxCreateOrderOneBody
|
||||||
|
{
|
||||||
|
public string? ClientNo { get; set; }
|
||||||
|
|
||||||
|
public string? TrackNo { get; set; }
|
||||||
|
|
||||||
|
public string? ServiceNo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 下游产品代码:取服务渠道「预报渠道 code」(ForecastCode)。
|
||||||
|
/// </summary>
|
||||||
|
public string? ProductCode { get; set; }
|
||||||
|
|
||||||
|
public string? CargoAttribute { get; set; }
|
||||||
|
|
||||||
|
public int? WarehouseId { get; set; }
|
||||||
|
|
||||||
|
public int? CustomerId { get; set; }
|
||||||
|
|
||||||
|
public decimal? Length { get; set; }
|
||||||
|
|
||||||
|
public decimal? Width { get; set; }
|
||||||
|
|
||||||
|
public decimal? Height { get; set; }
|
||||||
|
|
||||||
|
public decimal? ActualWeight { get; set; }
|
||||||
|
|
||||||
|
public decimal? VolumetricWeight { get; set; }
|
||||||
|
|
||||||
|
public decimal? ChargeableWeight { get; set; }
|
||||||
|
|
||||||
|
public decimal? Cubic { get; set; }
|
||||||
|
|
||||||
|
public bool IsCod { get; set; }
|
||||||
|
|
||||||
|
public string? CodCurrency { get; set; }
|
||||||
|
|
||||||
|
public string? CodSum { get; set; }
|
||||||
|
|
||||||
|
public string? DeclaredCurrency { get; set; }
|
||||||
|
|
||||||
|
public string? DeclaredSum { get; set; }
|
||||||
|
|
||||||
|
public string? Quantity { get; set; }
|
||||||
|
|
||||||
|
public OrderParcelReceiverInput? Receiver { get; set; }
|
||||||
|
|
||||||
|
public OrderParcelSenderInput? Sender { get; set; }
|
||||||
|
|
||||||
|
public List<OrderParcelItemInput> Items { get; set; } = [];
|
||||||
|
}
|
||||||
@ -13,7 +13,7 @@ public class OrderDownstreamCreateOneBodyOutput
|
|||||||
|
|
||||||
public string SupplierCode { get; set; } = string.Empty;
|
public string SupplierCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
public OrderParcelCreateInput Body { get; set; } = new();
|
public ItfxCreateOrderOneBody Body { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ internal sealed class ItfxForwardClient(
|
|||||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||||
{
|
{
|
||||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||||
|
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||||
WriteIndented = true
|
WriteIndented = true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@ using Oms.Application.Integrations.SmallBagTrack;
|
|||||||
using Oms.Application.Messaging;
|
using Oms.Application.Messaging;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
using Oms.Application.Storage;
|
using Oms.Application.Storage;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
using Oms.Domain.OrdersAggregate.Consts;
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
using Oms.Domain.OrdersAggregate.Repositories;
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
@ -17,6 +18,7 @@ namespace Oms.Application.Services.Orders.Batch;
|
|||||||
public sealed class OrderBatchActionExecutor(
|
public sealed class OrderBatchActionExecutor(
|
||||||
IOrderRepository orderRepository,
|
IOrderRepository orderRepository,
|
||||||
ICountryRepository countryRepository,
|
ICountryRepository countryRepository,
|
||||||
|
IServiceChannelRepository serviceChannelRepository,
|
||||||
IFileStorage fileStorage,
|
IFileStorage fileStorage,
|
||||||
ISmallBagTrackOrderClient smallBagTrackOrderClient,
|
ISmallBagTrackOrderClient smallBagTrackOrderClient,
|
||||||
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
|
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
|
||||||
@ -145,12 +147,12 @@ public sealed class OrderBatchActionExecutor(
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
OrderParcelCreateInput body;
|
ItfxCreateOrderOneBody body;
|
||||||
Order order;
|
Order order;
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(message.PayloadJson))
|
if (!string.IsNullOrWhiteSpace(message.PayloadJson))
|
||||||
{
|
{
|
||||||
body = JsonConvert.DeserializeObject<OrderParcelCreateInput>(message.PayloadJson!)
|
body = JsonConvert.DeserializeObject<ItfxCreateOrderOneBody>(message.PayloadJson!)
|
||||||
?? throw new BusinessException("下游预报消息体无效");
|
?? throw new BusinessException("下游预报消息体无效");
|
||||||
|
|
||||||
// 凭证解析仍需订单上的渠道/下游绑定字段;有 payload 时只查主表一次
|
// 凭证解析仍需订单上的渠道/下游绑定字段;有 payload 时只查主表一次
|
||||||
@ -178,6 +180,7 @@ public sealed class OrderBatchActionExecutor(
|
|||||||
bundle.Declarations,
|
bundle.Declarations,
|
||||||
bundle.Weights,
|
bundle.Weights,
|
||||||
countryRepository,
|
countryRepository,
|
||||||
|
serviceChannelRepository,
|
||||||
fileStorage,
|
fileStorage,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ using Oms.Application.Integrations.SmallBagTrack;
|
|||||||
using Oms.Application.Messaging;
|
using Oms.Application.Messaging;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
using Oms.Application.Storage;
|
using Oms.Application.Storage;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
using Oms.Domain.OrdersAggregate.Consts;
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
@ -24,6 +25,7 @@ public sealed class OrderBatchJobCoordinator(
|
|||||||
IOrderBatchJobRepository batchJobRepository,
|
IOrderBatchJobRepository batchJobRepository,
|
||||||
IOrderRepository orderRepository,
|
IOrderRepository orderRepository,
|
||||||
ICountryRepository countryRepository,
|
ICountryRepository countryRepository,
|
||||||
|
IServiceChannelRepository serviceChannelRepository,
|
||||||
IFileStorage fileStorage,
|
IFileStorage fileStorage,
|
||||||
IOrderBatchJobPublisher publisher,
|
IOrderBatchJobPublisher publisher,
|
||||||
OrderBatchJobProcessor batchJobProcessor,
|
OrderBatchJobProcessor batchJobProcessor,
|
||||||
@ -651,6 +653,7 @@ public sealed class OrderBatchJobCoordinator(
|
|||||||
bundle.Declarations,
|
bundle.Declarations,
|
||||||
bundle.Weights,
|
bundle.Weights,
|
||||||
countryRepository,
|
countryRepository,
|
||||||
|
serviceChannelRepository,
|
||||||
fileStorage,
|
fileStorage,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
result[orderId] = JsonConvert.SerializeObject(body);
|
result[orderId] = JsonConvert.SerializeObject(body);
|
||||||
|
|||||||
@ -82,7 +82,7 @@ public partial class OrderService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<(Domain.OrdersAggregate.Entities.Order order, OrderParcelCreateInput body)> LoadDownstreamCreateOneContextAsync(
|
private async Task<(Domain.OrdersAggregate.Entities.Order order, ItfxCreateOrderOneBody body)> LoadDownstreamCreateOneContextAsync(
|
||||||
int orderId)
|
int orderId)
|
||||||
{
|
{
|
||||||
var bundles = await OrderRepository.GetDownstreamCreateOneBundlesAsync([orderId]);
|
var bundles = await OrderRepository.GetDownstreamCreateOneBundlesAsync([orderId]);
|
||||||
@ -98,6 +98,7 @@ public partial class OrderService
|
|||||||
bundle.Declarations,
|
bundle.Declarations,
|
||||||
bundle.Weights,
|
bundle.Weights,
|
||||||
CountryRepository,
|
CountryRepository,
|
||||||
|
ServiceChannelRepository,
|
||||||
FileStorage);
|
FileStorage);
|
||||||
|
|
||||||
return (bundle.Order, body);
|
return (bundle.Order, body);
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Application.Storage;
|
using Oms.Application.Storage;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
using Oms.Domain.OrdersAggregate.Consts;
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
using Oms.Domain.SystemAggregate.Repositories;
|
using Oms.Domain.SystemAggregate.Repositories;
|
||||||
@ -7,17 +8,18 @@ using Oms.Domain.SystemAggregate.Repositories;
|
|||||||
namespace Oms.Application.Shared;
|
namespace Oms.Application.Shared;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 从订单表及关联子表组装 createOrderOne 标准请求体。
|
/// 从订单表及关联子表组装 itfx createOrderOne 标准请求体。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class OrderDownstreamCreateOneBodyBuilder
|
public static class OrderDownstreamCreateOneBodyBuilder
|
||||||
{
|
{
|
||||||
public static async Task<OrderParcelCreateInput> BuildAsync(
|
public static async Task<ItfxCreateOrderOneBody> BuildAsync(
|
||||||
Order order,
|
Order order,
|
||||||
OrderSender? sender,
|
OrderSender? sender,
|
||||||
OrderRecipient? recipient,
|
OrderRecipient? recipient,
|
||||||
IReadOnlyList<OrderDeclaration> declarations,
|
IReadOnlyList<OrderDeclaration> declarations,
|
||||||
IReadOnlyList<OrderWeight> weights,
|
IReadOnlyList<OrderWeight> weights,
|
||||||
ICountryRepository countryRepository,
|
ICountryRepository countryRepository,
|
||||||
|
IServiceChannelRepository serviceChannelRepository,
|
||||||
IFileStorage? fileStorage = null,
|
IFileStorage? fileStorage = null,
|
||||||
CancellationToken cancellationToken = default)
|
CancellationToken cancellationToken = default)
|
||||||
{
|
{
|
||||||
@ -41,16 +43,18 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
|||||||
string.Equals(x.Type, OrderWeightTypeCodes.Forecast, StringComparison.OrdinalIgnoreCase))
|
string.Equals(x.Type, OrderWeightTypeCodes.Forecast, StringComparison.OrdinalIgnoreCase))
|
||||||
?? weights.FirstOrDefault();
|
?? weights.FirstOrDefault();
|
||||||
|
|
||||||
return new OrderParcelCreateInput
|
var productCode = await ResolveForecastProductCodeAsync(
|
||||||
|
order,
|
||||||
|
serviceChannelRepository,
|
||||||
|
cancellationToken);
|
||||||
|
|
||||||
|
return new ItfxCreateOrderOneBody
|
||||||
{
|
{
|
||||||
RequestId = order.RequestId,
|
|
||||||
ClientNo = order.ClientNo,
|
ClientNo = order.ClientNo,
|
||||||
TrackNo = order.TrackNo,
|
TrackNo = order.TrackNo,
|
||||||
ServiceNo = order.ServiceNo,
|
ServiceNo = order.ServiceNo,
|
||||||
ProductCode = order.ProductCode,
|
ProductCode = productCode,
|
||||||
ChannelCode = order.ChannelCode,
|
|
||||||
CargoAttribute = order.CargoAttribute,
|
CargoAttribute = order.CargoAttribute,
|
||||||
ExportCustomsMode = order.ExportCustomsMode,
|
|
||||||
WarehouseId = order.WarehouseId,
|
WarehouseId = order.WarehouseId,
|
||||||
CustomerId = order.CustomerId,
|
CustomerId = order.CustomerId,
|
||||||
Length = forecast?.Length,
|
Length = forecast?.Length,
|
||||||
@ -62,16 +66,45 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
|||||||
Cubic = forecast?.Cubic,
|
Cubic = forecast?.Cubic,
|
||||||
IsCod = order.IsCod > 0,
|
IsCod = order.IsCod > 0,
|
||||||
CodCurrency = order.CodCurrency,
|
CodCurrency = order.CodCurrency,
|
||||||
CodSum = order.CodSum,
|
CodSum = FormatDecimal(order.CodSum),
|
||||||
DeclaredCurrency = order.DeclaredCurrency,
|
DeclaredCurrency = order.DeclaredCurrency,
|
||||||
DeclaredSum = order.DeclaredSum,
|
DeclaredSum = FormatDecimal(order.DeclaredSum),
|
||||||
Quantity = order.Quantity,
|
Quantity = order.Quantity?.ToString(),
|
||||||
Receiver = MapReceiver(recipient, order.ConsigneeCountry, idToCode),
|
Receiver = MapReceiver(recipient, order.ConsigneeCountry, idToCode),
|
||||||
Sender = MapSender(sender, idToCode),
|
Sender = MapSender(sender, idToCode),
|
||||||
Items = declarations.Select(x => MapItem(x, fileStorage)).ToList()
|
Items = declarations.Select(x => MapItem(x, fileStorage)).ToList()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// createOrderOne.productCode = 渠道「预报渠道 code」:
|
||||||
|
/// 优先订单固化的 ChannelForecastCode,否则 live 查 t_service_channel.ForecastCode。
|
||||||
|
/// </summary>
|
||||||
|
private static async Task<string?> ResolveForecastProductCodeAsync(
|
||||||
|
Order order,
|
||||||
|
IServiceChannelRepository serviceChannelRepository,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var snapshotted = order.ChannelForecastCode?.Trim();
|
||||||
|
if (!string.IsNullOrWhiteSpace(snapshotted))
|
||||||
|
{
|
||||||
|
return snapshotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
var channelCode = order.ChannelCode?.Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(channelCode))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode, cancellationToken);
|
||||||
|
var forecastCode = channel?.ForecastCode?.Trim();
|
||||||
|
return string.IsNullOrWhiteSpace(forecastCode) ? null : forecastCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? FormatDecimal(decimal? value) =>
|
||||||
|
value.HasValue ? value.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : null;
|
||||||
|
|
||||||
private static string? ResolveCountryIso(int? countryId, string? orderConsigneeCountry, IReadOnlyDictionary<int, string> idToCode)
|
private static string? ResolveCountryIso(int? countryId, string? orderConsigneeCountry, IReadOnlyDictionary<int, string> idToCode)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(orderConsigneeCountry))
|
if (!string.IsNullOrWhiteSpace(orderConsigneeCountry))
|
||||||
@ -83,9 +116,9 @@ public static class OrderDownstreamCreateOneBodyBuilder
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (countryId is > 0 && idToCode.TryGetValue(countryId.Value, out var code) && !string.IsNullOrWhiteSpace(code))
|
if (countryId is > 0 && idToCode.TryGetValue(countryId.Value, out var iso) && !string.IsNullOrWhiteSpace(iso))
|
||||||
{
|
{
|
||||||
return code.Trim().ToUpperInvariant();
|
return iso.Trim().ToUpperInvariant();
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ using Oms.Application.Dtos.Orders;
|
|||||||
using Oms.Application.Messaging;
|
using Oms.Application.Messaging;
|
||||||
using Oms.Application.Services.Orders.Batch;
|
using Oms.Application.Services.Orders.Batch;
|
||||||
using Oms.Application.Storage;
|
using Oms.Application.Storage;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
using Oms.Domain.OrdersAggregate.Consts;
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
using Oms.Domain.OrdersAggregate.Repositories;
|
using Oms.Domain.OrdersAggregate.Repositories;
|
||||||
@ -19,6 +20,7 @@ public class OrderBatchJobCoordinatorGovernanceTests
|
|||||||
private readonly Mock<IOrderBatchJobRepository> _batchJobRepository = new(MockBehavior.Strict);
|
private readonly Mock<IOrderBatchJobRepository> _batchJobRepository = new(MockBehavior.Strict);
|
||||||
private readonly Mock<IOrderRepository> _orderRepository = new(MockBehavior.Loose);
|
private readonly Mock<IOrderRepository> _orderRepository = new(MockBehavior.Loose);
|
||||||
private readonly Mock<ICountryRepository> _countryRepository = new(MockBehavior.Loose);
|
private readonly Mock<ICountryRepository> _countryRepository = new(MockBehavior.Loose);
|
||||||
|
private readonly Mock<IServiceChannelRepository> _serviceChannelRepository = new(MockBehavior.Loose);
|
||||||
private readonly Mock<IFileStorage> _fileStorage = new(MockBehavior.Loose);
|
private readonly Mock<IFileStorage> _fileStorage = new(MockBehavior.Loose);
|
||||||
private readonly Mock<IOrderBatchJobPublisher> _publisher = new(MockBehavior.Strict);
|
private readonly Mock<IOrderBatchJobPublisher> _publisher = new(MockBehavior.Strict);
|
||||||
private readonly OrderBatchJobCoordinator _coordinator;
|
private readonly OrderBatchJobCoordinator _coordinator;
|
||||||
@ -38,6 +40,7 @@ public class OrderBatchJobCoordinatorGovernanceTests
|
|||||||
_batchJobRepository.Object,
|
_batchJobRepository.Object,
|
||||||
_orderRepository.Object,
|
_orderRepository.Object,
|
||||||
_countryRepository.Object,
|
_countryRepository.Object,
|
||||||
|
_serviceChannelRepository.Object,
|
||||||
_fileStorage.Object,
|
_fileStorage.Object,
|
||||||
_publisher.Object,
|
_publisher.Object,
|
||||||
processor,
|
processor,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user