admin/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs
zzh 89233bc3da feat: add customer marketing module (M1-M6 backend)
Implement referral, points, coupons, commission, member levels, and order integration with migration SQL and background jobs.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-23 16:42:08 +08:00

941 lines
34 KiB
C#

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<PlatformShopPullOrdersResult> PullOrdersAsync(
SalesStore store,
PlatformShopPullOrdersRequest request,
CancellationToken ct = default);
Task<PlatformShopOrderStatusResult> FetchOrderStatusAsync(
SalesStore store,
string platformOrderNo,
CancellationToken ct = default);
Task<PlatformShopReadyToShipResult> ReadyToShipAsync(
SalesStore store,
string platformOrderNo,
CancellationToken ct = default);
Task<PlatformShopLabelResult> FetchShippingLabelAsync(
SalesStore store,
string platformOrderNo,
CancellationToken ct = default);
}
public sealed class PlatformShopPullOrdersRequest
{
public List<string> 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<PlatformShopPulledOrder> 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<PlatformShopPulledOrderLine> 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<PlatformShopFulfillmentClient> logger) : IPlatformShopFulfillmentClient
{
private const string LabelPendingText = "系统获取中";
public async Task<PlatformShopPullOrdersResult> 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<PlatformShopOrderStatusResult> 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<PlatformShopReadyToShipResult> 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<PlatformShopLabelResult> 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<PlatformShopPullOrdersResult> 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<string>();
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}&timestamp={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<PlatformShopPulledOrder>();
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<PlatformShopOrderStatusResult> 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}&timestamp={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<PlatformShopReadyToShipResult> 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}&timestamp={ts}";
var body = new Dictionary<string, object?>
{
["order_sn"] = platformOrderNo,
["dropoff"] = new Dictionary<string, object?>()
};
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<PlatformShopLabelResult> 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<PlatformShopPullOrdersResult> 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<string, string>(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<PlatformShopPulledOrder>();
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<PlatformShopOrderStatusResult> SyncLazadaAsync(
SalesStore store,
PlatformShopOptions options,
string platformOrderNo,
CancellationToken ct)
{
EnsureLazada(store, options);
var lazada = options.Lazada;
var apiPath = "/order/get";
var parameters = new SortedDictionary<string, string>(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<PlatformShopReadyToShipResult> 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<PlatformShopLabelResult> 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<string, string> 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<PlatformShopPullOrdersResult> 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}&timestamp={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<string, object?>
{
["order_status"] = request.OrderStatuses.Count > 0 ? request.OrderStatuses[0] : "AWAITING_SHIPMENT"
};
using var doc = await PostJsonAsync(url, body, ct, token);
var orders = new List<PlatformShopPulledOrder>();
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<PlatformShopOrderStatusResult> 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}&timestamp={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<PlatformShopReadyToShipResult> 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<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
{
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<PlatformShopPullOrdersResult> PullTemuAsync(
SalesStore store,
PlatformShopOptions options,
PlatformShopPullOrdersRequest request,
CancellationToken ct)
{
EnsureTemu(store, options);
var temu = options.Temu;
var body = new Dictionary<string, string>
{
["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<PlatformShopPulledOrder>();
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<PlatformShopOrderStatusResult> 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<PlatformShopLabelResult> 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<JsonDocument> 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<JsonDocument> 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<JsonDocument> PostFormAsync(
string url,
IDictionary<string, string> 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<JsonDocument> PostJsonFormAsync(string url, Dictionary<string, string> 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<string, string> 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
}