738 lines
30 KiB
C#
738 lines
30 KiB
C#
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.Repositories;
|
||
using Oms.Domain.WarehouseAggregate.Consts;
|
||
using Oms.Domain.WarehouseAggregate.Repositories;
|
||
|
||
namespace Oms.Application.Services.Orders;
|
||
|
||
/// <summary>
|
||
/// 店铺订单 API 服务。
|
||
/// </summary>
|
||
[ApiExplorerSettings(GroupName = "OMS-Order")]
|
||
public class PlatformStoreOrderService : ApplicationService
|
||
{
|
||
private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||
private ISalesStoreRepository StoreRepository => LazyServiceProvider.GetRequiredService<ISalesStoreRepository>();
|
||
private ISalesStoreOrderRepository StoreOrderRepository => LazyServiceProvider.GetRequiredService<ISalesStoreOrderRepository>();
|
||
private ISalesStoreOrderLineRepository StoreOrderLineRepository => LazyServiceProvider.GetRequiredService<ISalesStoreOrderLineRepository>();
|
||
private ICSkuRepository SkuRepository => LazyServiceProvider.GetRequiredService<ICSkuRepository>();
|
||
private ICSkuPlatformMapRepository SkuPlatformMapRepository => LazyServiceProvider.GetRequiredService<ICSkuPlatformMapRepository>();
|
||
private ICInventoryRepository InventoryRepository => LazyServiceProvider.GetRequiredService<ICInventoryRepository>();
|
||
private IOrderRepository OrderRepository => LazyServiceProvider.GetRequiredService<IOrderRepository>();
|
||
private IWarehouseAddressRepository WarehouseAddressRepository => LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
||
private ISalesProductRepository SalesProductRepository => LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
|
||
private OrderService OrderService => LazyServiceProvider.GetRequiredService<OrderService>();
|
||
private PlatformStoreOrderFulfillmentService FulfillmentService =>
|
||
LazyServiceProvider.GetRequiredService<PlatformStoreOrderFulfillmentService>();
|
||
|
||
/// <summary>
|
||
/// 查询店铺订单列表。
|
||
/// </summary>
|
||
/// <param name="input">入参:店铺订单分页查询条件。</param>
|
||
/// <returns>店铺订单分页结果。</returns>
|
||
[HttpPost("/order/platform-store-order/list")]
|
||
public async Task<OmsPageResult<PlatformStoreOrderOutput>> 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);
|
||
|
||
return new OmsPageResult<PlatformStoreOrderOutput>
|
||
{
|
||
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;
|
||
}
|
||
|
||
return MapOrderOutput(x, store?.StoreNickname ?? string.Empty, [], linkedServiceNo: linkedServiceNo);
|
||
}).ToList(),
|
||
Total = total
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询店铺订单详情。
|
||
/// </summary>
|
||
/// <param name="id">店铺订单 ID。</param>
|
||
/// <param name="customerId">客户 ID(可选)。</param>
|
||
/// <returns>店铺订单详情(含商品行)。</returns>
|
||
[HttpGet("/order/platform-store-order/detail")]
|
||
public async Task<PlatformStoreOrderOutput> 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);
|
||
|
||
return MapOrderOutput(order, store?.StoreNickname ?? string.Empty, lines, skuMap, linkedServiceNo);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 查询店铺订单状态汇总。
|
||
/// </summary>
|
||
/// <param name="customerId">客户 ID(可选)。</param>
|
||
/// <param name="storeId">店铺 ID(可选)。</param>
|
||
/// <returns>各履约状态数量汇总。</returns>
|
||
[HttpGet("/order/platform-store-order/summary")]
|
||
public async Task<PlatformStoreOrderSummaryOutput> 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)
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从平台拉取店铺订单并入库。
|
||
/// </summary>
|
||
/// <param name="input">入参:店铺、时间范围、平台状态。</param>
|
||
/// <returns>拉单结果(按店铺汇总)。</returns>
|
||
[HttpPost("/order/platform-store-order/pull-orders")]
|
||
public async Task<PlatformStoreOrderBatchActionOutput> PullOrdersAsync(PlatformStoreOrderPullInput input)
|
||
{
|
||
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
input.CustomerId);
|
||
|
||
return await FulfillmentService.PullOrdersAsync(customerId, input);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 同步店铺订单状态(从平台刷新选中订单)。
|
||
/// </summary>
|
||
/// <param name="input">入参:订单 ID 列表。</param>
|
||
/// <returns>同步结果(成功/失败明细)。</returns>
|
||
[HttpPost("/order/platform-store-order/sync-status")]
|
||
public async Task<PlatformStoreOrderBatchActionOutput> SyncStatusAsync(PlatformStoreOrderIdsInput input)
|
||
{
|
||
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
input.CustomerId);
|
||
|
||
return await FulfillmentService.SyncStatusAsync(customerId, input);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 店铺订单 RTS 就绪发货。
|
||
/// </summary>
|
||
/// <param name="input">入参:订单 ID 列表。</param>
|
||
/// <returns>操作结果(成功/失败明细)。</returns>
|
||
[HttpPost("/order/platform-store-order/ready-to-ship")]
|
||
public async Task<PlatformStoreOrderBatchActionOutput> ReadyToShipAsync(PlatformStoreOrderIdsInput input)
|
||
{
|
||
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
input.CustomerId);
|
||
|
||
return await FulfillmentService.ReadyToShipAsync(customerId, input);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 重新拉取店铺订单面单。
|
||
/// </summary>
|
||
/// <param name="input">入参:订单 ID。</param>
|
||
/// <returns>面单号与下载地址。</returns>
|
||
[HttpPost("/order/platform-store-order/fetch-label")]
|
||
public async Task<PlatformStoreOrderActionOutput> FetchLabelAsync(PlatformStoreOrderIdInput input)
|
||
{
|
||
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
input.CustomerId);
|
||
|
||
return await FulfillmentService.FetchLabelAsync(customerId, input);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 切换店铺订单搁置状态。
|
||
/// </summary>
|
||
/// <param name="input">入参:订单 ID 与是否搁置。</param>
|
||
[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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除店铺订单。
|
||
/// </summary>
|
||
/// <param name="input">入参:订单 ID 列表。</param>
|
||
[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);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 店铺订单匹配库存并创建小包单。
|
||
/// </summary>
|
||
/// <param name="input">入参:订单 ID、仓库、销售产品等。</param>
|
||
/// <returns>匹配结果与关联小包单号。</returns>
|
||
[HttpPost("/order/platform-store-order/match-inventory")]
|
||
public async Task<PlatformStoreOrderMatchInventoryOutput> 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
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量匹配店铺订单库存并创建小包单。
|
||
/// </summary>
|
||
/// <param name="input">入参:订单 ID 列表、仓库、销售产品等。</param>
|
||
/// <returns>批量匹配结果(成功/失败明细)。</returns>
|
||
[HttpPost("/order/platform-store-order/batch-match-inventory")]
|
||
public async Task<PlatformStoreOrderBatchMatchInventoryOutput> 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;
|
||
}
|
||
|
||
private async Task<OrderParcelCreateOutput> 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<InventoryDeduction> 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<int>());
|
||
|
||
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<SalesStoreOrderLine> 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<int, int> requiredQty)
|
||
{
|
||
foreach (var (skuId, qty) in requiredQty)
|
||
{
|
||
var total = await InventoryRepository.GetListAsync(x =>
|
||
x.CustomerId == customerId
|
||
&& x.WarehouseId == warehouseId
|
||
&& x.SkuId == skuId
|
||
&& x.Qty > 0);
|
||
var available = total.Sum(x => x.Qty);
|
||
if (available < qty)
|
||
{
|
||
throw new BusinessException($"SKU {skuId} 库存不足,需要 {qty},可用 {available}");
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task<string> 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<List<InventoryDeduction>> 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.Qty > 0);
|
||
rows = rows.OrderBy(x => x.BinLocationId).ToList();
|
||
|
||
var deductions = new List<InventoryDeduction>();
|
||
var remaining = requiredQty;
|
||
foreach (var row in rows)
|
||
{
|
||
if (remaining <= 0)
|
||
{
|
||
break;
|
||
}
|
||
|
||
var deduct = Math.Min(row.Qty, remaining);
|
||
row.Qty -= deduct;
|
||
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<InventoryDeduction> 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
|
||
};
|
||
await InventoryRepository.InsertAsync(inventory);
|
||
continue;
|
||
}
|
||
|
||
inventory.Qty += item.Qty;
|
||
await InventoryRepository.UpdateAsync(inventory);
|
||
}
|
||
}
|
||
|
||
private async Task<string?> 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<SalesStoreOrder> 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<Func<SalesStoreOrder, bool>> 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!));
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规范化履约状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。
|
||
/// </summary>
|
||
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<SalesStoreOrderLine> lines,
|
||
IReadOnlyDictionary<int, Domain.WarehouseAggregate.Entities.CSku>? skuMap = null,
|
||
string? linkedServiceNo = null) =>
|
||
new()
|
||
{
|
||
Id = order.Id,
|
||
CustomerId = order.CustomerId,
|
||
StoreId = order.StoreId,
|
||
StoreNickname = storeNickname,
|
||
PlatformCode = order.PlatformCode,
|
||
PlatformOrderNo = order.PlatformOrderNo,
|
||
OrderCountry = order.OrderCountry,
|
||
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);
|
||
}
|