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