diff --git a/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs b/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs
index d9fd2fd..3633ccd 100644
--- a/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs
+++ b/src/Oms.Application/Dtos/Orders/PickupOrderDtos.cs
@@ -176,6 +176,44 @@ public class PickupOrderRescheduleInput
public DateTime ExpectPickupTime { get; set; }
}
+///
+/// 修改揽收信息(未支付且 draft/scheduled)。
+///
+public class PickupOrderUpdateInfoInput
+{
+ [Required]
+ public int Id { get; set; }
+
+ public int? WarehouseId { get; set; }
+
+ public int? SenderAddressId { get; set; }
+
+ public DateTime? ExpectPickupTime { get; set; }
+
+ public string? CarrierService { get; set; }
+
+ public string? CarrierWaybillNo { get; set; }
+
+ [StringLength(512)]
+ public string? Remark { get; set; }
+}
+
+///
+/// 修改件重尺(未支付;不改费用,需另行重算)。
+///
+public class PickupOrderUpdateMeasureInput
+{
+ [Required]
+ public int Id { get; set; }
+
+ [Required]
+ public decimal TotalActualWeight { get; set; }
+
+ [Required]
+ [MinLength(1)]
+ public List PackageSizes { get; set; } = [];
+}
+
public class PickupOrderRejectItem
{
public int OrderId { get; set; }
diff --git a/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs b/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs
index 19eef43..fa7489d 100644
--- a/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs
+++ b/src/Oms.Application/Services/Orders/PickupOrderFeePushService.cs
@@ -74,15 +74,66 @@ public class PickupOrderFeePushService : IScopedDependency
var pickup = await _pickupOrderRepository.GetByCarrierWaybillNoAsync(carrierWaybillNo)
?? throw new BusinessException($"揽收单不存在:承运运单号 {carrierWaybillNo}");
- if (pickup.PayStatus == PickupPayStatusCodes.Paid)
+ EnsureCanReceiveMeasure(pickup);
+
+ return await ApplyMeasureAndBillAsync(
+ pickup,
+ input.TotalActualWeight,
+ sizes,
+ input.CarrierFreightWeight,
+ input.CarrierQuotedFreightAmount,
+ input.CarrierQuotedReceivableAmount,
+ input.ValueInfoList);
+ }
+
+ ///
+ /// 按已保存(或入参)件重尺,用当前承运服务卖价/买价重算出账。
+ ///
+ public async Task RecalcByPickupAsync(
+ PickupOrder pickup,
+ decimal? totalActualWeightOverride = null,
+ IReadOnlyList? sizesOverride = null)
+ {
+ EnsureCanReceiveMeasure(pickup);
+
+ var weight = totalActualWeightOverride ?? pickup.TotalActualWeight ?? 0;
+ if (weight <= 0)
{
- throw new BusinessException("揽收单已支付,拒绝修改件重尺与费用");
+ throw new BusinessException("请先填写实重后再重算费用");
}
- if (PickupOrderStatusCodes.IsTerminal(pickup.Status)
- && pickup.Status != PickupOrderStatusCodes.Inbound)
+ var sizes = sizesOverride != null && sizesOverride.Count > 0
+ ? NormalizeSizes(sizesOverride.ToList())
+ : NormalizeSizes(
+ OmsDocHelper.DeserializeJson>(pickup.PackageSizesJson));
+ if (sizes.Count == 0)
{
- throw new BusinessException("当前揽收单状态不可接收件重尺");
+ throw new BusinessException("请先填写件重尺尺寸后再重算费用");
+ }
+
+ return await ApplyMeasureAndBillAsync(pickup, weight, sizes);
+ }
+
+ public async Task ApplyMeasureAndBillAsync(
+ PickupOrder pickup,
+ decimal totalActualWeight,
+ IReadOnlyList sizes,
+ decimal? carrierFreightWeight = null,
+ decimal? carrierQuotedFreightAmount = null,
+ decimal? carrierQuotedReceivableAmount = null,
+ List? valueInfoList = null)
+ {
+ if (totalActualWeight <= 0)
+ {
+ throw new BusinessException("totalActualWeight 必须大于 0");
+ }
+
+ var normalizedSizes = sizes is List list
+ ? NormalizeSizes(list)
+ : NormalizeSizes(sizes.ToList());
+ if (normalizedSizes.Count == 0)
+ {
+ throw new BusinessException("packageSizes 不能为空");
}
if (string.IsNullOrWhiteSpace(pickup.CarrierService))
@@ -93,14 +144,14 @@ public class PickupOrderFeePushService : IScopedDependency
var product = await _salesProductRepository.GetByProductCodeAsync(pickup.CarrierService.Trim())
?? throw new BusinessException($"未找到承运服务对应销售产品「{pickup.CarrierService}」");
- var totalVolume = sizes.Sum(x => x.Length * x.Width * x.Height * x.Count);
+ var totalVolume = normalizedSizes.Sum(x => x.Length * x.Width * x.Height * x.Count);
var weightRow = new OrderWeight
{
Type = OrderWeightTypeCodes.Receive,
Length = totalVolume,
Width = 1,
Height = 1,
- ActualWeight = input.TotalActualWeight,
+ ActualWeight = totalActualWeight,
Cubic = ParcelBillingCalculator.CalculateCubic(totalVolume, 1, 1)
};
@@ -187,17 +238,33 @@ public class PickupOrderFeePushService : IScopedDependency
var freight = receivableFees.FirstOrDefault()?.Amount ?? receivableTotal;
var other = Math.Max(0, receivableTotal - freight);
- pickup.TotalActualWeight = input.TotalActualWeight;
+ pickup.TotalActualWeight = totalActualWeight;
pickup.VolumetricWeight = volumetricWeight;
pickup.ChargeableWeight = chargeableWeight;
- pickup.PackageSizesJson = OmsDocHelper.SerializeJson(sizes);
- pickup.CarrierFreightWeight = input.CarrierFreightWeight;
- pickup.CarrierQuotedFreightAmount = input.CarrierQuotedFreightAmount;
- pickup.CarrierQuotedReceivableAmount = input.CarrierQuotedReceivableAmount;
- pickup.CarrierQuoteJson = OmsDocHelper.SerializeJson(new PickupCarrierQuoteOutput
+ pickup.PackageSizesJson = OmsDocHelper.SerializeJson(normalizedSizes);
+ if (carrierFreightWeight.HasValue)
{
- ValueInfoList = input.ValueInfoList ?? []
- });
+ pickup.CarrierFreightWeight = carrierFreightWeight;
+ }
+
+ if (carrierQuotedFreightAmount.HasValue)
+ {
+ pickup.CarrierQuotedFreightAmount = carrierQuotedFreightAmount;
+ }
+
+ if (carrierQuotedReceivableAmount.HasValue)
+ {
+ pickup.CarrierQuotedReceivableAmount = carrierQuotedReceivableAmount;
+ }
+
+ if (valueInfoList != null)
+ {
+ pickup.CarrierQuoteJson = OmsDocHelper.SerializeJson(new PickupCarrierQuoteOutput
+ {
+ ValueInfoList = valueInfoList
+ });
+ }
+
pickup.ReceivableFeeJson = OmsDocHelper.SerializeJson(receivableFees);
pickup.PayableFeeJson = OmsDocHelper.SerializeJson(payableFees);
pickup.FreightAmount = freight;
@@ -211,15 +278,29 @@ public class PickupOrderFeePushService : IScopedDependency
return new PickupOpenApiPushFeeResult
{
PickupNo = pickup.PickupNo,
- TotalActualWeight = input.TotalActualWeight,
+ TotalActualWeight = totalActualWeight,
ChargeableWeight = chargeableWeight,
VolumetricWeight = volumetricWeight,
ReceivableAmount = receivableTotal,
CarrierPayableAmount = payableTotal,
- CarrierQuotedFreightAmount = input.CarrierQuotedFreightAmount
+ CarrierQuotedFreightAmount = pickup.CarrierQuotedFreightAmount
};
}
+ private static void EnsureCanReceiveMeasure(PickupOrder pickup)
+ {
+ if (pickup.PayStatus == PickupPayStatusCodes.Paid)
+ {
+ throw new BusinessException("揽收单已支付,拒绝修改件重尺与费用");
+ }
+
+ if (PickupOrderStatusCodes.IsTerminal(pickup.Status)
+ && pickup.Status != PickupOrderStatusCodes.Inbound)
+ {
+ throw new BusinessException("当前揽收单状态不可接收件重尺");
+ }
+ }
+
private async Task<(decimal VolumetricWeight, decimal ChargeableWeight)> CalculateChargeableAsync(
SalesProduct product,
OrderWeight weight)
diff --git a/src/Oms.Application/Services/Orders/PickupOrderService.cs b/src/Oms.Application/Services/Orders/PickupOrderService.cs
index 4ee5162..b6e5d8a 100644
--- a/src/Oms.Application/Services/Orders/PickupOrderService.cs
+++ b/src/Oms.Application/Services/Orders/PickupOrderService.cs
@@ -69,6 +69,9 @@ public class PickupOrderService : ApplicationService
private IPickupBigBagTrackSyncer PickupBigBagTrackSyncer =>
LazyServiceProvider.GetRequiredService();
+ private PickupOrderFeePushService PickupOrderFeePushService =>
+ LazyServiceProvider.GetRequiredService();
+
///
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
///
@@ -274,6 +277,19 @@ public class PickupOrderService : ApplicationService
await OrderRepository.UpdateAsync(order);
}
+ // 已选承运服务时:创建后自动预报下游拿承运运单号(失败不影响建单)
+ if (!string.IsNullOrWhiteSpace(entity.CarrierService))
+ {
+ try
+ {
+ await DownstreamForwardCoreAsync(entity);
+ }
+ catch
+ {
+ // 建单已成功,预报失败由列表「预报下游」重试
+ }
+ }
+
return await MapOutputAsync(entity, includeOrders: true);
}
@@ -551,6 +567,213 @@ public class PickupOrderService : ApplicationService
return await MapOutputAsync(pickup, includeOrders: true);
}
+ ///
+ /// 修改揽收信息(未支付且 draft/scheduled)。
+ ///
+ [HttpPost("/order/pickup-order/update-info")]
+ public async Task UpdateInfoAsync([FromBody] PickupOrderUpdateInfoInput input)
+ {
+ var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
+ EnsureUnpaidEditable(pickup);
+ if (!PickupOrderStatusCodes.IsMutableBinding(pickup.Status))
+ {
+ throw new BusinessException("当前状态不可修改揽收信息");
+ }
+
+ var warehouseChanged = false;
+ var expectTimeChanged = false;
+ var senderChanged = false;
+
+ if (input.WarehouseId is > 0 && input.WarehouseId.Value != pickup.WarehouseId)
+ {
+ var warehouse = await WarehouseAddressRepository.FirstOrDefaultAsync(x => x.Id == input.WarehouseId.Value);
+ if (warehouse == null)
+ {
+ throw new BusinessException("仓库不存在");
+ }
+
+ pickup.WarehouseId = input.WarehouseId.Value;
+ warehouseChanged = true;
+ }
+
+ if (input.SenderAddressId is > 0 && input.SenderAddressId.Value != pickup.SenderAddressId)
+ {
+ var senderAddress = await CustomerSenderAddressRepository.GetOwnedAsync(
+ input.SenderAddressId.Value,
+ pickup.CustomerId);
+ if (senderAddress == null)
+ {
+ throw new BusinessException("揽收地址不存在");
+ }
+
+ var addressType = string.IsNullOrWhiteSpace(senderAddress.AddressType)
+ ? CustomerAddressTypeCodes.Sender
+ : senderAddress.AddressType;
+ if (!CustomerAddressTypeCodes.IsPickupEligible(addressType))
+ {
+ throw new BusinessException("揽收单请选择「国内揽收」类型地址");
+ }
+
+ string? senderCountryCode = null;
+ if (senderAddress.Country.HasValue)
+ {
+ var country = await CountryRepository.FirstOrDefaultAsync(x => x.Id == senderAddress.Country.Value);
+ senderCountryCode = country?.CountryCode;
+ }
+
+ pickup.SenderAddressId = senderAddress.Id;
+ pickup.SenderName = senderAddress.SenderName ?? string.Empty;
+ pickup.SenderCompany = senderAddress.Company;
+ pickup.SenderCountry = senderAddress.Country;
+ pickup.SenderCountryCode = senderCountryCode;
+ pickup.SenderProvince = senderAddress.Province;
+ pickup.SenderCity = senderAddress.City;
+ pickup.SenderZipCode = senderAddress.ZipCode;
+ pickup.SenderStreetAddress1 = senderAddress.StreetAddress1 ?? string.Empty;
+ pickup.SenderStreetAddress2 = senderAddress.StreetAddress2;
+ pickup.SenderStreetAddress3 = senderAddress.StreetAddress3;
+ pickup.SenderHouseNumber = senderAddress.HouseNumber;
+ pickup.SenderPhone = senderAddress.Phone;
+ pickup.SenderTelePhone = senderAddress.TelePhone;
+ pickup.SenderEmail = senderAddress.Email;
+ senderChanged = true;
+ }
+
+ if (input.ExpectPickupTime.HasValue && input.ExpectPickupTime.Value != default)
+ {
+ pickup.ExpectPickupTime = input.ExpectPickupTime.Value;
+ expectTimeChanged = true;
+ }
+
+ if (input.CarrierService != null)
+ {
+ var carrierService = NormalizeOptional(input.CarrierService);
+ if (carrierService == null)
+ {
+ throw new BusinessException("承运服务不能为空");
+ }
+
+ pickup.CarrierService = carrierService;
+ }
+
+ if (input.CarrierWaybillNo != null)
+ {
+ pickup.CarrierWaybillNo = NormalizeOptional(input.CarrierWaybillNo);
+ }
+
+ if (input.Remark != null)
+ {
+ pickup.Remark = NormalizeOptional(input.Remark);
+ }
+
+ // 写入承运运单号后 draft → scheduled(已预约)
+ if (!string.IsNullOrWhiteSpace(pickup.CarrierWaybillNo)
+ && pickup.Status == PickupOrderStatusCodes.Draft)
+ {
+ pickup.Status = PickupOrderStatusCodes.Scheduled;
+ }
+
+ await PickupOrderRepository.UpdateAsync(pickup);
+
+ if (warehouseChanged || expectTimeChanged || senderChanged)
+ {
+ 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;
+ }
+
+ if (warehouseChanged)
+ {
+ order.WarehouseId = pickup.WarehouseId;
+ }
+
+ if (expectTimeChanged)
+ {
+ order.PickupTime = pickup.ExpectPickupTime;
+ }
+
+ if (senderChanged)
+ {
+ order.CustomerSenderAddressId = pickup.SenderAddressId;
+ }
+
+ await OrderRepository.UpdateAsync(order);
+ }
+ }
+
+ return await MapOutputAsync(pickup, includeOrders: true);
+ }
+
+ ///
+ /// 修改件重尺(未支付;仅落库,不改费用)。
+ ///
+ [HttpPost("/order/pickup-order/update-measure")]
+ public async Task UpdateMeasureAsync([FromBody] PickupOrderUpdateMeasureInput input)
+ {
+ var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
+ EnsureUnpaidEditable(pickup);
+ if (PickupOrderStatusCodes.IsTerminal(pickup.Status)
+ && pickup.Status != PickupOrderStatusCodes.Inbound)
+ {
+ throw new BusinessException("当前状态不可修改件重尺");
+ }
+
+ if (input.TotalActualWeight <= 0)
+ {
+ throw new BusinessException("实重必须大于 0");
+ }
+
+ var sizes = (input.PackageSizes ?? [])
+ .Select(x => new PickupPackageSizeOutput
+ {
+ Length = x.Length,
+ Width = x.Width,
+ Height = x.Height,
+ Count = x.Count <= 0 ? 1 : x.Count
+ })
+ .Where(x => x.Length > 0 && x.Width > 0 && x.Height > 0)
+ .ToList();
+ if (sizes.Count == 0)
+ {
+ throw new BusinessException("请至少填写一条有效尺寸(长宽高)");
+ }
+
+ pickup.TotalActualWeight = input.TotalActualWeight;
+ pickup.PackageSizesJson = OmsDocHelper.SerializeJson(sizes);
+ pickup.UpdateTime = DateTime.Now;
+ await PickupOrderRepository.UpdateAsync(pickup);
+ return await MapOutputAsync(pickup, includeOrders: true);
+ }
+
+ ///
+ /// 按当前件重尺与承运服务重算费用。
+ ///
+ [HttpPost("/order/pickup-order/recalc-fee")]
+ public async Task RecalcFeeAsync([FromBody] PickupOrderIdInput input)
+ {
+ var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
+ EnsureUnpaidEditable(pickup);
+ await PickupOrderFeePushService.RecalcByPickupAsync(pickup);
+ return await MapOutputAsync(pickup, includeOrders: true);
+ }
+
+ private static void EnsureUnpaidEditable(PickupOrder pickup)
+ {
+ if (pickup.PayStatus == PickupPayStatusCodes.Paid)
+ {
+ throw new BusinessException("已支付不可修改");
+ }
+
+ if (pickup.Status is PickupOrderStatusCodes.Cancelled or PickupOrderStatusCodes.Failed)
+ {
+ throw new BusinessException("当前状态不可修改");
+ }
+ }
+
///
/// 客户支付揽收运费(余额)。
///
@@ -930,6 +1153,14 @@ public class PickupOrderService : ApplicationService
public async Task DownstreamForwardAsync([FromBody] PickupOrderIdInput input)
{
var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
+ return await DownstreamForwardCoreAsync(pickup);
+ }
+
+ ///
+ /// 预报下游核心:回写承运运单号,draft 升为 scheduled。
+ ///
+ private async Task DownstreamForwardCoreAsync(PickupOrder pickup)
+ {
if (string.IsNullOrWhiteSpace(pickup.CarrierService))
{
throw new BusinessException("请先选择承运服务后再预报下游");
@@ -974,6 +1205,14 @@ public class PickupOrderService : ApplicationService
changed = true;
}
+ // 拿到承运运单号后 draft → scheduled(已预约)
+ if (!string.IsNullOrWhiteSpace(pickup.CarrierWaybillNo)
+ && pickup.Status == PickupOrderStatusCodes.Draft)
+ {
+ pickup.Status = PickupOrderStatusCodes.Scheduled;
+ changed = true;
+ }
+
if (string.IsNullOrWhiteSpace(pickup.CarrierCode)
&& !string.IsNullOrWhiteSpace(creds.SupplierCode))
{