我们在对接微信支付、支付宝支付的时候看到消息回调部分总是能看到支付宝或者微信支持一个时间周期内的用户未成功或者异常后再次发起回调。那么我们怎么去实现这个功能呢目标实现一个具有重试机制的异步通知系统用于向商户后台通知支付结果。设计要点当支付状态发生变化如支付成功时我们将通知信息发送到商户提供的回调地址。如果商户没有返回成功的响应例如没有返回HTTP 200或者返回的内容不是我们规定的成功标识我们需要进行重试。重试策略在24小时22分钟内最多重试8次重试间隔为4m,10m,10m,1h,2h,6h,15h。实现思路我们需要一个任务调度系统来管理这些通知任务并按照重试策略进行调度。需要持久化存储通知任务的状态包括重试次数、下次重试时间、最后一次响应等。由于重试时间间隔较长我们可以使用后台服务如IHostedService来扫描需要重试的任务然后发送通知。怎么做1. 系统架构设计支付通知服务端├── 通知队列管理├── 重试策略引擎├── HTTP客户端├── 状态监控└── 管理接口2. 数据库设计-- 通知任务表 CREATE TABLE PaymentNotifyTasks ( Id BIGINT IDENTITY(1,1) PRIMARY KEY, TaskId UNIQUEIDENTIFIER DEFAULT NEWID() NOT NULL, -- 任务唯一ID Channel NVARCHAR(20) NOT NULL, -- 支付渠道ALIPAY, WECHAT等 OrderNo NVARCHAR(64) NOT NULL, -- 商户订单号 TransactionId NVARCHAR(64), -- 交易流水号 NotifyUrl NVARCHAR(500) NOT NULL, -- 商户回调地址 RequestData NVARCHAR(MAX) NOT NULL, -- 请求数据JSON格式 RequestHeaders NVARCHAR(MAX), -- 请求头 ResponseStatus INT, -- 响应状态码 ResponseBody NVARCHAR(MAX), -- 响应内容 RetryCount INT DEFAULT 0, -- 已重试次数 MaxRetryCount INT DEFAULT 8, -- 最大重试次数 Status TINYINT DEFAULT 0, -- 0-待处理 1-处理中 2-成功 3-失败 NextRetryTime DATETIME2, -- 下次重试时间 ErrorMessage NVARCHAR(1000), -- 错误信息 CompletedTime DATETIME2, -- 完成时间 CreatedAt DATETIME2 DEFAULT GETUTCDATE(), UpdatedAt DATETIME2 DEFAULT GETUTCDATE(), INDEX IX_Status_NextRetryTime (Status, NextRetryTime), INDEX IX_OrderNo_Channel (OrderNo, Channel) ); -- 重试策略配置表 CREATE TABLE NotifyRetryPolicies ( Id INT IDENTITY(1,1) PRIMARY KEY, Name NVARCHAR(50) NOT NULL, -- 策略名称 Channel NVARCHAR(20) NOT NULL, -- 适用渠道 RetryCount INT NOT NULL, -- 重试次数 Intervals NVARCHAR(500) NOT NULL, -- 间隔配置JSON数组 TimeoutSeconds INT DEFAULT 30, -- 请求超时时间 SignatureEnabled BIT DEFAULT 1, -- 是否启用签名 SignatureKey NVARCHAR(100), -- 签名密钥 IsActive BIT DEFAULT 1, CreatedAt DATETIME2 DEFAULT GETUTCDATE() ); -- 插入支付宝重试策略 INSERT INTO NotifyRetryPolicies (Name, Channel, RetryCount, Intervals, TimeoutSeconds) VALUES (Alipay Standard, ALIPAY, 8, [4, 10, 10, 60, 120, 360, 900], 30), (Wechat Standard, WECHAT, 15, [15, 15, 30, 180, 600, 1200, 1800, 1800, 1800, 3600, 10800, 10800, 10800, 21600, 21600], 30);3. 完成核心服务实现// 通知表 public class NotifyTask { public long Id { get; set; } public Guid TaskId { get; set; } public string Channel { get; set; } string.Empty; public string OrderNo { get; set; } string.Empty; public string TransactionId { get; set; } string.Empty; public string NotifyUrl { get; set; } string.Empty; public string RequestData { get; set; } string.Empty; public Dictionarystring, string? Headers { get; set; } public NotifyStatus Status { get; set; } NotifyStatus.Pending; public int RetryCount { get; set; } public int MaxRetryCount { get; set; } 8; public DateTime? NextRetryTime { get; set; } public DateTime CreatedAt { get; set; } public DateTime? CompletedTime { get; set; } } //状态枚举 public enum NotifyStatus { Pending 0, // 待处理 Processing 1, // 处理中 Success 2, // 成功 Failed 3 // 失败 } // 重试设置 public class RetryPolicy { public int Id { get; set; } public string Name { get; set; } string.Empty; public string Channel { get; set; } string.Empty; public int RetryCount { get; set; } public Listint Intervals { get; set; } new(); // 单位分钟 public int TimeoutSeconds { get; set; } 30; public bool SignatureEnabled { get; set; } true; public string? SignatureKey { get; set; } }4. 通知服务核心// Services/INotifyService.cs public interface INotifyService { /// summary /// 创建通知任务 /// /summary TaskGuid CreateNotifyTaskAsync(NotifyTaskRequest request); /// summary /// 手动触发重试 /// /summary Taskbool RetryTaskAsync(Guid taskId); /// summary /// 获取任务状态 /// /summary TaskNotifyTaskStatus GetTaskStatusAsync(Guid taskId); /// summary /// 获取失败任务列表 /// /summary TaskListNotifyTask GetFailedTasksAsync(int page 1, int pageSize 20); } // Services/NotifyService.cs public class NotifyService : INotifyService { private readonly ILoggerNotifyService _logger; private readonly ApplicationDbContext _context; private readonly IHttpClientFactory _httpClientFactory; private readonly ISignatureService _signatureService; private readonly IBackgroundTaskQueue _taskQueue; public NotifyService( ILoggerNotifyService logger, ApplicationDbContext context, IHttpClientFactory httpClientFactory, ISignatureService signatureService, IBackgroundTaskQueue taskQueue) { _logger logger; _context context; _httpClientFactory httpClientFactory; _signatureService signatureService; _taskQueue taskQueue; } /// summary /// 创建通知任务 /// /summary public async TaskGuid CreateNotifyTaskAsync(NotifyTaskRequest request) { // 1. 幂等性检查 var existingTask await _context.NotifyTasks .FirstOrDefaultAsync(t t.Channel request.Channel t.OrderNo request.OrderNo t.Status ! (int)NotifyStatus.Failed); if (existingTask ! null) { _logger.LogWarning($订单 {request.OrderNo} 已存在通知任务: {existingTask.TaskId}); return existingTask.TaskId; } // 2. 获取重试策略 var policy await GetRetryPolicy(request.Channel); // 3. 构建请求数据 var requestData BuildNotifyData(request, policy); // 4. 创建通知任务 var task new NotifyTask { TaskId Guid.NewGuid(), Channel request.Channel, OrderNo request.OrderNo, TransactionId request.TransactionId, NotifyUrl request.NotifyUrl, RequestData requestData, Headers BuildHeaders(policy), Status NotifyStatus.Pending, MaxRetryCount policy.RetryCount, NextRetryTime DateTime.UtcNow, // 立即执行 CreatedAt DateTime.UtcNow }; _context.NotifyTasks.Add(task); await _context.SaveChangesAsync(); // 5. 加入队列处理 await _taskQueue.QueueTaskAsync(async () { await ProcessNotifyTask(task.Id); }); _logger.LogInformation($创建通知任务成功: TaskId{task.TaskId}, OrderNo{request.OrderNo}); return task.TaskId; } /// summary /// 处理通知任务 /// /summary private async Task ProcessNotifyTask(long taskId) { using var transaction await _context.Database.BeginTransactionAsync(); try { // 1. 锁定任务 var task await _context.NotifyTasks .FirstOrDefaultAsync(t t.Id taskId); if (task null || task.Status ! NotifyStatus.Pending) return; // 2. 更新为处理中状态 task.Status NotifyStatus.Processing; task.UpdatedAt DateTime.UtcNow; await _context.SaveChangesAsync(); await transaction.CommitAsync(); // 3. 执行通知 var success await ExecuteNotifyAsync(task); // 4. 更新结果 using var updateTransaction await _context.Database.BeginTransactionAsync(); task.Status success ? NotifyStatus.Success : NotifyStatus.Failed; task.RetryCount; task.UpdatedAt DateTime.UtcNow; if (success) { task.CompletedTime DateTime.UtcNow; } else if (task.RetryCount task.MaxRetryCount) { // 计算下次重试时间 var policy await GetRetryPolicy(task.Channel); var intervalMinutes GetNextRetryInterval(policy, task.RetryCount); task.NextRetryTime DateTime.UtcNow.AddMinutes(intervalMinutes); task.Status NotifyStatus.Pending; // 重置为待处理 // 重新加入队列 await _taskQueue.QueueTaskAsync(async () { await Task.Delay(TimeSpan.FromMinutes(intervalMinutes)); await ProcessNotifyTask(task.Id); }, TimeSpan.FromMinutes(intervalMinutes)); } await _context.SaveChangesAsync(); await updateTransaction.CommitAsync(); } catch (Exception ex) { await transaction.RollbackAsync(); _logger.LogError(ex, $处理通知任务失败: TaskId{taskId}); } } /// summary /// 执行通知请求 /// /summary private async Taskbool ExecuteNotifyAsync(NotifyTask task) { try { var client _httpClientFactory.CreateClient(NotifyClient); client.Timeout TimeSpan.FromSeconds(30); // 添加自定义请求头 var request new HttpRequestMessage(HttpMethod.Post, task.NotifyUrl); if (task.Headers ! null) { foreach (var header in task.Headers) { request.Headers.TryAddWithoutValidation(header.Key, header.Value); } } // 设置请求体 request.Content new StringContent(task.RequestData, Encoding.UTF8, application/json); // 发送请求 var response await client.SendAsync(request); // 记录响应 var responseBody await response.Content.ReadAsStringAsync(); // 更新任务记录 task.ResponseStatus (int)response.StatusCode; task.ResponseBody responseBody; // 判断是否成功 bool isSuccess await ValidateResponseAsync(task, response, responseBody); _logger.LogInformation($通知执行结果: TaskId{task.TaskId}, $Status{response.StatusCode}, Success{isSuccess}, $RetryCount{task.RetryCount 1}); return isSuccess; } catch (TaskCanceledException ex) when (!ex.CancellationToken.IsCancellationRequested) { _logger.LogWarning($通知请求超时: TaskId{task.TaskId}); task.ErrorMessage Request timeout; return false; } catch (Exception ex) { _logger.LogError(ex, $通知请求异常: TaskId{task.TaskId}); task.ErrorMessage ex.Message; return false; } } /// summary /// 验证响应是否成功 /// /summary private async Taskbool ValidateResponseAsync(NotifyTask task, HttpResponseMessage response, string responseBody) { // 1. HTTP状态码必须为200 if (!response.IsSuccessStatusCode) return false; // 2. 根据渠道验证响应内容 return task.Channel.ToUpper() switch { ALIPAY responseBody.Trim() success, // 支付宝要求返回success WECHAT await ValidateWechatResponse(responseBody), // 微信返回XML _ responseBody.Trim().ToLower() success || responseBody.Trim().ToLower() ok }; } /// summary /// 验证微信响应 /// /summary private async Taskbool ValidateWechatResponse(string responseBody) { try { // 解析XML响应 var doc new XmlDocument(); doc.LoadXml(responseBody); var returnCode doc.SelectSingleNode(xml/return_code)?.InnerText; var resultCode doc.SelectSingleNode(xml/result_code)?.InnerText; return returnCode SUCCESS resultCode SUCCESS; } catch { return false; } } /// summary /// 获取下次重试间隔 /// /summary private int GetNextRetryInterval(RetryPolicy policy, int retryCount) { if (policy.Intervals.Count 0) return 5; // 默认5分钟 // 重试次数从0开始所以减1 var index Math.Min(retryCount, policy.Intervals.Count) - 1; if (index 0) index 0; return policy.Intervals[index]; } /// summary /// 获取重试策略 /// /summary private async TaskRetryPolicy GetRetryPolicy(string channel) { var policy await _context.NotifyRetryPolicies .FirstOrDefaultAsync(p p.Channel channel p.IsActive); if (policy null) { // 默认策略 return new RetryPolicy { Channel channel, RetryCount 8, Intervals new Listint { 4, 10, 10, 60, 120, 360, 900 }, TimeoutSeconds 30 }; } return policy; } /// summary /// 构建通知数据 /// /summary private string BuildNotifyData(NotifyTaskRequest request, RetryPolicy policy) { var data new Dictionarystring, object { [channel] request.Channel, [order_no] request.OrderNo, [transaction_id] request.TransactionId, [amount] request.Amount, [pay_time] request.PayTime.ToString(yyyy-MM-dd HH:mm:ss), [status] request.Status, [notify_id] Guid.NewGuid().ToString(N), [timestamp] DateTimeOffset.UtcNow.ToUnixTimeSeconds(), [nonce] Guid.NewGuid().ToString(N).Substring(0, 16) }; // 添加业务数据 if (request.ExtraData ! null) { foreach (var item in request.ExtraData) { data[item.Key] item.Value; } } // 签名 if (policy.SignatureEnabled !string.IsNullOrEmpty(policy.SignatureKey)) { var signature _signatureService.GenerateSignature(data, policy.SignatureKey); data[sign] signature; data[sign_type] MD5; // 或RSA等 } return JsonSerializer.Serialize(data, new JsonSerializerOptions { PropertyNamingPolicy JsonNamingPolicy.CamelCase }); } /// summary /// 构建请求头 /// /summary private Dictionarystring, string BuildHeaders(RetryPolicy policy) { var headers new Dictionarystring, string { [User-Agent] Payment-Notify-Service/1.0, [Accept] application/json, [X-Notify-Source] PaymentGateway }; return headers; } public async Taskbool RetryTaskAsync(Guid taskId) { var task await _context.NotifyTasks .FirstOrDefaultAsync(t t.TaskId taskId); if (task null) return false; if (task.Status NotifyStatus.Success) { _logger.LogWarning($任务 {taskId} 已成功无需重试); return false; } // 重置状态为待处理 task.Status NotifyStatus.Pending; task.NextRetryTime DateTime.UtcNow; task.UpdatedAt DateTime.UtcNow; await _context.SaveChangesAsync(); // 加入队列 await _taskQueue.QueueTaskAsync(async () { await ProcessNotifyTask(task.Id); }); return true; } }5. 后台任务队列服务/// Services/BackgroundTaskQueue.cs public interface IBackgroundTaskQueue { ValueTask QueueTaskAsync(FuncCancellationToken, ValueTask workItem, TimeSpan? delay null); ValueTaskFuncCancellationToken, ValueTask? DequeueAsync(CancellationToken cancellationToken); } public class BackgroundTaskQueue : IBackgroundTaskQueue { private readonly ChannelFuncCancellationToken, ValueTask _queue; private readonly DictionaryFuncCancellationToken, ValueTask, DateTime _scheduledTasks new(); private readonly ILoggerBackgroundTaskQueue _logger; public BackgroundTaskQueue(ILoggerBackgroundTaskQueue logger) { _logger logger; _queue Channel.CreateUnboundedFuncCancellationToken, ValueTask(new UnboundedChannelOptions { SingleWriter false, SingleReader false }); // 启动调度器 _ Task.Run(async () await ScheduleMonitorAsync()); } public async ValueTask QueueTaskAsync(FuncCancellationToken, ValueTask workItem, TimeSpan? delay null) { if (delay.HasValue delay.Value TimeSpan.Zero) { lock (_scheduledTasks) { var scheduledTime DateTime.UtcNow.Add(delay.Value); _scheduledTasks[workItem] scheduledTime; } _logger.LogDebug($任务已调度: 延迟{delay.Value.TotalMinutes}分钟); } else { await _queue.Writer.WriteAsync(workItem); } } public async ValueTaskFuncCancellationToken, ValueTask? DequeueAsync(CancellationToken cancellationToken) { var workItem await _queue.Reader.ReadAsync(cancellationToken); return workItem; } private async Task ScheduleMonitorAsync() { while (true) { try { await Task.Delay(TimeSpan.FromSeconds(10)); var now DateTime.UtcNow; ListFuncCancellationToken, ValueTask readyTasks new(); lock (_scheduledTasks) { foreach (var kvp in _scheduledTasks.ToList()) { if (kvp.Value now) { readyTasks.Add(kvp.Key); _scheduledTasks.Remove(kvp.Key); } } } foreach (var task in readyTasks) { await _queue.Writer.WriteAsync(task); } } catch (Exception ex) { _logger.LogError(ex, 调度监控器异常); } } } }6. 后台工作服务// Services/NotifyBackgroundService.cs public class NotifyBackgroundService : BackgroundService { private readonly ILoggerNotifyBackgroundService _logger; private readonly IServiceProvider _serviceProvider; private readonly IBackgroundTaskQueue _taskQueue; private readonly int _workerCount Environment.ProcessorCount; public NotifyBackgroundService( ILoggerNotifyBackgroundService logger, IServiceProvider serviceProvider, IBackgroundTaskQueue taskQueue) { _logger logger; _serviceProvider serviceProvider; _taskQueue taskQueue; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation(通知后台服务启动工作线程数: {WorkerCount}, _workerCount); var workers new ListTask(); for (int i 0; i _workerCount; i) { workers.Add(ProcessTasksAsync(stoppingToken)); } await Task.WhenAll(workers); } private async Task ProcessTasksAsync(CancellationToken cancellationToken) { while (!cancellationToken.IsCancellationRequested) { try { var workItem await _taskQueue.DequeueAsync(cancellationToken); if (workItem ! null) { await workItem(cancellationToken); } } catch (OperationCanceledException) { // 正常退出 break; } catch (Exception ex) { _logger.LogError(ex, 处理后台任务时发生异常); await Task.Delay(1000, cancellationToken); } } } }7. API控制器[ApiController] [Route(api/[controller])] public class NotifyController : ControllerBase { private readonly INotifyService _notifyService; private readonly ILoggerNotifyController _logger; public NotifyController(INotifyService notifyService, ILoggerNotifyController logger) { _notifyService notifyService; _logger logger; } /// summary /// 创建支付通知 /// /summary [HttpPost(create)] public async TaskIActionResult CreateNotify([FromBody] NotifyTaskRequest request) { try { if (string.IsNullOrEmpty(request.OrderNo) || string.IsNullOrEmpty(request.NotifyUrl) || request.Amount 0) { return BadRequest(new { Code 400, Message 参数无效 }); } var taskId await _notifyService.CreateNotifyTaskAsync(request); return Ok(new { Code 0, Message 成功, Data new { TaskId taskId, OrderNo request.OrderNo } }); } catch (Exception ex) { _logger.LogError(ex, 创建通知失败); return StatusCode(500, new { Code 500, Message 系统错误 }); } } /// summary /// 查询通知状态 /// /summary [HttpGet(status/{taskId})] public async TaskIActionResult GetStatus(Guid taskId) { try { var status await _notifyService.GetTaskStatusAsync(taskId); if (status null) return NotFound(new { Code 404, Message 任务不存在 }); return Ok(new { Code 0, Message 成功, Data status }); } catch (Exception ex) { _logger.LogError(ex, 查询通知状态失败); return StatusCode(500, new { Code 500, Message 系统错误 }); } } /// summary /// 手动重试 /// /summary [HttpPost(retry/{taskId})] public async TaskIActionResult Retry(Guid taskId) { try { var result await _notifyService.RetryTaskAsync(taskId); if (!result) return BadRequest(new { Code 400, Message 重试失败 }); return Ok(new { Code 0, Message 重试请求已接受 }); } catch (Exception ex) { _logger.LogError(ex, 手动重试失败); return StatusCode(500, new { Code 500, Message 系统错误 }); } } /// summary /// 获取失败任务列表 /// /summary [HttpGet(failed)] public async TaskIActionResult GetFailedTasks( [FromQuery] int page 1, [FromQuery] int pageSize 20) { try { var tasks await _notifyService.GetFailedTasksAsync(page, pageSize); return Ok(new { Code 0, Message 成功, Data tasks }); } catch (Exception ex) { _logger.LogError(ex, 获取失败任务列表失败); return StatusCode(500, new { Code 500, Message 系统错误 }); } } } // DTOs public class NotifyTaskRequest { [Required] public string Channel { get; set; } string.Empty; // ALIPAY, WECHAT [Required] public string OrderNo { get; set; } string.Empty; public string? TransactionId { get; set; } [Required] [Url] public string NotifyUrl { get; set; } string.Empty; [Required] [Range(0.01, 100000000)] public decimal Amount { get; set; } [Required] public DateTime PayTime { get; set; } [Required] public string Status { get; set; } SUCCESS; // SUCCESS, FAILED, REFUND public Dictionarystring, object? ExtraData { get; set; } }8. 配置和启动// Program.cs var builder WebApplication.CreateBuilder(args); // 配置服务 builder.Services.AddDbContextApplicationDbContext(options options.UseSqlServer(builder.Configuration.GetConnectionString(DefaultConnection))); builder.Services.AddHttpClient(NotifyClient) .ConfigurePrimaryHttpMessageHandler(() new HttpClientHandler { ServerCertificateCustomValidationCallback (message, cert, chain, errors) true }); // 注册服务 builder.Services.AddScopedINotifyService, NotifyService(); builder.Services.AddScopedISignatureService, SignatureService(); builder.Services.AddSingletonIBackgroundTaskQueue, BackgroundTaskQueue(); builder.Services.AddHostedServiceNotifyBackgroundService(); // 配置定时清理任务 builder.Services.AddCronJobCleanupJob(0 2 * * *); // 每天凌晨2点清理 builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app builder.Build(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run(); // CronJob扩展 public static class ScheduledServiceExtensions { public static IServiceCollection AddCronJobT(this IServiceCollection services, string cronExpression) where T : class, ICronJob { services.AddHostedServiceCronServiceT(); services.AddScopedT(); var options new CronJobOptions(typeof(T), cronExpression); services.AddSingleton(options); return services; } } public class CleanupJob : ICronJob { private readonly ILoggerCleanupJob _logger; private readonly IServiceProvider _serviceProvider; public CleanupJob(ILoggerCleanupJob logger, IServiceProvider serviceProvider) { _logger logger; _serviceProvider serviceProvider; } public async Task ExecuteAsync(CancellationToken cancellationToken) { using var scope _serviceProvider.CreateScope(); var context scope.ServiceProvider.GetRequiredServiceApplicationDbContext(); // 清理30天前的成功记录 var cutoffDate DateTime.UtcNow.AddDays(-30); var deleted await context.NotifyTasks .Where(t t.Status (int)NotifyStatus.Success t.CreatedAt cutoffDate) .ExecuteDeleteAsync(cancellationToken); _logger.LogInformation($清理了 {deleted} 条历史通知记录); } }9. 使用示例// 创建通知任务 var request new NotifyTaskRequest { Channel ALIPAY, OrderNo ORDER20250101123456, TransactionId 202501011234567890, NotifyUrl https://merchant.com/api/payment/notify, Amount 100.00m, PayTime DateTime.UtcNow, Status SUCCESS, ExtraData new Dictionarystring, object { [product_name] 测试商品, [buyer_id] user123 } }; var response await httpClient.PostAsJsonAsync(https://your-service.com/api/notify/create, request);10. 监控和管理接口[ApiController] [Route(api/admin/[controller])] [Authorize(Roles Admin)] public class NotifyAdminController : ControllerBase { private readonly ApplicationDbContext _context; [HttpGet(statistics)] public async TaskIActionResult GetStatistics( [FromQuery] string? channel, [FromQuery] DateTime? startDate, [FromQuery] DateTime? endDate) { var query _context.NotifyTasks.AsQueryable(); if (!string.IsNullOrEmpty(channel)) query query.Where(t t.Channel channel); if (startDate.HasValue) query query.Where(t t.CreatedAt startDate.Value); if (endDate.HasValue) query query.Where(t t.CreatedAt endDate.Value); var total await query.CountAsync(); var success await query.CountAsync(t t.Status (int)NotifyStatus.Success); var failed await query.CountAsync(t t.Status (int)NotifyStatus.Failed); var pending await query.CountAsync(t t.Status (int)NotifyStatus.Pending); return Ok(new { Total total, Success success, Failed failed, Pending pending, SuccessRate total 0 ? (double)success / total * 100 : 0 }); } [HttpPost(bulk-retry)] public async TaskIActionResult BulkRetry([FromBody] BulkRetryRequest request) { var tasks await _context.NotifyTasks .Where(t request.TaskIds.Contains(t.TaskId)) .ToListAsync(); // 批量重置状态 foreach (var task in tasks) { task.Status (int)NotifyStatus.Pending; task.NextRetryTime DateTime.UtcNow; } await _context.SaveChangesAsync(); return Ok(new { Message $已重置 {tasks.Count} 个任务 }); } }大概就这些不善言辞。