All checks were successful
Build and Deploy / deploy (push) Successful in 1m1s
Co-authored-by: Cursor <cursoragent@cursor.com>
270 lines
9.6 KiB
C#
270 lines
9.6 KiB
C#
using Microsoft.AspNetCore.Http;
|
||
using Microsoft.Extensions.Logging;
|
||
using Oms.Domain.ChannelsAggregate.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 A0201)。
|
||
/// </summary>
|
||
public class PickupBigBagTrackSyncResult
|
||
{
|
||
public string PickupNo { get; set; } = string.Empty;
|
||
|
||
public bool Success { get; set; }
|
||
|
||
public string? FailStep { get; set; }
|
||
|
||
public string? Message { get; set; }
|
||
|
||
public SmallBagTrackUpsertResult? UpsertBigBag { get; set; }
|
||
|
||
public SmallBagTrackUpsertResult? Link { get; set; }
|
||
|
||
public SmallBagTrackUpsertResult? TrackUpsert { get; set; }
|
||
}
|
||
|
||
/// <summary>
|
||
/// 揽收单预报下游成功后 / 手动:按序 UpsertBigBag → Link → Track/Upsert(A0201)。
|
||
/// </summary>
|
||
public interface IPickupBigBagTrackSyncer
|
||
{
|
||
Task SyncAfterDownstreamForwardAsync(PickupOrder pickup, CancellationToken cancellationToken = default);
|
||
|
||
/// <summary>
|
||
/// 同步执行三步并返回逐步请求/响应(手动「创建轨迹订单」联调)。
|
||
/// </summary>
|
||
Task<PickupBigBagTrackSyncResult> SyncDebugAsync(PickupOrder pickup, CancellationToken cancellationToken = default);
|
||
}
|
||
|
||
internal sealed class PickupBigBagTrackSyncer(
|
||
ISmallBagTrackOrderClient trackClient,
|
||
IPickupOrderLineRepository pickupOrderLineRepository,
|
||
ISalesProductRepository salesProductRepository,
|
||
IServiceChannelRepository serviceChannelRepository,
|
||
IHttpContextAccessor httpContextAccessor,
|
||
ILogger<PickupBigBagTrackSyncer> logger) : IPickupBigBagTrackSyncer
|
||
{
|
||
public async Task SyncAfterDownstreamForwardAsync(
|
||
PickupOrder pickup,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
await SyncDebugAsync(pickup, cancellationToken);
|
||
}
|
||
|
||
public async Task<PickupBigBagTrackSyncResult> SyncDebugAsync(
|
||
PickupOrder pickup,
|
||
CancellationToken cancellationToken = default)
|
||
{
|
||
var result = new PickupBigBagTrackSyncResult
|
||
{
|
||
PickupNo = pickup?.PickupNo?.Trim() ?? string.Empty
|
||
};
|
||
|
||
if (pickup == null || string.IsNullOrWhiteSpace(pickup.PickupNo))
|
||
{
|
||
result.Success = false;
|
||
result.FailStep = "validate";
|
||
result.Message = "pickup or PickupNo empty";
|
||
logger.LogWarning("Skip big-bag track sync: pickup or PickupNo empty");
|
||
return result;
|
||
}
|
||
|
||
var bagNo = pickup.PickupNo.Trim();
|
||
result.PickupNo = bagNo;
|
||
var authorization = httpContextAccessor.HttpContext?.Request.Headers.Authorization.ToString();
|
||
if (string.IsNullOrWhiteSpace(authorization))
|
||
{
|
||
authorization = null;
|
||
}
|
||
|
||
try
|
||
{
|
||
var (channel, product) = await ResolveChannelProductAsync(pickup.CarrierService, cancellationToken);
|
||
var country = ResolveCountry(pickup);
|
||
|
||
// 1) UpsertBigBag:client 取 product(销售产品编码)
|
||
var upsertResult = await trackClient.UpsertBigBagAsync(
|
||
new SmallBagTrackUpsertBigBagRequest
|
||
{
|
||
Client = product ?? string.Empty,
|
||
BagNo = bagNo,
|
||
OtherNo = pickup.CarrierWaybillNo,
|
||
Channel = channel,
|
||
Product = product,
|
||
Country = country,
|
||
Authorization = authorization
|
||
},
|
||
cancellationToken);
|
||
result.UpsertBigBag = upsertResult;
|
||
|
||
if (upsertResult.Skipped)
|
||
{
|
||
result.Success = false;
|
||
result.FailStep = "UpsertBigBag";
|
||
result.Message = upsertResult.SkipReason ?? "skipped";
|
||
logger.LogDebug(
|
||
"UpsertBigBag skipped for pickup {PickupNo}: {Reason}",
|
||
bagNo,
|
||
upsertResult.SkipReason);
|
||
return result;
|
||
}
|
||
|
||
if (!upsertResult.Success)
|
||
{
|
||
result.Success = false;
|
||
result.FailStep = "UpsertBigBag";
|
||
result.Message = $"HTTP {upsertResult.StatusCode}";
|
||
logger.LogWarning(
|
||
"Stop big-bag track sync after UpsertBigBag failure for pickup {PickupNo}",
|
||
bagNo);
|
||
return result;
|
||
}
|
||
|
||
// 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);
|
||
result.Link = linkResult;
|
||
|
||
if (linkResult.Skipped)
|
||
{
|
||
logger.LogDebug(
|
||
"Link skipped for pickup {PickupNo}: {Reason}",
|
||
bagNo,
|
||
linkResult.SkipReason);
|
||
}
|
||
else if (!linkResult.Success)
|
||
{
|
||
result.Success = false;
|
||
result.FailStep = "Link";
|
||
result.Message = $"HTTP {linkResult.StatusCode}";
|
||
logger.LogWarning(
|
||
"Stop big-bag track sync after Link failure for pickup {PickupNo}",
|
||
bagNo);
|
||
return result;
|
||
}
|
||
}
|
||
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);
|
||
result.TrackUpsert = trackResult;
|
||
|
||
if (trackResult.Skipped)
|
||
{
|
||
result.Success = false;
|
||
result.FailStep = "TrackUpsert";
|
||
result.Message = trackResult.SkipReason ?? "skipped";
|
||
logger.LogDebug(
|
||
"Track A0201 skipped for pickup {PickupNo}: {Reason}",
|
||
bagNo,
|
||
trackResult.SkipReason);
|
||
return result;
|
||
}
|
||
|
||
if (!trackResult.Success)
|
||
{
|
||
result.Success = false;
|
||
result.FailStep = "TrackUpsert";
|
||
result.Message = $"HTTP {trackResult.StatusCode}";
|
||
logger.LogWarning(
|
||
"Track A0201 failed for pickup {PickupNo}: {StatusCode} {Body}",
|
||
bagNo,
|
||
trackResult.StatusCode,
|
||
trackResult.ResponseBody);
|
||
return result;
|
||
}
|
||
|
||
result.Success = true;
|
||
result.Message = "ok";
|
||
return result;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
result.Success = false;
|
||
result.FailStep ??= "exception";
|
||
result.Message = ex.Message;
|
||
logger.LogWarning(ex, "Big-bag track sync failed for pickup {PickupNo}", bagNo);
|
||
return result;
|
||
}
|
||
}
|
||
|
||
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";
|
||
}
|
||
}
|