refactor: 订阅任务按租户上下文执行

This commit is contained in:
root
2026-01-29 14:51:21 +00:00
parent f9053356c2
commit 1622c38043
17 changed files with 177 additions and 218 deletions

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Text.Json; using System.Text.Json;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
@@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// </summary> /// </summary>
public sealed class BatchExtendSubscriptionsCommandHandler( public sealed class BatchExtendSubscriptionsCommandHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IHttpContextAccessor httpContextAccessor,
IIdGenerator idGenerator, IIdGenerator idGenerator,
ILogger<BatchExtendSubscriptionsCommandHandler> logger) ILogger<BatchExtendSubscriptionsCommandHandler> logger)
: IRequestHandler<BatchExtendSubscriptionsCommand, BatchExtendResult> : IRequestHandler<BatchExtendSubscriptionsCommand, BatchExtendResult>
@@ -23,8 +21,6 @@ public sealed class BatchExtendSubscriptionsCommandHandler(
/// <inheritdoc /> /// <inheritdoc />
public async Task<BatchExtendResult> Handle(BatchExtendSubscriptionsCommand request, CancellationToken cancellationToken) public async Task<BatchExtendResult> Handle(BatchExtendSubscriptionsCommand request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
var successCount = 0; var successCount = 0;
var failures = new List<BatchFailureItem>(); var failures = new List<BatchFailureItem>();
@@ -41,8 +37,7 @@ public sealed class BatchExtendSubscriptionsCommandHandler(
// 查询所有订阅 // 查询所有订阅
var subscriptions = await subscriptionRepository.FindByIdsAsync( var subscriptions = await subscriptionRepository.FindByIdsAsync(
request.SubscriptionIds, request.SubscriptionIds,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
foreach (var subscriptionId in request.SubscriptionIds) foreach (var subscriptionId in request.SubscriptionIds)
{ {

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Text.Json; using System.Text.Json;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
@@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// </summary> /// </summary>
public sealed class BatchSendReminderCommandHandler( public sealed class BatchSendReminderCommandHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IHttpContextAccessor httpContextAccessor,
IIdGenerator idGenerator, IIdGenerator idGenerator,
ILogger<BatchSendReminderCommandHandler> logger) ILogger<BatchSendReminderCommandHandler> logger)
: IRequestHandler<BatchSendReminderCommand, BatchSendReminderResult> : IRequestHandler<BatchSendReminderCommand, BatchSendReminderResult>
@@ -23,16 +21,13 @@ public sealed class BatchSendReminderCommandHandler(
/// <inheritdoc /> /// <inheritdoc />
public async Task<BatchSendReminderResult> Handle(BatchSendReminderCommand request, CancellationToken cancellationToken) public async Task<BatchSendReminderResult> Handle(BatchSendReminderCommand request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
var successCount = 0; var successCount = 0;
var failures = new List<BatchFailureItem>(); var failures = new List<BatchFailureItem>();
// 查询所有订阅及租户信息 // 查询所有订阅及租户信息
var subscriptions = await subscriptionRepository.FindByIdsWithTenantAsync( var subscriptions = await subscriptionRepository.FindByIdsWithTenantAsync(
request.SubscriptionIds, request.SubscriptionIds,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
foreach (var subscriptionId in request.SubscriptionIds) foreach (var subscriptionId in request.SubscriptionIds)
{ {

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Dto;
using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Application.App.Subscriptions.Queries;
@@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// </summary> /// </summary>
public sealed class ChangeSubscriptionPlanCommandHandler( public sealed class ChangeSubscriptionPlanCommandHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IHttpContextAccessor httpContextAccessor,
IIdGenerator idGenerator, IIdGenerator idGenerator,
IMediator mediator) IMediator mediator)
: IRequestHandler<ChangeSubscriptionPlanCommand, SubscriptionDetailDto?> : IRequestHandler<ChangeSubscriptionPlanCommand, SubscriptionDetailDto?>
@@ -23,13 +21,10 @@ public sealed class ChangeSubscriptionPlanCommandHandler(
/// <inheritdoc /> /// <inheritdoc />
public async Task<SubscriptionDetailDto?> Handle(ChangeSubscriptionPlanCommand request, CancellationToken cancellationToken) public async Task<SubscriptionDetailDto?> Handle(ChangeSubscriptionPlanCommand request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
// 1. 查询订阅 // 1. 查询订阅
var subscription = await subscriptionRepository.FindByIdAsync( var subscription = await subscriptionRepository.FindByIdAsync(
request.SubscriptionId, request.SubscriptionId,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
if (subscription == null) if (subscription == null)
{ {

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Dto;
using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Application.App.Subscriptions.Queries;
@@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// </summary> /// </summary>
public sealed class ExtendSubscriptionCommandHandler( public sealed class ExtendSubscriptionCommandHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IHttpContextAccessor httpContextAccessor,
IIdGenerator idGenerator, IIdGenerator idGenerator,
IMediator mediator) IMediator mediator)
: IRequestHandler<ExtendSubscriptionCommand, SubscriptionDetailDto?> : IRequestHandler<ExtendSubscriptionCommand, SubscriptionDetailDto?>
@@ -23,13 +21,10 @@ public sealed class ExtendSubscriptionCommandHandler(
/// <inheritdoc /> /// <inheritdoc />
public async Task<SubscriptionDetailDto?> Handle(ExtendSubscriptionCommand request, CancellationToken cancellationToken) public async Task<SubscriptionDetailDto?> Handle(ExtendSubscriptionCommand request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
// 1. 查询订阅 // 1. 查询订阅
var subscription = await subscriptionRepository.FindByIdAsync( var subscription = await subscriptionRepository.FindByIdAsync(
request.SubscriptionId, request.SubscriptionId,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
if (subscription == null) if (subscription == null)
{ {

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Dto;
using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Application.App.Subscriptions.Queries;
using TakeoutSaaS.Application.App.Tenants; using TakeoutSaaS.Application.App.Tenants;
@@ -12,20 +11,16 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// 订阅详情查询处理器。 /// 订阅详情查询处理器。
/// </summary> /// </summary>
public sealed class GetSubscriptionDetailQueryHandler( public sealed class GetSubscriptionDetailQueryHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository)
IHttpContextAccessor httpContextAccessor)
: IRequestHandler<GetSubscriptionDetailQuery, SubscriptionDetailDto?> : IRequestHandler<GetSubscriptionDetailQuery, SubscriptionDetailDto?>
{ {
/// <inheritdoc /> /// <inheritdoc />
public async Task<SubscriptionDetailDto?> Handle(GetSubscriptionDetailQuery request, CancellationToken cancellationToken) public async Task<SubscriptionDetailDto?> Handle(GetSubscriptionDetailQuery request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
// 1. 查询订阅基础信息 // 1. 查询订阅基础信息
var detail = await subscriptionRepository.GetDetailAsync( var detail = await subscriptionRepository.GetDetailAsync(
request.SubscriptionId, request.SubscriptionId,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
if (detail == null) if (detail == null)
{ {
@@ -35,8 +30,7 @@ public sealed class GetSubscriptionDetailQueryHandler(
// 2. 查询配额使用情况 // 2. 查询配额使用情况
var quotaUsages = await subscriptionRepository.GetQuotaUsagesAsync( var quotaUsages = await subscriptionRepository.GetQuotaUsagesAsync(
detail.Subscription.TenantId, detail.Subscription.TenantId,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
var quotaUsageDtos = BuildQuotaUsageDtos(detail.Package, quotaUsages); var quotaUsageDtos = BuildQuotaUsageDtos(detail.Package, quotaUsages);

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Dto;
using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Application.App.Subscriptions.Queries;
using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Domain.Tenants.Repositories;
@@ -11,15 +10,12 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// 订阅分页查询处理器。 /// 订阅分页查询处理器。
/// </summary> /// </summary>
public sealed class GetSubscriptionListQueryHandler( public sealed class GetSubscriptionListQueryHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository)
IHttpContextAccessor httpContextAccessor)
: IRequestHandler<GetSubscriptionListQuery, PagedResult<SubscriptionListDto>> : IRequestHandler<GetSubscriptionListQuery, PagedResult<SubscriptionListDto>>
{ {
/// <inheritdoc /> /// <inheritdoc />
public async Task<PagedResult<SubscriptionListDto>> Handle(GetSubscriptionListQuery request, CancellationToken cancellationToken) public async Task<PagedResult<SubscriptionListDto>> Handle(GetSubscriptionListQuery request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
// 1. 构建查询过滤条件 // 1. 构建查询过滤条件
var filter = new SubscriptionSearchFilter var filter = new SubscriptionSearchFilter
{ {
@@ -36,8 +32,7 @@ public sealed class GetSubscriptionListQueryHandler(
// 2. 执行分页查询 // 2. 执行分页查询
var (items, total) = await subscriptionRepository.SearchPagedAsync( var (items, total) = await subscriptionRepository.SearchPagedAsync(
filter, filter,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
// 3. 映射为 DTO // 3. 映射为 DTO
var dtos = items.Select(x => new SubscriptionListDto var dtos = items.Select(x => new SubscriptionListDto

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Text.Json; using System.Text.Json;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
@@ -14,7 +13,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// </summary> /// </summary>
public sealed class ProcessAutoRenewalCommandHandler( public sealed class ProcessAutoRenewalCommandHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IHttpContextAccessor httpContextAccessor,
ITenantBillingRepository billingRepository, ITenantBillingRepository billingRepository,
IIdGenerator idGenerator, IIdGenerator idGenerator,
ILogger<ProcessAutoRenewalCommandHandler> logger) ILogger<ProcessAutoRenewalCommandHandler> logger)
@@ -23,8 +21,6 @@ public sealed class ProcessAutoRenewalCommandHandler(
/// <inheritdoc /> /// <inheritdoc />
public async Task<ProcessAutoRenewalResult> Handle(ProcessAutoRenewalCommand request, CancellationToken cancellationToken) public async Task<ProcessAutoRenewalResult> Handle(ProcessAutoRenewalCommand request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
// 1. 计算续费阈值时间 // 1. 计算续费阈值时间
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var renewalThreshold = now.AddDays(request.RenewalDaysBeforeExpiry); var renewalThreshold = now.AddDays(request.RenewalDaysBeforeExpiry);
@@ -33,8 +29,7 @@ public sealed class ProcessAutoRenewalCommandHandler(
var candidates = await subscriptionRepository.FindAutoRenewalCandidatesAsync( var candidates = await subscriptionRepository.FindAutoRenewalCandidatesAsync(
now, now,
renewalThreshold, renewalThreshold,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
var createdBillCount = 0; var createdBillCount = 0;
// 3. 遍历候选订阅,生成账单 // 3. 遍历候选订阅,生成账单

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System.Text.Json; using System.Text.Json;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
@@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// </summary> /// </summary>
public sealed class ProcessRenewalRemindersCommandHandler( public sealed class ProcessRenewalRemindersCommandHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IHttpContextAccessor httpContextAccessor,
ITenantNotificationRepository notificationRepository, ITenantNotificationRepository notificationRepository,
IIdGenerator idGenerator, IIdGenerator idGenerator,
ILogger<ProcessRenewalRemindersCommandHandler> logger) ILogger<ProcessRenewalRemindersCommandHandler> logger)
@@ -26,8 +24,6 @@ public sealed class ProcessRenewalRemindersCommandHandler(
/// <inheritdoc /> /// <inheritdoc />
public async Task<ProcessRenewalRemindersResult> Handle(ProcessRenewalRemindersCommand request, CancellationToken cancellationToken) public async Task<ProcessRenewalRemindersResult> Handle(ProcessRenewalRemindersCommand request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
// 1. 读取提醒配置 // 1. 读取提醒配置
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var candidateCount = 0; var candidateCount = 0;
@@ -46,8 +42,7 @@ public sealed class ProcessRenewalRemindersCommandHandler(
var candidates = await subscriptionRepository.FindRenewalReminderCandidatesAsync( var candidates = await subscriptionRepository.FindRenewalReminderCandidatesAsync(
startOfDay, startOfDay,
endOfDay, endOfDay,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
candidateCount += candidates.Count; candidateCount += candidates.Count;
foreach (var item in candidates) foreach (var item in candidates)

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Domain.Tenants.Enums;
@@ -12,26 +11,21 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// </summary> /// </summary>
public sealed class ProcessSubscriptionExpiryCommandHandler( public sealed class ProcessSubscriptionExpiryCommandHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IHttpContextAccessor httpContextAccessor,
ILogger<ProcessSubscriptionExpiryCommandHandler> logger) ILogger<ProcessSubscriptionExpiryCommandHandler> logger)
: IRequestHandler<ProcessSubscriptionExpiryCommand, ProcessSubscriptionExpiryResult> : IRequestHandler<ProcessSubscriptionExpiryCommand, ProcessSubscriptionExpiryResult>
{ {
/// <inheritdoc /> /// <inheritdoc />
public async Task<ProcessSubscriptionExpiryResult> Handle(ProcessSubscriptionExpiryCommand request, CancellationToken cancellationToken) public async Task<ProcessSubscriptionExpiryResult> Handle(ProcessSubscriptionExpiryCommand request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
// 1. 查询到期订阅 // 1. 查询到期订阅
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var expiredActive = await subscriptionRepository.FindExpiredActiveSubscriptionsAsync( var expiredActive = await subscriptionRepository.FindExpiredActiveSubscriptionsAsync(
now, now,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
var gracePeriodExpired = await subscriptionRepository.FindGracePeriodExpiredSubscriptionsAsync( var gracePeriodExpired = await subscriptionRepository.FindGracePeriodExpiredSubscriptionsAsync(
now, now,
request.GracePeriodDays, request.GracePeriodDays,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
// 2. 更新订阅状态 // 2. 更新订阅状态
foreach (var subscription in expiredActive) foreach (var subscription in expiredActive)

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Dto;
using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Application.App.Subscriptions.Queries;
@@ -12,20 +11,16 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// </summary> /// </summary>
public sealed class UpdateSubscriptionCommandHandler( public sealed class UpdateSubscriptionCommandHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IHttpContextAccessor httpContextAccessor,
IMediator mediator) IMediator mediator)
: IRequestHandler<UpdateSubscriptionCommand, SubscriptionDetailDto?> : IRequestHandler<UpdateSubscriptionCommand, SubscriptionDetailDto?>
{ {
/// <inheritdoc /> /// <inheritdoc />
public async Task<SubscriptionDetailDto?> Handle(UpdateSubscriptionCommand request, CancellationToken cancellationToken) public async Task<SubscriptionDetailDto?> Handle(UpdateSubscriptionCommand request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
// 1. 查询订阅 // 1. 查询订阅
var subscription = await subscriptionRepository.FindByIdAsync( var subscription = await subscriptionRepository.FindByIdAsync(
request.SubscriptionId, request.SubscriptionId,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
if (subscription == null) if (subscription == null)
{ {

View File

@@ -1,5 +1,4 @@
using MediatR; using MediatR;
using Microsoft.AspNetCore.Http;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Dto;
using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Application.App.Subscriptions.Queries;
@@ -12,20 +11,16 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers;
/// </summary> /// </summary>
public sealed class UpdateSubscriptionStatusCommandHandler( public sealed class UpdateSubscriptionStatusCommandHandler(
ISubscriptionRepository subscriptionRepository, ISubscriptionRepository subscriptionRepository,
IHttpContextAccessor httpContextAccessor,
IMediator mediator) IMediator mediator)
: IRequestHandler<UpdateSubscriptionStatusCommand, SubscriptionDetailDto?> : IRequestHandler<UpdateSubscriptionStatusCommand, SubscriptionDetailDto?>
{ {
/// <inheritdoc /> /// <inheritdoc />
public async Task<SubscriptionDetailDto?> Handle(UpdateSubscriptionStatusCommand request, CancellationToken cancellationToken) public async Task<SubscriptionDetailDto?> Handle(UpdateSubscriptionStatusCommand request, CancellationToken cancellationToken)
{ {
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
// 1. 查询订阅 // 1. 查询订阅
var subscription = await subscriptionRepository.FindByIdAsync( var subscription = await subscriptionRepository.FindByIdAsync(
request.SubscriptionId, request.SubscriptionId,
cancellationToken, cancellationToken);
ignoreTenantFilter: ignoreTenantFilter);
if (subscription == null) if (subscription == null)
{ {

View File

@@ -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;
}
}

View File

@@ -15,60 +15,50 @@ public interface ISubscriptionRepository
/// </summary> /// </summary>
/// <param name="subscriptionId">订阅 ID。</param> /// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>订阅实体,未找到返回 null。</returns> /// <returns>订阅实体,未找到返回 null。</returns>
Task<TenantSubscription?> FindByIdAsync( Task<TenantSubscription?> FindByIdAsync(
long subscriptionId, long subscriptionId,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
/// <summary> /// <summary>
/// 按 ID 列表批量查询订阅。 /// 按 ID 列表批量查询订阅。
/// </summary> /// </summary>
/// <param name="subscriptionIds">订阅 ID 列表。</param> /// <param name="subscriptionIds">订阅 ID 列表。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>订阅实体列表。</returns> /// <returns>订阅实体列表。</returns>
Task<IReadOnlyList<TenantSubscription>> FindByIdsAsync( Task<IReadOnlyList<TenantSubscription>> FindByIdsAsync(
IEnumerable<long> subscriptionIds, IEnumerable<long> subscriptionIds,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
/// <summary> /// <summary>
/// 分页查询订阅列表(含关联信息)。 /// 分页查询订阅列表(含关联信息)。
/// </summary> /// </summary>
/// <param name="filter">查询过滤条件。</param> /// <param name="filter">查询过滤条件。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>分页结果。</returns> /// <returns>分页结果。</returns>
Task<(IReadOnlyList<SubscriptionWithRelations> Items, int Total)> SearchPagedAsync( Task<(IReadOnlyList<SubscriptionWithRelations> Items, int Total)> SearchPagedAsync(
SubscriptionSearchFilter filter, SubscriptionSearchFilter filter,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
/// <summary> /// <summary>
/// 获取订阅详情(含关联信息)。 /// 获取订阅详情(含关联信息)。
/// </summary> /// </summary>
/// <param name="subscriptionId">订阅 ID。</param> /// <param name="subscriptionId">订阅 ID。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>订阅详情信息。</returns> /// <returns>订阅详情信息。</returns>
Task<SubscriptionDetailInfo?> GetDetailAsync( Task<SubscriptionDetailInfo?> GetDetailAsync(
long subscriptionId, long subscriptionId,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
/// <summary> /// <summary>
/// 按 ID 列表批量查询订阅(含租户信息)。 /// 按 ID 列表批量查询订阅(含租户信息)。
/// </summary> /// </summary>
/// <param name="subscriptionIds">订阅 ID 列表。</param> /// <param name="subscriptionIds">订阅 ID 列表。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>订阅与租户信息列表。</returns> /// <returns>订阅与租户信息列表。</returns>
Task<IReadOnlyList<SubscriptionWithTenant>> FindByIdsWithTenantAsync( Task<IReadOnlyList<SubscriptionWithTenant>> FindByIdsWithTenantAsync(
IEnumerable<long> subscriptionIds, IEnumerable<long> subscriptionIds,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
/// <summary> /// <summary>
/// 查询自动续费候选订阅(活跃 + 开启自动续费 + 即将到期)。 /// 查询自动续费候选订阅(活跃 + 开启自动续费 + 即将到期)。
@@ -76,13 +66,11 @@ public interface ISubscriptionRepository
/// <param name="now">当前时间UTC。</param> /// <param name="now">当前时间UTC。</param>
/// <param name="renewalThreshold">续费阈值时间UTC到期时间小于等于该时间视为候选。</param> /// <param name="renewalThreshold">续费阈值时间UTC到期时间小于等于该时间视为候选。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>候选订阅集合(含套餐信息)。</returns> /// <returns>候选订阅集合(含套餐信息)。</returns>
Task<IReadOnlyList<AutoRenewalCandidate>> FindAutoRenewalCandidatesAsync( Task<IReadOnlyList<AutoRenewalCandidate>> FindAutoRenewalCandidatesAsync(
DateTime now, DateTime now,
DateTime renewalThreshold, DateTime renewalThreshold,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
/// <summary> /// <summary>
/// 查询续费提醒候选订阅(活跃 + 未开启自动续费 + 到期时间落在指定日期范围)。 /// 查询续费提醒候选订阅(活跃 + 未开启自动续费 + 到期时间落在指定日期范围)。
@@ -90,25 +78,21 @@ public interface ISubscriptionRepository
/// <param name="startOfDay">筛选开始时间UTC。</param> /// <param name="startOfDay">筛选开始时间UTC。</param>
/// <param name="endOfDay">筛选结束时间UTC不含。</param> /// <param name="endOfDay">筛选结束时间UTC不含。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>候选订阅集合(含租户与套餐信息)。</returns> /// <returns>候选订阅集合(含租户与套餐信息)。</returns>
Task<IReadOnlyList<RenewalReminderCandidate>> FindRenewalReminderCandidatesAsync( Task<IReadOnlyList<RenewalReminderCandidate>> FindRenewalReminderCandidatesAsync(
DateTime startOfDay, DateTime startOfDay,
DateTime endOfDay, DateTime endOfDay,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
/// <summary> /// <summary>
/// 查询已到期仍处于 Active 的订阅(用于进入宽限期)。 /// 查询已到期仍处于 Active 的订阅(用于进入宽限期)。
/// </summary> /// </summary>
/// <param name="now">当前时间UTC。</param> /// <param name="now">当前时间UTC。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>到期订阅集合。</returns> /// <returns>到期订阅集合。</returns>
Task<IReadOnlyList<TenantSubscription>> FindExpiredActiveSubscriptionsAsync( Task<IReadOnlyList<TenantSubscription>> FindExpiredActiveSubscriptionsAsync(
DateTime now, DateTime now,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
/// <summary> /// <summary>
/// 查询宽限期已结束的订阅(用于自动暂停)。 /// 查询宽限期已结束的订阅(用于自动暂停)。
@@ -116,13 +100,11 @@ public interface ISubscriptionRepository
/// <param name="now">当前时间UTC。</param> /// <param name="now">当前时间UTC。</param>
/// <param name="gracePeriodDays">宽限期天数。</param> /// <param name="gracePeriodDays">宽限期天数。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>宽限期到期订阅集合。</returns> /// <returns>宽限期到期订阅集合。</returns>
Task<IReadOnlyList<TenantSubscription>> FindGracePeriodExpiredSubscriptionsAsync( Task<IReadOnlyList<TenantSubscription>> FindGracePeriodExpiredSubscriptionsAsync(
DateTime now, DateTime now,
int gracePeriodDays, int gracePeriodDays,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
#endregion #endregion
@@ -177,12 +159,10 @@ public interface ISubscriptionRepository
/// </summary> /// </summary>
/// <param name="tenantId">租户 ID。</param> /// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param> /// <param name="cancellationToken">取消标记。</param>
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于系统级任务)。</param>
/// <returns>配额使用列表。</returns> /// <returns>配额使用列表。</returns>
Task<IReadOnlyList<TenantQuotaUsage>> GetQuotaUsagesAsync( Task<IReadOnlyList<TenantQuotaUsage>> GetQuotaUsagesAsync(
long tenantId, long tenantId,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default);
bool ignoreTenantFilter = false);
#endregion #endregion

View File

@@ -17,29 +17,19 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
/// <inheritdoc /> /// <inheritdoc />
public async Task<TenantSubscription?> FindByIdAsync( public async Task<TenantSubscription?> FindByIdAsync(
long subscriptionId, long subscriptionId,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
var query = ignoreTenantFilter return await dbContext.TenantSubscriptions
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
: dbContext.TenantSubscriptions;
return await query
.FirstOrDefaultAsync(s => s.Id == subscriptionId, cancellationToken); .FirstOrDefaultAsync(s => s.Id == subscriptionId, cancellationToken);
} }
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<TenantSubscription>> FindByIdsAsync( public async Task<IReadOnlyList<TenantSubscription>> FindByIdsAsync(
IEnumerable<long> subscriptionIds, IEnumerable<long> subscriptionIds,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
var ids = subscriptionIds.ToList(); var ids = subscriptionIds.ToList();
var query = ignoreTenantFilter return await dbContext.TenantSubscriptions
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
: dbContext.TenantSubscriptions;
return await query
.Where(s => ids.Contains(s.Id)) .Where(s => ids.Contains(s.Id))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
@@ -47,15 +37,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
/// <inheritdoc /> /// <inheritdoc />
public async Task<(IReadOnlyList<SubscriptionWithRelations> Items, int Total)> SearchPagedAsync( public async Task<(IReadOnlyList<SubscriptionWithRelations> Items, int Total)> SearchPagedAsync(
SubscriptionSearchFilter filter, SubscriptionSearchFilter filter,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
// 1. 构建基础查询 // 1. 构建基础查询
var subscriptionQuery = ignoreTenantFilter var query = dbContext.TenantSubscriptions
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
: dbContext.TenantSubscriptions;
var query = subscriptionQuery
.AsNoTracking() .AsNoTracking()
.Join( .Join(
dbContext.Tenants, dbContext.Tenants,
@@ -133,14 +118,9 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
/// <inheritdoc /> /// <inheritdoc />
public async Task<SubscriptionDetailInfo?> GetDetailAsync( public async Task<SubscriptionDetailInfo?> GetDetailAsync(
long subscriptionId, long subscriptionId,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
var subscriptionQuery = ignoreTenantFilter var result = await dbContext.TenantSubscriptions
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
: dbContext.TenantSubscriptions;
var result = await subscriptionQuery
.AsNoTracking() .AsNoTracking()
.Where(s => s.Id == subscriptionId) .Where(s => s.Id == subscriptionId)
.Select(s => new .Select(s => new
@@ -172,16 +152,11 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<SubscriptionWithTenant>> FindByIdsWithTenantAsync( public async Task<IReadOnlyList<SubscriptionWithTenant>> FindByIdsWithTenantAsync(
IEnumerable<long> subscriptionIds, IEnumerable<long> subscriptionIds,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
var ids = subscriptionIds.ToList(); var ids = subscriptionIds.ToList();
var query = ignoreTenantFilter return await dbContext.TenantSubscriptions
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
: dbContext.TenantSubscriptions;
return await query
.Where(s => ids.Contains(s.Id)) .Where(s => ids.Contains(s.Id))
.Join( .Join(
dbContext.Tenants, dbContext.Tenants,
@@ -200,15 +175,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
public async Task<IReadOnlyList<AutoRenewalCandidate>> FindAutoRenewalCandidatesAsync( public async Task<IReadOnlyList<AutoRenewalCandidate>> FindAutoRenewalCandidatesAsync(
DateTime now, DateTime now,
DateTime renewalThreshold, DateTime renewalThreshold,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
// 1. 查询开启自动续费且即将到期的活跃订阅 // 1. 查询开启自动续费且即将到期的活跃订阅
var subscriptionQuery = ignoreTenantFilter var query = dbContext.TenantSubscriptions
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
: dbContext.TenantSubscriptions;
var query = subscriptionQuery
.Where(s => s.Status == SubscriptionStatus.Active .Where(s => s.Status == SubscriptionStatus.Active
&& s.AutoRenew && s.AutoRenew
&& s.EffectiveTo <= renewalThreshold && s.EffectiveTo <= renewalThreshold
@@ -231,15 +201,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
public async Task<IReadOnlyList<RenewalReminderCandidate>> FindRenewalReminderCandidatesAsync( public async Task<IReadOnlyList<RenewalReminderCandidate>> FindRenewalReminderCandidatesAsync(
DateTime startOfDay, DateTime startOfDay,
DateTime endOfDay, DateTime endOfDay,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
// 1. 查询到期落在指定区间的订阅(且未开启自动续费) // 1. 查询到期落在指定区间的订阅(且未开启自动续费)
var subscriptionQuery = ignoreTenantFilter var query = dbContext.TenantSubscriptions
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
: dbContext.TenantSubscriptions;
var query = subscriptionQuery
.Where(s => s.Status == SubscriptionStatus.Active .Where(s => s.Status == SubscriptionStatus.Active
&& !s.AutoRenew && !s.AutoRenew
&& s.EffectiveTo >= startOfDay && s.EffectiveTo >= startOfDay
@@ -267,15 +232,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<TenantSubscription>> FindExpiredActiveSubscriptionsAsync( public async Task<IReadOnlyList<TenantSubscription>> FindExpiredActiveSubscriptionsAsync(
DateTime now, DateTime now,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
var query = ignoreTenantFilter
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
: dbContext.TenantSubscriptions;
// 1. 查询已到期仍为 Active 的订阅 // 1. 查询已到期仍为 Active 的订阅
return await query return await dbContext.TenantSubscriptions
.Where(s => s.Status == SubscriptionStatus.Active && s.EffectiveTo < now) .Where(s => s.Status == SubscriptionStatus.Active && s.EffectiveTo < now)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
@@ -284,15 +244,10 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
public async Task<IReadOnlyList<TenantSubscription>> FindGracePeriodExpiredSubscriptionsAsync( public async Task<IReadOnlyList<TenantSubscription>> FindGracePeriodExpiredSubscriptionsAsync(
DateTime now, DateTime now,
int gracePeriodDays, int gracePeriodDays,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
var query = ignoreTenantFilter
? dbContext.TenantSubscriptions.IgnoreQueryFilters()
: dbContext.TenantSubscriptions;
// 1. 查询宽限期已结束的订阅 // 1. 查询宽限期已结束的订阅
return await query return await dbContext.TenantSubscriptions
.Where(s => s.Status == SubscriptionStatus.GracePeriod .Where(s => s.Status == SubscriptionStatus.GracePeriod
&& s.EffectiveTo.AddDays(gracePeriodDays) < now) && s.EffectiveTo.AddDays(gracePeriodDays) < now)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -363,14 +318,9 @@ public sealed class EfSubscriptionRepository(TakeoutAppDbContext dbContext, Take
/// <inheritdoc /> /// <inheritdoc />
public async Task<IReadOnlyList<TenantQuotaUsage>> GetQuotaUsagesAsync( public async Task<IReadOnlyList<TenantQuotaUsage>> GetQuotaUsagesAsync(
long tenantId, long tenantId,
CancellationToken cancellationToken = default, CancellationToken cancellationToken = default)
bool ignoreTenantFilter = false)
{ {
var query = ignoreTenantFilter return await dbContext.TenantQuotaUsages
? dbContext.TenantQuotaUsages.IgnoreQueryFilters()
: dbContext.TenantQuotaUsages;
return await query
.AsNoTracking() .AsNoTracking()
.Where(q => q.TenantId == tenantId) .Where(q => q.TenantId == tenantId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);

View File

@@ -2,7 +2,9 @@ using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Module.Scheduler.Options; using TakeoutSaaS.Module.Scheduler.Options;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Module.Scheduler.Jobs; namespace TakeoutSaaS.Module.Scheduler.Jobs;
@@ -11,6 +13,8 @@ namespace TakeoutSaaS.Module.Scheduler.Jobs;
/// </summary> /// </summary>
public sealed class SubscriptionAutoRenewalJob( public sealed class SubscriptionAutoRenewalJob(
IMediator mediator, IMediator mediator,
ITenantRepository tenantRepository,
ITenantContextAccessor tenantContextAccessor,
IOptionsMonitor<SubscriptionAutomationOptions> optionsMonitor, IOptionsMonitor<SubscriptionAutomationOptions> optionsMonitor,
ILogger<SubscriptionAutoRenewalJob> logger) ILogger<SubscriptionAutoRenewalJob> logger)
{ {
@@ -19,18 +23,48 @@ public sealed class SubscriptionAutoRenewalJob(
/// </summary> /// </summary>
public async Task ExecuteAsync() public async Task ExecuteAsync()
{ {
// 1. 读取配置并执行自动续费 // 1. 读取配置
var options = optionsMonitor.CurrentValue; var options = optionsMonitor.CurrentValue;
// 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 var result = await mediator.Send(new ProcessAutoRenewalCommand
{ {
RenewalDaysBeforeExpiry = options.AutoRenewalDaysBeforeExpiry RenewalDaysBeforeExpiry = options.AutoRenewalDaysBeforeExpiry
}); });
// 2. 记录执行结果 candidateCount += result.CandidateCount;
logger.LogInformation( createdBillCount += result.CreatedBillCount;
"定时任务:自动续费处理完成,候选 {CandidateCount},创建账单 {CreatedBillCount}",
result.CandidateCount,
result.CreatedBillCount);
} }
catch (Exception ex)
{
logger.LogError(ex, "定时任务:自动续费执行失败 TenantId={TenantId}", tenant.Id);
}
}
}
finally
{
tenantContextAccessor.Current = previousContext;
} }
// 4. (空行后) 记录执行结果
logger.LogInformation(
"定时任务:自动续费处理完成,处理租户 {TenantCount},候选 {CandidateCount},创建账单 {CreatedBillCount}",
targets.Count,
candidateCount,
createdBillCount);
}
}

View File

@@ -2,7 +2,9 @@ using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Module.Scheduler.Options; using TakeoutSaaS.Module.Scheduler.Options;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Module.Scheduler.Jobs; namespace TakeoutSaaS.Module.Scheduler.Jobs;
@@ -11,6 +13,8 @@ namespace TakeoutSaaS.Module.Scheduler.Jobs;
/// </summary> /// </summary>
public sealed class SubscriptionExpiryCheckJob( public sealed class SubscriptionExpiryCheckJob(
IMediator mediator, IMediator mediator,
ITenantRepository tenantRepository,
ITenantContextAccessor tenantContextAccessor,
IOptionsMonitor<SubscriptionAutomationOptions> optionsMonitor, IOptionsMonitor<SubscriptionAutomationOptions> optionsMonitor,
ILogger<SubscriptionExpiryCheckJob> logger) ILogger<SubscriptionExpiryCheckJob> logger)
{ {
@@ -19,17 +23,48 @@ public sealed class SubscriptionExpiryCheckJob(
/// </summary> /// </summary>
public async Task ExecuteAsync() public async Task ExecuteAsync()
{ {
// 1. 读取配置并执行到期处理 // 1. 读取配置
var options = optionsMonitor.CurrentValue; var options = optionsMonitor.CurrentValue;
// 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 var result = await mediator.Send(new ProcessSubscriptionExpiryCommand
{ {
GracePeriodDays = options.GracePeriodDays GracePeriodDays = options.GracePeriodDays
}); });
// 2. 记录执行结果 enteredGracePeriodCount += result.EnteredGracePeriodCount;
suspendedCount += result.SuspendedCount;
}
catch (Exception ex)
{
logger.LogError(ex, "定时任务:订阅到期检查执行失败 TenantId={TenantId}", tenant.Id);
}
}
}
finally
{
tenantContextAccessor.Current = previousContext;
}
// 4. (空行后) 记录执行结果
logger.LogInformation( logger.LogInformation(
"定时任务:订阅到期检查完成,进入宽限期 {EnteredGracePeriodCount},暂停 {SuspendedCount}", "定时任务:订阅到期检查完成,处理租户 {TenantCount}进入宽限期 {EnteredGracePeriodCount},暂停 {SuspendedCount}",
result.EnteredGracePeriodCount, targets.Count,
result.SuspendedCount); enteredGracePeriodCount,
suspendedCount);
} }
} }

View File

@@ -2,7 +2,9 @@ using MediatR;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Commands;
using TakeoutSaaS.Domain.Tenants.Repositories;
using TakeoutSaaS.Module.Scheduler.Options; using TakeoutSaaS.Module.Scheduler.Options;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Module.Scheduler.Jobs; namespace TakeoutSaaS.Module.Scheduler.Jobs;
@@ -11,6 +13,8 @@ namespace TakeoutSaaS.Module.Scheduler.Jobs;
/// </summary> /// </summary>
public sealed class SubscriptionRenewalReminderJob( public sealed class SubscriptionRenewalReminderJob(
IMediator mediator, IMediator mediator,
ITenantRepository tenantRepository,
ITenantContextAccessor tenantContextAccessor,
IOptionsMonitor<SubscriptionAutomationOptions> optionsMonitor, IOptionsMonitor<SubscriptionAutomationOptions> optionsMonitor,
ILogger<SubscriptionRenewalReminderJob> logger) ILogger<SubscriptionRenewalReminderJob> logger)
{ {
@@ -19,17 +23,48 @@ public sealed class SubscriptionRenewalReminderJob(
/// </summary> /// </summary>
public async Task ExecuteAsync() public async Task ExecuteAsync()
{ {
// 1. 读取配置并执行续费提醒 // 1. 读取配置
var options = optionsMonitor.CurrentValue; var options = optionsMonitor.CurrentValue;
// 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 var result = await mediator.Send(new ProcessRenewalRemindersCommand
{ {
ReminderDaysBeforeExpiry = options.ReminderDaysBeforeExpiry ReminderDaysBeforeExpiry = options.ReminderDaysBeforeExpiry
}); });
// 2. 记录执行结果 candidateCount += result.CandidateCount;
createdReminderCount += result.CreatedReminderCount;
}
catch (Exception ex)
{
logger.LogError(ex, "定时任务:续费提醒执行失败 TenantId={TenantId}", tenant.Id);
}
}
}
finally
{
tenantContextAccessor.Current = previousContext;
}
// 4. (空行后) 记录执行结果
logger.LogInformation( logger.LogInformation(
"定时任务:续费提醒处理完成,候选 {CandidateCount},创建 {CreatedReminderCount}", "定时任务:续费提醒处理完成,处理租户 {TenantCount}候选 {CandidateCount},创建 {CreatedReminderCount}",
result.CandidateCount, targets.Count,
result.CreatedReminderCount); candidateCount,
createdReminderCount);
} }
} }