admin/src/Oms.Application/Services/Warehouse/WarehouseService.ParcelArrival.cs
zzh 8a1649ea66
All checks were successful
Build and Deploy / deploy (push) Successful in 1m13s
feat: 入库箱唛/SKU 标签打印与订单改销售产品
支持按 box/sku 模板生成 PDF;待支付前可改产品并自动重排货重算费。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-22 18:39:07 +08:00

218 lines
8.8 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.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.WarehouseAggregate.Consts;
using Oms.Domain.WarehouseAggregate.Entities;
using Oms.Domain.WarehouseAggregate.Repositories;
namespace Oms.Application.Services.Warehouse;
public partial class WarehouseService
{
private ICParcelArrivalRepository ParcelArrivalRepository =>
LazyServiceProvider.GetRequiredService<ICParcelArrivalRepository>();
private OmsRbacUserDisplayNameResolver UserDisplayNameResolver =>
LazyServiceProvider.GetRequiredService<OmsRbacUserDisplayNameResolver>();
private IOrderStockBindRepository ParcelArrivalStockBindRepository =>
LazyServiceProvider.GetRequiredService<IOrderStockBindRepository>();
private IOrderRepository ParcelArrivalOrderRepository =>
LazyServiceProvider.GetRequiredService<IOrderRepository>();
/// <summary>
/// 快递到仓记录列表。
/// </summary>
[HttpPost("/warehouse/parcel-arrival/list")]
public async Task<OmsPageResult<WarehouseParcelArrivalOutput>> GetParcelArrivalListAsync(
WarehouseParcelArrivalListInput input)
{
var customerId = await ResolveListCustomerIdAsync(input.CustomerId);
var (hasCustomerFilter, customerIdValue) = CustomerFilter(customerId);
var keyword = input.Keyword?.Trim();
var hasKeyword = !string.IsNullOrWhiteSpace(keyword);
var hasWarehouseFilter = input.WarehouseId is > 0;
var warehouseIdValue = input.WarehouseId.GetValueOrDefault();
Expression<Func<CParcelArrival, bool>> predicate = x =>
(!hasCustomerFilter || x.CustomerId == customerIdValue)
&& (!hasWarehouseFilter || x.WarehouseId == warehouseIdValue)
&& (!hasKeyword || x.TrackingNo.Contains(keyword!));
var pageIndex = OmsDocHelper.NormalizePage(input.Page);
var pageSize = OmsDocHelper.NormalizePageSize(input.PageSize);
var entities = await ParcelArrivalRepository.GetPagedListAsync(
predicate,
pageIndex,
pageSize,
new Dictionary<string, bool>
{
[nameof(CParcelArrival.CreateTime)] = false,
[nameof(CParcelArrival.Id)] = false
});
var total = await ParcelArrivalRepository.CountAsync(predicate);
var warehouseIds = entities.Select(x => x.WarehouseId).Distinct().ToArray();
var warehouses = warehouseIds.Length == 0
? []
: await WarehouseAddressRepository.GetListAsync(x => warehouseIds.Contains(x.Id));
var warehouseNameMap = warehouses.ToDictionary(x => x.Id, x => x.WarehouseName);
var userIds = entities.Select(x => x.CreateUserId).Distinct().ToArray();
var userNameMap = await UserDisplayNameResolver.ResolveManyAsync(userIds);
return new OmsPageResult<WarehouseParcelArrivalOutput>
{
Items = entities.Select(x => new WarehouseParcelArrivalOutput
{
Id = x.Id,
CustomerId = x.CustomerId,
WarehouseId = x.WarehouseId,
WarehouseName = warehouseNameMap.GetValueOrDefault(x.WarehouseId),
ParcelId = x.ParcelId,
TrackingNo = x.TrackingNo,
CreateTime = x.CreateTime,
CreateUserId = x.CreateUserId,
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(x.CreateUserId, userNameMap)
}).ToList(),
Total = total
};
}
/// <summary>
/// 快递到仓:写入到仓记录,并将对应「我的包裹」状态改为 arrived。
/// </summary>
[HttpPost("/warehouse/parcel-arrival/create")]
public async Task<WarehouseParcelArrivalOutput> CreateParcelArrivalAsync(WarehouseParcelArrivalCreateInput input)
{
// 到仓按仓库 + 快递单号定位包裹;客户归属取自包裹本身,管理端无需先选客户。
// 客户门户会话仍会限制只能到自己的包裹。
var sessionCustomerId = await ResolveListCustomerIdAsync(null);
var (hasCustomerFilter, customerIdValue) = CustomerFilter(sessionCustomerId);
if (input.WarehouseId <= 0)
{
throw new BusinessException("仓库ID不能为空");
}
var trackingNo = input.TrackingNo.Trim();
if (string.IsNullOrWhiteSpace(trackingNo))
{
throw new BusinessException("国内快递单号不能为空");
}
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId)
?? throw new BusinessException("仓库不存在");
var parcels = await ParcelRepository.GetListAsync(x =>
x.WarehouseId == input.WarehouseId
&& x.TrackingNo == trackingNo
&& (!hasCustomerFilter || x.CustomerId == customerIdValue));
if (parcels.Count == 0)
{
throw new BusinessException("未找到对应包裹,请确认快递单号与仓库");
}
if (parcels.Count > 1)
{
throw new BusinessException("同一仓库下存在多条相同快递单号的包裹,请先在「我的包裹」核对后再到仓");
}
var parcel = parcels[0];
if (parcel.LifecycleStatus == WarehouseParcelLifecycleCodes.Stored)
{
throw new BusinessException("包裹已上架,无法再到仓");
}
parcel.LifecycleStatus = WarehouseParcelLifecycleCodes.Arrived;
parcel.ArrivalTime ??= DateTime.Now;
await ParcelRepository.UpdateAsync(parcel);
var entity = new CParcelArrival
{
CustomerId = parcel.CustomerId,
WarehouseId = input.WarehouseId,
ParcelId = parcel.Id,
TrackingNo = trackingNo
};
await ParcelArrivalRepository.InsertAsync(entity);
var linkedOrders = await LoadParcelArrivalLinkedOrdersAsync(parcel.Id);
var userNameMap = await UserDisplayNameResolver.ResolveManyAsync([entity.CreateUserId]);
return new WarehouseParcelArrivalOutput
{
Id = entity.Id,
CustomerId = entity.CustomerId,
WarehouseId = entity.WarehouseId,
WarehouseName = warehouse.WarehouseName,
ParcelId = entity.ParcelId,
TrackingNo = entity.TrackingNo,
CreateTime = entity.CreateTime,
CreateUserId = entity.CreateUserId,
CreateBy = OmsRbacUserDisplayNameResolver.FormatUserId(entity.CreateUserId, userNameMap),
LinkedOrders = linkedOrders
};
}
private async Task<List<WarehouseParcelArrivalLinkedOrderOutput>> LoadParcelArrivalLinkedOrdersAsync(int parcelId)
{
var binds = await ParcelArrivalStockBindRepository.GetListAsync(x =>
x.ParcelId == parcelId
&& !x.IsDeleted
&& x.Status == OrderStockBindStatusCodes.Bound);
if (binds.Count == 0)
{
return [];
}
var orderIds = binds.Select(x => x.OrderId).Distinct().ToArray();
var orders = await ParcelArrivalOrderRepository.GetListAsync(x => orderIds.Contains(x.Id));
var orderMap = orders.ToDictionary(x => x.Id);
return orderIds
.Where(orderMap.ContainsKey)
.Select(orderId =>
{
var order = orderMap[orderId];
var isWarehouse = FulfillmentTypeCodes.IsWarehouse(order.FulfillmentType);
return new WarehouseParcelArrivalLinkedOrderOutput
{
OrderId = orderId,
ServiceNo = order.ServiceNo,
ClientNo = order.ClientNo,
FulfillProgress = isWarehouse
? FulfillmentProgressCodes.ArrivedPendingPick
: null
};
})
.OrderBy(x => x.ServiceNo, StringComparer.OrdinalIgnoreCase)
.ToList();
}
/// <summary>
/// 删除快递到仓记录(不回退包裹状态)。
/// </summary>
[HttpPost("/warehouse/parcel-arrival/delete")]
public async Task DeleteParcelArrivalAsync(IdInput input)
{
var customerId = await ResolveListCustomerIdAsync(null);
var entity = customerId.HasValue
? await ParcelArrivalRepository.FirstOrDefaultAsync(x =>
x.Id == input.Id && x.CustomerId == customerId.Value)
: await ParcelArrivalRepository.FirstOrDefaultAsync(x => x.Id == input.Id);
if (entity == null)
{
throw new BusinessException("到仓记录不存在");
}
await ParcelArrivalRepository.DeleteAsync(entity);
}
}