feat: ????????????? A0101/A0301/A0701
Fix UpsertSmallBag ApiCode/TrackNo assembly and MQ-disabled direct fallback; append lifecycle tracks on parcel create, inbound, and outbound. Redact channel fields for customer order list. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
3d5eaa223a
commit
1545bc385f
@ -4,6 +4,15 @@ public interface ISmallBagTrackOrderClient
|
||||
{
|
||||
Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 追加一条生命周期轨迹节点(POST ProviderService/Track/Upsert)。
|
||||
/// </summary>
|
||||
Task AppendLifecycleTrackAsync(
|
||||
string orderNo,
|
||||
string trackCode,
|
||||
string? authorization = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 同步调用并返回完整请求/响应(仅调试)。
|
||||
/// </summary>
|
||||
|
||||
@ -6,4 +6,9 @@ namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
public interface ISmallBagTrackOrderNotifier
|
||||
{
|
||||
void Enqueue(SmallBagTrackUpsertRequest request, int orderId, int? customerId = null);
|
||||
|
||||
/// <summary>
|
||||
/// 异步追加生命周期轨迹节点。
|
||||
/// </summary>
|
||||
void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null);
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
/// <summary>
|
||||
/// 小包订单生命周期默认轨迹节点(POST ProviderService/Track/Upsert)。
|
||||
/// </summary>
|
||||
internal static class SmallBagTrackLifecycleEvents
|
||||
{
|
||||
public const string CreateTrackCode = "A0101";
|
||||
|
||||
public const string InboundTrackCode = "A0301";
|
||||
|
||||
public const string OutboundTrackCode = "A0701";
|
||||
|
||||
public const string TrackPlace = "SZ";
|
||||
|
||||
public const float TrackTimeZone = 8f;
|
||||
|
||||
/// <summary>轨迹订单类型:小包。</summary>
|
||||
public const int OrderType = 1;
|
||||
|
||||
/// <summary>数据来源:内部系统。</summary>
|
||||
public const int Source = 2;
|
||||
}
|
||||
@ -44,7 +44,23 @@ internal sealed class SmallBagTrackOrderClient(
|
||||
}
|
||||
|
||||
logger.LogInformation("UpsertSmallBag succeeded for order {OrderNo}", request.OrderNo);
|
||||
|
||||
if (request.AppendInitialTrackOnSuccess)
|
||||
{
|
||||
await AppendLifecycleTrackAsync(
|
||||
request.OrderNo,
|
||||
SmallBagTrackLifecycleEvents.CreateTrackCode,
|
||||
request.Authorization,
|
||||
cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
public Task AppendLifecycleTrackAsync(
|
||||
string orderNo,
|
||||
string trackCode,
|
||||
string? authorization = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
AppendLifecycleTrackCoreAsync(orderNo, trackCode, authorization, cancellationToken);
|
||||
|
||||
public async Task<SmallBagTrackUpsertResult> UpsertDebugAsync(
|
||||
SmallBagTrackUpsertRequest request,
|
||||
@ -67,14 +83,120 @@ internal sealed class SmallBagTrackOrderClient(
|
||||
}
|
||||
|
||||
var appKey = settings.AppKey.Trim();
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, settings.AppSecret.Trim());
|
||||
var body = SmallBagTrackUpsertApiRequest.FromBusinessRequest(request);
|
||||
var url = SmallBagTrackUpsertApiRequest.BuildEndpointUrl(settings.BaseUrl);
|
||||
var authorization = ResolveAuthorization(request);
|
||||
var curlCommand = SmallBagTrackCurlBuilder.Build(url, appKey, timestamp, sign, authorization, body, JsonOptions);
|
||||
var requestJson = JsonSerializer.Serialize(body, JsonOptions);
|
||||
|
||||
var response = await SendSignedPostAsync(
|
||||
settings,
|
||||
appKey,
|
||||
url,
|
||||
body,
|
||||
authorization,
|
||||
cancellationToken);
|
||||
|
||||
var curlCommand = SmallBagTrackCurlBuilder.Build(
|
||||
url,
|
||||
appKey,
|
||||
response.Timestamp,
|
||||
response.Sign,
|
||||
authorization,
|
||||
body,
|
||||
JsonOptions);
|
||||
|
||||
return new SmallBagTrackUpsertResult
|
||||
{
|
||||
Skipped = false,
|
||||
Success = response.Response.IsSuccessStatusCode,
|
||||
Url = url,
|
||||
RequestJson = requestJson,
|
||||
CurlCommand = curlCommand,
|
||||
StatusCode = (int)response.Response.StatusCode,
|
||||
ResponseBody = response.ResponseBody
|
||||
};
|
||||
}
|
||||
|
||||
private async Task AppendLifecycleTrackCoreAsync(
|
||||
string orderNo,
|
||||
string trackCode,
|
||||
string? authorization,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(orderNo))
|
||||
{
|
||||
logger.LogWarning("Skip lifecycle track append: orderNo empty");
|
||||
return;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(trackCode))
|
||||
{
|
||||
logger.LogWarning("Skip lifecycle track append for order {OrderNo}: trackCode empty", orderNo);
|
||||
return;
|
||||
}
|
||||
|
||||
var settings = await configProvider.GetAsync(cancellationToken);
|
||||
if (!settings.Enabled
|
||||
|| string.IsNullOrWhiteSpace(settings.BaseUrl)
|
||||
|| string.IsNullOrWhiteSpace(settings.AppKey)
|
||||
|| string.IsNullOrWhiteSpace(settings.AppSecret))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var appKey = settings.AppKey.Trim();
|
||||
var trackTime = DateTime.Now;
|
||||
var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent(
|
||||
appKey,
|
||||
orderNo.Trim(),
|
||||
trackCode.Trim(),
|
||||
trackTime);
|
||||
var url = SmallBagTrackUpsertTrackApiRequest.BuildEndpointUrl(settings.BaseUrl);
|
||||
|
||||
try
|
||||
{
|
||||
var response = await SendSignedPostAsync(
|
||||
settings,
|
||||
appKey,
|
||||
url,
|
||||
body,
|
||||
authorization,
|
||||
cancellationToken);
|
||||
|
||||
if (response.Response.IsSuccessStatusCode)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"Lifecycle track {TrackCode} appended for order {OrderNo} at {TrackPlace}",
|
||||
trackCode,
|
||||
orderNo,
|
||||
SmallBagTrackLifecycleEvents.TrackPlace);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogWarning(
|
||||
"Lifecycle track {TrackCode} append failed for order {OrderNo}: {StatusCode} {Body}",
|
||||
trackCode,
|
||||
orderNo,
|
||||
(int)response.Response.StatusCode,
|
||||
response.ResponseBody);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Lifecycle track {TrackCode} append failed for order {OrderNo}", trackCode, orderNo);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<SignedTrackHttpResponse> SendSignedPostAsync<TBody>(
|
||||
SmallBagTrackOptions settings,
|
||||
string appKey,
|
||||
string url,
|
||||
TBody body,
|
||||
string? authorization,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var sign = SmallBagTrackSignHelper.GenerateSign(appKey, timestamp, settings.AppSecret.Trim());
|
||||
|
||||
using var httpRequest = new HttpRequestMessage(HttpMethod.Post, url);
|
||||
httpRequest.Headers.TryAddWithoutValidation("appKey", appKey);
|
||||
httpRequest.Headers.TryAddWithoutValidation("timestamp", timestamp.ToString());
|
||||
@ -87,16 +209,7 @@ internal sealed class SmallBagTrackOrderClient(
|
||||
|
||||
using var response = await client.SendAsync(httpRequest, cancellationToken);
|
||||
var responseBody = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||
return new SmallBagTrackUpsertResult
|
||||
{
|
||||
Skipped = false,
|
||||
Success = response.IsSuccessStatusCode,
|
||||
Url = url,
|
||||
RequestJson = requestJson,
|
||||
CurlCommand = curlCommand,
|
||||
StatusCode = (int)response.StatusCode,
|
||||
ResponseBody = responseBody
|
||||
};
|
||||
return new SignedTrackHttpResponse(response, responseBody, timestamp, sign);
|
||||
}
|
||||
|
||||
private string? ResolveAuthorization(SmallBagTrackUpsertRequest request)
|
||||
@ -125,4 +238,10 @@ internal sealed class SmallBagTrackOrderClient(
|
||||
Skipped = true,
|
||||
SkipReason = reason
|
||||
};
|
||||
|
||||
private sealed record SignedTrackHttpResponse(
|
||||
HttpResponseMessage Response,
|
||||
string ResponseBody,
|
||||
long Timestamp,
|
||||
string Sign);
|
||||
}
|
||||
|
||||
@ -16,7 +16,11 @@ internal sealed class SmallBagTrackOrderNotifier(
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var coordinator = scope.ServiceProvider.GetRequiredService<OrderBatchJobCoordinator>();
|
||||
await coordinator.PublishTrackUpsertAsync(request, orderId, customerId);
|
||||
var published = await coordinator.PublishTrackUpsertAsync(request, orderId, customerId);
|
||||
if (!published)
|
||||
{
|
||||
await FallbackDirectAsync(request, orderId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@ -24,12 +28,12 @@ internal sealed class SmallBagTrackOrderNotifier(
|
||||
ex,
|
||||
"RabbitMQ track upsert publish failed, fallback to direct call. orderId={OrderId}",
|
||||
orderId);
|
||||
await FallbackDirectAsync(request);
|
||||
await FallbackDirectAsync(request, orderId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async Task FallbackDirectAsync(SmallBagTrackUpsertRequest request)
|
||||
private async Task FallbackDirectAsync(SmallBagTrackUpsertRequest request, int orderId)
|
||||
{
|
||||
try
|
||||
{
|
||||
@ -39,7 +43,29 @@ internal sealed class SmallBagTrackOrderNotifier(
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "Fallback UpsertSmallBag failed for order {OrderNo}", request.OrderNo);
|
||||
logger.LogWarning(ex, "Direct UpsertSmallBag failed for orderId={OrderId} orderNo={OrderNo}", orderId, request.OrderNo);
|
||||
}
|
||||
}
|
||||
|
||||
public void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null)
|
||||
{
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
using var scope = scopeFactory.CreateScope();
|
||||
var client = scope.ServiceProvider.GetRequiredService<ISmallBagTrackOrderClient>();
|
||||
await client.AppendLifecycleTrackAsync(orderNo, trackCode, authorization);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(
|
||||
ex,
|
||||
"Async lifecycle track append failed. orderId={OrderId} orderNo={OrderNo} trackCode={TrackCode}",
|
||||
orderId,
|
||||
orderNo,
|
||||
trackCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ public static class SmallBagTrackServiceExtensions
|
||||
services.Configure<SmallBagTrackOptions>(configuration.GetSection(SmallBagTrackOptions.SectionName));
|
||||
services.AddHttpClient(SmallBagTrackOrderClient.HttpClientName);
|
||||
services.AddScoped<ISmallBagTrackConfigProvider, SmallBagTrackConfigProvider>();
|
||||
services.AddScoped<ISmallBagTrackUpsertRequestBuilder, SmallBagTrackUpsertRequestBuilder>();
|
||||
services.AddScoped<ISmallBagTrackOrderClient, SmallBagTrackOrderClient>();
|
||||
services.AddSingleton<ISmallBagTrackOrderNotifier, SmallBagTrackOrderNotifier>();
|
||||
return services;
|
||||
|
||||
@ -18,7 +18,7 @@ public class SmallBagTrackUpsertRequest
|
||||
public string Product { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 轨迹服务商 code(对应轨迹 ServiceProviderCode,取订单 DownstreamSupplierCode)。
|
||||
/// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;优先 ChannelForecastCode)。
|
||||
/// </summary>
|
||||
public string ApiCode { get; set; } = string.Empty;
|
||||
|
||||
@ -28,6 +28,11 @@ public class SmallBagTrackUpsertRequest
|
||||
/// 转发到轨迹网关的 Authorization(异步入队前从当前 HTTP 请求捕获)。
|
||||
/// </summary>
|
||||
public string? Authorization { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// UpsertSmallBag 成功后是否追加创单默认轨迹(A0101/SZ)。
|
||||
/// </summary>
|
||||
public bool AppendInitialTrackOnSuccess { get; set; }
|
||||
}
|
||||
|
||||
public class SmallBagTrackConsigneeRequest
|
||||
|
||||
@ -0,0 +1,147 @@
|
||||
using Fake.DependencyInjection;
|
||||
using Oms.Application.Dtos.Orders;
|
||||
using Oms.Domain.ChannelsAggregate.Repositories;
|
||||
using Oms.Domain.CustomersAggregate.Entities;
|
||||
using Oms.Domain.CustomersAggregate.Repositories;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Oms.Domain.OrdersAggregate.Repositories;
|
||||
|
||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
public interface ISmallBagTrackUpsertRequestBuilder
|
||||
{
|
||||
Task<SmallBagTrackUpsertRequest> BuildAsync(
|
||||
Order order,
|
||||
OrderParcelReceiverInput? receiverInput = null,
|
||||
Customer? customer = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 组装 UpsertSmallBag 请求:ApiCode 优先 ChannelForecastCode,TrackNo 缺省用 ServiceNo。
|
||||
/// </summary>
|
||||
internal sealed class SmallBagTrackUpsertRequestBuilder(
|
||||
IOrderRepository orderRepository,
|
||||
ICustomerRepository customerRepository,
|
||||
IServiceChannelRepository serviceChannelRepository,
|
||||
ISalesProductRepository salesProductRepository) : ISmallBagTrackUpsertRequestBuilder, IScopedDependency
|
||||
{
|
||||
public async Task<SmallBagTrackUpsertRequest> BuildAsync(
|
||||
Order order,
|
||||
OrderParcelReceiverInput? receiverInput = null,
|
||||
Customer? customer = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
customer ??= await ResolveCustomerAsync(order, cancellationToken);
|
||||
|
||||
var recipients = await orderRepository.GetRecipientsByOrderIdsAsync([order.Id], cancellationToken);
|
||||
var recipient = recipients.FirstOrDefault();
|
||||
var apiCode = await ResolveTrackApiCodeAsync(order, cancellationToken);
|
||||
|
||||
return new SmallBagTrackUpsertRequest
|
||||
{
|
||||
Client = customer?.CustomerCode?.Trim() ?? string.Empty,
|
||||
OrderNo = order.ServiceNo?.Trim() ?? string.Empty,
|
||||
TrackNo = ResolveTrackNo(order),
|
||||
OtherNo = order.ClientNo?.Trim() ?? string.Empty,
|
||||
Channel = order.ChannelCode?.Trim() ?? string.Empty,
|
||||
Product = order.ProductCode?.Trim() ?? string.Empty,
|
||||
ApiCode = apiCode,
|
||||
Consignee = new SmallBagTrackConsigneeRequest
|
||||
{
|
||||
CountryCode = receiverInput?.CountryCode?.Trim() ?? order.ConsigneeCountry?.Trim() ?? string.Empty,
|
||||
Province = receiverInput?.Province?.Trim() ?? recipient?.Province?.Trim() ?? string.Empty,
|
||||
Postcode = receiverInput?.ZipCode?.Trim() ?? recipient?.ZipCode?.Trim() ?? string.Empty,
|
||||
City = receiverInput?.City?.Trim() ?? recipient?.City?.Trim() ?? string.Empty,
|
||||
Email = receiverInput?.Email?.Trim() ?? recipient?.Email?.Trim() ?? string.Empty
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
internal static string ResolveTrackNo(Order order)
|
||||
{
|
||||
var trackNo = order.TrackNo?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(trackNo))
|
||||
{
|
||||
return trackNo;
|
||||
}
|
||||
|
||||
return order.ServiceNo?.Trim() ?? string.Empty;
|
||||
}
|
||||
|
||||
private async Task<Customer?> ResolveCustomerAsync(Order order, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!order.CustomerId.HasValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return await customerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value, cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<string> ResolveTrackApiCodeAsync(Order order, CancellationToken cancellationToken)
|
||||
{
|
||||
var fromOrder = order.ChannelForecastCode?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(fromOrder))
|
||||
{
|
||||
return fromOrder;
|
||||
}
|
||||
|
||||
var channelCode = order.ChannelCode?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(channelCode))
|
||||
{
|
||||
var channel = await serviceChannelRepository.GetByChannelCodeAsync(channelCode, cancellationToken);
|
||||
var forecast = channel?.ForecastCode?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(forecast))
|
||||
{
|
||||
return forecast;
|
||||
}
|
||||
}
|
||||
|
||||
var provisional = await ResolveProvisionalApiCodeFromProductAsync(order, cancellationToken);
|
||||
return provisional ?? string.Empty;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创单尚未排货时,用销售产品排货规则里首个渠道的 ForecastCode 作为轨迹 ApiCode 预占位。
|
||||
/// </summary>
|
||||
private async Task<string?> ResolveProvisionalApiCodeFromProductAsync(
|
||||
Order order,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var productCode = order.ProductCode?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(productCode))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var product = await salesProductRepository.GetByProductCodeAsync(productCode);
|
||||
if (product == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var providers = await salesProductRepository.GetAllocationProvidersAsync(product.Id);
|
||||
var channelIds = providers
|
||||
.Where(x => x.ServiceChannelId.HasValue)
|
||||
.Select(x => x.ServiceChannelId!.Value)
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
if (channelIds.Length == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var channels = await serviceChannelRepository.GetListAsync(x => channelIds.Contains(x.Id));
|
||||
foreach (var channel in channels.OrderBy(x => x.Id))
|
||||
{
|
||||
var forecast = channel.ForecastCode?.Trim();
|
||||
if (!string.IsNullOrWhiteSpace(forecast))
|
||||
{
|
||||
return forecast;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,43 @@
|
||||
namespace Oms.Application.Integrations.SmallBagTrack;
|
||||
|
||||
internal sealed class SmallBagTrackUpsertTrackApiRequest
|
||||
{
|
||||
public string AppKey { get; set; } = string.Empty;
|
||||
|
||||
public int OrderType { get; set; }
|
||||
|
||||
public string OrderNo { get; set; } = string.Empty;
|
||||
|
||||
public bool IsAdd { get; set; } = true;
|
||||
|
||||
public string TrackCode { get; set; } = string.Empty;
|
||||
|
||||
public string TrackPlace { get; set; } = string.Empty;
|
||||
|
||||
public DateTime TrackTime { get; set; }
|
||||
|
||||
public float TrackTimeZone { get; set; }
|
||||
|
||||
public int Source { get; set; }
|
||||
|
||||
public static SmallBagTrackUpsertTrackApiRequest CreateLifecycleEvent(
|
||||
string appKey,
|
||||
string orderNo,
|
||||
string trackCode,
|
||||
DateTime trackTime) =>
|
||||
new()
|
||||
{
|
||||
AppKey = appKey,
|
||||
OrderType = SmallBagTrackLifecycleEvents.OrderType,
|
||||
OrderNo = orderNo,
|
||||
IsAdd = true,
|
||||
TrackCode = trackCode,
|
||||
TrackPlace = SmallBagTrackLifecycleEvents.TrackPlace,
|
||||
TrackTime = trackTime,
|
||||
TrackTimeZone = SmallBagTrackLifecycleEvents.TrackTimeZone,
|
||||
Source = SmallBagTrackLifecycleEvents.Source
|
||||
};
|
||||
|
||||
public static string BuildEndpointUrl(string baseUrl) =>
|
||||
$"{baseUrl.Trim().TrimEnd('/')}/ProviderService/Track/Upsert";
|
||||
}
|
||||
@ -17,6 +17,7 @@ public sealed class OrderBatchActionExecutor(
|
||||
IOrderRepository orderRepository,
|
||||
ICountryRepository countryRepository,
|
||||
ISmallBagTrackOrderClient smallBagTrackOrderClient,
|
||||
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
|
||||
OrderDownstreamCredentialResolver downstreamCredentialResolver,
|
||||
IItfxForwardClient itfxForwardClient,
|
||||
OrderBatchCreateActionHandler createActionHandler,
|
||||
@ -79,7 +80,7 @@ public sealed class OrderBatchActionExecutor(
|
||||
return OrderBatchActionExecuteResult.BusinessFail("订单运单号为空,无法同步轨迹");
|
||||
}
|
||||
|
||||
request = await BuildTrackRequestAsync(order, cancellationToken);
|
||||
request = await smallBagTrackUpsertRequestBuilder.BuildAsync(order, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
await smallBagTrackOrderClient.UpsertAsync(request, cancellationToken);
|
||||
@ -147,30 +148,4 @@ public sealed class OrderBatchActionExecutor(
|
||||
|
||||
return OrderBatchActionExecuteResult.Ok();
|
||||
}
|
||||
|
||||
private async Task<SmallBagTrackUpsertRequest> BuildTrackRequestAsync(
|
||||
Domain.OrdersAggregate.Entities.Order order,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var recipients = await orderRepository.GetRecipientsByOrderIdsAsync([order.Id], cancellationToken);
|
||||
var recipient = recipients.FirstOrDefault();
|
||||
|
||||
return new SmallBagTrackUpsertRequest
|
||||
{
|
||||
OrderNo = order.ServiceNo ?? string.Empty,
|
||||
TrackNo = order.TrackNo?.Trim() ?? string.Empty,
|
||||
OtherNo = order.ClientNo?.Trim() ?? string.Empty,
|
||||
Channel = order.ChannelCode?.Trim() ?? string.Empty,
|
||||
Product = order.ProductCode?.Trim() ?? string.Empty,
|
||||
ApiCode = order.DownstreamSupplierCode?.Trim() ?? string.Empty,
|
||||
Consignee = new SmallBagTrackConsigneeRequest
|
||||
{
|
||||
CountryCode = order.ConsigneeCountry?.Trim() ?? string.Empty,
|
||||
Province = recipient?.Province?.Trim() ?? string.Empty,
|
||||
Postcode = recipient?.ZipCode?.Trim() ?? string.Empty,
|
||||
City = recipient?.City?.Trim() ?? string.Empty,
|
||||
Email = recipient?.Email?.Trim() ?? string.Empty,
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -366,12 +366,12 @@ public sealed class OrderBatchJobCoordinator(
|
||||
output.TaskName = string.IsNullOrWhiteSpace(meta.TaskName) ? null : meta.TaskName.Trim();
|
||||
}
|
||||
|
||||
public async Task PublishTrackUpsertAsync(SmallBagTrackUpsertRequest request, int orderId, int? customerId)
|
||||
public async Task<bool> PublishTrackUpsertAsync(SmallBagTrackUpsertRequest request, int orderId, int? customerId)
|
||||
{
|
||||
if (!publisher.IsEnabled)
|
||||
{
|
||||
logger.LogDebug("RabbitMQ disabled, skip track upsert publish for order {OrderId}", orderId);
|
||||
return;
|
||||
logger.LogDebug("RabbitMQ disabled, track upsert will use direct call. orderId={OrderId}", orderId);
|
||||
return false;
|
||||
}
|
||||
|
||||
var message = new OrderBatchJobMessage
|
||||
@ -387,6 +387,7 @@ public sealed class OrderBatchJobCoordinator(
|
||||
};
|
||||
|
||||
await publisher.PublishAsync(message);
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task<OrderBatchJobOutput?> GetJobAsync(int jobId, CancellationToken cancellationToken = default)
|
||||
|
||||
@ -89,7 +89,7 @@ public partial class OrderService
|
||||
: await CustomerRepository.GetListAsync(x => customerIds.Contains(x.Id));
|
||||
var customerMap = customers.ToDictionary(x => x.Id);
|
||||
|
||||
return orders.Select(order =>
|
||||
var outputs = orders.Select(order =>
|
||||
{
|
||||
senderMap.TryGetValue(order.Id, out var sender);
|
||||
recipientMap.TryGetValue(order.Id, out var recipient);
|
||||
@ -198,6 +198,26 @@ public partial class OrderService
|
||||
ReceivableAmount = feeOutputs.Count == 0 ? null : receivableAmount
|
||||
};
|
||||
}).ToList();
|
||||
|
||||
if (OmsSessionHelper.IsCustomerLogin(CurrentUser))
|
||||
{
|
||||
RedactChannelFieldsForCustomer(outputs);
|
||||
}
|
||||
|
||||
return outputs;
|
||||
}
|
||||
|
||||
private static void RedactChannelFieldsForCustomer(IEnumerable<OrderOutput> outputs)
|
||||
{
|
||||
foreach (var output in outputs)
|
||||
{
|
||||
output.ChannelCode = null;
|
||||
output.ChannelName = null;
|
||||
output.DownstreamSupplierCode = null;
|
||||
output.DownstreamAppKey = null;
|
||||
output.DownstreamAppId = null;
|
||||
output.ChannelForecastCode = null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<object>> BuildOrderExportRowsAsync(IReadOnlyCollection<Order> orders)
|
||||
|
||||
@ -155,7 +155,7 @@ public partial class OrderService
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
|
||||
await EnqueueSmallBagTrackUpsertAsync(order, customer, input.Receiver);
|
||||
await EnqueueSmallBagTrackUpsertAsync(order, customer, input.Receiver, appendInitialTrack: true);
|
||||
await EnqueueGenerateLabelPdfAsync(order);
|
||||
|
||||
try
|
||||
@ -1625,6 +1625,16 @@ public partial class OrderService
|
||||
order.ReceiveTime = DateTime.Now;
|
||||
order.UpdateTime = DateTime.Now;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
|
||||
EnqueueLifecycleTrackAppend(order, SmallBagTrackLifecycleEvents.InboundTrackCode);
|
||||
|
||||
if (allocated)
|
||||
{
|
||||
var customer = order.CustomerId.HasValue
|
||||
? await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value)
|
||||
: null;
|
||||
await EnqueueSmallBagTrackUpsertAsync(order, customer, receiver: null);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -1706,6 +1716,8 @@ public partial class OrderService
|
||||
order.ShipTime = now;
|
||||
order.UpdateTime = now;
|
||||
await OrderRepository.UpdateAsync(order);
|
||||
|
||||
EnqueueLifecycleTrackAppend(order, SmallBagTrackLifecycleEvents.OutboundTrackCode);
|
||||
}
|
||||
|
||||
private async Task<OrderParcelCreateOutput> BuildParcelCreateOutputAsync(Order order)
|
||||
@ -1764,9 +1776,11 @@ public partial class OrderService
|
||||
private async Task EnqueueSmallBagTrackUpsertAsync(
|
||||
Order order,
|
||||
Customer? customer,
|
||||
OrderParcelReceiverInput? receiver)
|
||||
OrderParcelReceiverInput? receiver,
|
||||
bool appendInitialTrack = false)
|
||||
{
|
||||
var request = await BuildSmallBagTrackUpsertRequestAsync(order, receiver);
|
||||
request.AppendInitialTrackOnSuccess = appendInitialTrack;
|
||||
var authorization = LazyServiceProvider
|
||||
.GetRequiredService<IHttpContextAccessor>()
|
||||
.HttpContext?
|
||||
@ -1782,6 +1796,28 @@ public partial class OrderService
|
||||
SmallBagTrackOrderNotifier.Enqueue(request, order.Id, customer?.Id);
|
||||
}
|
||||
|
||||
private void EnqueueLifecycleTrackAppend(Order order, string trackCode)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(order.ServiceNo))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var authorization = LazyServiceProvider
|
||||
.GetRequiredService<IHttpContextAccessor>()
|
||||
.HttpContext?
|
||||
.Request
|
||||
.Headers
|
||||
.Authorization
|
||||
.ToString();
|
||||
|
||||
SmallBagTrackOrderNotifier.EnqueueLifecycleTrackAppend(
|
||||
order.ServiceNo,
|
||||
trackCode,
|
||||
order.Id,
|
||||
string.IsNullOrWhiteSpace(authorization) ? null : authorization);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按订单同步轨迹(UpsertSmallBag),返回完整请求与响应便于联调。
|
||||
/// </summary>
|
||||
@ -1802,36 +1838,11 @@ public partial class OrderService
|
||||
Order order,
|
||||
OrderParcelReceiverInput? receiverInput = null)
|
||||
{
|
||||
Customer? customer = null;
|
||||
if (order.CustomerId.HasValue)
|
||||
{
|
||||
customer = await CustomerRepository.FirstOrDefaultAsync(x => x.Id == order.CustomerId.Value);
|
||||
return await SmallBagTrackUpsertRequestBuilder.BuildAsync(order, receiverInput);
|
||||
}
|
||||
|
||||
var recipients = await OrderRepository.GetRecipientsByOrderIdsAsync([order.Id]);
|
||||
var recipient = recipients.FirstOrDefault();
|
||||
|
||||
var apiCode = order.DownstreamSupplierCode?.Trim() ?? string.Empty;
|
||||
|
||||
return new SmallBagTrackUpsertRequest
|
||||
{
|
||||
Client = customer?.CustomerCode?.Trim() ?? string.Empty,
|
||||
OrderNo = order.ServiceNo,
|
||||
TrackNo = order.TrackNo?.Trim() ?? string.Empty,
|
||||
OtherNo = order.ClientNo?.Trim() ?? string.Empty,
|
||||
Channel = order.ChannelCode?.Trim() ?? string.Empty,
|
||||
Product = order.ProductCode?.Trim() ?? string.Empty,
|
||||
ApiCode = apiCode,
|
||||
Consignee = new SmallBagTrackConsigneeRequest
|
||||
{
|
||||
CountryCode = receiverInput?.CountryCode?.Trim() ?? order.ConsigneeCountry?.Trim() ?? string.Empty,
|
||||
Province = receiverInput?.Province?.Trim() ?? recipient?.Province?.Trim() ?? string.Empty,
|
||||
Postcode = receiverInput?.ZipCode?.Trim() ?? recipient?.ZipCode?.Trim() ?? string.Empty,
|
||||
City = receiverInput?.City?.Trim() ?? recipient?.City?.Trim() ?? string.Empty,
|
||||
Email = receiverInput?.Email?.Trim() ?? recipient?.Email?.Trim() ?? string.Empty
|
||||
}
|
||||
};
|
||||
}
|
||||
private ISmallBagTrackUpsertRequestBuilder SmallBagTrackUpsertRequestBuilder =>
|
||||
LazyServiceProvider.GetRequiredService<ISmallBagTrackUpsertRequestBuilder>();
|
||||
|
||||
private ISmallBagTrackOrderClient SmallBagTrackOrderClient =>
|
||||
LazyServiceProvider.GetRequiredService<ISmallBagTrackOrderClient>();
|
||||
|
||||
@ -87,6 +87,10 @@ public sealed class OrderE2eFixture : IAsyncLifetime
|
||||
public void Enqueue(SmallBagTrackUpsertRequest request, int orderId, int? customerId = null)
|
||||
{
|
||||
}
|
||||
|
||||
public void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
67
src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs
Normal file
67
src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs
Normal file
@ -0,0 +1,67 @@
|
||||
using Oms.Application.Integrations.SmallBagTrack;
|
||||
using Oms.Domain.OrdersAggregate.Entities;
|
||||
using Xunit;
|
||||
|
||||
namespace Oms.Tests;
|
||||
|
||||
public class SmallBagTrackUpsertRequestBuilderTests
|
||||
{
|
||||
[Fact]
|
||||
public void ResolveTrackNo_UsesTrackNoWhenPresent()
|
||||
{
|
||||
var order = new Order
|
||||
{
|
||||
ServiceNo = "IDE2607221859123456",
|
||||
TrackNo = "LP123456789CN"
|
||||
};
|
||||
|
||||
Assert.Equal("LP123456789CN", SmallBagTrackUpsertRequestBuilder.ResolveTrackNo(order));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ResolveTrackNo_FallsBackToServiceNoWhenTrackNoEmpty()
|
||||
{
|
||||
var order = new Order
|
||||
{
|
||||
ServiceNo = "IDE2607221859123456",
|
||||
TrackNo = null
|
||||
};
|
||||
|
||||
Assert.Equal("IDE2607221859123456", SmallBagTrackUpsertRequestBuilder.ResolveTrackNo(order));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CreateLifecycleEvent_UsesConfiguredDefaults()
|
||||
{
|
||||
var trackTime = new DateTime(2026, 7, 22, 20, 7, 0);
|
||||
var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent(
|
||||
"APP001",
|
||||
"IDE2607221859123456",
|
||||
SmallBagTrackLifecycleEvents.CreateTrackCode,
|
||||
trackTime);
|
||||
|
||||
Assert.Equal("APP001", body.AppKey);
|
||||
Assert.Equal("IDE2607221859123456", body.OrderNo);
|
||||
Assert.True(body.IsAdd);
|
||||
Assert.Equal("A0101", body.TrackCode);
|
||||
Assert.Equal("SZ", body.TrackPlace);
|
||||
Assert.Equal(trackTime, body.TrackTime);
|
||||
Assert.Equal(8f, body.TrackTimeZone);
|
||||
Assert.Equal(1, body.OrderType);
|
||||
Assert.Equal(2, body.Source);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("A0301")]
|
||||
[InlineData("A0701")]
|
||||
public void CreateLifecycleEvent_SupportsInboundAndOutboundCodes(string trackCode)
|
||||
{
|
||||
var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent(
|
||||
"APP001",
|
||||
"IDE2607221859123456",
|
||||
trackCode,
|
||||
DateTime.Now);
|
||||
|
||||
Assert.Equal(trackCode, body.TrackCode);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user