fix: 加固本地存储失败不回滚创单,并完善揽收大包轨迹同步
All checks were successful
Build and Deploy / deploy (push) Successful in 1m2s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m2s
避免 /app/wwwroot 不可写时预报成功被回滚;同步推送 test 部署出口资质等已合入能力。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
c9084ee5ca
commit
26c840cdb1
@ -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);
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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";
|
||||
}
|
||||
@ -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; }
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user