feat: 实现订阅自动化定时任务系统
新增定时任务 (Scheduler Module): - SubscriptionAutoRenewalJob: 自动续费账单生成 - SubscriptionRenewalReminderJob: 续费提醒发送 (7/3/1天) - SubscriptionExpiryCheckJob: 到期检查与宽限期处理 新增 Command/Handler: - ProcessAutoRenewalCommand: 处理自动续费逻辑 - ProcessRenewalRemindersCommand: 处理续费提醒逻辑 - ProcessSubscriptionExpiryCommand: 处理订阅到期逻辑 配置项 (SubscriptionAutomationOptions): - AutoRenewalDaysBeforeExpiry: 到期前N天生成续费账单 - ReminderDaysBeforeExpiry: 提醒天数数组 - GracePeriodDays: 宽限期天数 - 各任务执行小时配置 Repository 增强: - ISubscriptionRepository: 新增自动化查询方法 - ITenantBillingRepository: 新增账单创建方法 - ITenantNotificationRepository: 新增通知创建方法 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,11 @@ public static class SchedulerServiceCollectionExtensions
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddOptions<SubscriptionAutomationOptions>()
|
||||
.Bind(configuration.GetSection("Scheduler:SubscriptionAutomation"))
|
||||
.ValidateDataAnnotations()
|
||||
.ValidateOnStart();
|
||||
|
||||
services.AddHangfire((serviceProvider, config) =>
|
||||
{
|
||||
var options = serviceProvider.GetRequiredService<IOptionsMonitor<SchedulerOptions>>().CurrentValue;
|
||||
@@ -51,6 +56,9 @@ public static class SchedulerServiceCollectionExtensions
|
||||
services.AddScoped<OrderTimeoutJob>();
|
||||
services.AddScoped<CouponExpireJob>();
|
||||
services.AddScoped<LogCleanupJob>();
|
||||
services.AddScoped<SubscriptionRenewalReminderJob>();
|
||||
services.AddScoped<SubscriptionExpiryCheckJob>();
|
||||
services.AddScoped<SubscriptionAutoRenewalJob>();
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Application.App.Subscriptions.Commands;
|
||||
using TakeoutSaaS.Module.Scheduler.Options;
|
||||
|
||||
namespace TakeoutSaaS.Module.Scheduler.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅自动续费任务:为即将到期且开启自动续费的订阅生成续费账单。
|
||||
/// </summary>
|
||||
public sealed class SubscriptionAutoRenewalJob(
|
||||
IMediator mediator,
|
||||
IOptionsMonitor<SubscriptionAutomationOptions> optionsMonitor,
|
||||
ILogger<SubscriptionAutoRenewalJob> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行自动续费账单生成。
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
// 1. 读取配置并执行自动续费
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
var result = await mediator.Send(new ProcessAutoRenewalCommand
|
||||
{
|
||||
RenewalDaysBeforeExpiry = options.AutoRenewalDaysBeforeExpiry
|
||||
});
|
||||
|
||||
// 2. 记录执行结果
|
||||
logger.LogInformation(
|
||||
"定时任务:自动续费处理完成,候选 {CandidateCount},创建账单 {CreatedBillCount}",
|
||||
result.CandidateCount,
|
||||
result.CreatedBillCount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Application.App.Subscriptions.Commands;
|
||||
using TakeoutSaaS.Module.Scheduler.Options;
|
||||
|
||||
namespace TakeoutSaaS.Module.Scheduler.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅到期检查任务:到期进入宽限期,宽限期到期自动暂停。
|
||||
/// </summary>
|
||||
public sealed class SubscriptionExpiryCheckJob(
|
||||
IMediator mediator,
|
||||
IOptionsMonitor<SubscriptionAutomationOptions> optionsMonitor,
|
||||
ILogger<SubscriptionExpiryCheckJob> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行订阅到期检查。
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
// 1. 读取配置并执行到期处理
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
var result = await mediator.Send(new ProcessSubscriptionExpiryCommand
|
||||
{
|
||||
GracePeriodDays = options.GracePeriodDays
|
||||
});
|
||||
|
||||
// 2. 记录执行结果
|
||||
logger.LogInformation(
|
||||
"定时任务:订阅到期检查完成,进入宽限期 {EnteredGracePeriodCount},暂停 {SuspendedCount}",
|
||||
result.EnteredGracePeriodCount,
|
||||
result.SuspendedCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using MediatR;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Application.App.Subscriptions.Commands;
|
||||
using TakeoutSaaS.Module.Scheduler.Options;
|
||||
|
||||
namespace TakeoutSaaS.Module.Scheduler.Jobs;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅续费提醒任务:到期前 7/3/1 天发送站内提醒。
|
||||
/// </summary>
|
||||
public sealed class SubscriptionRenewalReminderJob(
|
||||
IMediator mediator,
|
||||
IOptionsMonitor<SubscriptionAutomationOptions> optionsMonitor,
|
||||
ILogger<SubscriptionRenewalReminderJob> logger)
|
||||
{
|
||||
/// <summary>
|
||||
/// 执行续费提醒扫描与发送。
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync()
|
||||
{
|
||||
// 1. 读取配置并执行续费提醒
|
||||
var options = optionsMonitor.CurrentValue;
|
||||
var result = await mediator.Send(new ProcessRenewalRemindersCommand
|
||||
{
|
||||
ReminderDaysBeforeExpiry = options.ReminderDaysBeforeExpiry
|
||||
});
|
||||
|
||||
// 2. 记录执行结果
|
||||
logger.LogInformation(
|
||||
"定时任务:续费提醒处理完成,候选 {CandidateCount},创建 {CreatedReminderCount}",
|
||||
result.CandidateCount,
|
||||
result.CreatedReminderCount);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace TakeoutSaaS.Module.Scheduler.Options;
|
||||
|
||||
/// <summary>
|
||||
/// 订阅自动化相关配置(续费提醒、自动续费、宽限期处理)。
|
||||
/// </summary>
|
||||
public sealed class SubscriptionAutomationOptions
|
||||
{
|
||||
/// <summary>
|
||||
/// 自动续费任务执行小时(UTC)。
|
||||
/// </summary>
|
||||
[Range(0, 23)]
|
||||
public int AutoRenewalExecuteHourUtc { get; set; } = 1;
|
||||
|
||||
/// <summary>
|
||||
/// 自动续费:到期前 N 天生成续费账单。
|
||||
/// </summary>
|
||||
[Range(0, 365)]
|
||||
public int AutoRenewalDaysBeforeExpiry { get; set; } = 3;
|
||||
|
||||
/// <summary>
|
||||
/// 续费提醒任务执行小时(UTC)。
|
||||
/// </summary>
|
||||
[Range(0, 23)]
|
||||
public int RenewalReminderExecuteHourUtc { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// 续费提醒:到期前 N 天发送提醒。
|
||||
/// </summary>
|
||||
[MinLength(1)]
|
||||
public int[] ReminderDaysBeforeExpiry { get; set; } = [7, 3, 1];
|
||||
|
||||
/// <summary>
|
||||
/// 订阅到期检查任务执行小时(UTC)。
|
||||
/// </summary>
|
||||
[Range(0, 23)]
|
||||
public int SubscriptionExpiryCheckExecuteHourUtc { get; set; } = 2;
|
||||
|
||||
/// <summary>
|
||||
/// 宽限期天数。
|
||||
/// </summary>
|
||||
[Range(0, 365)]
|
||||
public int GracePeriodDays { get; set; } = 7;
|
||||
}
|
||||
|
||||
@@ -1,20 +1,40 @@
|
||||
using Hangfire;
|
||||
using Hangfire;
|
||||
using Microsoft.Extensions.Options;
|
||||
using TakeoutSaaS.Module.Scheduler.Abstractions;
|
||||
using TakeoutSaaS.Module.Scheduler.Jobs;
|
||||
using TakeoutSaaS.Module.Scheduler.Options;
|
||||
|
||||
namespace TakeoutSaaS.Module.Scheduler.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 周期性任务注册器。
|
||||
/// </summary>
|
||||
public sealed class RecurringJobRegistrar : IRecurringJobRegistrar
|
||||
public sealed class RecurringJobRegistrar(IOptionsMonitor<SubscriptionAutomationOptions> subscriptionAutomationOptions) : IRecurringJobRegistrar
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public Task RegisterAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
// 1. 业务占位任务(示例)
|
||||
RecurringJob.AddOrUpdate<OrderTimeoutJob>("orders.timeout-cancel", job => job.ExecuteAsync(), "*/5 * * * *");
|
||||
RecurringJob.AddOrUpdate<CouponExpireJob>("coupons.expire", job => job.ExecuteAsync(), "0 */1 * * *");
|
||||
RecurringJob.AddOrUpdate<LogCleanupJob>("logs.cleanup", job => job.ExecuteAsync(), "0 3 * * *");
|
||||
|
||||
// 2. (空行后) 订阅自动化任务(自动续费、续费提醒、到期进入宽限期)
|
||||
var options = subscriptionAutomationOptions.CurrentValue;
|
||||
RecurringJob.AddOrUpdate<SubscriptionAutoRenewalJob>(
|
||||
"subscriptions.auto-renewal",
|
||||
job => job.ExecuteAsync(),
|
||||
$"0 {options.AutoRenewalExecuteHourUtc} * * *");
|
||||
RecurringJob.AddOrUpdate<SubscriptionRenewalReminderJob>(
|
||||
"subscriptions.renewal-reminder",
|
||||
job => job.ExecuteAsync(),
|
||||
$"0 {options.RenewalReminderExecuteHourUtc} * * *");
|
||||
RecurringJob.AddOrUpdate<SubscriptionExpiryCheckJob>(
|
||||
"subscriptions.expiry-check",
|
||||
job => job.ExecuteAsync(),
|
||||
$"0 {options.SubscriptionExpiryCheckExecuteHourUtc} * * *");
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,5 +16,6 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\Core\TakeoutSaaS.Shared.Abstractions\TakeoutSaaS.Shared.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\..\Application\TakeoutSaaS.Application\TakeoutSaaS.Application.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user