fix: TikTok 拉单按订单详情映射收件人金额行商品并落库
All checks were successful
Build and Deploy / deploy (push) Successful in 59s
All checks were successful
Build and Deploy / deploy (push) Successful in 59s
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
a1a7524a0e
commit
9c97a218ec
@ -29,6 +29,14 @@ public interface IPlatformShopFulfillmentClient
|
||||
SalesStore store,
|
||||
string platformOrderNo,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// 拉取平台订单完整明细并映射为落库结构;不支持时返回 null。
|
||||
/// </summary>
|
||||
Task<PlatformShopPulledOrder?> FetchOrderDetailAsync(
|
||||
SalesStore store,
|
||||
string platformOrderNo,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
||||
public sealed class PlatformShopPullOrdersRequest
|
||||
@ -55,6 +63,24 @@ public sealed class PlatformShopPulledOrder
|
||||
|
||||
public string? OrderCountry { get; set; }
|
||||
|
||||
public string? RecipientName { get; set; }
|
||||
|
||||
public string? RecipientPhone { get; set; }
|
||||
|
||||
public string? RecipientEmail { get; set; }
|
||||
|
||||
public string? RecipientCountry { get; set; }
|
||||
|
||||
public string? RecipientProvince { get; set; }
|
||||
|
||||
public string? RecipientCity { get; set; }
|
||||
|
||||
public string? RecipientZipCode { get; set; }
|
||||
|
||||
public string? RecipientAddress1 { get; set; }
|
||||
|
||||
public string? RecipientAddress2 { get; set; }
|
||||
|
||||
public int ItemCount { get; set; }
|
||||
|
||||
public decimal? TotalAmount { get; set; }
|
||||
@ -73,6 +99,8 @@ public sealed class PlatformShopPulledOrder
|
||||
|
||||
public string? ShippingLabelUrl { get; set; }
|
||||
|
||||
public bool IsMultiLabel { get; set; }
|
||||
|
||||
public string? ExtraJson { get; set; }
|
||||
|
||||
public List<PlatformShopPulledOrderLine> Lines { get; set; } = [];
|
||||
@ -86,6 +114,10 @@ public sealed class PlatformShopPulledOrderLine
|
||||
|
||||
public string? ProductName { get; set; }
|
||||
|
||||
public string? ProductImageUrl { get; set; }
|
||||
|
||||
public string? VariantText { get; set; }
|
||||
|
||||
public int Quantity { get; set; }
|
||||
|
||||
public decimal? UnitPrice { get; set; }
|
||||
@ -212,6 +244,19 @@ internal sealed class PlatformShopFulfillmentClient(
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<PlatformShopPulledOrder?> FetchOrderDetailAsync(
|
||||
SalesStore store,
|
||||
string platformOrderNo,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var options = await configProvider.GetAsync(ct);
|
||||
return store.PlatformCode.ToLowerInvariant() switch
|
||||
{
|
||||
"tiktok" => await FetchTiktokOrderDetailAsync(store, options, platformOrderNo, ct),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
#region Shopee
|
||||
|
||||
private async Task<PlatformShopPullOrdersResult> PullShopeeAsync(
|
||||
@ -628,14 +673,8 @@ internal sealed class PlatformShopFulfillmentClient(
|
||||
{
|
||||
var id = GetString(item, "id");
|
||||
if (string.IsNullOrWhiteSpace(id) || orders.ContainsKey(id)) continue;
|
||||
var status = GetString(item, "status") ?? statusFilter;
|
||||
orders[id] = new PlatformShopPulledOrder
|
||||
{
|
||||
PlatformOrderNo = id,
|
||||
PlatformStatus = status,
|
||||
PlatformStatusLabel = status,
|
||||
FulfillStatus = MapTiktokFulfill(status)
|
||||
};
|
||||
// 搜索结果可能已带明细;没有 line_items 时后面再按详情接口补齐
|
||||
orders[id] = MapTiktokOrder(item, statusFilter);
|
||||
}
|
||||
}
|
||||
|
||||
@ -656,6 +695,8 @@ internal sealed class PlatformShopFulfillmentClient(
|
||||
};
|
||||
}
|
||||
|
||||
await EnrichTiktokOrderDetailsAsync(store, options, shopCipher, orders, errors, ct);
|
||||
|
||||
var messageText = $"TikTok 拉单完成,处理 {orders.Count} 单";
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
@ -670,6 +711,186 @@ internal sealed class PlatformShopFulfillmentClient(
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按订单 ID 批量拉详情(对齐 WMS getOrderDetail),补齐收件人、金额、行商品、运单号。
|
||||
/// </summary>
|
||||
private async Task EnrichTiktokOrderDetailsAsync(
|
||||
SalesStore store,
|
||||
PlatformShopOptions options,
|
||||
string shopCipher,
|
||||
Dictionary<string, PlatformShopPulledOrder> orders,
|
||||
List<string> errors,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var needDetail = orders.Values
|
||||
.Where(x => x.Lines.Count == 0
|
||||
|| string.IsNullOrWhiteSpace(x.RecipientAddress1)
|
||||
|| !x.TotalAmount.HasValue)
|
||||
.Select(x => x.PlatformOrderNo)
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Distinct(StringComparer.Ordinal)
|
||||
.ToList();
|
||||
if (needDetail.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var tiktok = options.Tiktok;
|
||||
for (var i = 0; i < needDetail.Count; i += 50)
|
||||
{
|
||||
var chunk = needDetail.Skip(i).Take(50).ToList();
|
||||
using var doc = await GetTiktokSignedAsync(options, store.AccessToken!, "/order/202309/orders", new Dictionary<string, string>
|
||||
{
|
||||
["app_key"] = tiktok.AppKey,
|
||||
["shop_cipher"] = shopCipher,
|
||||
["ids"] = string.Join(",", chunk),
|
||||
["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString()
|
||||
}, ct);
|
||||
if (!TryGetTiktokData(doc.RootElement, out var data, out var detailError)
|
||||
|| !data.TryGetProperty("orders", out var arr)
|
||||
|| arr.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
errors.Add("订单详情失败:" + detailError);
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var item in arr.EnumerateArray())
|
||||
{
|
||||
var mapped = MapTiktokOrder(item);
|
||||
if (string.IsNullOrWhiteSpace(mapped.PlatformOrderNo)) continue;
|
||||
orders[mapped.PlatformOrderNo] = mapped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 TikTok 订单明细 JSON 取值,字段对齐店铺订单主表/明细表。
|
||||
/// </summary>
|
||||
private static PlatformShopPulledOrder MapTiktokOrder(JsonElement order, string? fallbackStatus = null)
|
||||
{
|
||||
var id = GetString(order, "id") ?? string.Empty;
|
||||
var status = GetString(order, "status") ?? fallbackStatus ?? string.Empty;
|
||||
var recipient = order.TryGetProperty("recipient_address", out var addr) && addr.ValueKind == JsonValueKind.Object
|
||||
? addr
|
||||
: default;
|
||||
var payment = order.TryGetProperty("payment", out var pay) && pay.ValueKind == JsonValueKind.Object
|
||||
? pay
|
||||
: default;
|
||||
|
||||
var (province, city, countryFromDistrict) = ParseTiktokDistrict(recipient);
|
||||
var country = FirstNonEmpty(
|
||||
GetString(recipient, "region_code"),
|
||||
countryFromDistrict,
|
||||
GetString(recipient, "region"));
|
||||
|
||||
var lines = new List<PlatformShopPulledOrderLine>();
|
||||
var packageIds = new HashSet<string>(StringComparer.Ordinal);
|
||||
if (order.TryGetProperty("line_items", out var lineItems) && lineItems.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var line in lineItems.EnumerateArray())
|
||||
{
|
||||
var packageId = GetString(line, "package_id");
|
||||
if (!string.IsNullOrWhiteSpace(packageId)) packageIds.Add(packageId);
|
||||
lines.Add(new PlatformShopPulledOrderLine
|
||||
{
|
||||
PlatformLineId = GetString(line, "id"),
|
||||
PlatformSku = GetString(line, "seller_sku"),
|
||||
ProductName = GetString(line, "product_name"),
|
||||
ProductImageUrl = GetString(line, "sku_image"),
|
||||
VariantText = GetString(line, "sku_name"),
|
||||
// TikTok 一行一件,没有 quantity 字段
|
||||
Quantity = 1,
|
||||
UnitPrice = GetDecimal(line, "sale_price") ?? GetDecimal(line, "original_price"),
|
||||
Currency = GetString(line, "currency")
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (order.TryGetProperty("packages", out var packages) && packages.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
foreach (var pkg in packages.EnumerateArray())
|
||||
{
|
||||
var packageId = GetString(pkg, "id");
|
||||
if (!string.IsNullOrWhiteSpace(packageId)) packageIds.Add(packageId);
|
||||
}
|
||||
}
|
||||
|
||||
var tracking = GetString(order, "tracking_number");
|
||||
if (string.IsNullOrWhiteSpace(tracking)
|
||||
&& order.TryGetProperty("line_items", out var itemsForTrack)
|
||||
&& itemsForTrack.ValueKind == JsonValueKind.Array
|
||||
&& itemsForTrack.GetArrayLength() > 0)
|
||||
{
|
||||
tracking = GetString(itemsForTrack[0], "tracking_number");
|
||||
}
|
||||
|
||||
return new PlatformShopPulledOrder
|
||||
{
|
||||
PlatformOrderNo = id,
|
||||
OrderCountry = country,
|
||||
RecipientName = FirstNonEmpty(GetString(recipient, "name"), GetString(recipient, "full_address")),
|
||||
RecipientPhone = GetString(recipient, "phone_number"),
|
||||
RecipientEmail = GetString(order, "buyer_email"),
|
||||
RecipientCountry = country,
|
||||
RecipientProvince = province,
|
||||
RecipientCity = city,
|
||||
RecipientZipCode = GetString(recipient, "postal_code"),
|
||||
RecipientAddress1 = FirstNonEmpty(
|
||||
GetString(recipient, "address_line1"),
|
||||
GetString(recipient, "address_detail"),
|
||||
GetString(recipient, "full_address")),
|
||||
RecipientAddress2 = GetString(recipient, "address_line2"),
|
||||
ItemCount = lines.Count,
|
||||
TotalAmount = GetDecimal(payment, "total_amount") ?? GetDecimal(payment, "sub_total"),
|
||||
Currency = FirstNonEmpty(GetString(payment, "currency"), lines.FirstOrDefault()?.Currency),
|
||||
PlatformOrderTime = FromUnixSeconds(GetLong(order, "create_time")),
|
||||
PlatformStatus = status,
|
||||
PlatformStatusLabel = status,
|
||||
FulfillStatus = MapTiktokFulfill(status),
|
||||
ShippingLabelNo = tracking,
|
||||
IsMultiLabel = packageIds.Count > 1,
|
||||
ExtraJson = order.ValueKind == JsonValueKind.Undefined ? null : order.GetRawText(),
|
||||
Lines = lines
|
||||
};
|
||||
}
|
||||
|
||||
private static (string? Province, string? City, string? CountryCode) ParseTiktokDistrict(JsonElement recipient)
|
||||
{
|
||||
string? province = null;
|
||||
string? city = null;
|
||||
string? country = null;
|
||||
if (recipient.ValueKind != JsonValueKind.Object
|
||||
|| !recipient.TryGetProperty("district_info", out var districts)
|
||||
|| districts.ValueKind != JsonValueKind.Array)
|
||||
{
|
||||
return (null, null, null);
|
||||
}
|
||||
|
||||
foreach (var district in districts.EnumerateArray())
|
||||
{
|
||||
var level = GetString(district, "address_level");
|
||||
var name = GetString(district, "address_name");
|
||||
var iso = GetString(district, "iso_code");
|
||||
switch (level)
|
||||
{
|
||||
case "L0":
|
||||
country = FirstNonEmpty(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 Get Authorized Shops 取当前店的 shop_cipher。WMS 授权时落库,拉单查询必须带上。
|
||||
/// </summary>
|
||||
@ -791,34 +1012,57 @@ internal sealed class PlatformShopFulfillmentClient(
|
||||
return normalized.Count > 0 ? normalized : [.. TiktokPullStatuses];
|
||||
}
|
||||
|
||||
private async Task<PlatformShopOrderStatusResult> SyncTiktokAsync(
|
||||
private async Task<PlatformShopPulledOrder?> FetchTiktokOrderDetailAsync(
|
||||
SalesStore store,
|
||||
PlatformShopOptions options,
|
||||
string platformOrderNo,
|
||||
CancellationToken ct)
|
||||
{
|
||||
EnsureTiktok(store, options);
|
||||
var tiktok = options.Tiktok;
|
||||
var token = store.AccessToken!;
|
||||
var apiPath = $"/order/202309/orders?app_key={tiktok.AppKey}&ids={platformOrderNo}×tamp={PlatformShopCrypto.UnixSeconds()}";
|
||||
var sign = PlatformShopCrypto.HmacSha256Hex(tiktok.AppSecret + apiPath.Replace("?", "").Replace("&", "").Replace("=", "") + tiktok.AppSecret, tiktok.AppSecret);
|
||||
var url = $"{tiktok.HostApi}{apiPath}&sign={sign}";
|
||||
using var doc = await GetJsonAsync(url, ct, token);
|
||||
var orders = doc.RootElement.GetProperty("data").GetProperty("orders");
|
||||
if (orders.GetArrayLength() == 0)
|
||||
var shopCipher = await GetTiktokShopCipherAsync(store, options, ct);
|
||||
if (string.IsNullOrWhiteSpace(shopCipher))
|
||||
{
|
||||
return new PlatformShopOrderStatusResult { Success = false, Message = "TikTok 未找到订单" };
|
||||
return null;
|
||||
}
|
||||
|
||||
using var doc = await GetTiktokSignedAsync(options, store.AccessToken!, "/order/202309/orders", new Dictionary<string, string>
|
||||
{
|
||||
["app_key"] = options.Tiktok.AppKey,
|
||||
["shop_cipher"] = shopCipher,
|
||||
["ids"] = platformOrderNo,
|
||||
["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString()
|
||||
}, ct);
|
||||
if (!TryGetTiktokData(doc.RootElement, out var data, out _)
|
||||
|| !data.TryGetProperty("orders", out var orders)
|
||||
|| orders.ValueKind != JsonValueKind.Array
|
||||
|| orders.GetArrayLength() == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return MapTiktokOrder(orders[0]);
|
||||
}
|
||||
|
||||
private async Task<PlatformShopOrderStatusResult> SyncTiktokAsync(
|
||||
SalesStore store,
|
||||
PlatformShopOptions options,
|
||||
string platformOrderNo,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var mapped = await FetchTiktokOrderDetailAsync(store, options, platformOrderNo, ct);
|
||||
if (mapped == null)
|
||||
{
|
||||
return new PlatformShopOrderStatusResult { Success = false, Message = "TikTok 未找到订单或缺少 shop_cipher" };
|
||||
}
|
||||
|
||||
var order = orders[0];
|
||||
var status = GetString(order, "status") ?? string.Empty;
|
||||
return new PlatformShopOrderStatusResult
|
||||
{
|
||||
Success = true,
|
||||
Message = "同步成功",
|
||||
PlatformStatus = status,
|
||||
PlatformStatusLabel = status,
|
||||
FulfillStatus = MapTiktokFulfill(status)
|
||||
PlatformStatus = mapped.PlatformStatus,
|
||||
PlatformStatusLabel = mapped.PlatformStatusLabel,
|
||||
FulfillStatus = mapped.FulfillStatus,
|
||||
ShippingLabelNo = mapped.ShippingLabelNo
|
||||
};
|
||||
}
|
||||
|
||||
@ -1266,11 +1510,73 @@ internal sealed class PlatformShopFulfillmentClient(
|
||||
}
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement el, string name) =>
|
||||
el.TryGetProperty(name, out var p) && p.ValueKind == JsonValueKind.String ? p.GetString() :
|
||||
el.TryGetProperty(name, out p) && p.ValueKind is JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False
|
||||
? p.ToString()
|
||||
: null;
|
||||
private static string? GetString(JsonElement el, string name)
|
||||
{
|
||||
if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(name, out var p))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return p.ValueKind switch
|
||||
{
|
||||
JsonValueKind.String => p.GetString(),
|
||||
JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => p.ToString(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private static decimal? GetDecimal(JsonElement el, string name)
|
||||
{
|
||||
if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(name, out var p))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (p.ValueKind == JsonValueKind.Number && p.TryGetDecimal(out var number))
|
||||
{
|
||||
return number;
|
||||
}
|
||||
|
||||
if (p.ValueKind == JsonValueKind.String
|
||||
&& decimal.TryParse(p.GetString(), System.Globalization.NumberStyles.Any,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static long? GetLong(JsonElement el, string name)
|
||||
{
|
||||
if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(name, out var p))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (p.ValueKind == JsonValueKind.Number && p.TryGetInt64(out var number))
|
||||
{
|
||||
return number;
|
||||
}
|
||||
|
||||
if (p.ValueKind == JsonValueKind.String
|
||||
&& long.TryParse(p.GetString(), System.Globalization.NumberStyles.Any,
|
||||
System.Globalization.CultureInfo.InvariantCulture, out var parsed))
|
||||
{
|
||||
return parsed;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static DateTime? FromUnixSeconds(long? unix)
|
||||
{
|
||||
if (unix is null or <= 0) return null;
|
||||
return DateTimeOffset.FromUnixTimeSeconds(unix.Value).LocalDateTime;
|
||||
}
|
||||
|
||||
private static string? FirstNonEmpty(params string?[] values) =>
|
||||
values.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@ -187,6 +187,23 @@ internal sealed class PlatformStoreOrderFulfillmentService(
|
||||
|
||||
try
|
||||
{
|
||||
var detail = await fulfillmentClient.FetchOrderDetailAsync(store, order.PlatformOrderNo);
|
||||
if (detail != null)
|
||||
{
|
||||
await UpsertPulledOrderAsync(order.CustomerId, store, detail);
|
||||
var refreshed = await storeOrderRepository.FirstOrDefaultAsync(x => x.Id == order.Id) ?? order;
|
||||
return new PlatformStoreOrderBatchActionItemOutput
|
||||
{
|
||||
Id = refreshed.Id,
|
||||
PlatformOrderNo = refreshed.PlatformOrderNo,
|
||||
Success = true,
|
||||
Message = "同步成功",
|
||||
FulfillStatus = refreshed.FulfillStatus,
|
||||
ShippingLabelNo = refreshed.ShippingLabelNo,
|
||||
ShippingLabelUrl = refreshed.ShippingLabelUrl
|
||||
};
|
||||
}
|
||||
|
||||
var result = await fulfillmentClient.FetchOrderStatusAsync(store, order.PlatformOrderNo);
|
||||
if (!result.Success)
|
||||
{
|
||||
@ -307,6 +324,15 @@ internal sealed class PlatformStoreOrderFulfillmentService(
|
||||
}
|
||||
|
||||
entity.OrderCountry = pulled.OrderCountry ?? entity.OrderCountry;
|
||||
if (!string.IsNullOrWhiteSpace(pulled.RecipientName)) entity.RecipientName = pulled.RecipientName;
|
||||
if (!string.IsNullOrWhiteSpace(pulled.RecipientPhone)) entity.RecipientPhone = pulled.RecipientPhone;
|
||||
if (!string.IsNullOrWhiteSpace(pulled.RecipientEmail)) entity.RecipientEmail = pulled.RecipientEmail;
|
||||
if (!string.IsNullOrWhiteSpace(pulled.RecipientCountry)) entity.RecipientCountry = pulled.RecipientCountry;
|
||||
if (!string.IsNullOrWhiteSpace(pulled.RecipientProvince)) entity.RecipientProvince = pulled.RecipientProvince;
|
||||
if (!string.IsNullOrWhiteSpace(pulled.RecipientCity)) entity.RecipientCity = pulled.RecipientCity;
|
||||
if (!string.IsNullOrWhiteSpace(pulled.RecipientZipCode)) entity.RecipientZipCode = pulled.RecipientZipCode;
|
||||
if (!string.IsNullOrWhiteSpace(pulled.RecipientAddress1)) entity.RecipientAddress1 = pulled.RecipientAddress1;
|
||||
if (!string.IsNullOrWhiteSpace(pulled.RecipientAddress2)) entity.RecipientAddress2 = pulled.RecipientAddress2;
|
||||
entity.ItemCount = pulled.ItemCount > 0 ? pulled.ItemCount : entity.ItemCount;
|
||||
entity.TotalAmount = pulled.TotalAmount ?? entity.TotalAmount;
|
||||
entity.Currency = pulled.Currency ?? entity.Currency;
|
||||
@ -316,6 +342,7 @@ internal sealed class PlatformStoreOrderFulfillmentService(
|
||||
entity.FulfillStatus = pulled.FulfillStatus;
|
||||
entity.ShippingLabelNo = pulled.ShippingLabelNo ?? entity.ShippingLabelNo;
|
||||
entity.ShippingLabelUrl = pulled.ShippingLabelUrl ?? entity.ShippingLabelUrl;
|
||||
entity.IsMultiLabel = pulled.IsMultiLabel || entity.IsMultiLabel;
|
||||
entity.PlatformExtraJson = pulled.ExtraJson ?? entity.PlatformExtraJson;
|
||||
entity.LastSyncTime = now;
|
||||
PlatformStoreOrderParcelBuilder.TryFillRecipientFields(entity, pulled.ExtraJson);
|
||||
@ -348,7 +375,9 @@ internal sealed class PlatformStoreOrderFulfillmentService(
|
||||
PlatformLineId = line.PlatformLineId,
|
||||
PlatformSku = line.PlatformSku,
|
||||
ProductName = line.ProductName,
|
||||
Quantity = line.Quantity,
|
||||
ProductImageUrl = line.ProductImageUrl,
|
||||
VariantText = line.VariantText,
|
||||
Quantity = line.Quantity > 0 ? line.Quantity : 1,
|
||||
UnitPrice = line.UnitPrice,
|
||||
Currency = line.Currency
|
||||
});
|
||||
|
||||
@ -306,7 +306,10 @@ public static class PlatformStoreOrderParcelBuilder
|
||||
private static OrderParcelReceiverInput? ParseReceiverNode(JsonNode node)
|
||||
{
|
||||
var candidates = new List<JsonNode?> { node };
|
||||
foreach (var key in new[] { "recipient", "receiver", "shippingAddress", "address" })
|
||||
foreach (var key in new[]
|
||||
{
|
||||
"recipient_address", "recipientAddress", "recipient", "receiver", "shippingAddress", "address"
|
||||
})
|
||||
{
|
||||
if (node[key] != null)
|
||||
{
|
||||
@ -321,10 +324,22 @@ public static class PlatformStoreOrderParcelBuilder
|
||||
continue;
|
||||
}
|
||||
|
||||
var name = ReadText(candidate, "recipientsName", "recipientName", "receiverName", "name", "fullName");
|
||||
var country = ReadText(candidate, "countryCode", "country", "region", "orderCountry") ?? ReadText(node, "orderCountry");
|
||||
var zip = ReadText(candidate, "zipCode", "zip", "postalCode", "postcode");
|
||||
var address = ReadText(candidate, "streetAddress1", "address", "address1", "fullAddress", "detailAddress");
|
||||
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;
|
||||
@ -334,14 +349,14 @@ public static class PlatformStoreOrderParcelBuilder
|
||||
{
|
||||
RecipientsName = name,
|
||||
CompanyName = ReadText(candidate, "companyName", "company"),
|
||||
Phone = ReadText(candidate, "phone", "mobile", "phoneNumber"),
|
||||
Phone = ReadText(candidate, "phone", "mobile", "phone_number", "phoneNumber"),
|
||||
TelePhone = ReadText(candidate, "telePhone", "tel"),
|
||||
Email = ReadText(candidate, "email"),
|
||||
Email = ReadText(candidate, "email") ?? ReadText(node, "buyer_email"),
|
||||
CountryCode = country,
|
||||
Province = ReadText(candidate, "province", "state", "regionState"),
|
||||
City = ReadText(candidate, "city"),
|
||||
Province = ReadText(candidate, "province", "state", "regionState") ?? province,
|
||||
City = ReadText(candidate, "city") ?? city,
|
||||
StreetAddress1 = address,
|
||||
StreetAddress2 = ReadText(candidate, "streetAddress2", "address2"),
|
||||
StreetAddress2 = ReadText(candidate, "streetAddress2", "address_line2", "address2"),
|
||||
ZipCode = zip
|
||||
};
|
||||
}
|
||||
@ -349,6 +364,46 @@ public static class PlatformStoreOrderParcelBuilder
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析 TikTok recipient_address.district_info:L0 国家、L1 州省、L3 城市。
|
||||
/// </summary>
|
||||
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user