using Fake.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Oms.Application.Dtos.Orders; using Oms.Application.Messaging; using Oms.Domain.OrdersAggregate.Consts; using Oms.Domain.OrdersAggregate.Entities; using Oms.Domain.OrdersAggregate.Repositories; namespace Oms.Application.Services.Orders.Batch; /// /// 消费 RabbitMQ 消息后更新任务状态并执行业务。 /// 正常返回由 Fake EventBus Ack;系统失败可重试,耗尽后标记失败避免子项长期停留在执行中。 /// public sealed class OrderBatchJobProcessor( IOrderBatchJobRepository batchJobRepository, IOrderRepository orderRepository, IOrderBatchActionExecutor actionExecutor, IOptions batchOptions, ILogger logger) : IScopedDependency { public async Task ProcessAsync( OrderBatchJobMessage message, CancellationToken cancellationToken = default) { var maxSystemRetries = Math.Max(1, batchOptions.Value.MaxSystemRetries); var item = await ResolveJobItemAsync(message, cancellationToken); if (item != null) { if (item.Status == OrderBatchJobItemStatusCodes.Success) { return; } if (item.Status == OrderBatchJobItemStatusCodes.Cancelled) { logger.LogInformation("Skip cancelled job item {JobItemId}", item.Id); return; } item.Status = OrderBatchJobItemStatusCodes.Running; item.StartedAt ??= DateTime.Now; item.UpdateTime = DateTime.Now; await batchJobRepository.UpdateItemAsync(item, cancellationToken); } var result = await actionExecutor.ExecuteAsync(message, cancellationToken); if (item == null) { if (!result.Success) { logger.LogWarning( "Fire-and-forget job failed action={Action} orderId={OrderId} reason={Reason}", message.Action, message.OrderId, result.FailReason); } return; } item = await batchJobRepository.GetItemByIdAsync(item.Id, cancellationToken); if (item == null) { logger.LogWarning( "Job item {JobItemId} not found after execute, ack message {MessageId}", message.JobItemId, message.MessageId); return; } item.FinishedAt = DateTime.Now; item.UpdateTime = DateTime.Now; var action = string.IsNullOrWhiteSpace(item.Action) ? message.Action : item.Action; if (result.Success) { OrderBatchJobItemFeedback.ApplySuccess(item, result, action); await OrderBatchJobItemFeedback.EnrichIdentifiersAsync(item, action, orderRepository, cancellationToken); await batchJobRepository.UpdateItemAsync(item, cancellationToken); await RefreshJobSummaryAsync(item.JobId, cancellationToken); return; } if (result.IsBusinessFailure) { if (result.CreatedOrderId is > 0) { item.OrderId = result.CreatedOrderId.Value; } await OrderBatchJobItemFeedback.EnrichIdentifiersAsync(item, action, orderRepository, cancellationToken); OrderBatchJobItemFeedback.ApplyBusinessFailure(item, result, action); await batchJobRepository.UpdateItemAsync(item, cancellationToken); await RefreshJobSummaryAsync(item.JobId, cancellationToken); return; } await OrderBatchJobItemFeedback.EnrichIdentifiersAsync(item, action, orderRepository, cancellationToken); OrderBatchJobItemFeedback.ApplySystemRetry(item, result, action, maxSystemRetries); await batchJobRepository.UpdateItemAsync(item, cancellationToken); if (item.Status == OrderBatchJobItemStatusCodes.Failed) { logger.LogError( "Batch job item {JobItemId} action={Action} failed after {RetryCount} system retries: {Reason}", item.Id, action, item.RetryCount, item.FailReason); await RefreshJobSummaryAsync(item.JobId, cancellationToken); return; } throw new InvalidOperationException(item.FailReason ?? result.FailReason ?? "批量任务系统失败"); } private async Task ResolveJobItemAsync( OrderBatchJobMessage message, CancellationToken cancellationToken) { if (message.JobItemId > 0) { return await batchJobRepository.GetItemByIdAsync((int)message.JobItemId, cancellationToken); } if (message.JobId > 0 && !string.IsNullOrWhiteSpace(message.MessageId)) { var resolved = await batchJobRepository.GetItemByJobAndMessageIdAsync( (int)message.JobId, message.MessageId.Trim(), cancellationToken); if (resolved != null) { logger.LogWarning( "Resolved job item by messageId jobId={JobId} messageId={MessageId} itemId={JobItemId}", message.JobId, message.MessageId, resolved.Id); } return resolved; } return null; } private async Task RefreshJobSummaryAsync(int jobId, CancellationToken cancellationToken) { var job = await batchJobRepository.GetByIdAsync(jobId, cancellationToken); if (job == null) { return; } var items = await batchJobRepository.GetItemsByJobIdAsync(jobId, cancellationToken); job.SuccessCount = items.Count(x => x.Status == OrderBatchJobItemStatusCodes.Success); job.FailCount = items.Count(x => x.Status == OrderBatchJobItemStatusCodes.Failed); job.UpdateTime = DateTime.Now; if (items.All(x => x.Status is OrderBatchJobItemStatusCodes.Success or OrderBatchJobItemStatusCodes.Failed or OrderBatchJobItemStatusCodes.Cancelled)) { var cancelledCount = items.Count(x => x.Status == OrderBatchJobItemStatusCodes.Cancelled); if (cancelledCount == items.Count) { job.Status = OrderBatchJobStatusCodes.Cancelled; } else { job.Status = job.FailCount > 0 && job.SuccessCount == 0 ? OrderBatchJobStatusCodes.Failed : OrderBatchJobStatusCodes.Completed; } job.FinishedAt = DateTime.Now; } else { job.Status = OrderBatchJobStatusCodes.Running; } await batchJobRepository.UpdateJobAsync(job, cancellationToken); } }