Files
TakeoutSaaS.TenantApi/src/Infrastructure/TakeoutSaaS.Infrastructure/BackgroundServices/SubscriptionExpiryCheckService.cs
MSuMshk ab59e2e3e2 feat: 新增配额包/支付相关实体与迁移
App:新增 operation_logs/quota_packages/tenant_payments/tenant_quota_package_purchases 表

Identity:修正 Avatar 字段类型(varchar(256)->text),保持现有数据不变
2025-12-17 17:27:45 +08:00

133 lines
4.8 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 订阅到期检查后台服务。
/// 每天凌晨执行,检查即将到期和已到期的订阅,自动更新状态。
/// </summary>
public sealed class SubscriptionExpiryCheckService : BackgroundService
{
private readonly IServiceProvider _serviceProvider;
private readonly ILogger<SubscriptionExpiryCheckService> _logger;
private readonly SubscriptionExpiryCheckOptions _options;
public SubscriptionExpiryCheckService(
IServiceProvider serviceProvider,
ILogger<SubscriptionExpiryCheckService> logger,
IOptions<SubscriptionExpiryCheckOptions> 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<TakeoutAppDbContext>();
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;
}
}
}
/// <summary>
/// 订阅到期检查配置选项。
/// </summary>
public sealed class SubscriptionExpiryCheckOptions
{
/// <summary>
/// 执行时间小时UTC时间默认凌晨2点。
/// </summary>
public int ExecuteHour { get; set; } = 2;
/// <summary>
/// 宽限期天数默认7天。
/// </summary>
public int GracePeriodDays { get; set; } = 7;
}