diff --git a/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs b/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs
index ce0e09b..e73b5bd 100644
--- a/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs
+++ b/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs
@@ -29,6 +29,14 @@ public interface IPlatformShopFulfillmentClient
SalesStore store,
string platformOrderNo,
CancellationToken ct = default);
+
+ ///
+ /// 拉取平台订单完整明细并映射为落库结构;不支持时返回 null。
+ ///
+ Task 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 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 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 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(
};
}
+ ///
+ /// 按订单 ID 批量拉详情(对齐 WMS getOrderDetail),补齐收件人、金额、行商品、运单号。
+ ///
+ private async Task EnrichTiktokOrderDetailsAsync(
+ SalesStore store,
+ PlatformShopOptions options,
+ string shopCipher,
+ Dictionary orders,
+ List 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
+ {
+ ["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;
+ }
+ }
+ }
+
+ ///
+ /// 按 TikTok 订单明细 JSON 取值,字段对齐店铺订单主表/明细表。
+ ///
+ 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();
+ var packageIds = new HashSet(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);
+ }
+
///
/// 从 Get Authorized Shops 取当前店的 shop_cipher。WMS 授权时落库,拉单查询必须带上。
///
@@ -791,34 +1012,57 @@ internal sealed class PlatformShopFulfillmentClient(
return normalized.Count > 0 ? normalized : [.. TiktokPullStatuses];
}
- private async Task SyncTiktokAsync(
+ private async Task 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
+ {
+ ["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 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
}
diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs
index e8e06de..7f9bc3a 100644
--- a/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs
+++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs
@@ -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
});
diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs
index c4828d8..ae10249 100644
--- a/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs
+++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderParcelBuilder.cs
@@ -306,7 +306,10 @@ public static class PlatformStoreOrderParcelBuilder
private static OrderParcelReceiverInput? ParseReceiverNode(JsonNode node)
{
var candidates = new List { 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;
}
+ ///
+ /// 解析 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)