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,
|
SalesStore store,
|
||||||
string platformOrderNo,
|
string platformOrderNo,
|
||||||
CancellationToken ct = default);
|
CancellationToken ct = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 拉取平台订单完整明细并映射为落库结构;不支持时返回 null。
|
||||||
|
/// </summary>
|
||||||
|
Task<PlatformShopPulledOrder?> FetchOrderDetailAsync(
|
||||||
|
SalesStore store,
|
||||||
|
string platformOrderNo,
|
||||||
|
CancellationToken ct = default);
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class PlatformShopPullOrdersRequest
|
public sealed class PlatformShopPullOrdersRequest
|
||||||
@ -55,6 +63,24 @@ public sealed class PlatformShopPulledOrder
|
|||||||
|
|
||||||
public string? OrderCountry { get; set; }
|
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 int ItemCount { get; set; }
|
||||||
|
|
||||||
public decimal? TotalAmount { get; set; }
|
public decimal? TotalAmount { get; set; }
|
||||||
@ -73,6 +99,8 @@ public sealed class PlatformShopPulledOrder
|
|||||||
|
|
||||||
public string? ShippingLabelUrl { get; set; }
|
public string? ShippingLabelUrl { get; set; }
|
||||||
|
|
||||||
|
public bool IsMultiLabel { get; set; }
|
||||||
|
|
||||||
public string? ExtraJson { get; set; }
|
public string? ExtraJson { get; set; }
|
||||||
|
|
||||||
public List<PlatformShopPulledOrderLine> Lines { get; set; } = [];
|
public List<PlatformShopPulledOrderLine> Lines { get; set; } = [];
|
||||||
@ -86,6 +114,10 @@ public sealed class PlatformShopPulledOrderLine
|
|||||||
|
|
||||||
public string? ProductName { get; set; }
|
public string? ProductName { get; set; }
|
||||||
|
|
||||||
|
public string? ProductImageUrl { get; set; }
|
||||||
|
|
||||||
|
public string? VariantText { get; set; }
|
||||||
|
|
||||||
public int Quantity { get; set; }
|
public int Quantity { get; set; }
|
||||||
|
|
||||||
public decimal? UnitPrice { 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
|
#region Shopee
|
||||||
|
|
||||||
private async Task<PlatformShopPullOrdersResult> PullShopeeAsync(
|
private async Task<PlatformShopPullOrdersResult> PullShopeeAsync(
|
||||||
@ -628,14 +673,8 @@ internal sealed class PlatformShopFulfillmentClient(
|
|||||||
{
|
{
|
||||||
var id = GetString(item, "id");
|
var id = GetString(item, "id");
|
||||||
if (string.IsNullOrWhiteSpace(id) || orders.ContainsKey(id)) continue;
|
if (string.IsNullOrWhiteSpace(id) || orders.ContainsKey(id)) continue;
|
||||||
var status = GetString(item, "status") ?? statusFilter;
|
// 搜索结果可能已带明细;没有 line_items 时后面再按详情接口补齐
|
||||||
orders[id] = new PlatformShopPulledOrder
|
orders[id] = MapTiktokOrder(item, statusFilter);
|
||||||
{
|
|
||||||
PlatformOrderNo = id,
|
|
||||||
PlatformStatus = status,
|
|
||||||
PlatformStatusLabel = status,
|
|
||||||
FulfillStatus = MapTiktokFulfill(status)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,6 +695,8 @@ internal sealed class PlatformShopFulfillmentClient(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await EnrichTiktokOrderDetailsAsync(store, options, shopCipher, orders, errors, ct);
|
||||||
|
|
||||||
var messageText = $"TikTok 拉单完成,处理 {orders.Count} 单";
|
var messageText = $"TikTok 拉单完成,处理 {orders.Count} 单";
|
||||||
if (errors.Count > 0)
|
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>
|
/// <summary>
|
||||||
/// 从 Get Authorized Shops 取当前店的 shop_cipher。WMS 授权时落库,拉单查询必须带上。
|
/// 从 Get Authorized Shops 取当前店的 shop_cipher。WMS 授权时落库,拉单查询必须带上。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -791,34 +1012,57 @@ internal sealed class PlatformShopFulfillmentClient(
|
|||||||
return normalized.Count > 0 ? normalized : [.. TiktokPullStatuses];
|
return normalized.Count > 0 ? normalized : [.. TiktokPullStatuses];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<PlatformShopOrderStatusResult> SyncTiktokAsync(
|
private async Task<PlatformShopPulledOrder?> FetchTiktokOrderDetailAsync(
|
||||||
SalesStore store,
|
SalesStore store,
|
||||||
PlatformShopOptions options,
|
PlatformShopOptions options,
|
||||||
string platformOrderNo,
|
string platformOrderNo,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
EnsureTiktok(store, options);
|
EnsureTiktok(store, options);
|
||||||
var tiktok = options.Tiktok;
|
var shopCipher = await GetTiktokShopCipherAsync(store, options, ct);
|
||||||
var token = store.AccessToken!;
|
if (string.IsNullOrWhiteSpace(shopCipher))
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
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
|
return new PlatformShopOrderStatusResult
|
||||||
{
|
{
|
||||||
Success = true,
|
Success = true,
|
||||||
Message = "同步成功",
|
Message = "同步成功",
|
||||||
PlatformStatus = status,
|
PlatformStatus = mapped.PlatformStatus,
|
||||||
PlatformStatusLabel = status,
|
PlatformStatusLabel = mapped.PlatformStatusLabel,
|
||||||
FulfillStatus = MapTiktokFulfill(status)
|
FulfillStatus = mapped.FulfillStatus,
|
||||||
|
ShippingLabelNo = mapped.ShippingLabelNo
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1266,11 +1510,73 @@ internal sealed class PlatformShopFulfillmentClient(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string? GetString(JsonElement el, string name) =>
|
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
|
if (el.ValueKind != JsonValueKind.Object || !el.TryGetProperty(name, out var p))
|
||||||
? p.ToString()
|
{
|
||||||
: null;
|
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
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@ -187,6 +187,23 @@ internal sealed class PlatformStoreOrderFulfillmentService(
|
|||||||
|
|
||||||
try
|
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);
|
var result = await fulfillmentClient.FetchOrderStatusAsync(store, order.PlatformOrderNo);
|
||||||
if (!result.Success)
|
if (!result.Success)
|
||||||
{
|
{
|
||||||
@ -307,6 +324,15 @@ internal sealed class PlatformStoreOrderFulfillmentService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
entity.OrderCountry = pulled.OrderCountry ?? entity.OrderCountry;
|
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.ItemCount = pulled.ItemCount > 0 ? pulled.ItemCount : entity.ItemCount;
|
||||||
entity.TotalAmount = pulled.TotalAmount ?? entity.TotalAmount;
|
entity.TotalAmount = pulled.TotalAmount ?? entity.TotalAmount;
|
||||||
entity.Currency = pulled.Currency ?? entity.Currency;
|
entity.Currency = pulled.Currency ?? entity.Currency;
|
||||||
@ -316,6 +342,7 @@ internal sealed class PlatformStoreOrderFulfillmentService(
|
|||||||
entity.FulfillStatus = pulled.FulfillStatus;
|
entity.FulfillStatus = pulled.FulfillStatus;
|
||||||
entity.ShippingLabelNo = pulled.ShippingLabelNo ?? entity.ShippingLabelNo;
|
entity.ShippingLabelNo = pulled.ShippingLabelNo ?? entity.ShippingLabelNo;
|
||||||
entity.ShippingLabelUrl = pulled.ShippingLabelUrl ?? entity.ShippingLabelUrl;
|
entity.ShippingLabelUrl = pulled.ShippingLabelUrl ?? entity.ShippingLabelUrl;
|
||||||
|
entity.IsMultiLabel = pulled.IsMultiLabel || entity.IsMultiLabel;
|
||||||
entity.PlatformExtraJson = pulled.ExtraJson ?? entity.PlatformExtraJson;
|
entity.PlatformExtraJson = pulled.ExtraJson ?? entity.PlatformExtraJson;
|
||||||
entity.LastSyncTime = now;
|
entity.LastSyncTime = now;
|
||||||
PlatformStoreOrderParcelBuilder.TryFillRecipientFields(entity, pulled.ExtraJson);
|
PlatformStoreOrderParcelBuilder.TryFillRecipientFields(entity, pulled.ExtraJson);
|
||||||
@ -348,7 +375,9 @@ internal sealed class PlatformStoreOrderFulfillmentService(
|
|||||||
PlatformLineId = line.PlatformLineId,
|
PlatformLineId = line.PlatformLineId,
|
||||||
PlatformSku = line.PlatformSku,
|
PlatformSku = line.PlatformSku,
|
||||||
ProductName = line.ProductName,
|
ProductName = line.ProductName,
|
||||||
Quantity = line.Quantity,
|
ProductImageUrl = line.ProductImageUrl,
|
||||||
|
VariantText = line.VariantText,
|
||||||
|
Quantity = line.Quantity > 0 ? line.Quantity : 1,
|
||||||
UnitPrice = line.UnitPrice,
|
UnitPrice = line.UnitPrice,
|
||||||
Currency = line.Currency
|
Currency = line.Currency
|
||||||
});
|
});
|
||||||
|
|||||||
@ -306,7 +306,10 @@ public static class PlatformStoreOrderParcelBuilder
|
|||||||
private static OrderParcelReceiverInput? ParseReceiverNode(JsonNode node)
|
private static OrderParcelReceiverInput? ParseReceiverNode(JsonNode node)
|
||||||
{
|
{
|
||||||
var candidates = new List<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)
|
if (node[key] != null)
|
||||||
{
|
{
|
||||||
@ -321,10 +324,22 @@ public static class PlatformStoreOrderParcelBuilder
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var name = ReadText(candidate, "recipientsName", "recipientName", "receiverName", "name", "fullName");
|
var (province, city, countryFromDistrict) = ParseTiktokDistrictInfo(candidate);
|
||||||
var country = ReadText(candidate, "countryCode", "country", "region", "orderCountry") ?? ReadText(node, "orderCountry");
|
var name = ReadText(candidate, "recipientsName", "recipientName", "receiverName", "name", "fullName", "first_name");
|
||||||
var zip = ReadText(candidate, "zipCode", "zip", "postalCode", "postcode");
|
var country = ReadText(candidate, "countryCode", "country", "region_code", "region", "orderCountry")
|
||||||
var address = ReadText(candidate, "streetAddress1", "address", "address1", "fullAddress", "detailAddress");
|
?? 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))
|
if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(country) || string.IsNullOrWhiteSpace(zip) || string.IsNullOrWhiteSpace(address))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@ -334,14 +349,14 @@ public static class PlatformStoreOrderParcelBuilder
|
|||||||
{
|
{
|
||||||
RecipientsName = name,
|
RecipientsName = name,
|
||||||
CompanyName = ReadText(candidate, "companyName", "company"),
|
CompanyName = ReadText(candidate, "companyName", "company"),
|
||||||
Phone = ReadText(candidate, "phone", "mobile", "phoneNumber"),
|
Phone = ReadText(candidate, "phone", "mobile", "phone_number", "phoneNumber"),
|
||||||
TelePhone = ReadText(candidate, "telePhone", "tel"),
|
TelePhone = ReadText(candidate, "telePhone", "tel"),
|
||||||
Email = ReadText(candidate, "email"),
|
Email = ReadText(candidate, "email") ?? ReadText(node, "buyer_email"),
|
||||||
CountryCode = country,
|
CountryCode = country,
|
||||||
Province = ReadText(candidate, "province", "state", "regionState"),
|
Province = ReadText(candidate, "province", "state", "regionState") ?? province,
|
||||||
City = ReadText(candidate, "city"),
|
City = ReadText(candidate, "city") ?? city,
|
||||||
StreetAddress1 = address,
|
StreetAddress1 = address,
|
||||||
StreetAddress2 = ReadText(candidate, "streetAddress2", "address2"),
|
StreetAddress2 = ReadText(candidate, "streetAddress2", "address_line2", "address2"),
|
||||||
ZipCode = zip
|
ZipCode = zip
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -349,6 +364,46 @@ public static class PlatformStoreOrderParcelBuilder
|
|||||||
return null;
|
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)
|
private static string? ReadText(JsonNode node, params string[] keys)
|
||||||
{
|
{
|
||||||
foreach (var key in keys)
|
foreach (var key in keys)
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user