All checks were successful
Build and Deploy / deploy (push) Successful in 1m13s
支持按 box/sku 模板生成 PDF;待支付前可改产品并自动重排货重算费。 Co-authored-by: Cursor <cursoragent@cursor.com>
218 lines
8.8 KiB
C#
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);
|
|
}
|
|
}
|