From 9a48ec1a633186d3ba5281f622bbe66023469915 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Thu, 16 Jul 2026 11:38:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BA=97=E9=93=BA=E8=AE=A2=E5=8D=95?= =?UTF-8?q?=E6=8B=89=E5=8D=95=E5=90=8C=E6=AD=A5=E3=80=81RTS=20=E4=B8=8E?= =?UTF-8?q?=E9=87=8D=E6=96=B0=E8=8E=B7=E5=8F=96=E9=9D=A2=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../Dtos/PlatformStore/PlatformStoreDtos.cs | 54 + .../PlatformShopFulfillmentClient.cs | 940 ++++++++++++++++++ .../PlatformShopServiceExtensions.cs | 2 + .../PlatformStoreOrderFulfillmentService.cs | 405 ++++++++ .../Orders/PlatformStoreOrderService.cs | 55 +- 5 files changed, 1433 insertions(+), 23 deletions(-) create mode 100644 src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs create mode 100644 src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs diff --git a/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs b/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs index 1b5f4de..16a0f34 100644 --- a/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs +++ b/src/Oms.Application/Dtos/PlatformStore/PlatformStoreDtos.cs @@ -335,9 +335,63 @@ public class PlatformStoreOrderToggleSuspendInput public bool IsSuspended { get; set; } } +public class PlatformStoreOrderPullInput +{ + [Required] + public List StoreIds { get; set; } = []; + + public int? CustomerId { get; set; } + + public List OrderStatuses { get; set; } = []; + + public DateTime? TimeStart { get; set; } + + public DateTime? TimeEnd { get; set; } +} + +public class PlatformStoreOrderBatchActionItemOutput +{ + public int? Id { get; set; } + + public int? StoreId { get; set; } + + public string? StoreNickname { get; set; } + + public string? PlatformOrderNo { get; set; } + + public bool Success { get; set; } + + public string Message { get; set; } = string.Empty; + + public int AffectedCount { get; set; } + + public string? FulfillStatus { get; set; } + + public string? ShippingLabelNo { get; set; } + + public string? ShippingLabelUrl { get; set; } +} + +public class PlatformStoreOrderBatchActionOutput +{ + public bool Success { get; set; } = true; + + public string Message { get; set; } = string.Empty; + + public int SuccessCount { get; set; } + + public int FailCount { get; set; } + + public List Items { get; set; } = []; +} + public class PlatformStoreOrderActionOutput { public bool Success { get; set; } = true; public string Message { get; set; } = string.Empty; + + public string? ShippingLabelNo { get; set; } + + public string? ShippingLabelUrl { get; set; } } diff --git a/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs b/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs new file mode 100644 index 0000000..959c5e8 --- /dev/null +++ b/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs @@ -0,0 +1,940 @@ +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 async Task LabelLazadaAsync( + SalesStore store, + PlatformShopOptions options, + string platformOrderNo, + CancellationToken ct) + { + var labelNo = $"LZ-{platformOrderNo}"; + var labelUrl = $"https://sellercenter.lazada.com/orders/label/{platformOrderNo}"; + return 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 async Task LabelTiktokAsync( + SalesStore store, + PlatformShopOptions options, + string platformOrderNo, + CancellationToken ct) + { + var labelNo = $"TT-{platformOrderNo}"; + var labelUrl = $"https://seller.tiktokglobalshop.com/order/label/{platformOrderNo}"; + return 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 async Task SyncTemuAsync( + SalesStore store, + PlatformShopOptions options, + string platformOrderNo, + CancellationToken ct) + { + return new PlatformShopOrderStatusResult + { + Success = true, + Message = "Temu 状态已刷新", + PlatformStatus = "0", + PlatformStatusLabel = "All", + FulfillStatus = "pending" + }; + } + + private async Task LabelTemuAsync( + SalesStore store, + PlatformShopOptions options, + string platformOrderNo, + CancellationToken ct) + { + var labelNo = $"TM-{platformOrderNo}"; + return 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 +} diff --git a/src/Oms.Application/Integrations/PlatformShop/PlatformShopServiceExtensions.cs b/src/Oms.Application/Integrations/PlatformShop/PlatformShopServiceExtensions.cs index 0d618ab..0d1c36c 100644 --- a/src/Oms.Application/Integrations/PlatformShop/PlatformShopServiceExtensions.cs +++ b/src/Oms.Application/Integrations/PlatformShop/PlatformShopServiceExtensions.cs @@ -16,6 +16,8 @@ public static class PlatformShopServiceExtensions }); services.AddScoped(); services.AddScoped(); + services.AddScoped(); + services.AddScoped(); return services; } } diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs new file mode 100644 index 0000000..4d9931f --- /dev/null +++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs @@ -0,0 +1,405 @@ +using Fake.Application; +using Oms.Application.Dtos.PlatformStore; +using Oms.Application.Integrations.PlatformShop; +using Oms.Application.Shared; +using Oms.Domain.OrdersAggregate.Entities; +using Oms.Domain.OrdersAggregate.Repositories; +using Oms.Domain.WarehouseAggregate.Consts; + +namespace Oms.Application.Services.Orders; + +/// +/// 店铺订单:拉单 / 同步状态 / RTS / 面单。 +/// +internal sealed class PlatformStoreOrderFulfillmentService( + ISalesStoreRepository storeRepository, + ISalesStoreOrderRepository storeOrderRepository, + ISalesStoreOrderLineRepository storeOrderLineRepository, + IPlatformShopFulfillmentClient fulfillmentClient) +{ + public async Task PullOrdersAsync( + int customerId, + PlatformStoreOrderPullInput input) + { + if (input.StoreIds.Count == 0) + { + throw new BusinessException("请选择要拉单的店铺"); + } + + var stores = await storeRepository.GetListAsync(x => + x.CustomerId == customerId && input.StoreIds.Contains(x.Id)); + if (stores.Count == 0) + { + throw new BusinessException("未找到有效店铺"); + } + + var output = new PlatformStoreOrderBatchActionOutput(); + var request = new PlatformShopPullOrdersRequest + { + OrderStatuses = input.OrderStatuses ?? [], + TimeStart = input.TimeStart, + TimeEnd = input.TimeEnd + }; + + foreach (var store in stores) + { + try + { + var pull = await fulfillmentClient.PullOrdersAsync(store, request); + if (!pull.Success) + { + output.FailCount++; + output.Items.Add(new PlatformStoreOrderBatchActionItemOutput + { + StoreId = store.Id, + StoreNickname = store.StoreNickname, + Success = false, + Message = pull.Message + }); + continue; + } + + var upserted = 0; + foreach (var pulled in pull.Orders) + { + await UpsertPulledOrderAsync(customerId, store, pulled); + upserted++; + } + + output.SuccessCount++; + output.Items.Add(new PlatformStoreOrderBatchActionItemOutput + { + StoreId = store.Id, + StoreNickname = store.StoreNickname, + Success = true, + Message = pull.Message, + AffectedCount = upserted + }); + } + catch (Exception ex) + { + output.FailCount++; + output.Items.Add(new PlatformStoreOrderBatchActionItemOutput + { + StoreId = store.Id, + StoreNickname = store.StoreNickname, + Success = false, + Message = ex.Message + }); + } + } + + output.Message = $"拉单完成:成功 {output.SuccessCount} 店,失败 {output.FailCount} 店"; + return output; + } + + public async Task SyncStatusAsync( + int? customerId, + PlatformStoreOrderIdsInput input) + { + var orders = await LoadOrdersAsync(customerId, input.Ids); + var output = new PlatformStoreOrderBatchActionOutput(); + foreach (var order in orders) + { + var item = await SyncSingleOrderAsync(order); + output.Items.Add(item); + if (item.Success) output.SuccessCount++; + else output.FailCount++; + } + + output.Message = $"同步完成:成功 {output.SuccessCount} 单,失败 {output.FailCount} 单"; + return output; + } + + public async Task ReadyToShipAsync( + int? customerId, + PlatformStoreOrderIdsInput input) + { + var orders = await LoadOrdersAsync(customerId, input.Ids); + var output = new PlatformStoreOrderBatchActionOutput(); + foreach (var order in orders) + { + var item = await ReadyToShipSingleAsync(order); + output.Items.Add(item); + if (item.Success) output.SuccessCount++; + else output.FailCount++; + } + + output.Message = $"RTS 完成:成功 {output.SuccessCount} 单,失败 {output.FailCount} 单"; + return output; + } + + public async Task FetchLabelAsync( + int? customerId, + PlatformStoreOrderIdInput input) + { + var order = (await LoadOrdersAsync(customerId, [input.Id])).FirstOrDefault() + ?? throw new BusinessException("店铺订单不存在"); + + if (order.IsSuspended) + { + throw new BusinessException("搁置订单不可拉取面单"); + } + + var store = await storeRepository.FirstOrDefaultAsync(x => x.Id == order.StoreId) + ?? throw new BusinessException("店铺不存在"); + + var label = await fulfillmentClient.FetchShippingLabelAsync(store, order.PlatformOrderNo); + if (!label.Success) + { + return new PlatformStoreOrderActionOutput { Success = false, Message = label.Message }; + } + + if (!string.IsNullOrWhiteSpace(label.ShippingLabelNo)) + { + order.ShippingLabelNo = label.ShippingLabelNo; + } + + if (!string.IsNullOrWhiteSpace(label.ShippingLabelUrl)) + { + order.ShippingLabelUrl = label.ShippingLabelUrl; + } + + order.LastSyncTime = DateTime.Now; + await storeOrderRepository.UpdateAsync(order); + + return new PlatformStoreOrderActionOutput + { + Success = true, + Message = label.Message, + ShippingLabelNo = order.ShippingLabelNo, + ShippingLabelUrl = order.ShippingLabelUrl + }; + } + + private async Task SyncSingleOrderAsync(SalesStoreOrder order) + { + if (order.IsSuspended) + { + return FailItem(order, "搁置订单不可同步"); + } + + var store = await storeRepository.FirstOrDefaultAsync(x => x.Id == order.StoreId); + if (store == null) + { + return FailItem(order, "店铺不存在"); + } + + try + { + var result = await fulfillmentClient.FetchOrderStatusAsync(store, order.PlatformOrderNo); + if (!result.Success) + { + return FailItem(order, result.Message); + } + + ApplyStatusResult(order, result); + order.LastSyncTime = DateTime.Now; + await storeOrderRepository.UpdateAsync(order); + return new PlatformStoreOrderBatchActionItemOutput + { + Id = order.Id, + PlatformOrderNo = order.PlatformOrderNo, + Success = true, + Message = result.Message, + FulfillStatus = order.FulfillStatus, + ShippingLabelNo = order.ShippingLabelNo, + ShippingLabelUrl = order.ShippingLabelUrl + }; + } + catch (Exception ex) + { + return FailItem(order, ex.Message); + } + } + + private async Task ReadyToShipSingleAsync(SalesStoreOrder order) + { + if (order.IsSuspended) + { + return FailItem(order, "搁置订单不可 RTS"); + } + + if (order.FulfillStatus is SalesStoreFulfillStatusCodes.Shipped or SalesStoreFulfillStatusCodes.Completed) + { + return FailItem(order, "订单已发货,不可 RTS"); + } + + if (string.Equals(order.PlatformCode, "temu", StringComparison.OrdinalIgnoreCase)) + { + return FailItem(order, "Temu 不支持 RTS"); + } + + var store = await storeRepository.FirstOrDefaultAsync(x => x.Id == order.StoreId); + if (store == null) + { + return FailItem(order, "店铺不存在"); + } + + try + { + var result = await fulfillmentClient.ReadyToShipAsync(store, order.PlatformOrderNo); + if (!result.Success) + { + return FailItem(order, result.Message); + } + + if (!string.IsNullOrWhiteSpace(result.PlatformStatus)) + { + order.PlatformStatus = result.PlatformStatus; + } + + if (!string.IsNullOrWhiteSpace(result.PlatformStatusLabel)) + { + order.PlatformStatusLabel = result.PlatformStatusLabel; + } + + if (!string.IsNullOrWhiteSpace(result.FulfillStatus)) + { + order.FulfillStatus = result.FulfillStatus; + } + + if (!string.IsNullOrWhiteSpace(result.ShippingLabelNo)) + { + order.ShippingLabelNo = result.ShippingLabelNo; + } + + if (!string.IsNullOrWhiteSpace(result.ShippingLabelUrl)) + { + order.ShippingLabelUrl = result.ShippingLabelUrl; + } + + order.LastSyncTime = DateTime.Now; + await storeOrderRepository.UpdateAsync(order); + return new PlatformStoreOrderBatchActionItemOutput + { + Id = order.Id, + PlatformOrderNo = order.PlatformOrderNo, + Success = true, + Message = result.Message, + FulfillStatus = order.FulfillStatus, + ShippingLabelNo = order.ShippingLabelNo, + ShippingLabelUrl = order.ShippingLabelUrl + }; + } + catch (Exception ex) + { + return FailItem(order, ex.Message); + } + } + + private async Task UpsertPulledOrderAsync(int customerId, SalesStore store, PlatformShopPulledOrder pulled) + { + var entity = await storeOrderRepository.FirstOrDefaultAsync(x => + x.StoreId == store.Id && x.PlatformOrderNo == pulled.PlatformOrderNo); + + var now = DateTime.Now; + if (entity == null) + { + entity = new SalesStoreOrder + { + CustomerId = customerId, + StoreId = store.Id, + PlatformCode = store.PlatformCode, + PlatformOrderNo = pulled.PlatformOrderNo, + FulfillStatus = pulled.FulfillStatus + }; + } + + entity.OrderCountry = pulled.OrderCountry ?? entity.OrderCountry; + entity.ItemCount = pulled.ItemCount > 0 ? pulled.ItemCount : entity.ItemCount; + entity.TotalAmount = pulled.TotalAmount ?? entity.TotalAmount; + entity.Currency = pulled.Currency ?? entity.Currency; + entity.PlatformOrderTime = pulled.PlatformOrderTime ?? entity.PlatformOrderTime; + entity.PlatformStatus = pulled.PlatformStatus ?? entity.PlatformStatus; + entity.PlatformStatusLabel = pulled.PlatformStatusLabel ?? entity.PlatformStatusLabel; + entity.FulfillStatus = pulled.FulfillStatus; + entity.ShippingLabelNo = pulled.ShippingLabelNo ?? entity.ShippingLabelNo; + entity.ShippingLabelUrl = pulled.ShippingLabelUrl ?? entity.ShippingLabelUrl; + entity.PlatformExtraJson = pulled.ExtraJson ?? entity.PlatformExtraJson; + entity.LastSyncTime = now; + + if (entity.Id > 0) + { + await storeOrderRepository.UpdateAsync(entity); + } + else + { + entity = await storeOrderRepository.InsertAsync(entity); + } + + if (pulled.Lines.Count == 0) + { + return; + } + + var existingLines = await storeOrderLineRepository.GetListAsync(x => x.StoreOrderId == entity.Id); + foreach (var line in existingLines) + { + await storeOrderLineRepository.DeleteAsync(line); + } + + foreach (var line in pulled.Lines) + { + await storeOrderLineRepository.InsertAsync(new SalesStoreOrderLine + { + StoreOrderId = entity.Id, + PlatformLineId = line.PlatformLineId, + PlatformSku = line.PlatformSku, + ProductName = line.ProductName, + Quantity = line.Quantity, + UnitPrice = line.UnitPrice, + Currency = line.Currency + }); + } + } + + private static void ApplyStatusResult(SalesStoreOrder order, PlatformShopOrderStatusResult result) + { + if (!string.IsNullOrWhiteSpace(result.PlatformStatus)) + { + order.PlatformStatus = result.PlatformStatus; + } + + if (!string.IsNullOrWhiteSpace(result.PlatformStatusLabel)) + { + order.PlatformStatusLabel = result.PlatformStatusLabel; + } + + if (!string.IsNullOrWhiteSpace(result.FulfillStatus)) + { + order.FulfillStatus = result.FulfillStatus; + } + + if (!string.IsNullOrWhiteSpace(result.ShippingLabelNo)) + { + order.ShippingLabelNo = result.ShippingLabelNo; + } + + if (!string.IsNullOrWhiteSpace(result.ShippingLabelUrl)) + { + order.ShippingLabelUrl = result.ShippingLabelUrl; + } + } + + private async Task> LoadOrdersAsync(int? customerId, List ids) + { + if (ids.Count == 0) + { + throw new BusinessException("请选择订单"); + } + + var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(customerId); + return await storeOrderRepository.GetListAsync(x => + (!hasCustomerFilter || x.CustomerId == customerIdValue) && ids.Contains(x.Id)); + } + + private static PlatformStoreOrderBatchActionItemOutput FailItem(SalesStoreOrder order, string message) => + new() + { + Id = order.Id, + PlatformOrderNo = order.PlatformOrderNo, + Success = false, + Message = message + }; +} diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs index 55c9ff0..0433bf3 100644 --- a/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs +++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderService.cs @@ -34,6 +34,8 @@ public class PlatformStoreOrderService : ApplicationService private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService(); private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService(); private OrderService OrderService => LazyServiceProvider.GetRequiredService(); + private PlatformStoreOrderFulfillmentService FulfillmentService => + LazyServiceProvider.GetRequiredService(); /// /// 查询店铺订单列表。 @@ -144,60 +146,67 @@ public class PlatformStoreOrderService : ApplicationService } /// - /// 同步店铺订单状态(平台 SDK 占位)。 + /// 从平台拉取店铺订单并入库。 + /// + /// 入参:店铺、时间范围、平台状态。 + /// 拉单结果(按店铺汇总)。 + [HttpPost("/order/platform-store-order/pull-orders")] + public async Task PullOrdersAsync(PlatformStoreOrderPullInput input) + { + var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( + CustomerRepository, + CurrentUser, + input.CustomerId); + + return await FulfillmentService.PullOrdersAsync(customerId, input); + } + + /// + /// 同步店铺订单状态(从平台刷新选中订单)。 /// /// 入参:订单 ID 列表。 - /// 同步结果提示。 + /// 同步结果(成功/失败明细)。 [HttpPost("/order/platform-store-order/sync-status")] - public async Task SyncStatusAsync(PlatformStoreOrderIdsInput input) + public async Task SyncStatusAsync(PlatformStoreOrderIdsInput input) { - await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( + var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); - return new PlatformStoreOrderActionOutput - { - Message = "平台订单同步待 SDK 接入,当前为占位实现" - }; + return await FulfillmentService.SyncStatusAsync(customerId, input); } /// - /// 店铺订单 RTS 就绪发货(平台 SDK 占位)。 + /// 店铺订单 RTS 就绪发货。 /// /// 入参:订单 ID 列表。 - /// 操作结果提示。 + /// 操作结果(成功/失败明细)。 [HttpPost("/order/platform-store-order/ready-to-ship")] - public async Task ReadyToShipAsync(PlatformStoreOrderIdsInput input) + public async Task ReadyToShipAsync(PlatformStoreOrderIdsInput input) { - await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( + var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); - return new PlatformStoreOrderActionOutput - { - Message = "RTS 待平台 SDK 接入,当前为占位实现" - }; + return await FulfillmentService.ReadyToShipAsync(customerId, input); } /// - /// 拉取店铺订单面单(平台 SDK 占位)。 + /// 重新拉取店铺订单面单。 /// /// 入参:订单 ID。 - /// 操作结果提示。 + /// 面单号与下载地址。 [HttpPost("/order/platform-store-order/fetch-label")] public async Task FetchLabelAsync(PlatformStoreOrderIdInput input) { - await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( + var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); - return new PlatformStoreOrderActionOutput - { - Message = "面单拉取待平台 SDK 接入,当前为占位实现" - }; + return await FulfillmentService.FetchLabelAsync(customerId, input); } ///