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); }