1.06 #10
@ -1,7 +1,9 @@
|
|||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Fake.ObjectStorage;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Oms.Application.Storage;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
|
|
||||||
namespace Oms.Application.Integrations.PlatformShop;
|
namespace Oms.Application.Integrations.PlatformShop;
|
||||||
@ -141,6 +143,7 @@ public sealed class PlatformShopLabelResult
|
|||||||
internal sealed class PlatformShopFulfillmentClient(
|
internal sealed class PlatformShopFulfillmentClient(
|
||||||
IHttpClientFactory httpClientFactory,
|
IHttpClientFactory httpClientFactory,
|
||||||
IPlatformShopConfigProvider configProvider,
|
IPlatformShopConfigProvider configProvider,
|
||||||
|
IObjectStorage objectStorage,
|
||||||
ILogger<PlatformShopFulfillmentClient> logger) : IPlatformShopFulfillmentClient
|
ILogger<PlatformShopFulfillmentClient> logger) : IPlatformShopFulfillmentClient
|
||||||
{
|
{
|
||||||
private const string LabelPendingText = "系统获取中";
|
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,
|
SalesStore store,
|
||||||
PlatformShopOptions options,
|
PlatformShopOptions options,
|
||||||
string platformOrderNo,
|
string platformOrderNo,
|
||||||
CancellationToken ct)
|
CancellationToken ct)
|
||||||
{
|
{
|
||||||
var labelNo = $"TT-{platformOrderNo}";
|
EnsureTiktok(store, options);
|
||||||
var labelUrl = $"https://seller.tiktokglobalshop.com/order/label/{platformOrderNo}";
|
var shopCipher = await GetTiktokShopCipherAsync(store, options, ct);
|
||||||
return Task.FromResult(new PlatformShopLabelResult
|
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,
|
Success = true,
|
||||||
Message = "TikTok 面单信息已更新",
|
Message = "面单获取成功",
|
||||||
ShippingLabelNo = labelNo,
|
ShippingLabelNo = string.IsNullOrWhiteSpace(trackingNumber) ? null : trackingNumber,
|
||||||
ShippingLabelUrl = labelUrl
|
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
|
private static string MapTiktokFulfill(string? status) => status?.ToUpperInvariant() switch
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user