diff --git a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs
index 9714178..c3e8190 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/ISmallBagTrackOrderClient.cs
@@ -5,7 +5,7 @@ public interface ISmallBagTrackOrderClient
Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
///
- /// 追加一条生命周期轨迹节点(POST ProviderService/Track/Upsert)。
+ /// 追加一条生命周期轨迹节点(POST ProviderService/Track/Upsert),默认小包 orderType=1。
///
Task AppendLifecycleTrackAsync(
string orderNo,
@@ -13,8 +13,36 @@ public interface ISmallBagTrackOrderClient
string? authorization = null,
CancellationToken cancellationToken = default);
+ ///
+ /// 追加生命周期轨迹节点,返回调用结果(用于按序编排)。
+ ///
+ Task AppendLifecycleTrackResultAsync(
+ string orderNo,
+ string trackCode,
+ int orderType,
+ string? authorization = null,
+ CancellationToken cancellationToken = default);
+
///
/// 同步调用并返回完整请求/响应(仅调试)。
///
Task UpsertDebugAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
+
+ ///
+ /// 创建/更新大包轨迹订单(POST ProviderService/Order/UpsertBigBag)。
+ ///
+ Task UpsertBigBagAsync(
+ SmallBagTrackUpsertBigBagRequest request,
+ CancellationToken cancellationToken = default);
+
+ ///
+ /// 关联大包与小包(POST ProviderService/Order/Link)。
+ ///
+ Task LinkOrdersAsync(
+ string parentOrderNo,
+ int parentOrderType,
+ IReadOnlyList linkOrderNos,
+ int linkOrderType,
+ string? authorization = null,
+ CancellationToken cancellationToken = default);
}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/PickupBigBagTrackSyncer.cs b/src/Oms.Application/Integrations/SmallBagTrack/PickupBigBagTrackSyncer.cs
new file mode 100644
index 0000000..e3bca81
--- /dev/null
+++ b/src/Oms.Application/Integrations/SmallBagTrack/PickupBigBagTrackSyncer.cs
@@ -0,0 +1,205 @@
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
+using Oms.Domain.ChannelsAggregate.Repositories;
+using Oms.Domain.CustomersAggregate.Repositories;
+using Oms.Domain.OrdersAggregate.Consts;
+using Oms.Domain.OrdersAggregate.Entities;
+using Oms.Domain.OrdersAggregate.Repositories;
+
+namespace Oms.Application.Integrations.SmallBagTrack;
+
+///
+/// 揽收单预报下游成功后:按序 UpsertBigBag → Link → Track/Upsert(A0201)。
+///
+public interface IPickupBigBagTrackSyncer
+{
+ Task SyncAfterDownstreamForwardAsync(PickupOrder pickup, CancellationToken cancellationToken = default);
+}
+
+internal sealed class PickupBigBagTrackSyncer(
+ ISmallBagTrackOrderClient trackClient,
+ ICustomerRepository customerRepository,
+ IPickupOrderLineRepository pickupOrderLineRepository,
+ ISalesProductRepository salesProductRepository,
+ IServiceChannelRepository serviceChannelRepository,
+ IHttpContextAccessor httpContextAccessor,
+ ILogger logger) : IPickupBigBagTrackSyncer
+{
+ public async Task SyncAfterDownstreamForwardAsync(
+ PickupOrder pickup,
+ CancellationToken cancellationToken = default)
+ {
+ if (pickup == null || string.IsNullOrWhiteSpace(pickup.PickupNo))
+ {
+ logger.LogWarning("Skip big-bag track sync: pickup or PickupNo empty");
+ return;
+ }
+
+ var bagNo = pickup.PickupNo.Trim();
+ var authorization = httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString();
+ if (string.IsNullOrWhiteSpace(authorization))
+ {
+ authorization = null;
+ }
+
+ try
+ {
+ var customer = await customerRepository.FirstOrDefaultAsync(x => x.Id == pickup.CustomerId, cancellationToken);
+ var clientCode = customer?.CustomerCode?.Trim() ?? string.Empty;
+ var (channel, product) = await ResolveChannelProductAsync(pickup.CarrierService, cancellationToken);
+ var country = ResolveCountry(pickup);
+
+ // 1) UpsertBigBag
+ var upsertResult = await trackClient.UpsertBigBagAsync(
+ new SmallBagTrackUpsertBigBagRequest
+ {
+ Client = clientCode,
+ BagNo = bagNo,
+ OtherNo = pickup.CarrierWaybillNo,
+ Channel = channel,
+ Product = product,
+ Country = country,
+ Authorization = authorization
+ },
+ cancellationToken);
+
+ if (upsertResult.Skipped)
+ {
+ logger.LogDebug(
+ "UpsertBigBag skipped for pickup {PickupNo}: {Reason}",
+ bagNo,
+ upsertResult.SkipReason);
+ return;
+ }
+
+ if (!upsertResult.Success)
+ {
+ logger.LogWarning(
+ "Stop big-bag track sync after UpsertBigBag failure for pickup {PickupNo}",
+ bagNo);
+ return;
+ }
+
+ // 2) Link(无明细单号则跳过,继续写轨迹)
+ var lines = await pickupOrderLineRepository.GetByPickupOrderIdAsync(pickup.Id, cancellationToken);
+ var serviceNos = lines
+ .Select(x => x.ServiceNo)
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x!.Trim())
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ if (serviceNos.Count > 0)
+ {
+ var linkResult = await trackClient.LinkOrdersAsync(
+ bagNo,
+ SmallBagTrackLifecycleEvents.BigBagOrderType,
+ serviceNos,
+ SmallBagTrackLifecycleEvents.OrderType,
+ authorization,
+ cancellationToken);
+
+ if (linkResult.Skipped)
+ {
+ logger.LogDebug(
+ "Link skipped for pickup {PickupNo}: {Reason}",
+ bagNo,
+ linkResult.SkipReason);
+ }
+ else if (!linkResult.Success)
+ {
+ logger.LogWarning(
+ "Stop big-bag track sync after Link failure for pickup {PickupNo}",
+ bagNo);
+ return;
+ }
+ }
+ else
+ {
+ logger.LogDebug("Skip Link for pickup {PickupNo}: no bound ServiceNo", bagNo);
+ }
+
+ // 3) Track Upsert A0201
+ var trackResult = await trackClient.AppendLifecycleTrackResultAsync(
+ bagNo,
+ SmallBagTrackLifecycleEvents.PickupCreateTrackCode,
+ SmallBagTrackLifecycleEvents.BigBagOrderType,
+ authorization,
+ cancellationToken);
+
+ if (trackResult.Skipped)
+ {
+ logger.LogDebug(
+ "Track A0201 skipped for pickup {PickupNo}: {Reason}",
+ bagNo,
+ trackResult.SkipReason);
+ return;
+ }
+
+ if (!trackResult.Success)
+ {
+ logger.LogWarning(
+ "Track A0201 failed for pickup {PickupNo}: {StatusCode} {Body}",
+ bagNo,
+ trackResult.StatusCode,
+ trackResult.ResponseBody);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Big-bag track sync failed for pickup {PickupNo}", bagNo);
+ }
+ }
+
+ private async Task<(string? Channel, string? Product)> ResolveChannelProductAsync(
+ string? carrierService,
+ CancellationToken cancellationToken)
+ {
+ var salesProductCode = carrierService?.Trim();
+ if (string.IsNullOrWhiteSpace(salesProductCode))
+ {
+ return (null, null);
+ }
+
+ var product = await salesProductRepository.GetByProductCodeAsync(salesProductCode, cancellationToken);
+ if (product == null)
+ {
+ return (null, 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 (null, product.ProductCode);
+ }
+
+ var channel = await serviceChannelRepository.FirstOrDefaultAsync(x => x.Id == channelId, cancellationToken);
+ var channelCode = channel?.ChannelCode?.Trim();
+ return (
+ string.IsNullOrWhiteSpace(channelCode) ? null : channelCode,
+ product.ProductCode);
+ }
+
+ private static string ResolveCountry(PickupOrder pickup)
+ {
+ var isReverse = string.Equals(
+ PickupDirectionCodes.NormalizeOrDefault(pickup.Direction),
+ PickupDirectionCodes.Reverse,
+ StringComparison.OrdinalIgnoreCase);
+
+ if (isReverse)
+ {
+ return string.IsNullOrWhiteSpace(pickup.SenderCountryCode)
+ ? "CN"
+ : pickup.SenderCountryCode.Trim().ToUpperInvariant();
+ }
+
+ // 正向:目的仓,仓库无国别字段,默认 CN
+ return "CN";
+ }
+}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs
index fceed43..15d6ca8 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLifecycleEvents.cs
@@ -1,12 +1,15 @@
namespace Oms.Application.Integrations.SmallBagTrack;
///
-/// 小包订单生命周期默认轨迹节点(POST ProviderService/Track/Upsert)。
+/// 订单生命周期默认轨迹节点(POST ProviderService/Track/Upsert)。
///
internal static class SmallBagTrackLifecycleEvents
{
public const string CreateTrackCode = "A0101";
+ /// 揽收大包创单轨迹。
+ public const string PickupCreateTrackCode = "A0201";
+
public const string InboundTrackCode = "A0301";
public const string OutboundTrackCode = "A0701";
@@ -18,6 +21,9 @@ internal static class SmallBagTrackLifecycleEvents
/// 轨迹订单类型:小包。
public const int OrderType = 1;
+ /// 轨迹订单类型:大包。
+ public const int BigBagOrderType = 2;
+
/// 数据来源:内部系统。
public const int Source = 2;
}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLinkOrderApiRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLinkOrderApiRequest.cs
new file mode 100644
index 0000000..9a32edd
--- /dev/null
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackLinkOrderApiRequest.cs
@@ -0,0 +1,24 @@
+namespace Oms.Application.Integrations.SmallBagTrack;
+
+///
+/// POST /ProviderService/Order/Link 业务请求体。
+///
+internal sealed class SmallBagTrackLinkOrderApiRequest
+{
+ public string AppKey { get; set; } = string.Empty;
+
+ public bool IsLink { get; set; } = true;
+
+ public int OrderType { get; set; }
+
+ public string OrderNo { get; set; } = string.Empty;
+
+ public string LinkAppKey { get; set; } = string.Empty;
+
+ public int LinkOrderType { get; set; }
+
+ public List LinkOrderNos { get; set; } = [];
+
+ public static string BuildEndpointUrl(string baseUrl) =>
+ $"{baseUrl.Trim().TrimEnd('/')}/ProviderService/Order/Link";
+}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs
index b97d847..b6e5077 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackOrderClient.cs
@@ -51,7 +51,114 @@ internal sealed class SmallBagTrackOrderClient(
string trackCode,
string? authorization = null,
CancellationToken cancellationToken = default) =>
- AppendLifecycleTrackCoreAsync(orderNo, trackCode, authorization, cancellationToken);
+ AppendLifecycleTrackResultAsync(
+ orderNo,
+ trackCode,
+ SmallBagTrackLifecycleEvents.OrderType,
+ authorization,
+ cancellationToken);
+
+ public async Task AppendLifecycleTrackResultAsync(
+ string orderNo,
+ string trackCode,
+ int orderType,
+ string? authorization = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(orderNo))
+ {
+ logger.LogWarning("Skip lifecycle track append: orderNo empty");
+ return Skipped("orderNo empty");
+ }
+
+ if (string.IsNullOrWhiteSpace(trackCode))
+ {
+ logger.LogWarning("Skip lifecycle track append for order {OrderNo}: trackCode empty", orderNo);
+ return Skipped("trackCode empty");
+ }
+
+ var settings = await configProvider.GetAsync(cancellationToken);
+ if (!settings.Enabled)
+ {
+ return Skipped("TrackService disabled");
+ }
+
+ if (string.IsNullOrWhiteSpace(settings.BaseUrl))
+ {
+ return Skipped("BaseUrl empty");
+ }
+
+ if (string.IsNullOrWhiteSpace(settings.AppKey) || string.IsNullOrWhiteSpace(settings.AppSecret))
+ {
+ return Skipped("AppKey or AppSecret not configured");
+ }
+
+ var appKey = settings.AppKey.Trim();
+ var auth = ResolveAuthorization(authorization);
+ var trackTime = DateTime.Now;
+ var body = SmallBagTrackUpsertTrackApiRequest.CreateLifecycleEvent(
+ appKey,
+ orderNo.Trim(),
+ trackCode.Trim(),
+ trackTime,
+ orderType);
+ var url = SmallBagTrackUpsertTrackApiRequest.BuildEndpointUrl(settings.BaseUrl);
+ var requestJson = JsonSerializer.Serialize(body, JsonOptions);
+
+ try
+ {
+ var response = await SendSignedPostAsync(
+ settings,
+ appKey,
+ url,
+ body,
+ auth,
+ cancellationToken);
+
+ var result = new SmallBagTrackUpsertResult
+ {
+ Skipped = false,
+ Success = response.Response.IsSuccessStatusCode,
+ Url = url,
+ RequestJson = requestJson,
+ StatusCode = (int)response.Response.StatusCode,
+ ResponseBody = response.ResponseBody
+ };
+
+ if (result.Success)
+ {
+ logger.LogInformation(
+ "Lifecycle track {TrackCode} appended for order {OrderNo} (orderType={OrderType}) at {TrackPlace}",
+ trackCode,
+ orderNo,
+ orderType,
+ SmallBagTrackLifecycleEvents.TrackPlace);
+ }
+ else
+ {
+ logger.LogWarning(
+ "Lifecycle track {TrackCode} append failed for order {OrderNo}: {StatusCode} {Body}",
+ trackCode,
+ orderNo,
+ result.StatusCode,
+ result.ResponseBody);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "Lifecycle track {TrackCode} append failed for order {OrderNo}", trackCode, orderNo);
+ return new SmallBagTrackUpsertResult
+ {
+ Skipped = false,
+ Success = false,
+ Url = url,
+ RequestJson = requestJson,
+ ResponseBody = ex.Message
+ };
+ }
+ }
public async Task UpsertDebugAsync(
SmallBagTrackUpsertRequest request,
@@ -76,7 +183,7 @@ internal sealed class SmallBagTrackOrderClient(
var appKey = settings.AppKey.Trim();
var body = SmallBagTrackUpsertApiRequest.FromBusinessRequest(request);
var url = SmallBagTrackUpsertApiRequest.BuildEndpointUrl(settings.BaseUrl);
- var authorization = ResolveAuthorization(request);
+ var authorization = ResolveAuthorization(request.Authorization);
var requestJson = JsonSerializer.Serialize(body, JsonOptions);
var response = await SendSignedPostAsync(
@@ -120,41 +227,44 @@ internal sealed class SmallBagTrackOrderClient(
return result;
}
- private async Task AppendLifecycleTrackCoreAsync(
- string orderNo,
- string trackCode,
- string? authorization,
- CancellationToken cancellationToken)
+ public async Task UpsertBigBagAsync(
+ SmallBagTrackUpsertBigBagRequest request,
+ CancellationToken cancellationToken = default)
{
- if (string.IsNullOrWhiteSpace(orderNo))
+ if (string.IsNullOrWhiteSpace(request.BagNo))
{
- 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;
+ return Skipped("bagNo empty");
}
var settings = await configProvider.GetAsync(cancellationToken);
- if (!settings.Enabled
- || string.IsNullOrWhiteSpace(settings.BaseUrl)
- || string.IsNullOrWhiteSpace(settings.AppKey)
- || string.IsNullOrWhiteSpace(settings.AppSecret))
+ if (!settings.Enabled)
{
- return;
+ return Skipped("TrackService disabled");
+ }
+
+ if (string.IsNullOrWhiteSpace(settings.BaseUrl))
+ {
+ return Skipped("BaseUrl empty");
+ }
+
+ if (string.IsNullOrWhiteSpace(settings.AppKey) || string.IsNullOrWhiteSpace(settings.AppSecret))
+ {
+ return Skipped("AppKey or AppSecret not configured");
}
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);
+ var body = new SmallBagTrackUpsertBigBagApiRequest
+ {
+ Client = request.Client?.Trim() ?? string.Empty,
+ BagNo = request.BagNo.Trim(),
+ OtherNo = request.OtherNo?.Trim() ?? string.Empty,
+ Channel = request.Channel?.Trim() ?? string.Empty,
+ Product = request.Product?.Trim() ?? string.Empty,
+ Country = request.Country?.Trim() ?? string.Empty
+ };
+ var url = SmallBagTrackUpsertBigBagApiRequest.BuildEndpointUrl(settings.BaseUrl);
+ var authorization = ResolveAuthorization(request.Authorization);
+ var requestJson = JsonSerializer.Serialize(body, JsonOptions);
try
{
@@ -166,26 +276,150 @@ internal sealed class SmallBagTrackOrderClient(
authorization,
cancellationToken);
- if (response.Response.IsSuccessStatusCode)
+ var result = new SmallBagTrackUpsertResult
{
- logger.LogInformation(
- "Lifecycle track {TrackCode} appended for order {OrderNo} at {TrackPlace}",
- trackCode,
- orderNo,
- SmallBagTrackLifecycleEvents.TrackPlace);
- return;
+ Skipped = false,
+ Success = response.Response.IsSuccessStatusCode,
+ Url = url,
+ RequestJson = requestJson,
+ StatusCode = (int)response.Response.StatusCode,
+ ResponseBody = response.ResponseBody
+ };
+
+ if (result.Success)
+ {
+ logger.LogInformation("UpsertBigBag succeeded for bag {BagNo}", body.BagNo);
+ }
+ else
+ {
+ logger.LogWarning(
+ "UpsertBigBag failed for bag {BagNo}: {StatusCode} {Body}",
+ body.BagNo,
+ result.StatusCode,
+ result.ResponseBody);
}
- logger.LogWarning(
- "Lifecycle track {TrackCode} append failed for order {OrderNo}: {StatusCode} {Body}",
- trackCode,
- orderNo,
- (int)response.Response.StatusCode,
- response.ResponseBody);
+ return result;
}
catch (Exception ex)
{
- logger.LogWarning(ex, "Lifecycle track {TrackCode} append failed for order {OrderNo}", trackCode, orderNo);
+ logger.LogWarning(ex, "UpsertBigBag failed for bag {BagNo}", request.BagNo);
+ return new SmallBagTrackUpsertResult
+ {
+ Skipped = false,
+ Success = false,
+ Url = url,
+ RequestJson = requestJson,
+ ResponseBody = ex.Message
+ };
+ }
+ }
+
+ public async Task LinkOrdersAsync(
+ string parentOrderNo,
+ int parentOrderType,
+ IReadOnlyList linkOrderNos,
+ int linkOrderType,
+ string? authorization = null,
+ CancellationToken cancellationToken = default)
+ {
+ if (string.IsNullOrWhiteSpace(parentOrderNo))
+ {
+ return Skipped("parentOrderNo empty");
+ }
+
+ var nos = (linkOrderNos ?? [])
+ .Where(x => !string.IsNullOrWhiteSpace(x))
+ .Select(x => x.Trim())
+ .Where(x => !string.Equals(x, parentOrderNo.Trim(), StringComparison.OrdinalIgnoreCase))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ if (nos.Count == 0)
+ {
+ return Skipped("linkOrderNos empty");
+ }
+
+ var settings = await configProvider.GetAsync(cancellationToken);
+ if (!settings.Enabled)
+ {
+ return Skipped("TrackService disabled");
+ }
+
+ if (string.IsNullOrWhiteSpace(settings.BaseUrl))
+ {
+ return Skipped("BaseUrl empty");
+ }
+
+ if (string.IsNullOrWhiteSpace(settings.AppKey) || string.IsNullOrWhiteSpace(settings.AppSecret))
+ {
+ return Skipped("AppKey or AppSecret not configured");
+ }
+
+ var appKey = settings.AppKey.Trim();
+ var body = new SmallBagTrackLinkOrderApiRequest
+ {
+ AppKey = appKey,
+ IsLink = true,
+ OrderType = parentOrderType,
+ OrderNo = parentOrderNo.Trim(),
+ LinkAppKey = appKey,
+ LinkOrderType = linkOrderType,
+ LinkOrderNos = nos
+ };
+ var url = SmallBagTrackLinkOrderApiRequest.BuildEndpointUrl(settings.BaseUrl);
+ var auth = ResolveAuthorization(authorization);
+ var requestJson = JsonSerializer.Serialize(body, JsonOptions);
+
+ try
+ {
+ var response = await SendSignedPostAsync(
+ settings,
+ appKey,
+ url,
+ body,
+ auth,
+ cancellationToken);
+
+ var result = new SmallBagTrackUpsertResult
+ {
+ Skipped = false,
+ Success = response.Response.IsSuccessStatusCode,
+ Url = url,
+ RequestJson = requestJson,
+ StatusCode = (int)response.Response.StatusCode,
+ ResponseBody = response.ResponseBody
+ };
+
+ if (result.Success)
+ {
+ logger.LogInformation(
+ "LinkOrders succeeded for parent {OrderNo} -> {Count} children",
+ body.OrderNo,
+ nos.Count);
+ }
+ else
+ {
+ logger.LogWarning(
+ "LinkOrders failed for parent {OrderNo}: {StatusCode} {Body}",
+ body.OrderNo,
+ result.StatusCode,
+ result.ResponseBody);
+ }
+
+ return result;
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "LinkOrders failed for parent {OrderNo}", parentOrderNo);
+ return new SmallBagTrackUpsertResult
+ {
+ Skipped = false,
+ Success = false,
+ Url = url,
+ RequestJson = requestJson,
+ ResponseBody = ex.Message
+ };
}
}
@@ -215,11 +449,11 @@ internal sealed class SmallBagTrackOrderClient(
return new SignedTrackHttpResponse(response, responseBody, timestamp, sign);
}
- private string? ResolveAuthorization(SmallBagTrackUpsertRequest request)
+ private string? ResolveAuthorization(string? authorization)
{
- if (!string.IsNullOrWhiteSpace(request.Authorization))
+ if (!string.IsNullOrWhiteSpace(authorization))
{
- return request.Authorization.Trim();
+ return authorization.Trim();
}
return httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString();
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs
index d8d0bb3..2427d8e 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackServiceExtensions.cs
@@ -12,6 +12,7 @@ public static class SmallBagTrackServiceExtensions
services.AddScoped();
services.AddScoped();
services.AddScoped();
+ services.AddScoped();
services.AddSingleton();
return services;
}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertBigBagApiRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertBigBagApiRequest.cs
new file mode 100644
index 0000000..72e1c00
--- /dev/null
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertBigBagApiRequest.cs
@@ -0,0 +1,22 @@
+namespace Oms.Application.Integrations.SmallBagTrack;
+
+///
+/// POST /ProviderService/Order/UpsertBigBag 业务请求体。
+///
+internal sealed class SmallBagTrackUpsertBigBagApiRequest
+{
+ public string Client { get; set; } = string.Empty;
+
+ public string BagNo { get; set; } = string.Empty;
+
+ public string OtherNo { get; set; } = string.Empty;
+
+ public string Channel { get; set; } = string.Empty;
+
+ public string Product { get; set; } = string.Empty;
+
+ public string Country { get; set; } = string.Empty;
+
+ public static string BuildEndpointUrl(string baseUrl) =>
+ $"{baseUrl.Trim().TrimEnd('/')}/ProviderService/Order/UpsertBigBag";
+}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertBigBagRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertBigBagRequest.cs
new file mode 100644
index 0000000..3635ec7
--- /dev/null
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertBigBagRequest.cs
@@ -0,0 +1,21 @@
+namespace Oms.Application.Integrations.SmallBagTrack;
+
+///
+/// 大包轨迹订单业务请求(UpsertBigBag)。
+///
+public class SmallBagTrackUpsertBigBagRequest
+{
+ public string Client { get; set; } = string.Empty;
+
+ public string BagNo { get; set; } = string.Empty;
+
+ public string? OtherNo { get; set; }
+
+ public string? Channel { get; set; }
+
+ public string? Product { get; set; }
+
+ public string? Country { get; set; }
+
+ public string? Authorization { get; set; }
+}
diff --git a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs
index 24f2c05..261462f 100644
--- a/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs
+++ b/src/Oms.Application/Integrations/SmallBagTrack/SmallBagTrackUpsertTrackApiRequest.cs
@@ -24,11 +24,12 @@ internal sealed class SmallBagTrackUpsertTrackApiRequest
string appKey,
string orderNo,
string trackCode,
- DateTime trackTime) =>
+ DateTime trackTime,
+ int orderType = SmallBagTrackLifecycleEvents.OrderType) =>
new()
{
AppKey = appKey,
- OrderType = SmallBagTrackLifecycleEvents.OrderType,
+ OrderType = orderType,
OrderNo = orderNo,
IsAdd = true,
TrackCode = trackCode,
diff --git a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
index 79d6295..7d1d00b 100644
--- a/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.Helpers.cs
@@ -640,7 +640,18 @@ public partial class OrderService
? Path.Combine(WebHostEnvironment.ContentRootPath, "wwwroot")
: WebHostEnvironment.WebRootPath;
// 只确保 uploads 子目录可写,避免在只读 ContentRoot 上强建 wwwroot 根目录。
- Directory.CreateDirectory(Path.Combine(webRoot, "uploads"));
+ try
+ {
+ Directory.CreateDirectory(Path.Combine(webRoot, "uploads"));
+ }
+ catch (UnauthorizedAccessException)
+ {
+ // 容器非 root + /app/wwwroot 未预建/权限错误时,导出清理等旁路不应炸主流程
+ }
+ catch (IOException)
+ {
+ }
+
return webRoot;
}
diff --git a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
index 1217f92..fb58dee 100644
--- a/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
+++ b/src/Oms.Application/Services/Orders/OrderService.ParcelBilling.cs
@@ -2588,26 +2588,49 @@ public partial class OrderService
private async Task BuildParcelCreateOutputAsync(Order order)
{
- var mapped = (await MapOrderOutputsAsync([order])).First();
- var systemLabelUrl = await OrderFileManager.GetSystemLabelUrlAsync(order.Id);
- var labelUrl = systemLabelUrl ?? string.Empty;
- if (string.IsNullOrWhiteSpace(labelUrl) && order.OrderStatus == OrderStatusCodes.Predeclared)
+ // 预报已成功时,映射/面单/本地存储失败不得回滚订单(常见:容器 /app/wwwroot 不可写)。
+ var logger = LazyServiceProvider.GetRequiredService()
+ .CreateLogger("Oms.OrderParcelCreate");
+ var labelUrl = string.Empty;
+ try
{
- // 回落 HTML 面单仅用于返回 LabelUrl;写盘失败不得回滚已预报成功的订单。
+ var mapped = (await MapOrderOutputsAsync([order])).First();
try
{
- labelUrl = await CreateOrderPrintFileAsync(mapped);
+ labelUrl = await OrderFileManager.GetSystemLabelUrlAsync(order.Id) ?? string.Empty;
}
catch (Exception ex)
{
- var logger = LazyServiceProvider.GetRequiredService()
- .CreateLogger("Oms.OrderParcelCreate");
logger.LogWarning(
ex,
- "预报成功但回落面单落盘失败 OrderId={OrderId} ServiceNo={ServiceNo}",
+ "预报成功但读取系统面单 URL 失败 OrderId={OrderId} ServiceNo={ServiceNo}",
order.Id,
order.ServiceNo);
}
+
+ if (string.IsNullOrWhiteSpace(labelUrl) && order.OrderStatus == OrderStatusCodes.Predeclared)
+ {
+ try
+ {
+ labelUrl = await CreateOrderPrintFileAsync(mapped);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(
+ ex,
+ "预报成功但回落面单落盘失败 OrderId={OrderId} ServiceNo={ServiceNo}",
+ order.Id,
+ order.ServiceNo);
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(
+ ex,
+ "预报成功但组装创单出参失败 OrderId={OrderId} ServiceNo={ServiceNo}",
+ order.Id,
+ order.ServiceNo);
}
return new OrderParcelCreateOutput
diff --git a/src/Oms.Application/Services/Orders/PickupOrderService.cs b/src/Oms.Application/Services/Orders/PickupOrderService.cs
index fc10454..bd33958 100644
--- a/src/Oms.Application/Services/Orders/PickupOrderService.cs
+++ b/src/Oms.Application/Services/Orders/PickupOrderService.cs
@@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Mvc;
using Oms.Application.Dtos.Orders;
using Oms.Application.Dtos.Shared;
using Oms.Application.Integrations.Itfx;
+using Oms.Application.Integrations.SmallBagTrack;
using Oms.Application.Shared;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.CustomersAggregate.Repositories;
@@ -65,6 +66,9 @@ public class PickupOrderService : ApplicationService
private IItfxMetadataClient ItfxMetadataClient =>
LazyServiceProvider.GetRequiredService();
+ private IPickupBigBagTrackSyncer PickupBigBagTrackSyncer =>
+ LazyServiceProvider.GetRequiredService();
+
///
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
///
@@ -983,6 +987,11 @@ public class PickupOrderService : ApplicationService
}
}
+ if (forward.Success)
+ {
+ await PickupBigBagTrackSyncer.SyncAfterDownstreamForwardAsync(pickup);
+ }
+
return new OrderDownstreamForwardResult
{
Success = forward.Success,
diff --git a/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs b/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs
index ab34314..28910bc 100644
--- a/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs
+++ b/src/Oms.Application/Storage/OmsObjectStorageExtensions.cs
@@ -33,6 +33,7 @@ public static class OmsObjectStorageExtensions
///
/// 临时访问地址。空值返回空串;外链原样返回;逻辑 key 走 。
+ /// Local 存储目录不可写/不可读时降级返回 key,避免拖垮下单等主流程。
///
public static async Task GetPublicUrlAsync(
this IObjectStorage storage,
@@ -51,7 +52,22 @@ public static class OmsObjectStorageExtensions
}
var key = FileStoragePath.NormalizeKey(raw);
- return await storage.GetUrlAsync(key, cancellationToken: cancellationToken);
+ try
+ {
+ return await storage.GetUrlAsync(key, cancellationToken: cancellationToken);
+ }
+ catch (UnauthorizedAccessException)
+ {
+ return key;
+ }
+ catch (DirectoryNotFoundException)
+ {
+ return key;
+ }
+ catch (IOException)
+ {
+ return key;
+ }
}
///