feat: 揽收单支持改信息/件重尺/重算费用,创建后自动预报下游
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
创建成功后自动下游预报回写承运单号并升为已预约;详情可改揽收信息与件重尺并按卖买价重算。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
6852135774
commit
358738c333
@ -176,6 +176,44 @@ public class PickupOrderRescheduleInput
|
||||
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 int OrderId { get; set; }
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <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)
|
||||
&& pickup.Status != PickupOrderStatusCodes.Inbound)
|
||||
var sizes = sizesOverride != null && sizesOverride.Count > 0
|
||||
? 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))
|
||||
@ -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)
|
||||
|
||||
@ -69,6 +69,9 @@ public class PickupOrderService : ApplicationService
|
||||
private IPickupBigBagTrackSyncer PickupBigBagTrackSyncer =>
|
||||
LazyServiceProvider.GetRequiredService<IPickupBigBagTrackSyncer>();
|
||||
|
||||
private PickupOrderFeePushService PickupOrderFeePushService =>
|
||||
LazyServiceProvider.GetRequiredService<PickupOrderFeePushService>();
|
||||
|
||||
/// <summary>
|
||||
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
|
||||
/// </summary>
|
||||
@ -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);
|
||||
}
|
||||
|
||||
/// <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>
|
||||
@ -930,6 +1153,14 @@ public class PickupOrderService : ApplicationService
|
||||
public async Task<OrderDownstreamForwardResult> DownstreamForwardAsync([FromBody] PickupOrderIdInput input)
|
||||
{
|
||||
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))
|
||||
{
|
||||
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))
|
||||
{
|
||||
|
||||
Loading…
Reference in New Issue
Block a user