diff --git a/src/Oms.Application/Dtos/Orders/ItfxCreateOrderOneBody.cs b/src/Oms.Application/Dtos/Orders/ItfxCreateOrderOneBody.cs
new file mode 100644
index 0000000..cd36ea9
--- /dev/null
+++ b/src/Oms.Application/Dtos/Orders/ItfxCreateOrderOneBody.cs
@@ -0,0 +1,57 @@
+namespace Oms.Application.Dtos.Orders;
+
+///
+/// itfx createOrderOne(ForwardWithSubscribe)标准请求体。
+/// 仅包含下游接口字段,不含 OMS 内部扩展参数。
+///
+public class ItfxCreateOrderOneBody
+{
+ public string? ClientNo { get; set; }
+
+ public string? TrackNo { get; set; }
+
+ public string? ServiceNo { get; set; }
+
+ ///
+ /// 下游产品代码:取服务渠道「预报渠道 code」(ForecastCode)。
+ ///
+ 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 Items { get; set; } = [];
+}
diff --git a/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs b/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs
index 472212e..c69e78c 100644
--- a/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs
+++ b/src/Oms.Application/Dtos/Orders/OrderDownstreamForwardDtos.cs
@@ -13,7 +13,7 @@ public class OrderDownstreamCreateOneBodyOutput
public string SupplierCode { get; set; } = string.Empty;
- public OrderParcelCreateInput Body { get; set; } = new();
+ public ItfxCreateOrderOneBody Body { get; set; } = new();
}
///
diff --git a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
index 4ab5564..c61ad95 100644
--- a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
+++ b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs
@@ -17,6 +17,7 @@ internal sealed class ItfxForwardClient(
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
WriteIndented = true
};
diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
index 77113cc..a88f537 100644
--- a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
+++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
@@ -7,6 +7,7 @@ using Oms.Application.Integrations.SmallBagTrack;
using Oms.Application.Messaging;
using Oms.Application.Shared;
using Oms.Application.Storage;
+using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
@@ -17,6 +18,7 @@ namespace Oms.Application.Services.Orders.Batch;
public sealed class OrderBatchActionExecutor(
IOrderRepository orderRepository,
ICountryRepository countryRepository,
+ IServiceChannelRepository serviceChannelRepository,
IFileStorage fileStorage,
ISmallBagTrackOrderClient smallBagTrackOrderClient,
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
@@ -145,12 +147,12 @@ public sealed class OrderBatchActionExecutor(
try
{
- OrderParcelCreateInput body;
+ ItfxCreateOrderOneBody body;
Order order;
if (!string.IsNullOrWhiteSpace(message.PayloadJson))
{
- body = JsonConvert.DeserializeObject(message.PayloadJson!)
+ body = JsonConvert.DeserializeObject(message.PayloadJson!)
?? throw new BusinessException("下游预报消息体无效");
// 凭证解析仍需订单上的渠道/下游绑定字段;有 payload 时只查主表一次
@@ -178,6 +180,7 @@ public sealed class OrderBatchActionExecutor(
bundle.Declarations,
bundle.Weights,
countryRepository,
+ serviceChannelRepository,
fileStorage,
cancellationToken);
}
diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs
index b7df2fa..fed05fe 100644
--- a/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs
+++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs
@@ -9,6 +9,7 @@ using Oms.Application.Integrations.SmallBagTrack;
using Oms.Application.Messaging;
using Oms.Application.Shared;
using Oms.Application.Storage;
+using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using System.Linq.Expressions;
using Oms.Domain.OrdersAggregate.Entities;
@@ -24,6 +25,7 @@ public sealed class OrderBatchJobCoordinator(
IOrderBatchJobRepository batchJobRepository,
IOrderRepository orderRepository,
ICountryRepository countryRepository,
+ IServiceChannelRepository serviceChannelRepository,
IFileStorage fileStorage,
IOrderBatchJobPublisher publisher,
OrderBatchJobProcessor batchJobProcessor,
@@ -651,6 +653,7 @@ public sealed class OrderBatchJobCoordinator(
bundle.Declarations,
bundle.Weights,
countryRepository,
+ serviceChannelRepository,
fileStorage,
cancellationToken);
result[orderId] = JsonConvert.SerializeObject(body);
diff --git a/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs b/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs
index 3c90eda..b8eb919 100644
--- a/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.DownstreamForward.cs
@@ -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)
{
var bundles = await OrderRepository.GetDownstreamCreateOneBundlesAsync([orderId]);
@@ -98,6 +98,7 @@ public partial class OrderService
bundle.Declarations,
bundle.Weights,
CountryRepository,
+ ServiceChannelRepository,
FileStorage);
return (bundle.Order, body);
diff --git a/src/Oms.Application/Shared/OrderDownstreamCreateOneBodyBuilder.cs b/src/Oms.Application/Shared/OrderDownstreamCreateOneBodyBuilder.cs
index fadb54e..aa8c17a 100644
--- a/src/Oms.Application/Shared/OrderDownstreamCreateOneBodyBuilder.cs
+++ b/src/Oms.Application/Shared/OrderDownstreamCreateOneBodyBuilder.cs
@@ -1,5 +1,6 @@
using Oms.Application.Dtos.Orders;
using Oms.Application.Storage;
+using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.SystemAggregate.Repositories;
@@ -7,17 +8,18 @@ using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Shared;
///
-/// 从订单表及关联子表组装 createOrderOne 标准请求体。
+/// 从订单表及关联子表组装 itfx createOrderOne 标准请求体。
///
public static class OrderDownstreamCreateOneBodyBuilder
{
- public static async Task BuildAsync(
+ public static async Task BuildAsync(
Order order,
OrderSender? sender,
OrderRecipient? recipient,
IReadOnlyList declarations,
IReadOnlyList weights,
ICountryRepository countryRepository,
+ IServiceChannelRepository serviceChannelRepository,
IFileStorage? fileStorage = null,
CancellationToken cancellationToken = default)
{
@@ -41,16 +43,18 @@ public static class OrderDownstreamCreateOneBodyBuilder
string.Equals(x.Type, OrderWeightTypeCodes.Forecast, StringComparison.OrdinalIgnoreCase))
?? weights.FirstOrDefault();
- return new OrderParcelCreateInput
+ var productCode = await ResolveForecastProductCodeAsync(
+ order,
+ serviceChannelRepository,
+ cancellationToken);
+
+ return new ItfxCreateOrderOneBody
{
- RequestId = order.RequestId,
ClientNo = order.ClientNo,
TrackNo = order.TrackNo,
ServiceNo = order.ServiceNo,
- ProductCode = order.ProductCode,
- ChannelCode = order.ChannelCode,
+ ProductCode = productCode,
CargoAttribute = order.CargoAttribute,
- ExportCustomsMode = order.ExportCustomsMode,
WarehouseId = order.WarehouseId,
CustomerId = order.CustomerId,
Length = forecast?.Length,
@@ -62,16 +66,45 @@ public static class OrderDownstreamCreateOneBodyBuilder
Cubic = forecast?.Cubic,
IsCod = order.IsCod > 0,
CodCurrency = order.CodCurrency,
- CodSum = order.CodSum,
+ CodSum = FormatDecimal(order.CodSum),
DeclaredCurrency = order.DeclaredCurrency,
- DeclaredSum = order.DeclaredSum,
- Quantity = order.Quantity,
+ DeclaredSum = FormatDecimal(order.DeclaredSum),
+ Quantity = order.Quantity?.ToString(),
Receiver = MapReceiver(recipient, order.ConsigneeCountry, idToCode),
Sender = MapSender(sender, idToCode),
Items = declarations.Select(x => MapItem(x, fileStorage)).ToList()
};
}
+ ///
+ /// createOrderOne.productCode = 渠道「预报渠道 code」:
+ /// 优先订单固化的 ChannelForecastCode,否则 live 查 t_service_channel.ForecastCode。
+ ///
+ private static async Task 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 idToCode)
{
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;
diff --git a/src/Oms.Tests/OrderBatchJobCoordinatorGovernanceTests.cs b/src/Oms.Tests/OrderBatchJobCoordinatorGovernanceTests.cs
index aa50039..444ade9 100644
--- a/src/Oms.Tests/OrderBatchJobCoordinatorGovernanceTests.cs
+++ b/src/Oms.Tests/OrderBatchJobCoordinatorGovernanceTests.cs
@@ -7,6 +7,7 @@ using Oms.Application.Dtos.Orders;
using Oms.Application.Messaging;
using Oms.Application.Services.Orders.Batch;
using Oms.Application.Storage;
+using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
@@ -19,6 +20,7 @@ public class OrderBatchJobCoordinatorGovernanceTests
private readonly Mock _batchJobRepository = new(MockBehavior.Strict);
private readonly Mock _orderRepository = new(MockBehavior.Loose);
private readonly Mock _countryRepository = new(MockBehavior.Loose);
+ private readonly Mock _serviceChannelRepository = new(MockBehavior.Loose);
private readonly Mock _fileStorage = new(MockBehavior.Loose);
private readonly Mock _publisher = new(MockBehavior.Strict);
private readonly OrderBatchJobCoordinator _coordinator;
@@ -38,6 +40,7 @@ public class OrderBatchJobCoordinatorGovernanceTests
_batchJobRepository.Object,
_orderRepository.Object,
_countryRepository.Object,
+ _serviceChannelRepository.Object,
_fileStorage.Object,
_publisher.Object,
processor,