From fd9f57bac14c98bf64dd2d4699318a1738797273 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Sat, 18 Jul 2026 12:07:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20SKU/=E5=85=A5=E5=BA=93=E9=A2=84?= =?UTF-8?q?=E6=8A=A5=E8=BF=94=E5=9B=9E=E5=AE=A2=E6=88=B7=E4=BF=A1=E6=81=AF?= =?UTF-8?q?=EF=BC=8C=E5=BA=93=E5=AD=98=E5=87=BA=E5=85=A5=E5=BA=93=E5=8E=86?= =?UTF-8?q?=E5=8F=B2=E4=B8=8E=E5=8F=AF=E7=94=A8=E5=86=BB=E7=BB=93=E9=87=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../Dtos/Warehouse/WarehouseDtos.cs | 47 ++++++ .../Warehouse/WarehouseService.Inbound.cs | 18 ++- .../Warehouse/WarehouseService.Inventory.cs | 144 +++++++++++++++++- .../Warehouse/WarehouseService.Parcel.cs | 40 +++-- .../Warehouse/WarehouseService.Sku.cs | 18 ++- 5 files changed, 248 insertions(+), 19 deletions(-) diff --git a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs index 5aa9e78..5b0e84f 100644 --- a/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs +++ b/src/Oms.Application/Dtos/Warehouse/WarehouseDtos.cs @@ -24,6 +24,10 @@ public class WarehouseSkuOutput public int CustomerId { get; set; } + public string? CustomerName { get; set; } + + public string? CustomerCode { get; set; } + public string SkuCode { get; set; } = string.Empty; public string SkuName { get; set; } = string.Empty; @@ -166,6 +170,10 @@ public class WarehouseInboundOutput public int CustomerId { get; set; } + public string? CustomerName { get; set; } + + public string? CustomerCode { get; set; } + public int WarehouseId { get; set; } public string NoticeNo { get; set; } = string.Empty; @@ -389,6 +397,15 @@ public class WarehouseParcelOutput public string? Remark { get; set; } + /// 商品总数量。 + public int TotalQty { get; set; } + + /// 已冻结数量(订单绑定占用)。 + public int FrozenQty { get; set; } + + /// 可用数量 = 总量 - 冻结。 + public int AvailableQty { get; set; } + public List Items { get; set; } = []; public DateTime CreateTime { get; set; } @@ -505,6 +522,36 @@ public class WarehouseInventoryRelocateInput public int Qty { get; set; } } +public class WarehouseInventoryHistoryInput : WarehousePageInput +{ + [Required] + public int SkuId { get; set; } + + [Required] + public int WarehouseId { get; set; } + + /// + /// 流水类型:all / inbound / deduct;空视为 all。 + /// + public string? MovementType { get; set; } +} + +public class WarehouseInventoryHistoryOutput +{ + /// inbound 入库 / deduct 扣减。 + public string MovementType { get; set; } = string.Empty; + + /// 变动数量(入库为正,扣减为正数,前端按类型展示符号)。 + public int Qty { get; set; } + + /// 关联单号:入库预报号 / 物流单号。 + public string? RefNo { get; set; } + + public string? Remark { get; set; } + + public DateTime OccurredAt { get; set; } +} + // --- Parcel arrival(快递到仓)--- public class WarehouseParcelArrivalListInput : WarehousePageInput diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Inbound.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Inbound.cs index 6471831..f4ce3cf 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.Inbound.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Inbound.cs @@ -58,9 +58,25 @@ public partial class WarehouseService ? [] : await InboundBoxLineRepository.GetListAsync(x => boxIds.Contains(x.InboundBoxId)); + 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 => MapInboundOutput(x, boxes, lines)).ToList(), + Items = entities.Select(x => + { + var output = MapInboundOutput(x, boxes, lines); + if (customerMap.TryGetValue(x.CustomerId, out var customer)) + { + output.CustomerName = customer.CustomerName; + output.CustomerCode = customer.CustomerCode; + } + + return output; + }).ToList(), Total = total }; } diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Inventory.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Inventory.cs index e40e552..0aca173 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.Inventory.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Inventory.cs @@ -3,11 +3,19 @@ using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Warehouse; using Oms.Application.Shared; +using Oms.Domain.OrdersAggregate.Consts; +using Oms.Domain.OrdersAggregate.Repositories; namespace Oms.Application.Services.Warehouse; public partial class WarehouseService { + private IOrderStockBindRepository OrderStockBindRepository => + LazyServiceProvider.GetRequiredService(); + + private IOrderRepository OrderRepository => + LazyServiceProvider.GetRequiredService(); + /// /// 查询库存列表。 /// @@ -20,9 +28,10 @@ public partial class WarehouseService var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId); var hasWarehouseFilter = input.WarehouseId.HasValue; var warehouseIdValue = input.WarehouseId.GetValueOrDefault(); + // 有总量即展示(含全部冻结、可用为 0 的行),避免客户看不到被占用库存 var inventories = await InventoryRepository.GetListAsync(x => (!hasCustomerFilter || x.CustomerId == customerIdValue) - && x.AvailableQty > 0 + && x.Qty > 0 && (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)); var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray(); @@ -165,4 +174,137 @@ public partial class WarehouseService await UpsertInventoryQtyAsync(customerId, input.WarehouseId, input.SkuId, input.SourceBinLocationId, -input.Qty); await UpsertInventoryQtyAsync(customerId, input.WarehouseId, input.SkuId, input.TargetBinLocationId, input.Qty); } + + /// + /// 查询 SKU 库存变动历史:入库收货 + 订单扣减。 + /// + [HttpPost("/warehouse/inventory/history")] + public async Task> GetInventoryHistoryAsync( + WarehouseInventoryHistoryInput input) + { + if (input.SkuId <= 0 || input.WarehouseId <= 0) + { + throw new BusinessException("SKU 和仓库不能为空"); + } + + var customerId = await ResolveListCustomerIdAsync(input.CustomerId); + var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId); + await GetSkuOrThrowAsync(customerId, input.SkuId); + + var movementType = (input.MovementType ?? "all").Trim().ToLowerInvariant(); + if (movementType is not ("all" or "inbound" or "deduct")) + { + movementType = "all"; + } + + var rows = new List(); + + if (movementType is "all" or "inbound") + { + var notices = await InboundNoticeRepository.GetListAsync(x => + (!hasCustomerFilter || x.CustomerId == customerIdValue) + && x.WarehouseId == input.WarehouseId); + var noticeMap = notices.ToDictionary(x => x.Id); + var noticeIds = notices.Select(x => x.Id).ToArray(); + var boxes = noticeIds.Length == 0 + ? [] + : await InboundBoxRepository.GetListAsync(x => noticeIds.Contains(x.InboundNoticeId)); + var boxMap = boxes.ToDictionary(x => x.Id); + var boxIds = boxes.Select(x => x.Id).ToArray(); + var lines = boxIds.Length == 0 + ? [] + : await InboundBoxLineRepository.GetListAsync(x => + boxIds.Contains(x.InboundBoxId) + && x.SkuId == input.SkuId + && x.ReceivedQty != null + && x.ReceivedQty > 0); + + foreach (var line in lines) + { + if (!boxMap.TryGetValue(line.InboundBoxId, out var box)) + { + continue; + } + + noticeMap.TryGetValue(box.InboundNoticeId, out var notice); + var occurredAt = line.UpdateTime != default ? line.UpdateTime : line.CreateTime; + if (notice?.ActualArrivalTime != null) + { + occurredAt = notice.ActualArrivalTime.Value; + } + + rows.Add(new WarehouseInventoryHistoryOutput + { + MovementType = "inbound", + Qty = line.ReceivedQty.GetValueOrDefault(), + RefNo = notice?.NoticeNo, + Remark = string.IsNullOrWhiteSpace(box.MarkBoxNo) + ? null + : $"箱 {box.MarkBoxNo}", + OccurredAt = occurredAt + }); + } + } + + if (movementType is "all" or "deduct") + { + var binds = await OrderStockBindRepository.GetListAsync(x => + (!hasCustomerFilter || x.CustomerId == customerIdValue) + && x.WarehouseId == input.WarehouseId + && x.SkuId == input.SkuId + && x.BindType == OrderStockBindTypeCodes.Inventory + && x.Status == OrderStockBindStatusCodes.Deducted + && x.BindQty > 0); + + var orderIds = binds.Select(x => x.OrderId).Distinct().ToArray(); + var orders = orderIds.Length == 0 + ? [] + : await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id)); + var orderMap = orders.ToDictionary(x => x.Id); + + foreach (var bind in binds) + { + orderMap.TryGetValue(bind.OrderId, out var order); + var refNo = order?.ServiceNo; + if (string.IsNullOrWhiteSpace(refNo)) + { + refNo = order?.TrackNo; + } + + if (string.IsNullOrWhiteSpace(refNo)) + { + refNo = order?.ClientNo; + } + + if (string.IsNullOrWhiteSpace(refNo)) + { + refNo = $"#{bind.OrderId}"; + } + + rows.Add(new WarehouseInventoryHistoryOutput + { + MovementType = "deduct", + Qty = bind.BindQty, + RefNo = refNo, + Remark = string.IsNullOrWhiteSpace(bind.Remark) ? "订单出库扣减" : bind.Remark, + OccurredAt = bind.UpdateTime != default ? bind.UpdateTime : bind.CreateTime + }); + } + } + + var ordered = rows + .OrderByDescending(x => x.OccurredAt) + .ThenByDescending(x => x.RefNo) + .ToList(); + + var pageIndex = OmsDocHelper.NormalizePage(input.Page); + var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); + var skip = (pageIndex - 1) * pageSize; + + return new OmsPageResult + { + Items = ordered.Skip(skip).Take(pageSize).ToList(), + Total = ordered.Count + }; + } } diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs index 53b1641..3f7cbff 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs @@ -308,8 +308,25 @@ public partial class WarehouseService } } - private static WarehouseParcelOutput MapParcelOutput(CParcel parcel, IEnumerable items) => - new() + private static WarehouseParcelOutput MapParcelOutput(CParcel parcel, IEnumerable items) + { + var itemList = items.ToList(); + var mappedItems = itemList.Select(item => new WarehouseParcelItemOutput + { + Id = item.Id, + ProductName = item.ProductName, + AttributeText = item.AttributeText, + ImageUrl = item.ImageUrl, + Quantity = item.Quantity, + FrozenQty = item.FrozenQty, + AvailableQty = item.AvailableQty > 0 ? item.AvailableQty : Math.Max(0, item.Quantity - item.FrozenQty), + SkuId = item.SkuId, + SkuCode = item.SkuCode, + BinLocationId = item.BinLocationId, + Remark = item.Remark + }).ToList(); + + return new WarehouseParcelOutput { Id = parcel.Id, CustomerId = parcel.CustomerId, @@ -320,19 +337,10 @@ public partial class WarehouseService ArrivalTime = parcel.ArrivalTime, Remark = parcel.Remark, CreateTime = parcel.CreateTime, - Items = items.Select(item => new WarehouseParcelItemOutput - { - Id = item.Id, - ProductName = item.ProductName, - AttributeText = item.AttributeText, - ImageUrl = item.ImageUrl, - Quantity = item.Quantity, - FrozenQty = item.FrozenQty, - AvailableQty = item.AvailableQty > 0 ? item.AvailableQty : Math.Max(0, item.Quantity - item.FrozenQty), - SkuId = item.SkuId, - SkuCode = item.SkuCode, - BinLocationId = item.BinLocationId, - Remark = item.Remark - }).ToList() + TotalQty = mappedItems.Sum(x => x.Quantity), + FrozenQty = mappedItems.Sum(x => x.FrozenQty), + AvailableQty = mappedItems.Sum(x => x.AvailableQty), + Items = mappedItems }; + } } diff --git a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs index c785b9a..ecd753e 100644 --- a/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs +++ b/src/Oms.Application/Services/Warehouse/WarehouseService.Sku.cs @@ -49,9 +49,25 @@ public partial class WarehouseService .GroupBy(x => x.SkuId) .ToDictionary(g => g.Key, g => g.Sum(x => x.Qty)); + 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 => MapSkuOutput(x, qtyMap.GetValueOrDefault(x.Id))).ToList(), + Items = entities.Select(x => + { + var output = MapSkuOutput(x, qtyMap.GetValueOrDefault(x.Id)); + if (customerMap.TryGetValue(x.CustomerId, out var customer)) + { + output.CustomerName = customer.CustomerName; + output.CustomerCode = customer.CustomerCode; + } + + return output; + }).ToList(), Total = total }; }