using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using TakeoutSaaS.Domain.Tenants.Enums; using TakeoutSaaS.Infrastructure.App.Persistence; namespace TakeoutSaaS.Infrastructure.BackgroundServices; /// /// 订阅到期检查后台服务。 /// 每天凌晨执行,检查即将到期和已到期的订阅,自动更新状态。 /// public sealed class SubscriptionExpiryCheckService : BackgroundService { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; private readonly SubscriptionExpiryCheckOptions _options; public SubscriptionExpiryCheckService( IServiceProvider serviceProvider, ILogger logger, IOptions options) { _serviceProvider = serviceProvider; _logger = logger; _options = options.Value; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { _logger.LogInformation("订阅到期检查服务已启动"); while (!stoppingToken.IsCancellationRequested) { try { // 计算下次执行时间(每天凌晨) var now = DateTime.UtcNow; var nextRun = now.Date.AddDays(1).AddHours(_options.ExecuteHour); var delay = nextRun - now; _logger.LogInformation("订阅到期检查服务将在 {NextRun} 执行,等待 {Delay}", nextRun, delay); await Task.Delay(delay, stoppingToken); if (stoppingToken.IsCancellationRequested) break; await CheckExpiringSubscriptionsAsync(stoppingToken); } catch (Exception ex) { _logger.LogError(ex, "订阅到期检查服务执行异常"); // 出错后等待一段时间再重试 await Task.Delay(TimeSpan.FromMinutes(5), stoppingToken); } } _logger.LogInformation("订阅到期检查服务已停止"); } private async Task CheckExpiringSubscriptionsAsync(CancellationToken cancellationToken) { _logger.LogInformation("开始执行订阅到期检查"); using var scope = _serviceProvider.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); var now = DateTime.UtcNow; var gracePeriodDays = _options.GracePeriodDays; try { // 1. 检查活跃订阅中已到期的,转为宽限期 var expiredActive = await dbContext.TenantSubscriptions .Where(s => s.Status == SubscriptionStatus.Active && s.EffectiveTo < now) .ToListAsync(cancellationToken); foreach (var subscription in expiredActive) { subscription.Status = SubscriptionStatus.GracePeriod; _logger.LogInformation( "订阅 {SubscriptionId} (租户 {TenantId}) 已到期,进入宽限期", subscription.Id, subscription.TenantId); } // 2. 检查宽限期订阅中超过宽限期的,转为暂停 var gracePeriodExpired = await dbContext.TenantSubscriptions .Where(s => s.Status == SubscriptionStatus.GracePeriod && s.EffectiveTo.AddDays(gracePeriodDays) < now) .ToListAsync(cancellationToken); foreach (var subscription in gracePeriodExpired) { subscription.Status = SubscriptionStatus.Suspended; _logger.LogInformation( "订阅 {SubscriptionId} (租户 {TenantId}) 宽限期已结束,已暂停", subscription.Id, subscription.TenantId); } // 3. 保存更改 var changedCount = await dbContext.SaveChangesAsync(cancellationToken); _logger.LogInformation( "订阅到期检查完成,共更新 {Count} 条记录 (到期转宽限期: {ExpiredCount}, 宽限期转暂停: {SuspendedCount})", changedCount, expiredActive.Count, gracePeriodExpired.Count); } catch (Exception ex) { _logger.LogError(ex, "订阅到期检查失败"); throw; } } } /// /// 订阅到期检查配置选项。 /// public sealed class SubscriptionExpiryCheckOptions { /// /// 执行时间(小时,UTC时间),默认凌晨2点。 /// public int ExecuteHour { get; set; } = 2; /// /// 宽限期天数,默认7天。 /// public int GracePeriodDays { get; set; } = 7; }