App:新增 operation_logs/quota_packages/tenant_payments/tenant_quota_package_purchases 表 Identity:修正 Avatar 字段类型(varchar(256)->text),保持现有数据不变
133 lines
4.8 KiB
C#
133 lines
4.8 KiB
C#
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;
|
||
}
|