feat: 揽收单支持改信息/件重尺/重算费用,创建后自动预报下游
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s

创建成功后自动下游预报回写承运单号并升为已预约;详情可改揽收信息与件重尺并按卖买价重算。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-10-09 16:12:12 +08:00
parent 6852135774
commit 358738c333
3 changed files with 375 additions and 17 deletions

View File

@ -176,6 +176,44 @@ public class PickupOrderRescheduleInput
public DateTime ExpectPickupTime { get; set; } public DateTime ExpectPickupTime { get; set; }
} }
/// <summary>
/// 修改揽收信息(未支付且 draft/scheduled)。
/// </summary>
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; }
}
/// <summary>
/// 修改件重尺(未支付;不改费用,需另行重算)。
/// </summary>
public class PickupOrderUpdateMeasureInput
{
[Required]
public int Id { get; set; }
[Required]
public decimal TotalActualWeight { get; set; }
[Required]
[MinLength(1)]
public List<PickupPackageSizeOutput> PackageSizes { get; set; } = [];
}
public class PickupOrderRejectItem public class PickupOrderRejectItem
{ {
public int OrderId { get; set; } public int OrderId { get; set; }

View File

@ -74,15 +74,66 @@ public class PickupOrderFeePushService : IScopedDependency
var pickup = await _pickupOrderRepository.GetByCarrierWaybillNoAsync(carrierWaybillNo) var pickup = await _pickupOrderRepository.GetByCarrierWaybillNoAsync(carrierWaybillNo)
?? throw new BusinessException($"揽收单不存在:承运运单号 {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);
}
/// <summary>
/// 按已保存(或入参)件重尺,用当前承运服务卖价/买价重算出账。
/// </summary>
public async Task<PickupOpenApiPushFeeResult> RecalcByPickupAsync(
PickupOrder pickup,
decimal? totalActualWeightOverride = null,
IReadOnlyList<PickupPackageSizeOutput>? sizesOverride = null)
{
EnsureCanReceiveMeasure(pickup);
var weight = totalActualWeightOverride ?? pickup.TotalActualWeight ?? 0;
if (weight <= 0)
{ {
throw new BusinessException("揽收单已支付,拒绝修改件重尺与费用"); throw new BusinessException("请先填写实重后再重算费用");
} }
if (PickupOrderStatusCodes.IsTerminal(pickup.Status) var sizes = sizesOverride != null && sizesOverride.Count > 0
&& pickup.Status != PickupOrderStatusCodes.Inbound) ? NormalizeSizes(sizesOverride.ToList())
: NormalizeSizes(
OmsDocHelper.DeserializeJson<List<PickupPackageSizeOutput>>(pickup.PackageSizesJson));
if (sizes.Count == 0)
{ {
throw new BusinessException("当前揽收单状态不可接收件重尺"); throw new BusinessException("请先填写件重尺尺寸后再重算费用");
}
return await ApplyMeasureAndBillAsync(pickup, weight, sizes);
}
public async Task<PickupOpenApiPushFeeResult> ApplyMeasureAndBillAsync(
PickupOrder pickup,
decimal totalActualWeight,
IReadOnlyList<PickupPackageSizeOutput> sizes,
decimal? carrierFreightWeight = null,
decimal? carrierQuotedFreightAmount = null,
decimal? carrierQuotedReceivableAmount = null,
List<PickupCarrierValueFeeOutput>? valueInfoList = null)
{
if (totalActualWeight <= 0)
{
throw new BusinessException("totalActualWeight 必须大于 0");
}
var normalizedSizes = sizes is List<PickupPackageSizeOutput> list
? NormalizeSizes(list)
: NormalizeSizes(sizes.ToList());
if (normalizedSizes.Count == 0)
{
throw new BusinessException("packageSizes 不能为空");
} }
if (string.IsNullOrWhiteSpace(pickup.CarrierService)) if (string.IsNullOrWhiteSpace(pickup.CarrierService))
@ -93,14 +144,14 @@ public class PickupOrderFeePushService : IScopedDependency
var product = await _salesProductRepository.GetByProductCodeAsync(pickup.CarrierService.Trim()) var product = await _salesProductRepository.GetByProductCodeAsync(pickup.CarrierService.Trim())
?? throw new BusinessException($"未找到承运服务对应销售产品「{pickup.CarrierService}」"); ?? 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 var weightRow = new OrderWeight
{ {
Type = OrderWeightTypeCodes.Receive, Type = OrderWeightTypeCodes.Receive,
Length = totalVolume, Length = totalVolume,
Width = 1, Width = 1,
Height = 1, Height = 1,
ActualWeight = input.TotalActualWeight, ActualWeight = totalActualWeight,
Cubic = ParcelBillingCalculator.CalculateCubic(totalVolume, 1, 1) Cubic = ParcelBillingCalculator.CalculateCubic(totalVolume, 1, 1)
}; };
@ -187,17 +238,33 @@ public class PickupOrderFeePushService : IScopedDependency
var freight = receivableFees.FirstOrDefault()?.Amount ?? receivableTotal; var freight = receivableFees.FirstOrDefault()?.Amount ?? receivableTotal;
var other = Math.Max(0, receivableTotal - freight); var other = Math.Max(0, receivableTotal - freight);
pickup.TotalActualWeight = input.TotalActualWeight; pickup.TotalActualWeight = totalActualWeight;
pickup.VolumetricWeight = volumetricWeight; pickup.VolumetricWeight = volumetricWeight;
pickup.ChargeableWeight = chargeableWeight; pickup.ChargeableWeight = chargeableWeight;
pickup.PackageSizesJson = OmsDocHelper.SerializeJson(sizes); pickup.PackageSizesJson = OmsDocHelper.SerializeJson(normalizedSizes);
pickup.CarrierFreightWeight = input.CarrierFreightWeight; if (carrierFreightWeight.HasValue)
pickup.CarrierQuotedFreightAmount = input.CarrierQuotedFreightAmount;
pickup.CarrierQuotedReceivableAmount = input.CarrierQuotedReceivableAmount;
pickup.CarrierQuoteJson = OmsDocHelper.SerializeJson(new PickupCarrierQuoteOutput
{ {
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.ReceivableFeeJson = OmsDocHelper.SerializeJson(receivableFees);
pickup.PayableFeeJson = OmsDocHelper.SerializeJson(payableFees); pickup.PayableFeeJson = OmsDocHelper.SerializeJson(payableFees);
pickup.FreightAmount = freight; pickup.FreightAmount = freight;
@ -211,15 +278,29 @@ public class PickupOrderFeePushService : IScopedDependency
return new PickupOpenApiPushFeeResult return new PickupOpenApiPushFeeResult
{ {
PickupNo = pickup.PickupNo, PickupNo = pickup.PickupNo,
TotalActualWeight = input.TotalActualWeight, TotalActualWeight = totalActualWeight,
ChargeableWeight = chargeableWeight, ChargeableWeight = chargeableWeight,
VolumetricWeight = volumetricWeight, VolumetricWeight = volumetricWeight,
ReceivableAmount = receivableTotal, ReceivableAmount = receivableTotal,
CarrierPayableAmount = payableTotal, 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( private async Task<(decimal VolumetricWeight, decimal ChargeableWeight)> CalculateChargeableAsync(
SalesProduct product, SalesProduct product,
OrderWeight weight) OrderWeight weight)

View File

@ -69,6 +69,9 @@ public class PickupOrderService : ApplicationService
private IPickupBigBagTrackSyncer PickupBigBagTrackSyncer => private IPickupBigBagTrackSyncer PickupBigBagTrackSyncer =>
LazyServiceProvider.GetRequiredService<IPickupBigBagTrackSyncer>(); LazyServiceProvider.GetRequiredService<IPickupBigBagTrackSyncer>();
private PickupOrderFeePushService PickupOrderFeePushService =>
LazyServiceProvider.GetRequiredService<PickupOrderFeePushService>();
/// <summary> /// <summary>
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。 /// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
/// </summary> /// </summary>
@ -274,6 +277,19 @@ public class PickupOrderService : ApplicationService
await OrderRepository.UpdateAsync(order); await OrderRepository.UpdateAsync(order);
} }
// 已选承运服务时:创建后自动预报下游拿承运运单号(失败不影响建单)
if (!string.IsNullOrWhiteSpace(entity.CarrierService))
{
try
{
await DownstreamForwardCoreAsync(entity);
}
catch
{
// 建单已成功,预报失败由列表「预报下游」重试
}
}
return await MapOutputAsync(entity, includeOrders: true); return await MapOutputAsync(entity, includeOrders: true);
} }
@ -551,6 +567,213 @@ public class PickupOrderService : ApplicationService
return await MapOutputAsync(pickup, includeOrders: true); return await MapOutputAsync(pickup, includeOrders: true);
} }
/// <summary>
/// 修改揽收信息(未支付且 draft/scheduled)。
/// </summary>
[HttpPost("/order/pickup-order/update-info")]
public async Task<PickupOrderOutput> 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);
}
/// <summary>
/// 修改件重尺(未支付;仅落库,不改费用)。
/// </summary>
[HttpPost("/order/pickup-order/update-measure")]
public async Task<PickupOrderOutput> 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);
}
/// <summary>
/// 按当前件重尺与承运服务重算费用。
/// </summary>
[HttpPost("/order/pickup-order/recalc-fee")]
public async Task<PickupOrderOutput> 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("当前状态不可修改");
}
}
/// <summary> /// <summary>
/// 客户支付揽收运费(余额)。 /// 客户支付揽收运费(余额)。
/// </summary> /// </summary>
@ -930,6 +1153,14 @@ public class PickupOrderService : ApplicationService
public async Task<OrderDownstreamForwardResult> DownstreamForwardAsync([FromBody] PickupOrderIdInput input) public async Task<OrderDownstreamForwardResult> DownstreamForwardAsync([FromBody] PickupOrderIdInput input)
{ {
var pickup = await GetOwnedPickupOrThrowAsync(input.Id); var pickup = await GetOwnedPickupOrThrowAsync(input.Id);
return await DownstreamForwardCoreAsync(pickup);
}
/// <summary>
/// 预报下游核心:回写承运运单号,draft 升为 scheduled。
/// </summary>
private async Task<OrderDownstreamForwardResult> DownstreamForwardCoreAsync(PickupOrder pickup)
{
if (string.IsNullOrWhiteSpace(pickup.CarrierService)) if (string.IsNullOrWhiteSpace(pickup.CarrierService))
{ {
throw new BusinessException("请先选择承运服务后再预报下游"); throw new BusinessException("请先选择承运服务后再预报下游");
@ -974,6 +1205,14 @@ public class PickupOrderService : ApplicationService
changed = true; changed = true;
} }
// 拿到承运运单号后 draft → scheduled(已预约)
if (!string.IsNullOrWhiteSpace(pickup.CarrierWaybillNo)
&& pickup.Status == PickupOrderStatusCodes.Draft)
{
pickup.Status = PickupOrderStatusCodes.Scheduled;
changed = true;
}
if (string.IsNullOrWhiteSpace(pickup.CarrierCode) if (string.IsNullOrWhiteSpace(pickup.CarrierCode)
&& !string.IsNullOrWhiteSpace(creds.SupplierCode)) && !string.IsNullOrWhiteSpace(creds.SupplierCode))
{ {