From 1622c3804302b2984b7eb9d31a82b93bf227c607 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 29 Jan 2026 14:51:21 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E8=AE=A2=E9=98=85=E4=BB=BB?= =?UTF-8?q?=E5=8A=A1=E6=8C=89=E7=A7=9F=E6=88=B7=E4=B8=8A=E4=B8=8B=E6=96=87?= =?UTF-8?q?=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../BatchExtendSubscriptionsCommandHandler.cs | 7 +- .../BatchSendReminderCommandHandler.cs | 7 +- .../ChangeSubscriptionPlanCommandHandler.cs | 7 +- .../ExtendSubscriptionCommandHandler.cs | 7 +- .../GetSubscriptionDetailQueryHandler.cs | 12 +-- .../GetSubscriptionListQueryHandler.cs | 9 +- .../ProcessAutoRenewalCommandHandler.cs | 7 +- .../ProcessRenewalRemindersCommandHandler.cs | 7 +- ...ProcessSubscriptionExpiryCommandHandler.cs | 10 +-- .../UpdateSubscriptionCommandHandler.cs | 7 +- .../UpdateSubscriptionStatusCommandHandler.cs | 7 +- .../Subscriptions/SubscriptionTenantAccess.cs | 18 ---- .../Repositories/ISubscriptionRepository.cs | 40 +++------ .../Repositories/EfSubscriptionRepository.cs | 90 +++++-------------- .../Jobs/SubscriptionAutoRenewalJob.cs | 54 ++++++++--- .../Jobs/SubscriptionExpiryCheckJob.cs | 53 +++++++++-- .../Jobs/SubscriptionRenewalReminderJob.cs | 53 +++++++++-- 17 files changed, 177 insertions(+), 218 deletions(-) delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs index 2760e90..a617f3f 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.Text.Json; using TakeoutSaaS.Application.App.Subscriptions.Commands; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class BatchExtendSubscriptionsCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, ILogger logger) : IRequestHandler @@ -23,8 +21,6 @@ public sealed class BatchExtendSubscriptionsCommandHandler( /// public async Task Handle(BatchExtendSubscriptionsCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var successCount = 0; var failures = new List(); @@ -41,8 +37,7 @@ public sealed class BatchExtendSubscriptionsCommandHandler( // 查询所有订阅 var subscriptions = await subscriptionRepository.FindByIdsAsync( request.SubscriptionIds, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); foreach (var subscriptionId in request.SubscriptionIds) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs index cea5313..95f36b1 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.Text.Json; using TakeoutSaaS.Application.App.Subscriptions.Commands; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class BatchSendReminderCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, ILogger logger) : IRequestHandler @@ -23,16 +21,13 @@ public sealed class BatchSendReminderCommandHandler( /// public async Task Handle(BatchSendReminderCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var successCount = 0; var failures = new List(); // 查询所有订阅及租户信息 var subscriptions = await subscriptionRepository.FindByIdsWithTenantAsync( request.SubscriptionIds, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); foreach (var subscriptionId in request.SubscriptionIds) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs index 7d443c0..b744a34 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ChangeSubscriptionPlanCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, IMediator mediator) : IRequestHandler @@ -23,13 +21,10 @@ public sealed class ChangeSubscriptionPlanCommandHandler( /// public async Task Handle(ChangeSubscriptionPlanCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅 var subscription = await subscriptionRepository.FindByIdAsync( request.SubscriptionId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); if (subscription == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs index fbfb825..2717c86 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ExtendSubscriptionCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, IMediator mediator) : IRequestHandler @@ -23,13 +21,10 @@ public sealed class ExtendSubscriptionCommandHandler( /// public async Task Handle(ExtendSubscriptionCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅 var subscription = await subscriptionRepository.FindByIdAsync( request.SubscriptionId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); if (subscription == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs index f77fda1..1644bb9 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Application.App.Tenants; @@ -12,20 +11,16 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// 订阅详情查询处理器。 /// public sealed class GetSubscriptionDetailQueryHandler( - ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor) + ISubscriptionRepository subscriptionRepository) : IRequestHandler { /// public async Task Handle(GetSubscriptionDetailQuery request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅基础信息 var detail = await subscriptionRepository.GetDetailAsync( request.SubscriptionId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); if (detail == null) { @@ -35,8 +30,7 @@ public sealed class GetSubscriptionDetailQueryHandler( // 2. 查询配额使用情况 var quotaUsages = await subscriptionRepository.GetQuotaUsagesAsync( detail.Subscription.TenantId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); var quotaUsageDtos = BuildQuotaUsageDtos(detail.Package, quotaUsages); diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs index 58d8268..d6bfc0f 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Domain.Tenants.Repositories; @@ -11,15 +10,12 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// 订阅分页查询处理器。 /// public sealed class GetSubscriptionListQueryHandler( - ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor) + ISubscriptionRepository subscriptionRepository) : IRequestHandler> { /// public async Task> Handle(GetSubscriptionListQuery request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 构建查询过滤条件 var filter = new SubscriptionSearchFilter { @@ -36,8 +32,7 @@ public sealed class GetSubscriptionListQueryHandler( // 2. 执行分页查询 var (items, total) = await subscriptionRepository.SearchPagedAsync( filter, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); // 3. 映射为 DTO var dtos = items.Select(x => new SubscriptionListDto diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs index 64548ea..960ed10 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.Text.Json; using TakeoutSaaS.Application.App.Subscriptions.Commands; @@ -14,7 +13,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ProcessAutoRenewalCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, ITenantBillingRepository billingRepository, IIdGenerator idGenerator, ILogger logger) @@ -23,8 +21,6 @@ public sealed class ProcessAutoRenewalCommandHandler( /// public async Task Handle(ProcessAutoRenewalCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 计算续费阈值时间 var now = DateTime.UtcNow; var renewalThreshold = now.AddDays(request.RenewalDaysBeforeExpiry); @@ -33,8 +29,7 @@ public sealed class ProcessAutoRenewalCommandHandler( var candidates = await subscriptionRepository.FindAutoRenewalCandidatesAsync( now, renewalThreshold, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); var createdBillCount = 0; // 3. 遍历候选订阅,生成账单 diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs index 27be88d..f1c1aba 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.Text.Json; using TakeoutSaaS.Application.App.Subscriptions.Commands; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ProcessRenewalRemindersCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, ITenantNotificationRepository notificationRepository, IIdGenerator idGenerator, ILogger logger) @@ -26,8 +24,6 @@ public sealed class ProcessRenewalRemindersCommandHandler( /// public async Task Handle(ProcessRenewalRemindersCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 读取提醒配置 var now = DateTime.UtcNow; var candidateCount = 0; @@ -46,8 +42,7 @@ public sealed class ProcessRenewalRemindersCommandHandler( var candidates = await subscriptionRepository.FindRenewalReminderCandidatesAsync( startOfDay, endOfDay, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); candidateCount += candidates.Count; foreach (var item in candidates) diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs index 0d798bb..9b655e5 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Domain.Tenants.Enums; @@ -12,26 +11,21 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ProcessSubscriptionExpiryCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler { /// public async Task Handle(ProcessSubscriptionExpiryCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询到期订阅 var now = DateTime.UtcNow; var expiredActive = await subscriptionRepository.FindExpiredActiveSubscriptionsAsync( now, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); var gracePeriodExpired = await subscriptionRepository.FindGracePeriodExpiredSubscriptionsAsync( now, request.GracePeriodDays, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); // 2. 更新订阅状态 foreach (var subscription in expiredActive) diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs index 225700f..70c852c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; @@ -12,20 +11,16 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class UpdateSubscriptionCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IMediator mediator) : IRequestHandler { /// public async Task Handle(UpdateSubscriptionCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅 var subscription = await subscriptionRepository.FindByIdAsync( request.SubscriptionId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); if (subscription == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs index 9f32016..58c2652 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; @@ -12,20 +11,16 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class UpdateSubscriptionStatusCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IMediator mediator) : IRequestHandler { /// public async Task Handle(UpdateSubscriptionStatusCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅 var subscription = await subscriptionRepository.FindByIdAsync( request.SubscriptionId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); if (subscription == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs deleted file mode 100644 index 0458f4a..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs +++ /dev/null @@ -1,18 +0,0 @@ -using Microsoft.AspNetCore.Http; - -namespace TakeoutSaaS.Application.App.Subscriptions; - -internal static class SubscriptionTenantAccess -{ - public static bool ShouldIgnoreTenantFilter(IHttpContextAccessor httpContextAccessor) - { - var httpContext = httpContextAccessor.HttpContext; - if (httpContext == null) - { - // Background jobs / out-of-request execution should process across tenants. - return true; - } - // (空行后) 请求上下文下强制不允许跨租户 - return false; - } -} diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs index 596d003..f72d8d3 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs @@ -15,60 +15,50 @@ public interface ISubscriptionRepository /// /// 订阅 ID。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 订阅实体,未找到返回 null。 Task FindByIdAsync( long subscriptionId, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); /// /// 按 ID 列表批量查询订阅。 /// /// 订阅 ID 列表。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 订阅实体列表。 Task> FindByIdsAsync( IEnumerable subscriptionIds, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); /// /// 分页查询订阅列表(含关联信息)。 /// /// 查询过滤条件。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 分页结果。 Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( SubscriptionSearchFilter filter, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); /// /// 获取订阅详情(含关联信息)。 /// /// 订阅 ID。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 订阅详情信息。 Task GetDetailAsync( long subscriptionId, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); /// /// 按 ID 列表批量查询订阅(含租户信息)。 /// /// 订阅 ID 列表。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 订阅与租户信息列表。 Task> FindByIdsWithTenantAsync( IEnumerable subscriptionIds, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); /// /// 查询自动续费候选订阅(活跃 + 开启自动续费 + 即将到期)。 @@ -76,13 +66,11 @@ public interface ISubscriptionRepository /// 当前时间(UTC)。 /// 续费阈值时间(UTC),到期时间小于等于该时间视为候选。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 候选订阅集合(含套餐信息)。 Task> FindAutoRenewalCandidatesAsync( DateTime now, DateTime renewalThreshold, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); /// /// 查询续费提醒候选订阅(活跃 + 未开启自动续费 + 到期时间落在指定日期范围)。 @@ -90,25 +78,21 @@ public interface ISubscriptionRepository /// 筛选开始时间(UTC,含)。 /// 筛选结束时间(UTC,不含)。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 候选订阅集合(含租户与套餐信息)。 Task> FindRenewalReminderCandidatesAsync( DateTime startOfDay, DateTime endOfDay, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); /// /// 查询已到期仍处于 Active 的订阅(用于进入宽限期)。 /// /// 当前时间(UTC)。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 到期订阅集合。 Task> FindExpiredActiveSubscriptionsAsync( DateTime now, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); /// /// 查询宽限期已结束的订阅(用于自动暂停)。 @@ -116,13 +100,11 @@ public interface ISubscriptionRepository /// 当前时间(UTC)。 /// 宽限期天数。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 宽限期到期订阅集合。 Task> FindGracePeriodExpiredSubscriptionsAsync( DateTime now, int gracePeriodDays, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); #endregion @@ -177,12 +159,10 @@ public interface ISubscriptionRepository /// /// 租户 ID。 /// 取消标记。 - /// 是否忽略租户过滤(用于系统级任务)。 /// 配额使用列表。 Task> GetQuotaUsagesAsync( long tenantId, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + CancellationToken cancellationToken = default); #endregion diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs index 5eca26d..5168764 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs @@ -17,29 +17,19 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take /// public async Task FindByIdAsync( long subscriptionId, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { - var query = ignoreTenantFilter - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - return await query + return await dbContext.TenantSubscriptions .FirstOrDefaultAsync(s => s.Id == subscriptionId, cancellationToken); } /// public async Task> FindByIdsAsync( IEnumerable subscriptionIds, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { var ids = subscriptionIds.ToList(); - var query = ignoreTenantFilter - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - return await query + return await dbContext.TenantSubscriptions .Where(s => ids.Contains(s.Id)) .ToListAsync(cancellationToken); } @@ -47,15 +37,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take /// public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( SubscriptionSearchFilter filter, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { // 1. 构建基础查询 - var subscriptionQuery = ignoreTenantFilter - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - var query = subscriptionQuery + var query = dbContext.TenantSubscriptions .AsNoTracking() .Join( dbContext.Tenants, @@ -133,14 +118,9 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take /// public async Task GetDetailAsync( long subscriptionId, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { - var subscriptionQuery = ignoreTenantFilter - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - var result = await subscriptionQuery + var result = await dbContext.TenantSubscriptions .AsNoTracking() .Where(s => s.Id == subscriptionId) .Select(s => new @@ -172,16 +152,11 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take /// public async Task> FindByIdsWithTenantAsync( IEnumerable subscriptionIds, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { var ids = subscriptionIds.ToList(); - var query = ignoreTenantFilter - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - return await query + return await dbContext.TenantSubscriptions .Where(s => ids.Contains(s.Id)) .Join( dbContext.Tenants, @@ -200,15 +175,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take public async Task> FindAutoRenewalCandidatesAsync( DateTime now, DateTime renewalThreshold, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { // 1. 查询开启自动续费且即将到期的活跃订阅 - var subscriptionQuery = ignoreTenantFilter - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - var query = subscriptionQuery + var query = dbContext.TenantSubscriptions .Where(s => s.Status == SubscriptionStatus.Active && s.AutoRenew && s.EffectiveTo <= renewalThreshold @@ -231,15 +201,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take public async Task> FindRenewalReminderCandidatesAsync( DateTime startOfDay, DateTime endOfDay, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { // 1. 查询到期落在指定区间的订阅(且未开启自动续费) - var subscriptionQuery = ignoreTenantFilter - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - - var query = subscriptionQuery + var query = dbContext.TenantSubscriptions .Where(s => s.Status == SubscriptionStatus.Active && !s.AutoRenew && s.EffectiveTo >= startOfDay @@ -267,15 +232,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take /// public async Task> FindExpiredActiveSubscriptionsAsync( DateTime now, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { - var query = ignoreTenantFilter - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - // 1. 查询已到期仍为 Active 的订阅 - return await query + return await dbContext.TenantSubscriptions .Where(s => s.Status == SubscriptionStatus.Active && s.EffectiveTo < now) .ToListAsync(cancellationToken); } @@ -284,15 +244,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take public async Task> FindGracePeriodExpiredSubscriptionsAsync( DateTime now, int gracePeriodDays, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { - var query = ignoreTenantFilter - ? dbContext.TenantSubscriptions.IgnoreQueryFilters() - : dbContext.TenantSubscriptions; - // 1. 查询宽限期已结束的订阅 - return await query + return await dbContext.TenantSubscriptions .Where(s => s.Status == SubscriptionStatus.GracePeriod && s.EffectiveTo.AddDays(gracePeriodDays) < now) .ToListAsync(cancellationToken); @@ -363,14 +318,9 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take /// public async Task> GetQuotaUsagesAsync( long tenantId, - CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + CancellationToken cancellationToken = default) { - var query = ignoreTenantFilter - ? dbContext.TenantQuotaUsages.IgnoreQueryFilters() - : dbContext.TenantQuotaUsages; - - return await query + return await dbContext.TenantQuotaUsages .AsNoTracking() .Where(q => q.TenantId == tenantId) .ToListAsync(cancellationToken); diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionAutoRenewalJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionAutoRenewalJob.cs index 648f889..3a285ca 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionAutoRenewalJob.cs +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionAutoRenewalJob.cs @@ -2,7 +2,9 @@ using MediatR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Module.Scheduler.Options; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Module.Scheduler.Jobs; @@ -11,6 +13,8 @@ namespace TakeoutSaaS.Module.Scheduler.Jobs; /// public sealed class SubscriptionAutoRenewalJob( IMediator mediator, + ITenantRepository tenantRepository, + ITenantContextAccessor tenantContextAccessor, IOptionsMonitor optionsMonitor, ILogger logger) { @@ -19,18 +23,48 @@ public sealed class SubscriptionAutoRenewalJob( /// public async Task ExecuteAsync() { - // 1. 读取配置并执行自动续费 + // 1. 读取配置 var options = optionsMonitor.CurrentValue; - var result = await mediator.Send(new ProcessAutoRenewalCommand - { - RenewalDaysBeforeExpiry = options.AutoRenewalDaysBeforeExpiry - }); - // 2. 记录执行结果 + // 2. (空行后) 获取需要处理的租户列表(排除系统租户) + var tenants = await tenantRepository.SearchAsync(null, null, CancellationToken.None); + var targets = tenants.Where(x => x.Id > 0).ToList(); + + // 3. (空行后) 按租户逐个执行自动续费 + var candidateCount = 0; + var createdBillCount = 0; + var previousContext = tenantContextAccessor.Current; + try + { + foreach (var tenant in targets) + { + tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "scheduler"); + try + { + var result = await mediator.Send(new ProcessAutoRenewalCommand + { + RenewalDaysBeforeExpiry = options.AutoRenewalDaysBeforeExpiry + }); + + candidateCount += result.CandidateCount; + createdBillCount += result.CreatedBillCount; + } + catch (Exception ex) + { + logger.LogError(ex, "定时任务:自动续费执行失败 TenantId={TenantId}", tenant.Id); + } + } + } + finally + { + tenantContextAccessor.Current = previousContext; + } + + // 4. (空行后) 记录执行结果 logger.LogInformation( - "定时任务:自动续费处理完成,候选 {CandidateCount},创建账单 {CreatedBillCount}", - result.CandidateCount, - result.CreatedBillCount); + "定时任务:自动续费处理完成,处理租户 {TenantCount},候选 {CandidateCount},创建账单 {CreatedBillCount}", + targets.Count, + candidateCount, + createdBillCount); } } - diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionExpiryCheckJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionExpiryCheckJob.cs index a6de16a..b0ca568 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionExpiryCheckJob.cs +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionExpiryCheckJob.cs @@ -2,7 +2,9 @@ using MediatR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Module.Scheduler.Options; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Module.Scheduler.Jobs; @@ -11,6 +13,8 @@ namespace TakeoutSaaS.Module.Scheduler.Jobs; /// public sealed class SubscriptionExpiryCheckJob( IMediator mediator, + ITenantRepository tenantRepository, + ITenantContextAccessor tenantContextAccessor, IOptionsMonitor optionsMonitor, ILogger logger) { @@ -19,17 +23,48 @@ public sealed class SubscriptionExpiryCheckJob( /// public async Task ExecuteAsync() { - // 1. 读取配置并执行到期处理 + // 1. 读取配置 var options = optionsMonitor.CurrentValue; - var result = await mediator.Send(new ProcessSubscriptionExpiryCommand - { - GracePeriodDays = options.GracePeriodDays - }); - // 2. 记录执行结果 + // 2. (空行后) 获取需要处理的租户列表(排除系统租户) + var tenants = await tenantRepository.SearchAsync(null, null, CancellationToken.None); + var targets = tenants.Where(x => x.Id > 0).ToList(); + + // 3. (空行后) 按租户逐个执行到期处理 + var enteredGracePeriodCount = 0; + var suspendedCount = 0; + var previousContext = tenantContextAccessor.Current; + try + { + foreach (var tenant in targets) + { + tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "scheduler"); + try + { + var result = await mediator.Send(new ProcessSubscriptionExpiryCommand + { + GracePeriodDays = options.GracePeriodDays + }); + + enteredGracePeriodCount += result.EnteredGracePeriodCount; + suspendedCount += result.SuspendedCount; + } + catch (Exception ex) + { + logger.LogError(ex, "定时任务:订阅到期检查执行失败 TenantId={TenantId}", tenant.Id); + } + } + } + finally + { + tenantContextAccessor.Current = previousContext; + } + + // 4. (空行后) 记录执行结果 logger.LogInformation( - "定时任务:订阅到期检查完成,进入宽限期 {EnteredGracePeriodCount},暂停 {SuspendedCount}", - result.EnteredGracePeriodCount, - result.SuspendedCount); + "定时任务:订阅到期检查完成,处理租户 {TenantCount},进入宽限期 {EnteredGracePeriodCount},暂停 {SuspendedCount}", + targets.Count, + enteredGracePeriodCount, + suspendedCount); } } diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionRenewalReminderJob.cs b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionRenewalReminderJob.cs index 80b7462..d3e284d 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionRenewalReminderJob.cs +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/Jobs/SubscriptionRenewalReminderJob.cs @@ -2,7 +2,9 @@ using MediatR; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TakeoutSaaS.Application.App.Subscriptions.Commands; +using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Module.Scheduler.Options; +using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Module.Scheduler.Jobs; @@ -11,6 +13,8 @@ namespace TakeoutSaaS.Module.Scheduler.Jobs; /// public sealed class SubscriptionRenewalReminderJob( IMediator mediator, + ITenantRepository tenantRepository, + ITenantContextAccessor tenantContextAccessor, IOptionsMonitor optionsMonitor, ILogger logger) { @@ -19,17 +23,48 @@ public sealed class SubscriptionRenewalReminderJob( /// public async Task ExecuteAsync() { - // 1. 读取配置并执行续费提醒 + // 1. 读取配置 var options = optionsMonitor.CurrentValue; - var result = await mediator.Send(new ProcessRenewalRemindersCommand - { - ReminderDaysBeforeExpiry = options.ReminderDaysBeforeExpiry - }); - // 2. 记录执行结果 + // 2. (空行后) 获取需要处理的租户列表(排除系统租户) + var tenants = await tenantRepository.SearchAsync(null, null, CancellationToken.None); + var targets = tenants.Where(x => x.Id > 0).ToList(); + + // 3. (空行后) 按租户逐个执行续费提醒 + var candidateCount = 0; + var createdReminderCount = 0; + var previousContext = tenantContextAccessor.Current; + try + { + foreach (var tenant in targets) + { + tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "scheduler"); + try + { + var result = await mediator.Send(new ProcessRenewalRemindersCommand + { + ReminderDaysBeforeExpiry = options.ReminderDaysBeforeExpiry + }); + + candidateCount += result.CandidateCount; + createdReminderCount += result.CreatedReminderCount; + } + catch (Exception ex) + { + logger.LogError(ex, "定时任务:续费提醒执行失败 TenantId={TenantId}", tenant.Id); + } + } + } + finally + { + tenantContextAccessor.Current = previousContext; + } + + // 4. (空行后) 记录执行结果 logger.LogInformation( - "定时任务:续费提醒处理完成,候选 {CandidateCount},创建 {CreatedReminderCount}", - result.CandidateCount, - result.CreatedReminderCount); + "定时任务:续费提醒处理完成,处理租户 {TenantCount},候选 {CandidateCount},创建 {CreatedReminderCount}", + targets.Count, + candidateCount, + createdReminderCount); } }