diff --git a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs
index bc339a2..9714178 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs
@@ -4,6 +4,15 @@ public interface ISmallBagTrackOrderClient
{
Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
+ ///
+ /// 追加一条生命周期轨迹节点(POST ProviderService/Track/Upsert)。
+ ///
+ Task AppendLifecycleTrackAsync(
+ string orderNo,
+ string trackCode,
+ string? authorization = null,
+ CancellationToken cancellationToken = default);
+
///
/// 同步调用并返回完整请求/响应(仅调试)。
///
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderNotifier.cs b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderNotifier.cs
index 000217b..9858605 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderNotifier.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderNotifier.cs
@@ -6,4 +6,9 @@ namespace Oms.Application.Integrations.SmallBagTrack;
public interface ISmallBagTrackOrderNotifier
{
void Enqueue(SmallBagTrackUpsertRequest request, int orderId, int? customerId = null);
+
+ ///
+ /// 异步追加生命周期轨迹节点。
+ ///
+ void EnqueueLifecycleTrackAppend(string orderNo, string trackCode, int orderId, string? authorization = null);
}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs
new file mode 100644
index 0000000..fceed43
--- /dev/null
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs
@@ -0,0 +1,23 @@
+namespace Oms.Application.Integrations.SmallBagTrack;
+
+///
+/// 小包订单生命周期默认轨迹节点(POST ProviderService/Track/Upsert)。
+///
+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;
+
+ /// 轨迹订单类型:小包。
+ public const int OrderType = 1;
+
+ /// 数据来源:内部系统。
+ public const int Source = 2;
+}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs
index 5149e26..3059463 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs
@@ -44,8 +44,24 @@ 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 UpsertDebugAsync(
SmallBagTrackUpsertRequest request,
CancellationToken cancellationToken = default)
@@ -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 SendSignedPostAsync(
+ 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);
}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderNotifier.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderNotifier.cs
index 31024d3..2f728b6 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderNotifier.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderNotifier.cs
@@ -16,7 +16,11 @@ internal sealed class SmallBagTrackOrderNotifier(
{
using var scope = scopeFactory.CreateScope();
var coordinator = scope.ServiceProvider.GetRequiredService();
- 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();
+ 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);
+ }
+ });
+ }
}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs
index 74cc34b..d8d0bb3 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs
@@ -10,6 +10,7 @@ public static class SmallBagTrackServiceExtensions
services.Configure(configuration.GetSection(SmallBagTrackOptions.SectionName));
services.AddHttpClient(SmallBagTrackOrderClient.HttpClientName);
services.AddScoped();
+ services.AddScoped();
services.AddScoped();
services.AddSingleton();
return services;
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs
index 726d2ce..2cbf4b5 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequest.cs
@@ -18,7 +18,7 @@ public class SmallBagTrackUpsertRequest
public string Product { get; set; } = string.Empty;
///
- /// 轨迹服务商 code(对应轨迹 ServiceProviderCode,取订单 DownstreamSupplierCode)。
+ /// 轨迹拉取配置 code(对应轨迹 ServiceProviderCode;优先 ChannelForecastCode)。
///
public string ApiCode { get; set; } = string.Empty;
@@ -28,6 +28,11 @@ public class SmallBagTrackUpsertRequest
/// 转发到轨迹网关的 Authorization(异步入队前从当前 HTTP 请求捕获)。
///
public string? Authorization { get; set; }
+
+ ///
+ /// UpsertSmallBag 成功后是否追加创单默认轨迹(A0101/SZ)。
+ ///
+ public bool AppendInitialTrackOnSuccess { get; set; }
}
public class SmallBagTrackConsigneeRequest
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs
new file mode 100644
index 0000000..7c20b4e
--- /dev/null
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertRequestBuilder.cs
@@ -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 BuildAsync(
+ Order order,
+ OrderParcelReceiverInput? receiverInput = null,
+ Customer? customer = null,
+ CancellationToken cancellationToken = default);
+}
+
+///
+/// 组装 UpsertSmallBag 请求:ApiCode 优先 ChannelForecastCode,TrackNo 缺省用 ServiceNo。
+///
+internal sealed class SmallBagTrackUpsertRequestBuilder(
+ IOrderRepository orderRepository,
+ ICustomerRepository customerRepository,
+ IServiceChannelRepository serviceChannelRepository,
+ ISalesProductRepository salesProductRepository) : ISmallBagTrackUpsertRequestBuilder, IScopedDependency
+{
+ public async Task 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 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 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;
+ }
+
+ ///
+ /// 创单尚未排货时,用销售产品排货规则里首个渠道的 ForecastCode 作为轨迹 ApiCode 预占位。
+ ///
+ private async Task 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;
+ }
+}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs
new file mode 100644
index 0000000..24f2c05
--- /dev/null
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs
@@ -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";
+}
diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
index e989bc9..66dd698 100644
--- a/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
+++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
@@ -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 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,
- }
- };
- }
}
diff --git a/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs b/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs
index 4e29a8b..903e1d7 100644
--- a/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs
+++ b/src/Oms.Application/Services/Orders/Batch/OrderBatchJobCoordinator.cs
@@ -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 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 GetJobAsync(int jobId, CancellationToken cancellationToken = default)
diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
index 40fabab..8105ddd 100644
--- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
@@ -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 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> BuildOrderExportRowsAsync(IReadOnlyCollection orders)
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
index e06e475..a8393a0 100644
--- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
@@ -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);
+ }
}
///
@@ -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 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()
.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()
+ .HttpContext?
+ .Request
+ .Headers
+ .Authorization
+ .ToString();
+
+ SmallBagTrackOrderNotifier.EnqueueLifecycleTrackAppend(
+ order.ServiceNo,
+ trackCode,
+ order.Id,
+ string.IsNullOrWhiteSpace(authorization) ? null : authorization);
+ }
+
///
/// 按订单同步轨迹(UpsertSmallBag),返回完整请求与响应便于联调。
///
@@ -1802,37 +1838,12 @@ 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);
- }
-
- 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
- }
- };
+ return await SmallBagTrackUpsertRequestBuilder.BuildAsync(order, receiverInput);
}
+ private ISmallBagTrackUpsertRequestBuilder SmallBagTrackUpsertRequestBuilder =>
+ LazyServiceProvider.GetRequiredService();
+
private ISmallBagTrackOrderClient SmallBagTrackOrderClient =>
LazyServiceProvider.GetRequiredService();
diff --git a/src/Oms.E2E.Tests/Fixtures/OrderE2eFixture.cs b/src/Oms.E2E.Tests/Fixtures/OrderE2eFixture.cs
index 292851f..f9938e1 100644
--- a/src/Oms.E2E.Tests/Fixtures/OrderE2eFixture.cs
+++ b/src/Oms.E2E.Tests/Fixtures/OrderE2eFixture.cs
@@ -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)
+ {
+ }
}
}
diff --git a/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs b/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs
new file mode 100644
index 0000000..4880647
--- /dev/null
+++ b/src/Oms.Tests/SmallBagTrackUpsertRequestBuilderTests.cs
@@ -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);
+ }
+}