fix: TikTok 准备发货改为真实 shipPackage,并回写面单状态
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-16 17:31:19 +08:00
parent 9c97a218ec
commit 7a611f8cf7
2 changed files with 182 additions and 9 deletions

View File

@ -1066,40 +1066,178 @@ internal sealed class PlatformShopFulfillmentClient(
};
}
/// <summary>
/// 对齐 WMS ShopordTiktokImpl.send:订单详情取 package_id → 校验 DropOff → shipPackage,再尝试拉面单。
/// </summary>
private async Task<PlatformShopReadyToShipResult> 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<string, string>
{
["app_key"] = tiktok.AppKey,
["shop_cipher"] = shopCipher,
["ids"] = platformOrderNo,
["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString()
}, ct);
if (!TryGetTiktokData(detail.RootElement, out var data, out var detailError))
{
return new 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<string>();
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<string, string>
{
["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<string, string>
{
["app_key"] = tiktok.AppKey,
["shop_cipher"] = shopCipher,
["timestamp"] = PlatformShopCrypto.UnixSeconds().ToString()
};
var shipBody = new Dictionary<string, object> { ["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<JsonDocument> PostTiktokSignedAsync(
PlatformShopOptions options,
string accessToken,
string path,
Dictionary<string, string> 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);
}
/// <summary>
/// 对齐 WMS ShopordTiktokImpl.retryOrderSheet:订单详情取 package_id,再取 SHIPPING_LABEL 并落库。
/// </summary>

View File

@ -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,