admin/src/Oms.Application/Shared/OrderDownstreamCreateOneBodyBuilder.cs
zzh ca95e19186
All checks were successful
Build and Deploy / deploy (push) Successful in 1m0s
feat: SKU/预报下游申报明细带产品标识编码三字段
库内 SKU 与 createOrderOne items 对齐 productId/nonStdProductId/stdProductId;无值时仍序列化为 null。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-23 11:19:12 +08:00

228 lines
8.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Fake.ObjectStorage;
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;
namespace Oms.Application.Shared;
/// <summary>
/// 从订单表及关联子表组装 itfx createOrderOne 标准请求体。
/// </summary>
public static class OrderDownstreamCreateOneBodyBuilder
{
public static async Task<ItfxCreateOrderOneBody> BuildAsync(
Order order,
OrderSender? sender,
OrderRecipient? recipient,
IReadOnlyList<OrderDeclaration> declarations,
IReadOnlyList<OrderWeight> weights,
ICountryRepository countryRepository,
IServiceChannelRepository serviceChannelRepository,
IObjectStorage? fileStorage = null,
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();
var productCode = await ResolveForecastProductCodeAsync(
order,
serviceChannelRepository,
cancellationToken);
return new ItfxCreateOrderOneBody
{
ClientNo = order.ClientNo,
TrackNo = order.TrackNo,
ServiceNo = order.ServiceNo,
ProductCode = productCode,
CargoAttribute = order.CargoAttribute,
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 = FormatDecimal(order.CodSum),
DeclaredCurrency = order.DeclaredCurrency,
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))
{
var trimmed = orderConsigneeCountry.Trim();
if (trimmed.Length == 2)
{
return trimmed.ToUpperInvariant();
}
}
if (countryId is > 0 && idToCode.TryGetValue(countryId.Value, out var iso) && !string.IsNullOrWhiteSpace(iso))
{
return iso.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, IObjectStorage? fileStorage) =>
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,
DeclareElement = x.DeclareElement,
// 始终赋值;空白→null。配合 OrderParcelItemInput 上 JsonIgnore(Never),请求体固定出现 "xxx": null。
ProductId = NullIfWhiteSpace(x.ProductId),
NonStdProductId = NullIfWhiteSpace(x.NonStdProductId),
StdProductId = NullIfWhiteSpace(x.StdProductId),
SkuImage = fileStorage?.GetPublicUrl(x.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
};
private static string? NullIfWhiteSpace(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
}