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; /// /// 揽收大包轨迹同步结果(UpsertBigBag → Link → Track A0201)。 /// 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; } } /// /// 揽收单预报下游成功后 / 手动:按序 UpsertBigBag → Link → Track/Upsert(A0201)。 /// public interface IPickupBigBagTrackSyncer { Task SyncAfterDownstreamForwardAsync(PickupOrder pickup, CancellationToken cancellationToken = default); /// /// 同步执行三步并返回逐步请求/响应(手动「创建轨迹订单」联调)。 /// Task SyncDebugAsync(PickupOrder pickup, CancellationToken cancellationToken = default); } internal sealed class PickupBigBagTrackSyncer( ISmallBagTrackOrderClient trackClient, IPickupOrderLineRepository pickupOrderLineRepository, ISalesProductRepository salesProductRepository, IServiceChannelRepository serviceChannelRepository, IHttpContextAccessor httpContextAccessor, ILogger logger) : IPickupBigBagTrackSyncer { public async Task SyncAfterDownstreamForwardAsync( PickupOrder pickup, CancellationToken cancellationToken = default) { await SyncDebugAsync(pickup, cancellationToken); } public async Task 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"; } }