All checks were successful
Build and Deploy / deploy (push) Successful in 1m43s
新增 /order/pickup-order/downstream-forward,productCode/supplierCode/地址重量商品均取揽收单,不再依赖关联物流单。 Co-authored-by: Cursor <cursoragent@cursor.com>
1205 lines
44 KiB
C#
1205 lines
44 KiB
C#
using Fake.Application;
|
||
using Microsoft.AspNetCore.Mvc;
|
||
using Oms.Application.Dtos.Orders;
|
||
using Oms.Application.Dtos.Shared;
|
||
using Oms.Application.Integrations.Itfx;
|
||
using Oms.Application.Shared;
|
||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||
using Oms.Domain.CustomersAggregate.Repositories;
|
||
using Oms.Domain.OrdersAggregate.Consts;
|
||
using Oms.Domain.OrdersAggregate.Entities;
|
||
using Oms.Domain.OrdersAggregate.Repositories;
|
||
using Oms.Domain.SystemAggregate.Repositories;
|
||
|
||
namespace Oms.Application.Services.Orders;
|
||
|
||
/// <summary>
|
||
/// 揽收单(1 揽收 : N 物流)管理。
|
||
/// </summary>
|
||
[ApiExplorerSettings(GroupName = "OMS-Order")]
|
||
public class PickupOrderService : ApplicationService
|
||
{
|
||
private static readonly HashSet<string> BindableOrderStatuses = new(StringComparer.OrdinalIgnoreCase)
|
||
{
|
||
OrderStatusCodes.Draft,
|
||
OrderStatusCodes.Predeclared,
|
||
OrderStatusCodes.PendingAllocation,
|
||
OrderStatusCodes.PendingPayment,
|
||
OrderStatusCodes.PendingShip,
|
||
OrderStatusCodes.PendingOnline,
|
||
};
|
||
|
||
private IPickupOrderRepository PickupOrderRepository =>
|
||
LazyServiceProvider.GetRequiredService<IPickupOrderRepository>();
|
||
|
||
private IPickupOrderLineRepository PickupOrderLineRepository =>
|
||
LazyServiceProvider.GetRequiredService<IPickupOrderLineRepository>();
|
||
|
||
private IOrderRepository OrderRepository =>
|
||
LazyServiceProvider.GetRequiredService<IOrderRepository>();
|
||
|
||
private ICustomerRepository CustomerRepository =>
|
||
LazyServiceProvider.GetRequiredService<ICustomerRepository>();
|
||
|
||
private ICustomerSenderAddressRepository CustomerSenderAddressRepository =>
|
||
LazyServiceProvider.GetRequiredService<ICustomerSenderAddressRepository>();
|
||
|
||
private IWarehouseAddressRepository WarehouseAddressRepository =>
|
||
LazyServiceProvider.GetRequiredService<IWarehouseAddressRepository>();
|
||
|
||
private ICountryRepository CountryRepository =>
|
||
LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
||
|
||
private ISalesProductRepository SalesProductRepository =>
|
||
LazyServiceProvider.GetRequiredService<ISalesProductRepository>();
|
||
|
||
private IServiceChannelRepository ServiceChannelRepository =>
|
||
LazyServiceProvider.GetRequiredService<IServiceChannelRepository>();
|
||
|
||
private IServiceChannelGroupRepository ServiceChannelGroupRepository =>
|
||
LazyServiceProvider.GetRequiredService<IServiceChannelGroupRepository>();
|
||
|
||
private IItfxForwardClient ItfxForwardClient =>
|
||
LazyServiceProvider.GetRequiredService<IItfxForwardClient>();
|
||
|
||
private IItfxMetadataClient ItfxMetadataClient =>
|
||
LazyServiceProvider.GetRequiredService<IItfxMetadataClient>();
|
||
|
||
/// <summary>
|
||
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/create")]
|
||
public async Task<PickupOrderOutput> CreateAsync([FromBody] PickupOrderCreateInput input)
|
||
{
|
||
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
input.CustomerId);
|
||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == customerId);
|
||
CustomerContactHelper.EnsureContactComplete(customer);
|
||
|
||
if (input.SenderAddressId <= 0)
|
||
{
|
||
throw new BusinessException("揽收地址不能为空");
|
||
}
|
||
|
||
if (input.WarehouseId <= 0)
|
||
{
|
||
throw new BusinessException("仓库不能为空");
|
||
}
|
||
|
||
if (input.ExpectPickupTime == default)
|
||
{
|
||
throw new BusinessException("期望揽收时间不能为空");
|
||
}
|
||
|
||
var direction = PickupDirectionCodes.NormalizeOrDefault(input.Direction);
|
||
|
||
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList();
|
||
if (orderIds.Count == 0)
|
||
{
|
||
throw new BusinessException("请至少选择一票物流订单");
|
||
}
|
||
|
||
var senderAddress = await CustomerSenderAddressRepository.GetOwnedAsync(input.SenderAddressId, customerId);
|
||
if (senderAddress == null)
|
||
{
|
||
throw new BusinessException("揽收地址不存在");
|
||
}
|
||
|
||
var addressType = string.IsNullOrWhiteSpace(senderAddress.AddressType)
|
||
? CustomerAddressTypeCodes.Sender
|
||
: senderAddress.AddressType;
|
||
if (!CustomerAddressTypeCodes.IsPickupEligible(addressType))
|
||
{
|
||
throw new BusinessException("揽收单请选择「国内揽收」类型地址");
|
||
}
|
||
|
||
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId);
|
||
if (warehouse == null)
|
||
{
|
||
throw new BusinessException("仓库不存在");
|
||
}
|
||
|
||
var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id));
|
||
var orderMap = orders.ToDictionary(x => x.Id);
|
||
var existingLines = await PickupOrderLineRepository.GetActiveByOrderIdsAsync(orderIds);
|
||
var existingByOrderId = existingLines.ToDictionary(x => x.OrderId);
|
||
|
||
var rejected = new List<PickupOrderRejectItem>();
|
||
var accepted = new List<Order>();
|
||
foreach (var orderId in orderIds)
|
||
{
|
||
if (!orderMap.TryGetValue(orderId, out var order))
|
||
{
|
||
rejected.Add(new PickupOrderRejectItem { OrderId = orderId, Reason = "订单不存在" });
|
||
continue;
|
||
}
|
||
|
||
var reason = ValidateOrderForBind(
|
||
order,
|
||
customerId,
|
||
input.WarehouseId,
|
||
input.SenderAddressId,
|
||
existingByOrderId.GetValueOrDefault(orderId));
|
||
if (reason != null)
|
||
{
|
||
rejected.Add(new PickupOrderRejectItem
|
||
{
|
||
OrderId = orderId,
|
||
ServiceNo = order.ServiceNo,
|
||
Reason = reason
|
||
});
|
||
continue;
|
||
}
|
||
|
||
accepted.Add(order);
|
||
}
|
||
|
||
if (rejected.Count > 0)
|
||
{
|
||
throw new BusinessException(
|
||
"部分运单不能合并揽收:" +
|
||
string.Join(";", rejected.Select(x => $"{x.ServiceNo ?? x.OrderId.ToString()} — {x.Reason}")));
|
||
}
|
||
|
||
if (accepted.Count == 0)
|
||
{
|
||
throw new BusinessException("没有可挂载的物流订单");
|
||
}
|
||
|
||
string? senderCountryCode = null;
|
||
if (senderAddress.Country.HasValue)
|
||
{
|
||
var country = await CountryRepository.FirstOrDefaultAsync(x => x.Id == senderAddress.Country.Value);
|
||
senderCountryCode = country?.CountryCode;
|
||
}
|
||
|
||
if (!string.IsNullOrWhiteSpace(input.RequestId))
|
||
{
|
||
var existedByRequest = await PickupOrderRepository.FirstOrDefaultAsync(x => x.RequestId == input.RequestId.Trim());
|
||
if (existedByRequest != null)
|
||
{
|
||
if (existedByRequest.CustomerId != customerId)
|
||
{
|
||
throw new BusinessException("幂等请求已存在");
|
||
}
|
||
|
||
return await MapOutputAsync(existedByRequest, includeOrders: true);
|
||
}
|
||
}
|
||
|
||
var weights = await OrderRepository.GetWeightsByOrderIdsAsync(accepted.Select(x => x.Id));
|
||
var weightSum = weights
|
||
.GroupBy(x => x.OrderId)
|
||
.Select(g => g.OrderByDescending(w => w.Type == OrderWeightTypeCodes.Receive ? 2 : 1).FirstOrDefault())
|
||
.Where(x => x != null)
|
||
.Sum(x => x!.ActualWeight ?? 0);
|
||
|
||
var freight = input.FreightAmount;
|
||
var receivable = freight;
|
||
var orgId = accepted
|
||
.Select(x => x.OrganizationId)
|
||
.FirstOrDefault(x => !string.IsNullOrWhiteSpace(x));
|
||
var secretLevel = accepted
|
||
.Select(x => x.SecretLevel)
|
||
.FirstOrDefault(x => x > 0);
|
||
if (secretLevel <= 0)
|
||
{
|
||
secretLevel = 2;
|
||
}
|
||
|
||
var entity = new PickupOrder
|
||
{
|
||
PickupNo = await GeneratePickupNoAsync(),
|
||
RequestId = NormalizeOptional(input.RequestId),
|
||
CustomerId = customerId,
|
||
OrganizationId = orgId,
|
||
SecretLevel = secretLevel,
|
||
SenderAddressId = senderAddress.Id,
|
||
Direction = direction,
|
||
SenderName = senderAddress.SenderName ?? string.Empty,
|
||
SenderCompany = senderAddress.Company,
|
||
SenderCountry = senderAddress.Country,
|
||
SenderCountryCode = senderCountryCode,
|
||
SenderProvince = senderAddress.Province,
|
||
SenderCity = senderAddress.City,
|
||
SenderZipCode = senderAddress.ZipCode,
|
||
SenderStreetAddress1 = senderAddress.StreetAddress1 ?? string.Empty,
|
||
SenderStreetAddress2 = senderAddress.StreetAddress2,
|
||
SenderStreetAddress3 = senderAddress.StreetAddress3,
|
||
SenderHouseNumber = senderAddress.HouseNumber,
|
||
SenderPhone = senderAddress.Phone,
|
||
SenderTelePhone = senderAddress.TelePhone,
|
||
SenderEmail = senderAddress.Email,
|
||
WarehouseId = input.WarehouseId,
|
||
CarrierCode = NormalizeOptional(input.CarrierCode),
|
||
CarrierService = NormalizeOptional(input.CarrierService),
|
||
ExpectPickupTime = input.ExpectPickupTime,
|
||
Status = PickupOrderStatusCodes.Draft,
|
||
ParcelCount = accepted.Count,
|
||
TotalActualWeight = weightSum > 0 ? weightSum : null,
|
||
Currency = "CNY",
|
||
FreightAmount = freight,
|
||
OtherFeeAmount = null,
|
||
ReceivableAmount = receivable,
|
||
PayStatus = PickupPayStatusCodes.Unpaid,
|
||
PayMode = PickupPayModeCodes.Prepaid,
|
||
SettlementStatus = PickupSettlementStatusCodes.None,
|
||
Remark = NormalizeOptional(input.Remark)
|
||
};
|
||
|
||
await PickupOrderRepository.InsertAsync(entity);
|
||
|
||
var sort = 0;
|
||
foreach (var order in accepted)
|
||
{
|
||
await PickupOrderLineRepository.InsertAsync(new PickupOrderLine
|
||
{
|
||
PickupOrderId = entity.Id,
|
||
OrderId = order.Id,
|
||
ServiceNo = order.ServiceNo,
|
||
ClientNo = order.ClientNo,
|
||
SortNo = sort++
|
||
});
|
||
|
||
order.PickupOrderId = entity.Id;
|
||
order.InboundMethod = OrderInboundMethodCodes.DoorPickup;
|
||
order.PickupTime = entity.ExpectPickupTime;
|
||
order.CustomerSenderAddressId = entity.SenderAddressId;
|
||
await OrderRepository.UpdateAsync(order);
|
||
}
|
||
|
||
return await MapOutputAsync(entity, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 追加物流单到已有揽收单。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/add-orders")]
|
||
public async Task<PickupOrderOutput> AddOrdersAsync([FromBody] PickupOrderOrdersInput input)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||
if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status))
|
||
{
|
||
throw new BusinessException("当前揽收单状态不可追加运单");
|
||
}
|
||
|
||
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToList();
|
||
if (orderIds.Count == 0)
|
||
{
|
||
throw new BusinessException("请至少选择一票物流订单");
|
||
}
|
||
|
||
var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id));
|
||
var orderMap = orders.ToDictionary(x => x.Id);
|
||
var existingLines = await PickupOrderLineRepository.GetActiveByOrderIdsAsync(orderIds);
|
||
var existingByOrderId = existingLines.ToDictionary(x => x.OrderId);
|
||
var currentLines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id);
|
||
var sort = currentLines.Count == 0 ? 0 : currentLines.Max(x => x.SortNo) + 1;
|
||
|
||
var rejected = new List<PickupOrderRejectItem>();
|
||
var accepted = new List<Order>();
|
||
foreach (var orderId in orderIds)
|
||
{
|
||
if (!orderMap.TryGetValue(orderId, out var order))
|
||
{
|
||
rejected.Add(new PickupOrderRejectItem { OrderId = orderId, Reason = "订单不存在" });
|
||
continue;
|
||
}
|
||
|
||
var reason = ValidateOrderForBind(
|
||
order,
|
||
pickup.CustomerId,
|
||
pickup.WarehouseId,
|
||
pickup.SenderAddressId,
|
||
existingByOrderId.GetValueOrDefault(orderId));
|
||
if (reason != null)
|
||
{
|
||
rejected.Add(new PickupOrderRejectItem
|
||
{
|
||
OrderId = orderId,
|
||
ServiceNo = order.ServiceNo,
|
||
Reason = reason
|
||
});
|
||
continue;
|
||
}
|
||
|
||
accepted.Add(order);
|
||
}
|
||
|
||
if (rejected.Count > 0)
|
||
{
|
||
throw new BusinessException(
|
||
"部分运单不能追加:" +
|
||
string.Join(";", rejected.Select(x => $"{x.ServiceNo ?? x.OrderId.ToString()} — {x.Reason}")));
|
||
}
|
||
|
||
foreach (var order in accepted)
|
||
{
|
||
await PickupOrderLineRepository.InsertAsync(new PickupOrderLine
|
||
{
|
||
PickupOrderId = pickup.Id,
|
||
OrderId = order.Id,
|
||
ServiceNo = order.ServiceNo,
|
||
ClientNo = order.ClientNo,
|
||
SortNo = sort++
|
||
});
|
||
order.PickupOrderId = pickup.Id;
|
||
order.InboundMethod = OrderInboundMethodCodes.DoorPickup;
|
||
order.PickupTime = pickup.ExpectPickupTime;
|
||
order.CustomerSenderAddressId = pickup.SenderAddressId;
|
||
await OrderRepository.UpdateAsync(order);
|
||
}
|
||
|
||
await RefreshParcelCountAsync(pickup);
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从揽收单移除物流单;移除后若无关联则自动取消揽收单。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/remove-orders")]
|
||
public async Task<PickupOrderOutput> RemoveOrdersAsync([FromBody] PickupOrderOrdersInput input)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||
if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status))
|
||
{
|
||
throw new BusinessException("当前揽收单状态不可移除运单");
|
||
}
|
||
|
||
var orderIds = (input.OrderIds ?? []).Where(x => x > 0).Distinct().ToHashSet();
|
||
if (orderIds.Count == 0)
|
||
{
|
||
throw new BusinessException("请至少选择一票物流订单");
|
||
}
|
||
|
||
var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id);
|
||
var toRemove = lines.Where(x => orderIds.Contains(x.OrderId)).ToList();
|
||
if (toRemove.Count == 0)
|
||
{
|
||
throw new BusinessException("所选运单不在该揽收单中");
|
||
}
|
||
|
||
foreach (var line in toRemove)
|
||
{
|
||
await PickupOrderLineRepository.HardDeleteAsync(line);
|
||
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == line.OrderId);
|
||
if (order != null && order.PickupOrderId == pickup.Id)
|
||
{
|
||
order.PickupOrderId = null;
|
||
await OrderRepository.UpdateAsync(order);
|
||
}
|
||
}
|
||
|
||
var remain = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id);
|
||
if (remain.Count == 0)
|
||
{
|
||
if (pickup.PayStatus == PickupPayStatusCodes.Paid)
|
||
{
|
||
throw new BusinessException("揽收单已支付且将无关联运单,请先处理退款后再取消(P1 请联系客服)");
|
||
}
|
||
|
||
pickup.Status = PickupOrderStatusCodes.Cancelled;
|
||
pickup.ParcelCount = 0;
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
}
|
||
else
|
||
{
|
||
await RefreshParcelCountAsync(pickup);
|
||
}
|
||
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 揽收单分页列表。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/list")]
|
||
public async Task<OmsPageResult<PickupOrderOutput>> ListAsync([FromBody] PickupOrderListInput input)
|
||
{
|
||
var page = input.Page <= 0 ? 1 : input.Page;
|
||
var pageSize = input.PageSize <= 0 ? 30 : Math.Min(input.PageSize, 200);
|
||
var customerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
input.CustomerId);
|
||
|
||
var keyword = NormalizeOptional(input.Keyword);
|
||
var status = NormalizeOptional(input.Status);
|
||
var payStatus = NormalizeOptional(input.PayStatus);
|
||
var direction = string.IsNullOrWhiteSpace(input.Direction)
|
||
? null
|
||
: PickupDirectionCodes.NormalizeOrDefault(input.Direction);
|
||
|
||
var all = await PickupOrderRepository.GetListAsync(x =>
|
||
(customerId == null || x.CustomerId == customerId.Value) &&
|
||
(direction == null || x.Direction == direction) &&
|
||
(status == null || x.Status == status) &&
|
||
(payStatus == null || x.PayStatus == payStatus) &&
|
||
(input.WarehouseId == null || input.WarehouseId <= 0 || x.WarehouseId == input.WarehouseId.Value) &&
|
||
(input.CreateTimeStart == null || x.CreateTime >= input.CreateTimeStart) &&
|
||
(input.CreateTimeEnd == null || x.CreateTime <= input.CreateTimeEnd) &&
|
||
(input.ExpectPickupTimeStart == null || x.ExpectPickupTime >= input.ExpectPickupTimeStart) &&
|
||
(input.ExpectPickupTimeEnd == null || x.ExpectPickupTime <= input.ExpectPickupTimeEnd) &&
|
||
(keyword == null || x.PickupNo.Contains(keyword) ||
|
||
(x.CarrierWaybillNo != null && x.CarrierWaybillNo.Contains(keyword)) ||
|
||
(x.SenderName != null && x.SenderName.Contains(keyword))));
|
||
|
||
var ordered = all.OrderByDescending(x => x.CreateTime).ThenByDescending(x => x.Id).ToList();
|
||
var total = ordered.Count;
|
||
var pageItems = ordered.Skip((page - 1) * pageSize).Take(pageSize).ToList();
|
||
var outputs = new List<PickupOrderOutput>();
|
||
foreach (var item in pageItems)
|
||
{
|
||
outputs.Add(await MapOutputAsync(item, includeOrders: false));
|
||
}
|
||
|
||
return new OmsPageResult<PickupOrderOutput> { Items = outputs, Total = total };
|
||
}
|
||
|
||
/// <summary>
|
||
/// 揽收单状态计数(Tab 徽章)。
|
||
/// </summary>
|
||
[HttpGet("/order/pickup-order/status-counts")]
|
||
public async Task<PickupOrderStatusCountsOutput> GetStatusCountsAsync(
|
||
[FromQuery] int? customerId,
|
||
[FromQuery] string? direction = null)
|
||
{
|
||
var resolvedCustomerId = await WarehouseCustomerHelper.ResolveListCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
customerId);
|
||
|
||
var directionFilter = string.IsNullOrWhiteSpace(direction)
|
||
? null
|
||
: PickupDirectionCodes.NormalizeOrDefault(direction);
|
||
|
||
var all = await PickupOrderRepository.GetListAsync(x =>
|
||
(resolvedCustomerId == null || x.CustomerId == resolvedCustomerId.Value) &&
|
||
(directionFilter == null || x.Direction == directionFilter));
|
||
|
||
var terminal = new[] { PickupOrderStatusCodes.Cancelled, PickupOrderStatusCodes.Failed };
|
||
var pendingPayment = all.Count(x =>
|
||
!terminal.Contains(x.Status) &&
|
||
x.PayStatus != PickupPayStatusCodes.Paid &&
|
||
x.ReceivableAmount is > 0);
|
||
|
||
var counts = all
|
||
.GroupBy(x => x.Status ?? string.Empty, StringComparer.OrdinalIgnoreCase)
|
||
.ToDictionary(g => g.Key, g => g.Count(), StringComparer.OrdinalIgnoreCase);
|
||
|
||
return new PickupOrderStatusCountsOutput
|
||
{
|
||
Total = all.Count,
|
||
PendingPayment = pendingPayment,
|
||
Counts = counts
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 揽收单详情。
|
||
/// </summary>
|
||
[HttpGet("/order/pickup-order/detail")]
|
||
public async Task<PickupOrderOutput> GetDetailAsync([FromQuery] int id)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(id);
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 确认对客揽收运费。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/confirm-freight")]
|
||
public async Task<PickupOrderOutput> ConfirmFreightAsync([FromBody] PickupOrderConfirmFreightInput input)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||
if (pickup.Status is PickupOrderStatusCodes.Cancelled or PickupOrderStatusCodes.Failed)
|
||
{
|
||
throw new BusinessException("已取消/失败的揽收单不可确认运费");
|
||
}
|
||
|
||
if (pickup.PayStatus == PickupPayStatusCodes.Paid)
|
||
{
|
||
throw new BusinessException("已支付不可修改运费");
|
||
}
|
||
|
||
if (input.FreightAmount < 0)
|
||
{
|
||
throw new BusinessException("运费不能为负数");
|
||
}
|
||
|
||
var other = input.OtherFeeAmount ?? 0;
|
||
if (other < 0)
|
||
{
|
||
throw new BusinessException("其他费用不能为负数");
|
||
}
|
||
|
||
pickup.FreightAmount = input.FreightAmount;
|
||
pickup.OtherFeeAmount = other;
|
||
pickup.ReceivableAmount = input.FreightAmount + other;
|
||
if (input.CarrierPayableAmount.HasValue)
|
||
{
|
||
pickup.CarrierPayableAmount = input.CarrierPayableAmount;
|
||
}
|
||
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 客户支付揽收运费(余额)。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/pay")]
|
||
public async Task<PickupOrderOutput> PayAsync([FromBody] PickupOrderIdInput input)
|
||
{
|
||
var pickup = await PayCoreAsync(input.Id);
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量支付揽收运费(逐单扣余额,部分失败不回滚已成功项)。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/pay-batch")]
|
||
public async Task<PickupOrderBatchPayOutput> PayBatchAsync([FromBody] PickupOrderIdsInput input)
|
||
{
|
||
var ids = (input.Ids ?? []).Where(x => x > 0).Distinct().ToList();
|
||
if (ids.Count == 0)
|
||
{
|
||
throw new BusinessException("请至少选择一张揽收单");
|
||
}
|
||
|
||
if (ids.Count > 100)
|
||
{
|
||
throw new BusinessException("单次最多支付 100 张揽收单");
|
||
}
|
||
|
||
var output = new PickupOrderBatchPayOutput();
|
||
foreach (var id in ids)
|
||
{
|
||
try
|
||
{
|
||
var pickup = await PayCoreAsync(id);
|
||
var amount = pickup.ReceivableAmount ?? pickup.FreightAmount ?? 0;
|
||
output.SuccessCount++;
|
||
output.PaidAmount += amount;
|
||
output.Items.Add(new PickupOrderBatchPayItemOutput
|
||
{
|
||
Id = pickup.Id,
|
||
PickupNo = pickup.PickupNo,
|
||
Success = true,
|
||
Message = "支付成功",
|
||
Amount = amount
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
output.FailCount++;
|
||
var pickupNo = await TryGetPickupNoAsync(id);
|
||
output.Items.Add(new PickupOrderBatchPayItemOutput
|
||
{
|
||
Id = id,
|
||
PickupNo = pickupNo,
|
||
Success = false,
|
||
Message = ex.Message
|
||
});
|
||
}
|
||
}
|
||
|
||
output.Message = $"支付完成:成功 {output.SuccessCount},失败 {output.FailCount}";
|
||
return output;
|
||
}
|
||
|
||
private async Task<PickupOrder> PayCoreAsync(int id)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(id);
|
||
if (pickup.PayStatus == PickupPayStatusCodes.Paid)
|
||
{
|
||
throw new BusinessException("揽收单已支付");
|
||
}
|
||
|
||
if (pickup.Status is PickupOrderStatusCodes.Cancelled or PickupOrderStatusCodes.Failed)
|
||
{
|
||
throw new BusinessException("当前状态不可支付");
|
||
}
|
||
|
||
var amount = pickup.ReceivableAmount ?? pickup.FreightAmount ?? 0;
|
||
if (amount <= 0)
|
||
{
|
||
throw new BusinessException("请先确认揽收运费");
|
||
}
|
||
|
||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == pickup.CustomerId);
|
||
if (customer == null)
|
||
{
|
||
throw new BusinessException("客户不存在");
|
||
}
|
||
|
||
var balanceMutator = LazyServiceProvider.GetRequiredService<CustomerBalanceMutator>();
|
||
await balanceMutator.DebitPickupPayAsync(
|
||
customer,
|
||
amount,
|
||
pickup.Id,
|
||
pickup.PickupNo,
|
||
$"揽收支付 {pickup.PickupNo}");
|
||
|
||
pickup.PayStatus = PickupPayStatusCodes.Paid;
|
||
pickup.PayTime = DateTime.Now;
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
return pickup;
|
||
}
|
||
|
||
private async Task<string?> TryGetPickupNoAsync(int id)
|
||
{
|
||
var pickup = await PickupOrderRepository.FirstOrDefaultAsync(x => x.Id == id);
|
||
return pickup?.PickupNo;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 补承运商并预约(draft → scheduled)。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/schedule")]
|
||
public async Task<PickupOrderOutput> ScheduleAsync([FromBody] PickupOrderScheduleInput input)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||
if (pickup.Status is not (PickupOrderStatusCodes.Draft or PickupOrderStatusCodes.Scheduled))
|
||
{
|
||
throw new BusinessException("当前状态不可预约下发");
|
||
}
|
||
|
||
var carrierService = NormalizeOptional(input.CarrierService);
|
||
if (carrierService == null)
|
||
{
|
||
throw new BusinessException("承运服务不能为空");
|
||
}
|
||
|
||
if (string.Equals(pickup.PayMode, PickupPayModeCodes.Prepaid, StringComparison.OrdinalIgnoreCase)
|
||
&& pickup.PayStatus != PickupPayStatusCodes.Paid)
|
||
{
|
||
throw new BusinessException("请先支付揽收运费");
|
||
}
|
||
|
||
var amount = pickup.ReceivableAmount ?? pickup.FreightAmount ?? 0;
|
||
if (amount <= 0)
|
||
{
|
||
throw new BusinessException("请先确认揽收运费");
|
||
}
|
||
|
||
pickup.CarrierService = carrierService;
|
||
pickup.CarrierCode = NormalizeOptional(input.CarrierCode);
|
||
var carrierWaybillNo = NormalizeOptional(input.CarrierWaybillNo);
|
||
if (carrierWaybillNo != null)
|
||
{
|
||
pickup.CarrierWaybillNo = carrierWaybillNo;
|
||
}
|
||
|
||
if (input.ExpectPickupTime.HasValue && input.ExpectPickupTime.Value != default)
|
||
{
|
||
pickup.ExpectPickupTime = input.ExpectPickupTime.Value;
|
||
}
|
||
|
||
pickup.Status = PickupOrderStatusCodes.Scheduled;
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
|
||
var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id);
|
||
foreach (var line in lines)
|
||
{
|
||
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == line.OrderId);
|
||
if (order == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
order.PickupTime = pickup.ExpectPickupTime;
|
||
await OrderRepository.UpdateAsync(order);
|
||
}
|
||
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 改约(未取件)。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/reschedule")]
|
||
public async Task<PickupOrderOutput> RescheduleAsync([FromBody] PickupOrderRescheduleInput input)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||
if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status))
|
||
{
|
||
throw new BusinessException("当前状态不可改约");
|
||
}
|
||
|
||
if (input.ExpectPickupTime == default)
|
||
{
|
||
throw new BusinessException("期望揽收时间不能为空");
|
||
}
|
||
|
||
pickup.ExpectPickupTime = input.ExpectPickupTime;
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
|
||
var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id);
|
||
foreach (var line in lines)
|
||
{
|
||
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == line.OrderId);
|
||
if (order == null)
|
||
{
|
||
continue;
|
||
}
|
||
|
||
order.PickupTime = pickup.ExpectPickupTime;
|
||
await OrderRepository.UpdateAsync(order);
|
||
}
|
||
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 取消揽收单。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/cancel")]
|
||
public async Task<PickupOrderOutput> CancelAsync([FromBody] PickupOrderIdInput input)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||
if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status))
|
||
{
|
||
throw new BusinessException("当前状态不可取消");
|
||
}
|
||
|
||
if (pickup.PayStatus == PickupPayStatusCodes.Paid)
|
||
{
|
||
throw new BusinessException("已支付揽收单取消请先处理退款(P1 人工)");
|
||
}
|
||
|
||
var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id);
|
||
foreach (var line in lines)
|
||
{
|
||
await PickupOrderLineRepository.HardDeleteAsync(line);
|
||
var order = await OrderRepository.FirstOrDefaultAsync(x => x.Id == line.OrderId);
|
||
if (order != null && order.PickupOrderId == pickup.Id)
|
||
{
|
||
order.PickupOrderId = null;
|
||
await OrderRepository.UpdateAsync(order);
|
||
}
|
||
}
|
||
|
||
pickup.Status = PickupOrderStatusCodes.Cancelled;
|
||
pickup.ParcelCount = 0;
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 标记已取件。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/mark-picked")]
|
||
public async Task<PickupOrderOutput> MarkPickedAsync([FromBody] PickupOrderIdInput input)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||
if (pickup.Status is not (PickupOrderStatusCodes.Scheduled or PickupOrderStatusCodes.Dispatched))
|
||
{
|
||
throw new BusinessException("仅已预约/已派车状态可标记取件");
|
||
}
|
||
|
||
pickup.Status = PickupOrderStatusCodes.Picked;
|
||
pickup.ActualPickupTime = DateTime.Now;
|
||
if (pickup.SettlementStatus == PickupSettlementStatusCodes.None)
|
||
{
|
||
pickup.SettlementStatus = PickupSettlementStatusCodes.Pending;
|
||
}
|
||
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 标记已到仓。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/mark-inbound")]
|
||
public async Task<PickupOrderOutput> MarkInboundAsync([FromBody] PickupOrderIdInput input)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||
if (pickup.Status != PickupOrderStatusCodes.Picked)
|
||
{
|
||
throw new BusinessException("仅已取件状态可确认到仓");
|
||
}
|
||
|
||
pickup.Status = PickupOrderStatusCodes.Inbound;
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
return await MapOutputAsync(pickup, includeOrders: true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 供小包创建成功后内部挂载(失败由调用方吞掉并回显)。
|
||
/// </summary>
|
||
public async Task<PickupOrderOutput> CreateForParcelInternalAsync(PickupOrderCreateInput input)
|
||
{
|
||
return await CreateAsync(input);
|
||
}
|
||
|
||
private async Task<PickupOrder> GetOwnedPickupOrThrowAsync(int id)
|
||
{
|
||
if (id <= 0)
|
||
{
|
||
throw new BusinessException("揽收单Id无效");
|
||
}
|
||
|
||
var pickup = await PickupOrderRepository.FirstOrDefaultAsync(x => x.Id == id);
|
||
if (pickup == null)
|
||
{
|
||
throw new BusinessException("揽收单不存在");
|
||
}
|
||
|
||
if (OmsSessionHelper.IsCustomerLogin(CurrentUser))
|
||
{
|
||
var customerId = await WarehouseCustomerHelper.ResolveRequiredCustomerIdAsync(
|
||
CustomerRepository,
|
||
CurrentUser,
|
||
null);
|
||
if (pickup.CustomerId != customerId)
|
||
{
|
||
throw new BusinessException("揽收单不存在");
|
||
}
|
||
}
|
||
|
||
return pickup;
|
||
}
|
||
|
||
private static string? ValidateOrderForBind(
|
||
Order order,
|
||
int customerId,
|
||
int warehouseId,
|
||
int senderAddressId,
|
||
PickupOrderLine? existingLine)
|
||
{
|
||
if (order.CustomerId != customerId)
|
||
{
|
||
return "不属于当前客户";
|
||
}
|
||
|
||
if (string.Equals(order.OrderStatus, OrderStatusCodes.Deleted, StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return "订单已删除";
|
||
}
|
||
|
||
if (!BindableOrderStatuses.Contains(order.OrderStatus ?? string.Empty))
|
||
{
|
||
return $"订单状态不可挂载揽收({order.OrderStatus})";
|
||
}
|
||
|
||
if (order.WarehouseId != warehouseId)
|
||
{
|
||
return "仓库不一致";
|
||
}
|
||
|
||
if (existingLine != null)
|
||
{
|
||
return existingLine.PickupOrderId == order.PickupOrderId
|
||
? "已在当前揽收单中"
|
||
: $"已绑定其他揽收单(Id={existingLine.PickupOrderId})";
|
||
}
|
||
|
||
if (order.PickupOrderId is > 0)
|
||
{
|
||
return $"已绑定其他揽收单(Id={order.PickupOrderId})";
|
||
}
|
||
|
||
// 已有发件地址快照时要求与揽收同址;尚未绑定时以本次选择为准
|
||
if (order.CustomerSenderAddressId is > 0 && order.CustomerSenderAddressId != senderAddressId)
|
||
{
|
||
return "取件地址不一致";
|
||
}
|
||
|
||
return null;
|
||
}
|
||
|
||
private async Task RefreshParcelCountAsync(PickupOrder pickup)
|
||
{
|
||
var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id);
|
||
pickup.ParcelCount = lines.Count;
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 揽收单预报下游:createOrderOne 请求体全部从 t_pickup_order 组装(不读关联物流单)。
|
||
/// </summary>
|
||
[HttpPost("/order/pickup-order/downstream-forward")]
|
||
public async Task<OrderDownstreamForwardResult> DownstreamForwardAsync([FromBody] PickupOrderIdInput input)
|
||
{
|
||
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
|
||
if (string.IsNullOrWhiteSpace(pickup.CarrierService))
|
||
{
|
||
throw new BusinessException("请先选择承运服务后再预报下游");
|
||
}
|
||
|
||
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == pickup.WarehouseId);
|
||
if (warehouse == null)
|
||
{
|
||
throw new BusinessException("仓库不存在");
|
||
}
|
||
|
||
var (body, _) = await PickupOrderDownstreamCreateOneBodyBuilder.BuildAsync(
|
||
pickup,
|
||
warehouse,
|
||
SalesProductRepository,
|
||
ServiceChannelRepository);
|
||
|
||
if (string.IsNullOrWhiteSpace(body.ProductCode))
|
||
{
|
||
throw new BusinessException("无法解析下游 productCode,请检查承运服务对应渠道的预报编码");
|
||
}
|
||
|
||
var creds = await ResolvePickupDownstreamCredentialsAsync(pickup);
|
||
var forward = await ItfxForwardClient.ForwardCreateOrderOneAsync(
|
||
creds.SupplierCode,
|
||
creds.AppKey,
|
||
creds.AppSecret,
|
||
body);
|
||
|
||
if (forward.Success
|
||
&& ItfxCreateOrderOneDataParser.TryParse(forward.ResponseBody, out var data)
|
||
&& data != null)
|
||
{
|
||
var changed = false;
|
||
var waybill = !string.IsNullOrWhiteSpace(data.TrackNo)
|
||
? data.TrackNo.Trim()
|
||
: data.ServiceNo?.Trim();
|
||
if (!string.IsNullOrWhiteSpace(waybill)
|
||
&& !string.Equals(pickup.CarrierWaybillNo, waybill, StringComparison.Ordinal))
|
||
{
|
||
pickup.CarrierWaybillNo = waybill;
|
||
changed = true;
|
||
}
|
||
|
||
if (string.IsNullOrWhiteSpace(pickup.CarrierCode)
|
||
&& !string.IsNullOrWhiteSpace(creds.SupplierCode))
|
||
{
|
||
pickup.CarrierCode = creds.SupplierCode;
|
||
changed = true;
|
||
}
|
||
|
||
if (changed)
|
||
{
|
||
await PickupOrderRepository.UpdateAsync(pickup);
|
||
}
|
||
}
|
||
|
||
return new OrderDownstreamForwardResult
|
||
{
|
||
Success = forward.Success,
|
||
SupplierCode = creds.SupplierCode,
|
||
Url = forward.Url,
|
||
RequestJson = forward.RequestJson,
|
||
StatusCode = forward.StatusCode,
|
||
ResponseBody = forward.ResponseBody,
|
||
DownstreamServiceNo = pickup.CarrierWaybillNo,
|
||
TrackNo = pickup.CarrierWaybillNo,
|
||
};
|
||
}
|
||
|
||
/// <summary>
|
||
/// 凭证:优先揽收单 CarrierCode 作为 supplierCode;AppKey/Secret 仍按承运服务→渠道分组解析。
|
||
/// </summary>
|
||
private async Task<OrderDownstreamCredentials> ResolvePickupDownstreamCredentialsAsync(PickupOrder pickup)
|
||
{
|
||
var salesProductCode = pickup.CarrierService!.Trim();
|
||
var product = await SalesProductRepository.GetByProductCodeAsync(salesProductCode);
|
||
if (product == null)
|
||
{
|
||
throw new BusinessException($"未找到承运服务对应销售产品「{salesProductCode}」");
|
||
}
|
||
|
||
var providers = await SalesProductRepository.GetAllocationProvidersAsync(product.Id);
|
||
var channelId = providers
|
||
.Where(x => x.ServiceChannelId is > 0)
|
||
.OrderBy(x => x.SerialNumber)
|
||
.Select(x => x.ServiceChannelId!.Value)
|
||
.FirstOrDefault();
|
||
if (channelId <= 0)
|
||
{
|
||
throw new BusinessException($"销售产品「{salesProductCode}」未配置服务渠道");
|
||
}
|
||
|
||
var channel = await ServiceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId);
|
||
if (channel == null)
|
||
{
|
||
throw new BusinessException("承运服务关联的服务渠道不存在");
|
||
}
|
||
|
||
if (!channel.GroupId.HasValue || channel.GroupId.Value <= 0)
|
||
{
|
||
throw new BusinessException($"渠道「{channel.ChannelCode}」未关联服务商分组");
|
||
}
|
||
|
||
var group = await ServiceChannelGroupRepository.FirstOrDefaultAsync(x => x.Id == channel.GroupId.Value);
|
||
if (group == null)
|
||
{
|
||
throw new BusinessException("服务商分组不存在或已删除");
|
||
}
|
||
|
||
if (group.AppId <= 0 || group.ApiServiceProviderId <= 0)
|
||
{
|
||
throw new BusinessException($"服务商「{group.Name}」未配置应用或 API 服务商");
|
||
}
|
||
|
||
var appInfo = await ItfxMetadataClient.GetAppAsync(group.AppId);
|
||
var appKey = appInfo?.AppKey?.Trim();
|
||
var appSecret = appInfo?.AppSecret?.Trim();
|
||
if (string.IsNullOrWhiteSpace(appKey) || string.IsNullOrWhiteSpace(appSecret))
|
||
{
|
||
throw new BusinessException(
|
||
"无法获取应用 AppKey/AppSecret,请检查服务商应用配置或「其他配置-API中心服务」");
|
||
}
|
||
|
||
var supplierCode = pickup.CarrierCode?.Trim();
|
||
if (string.IsNullOrWhiteSpace(supplierCode))
|
||
{
|
||
var sp = await ItfxMetadataClient.GetServiceProviderAsync(group.ApiServiceProviderId);
|
||
if (sp == null || string.IsNullOrWhiteSpace(sp.Code))
|
||
{
|
||
throw new BusinessException("API 服务商编码(supplierCode)未配置");
|
||
}
|
||
|
||
supplierCode = sp.Code.Trim();
|
||
}
|
||
|
||
return new OrderDownstreamCredentials
|
||
{
|
||
SupplierCode = supplierCode,
|
||
AppKey = appKey,
|
||
AppSecret = appSecret
|
||
};
|
||
}
|
||
|
||
private async Task<string> GeneratePickupNoAsync()
|
||
{
|
||
for (var i = 0; i < 8; i++)
|
||
{
|
||
var no = $"PK{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
|
||
var existed = await PickupOrderRepository.GetByPickupNoAsync(no);
|
||
if (existed == null)
|
||
{
|
||
return no;
|
||
}
|
||
}
|
||
|
||
throw new BusinessException("揽收单号生成失败");
|
||
}
|
||
|
||
private async Task<PickupOrderOutput> MapOutputAsync(PickupOrder entity, bool includeOrders)
|
||
{
|
||
string? warehouseName = null;
|
||
var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == entity.WarehouseId);
|
||
if (warehouse != null)
|
||
{
|
||
warehouseName = warehouse.WarehouseName;
|
||
}
|
||
|
||
string? customerName = null;
|
||
var customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == entity.CustomerId);
|
||
if (customer != null)
|
||
{
|
||
customerName = customer.CustomerName;
|
||
}
|
||
|
||
var output = new PickupOrderOutput
|
||
{
|
||
Id = entity.Id,
|
||
PickupNo = entity.PickupNo,
|
||
RequestId = entity.RequestId,
|
||
CustomerId = entity.CustomerId,
|
||
CustomerName = customerName,
|
||
OrganizationId = entity.OrganizationId,
|
||
SecretLevel = entity.SecretLevel,
|
||
SenderAddressId = entity.SenderAddressId,
|
||
Direction = PickupDirectionCodes.NormalizeOrDefault(entity.Direction),
|
||
SenderName = entity.SenderName,
|
||
SenderPhone = entity.SenderPhone,
|
||
SenderTelePhone = entity.SenderTelePhone,
|
||
SenderEmail = entity.SenderEmail,
|
||
SenderCompany = entity.SenderCompany,
|
||
SenderCountry = entity.SenderCountry,
|
||
SenderCountryCode = entity.SenderCountryCode,
|
||
SenderProvince = entity.SenderProvince,
|
||
SenderCity = entity.SenderCity,
|
||
SenderZipCode = entity.SenderZipCode,
|
||
SenderStreetAddress1 = entity.SenderStreetAddress1,
|
||
SenderStreetAddress2 = entity.SenderStreetAddress2,
|
||
SenderStreetAddress3 = entity.SenderStreetAddress3,
|
||
SenderHouseNumber = entity.SenderHouseNumber,
|
||
WarehouseId = entity.WarehouseId,
|
||
WarehouseName = warehouseName,
|
||
CarrierCode = entity.CarrierCode,
|
||
CarrierService = entity.CarrierService,
|
||
CarrierWaybillNo = entity.CarrierWaybillNo,
|
||
ExpectPickupTime = entity.ExpectPickupTime,
|
||
ActualPickupTime = entity.ActualPickupTime,
|
||
Status = entity.Status,
|
||
ParcelCount = entity.ParcelCount,
|
||
TotalActualWeight = entity.TotalActualWeight,
|
||
ChargeableWeight = entity.ChargeableWeight,
|
||
Currency = entity.Currency,
|
||
FreightAmount = entity.FreightAmount,
|
||
OtherFeeAmount = entity.OtherFeeAmount,
|
||
ReceivableAmount = entity.ReceivableAmount,
|
||
PayStatus = entity.PayStatus,
|
||
PayTime = entity.PayTime,
|
||
PayMode = entity.PayMode,
|
||
CarrierPayableAmount = entity.CarrierPayableAmount,
|
||
SettlementStatus = entity.SettlementStatus,
|
||
SettlementBatchNo = entity.SettlementBatchNo,
|
||
Remark = entity.Remark,
|
||
CreateTime = entity.CreateTime,
|
||
UpdateTime = entity.UpdateTime
|
||
};
|
||
|
||
if (!includeOrders)
|
||
{
|
||
return output;
|
||
}
|
||
|
||
var lines = await PickupOrderLineRepository.GetByPickupOrderIdAsync(entity.Id);
|
||
if (lines.Count == 0)
|
||
{
|
||
return output;
|
||
}
|
||
|
||
var orderIds = lines.Select(x => x.OrderId).ToList();
|
||
var orders = await OrderRepository.GetListAsync(x => orderIds.Contains(x.Id));
|
||
var orderMap = orders.ToDictionary(x => x.Id);
|
||
var weights = await OrderRepository.GetWeightsByOrderIdsAsync(orderIds);
|
||
var weightMap = weights
|
||
.GroupBy(x => x.OrderId)
|
||
.ToDictionary(
|
||
g => g.Key,
|
||
g => g.OrderByDescending(w => w.Type == OrderWeightTypeCodes.Receive ? 2 : 1)
|
||
.Select(w => w.ChargeableWeight ?? w.ActualWeight)
|
||
.FirstOrDefault());
|
||
|
||
output.Orders = lines
|
||
.OrderBy(x => x.SortNo)
|
||
.Select(line =>
|
||
{
|
||
orderMap.TryGetValue(line.OrderId, out var order);
|
||
weightMap.TryGetValue(line.OrderId, out var cw);
|
||
return new PickupOrderLineOutput
|
||
{
|
||
OrderId = line.OrderId,
|
||
ServiceNo = order?.ServiceNo,
|
||
ClientNo = order?.ClientNo,
|
||
OrderStatus = order?.OrderStatus,
|
||
WarehouseId = order?.WarehouseId,
|
||
ChargeableWeight = cw
|
||
};
|
||
})
|
||
.ToList();
|
||
|
||
return output;
|
||
}
|
||
|
||
private static string? NormalizeOptional(string? value)
|
||
{
|
||
var v = value?.Trim();
|
||
return string.IsNullOrEmpty(v) ? null : v;
|
||
}
|
||
}
|