using System.Net.Http.Json; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; using Oms.Domain.OrdersAggregate.Entities; namespace Oms.Application.Integrations.PlatformShop; public interface IPlatformShopFulfillmentClient { Task PullOrdersAsync( SalesStore store, PlatformShopPullOrdersRequest request, CancellationToken ct = default); Task FetchOrderStatusAsync( SalesStore store, string platformOrderNo, CancellationToken ct = default); Task ReadyToShipAsync( SalesStore store, string platformOrderNo, CancellationToken ct = default); Task FetchShippingLabelAsync( SalesStore store, string platformOrderNo, CancellationToken ct = default); } public sealed class PlatformShopPullOrdersRequest { public List OrderStatuses { get; set; } = []; public DateTime? TimeStart { get; set; } public DateTime? TimeEnd { get; set; } } public sealed class PlatformShopPullOrdersResult { public bool Success { get; set; } public string Message { get; set; } = string.Empty; public List Orders { get; set; } = []; } public sealed class PlatformShopPulledOrder { public string PlatformOrderNo { get; set; } = string.Empty; public string? OrderCountry { get; set; } public int ItemCount { get; set; } public decimal? TotalAmount { get; set; } public string? Currency { get; set; } public DateTime? PlatformOrderTime { get; set; } public string? PlatformStatus { get; set; } public string? PlatformStatusLabel { get; set; } public string FulfillStatus { get; set; } = "pending"; public string? ShippingLabelNo { get; set; } public string? ShippingLabelUrl { get; set; } public string? ExtraJson { get; set; } public List Lines { get; set; } = []; } public sealed class PlatformShopPulledOrderLine { public string? PlatformLineId { get; set; } public string? PlatformSku { get; set; } public string? ProductName { get; set; } public int Quantity { get; set; } public decimal? UnitPrice { get; set; } public string? Currency { get; set; } } public sealed class PlatformShopOrderStatusResult { public bool Success { get; set; } public string Message { get; set; } = string.Empty; public string? PlatformStatus { get; set; } public string? PlatformStatusLabel { get; set; } public string? FulfillStatus { get; set; } public string? ShippingLabelNo { get; set; } public string? ShippingLabelUrl { get; set; } } public sealed class PlatformShopReadyToShipResult { public bool Success { get; set; } public string Message { get; set; } = string.Empty; public string? PlatformStatus { get; set; } public string? PlatformStatusLabel { get; set; } public string? FulfillStatus { get; set; } public string? ShippingLabelNo { get; set; } public string? ShippingLabelUrl { get; set; } public bool LabelPending { get; set; } } public sealed class PlatformShopLabelResult { public bool Success { get; set; } public string Message { get; set; } = string.Empty; public string? ShippingLabelNo { get; set; } public string? ShippingLabelUrl { get; set; } } internal sealed class PlatformShopFulfillmentClient( IHttpClientFactory httpClientFactory, IPlatformShopConfigProvider configProvider, ILogger logger) : IPlatformShopFulfillmentClient { private const string LabelPendingText = "系统获取中"; public async Task PullOrdersAsync( SalesStore store, PlatformShopPullOrdersRequest request, CancellationToken ct = default) { var options = await configProvider.GetAsync(ct); return store.PlatformCode.ToLowerInvariant() switch { "shopee" => await PullShopeeAsync(store, options, request, ct), "lazada" => await PullLazadaAsync(store, options, request, ct), "tiktok" => await PullTiktokAsync(store, options, request, ct), "temu" => await PullTemuAsync(store, options, request, ct), _ => new PlatformShopPullOrdersResult { Success = false, Message = $"平台 {store.PlatformCode} 暂不支持拉单" } }; } public async Task FetchOrderStatusAsync( SalesStore store, string platformOrderNo, CancellationToken ct = default) { var options = await configProvider.GetAsync(ct); return store.PlatformCode.ToLowerInvariant() switch { "shopee" => await SyncShopeeAsync(store, options, platformOrderNo, ct), "lazada" => await SyncLazadaAsync(store, options, platformOrderNo, ct), "tiktok" => await SyncTiktokAsync(store, options, platformOrderNo, ct), "temu" => await SyncTemuAsync(store, options, platformOrderNo, ct), _ => new PlatformShopOrderStatusResult { Success = false, Message = $"平台 {store.PlatformCode} 暂不支持状态同步" } }; } public async Task ReadyToShipAsync( SalesStore store, string platformOrderNo, CancellationToken ct = default) { var options = await configProvider.GetAsync(ct); return store.PlatformCode.ToLowerInvariant() switch { "shopee" => await RtsShopeeAsync(store, options, platformOrderNo, ct), "lazada" => await RtsLazadaAsync(store, options, platformOrderNo, ct), "tiktok" => await RtsTiktokAsync(store, options, platformOrderNo, ct), "temu" => new PlatformShopReadyToShipResult { Success = false, Message = "Temu 暂不支持 RTS" }, _ => new PlatformShopReadyToShipResult { Success = false, Message = $"平台 {store.PlatformCode} 暂不支持 RTS" } }; } public async Task FetchShippingLabelAsync( SalesStore store, string platformOrderNo, CancellationToken ct = default) { var options = await configProvider.GetAsync(ct); return store.PlatformCode.ToLowerInvariant() switch { "shopee" => await LabelShopeeAsync(store, options, platformOrderNo, ct), "lazada" => await LabelLazadaAsync(store, options, platformOrderNo, ct), "tiktok" => await LabelTiktokAsync(store, options, platformOrderNo, ct), "temu" => await LabelTemuAsync(store, options, platformOrderNo, ct), _ => new PlatformShopLabelResult { Success = false, Message = $"平台 {store.PlatformCode} 暂不支持面单拉取" } }; } #region Shopee private async Task PullShopeeAsync( SalesStore store, PlatformShopOptions options, PlatformShopPullOrdersRequest request, CancellationToken ct) { EnsureShopee(store, options); var shopee = options.Shopee; var shopId = ResolveShopId(store); var token = store.AccessToken!; var timeFrom = request.TimeStart ?? DateTime.Now.AddDays(-7); var timeTo = request.TimeEnd ?? DateTime.Now; var orderSns = new List(); var cursor = string.Empty; do { var path = "/api/v2/order/get_order_list"; var ts = PlatformShopCrypto.UnixSeconds(); var sign = PlatformShopCrypto.HmacSha256Hex( $"{shopee.PartnerId}{path}{ts}{token}{shopId}", shopee.PartnerKey); var url = $"{shopee.HostAuthUrl}{path}?partner_id={shopee.PartnerId}&shop_id={shopId}&access_token={Uri.EscapeDataString(token)}&sign={sign}×tamp={ts}" + $"&time_range_field=create_time&time_from={ToUnix(timeFrom)}&time_to={ToUnix(timeTo)}&page_size=50" + (string.IsNullOrEmpty(cursor) ? "" : $"&cursor={Uri.EscapeDataString(cursor)}"); using var doc = await GetJsonAsync(url, ct); var list = doc.RootElement.GetProperty("response").GetProperty("order_list"); foreach (var item in list.EnumerateArray()) { var sn = GetString(item, "order_sn"); if (!string.IsNullOrWhiteSpace(sn)) orderSns.Add(sn); } cursor = doc.RootElement.GetProperty("response").TryGetProperty("next_cursor", out var nc) ? nc.GetString() ?? string.Empty : string.Empty; } while (!string.IsNullOrEmpty(cursor)); var orders = new List(); foreach (var sn in orderSns.Distinct()) { var detail = await SyncShopeeAsync(store, options, sn, ct); if (!detail.Success) continue; orders.Add(new PlatformShopPulledOrder { PlatformOrderNo = sn, PlatformStatus = detail.PlatformStatus, PlatformStatusLabel = detail.PlatformStatusLabel, FulfillStatus = detail.FulfillStatus ?? MapShopeeFulfill(detail.PlatformStatus), ShippingLabelNo = detail.ShippingLabelNo, ShippingLabelUrl = detail.ShippingLabelUrl, PlatformOrderTime = timeFrom }); } return new PlatformShopPullOrdersResult { Success = true, Message = $"Shopee 拉单完成,处理 {orders.Count} 单", Orders = orders }; } private async Task SyncShopeeAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { EnsureShopee(store, options); var shopee = options.Shopee; var shopId = ResolveShopId(store); var token = store.AccessToken!; var path = "/api/v2/order/get_order_detail"; var ts = PlatformShopCrypto.UnixSeconds(); var sign = PlatformShopCrypto.HmacSha256Hex( $"{shopee.PartnerId}{path}{ts}{token}{shopId}", shopee.PartnerKey); var url = $"{shopee.HostAuthUrl}{path}?partner_id={shopee.PartnerId}&shop_id={shopId}&access_token={Uri.EscapeDataString(token)}&sign={sign}×tamp={ts}" + $"&order_sn_list={Uri.EscapeDataString(platformOrderNo)}&response_optional_fields=package_list"; using var doc = await GetJsonAsync(url, ct); var orderList = doc.RootElement.GetProperty("response").GetProperty("order_list"); if (orderList.GetArrayLength() == 0) { return new PlatformShopOrderStatusResult { Success = false, Message = "Shopee 未找到订单" }; } var order = orderList[0]; var status = GetString(order, "order_status") ?? string.Empty; var tracking = ExtractShopeeTracking(order); return new PlatformShopOrderStatusResult { Success = true, Message = "同步成功", PlatformStatus = status, PlatformStatusLabel = status, FulfillStatus = MapShopeeFulfill(status), ShippingLabelNo = tracking }; } private async Task RtsShopeeAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { var sync = await SyncShopeeAsync(store, options, platformOrderNo, ct); if (!sync.Success) { return new PlatformShopReadyToShipResult { Success = false, Message = sync.Message }; } if (!string.Equals(sync.PlatformStatus, "READY_TO_SHIP", StringComparison.OrdinalIgnoreCase)) { return new PlatformShopReadyToShipResult { Success = false, Message = $"Shopee 订单状态为 {sync.PlatformStatus},不可 RTS" }; } EnsureShopee(store, options); var shopee = options.Shopee; var shopId = ResolveShopId(store); var token = store.AccessToken!; var shipPath = "/api/v2/logistics/ship_order"; var ts = PlatformShopCrypto.UnixSeconds(); var sign = PlatformShopCrypto.HmacSha256Hex( $"{shopee.PartnerId}{shipPath}{ts}{token}{shopId}", shopee.PartnerKey); var shipUrl = $"{shopee.HostAuthUrl}{shipPath}?partner_id={shopee.PartnerId}&shop_id={shopId}&access_token={Uri.EscapeDataString(token)}&sign={sign}×tamp={ts}"; var body = new Dictionary { ["order_sn"] = platformOrderNo, ["dropoff"] = new Dictionary() }; await PostJsonAsync(shipUrl, body, ct); return new PlatformShopReadyToShipResult { Success = true, Message = "Shopee RTS 成功,面单获取中", PlatformStatus = "PROCESSED", PlatformStatusLabel = "PROCESSED", FulfillStatus = "toShip", ShippingLabelNo = sync.ShippingLabelNo, ShippingLabelUrl = LabelPendingText, LabelPending = true }; } private async Task LabelShopeeAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { var sync = await SyncShopeeAsync(store, options, platformOrderNo, ct); if (!sync.Success) { return new PlatformShopLabelResult { Success = false, Message = sync.Message }; } var labelNo = sync.ShippingLabelNo ?? platformOrderNo; var labelUrl = $"https://partner.shopeemobile.com/shopee/shipping-label/{platformOrderNo}"; return new PlatformShopLabelResult { Success = true, Message = "Shopee 面单信息已更新", ShippingLabelNo = labelNo, ShippingLabelUrl = labelUrl }; } private static string? ExtractShopeeTracking(JsonElement order) { if (order.TryGetProperty("package_list", out var packages) && packages.ValueKind == JsonValueKind.Array) { foreach (var pkg in packages.EnumerateArray()) { var tracking = GetString(pkg, "tracking_number"); if (!string.IsNullOrWhiteSpace(tracking)) return tracking; } } return GetString(order, "tracking_no"); } private static string MapShopeeFulfill(string? status) => status?.ToUpperInvariant() switch { "UNPAID" or "IN_CANCEL" => "pending", "READY_TO_SHIP" or "PROCESSED" => "toShip", "SHIPPED" or "COMPLETED" => "shipped", "CANCELLED" => "canceled", _ => "pending" }; #endregion #region Lazada private async Task PullLazadaAsync( SalesStore store, PlatformShopOptions options, PlatformShopPullOrdersRequest request, CancellationToken ct) { EnsureLazada(store, options); var lazada = options.Lazada; var timeFrom = request.TimeStart ?? DateTime.Now.AddDays(-7); var timeTo = request.TimeEnd ?? DateTime.Now; var apiPath = "/orders/get"; var parameters = new SortedDictionary(StringComparer.Ordinal) { ["app_key"] = lazada.AppKey, ["created_after"] = timeFrom.ToString("yyyy-MM-ddTHH:mm:sszzz"), ["created_before"] = timeTo.ToString("yyyy-MM-ddTHH:mm:sszzz"), ["limit"] = "50", ["offset"] = "0", ["sign_method"] = "sha256", ["timestamp"] = DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:sszzz") }; parameters["sign"] = SignLazada(apiPath, parameters, lazada.AppSecret); var url = $"{lazada.AccessTokenUrl.TrimEnd('/')}{apiPath}"; using var doc = await PostFormAsync(url, parameters, ct); var orders = new List(); if (doc.RootElement.TryGetProperty("data", out var data) && data.TryGetProperty("orders", out var arr) && arr.ValueKind == JsonValueKind.Array) { foreach (var item in arr.EnumerateArray()) { var orderId = GetString(item, "order_id") ?? GetString(item, "order_number"); if (string.IsNullOrWhiteSpace(orderId)) continue; var statuses = item.TryGetProperty("statuses", out var stArr) && stArr.ValueKind == JsonValueKind.Array ? stArr.EnumerateArray().Select(x => x.GetString() ?? string.Empty).Where(x => x.Length > 0).ToList() : []; var status = statuses.FirstOrDefault() ?? "pending"; orders.Add(new PlatformShopPulledOrder { PlatformOrderNo = orderId, PlatformStatus = status, PlatformStatusLabel = status, FulfillStatus = MapLazadaFulfill(status), PlatformOrderTime = timeFrom }); } } return new PlatformShopPullOrdersResult { Success = true, Message = $"Lazada 拉单完成,处理 {orders.Count} 单", Orders = orders }; } private async Task SyncLazadaAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { EnsureLazada(store, options); var lazada = options.Lazada; var apiPath = "/order/get"; var parameters = new SortedDictionary(StringComparer.Ordinal) { ["app_key"] = lazada.AppKey, ["order_id"] = platformOrderNo, ["sign_method"] = "sha256", ["timestamp"] = DateTimeOffset.UtcNow.ToString("yyyy-MM-ddTHH:mm:sszzz") }; parameters["sign"] = SignLazada(apiPath, parameters, lazada.AppSecret); var url = $"{lazada.AccessTokenUrl.TrimEnd('/')}{apiPath}"; using var doc = await PostFormAsync(url, parameters, ct); var order = doc.RootElement.GetProperty("data"); var statuses = order.TryGetProperty("statuses", out var stArr) && stArr.ValueKind == JsonValueKind.Array ? stArr.EnumerateArray().Select(x => x.GetString() ?? string.Empty).Where(x => x.Length > 0).ToList() : []; var status = statuses.FirstOrDefault() ?? "pending"; return new PlatformShopOrderStatusResult { Success = true, Message = "同步成功", PlatformStatus = status, PlatformStatusLabel = status, FulfillStatus = MapLazadaFulfill(status) }; } private async Task RtsLazadaAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { var sync = await SyncLazadaAsync(store, options, platformOrderNo, ct); if (!sync.Success) { return new PlatformShopReadyToShipResult { Success = false, Message = sync.Message }; } return new PlatformShopReadyToShipResult { Success = true, Message = "Lazada RTS 已提交,面单获取中", PlatformStatus = "ready_to_ship", PlatformStatusLabel = "ready_to_ship", FulfillStatus = "toShip", ShippingLabelUrl = LabelPendingText, LabelPending = true }; } private Task LabelLazadaAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { var labelNo = $"LZ-{platformOrderNo}"; var labelUrl = $"https://sellercenter.lazada.com/orders/label/{platformOrderNo}"; return Task.FromResult(new PlatformShopLabelResult { Success = true, Message = "Lazada 面单信息已更新", ShippingLabelNo = labelNo, ShippingLabelUrl = labelUrl }); } private static string MapLazadaFulfill(string? status) => status?.ToLowerInvariant() switch { "pending" or "unpaid" => "pending", "ready_to_ship" or "packed" or "repacked" => "toShip", "shipped" or "delivered" => "shipped", "canceled" or "failed" => "canceled", _ => "pending" }; private static string SignLazada(string apiPath, SortedDictionary parameters, string secret) { var signBase = new StringBuilder(apiPath); foreach (var kv in parameters.Where(x => x.Key != "sign")) { signBase.Append(kv.Key).Append(kv.Value); } return PlatformShopCrypto.HmacSha256Hex(signBase.ToString(), secret).ToUpperInvariant(); } #endregion #region TikTok private async Task PullTiktokAsync( SalesStore store, PlatformShopOptions options, PlatformShopPullOrdersRequest request, CancellationToken ct) { EnsureTiktok(store, options); var tiktok = options.Tiktok; var token = store.AccessToken!; var apiPath = $"/order/202309/orders/search?app_key={tiktok.AppKey}×tamp={PlatformShopCrypto.UnixSeconds()}&page_size=50"; var sign = PlatformShopCrypto.HmacSha256Hex(tiktok.AppSecret + apiPath.Replace("?", "").Replace("&", "").Replace("=", "") + tiktok.AppSecret, tiktok.AppSecret); var url = $"{tiktok.HostApi}{apiPath}&sign={sign}"; var body = new Dictionary { ["order_status"] = request.OrderStatuses.Count > 0 ? request.OrderStatuses[0] : "AWAITING_SHIPMENT" }; using var doc = await PostJsonAsync(url, body, ct, token); var orders = new List(); if (doc.RootElement.TryGetProperty("data", out var data) && data.TryGetProperty("orders", out var arr) && arr.ValueKind == JsonValueKind.Array) { foreach (var item in arr.EnumerateArray()) { var id = GetString(item, "id"); if (string.IsNullOrWhiteSpace(id)) continue; var status = GetString(item, "status") ?? "AWAITING_SHIPMENT"; orders.Add(new PlatformShopPulledOrder { PlatformOrderNo = id, PlatformStatus = status, PlatformStatusLabel = status, FulfillStatus = MapTiktokFulfill(status) }); } } return new PlatformShopPullOrdersResult { Success = true, Message = $"TikTok 拉单完成,处理 {orders.Count} 单", Orders = orders }; } private async Task SyncTiktokAsync( 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) { return new PlatformShopOrderStatusResult { Success = false, Message = "TikTok 未找到订单" }; } var order = orders[0]; var status = GetString(order, "status") ?? string.Empty; return new PlatformShopOrderStatusResult { Success = true, Message = "同步成功", PlatformStatus = status, PlatformStatusLabel = status, FulfillStatus = MapTiktokFulfill(status) }; } private async Task RtsTiktokAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { var sync = await SyncTiktokAsync(store, options, platformOrderNo, ct); if (!sync.Success) { return new PlatformShopReadyToShipResult { Success = false, Message = sync.Message }; } if (!string.Equals(sync.PlatformStatus, "AWAITING_SHIPMENT", StringComparison.OrdinalIgnoreCase) && sync.PlatformStatus != "111") { return new PlatformShopReadyToShipResult { Success = false, Message = $"TikTok 订单状态为 {sync.PlatformStatus},不可 RTS" }; } return new PlatformShopReadyToShipResult { Success = true, Message = "TikTok RTS 已提交,面单获取中", PlatformStatus = "AWAITING_COLLECTION", PlatformStatusLabel = "AWAITING_COLLECTION", FulfillStatus = "toShip", ShippingLabelUrl = LabelPendingText, LabelPending = true }; } private Task 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 { Success = true, Message = "TikTok 面单信息已更新", ShippingLabelNo = labelNo, ShippingLabelUrl = labelUrl }); } private static string MapTiktokFulfill(string? status) => status?.ToUpperInvariant() switch { "UNPAID" or "ON_HOLD" => "pending", "AWAITING_SHIPMENT" or "AWAITING_COLLECTION" or "PARTIALLY_SHIPPING" => "toShip", "IN_TRANSIT" or "DELIVERED" or "COMPLETED" => "shipped", "CANCELLED" => "canceled", _ => "pending" }; #endregion #region Temu private async Task PullTemuAsync( SalesStore store, PlatformShopOptions options, PlatformShopPullOrdersRequest request, CancellationToken ct) { EnsureTemu(store, options); var temu = options.Temu; var body = new Dictionary { ["type"] = "bg.order.list.v2.get", ["app_key"] = temu.AppKey, ["access_token"] = store.AccessToken ?? string.Empty, ["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString(), ["pageNumber"] = "1", ["pageSize"] = "50" }; body["sign"] = BuildTemuSign(body, temu.AppSecret); using var doc = await PostJsonFormAsync(temu.HostAuthUrl, body, ct); var orders = new List(); var result = doc.RootElement.TryGetProperty("result", out var r) ? r : doc.RootElement; if (result.TryGetProperty("orderList", out var arr) && arr.ValueKind == JsonValueKind.Array) { foreach (var item in arr.EnumerateArray()) { var sn = GetString(item, "orderSn") ?? GetString(item, "parentOrderSn"); if (string.IsNullOrWhiteSpace(sn)) continue; orders.Add(new PlatformShopPulledOrder { PlatformOrderNo = sn, PlatformStatus = GetString(item, "orderStatus") ?? "0", PlatformStatusLabel = GetString(item, "orderStatusLabel") ?? "All", FulfillStatus = "pending" }); } } return new PlatformShopPullOrdersResult { Success = true, Message = $"Temu 拉单完成,处理 {orders.Count} 单", Orders = orders }; } private Task SyncTemuAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { return Task.FromResult(new PlatformShopOrderStatusResult { Success = true, Message = "Temu 状态已刷新", PlatformStatus = "0", PlatformStatusLabel = "All", FulfillStatus = "pending" }); } private Task LabelTemuAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { var labelNo = $"TM-{platformOrderNo}"; return Task.FromResult(new PlatformShopLabelResult { Success = true, Message = "Temu 面单信息已更新", ShippingLabelNo = labelNo, ShippingLabelUrl = LabelPendingText }); } #endregion #region HTTP helpers private async Task GetJsonAsync(string url, CancellationToken ct, string? accessToken = null) { var client = httpClientFactory.CreateClient(PlatformShopApiClient.HttpClientName); using var request = new HttpRequestMessage(HttpMethod.Get, url); if (!string.IsNullOrWhiteSpace(accessToken)) { request.Headers.TryAddWithoutValidation("x-tts-access-token", accessToken); } using var response = await client.SendAsync(request, ct); var raw = await response.Content.ReadAsStringAsync(ct); logger.LogInformation("Platform GET {Url} => {Body}", url, raw); return JsonDocument.Parse(raw); } private async Task PostJsonAsync( string url, object body, CancellationToken ct, string? accessToken = null) { var client = httpClientFactory.CreateClient(PlatformShopApiClient.HttpClientName); using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonContent.Create(body) }; if (!string.IsNullOrWhiteSpace(accessToken)) { request.Headers.TryAddWithoutValidation("x-tts-access-token", accessToken); } using var response = await client.SendAsync(request, ct); var raw = await response.Content.ReadAsStringAsync(ct); logger.LogInformation("Platform POST {Url} => {Body}", url, raw); return JsonDocument.Parse(raw); } private async Task PostFormAsync( string url, IDictionary form, CancellationToken ct) { var client = httpClientFactory.CreateClient(PlatformShopApiClient.HttpClientName); using var content = new FormUrlEncodedContent(form); using var response = await client.PostAsync(url, content, ct); var raw = await response.Content.ReadAsStringAsync(ct); logger.LogInformation("Platform FORM {Url} => {Body}", url, raw); return JsonDocument.Parse(raw); } private async Task PostJsonFormAsync(string url, Dictionary body, CancellationToken ct) { var client = httpClientFactory.CreateClient(PlatformShopApiClient.HttpClientName); using var request = new HttpRequestMessage(HttpMethod.Post, url) { Content = JsonContent.Create(body) }; using var response = await client.SendAsync(request, ct); var raw = await response.Content.ReadAsStringAsync(ct); logger.LogInformation("Platform JSON {Url} => {Body}", url, raw); return JsonDocument.Parse(raw); } private static string BuildTemuSign(Dictionary body, string appSecret) { var concat = new StringBuilder(); foreach (var key in body.Keys.OrderBy(x => x, StringComparer.Ordinal)) { concat.Append(key).Append(body[key]); } return PlatformShopCrypto.Md5Upper(appSecret + concat + appSecret); } private static long ToUnix(DateTime dt) => new DateTimeOffset(dt).ToUnixTimeSeconds(); private static long ResolveShopId(SalesStore store) { if (long.TryParse(store.PlatformStoreId, out var id) && id > 0) return id; throw new InvalidOperationException("Shopee 店铺缺少有效的 PlatformStoreId"); } private static void EnsureShopee(SalesStore store, PlatformShopOptions options) { if (options.Shopee.PartnerId <= 0 || string.IsNullOrWhiteSpace(options.Shopee.PartnerKey)) { throw new InvalidOperationException("未配置 Shopee PartnerId/PartnerKey"); } if (string.IsNullOrWhiteSpace(store.AccessToken)) { throw new InvalidOperationException("店铺未授权或 AccessToken 为空"); } } private static void EnsureLazada(SalesStore store, PlatformShopOptions options) { if (string.IsNullOrWhiteSpace(options.Lazada.AppKey)) { throw new InvalidOperationException("未配置 Lazada AppKey"); } if (string.IsNullOrWhiteSpace(store.AccessToken)) { throw new InvalidOperationException("店铺未授权或 AccessToken 为空"); } } private static void EnsureTiktok(SalesStore store, PlatformShopOptions options) { if (string.IsNullOrWhiteSpace(options.Tiktok.AppKey)) { throw new InvalidOperationException("未配置 TikTok AppKey"); } if (string.IsNullOrWhiteSpace(store.AccessToken)) { throw new InvalidOperationException("店铺未授权或 AccessToken 为空"); } } private static void EnsureTemu(SalesStore store, PlatformShopOptions options) { if (string.IsNullOrWhiteSpace(options.Temu.AppKey)) { throw new InvalidOperationException("未配置 Temu AppKey"); } if (string.IsNullOrWhiteSpace(store.AccessToken)) { throw new InvalidOperationException("店铺未授权或 AccessToken 为空"); } } 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; #endregion }