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 OrderParcelCreateInput Body { get; set; } = new();
public ItfxCreateOrderOneBody Body { get; set; } = new();
}
/// <summary>

View File

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

View File

@ -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<OrderParcelCreateInput>(message.PayloadJson!)
body = JsonConvert.DeserializeObject<ItfxCreateOrderOneBody>(message.PayloadJson!)
?? throw new BusinessException("下游预报消息体无效");
// 凭证解析仍需订单上的渠道/下游绑定字段;有 payload 时只查主表一次
@ -178,6 +180,7 @@ public sealed class OrderBatchActionExecutor(
bundle.Declarations,
bundle.Weights,
countryRepository,
serviceChannelRepository,
fileStorage,
cancellationToken);
}

View File

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

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

View File

@ -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;
/// <summary>
/// 从订单表及关联子表组装 createOrderOne 标准请求体。
/// 从订单表及关联子表组装 itfx createOrderOne 标准请求体。
/// </summary>
public static class OrderDownstreamCreateOneBodyBuilder
{
public static async Task<OrderParcelCreateInput> BuildAsync(
public static async Task<ItfxCreateOrderOneBody> BuildAsync(
Order order,
OrderSender? sender,
OrderRecipient? recipient,
IReadOnlyList<OrderDeclaration> declarations,
IReadOnlyList<OrderWeight> 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()
};
}
/// <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)
{
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;

View File

@ -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<IOrderBatchJobRepository> _batchJobRepository = new(MockBehavior.Strict);
private readonly Mock<IOrderRepository> _orderRepository = 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<IOrderBatchJobPublisher> _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,