业务场景:电商订单超时自动取消系统
假设我们需要实现一个电商系统,当客户下单后30分钟未支付,系统自动取消订单并释放库存。
1. 项目结构和依赖安装
首先安装必要的NuGet包:
<PackageReference Include="Quartz" Version="3.8.1" />
<PackageReference Include="Quartz.Extensions.Hosting" Version="3.8.1" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="7.0.1" />
2. 领域模型和数据库上下文
// 订单实体
public class Order
{
public int Id { get; set; }
public string OrderNo { get; set; }
public decimal Amount { get; set; }
public OrderStatus Status { get; set; } = OrderStatus.PendingPayment;
public DateTime CreateTime { get; set; } = DateTime.Now;
public DateTime? PaymentTime { get; set; }
public DateTime? CancelTime { get; set; }
}
public enum OrderStatus
{
PendingPayment = 0, // 待支付
Paid = 1, // 已支付
Cancelled = 2 // 已取消
}
// DbContext
public class AppDbContext : DbContext
{
public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }
public DbSet<Order> Orders { get; set; }
}
3. 定义Quartz作业
using Quartz;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
// 订单超时取消作业
[DisallowConcurrentExecution] // 防止并发执行
public class OrderTimeoutCancelJob : IJob
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<OrderTimeoutCancelJob> _logger;
public OrderTimeoutCancelJob(IServiceProvider serviceProvider, ILogger<OrderTimeoutCancelJob> logger)
{
_serviceProvider = serviceProvider;
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
_logger.LogInformation($"订单超时取消作业开始执行,时间:{DateTime.Now}");
using var scope = _serviceProvider.CreateScope();
var dbContext = scope.ServiceProvider.GetRequiredService<AppDbContext>();
try
{
// 查找创建时间超过30分钟且未支付的订单
var timeoutOrders = await dbContext.Orders
.Where(o => o.Status == OrderStatus.PendingPayment &&
o.CreateTime <= DateTime.Now.AddMinutes(-30) &&
!o.CancelTime.HasValue)
.ToListAsync();
if (!timeoutOrders.Any())
{
_logger.LogInformation("没有找到需要取消的超时订单");
return;
}
// 批量取消订单
foreach (var order in timeoutOrders)
{
order.Status = OrderStatus.Cancelled;
order.CancelTime = DateTime.Now;
_logger.LogWarning($"订单 {order.OrderNo} 因超时未支付已被自动取消");
// 这里可以添加释放库存的逻辑
await ReleaseInventory(order.Id);
}
await dbContext.SaveChangesAsync();
_logger.LogInformation($"成功取消 {timeoutOrders.Count} 个超时订单");
}
catch (Exception ex)
{
_logger.LogError(ex, "订单超时取消作业执行失败");
throw; // 抛出异常让Quartz记录失败状态
}
}
// 释放库存方法(示例)
private async Task ReleaseInventory(int orderId)
{
// 实际项目中这里会调用库存服务
await Task.Delay(10); // 模拟异步操作
_logger.LogInformation($"订单 {orderId} 的库存已释放");
}
}
4. 动态创建单次触发器作业(为每个新订单)
// 动态订单超时监控作业
public class OrderMonitorJob : IJob
{
private readonly ILogger<OrderMonitorJob> _logger;
public OrderMonitorJob(ILogger<OrderMonitorJob> logger)
{
_logger = logger;
}
public async Task Execute(IJobExecutionContext context)
{
var orderId = context.JobDetail.JobDataMap.GetInt("OrderId");
var orderNo = context.JobDetail.JobDataMap.GetString("OrderNo");
_logger.LogInformation($"监控订单 {orderNo} (ID:{orderId}) 的支付状态");
// 这里可以添加检查订单是否已支付的业务逻辑
// 如果已支付,可以删除这个监控作业
await Task.Delay(100);
}
}
5. 调度服务和管理器
using Quartz;
using Quartz.Impl;
using Quartz.Spi;
// 自定义Job工厂(用于依赖注入)
public class IoCJobFactory : IJobFactory
{
private readonly IServiceProvider _serviceProvider;
public IoCJobFactory(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public IJob NewJob(TriggerFiredBundle bundle, IScheduler scheduler)
{
var jobDetail = bundle.JobDetail;
var jobType = jobDetail.JobType;
// 使用ActivatorUtilities创建作业实例,支持依赖注入
return (IJob)ActivatorUtilities.CreateInstance(_serviceProvider, jobType);
}
public void ReturnJob(IJob job)
{
// 如果需要清理资源可以在这里处理
if (job is IDisposable disposable)
{
disposable.Dispose();
}
}
}
// 调度服务
public interface ISchedulerService
{
Task InitializeAsync();
Task ScheduleOrderMonitoring(int orderId, string orderNo);
Task CancelOrderMonitoring(int orderId);
Task TriggerOrderTimeoutCheckNow();
}
public class SchedulerService : ISchedulerService
{
private readonly IScheduler _scheduler;
private readonly IConfiguration _configuration;
private readonly ILogger<SchedulerService> _logger;
public SchedulerService(
IScheduler scheduler,
IConfiguration configuration,
ILogger<SchedulerService> logger)
{
_scheduler = scheduler;
_configuration = configuration;
_logger = logger;
}
public async Task InitializeAsync()
{
try
{
// 配置调度器
_scheduler.JobFactory = _scheduler.SchedulerServices?.JobFactory as IoCJobFactory;
// 启动调度器
await _scheduler.Start();
_logger.LogInformation("Quartz调度器启动成功");
// 创建定时全量检查任务(每分钟执行一次作为备用)
await CreateRecurringOrderCheckJob();
}
catch (Exception ex)
{
_logger.LogError(ex, "初始化调度器失败");
throw;
}
}
private async Task CreateRecurringOrderCheckJob()
{
// 创建重复执行的触发器(每分钟检查一次所有超时订单)
var trigger = TriggerBuilder.Create()
.WithIdentity("OrderTimeoutCheckTrigger", "OrderGroup")
.WithCronSchedule("0 * * * * ?") // 每分钟第0秒执行
.Build();
var jobDetail = JobBuilder.Create<OrderTimeoutCancelJob>()
.WithIdentity("OrderTimeoutCheckJob", "OrderGroup")
.WithDescription("定时检查并取消超时订单")
.Build();
await _scheduler.ScheduleJob(jobDetail, trigger);
_logger.LogInformation("定时订单检查任务创建成功");
}
public async Task ScheduleOrderMonitoring(int orderId, string orderNo)
{
try
{
// 计算精确的超时时间点(30分钟后)
var timeoutTime = DateTimeOffset.Now.AddMinutes(30);
// 创建一次性触发器
var trigger = TriggerBuilder.Create()
.WithIdentity($"OrderMonitor_{orderId}", "OrderMonitorGroup")
.StartAt(timeoutTime) // 在指定时间触发一次
.WithSimpleSchedule(x => x.WithMisfireHandlingInstructionFireNow()) // 错过执行立即触发
.Build();
var jobDetail = JobBuilder.Create<OrderMonitorJob>()
.WithIdentity($"OrderMonitorJob_{orderId}", "OrderMonitorGroup")
.UsingJobData("OrderId", orderId)
.UsingJobData("OrderNo", orderNo)
.Build();
await _scheduler.ScheduleJob(jobDetail, trigger);
_logger.LogInformation($"订单 {orderNo} 的监控任务已安排,将在 {timeoutTime} 执行");
}
catch (Exception ex)
{
_logger.LogError(ex, $"安排订单 {orderNo} 监控任务失败");
throw;
}
}
public async Task CancelOrderMonitoring(int orderId)
{
try
{
var jobKey = new JobKey($"OrderMonitorJob_{orderId}", "OrderMonitorGroup");
var triggerKey = new TriggerKey($"OrderMonitor_{orderId}", "OrderMonitorGroup");
// 删除特定的监控任务
await _scheduler.UnscheduleJob(triggerKey);
await _scheduler.DeleteJob(jobKey);
_logger.LogInformation($"订单 {orderId} 的监控任务已取消");
}
catch (Exception ex)
{
_logger.LogError(ex, $"取消订单 {orderId} 监控任务失败");
}
}
// 手动触发立即检查(用于测试或紧急处理)
public async Task TriggerOrderTimeoutCheckNow()
{
var jobKey = new JobKey("OrderTimeoutCheckJob", "OrderGroup");
await _scheduler.TriggerJob(jobKey);
_logger.LogInformation("手动触发订单超时检查");
}
}
6. 依赖注入配置
// Program.cs 或 Startup.cs
public static class ServiceCollectionExtensions
{
public static IServiceCollection AddQuartzServices(this IServiceCollection services, IConfiguration configuration)
{
// 注册DbContext
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(configuration.GetConnectionString("DefaultConnection")));
// 注册作业工厂
services.AddSingleton<IJobFactory, IoCJobFactory>();
// 注册调度器
services.AddSingleton<ISchedulerFactory>(provider =>
{
var factory = new StdSchedulerFactory();
return factory;
});
services.AddSingleton(provider =>
{
var factory = provider.GetRequiredService<ISchedulerFactory>();
var scheduler = factory.GetScheduler().Result;
scheduler.JobFactory = provider.GetRequiredService<IJobFactory>();
return scheduler;
});
// 注册调度服务
services.AddScoped<ISchedulerService, SchedulerService>();
// 注册作业(必须注册为Scoped或Transient)
services.AddScoped<OrderTimeoutCancelJob>();
services.AddScoped<OrderMonitorJob>();
// 配置Quartz hosted service(可选,用于后台服务)
services.AddQuartz(q =>
{
q.UseMicrosoftDependencyInjectionJobFactory();
});
services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true);
return services;
}
}
7. 使用示例
// 在订单服务中使用
public class OrderService : IOrderService
{
private readonly AppDbContext _dbContext;
private readonly ISchedulerService _schedulerService;
private readonly ILogger<OrderService> _logger;
public OrderService(AppDbContext dbContext, ISchedulerService schedulerService, ILogger<OrderService> logger)
{
_dbContext = dbContext;
_schedulerService = schedulerService;
_logger = logger;
}
public async Task<Order> CreateOrderAsync(CreateOrderRequest request)
{
// 创建订单
var order = new Order
{
OrderNo = GenerateOrderNo(),
Amount = request.Amount
};
_dbContext.Orders.Add(order);
await _dbContext.SaveChangesAsync();
// 安排订单超时监控
await _schedulerService.ScheduleOrderMonitoring(order.Id, order.OrderNo);
_logger.LogInformation($"订单 {order.OrderNo} 创建成功,已安排超时监控");
return order;
}
public async Task<bool> PayOrderAsync(string orderNo)
{
var order = await _dbContext.Orders.FirstOrDefaultAsync(o => o.OrderNo == orderNo);
if (order == null || order.Status != OrderStatus.PendingPayment)
return false;
// 更新订单状态
order.Status = OrderStatus.Paid;
order.PaymentTime = DateTime.Now;
// 取消超时监控(因为已经支付了)
await _schedulerService.CancelOrderMonitoring(order.Id);
await _dbContext.SaveChangesAsync();
_logger.LogInformation($"订单 {orderNo} 支付成功,已取消超时监控");
return true;
}
private string GenerateOrderNo()
{
return $"ORD{DateTime.Now:yyyyMMddHHmmss}{Random.Shared.Next(1000, 9999)}";
}
}
执行过程详解
场景1:新订单创建
-
用户下单 →
CreateOrderAsync被调用 -
订单保存到数据库,状态为"待支付"
-
调用
_schedulerService.ScheduleOrderMonitoring() -
Quartz创建一个一次性触发器,设置在30分钟后执行
-
同时,每分钟执行的全局检查任务也在运行
场景2:正常支付
-
用户在30分钟内支付 →
PayOrderAsync被调用 -
订单状态更新为"已支付"
-
调用
_schedulerService.CancelOrderMonitoring()删除对应的监控任务 -
订单不会被超时取消
场景3:超时未支付
-
30分钟后,对应的监控任务触发(或每分钟的全局检查任务发现)
-
OrderTimeoutCancelJob.Execute()方法执行 -
查询所有超时未支付的订单
-
批量更新订单状态为"已取消"
-
记录日志,释放库存
优势说明
-
双重保障:既有针对每个订单的精确监控,又有全局定期检查作为备用
-
资源高效:支付成功的订单会及时取消监控,避免无效检查
-
可扩展:容易添加新的作业类型和调度策略
-
容错性强:支持错过执行策略,确保重要业务不丢失
这个例子展示了Quartz.NET在实际业务中的完整应用,包括动态任务创建、依赖注入、错误处理等关键特性。