fix: TikTok 重新获取面单改为按包裹下载发货文档
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-16 16:15:51 +08:00
parent 8f64bbf02d
commit a1a7524a0e

View File

@ -1,7 +1,9 @@
using System.Net.Http.Json;
using System.Text;
using System.Text.Json;
using Fake.ObjectStorage;
using Microsoft.Extensions.Logging;
using Oms.Application.Storage;
using Oms.Domain.OrdersAggregate.Entities;
namespace Oms.Application.Integrations.PlatformShop;
@ -141,6 +143,7 @@ public sealed class PlatformShopLabelResult
internal sealed class PlatformShopFulfillmentClient(
IHttpClientFactory httpClientFactory,
IPlatformShopConfigProvider configProvider,
IObjectStorage objectStorage,
ILogger<PlatformShopFulfillmentClient> logger) : IPlatformShopFulfillmentClient
{
private const string LabelPendingText = "系统获取中";
@ -853,21 +856,161 @@ internal sealed class PlatformShopFulfillmentClient(
};
}
private Task<PlatformShopLabelResult> LabelTiktokAsync(
/// <summary>
/// 对齐 WMS ShopordTiktokImpl.retryOrderSheet:订单详情取 package_id,再取 SHIPPING_LABEL 并落库。
/// </summary>
private async Task<PlatformShopLabelResult> LabelTiktokAsync(
SalesStore store,
PlatformShopOptions options,
string platformOrderNo,
CancellationToken ct)
{
var labelNo = $"TT-{platformOrderNo}";
var labelUrl = $"https://seller.tiktokglobalshop.com/order/label/{platformOrderNo}";
return Task.FromResult(new PlatformShopLabelResult
EnsureTiktok(store, options);
var shopCipher = await GetTiktokShopCipherAsync(store, options, ct);
if (string.IsNullOrWhiteSpace(shopCipher))
{
return new PlatformShopLabelResult { Success = false, Message = "未获取到 TikTok shop_cipher,请重新授权店铺" };
}
var tiktok = options.Tiktok;
using var detail = await GetTiktokSignedAsync(options, store.AccessToken!, "/order/202309/orders", new Dictionary<string, string>
{
["app_key"] = tiktok.AppKey,
["shop_cipher"] = shopCipher,
["ids"] = platformOrderNo,
["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString()
}, ct);
if (!TryGetTiktokData(detail.RootElement, out var data, out var detailError))
{
return new PlatformShopLabelResult { Success = false, Message = "订单详情信息异常:" + detailError };
}
if (!data.TryGetProperty("orders", out var orders)
|| orders.ValueKind != JsonValueKind.Array
|| orders.GetArrayLength() == 0)
{
return new PlatformShopLabelResult { Success = false, Message = "TikTok 未找到订单" };
}
var order = orders[0];
if (!order.TryGetProperty("line_items", out var lines)
|| lines.ValueKind != JsonValueKind.Array
|| lines.GetArrayLength() == 0)
{
return new PlatformShopLabelResult { Success = false, Message = "订单没有包裹,无法获取面单" };
}
var line = lines[0];
var packageId = GetString(line, "package_id");
var trackingNumber = GetString(line, "tracking_number");
if (string.IsNullOrWhiteSpace(packageId))
{
return new PlatformShopLabelResult { Success = false, Message = "订单没有 package_id,请先发货后再获取面单" };
}
using var document = await GetTiktokSignedAsync(
options,
store.AccessToken!,
$"/fulfillment/202309/packages/{packageId}/shipping_documents",
new Dictionary<string, string>
{
["app_key"] = tiktok.AppKey,
["shop_cipher"] = shopCipher,
["document_type"] = "SHIPPING_LABEL",
["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString()
},
ct);
if (!TryGetTiktokData(document.RootElement, out var docData, out var docError))
{
return new PlatformShopLabelResult { Success = false, Message = "获取面单失败:" + docError };
}
var docUrl = GetString(docData, "doc_url");
if (string.IsNullOrWhiteSpace(docUrl) || !docUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
return new PlatformShopLabelResult
{
Success = false,
Message = string.IsNullOrWhiteSpace(docUrl) ? "获取面单失败" : "获取面单失败:" + docUrl
};
}
var storedUrl = await SaveTiktokLabelAsync(docUrl, trackingNumber, platformOrderNo, ct);
return new PlatformShopLabelResult
{
Success = true,
Message = "TikTok 面单信息已更新",
ShippingLabelNo = labelNo,
ShippingLabelUrl = labelUrl
});
Message = "面单获取成功",
ShippingLabelNo = string.IsNullOrWhiteSpace(trackingNumber) ? null : trackingNumber,
ShippingLabelUrl = storedUrl
};
}
/// <summary>
/// 下载 TikTok 面单 PDF 并写入对象存储。下载失败时退回平台临时地址。
/// </summary>
private async Task<string> SaveTiktokLabelAsync(
string docUrl,
string? trackingNumber,
string platformOrderNo,
CancellationToken ct)
{
try
{
var client = httpClientFactory.CreateClient(PlatformShopApiClient.HttpClientName);
var bytes = await client.GetByteArrayAsync(docUrl, ct);
if (bytes.Length == 0)
{
return docUrl;
}
var name = string.IsNullOrWhiteSpace(trackingNumber) ? platformOrderNo : trackingNumber;
var key = $"uploads/platform-label/{DateTime.Now:yyyyMM}/md_{name}_{Guid.NewGuid():N}.pdf";
await using var stream = new MemoryStream(bytes);
var objectKey = await objectStorage.SaveObjectAsync(stream, key, "application/pdf", ct);
var publicUrl = await objectStorage.GetPublicUrlAsync(objectKey, ct);
return publicUrl.StartsWith("http", StringComparison.OrdinalIgnoreCase) ? publicUrl : docUrl;
}
catch (Exception ex)
{
logger.LogWarning(ex, "TikTok 面单下载落库失败,改用平台地址");
return docUrl;
}
}
private async Task<JsonDocument> GetTiktokSignedAsync(
PlatformShopOptions options,
string accessToken,
string path,
Dictionary<string, string> query,
CancellationToken ct)
{
var sign = PlatformShopCrypto.TiktokSign(path, query, null, options.Tiktok.AppSecret);
var url = $"{options.Tiktok.HostApi}{path}?{BuildTiktokQuery(query, sign)}";
return await GetJsonAsync(url, ct, accessToken);
}
private static bool TryGetTiktokData(JsonElement root, out JsonElement data, out string error)
{
data = default;
error = "返回为空";
if (root.ValueKind != JsonValueKind.Object)
{
return false;
}
var code = GetString(root, "code");
error = GetString(root, "message") ?? GetString(root, "msg") ?? error;
if (code is not ("0" or null))
{
return false;
}
if (!root.TryGetProperty("data", out data) || data.ValueKind != JsonValueKind.Object)
{
return false;
}
return true;
}
private static string MapTiktokFulfill(string? status) => status?.ToUpperInvariant() switch