using System.Text.Json; using System.Text.Json.Nodes; using Fake.Application; using Oms.Application.Dtos.Orders; using Oms.Application.Dtos.PlatformStore; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.SystemAggregate.Entities; using Oms.Domain.WarehouseAggregate.Entities; namespace Oms.Application.Services.Orders; /// /// 将店铺订单转换为小包创建入参。 /// public static class PlatformStoreOrderParcelBuilder { private const decimal DefaultLengthCm = 10m; private const decimal DefaultWidthCm = 10m; private const decimal DefaultHeightCm = 10m; private const decimal MinActualWeightKg = 0.01m; public static OrderParcelCreateInput Build( SalesStoreOrder storeOrder, IReadOnlyCollection lines, IReadOnlyDictionary skuMap, WarehouseAddress warehouse, string productCode, int warehouseId, int customerId, OrderParcelReceiverInput? receiverOverride = null, string? trackNoOverride = null) { var receiver = receiverOverride ?? ParseReceiver(storeOrder); ValidateReceiver(receiver); var declarationLines = BuildDeclarationLines(lines, skuMap); var (length, width, height, actualWeightKg) = ResolveParcelMetrics(declarationLines, lines, skuMap); return new OrderParcelCreateInput { RequestId = $"platform-store-order:{storeOrder.Id}", ClientNo = storeOrder.PlatformOrderNo, TrackNo = string.IsNullOrWhiteSpace(trackNoOverride) ? storeOrder.ShippingLabelNo : trackNoOverride.Trim(), ProductCode = productCode, WarehouseId = warehouseId, CustomerId = customerId, Length = length, Width = width, Height = height, ActualWeight = actualWeightKg, ChargeableWeight = actualWeightKg, CargoAttribute = CargoAttributeCodes.General, DeclaredCurrency = string.IsNullOrWhiteSpace(storeOrder.Currency) ? "USD" : storeOrder.Currency.Trim(), DeclaredSum = storeOrder.TotalAmount, Quantity = 1, Receiver = receiver, Sender = BuildSender(warehouse), Items = declarationLines }; } /// /// 将下单页同款「运单基本信息」覆盖到组装好的小包入参。 /// public static void ApplyParcelBasic( OrderParcelCreateInput parcelInput, PlatformStoreOrderParcelBasicInput? basic, string? clientNoOverride = null) { if (!string.IsNullOrWhiteSpace(clientNoOverride)) { parcelInput.ClientNo = clientNoOverride.Trim(); } else if (!string.IsNullOrWhiteSpace(basic?.ClientNo)) { parcelInput.ClientNo = basic.ClientNo.Trim(); } if (basic == null) { return; } if (!string.IsNullOrWhiteSpace(basic.CargoAttribute)) { parcelInput.CargoAttribute = basic.CargoAttribute.Trim(); } if (basic.ActualWeight is > 0) { parcelInput.ActualWeight = basic.ActualWeight; parcelInput.ChargeableWeight = basic.ActualWeight; } if (basic.Quantity is > 0) { parcelInput.Quantity = basic.Quantity; } if (basic.Length is > 0) { parcelInput.Length = basic.Length; } if (basic.Width is > 0) { parcelInput.Width = basic.Width; } if (basic.Height is > 0) { parcelInput.Height = basic.Height; } if (!string.IsNullOrWhiteSpace(basic.ExportCustomsMode)) { parcelInput.ExportCustomsMode = basic.ExportCustomsMode.Trim(); } if (basic.ExportQualificationId is > 0) { parcelInput.ExportQualificationId = basic.ExportQualificationId; } if (!string.IsNullOrWhiteSpace(basic.PlatformCode)) { parcelInput.PlatformCode = basic.PlatformCode.Trim(); } if (!string.IsNullOrWhiteSpace(basic.InboundMethod)) { parcelInput.InboundMethod = basic.InboundMethod.Trim(); } if (basic.SenderAddressId is > 0) { parcelInput.SenderAddressId = basic.SenderAddressId; } if (basic.ExpectPickupTime.HasValue && basic.ExpectPickupTime != default) { parcelInput.ExpectPickupTime = basic.ExpectPickupTime; } if (!string.IsNullOrWhiteSpace(basic.PickupDirection)) { parcelInput.PickupDirection = basic.PickupDirection.Trim(); } if (!string.IsNullOrWhiteSpace(basic.CarrierCode)) { parcelInput.CarrierCode = basic.CarrierCode.Trim(); } if (!string.IsNullOrWhiteSpace(basic.CarrierService)) { parcelInput.CarrierService = basic.CarrierService.Trim(); } } /// /// 按平台商品行组装小包入参(不要求库内 SKU 映射;用于创建物流 / 物流&代发)。 /// public static OrderParcelCreateInput BuildFromPlatformLines( SalesStoreOrder storeOrder, IReadOnlyCollection lines, WarehouseAddress? warehouse, string productCode, int? warehouseId, int customerId, OrderParcelReceiverInput? receiverOverride = null, string? trackNoOverride = null) { var receiver = receiverOverride ?? ParseReceiver(storeOrder); ValidateReceiver(receiver); var declarationLines = BuildDeclarationLinesFromPlatform(lines); var totalWeightKg = Math.Max( declarationLines.Sum(x => x.Weight ?? 0m), MinActualWeightKg); return new OrderParcelCreateInput { RequestId = $"platform-store-order:{storeOrder.Id}", ClientNo = storeOrder.PlatformOrderNo, TrackNo = string.IsNullOrWhiteSpace(trackNoOverride) ? storeOrder.ShippingLabelNo : trackNoOverride?.Trim(), ProductCode = productCode, WarehouseId = warehouseId, CustomerId = customerId, Length = DefaultLengthCm, Width = DefaultWidthCm, Height = DefaultHeightCm, ActualWeight = totalWeightKg, ChargeableWeight = totalWeightKg, CargoAttribute = CargoAttributeCodes.General, DeclaredCurrency = string.IsNullOrWhiteSpace(storeOrder.Currency) ? "USD" : storeOrder.Currency.Trim(), DeclaredSum = storeOrder.TotalAmount, Quantity = 1, Receiver = receiver, Sender = warehouse != null ? BuildSender(warehouse) : BuildFallbackSender(storeOrder), Items = declarationLines }; } private static List BuildDeclarationLinesFromPlatform( IReadOnlyCollection lines) { var items = new List(); foreach (var line in lines) { var cnName = string.IsNullOrWhiteSpace(line.ProductName) ? (string.IsNullOrWhiteSpace(line.PlatformSku) ? "Platform Item" : line.PlatformSku.Trim()) : line.ProductName.Trim(); var sku = string.IsNullOrWhiteSpace(line.PlatformSku) ? (cnName.Length > 64 ? cnName[..64] : cnName) : line.PlatformSku.Trim(); var unitPrice = line.UnitPrice is > 0 ? line.UnitPrice.Value : 1m; var qty = Math.Max(1, line.Quantity); items.Add(new OrderParcelItemInput { CnName = cnName, EnName = cnName, Sku = sku, Unit = "PCS", UnitPrice = unitPrice, UnitWeight = MinActualWeightKg, Quantity = qty, Amount = unitPrice * qty, Weight = MinActualWeightKg * qty }); } if (items.Count == 0) { throw new BusinessException("没有可申报的商品行"); } return items; } private static OrderParcelSenderInput BuildFallbackSender(SalesStoreOrder storeOrder) { return new OrderParcelSenderInput { SenderName = string.IsNullOrWhiteSpace(storeOrder.PlatformOrderNo) ? "Platform Seller" : $"Store#{storeOrder.StoreId}", StreetAddress1 = "TBD", CountryCode = storeOrder.OrderCountry }; } private static List BuildDeclarationLines( IReadOnlyCollection lines, IReadOnlyDictionary skuMap) { var items = new List(); foreach (var line in lines) { if (!line.MappedSkuId.HasValue || !skuMap.TryGetValue(line.MappedSkuId.Value, out var sku)) { throw new BusinessException($"申报行「{line.PlatformSku}」缺少库内 SKU 映射"); } var unitWeightKg = sku.WeightG is > 0 ? Math.Max(sku.WeightG.Value / 1000m, MinActualWeightKg) : MinActualWeightKg; var unitPrice = line.UnitPrice is > 0 ? line.UnitPrice.Value : 1m; var cnName = string.IsNullOrWhiteSpace(line.ProductName) ? sku.SkuName : line.ProductName.Trim(); var enName = string.IsNullOrWhiteSpace(sku.SkuNameEn) ? (string.IsNullOrWhiteSpace(sku.SkuName) ? cnName : sku.SkuName) : sku.SkuNameEn.Trim(); items.Add(new OrderParcelItemInput { CnName = cnName, EnName = enName, Sku = sku.SkuCode, Unit = "PCS", UnitPrice = unitPrice, UnitWeight = unitWeightKg, Quantity = line.Quantity, Amount = unitPrice * line.Quantity, Weight = unitWeightKg * line.Quantity, ExHsCode = sku.ExHsCode, ImHsCode = sku.ImHsCode, DeclareElement = sku.DeclareElement, ProductId = sku.ProductId, NonStdProductId = sku.NonStdProductId, StdProductId = sku.StdProductId, }); } if (items.Count == 0) { throw new BusinessException("没有可申报的商品行"); } return items; } private static (decimal Length, decimal Width, decimal Height, decimal ActualWeightKg) ResolveParcelMetrics( IReadOnlyCollection items, IReadOnlyCollection lines, IReadOnlyDictionary skuMap) { decimal maxLength = DefaultLengthCm; decimal maxWidth = DefaultWidthCm; decimal maxHeight = DefaultHeightCm; decimal totalWeightKg = 0m; foreach (var line in lines.Where(x => x.MappedSkuId.HasValue)) { if (!skuMap.TryGetValue(line.MappedSkuId!.Value, out var sku)) { continue; } if (sku.LengthCm is > 0) { maxLength = Math.Max(maxLength, sku.LengthCm.Value); } if (sku.WidthCm is > 0) { maxWidth = Math.Max(maxWidth, sku.WidthCm.Value); } if (sku.HeightCm is > 0) { maxHeight = Math.Max(maxHeight, sku.HeightCm.Value); } var unitWeightKg = sku.WeightG is > 0 ? sku.WeightG.Value / 1000m : MinActualWeightKg; totalWeightKg += unitWeightKg * line.Quantity; } if (totalWeightKg <= 0) { totalWeightKg = Math.Max(items.Sum(x => x.Weight ?? 0m), MinActualWeightKg); } return (maxLength, maxWidth, maxHeight, Math.Max(totalWeightKg, MinActualWeightKg)); } private static OrderParcelSenderInput BuildSender(WarehouseAddress warehouse) { var senderName = string.IsNullOrWhiteSpace(warehouse.Recipient) ? warehouse.WarehouseName : warehouse.Recipient.Trim(); return new OrderParcelSenderInput { SenderName = senderName, Phone = warehouse.ContactPhone, StreetAddress1 = string.IsNullOrWhiteSpace(warehouse.ShippingAddress) ? warehouse.WarehouseName : warehouse.ShippingAddress.Trim() }; } private static OrderParcelReceiverInput? ParseReceiver(SalesStoreOrder storeOrder) { if (!string.IsNullOrWhiteSpace(storeOrder.RecipientName) && !string.IsNullOrWhiteSpace(storeOrder.RecipientCountry ?? storeOrder.OrderCountry) && !string.IsNullOrWhiteSpace(storeOrder.RecipientZipCode) && !string.IsNullOrWhiteSpace(storeOrder.RecipientAddress1)) { return new OrderParcelReceiverInput { RecipientsName = storeOrder.RecipientName.Trim(), CompanyName = storeOrder.RecipientCompany, Phone = storeOrder.RecipientPhone, TelePhone = storeOrder.RecipientTelePhone, Email = storeOrder.RecipientEmail, CountryCode = (storeOrder.RecipientCountry ?? storeOrder.OrderCountry)!.Trim(), Province = storeOrder.RecipientProvince, City = storeOrder.RecipientCity, StreetAddress1 = storeOrder.RecipientAddress1.Trim(), StreetAddress2 = storeOrder.RecipientAddress2, ZipCode = storeOrder.RecipientZipCode.Trim() }; } if (!string.IsNullOrWhiteSpace(storeOrder.PlatformExtraJson)) { try { var node = JsonNode.Parse(storeOrder.PlatformExtraJson); if (node != null) { var parsed = ParseReceiverNode(node); if (parsed != null) { return parsed; } } } catch (JsonException) { // 忽略 JSON 解析错误,走兜底 } } if (string.IsNullOrWhiteSpace(storeOrder.OrderCountry)) { return null; } return new OrderParcelReceiverInput { RecipientsName = "Platform Recipient", CountryCode = storeOrder.OrderCountry.Trim(), ZipCode = "00000", StreetAddress1 = "TBD" }; } private static OrderParcelReceiverInput? ParseReceiverNode(JsonNode node) { var candidates = new List { node }; foreach (var key in new[] { "recipient_address", "recipientAddress", "recipient", "receiver", "shippingAddress", "address" }) { if (node[key] != null) { candidates.Add(node[key]); } } foreach (var candidate in candidates) { if (candidate == null) { continue; } var (province, city, countryFromDistrict) = ParseTiktokDistrictInfo(candidate); var name = ReadText(candidate, "recipientsName", "recipientName", "receiverName", "name", "fullName", "first_name"); var country = ReadText(candidate, "countryCode", "country", "region_code", "region", "orderCountry") ?? countryFromDistrict ?? ReadText(node, "orderCountry"); var zip = ReadText(candidate, "zipCode", "zip", "postal_code", "postalCode", "postcode"); var address = ReadText( candidate, "streetAddress1", "address_line1", "address_detail", "address", "address1", "full_address", "fullAddress", "detailAddress"); if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(country) || string.IsNullOrWhiteSpace(zip) || string.IsNullOrWhiteSpace(address)) { continue; } return new OrderParcelReceiverInput { RecipientsName = name, CompanyName = ReadText(candidate, "companyName", "company"), Phone = ReadText(candidate, "phone", "mobile", "phone_number", "phoneNumber"), TelePhone = ReadText(candidate, "telePhone", "tel"), Email = ReadText(candidate, "email") ?? ReadText(node, "buyer_email"), CountryCode = country, Province = ReadText(candidate, "province", "state", "regionState") ?? province, City = ReadText(candidate, "city") ?? city, StreetAddress1 = address, StreetAddress2 = ReadText(candidate, "streetAddress2", "address_line2", "address2"), ZipCode = zip }; } return null; } /// /// 解析 TikTok recipient_address.district_info:L0 国家、L1 州省、L3 城市。 /// private static (string? Province, string? City, string? CountryCode) ParseTiktokDistrictInfo(JsonNode candidate) { var districts = candidate["district_info"]?.AsArray(); if (districts == null || districts.Count == 0) { return (null, null, null); } string? province = null; string? city = null; string? country = null; foreach (var district in districts) { if (district == null) continue; var level = district["address_level"]?.ToString(); var name = district["address_name"]?.ToString()?.Trim(); var iso = district["iso_code"]?.ToString()?.Trim(); switch (level) { case "L0": country = !string.IsNullOrWhiteSpace(iso) ? iso : name; break; case "L1": province = name; break; case "L3": city = name; break; case "L2" when string.IsNullOrWhiteSpace(city): city = name; break; } } return (province, city, country); } private static string? ReadText(JsonNode node, params string[] keys) { foreach (var key in keys) { var value = node[key]?.ToString()?.Trim(); if (!string.IsNullOrWhiteSpace(value)) { return value; } } return null; } private static void ValidateReceiver(OrderParcelReceiverInput? receiver) { if (receiver == null || string.IsNullOrWhiteSpace(receiver.RecipientsName) || string.IsNullOrWhiteSpace(receiver.CountryCode) || string.IsNullOrWhiteSpace(receiver.ZipCode) || string.IsNullOrWhiteSpace(receiver.StreetAddress1)) { throw new BusinessException("收件人信息不完整,请同步平台订单或在匹配时补充收件人"); } } /// /// 从 PlatformExtraJson / ExtraJson 解析收件人并写入结构化字段(已有值不覆盖)。 /// public static void TryFillRecipientFields(SalesStoreOrder order, string? extraJson = null) { if (!string.IsNullOrWhiteSpace(order.RecipientName) && !string.IsNullOrWhiteSpace(order.RecipientAddress1) && !string.IsNullOrWhiteSpace(order.RecipientZipCode) && !string.IsNullOrWhiteSpace(order.RecipientCountry ?? order.OrderCountry)) { return; } var json = !string.IsNullOrWhiteSpace(extraJson) ? extraJson : order.PlatformExtraJson; if (string.IsNullOrWhiteSpace(json)) { return; } try { var node = JsonNode.Parse(json); if (node == null) { return; } var parsed = ParseReceiverNode(node); if (parsed == null) { return; } order.RecipientName ??= parsed.RecipientsName; order.RecipientCompany ??= parsed.CompanyName; order.RecipientPhone ??= parsed.Phone; order.RecipientTelePhone ??= parsed.TelePhone; order.RecipientEmail ??= parsed.Email; order.RecipientCountry ??= parsed.CountryCode; order.RecipientProvince ??= parsed.Province; order.RecipientCity ??= parsed.City; order.RecipientZipCode ??= parsed.ZipCode; order.RecipientAddress1 ??= parsed.StreetAddress1; order.RecipientAddress2 ??= parsed.StreetAddress2; if (string.IsNullOrWhiteSpace(order.OrderCountry) && !string.IsNullOrWhiteSpace(parsed.CountryCode)) { order.OrderCountry = parsed.CountryCode; } } catch (JsonException) { // ignore } } }