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 Microsoft.AspNetCore.Mvc;
|
||||||
using Oms.Application.Dtos.Orders;
|
using Oms.Application.Dtos.Orders;
|
||||||
using Oms.Application.Dtos.Shared;
|
using Oms.Application.Dtos.Shared;
|
||||||
|
using Oms.Application.Integrations.Itfx;
|
||||||
using Oms.Application.Shared;
|
using Oms.Application.Shared;
|
||||||
|
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||||
using Oms.Domain.CustomersAggregate.Repositories;
|
using Oms.Domain.CustomersAggregate.Repositories;
|
||||||
using Oms.Domain.OrdersAggregate.Consts;
|
using Oms.Domain.OrdersAggregate.Consts;
|
||||||
using Oms.Domain.OrdersAggregate.Entities;
|
using Oms.Domain.OrdersAggregate.Entities;
|
||||||
@ -48,6 +50,21 @@ public class PickupOrderService : ApplicationService
|
|||||||
private ICountryRepository CountryRepository =>
|
private ICountryRepository CountryRepository =>
|
||||||
LazyServiceProvider.GetRequiredService<ICountryRepository>();
|
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>
|
||||||
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
|
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -902,6 +919,157 @@ public class PickupOrderService : ApplicationService
|
|||||||
await PickupOrderRepository.UpdateAsync(pickup);
|
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()
|
private async Task<string> GeneratePickupNoAsync()
|
||||||
{
|
{
|
||||||
for (var i = 0; i < 8; i++)
|
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