20261008-ide1.0发布 #1

Merged
zzh merged 42 commits from test into master 2026-10-08 09:18:00 +08:00
13 changed files with 661 additions and 60 deletions
Showing only changes of commit 26c840cdb1 - Show all commits

View File

@ -5,7 +5,7 @@ public interface ISmallBagTrackOrderClient
Task UpsertAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// 追加一条生命周期轨迹节点(POST ProviderService/Track/Upsert)。
/// 追加一条生命周期轨迹节点(POST ProviderService/Track/Upsert),默认小包 orderType=1。
/// </summary>
Task AppendLifecycleTrackAsync(
string orderNo,
@ -13,8 +13,36 @@ public interface ISmallBagTrackOrderClient
string? authorization = null,
CancellationToken cancellationToken = default);
/// <summary>
/// 追加生命周期轨迹节点,返回调用结果(用于按序编排)。
/// </summary>
Task<SmallBagTrackUpsertResult> AppendLifecycleTrackResultAsync(
string orderNo,
string trackCode,
int orderType,
string? authorization = null,
CancellationToken cancellationToken = default);
/// <summary>
/// 同步调用并返回完整请求/响应(仅调试)。
/// </summary>
Task<SmallBagTrackUpsertResult> UpsertDebugAsync(SmallBagTrackUpsertRequest request, CancellationToken cancellationToken = default);
/// <summary>
/// 创建/更新大包轨迹订单(POST ProviderService/Order/UpsertBigBag)。
/// </summary>
Task<SmallBagTrackUpsertResult> UpsertBigBagAsync(
SmallBagTrackUpsertBigBagRequest request,
CancellationToken cancellationToken = default);
/// <summary>
/// 关联大包与小包(POST ProviderService/Order/Link)。
/// </summary>
Task<SmallBagTrackUpsertResult> LinkOrdersAsync(
string parentOrderNo,
int parentOrderType,
IReadOnlyList<string> linkOrderNos,
int linkOrderType,
string? authorization = null,
CancellationToken cancellationToken = default);
}

View File

@ -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;
/// <summary>
/// 揽收单预报下游成功后:按序 UpsertBigBag → Link → Track/Upsert(A0201)。
/// </summary>
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<PickupBigBagTrackSyncer> 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";
}
}

View File

@ -1,12 +1,15 @@
namespace Oms.Application.Integrations.SmallBagTrack;
/// <summary>
/// 小包订单生命周期默认轨迹节点(POST ProviderService/Track/Upsert)。
/// 订单生命周期默认轨迹节点(POST ProviderService/Track/Upsert)。
/// </summary>
internal static class SmallBagTrackLifecycleEvents
{
public const string CreateTrackCode = "A0101";
/// <summary>揽收大包创单轨迹。</summary>
public const string PickupCreateTrackCode = "A0201";
public const string InboundTrackCode = "A0301";
public const string OutboundTrackCode = "A0701";
@ -18,6 +21,9 @@ internal static class SmallBagTrackLifecycleEvents
/// <summary>轨迹订单类型:小包。</summary>
public const int OrderType = 1;
/// <summary>轨迹订单类型:大包。</summary>
public const int BigBagOrderType = 2;
/// <summary>数据来源:内部系统。</summary>
public const int Source = 2;
}

View File

@ -0,0 +1,24 @@
namespace Oms.Application.Integrations.SmallBagTrack;
/// <summary>
/// POST /ProviderService/Order/Link 业务请求体。
/// </summary>
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<string> LinkOrderNos { get; set; } = [];
public static string BuildEndpointUrl(string baseUrl) =>
$"{baseUrl.Trim().TrimEnd('/')}/ProviderService/Order/Link";
}

View File

@ -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<SmallBagTrackUpsertResult> 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<SmallBagTrackUpsertResult> 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<SmallBagTrackUpsertResult> 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<SmallBagTrackUpsertResult> LinkOrdersAsync(
string parentOrderNo,
int parentOrderType,
IReadOnlyList<string> 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();

View File

@ -12,6 +12,7 @@ public static class SmallBagTrackServiceExtensions
services.AddScoped<ISmallBagTrackConfigProvider, SmallBagTrackConfigProvider>();
services.AddScoped<ISmallBagTrackUpsertRequestBuilder, SmallBagTrackUpsertRequestBuilder>();
services.AddScoped<ISmallBagTrackOrderClient, SmallBagTrackOrderClient>();
services.AddScoped<IPickupBigBagTrackSyncer, PickupBigBagTrackSyncer>();
services.AddSingleton<ISmallBagTrackOrderNotifier, SmallBagTrackOrderNotifier>();
return services;
}

View File

@ -0,0 +1,22 @@
namespace Oms.Application.Integrations.SmallBagTrack;
/// <summary>
/// POST /ProviderService/Order/UpsertBigBag 业务请求体。
/// </summary>
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";
}

View File

@ -0,0 +1,21 @@
namespace Oms.Application.Integrations.SmallBagTrack;
/// <summary>
/// 大包轨迹订单业务请求(UpsertBigBag)。
/// </summary>
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; }
}

View File

@ -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,

View File

@ -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;
}

View File

@ -2588,26 +2588,49 @@ public partial class OrderService
private async Task<OrderParcelCreateOutput> 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<ILoggerFactory>()
.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<ILoggerFactory>()
.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

View File

@ -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<IItfxMetadataClient>();
private IPickupBigBagTrackSyncer PickupBigBagTrackSyncer =>
LazyServiceProvider.GetRequiredService<IPickupBigBagTrackSyncer>();
/// <summary>
/// 创建揽收单并挂载物流单(原子:全部校验通过才落库)。
/// </summary>
@ -983,6 +987,11 @@ public class PickupOrderService : ApplicationService
}
}
if (forward.Success)
{
await PickupBigBagTrackSyncer.SyncAfterDownstreamForwardAsync(pickup);
}
return new OrderDownstreamForwardResult
{
Success = forward.Success,

View File

@ -33,6 +33,7 @@ public static class OmsObjectStorageExtensions
/// <summary>
/// 临时访问地址。空值返回空串;外链原样返回;逻辑 key 走 <see cref="IObjectStorage.GetUrlAsync"/>。
/// Local 存储目录不可写/不可读时降级返回 key,避免拖垮下单等主流程。
/// </summary>
public static async Task<string> 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;
}
}
/// <summary>