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 detail = await fulfillmentClient.FetchOrderDetailAsync(store, order.PlatformOrderNo); if (detail != null) { await UpsertPulledOrderAsync(order.CustomerId, store, detail); var refreshed = await storeOrderRepository.FirstOrDefaultAsync(x => x.Id == order.Id) ?? order; return new PlatformStoreOrderBatchActionItemOutput { Id = refreshed.Id, PlatformOrderNo = refreshed.PlatformOrderNo, Success = true, Message = "同步成功", FulfillStatus = refreshed.FulfillStatus, ShippingLabelNo = refreshed.ShippingLabelNo, ShippingLabelUrl = refreshed.ShippingLabelUrl }; } 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; } else if (result.LabelPending) { // 对齐 WMS:准备发货后先标记面单待获取 order.ShippingLabelNo = "系统获取中"; } if (!string.IsNullOrWhiteSpace(result.ShippingLabelUrl)) { order.ShippingLabelUrl = result.ShippingLabelUrl; } 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, 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; if (!string.IsNullOrWhiteSpace(pulled.RecipientName)) entity.RecipientName = pulled.RecipientName; if (!string.IsNullOrWhiteSpace(pulled.RecipientPhone)) entity.RecipientPhone = pulled.RecipientPhone; if (!string.IsNullOrWhiteSpace(pulled.RecipientEmail)) entity.RecipientEmail = pulled.RecipientEmail; if (!string.IsNullOrWhiteSpace(pulled.RecipientCountry)) entity.RecipientCountry = pulled.RecipientCountry; if (!string.IsNullOrWhiteSpace(pulled.RecipientProvince)) entity.RecipientProvince = pulled.RecipientProvince; if (!string.IsNullOrWhiteSpace(pulled.RecipientCity)) entity.RecipientCity = pulled.RecipientCity; if (!string.IsNullOrWhiteSpace(pulled.RecipientZipCode)) entity.RecipientZipCode = pulled.RecipientZipCode; if (!string.IsNullOrWhiteSpace(pulled.RecipientAddress1)) entity.RecipientAddress1 = pulled.RecipientAddress1; if (!string.IsNullOrWhiteSpace(pulled.RecipientAddress2)) entity.RecipientAddress2 = pulled.RecipientAddress2; 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.IsMultiLabel = pulled.IsMultiLabel || entity.IsMultiLabel; entity.PlatformExtraJson = pulled.ExtraJson ?? entity.PlatformExtraJson; entity.LastSyncTime = now; PlatformStoreOrderParcelBuilder.TryFillRecipientFields(entity, pulled.ExtraJson); 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, ProductImageUrl = line.ProductImageUrl, VariantText = line.VariantText, Quantity = line.Quantity > 0 ? line.Quantity : 1, 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 }; }