fix: 对齐 createOrderOne 下游预报请求体,productCode 取预报渠道 code
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:
zzh 2026-09-16 17:43:30 +08:00
parent 14f3df3fa5
commit 6b920cc49d
8 changed files with 117 additions and 16 deletions

View 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; } = [];
}

View File

@ -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>

View File

@ -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
}; };

View File

@ -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);
} }

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -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,