using System.Linq.Expressions;
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Shared;
using Oms.Application.Dtos.Warehouse;
using Oms.Application.Shared;
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));
return new OmsPageResult
{
Items = entities.Select(x => MapParcelOutput(x, items.Where(i => i.ParcelId == x.Id))).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 item = new CParcelItem
{
ParcelId = parcel.Id,
ProductName = string.IsNullOrWhiteSpace(itemInput.ProductName) ? null : itemInput.ProductName.Trim(),
AttributeText = string.IsNullOrWhiteSpace(itemInput.AttributeText)
? null
: itemInput.AttributeText.Trim(),
Quantity = 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);
}
///
/// 删除包裹。
///
/// 入参:包裹 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 static WarehouseParcelOutput MapParcelOutput(CParcel parcel, IEnumerable items) =>
new()
{
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,
Items = items.Select(item => new WarehouseParcelItemOutput
{
Id = item.Id,
ProductName = item.ProductName,
AttributeText = item.AttributeText,
Quantity = item.Quantity,
SkuId = item.SkuId,
SkuCode = item.SkuCode,
BinLocationId = item.BinLocationId,
Remark = item.Remark
}).ToList()
};
}