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:
@@ -56,6 +56,52 @@ public interface ISubscriptionRepository
|
||||
IEnumerable<long> subscriptionIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询自动续费候选订阅(活跃 + 开启自动续费 + 即将到期)。
|
||||
/// </summary>
|
||||
/// <param name="now">当前时间(UTC)。</param>
|
||||
/// <param name="renewalThreshold">续费阈值时间(UTC),到期时间小于等于该时间视为候选。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>候选订阅集合(含套餐信息)。</returns>
|
||||
Task<IReadOnlyList<AutoRenewalCandidate>> FindAutoRenewalCandidatesAsync(
|
||||
DateTime now,
|
||||
DateTime renewalThreshold,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询续费提醒候选订阅(活跃 + 未开启自动续费 + 到期时间落在指定日期范围)。
|
||||
/// </summary>
|
||||
/// <param name="startOfDay">筛选开始时间(UTC,含)。</param>
|
||||
/// <param name="endOfDay">筛选结束时间(UTC,不含)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>候选订阅集合(含租户与套餐信息)。</returns>
|
||||
Task<IReadOnlyList<RenewalReminderCandidate>> FindRenewalReminderCandidatesAsync(
|
||||
DateTime startOfDay,
|
||||
DateTime endOfDay,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询已到期仍处于 Active 的订阅(用于进入宽限期)。
|
||||
/// </summary>
|
||||
/// <param name="now">当前时间(UTC)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>到期订阅集合。</returns>
|
||||
Task<IReadOnlyList<TenantSubscription>> FindExpiredActiveSubscriptionsAsync(
|
||||
DateTime now,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 查询宽限期已结束的订阅(用于自动暂停)。
|
||||
/// </summary>
|
||||
/// <param name="now">当前时间(UTC)。</param>
|
||||
/// <param name="gracePeriodDays">宽限期天数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>宽限期到期订阅集合。</returns>
|
||||
Task<IReadOnlyList<TenantSubscription>> FindGracePeriodExpiredSubscriptionsAsync(
|
||||
DateTime now,
|
||||
int gracePeriodDays,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
#endregion
|
||||
|
||||
#region 套餐查询
|
||||
@@ -271,6 +317,43 @@ public record SubscriptionWithTenant
|
||||
public required Tenant Tenant { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 自动续费候选订阅信息。
|
||||
/// </summary>
|
||||
public sealed record AutoRenewalCandidate
|
||||
{
|
||||
/// <summary>
|
||||
/// 订阅实体。
|
||||
/// </summary>
|
||||
public required TenantSubscription Subscription { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前套餐实体。
|
||||
/// </summary>
|
||||
public required TenantPackage Package { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 续费提醒候选订阅信息。
|
||||
/// </summary>
|
||||
public sealed record RenewalReminderCandidate
|
||||
{
|
||||
/// <summary>
|
||||
/// 订阅实体。
|
||||
/// </summary>
|
||||
public required TenantSubscription Subscription { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户实体。
|
||||
/// </summary>
|
||||
public required Tenant Tenant { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前套餐实体。
|
||||
/// </summary>
|
||||
public required TenantPackage Package { get; init; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订阅历史(含套餐名称)。
|
||||
/// </summary>
|
||||
|
||||
@@ -42,6 +42,18 @@ public interface ITenantBillingRepository
|
||||
/// <returns>账单实体或 null。</returns>
|
||||
Task<TenantBillingStatement?> FindByStatementNoAsync(long tenantId, string statementNo, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否已存在指定周期开始时间的未取消账单(用于自动续费幂等)。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="periodStart">账单周期开始时间(UTC)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>存在返回 true,否则 false。</returns>
|
||||
Task<bool> ExistsNotCancelledByPeriodStartAsync(
|
||||
long tenantId,
|
||||
DateTime periodStart,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增账单。
|
||||
/// </summary>
|
||||
|
||||
@@ -35,6 +35,22 @@ public interface ITenantNotificationRepository
|
||||
/// <returns>通知实体或 null。</returns>
|
||||
Task<TenantNotification?> FindByIdAsync(long tenantId, long notificationId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否已发送过指定元数据的通知(用于幂等控制)。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="title">通知标题。</param>
|
||||
/// <param name="metadataJson">元数据 JSON(需与写入值完全一致)。</param>
|
||||
/// <param name="sentAfter">只在该时间之后发送的记录范围内判断(UTC)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>存在返回 true,否则 false。</returns>
|
||||
Task<bool> ExistsByMetadataAsync(
|
||||
long tenantId,
|
||||
string title,
|
||||
string metadataJson,
|
||||
DateTime sentAfter,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增通知。
|
||||
/// </summary>
|
||||
|
||||
Reference in New Issue
Block a user