feat: 添加订单超时自动取消与 SMS 告警任务
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 1m50s
All checks were successful
Build and Deploy TenantApi + SkuWorker / build-and-deploy (push) Successful in 1m50s
- OrderTimeoutJob 替换占位实现为真实逻辑(查询超时订单 → 批量取消 → 发布事件) - 新增 OrderEscalationJob(每 2 分钟检查超时未接单订单,预留 SMS 告警) - 新增 OrderTimeoutOptions(AutoCancelMinutes=15, SmsEscalationMinutes=10) - RecurringJobRegistrar 注册 OrderEscalationJob - SchedulerServiceCollectionExtensions 注册 Job + Options
This commit is contained in:
@@ -37,6 +37,11 @@ public static class SchedulerServiceCollectionExtensions
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddOptions<OrderTimeoutOptions>()
|
||||
.Bind(configuration.GetSection("Scheduler:OrderTimeout"))
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddHangfire((serviceProvider, config) =>
|
||||
{
|
||||
var options = serviceProvider.GetRequiredService<IOptionsMonitor<SchedulerOptions>>().CurrentValue;
|
||||
@@ -59,6 +64,7 @@ public static class SchedulerServiceCollectionExtensions
|
||||
services.AddHostedService<RecurringJobHostedService>();
|
||||
|
||||
services.AddScoped<OrderTimeoutJob>();
|
||||
services.AddScoped<OrderEscalationJob>();
|
||||
services.AddScoped<CouponExpireJob>();
|
||||
services.AddScoped<LogCleanupJob>();
|
||||
services.AddScoped<SubscriptionRenewalReminderJob>();
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Domain.Orders.Enums;
|
||||
using TakeoutSaaS.Domain.Orders.Repositories;
|
||||
using TakeoutSaaS.Module.Scheduler.Options;
|
||||
|
||||
namespace TakeoutSaaS.Module.Scheduler.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// 订单超时 SMS 告警任务。
|
||||
/// </summary>
|
||||
public sealed class OrderEscalationJob(
|
||||
IOrderRepository orderRepository,
|
||||
IOptionsMonitor<OrderTimeoutOptions> options,
|
||||
ILogger<OrderEscalationJob> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行超时告警检查。
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
var config = options.CurrentValue;
|
||||
var cutoff = DateTime.UtcNow.AddMinutes(-config.SmsEscalationMinutes);
|
||||
|
||||
// 1. 查询超时待接单订单
|
||||
var pendingOrders = await orderRepository.SearchAsync(
|
||||
tenantId: 0,
|
||||
status: OrderStatus.AwaitingPreparation,
|
||||
paymentStatus: null);
|
||||
|
||||
var escalationOrders = pendingOrders
|
||||
.Where(o => o.CreatedAt < cutoff)
|
||||
.ToList();
|
||||
|
||||
if (escalationOrders.Count == 0)
|
||||
{
|
||||
logger.LogDebug("无需告警的超时订单");
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 逐笔发送告警(SMS 模块后续接入)
|
||||
foreach (var order in escalationOrders)
|
||||
{
|
||||
// TODO: 接入 ISmsSenderResolver 发送短信给店长
|
||||
// TODO: 使用 Redis key order:escalation:{orderId} 防重复发送
|
||||
logger.LogWarning(
|
||||
"订单 {OrderNo} ({OrderId}) 超过 {Minutes} 分钟未接单,需告警",
|
||||
order.OrderNo, order.Id, config.SmsEscalationMinutes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,92 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Application.Messaging;
|
||||
using TakeoutSaaS.Application.Messaging.Abstractions;
|
||||
using TakeoutSaaS.Application.Messaging.Events;
|
||||
using TakeoutSaaS.Domain.Orders.Entities;
|
||||
using TakeoutSaaS.Domain.Orders.Enums;
|
||||
using TakeoutSaaS.Domain.Orders.Repositories;
|
||||
using TakeoutSaaS.Module.Scheduler.Options;
|
||||
using TakeoutSaaS.Shared.Abstractions.Ids;
|
||||
|
||||
namespace TakeoutSaaS.Module.Scheduler.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// 订单超时取消任务(占位,后续接入订单服务)。
|
||||
/// 订单超时自动取消任务。
|
||||
/// </summary>
|
||||
public sealed class OrderTimeoutJob(ILogger<OrderTimeoutJob> logger)
|
||||
public sealed class OrderTimeoutJob(
|
||||
IOrderRepository orderRepository,
|
||||
IEventPublisher eventPublisher,
|
||||
IIdGenerator idGenerator,
|
||||
IOptionsMonitor<OrderTimeoutOptions> options,
|
||||
ILogger<OrderTimeoutJob> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行超时订单检查。
|
||||
/// 执行超时订单检查与自动取消。
|
||||
/// </summary>
|
||||
public Task ExecuteAsync()
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
logger.LogInformation("定时任务:检查超时未支付订单并取消(占位实现)");
|
||||
return Task.CompletedTask;
|
||||
var config = options.CurrentValue;
|
||||
var cutoff = DateTime.UtcNow.AddMinutes(-config.AutoCancelMinutes);
|
||||
|
||||
// 1. 查询所有超时待接单订单
|
||||
var pendingOrders = await orderRepository.SearchAsync(
|
||||
tenantId: 0, // 跨租户查询,由仓储层处理
|
||||
status: OrderStatus.AwaitingPreparation,
|
||||
paymentStatus: null);
|
||||
|
||||
var timedOutOrders = pendingOrders
|
||||
.Where(o => o.CreatedAt < cutoff)
|
||||
.ToList();
|
||||
|
||||
if (timedOutOrders.Count == 0)
|
||||
{
|
||||
logger.LogDebug("无超时待接单订单");
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogInformation("发现 {Count} 笔超时待接单订单,开始自动取消", timedOutOrders.Count);
|
||||
|
||||
// 2. 逐笔取消
|
||||
foreach (var order in timedOutOrders)
|
||||
{
|
||||
var oldStatus = order.Status;
|
||||
order.Status = OrderStatus.Cancelled;
|
||||
order.CancelledAt = DateTime.UtcNow;
|
||||
order.CancelReason = "超时未接单,系统自动取消";
|
||||
|
||||
// 3. 写入状态流转记录
|
||||
var history = new OrderStatusHistory
|
||||
{
|
||||
Id = idGenerator.NextId(),
|
||||
OrderId = order.Id,
|
||||
TenantId = order.TenantId,
|
||||
Status = OrderStatus.Cancelled,
|
||||
Notes = "超时未接单,系统自动取消",
|
||||
OccurredAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await orderRepository.UpdateOrderAsync(order);
|
||||
await orderRepository.AddStatusHistoryAsync(history);
|
||||
await orderRepository.SaveChangesAsync();
|
||||
|
||||
// 4. 发布状态变更事件
|
||||
await eventPublisher.PublishAsync(EventRoutingKeys.OrderStatusChanged, new OrderStatusChangedEvent
|
||||
{
|
||||
OrderId = order.Id,
|
||||
OrderNo = order.OrderNo,
|
||||
TenantId = order.TenantId,
|
||||
StoreId = order.StoreId,
|
||||
OldStatus = (int)oldStatus,
|
||||
NewStatus = (int)OrderStatus.Cancelled,
|
||||
Channel = (int)order.Channel,
|
||||
DeliveryType = (int)order.DeliveryType,
|
||||
CustomerName = order.CustomerName,
|
||||
PaidAmount = order.PaidAmount,
|
||||
OccurredAt = DateTime.UtcNow
|
||||
});
|
||||
|
||||
logger.LogInformation("自动取消超时订单 {OrderNo} ({OrderId})", order.OrderNo, order.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace TakeoutSaaS.Module.Scheduler.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 订单超时配置。
|
||||
/// </summary>
|
||||
public sealed class OrderTimeoutOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动取消超时分钟数(默认 15 分钟)。
|
||||
/// </summary>
|
||||
public int AutoCancelMinutes { get; init; } = 15;
|
||||
|
||||
/// <summary>
|
||||
/// SMS 告警超时分钟数(默认 10 分钟)。
|
||||
/// </summary>
|
||||
public int SmsEscalationMinutes { get; init; } = 10;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ public sealed class RecurringJobRegistrar(
|
||||
{
|
||||
// 1. 业务占位任务(示例)
|
||||
RecurringJob.AddOrUpdate<OrderTimeoutJob>("orders.timeout-cancel", job => job.ExecuteAsync(), "*/5 * * * *");
|
||||
RecurringJob.AddOrUpdate<OrderEscalationJob>("orders.escalation-sms", job => job.ExecuteAsync(), "*/2 * * * *");
|
||||
RecurringJob.AddOrUpdate<CouponExpireJob>("coupons.expire", job => job.ExecuteAsync(), "0 */1 * * *");
|
||||
RecurringJob.AddOrUpdate<LogCleanupJob>("logs.cleanup", job => job.ExecuteAsync(), "0 3 * * *");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user