admin/src/Oms.Application/Services/Warehouse/WarehouseService.Parcel.cs
2026-07-15 13:45:08 +08:00

298 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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