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;
///
/// 揽收单(1 揽收 : N 物流)管理。
///
[ApiExplorerSettings(GroupName = "OMS-Order")]
public class PickupOrderService : ApplicationService
{
private static readonly HashSet BindableOrderStatuses = new(StringComparer.OrdinalIgnoreCase)
{
OrderStatusCodes.Draft,
OrderStatusCodes.Predeclared,
OrderStatusCodes.PendingAllocation,
OrderStatusCodes.PendingPayment,
OrderStatusCodes.PendingShip,
OrderStatusCodes.PendingOnline,
};
private IPickupOrderRepository PickupOrderRepository =>
LazyServiceProvider.GetRequiredService();
private IPickupOrderLineRepository PickupOrderLineRepository =>
LazyServiceProvider.GetRequiredService();
private IOrderRepository OrderRepository =>
LazyServiceProvider.GetRequiredService();
private ICustomerRepository CustomerRepository =>
LazyServiceProvider.GetRequiredService();
private ICustomerSenderAddressRepository CustomerSenderAddressRepository =>
LazyServiceProvider.GetRequiredService();
private IWarehouseAddressRepository WarehouseAddressRepository =>
LazyServiceProvider.GetRequiredService();
private ICountryRepository CountryRepository =>
LazyServiceProvider.GetRequiredService();
private ISalesProductRepository SalesProductRepository =>
LazyServiceProvider.GetRequiredService();
private IServiceChannelRepository ServiceChannelRepository =>
LazyServiceProvider.GetRequiredService();
private IServiceChannelGroupRepository ServiceChannelGroupRepository =>
LazyServiceProvider.GetRequiredService();
private IItfxForwardClient ItfxForwardClient =>
LazyServiceProvider.GetRequiredService();
private IItfxMetadataClient ItfxMetadataClient =>
LazyServiceProvider.GetRequiredService();
///
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
///
[HttpPost("/order/pickup-order/create")]
public async Task 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();
var accepted = new List();
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);
}
///
/// 追加物流单到已有揽收单。
///
[HttpPost("/order/pickup-order/add-orders")]
public async Task 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();
var accepted = new List();
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);
}
///
/// 从揽收单移除物流单;移除后若无关联则自动取消揽收单。
///
[HttpPost("/order/pickup-order/remove-orders")]
public async Task 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);
}
///
/// 揽收单分页列表。
///
[HttpPost("/order/pickup-order/list")]
public async Task> 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();
foreach (var item in pageItems)
{
outputs.Add(await MapOutputAsync(item, includeOrders: false));
}
return new OmsPageResult { Items = outputs, Total = total };
}
///
/// 揽收单状态计数(Tab 徽章)。
///
[HttpGet("/order/pickup-order/status-counts")]
public async Task 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
};
}
///
/// 揽收单详情。
///
[HttpGet("/order/pickup-order/detail")]
public async Task GetDetailAsync([FromQuery] int id)
{
var pickup = await GetOwnedPickupOrThrowAsync(id);
return await MapOutputAsync(pickup, includeOrders: true);
}
///
/// 确认对客揽收运费。
///
[HttpPost("/order/pickup-order/confirm-freight")]
public async Task 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);
}
///
/// 客户支付揽收运费(余额)。
///
[HttpPost("/order/pickup-order/pay")]
public async Task PayAsync([FromBody] PickupOrderIdInput input)
{
var pickup = await PayCoreAsync(input.Id);
return await MapOutputAsync(pickup, includeOrders: true);
}
///
/// 批量支付揽收运费(逐单扣余额,部分失败不回滚已成功项)。
///
[HttpPost("/order/pickup-order/pay-batch")]
public async Task 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 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();
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 TryGetPickupNoAsync(int id)
{
var pickup = await PickupOrderRepository.FirstOrDefaultAsync(x => x.Id == id);
return pickup?.PickupNo;
}
///
/// 补承运商并预约(draft → scheduled)。
///
[HttpPost("/order/pickup-order/schedule")]
public async Task 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);
}
///
/// 改约(未取件)。
///
[HttpPost("/order/pickup-order/reschedule")]
public async Task 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);
}
///
/// 取消揽收单。
///
[HttpPost("/order/pickup-order/cancel")]
public async Task 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);
}
///
/// 标记已取件。
///
[HttpPost("/order/pickup-order/mark-picked")]
public async Task 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);
}
///
/// 标记已到仓。
///
[HttpPost("/order/pickup-order/mark-inbound")]
public async Task 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);
}
///
/// 供小包创建成功后内部挂载(失败由调用方吞掉并回显)。
///
public async Task CreateForParcelInternalAsync(PickupOrderCreateInput input)
{
return await CreateAsync(input);
}
private async Task 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);
}
///
/// 揽收单预报下游:createOrderOne 请求体全部从 t_pickup_order 组装(不读关联物流单)。
///
[HttpPost("/order/pickup-order/downstream-forward")]
public async Task 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,
};
}
///
/// 凭证:优先揽收单 CarrierCode 作为 supplierCode;AppKey/Secret 仍按承运服务→渠道分组解析。
///
private async Task 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 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 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;
}
}