admin/src/Oms.Application/Services/Orders/Batch/OrderBatchActionExecutor.cs
zzh 64737284f3
All checks were successful
Build and Deploy / deploy (push) Successful in 1m3s
feat: 运单操作日志改为列表摘要,补齐费用/支付/预报/打印埋点
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-10 11:46:33 +08:00

291 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using Fake.Application;
using Fake.DependencyInjection;
using Fake.ObjectStorage;
using Newtonsoft.Json;
using Oms.Application.Dtos.Orders;
using Oms.Application.Integrations.Itfx;
using Oms.Application.Integrations.SmallBagTrack;
using Oms.Application.Messaging;
using Oms.Application.Shared;
using Oms.Application.Storage;
using Oms.Domain.ChannelsAggregate.Repositories;
using Oms.Domain.OrdersAggregate.Consts;
using Oms.Domain.OrdersAggregate.Entities;
using Oms.Domain.OrdersAggregate.Repositories;
using Oms.Domain.SystemAggregate.Repositories;
namespace Oms.Application.Services.Orders.Batch;
public sealed class OrderBatchActionExecutor(
IOrderRepository orderRepository,
ICountryRepository countryRepository,
IServiceChannelRepository serviceChannelRepository,
IObjectStorage fileStorage,
ISmallBagTrackOrderClient smallBagTrackOrderClient,
ISmallBagTrackUpsertRequestBuilder smallBagTrackUpsertRequestBuilder,
OrderDownstreamCredentialResolver downstreamCredentialResolver,
IItfxForwardClient itfxForwardClient,
OrderFileManager orderFileManager,
OrderOperationLogWriter operationLogWriter,
OrderBatchCreateActionHandler createActionHandler,
OrderBatchOrderActionHandler orderActionHandler) : IOrderBatchActionExecutor, IScopedDependency
{
public async Task<OrderBatchActionExecuteResult> ExecuteAsync(
OrderBatchJobMessage message,
CancellationToken cancellationToken = default)
{
try
{
return message.Action switch
{
OrderBatchActionCodes.TrackUpsertSmallBag => await ExecuteTrackUpsertAsync(message, cancellationToken),
OrderBatchActionCodes.DownstreamForwardCreateOne => await ExecuteDownstreamForwardAsync(message, cancellationToken),
OrderBatchActionCodes.ImportRow => await createActionHandler.ExecuteImportRowAsync(message, cancellationToken),
OrderBatchActionCodes.ParcelCreate => await createActionHandler.ExecuteParcelCreateAsync(message, cancellationToken),
OrderBatchActionCodes.Pay => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.PreDeclare => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.Intercept => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.CancelIntercept => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.Delete => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.Print => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.GenerateLabelPdf => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.InboundBilling => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.ForecastBilling => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.Reallocate => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.MarkProblematic => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.MarkClaimed => await orderActionHandler.ExecuteAsync(message, cancellationToken),
OrderBatchActionCodes.HandleStatusUpdate => await orderActionHandler.ExecuteAsync(message, cancellationToken),
_ => OrderBatchActionExecuteResult.BusinessFail($"未实现的批量操作: {message.Action}")
};
}
catch (BusinessException ex)
{
return OrderBatchActionExecuteResult.BusinessFail(ex.Message);
}
catch (Exception ex)
{
return OrderBatchActionExecuteResult.SystemFail(ex.Message);
}
}
private async Task<OrderBatchActionExecuteResult> ExecuteTrackUpsertAsync(
OrderBatchJobMessage message,
CancellationToken cancellationToken)
{
SmallBagTrackUpsertRequest request;
if (!string.IsNullOrWhiteSpace(message.PayloadJson))
{
request = JsonConvert.DeserializeObject<SmallBagTrackUpsertRequest>(message.PayloadJson!)
?? throw new BusinessException("轨迹同步消息体无效");
}
else
{
var order = await orderRepository.FirstOrDefaultAsync(x => x.Id == message.OrderId, cancellationToken);
if (order == null)
{
return OrderBatchActionExecuteResult.BusinessFail("订单不存在");
}
if (string.IsNullOrWhiteSpace(order.ServiceNo))
{
return OrderBatchActionExecuteResult.BusinessFail("订单运单号为空,无法同步轨迹");
}
request = await smallBagTrackUpsertRequestBuilder.BuildAsync(order, cancellationToken: cancellationToken);
// 手动「创建轨迹」无 payload:创单成功后补默认轨迹节点 A0101
request.AppendInitialTrackOnSuccess = true;
}
if (string.IsNullOrWhiteSpace(request.Authorization)
&& !string.IsNullOrWhiteSpace(message.Authorization))
{
request.Authorization = message.Authorization.Trim();
}
var trackResult = await smallBagTrackOrderClient.UpsertDebugAsync(request, cancellationToken);
var serviceNo = request.OrderNo?.Trim();
if (trackResult.Skipped)
{
var reason = string.IsNullOrWhiteSpace(trackResult.SkipReason)
? "轨迹服务未启用或配置不完整"
: trackResult.SkipReason.Trim();
return OrderBatchActionExecuteResult.BusinessFail(
reason,
resultJson: OrderBatchTrackUpsertResultBuilder.BuildFailure(
trackResult,
message.OrderId,
serviceNo,
reason));
}
if (!trackResult.Success)
{
var reason = $"轨迹订单创建失败: HTTP {trackResult.StatusCode}";
return OrderBatchActionExecuteResult.BusinessFail(
reason,
resultJson: OrderBatchTrackUpsertResultBuilder.BuildFailure(
trackResult,
message.OrderId,
serviceNo,
reason));
}
return OrderBatchActionExecuteResult.Ok(
serviceNo: serviceNo,
resultJson: OrderBatchTrackUpsertResultBuilder.BuildSuccess(
trackResult,
message.OrderId,
serviceNo));
}
private async Task<OrderBatchActionExecuteResult> ExecuteDownstreamForwardAsync(
OrderBatchJobMessage message,
CancellationToken cancellationToken)
{
var authorization = message.Authorization;
if (!string.IsNullOrWhiteSpace(authorization))
{
ItfxAuthorizationContext.Authorization = authorization.Trim();
}
try
{
ItfxCreateOrderOneBody body;
Order order;
if (!string.IsNullOrWhiteSpace(message.PayloadJson))
{
body = JsonConvert.DeserializeObject<ItfxCreateOrderOneBody>(message.PayloadJson!)
?? throw new BusinessException("下游预报消息体无效");
// 凭证解析仍需订单上的渠道/下游绑定字段;有 payload 时只查主表一次
var existing = await orderRepository.FirstOrDefaultAsync(x => x.Id == message.OrderId, cancellationToken);
if (existing == null)
{
return OrderBatchActionExecuteResult.BusinessFail("订单不存在");
}
order = existing;
}
else
{
var bundles = await orderRepository.GetDownstreamCreateOneBundlesAsync([message.OrderId], cancellationToken);
if (!bundles.TryGetValue(message.OrderId, out var bundle))
{
return OrderBatchActionExecuteResult.BusinessFail("订单不存在");
}
order = bundle.Order;
body = await OrderDownstreamCreateOneBodyBuilder.BuildAsync(
bundle.Order,
bundle.Sender,
bundle.Recipient,
bundle.Declarations,
bundle.Weights,
countryRepository,
serviceChannelRepository,
fileStorage,
cancellationToken);
}
if (string.IsNullOrWhiteSpace(order.ServiceNo))
{
return OrderBatchActionExecuteResult.BusinessFail("订单运单号为空,无法预报下游");
}
var creds = await downstreamCredentialResolver.ResolveAsync(order, cancellationToken);
var forward = await itfxForwardClient.ForwardCreateOrderOneAsync(
creds.SupplierCode,
creds.AppKey,
creds.AppSecret,
body,
cancellationToken);
ItfxForwardResultEvaluator.IsBusinessSuccess(
forward.StatusCode,
forward.ResponseBody,
out var businessCode,
out var businessMessage);
if (!forward.Success)
{
var reason = ItfxForwardResultEvaluator.BuildFailReason(
forward.StatusCode,
forward.ResponseBody,
businessCode,
businessMessage);
return OrderBatchActionExecuteResult.BusinessFail(
reason,
resultJson: OrderBatchDownstreamForwardResultBuilder.BuildFailure(
forward,
message.OrderId,
order.ServiceNo,
creds.SupplierCode,
reason,
businessCode,
businessMessage));
}
var beforeSnap = OrderOperationLogSnapshot.CaptureOrder(order);
if (ItfxCreateOrderOneDataParser.TryParse(forward.ResponseBody, out var data)
&& data != null)
{
if (ItfxCreateOrderOneDataParser.ApplyToOrder(order, data))
{
await orderRepository.UpdateAsync(order, cancellationToken);
}
if (!string.IsNullOrWhiteSpace(data.LabelUrl))
{
await orderFileManager.AddOrReplaceDownstreamLabelAsync(
order.Id,
data.LabelUrl!,
message.OperatorUserId,
cancellationToken);
}
}
var afterSnap = OrderOperationLogSnapshot.CaptureOrder(order);
var remarkParts = new List<string> { $"供应商 {creds.SupplierCode}" };
if (!string.IsNullOrWhiteSpace(order.DownstreamServiceNo))
{
remarkParts.Add($"服务商单号 {order.DownstreamServiceNo}");
}
if (!string.IsNullOrWhiteSpace(order.TrackNo))
{
remarkParts.Add($"跟踪号 {order.TrackNo}");
}
await operationLogWriter.TryWriteAsync(
order,
OrderOperationActionCodes.DownstreamForward,
beforeSnap,
afterSnap,
string.Join(";", remarkParts),
message.OperatorUserId > 0 ? message.OperatorUserId : null,
operatorName: null,
operatorType: "staff",
source: "job");
return OrderBatchActionExecuteResult.Ok(
serviceNo: order.ServiceNo,
trackNo: order.TrackNo,
resultJson: OrderBatchDownstreamForwardResultBuilder.BuildSuccess(
forward,
message.OrderId,
order.ServiceNo,
creds.SupplierCode,
businessCode,
businessMessage,
order.DownstreamServiceNo,
order.TrackNo));
}
finally
{
ItfxAuthorizationContext.Authorization = null;
}
}
}