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; /// /// 店铺订单 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(); [HttpPost("/order/platform-store-order/list")] public async Task> GetListAsync(PlatformStoreOrderListInput input) { var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); var predicate = BuildListPredicate(customerId, input); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); var entities = await StoreOrderRepository.GetPagedListAsync( predicate, pageIndex, pageSize, new Dictionary { [nameof(SalesStoreOrder.UpdateTime)] = false, [nameof(SalesStoreOrder.Id)] = false }); var total = await StoreOrderRepository.CountAsync(predicate); 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 { 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 }; } [HttpGet("/order/platform-store-order/detail")] public async Task GetDetailAsync([FromQuery] int id, [FromQuery] int? customerId = null) { var resolvedCustomerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( 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); } [HttpGet("/order/platform-store-order/summary")] public async Task GetSummaryAsync([FromQuery] int? customerId = null, [FromQuery] int? storeId = null) { var resolvedCustomerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( CustomerRepository, CurrentUser, customerId); var orders = await StoreOrderRepository.GetListAsync(x => x.CustomerId == resolvedCustomerId && (!storeId.HasValue || x.StoreId == storeId.Value)); 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/sync-status")] public async Task SyncStatusAsync(PlatformStoreOrderIdsInput input) { await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); return new PlatformStoreOrderActionOutput { Message = "平台订单同步待 SDK 接入,当前为占位实现" }; } [HttpPost("/order/platform-store-order/ready-to-ship")] public async Task ReadyToShipAsync(PlatformStoreOrderIdsInput input) { await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); return new PlatformStoreOrderActionOutput { Message = "RTS 待平台 SDK 接入,当前为占位实现" }; } [HttpPost("/order/platform-store-order/fetch-label")] public async Task FetchLabelAsync(PlatformStoreOrderIdInput input) { await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); return new PlatformStoreOrderActionOutput { Message = "面单拉取待平台 SDK 接入,当前为占位实现" }; } [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); } [HttpPost("/order/platform-store-order/delete")] public async Task DeleteAsync(PlatformStoreOrderIdsInput input) { if (input.Ids.Count == 0) { throw new BusinessException("请选择要删除的订单"); } var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync( CustomerRepository, CurrentUser, input.CustomerId); var orders = await StoreOrderRepository.GetListAsync(x => x.CustomerId == customerId && 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); } } [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 }; } [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; } 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.Qty > 0); var available = total.Sum(x => x.Qty); 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.Qty > 0); rows = rows.OrderBy(x => x.BinLocationId).ToList(); var deductions = new List(); 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 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 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 = await StoreOrderRepository.FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId); if (entity == null) { throw new BusinessException("店铺订单不存在"); } return entity; } private static Expression> BuildListPredicate(int customerId, PlatformStoreOrderListInput input) { var fulfillStatus = input.FulfillStatus?.Trim(); var platformOrderNo = input.PlatformOrderNo?.Trim(); return x => x.CustomerId == customerId && (!input.StoreId.HasValue || x.StoreId == input.StoreId.Value) && (string.IsNullOrWhiteSpace(fulfillStatus) || x.FulfillStatus == fulfillStatus) && (!input.IsSuspended.HasValue || x.IsSuspended == input.IsSuspended.Value) && (string.IsNullOrWhiteSpace(platformOrderNo) || x.PlatformOrderNo.Contains(platformOrderNo)); } private static PlatformStoreOrderOutput MapOrderOutput( SalesStoreOrder order, string storeNickname, IReadOnlyCollection lines, IReadOnlyDictionary? 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); }