refactor: 订阅任务按租户上下文执行
This commit is contained in:
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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. 遍历候选订阅,生成账单
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
createdBillCount += result.CreatedBillCount;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.LogError(ex, "定时任务:自动续费执行失败 TenantId={TenantId}", tenant.Id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
tenantContextAccessor.Current = previousContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. (空行后) 记录执行结果
|
||||||
logger.LogInformation(
|
logger.LogInformation(
|
||||||
"定时任务:自动续费处理完成,候选 {CandidateCount},创建账单 {CreatedBillCount}",
|
"定时任务:自动续费处理完成,处理租户 {TenantCount},候选 {CandidateCount},创建账单 {CreatedBillCount}",
|
||||||
result.CandidateCount,
|
targets.Count,
|
||||||
result.CreatedBillCount);
|
candidateCount,
|
||||||
|
createdBillCount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user