All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
Co-authored-by: Cursor <cursoragent@cursor.com>
471 lines
18 KiB
C#
471 lines
18 KiB
C#
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;
|
||
|
||
/// <summary>
|
||
/// 店铺订单:拉单 / 同步状态 / RTS / 面单。
|
||
/// </summary>
|
||
internal sealed class PlatformStoreOrderFulfillmentService(
|
||
ISalesStoreRepository storeRepository,
|
||
ISalesStoreOrderRepository storeOrderRepository,
|
||
ISalesStoreOrderLineRepository storeOrderLineRepository,
|
||
IPlatformShopFulfillmentClient fulfillmentClient)
|
||
{
|
||
public async Task<PlatformStoreOrderBatchActionOutput> 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<PlatformStoreOrderBatchActionOutput> 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<PlatformStoreOrderBatchActionOutput> 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<PlatformStoreOrderActionOutput> 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<PlatformStoreOrderBatchActionItemOutput> 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<PlatformStoreOrderBatchActionItemOutput> 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<List<SalesStoreOrder>> LoadOrdersAsync(int? customerId, List<int> 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
|
||
};
|
||
}
|