using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.PlatformStore;
using Oms.Application.Dtos.Shared;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.CustomersAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.Shared.Enums;
using Oms.Domain.SystemAggregate.Entities;
using Oms.Domain.SystemAggregate.Repositories;
using Oms.Domain.WarehouseAggregate.Consts;
using Oms.Domain.WarehouseAggregate.Repositories;
namespace Oms.Application.Services.Orders;
///
/// 店铺订单 API 服务。
///
[ApiExplorerSettings(GroupName = "OMS-Order")]
public class PlatformStoreOrderService : ApplicationService
{
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService();
private ISalesStoreRepository StoreRepository => LazyServiceProvider.GetRequiredService();
private ISalesStoreOrderRepository StoreOrderRepository => LazyServiceProvider.GetRequiredService();
private ISalesStoreOrderLineRepository StoreOrderLineRepository => LazyServiceProvider.GetRequiredService();
private ICSkuRepository SkuRepository => LazyServiceProvider.GetRequiredService();
private ICSkuPlatformMapRepository SkuPlatformMapRepository => LazyServiceProvider.GetRequiredService();
private ICInventoryRepository InventoryRepository => LazyServiceProvider.GetRequiredService();
private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService();
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService();
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService();
private OrderService OrderService => LazyServiceProvider.GetRequiredService();
private PlatformStoreOrderFulfillmentService FulfillmentService =>
LazyServiceProvider.GetRequiredService();
///
/// 查询店铺订单列表。
///
/// 入参:店铺订单分页查询条件。
/// 店铺订单分页结果。
[HttpPost("/order/platform-store-order/list")]
public async Task> GetListAsync(PlatformStoreOrderListInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var predicate = BuildListPredicate(customerId, input);
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
// 避免 Fake GetPagedListAsync(sorting) 丢失 Where:total 正确但 items 返回全量
var (entities, total) = await RepositoryPageHelper.GetPageByUpdateTimeDescAsync(
StoreOrderRepository,
predicate,
pageIndex,
pageSize);
var storeIds = entities.Select(x => x.StoreId).Distinct().ToArray();
var stores = storeIds.Length == 0
? []
: await StoreRepository.GetListAsync(x => storeIds.Contains(x.Id));
var storeMap = stores.ToDictionary(x => x.Id);
var linkedOrderIds = entities.Where(x => x.LinkedOrderId is > 0).Select(x => x.LinkedOrderId!.Value).Distinct().ToArray();
var linkedOrders = linkedOrderIds.Length == 0
? []
: await OrderRepository.GetListAsync(x => linkedOrderIds.Contains(x.Id));
var linkedOrderMap = linkedOrders.ToDictionary(x => x.Id);
var customerIds = entities.Select(x => x.CustomerId).Distinct().ToArray();
var customers = customerIds.Length == 0
? []
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
var customerMap = customers.ToDictionary(x => x.Id);
return new OmsPageResult
{
Items = entities.Select(x =>
{
storeMap.TryGetValue(x.StoreId, out var store);
string? linkedServiceNo = null;
if (x.LinkedOrderId is > 0 && linkedOrderMap.TryGetValue(x.LinkedOrderId.Value, out var linkedOrder))
{
linkedServiceNo = linkedOrder.ServiceNo;
}
customerMap.TryGetValue(x.CustomerId, out var customer);
return MapOrderOutput(
x,
store?.StoreNickname ?? string.Empty,
[],
linkedServiceNo: linkedServiceNo,
customerName: customer?.CustomerName,
customerCode: customer?.CustomerCode);
}).ToList(),
Total = total
};
}
///
/// 查询店铺订单详情。
///
/// 店铺订单 ID。
/// 客户 ID(可选)。
/// 店铺订单详情(含商品行)。
[HttpGet("/order/platform-store-order/detail")]
public async Task GetDetailAsync([FromQuery] int id, [FromQuery] int? customerId = null)
{
var resolvedCustomerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
customerId);
var order = await GetOrderOrThrowAsync(resolvedCustomerId, id);
var store = await StoreRepository.FirstOrDefaultAsync(x => x.Id == order.StoreId);
var lines = await StoreOrderLineRepository.GetListAsync(x => x.StoreOrderId == order.Id);
var skuIds = lines.Where(x => x.MappedSkuId.HasValue).Select(x => x.MappedSkuId!.Value).Distinct().ToArray();
var skus = skuIds.Length == 0
? []
: await SkuRepository.GetListAsync(x => skuIds.Contains(x.Id));
var skuMap = skus.ToDictionary(x => x.Id);
var linkedServiceNo = await ResolveLinkedServiceNoAsync(order.LinkedOrderId);
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId);
return MapOrderOutput(
order,
store?.StoreNickname ?? string.Empty,
lines,
skuMap,
linkedServiceNo,
customer?.CustomerName,
customer?.CustomerCode);
}
///
/// 查询店铺订单状态汇总。
///
/// 客户 ID(可选)。
/// 店铺 ID(可选)。
/// 各履约状态数量汇总。
[HttpGet("/order/platform-store-order/summary")]
public async Task GetSummaryAsync([FromQuery] int? customerId = null, [FromQuery] int? storeId = null)
{
var resolvedCustomerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
customerId);
var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(resolvedCustomerId);
var hasStoreFilter = storeId.HasValue;
var storeIdValue = storeId.GetValueOrDefault();
var orders = await StoreOrderRepository.GetListAsync(x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasStoreFilter || x.StoreId == storeIdValue));
return new PlatformStoreOrderSummaryOutput
{
Pending = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.Pending),
ToShip = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.ToShip),
Shipped = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.Shipped),
Canceled = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.Canceled),
Suspended = orders.Count(x => x.IsSuspended),
Completed = orders.Count(x => x.FulfillStatus == SalesStoreFulfillStatusCodes.Completed)
};
}
///
/// 从平台拉取店铺订单并入库。
///
/// 入参:店铺、时间范围、平台状态。
/// 拉单结果(按店铺汇总)。
[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)
{
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
return await FulfillmentService.SyncStatusAsync(customerId, input);
}
///
/// 店铺订单 RTS 就绪发货。
///
/// 入参:订单 ID 列表。
/// 操作结果(成功/失败明细)。
[HttpPost("/order/platform-store-order/ready-to-ship")]
public async Task ReadyToShipAsync(PlatformStoreOrderIdsInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
return await FulfillmentService.ReadyToShipAsync(customerId, input);
}
///
/// 重新拉取店铺订单面单。
///
/// 入参:订单 ID。
/// 面单号与下载地址。
[HttpPost("/order/platform-store-order/fetch-label")]
public async Task FetchLabelAsync(PlatformStoreOrderIdInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
return await FulfillmentService.FetchLabelAsync(customerId, input);
}
///
/// 切换店铺订单搁置状态。
///
/// 入参:订单 ID 与是否搁置。
[HttpPost("/order/platform-store-order/toggle-suspend")]
public async Task ToggleSuspendAsync(PlatformStoreOrderToggleSuspendInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var order = await GetOrderOrThrowAsync(customerId, input.Id);
order.IsSuspended = input.IsSuspended;
order.FulfillStatus = input.IsSuspended
? SalesStoreFulfillStatusCodes.Suspended
: SalesStoreFulfillStatusCodes.Pending;
await StoreOrderRepository.UpdateAsync(order);
}
///
/// 删除店铺订单。
///
/// 入参:订单 ID 列表。
[HttpPost("/order/platform-store-order/delete")]
public async Task DeleteAsync(PlatformStoreOrderIdsInput input)
{
if (input.Ids.Count == 0)
{
throw new BusinessException("请选择要删除的订单");
}
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(customerId);
var orders = await StoreOrderRepository.GetListAsync(x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue) && input.Ids.Contains(x.Id));
foreach (var order in orders)
{
var lines = await StoreOrderLineRepository.GetListAsync(x => x.StoreOrderId == order.Id);
foreach (var line in lines)
{
await StoreOrderLineRepository.DeleteAsync(line);
}
await StoreOrderRepository.DeleteAsync(order);
}
}
///
/// 店铺订单匹配库存并创建小包单。
///
/// 入参:订单 ID、仓库、销售产品等。
/// 匹配结果与关联小包单号。
[HttpPost("/order/platform-store-order/match-inventory")]
public async Task MatchInventoryAsync(PlatformStoreOrderMatchInventoryInput input)
{
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var result = await MatchInventoryCoreAsync(
customerId,
input.Id,
input.WarehouseId,
input.ProductCode,
input.TrackNo,
input.Receiver);
return new PlatformStoreOrderMatchInventoryOutput
{
Success = true,
Message = $"库存匹配成功,已创建小包订单 {result.ServiceNo}",
LinkedOrderId = result.OrderId,
ServiceNo = result.ServiceNo,
ClientNo = result.ClientNo
};
}
///
/// 批量匹配店铺订单库存并创建小包单。
///
/// 入参:订单 ID 列表、仓库、销售产品等。
/// 批量匹配结果(成功/失败明细)。
[HttpPost("/order/platform-store-order/batch-match-inventory")]
public async Task BatchMatchInventoryAsync(
PlatformStoreOrderBatchMatchInventoryInput input)
{
if (input.Ids.Count == 0)
{
throw new BusinessException("请选择要匹配的订单");
}
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
input.CustomerId);
var output = new PlatformStoreOrderBatchMatchInventoryOutput();
foreach (var id in input.Ids.Distinct())
{
var storeOrder = await StoreOrderRepository.FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId);
var item = new PlatformStoreOrderBatchMatchItemOutput
{
Id = id,
PlatformOrderNo = storeOrder?.PlatformOrderNo ?? id.ToString()
};
try
{
var result = await MatchInventoryCoreAsync(
customerId,
id,
input.WarehouseId,
input.ProductCode,
null,
null);
item.Success = true;
item.Message = $"已创建小包订单 {result.ServiceNo}";
item.LinkedOrderId = result.OrderId;
item.ServiceNo = result.ServiceNo;
output.SuccessCount++;
}
catch (BusinessException ex)
{
item.Success = false;
item.Message = ex.Message;
output.FailCount++;
}
catch (Exception ex)
{
item.Success = false;
item.Message = ex.Message;
output.FailCount++;
}
output.Items.Add(item);
}
return output;
}
///
/// 批量创建物流订单(不扣库存、不要求 SKU 映射)。
///
[HttpPost("/order/platform-store-order/batch-create-logistics")]
public async Task BatchCreateLogisticsAsync(
PlatformStoreOrderBatchCreateLogisticsInput input)
{
return await BatchCreateLogisticsInternalAsync(
input.CustomerId,
input.ProductCode,
input.WarehouseId,
input.Items,
requireBind: false);
}
///
/// 批量创建物流订单并绑定包裹/库存(冻结,不即时扣实物)。
///
[HttpPost("/order/platform-store-order/batch-create-fulfill")]
public async Task BatchCreateFulfillAsync(
PlatformStoreOrderBatchCreateFulfillInput input)
{
if (input.WarehouseId <= 0)
{
throw new BusinessException("请指定发货仓库");
}
return await BatchCreateLogisticsInternalAsync(
input.CustomerId,
input.ProductCode,
input.WarehouseId,
input.Items,
requireBind: true);
}
private async Task BatchCreateLogisticsInternalAsync(
int? customerIdInput,
string? productCode,
int? warehouseId,
List items,
bool requireBind)
{
if (items == null || items.Count == 0)
{
throw new BusinessException("请选择要创建的店铺订单");
}
if (items.Count > 50)
{
throw new BusinessException("单次最多创建 50 单");
}
if (string.IsNullOrWhiteSpace(productCode))
{
throw new BusinessException("请选择运输方式");
}
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
CustomerRepository,
CurrentUser,
customerIdInput);
var resolvedProductCode = await ResolveProductCodeRequiredAsync(productCode);
var batchNo = $"PSL{DateTime.Now:yyyyMMddHHmmss}";
var output = new PlatformStoreOrderBatchCreateLogisticsOutput { BatchNo = batchNo };
foreach (var itemInput in items)
{
var storeOrderId = itemInput.StoreOrderId;
var storeOrder = await StoreOrderRepository.FirstOrDefaultAsync(x =>
x.Id == storeOrderId && x.CustomerId == customerId);
var item = new PlatformStoreOrderBatchMatchItemOutput
{
Id = storeOrderId,
PlatformOrderNo = storeOrder?.PlatformOrderNo ?? storeOrderId.ToString()
};
try
{
if (requireBind && (itemInput.BindItems == null || itemInput.BindItems.Count == 0))
{
throw new BusinessException("请先绑定包裹或库存");
}
var result = await CreateLogisticsCoreAsync(
customerId,
storeOrderId,
warehouseId,
resolvedProductCode,
itemInput.Receiver,
requireBind ? itemInput.BindItems : null);
item.Success = true;
item.Message = requireBind
? $"已创建物流单 {result.ServiceNo} 并绑定"
: $"已创建物流单 {result.ServiceNo}";
item.LinkedOrderId = result.OrderId;
item.ServiceNo = result.ServiceNo;
output.SuccessCount++;
}
catch (BusinessException ex)
{
item.Success = false;
item.Message = ex.Message;
output.FailCount++;
}
catch (Exception ex)
{
item.Success = false;
item.Message = ex.Message;
output.FailCount++;
}
output.Items.Add(item);
}
return output;
}
private async Task CreateLogisticsCoreAsync(
int customerId,
int storeOrderId,
int? warehouseId,
string productCode,
OrderParcelReceiverInput? receiver,
List? bindItems)
{
var order = await GetOrderOrThrowAsync(customerId, storeOrderId);
await EnsureMatchableAsync(order);
var lines = await StoreOrderLineRepository.GetListAsync(x => x.StoreOrderId == order.Id && !x.IsShipExcluded);
if (lines.Count == 0)
{
throw new BusinessException("没有可发货的商品行");
}
var resolvedWarehouseId = warehouseId ?? order.WarehouseId;
WarehouseAddress? warehouse = null;
if (resolvedWarehouseId is > 0)
{
warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == resolvedWarehouseId.Value)
?? throw new BusinessException("仓库不存在");
}
if (bindItems is { Count: > 0 } && resolvedWarehouseId is not > 0)
{
throw new BusinessException("请指定发货仓库");
}
var parcelInput = PlatformStoreOrderParcelBuilder.BuildFromPlatformLines(
order,
lines,
warehouse,
productCode,
resolvedWarehouseId,
customerId,
receiver);
var parcelOutput = await OrderService.CreateParcelForBatchAsync(
parcelInput,
CurrentUser.GetId(),
OrderSourceCodes.Platform,
null);
order.WarehouseId = resolvedWarehouseId ?? order.WarehouseId;
order.LinkedOrderId = parcelOutput.OrderId;
order.FulfillStatus = SalesStoreFulfillStatusCodes.ToShip;
order.LastSyncTime = DateTime.Now;
if (!string.IsNullOrWhiteSpace(parcelOutput.TrackNo))
{
order.ShippingLabelNo = parcelOutput.TrackNo;
}
await StoreOrderRepository.UpdateAsync(order);
if (bindItems is not { Count: > 0 })
{
return parcelOutput;
}
try
{
// 确保物流单带仓库,绑定校验依赖 WarehouseId
if (resolvedWarehouseId is > 0)
{
var logisticsOrder = await OrderRepository.FirstOrDefaultAsync(x => x.Id == parcelOutput.OrderId);
if (logisticsOrder != null && logisticsOrder.WarehouseId is not > 0)
{
logisticsOrder.WarehouseId = resolvedWarehouseId;
await OrderRepository.UpdateAsync(logisticsOrder);
}
}
await OrderService.SaveStockBindAsync(new OrderStockBindSaveInput
{
OrderId = parcelOutput.OrderId,
Items = bindItems
});
return parcelOutput;
}
catch (Exception)
{
await RollbackCreatedLogisticsAsync(order, parcelOutput.OrderId);
throw;
}
}
private async Task RollbackCreatedLogisticsAsync(SalesStoreOrder storeOrder, int logisticsOrderId)
{
try
{
await OrderService.ClearStockBindAsync(new OrderStockBindOrderInput { OrderId = logisticsOrderId });
}
catch
{
// 可能尚未写入绑定
}
storeOrder.LinkedOrderId = null;
storeOrder.FulfillStatus = SalesStoreFulfillStatusCodes.Pending;
storeOrder.LastSyncTime = DateTime.Now;
await StoreOrderRepository.UpdateAsync(storeOrder);
var logisticsOrder = await OrderRepository.FirstOrDefaultAsync(x => x.Id == logisticsOrderId);
if (logisticsOrder == null)
{
return;
}
logisticsOrder.OrderStatus = OrderStatusCodes.Deleted;
if (!string.IsNullOrWhiteSpace(logisticsOrder.RequestId))
{
logisticsOrder.RequestId = $"{logisticsOrder.RequestId}:rolled-back:{DateTime.Now.Ticks}";
}
await OrderRepository.UpdateAsync(logisticsOrder);
}
private async Task ResolveProductCodeRequiredAsync(string productCode)
{
var normalized = productCode.Trim();
var product = await SalesProductRepository.GetByProductCodeAsync(normalized);
if (product == null || product.IsActive != EnableStatus.Enabled)
{
throw new BusinessException("销售产品不存在或未启用");
}
return normalized;
}
private async Task MatchInventoryCoreAsync(
int customerId,
int storeOrderId,
int? warehouseId,
string? productCode,
string? trackNo,
OrderParcelReceiverInput? receiver)
{
var order = await GetOrderOrThrowAsync(customerId, storeOrderId);
await EnsureMatchableAsync(order);
var resolvedWarehouseId = warehouseId ?? order.WarehouseId;
if (!resolvedWarehouseId.HasValue || resolvedWarehouseId.Value <= 0)
{
throw new BusinessException("请指定发货仓库");
}
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == resolvedWarehouseId.Value)
?? throw new BusinessException("仓库不存在");
var lines = await StoreOrderLineRepository.GetListAsync(x => x.StoreOrderId == order.Id && !x.IsShipExcluded);
if (lines.Count == 0)
{
throw new BusinessException("没有可发货的商品行");
}
await ResolveMappedSkuLinesAsync(customerId, order, lines);
var requiredQty = lines
.Where(x => x.MappedSkuId.HasValue)
.GroupBy(x => x.MappedSkuId!.Value)
.ToDictionary(g => g.Key, g => g.Sum(x => x.Quantity));
await EnsureInventoryAvailableAsync(customerId, resolvedWarehouseId.Value, requiredQty);
var skuIds = requiredQty.Keys.ToArray();
var skus = await SkuRepository.GetListAsync(x => x.CustomerId == customerId && skuIds.Contains(x.Id));
var skuMap = skus.ToDictionary(x => x.Id);
if (skuMap.Count != skuIds.Length)
{
throw new BusinessException("部分库内 SKU 不存在");
}
var resolvedProductCode = await ResolveProductCodeAsync(productCode);
var parcelInput = PlatformStoreOrderParcelBuilder.Build(
order,
lines,
skuMap,
warehouse,
resolvedProductCode,
resolvedWarehouseId.Value,
customerId,
receiver,
trackNo);
List deductions = [];
try
{
foreach (var (skuId, qty) in requiredQty)
{
deductions.AddRange(await DeductInventoryAsync(customerId, resolvedWarehouseId.Value, skuId, qty));
}
var parcelOutput = await OrderService.CreateParcelForBatchAsync(parcelInput, CurrentUser.GetId());
order.WarehouseId = resolvedWarehouseId;
order.LinkedOrderId = parcelOutput.OrderId;
order.FulfillStatus = SalesStoreFulfillStatusCodes.ToShip;
order.LastSyncTime = DateTime.Now;
if (!string.IsNullOrWhiteSpace(parcelOutput.TrackNo))
{
order.ShippingLabelNo = parcelOutput.TrackNo;
}
await StoreOrderRepository.UpdateAsync(order);
return parcelOutput;
}
catch
{
await RestoreInventoryAsync(customerId, resolvedWarehouseId.Value, deductions);
throw;
}
}
private async Task EnsureMatchableAsync(SalesStoreOrder order)
{
if (order.IsSuspended)
{
throw new BusinessException("订单已搁置,不能匹配库存");
}
if (order.FulfillStatus is SalesStoreFulfillStatusCodes.Shipped or SalesStoreFulfillStatusCodes.Completed)
{
throw new BusinessException("订单已发货或完成,不能重复匹配");
}
if (order.LinkedOrderId is > 0)
{
var linked = await OrderRepository.FirstOrDefaultAsync(x => x.Id == order.LinkedOrderId.Value);
if (linked != null && linked.OrderStatus != OrderStatusCodes.Deleted)
{
throw new BusinessException($"已关联小包订单 {linked.ServiceNo},不能重复匹配");
}
}
}
private async Task ResolveMappedSkuLinesAsync(int customerId, SalesStoreOrder order, List lines)
{
var mappings = await SkuPlatformMapRepository.GetListAsync(x => x.CustomerId == customerId && x.StoreId == order.StoreId);
var mappingDict = mappings.ToDictionary(x => x.PlatformSku, x => x, StringComparer.OrdinalIgnoreCase);
foreach (var line in lines)
{
if (line.MappedSkuId.HasValue)
{
continue;
}
if (!string.IsNullOrWhiteSpace(line.PlatformSku) && mappingDict.TryGetValue(line.PlatformSku, out var map))
{
line.MappedSkuId = map.SkuId;
await StoreOrderLineRepository.UpdateAsync(line);
continue;
}
throw new BusinessException($"平台 SKU「{line.PlatformSku}」未映射库内 SKU");
}
}
private async Task EnsureInventoryAvailableAsync(int customerId, int warehouseId, Dictionary requiredQty)
{
foreach (var (skuId, qty) in requiredQty)
{
var total = await InventoryRepository.GetListAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == warehouseId
&& x.SkuId == skuId
&& (x.AvailableQty > 0 || x.Qty - x.FrozenQty > 0));
var available = total.Sum(x => Math.Max(0, x.Qty - x.FrozenQty));
if (available < qty)
{
throw new BusinessException($"SKU {skuId} 库存不足,需要 {qty},可用 {available}");
}
}
}
private async Task ResolveProductCodeAsync(string? productCode)
{
var normalized = productCode?.Trim();
if (!string.IsNullOrWhiteSpace(normalized))
{
var product = await SalesProductRepository.GetByProductCodeAsync(normalized);
if (product == null || product.IsActive != EnableStatus.Enabled)
{
throw new BusinessException("销售产品不存在或未启用");
}
return normalized;
}
var products = await SalesProductRepository.GetListAsync(x => x.IsActive == EnableStatus.Enabled);
var first = products.OrderBy(x => x.ProductCode).FirstOrDefault();
if (first == null || string.IsNullOrWhiteSpace(first.ProductCode))
{
throw new BusinessException("请指定销售产品编码");
}
return first.ProductCode;
}
private async Task> DeductInventoryAsync(int customerId, int warehouseId, int skuId, int requiredQty)
{
var rows = await InventoryRepository.GetListAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == warehouseId
&& x.SkuId == skuId
&& (x.AvailableQty > 0 || x.Qty > x.FrozenQty));
rows = rows.OrderBy(x => x.BinLocationId).ToList();
var deductions = new List();
var remaining = requiredQty;
foreach (var row in rows)
{
if (remaining <= 0)
{
break;
}
var available = Math.Max(0, row.Qty - row.FrozenQty);
var deduct = Math.Min(available, remaining);
if (deduct <= 0)
{
continue;
}
row.Qty -= deduct;
row.AvailableQty = Math.Max(0, row.Qty - row.FrozenQty);
remaining -= deduct;
await InventoryRepository.UpdateAsync(row);
deductions.Add(new InventoryDeduction(skuId, row.BinLocationId, deduct));
}
if (remaining > 0)
{
throw new BusinessException($"SKU {skuId} 库存扣减失败");
}
return deductions;
}
private async Task RestoreInventoryAsync(int customerId, int warehouseId, IReadOnlyCollection deductions)
{
foreach (var item in deductions)
{
var inventory = await InventoryRepository.FirstOrDefaultAsync(x =>
x.CustomerId == customerId
&& x.WarehouseId == warehouseId
&& x.SkuId == item.SkuId
&& x.BinLocationId == item.BinLocationId);
if (inventory == null)
{
inventory = new Domain.WarehouseAggregate.Entities.CInventory
{
CustomerId = customerId,
WarehouseId = warehouseId,
SkuId = item.SkuId,
BinLocationId = item.BinLocationId,
Qty = item.Qty,
FrozenQty = 0,
AvailableQty = item.Qty
};
await InventoryRepository.InsertAsync(inventory);
continue;
}
inventory.Qty += item.Qty;
inventory.AvailableQty = Math.Max(0, inventory.Qty - inventory.FrozenQty);
await InventoryRepository.UpdateAsync(inventory);
}
}
private async Task ResolveLinkedServiceNoAsync(int? linkedOrderId)
{
if (linkedOrderId is not > 0)
{
return null;
}
var linked = await OrderRepository.FirstOrDefaultAsync(x => x.Id == linkedOrderId.Value);
return linked?.ServiceNo;
}
private async Task GetOrderOrThrowAsync(int? customerId, int id)
{
var entity = customerId.HasValue
? await StoreOrderRepository.FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId.Value)
: await StoreOrderRepository.FirstOrDefaultAsync(x => x.Id == id);
if (entity == null)
{
throw new BusinessException("店铺订单不存在");
}
return entity;
}
private static Expression> BuildListPredicate(int? customerId, PlatformStoreOrderListInput input)
{
var (hasCustomerFilter, customerIdValue) = WarehouseCustomerHelper.ToCustomerFilter(customerId);
// 前端 Tab「全部」会传 all / 空;需忽略,不能按字面 FulfillStatus == "all" 过滤
var fulfillStatus = NormalizeFulfillStatusFilter(input.FulfillStatus);
var hasFulfillStatusFilter = !string.IsNullOrWhiteSpace(fulfillStatus);
var platformOrderNo = input.PlatformOrderNo?.Trim();
var hasStoreFilter = input.StoreId.HasValue;
var storeIdValue = input.StoreId.GetValueOrDefault();
var hasPlatformOrderNoFilter = !string.IsNullOrWhiteSpace(platformOrderNo);
var hasSuspendedFilter = input.IsSuspended.HasValue;
var isSuspendedValue = input.IsSuspended.GetValueOrDefault();
return hasFulfillStatusFilter
? x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasStoreFilter || x.StoreId == storeIdValue)
&& x.FulfillStatus == fulfillStatus
&& (!hasSuspendedFilter || x.IsSuspended == isSuspendedValue)
&& (!hasPlatformOrderNoFilter || x.PlatformOrderNo.Contains(platformOrderNo!))
: x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasStoreFilter || x.StoreId == storeIdValue)
&& (!hasSuspendedFilter || x.IsSuspended == isSuspendedValue)
&& (!hasPlatformOrderNoFilter || x.PlatformOrderNo.Contains(platformOrderNo!));
}
///
/// 规范化履约状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。
///
private static string? NormalizeFulfillStatusFilter(string? status)
{
var value = status?.Trim();
if (string.IsNullOrEmpty(value)
|| value.Equals("all", StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Pending, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Pending;
}
if (value.Equals(SalesStoreFulfillStatusCodes.ToShip, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.ToShip;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Canceled, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Canceled;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Suspended, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Suspended;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Shipped, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Shipped;
}
if (value.Equals(SalesStoreFulfillStatusCodes.Completed, StringComparison.OrdinalIgnoreCase))
{
return SalesStoreFulfillStatusCodes.Completed;
}
return value;
}
private static PlatformStoreOrderOutput MapOrderOutput(
SalesStoreOrder order,
string storeNickname,
IReadOnlyCollection lines,
IReadOnlyDictionary? skuMap = null,
string? linkedServiceNo = null,
string? customerName = null,
string? customerCode = null) =>
new()
{
Id = order.Id,
CustomerId = order.CustomerId,
CustomerName = customerName,
CustomerCode = customerCode,
StoreId = order.StoreId,
StoreNickname = storeNickname,
PlatformCode = order.PlatformCode,
PlatformOrderNo = order.PlatformOrderNo,
OrderCountry = order.OrderCountry,
RecipientName = order.RecipientName,
RecipientCompany = order.RecipientCompany,
RecipientPhone = order.RecipientPhone,
RecipientTelePhone = order.RecipientTelePhone,
RecipientEmail = order.RecipientEmail,
RecipientCountry = order.RecipientCountry,
RecipientProvince = order.RecipientProvince,
RecipientCity = order.RecipientCity,
RecipientZipCode = order.RecipientZipCode,
RecipientAddress1 = order.RecipientAddress1,
RecipientAddress2 = order.RecipientAddress2,
ItemCount = order.ItemCount,
TotalAmount = order.TotalAmount,
Currency = order.Currency,
PlatformOrderTime = order.PlatformOrderTime,
FulfillStatus = order.FulfillStatus,
PlatformStatusLabel = order.PlatformStatusLabel,
ShippingLabelNo = order.ShippingLabelNo,
ShippingLabelUrl = order.ShippingLabelUrl,
IsMultiLabel = order.IsMultiLabel,
CustomTags = order.CustomTags,
IsSuspended = order.IsSuspended,
WarehouseId = order.WarehouseId,
LinkedOrderId = order.LinkedOrderId,
LinkedServiceNo = linkedServiceNo,
LastSyncTime = order.LastSyncTime,
Lines = lines.Select(line =>
{
string? mappedSkuCode = null;
if (line.MappedSkuId.HasValue && skuMap != null && skuMap.TryGetValue(line.MappedSkuId.Value, out var sku))
{
mappedSkuCode = sku.SkuCode;
}
return new PlatformStoreOrderLineOutput
{
Id = line.Id,
PlatformLineId = line.PlatformLineId,
PlatformSku = line.PlatformSku,
ProductName = line.ProductName,
ProductImageUrl = line.ProductImageUrl,
VariantText = line.VariantText,
UnitPrice = line.UnitPrice,
Quantity = line.Quantity,
Currency = line.Currency,
IsShipExcluded = line.IsShipExcluded,
MappedSkuId = line.MappedSkuId,
MappedSkuCode = mappedSkuCode
};
}).ToList()
};
private sealed record InventoryDeduction(int SkuId, int BinLocationId, int Qty);
}