feat: 揽收单预报下游改从 t_pickup_order 组装 createOrderOne
All checks were successful
Build and Deploy / deploy (push) Successful in 1m43s
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>
This commit is contained in:
parent
623db4ffdb
commit
26f9d8be7f
@ -2,7 +2,9 @@ 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;
|
||||
@ -48,6 +50,21 @@ public class PickupOrderService : ApplicationService
|
||||
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>
|
||||
@ -902,6 +919,157 @@ public class PickupOrderService : ApplicationService
|
||||
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++)
|
||||
|
||||
@ -0,0 +1,190 @@
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Consts;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.SystemAggregate.Entities;
|
||||
|
||||
namespace Oms.Application.Shared;
|
||||
|
||||
/// <summary>
|
||||
/// 从揽收单表组装 itfx createOrderOne 标准请求体(不读关联物流单)。
|
||||
/// </summary>
|
||||
public static class PickupOrderDownstreamCreateOneBodyBuilder
|
||||
{
|
||||
public static async Task<(ItfxCreateOrderOneBody Body, string? ForecastProductCode)> BuildAsync(
|
||||
PickupOrder pickup,
|
||||
WarehouseAddress warehouse,
|
||||
ISalesProductRepository salesProductRepository,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var productCode = await ResolveForecastProductCodeAsync(
|
||||
pickup.CarrierService,
|
||||
salesProductRepository,
|
||||
serviceChannelRepository,
|
||||
cancellationToken);
|
||||
|
||||
var isReverse = string.Equals(
|
||||
PickupDirectionCodes.NormalizeOrDefault(pickup.Direction),
|
||||
PickupDirectionCodes.Reverse,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var customerSide = MapCustomerSide(pickup);
|
||||
var warehouseSide = MapWarehouseSide(warehouse);
|
||||
|
||||
var weight = pickup.TotalActualWeight ?? pickup.ChargeableWeight;
|
||||
var qty = pickup.ParcelCount > 0 ? pickup.ParcelCount : 1;
|
||||
var cargoName = string.IsNullOrWhiteSpace(pickup.Remark)
|
||||
? "上门揽收包裹"
|
||||
: pickup.Remark.Trim();
|
||||
|
||||
return (new ItfxCreateOrderOneBody
|
||||
{
|
||||
ClientNo = pickup.PickupNo,
|
||||
ServiceNo = pickup.PickupNo,
|
||||
TrackNo = pickup.CarrierWaybillNo,
|
||||
ProductCode = productCode,
|
||||
CargoAttribute = CargoAttributeCodes.General,
|
||||
WarehouseId = pickup.WarehouseId,
|
||||
CustomerId = pickup.CustomerId,
|
||||
Length = 1,
|
||||
Width = 1,
|
||||
Height = 1,
|
||||
ActualWeight = weight,
|
||||
ChargeableWeight = pickup.ChargeableWeight ?? weight,
|
||||
IsCod = false,
|
||||
DeclaredCurrency = pickup.Currency,
|
||||
DeclaredSum = FormatDecimal(pickup.ReceivableAmount ?? pickup.FreightAmount ?? 1),
|
||||
Quantity = qty.ToString(),
|
||||
Sender = isReverse ? warehouseSide.Sender : customerSide.Sender,
|
||||
Receiver = isReverse ? customerSide.Receiver : warehouseSide.Receiver,
|
||||
Items =
|
||||
[
|
||||
new OrderParcelItemInput
|
||||
{
|
||||
CnName = cargoName,
|
||||
EnName = cargoName,
|
||||
UnitPrice = 1,
|
||||
UnitWeight = weight,
|
||||
Quantity = qty,
|
||||
Amount = 1,
|
||||
Weight = weight
|
||||
}
|
||||
]
|
||||
}, productCode);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// createOrderOne.productCode:承运服务(销售产品)对应渠道的 ForecastCode,缺省回退 CarrierService。
|
||||
/// </summary>
|
||||
public static async Task<string?> ResolveForecastProductCodeAsync(
|
||||
string? carrierService,
|
||||
ISalesProductRepository salesProductRepository,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var salesProductCode = carrierService?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(salesProductCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var product = await salesProductRepository.GetByProductCodeAsync(salesProductCode, cancellationToken);
|
||||
if (product == null)
|
||||
{
|
||||
return salesProductCode;
|
||||
}
|
||||
|
||||
var providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id, cancellationToken);
|
||||
var channelId = providers
|
||||
.Where(x => x.ServiceChannelId is > 0)
|
||||
.OrderBy(x => x.SerialNumber)
|
||||
.Select(x => x.ServiceChannelId!.Value)
|
||||
.FirstOrDefault();
|
||||
if (channelId <= 0)
|
||||
{
|
||||
return salesProductCode;
|
||||
}
|
||||
|
||||
var channel = await serviceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId, cancellationToken);
|
||||
var forecastCode = channel?.ForecastCode?.Trim();
|
||||
return string.IsNullOrWhiteSpace(forecastCode) ? salesProductCode : forecastCode;
|
||||
}
|
||||
|
||||
private static string? FormatDecimal(decimal? value) =>
|
||||
value.HasValue ? value.Value.ToString(System.Globalization.CultureInfo.InvariantCulture) : null;
|
||||
|
||||
private static (OrderParcelSenderInput Sender, OrderParcelReceiverInput Receiver) MapCustomerSide(PickupOrder pickup)
|
||||
{
|
||||
var sender = new OrderParcelSenderInput
|
||||
{
|
||||
SenderName = pickup.SenderName,
|
||||
Company = pickup.SenderCompany,
|
||||
Phone = pickup.SenderPhone,
|
||||
TelePhone = pickup.SenderTelePhone,
|
||||
Email = pickup.SenderEmail,
|
||||
CountryCode = string.IsNullOrWhiteSpace(pickup.SenderCountryCode)
|
||||
? "CN"
|
||||
: pickup.SenderCountryCode.Trim().ToUpperInvariant(),
|
||||
Province = pickup.SenderProvince,
|
||||
City = pickup.SenderCity,
|
||||
ZipCode = pickup.SenderZipCode,
|
||||
StreetAddress1 = pickup.SenderStreetAddress1,
|
||||
StreetAddress2 = pickup.SenderStreetAddress2,
|
||||
StreetAddress3 = pickup.SenderStreetAddress3,
|
||||
HouseNumber = pickup.SenderHouseNumber
|
||||
};
|
||||
|
||||
var receiver = new OrderParcelReceiverInput
|
||||
{
|
||||
RecipientsName = pickup.SenderName,
|
||||
CompanyName = pickup.SenderCompany,
|
||||
Phone = pickup.SenderPhone,
|
||||
TelePhone = pickup.SenderTelePhone,
|
||||
Email = pickup.SenderEmail,
|
||||
CountryCode = string.IsNullOrWhiteSpace(pickup.SenderCountryCode)
|
||||
? "CN"
|
||||
: pickup.SenderCountryCode.Trim().ToUpperInvariant(),
|
||||
Province = pickup.SenderProvince,
|
||||
City = pickup.SenderCity,
|
||||
ZipCode = string.IsNullOrWhiteSpace(pickup.SenderZipCode) ? "000000" : pickup.SenderZipCode,
|
||||
StreetAddress1 = pickup.SenderStreetAddress1,
|
||||
StreetAddress2 = pickup.SenderStreetAddress2,
|
||||
StreetAddress3 = pickup.SenderStreetAddress3,
|
||||
StreetNumber = pickup.SenderHouseNumber
|
||||
};
|
||||
|
||||
return (sender, receiver);
|
||||
}
|
||||
|
||||
private static (OrderParcelSenderInput Sender, OrderParcelReceiverInput Receiver) MapWarehouseSide(
|
||||
WarehouseAddress warehouse)
|
||||
{
|
||||
var contact = string.IsNullOrWhiteSpace(warehouse.Recipient)
|
||||
? warehouse.WarehouseName
|
||||
: warehouse.Recipient!.Trim();
|
||||
var street = string.IsNullOrWhiteSpace(warehouse.ShippingAddress)
|
||||
? warehouse.WarehouseName
|
||||
: warehouse.ShippingAddress!.Trim();
|
||||
|
||||
var sender = new OrderParcelSenderInput
|
||||
{
|
||||
SenderName = contact,
|
||||
Phone = warehouse.ContactPhone,
|
||||
CountryCode = "CN",
|
||||
StreetAddress1 = street,
|
||||
ZipCode = "000000"
|
||||
};
|
||||
|
||||
var receiver = new OrderParcelReceiverInput
|
||||
{
|
||||
RecipientsName = contact,
|
||||
Phone = warehouse.ContactPhone,
|
||||
CountryCode = "CN",
|
||||
ZipCode = "000000",
|
||||
StreetAddress1 = street
|
||||
};
|
||||
|
||||
return (sender, receiver);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user