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; + } } ///