diff --git a/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs b/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs index e73b5bd..9617dcd 100644 --- a/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs +++ b/src/Oms.Application/Integrations/PlatformShop/PlatformShopFulfillmentClient.cs @@ -1066,40 +1066,178 @@ internal sealed class PlatformShopFulfillmentClient( }; } + /// + /// 对齐 WMS ShopordTiktokImpl.send:订单详情取 package_id → 校验 DropOff → shipPackage,再尝试拉面单。 + /// private async Task RtsTiktokAsync( SalesStore store, PlatformShopOptions options, string platformOrderNo, CancellationToken ct) { - var sync = await SyncTiktokAsync(store, options, platformOrderNo, ct); - if (!sync.Success) + EnsureTiktok(store, options); + var shopCipher = await GetTiktokShopCipherAsync(store, options, ct); + if (string.IsNullOrWhiteSpace(shopCipher)) { - return new PlatformShopReadyToShipResult { Success = false, Message = sync.Message }; + return new PlatformShopReadyToShipResult { Success = false, Message = "未获取到 TikTok shop_cipher,请重新授权店铺" }; } - if (!string.Equals(sync.PlatformStatus, "AWAITING_SHIPMENT", StringComparison.OrdinalIgnoreCase) - && sync.PlatformStatus != "111") + var tiktok = options.Tiktok; + using var detail = await GetTiktokSignedAsync(options, store.AccessToken!, "/order/202309/orders", new Dictionary + { + ["app_key"] = tiktok.AppKey, + ["shop_cipher"] = shopCipher, + ["ids"] = platformOrderNo, + ["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString() + }, ct); + if (!TryGetTiktokData(detail.RootElement, out var data, out var detailError)) + { + return new PlatformShopReadyToShipResult { Success = false, Message = "订单详情信息异常:" + detailError }; + } + + if (!data.TryGetProperty("orders", out var orders) + || orders.ValueKind != JsonValueKind.Array + || orders.GetArrayLength() == 0) + { + return new PlatformShopReadyToShipResult { Success = false, Message = "TikTok 未找到订单" }; + } + + var order = orders[0]; + var status = GetString(order, "status") ?? string.Empty; + if (string.Equals(status, "CANCELLED", StringComparison.OrdinalIgnoreCase)) + { + return new PlatformShopReadyToShipResult { Success = false, Message = "订单已取消,不可准备发货" }; + } + + if (status is "IN_TRANSIT" or "DELIVERED" or "COMPLETED" + || string.Equals(status, "AWAITING_COLLECTION", StringComparison.OrdinalIgnoreCase)) { return new PlatformShopReadyToShipResult { Success = false, - Message = $"TikTok 订单状态为 {sync.PlatformStatus},不可 RTS" + Message = $"TikTok 订单状态为 {status},不可准备发货" }; } + if (!order.TryGetProperty("line_items", out var lineItems) + || lineItems.ValueKind != JsonValueKind.Array + || lineItems.GetArrayLength() == 0) + { + return new PlatformShopReadyToShipResult { Success = false, Message = "商品信息为空" }; + } + + var packageIds = new List(); + foreach (var line in lineItems.EnumerateArray()) + { + var packageId = GetString(line, "package_id"); + if (string.IsNullOrWhiteSpace(packageId)) continue; + if (!packageIds.Contains(packageId, StringComparer.Ordinal)) + { + packageIds.Add(packageId); + } + } + + if (packageIds.Count == 0) + { + return new PlatformShopReadyToShipResult { Success = false, Message = "订单没有 package_id,无法准备发货" }; + } + + foreach (var packageId in packageIds) + { + using var handoverDoc = await GetTiktokSignedAsync( + options, + store.AccessToken!, + $"/fulfillment/202309/packages/{packageId}/handover_time_slots", + new Dictionary + { + ["app_key"] = tiktok.AppKey, + ["shop_cipher"] = shopCipher, + ["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString() + }, + ct); + if (!TryGetTiktokData(handoverDoc.RootElement, out var handoverData, out var handoverError)) + { + return new PlatformShopReadyToShipResult + { + Success = false, + Message = "包裹支持发货类型获异常:" + handoverError + }; + } + + var canDropOff = handoverData.TryGetProperty("can_drop_off", out var drop) + && drop.ValueKind is JsonValueKind.True or JsonValueKind.False + && drop.GetBoolean(); + if (!canDropOff) + { + return new PlatformShopReadyToShipResult { Success = false, Message = "包裹不支持 DropOff 发货" }; + } + + var shipPath = $"/fulfillment/202309/packages/{packageId}/ship"; + var shipQuery = new Dictionary + { + ["app_key"] = tiktok.AppKey, + ["shop_cipher"] = shopCipher, + ["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString() + }; + var shipBody = new Dictionary { ["handover_method"] = "DROP_OFF" }; + using var shipDoc = await PostTiktokSignedAsync(options, store.AccessToken!, shipPath, shipQuery, shipBody, ct); + // ship 成功时 data 可能为 null,只认 code=0 + var shipCode = GetString(shipDoc.RootElement, "code"); + if (shipCode is not ("0" or null)) + { + var msg = GetString(shipDoc.RootElement, "message") ?? "发货失败"; + return new PlatformShopReadyToShipResult { Success = false, Message = "发货异常:" + msg }; + } + } + + string? labelNo = null; + string? labelUrl = LabelPendingText; + var labelPending = true; + try + { + var label = await LabelTiktokAsync(store, options, platformOrderNo, ct); + if (label.Success) + { + labelNo = label.ShippingLabelNo; + if (!string.IsNullOrWhiteSpace(label.ShippingLabelUrl)) + { + labelUrl = label.ShippingLabelUrl; + labelPending = false; + } + } + } + catch (Exception ex) + { + logger.LogWarning(ex, "TikTok 准备发货后拉面单失败:{OrderNo}", platformOrderNo); + } + return new PlatformShopReadyToShipResult { Success = true, - Message = "TikTok RTS 已提交,面单获取中", + Message = labelPending ? "准备发货成功,面单获取中" : "准备发货成功", PlatformStatus = "AWAITING_COLLECTION", PlatformStatusLabel = "AWAITING_COLLECTION", FulfillStatus = "toShip", - ShippingLabelUrl = LabelPendingText, - LabelPending = true + ShippingLabelNo = labelNo, + ShippingLabelUrl = labelUrl, + LabelPending = labelPending }; } + private async Task PostTiktokSignedAsync( + PlatformShopOptions options, + string accessToken, + string path, + Dictionary query, + object body, + CancellationToken ct) + { + var bodyJson = JsonSerializer.Serialize(body); + var sign = PlatformShopCrypto.TiktokSign(path, query, bodyJson, options.Tiktok.AppSecret); + var url = $"{options.Tiktok.HostApi}{path}?{BuildTiktokQuery(query, sign)}"; + return await PostRawJsonAsync(url, bodyJson, ct, accessToken); + } + /// /// 对齐 WMS ShopordTiktokImpl.retryOrderSheet:订单详情取 package_id,再取 SHIPPING_LABEL 并落库。 /// diff --git a/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs b/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs index 7f9bc3a..6b4a8dd 100644 --- a/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs +++ b/src/Oms.Application/Services/Orders/PlatformStoreOrderFulfillmentService.cs @@ -280,6 +280,11 @@ internal sealed class PlatformStoreOrderFulfillmentService( { order.ShippingLabelNo = result.ShippingLabelNo; } + else if (result.LabelPending) + { + // 对齐 WMS:准备发货后先标记面单待获取 + order.ShippingLabelNo = "系统获取中"; + } if (!string.IsNullOrWhiteSpace(result.ShippingLabelUrl)) { @@ -288,6 +293,36 @@ internal sealed class PlatformStoreOrderFulfillmentService( order.LastSyncTime = DateTime.Now; await storeOrderRepository.UpdateAsync(order); + + // TikTok:发货后再拉详情,回写运单号等;面单未就绪时保留「系统获取中」 + if (string.Equals(order.PlatformCode, "tiktok", StringComparison.OrdinalIgnoreCase)) + { + try + { + var detail = await fulfillmentClient.FetchOrderDetailAsync(store, order.PlatformOrderNo); + if (detail != null) + { + if (result.LabelPending && string.IsNullOrWhiteSpace(detail.ShippingLabelUrl)) + { + detail.ShippingLabelUrl = "系统获取中"; + detail.ShippingLabelNo ??= "系统获取中"; + } + + detail.PlatformStatus ??= "AWAITING_COLLECTION"; + detail.PlatformStatusLabel ??= detail.PlatformStatus; + detail.FulfillStatus = string.IsNullOrWhiteSpace(detail.FulfillStatus) + ? "toShip" + : detail.FulfillStatus; + await UpsertPulledOrderAsync(order.CustomerId, store, detail); + order = await storeOrderRepository.FirstOrDefaultAsync(x => x.Id == order.Id) ?? order; + } + } + catch + { + // 详情回写失败不影响 RTS 成功结果 + } + } + return new PlatformStoreOrderBatchActionItemOutput { Id = order.Id,