feat: SKU/入库预报返回客户信息,库存出入库历史与可用冻结量

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-07-18 12:07:07 +08:00
parent f4ddd993b9
commit fd9f57bac1
5 changed files with 248 additions and 19 deletions

View File

@ -24,6 +24,10 @@ public class WarehouseSkuOutput
public int CustomerId { get; set; } public int CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? CustomerCode { get; set; }
public string SkuCode { get; set; } = string.Empty; public string SkuCode { get; set; } = string.Empty;
public string SkuName { get; set; } = string.Empty; public string SkuName { get; set; } = string.Empty;
@ -166,6 +170,10 @@ public class WarehouseInboundOutput
public int CustomerId { get; set; } public int CustomerId { get; set; }
public string? CustomerName { get; set; }
public string? CustomerCode { get; set; }
public int WarehouseId { get; set; } public int WarehouseId { get; set; }
public string NoticeNo { get; set; } = string.Empty; public string NoticeNo { get; set; } = string.Empty;
@ -389,6 +397,15 @@ public class WarehouseParcelOutput
public string? Remark { get; set; } public string? Remark { get; set; }
/// <summary>商品总数量。</summary>
public int TotalQty { get; set; }
/// <summary>已冻结数量(订单绑定占用)。</summary>
public int FrozenQty { get; set; }
/// <summary>可用数量 = 总量 - 冻结。</summary>
public int AvailableQty { get; set; }
public List<WarehouseParcelItemOutput> Items { get; set; } = []; public List<WarehouseParcelItemOutput> Items { get; set; } = [];
public DateTime CreateTime { get; set; } public DateTime CreateTime { get; set; }
@ -505,6 +522,36 @@ public class WarehouseInventoryRelocateInput
public int Qty { get; set; } public int Qty { get; set; }
} }
public class WarehouseInventoryHistoryInput : WarehousePageInput
{
[Required]
public int SkuId { get; set; }
[Required]
public int WarehouseId { get; set; }
/// <summary>
/// 流水类型:all / inbound / deduct;空视为 all。
/// </summary>
public string? MovementType { get; set; }
}
public class WarehouseInventoryHistoryOutput
{
/// <summary>inbound 入库 / deduct 扣减。</summary>
public string MovementType { get; set; } = string.Empty;
/// <summary>变动数量(入库为正,扣减为正数,前端按类型展示符号)。</summary>
public int Qty { get; set; }
/// <summary>关联单号:入库预报号 / 物流单号。</summary>
public string? RefNo { get; set; }
public string? Remark { get; set; }
public DateTime OccurredAt { get; set; }
}
// --- Parcel arrival(快递到仓)--- // --- Parcel arrival(快递到仓)---
public class WarehouseParcelArrivalListInput : WarehousePageInput public class WarehouseParcelArrivalListInput : WarehousePageInput

View File

@ -58,9 +58,25 @@ public partial class WarehouseService
? [] ? []
: await InboundBoxLineRepository.GetListAsync(x => boxIds.Contains(x.InboundBoxId)); : 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<WarehouseInboundOutput> return new OmsPageResult<WarehouseInboundOutput>
{ {
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 Total = total
}; };
} }

View File

@ -3,11 +3,19 @@ using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse; using Oms.Application.Dtos.Warehouse;
using Oms.Application.Shared; using Oms.Application.Shared;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Repositories;
namespace Oms.Application.Services.Warehouse; namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService public partial class WarehouseService
{ {
private IOrderStockBindRepository OrderStockBindRepository =>
LazyServiceProvider.GetRequiredService<IOrderStockBindRepository>();
private IOrderRepository OrderRepository =>
LazyServiceProvider.GetRequiredService<IOrderRepository>();
/// <summary> /// <summary>
/// 查询库存列表。 /// 查询库存列表。
/// </summary> /// </summary>
@ -20,9 +28,10 @@ public partial class WarehouseService
var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId); var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId);
var hasWarehouseFilter = input.WarehouseId.HasValue; var hasWarehouseFilter = input.WarehouseId.HasValue;
var warehouseIdValue = input.WarehouseId.GetValueOrDefault(); var warehouseIdValue = input.WarehouseId.GetValueOrDefault();
// 有总量即展示(含全部冻结、可用为 0 的行),避免客户看不到被占用库存
var inventories = await InventoryRepository.GetListAsync(x => var inventories = await InventoryRepository.GetListAsync(x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue) (!hasCustomerFilter || x.CustomerId == customerIdValue)
&& x.AvailableQty > 0 && x.Qty > 0
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)); && (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue));
var skuIds = inventories.Select(x => x.SkuId).Distinct().ToArray(); 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.SourceBinLocationId, -input.Qty);
await UpsertInventoryQtyAsync(customerId, input.WarehouseId, input.SkuId, input.TargetBinLocationId, input.Qty); await UpsertInventoryQtyAsync(customerId, input.WarehouseId, input.SkuId, input.TargetBinLocationId, input.Qty);
} }
/// <summary>
/// 查询 SKU 库存变动历史:入库收货 + 订单扣减。
/// </summary>
[HttpPost("/warehouse/inventory/history")]
public async Task<OmsPageResult<WarehouseInventoryHistoryOutput>> 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<WarehouseInventoryHistoryOutput>();
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<WarehouseInventoryHistoryOutput>
{
Items = ordered.Skip(skip).Take(pageSize).ToList(),
Total = ordered.Count
};
}
} }

View File

@ -308,8 +308,25 @@ public partial class WarehouseService
} }
} }
private static WarehouseParcelOutput MapParcelOutput(CParcel parcel, IEnumerable<CParcelItem> items) => private static WarehouseParcelOutput MapParcelOutput(CParcel parcel, IEnumerable<CParcelItem> items)
new() {
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, Id = parcel.Id,
CustomerId = parcel.CustomerId, CustomerId = parcel.CustomerId,
@ -320,19 +337,10 @@ public partial class WarehouseService
ArrivalTime = parcel.ArrivalTime, ArrivalTime = parcel.ArrivalTime,
Remark = parcel.Remark, Remark = parcel.Remark,
CreateTime = parcel.CreateTime, CreateTime = parcel.CreateTime,
Items = items.Select(item => new WarehouseParcelItemOutput TotalQty = mappedItems.Sum(x => x.Quantity),
{ FrozenQty = mappedItems.Sum(x => x.FrozenQty),
Id = item.Id, AvailableQty = mappedItems.Sum(x => x.AvailableQty),
ProductName = item.ProductName, Items = mappedItems
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()
}; };
}
} }

View File

@ -49,9 +49,25 @@ public partial class WarehouseService
.GroupBy(x => x.SkuId) .GroupBy(x => x.SkuId)
.ToDictionary(g => g.Key, g => g.Sum(x => x.Qty)); .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<WarehouseSkuOutput> return new OmsPageResult<WarehouseSkuOutput>
{ {
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 Total = total
}; };
} }