298 lines
11 KiB
C#
298 lines
11 KiB
C#
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
|
||
{
|
||
/// <summary>
|
||
/// 查询包裹列表。
|
||
/// </summary>
|
||
/// <param name="input">入参:包裹分页查询条件。</param>
|
||
/// <returns>包裹分页结果。</returns>
|
||
[HttpPost("/warehouse/parcel/list")]
|
||
public async Task<OmsPageResult<WarehouseParcelOutput>> 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<Func<CParcel, bool>> 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<WarehouseParcelOutput>
|
||
{
|
||
Items = entities.Select(x => MapParcelOutput(x, items.Where(i => i.ParcelId == x.Id))).ToList(),
|
||
Total = total
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 规范化列表状态筛选:空 / all 表示不按状态过滤;已知编码统一为规范值。
|
||
/// </summary>
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存包裹(新增或编辑)。
|
||
/// </summary>
|
||
/// <param name="input">入参:包裹保存数据。</param>
|
||
/// <returns>保存后的包裹。</returns>
|
||
[HttpPost("/warehouse/parcel/save")]
|
||
public async Task<WarehouseParcelOutput> 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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除包裹。
|
||
/// </summary>
|
||
/// <param name="input">入参:包裹 ID。</param>
|
||
[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);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 包裹上架入库。
|
||
/// </summary>
|
||
/// <param name="input">入参:上架明细(SKU、库位、数量)。</param>
|
||
[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<CParcel> 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<CParcelItem> 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()
|
||
};
|
||
}
|