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