using System.Linq.Expressions; using Fake.Application; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Oms.Application.Dtos.Shared; using Oms.Application.Dtos.Warehouse; using Oms.Application.Shared; using Oms.Application.Storage; using Oms.Domain.WarehouseAggregate.Consts; using Oms.Domain.WarehouseAggregate.Entities; namespace Oms.Application.Services.Warehouse; public partial class WarehouseService { /// /// 查询包裹列表。 /// /// 入参:包裹分页查询条件。 /// 包裹分页结果。 [HttpPost("/warehouse/parcel/list")] public async Task> GetParcelListAsync(WarehouseParcelListInput input) { var customerId = await ResolveListCustomerIdAsync(input.CustomerId); var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId); var keyword = input.Keyword?.Trim(); var hasKeyword = !string.IsNullOrWhiteSpace(keyword); // 前端 Tab「全部」会传 all / 空;需忽略,不能按字面 LifecycleStatus == "all" 过滤 var lifecycleStatus = NormalizeParcelLifecycleStatusFilter(input.LifecycleStatus); var hasLifecycleStatusFilter = !string.IsNullOrWhiteSpace(lifecycleStatus); var hasWarehouseFilter = input.WarehouseId.HasValue; var warehouseIdValue = input.WarehouseId.GetValueOrDefault(); Expression> predicate = hasLifecycleStatusFilter ? x => (!hasCustomerFilter || x.CustomerId == customerIdValue) && (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue) && x.LifecycleStatus == lifecycleStatus && (!hasKeyword || x.TrackingNo.Contains(keyword!) || (x.CarrierName != null && x.CarrierName.Contains(keyword!))) : x => (!hasCustomerFilter || x.CustomerId == customerIdValue) && (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue) && (!hasKeyword || x.TrackingNo.Contains(keyword!) || (x.CarrierName != null && x.CarrierName.Contains(keyword!))); var pageIndex = OmsDocHelper.NormalizePage(input.Page); var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize); // 避免 Fake GetPagedListAsync(sorting) 丢失 Where:total 正确但 items 返回全量 var (entities, total) = await RepositoryPageHelper.GetPageByUpdateTimeDescAsync( ParcelRepository, predicate, pageIndex, pageSize); var parcelIds = entities.Select(x => x.Id).ToArray(); var items = parcelIds.Length == 0 ? [] : await ParcelItemRepository.GetListAsync(x => parcelIds.Contains(x.ParcelId)); 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 => { var output = MapParcelOutput(x, items.Where(i => i.ParcelId == x.Id)); if (customerMap.TryGetValue(x.CustomerId, out var customer)) { output.CustomerName = customer.CustomerName; output.CustomerCode = customer.CustomerCode; } return output; }).ToList(), Total = total }; } /// /// 规范化列表状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。 /// private static string? NormalizeParcelLifecycleStatusFilter(string? status) { var value = status?.Trim(); if (string.IsNullOrEmpty(value) || value.Equals("all", StringComparison.OrdinalIgnoreCase)) { return null; } if (value.Equals(WarehouseParcelLifecycleCodes.InTransit, StringComparison.OrdinalIgnoreCase)) { return WarehouseParcelLifecycleCodes.InTransit; } if (value.Equals(WarehouseParcelLifecycleCodes.Arrived, StringComparison.OrdinalIgnoreCase)) { return WarehouseParcelLifecycleCodes.Arrived; } if (value.Equals(WarehouseParcelLifecycleCodes.Stored, StringComparison.OrdinalIgnoreCase)) { return WarehouseParcelLifecycleCodes.Stored; } return value; } /// /// 保存包裹(新增或编辑)。 /// /// 入参:包裹保存数据。 /// 保存后的包裹。 [HttpPost("/warehouse/parcel/save")] public async Task SaveParcelAsync(WarehouseParcelSaveInput input) { var customerId = await ResolveMutationCustomerIdAsync(input.CustomerId); if (input.WarehouseId <= 0) { throw new BusinessException("仓库ID不能为空"); } var trackingNo = input.TrackingNo.Trim(); if (string.IsNullOrWhiteSpace(trackingNo)) { throw new BusinessException("快递单号不能为空"); } CParcel parcel; if (input.Id > 0) { parcel = await GetParcelOrThrowAsync(customerId, input.Id); if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored) { throw new BusinessException("已上架包裹不可编辑"); } } else { parcel = new CParcel { CustomerId = customerId }; } var duplicate = await ParcelRepository.FirstOrDefaultAsync(x => x.CustomerId == customerId && x.WarehouseId == input.WarehouseId && x.TrackingNo == trackingNo && x.Id != parcel.Id); if (duplicate != null) { throw new BusinessException("快递单号已存在"); } parcel.WarehouseId = input.WarehouseId; parcel.TrackingNo = trackingNo; parcel.CarrierName = string.IsNullOrWhiteSpace(input.CarrierName) ? null : input.CarrierName.Trim(); parcel.LifecycleStatus = string.IsNullOrWhiteSpace(input.LifecycleStatus) ? WarehouseParcelLifecycleCodes.InTransit : input.LifecycleStatus.Trim(); parcel.Remark = string.IsNullOrWhiteSpace(input.Remark) ? null : input.Remark.Trim(); if (input.Id > 0) { await ParcelRepository.UpdateAsync(parcel); await ClearParcelItemsAsync(parcel.Id); } else { await ParcelRepository.InsertAsync(parcel); } foreach (var itemInput in input.Items) { if (itemInput.Quantity <= 0) { throw new BusinessException("商品数量必须大于 0"); } var imageUrl = FileStorage.ToStorageKey(itemInput.ImageUrl); if (imageUrl != null && imageUrl.Length > 1024) { imageUrl = imageUrl[..1024]; } var item = new CParcelItem { ParcelId = parcel.Id, ProductName = string.IsNullOrWhiteSpace(itemInput.ProductName) ? null : itemInput.ProductName.Trim(), AttributeText = string.IsNullOrWhiteSpace(itemInput.AttributeText) ? null : itemInput.AttributeText.Trim(), ImageUrl = imageUrl, Quantity = itemInput.Quantity, FrozenQty = 0, AvailableQty = itemInput.Quantity, Remark = string.IsNullOrWhiteSpace(itemInput.Remark) ? null : itemInput.Remark.Trim() }; await ParcelItemRepository.InsertAsync(item); } var savedItems = await ParcelItemRepository.GetListAsync(x => x.ParcelId == parcel.Id); return MapParcelOutput(parcel, savedItems); } /// /// 上传包裹商品图片,返回可访问相对路径。 /// [HttpPost("/warehouse/parcel/upload-image")] [Consumes("multipart/form-data")] public async Task UploadParcelImageAsync(IFormFile file, [FromQuery] int? customerId = null) { await ResolveMutationCustomerIdAsync(customerId); var key = await OmsFileUploadHelper.SaveWarehouseParcelImageAsync(file, FileStorage); var url = FileStorage.GetPublicUrl(key); return new { key, url, fileUrl = key }; } /// /// 删除包裹。 /// /// 入参:包裹 ID。 [HttpPost("/warehouse/parcel/delete")] public async Task DeleteParcelAsync(IdInput input) { var customerId = await ResolveListCustomerIdAsync(null); var parcel = await GetParcelOrThrowAsync(customerId, input.Id); if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored) { throw new BusinessException("已上架包裹不可删除"); } await ClearParcelItemsAsync(parcel.Id); await ParcelRepository.DeleteAsync(parcel); } /// /// 包裹上架入库。 /// /// 入参:上架明细(SKU、库位、数量)。 [HttpPost("/warehouse/parcel/putaway")] public async Task PutawayParcelAsync(WarehouseParcelPutawayInput input) { var customerId = await ResolveMutationCustomerIdAsync(input.CustomerId); var parcel = await GetParcelOrThrowAsync(customerId, input.ParcelId); if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored) { throw new BusinessException("包裹已上架"); } if (input.Items.Count == 0) { throw new BusinessException("请指定上架明细"); } var items = await ParcelItemRepository.GetListAsync(x => x.ParcelId == parcel.Id); var itemMap = items.ToDictionary(x => x.Id); foreach (var putawayItem in input.Items) { if (!itemMap.TryGetValue(putawayItem.ItemId, out var item)) { throw new BusinessException($"包裹明细 {putawayItem.ItemId} 不存在"); } if (putawayItem.SkuId <= 0 || putawayItem.BinLocationId <= 0) { throw new BusinessException("SKU 和库位不能为空"); } var sku = await GetSkuOrThrowAsync(customerId, putawayItem.SkuId); item.SkuId = sku.Id; item.SkuCode = sku.SkuCode; item.BinLocationId = putawayItem.BinLocationId; await ParcelItemRepository.UpdateAsync(item); await UpsertInventoryQtyAsync( customerId, parcel.WarehouseId, sku.Id, putawayItem.BinLocationId, item.Quantity); } parcel.LifecycleStatus = WarehouseParcelLifecycleCodes.Stored; parcel.ArrivalTime = DateTime.Now; await ParcelRepository.UpdateAsync(parcel); } private async Task GetParcelOrThrowAsync(int? customerId, int id) { var entity = customerId.HasValue ? await ParcelRepository.FirstOrDefaultAsync(x => x.Id == id && x.CustomerId == customerId.Value) : await ParcelRepository.FirstOrDefaultAsync(x => x.Id == id); if (entity == null) { throw new BusinessException("包裹不存在"); } return entity; } private async Task ClearParcelItemsAsync(int parcelId) { var items = await ParcelItemRepository.GetListAsync(x => x.ParcelId == parcelId); foreach (var item in items) { await ParcelItemRepository.DeleteAsync(item); } } private 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 = FileStorage.GetPublicUrl(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, WarehouseId = parcel.WarehouseId, TrackingNo = parcel.TrackingNo, CarrierName = parcel.CarrierName, LifecycleStatus = parcel.LifecycleStatus, ArrivalTime = parcel.ArrivalTime, Remark = parcel.Remark, CreateTime = parcel.CreateTime, TotalQty = mappedItems.Sum(x => x.Quantity), FrozenQty = mappedItems.Sum(x => x.FrozenQty), AvailableQty = mappedItems.Sum(x => x.AvailableQty), Items = mappedItems }; } }